18 声明

说明:通过声明变量和函数,可以再检查程序潜在的错误以及把程序翻译成目标代码两方面为编译器提供至关重要的信息。

18.1 声明的语法

语法:声明说明符 声明符;

18.1.1 声明说明符

分类:分3类(可组合)

分类 包括 位置 可多个
存储类型 auto static extern register 声明中的首要位置
类型限定符 const volatile 存储类型的后边
类型说明符 void char short int long float double signed unsigned 结构 联合 枚举 typedef创建的类型名 存储类型的后边 是,出现顺序无限制

注意:类型限定符和类型说明符的顺序没有限制,习惯上前者在前。

18.1.2 声明符

说明:一次可以声明多个声明符,彼此用,隔开。

分类 描述 是否可跟初始化式
简单变量名 标识符
数组名 后边跟随[]的标识符
指针名 前边放置*的标识符
函数名 后边跟随()的标识符

18.1.3 举例

同时声明多个声明符
Alt text

带有初始化式
Alt text

同时使用多种类型说明符
Alt text

函数声明
Alt text

18.2 存储类型

说明:可以用于变量、较小范围的函数和形式参数的说明。根据声明位置的不同,在不用存储类型修饰的情况下,变量具有默认的存储类型,当默认的性质无法满足要求时,可以通过指定明确的存储类型来改变变量的性质。
关键字:auto static extern register
块(block):表示函数体(大括号闭合的部分)或块语句(包含声明的复合语句)。

18.2.1 变量的特性

说明:变量的3个性质

性质 说明 分类
存储期限 为变量预留和释放内存的时间 自动存储期限、静态存储期限
作用域 指引用变量的那部分程序文件 块作用域、文件作用域
链接 程序的不同部分可以共享此变量的范围 外部链接、内部链接、无链接

声明位置决定的存储特性:对许多变量而言,默认的存储期限、作用域和链接就可以满足要求。

声明位置 性质
块内部 自动存储期限、块作用域、无链接
程序的最外层 静态存储期限、文件作用域、外部链接

1. 存储期限

存储期限 描述 备注
自动存储期限 在所属块被执行时获得内存单元,并在块终止时释放内存单元(变量失去值)
静态存储期限 在程序运行期间占有同一块存储单元 可以允许变量无限期地保留它的值

2. 作用域

作用域 描述 备注
块作用域 变量从声明的地方一直到闭合块的末尾都是可见的
文件作用域 从声明的地方一直到闭合文件的末尾都时可见的

3. 链接

链接 描述 备注
外部链接 可以被程序中的几个(或者全部)文件共享
内部链接 只能属于单独一个文件
无链接 属于单独一个函数,而且根本不能被共享
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 静态存储期限
* 文件作用域
* 外部链接
*/

int i;

void f(void) {
/**
* 自动存储期限
* 块作用域
* 无链接
*/

int j;
}

18.2.2 auto存储类型

关键字:auto
注意:auto存储类型几乎从来不用明确地指明,因为对于在块内部声明的变量,它是默认的。

性质
自动存储期限
块作用域
无链接

18.2.3 static存储类型

关键字:static
可以修饰:全部变量
特性:修饰块外部声明的变量和块内部声明的变量会有不同的效果

  • 块外部:static使变量由外部链接变为内部链接(即信息隐藏,因为本质上隐藏了它所在声明文件内的变量,只有出现在同一文件中的函数可以看到此变量)
  • 块内部:static 使变量的存储期限从自动变成静态的(无限期保留值)
1. 块内的`static` 型变量只在程序执行前进行一次初始化,而`auto`型变量则会在每次变成有效时进行初始化
2. 每次函数进行递归调用时,它都会获得一组新的`auto`型变量的集合。`static`修饰的变量则会被共用
3. 函数不能返回`auto`型变量的指针,但可以返回指向`static`型变量的指针

用途:

  1. 提升性能:修饰块内部变量,避免函数每次调用都对变量进行初始化
  2. 信息隐藏:修饰块外部变量,用于在“隐藏”区域内的调用之间保留信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 静态存储期限
// 文件作用域
// 内部链接
static int i;

