名词解释

1. 对象: 从硬件方面来看,被储存的每个值都占用一定的物理内存,C 语言把这样的一块内存称为对象(object)。对象可以储存一个或多个值。一个对象可能并未储存实际的值,但是它在储存适当的值时一定具有相应的大小。

这里的对象和面向对象编程的对象不是一个概念,C 语言是面向过程编程。

2. 存储期: 存储期是指对象在内存中保留了多长时间。存储期可用于描述对象。有自动存储、静态存储、动态分配存储、线程存储。

3. 作用域: 作用域描述程序中可访问标识符的区域。有块作用域、函数作用域、函数原型作用域、文件作用域。

4. 链接: 有无链接、内部链接、外部链接。

5. 翻译单元: 编译器源代码文件和所有的头文件都看成是一个包含信息的单独文件,这个文件被称为翻译单元。如果程序由多个源代码文件组成,那么该程序也将由多个 翻译单元组成。每个翻译单元均对应一个源代码文件和它所包含的文件。

1. 作用域

作用域描述程序中可访问标识符的区域。有块作用域、函数作用域、函数原型作用域、文件作用域。

函数原型作用域和函数作用域决定了在函数原型中可以不使用变量名,以及函数原型作用域中的变量名,和函数作用域中的变量名可以不同。
PS:边长数组不适用。

函数作用域和函数原型作用域实际上也是一种块作用域,因此作用域主要区分块作用域和文件作用域。

块作用域: 定义在块中的变量具有块作用域。块作用域变量的可见范围是从定义处到包含该定义的块的末尾

所谓的块,就是包裹代码的 { }。

文件作用域: 定义在函数的外面的变量具有文件作用域。文件作用域变量的可见范围是从它的定义处到该定义所在文件的末尾均可见
由于这样的变量可用于多个函数,所以文件作用域变量也称为全局变量。

块作用域变量和文件作用域变量可以根据链接和存储期的不同进行细分。

2. 链接

C 语言的变量有三种链接属性:无连接、外部链接、内部链接。

所谓的链接,在我理解中就是能不能被公用。
块作用域、函数作用域、函数原型作用域的变量就是无链接的变量,这些变量只能被定义它们的块、函数、函数原型所使用,不能被别人访问。
文件作用域的变量是有链接的变量,可以被位于该变量声明之后的多个函数共用。根据共用文件域变量的范围,又分为外部链接和内部链接。
外部链接和内部链接的区别:外部链接变量可以供多个翻译单元共用,内部链接变量只在一个翻译单元中使用。

外部链接变量的定义:在函数的外部像定义普通变量一样。
内部链接变量的定义:在函数的外部,使用 static 关键字定义。

  1. int giants = 5; // 文件作用域,外部链接
  2. static int dodgers = 3; // 文件作用域,内部链接

3. 存储期

作用域和链接描述了一个标识符的可见性(在程序的那些地方可以访问标识符)。
存储期描述的是通过这些标识符访问的对象的生存期

C 语言的对象有4中存储期:自动存储期、静态存储期、动态分配存储期、线程存储期

3.1 自动存储期

块作用域的变量通常都具有自动存储期。当程序进入定义这些变量的块时,为这些变量分配内存;当退出这个块时,释放刚才为变量分配的内存。 这种做法相当于把自动变量占用的内存视为一个可重复使用的工作区或暂存区。例如,一个函数调用结束后,其变量占用的内存可用于储存下一个被调用函数的变量。

PS:块作用域变量除了可以具有自动存储期,也能具有静态存储期。在创建静态存储期的块作用域变量时使用 static 关键字来声明。

  1. void more()
  2. {
  3. static int ct = 0;
  4. }

static 关键字用于修饰块作用域变量的声明时,表示该变量具有静态存储期,和存储期有关。
static 关键字用于修饰文件域变量的声明时,表示该变量是内部链接的变量,和存储期无关,因为内部链接和外部链接的变量都是静态存储期的。

3.2 静态存储期

如果对象具有静态存储期,那么它在程序的执行期间一直存在。文件作用域变量和 static 修饰的块作用域变量具有静态存储期。

对于文件作用域变量,关键字 static 表明了其链接属性,而非存储期。

