1. 程式人生 > >《5.陣列&字串&結構體&共用體&列舉》

《5.陣列&字串&結構體&共用體&列舉》

《5.陣列&字串&結構體&共用體&列舉》

第一部分、章節目錄
4.5.1.程式中記憶體從哪裡來1
4.5.2.程式中記憶體從哪裡來2
4.5.3.程式中記憶體從哪裡來2
4.5.4.程式中記憶體從哪裡來4
4.5.5.C語言的字串型別
4.5.6.字串和字元陣列的細節
4.5.7.C語言之結構體概述
4.5.8.結構體的對齊訪問1
4.5.9.結構體的對齊訪問2
4.5.10.結構體的對齊訪問3
4.5.11.offsetof巨集與container_of巨集
4.5.12.共用體union
4.5.13.大小端模式1
4.5.14.大小端模式2
4.5.15.列舉

第二部分、章節介紹
4.5.1.程式中記憶體從哪裡來1
本節首先引出程式執行時對記憶體的依賴,然後分析這些變數需要的記憶體從哪裡來,進而引出棧、堆、資料區這幾個重要的記憶體來源,並詳解棧記憶體分配及使用的細節。
4.5.2.程式中記憶體從哪裡來2
本節詳細講解堆記憶體的特點和使用,寫程式碼帶大家來學會使用malloc、free函式獲取及釋放堆記憶體,並且講解了堆記憶體的一些程式設計細節。
4.5.3.程式中記憶體從哪裡來3
本節詳細講解堆記憶體的特點和使用,寫程式碼帶大家來學會使用malloc、free函式獲取及釋放堆記憶體,並且講解了堆記憶體的一些程式設計細節。
4.5.4.程式中記憶體從哪裡來4
本節首先引入程式碼段、資料段、bss段等概念,然後重點分析了資料段和bss段與C語言程式設計變數的關係,最後給大家總結了程式通過資料段方式獲取記憶體的原理和使用細節。
4.5.5.C語言的字串型別
本節講述C語言字串型別的本質,著重解釋了C語言字串的三個特點:指標指向頭、固定結尾、記憶體相連分佈。
4.5.6.字串和字元陣列的細節
本節講述字串和字元陣列及其各種初始化式,然後結合sizeof運算子和strlen函式,通過程式碼實踐試圖讓大家理解字串和字元陣列的具體表現。
4.5.7.C語言之結構體概述
本節概述結構體,主要講了結構體和陣列的差異,然後以點號方式和指標方式這兩種方式來訪問結構體成員,以此告訴大家結構體成員訪問的原理。
4.5.8.結構體的對齊訪問1
本節開始講解結構體的對齊訪問,通過數個結構體例項來講解對齊訪問的規則和sizeof測試結構體的大小,最後補充講解了#prgama和__attribute__這兩種方式下對結構體對齊規則的影響。共分3節,本節為第1節。
4.5.9.結構體的對齊訪問2
本節開始講解結構體的對齊訪問,通過數個結構體例項來講解對齊訪問的規則和sizeof測試結構體的大小,最後補充講解了#prgama和__attribute__這兩種方式下對結構體對齊規則的影響。共分3節,本節為第2節。
4.5.10.結構體的對齊訪問3
本節開始講解結構體的對齊訪問,通過數個結構體例項來講解對齊訪問的規則和sizeof測試結構體的大小,最後補充講解了#prgama和__attribute__這兩種方式下對結構體對齊規則的影響。共分3節,本節為第3節。
4.5.11.offsetof巨集與container_of巨集
本節講解linux核心中常用的2個結構體相關的巨集:offsetof和container_of,這兩個巨集充分利用了結構體、指標、資料型別、記憶體等知識,是C語言中比較高階的語法應用。通過理解及使用這兩個巨集可以極大提升C語言的功底,並且為學習linux核心做積累。
4.5.12.共用體union
本節首先介紹union,然後著重對比分析了union和struct的異同點,重點強調了共用體和結構體的差別,讓大家理解共用體的實質。

4.5.13.大小端模式1
本節首先引入大小端的概念,然後介紹了常用的幾種用程式碼來測試機器大小端模式的方式.
4.5.14.大小端模式2
本節繼續講解大小端,介紹了幾種看起來似乎可以但是實際上並不工作的大小端測試程式碼。同時介紹了在通訊系統中應用的大小端概念。
4.5.15.列舉
本節講述C語言中的列舉常量,首先通過例項告訴大家列舉常量的定義和使用方法,然後講解列舉和巨集定義的異同,旨在告訴大傢什麼時候可以用列舉。

