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

6.陣列&字串&結構體&共用體&列舉

1.程式中記憶體從哪裡來1

1.1、程式執行需要記憶體支援

()對程式來說,記憶體就是程式的立足之地(程式是被放在記憶體中執行的);程式執行時需要記憶體來儲存一些臨時變數。

1.2、記憶體管理最終是由作業系統完成的

(1)記憶體本身在物理上是一個硬體器件,由硬體系統提供。

(2)記憶體是由作業系統統一管理。為了記憶體管理方便又合理,作業系統提供了多種機制來讓我們應用程式使用記憶體。這些機制彼此不同,各自有各自的特點,我們程式根據自己的實際情況來選擇某種方式獲取記憶體(在作業系統處登記這塊記憶體的臨時使用許可權)、使用記憶體、釋放記憶體(向作業系統歸還這塊記憶體的使用許可權)。

1.3、三種記憶體來源:棧(stack)、堆(heap)、資料區(.data)

(1)在一個C語言程式中,能夠獲取的記憶體就是三種情況:棧(stack)、堆(heap)、資料區(.data)

1.4、棧的詳解

    執行時自動分配&自動回收:棧是自動管理的,程式設計師不需要手工干預。方便簡單。

    反覆使用:棧記憶體在程式中其實就是那一塊空間,程式反覆使用這一塊空間。

    髒記憶體:棧記憶體由於反覆使用,每次使用後程序不會去清理,因此分配到時保留原來的值。

    臨時性:(函式不能返回棧變數的指標,因為這個空間是臨時的)

    棧會溢位:因為作業系統事先給定了棧的大小,如果在函式中無窮盡的分配棧記憶體總能用完。

2.程式中記憶體從哪裡來2

2.1、堆記憶體詳解

    作業系統堆管理器管理:堆管理器是作業系統的一個模組,堆管理記憶體分配靈活,按需分配。

    大塊記憶體:堆記憶體管理者總量很大的作業系統記憶體塊,各程序可以按需申請使用,使用完釋放。

    程式手動申請&釋放:手工意思是需要寫程式碼去申請malloc和釋放free。

    髒記憶體:堆記憶體也是反覆使用的,而且使用者用完釋放前不會清除,因此也是髒的。

    臨時性:堆記憶體只在malloc和free之間屬於我這個程序,而可以訪問。在malloc之前和free之後都不能再訪問,否則會有不可預料的後果。

(1)void *是個指標型別,malloc返回的是一個void *型別的指標,實質上malloc返回的是堆管理器分配給我本次申請的那段記憶體空間的首地址(malloc返回的值其實是一個數字,這個數字表示一個記憶體地址)。為什麼要使用void *作為型別?主要原因是malloc幫我們分配記憶體時只是分配了記憶體空間,至於這段空間將來用來儲存什麼型別的元素malloc是不關心的,由我們程式自己來決定。

(2)什麼是void型別。早期被翻譯成空型,這個翻譯非常不好,會誤導人。void型別不表示沒有型別,而表示萬能型別。void的意思就是說這個資料的型別當前是不確定的,在需要的時候可以再去指定它的具體型別。void *型別是一個指標型別,這個指標本身佔4個位元組,但是指標指向的型別是不確定的,換句話說這個指標在需要的時候可以被強制轉化成其他任何一種確定型別的指標,也就是說這個指標可以指向任何型別的元素。

(3)malloc的返回值:成功申請空間後返回這個記憶體空間的指標,申請失敗時返回NULL。所以malloc獲取的記憶體指標使用前一定要先檢驗是否為NULL。

(4)malloc申請的記憶體時用完後要free釋放。free(p);會告訴堆管理器這段記憶體我用完了你可以回收了。堆管理器回收了這段記憶體後這段記憶體當前程序就不應該再使用了。因為釋放後堆管理器就可能把這段記憶體再次分配給別的程序,所以你就不能再使用了。

