C 语言声明:问与答

问:从 C99 开始,为什么把选择语句和重复语句(以及它们的“内部”语句)视为块?

答:这条奇怪的规则源于把复合字面量(C 语言函数中的形式参数和实际参数C 语言结构类型简介)用于选择语句和重复语句时出现的一个问题。该问题与复合字面量的存储期有关,所以我们先花点时间讨论一下这个问题。

C99 及之后的标准指出,如果复合字面量出现在函数体之外,那么复合字面量所表示的对象具有静态存储期。否则,它具有自动存储期,因而对象所占有的内存会在复合字面量所在块的末尾释放。考虑下面的函数,该函数返回使用复合字面量创建的 point 结构:

1
2
3
4
struct point create_point(int x, int y)
{
  return (struct point) {x, y};
}

这个函数可以正确地工作,因为复合字面量创建的对象会在函数返回时被复制。原始的对象将不复存在,但副本会保留。现在假设我们对函数进行微小的改动:

1
2
3
4
struct point *create_point(int x, int y)
{
  return &(struct point) {x, y};
}

这一版本的 create_point 函数会导致未定义的行为,因为它返回的指针所指向的对象具有自动存储期,函数返回后该对象就不复存在。

现在回到开始时提到的问题:为什么把选择语句和重复语句视为块?考虑下面的示例 1:

1
2
3
4
5
6
7
8
9
/* Example 1 - if statement without braces */

double *coefficients, value;

if (polynomial_selected == 1)
  coefficients = (double[3]) {1.5, -3.0, 6.0};
else
  coefficients = (double[3]) {4.5, 1.0, -3.5};
value = evaluate_polynomial(coefficients);

这个程序片段显然能按需要的方式工作(但是请继续阅读)。coefficients 将指向由复合字面量创建的两个对象之一,并且该对象在调用 evaluate_polynomial 时仍然存在。现在考虑一下示例 2,如果在内部语句(if 语句控制的语句)两边加上花括号,会有什么不同:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/* Example 2 - if statement with braces */

double *coefficients, value;

if (polynomial_selected == 1) {
  coefficients = (double[3]) {1.5, -3.0, 6.0};
} else {
  coefficients = (double[3]) {4.5, 1.0, -3.5};
}
value = evaluate_polynomial(coefficients);

现在我们遇到问题了。每个复合字面量会创建一个对象,但该对象只存在于包含相应语句的花括号所形成的块内。调用 evaluate_polynomial 时,coefficients 指向一个不存在的对象,从而导致未定义的行为。

C99 的创立者对这种现象很不满意,因为程序员不可能预料到,在 if 语句中简单地增加花括号就会导致未定义的行为。为了避免这一问题,他们决定始终把内部语句视为块。这样一来,示例 1 和示例 2 就等价了,都会导致未定义的行为。

当复合字面量是选择语句或重复语句的控制表达式的一部分时,类似的问题也会发生。因此,我们把整个选择语句和重复语句也都看作块(就好像有一对不可见的花括号包裹在整个语句外面一样)。因此,带有 else 子句的 if 语句包含三个块:两个内部语句分别是一个块,整个 if 语句又是一个块。

问:你曾说过,具有自动存储期的变量在所在块开始执行时分配内存空间。这对于 C99 及之后的变长数组是否也成立?

答:不成立。变长数组的空间不会在所在块开始执行时就分配,因为那时候还不知道数组的长度。事实上,在块的执行到达变长数组声明时才会为其分配空间。从这一方面说,变长数组不同于其他所有的自动变量。

问:“作用域”和“链接”之间的区别到底是什么?

答:作用域是为编译器服务的,链接是为链接器服务的。编译器用标识符的作用域来确定在文件的给定位置访问标识符是否合法。当编译器把源文件翻译成目标代码时,它会注意到具有外部链接的名字,并最终把这些名字存储到目标文件内的一个表中。因此,链接器可以访问到具有外部链接的名字,而内部链接的名字或无链接的名字对链接器而言是不可见的。

问:我无法理解一个名字具有块作用域但又有着外部链接。可否详细解释一下?

答:当然可以。假设某个源文件定义了变量 i

1
int i;

假设变量 i 的定义放在了任意函数之外,所以默认情况下它具有外部链接。在另一个文件中,有一个函数 f 需要访问变量 i,所以 f 的函数体把 i 声明为 extern

1
2
3
4
5
void f(void)
{
  extern int i;
  ...
}

在第一个文件中,变量 i 具有文件作用域。但在函数 f 内,i 具有块作用域。如果除函数 f 以外的其他函数需要访问变量 i,那么它们将需要单独声明 i。(或者简单地把变量 i 的声明移到函数 f 外,从而使其具有文件作用域。)在整个过程中会混淆的就是,每次声明或定义 i 都会建立不同的作用域,有时是文件作用域,有时是块作用域。

问:为什么不能把 const 对象用在常量表达式中呢?“constant”不就是常量吗?

答:在 C 语言中,const 表示“只读”而不是“常量”。下面用几个例子说明为什么 const 对象不能用于常量表达式。

首先,const 对象只在它的生命期内为常量,而不是在程序的整个执行期内。假设在函数体内声明了一个 const 对象:

1
2
3
4
5
void f(int n)
{
  const int m = n / 2;
  ...
}

当调用函数 f 时,m 将被初始化为 n/2m 的值在函数 f 返回之前都保持不变。当再次调用函数 f 时,m 可能会得到不同的值。这就是问题出现的地方。假设 m 出现在 switch 语句中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void f(int n)
{
  const int m = n / 2;
  ...
  switch (...) {
    ...
    case m: ...   /*** WRONG ***/
    ...
  }
  ...
}

那么直到函数 f 调用之前 m 的值都是未知的,这违反了 C 语言的规则——分支标号的值必须是常量表达式。

接下来看看声明在块外部的 const 对象。这些对象具有外部链接,并且可以在文件之间共享。如果 C 语言允许在常量表达式中使用 const 对象,就很容易遇到下列情况:

1
2
extern const int n;
int a[n];   /*** WRONG ***/

n 可能在其他文件中定义,这使编译器无法确定数组 a 的长度。(假设 a 是外部变量,所以它不可能是变长数组。)

如果这样还不能让你信服,考虑下面的情况:如果一个 const 对象也用 volatile 类型限定符声明,它的值可能在程序执行过程中的任何时间发生改变。下面是 C 标准中的一个例子:

1
extern const volatile int real_time_clock;

程序可能不会改变变量 real_time_clock 的值(因为其声明为 const),但可以通过其他的某种机制修改它的值(因其被声明为 volatile)。

问:为什么声明符的语法如此古怪?

答:声明试图进行模拟使用。指针声明符的格式为 *p,这种格式和稍后将用于 p 的间接寻址运算符方式相匹配。数组声明符的格式为 a[...],这种格式和数组稍后的取下标方式相匹配。函数声明符的格式为 f(...),这种格式和函数调用的语法相匹配。这种推理甚至可以扩展到最复杂的声明符上。请思考一下 C 语言指向函数的指针 中的数组 file_cmd,此数组的元素都是指向函数的指针。数组 file_cmd 的声明符格式为

1
(*file_cmd[])(void)

而这些函数的调用格式为

1
(*file_cmd[n])()

其中圆括号、方括号和 * 的位置都一样。

请参阅

(完)

comments powered by Disqus