1.程序中內(nèi)存從哪里來
1.1、程序執(zhí)行需要內(nèi)存支持
對程序來說却盘,內(nèi)存就是程序的立足之地(程序是被放在內(nèi)存中運行的)夸研;程序運行時需要內(nèi)存來存儲一些臨時變量邦蜜。
1.2、內(nèi)存管理最終是由操作系統(tǒng)完成的
(1)內(nèi)存本身在物理上是一個硬件期間亥至,由硬件系統(tǒng)提供悼沈;
(2)內(nèi)存是由操作系統(tǒng)統(tǒng)一管理的贱迟,為了內(nèi)存管理方便又合理,操作系統(tǒng)提供了多種機制來讓我們應用程序使用內(nèi)存絮供。這些機制彼此不同衣吠,各自有各自的特點,我們程序根據(jù)自己的實際情況來選擇某種方式獲取內(nèi)存(在操作系統(tǒng)處登記這塊內(nèi)存的臨時使用權(quán)限)壤靶,使用內(nèi)存缚俏、釋放內(nèi)存(向操作系統(tǒng)歸還這塊內(nèi)存的使用權(quán)限)。
1.3贮乳、三種內(nèi)存來源:棧(stack)忧换、堆(heap)、數(shù)據(jù)區(qū)(.data)
在一個C語言程序中向拆,能夠獲取的內(nèi)存就是三種情況:棧(stack)包雀、堆(heap)、數(shù)據(jù)區(qū)(.data)亲铡。
1.4、棧的詳解
(1)運行時自動分配&自動回收:棧是自動管理的葡兑,程序員不需要手工干預奖蔓、方便簡單。
(2)反復使用:棧內(nèi)存在程序中其實就是那一塊空間讹堤,程序反復使用這一塊空間吆鹤。
(3)臟內(nèi)存:棧內(nèi)存由于反復使用,每次使用后程序不會去清理洲守,因此分配到時保留原來的值疑务,所以說是隨機的。
(4)臨時性:函數(shù)不能返回棧變量的指針梗醇,因為這個空間是臨時的知允。
(5)棧會溢出:因為操作系統(tǒng)實現(xiàn)給定了棧的大小,如果在函數(shù)中無窮無盡的分配棧內(nèi)存總能用完叙谨。
1.5温鸽、堆內(nèi)存詳解
(1)操作系統(tǒng)堆管理器管理:堆管理是操作系統(tǒng)的一個模塊,堆管理分配靈活手负,按需分配涤垫。
(2)大塊內(nèi)存:堆內(nèi)存管理著總量很大的操作系統(tǒng)內(nèi)存塊,各進程可以按需申請使用竟终,使用完釋放蝠猬。
(3)程序手動申請&釋放:手動的意思是需要寫代碼去申請malloc賀師傅free。
(4)臟內(nèi)存:堆內(nèi)存也是反復使用的统捶,而且使用者用完釋放前不會清除榆芦,因此也是臟的柄粹。
(5)臨時性:堆內(nèi)存只在malloc和free之間屬于我這個進程,而可以訪問歧杏。在malloc之前和free之后都不能再訪問镰惦,否則會有錯誤。
1.6犬绒、堆內(nèi)存范例
int main(void)
{
// 需要一個1000個int類型元素的數(shù)組旺入,申請與綁定
int *p = (int *)malloc(1000*sizeof(int));
if (NULL == p)
{
printf("malloc error.\n");
return -1;
}
return 0;
}
(1)void *是個指針類型,malloc返回的是一個void *類型的指針,實質(zhì)上malloc返回的是堆管理器分配給我本次申請的那段內(nèi)存空間的首地址(malloc返回的值其實是個數(shù)字苦酱,這個數(shù)字表示一個內(nèi)存的地址)笼吟。為什么要用void *作為類型呢?
主要原因是malloc幫我們分配內(nèi)存時只是分配了內(nèi)存空間拗秘,至于這段內(nèi)存空間將來用來存儲什么類型的元素malloc是不關(guān)心的,由我們程序自己來決定祈惶。
(2)什么是void類型雕旨?早起被翻譯成空型,這個翻譯非常不好捧请,會誤導人凡涩。void類型不表示沒有類型,而表示任意類型疹蛉。void的意思就是說這個數(shù)據(jù)的類型當前是不確定的活箕,在需要的時候可以再去指定它的具體類型,如(int *)malloc(1000*sizeof(int));
可款,void *類型是一個指針類型育韩,這個指針本身占4字節(jié),但是指針指向的類型是不確定的闺鲸,換句話說筋讨,這個指針在需要的時候可以被強制轉(zhuǎn)化成其他任何一種確定類型的指針,也就是說這個指針可以指向任何類型的元素翠拣。
(3)malloc的返回值:成功申請空間后返回這個內(nèi)存空間的指針版仔,申請失敗后返回的是NULL。所以malloc獲取的內(nèi)存指針使用前一定要先檢查是否為NULL误墓。
(4)malloc申請的內(nèi)存用完后要釋放蛮粮,free(q);
會告訴堆管理器這段內(nèi)存我用完了你可以回收了。堆管理器回收了這段內(nèi)存后這段內(nèi)存當前進程就不應該再使用了谜慌。(雖然還能使用然想,但隨時被堆管理器分配給別的進程)因為釋放后堆管理器就可能把這段內(nèi)存再次分配給其他進程,所以你就不能再使用了欣范。
(5)在調(diào)用free歸還這段內(nèi)存之前变泄,指向這段內(nèi)存的指針p一定不能丟(也就是不能給p另外賦值)令哟,因為p一旦丟失這段malloc來的內(nèi)存就再也找不回了(內(nèi)存泄露),直到當前程序結(jié)束時操作系統(tǒng)才會回收這段內(nèi)存妨蛹。
1.7屏富、malloc的一些細節(jié)
(1)malloc(4)
gcc中的malloc默認最小是以16B為分配單位的,如果malloc小于16B的大小時都會返回一個16字節(jié)大小的內(nèi)存蛙卤。malloc實現(xiàn)時沒有實現(xiàn)任意自己的分配而是允許一些大小的塊內(nèi)存的分配狠半。
(2)malloc(20)
去訪問超限的第25、250颤难、2000會怎樣神年?
實驗后:120字節(jié)處正確,1200字節(jié)處正確行嗤,終于繼續(xù)往后訪問總有一個數(shù)字處開始發(fā)生段錯誤已日,超出了堆內(nèi)存區(qū)。
1.8栅屏、代碼段飘千、數(shù)據(jù)段、bss段
(1)編譯器在編譯程序的時候栈雳,將程序中的所有元素分成了一些組成部分占婉,各部分構(gòu)成一個段,所以說段是可執(zhí)行程序的組成部分甫恩。
(2)代碼段:代碼段就是程序中的可執(zhí)行部分,直觀理解代碼段就是函數(shù)堆疊組成的酌予。
(3)數(shù)據(jù)段(也稱為數(shù)據(jù)區(qū)磺箕、靜態(tài)數(shù)據(jù)區(qū)、靜態(tài)區(qū)):數(shù)據(jù)段就是程序中的數(shù)據(jù)抛虫,直觀理解就是C語言程序中的全局變量松靡。(注意:全局變量才算是程序的數(shù)據(jù),局部變量不算程序的數(shù)據(jù)建椰,只能算是函數(shù)的數(shù)據(jù))雕欺。
(4)bss段(又叫ZI,zero initial段):bss段的特點就是初始化為0棉姐,bss段本質(zhì)上也屬于數(shù)據(jù)段屠列,bss段就是初始化為0的數(shù)據(jù)段。
(5)注意區(qū)分:
數(shù)據(jù)段(.data)和bss段的區(qū)別和聯(lián)系:
二者本來沒有任何區(qū)別伞矩,都是用來存放C語言程序中的全局變量的笛洛。區(qū)別在于把顯式初始化為非零的全局變量存在.data段中,而把顯示初始化為0或者未顯示初始化的全局變量存在bss段乃坤。(C語言規(guī)定苛让,未顯式初始化的全局變量值默認為0)
1.9沟蔑、有些特殊數(shù)據(jù)會被放到代碼段
(1)C語言中使用const char *p = "Linux";
,定義字符串時狱杰,字符串"Linux"實際被分配在代碼段瘦材,也就是說這個"Linux"字符串實際上是一個常量字符串而不是變量字符串。
(2)const型常量:C語言中const關(guān)鍵字用來定義常量仿畸,常量就是不能被改變的量食棕。const的實現(xiàn)方法至少有2種:第一種,就是編譯將const修飾的變量放在代碼段去以實現(xiàn)不能修改(普遍見于各種單片機的編譯器)颁湖;第二種宣蠕,就是由編譯器來檢查以確保const型的常量不會被修改,實際上const型的常量還是和普通變量一樣放在數(shù)據(jù)段的(gcc中就是這樣實現(xiàn)的)甥捺。
1.10抢蚀、顯式初始化為非零的全局變量和靜態(tài)局部變量放在數(shù)據(jù)段
放在.data段的變量有兩種:
第一種,是顯式初始化為非零的全局變量镰禾;
第二種皿曲,是靜態(tài)局部變量,也就是static修飾的局部變量吴侦。
普通局部變量分配在棧上屋休,靜態(tài)局部變量分配在.data段上。
1.11备韧、未初始化或顯式初始化為0的全局變量放在bss段
bss段和.data段并沒有本質(zhì)區(qū)別劫樟,幾乎可以不用明確去區(qū)分這兩種。
1.12织堂、總結(jié):C語言中所有變量和常量所使用的內(nèi)存無非以上三種
(1)相同點:三種獲取內(nèi)存的方法叠艳,都可以給程序提供可用內(nèi)存,都可以來定義變量給程序用易阳。
(2)不同點:①棧內(nèi)存對應C中的普通局部變量(別的變量還用不了棧附较,而且棧是自動的,由編譯器和運行時環(huán)境共同提供服務潦俺,程序員五福手動控制)拒课;②堆內(nèi)存完全是獨立于我們的程序存在和管理的,程序需要內(nèi)存時可以去手動申請malloc事示,使用完成后必須盡快free釋放(堆內(nèi)存對程序就好像圖書館對人)早像。③數(shù)據(jù)段對應用程序來說對應C程序中的全局變量和靜態(tài)局部變量。
(3)①函數(shù)內(nèi)部臨時使用肖爵,除了函數(shù)不會用到扎酷,就定義局部變量;
②堆內(nèi)存和數(shù)據(jù)段幾乎擁有完全相同的屬性遏匆,大部分時候是可以完全替換的法挨,但是生命周期不一谁榜,堆內(nèi)存的生命周期是從malloc開始到free結(jié)束,而全局變量是從整個程序一開始執(zhí)行就開始凡纳,直到整個程序才會消滅窃植,伴隨程序運行的一生。
③啟示:如果你這個變量只是在程序的一個階段有用荐糜,用完就不用了巷怜,就適合用堆內(nèi)存;如果這個變量本身和程序 的一生相伴的暴氏,那就適合用全局變量延塑。(堆內(nèi)存就好像圖書館借書,數(shù)據(jù)段就好像自己書店買書答渔,堆內(nèi)存的使用比全局變量廣泛)关带。
2.C語言的字符串類型
2.1、C語言沒有原生字符串類型
(1)很多高級語言像JAVA沼撕、C#等就有字符串類型宋雏,有個String來表示字符串,用法和int這些很像务豺,可以String s1 = "Linux";
來定義字符串類型的變量磨总。
(2)C語言沒String類型,C語言中的字符串是通過字符指針來間接實現(xiàn)的笼沥。
2.2蚪燕、C語言使用指針來管理字符串
C語言中定義字符串的方法:char *p = "Linux";
,此時p就叫做字符串奔浅,但是實際上p只是一個字符指針(本質(zhì)上就是一個指針變量邻薯,只是p指向了一個字符串的起始地址而已)。
2.3乘凸、C語言中字符串的本質(zhì):指針指向頭、固定尾部的地址相連的一段內(nèi)存
(1)字符串就是一串字符累榜。字符反映在現(xiàn)實中就是文字营勤、符號、數(shù)字等壹罚,人用來表達的字符葛作,反映在編程中就是字符類型的變量。C語言中使用ASCII編碼對字符進行編程猖凛,編碼后可以用char型變量來表示一個字符赂蠢。字符串就是多個字符打包在一起共同組成的。
(2)字符串在內(nèi)存中其實就是多個字節(jié)連續(xù)分布構(gòu)成的(類似于數(shù)組辨泳、字符串和字符數(shù)組)虱岂。
(3)C語言中字符串有3個核心要素:第一是用一個指針指向字符串頭玖院;第二是固定尾部(字符串總是以'\0'來結(jié)尾的);第三是組成字符串的各字符彼此地址相連第岖。
(4)'\0'是一個ASCII字符难菌,其實就是編碼為0的那個字符(真正的0,和數(shù)字0是不同的蔑滓,數(shù)字0有它自己的ASCII編碼)郊酒。要注意區(qū)分'\0'、'0'和0(0就是'\0','0'就是48)键袱。
(5)'\0'作為一個特殊的數(shù)字被字符串定義為結(jié)束標志燎窘。產(chǎn)生的副作用就是:字符串中無法包含'\0'這個字符。(C語言中不可能包含存在一個'\0'字符的字符串)蹄咖,這個思路就叫“魔數(shù)”褐健。(魔數(shù)就是選出來的一個特殊的數(shù)字,這個數(shù)字表示一個特殊的含義比藻,你的正式內(nèi)容中不能包含這個魔數(shù))铝量。
2.4、注意:指向字符串的指針和字符串本身是分開的兩個東西
char *p = "Linux";
在這段代碼中银亲,p本身是一個字符指針慢叨,''Linux"分配在代碼段,占6個字節(jié)务蝠,實際上總共耗費了10個字節(jié)拍谐,這10個字節(jié)中:4字節(jié)的指針p叫做字符指針(用來指向字符串的,理解為字符串的引子馏段,但是它本身不是字符串)轩拨,5字節(jié)的用來存Linux這5個字符的內(nèi)存才是真正的字符串,最后一個用來存'\0'的內(nèi)存是字符串結(jié)尾標志院喜。
2.5亡蓉、存儲多個字符的2種方式:字符串和字符數(shù)組
我們有多個連續(xù)字符(典型就是Linux這個字符串)需要存儲,實際上有兩種存儲方式:第一種喷舀,字符串砍濒;第二種,字符數(shù)組硫麻。
3.字符串和字符數(shù)組的細節(jié)
3.1爸邢、字符數(shù)組初始化與sizeof、strlen
(1)sizeof是C語言的一個關(guān)鍵字拿愧,也是C語言的一個運算符(sizeof使用時是sizeof(類型或?qū)ο竺?杠河,所以很多人誤以為sizeof是函數(shù),其實不是),sizeof運算符用來返回一個類型或者是變量所占用的內(nèi)存字節(jié)數(shù)券敌。
為什么需要用sizeof唾戚?
主要原因是:①int、double等原生類型占幾個字節(jié)和平臺有關(guān)陪白;②C語言中除了ADT外還有UDT颈走,這些用戶自定義類型占幾個字節(jié)無法一眼看出,所以用sizeof運算符來讓編譯器幫忙計算咱士。
(2)strlen是一個C語言庫函數(shù)立由,這個庫函數(shù)的原型是:size_t strlen(const char *s)
,這個函數(shù)接收一個字符串的指針序厉,返回這個字符串的長度(以字節(jié)為單位)锐膜。注意一點是:strlen返回的字符串長度是不包含字符串結(jié)尾的'\0'的。
為什么需要strlen庫函數(shù)弛房?
因為從字符串的定義(指針指向頭道盏、固定結(jié)尾、中間依次相連)可以看出無法直接得到字符串的長度文捶,需要用strlen函數(shù)來計算得到字符串的長度荷逞。
(3)sizeof(數(shù)組名)得到的永遠是數(shù)組的元素個數(shù)的內(nèi)存大小(也就是數(shù)組的大写馀拧)种远,和數(shù)組中有無初始化,初始化多或少等都是沒有關(guān)系的顽耳;strlen是用來計算字符串的長度的坠敷,只能傳遞合法的字符串進去才有意義,如果隨便傳遞一個字符指針射富,但是這個字符指針并不是指向字符串的膝迎,這樣是沒有意義的。
(4)當我們定義數(shù)組時如果沒有明確給出數(shù)組的大小胰耗,則必須同時給出初始化限次,編譯器會根據(jù)初始化去自動計算數(shù)組的大小(數(shù)組定義時要么給出大小柴灯,要么直接給全部元素卖漫,要么給初始化式)。
3.2弛槐、字符串初始化與sizeof、strlen
(1)char * p = "Linux"; sizeof(p);
得到的永遠是4依啰,因為這時候測的是字符指針p本身的長度乎串,和字符串的長度是無關(guān)的。
(2)strlen是轉(zhuǎn)么解決計算字符串的長度的問題的,解決了上一步的問題叹誉。
3.3字符數(shù)組與字符串的本質(zhì)差別(內(nèi)存分配角度)
(1)字符數(shù)組char a[] = "Linux";
來說鸯两,定義了一個數(shù)組a,數(shù)組a占6字節(jié)长豁,右值"Linux"本身存儲在.data數(shù)據(jù)段钧唐,"Linux"只是代表一個地址,編譯器將這個地址從.data數(shù)據(jù)段取出"Linux"字符串匠襟,然后初始化字符數(shù)組a后丟掉钝侠,這句就相當于char a[] = {'L', 'i', 'n', 'u', 'x', '\0' };
(2)字符串char *p = "Linux";
定義了一個字符指針p,p占4字節(jié)酸舍,分配在棧上帅韧,同時還定義了一個字符串"Linux",分配在代碼段啃勉,然后把代碼中的字符串(一共占6字節(jié))的首地址(也就是'L'的地址)賦值給p忽舟。
總結(jié)對比:字符數(shù)組和字符串有本質(zhì)區(qū)別
①字符數(shù)組本身是數(shù)組,數(shù)組自身帶內(nèi)存空間淮阐,可以用來存東西(所以數(shù)組類似于容器)叮阅;
②字符串本身是指針,本身永遠只占4字節(jié)泣特,而且這4字節(jié)還不能用來存有效數(shù)據(jù)浩姥,所以只能把有效數(shù)據(jù)存到別的地方,然后把地址存在p中群扶。
③字符數(shù)組自己存那些字符及刻;字符串一定需要額外的內(nèi)存來存儲那些字符,字符串本身只存真正的那些字符所在的內(nèi)存空間的首地址竞阐。
char b[5];
int main(void)
{
// 字符串存在棧上
char a[7];
char *p = a;
// 字符串存在數(shù)據(jù)段
char *p = b;
// 字符串存在堆空間
char *p = (char *)malloc(5);
}
4.C語言結(jié)構(gòu)體概述
4.1缴饭、結(jié)構(gòu)體類型也是一種自定義類型
(1)C語言中的2種類型:原生類型和自定義類型;
(2)結(jié)構(gòu)體使用時先定義結(jié)構(gòu)體再用類型定義變量骆莹;
(3)結(jié)構(gòu)體可以認為是數(shù)組發(fā)展而來的颗搂。其實數(shù)組和結(jié)構(gòu)體都算是數(shù)據(jù)結(jié)構(gòu)的范疇了,數(shù)組就是最簡單的數(shù)據(jù)結(jié)構(gòu)幕垦、結(jié)構(gòu)體比數(shù)組更復雜一些丢氢,然后是鏈表、哈希表先改、二叉樹疚察、圖等。
4.2仇奶、結(jié)構(gòu)體變量中的元素如何訪問貌嫡?
(1)數(shù)組中元素的訪問方式:表面上2種方式(數(shù)組下標方式和指針方式);實質(zhì)上都是指針方式訪問。
(2)結(jié)構(gòu)體變量中的元素訪問方式:只有一種岛抄,用.或者->方式來訪問别惦。用結(jié)構(gòu)體變量來訪問元素用點.,用結(jié)構(gòu)體變量的指針方式來訪問元素用->夫椭。(高級語言中都用點.)
5.結(jié)構(gòu)體的對齊訪問
5.1掸掸、舉例說明什么是結(jié)構(gòu)體對齊訪問
(1)結(jié)構(gòu)體中元素的訪問其實本質(zhì)上還是用指針方式,結(jié)合這個元素再整個結(jié)構(gòu)體中的偏移量和這個元素的類型來進行訪問蹭秋。
(2)一般來說扰付,我用點.的方式來發(fā)訪問結(jié)構(gòu)體元素時,我們是不用考慮結(jié)構(gòu)體的元素對齊的感凤。因為編譯器會幫我們處理這個細節(jié)悯周。但是因為C語言本身是很底層的語言,而且做嵌入式開發(fā)經(jīng)常要從內(nèi)存的角度陪竿,以指針方式來處理結(jié)構(gòu)體及其中的元素禽翼,因此還是需要掌握結(jié)構(gòu)體對齊的規(guī)則。
5.2族跛、結(jié)構(gòu)體為何要對齊訪問
(1)結(jié)構(gòu)體中元素對齊訪問主要原因是為了配合硬件闰挡,對齊排布和訪問會提高效率,否則會大大降低效率礁哄。(空間換時間的例子無處不在)
(2)內(nèi)存本身就是一個物理器件(DDR內(nèi)存芯片长酗,Soc上的DDR控制器,本身有一定的局限性)桐绒;如果內(nèi)存每次訪問時按照4字節(jié)對齊訪問夺脾,那么效率是最高的;如果不對齊訪問效率要低的很多茉继。
(3)還有很多別的因素和原因咧叭,導致我們需要對齊訪問。比如cache的一些緩存特性烁竭,還有其他硬件(比如MMU菲茬、LCD顯示器)的一些內(nèi)存依賴特性,所以會要求內(nèi)存對齊訪問派撕。
5.3婉弹、結(jié)構(gòu)退對齊的規(guī)則和運算
(1)編譯器本身可以設(shè)置內(nèi)存對齊的規(guī)則,需要記字蘸稹:32位編譯器镀赌,一般編譯器默認對齊方式是4字節(jié)。
(2)總結(jié):結(jié)構(gòu)體對齊的分析要點和關(guān)鍵
①結(jié)構(gòu)體對齊要考慮:結(jié)構(gòu)體整體本身必須安置在4字節(jié)處际跪,結(jié)構(gòu)體對齊后的大小必須是4的倍數(shù)(編譯器設(shè)置為4字節(jié)對齊時商佛,如果編譯器設(shè)置為8字節(jié)對齊蛙粘,則這里的4就變成8)。
②結(jié)構(gòu)體中每個元素本身都必須對其存放威彰,而每個元素都有自己的對齊規(guī)則。
③編譯器考慮結(jié)構(gòu)體存放時穴肘,滿足以上2點要求的最少內(nèi)存需要的排布來算歇盼。
5.4、gcc支持但不推薦的對齊指令:#pragma pack()
(1)#pragma是用來指揮編譯器评抚,或者說設(shè)置編譯器的對齊方式的豹缀。編譯器的默認對齊方式是4,但是有時候我們并不希望對齊方式是4慨代,而希望是別的(比如希望1字節(jié)對齊邢笙,而可能是8,甚至可能是128字節(jié)對齊)侍匙。
(2)常用的設(shè)置編譯器對齊命令有2中:
①第一種氮惯,#pragma pack(),這種就是設(shè)置編譯器1字節(jié)對齊(有些人喜歡講:設(shè)置編譯器不對齊訪問想暗,還有些講:取消編譯器對齊訪問)妇汗;
②第二種,#pragma pack(4)说莫,這個括號的數(shù)組就表示我們希望多少字節(jié)對齊杨箭。
#pragma pack(128)
// 結(jié)構(gòu)體定義
#pragma pack()
(3)我們需要#pragma pack(n)開頭,以#pragma pack()結(jié)尾储狭,定義一個區(qū)間互婿,這個區(qū)間的對齊參數(shù)就是n。
(4)#pragma pack的方式在很多C語言環(huán)境下都支持辽狈,但gcc雖然也可以但不建議使用慈参。
5.5、gcc推薦的對齊指令attribute((packed))稻艰、attribute((aligned(n)))
(1)attribute((packed))使用時直接放在要進行內(nèi)存對齊的類型定義的后面懂牧,然后它起作用的范圍只有加了這個東西的這個類型。packed的作用 就是取消對齊訪問尊勿。
struct mystruct11
{
int a;
char b;
short c;
}__attribute__((packed));
(2)attribute((aligned(n)))使用時直接放在要進行內(nèi)存對齊的類型定義的后面僧凤,然后它起作用是讓整個結(jié)構(gòu)體變量整體進行n字節(jié) 對齊(注意是結(jié)構(gòu)體變量整體n字節(jié)對齊,而不是結(jié)構(gòu)體內(nèi)各元素要n字節(jié)對齊)元扔。
typedef struct mystruct11
{
int a;
char b;
short c;
}__attribute__((aligned(1024))) My11;
6.offsetof宏與container_of宏
6.1躯保、由結(jié)構(gòu)體指針進而訪問個元素的原理
通過結(jié)構(gòu)體整體變量來訪問其中各個元素,本質(zhì)上是通過指針方式來訪問的澎语,形式上市通過點.的方式訪問的途事。
6.2验懊、offsetof宏(本人還未完全理解)
(1)offsetof宏的作用:用宏來計算結(jié)構(gòu)體中某個元素和結(jié)構(gòu)體首地址的偏移量(其實質(zhì)是通過編譯器來幫我們計算的)。
(2)offsetof宏的原理:我們虛擬一個type類型結(jié)構(gòu)變量尸变,然后用type.member的方式來訪問哪個member元素义图,繼而得到member相對于整個變量首地址的便宜了了。
(3)學習思路:第一步先學會用offsetof宏召烂,第二步再去理解這個宏的原理碱工。
struct mystruct
{
char a;
int b;
short c;
};
#define offsetof(TYPE.MEMBER)((size_t)&((TYPE *)0)->MEMBER)
int main(void)
{
int offsetof a = offsetof(struct mystruct, a);
printf("offsetof a = %d.\n", offsetof a);;
}
(TYPE *)
這是一個強制類型轉(zhuǎn)換,把0地址強制類型轉(zhuǎn)換成一個指針奏夫,這個指針指向一個TYPE類型的結(jié)構(gòu)體變量怕篷。(實際上這個結(jié)構(gòu)體變量可能不存在,只要不去解引用就不會有錯)
((TYPE *)0)->MEMBER
(TYPE *)0
是一個TYPE類型結(jié)構(gòu)體變量的指針酗昼,通過指針來訪問這個結(jié)構(gòu)體變量MEMBER元素
&((TYPE *)0)->MEMBER
意義就是得到MEMBER元素的地址廊谓。但是因為整個結(jié)構(gòu)體變量的首地址是0。
6.3麻削、container_of宏
#define container_of(ptr, type, member) ({const typeof(((type *)0)->member) * __mptr = (ptr); (type *)((char *) __mptr __offsetof(type, member));})
ptr是指向結(jié)構(gòu)體元素member的指針
type是結(jié)構(gòu)體類型
member是結(jié)構(gòu)體中一個元素的元素名
這個宏返回的就是指向整個結(jié)構(gòu)體變量的指針蒸痹,類型是(type *)。
short *p = &(s1.c);
struct mystruct *pS = NULL;
pS = container_of(p, struct mystruct, c)
//通過p來計算得到s1的指針
(1)作用:知道一個結(jié)構(gòu)體中某個元素的指針呛哟,反推這個結(jié)構(gòu)體變量的指針电抚。有了container_of宏,我們可以從一個元素的指針得到整個結(jié)構(gòu)體變量的指針竖共,繼而得到結(jié)構(gòu)體中其他元素的指針蝙叛。
(2)typeof關(guān)鍵字的作用是:typeof(a)時由變量a得到變量a的類型,typeof就是由變量名得到變量數(shù)據(jù)類型的公给。
(3)這個宏的工作原理:先用typeof得到member元素的類型借帘,然后定義一個指針,然后用這個指針減去該元素相對于整個結(jié)構(gòu)體變量的偏移量(偏移量用offsetof宏得到)淌铐,減去之后得到的就是整個結(jié)構(gòu)體變量的首地址肺然,再把這個地址強制類型轉(zhuǎn)換成type *類型即可。
6.4腿准、學習指南和要求
(1)最基本的要求:必須要會這兩個宏的使用际起。就是說能知道這兩個宏接收什么參數(shù),返回什么參數(shù)吐葱,會用這兩個宏寫代碼街望。看見別人代碼用這兩個宏能夠理解弟跑。
(2)進一步要求:能理解這兩個宏的工作原理灾前,能表述出來(面試可能會考到)。
7.共用體union
7.1孟辑、共用體類型的定義哎甲、變量定義和使用
(1)共用體union和結(jié)構(gòu)體struct在類型定義蔫敲、變量定義、使用方法上有些相似炭玫。
(2)共用體和結(jié)構(gòu)體的不同:結(jié)構(gòu)體類似于一個包裹奈嘿,結(jié)構(gòu)體中的成員彼此是獨立存在的額,分布在內(nèi)存的不同單元中吞加,他們只是被打包成一個整體叫做結(jié)構(gòu)體而已指么;共用體中的各成員其實是一體的,彼此不獨立榴鼎,他們使用同一個內(nèi)存單元。同一個內(nèi)存空間有多種解釋方式晚唇。
(3)共用體union就是對同一塊內(nèi)存中存儲的二進制的不同的理解方式巫财。
(4)union的sizeof測到的大小實際上是union中各個元素里面占用內(nèi)存最大的那個元素的大小。因為可以存的下這個就一定能夠存的下其他的元素哩陕。
(5)union中的元素不存在內(nèi)存對齊的問題平项。
7.2、共用體和結(jié)構(gòu)體相同點和不同點
(1)相同點就是操作語法幾乎相同悍及。
(2)不同點是本質(zhì)上的不同闽瓢,struct是多個獨立元素(內(nèi)存空間)打包在一起,union是一個元素(內(nèi)存空間)的多種不同解析方式心赶。
7.3扣讼、共用體的主要用途
(1)共用體就用在那種對同一個內(nèi)存單元進行多種不同規(guī)則解析的這種情況下。
(2)C語言中其實是可以沒有共用體的缨叫,用指針和強制類型轉(zhuǎn)換可以替代共用體完成同樣的功能椭符,但是共用體的方式更好理解。
8.大小端模式
8.1耻姥、什么是大小端模式
(1)大小端模式(big endian)和小端模式(little endian)最早是小說中出現(xiàn)的詞销钝,和計算機本來沒有關(guān)系的。
(2)后來計算機通信發(fā)展起來后琐簇,遇到一個問題就是:在串口等串行通信中蒸健,一次只能發(fā)送一個字節(jié)。這時候我要發(fā)送一個int類型的數(shù)就遇到一個問題婉商。int類型有4個字節(jié)似忧,我是按照:byte0 byte1 byte2 byte3 這樣的順序發(fā)送還是反序發(fā)送。規(guī)則就是發(fā)送方和接收方必須按照同樣的規(guī)則來通信丈秩,否則就會出現(xiàn)錯誤橡娄。這就叫通信系統(tǒng)的大小端模式。
(3)現(xiàn)在講大小端癣籽,更多的是指計算機存儲系統(tǒng)的大小端挽唉。在計算機內(nèi)存/硬盤/Nnad中滤祖。因為存儲系統(tǒng)是32位的足丢,但是數(shù)據(jù)仍然是按照字節(jié)為單位的炕置。于是乎一個32位的二進制在內(nèi)存中存儲時有2種分布方式:
①大端模式:高字節(jié)對應高字節(jié)既荚;
②小端模式:高字節(jié)對應低字節(jié)梨水。
(4)大端模式和小端模式本身沒有對錯钥庇,沒有優(yōu)劣淳附,理論上按照大端或小端都可以挨稿,但是要求必須存儲時和讀取時按照同樣的大小端模式來進行肴楷,否則會出錯严拒。
(5)現(xiàn)實的情況是:有些CPU公司用大端(比如單片機)扬绪;有些CPU用小端(比如ARM)。(大部分是用小端模式裤唠,大端模式的不算多)挤牛。于是乎我們寫代碼時,當不知道當前環(huán)境是用大端模式還是小端模式時就需要用代碼來檢測當前系統(tǒng)的大小端种蘸。
8.2墓赴、共用體union測試大小端
經(jīng)典筆試題:寫一段代碼測試大小端
#include <stdio.h>
union myunion
{
int a;
char b;
};
// 如果是小端模式則返回1,大端模式則返回0
int is_little_endian(void)
{
union myunion u1;
u1.a = 1;
return u1.b;
}
int main(void)
{
int i = is_little_endian();
if (i == 1)
{
printf("小端模式.\n");
}
else
{
printf("大端模式.\n");
}
return 0;
}
共用體測試大小端的思路:
一開始用int類型的a來存儲1(00000001)航瞭,然后用char類型的b來解析這個1(00000001)诫硕,因為訪問是從低地址開始訪問的,所以要么訪問到00刊侯,要么訪問到01章办。01對于00000001來說屬于低字節(jié),若這個01保存在低地址的話滨彻,就是小端模式纲菌,若這個01保存在高地址的話,就是大端模式疮绷。
8.3翰舌、指針方式測試大小端(本質(zhì)方式)
#include <stdio.h>
union myunion
{
int a;
char b;
};
int is_little_endian(void)
{
int a = 1;
char b = *((char *)(&a));
// 將a的地址強制類型轉(zhuǎn)換為char解析方式,然后去解引用得到首地址的內(nèi)容
return b;
}
int main(void)
{
int i = is_little_endian();
if (i == 1)
{
printf("小端模式.\n");
}
else
{
printf("大端模式.\n");
}
return 0;
}
解析同上一節(jié)
8.4冬骚、看似可行失責不可行的測試大小端方式:位與椅贱、移位、強制類型轉(zhuǎn)換
(1)位與運算
#include <stdio.h>
int main(void)
{
// 位與
int a = 1;
int b = a & 0xff;
printf("b = %d.\n",b);
return 0;
}
實驗結(jié)論:位與的方式無法測試機器的大小端模式只冻。表現(xiàn)就是大端機器和小端機器的位與運算后的值是相同的庇麦,都是1。
結(jié)果分析:位與運算是編譯器提供的運算喜德,這個運算是高于內(nèi)存層次的(或者說&運算在二進制層次具有可移植性山橄,也就是說&的時候一定是高字節(jié)&高字節(jié),低字節(jié)&低字節(jié)舍悯,和二進制存儲無關(guān))航棱,簡單的說睡雇,位與運算(&)在C語言編譯器已經(jīng)做了優(yōu)化處理。
理想模型分析:實際不可行
①大端模式:低字節(jié)放到了高地址饮醇,位與操作結(jié)果為0x10000000
②小端模式:低字節(jié)放到低地址它抱,位與操作結(jié)果為0x00000001
(2)移位運算
#include <stdio.h>
int main(void)
{
// 移位
int a, b;
a = 1;
b = a >> 1;
printf("b = %d.\n",b);
return 0;
}
實驗結(jié)論:移位的方式也不能測試機器的大小端模式。表現(xiàn)就是大端機器和小端機器的位與運算后的值是相同的朴艰,都是0观蓄。
結(jié)果分析:
原因和位與運算&不能測試是一樣的。因為C語言對運算符的級別是高于二進制層次的祠墅。右移運算永遠是低字節(jié)移除侮穿,而和二進制存儲時這個低字節(jié)在高位還是低位無關(guān)。
理想模型分析:實際不可行
①大端模式:低字節(jié)放到低地址毁嗦,移位操作結(jié)果為0x800000
②小端模式:低字節(jié)放到低地址亲茅,移位操作結(jié)果為0
(3)強制類型轉(zhuǎn)換
#include <stdio.h>
int main(void)
{
// 強制類型轉(zhuǎn)換
int a;
char b;
a = 1;
b = (char)a;
printf("b = %d.\n",b);
return 0;
}
實驗結(jié)論:強制類型轉(zhuǎn)換的方式也不能測試機器的大小端模式。表現(xiàn)就是大端機器和小端機器的位與運算后的值是相同的金矛,都是1。
結(jié)果分析:強制類型轉(zhuǎn)換不可以的原因同上勺届,也是編譯器進行了優(yōu)化處理驶俊。
理想模型分析:實際不可行
①大端模式:低字節(jié)放到高地址,強制類型轉(zhuǎn)換操作結(jié)果為0
②小端模式:低字節(jié)放到低地址免姿,強制類型轉(zhuǎn)換操作結(jié)果為1
8.5饼酿、通信系統(tǒng)的大小端(數(shù)組的大小端)
(1)比如要通過串口發(fā)送一個0x12345678給接收方,但是因為串口本身限制胚膊,只能以字節(jié)單位來發(fā)送故俐,所以需要發(fā)4次;接收方分4次接收紊婉,內(nèi)容分別是:0x12药版、0x34、0x56喻犁、0x78槽片。接收方接收到這4個字節(jié)之后需要去重組得到0x12345678(而不是0x78563412)。
(2)所以通信雙方需要有一個默契肢础,就是:先發(fā)/接收的是高位還是低位还栓?這就是通信中的大小端問題。
(3)一般來說:先發(fā)低字節(jié)叫做小端传轰;先發(fā)高字節(jié)叫做大端剩盒。實際操作中,在通信協(xié)議里面回去定義大小端慨蛙,明確告訴你先發(fā)的是低字節(jié)還是高字節(jié)辽聊。
(4)在通信協(xié)議中纪挎,大小端是非常重要的,大家使用別人的定義的通信協(xié)議還是自己要去定義通信協(xié)議身隐,一定都要注意標明通信協(xié)議中的大小端問題廷区。
9.枚舉
9.1枚舉是用來做什么的?
(1)枚舉在C語言中其實是一些符號常量集贾铝。枚舉定義了一些符號隙轻,這些符號的本質(zhì)就是int類型的常量,每個符號和一個常量綁定垢揩。這個符號就表示一個定義的一個識別碼玖绿,編譯器對枚舉的認識就是符號常量所綁定的那個int類型的數(shù)字。
(2)枚舉符號常量和其對應的常量數(shù)字相對來說叁巨,數(shù)字不重要斑匪,符號才重要。一般情況下我們都不明確指定這個符號所對應的數(shù)字锋勺,而讓編譯器自動分配蚀瘸。自動分配原則是:從0開始依次增加,如果用戶自定義了一個值庶橱,則從那個值開始往后依次增加1贮勃。
9.2、C語言為何需要枚舉
(1)C語言沒有枚舉是可以的苏章。使用枚舉其實就是就是對1寂嘉、0這些數(shù)字進行符號化編碼,這樣的好處就是編程時可以不用看數(shù)字而直接看符號枫绅。符號的意義很明顯泉孩,一眼可以看出。而數(shù)字所代表的的含義除非看文檔或注釋并淋。
(2)宏定義的目的和意義:不用數(shù)字而用符號寓搬,意義更加明顯。
9.3县耽、宏定義和枚舉的區(qū)別
(1)枚舉:是將多個有關(guān)聯(lián)的符號封裝在一個枚舉中订咸,而宏定義完全是散的。也就是說枚舉其實是多選一酬诀。
(2)什么情況下用枚舉脏嚷?當我們要定義的常量是一個有限集合時(比如一星期7天,性別男女)瞒御。