(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.程式中記憶體從哪裡來3

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.2、有些特殊資料會被放到程式碼段

(1)C語言中使用char *p = "linux";定義字串時,字串"linux"實際被分配在程式碼段,也就是說這個"linux"字串實際上是一個常量字串而不是變數字串。

(2)const型常量:C語言中const關鍵字用來定義常量,常量就是不能被改變的量。const的實現方法至少有2種:第一種就是編譯將const修飾的變數放在程式碼段去以實現不能修改(普遍見於各種微控制器的編譯器);第二種就是由編譯器來檢查以確保const型的常量不會被修改,實際上const型的常量還是和普通變數一樣放在資料段的(gcc中就是這樣實現的)。

4.3、顯式初始化為非零的全域性變數和靜態區域性變數放在資料段

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

4.4、未初始化或顯式初始化為0的全域性變數放在bss段

(1)bss段和.data段並沒有本質區別,幾乎可以不用明確去區分這兩種。

4.5、總結:C語言中所有變數和常量所使用的記憶體無非以上三種情況。

(1)相同點:三種獲取記憶體的方法,都可以給程式提供可用記憶體,都可以用來定義變數給程式用。

(2)不同點:棧記憶體對應C中的普通區域性變數(別的變數還用不了棧,而且棧是自動的,由編譯器和執行時環境共同來提供服務的,程式設計師無法手工控制);堆記憶體完全是獨立於我們的程式存在和管理的,程式需要記憶體時可以去手工申請malloc,使用完成後必須儘快free釋放。(堆記憶體對程式就好象公共圖書館對於人);資料段對於程式來說對應C程式中的全域性變數和靜態區域性變數。

(3)如果我需要一段記憶體來儲存資料,我究竟應該把這個資料儲存在哪裡?(或者說我要定義一個變數,我究竟應該定義為區域性變數還是全域性變數還是用malloc來實現)。不同的儲存方式有不同的特點,簡單總結如下:

    * 函式內部臨時使用,出了函式不會用到,就定義區域性變數

    * 堆記憶體和資料段幾乎擁有完全相同的屬性,大部分時候是可以完全替換的。但是生命週期不一

      堆記憶體的生命週期是從malloc開始到free結束,而全域性變數是從整個程式一開始執行就開始,

      直到整個程式結束才會消滅,伴隨程式執行的一生。啟示:如果你這個變數只是在程式的一個階段有用,用完就不用了,就適合用堆記憶體;如果這個變數本身和程式是一生相伴的,那就適合用全域性變數。(堆記憶體就好象租房、資料段就好象買房。堆記憶體就好象圖書館借書,資料段就好象自己書店買書)你以後會慢慢發現:買不如租,堆記憶體的使用比全域性變數廣泛。

5.C語言的字串型別

5.1、C語言沒有原生字串型別

(1)很多高階語言像java、C#等就有字串型別,有個String來表示字串,用法和int這些很像,可以String s1 = "linux";來定義字串型別的變數。

(2)C語言沒有String型別,C語言中的字串是通過字元指標來間接實現的。

5.2、C語言使用指標來管理字串

(1)C語言中定義字串方法:char *p = "linux";此時p就叫做字串,但是實際上p只是一個字元指標(本質上就是一個指標變數,只是p指向了一個字串的起始地址而已)。

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'字元的字串),這種思路就叫“魔數”(魔數就是選出來的一個特殊的數字,這個數字表示一個特殊的含義,你的正式內容中不能包含這個魔數作為內容)。

5.4、注意:指向字串的指標和字串本身是分開的兩個東西

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

5.5、儲存多個字元的2種方式:字串和字元陣列

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

6.字串和字元陣列的細節

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)當我們定義陣列時如果沒有明確給出陣列大小,則必須同時給出初始化式,編譯器會根據初始化式去自動計算陣列的大小(陣列定義時必須給出大小,要麼直接給,要麼給初始化式)

6.2、字串初始化與sizeof、strlen

(1)char *p = "linux"; sizeof(p)得到的永遠是4,因為這時候sizeof測的是字元指標p本身的長度,和字串的長度是無關的。