void f(void) {
// 静态存储期限
// 块作用域
// 无链接
static int j;
}

/**
* 将10进制的数字转换为16进制的
* param {int} digit 10进制数字(16以内)
* return {char} 表示相应16进制数字的字符
*/

char digit_to_hex_char (int digit) {
// 无论函数被调用多少次,只会初始化一次
const chat hex_chars[16] = "0123456789ABCDEF";
return hex_chars[digit];
}

18.2.4 extern存储类型

说明:只声明变量(不初始化)
关键字:extern
用途:多文件共享同一个变量
性质:

  • 存储期限:使变量具有静态存储期限(即使原本是在块中声明的变量)
  • 作用域:extern不影响变量作用域(块内声明为块作用域,否则是文件作用域)
  • 链接: 通常情况为外部链接;当同时被static修饰且声明位置为任何函数外部时为内部链接

注意:extern修饰的变量仍然可以同时初始化(但就没有了用extern的意义)

1
extern int i = 0; //等价于int i = 0;
1
2
3
4
5
6
7
8
9
10
11
// 静态存储期限
// 文件作用域
// 链接?
extern int i;

void f(void) {
// 静态存储期限
// 块作用域
// 链接?
extern int j;
}

18.2.5 register存储类型

关键字:register
用途:使变量存储在寄存器而不是内存中,提高访问和更新速度。
寄存器:使驻留在计算机CPU中的存储单元。在传统计算机架构中,存储在寄存器中的数据会比存储在普通内存中的数据访问和更新速度更快。
特点:

  • 具有和auto型变量一样的存储期限、作用域和链接
  • register型变量使用取地址运动符&是非法的(因为寄存器没有地址)

注意:随着编译器变得更加复杂和高效,一些编译器可以自动决定变量保存在寄存器中还是内存中来达到最优性能。因此register的使用不再流行了。

1
2
3
4
5
6
7
8
9
// for语句的循环控制变量应用`register`是一个很好的选择
int sun_array (int a[], int n) {
register int i;
int sum = 0;
for (i = 0; i < n; i++) {
sum += a[i];
}
return sum;
}

18.2.6 函数的存储类型

说明:函数只能用staticextern修饰

不修饰或extern static
静态存储期限 静态存储期限
文件作用域 文件作用域
外部链接 内部链接

技巧: 当声明不打算被其他文件调用的任意函数时,建议使用static修饰

1. *更容易维护:*稍后修改文件的人可以知道对被`static`修饰的函数的修改一般不会影响其他文件中的函数(即便该函数所在文件中其它函数将指向该函数的指针传递了出去,也可以在当前文件发现)
2. *减少“命名空间污染”:*可以在其它文件中使用相同的名字命名函数而不会发生冲突

注意:使用extern画蛇添足,不必使用但也无害。

扩展:函数形参的存储类型(只能用register修饰)

默认(等同于块中的auto型变量)
自动存储期限
块作用域
无链接

18.3 类型限定符

分类 说明
const 声明只读类型(也称为常量)
volatile 20.3节

const介绍

用途:定义常量

  1. 提示阅读程序的人,对象的值不能改变
  2. 让编译器检查防止程序改变对象的值
  3. 可能的话(特别是嵌入式系统),编译器可以用让const修饰的变量存储到ROM(只读内存)中

