const extern static 终极指南
不管是从事哪种语言的开发工作,const extern static 这三个关键字的用法和原理都是我们必须明白的。本文将对此做出非常详细的讲解。
const
const是这三个中最简单的一个关键字。主要用于声明常量。常量和变量的样子没什么两样,只是前者的值是不可修改的。
举个例子:
int const a;
const int a;
这两条语句都把a声明为一个整数,它的值不能被修改,在这里,这两条语句是等价的,只是表现形式不同。
那么问题来了,既然a的值是不能被修改的,那我应该如何让a在一开始就有一个值呢?答案分两种情况:
-
在声明时就赋值
int const a = 15;
-
函数中被声明为const的形参在函数被调用时会得到形参的值。
int func(int const a) { return a + 10; } int b = func(10); printf("%i",b);
打印结果为20,可以看出,a在调用func的时候被赋值为10;
当const修饰指针变量的时候,情况就变得更加有趣了,因为指针变量有两样东西都有可能成为常量,指针变量 和 它指向的实体。我们再看下边的这个例子:
首先我们先声明一个普通的指针:
int *p;
我们在int和*之间加上const,就变成了下边的代码:
int const *p;
那么现在p就变成了一个指向整型常量的指针了,你可以修改指针p的值,但不能修改p指向的值。我们举个例子:
int a = 10;
int b = 20;
int const *p = &a;
int *p1 = &b;
p = p1;
printf("%i",*p);
上边的代码中,我们让p指向a,p1指向b,然后修改指针p,最后打印结果为20,这说明我们可以修改指针p的值。再看下边的代码:
int a = 10;
int b = 20;
int const *p = &a;
int *p1 = &b;
// 下边的代码会报错
*p = 60;
printf("%i",*p);
好了,通过上边的代码相信大家应该能够明白const放在int和 * 之间的作用了,那么我们现在把const放在 *和p之间会发生什么呢?
int * const p;
此时,指针p为一个指向整型的常量指针,也就是说不能修改指针的值,可以修改它指向的值。我们依然举例说明:
int a = 10;
int b = 20;
int * const p = &a;
*p = 50;
printf("%i",a);
由上边代码可以看出,我们通过p修改了a的值,同样,打印结果为50。我们尝试修改指针p:
int a = 10;
int b = 20;
int * const p = &a;
*p = 50;
int *p1 = &b;
// 下边代码会报错
p = p1;
printf("%i",a);
注意:如果把代码写成这样int const * const p;
.无论是指针还是它指向的整型都是常量,不可修改。
使用说明: 当你声明变量时,如果变量的值不会被修改,你应该在声明中使用const关键字。这种做法不仅使你的意图在其他阅读你的程序的人面前得到更清晰的展现,而且当这个值被意外修改时,编译器能够发现这个问题。
作用域
可能很多同学会认为 作用域 很简单,其实不然,很多优秀的代码都有一个共同点,就是合理的利用了标识符的作用域。同样,这也与变量的存储属性有关系,后边我们会解释到的。
编译器能够确认四种不同的作用域,他们分别为:
- 代码块作用域(block scope)
- 文件作用域(file scope)
- 原型作用域(prototype scope)
- 函数作用域(function scope)
1.代码块作用域(block scope)
位于一对花括号之间的所有语句成为一个代码块。任何在代码块的开始位置声明的标识符都具有代码块作用域,表示他们可以被这个代码中的所有语句访问。
上图中6,7,9,10的变量都具有代码块作用域。函数定义的形参(上图中的5)在函数体内部也具有代码块作用域。
当代码块处于嵌套转态时,声明于内层代码块的标识符的作用域到达该代码块的尾部便告终止。然而,如果内层代码块有一个标识符的名字与外层代码块的一个标识符同名,内层的那个标识符就将隐藏外层的标识符-----外层的那个标识符无法在内层代码中通过名字访问。
上图中声明9的f和声明6的f是不同的变量,在声明9的代码快中,无法通过名字f方位声明6的f。
这里说一些有意思的事,由于两个代码块的变量不可能同时存在,所以编译器可以把他们放到同一个内存地址中。
注意: 我们应该避免在嵌套的代码块中使用相同的变量名,这样会在程序的调试和维护期间引起混淆。
2.文件作用域(file scope)
任何在所有代码块之外声明的标识符都具有文件作用域(file scope),它表示这些标识符从他们的声明之处直到它所在的源文件结尾处都是可以访问的。
在上图中的1和2就具有这样的文件作用域,上图中的4是一个函数,由于函数名本身不属于任何代码块,所以4也具有文件作用域。
注意: 在头文件中通过#include或者#import导入的文件,就好像写到该头文件中一样,他们的作用域并不会局限于他们自身的文件中。
3.原型作用域(prototype scope)
原型作用域(prototype scope)可能不太好理解,我们看上图中的3和8,这两个函数被声明了。也就是说只在声明函数的时候,这个形参可有可无,名字和函数定义的形参可以相同,也可以不同,唯一能冲突的地方是,不能再同一函数声明中不止一次的使用同一名字。
4.函数作用域(function scope)
该作用域只适用于语句标签,语句标签用于goto语句,一个函数中的所有语句标签必须是唯一的。举个例子:
#include <stdio.h>
int main(int argc, char *argv[])
{
int i=1;
tt:printf("%d
",i++);
if (i<10)
goto tt;
return 0;
}
链接属性
在讲解extern static之前,还必须了解链接属性这个概念,出现了链接这两个字,说明跟代码的编译有关系。
我们知道,当组成一个程序的各个源文件分别被编译后,所有的目标文件以及那些从一个或多个函数库引用的函数链接在一起,形成可执行文件。然而,会有这样一种情况,如果相同的标识符,比如说int a,出现在几个不同的源文件中时, 该怎么处理这种情况呢?
我们为每个标识符加一个属性,这个属性告诉编译器如何处理不同源文件中的标识符,那么这个属性就是链接属性。链接属性一共有三种:
- external(外部) 属于external属性的标识符不论声明多少次,位于几个源文件,都表示同一实体。
- internal(内部) 属于internal属性的标识符在同一个源文件内的所有声明都指向同一个实体,但不同源文件的多个声明分属不同的实体。
- none(无) 没有链接属性的标识符(none)总是被当做单独的个体,也就是说该标识符的多个声明被当做独立不同的个体。
下边我用一个例子来演示一下上边说的内容:
首先我新建了一个工程,在main.m中写了下边代码:
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#include <float.h>
int a = 100;
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
可以看出,定义了一个变量a,这个变量a默认属性为外部链接(external),也就是说我不能在别的文件中再次声明变量a了。然后我在ViewController.m中写了下边的代码:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
extern int a;
printf("a:%i -p:%p
",a,&a);
extern int a;
printf("a:%i -p:%p
",a,&a);
extern int a;
printf("a:%i -p:%p
",a,&a);
extern int a;
printf("a:%i -p:%p
",a,&a);
extern int a;
printf("a:%i -p:%p
",a,&a);
}
打印结果:
a:100 -p:0x10d9a0dc8
a:100 -p:0x10d9a0dc8
a:100 -p:0x10d9a0dc8
a:100 -p:0x10d9a0dc8
a:100 -p:0x10d9a0dc8
可以看出,extern int a;
这一句的extern关键字把int a 的链接属性变成了外部链接,因此编译器就会去取main.m中的a的值。我虽然多次声明了extern int a;
,但都指向了同一实体。
我们通过上面的代码,再次对链接属性进行解释。在缺省情况下,标识符b,c和f的链接属性为external。其余标识符的链接属性为none。比较特殊的是函数f。他其实是一个外部链接属性,就像我们调用系统函数一样,f只是一个函数名称。但我们在该源文件调用函数f时,可能会链接别的源文件中f的定义。甚至这个函数的定义出现在某个函数库中。
extern / static
其实,关键字extern和static用于在声明中修改标识符的链接属性。如果某个声明在正常情况下具有external链接属性,在它前面加上static关键字,可以使他的链接属性变为internal,也就是只能在源文件中被操作。
static是很有用的,当我们只想把一个变量或者函数限制在本源文件中,不行被别的文件或人员访问的时候,就是使用static的时候。
注意: static只对缺省链接属性为external的声明才具有改变链接属性的作用,在上图中的5中,把代码改成static int e;
,这个时候static的作用就不是改变链接属性。而是为变量e分配一个静态内存,每次访问这个变量,都会在这个静态内存中取值。
从技术角度讲,这两个关键字只有在声明中才是必须的,当用于具有文件作用域的声明时,这个关键字是可选的。然而,如果你在一个地方定义变量,并在其他源文件使用这个变量的声明时添加extern关键字,可以使读者更容易理解你的用途。
当extern关键字用于源文件中一个标识符的第一次声明时,它指定该标识符具有external链接属性。但是,如果它由于该标识符的第2次或以后的声明时,它并不会更改由第一次声明所指定的链接属性。在下图中的声明4并不会改变i的链接属性。
存储类型(storage class)
变量的存储类型这个概念对我们来说也很重要。变量的存储类型是指存储变量的内存类型。变量的存储类型决定变量何时创建,何时销毁以及它的值将保持多久。有三个地方可以存储变量:普通内存,运行时堆栈,硬件寄存器。
变量的缺省存储类型取决于它的声明位置。凡是在任何代码块之外声明的变量总是存储于静态内存中,也就是不属于堆栈的内存,这类变量称为静态变量。对于这类变量,你无法为它们指定其他存储类型。 静态变量在程序运行之前创建,在程序的整个执行期间始终存在,它始终保持原来的值,除非给他赋一个不同的值或程序结束。
在代码块内部声明的变量的缺省存储类型是自动的(automatic),也就是说它存储于堆栈中,称为自动变量。 有一关键字auto就是用于修饰这种存储类型的,但它极少用,因为代码块中的变量在缺省情况下就是自动变量。当程序执行到声明自动变量的代码块时,自动变量才被创建,当程序的执行流离开该代码块时,这些自动变量便自行销毁。 也就是说函数的调用也是有时间的,为了让代码更快,可以适当的加入内联函数。
如果该代码块被数次执行,例如一个函数被反复调用,这些自动变量每次都将从新创建。在代码块再次执行时,这些自动变量在堆栈中所占用的内存位置有可能和原先的位置相同,也可能不同。即使他们所占据的位置相同,你也不能保证这块内存同时不会有其他的用途。因此我们可以说,自动变量在代码块执行完毕后就消失。
对于在代码块内部声明的变量,如果给他加上关键字static,可以使它的存储类型从自动变为静态。 具有静态存储类型的变量在整个程序执行过程中一直存在,而不仅仅在声明它的代码块的执行时存在。注意:修改变量的存储属性并不表示修改变量的作用域,它仍然只能在代码块内部按名字访问。
函数的形式参数不能声明为静态,因为实参总是在堆栈中传递给函数,用于支持递归。
关键字register可以用于自动变量的声明,提示他们应该存储于机器的硬件寄存器而不是内存中,这类变量称为寄存器变量。通常寄存器变量比存储于内存中的变量访问起来效率更高。但是编译器不一定要理睬register关键字,如果有太多的变量被声明为register,它只会选取前几个实际存储于寄存器中,其他的就按普通的自动变量处理。如果一个编译器自己具有一套寄存器优化方法,它也可能忽略register关键字,其依据是由编译器决定哪些变量存储于寄存器中比人脑的决定更为合理些。
在典型情况下,你希望把使用频率最高的那些变量声明为寄存器变量。在有些计算机中,如果把指针声明为寄存器变量,程序的效率将能得到提高,尤其是那些频繁执行间接访操作的指针。你可以把函数的形参声明为寄存器变量,编译器会在函数的起始位置生成指令,把这些值从堆栈复制到寄存器中。但是,完全有可能,这个优化措施所节省的时间和空间上的开销还抵不上复制和几个值所使用的开销。
我们举个例子,程序的计算是在cpu中执行的,那么要获取计算用的数据,可以在内存中获取,也可以在寄存器中获取,相对于内存来说,寄存器的读取效率更高一些。我们把数据放到寄存器中拱cpu读取,执行完毕后,在把寄存器原来的值恢复。
寄存器变量的创建和销毁时间和自动变量相同,但它需要一些额外的工作。在一个使用寄存器变量的函数返回之前,这些寄存器先前存储的值必须恢复,确保调用者的寄存器变量未被破坏,这也是为什么寄存器效率高德原因,不需要反复的创建和销毁实际存储的实体。许多机器使用运行时堆栈来完成这个任务。当程序开始执行时,它把需要使用的寄存器的内容都保存到堆栈中,当函数返回时,这些值再复制回寄存器中。
在许多机器的硬件实现中,并不为寄存器指定地址。同样,由于寄存器值得保存和恢复,某个特定的寄存器在不同的时刻所保存的值不一定相同。基于这个原理,机器并不向你提供寄存器变量的地址。
初始化
现在我们把话题返回到变量声明中变量的初始化问题。自动变量和静态变量的初始化存在一个重要区别。
在静态变量的初始化中,我们可以把想要初始化的值放在当程序执行时变量将会使用的那个位置。当可执行文件载入到内存中时,这个已经保存了正确初始值的位置将赋值给那个变量。完成这个任务并不需要额外的时间,也不需要额外的指令,变量将会得到正确的值,因为已经知道变量的内存地址。如果不显式地指定初始值,静态变量将初始值为0.
自动变量的初始化需要更多的开销,因为当程序链接时还无法确定自动变量的存储位置。事实上,函数的局部变量在函数的每次调用中占据不同的位置。基于这个理由,自动变量没有缺省的初始值,而显式的初始化将在代码块的起始处插入一条隐式的赋值语句。
这种技巧造成4种后果:
-
自动变量的初始化较之赋值语句效率并无提高。 除了声明为const的变量之外,在声明变量的同时进行初始化和先声明后赋值只有风格之差,并无效率之别。
int func(int x) { int a; a = 100; // 上边的代码是先声明后初始化,对于自动变量,跟 int a = 100; 没有效率上的区别 return a; }
-
这条隐式的赋值语句,是自动变量在程序执行到他们所声明的函数时,每次都将重新初始化。这个与静态变量有大不同,后者只是在程序开始执行前初始化一次。
-
由于初始化在运行时进行,你可以用任何表达式作为初始化值。
int func(int x) { // 由于自动变量实在运行时才赋值的,所以当运行到下边的代码时,已经知道x的值,然后给a赋值 int a = x + 10; return a; }
-
除非你对自动变量进行显示的初始化,否则当自动变量创建时,它们的值总是垃圾。
static总结
当用于不同的上下文环境时,static关键字具有不同的意思。
- 当它用于函数定义时,或用于代码块之外的变量声明时,static关键字用于修改标识符的链接属性,从external改为internal,但标识符的存储类型和作用域不受影响。用这种方式声明的函数或变量只能在声明他们的源文件中访问。
- 当它用于代码块内部的变量声明时,static关键字用于修改变量的存储类型。从自动变量修改为静态变量,但变量的链接属性和作用域不受影响。用这种方式声明的变量在程序执行之前创建,并在程序的整个执行期间一直存在。
作用域,存储类型示例
总结
参考
该文件需要翻墙才能下载,感兴趣的朋友可以自行下载,或者留下邮箱