(2)strlen剛好用來計算字串的長度。

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中。

也就是說字元陣列自己存那些字元;字串一定需要額外的記憶體來存那些字元,字串本身只存真正的那些字元所在的記憶體空間的首地址。

7.C語言之結構體概述

7.1、結構體型別是一種自定義型別

(1)C語言中的2種類型:原生型別和自定義型別。

7.2、結構體使用時先定義結構體型別再用型別定義變數

(1)結構體定義時需要先定義結構體型別,然後再用型別來定義變數。

(2)也可以在定義結構體型別的同時定義結構體變數。

7.3、從陣列到結構體的進步之處

(1)結構體可以認為是從陣列發展而來的。其實陣列和結構體都算是資料結構的範疇了,陣列就是最簡單的資料結構、結構體比陣列更復雜一些,連結串列、雜湊表之類的比結構體又複雜一些;二叉樹、圖等又更復雜一些。

(2)陣列有2個明顯的缺陷:第一個是定義時必須明確給出大小,且這個大小在以後不能再更改;第二個是陣列要求所有的元素的型別必須一致。更復雜的資料結構中就致力於解決陣列的這兩個缺陷。

(3)結構體是用來解決陣列的第二個缺陷的,可以將結構體理解為一個其中元素型別可以不相同的陣列。結構體完全可以取代陣列,只是在陣列可用的範圍內陣列比結構體更簡單。

7.4、結構體變數中的元素如何訪問?

(1)陣列中元素的訪問方式:表面上有2種方式(陣列下標方式和指標方式);實質上都是指標方式訪問。

(2)結構體變數中的元素訪問方式:只有一種,用.或者->的方式來訪問。(.和->訪問結構體元素其實質是一樣的,只是C語言規定用結構體變數來訪問元素用. 用結構體變數的指標來訪問元素用->。實際上在高階語言中已經不區分了,都用.)

(3)結構體的訪問方式有點類似於陣列下標的方式

思考:結構體變數的點號或者->訪問元素的實質是什麼?其實本質上還是用指標來訪問的。

8.結構體的對齊訪問1

8.1、舉例說明什麼是結構體對齊訪問

(1)上節講過結構體中元素的訪問其實本質上還是用指標方式,結合這個元素在整個結構體中的偏移量和這個元素的型別來進行訪問的。

(2)但是實際上結構體的元素的偏移量比我們上節講的還要複雜,因為結構體要考慮元素的對齊訪問,所以每個元素時間佔的位元組數和自己本身的型別所佔的位元組數不一定完全一樣。(譬如char c實際佔位元組數可能是1,也可以是2,也可能是3,也可以能4····)

(3)一般來說,我們用.的方式來訪問結構體元素時,我們是不用考慮結構體的元素對齊的。因為編譯器會幫我們處理這個細節。但是因為C語言本身是很底層的語言,而且做嵌入式開發經常需要從記憶體角度,以指標方式來處理結構體及其中的元素,因此還是需要掌握結構體對齊規則。

8.2、結構體為何要對齊訪問

(1)結構體中元素對齊訪問主要原因是為了配合硬體,也就是說硬體本身有物理上的限制,如果對齊排布和訪問會提高效率,否則會大大降低效率。

(2)記憶體本身是一個物理器件(DDR記憶體晶片,SoC上的DDR控制器),本身有一定的侷限性:如果記憶體每次訪問時按照4位元組對齊訪問,那麼效率是最高的;如果你不對齊訪問效率要低很多。

(3)還有很多別的因素和原因,導致我們需要對齊訪問。譬如Cache的一些快取特性,還有其他硬體(譬如MMU、LCD顯示器)的一些記憶體依賴特性,所以會要求記憶體對齊訪問。

(4)對比對齊訪問和不對齊訪問:對齊訪問犧牲了記憶體空間,換取了速度效能;而非對齊訪問犧牲了訪問速度效能,換取了記憶體空間的完全利用。

8.3、結構體對齊的規則和運算