第三部分、隨堂記錄
4.5.1.程式中記憶體從哪裡來1
4.5.1.1、程式執行需要記憶體支援
()對程式來說,記憶體就是程式的立足之地(程式是被放在記憶體中執行的);程式執行時需要記憶體來儲存一些臨時變數。
4.5.1.2、記憶體管理最終是由作業系統完成的
(1)記憶體本身在物理上是一個硬體器件,由硬體系統提供。
(2)記憶體是由作業系統統一管理。為了記憶體管理方便又合理,作業系統提供了多種機制來讓我們應用程式使用記憶體。這些機制彼此不同,各自有各自的特點,我們程式根據自己的實際情況來選擇某種方式獲取記憶體(在作業系統處登記這塊記憶體的臨時使用許可權)、使用記憶體、釋放記憶體(向作業系統歸還這塊記憶體的使用許可權)。
4.5.1.3、三種記憶體來源:棧(stack)、堆(heap)、資料區(.data)
(1)在一個C語言程式中,能夠獲取的記憶體就是三種情況:棧(stack)、堆(heap)、資料區(.data)
4.5.1.4、棧的詳解
執行時自動分配&自動回收:棧是自動管理的,程式設計師不需要手工干預。方便簡單。
反覆使用:棧記憶體在程式中其實就是那一塊空間,程式反覆使用這一塊空間。
髒記憶體:棧記憶體由於反覆使用,每次使用後程序不會去清理,因此分配到時保留原來的值。
臨時性:(函式不能返回棧變數的指標,因為這個空間是臨時的)
棧會溢位:因為作業系統事先給定了棧的大小,如果在函式中無窮盡的分配棧記憶體總能用完。

4.5.2.程式中記憶體從哪裡來2
4.5.2.1、堆記憶體詳解
作業系統堆管理器管理:堆管理器是作業系統的一個模組,堆管理記憶體分配靈活,按需分配。
大塊記憶體:堆記憶體管理者總量很大的作業系統記憶體塊,各程序可以按需申請使用,使用完釋放。
程式手動申請&釋放:手工意思是需要寫程式碼去申請malloc和釋放free。
髒記憶體:堆記憶體也是反覆使用的,而且使用者用完釋放前不會清除,因此也是髒的。
臨時性:堆記憶體只在malloc和free之間屬於我這個程序,而可以訪問。在malloc之前和free之後
都不能再訪問,否則會有不可預料的後果。

4.5.2.2、堆記憶體使用範例
(1)void *是個指標型別,malloc返回的是一個void *型別的指標,實質上malloc返回的是堆管理器分配給我本次申請的那段記憶體空間的首地址(malloc返回的值其實是一個數字,這個數字表示一個記憶體地址)。為什麼要使用void *作為型別?主要原因是malloc幫我們分配記憶體時只是分配了記憶體空間,至於這段空間將來用來儲存什麼型別的元素malloc是不關心的,由我們程式自己來決定。
(2)什麼是void型別。早期被翻譯成空型,這個翻譯非常不好,會誤導人。void型別不表示沒有型別,而表示萬能型別。void的意思就是說這個資料的型別當前是不確定的,在需要的時候可以再去指定它的具體型別。void *型別是一個指標型別,這個指標本身佔4個位元組,但是指標指向的型別是不確定的,換句話說這個指標在需要的時候可以被強制轉化成其他任何一種確定型別的指標,也就是說這個指標可以指向任何型別的元素。
(3)malloc的返回值:成功申請空間後返回這個記憶體空間的指標,申請失敗時返回NULL。所以malloc獲取的記憶體指標使用前一定要先檢驗是否為NULL。
(4)malloc申請的記憶體時用完後要free釋放。free§;會告訴堆管理器這段記憶體我用完了你可以回收了。堆管理器回收了這段記憶體後這段記憶體當前程序就不應該再使用了。因為釋放後堆管理器就可能把這段記憶體再次分配給別的程序,所以你就不能再使用了。
(5)再呼叫free歸還這段記憶體之前,指向這段記憶體的指標p一定不能丟(也就是不能給p另外賦值)。因為p一旦丟失這段malloc來的記憶體就永遠的丟失了(記憶體洩漏),直到當前程式結束時作業系統才會回收這段記憶體。
4.5.2.3、malloc的一些細節表現
malloc(0)
malloc申請0位元組記憶體本身就是一件無厘頭事情,一般不會碰到這個需要。
如果真的malloc(0)返回的是NULL還是一個有效指標?答案是:實際分配了16Byte的一段記憶體並且返回了這段記憶體的地址。這個答案不是確定的,因為C語言並沒有明確規定malloc(0)時的表現,由各malloc函式庫的實現者來定義。

