此篇博文根据斯坦福公开课 《编程范式》整理,来阐述C语言函数执行时,在内存中的简单过程,也算是一个简单的笔记。

函数的活动记录

所谓活动记录,便是C语言程序在调用过程中的储存分配方案的记录。即:当一个过程被调用时,就把它的活动记录推入运行时存储栈的栈顶,而在控制返回调用程序时,再从栈顶弹出相应的活动记录。如此反复,以执行整个程序。

我们首先将内存抽象为一个索引从 0 开始的庞大数组,然后为了方便说明函数的具体执行过程,我们以一段简单的代码作为说明:

1
2
3
4
void foo(int a, int b) {
char c[4];
int d;
}

当我们写下这样一个函数的时候,在编译过程中,在内存中会如下图这样分配空间:

C函数活动记录1.png
C函数活动记录1.png

在该函数编译之时,以 Save PC 为分界线,内存上半部分依次为形参空间,下半部分为函数中所申明的变量的空间。

注:save PC 我其实并没有搞得很懂,但是按照老师的说法,应当可以理解为函数本身的地址(此处存疑)

函数的执行

为了较为顺畅地理解,我们依旧以示例代码来说明:

1
2
3
4
5
6
7
8
9
int foo(int a) {
if (a == 0) return 1;
return foo(a-1);
}

int main() {
int n = 3;
foo(n);
}

此时,编译之时,会像上边一般,在内存中以相同的方式分配空间,然后,函数开始执行:

执行过程会在内存的栈区进行,在调用 foo 函数之后,PC 会直接跳转到 foo 函数所在的内存空间之处,也就是 Save PC 所在,如下图所示:

C函数活动记录2.png
C函数活动记录2.png

由于 foo 函数的形参刚好在其上,所以,参数传递就如此完成了。此后,便会执行 foo 函数中的语句。

有趣的例子

利用上述原理,有这样一个有趣的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

void createArray() {
int array[100];
for (int i = 0; i < 100; i++)
array[i] = i;
}

void printArray() {
int array[100];
for (int i = 0; i < 100; i++)
printf("%d ", array[i]);
}

int main() {
createArray();
// printf("Hello");
printArray();
return 0;
}

看到代码,你会有点懵,这不就是打印 0-99 吗?这代码有什么特别呢?然而,你再看看,便会发现两个 array 数组是在两个函数中,这时候你可能会记起当年学的C语言,不同函数中的声明是相互独立的啊?这样写不是脱裤子放屁吗?你就会心有疑虑,对这样是否能够实现相关功能而存疑。

然而,通过上述原理我们知道,函数调用结束之后,只是指针在内存中的跳转,而内容并不会被擦去,所以答案是肯定的,运行代码,会正常输出 0 1 2 3 ...99

那么如果在两次调用中加上一句呢?也就是将上面代码的的注释去掉。然后在运行你便会发现不成了,这是因为两次调用中间如果加入了其余语句,便会扰乱前一个函数所初始化的内存,从而使得第二个函数不能正常打印