(1)編譯器本身可以設定記憶體對齊的規則,有以下的規則需要記住:

第一個:32位編譯器,一般編譯器預設對齊方式是4位元組對齊。

總結下:結構體對齊的分析要點和關鍵:

1、結構體對齊要考慮:結構體整體本身必須安置在4位元組對齊處,結構體對齊後的大小必須4的倍數(編譯器設定為4位元組對齊時,如果編譯器設定為8位元組對齊,則這裡的4是8)

2、結構體中每個元素本身都必須對其存放,而每個元素本身都有自己的對齊規則。

3、編譯器考慮結構體存放時,以滿足以上2點要求的最少記憶體需要的排布來算。

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位元組對齊)

#include <stdio.h>
 
//#pragma pack(128)
 
// 分析過程:
// 首先是整個結構體,整個結構體變數4位元組對齊是由編譯器保證的,我們不用操心。
// 然後是第一個元素a,a的開始地址就是整個結構體的開始地址,所以自然是4位元組對齊的。但是a
// 的結束地址要由下一個元素說了算。
// 然後是第二個元素b,因為上一個元素a本身佔4位元組,本身就是對齊的。所以留給b的開始地址也是
// 4位元組對齊地址,所以b可以直接放(b放的位置就決定了a一共佔4位元組,因為不需要填充)。
// b的起始地址定了後,結束地址不能定(因為可能需要填充),結束地址要看下一個元素來定。
// 然後是第三個元素c,short型別需要2位元組對齊(short型別元素必須放在類似0,2,4,8這樣的
// 地址處,不能放在1,3這樣的奇數地址處),因此c不能緊挨著b來存放,解決方案是在b之後新增1
// 位元組的填充(padding),然後再開始放c。c放完之後還沒結束
// 當整個結構體的所有元素都對齊存放後,還沒結束,因為整個結構體大小還要是4的整數倍。
struct mystruct1
{                   // 1位元組對齊   4位元組對齊
    int a;         // 4           4
    char b;        // 1           2(1+1)
    short c;       // 2           2
};
 
struct mystruct11
{                  // 1位元組對齊   4位元組對齊
    int a;         // 4           4
    char b;        // 1           2(1+1)
    short c;       // 2           2
}__attribute__((packed));
 
typedef struct mystruct111
{                  // 1位元組對齊    4位元組對齊       2位元組對齊
    int a;         // 4           4               4
    char b;        // 1           2(1+1)          2
    short c;       // 2           2               2
    short d;       // 2           4(2+2)          2
}__attribute__((aligned(1024))) My111;
 
typedef struct mystruct2
{                  // 1位元組對齊    4位元組對齊
    char a;        // 1           4(1+3)
    int b;         // 4           4
    short c;       // 2           4(2+2)
}MyS2;
 
struct mystruct21
{                  // 1位元組對齊    4位元組對齊
    char a;        // 1           4(1+3)
    int b;         // 4           4
    short c;       // 2           4(2+2)
} __attribute__((packed));
 
 
/*
typedef struct 
{
    int a;
    short b;
    static int c;
}MyS3;
*/
 
typedef struct myStruct5
{                         // 1位元組對齊   4位元組對齊
    int a;                // 4           4
    struct mystruct1 s1;  // 7           8
    double b;             // 8           8
    int c;                // 4           4  
}MyS5;
 
struct stu
{                           // 1位元組對齊   4位元組對齊
    char sex;              // 1           4(1+3)
    int length;                // 4           4
    char name[10];         // 10          12(10+2)
};
 
//#pragma pack()
 