3.3 动态分配存储期

用 malloc() 和 free() 进行动态内存管理。

3.4 线程存储期

线程存储期用于并发程序设计,程序执行可被分为多个线程。具有线程存储期的对象,从被声明时到线程结束一直存在

4. 存储类别

C 语言使用作用域、链接和存储期为变量定义多种存储方案。这里不涉及并发程序设计,因此不包括线程存储期,而动态分配内存期后面再介绍,因此就剩下5中存储类别:自动、寄存器、静态无链接、静态内部链接、静态外部链接。

存储类别 存储期 作用域 链接 声明方式
自动 自动存储期 块作用域 无链接 块内
寄存器 自动存储期 块作用域 无链接 块内、使用 register 关键字
静态无链接 静态存储期 块作用域 无链接 块内、使用 static 关键字
静态内部链接 静态存储期 文件作用域 内部链接 所有函数外,使用static 关键字
静态外部链接 静态存储期 文件作用域 外部链接 所有函数外

4.1 自动变量

属于自动存储类别的变量具有自动存储期、块作用域且无链接。
默认情况下,声明在块或函数头中的任何变量都属于自动存储类别。
可以显式地使用关键字 auto 来声明一个自动变量。

一般情况下,使用 auto 来声明自动变量,是为了表明故意覆盖一个同名的外部变量,或者强调不要把该变量改为其他存储类别。
auto 在 C 中表示存储类别说明符,在 C++ 中用法不同。

块作用域和无链接意味着只有在变量定义所在的块中才能通过变量名访问该变量(当然,参数用于传递变量的值和地址给另一个函数,但是这是间接的方法)。另一个函数可以使用同名变量,但是该变量是储存在不同内存位置上的另一个变量。

变量具有自动存储期意味着,程序在进入该变量声明所在的块时变量存在,程序在退出该块时变量消失。原来该变量占用的内存位置现在可做他用。

初始化问题

自动变量不会自动初始化,如果不初始化,该值是垃圾值。
可以使用非常量表达式初始化自动变量,前提是所用的变量在之前已经定义过。

  1. void main(void)
  2. {
  3. int a;// 未初始化,直接使用是垃圾值
  4. int b = 1; // 使用常量初始化自动变量 b
  5. int c = 2*b; // 使用非常量表达式初始化自动变量 c
  6. }

4.2 寄存器变量

变量通常储存在计算机内存中。如果幸运的话,寄存器变量储存在 CPU 的寄存器中。而寄存器可以概括地认为是最快的可用内存。
与普通变量相比,访问和处理寄存器变量的速度更快。但由于寄存器变量储存在寄存器而非内存中,所以无法获取寄存器变量的地址。
绝大多数方面,寄存器变量和自动变量都一样。也就是说,它们都是块作用域、无链接和自动存储期。但是寄存器变量无法获取地址,即无法使用取地址符 &。

使用存储类别说明符 register 便可声明寄存器变量
需要注意的是,register 更像是一种请求,表示希望该变量是寄存器变量,至于会不会放到寄存器中计算,则是编译器根据寄存器或最快可用内存的数量衡量你的请求,或者直接忽略你的请求,所以可能不会如你所愿。在这种情况下,寄存器变量就变成普通的自动变量。即使是这样,仍然不能对该变量使用地址运算符。

可声明为 register 的数据类型有限。例如,处理器中的寄存器可能没有足够大的空间来储存 double 类型的值

4.3 块作用域的静态变量

静态变量听起来像是静止不变的变量,可能会认为不可变的变量,事实上 const 才是用来修饰常量的关键字,表示值不可变,而静态 static 则是表示变量的地址不可变
静态变量在程序被载入内存时就执行完毕了。
例如,trystatic() 中的 a 变量在程序载入内存时就已经创建了,在 trystatic() 中声明只是为了告诉编译器只有 tyrstatic() 函数可以看见该变量。

  1. void trystatic(void)
  2. {
  3. static int a = 1;
  4. }

如果希望多个函数都可以访问同一个变量,则需要将该变量的声明放在所有函数之外,作为文件作用域变量,所有的文件作用域变量都是静态的。
函数的形参中不允许使用 static 变量

