2.2 C 的内存的使用方法

2.2.1 C的变量的种类

C 语言的变量具有区间性的作用域*

* 在标准中,“作用域”( scope )和 “连接”(linkage)是分别定义的,用语句块包围的是作用域,static 和 extern 分别控制静态 连接和外部连接。对于全局变量,作用域指文件作用域,链接指外部链接。对于程序员来说,这些方式都是控制命名空间的,它们没有什么不同。在本书中,我们统一使用“作用域”这种叫法。

在开发一些小程序的时候,也许我们并不在意作用域的必要性。可是,当你书写几万行,甚至几十万行的代码的时候,没有作用域肯定是不能忍受的。

C 语言有如下三种作用域。

  • 全局变量

在函数之外声明的变量,默认地会成为全局变量。全局变量在任何地方都是可见的。当程序被分割为多个源代码文件进行编译时,声明为全局变量的变量也是可以从其他源代码文件中引用的。

  • 文件内部的静态变量

就算对于像全局变量那样被定义在函数外面的变量,一旦添加了 static,作用域就只限定在当前所在的源代码文件中。通过 static 指定的变量(包括函数),对于其他源代码文件是不可见的。在英语中,static 是“静态的”的意思,我实在想不明白为什么这个功能莫名其妙地被冠以“static”,这一点可以算是 C 语言的一个未解之谜。

  • 局部变量

局部变量是指在函数中声明的变量。局部变量只能在包含它的声明的语句块(使用{}括起来的范围)中被引用。

局部变量通常在函数的开头部分进行声明,但也可以在函数内部某语句块的开头进行声明。例如,在“交换两个变量的内容时,需要使用一下临时变量”的情况下,将局部变量声明放在当前语句块开头还是比较方便的。

局部变量通常在它所在的语句块结束的时候被释放。如果你不想释放某个局部变量,可以在局部变量上加上 static 进行声明(在后面有详细说明)。

另外,除了作用域不同,C 的变量之间还有存储期(storage duration)的 差别。

  • 静态存储期(static storage duration)

全局变量、文件内的 static 变量、指定 static 的局部变量都持有静态存储期。这些变量被统称为静态变量。

持有静态存储期的变量的寿命从程序运行时开始,到程序关闭时结束。换句话说,静态变量一直存在于内存的同一个地址上。

  • 自动存储期(auto storage duration)

没有指定 static 的局部变量,持有自动存储期。这样的变量被称为自动变量。

持有自动存储期的变量,在程序运行进入它所在的语句块时被分配以内存区域,该语句块执行结束后这片内存区域被释放*。

  • 如果说明得细致一些,在几乎所有的处理环境中,并不是“程序执行进入语句块时”给自动变量分配内存区域,而是在“程序执行进入函数时”统一地进行内存区域分配的。

这个特征通常使用“栈”的机制来实现。2.5 节中会对此做详细说明。

接下来就不是“变量”了。C 中可以使用 malloc()函数动态分配内存。通过 malloc()动态分配的内存,寿命一直延续到使用 free()释放它为止。

在程序中,如果需要保持一些数据,必须在内存中的某个场所取得相应大小的内存区域。总结一下,在 C 中有三种内存区域的寿命。

  • 静态变量

寿命从程序运行时开始,到程序关闭时结束。

  • 自动变量

寿命到声明该变量的语句块被执行结束为止。

  • 通过 malloc()分配的领域

寿命到调用 free()为止。

要 点

C 中有三种内存领域的寿命。

  • 静态变量的寿命从程序运行时开始,到程序关闭时结束。

  • 自动变量的寿命到声明该变量的语句块执行结束为止。

  • 通过 malloc() 分配的领域的寿命到调用 free() 为止。

 

补充 存储类型修饰符

在 C 的语法中,以下关键字被定义为“存储类型修饰符”。

  1. typedef extern static auto register

可是,在这些关键字中,真正是“指定存储区间”的关键字,只有 static

可是,当你在函数的外面使用 static 的时 候,就是使用作用域来控制了,而不是使用存储期。

extern 使得在其他地方定义的外部变量可以在本地可见;auto 是默认的,所以没有显式指定的必要;register 可以给出编译器优化提示(如今的编译已经很先进了,所以一般也不会使用这个关键字);至于typedef,它只是因为可以给编码带来便利才被归纳到存储类型修饰符中来的。

希望大家不要被这众多的“存储类型修饰符”搞得手忙脚乱。

2.2.2 输出地址

正如之前所说,C 的变量中有几个阶段的作用域,而且变量之间还有“存储期”的区别。此外,通过 malloc()可以动态分配内存。

在内存中,这些变量究竟是怎样配置的呢?不如让我们来写个测试程序验证一下(参照代码清单 2-2)。