int main(void)
{
    printf("sizeof(struct mystruct1) = %d.\n", sizeof(struct mystruct1));
    printf("sizeof(struct mystruct2) = %d.\n", sizeof(struct mystruct2));
    printf("sizeof(struct mystruct5) = %d.\n", sizeof(MyS5));
    printf("sizeof(struct stu) = %d.\n", sizeof(struct stu));
     
    printf("sizeof(struct mystruct11) = %d.\n", sizeof(struct mystruct11));
    printf("sizeof(struct mystruct21) = %d.\n", sizeof(struct mystruct21));
     
    printf("sizeof(struct mystruct111) = %d.\n", sizeof(My111));
     
    struct mystruct1 s1;
    struct mystruct1 __attribute__((packed)) s2 ;       // 定義變數時加這個沒作用
    printf("sizeof(s1) = %d.\n", sizeof(s1));
    printf("sizeof(s2) = %d.\n", sizeof(s2));
     
    struct stu s3 __attribute__((aligned(2)));
    printf("sizeof(s3) = %d.\n", sizeof(s3));
 
     
    return 0;
}

8.6、參考閱讀blog:

11.offsetof巨集與container_of巨集

11.1、由結構體指標進而訪問各元素的原理

(1)通過結構體整體變數來訪問其中各個元素,本質上是通過指標方式來訪問的,形式上是通過.的方式來訪問的(這時候其實是編譯器幫我們自動計算了偏移量)。

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,

11.3、container_of巨集:

(1)作用:知道一個結構體中某個元素的指標,反推這個結構體變數的指標。有了container_of巨集,我們可以從一個元素的指標得到整個結構體變數的指標,繼而得到結構體中其他元素的指標。

(2)typeof關鍵字的作用是:typepof(a)時由變數a得到a的型別,typeof就是由變數名得到變數資料型別的。

(3)這個巨集的工作原理:先用typeof得到member元素的型別定義成一個指標,然後用這個指標減去該元素相對於整個結構體變數的偏移量(偏移量用offsetof巨集得到的),減去之後得到的就是整個結構體變數的首地址了,再把這個地址強制型別轉換為type *即可。

11.4、學習指南和要求:

(1)最基本要求是:必須要會這兩個巨集的使用。就是說能知道這兩個巨集接收什麼引數,返回什麼值,會用這兩個巨集來寫程式碼。看見程式碼中別人用這兩個巨集能理解什麼意思。

(2)升級要求:能理解這兩個巨集的工作原理,能表述出來。(有些面試筆試題會這麼要求)

(3)更高階要求:能自己寫出這兩個巨集(不要著急,慢慢來)

#include <stdio.h>
 
struct mystruct
{
    char a;        // 0
    int b;         // 4
    short c;       // 8
};
 
// TYPE是結構體型別,MEMBER是結構體中一個元素的元素名
// 這個巨集返回的是member元素相對於整個結構體變數的首地址的偏移量,型別是int
#define offsetof(TYPE, MEMBER) ((int) &((TYPE *)0)->MEMBER)
 
// ptr是指向結構體元素member的指標,type是結構體型別,member是結構體中一個元素的元素名
// 這個巨集返回的就是指向整個結構體變數的指標,型別是(type *)
#define container_of(ptr, type, member) ({          \
    const typeof(((type *)0)->member) * __mptr = (ptr);    \
    (type *)((char *)__mptr - offsetof(type, member)); })
 
int main(void)
{
    struct mystruct s1;
    struct mystruct *pS = NULL;
     
    short *p = &(s1.c);      // p就是指向結構體中某個member的指標
     
    printf("s1的指標等於:%p.\n", &s1);
     
    // 問題是要通過p來計算得到s1的指標
    pS = container_of(p, struct mystruct, c);
    printf("pS等於:%p.\n", pS);
     
/*
    struct mystruct s1;
    s1.b = 12;
     
    int *p = (int *)((char *)&s1 + 4);
    printf("*p = %d.\n", *p);
     
    int offsetofa = offsetof(struct mystruct, a);
    printf("offsetofa = %d.\n", offsetofa);
     
    int offsetofb = offsetof(struct mystruct, b);
    printf("offsetofb = %d.\n", offsetofb);
     
    int offsetofc = offsetof(struct mystruct, c);
    printf("offsetofc = %d.\n", offsetofc);
     
    printf("整個結構體變數的首地址:%p.\n", &s1);
    printf("s1.b的首地址:%p.\n", &(s1.b));
    printf("偏移量是:%d.\n", (char *)&(s1.b) - (char *)&s1);
*/ 
     
     
     
     
    return 0;
}

