19-声明
声明
Wherever there is modularity there is the potential for misunderstanding: Hiding information implies a need to check communication. [^1]
目录
[TOC]
声明
零 前言
声明在 C 语言编程中起着核心的作用。通过声明变量和函数,可以检查程序潜在的错误以及把程序翻译成目标代码两方面为编译器提供至关重要的信息。
一 声明的语法
声明说明符 声明符; |
声明说明符(declaration specifier):描述声明的变量或函数的性质。声明符(declarator)给出了它们单独名字,并且提供了关于其他性质的额外信息。
声明说明符分为以下 3 大类:
- 存储类型。
auto
,static
,extern
和register
。在声明中最多可以出现一种存储类型。 - 类型限定符。C89 :
const
,volatile
。C99:restrict
。声明可以包含多个类型限定符。 - 类型说明符。关键字 int, double, char 等。类型说明符也包含结构,联合和枚举。
C99 中还有一种说明符,函数说明符,它只用于函数声明。这类说明符只有一个:inline
声明符包括标识符,可以组合*
, []
, ()
一起看一些说明这些规则的例子:
二 存储类型
这一部分集中讨论变量的存储类型。
块(block),表示函数体或者复合语句(可以理解为使用花括号的地方)。C99 中,选择语句和循环语句也被视为块,尽管本质上有些区别。
1. 变量的性质
C 程序中的每个变量都具有以下 3 个性质:
-
存储期限。变量的存储期限决定了为变量预留和内存被释放的时间。
-
自动存储期限:变量在所属块被执行时获得内存单元,并在程序终止时释放内存单元,从而导致变量失去值。
- 静态存储期限:程序运行期间占有同一个的存储单元,也就允许变量无限期地保留它的值。
-
作用域。变量的作用域是指可以引用变量的那部分程序文本。
- 块作用域:变量从声明的地方一直到所在块的末尾都是可见的。
- 文件作用域:变量从声明的地方一直到所在文件的末尾都是可见的。
-
链接。变量的链接确定了程序的不同部分可以共享此变量的范围。
- 外部链接:变量可以被程序中的几个(或全部)文件共享。
- 内部链接:变量只能属于单独的一个文件,但是此文件中的函数可以共享这个变量。
- 无连接:变量属于单独一个函数,而且根本不能被共享。
变量的默认存储期限,作用域和链接都依赖于变量声明的位置:
- 在块内声明的变量(如图)
- 在程序外层(任意块外部)声明的变量(如图)
2. auto
存储类型
auto
存储类型只对属于块的变量有效。auto 变量具有自动存储期限,块作用域,无链接。auto 存储类型几乎从来不用明确的指明,因为在块内部声明的变量,它是默认的。
3. static
存储类型
static
作用于块外部和块内部的变量效果不同。如图:
下面的例子中,函数 f1 和 f2 可以访问变量 i,但是其他文件中的函数不可以:
static int i; |
static
的此用法可以用来实现一种称为信息隐藏的技术。
块内声明的 static 变量在程序执行期间驻留在同意存储单元内。和每次程序离开所在块就会丢失值的自动变量不同, static 变量会无限期地保留值。static 变量具有以下性质:
- 块内声明的
static
变量只在程序执行前进行一次初始化,而auto
变量则会在每次出现时进行初始化。(当然,假设它有初始化式) - 含有 static 变量的函数全部调用共享这个 static 变量。
- 虽然函数不应该返回指向
auto
变量的指针,但是函数返回指向static
变量的指针是没有错误的。
声明函数中的一个变量为 static,这样做允许函数在“隐藏区域”的调用之间保留信息。隐藏区域是程序其他部分无法访问到的地方。思考下列函数:
char digit_to_hex_char(int digit){ |
每次调用 digit_to_hex_char 函数时,都会把字符串字面量"0123456789ABCDEF"赋值给数组 hex_chars[16] 来对其初始化。现在,把数组设为 static:
char digit_to_hex_char(int digit){ |
由于 static 变量只进行一次初始化,这样就改进了 digit_to_hex_char 函数的速度。
4. extern
存储类型
extern
存储类型可以使几个源文件可以共享同一个变量。前面我们也讲过它,这里不再重复。
下列声明给编译器提供的信息是 i 是 int 型变量:
extern int i; |
但是这样不会导致编译器为变量 i 分配存储单元。用 C 的术语来说,上述声明不是变量 i 的定义,他只是提示编译器需要访问定义在别处的变量。(可能稍后在同一文件中,更常见的是在另一个文件中。)变量在程序中可以有多次声明,但是定义只能有一次。
对变量进行初始化的 extern 声明是变量的定义。例如:
extern int i = 0; |
等效于:
int i = 0; |
extern 声明中的变量始终具有静态存储期限。变量的作用域依赖于声明的位置。如图:
确定 extern 型变量的链接有一定难度。如果变量在文件中较早的位置(任何函数外部)声明为 static ,那么它具有内部链接;否则(通常情况下),变量具有外部链接。
如何理解上面这段话呢,请看下面的程序:
int main(void) { |
编译运行这个程序,没有编译错误和链接错误。程序执行结束,n 会被增加 1 。
如果我们在另一个文件的函数中想访问 n:
file1.c
void f(); |
file2.c
void f() { |
编译运行这个程序,也没有编译错误和链接错误。程序执行结束,n 会被增加 1 。
这时,n 具有外部链接
我们对程序稍作修改:
int main(void) { |
编译运行这个程序,出现链接错误。我们需要将 n 的定义放在调用前:
static int n = 0; |
编译运行这个程序,没有编译错误和链接错误。程序执行结束,n 会被增加 1 。
这时,如果我们想在另一个文件中访问 n,可以实现吗?
file1.c
void f(); |
file2.c
void f() { |
编译运行这个程序,出现链接错误。
此时,n 具有内部链接。
5. register
存储类型
声明变量具有register
类型就要求编译器把变量存储在寄存器中,而不是像其他变量一样保留在内存中。(寄存器是驻留在 计算机 CPU 中的存储单元。存储在寄存器中的数据会比存储在普通内存中的数据访问和更新速度更快。指明变量的存储类型是 register 是一种请求,而不是命令。编译器可以选择把 register 类型的变量存储在内存中。
register 存储类型只对声明在块内的变量有效。register 变量具有和 auto 变量一样的存储期限,作用域和链接。但是,由于寄存器没有地址,所以对 register 变量取地址&
是非法的。即使编译器选择将其存储在内存中,这一限制仍然适用。
register 存储类型最好用于需要频繁进行访问或更新的变量。例如:
for(register int i = 0; i < N; i++){ |
现在 register 不像以前那么流行了。当今的编译器比早期的 C 语言编译器复杂多了,许多编译器可以自动确定哪些变量保存在寄存器中可以获得最大好处。
6. 函数的存储类型
函数声明或定义存储类型选项只有:extern
和 static
在函数声明开始处的单词extern
说明函数具有外部链接,也就是允许其他文件调用此函数(默认情况下);static
说明是内部链接,也就是说只有在定义函数的文件内调用此函数。思考下面的函数声明:
extern int f(int i);// same as: int f(int i); |
把 g 声明为 static 不能完全阻止在别的文件中对它的调用,通过函数指针进行间接调用仍然是可能的。
使用 static 的好处:
- 更容易维护。把函数声明为 static 存储类型保证在函数定义出现的文件之外函数 f 都是不可见的。因此,以后修改程序的人可以知道对函数 f 的变化不会影响其他文件中的函数。(另一个文件中如果传入了指向函数 f 的指针,它可能会收到函数 f 变化的影响。幸运的是,这种问题很容易通过检查定义函数 f 的文件来发现,因为传递 f 的函数一定也定义在此文件中。)
- 减少了“名字空间污染”。用于声明 static 的函数具有内部链接,所以可以在其他文件中重新使用这些函数名。虽然我们不太可能会为一些其他目的故意使用相同的函数名,但是在大规模程序中这种现象是难以避免的。
三 类型限定符
C 语言中一共有两种类型限定符:const
和volatile
(C99 中还有第三种:restrict
,它只用于指针。)因为 volatile
只用于底层编程中,我们会在后面的章节中进行讨论。
const
用来声明一些类似于变量的对象。但这些变量是“只读”的。程序可以访问 const 型对象的值,但是无法改变它的值。例如:
const int n = 10; |
把对象声明为 const 有以下几个好处:
- const 是文档格式。声明对象是 const 类型可以提示阅读程序的人,该对象的值不会改变。
- 编译器可以检查程序没有特意地试图改变该对象的值。
const 与 #define 之间的差异:
-
#define
指令为数值,字符或字符串常量创建名字;const
可用于产生任何类型的只读对象,包括数组,指针,结构或联合。 -
const 对象遵循与变量相同的作用域规则;#define 创建的常量不受这些规则的限制。特别是,不能用 #define 创建具有块作用域的常量。
-
和宏的值不同,const 对象的值可以在调试器中看到。
-
不同于宏,const 对象的值不可用于常量表达式。比如:
const int n = 10;
int a[n]; //wrong在 C99 中,如果 a 具有自动存储期限,那么这个例子是合法的——它会被视为变长数组;但是如果 a 具有静态存储期限,那么这个例子是不合法的。
-
对 const 对象应用取地址运算符
&
是合法的,因为它有地址。宏没有地址。
四 声明符
声明符包含标识符,符号(*
,[]
,()
)
1. 解释复杂声明
下面这个声明符是什么意思呢?
int *(*x[10])(void); |
理解声明符的规则:
- 从内向外读声明符。定位声明的标识符,并且从此处开始解释声明。
- 在做选择时,使用使
[]
和()
优先于*
。
先看一些简单的声明:
int *ap[10]; |
ap 是标识符,[] 优先级高于 *,所以 ap 是指针数组。
float *fp(float); |
fp 是标识符,() 优先于 *,所以 fp 是返回指针的函数。
void (*pf)(int); |
由于 *pf 包含在圆括号内,所以 pf 一定是一个函数指针,此函数返回值类型为 void ,参数为 int 类型。
再来看前面的这个声明:
int *(*x[10])(void); |
找到 x,x[10]
表示数组,*x[10]
表示指针数组,(*[x10])
表示这是一个元素都是指向函数的指针数组,此函数返回值类型是 int*
,没有参数。
五 初始化式
初始化式我们并不陌生,现在我们来看一些控制初始化式的额外规则:
-
具有静态存储期限的变量的初始化式必须是常量:
static int i = LAST - FIRST + 1; -
如果变量具有自动存储期限,那么它的初始化式不需要式常量:
int f(int i){
int last = n - 1;
} -
包含在花括号中的数组,结构和联合的初始化式必须只包含常量表达式,允许有变量或函数调用:
int powers[5] = {1, N, N * N, N * N * N, N * N * N * N};C99 中,仅当变量具有静态存储期限时,这一限制才生效。
-
自动类型的结构或联合的初始化式可以是另一个结构或联合:
struct part part2 = part1;
初始化式不一定非要是变量。比如:
struct part part2 = *p;//p 指向 struct part 类型变量
struct part part2 = f(part1); // f 返回值为 struct part 类型
1. 未初始化的变量
变量的默认初始化依赖于变量的存储类型:
- 具有自动存储期限的变量没有默认初始值。不能预测自动变量的初始值,每次变量变为有效时只可能不同。
- 具有静态存储期限的变量默认情况下为 0 。整型变量初始化为 0,符点变量初始化为 0.0,指针初始化为 NULL(空指针)。
出于书写风格和可读性的考虑,最好为静态类型的变量提供初始化式。
六 内联函数(C99)
略
[^1]: 模块是误解之源;信息隐藏预示沟通的必要。Epigrams on Programming 编程警句
参考资料:《C语言程序设计:现代方法》