4.4 内部链接静态变量

该存储类别的变量具有静态存储期、文件作用域和内部链接。在所有函数外部(这点与外部变量相同),用存储类别说明符 static 定义的变量具有这种存储类别。
可以使用存储类别说明符 extern,在函数中重复声明任何具有文件作用域的变量。这样的声明并不会改变其链接属性。也可以不使用 extern,在函数中直接使用内部链接静态变量的变量名。

  1. int traveler = 1; // 外部链接
  2. static int stayhome = 1; // 内部链接
  3. int main()
  4. {
  5. extern int traveler; // 使用定义在别处的 traveler
  6. extern int stayhome; // 使用定义在别处的 stayhom
  7. }

4.5 外部链接静态变量

外部链接的静态变量具有文件作用域、外部链接和静态存储期。该类别有时称为外部存储类别,属于该类别的变量称为外部变量。

创建外部变量: 把变量的定义性声明放在在所有函数的外面便创建了外部变量。
使用别的文件中创建的外部变量: 使用 extern 关键字再次声明。

  1. int a = 1; // 创建了一个外部变量 a
  2. extern int b; // 声明了变量 b,该变量的定义式声明在别的文件中
  3. void main(void)
  4. {
  5. extern int c;// 声明了变量 b,该变量的定义式声明在别的文件中
  6. auto int b; // 声明了一个和外部变量 b 同名的自动变量
  7. }

对于 extern int b 省略 extern 则表示创建了一个外部变量 b。
对于 extern int c 省略 extern 则表示创建了一个自动变量 c。
对于 auto int b; 省略 auto 依旧表示创建一个自动变量 b,但是为了显式表达这里的目的就是创建一个和外部变量 b 同名的自动变量,不推荐省略 auto。

初始化

  1. 如果不初始化外部变量,会自动被初始化为 0。
  2. 外部变量只能初始化一次,且必须在定义该变量时进行。
  3. 只能使用常量表达式初始化外部变量。
    1. // file1.c
    2. int a = 1;
    3. // file2.c
    4. extern int a = 2; // 错误

    4.6 多文件

    只有当程序由多个翻译单元组成时,才体现区别内部链接和外部链接的重要性。接下来简要介绍一下。 复杂的 C 程序通常由多个单独的源代码文件组成。有时,这些文件可能要共享一个外部变量。C 通过在一个文件中进行定义式声明,然后在其他文件中进行引用式声明来实现共享。也就是说,除了一个定义式声明外,其他声明都要使用 extern 关键字。而且,只有定义式声明才能初始化变量。 注意,如果外部变量定义在一个文件中,那么其他文件在使用该变量之 前必须先声明它(用 extern关键字)。也就是说,在某文件中对外部变量进行定义式声明只是单方面允许其他文件使用该变量,其他文件在用 extern 声明之前不能直接使用它。

    4.7 函数存储类别

    函数的存储类别有外部函数(默认)、静态函数、内联函数。
    外部函数可以被其他文件函数访问,静态函数只能被用于其定义所在文件。
double gamma(double); // 定义一个外部函数
extern double delta(double, int); // 声明一个定义在其他文件中的函数
static double beta(int, int); // 静态函数

通常的做法是:用 extern 关键字声明定义在其他文件中的函数。这样做是为了表明当前文件中使用的函数被定义在别处。除非使用 static 关键字,否则一般函数声明都默认为 extern。

4.8 存储类别的选择

对于“使用哪种存储类别”的回答绝大多数是“自动存储类别”,要知道默认类别就是自动存储类别。

初学者会认为外部存储类别很不错,为何不把所有的变量都设置成外部变量,这样就不必使用参数和指针在函数间传递信息了。然而,这背后隐藏着一个陷阱。如果这样做,A() 函数可能违背你的意图,私下修改 B() 函数使用的变量。
多年来,无数程序员的经验表明,随意使用外部存储类别的变量导致的后果远远超过了它所带来的便利。
唯一例外的是const数据。因为它们在初始化后就不会被修改,所以不用担心它们被意外篡改。

保护性程序设计的黄金法则是:“按需知道”原则。尽量在函数内部解决该函数的任务,只共享那些需要共享的变量。