12.共用體union

12.1、共用體型別的定義、變數定義和使用

(1)共用體union和結構體struct在型別定義、變數定義、使用方法上很相似。

(2)共用體和結構體的不同:結構體類似於一個包裹,結構體中的成員彼此是獨立存在的,分佈在記憶體的不同單元中,他們只是被打包成一個整體叫做結構體而已;共用體中的各個成員其實是一體的,彼此不獨立,他們使用同一個記憶體單元。可以理解為:有時候是這個元素,有時候是那個元素。更準確的說法是同一個記憶體空間有多種解釋方式。

(3)共用體union就是對同一塊記憶體中儲存的二進位制的不同的理解方式。

(4)在有些書中把union翻譯成聯合(聯合體),這個名字不好。現在翻譯成共用體比較合適。

(5)union的sizeof測到的大小實際是union中各個元素裡面佔用記憶體最大的那個元素的大小。因為可以存的下這個就一定能夠存的下其他的元素。

(6)union中的元素不存在記憶體對齊的問題,因為union中實際只有1個記憶體空間,都是從同一個地址開始的(開始地址就是整個union佔有的記憶體空間的首地址),所以不涉及記憶體對齊。

12.2、共用體和結構體的相同和不同

(1)相同點就是操作語法幾乎相同。

(2)不同點是本質上的不同。struct是多個獨立元素(記憶體空間)打包在一起;union是一個元素(記憶體空間)的多種不同解析方式。

12.3、共用體的主要用途

(1)共用體就用在那種對同一個記憶體單元進行多種不同規則解析的這種情況下。

(2)C語言中其實是可以沒有共用體的,用指標和強制型別轉換可以替代共用體完成同樣的功能,但是共用體的方式更簡單、更便捷、更好理解。

13.大小端模式1

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語言寫一個函式來測試當前機器的大小端模式。

#include <stdio.h>
 
int main (void)
{
    int i=big_or_small();
    if (i==0)
        printf("大端模式\n");
    else
        printf("小端模式\n");

    return 0;
}
 
int big_or_small (void)
{
    int a=1;
    char b=*((char*)&a);
    return b ;
     
}

4.大小端模式2

14.1、看似可行實則不行的測試大小端方式:位與、移位、強制型別轉化

(1)位與運算。

結論:位與的方式無法測試機器的大小端模式。(表現就是大端機器和小端機器的&運算後的值相同的)

理論分析:位與運算是編譯器提供的運算,這個運算是高於記憶體層次的(或者說&運算在二進位制層次具有可移植性,也就是說&的時候一定是高位元組&高位元組,低位元組&低位元組,和二進位制儲存無關)。

(2)移位

結論:移位的方式也不能測試機器大小端。

理論分析:原因和&運算子不能測試一樣,因為C語言對運算子的級別是高於二進位制層次的。右移運算永遠是將低位元組移除,而和二進位制儲存時這個低位元組在高位還是低位無關的。

(3)強制型別轉換

同上

14.2、通訊系統中的大小端(陣列的大小端)

(1)譬如要通過串列埠傳送一個0x12345678給接收方,但是因為串列埠本身限制,只能以位元組為單位來發送,所以需要發4次;接收方分4次接收,內容分別是:0x12、0x34、0x56、0x78.接收方接收到這4個位元組之後需要去重組得到0x12345678(而不是得到0x78563412).

(2)所以在通訊雙方需要有一個默契,就是:先發/先接的是高位還是低位?這就是通訊中的大小端問題。

(3)一般來說是:先發低位元組叫小端;先發高位元組就叫大端。(我不能確定)實際操作中,在通訊協議裡面會去定義大小端,明確告訴你先發的是低位元組還是高位元組。

(4)在通訊協議中,大小端是非常重要的,大家使用別人定義的通訊協議還是自己要去定義通訊協議,一定都要注意標明通訊協議中大小端的問題。