malloc(4)

gcc中的malloc預設最小是以16B為分配單位的。如果malloc小於16B的大小時都會返回一個16位元組的大小的記憶體。malloc實現時沒有實現任意自己的分配而是允許一些大小的塊記憶體的分配。

malloc(20)去訪問第25、第250、第2500····會怎麼樣

實戰中:120位元組處正確,1200位元組處正確····終於繼續往後訪問總有一個數字處開始段錯誤了。

4.5.4.程式中記憶體從哪裡來3
4.5.4.1、程式碼段、資料段、bss段
(1)編譯器在編譯程式的時候,將程式中的所有的元素分成了一些組成部分,各部分構成一個段,所以說段是可執行程式的組成部分。
(2)程式碼段:程式碼段就是程式中的可執行部分,直觀理解程式碼段就是函式堆疊組成的。
(3)資料段(也被稱為資料區、靜態資料區、靜態區):資料段就是程式中的資料,直觀理解就是C語言程式中的全域性變數。(注意:全域性變數才算是程式的資料,區域性變數不算程式的資料,只能算是函式的資料)
(4)bss段(又叫ZI(zero initial)段):bss段的特點就是被初始化為0,bss段本質上也是屬於資料段,bss段就是被初始化為0的資料段。
注意區分:資料段(.data)和bss段的區別和聯絡:二者本來沒有本質區別,都是用來存放C程式中的全域性變數的。區別在於把顯示初始化為非零的全域性變數存在.data段中,而把顯式初始化為0或者並未顯式初始化(C語言規定未顯式初始化的全域性變數值預設為0)的全域性變數存在bss段。

4.5.4.2、有些特殊資料會被放到程式碼段
(1)C語言中使用char *p = “linux”;定義字串時,字串"linux"實際被分配在程式碼段,也就是說這個"linux"字串實際上是一個常量字串而不是變數字串。
(2)const型常量:C語言中const關鍵字用來定義常量,常量就是不能被改變的量。const的實現方法至少有2種:第一種就是編譯將const修飾的變數放在程式碼段去以實現不能修改(普遍見於各種微控制器的編譯器);第二種就是由編譯器來檢查以確保const型的常量不會被修改,實際上const型的常量還是和普通變數一樣放在資料段的(gcc中就是這樣實現的)。

4.5.4.3、顯式初始化為非零的全域性變數和靜態區域性變數放在資料段
(1)放在.data段的變數有2種:第一種是顯式初始化為非零的全域性變數。第二種是靜態區域性變數,也就是static修飾的區域性變數。(普通區域性變數分配在棧上,靜態區域性變數分配在.data段)

4.5.4.4、未初始化或顯式初始化為0的全域性變數放在bss段
(1)bss段和.data段並沒有本質區別,幾乎可以不用明確去區分這兩種。

4.5.4.5、總結:C語言中所有變數和常量所使用的記憶體無非以上三種情況。
(1)相同點:三種獲取記憶體的方法,都可以給程式提供可用記憶體,都可以用來定義變數給程式用。
(2)不同點:棧記憶體對應C中的普通區域性變數(別的變數還用不了棧,而且棧是自動的,由編譯器和執行時環境共同來提供服務的,程式設計師無法手工控制);堆記憶體完全是獨立於我們的程式存在和管理的,程式需要記憶體時可以去手工申請malloc,使用完成後必須儘快free釋放。(堆記憶體對程式就好象公共圖書館對於人);資料段對於程式來說對應C程式中的全域性變數和靜態區域性變數。
(3)如果我需要一段記憶體來儲存資料,我究竟應該把這個資料儲存在哪裡?(或者說我要定義一個變數,我究竟應該定義為區域性變數還是全域性變數還是用malloc來實現)。不同的儲存方式有不同的特點,簡單總結如下:
* 函式內部臨時使用,出了函式不會用到,就定義區域性變數
* 堆記憶體和資料段幾乎擁有完全相同的屬性,大部分時候是可以完全替換的。但是生命週期不一
堆記憶體的生命週期是從malloc開始到free結束,而全域性變數是從整個程式一開始執行就開始,
直到整個程式結束才會消滅,伴隨程式執行的一生。啟示:如果你這個變數只是在程式的一個
階段有用,用完就不用了,就適合用堆記憶體;如果這個變數本身和程式是一生相伴的,那就
適合用全域性變數。(堆記憶體就好象租房、資料段就好象買房。堆記憶體就好象圖書館借書,數
據段就好象自己書店買書)你以後會慢慢發現:買不如租,堆記憶體的使用比全域性變數廣泛。

