函式呼叫與空間分配
我們在程式設計序的時候,都會把某一個特定功能封裝在一個函式裡面,對外暴露一個介面,而隱藏了函式行為的具體實現,一個大型的複雜系統裡面包含了很多這樣的小函式,我們稱之為過程。
過程是相對獨立的小模組,系統的執行需要這些過程的緊密合作,這種合作就是函式呼叫。
在一個函式執行時呼叫別的函式,比如 P 呼叫 Q,需要執行一些特定的動作。傳遞控制
,在呼叫 Q 之前,控制權在 P 的手裡,既然要呼叫 Q,那麼就需要把控制權交給 Q;傳遞資料
,就是函式傳參;分配與釋放記憶體
,在開始時,Q 可能需要位區域性變數分配空間,結束時又必須釋放這些儲存空間。
大多數語言都使用棧提供的先進後出機制來管理記憶體,x86-64 可以通過通用暫存器傳遞最多 6 個整數值(整數或地址),如果超過 6 個,那就需要在棧中分配記憶體,並且通過棧傳遞引數時,所有資料的大小都要向 8 的倍數對齊。將控制權從 P 轉交給 Q,只需要將 PC(程式計數器)的值置為 Q 程式碼的起始位置,並記錄好 P 執行的位置,方便 Q 執行完了,繼續執行 P 剩餘的程式碼。
在函式的傳參、執行中,多多少少都需要空間來儲存變數,區域性資料能儲存在暫存器中就會儲存在暫存器中,如果暫存器不夠,將會儲存在記憶體中。除了暫存器不夠用的情況,還有陣列、結構體和地址等區域性變數都必須儲存在記憶體中。分配記憶體很簡單,只需要減小棧指標的值就行了,同樣釋放也只需要增加棧指標。
在函式執行過程中,處理棧指標%rsp
,其它暫存器都被分類為被呼叫者儲存暫存器
,即當過程 P 呼叫過程 Q 時,Q 必須儲存這些暫存器的值,保證它們的值在 Q 返回到 P 時與 Q 被呼叫時是一樣的。
所以遞迴也就不難理解了,初學演算法總覺得遞迴有點奇妙,怎麼自己呼叫自己,而實際上對於計算機來說,它和呼叫其它函式沒什麼區別,在計算機眼裡,沒有自身與其它函式的區別,所有被呼叫者都是其它人。
陣列是程式設計中不可或缺的一種結構,“陣列是分配在連續的記憶體中”這句話已經爛熟於心了,歷史上,C 語言只支援大小在編譯時就能確定的多維陣列,這個多多少少有一些不便利,所以在ISO C99
標準中就引入了新的功能,允許陣列的維度是表示式。
int A[expr1][expr2]
因為陣列是連續的記憶體,所以很容易就能訪問到指定位置的元素,它通過首地址加上偏移量即可計算出對應元素的地址,這個偏移量一定意義上就是由索引給出。
比如現在有一個陣列A
,那麼A[i]
就等同於表示式* (A + i)
,這是一個指標運算。C 語言的一大特性就是指標,既是優點也是難點,單操作符&
和*
可以產生指標和簡介引用指標,也就是,對於一個表示某個物件的表示式expr
,&expr
給出該物件地址的一個指標,而對於一個表示地址的表示式Aexpr
,*Aexpr
給出該地址的值。
即使我們建立巢狀(多維)陣列,上面的一般原則也是成立的,比如下面的例子。
int A[5][3];
// 上面宣告等價於下面
typedef int row3_t[3];
row3_t A[5];
這個陣列在記憶體的中就是下面那個樣子的。
還有一個重要的概念叫做資料對齊
,即很多計算機系統要求某種型別的物件的地址必須是某個值 K(一般是2、4 或 8)的倍數,這種限制簡化了處理器和記憶體介面之間的設計,甚至有的系統沒有進行資料對齊,程式就無法正常執行。
比如現在有一個如下的結構體。
struct S1 {
int i;
char c;
int j;
}
如果編譯器用最小的 9 位元組分配,那麼將是下面的這個樣子。
但是上面這種結構無法滿足 i 和 j 的 4 位元組對齊要求,所以編譯器會在 c 和 j 之間插入 3 個位元組的間隙。
在極客時間專欄中有這樣一段程式碼。
int main(int argc, char *argv[]){
int i = 0;
int arr[3] = {0};
for(; i <= 3; i++){
arr[i] = 0;
printf("Hello world!\n");
}
return 0;
}
這段程式碼神奇的是在某種情況下會一直迴圈的輸出Hello world
,並不會結束,在計算機系統漫遊(補充)中也提到過。
造成上面這種結果是因為函式體內的區域性變數存在棧中,並且是連續壓棧,而 Linux 中棧又是從高向低增長。陣列arr
中是 3 個元素,加上 i 是 4 個元素,剛好滿足 8 位元組對齊(編譯器 64 位系統下預設會 8 位元組對齊),變數i
在陣列arr
之前,即i
的地址與arr
相鄰且比它大。
程式碼中很明顯訪問陣列時越界了,當i
為 3 時,實際上正好訪問到變數i
的地址,而迴圈體中又有一句arr[i] = 0;
,即又把i
的值設定為了 0,由此就導致了死迴圈。
相關文章
- 棧空間受限情況下C/C++函式呼叫注意事項C++函式
- 普通函式與函式模板呼叫規則函式
- 建構函式之間的呼叫函式
- 普通函式與函式模板呼叫規則2函式
- js 實現鏈式呼叫名稱空間JS
- MySQL空間函式實現位置打卡MySql函式
- C++ 動態記憶體分配與名稱空間C++記憶體
- makefile--函式定義與呼叫函式
- [20180531]函式呼叫與遞迴.txt函式遞迴
- [20231123]函式與bash shell呼叫.txt函式
- 兩個JS之間的函式互相呼叫JS函式
- 第 8 節:函式-函式巢狀呼叫與返回值函式巢狀
- 函式呼叫的代價與優化函式優化
- 子函式呼叫函式
- 函式呼叫棧函式
- Swoole 回撥函式的註冊與呼叫函式
- Golang記憶體分配內建函式之new函式Golang記憶體函式
- 外部函式的呼叫函式
- gdb 如何呼叫函式?函式
- C程式函式呼叫&系統呼叫C程式函式
- PostgreSQL函式裡呼叫函式(SETOF + RETURN QUERY)SQL函式
- 時間函式:與時間相關那些事。。。函式
- 好程式設計師Python教程系列遞迴函式與匿名函式呼叫程式設計師Python遞迴函式
- [20210528]oracle大表空間預分配問題.txtOracle
- vue 全域性函式的 定義與任意呼叫Vue函式
- 記錄個Java/Groovy的小問題:空字串呼叫split函式返回非空陣列Java字串函式陣列
- httprunner yml 呼叫外部函式HTTP函式
- 前端-如何始終平均分配剩餘空間前端
- php名稱空間的呼叫順序PHP
- AArch64函式棧的分配,指令生成與GCC實現(上)函式GC
- 『無為則無心』Python函式 — 31、名稱空間(namespace)Python函式namespace
- 如何使用函式指標呼叫類中的函式和普通函式函式指標
- 核心函式 系統呼叫 系統命令 庫函式函式
- oracle建立使用者,表空間,臨時表空間,分配許可權步驟詳解Oracle
- 微信小程式--頁面與元件之間如何進行資訊傳遞和函式呼叫微信小程式元件函式
- GOPATH 與工作空間Go
- 好程式設計師Python培訓分享Python的遞迴函式與匿名函式呼叫程式設計師Python遞迴函式
- 函式呼叫棧的問題函式