0%

程序和内存 02.站

程序和内存 2.站

我们进入今天的主题之前需要补充说明上一篇文章缺失或者错漏的部分, 然后简单回顾C语言关于变量的一些重要概念。

内存的静态区域包括了好几个部分, 我们看到了静态常量区, 此外还有静态全局数据区, 代码区等等, 当然这些都是非常笼统的说法, 通常情况下我们也不需要过于关注内存的这一区域, 我们还是集中精力关注站和堆。

一般情况下静态常量区和代码区受保护, 不可修改, 而静态全局数据区则可以修改上面存储的数据。

比如我们c语言的全局变量; 静态全局变量和静态局部变量都可以随意修改它们的值, 而且这些变量都是在静态全局数据区域存取的, 三者的简要说明如下所示:

  • 全局变量 在函数外部定义, 在整个程序的生命周期都可以使用

  • 静态全局变量 用static关键词修饰的全局变量 特点是只能在当前的.c源文件里访问, 其他方面和全局变量没有区别

  • 静态局部变量 用static关键字修饰的局部变量 在函数内部定义和初始化, 首次函数被调用初始化后不会二次调用函数的时候第二次初始化, 只能在函数内部使用, 但是可以通过其他手段从函数外部改变其值

    三种类型的变量都是在全局数据区存储, 我们知道这些都是有编译器生成, 而且可执行文件载入开始运行的时候有操作系统管理就可以了, 我们今天集中精力讨论函数的局部变量!

局部变量

这里讨论的局部变量包含函数的实参和在函数内部定义的所有局部变量。

我们已经知道局部变量只能在函数内部使用, 在函数开始调用的时候创建; 函数返回后销毁。

知道这个以后我们可以做一个实验, 在一个递归函数里打印函数局部变量的地址, 观察这些地址之间有什么规律。

void f(int x)
{
printf(%ld, %d\n", (long int)&x % 1000, x);
if (x <= 100) f(x + 1);
}

实际上(%p)可以打印某个地址, 但是格式按照十六进制来输出, 不利于我们这些普通人直观的观察, 不符合我们的十进制直觉, 所以使用了%ld来输出这个地址。

此外我们保留地址的后三位就可以了, 前面的一长串数字太干扰我们的听觉了, 编译出现警告不必理会。

我这边用好几个编译器编译这段代码, 然后总结如下结论:

  • 函数局部变量的地址从高地址往低地址增长

  • 递归调用的时候本次递归调用的局部变量的地址和其他任何一次递归调用局部变量的地址跨度相等, 比如在mingw gcc x64编译器的结果

    > a.exe
    100, 1
    52, 2
    4, 3
    956, 4
    908, 5
    860, 6

    这里是一个输出结果, 当然你那边和我不相同, 但是每次递归调用的地址跨度都是一样的, 牢记上面得出的结论。

    如下是每个函数占用的最小内存空间的大小, 每种编译器有差别:

  • mingw-gcc_x64 48字节

  • linux-gcc 32字节

  • msvc_x64 48字节

  • msvc_x86 12字节

    在f函数上加上几个参数看看输出的地址跨度如何变化。 如下是结论, 建议大家动手实验。

  • mingw-gcc_x64 最少48字节, 根据局部变量的个数动态变化范围固定16字节

  • linux-gcc 最少32字节, 根据局部变量的个数动态变化范围固定16字节

  • msvc_x64 最少48字节, 根据局部变量的个数动态变化范围固定16字节

  • msvc_x86 最少12字节, 根据局部变量的个数动态变化范围根据局部变量的个数不多不少

    上面的总结表明要想节省内存需要用msvc编译器, 哈哈哈哈。 首先它最少空间最少12个字节, 而且它不会浪费内存空间, 用几个字节就是几个字节, 不会多要。

    最后用一个骚操作来巩固一下我们上面所作的实验结论。

跨越函数访问

实现一个功能在递归函数的最后一次递归的时候把所有前面递归的参数累加返回。

函数原型如下:

int total(int n);

你不能用循环, 也不能用等差数列, 递归函数不能有额外的参数。

必须用mingw gcc; linux gcc和msvc_x86-x64编译通过。

为了程序的兼容性我们需要如下定义,

#ifdef LINUX
#define MIN_STACK_SIZE 32
#else
#ifdef WINDOWS_X64
#define MIN_STACK_SIZE 48
#else
#define MIN_STACK_SIZE 12
#endif
#endif

上面我们定义了每一个平台的局部变量初始容量。

有了上面的定义后可以这样编译, 这些分别是Linux gcc; mingw gcc; msvc_x64和msvc-x86的编译命令:

gcc -D LINUX sum.c -o sum
gcc -D WINDOWS_X64 sum.c -o sum
cl -D WINDOWS_X64 sum.c
cl sum.c

接下来我们编写功能函数

int total(int n)
{
if (n == 1)
{
int *p = &n;
// 把所有额外的局部变量的大小加起来, 一个int指针和四个int类型的变量
int allVarSize = sizeof(p) + sizeof(int) * 4;
// 非msvc-x86平台补齐空间, 要不然访问出错
if (sizeof(p) == 8 && allVarSize % 16 != 0)
{
allVarSize = allVarSize + 16 - allVarSize % 16;
}
// 初始大小和变量大小相加得到全部大小字节
// 首先得到的是字节大小, 而我们知道int占用若干字节, 所以相加后在除以int类型的大小
// 因为我们的指针类型是int类型, p[i]; 按照int大小偏移
int span = (MIN_STACK_SIZE + allVarSize) / sizeof(int);
int i = 0;
int s = 0;
// 开始便利本函数的网上所有递归调用的n参数的地址
// 我们观察可以发现p[i * span]里的数据和变量i相等
while (i == p[i * span] - 1)
{
s += p[i * span];
i++;
}
return s;
}
return total(n - 1);
}

编译程序的时候需要传入合理的符号, LINUX; WINDOWS_X64或者默认WINDOWS_X86。

最后我们得出了如下结论:

  • 函数局部变量有个初始容量
  • 初始容量在不同的编译器操作系统上有差异
  • 这些容量根据函数局部变量的个数动态调整
  • 每一个编译器的调整策略也有着差异

栈溢出stackOverflow

我们所说的所谓内存里的站区指的就是我们折腾半天的存取函数局部变量的区域!

接下来我们在写一个函数看看整个战区的大小。

void stackOverflow(int n)
{
printf("%ld\n", (long int)&n);
stackOverflow(n);
}

我们需要把结果重定向到一个文件里。

./a.out > out.txt

然后从文件的第一个数字减去最后一个数字, 两次除以1024, 结果Windows2mb, Linux8mb。

这就是整个战区的大小了, 如果函数的递归调用超出这个容量就会引发著名的错误StackOverflow站溢出, 我们的示例程序就是引发这一错误意外终止的。

做一个小结

  • 战区总是从高地址往低地址延申

  • 每次调用有个初始大小, 如果初始大小放不下局部变量就会动态调整其大小

  • 战区大小是有限的, 如果超出这个范围就会引发栈溢出错误

    到此为止我们可以告一段落了, 下次继续研究站区。 未完待续……