代码清单 2-2 print_address.c

  1. 1: #include <stdio.h>
  2. 2: #include <stdlib.h>
  3. 3:
  4. 4: int global_variable;
  5. 5: static int file_static_variable;
  6. 6:
  7. 7: void func1(void)
  8. 8: {
  9. 9: int func1_variable;
  10. 10: static int func1_static_variable;
  11. 11:
  12. 12: printf("&func1_variable..%p\n", &func1_variable);
  13. 13: printf("&func1_static_variable..%p\n", &func1_static_var iable);
  14. 14: }
  15. 15:
  16. 16: void func2(void)
  17. 17: {
  18. 18: int func2_variable;
  19. 19:
  20. 20: printf("&func2_variable..%p\n", &func2_variable);
  21. 21: }
  22. 22:
  23. 23: int main(void)
  24. 24: {
  25. 25: int *p;
  26. 26:
  27. 27: /*输出指向函数的指针*/
  28. 28: printf("&func1..%p\n", func1);
  29. 29: printf("&func2..%p\n", func2);
  30. 30:
  31. 31: /*输出字符串常量的地址*/
  32. 32: printf("string literal..%p\n", "abc");
  33. 33:
  34. 34: /*输出全局变量*/
  35. 35: printf("&global_variable..%p\n", &global_variable);
  36. 36:
  37. 37: /*输出文件内的static 变量的地址*/
  38. 38: printf("&file_static_variable..%p\n", &file_static_var iable);
  39. 39:
  40. 40: /*输出局部变量*/
  41. 41: func1();
  42. 42: func2();
  43. 43:
  44. 44: /*通过malloc 申请的内存区域的地址*/
  45. 45: p = malloc(sizeof(int));
  46. 46: printf("malloc address..%p\n", p);
  47. 47:
  48. 48: return 0;
  49. 49: }

在我的环境中运行结果如下:

  1. &func1..0x8048414
  2. &func2..0x8048440
  3. string literal..0x8048551
  4. &global_variable..0x804965c
  5. &file_static_variable..0x8049654
  6. &func1_variable..0xbfbfd9d8
  7. &func1_static_variable..0x8049650
  8. &func2_variable..0xbfbfd9d8
  9. malloc address..0x805b030

一开始我们说要将变量的地址输出,代码清单 2-2 的第 28~29 行却输出了指向函数的指针

尽管到目前为止没有提到过函数指针的问题,但是这次的代码中出现了函数指针。函数通过编译器解释成机器码,并且被配置在内存的某个地方的地址上。

在 C 中,正如数组在表达式中可以被解读成指针一样,“函数”也同时意味着“指向函数的指针”。通常,这个指针指向函数的初始地址。

第 32 行输出使用""包围的字符串(字符串常量)的地址。

在 C 中,“字符串”是作为“char 的数组”来表现的。字符串常量类型也是“char 的数组”,因为表达式中的数组可以解读成“指向初始元素的指针”,所以表达式中的“abc”,同样也意味着保存这个字符串内存区域的初始地址。

字符串常量在 C 中也被做了特别对待,它总让人感觉“不知道它被保存在内存的哪一片区域”,所以在这里我们也尝试输出它的地址。

第 35 行和第 38 行,分别输出了全局变量的地址和文件内 static 变量的地址。

第 41 行和第 42 行,调用了函数 func1()func2()。第 12 行和第 20 行输出自动变量的地址,第 13 行输出 static 局部变量的地址。

再回到 main()函数,在第 46 行输出利用 malloc()分配的内存区域的地址。

接下来,让我们来观察一下实际被输出的地址。

乍一看,都是 0x80……、0xbf……这样的地址,有点晕。

将地址按照顺序重新排列,并且整理成表 2-1。

表2-1 地址一览表

地  址 内  容
0x8048414 函数func1()的地址
0x8048440 函数func2()的地址
0x8048551 字符串常量
0x8049650 函数内的static变量
0x8049654 文件内static变量
0x804965c 全局变量
0x805b030 利用malloc()分配的内存区域
0xbfbfd9d8 func1()中的自动变量
0xbfbfd9d8 func2()中的自动变量

通过观察,我们发现“指向函数的指针”和“字符串常量”被配置在非常近的内存区域。此外,函数内 static 变量、文件内 static 变量、全局变量等这些静态变量,也是被配置在非常近的内存区域。接下来就是 malloc() 分配的内存区域,它看上去和自动变量的区域离得很远。最后你可以发现,func1()func2()的自动变量被分配了完全相同的内存地址。

如果使用图来说明,应该是下面这样的感觉(参照图 2-3)。

2.2 C 的内存的使用方法 - 图1

图 2-3 各种各样的地址

在后面的章节中,我们将会逐一对这些内存区域进行详细的说明。