前言
基礎(chǔ)篇介紹了一些關(guān)于C語(yǔ)言?xún)?nèi)存管理的常見(jiàn)概念授嘀,包括內(nèi)存編址气忠、堆棧锰蓬、內(nèi)存操作函數(shù)幔睬、變量和數(shù)組存儲(chǔ)簡(jiǎn)介等等。本文將在前文的基礎(chǔ)上擴(kuò)展以下知識(shí):結(jié)構(gòu)體變量的存儲(chǔ)芹扭、函數(shù)調(diào)用與內(nèi)存分配麻顶、遞歸函數(shù)的調(diào)用過(guò)程。如果有需要瀏覽上一篇文章的同學(xué)請(qǐng)點(diǎn)擊C語(yǔ)言-內(nèi)存管理基礎(chǔ)舱卡。希望本文能給正在學(xué)習(xí)C辅肾、Objective-C、C++等語(yǔ)言的小伙伴們更多啟發(fā)轮锥。
結(jié)構(gòu)體變量的存儲(chǔ)
結(jié)構(gòu)體在C語(yǔ)言中是一種常見(jiàn)的數(shù)據(jù)結(jié)構(gòu)矫钓,開(kāi)發(fā)中系統(tǒng)提供的基本類(lèi)型往往不能滿(mǎn)足需要,所以會(huì)常常使用到“結(jié)構(gòu)體”舍杜。作為由基本數(shù)據(jù)類(lèi)型組合而成的結(jié)構(gòu)體與普通變量存儲(chǔ)不一樣新娜,結(jié)構(gòu)體變量占用空間不是簡(jiǎn)單的結(jié)構(gòu)體成員空間單純相加,其在內(nèi)存中分配空間和數(shù)組很類(lèi)似既绩,可以總結(jié)為以下幾點(diǎn):
- 結(jié)構(gòu)體變量占用的內(nèi)存空間是其成員占用最大內(nèi)存空間的整數(shù)倍
struct Person {
int age;
int hegiht;
int weight;
};
int main(int argc, const char * argv[]) {
struct Person p1 = {1, 2, 3};
int size = sizeof(p1);
printf("%i\n",size); //12 = 4 * 3
printf("變量的地址:%p\n",&p1); //0x7fff5fbff750
printf("%p\n",&p1.age); //0x7fff5fbff750
printf("%p\n",&p1.hegiht); //0x7fff5fbff754
printf("%p\n",&p1.weight); //0x7fff5fbff758
return 0;
}
說(shuō)明:在
p1
變量中概龄,由于其成員都是int
類(lèi)型,在64位編譯器中占用4個(gè)字節(jié)空間饲握,所以系統(tǒng)為p1
變量分配的內(nèi)存空間大小應(yīng)該是:4 X 3 = 12個(gè)字節(jié)私杜。另外:結(jié)構(gòu)體成員內(nèi)存地址分配是從低地址到高地址的,結(jié)構(gòu)體變量的地址是其首成員的地址互拾,這點(diǎn)和數(shù)組是一致的歪今。
- 系統(tǒng)從結(jié)構(gòu)體首個(gè)成員分配空間;如果空間不夠則重新分配颜矿,如果空間剩余則會(huì)把下一個(gè)成員的數(shù)據(jù)存儲(chǔ)到剩余的空間中
struct Student {
char id;
double score;
int age;
};
int main(int argc, const char * argv[]) {
struct Student s1 = {'A', 10.0, 15};
int size = sizeof(s1);
printf("%i\n",size); //24
printf("%p\n",&s1.id); //0x7fff5fbff788
printf("%p\n",&s1.score); //0x7fff5fbff790
printf("%p\n",&s1.age); //0x7fff5fbff798
return 0;
}
說(shuō)明:按照前面的說(shuō)明寄猩,系統(tǒng)為
s1
分配內(nèi)存時(shí)以sizeof(double)
8個(gè)字節(jié)為單位,所以為s1.id
分配了8個(gè)字節(jié)的空間骑疆,但是由于id
定義為char
類(lèi)型田篇,所以只占了8個(gè)字節(jié)中數(shù)值最小的一個(gè)內(nèi)存空間;由于前面剩下的 8 - 1 = 7個(gè)字節(jié)不足以存放double
類(lèi)型的值箍铭,所以接著為s1.score
分配8個(gè)字節(jié)并占滿(mǎn)8個(gè)字節(jié)空間泊柬;最后為s1.age
分配8個(gè)字節(jié)并占用了前4個(gè)字節(jié)空間。故s1
變量在內(nèi)存中占用的內(nèi)存大小為 8 X 3 = 24個(gè)字節(jié)诈火。如果調(diào)換結(jié)構(gòu)體
Student
成員之間的順序如下兽赁,情況又會(huì)發(fā)生變化。
struct Student {
double score;
char id;
int age;
};
int main(int argc, const char * argv[]) {
struct Student s1 = {'A', 10.0, 15};
int size = sizeof(s1);
printf("%i\n",size); //16
printf("%p\n",&s1.score) //0x7fff5fbff790
printf("%p\n",&s1.id); //0x7fff5fbff790
printf("%p\n",&s1.age); //0x7fff5fbff79c
return 0;
}
說(shuō)明:這是由于第二次為
s1.id
分配內(nèi)存時(shí)沒(méi)有完全占滿(mǎn)8個(gè)字節(jié)的空間,而且第三次為s1.age
分配時(shí)其需要的4個(gè)字節(jié)空間也沒(méi)有超出剩余的8-1 = 7個(gè)字節(jié)空間刀崖,所以s1.age
的值按照內(nèi)存對(duì)齊的原則就存放在了第二次分配的8個(gè)字節(jié)的后4位空間中惊科。
-
為結(jié)構(gòu)體變量分配內(nèi)存還需要遵循內(nèi)存對(duì)齊原則
內(nèi)存對(duì)齊:系統(tǒng)在為不同類(lèi)型的變量分配空間時(shí)按照一定的規(guī)則在空間上排列,而不是順序的一個(gè)接一個(gè)的排放亮钦,這是對(duì)內(nèi)存對(duì)齊的模糊解釋馆截。簡(jiǎn)單來(lái)說(shuō)每個(gè)特定平臺(tái)上的編譯器都有自己的默認(rèn)“對(duì)齊系數(shù)”(也叫對(duì)齊模數(shù) n=1,2,4,8,16)。不同的對(duì)齊系數(shù)會(huì)對(duì)結(jié)構(gòu)體變量分配的內(nèi)存空間產(chǎn)生影響蜂莉。關(guān)于“內(nèi)存對(duì)齊和結(jié)構(gòu)體變量“的更多內(nèi)容這里引用幾篇相關(guān)文章幫助大家深入理解:
【知乎】-如何理解 struct 的內(nèi)存對(duì)齊蜡娶?
【CSDN】-C內(nèi)存對(duì)齊
【CSDN】-C/C++內(nèi)存對(duì)齊原則及作用
【個(gè)人博客】-內(nèi)存對(duì)齊
總結(jié):結(jié)構(gòu)體變量所占存儲(chǔ)空間受其不同類(lèi)型的成員排列順序及編譯器內(nèi)存對(duì)齊影響,開(kāi)發(fā)中盡量將相同類(lèi)型的成員依次定義映穗,有助于節(jié)省內(nèi)存空間窖张。
函數(shù)調(diào)用與內(nèi)存分配
函數(shù)作為編程中實(shí)現(xiàn)功能的重要手段,深入理解函數(shù)的調(diào)用過(guò)程對(duì)提升開(kāi)發(fā)能力有很大的幫助蚁滋,首先了解一下兩個(gè)任意函數(shù)之間進(jìn)行調(diào)用的情形荤堪,與匯編程序設(shè)計(jì)中主程序和子程序之間的鏈接及信息交換類(lèi)似,在高級(jí)語(yǔ)言編寫(xiě)的程序中(比如C語(yǔ)言)枢赔,調(diào)用函數(shù)與被調(diào)用函數(shù)之間的鏈接及信息交換需要通過(guò)棧來(lái)進(jìn)行∮抵《C程序設(shè)計(jì)》一書(shū)中對(duì)于函數(shù)之間調(diào)用提出兩個(gè)注意點(diǎn):
- 函數(shù)運(yùn)行期間調(diào)用另外一個(gè)函數(shù)踏拜,在運(yùn)行被調(diào)用函數(shù)之前,系統(tǒng)需要先完成3件事情:
- 將所有的實(shí)參低剔、返回地址等信息傳遞給被調(diào)用函數(shù)保存
- 為被調(diào)用函數(shù)局部變量在棧上分配內(nèi)存
- 將控制轉(zhuǎn)移到被調(diào)用函數(shù)入口
-
被調(diào)用函數(shù)返回調(diào)用函數(shù)之前速梗,系統(tǒng)也需要相應(yīng)的完成3件事情:
- 在棧中保存被調(diào)用函數(shù)的計(jì)算結(jié)果(返回值)
- 釋放在棧中為被調(diào)用函數(shù)分配的數(shù)據(jù)區(qū)
- 依照被調(diào)用函數(shù)保存的返回地址將控制轉(zhuǎn)移到調(diào)用函數(shù)
當(dāng)有多個(gè)函數(shù)嵌套調(diào)用時(shí),按照 “后調(diào)用先返回” 的原則依次進(jìn)行襟齿,看到這里想必大家一目了然姻锁,函數(shù)之間的調(diào)用規(guī)則和 “棧” 管理數(shù)據(jù)的方式完全相同猜欺,因此函數(shù)之間的信息傳遞和控制轉(zhuǎn)移必須通過(guò) “椢涣ィ”來(lái)實(shí)現(xiàn)。
《數(shù)據(jù)結(jié)構(gòu)(C語(yǔ)言版)》一書(shū)中對(duì)函數(shù)的調(diào)用過(guò)程在內(nèi)存中的描述是這樣的:
函數(shù)之間信息傳遞和控制轉(zhuǎn)移必須通過(guò)“椏螅”來(lái)實(shí)現(xiàn)涧黄,即系統(tǒng)將整個(gè)程序運(yùn)行所需的數(shù)據(jù)空間安排在一個(gè)棧中,每當(dāng)調(diào)用一個(gè)函數(shù)就為它在棧頂分配一個(gè)存儲(chǔ)區(qū)赋荆,每當(dāng)從一個(gè)函數(shù)退出時(shí)笋妥,就釋放它的存儲(chǔ)區(qū),當(dāng)前正在運(yùn)行的函數(shù)存儲(chǔ)區(qū)必在棧頂窄潭。
-
幀棧
幀棧也叫過(guò)程活動(dòng)記錄春宣,是編譯器用來(lái)實(shí)現(xiàn)過(guò)程/函數(shù)調(diào)用的一種數(shù)據(jù)結(jié)構(gòu)。每個(gè)棧幀對(duì)應(yīng)著一個(gè)未運(yùn)行完的函數(shù)。棧幀中保存了該函數(shù)形參月帝、返回地址躏惋、局部變量等信息。簡(jiǎn)而言之棧幀就是一個(gè)函數(shù)執(zhí)行的環(huán)境嫁赏。有時(shí)候函數(shù)嵌套調(diào)用其掂,棧中會(huì)有多個(gè)函數(shù)的信息,每個(gè)函數(shù)占用一個(gè)連續(xù)的區(qū)域潦蝇。引文中提到的“存儲(chǔ)區(qū)”就是指一個(gè)函數(shù)對(duì)應(yīng)的分配空間也就是一個(gè)函數(shù)幀款熬。 -
參數(shù)的傳遞和內(nèi)存分配
被調(diào)用函數(shù)的形參,在未出現(xiàn)函數(shù)調(diào)用時(shí)不占用內(nèi)存空間攘乒,發(fā)生函數(shù)調(diào)用時(shí)贤牛,形參按照確定的類(lèi)型在該函數(shù)幀中被分配指定大小的空間。并且由調(diào)用函數(shù)的實(shí)參傳遞給被調(diào)用函數(shù)的形參保存则酝。 -
函數(shù)具體調(diào)用過(guò)程:
- 為被調(diào)用函數(shù)在棧頂分配空間殉簸,為被調(diào)用函數(shù)的形參分配空間
- 將實(shí)參的值傳遞給形參
- 被調(diào)用函數(shù)利用形參(如果存在)進(jìn)行運(yùn)算
- 通過(guò)
return
語(yǔ)句將返回值帶回調(diào)用函數(shù) - 調(diào)用結(jié)束,釋放被調(diào)用函數(shù)空間沽讹,釋放其形參分配空間
舉個(gè)例子說(shuō)明問(wèn)題
int add(int num1, int num2) {
int tempSum = num1 + num2;
return tempSum;
}
int main(int argc, const char * argv[]) {
int a = 10;
int b = 20;
int sum = add(a, b);
printf("%i\n",sum); // sum = 30
return 0;
}
上面的main()
般卑、add()
函數(shù)之間調(diào)用的過(guò)程及參數(shù)傳遞在內(nèi)存的示意圖如下:
(1)首先執(zhí)行
main()
函數(shù),系統(tǒng)為main()
函數(shù)在棧頂分配一定大小的空間爽雄,其次為a蝠检、b局部變量分配空間;(2)調(diào)用add()
函數(shù)挚瘟,main()
函數(shù)壓入棧底叹谁,棧頂指針上移,系統(tǒng)為add()
函數(shù)在棧頂分配一定大小的空間乘盖,其次為num1焰檩、num2局部變量分配空間;(3)執(zhí)行兩個(gè)整數(shù)的加法運(yùn)算订框,在add()
函數(shù)幀中新開(kāi)辟一塊空間存放計(jì)算后的結(jié)果tempSum析苫;(4)最后add()
函數(shù)返回,在main()
函數(shù)幀中開(kāi)辟一塊新的空間存放add()
函數(shù)的返回值sum穿扳,(5)add()
函數(shù)幀調(diào)用結(jié)束出棧藤违,系統(tǒng)釋放其空間并且棧頂指針下移,main()
函數(shù)重新回到棧頂纵揍。
注意:當(dāng)前正在運(yùn)行的函數(shù)存儲(chǔ)區(qū)必在棧頂顿乒。
以上就是兩個(gè)函數(shù)在調(diào)用過(guò)程中棧內(nèi)存完整的工作情況(省略了main()
函數(shù)形參的內(nèi)存分配)。雖然函數(shù)在開(kāi)發(fā)中無(wú)處不見(jiàn)泽谨,但是執(zhí)行過(guò)程在內(nèi)存中的表現(xiàn)形式還是有很多值得研究的璧榄。掌握其內(nèi)存分配原理有助于我們更加深入理解函數(shù)特漩。
遞歸函數(shù)的調(diào)用過(guò)程
- 遞歸函數(shù)
在調(diào)用一個(gè)函數(shù)過(guò)程中又出現(xiàn)直接或間接調(diào)用該函數(shù)本身的情況,稱(chēng)為函數(shù)的遞歸調(diào)用骨杂。C語(yǔ)言的特點(diǎn)之一就是允許函數(shù)的遞歸調(diào)用涂身。 -
遞歸函數(shù)的調(diào)用過(guò)程
一個(gè)遞歸函數(shù)的運(yùn)行過(guò)程類(lèi)似于多個(gè)函數(shù)之間的嵌套調(diào)用,只是調(diào)用函數(shù)和被調(diào)用函數(shù)是同一個(gè)函數(shù)搓蚪,因此蛤售,和每次調(diào)用相關(guān)的一個(gè)重要概念是遞歸函數(shù)運(yùn)行的“層數(shù)”。假設(shè)調(diào)用該遞歸函數(shù)的主函數(shù)為第 0 層妒潭,主函數(shù)調(diào)用遞歸函數(shù)進(jìn)入第一層悴能;從第 i 層調(diào)用本函數(shù)為進(jìn)入第 i+1 層,反之雳灾,退出第 i 層遞歸則應(yīng)返回至第 i - 1 層漠酿。例如:
int getAge(int n) {
if (n == 1) {
return 10;
} else {
return getAge(n-1) + 2;
}
}
int main(int argc, const char * argv[]) {
printf("NO.5.age: %d\n",getAge(5));
return 0;
}
《數(shù)據(jù)結(jié)構(gòu)(C語(yǔ)言版)》對(duì)遞歸函數(shù)調(diào)用從內(nèi)存分配角度的解釋如下:
為了保證遞歸函數(shù)正確執(zhí)行,系統(tǒng)建立一個(gè)“遞歸工作椈涯叮”作為整個(gè)遞歸函數(shù)運(yùn)行期間使用的數(shù)據(jù)存儲(chǔ)區(qū)炒嘲,每一層遞歸所需信息構(gòu)成一個(gè)“工作記錄”,其中包括所有的實(shí)參匈庭、局部變量以及上一層的返回地址夫凸。每進(jìn)入一層遞歸,就產(chǎn)生一個(gè)新的工作記錄壓入棧頂阱持。每退出一層遞歸寸痢,就從棧頂彈出一個(gè)工作記錄。當(dāng)前活動(dòng)的工作記錄成為“活動(dòng)記錄”紊选,并稱(chēng)活動(dòng)記錄的棧頂指針為“當(dāng)前環(huán)境指針”。
一個(gè)遞歸問(wèn)題可以分為“遞推”和“回溯”兩個(gè)階段道逗,要經(jīng)歷若干步才能求出最后的結(jié)果兵罢。但是其原理和一般函數(shù)的調(diào)用沒(méi)有本質(zhì)區(qū)別。遞歸函數(shù)調(diào)用次數(shù)越多滓窍,在棧上為其分配的空間就越大卖词,所以我們應(yīng)該避免調(diào)用次數(shù)過(guò)多的遞歸函數(shù),因?yàn)樵摬僮骱芸赡軙?huì)使棧的容量“溢出”吏夯。
由于”遞歸函數(shù)“概念本身不是本文的重點(diǎn)此蜈,這里僅僅是介紹一下遞歸函數(shù)調(diào)用在內(nèi)存中分配情況。對(duì)”遞歸函數(shù)“不甚了解的同學(xué)可以查閱一下相關(guān)資料噪生,這里就不再贅述了裆赵。
文章最后
以上就是筆者對(duì)于C語(yǔ)言?xún)?nèi)存管理深入的學(xué)習(xí)心得,知識(shí)點(diǎn)比較少跺嗽,部分描述引自書(shū)籍文檔战授。如果文中有任何紕漏或錯(cuò)誤歡迎在評(píng)論區(qū)留言指出页藻,本人將在第一時(shí)間修改過(guò)來(lái);喜歡我的文章植兰,可以關(guān)注我以此促進(jìn)交流學(xué)習(xí)份帐; 如果覺(jué)得此文戳中了你的G點(diǎn)請(qǐng)隨手點(diǎn)贊;轉(zhuǎn)載請(qǐng)注明出處楣导,謝謝支持废境。