4.5.5.C語言的字串型別
4.5.5.1、C語言沒有原生字串型別
(1)很多高階語言像java、C#等就有字串型別,有個String來表示字串,用法和int這些很像,可以String s1 = “linux”;來定義字串型別的變數。
(2)C語言沒有String型別,C語言中的字串是通過字元指標來間接實現的。

4.5.5.2、C語言使用指標來管理字串
(1)C語言中定義字串方法:char *p = “linux”;此時p就叫做字串,但是實際上p只是一個字元指標(本質上就是一個指標變數,只是p指向了一個字串的起始地址而已)。
4.5.5.3、C語言中字串的本質:指標指向頭、固定尾部的地址相連的一段記憶體
(1)字串就是一串字元。字元反映在現實中就是文字、符號、數字等人用來表達的字元,反映在程式設計中字元就是字元型別的變數。C語言中使用ASCII編碼對字元進行程式設計,編碼後可以用char型變數來表示一個字元。字串就是多個字元打包在一起共同組成的。
(2)字串在記憶體中其實就是多個位元組連續分佈構成的(類似於陣列,字串和字元陣列非常像)
(3)C語言中字串有3個核心要點:第一是用一個指標指向字串頭;第二是固定尾部(字串總是以’\0’來結尾);第三是組成字串的各字元彼此地址相連。
(4)’\0’是一個ASCII字元,其實就是編碼為0的那個字元(真正的0,和數字0是不同的,數字0有它自己的ASCII編碼)。要注意區分’\0’和’0’和0.(0等於’\0’,‘0’等於48)
(5)’\0’作為一個特殊的數字被字串定義為(幸運的選為)結尾標誌。產生的副作用就是:字串中無法包含’\0’這個字元。(C語言中不可能存在一個包含’\0’字元的字串),這種思路就叫“魔數”(魔數就是選出來的一個特殊的數字,這個數字表示一個特殊的含義,你的正式內容中不能包含這個魔數作為內容)。

4.5.5.4、注意:指向字串的指標和字串本身是分開的兩個東西
(1)char *p = “linux”;在這段程式碼中,p本質上是一個字元指標,佔4位元組;"linux"分配在程式碼段,佔6個位元組;實際上總共耗費了10個位元組,這10個位元組中:4位元組的指標p叫做字串指標(用來指向字串的,理解為字串的引子,但是它本身不是字串),5位元組的用來存linux這5個字元的記憶體才是真正的字串,最後一個用來存’\0’的記憶體是字串結尾標誌(本質上也不屬於字串)。

4.5.5.5、儲存多個字元的2種方式:字串和字元陣列
(1)我們有多個連續字元(典型就是linux這個字串)需要儲存,實際上有兩種方式:第一種就是字串;第二種是字元陣列。

4.5.6.字串和字元陣列的細節
4.5.6.1、字元陣列初始化與sizeof、strlen
(1)sizeof是C語言的一個關鍵字,也是C語言的一個運算子(sizeof使用時是sizeof(型別或變數名),所以很多人誤以為sizeof是函式,其實不是),sizeof運算子用來返回一個型別或者是變數所佔用的記憶體位元組數。為什麼需要sizeof?主要原因一是int、double等原生型別佔幾個位元組和平臺有關;二是C語言中除了ADT之外還有UDT,這些使用者自定義型別佔幾個位元組無法一眼看出,所以用sizeof運算子來讓編譯器幫忙計算。
(2)strlen是一個C語言庫函式,這個庫函式的原型是:size_t strlen(const char *s);這個函式接收一個字串的指標,返回這個字串的長度(以位元組為單位)。注意一點是:strlen返回的字串長度是不包含字串結尾的’\0’的。我們為什麼需要strlen庫函式?因為從字串的定義(指標指向頭、固定結尾、中間依次相連)可以看出無法直接得到字串的長度,需要用strlen函式來計算得到字串的長度。
(3)sizeof(陣列名)得到的永遠是陣列的元素個數(也就是陣列的大小),和陣列中有無初始化,初始化多、少等是沒有關係的;strlen是用來計算字串的長度的,只能傳遞合法的字串進去才有意義,如果隨便傳遞一個字元指標,但是這個字元指標並不是字串是沒有意義的。
(4)當我們定義陣列時如果沒有明確給出陣列大小,則必須同時給出初始化式,編譯器會根據初始化式去自動計算陣列的大小(陣列定義時必須給出大小,要麼直接給,要麼給初始化式)

