栈是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今能见到的所有计算机的语言。
栈的定义:
在经典的计算机科学中,栈被定义为一个特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将压入栈中的数据弹出(出栈,pop),但是栈容量必须遵循一条规则:先入栈的数据最后出栈。
在经典的操作系统中,栈总是向下增长的,压栈的操作使得栈顶的地址减小,弹出操作使得栈顶地址增大。
宏函数(define)
- 宏函数不是真正的函数(只是预处理器进行简单的文本替换)
- 宏函数在一定场景下效率比函数高。以空间换时间
- 对于频繁使用,并且短小的函数,我们一般使用宏函数代替,因为宏函数没有普通函数调用的开销(函数压栈,跳转,返回等)
函数调用流程
栈在程序运行中具有极其重要的地位。最重要的,栈保存一个函数调用所需要维护的信息,这通常被称为堆栈帧或者活动记录,一个函数调用过程所需要的信息一般包括以下几个方面:
- 函数的返回地址;
- 函数的参数;
- 临时变量
- 保存的上下文:包括在函数调用前后需要保存不变的寄存器
函数调用惯例
如果函数调用方在传递参数的时候先压入a参数,再压入b参数,而被调用函数则认为先压入的是b,后压入的是a,那么被调用函数在使用a,b值的时候,就会颠倒。
因此,函数的调用方和被调用方对于函数是如何调用的必须有一个明确的约定,只有双方都遵循同样的约定,函数才能够被正确的调用,这样的约定被称为“调用惯例”,一个调用惯例一般包含以下几个方面:
函数参数的传递顺序和方式
函数的传递有很多种方式,最常见的是通过栈传递,函数的调用方将参数压入栈中,函数自己再从栈中将参数取出,对于有多个参数的函数,调用惯例要规定函数调用方将参数压栈的顺序:从左向右,还是从右向左,有些调用惯例还允许使用寄存器传参,以提高性能
栈的维护方式
在函数将参数压入栈中之后,函数体会被调用,此后需要将压入栈中的参数全部弹出以使得栈在函数调用前后保持一致,这个弹出的工作可以由函数的调用方来完成,也可以由函数本身来完成
为了在链接的时候对调用惯例进行区分,调用惯例要对函数本身的名字进行修饰,不同的调用惯例由不同的名字修饰策略。
在c语言里,存在着多个调用惯例,而默认的是cdecl.任何一个没有显示指定调用惯例的函数都是默认是cdecl惯例。
比如:int func(int a,int b);
应该是int_cdecl func(int a,int b);
cdecl不是标准的关键字,不同的编译器里可能有不同的写法
调用惯例
调用惯例 出栈方 参数传递 名字
cdecl 函数调用方 从右之左参数入栈 下划线+函数名
stdcall 函数本身 从右之左参数入栈 下划线+函数名+@+参数字节数
fastcall 函数本身 前两个参数由寄存器传递,其余参数通过堆栈传递 @+函数名+@+参数的字节数
pascal 函数本身 从左到右参数入栈 相当复杂,参考文档
变量传递
int main()
{
void zi1()
{
void zi2()
{
}
}
}
- main函数在栈区开辟的内存,所有子函数均可使用
- main函数在堆区开辟的内存,所有子函数均可使用
- 子函数1在栈区开辟的内存,子函数1和子函数2均可使用
- 子函数2在全局区开辟的,所有子函数均可使用
栈的生长方向
从高地址向低地址生长
小端模式:高位字节放在高地址,低位字节放在低地址