只读(const)宏(#define):如何恰当使用两者?
技巧:建议对表示数字(比如数组维数)或字符的常量使用#define;const常用于保护存储在数组中的常量数据。

区别 只读(const) 宏(#define)
类型 数字常量、字符常量、字符串常量 任何类型(包括常量数组、常量指针、常量结构、常量联合)
作用域 遵守作用域规则 不遵守,不能产生具有块作用域的常量
能否在调试器观察 不能
能否用于常量表达式 不能,比如数组大小(常量表达式)不能用const定义的常量
1
2
const int n = 10;
int a[n]; // 使用错误,只读类型不能用于常量表达式

18.4 声明符

声明符组成

元素 说明 必须
标识符 声明的变量或函数的名字
* 声明指针或对指针进行索引
[] 声明数组
() 声明函数或提高优先级

简单的声明规则

符号(用于声明) 简单规则
* 用*开头的声明符表示指针
[] 用[]结尾的声明符表示数组
() 用()结尾的声明符表示函数

不合法的声明符

  1. 函数不能返回数组
  2. 函数不能返回函数
  3. 数组不能是函数型的
1
2
3
4
5
6
7
8
9
10
11
12
13
// 最简单的情况:标识符就是声明符
int i;

// 指针
int *p;

// 数组
int a[10]; //extern int a[];

// 函数
int abs(); // 空括号形式使得编译器不检查函数调用的参数情况,不推荐
int swap(void); // 明确告诉编译器没有参数,编译器会检查参数情况
int find_largest(int [], int); // 允许在函数声明中忽略形式参数的名字

18.4.1 解释复杂声明

规则:无论多么复杂的声明都可以被下面的两条规则解释

  1. 从标识符开始,由内往外解读
  2. 当符号位于同一层级(一左一右)时,确定声明的是什么东西的优先级是:[] > () > *(数组 > 函数 > 指针)

    技巧:作为上面规则的补充,符号()*有存在歧义的时候,下面是甄别的依据

    1. ():当位于声明符最右端时代表“函数”;否则是用来进行指针索引的,像这样:(*其它部分)
    2. *(*其它部分)代表指针索引,否则是定义函数的返回值类型为指针(如果是函数)或者是指针类型的定义。
1
2
3
4
5
6
7
8
9
10
11
// 根据规则2,ap是数组(元素是int *型的指针)
int *ap[10];

// 根据规则2,fp是函数(返回值类型为float *)
float *fp(float);

// 通过技巧1,(*pf)是函数,所以pf是指向函数的指针(函数的返回值为void)
void (*pf)(int);

// 根据规则2,(*x[10])是函数(返回值为int *, 参数为void),则x[10]为函数指针,所以x是存储函数指针的数组。
int *(*x[10])(void);

18.4.2 使用类型定义来简化声明

说明:利用一组类型定义拆分复杂的声明

1
2
3
4
5
// 等价:int *(*x[10])(void);
typedef int *Fcn(void);
typedef Fcn *Fcn_ptr;
typedef Fcn_ptr Fcn_ptr_array[10];
Fcn_ptr_array x;

18.5 初始化式

说明:可以在声明符的后边书写=, 后边再跟上初始化式(不同于赋值,赋值只要是合法的右值即可,而初始化式存在诸多限制)。
变量的默认值(声明时不给初始化式):变量的初始化值依赖于变量的存储期限

  • 自动存储期限:没有默认的初始值,不能预测初始值
  • 静态存储期限:基于类型初始化为“零”(整型初始化为0,浮点数初始化为0.0,指针初始化为空指针)

技巧:推荐为静态类型的变量提供初始化式,便于阅读者确定变量的值,也便于查看初始化赋值的位置。

1
2
3
4
5
6
7
8
9
10
11
// 简单变量的初始化:一个变量,与变量类型一样的表达式
int i = 5 / 2;

// 如果类型不匹配,采用和赋值运算相同的规则进行自动类型转换(7.5节)
int j = 5.5;

// 指针变量的初始化:必须是具有和变量相同类型或void *类型的指针表达式
int *p = &i;

// 数组、结构或联合的初始化式通常是一串封闭在大括号内的值
int a[5] = {1, 2, 3, 4, 5};

额外规则

  • 具有静态存储期限的变量:初始化式必须是常量
1
2
3
#define FIRST 1
#define LAST 100
static int i = LAST - FIRST + 1;
  • 具有自动存储期限的变量:初始化式不必要是常量
1
2
3
4
int f (int n) {
int last = n - 1;
...
}
  • 用大括号闭合的数组、结构或联合的初始化式必须只能包含常量表达式,不允许有变量或函数调用
1
2
#define N 2
int powers[3] = {1, N, N*N, N*N*N}; // N是常量,所以合法
  • 自动类型的结构或联合:初始化式可以是另外一个结构或联合
1
2
3
void g (struct complex c1) {
struct complex c2 = c1;
}