4.5.6.2、字串初始化與sizeof、strlen
(1)char *p = “linux”; sizeof§得到的永遠是4,因為這時候sizeof測的是字元指標p本身的長度,和字串的長度是無關的。
(2)strlen剛好用來計算字串的長度。

4.5.6.3、字元陣列與字串的本質差異(記憶體分配角度)
(1)字元陣列char a[] = “linux”;來說,定義了一個數組a,陣列a佔6位元組,右值"linux"本身只存在於編譯器中,編譯器將它用來初始化字元陣列a後丟棄掉(也就是說記憶體中是沒有"linux"這個字串的);這句就相當於是:char a[] = {‘l’, ‘i’, ‘n’, ‘u’, ‘x’, ‘\0’};
(2)字串char *p = “linux”;定義了一個字元指標p,p佔4位元組,分配在棧上;同時還定義了一個字串"linux",分配在程式碼段;然後把程式碼段中的字串(一共佔6位元組)的首地址(也就是’l’的地址)賦值給p。
總結對比:字元陣列和字串有本質差別。字元陣列本身是陣列,陣列自身自帶記憶體空間,可以用來存東西(所以陣列類似於容器);而字串本身是指標,本身永遠只佔4位元組,而且這4個位元組還不能用來存有效資料,所以只能把有效資料存到別的地方,然後把地址存在p中。
也就是說字元陣列自己存那些字元;字串一定需要額外的記憶體來存那些字元,字串本身只存真正的那些字元所在的記憶體空間的首地址。

4.5.7.C語言之結構體概述
4.5.7.1、結構體型別是一種自定義型別
(1)C語言中的2種類型:原生型別和自定義型別。
4.5.7.2、結構體使用時先定義結構體型別再用型別定義變數
(1)結構體定義時需要先定義結構體型別,然後再用型別來定義變數。
(2)也可以在定義結構體型別的同時定義結構體變數。

4.5.7.3、從陣列到結構體的進步之處
(1)結構體可以認為是從陣列發展而來的。其實陣列和結構體都算是資料結構的範疇了,陣列就是最簡單的資料結構、結構體比陣列更復雜一些,連結串列、雜湊表之類的比結構體又複雜一些;二叉樹、圖等又更復雜一些。
(2)陣列有2個明顯的缺陷:第一個是定義時必須明確給出大小,且這個大小在以後不能再更改;第二個是陣列要求所有的元素的型別必須一致。更復雜的資料結構中就致力於解決陣列的這兩個缺陷。
(3)結構體是用來解決陣列的第二個缺陷的,可以將結構體理解為一個其中元素型別可以不相同的陣列。結構體完全可以取代陣列,只是在陣列可用的範圍內陣列比結構體更簡單。

4.5.7.4、結構體變數中的元素如何訪問?
(1)陣列中元素的訪問方式:表面上有2種方式(陣列下標方式和指標方式);實質上都是指標方式訪問。
(2)結構體變數中的元素訪問方式:只有一種,用.或者->的方式來訪問。(.和->訪問結構體元素其實質是一樣的,只是C語言規定用結構體變數來訪問元素用. 用結構體變數的指標來訪問元素用->。實際上在高階語言中已經不區分了,都用.)
(3)結構體的訪問方式有點類似於陣列下標的方式
思考:結構體變數的點號或者->訪問元素的實質是什麼?其實本質上還是用指標來訪問的。

4.5.8.結構體的對齊訪問1
4.5.8.1、舉例說明什麼是結構體對齊訪問
(1)上節講過結構體中元素的訪問其實本質上還是用指標方式,結合這個元素在整個結構體中的偏移量和這個元素的型別來進行訪問的。
(2)但是實際上結構體的元素的偏移量比我們上節講的還要複雜,因為結構體要考慮元素的對齊訪問,所以每個元素時間佔的位元組數和自己本身的型別所佔的位元組數不一定完全一樣。(譬如char c實際佔位元組數可能是1,也可以是2,也可能是3,也可以能4····)
(3)一般來說,我們用.的方式來訪問結構體元素時,我們是不用考慮結構體的元素對齊的。因為編譯器會幫我們處理這個細節。但是因為C語言本身是很底層的語言,而且做嵌入式開發經常需要從記憶體角度,以指標方式來處理結構體及其中的元素,因此還是需要掌握結構體對齊規則。

