咱們在編程序的時候,都會把某一個特定功能封裝在一個函數裏面,對外暴露一個接口,而隱藏了函數行爲的具體實現,一個大型的複雜系統裏面包含了不少這樣的小函數,咱們稱之爲過程。git
過程是相對獨立的小模塊,系統的運行須要這些過程的緊密合做,這種合做就是函數調用。github
在一個函數執行時調用別的函數,好比 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 被調用時是同樣的。函數
因此遞歸也就不難理解了,初學算法總以爲遞歸有點奇妙,怎麼本身調用本身,而實際上對於計算機來講,它和調用其它函數沒什麼區別,在計算機眼裏,沒有自身與其它函數的區別,全部被調用者都是其它人。post
數組是編程中不可或缺的一種結構,「數組是分配在連續的內存中」這句話已經爛熟於心了,歷史上,C 語言只支持大小在編譯時就能肯定的多維數組,這個多多少少有一些不便利,因此在ISO C99
標準中就引入了新的功能,容許數組的維度是表達式。ui
int A[expr1][expr2]
複製代碼
由於數組是連續的內存,因此很容易就能訪問到指定位置的元素,它經過首地址加上偏移量便可計算出對應元素的地址,這個偏移量必定意義上就是由索引給出。spa
好比如今有一個數組A
,那麼A[i]
就等同於表達式* (A + i)
,這是一個指針運算。C 語言的一大特性就是指針,既是優勢也是難點,單操做符&
和*
能夠產生指針和簡介引用指針,也就是,對於一個表示某個對象的表達式expr
,&expr
給出該對象地址的一個指針,而對於一個表示地址的表達式Aexpr
,*Aexpr
給出該地址的值。設計
即便咱們建立嵌套(多維)數組,上面的通常原則也是成立的,好比下面的例子。
int A[5][3];
// 上面聲明等價於下面
typedef int row3_t[3];
row3_t A[5];
複製代碼
這個數組在內存的中就是下面那個樣子的。
還有一個重要的概念叫作數據對齊
,即不少計算機系統要求某種類型的對象的地址必須是某個值 K(通常是二、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,由此就致使了死循環。