我们先看带有一个自定义函数时的内存分配情况。
int add(int a, int b) {
int c = 25;
printf("%10s: %p\n", "add_c", &c);
printf("%10s: %p\n", "add_b", &b);
printf("%10s: %p\n", "add_a", &a);
return a + b + c;
}
int main() {
printf("------- Function Addresses -------\n\n");
int (*fp1)(int, int) = add;
printf("%10s: %p\n", "add_func", fp1);
int (*fp)(void) = main;
printf("%10s: %p\n", "main_func", fp);
printf("\n");
printf("------- Stack Frame Addresses -------\n");
printf("\n");
fp1(2, 3);
printf("\n");
fp1(5, 8);
printf("\n");
printf("------- Main Function Local Variable Addresses -------\n\n");
int a = 55;
int b = 88;
printf("%10s: %p\n", "main_b", &b);
printf("%10s: %p\n", "main_a", &a);
printf("%10s: %p\n", "main_fp", &fp);
printf("%10s: %p\n", "main_fp1", &fp1);
return 0;
}
显示:
------- Function Addresses -------
add_func: 0x100003bd0
main_func: 0x100003c50
------- Stack Frame Addresses -------
add_c: 0x7ffeefbff3e4
add_b: 0x7ffeefbff3e8
add_a: 0x7ffeefbff3ec
add_c: 0x7ffeefbff3e4
add_b: 0x7ffeefbff3e8
add_a: 0x7ffeefbff3ec
------- Main Function Local Variable Addresses -------
main_b: 0x7ffeefbff440
main_a: 0x7ffeefbff444
main_fp: 0x7ffeefbff448
main_fp1: 0x7ffeefbff450
这段代码中,共有main及add两个函数,main函数有4个本地变量,add函数有2个形参及1个本地变量。
先不考虑程序运行的时间顺序,只看内存的分配地址。从上到下,内存地址从低位到高位排列。正如上面的标题所分离,这里的内存分成3大区域,分别是add及main两个函数地址所在区域、add函数的形参及其本地变量的内存区域,以及main函数中4个本地变量的内存区域。
add及main两个函数的地址都在最低位。函数地址的内存区域,按函数的声明顺序分配内存地址。
接着是add函数的形参a, b及本地变量c的地址。这三个变量中,本地变量c的地址最低,然后是第2个形参b, 最后才是第1个形参a。
从进栈顺序来看,第1个形参a先进栈,然后是第2个形参b进栈,最后是add函数本地变量c进栈。其规律是,最先声明的变量先进栈。
最后是main函数4个本地变量的内存区域。同样,也是最先声明的变量先进栈。
我们看到,main函数调用了2次add函数,第2次调用,其2个形参与其1个本地变量的地址,与第1次调用时的地址都是完全一样的。根据此特点,我们稍微调整一下文本示意图:
1st 2nd
--------------------------------------------
var | val || var | val
-----------------------------
0x7ffeefbff3e4: add_c | 25 || add_c | 25
0x7ffeefbff3e8: add_b | 3 || add_b | 8
0x7ffeefbff3ec: add_a | 2 || add_a | 5
因为add函数中的变量c每次调用被初始化为“25”,因此它两次调用的值都不会变,但均在“0x7ffeefbff3e4”这个地址上存储其值。而add函数中的形参a与b两次调用所传入的值都不一样,但在不同调用期间其所分配的地址也都是固定的,分别为“0x7ffeefbff3ec”及“0x7ffeefbff3e8”。
也就是说,每调用一次add函数,都会以“0x7ffeefbff3ec”的地址为该函数的栈区,依序将各变量压进栈区。退出函数后,这一部分的内存区域的数据不会被改变,再一次调用函数时,再次修改该内存区域的内容。
这个特点,对C语言程序员来讲很重要。因为形参a与b会随着实参的变化而变化,因此对于形参,我们能犯错误的机会较少。而对于函数内部的自动变量来讲,我们非常容易犯错。
如果从函数内部返回一个数值,因为是传值的原因,问题不大。
int test() {
int a = 3;
return a;
}
int main() {
int x = test();
}
当从test函数返回a时,其值“3”被赋值于main函数中的x变量,main函数即与test函数断开了连接,以后无论哪个进程、哪段代码重新调用test函数,main函数中的x变量都不会受到影响。
但是,如果从test函数返回的是该本地变量的指针,则就需要特别小心了。
int *test(int a, int b) {
int c = a + b;
return &c;
}
int main() {
int *x = test(2, 3);
printf("%d\n", *x); // 5
test(20, 30);
printf("%d\n", *x); // 50
}
test函数返回的是两数相加结果的变量的指针。第一次调用时,main函数的x指针变量的值为5,这没问题。但第2次调用test函数后,还是同样的地址,但该地址所存储的值已经被第2次调用所改变,此时再来打印x的值,已经悄悄地被改变了。
其实,在遇到这类问题的时候,编译器会给出警示:
Address of stack memory associated with local variable 'c' returned
即,在栈区中返回了本地变量的地址。原因如同上面所分析的一样,栈区中某个固定的地址,其内容是就像万花筒一样,千变万化而不可预料。
对于上例,我们说内存地址从“0x7ffeefbff3ec”到“0x7ffeefbff3e4”的内存空间为栈帧(stack frame),即系统将在这里进行进栈出栈操作。存放在这一区域的本地变量,因为由系统根据需要来分配内存地址及改变其值,因此也称为自动变量。
推而广之,如果从某个函数中返回char *类型的字符指针,是安全的,因为字符指针是一种静态变量,其生命周期与全局变量一样,存活于整个应用程序期间,其内存空间不在栈帧中,而在特定的内存区域,不会被随意修改。但如果传回char str[n]类型的字符数组,同属于本地变量,就需要我们特别小心了。
现在,我们再加入另外一个有3个形参的sub函数。
int sub(int a, int b, int c) {
int d = a - b - c;
printf("%10s: %p\n", "sub_d", &d);
printf("%10s: %p\n", "sub_c", &c);
printf("%10s: %p\n", "sub_b", &b);
printf("%10s: %p\n", "sub_a", &a);
return d;
}
int add(int a, int b) {
int c = 25;
printf("%10s: %p\n", "add_c", &c);
printf("%10s: %p\n", "add_b", &b);
printf("%10s: %p\n", "add_a", &a);
return a + b + c;
}
int main() {
printf("------- Function Addresses -------\n\n");
int (*fp2)(int, int, int) = sub;
printf("%10s: %p\n", "sub_func", fp2);
int (*fp1)(int, int) = add;
printf("%10s: %p\n", "add_func", fp1);
int (*fp)(void) = main;
printf("%10s: %p\n", "main_func", fp);
printf("\n");
printf("------- Stack Frame Addresses -------\n");
printf("\n");
fp1(2, 3);
printf("\n");
fp2(15, 7, 1);
printf("\n");
printf("------- Main Function Local Variable Addresses -------\n\n");
int a = 55;
int b = 88;
printf("%10s: %p\n", "main_b", &b);
printf("%10s: %p\n", "main_a", &a);
printf("%10s: %p\n", "main_fp", &fp);
printf("%10s: %p\n", "main_fp1", &fp1);
printf("%10s: %p\n", "main_fp2", &fp2);
return 0;
}
显示:
------- Function Addresses -------
sub_func: 0x100003ac0
add_func: 0x100003b60
main_func: 0x100003be0
------- Stack Frame Addresses -------
add_c: 0x7ffeefbff3d4
add_b: 0x7ffeefbff3d8
add_a: 0x7ffeefbff3dc
sub_d: 0x7ffeefbff3d0
sub_c: 0x7ffeefbff3d4
sub_b: 0x7ffeefbff3d8
sub_a: 0x7ffeefbff3dc
------- Main Function Local Variable Addresses -------
main_b: 0x7ffeefbff438
main_a: 0x7ffeefbff43c
main_fp: 0x7ffeefbff440
main_fp1: 0x7ffeefbff448
main_fp2: 0x7ffeefbff450
函数地址按是按声明的顺序,从低位到高位分配空间。main函数内的各个局域变量,还是按声明的顺序进栈。
注意“Stack Frame Addresses”部分,add函数及sub函数的形参、局域变量都共享相同的内存空间! 且因为sub函数的形参、局域变量的数量较多,因此最后一个变量d依照从高位内存到低位内存进栈的顺序最后一个进栈。
综上,函数的内存分配规律如下:
函数内存分配机制并不复杂。函数栈帧的特点,更是与汇编语言如此之近。这是C语言作为一门高级语言,在方便编码的同时,又能灵活地操纵底层细节的一个例子。了解并掌握函数内存分配机制,可让我们在高效地使用指针时清楚地知道自己在干什么,从而避免出现一些不易察觉的bug。