咱们在编程序的时候,都会把某一个特定功能封装在一个函数里面,对外暴露一个接口,而隐藏了函数行为的具体实现,一个大型的复杂系统里面包含了不少这样的小函数,咱们称之为过程。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,由此就致使了死循环。