10W+字C语言硬核总结(三),值得阅读收藏!

2.3.2.3 全局/静态区


全局静态区内的变量在编译阶段已经分配好内存空间并初始化。这块内存在程序运行期间一直存在,它主要存储全局变量、静态变量和常量。


注意:


(1)这里不区分初始化和未初始化的数据区,是因为静态存储区内的变量若不显示初始化,则编译器会自动以默认的方式进行初始化,即静态存储区内不存在未初始化的变量。


(2)全局静态存储区内的常量分为常变量和字符串常量,一经初始化,不可修改。静态存储内的常变量是全局变量,与局部常变量不同,区别在于局部常变量存放于栈,实际可间接通过指针或者引用进行修改,而全局常变量存放于静态常量区则不可以间接修改。


(3)字符串常量存储在全局/静态存储区的常量区。


int v1 = 10;//全局/静态区
const int v2 = 20; //常量,一旦初始化,不可修改
static int v3 = 20; //全局/静态区
char *p1; //全局/静态区,编译器默认初始化为NULL
//那么全局static int 和 全局int变量有什么区别?
void test(){
 static int v4 = 20; //全局/静态区
}
char* func(){
 static char arr[] = "hello world!"; //在静态区存储 可读可写
 arr[2] = 'c';
 char* p = "hello world!"; //全局/静态区-字符串常量区 
 //p[2] = 'c'; //只读,不可修改 
 printf("%d\n",arr);
 printf("%d\n",p);
 printf("%s\n", arr);
 return arr;
}
void test(){
 char* p = func();
 printf("%s\n",p);
}


2.3.2.4 总结


在理解C/C++内存分区时,常会碰到如下术语:数据区,堆,栈,静态区,常量区,全局区,字符串常量区,文字常量区,代码区等等,初学者被搞得云里雾里。在这里,尝试捋清楚以上分区的关系。


数据区包括:堆,栈,全局/静态存储区。


全局/静态存储区包括:常量区,全局区、静态区。


常量区包括:字符串常量区、常变量区。


代码区:存放程序编译后的二进制代码,不可寻址区。


可以说,C/C++内存分区其实只有两个,即代码区和数据区。


2.3.3 函数调用模型


2.3.3.1 函数调用流程


栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今能见到的所有计算机的语言。在解释为什么栈如此重要之前,我们先了解一下传统的栈的定义:


在经典的计算机科学中,栈被定义为一个特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将压入栈中的数据弹出(出栈,pop),但是栈容器必须遵循一条规则:先入栈的数据最后出栈(First In Last Out,FILO).


在经典的操作系统中,栈总是向下增长的。压栈的操作使得栈顶的地址减小,弹出操作使得栈顶地址增大。


栈在程序运行中具有极其重要的地位。最重要的,栈保存一个函数调用所需要维护的信息,这通常被称为堆栈帧(Stack Frame)或者活动记录(Activate Record).一个函数调用过程所需要的信息一般包括以下几个方面:


函数的返回地址;


函数的参数;


临时变量;


保存的上下文:包括在函数调用前后需要保持不变的寄存器。


我们从下面的代码,分析以下函数的调用过程:


int func(int a,int b){
 int t_a = a;
 int t_b = b;
 return t_a + t_b;
}
int main(){
 int ret = 0;
 ret = func(10, 20);
 return EXIT_SUCCESS;
}

10W+字C语言硬核总结(三),值得阅读收藏!

程序员必备硬核资料,点击下载

2.3.3.2 调用惯例


现在,我们大致了解了函数调用的过程,这期间有一个现象,那就是函数的调用者和被调用者对函数调用有着一致的理解,例如,它们双方都一致的认为函数的参数是按照某个固定的方式压入栈中。如果不这样的话,函数将无法正确运行。


如果函数调用方在传递参数的时候先压入a参数,再压入b参数,而被调用函数则认为先压入的是b,后压入的是a,那么被调用函数在使用a,b值时候,就会颠倒。


因此,函数的调用方和被调用方对于函数是如何调用的必须有一个明确的约定,只有双方都遵循同样的约定,函数才能够被正确的调用,这样的约定被称为”调用惯例(Calling Convention)”.一个调用惯例一般包含以下几个方面:


函数参数的传递顺序和方式


函数的传递有很多种方式,最常见的是通过栈传递。函数的调用方将参数压入栈中,函数自己再从栈中将参数取出。对于有多个参数的函数,调用惯例要规定函数调用方将参数压栈的顺序:从左向右,还是从右向左。有些调用惯例还允许使用寄存器传递参数,以提高性能。


栈的维护方式


在函数将参数压入栈中之后,函数体会被调用,此后需要将被压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致。这个弹出的工作可以由函数的调用方来完成,也可以由函数本身来完成。


为了在链接的时候对调用惯例进行区分,调用惯例要对函数本身的名字进行修饰。不同的调用惯例有不同的名字修饰策略。


事实上,在c语言里,存在着多个调用惯例,而默认的是cdecl.任何一个没有显示指定调用惯例的函数都是默认是cdecl惯例。比如我们上面对于func函数的声明,它的完整写法应该是:


int _cdecl func(int a,int b);

注意: cdecl不是标准的关键字,在不同的编译器里可能有不同的写法,例如gcc里就不存在_cdecl这样的关键字,而是使用__attribute_((cdecl)).


10W+字C语言硬核总结(三),值得阅读收藏!


2.3.3.2 函数变量传递分析

10W+字C语言硬核总结(三),值得阅读收藏!

10W+字C语言硬核总结(三),值得阅读收藏!


程序员必备硬核资料,点击下载


2.3.4 栈的生长方向和内存存放方向

10W+字C语言硬核总结(三),值得阅读收藏!



//1. 栈的生长方向
void test01(){
 int a = 10;
 int b = 20;
 int c = 30;
 int d = 40;
 printf("a = %d\n", &a);
 printf("b = %d\n", &b);
 printf("c = %d\n", &c);
 printf("d = %d\n", &d);
 //a的地址大于b的地址,故而生长方向向下
}
//2. 内存生长方向(小端模式)
void test02(){
 //高位字节 -> 地位字节
 int num = 0xaabbccdd;
 unsigned char* p = #
 //从首地址开始的第一个字节
 printf("%x\n",*p);
 printf("%x\n", *(p + 1));
 printf("%x\n", *(p + 2));
 printf("%x\n", *(p + 3));
}


上一篇:笔试题目“翻转字符串”的实现


下一篇:教妹学Java(二十一):一文带你了解面向对象编程的所有概念