4.5.8.2、結構體為何要對齊訪問
(1)結構體中元素對齊訪問主要原因是為了配合硬體,也就是說硬體本身有物理上的限制,如果對齊排布和訪問會提高效率,否則會大大降低效率。
(2)記憶體本身是一個物理器件(DDR記憶體晶片,SoC上的DDR控制器),本身有一定的侷限性:如果記憶體每次訪問時按照4位元組對齊訪問,那麼效率是最高的;如果你不對齊訪問效率要低很多。
(3)還有很多別的因素和原因,導致我們需要對齊訪問。譬如Cache的一些快取特性,還有其他硬體(譬如MMU、LCD顯示器)的一些記憶體依賴特性,所以會要求記憶體對齊訪問。
(4)對比對齊訪問和不對齊訪問:對齊訪問犧牲了記憶體空間,換取了速度效能;而非對齊訪問犧牲了訪問速度效能,換取了記憶體空間的完全利用。

4.3.8.3、結構體對齊的規則和運算
(1)編譯器本身可以設定記憶體對齊的規則,有以下的規則需要記住:
第一個:32位編譯器,一般編譯器預設對齊方式是4位元組對齊。

總結下:結構體對齊的分析要點和關鍵:
1、結構體對齊要考慮:結構體整體本身必須安置在4位元組對齊處,結構體對齊後的大小必須4的倍數(編譯器設定為4位元組對齊時,如果編譯器設定為8位元組對齊,則這裡的4是8)
2、結構體中每個元素本身都必須對其存放,而每個元素本身都有自己的對齊規則。
3、編譯器考慮結構體存放時,以滿足以上2點要求的最少記憶體需要的排布來算。

4.5.8.4、gcc支援但不推薦的對齊指令:#pragma pack() #pragma pack(n) (n=1/2/4/8)
(1)#pragma是用來指揮編譯器,或者說設定編譯器的對齊方式的。編譯器的預設對齊方式是4,但是有時候我不希望對齊方式是4,而希望是別的(譬如希望1位元組對齊,也可能希望是8,甚至可能希望128位元組對齊)。
(2)常用的設定編譯器編譯器對齊命令有2種:第一種是#pragma pack(),這種就是設定編譯器1位元組對齊(有些人喜歡講:設定編譯器不對齊訪問,還有些講:取消編譯器對齊訪問);第二種是#pragma pack(4),這個括號中的數字就表示我們希望多少位元組對齊。
(3)我們需要#prgama pack(n)開頭,以#pragma pack()結尾,定義一個區間,這個區間內的對齊引數就是n。
(4)#prgma pack的方式在很多C環境下都是支援的,但是gcc雖然也可以不過不建議使用。

4.5.8.5、gcc推薦的對齊指令__attribute__((packed)) attribute((aligned(n)))
(1)attribute((packed))使用時直接放在要進行記憶體對齊的型別定義的後面,然後它起作用的範圍只有加了這個東西的這一個型別。packed的作用就是取消對齊訪問。
(2)attribute((aligned(n)))使用時直接放在要進行記憶體對齊的型別定義的後面,然後它起作用的範圍只有加了這個東西的這一個型別。它的作用是讓整個結構體變數整體進行n位元組對齊(注意是結構體變數整體n位元組對齊,而不是結構體內各元素也要n位元組對齊)

4.5.8.6、參考閱讀blog:
http://www.cnblogs.com/dolphin0520/archive/2011/09/17/2179466.html
http://blog.csdn.net/sno_guo/article/details/8042332

4.5.11.offsetof巨集與container_of巨集
4.5.11.1、由結構體指標進而訪問各元素的原理
(1)通過結構體整體變數來訪問其中各個元素,本質上是通過指標方式來訪問的,形式上是通過.的方式來訪問的(這時候其實是編譯器幫我們自動計算了偏移量)。
4.5.11.2、offsetof巨集:
(1)offsetof巨集的作用是:用巨集來計算結構體中某個元素和結構體首地址的偏移量(其實質是通過編譯器來幫我們計算)。
(2)offsetof巨集的原理:我們虛擬一個type型別結構體變數,然後用type.member的方式來訪問那個member元素,繼而得到member相對於整個變數首地址的偏移量。
(3)學習思路:第一步先學會用offsetof巨集,第二步再去理解這個巨集的實現原理。
(TYPE *)0 這是一個強制型別轉換,把0地址強制型別轉換成一個指標,這個指標指向一個TYPE型別的結構體變數。 (實際上這個結構體變數可能不存在,但是隻要我不去解引用這個指標就不會出錯)。
((TYPE *)0)->MEMBER (TYPE *)0是一個TYPE型別結構體變數的指標,通過指標指標來訪問這個結構體變數的member元素

