【预备知识】
1)C 语言 - 存储时期
2)C 语言 - 链接属性
3)C 语言 - 作用域
1. 分类:
· C 语言为变量提供了 5 中不同的存储模型(即存储类)。
(此外还有基于指针的第 6 种存储模型,不在本篇笔记范围内,后续补充)。
· C 语言提供了以下 5 个存储类说明符(storage-class-specifier):
auto
register
static
extern
typedef
由于 typedef 和其他四个的语义不同,所以不在这里进行讨论。
2. 自动变量 auto
自动变量具有代码块作用域、自动存储时期和空链接。
1) 默认情况下,在代码块或函数头部定义的任意变量都是自动变量。
2)可以使用关键字 auto 对块作用域变量进行显式声明。
// ivar, ilen 都是自动变量
void func (int ivar) {
auto int ilen = 5;
// to do
return 0;
}
· 注意不带 ‘{}’ 的代码块,比如 if 或者 while 的子句。
· 注意 auto 只能用于块作用域变量的声明。虽然函数头部定义的变量也是自动变量,但不可以使用 auto 来显式声明。
2)auto 关键字的作用:
· 显式声明该变量为自动存储模型;
· 用来有意覆盖一个外部函数定义;
3)对于自动变量,只能在变量定义所在的代码块(包含其嵌套的代码块)通过名字访问该变量。
【注意】
a. 可以用参数向其他函数传送自动变量的值和地址,但那是间接的方式。
b. 另一个代码块(包括其嵌套的代码块)可以存在同名变量,但那是存储在不同内存位置的另一个独立变量。
4)对于嵌套代码块中的同名变量,外层代码块的变量会被内层代码块中的同名变量覆盖(程序离开内层代码块时就会恢复外层代码块中的同名变量)。
5)自动变量的优点:在程序离开当前代码块时,自动变量消失了,其占用的内存可以用来做别的事情。
6)自动变量需要显式初始化!切记!
一些编辑器会默认初始化变量,但最好自己来初始化!给变量赋初值是一个好习惯。
7)auto 关键字的实际用途?
在我查阅的大部分资料里,可以说,auto 基本是没有什么用的,因为 auto 声明了一个变量是自动变量,且 auto 只能用在代码块作用域内,但是别忘了,代码块作用域内定义的变量默认就是自动变量……这样一来,auto 的显式声明就显得十分鸡肋。
那为什么 C 语言还保留了 auto 关键字呢?(C++11 开始弃用了 auto 作为自动存储期的语义)主要是为了向后兼容,C 语言之前的代码使用 auto 来声明变量,在过去有很多代码需要移植到新的 C 语言中,在这种情况下,将关键字 auto 添加到语言中是很有帮助的。
3. 寄存器变量 register
1)通常,寄存器变量存储在速度最快的可用内存中(通常是 CPU 寄存器),目的是为了比普通变量更快地被访问和操作。
2)使用 register 声明符声明寄存器变量,该声明符只能用于块作用域的对象和函数原型作用域的对象。
3)因为寄存器变量多是存放在一个寄存器中,因此无法对一个寄存器变量进行一元取值运算(&),也不能将声明为 register 的数组转换为指针。
4)register 仅仅是一个请求而不是指令,如果没有可用的寄存器或者高速内存,那么编译器会把它当自动变量处理,即便如此依然存在 3)中的限制。
注意,虽然使用 register 修饰变量理论上可以提高效率,但不应该在程序中大量使用 register,毕竟寄存器的数量是有限的,而且将变量存储在寄存器中也会消耗一定的处理时间,反而会降低程序的运行效率。
5)使用 register 声明的类型是有限的(例如,处理器可能没有足够大的寄存器来容纳 double)。
6)register 关键字在实际开发中的用途?
很遗憾,在实际的开发中很少会用到 register 这个关键字,但这并不是因为它和 auto 一样鸡肋。早期的编译器不太善于决定是否要将机器寄存器应用于一个变量,因此 C 语言提供了 register 以便开发人员手动执行这个操作。随着技术的改进编译器变得更聪明了,在决定变量存储方面往往比开发人员做得更好(很多时候开发人员的决定只会让程序变得更慢),这就是为什么很多编译器在涉及存储时总是忽略 register。
不过,使用 register 声明的变量无法进行取址操作,可以使用 register 来确保变量不会被地址访问导致变量在期望的范围之外被更改。(奇怪的思路,不建议)
7)关于寄存器的补充。
a. 寄存器存在的原因:CPU 是负责进行计算的硬件单元,为了方便运算,需要先把数据从内存读取到 CPU 内,这就要求 CPU 具有一定的数据临时存储能力,但 CPU 并不是在需要开始计算时才把特定数据读进 CPU 里面,那太慢了。 因此现代 CPU 内都集成了一组叫做寄存器的硬件,用于保存临时数据。
b. 寄存器存在的本质: 在硬件层面上提高计算机的运算效率(因为不需要反复从内存里读取数据)。
4. 具有代码块作用域的静态变量
1)静态指的是变量的地址是固定的,变量的值是可以修改的。
2)使用 static 声明代码块作用域变量为具有代码块作用域的静态。
【注】对于常量字符串,使用 static 是有用的,因为它减轻了频繁初始化经常调用的函数的开销。
3)具有代码块作用域的静态变量具有全局生存期(在程序载入时创建,在程序结束时销毁),但只在定义的代码块内可见。
4)作用:保存程序运行时的中间结果。
void checkNum(int ivar) {
int iflag = 0;
static int icnt = 0;
if (ivar == iflag) {
printf("Found %d %d time(s).\n", iflag, ++icnt);
}
上面例子中,“int iflag = 0;” 是 checkNum() 的一部分,该语句在每次调用函数时都会执行。
但语句 “static int icnt = 0;” 实际上并不是函数 checkNum() 的一部分,该语句放在 checkNum() 函数中只是为了告诉编译器,icnt 的可见范围仅限于函数 checkNum() 的函数体内。并且它不是在运行时执行的语句(可以在调试的时候进行测试,程序会跳过这一语句)。
程序离开了 checkNum() 后并不会销毁 icnt 变量,只是这个时候变量 icnt 在外层不可见,当重新回到该函数时,可以看到该变量保持了上次离开时的状态(值和地址)。
5)不应该在结构体中声明静态变量。
C 语言要求结构体中的所有成员都放置在内存中的同一地方,即结构成员的内存分配应该是连续的,因为 struct 成员的值是通过计算元素从所在结构体的起始地址的偏移量来获取的。如果确实有需要,可以使整个结构保持静态。
(【注意】可以在函数内部(堆栈段 stack segment)声明一个 struct,或动态分配内存(堆段 heap segment),甚至可以是全局的(BSS或数据段 data segment),只要能保证内存连续。)
5. 具有外部链接的静态变量
具有外部链接的静态变量,也称为外部存储类(external storage class),外部变量(external variable)。
1)外部存储类具有文件作用域、外部链接和静态存储时期。
2)外部存储类声明在所有函数之外。
3)可以在使用外部变量的函数中通过使用 extern 关键字再次声明它,这个声明是可选的。
// assist.c
int ierrupt; // 定义在所有函数之外的变量;
double darr[100]; // 外部定义的数组;
char cflag = 'Q';
int func() {
extern int ierrupt; // 可选的声明
extern double darr[]; // 可选的声明
char cflag = 'q'; // 这里的 cflag 覆盖了外部的 cflag
// to do
return 0;
}
注意,这里 “可选的声明” 的意思是:整个声明语句是可选的。而不是声明语句中的 extern 关键字是可选的。如果在函数中声明了和外部变量同名的变量,且漏掉了 extern,那么将创建一个独立的自动变量!(比如上面例子中的 cflag)
注意上面的数组 darr,不必在可选的声明中指明数组大小,因为第一次声明的时候已经提供了这一信息。
上面例子中的两句可选声明完全可以省略并且不会影响程序的逻辑。
一个建议是,在程序中(尤其是复杂的程序中)写上这句可选的声明,以提高程序的可读性。
另外,如果变量定义在别的文件中,那么必须使用 extern 来声明该变量,这个声明是必要的。
// main.c
extern char cflag; // 必要的声明,
// 因为 cflag 定义在 assist.c 中
int main() {
// to do
return 0;
}
4)不同于自动变量,外部变量在没有显示初始化的情况下,会自动赋初值 0。
a. 还是那句话,给变量赋初值是一个好习惯。
b. 一个外部变量只可以进行一次初始化,而且一定是在变量被定义的时候被初始化。(区别于赋值)
// 这个语句是错误的。
// 因为 extern 说明这是一个引用声明,
// 而非定义声明!
extern char c_permis = 'Y'; // error!!
5)只能用常量表达式来初始化文件作用域变量。
常量表达式是指在编译时可以完全确定其值的表达式。
6)外部变量可以被程序的任一文件中所包含的函数使用。
外部变量不需要使用参数和指针就能完成值的传递,使用上确实方便,但这是有代价的,需要谨慎对待。因为它太不明显,可能会在不知不觉中被偷偷修改,而又很难排查。
一个建议:不要随意使用外部变量。
6. 具有内部链接的静态变量
1)具有内部链接的静态变量具有静态存储时期、文件作用域和内部链接。
2)使用 static 关键字在所有函数外部对变量进行定义。
a. 区别于上面讨论的具有外部链接的静态变量,具有内部链接的静态变量在定义的时候多了一个 static
b . extern 关键字是在引用声明时使用而不是定义时使用!!
c. 可以在函数中使用 extern 来再次声明任何具有文件作用域的变量,但这并不改变链接!链接是在定义的时候就决定的!
3)只可以被与它在同一个文件中的函数使用。