&((TYPE *)0)->MEMBER 等效於&(((TYPE *)0)->MEMBER),意義就是得到member元素的地址。但是因為整個結構體變數的首地址是0,

4.5.11.3、container_of巨集:
(1)作用:知道一個結構體中某個元素的指標,反推這個結構體變數的指標。有了container_of巨集,我們可以從一個元素的指標得到整個結構體變數的指標,繼而得到結構體中其他元素的指標。
(2)typeof關鍵字的作用是:typepef(a)時由變數a得到a的型別,typeof就是由變數名得到變數資料型別的。
(3)這個巨集的工作原理:先用typeof得到member元素的型別定義成一個指標,然後用這個指標減去該元素相對於整個結構體變數的偏移量(偏移量用offsetof巨集得到的),減去之後得到的就是整個結構體變數的首地址了,再把這個地址強制型別轉換為type *即可。

4.5.11.4、學習指南和要求:
(1)最基本要求是:必須要會這兩個巨集的使用。就是說能知道這兩個巨集接收什麼引數,返回什麼值,會用這兩個巨集來寫程式碼。看見程式碼中別人用這兩個巨集能理解什麼意思。
(2)升級要求:能理解這兩個巨集的工作原理,能表述出來。(有些面試筆試題會這麼要求)
(3)更高階要求:能自己寫出這兩個巨集(不要著急,慢慢來)

4.5.12.共用體union
4.5.12.1、共用體型別的定義、變數定義和使用
(1)共用體union和結構體struct在型別定義、變數定義、使用方法上很相似。
(2)共用體和結構體的不同:結構體類似於一個包裹,結構體中的成員彼此是獨立存在的,分佈在記憶體的不同單元中,他們只是被打包成一個整體叫做結構體而已;共用體中的各個成員其實是一體的,彼此不獨立,他們使用同一個記憶體單元。可以理解為:有時候是這個元素,有時候是那個元素。更準確的說法是同一個記憶體空間有多種解釋方式。
(3)共用體union就是對同一塊記憶體中儲存的二進位制的不同的理解方式。
(4)在有些書中把union翻譯成聯合(聯合體),這個名字不好。現在翻譯成共用體比較合適。
(5)union的sizeof測到的大小實際是union中各個元素裡面佔用記憶體最大的那個元素的大小。因為可以存的下這個就一定能夠存的下其他的元素。
(6)union中的元素不存在記憶體對齊的問題,因為union中實際只有1個記憶體空間,都是從同一個地址開始的(開始地址就是整個union佔有的記憶體空間的首地址),所以不涉及記憶體對齊。

4.5.12.2、共用體和結構體的相同和不同
(1)相同點就是操作語法幾乎相同。
(2)不同點是本質上的不同。struct是多個獨立元素(記憶體空間)打包在一起;union是一個元素(記憶體空間)的多種不同解析方式。

4.5.12.3、共用體的主要用途
(1)共用體就用在那種對同一個記憶體單元進行多種不同規則解析的這種情況下。
(2)C語言中其實是可以沒有共用體的,用指標和強制型別轉換可以替代共用體完成同樣的功能,但是共用體的方式更簡單、更便捷、更好理解。

4.5.13.大小端模式1
4.5.13.1、什麼是大小端模式
(1)大端模式(big endian)和小端模式(little endian)。最早是小說中出現的詞,和計算機本來沒關係的。
(2)後來計算機通訊發展起來後,遇到一個問題就是:在串列埠等序列通訊中,一次只能傳送1個位元組。這時候我要傳送一個int型別的數就遇到一個問題。int型別有4個位元組,我是按照:byte0 byte1 byte2 byte3這樣的順序傳送,還是按照byte3 byte2 byte1 byte0這樣的順序傳送。規則就是傳送方和接收方必須按照同樣的位元組順序來通訊,否則就會出現錯誤。這就叫通訊系統中的大小端模式。這是大小端這個詞和計算機掛鉤的最早問題。
(3)現在我們講的這個大小端模式,更多是指計算機儲存系統的大小端。在計算機記憶體/硬碟/Nnad中。因為儲存系統是32位的,但是資料仍然是按照位元組為單位的。於是乎一個32位的二進位制在記憶體中儲存時有2種分佈方式:高位元組對應高地址(大端模式)、高位元組對應低地址(小端模式)
(4)大端模式和小端模式本身沒有對錯,沒有優劣,理論上按照大端或小端都可以,但是要求必須儲存時和讀取時按照同樣的大小端模式來進行,否則會出錯。
(5)現實的情況就是:有些CPU公司用大端(譬如C51微控制器);有些CPU用小端(譬如ARM)。(大部分是用小端模式,大端模式的不算多)。於是乎我們寫程式碼時,當不知道當前環境是用大端模式還是小端模式時就需要用程式碼來檢測當前系統的大小端。

經典筆試題:用C語言寫一個函式來測試當前機器的大小端模式。

4.5.13.2、用union來測試機器的大小端模式

4.5.13.3、指標方式來測試機器的大小端

4.5.14.大小端模式2
4.5.14.1、看似可行實則不行的測試大小端方式:位與、移位、強制型別轉化
(1)位與運算。
結論:位與的方式無法測試機器的大小端模式。(表現就是大端機器和小端機器的&運算後的值相同的)
理論分析:位與運算是編譯器提供的運算,這個運算是高於記憶體層次的(或者說&運算在二進位制層次具有可移植性,也就是說&的時候一定是高位元組&高位元組,低位元組&低位元組,和二進位制儲存無關)。
(2)移位
結論:移位的方式也不能測試機器大小端。
理論分析:原因和&運算子不能測試一樣,因為C語言對運算子的級別是高於二進位制層次的。右移運算永遠是將低位元組移除,而和二進位制儲存時這個低位元組在高位還是低位無關的。
(3)強制型別轉換
同上

4.5.14.2、通訊系統中的大小端(陣列的大小端)
(1)譬如要通過串列埠傳送一個0x12345678給接收方,但是因為串列埠本身限制,只能以位元組為單位來發送,所以需要發4次;接收方分4次接收,內容分別是:0x12、0x34、0x56、0x78.接收方接收到這4個位元組之後需要去重組得到0x12345678(而不是得到0x78563412).
(2)所以在通訊雙方需要有一個默契,就是:先發/先接的是高位還是低位?這就是通訊中的大小端問題。
(3)一般來說是:先發低位元組叫小端;先發高位元組就叫大端。(我不能確定)實際操作中,在通訊協議裡面會去定義大小端,明確告訴你先發的是低位元組還是高位元組。
(4)在通訊協議中,大小端是非常重要的,大家使用別人定義的通訊協議還是自己要去定義通訊協議,一定都要注意標明通訊協議中大小端的問題。

4.5.15.列舉
4.5.15.1、列舉是用來幹嘛的?
(1)列舉在C語言中其實是一些符號常量集。直白點說:列舉定義了一些符號,這些符號的本質就是int型別的常量,每個符號和一個常量繫結。這個符號就表示一個自定義的一個識別碼,編譯器對列舉的認知就是符號常量所繫結的那個int型別的數字。
(2)列舉中的列舉值都是常量,怎麼驗證?
(3)列舉符號常量和其對應的常量數字相對來說,數字不重要,符號才重要。符號對應的數字只要彼此不相同即可,沒有別的要求。所以一般情況下我們都不明確指定這個符號所對應的數字,而讓編譯器自動分配。(編譯器自動分配的原則是:從0開始依次增加。如果使用者自己定義了一個值,則從那個值開始往後依次增加)
4.5.15.2、C語言為何需要列舉
(1)C語言沒有列舉是可以的。使用列舉其實就是對1、0這些數字進行符號化編碼,這樣的好處就是程式設計時可以不用看數字而直接看符號。符號的意義是顯然的,一眼可以看出。而數字所代表的含義除非看文件或者註釋。
(2)巨集定義的目的和意義是:不用數字而用符號。從這裡可以看出:巨集定義和列舉有內在聯絡。巨集定義和列舉經常用來解決類似的問題,他們倆基本相當可以互換,但是有一些細微差別。
4.5.15.3、巨集定義和列舉的區別
(1)列舉是將多個有關聯的符號封裝在一個列舉中,而巨集定義是完全散的。也就是說列舉其實是多選一。
(2)什麼情況下用列舉?當我們要定義的常量是一個有限集合時(譬如一星期有7天,譬如一個月有31天,譬如一年有12個月····),最適合用列舉。(其實巨集定義也行,但是列舉更好)
(3)不能用列舉的情況下(定義的常量符號之間無關聯,或者無限的)用巨集定義。
總結:巨集定義先出現,用來解決符號常量的問題;後來人們發現有時候定義的符號常量彼此之間有關聯(多選一的關係),用巨集定義來做雖然可以但是不貼切,於是乎發明了列舉來解決這種情況。

4.5.15.3、列舉的定義和使用