Extern 的用法
在 C 语言中, 修饰符 extern 用在变量或者函数声明之前, 用来标识变量或者函数的定义在别的文件中已经给出, 告知编译器运行到此变量或函数时, 在其他位置寻找其定义. 其常见的用法如下:
1 | extern int a; |
此处如果不在开头加上 extern int a,那么main函数是无法访问变量 a,b 的, 即使变量 a,b 是一个外部变量, 但其作用域是定义以后至程序结束, 而修饰符 extern 引导的变量声明延拓了该外部变量的存在域. 实际上, extern 修饰符主要还是在多源文件的情况下使用.
Extern 修饰变量的声明
extern关键字可以用来修饰变量,表示该变量在别的文件中已有声明。例如:我们在文件file1.c中声明了变量int var,然后我们又需要再file2.c中使用该变量,则可在文件file2.c中声明extern int var,就可在文件file2.c中使用该变量了。
值得强调的是 extern 修饰的是变量的声明, 变量的声明仅仅向编译器传递了变量的信息, 但并不会分配内存; 分配内存是通过变量定义来实现的. 因此我们如果使用 extern 修饰变量声明, 是不可以同时在其后初始化, 因为声明并不会对变量, 对内存操作的. 其次, 使用 extern 修饰声明的变量一定要是全局变量, 这是因为如果声明的是局部变量, 那么他的存活域只在某个函数体内部, 在其他文件中引用这个变量也是没有意义的.
进一步, 我们可以讨论由 extern 修饰的变量声明对应的存活域. 首先, 由前面的介绍知道, extern 声明的变量只能是全局变量, 但是如果 extern 修饰的变量声明位于某个函数体内, 那么其存活域也只在这个函数内部, 与在函数内部设置的局部变量一致.
Extern 修饰函数声明
从本质来看, extern 修饰函数声明与修饰变量声明是等价的. 但我们更为常用的引用其他文件的函数是通过头文件的方式来引用的. 在程序实现层面, 这两种方式是有所不同的, 引用头文件的编译处理是一种预处理方式, 而extern 修饰的声明则是代码链接层面的实现. 因此一般而言, 如果需要大批量引用函数声明, 那么就用头文件更为便利, 而如果只是引用少数函数, 那么用 extern 修饰声明即可.
Extern 用来实现链接指定
extern用来进行链接指定一般来说是用来实现混合编程. 例如我们如果要在C++程序中调用C代码, 那么我们就需要用extern “C”来声明.
extern “C” 可用于单一语句
1 | extern “C” double sqrt(double); |
也可以是复合语句, 相当于复合语句中的声明都加了 extern “C”
1 | extern “C” |
同样可以包含头文件,相当于头文件中的声明都加了extern “C” (不建议这样写,会有嵌套可能)
1 | extern “C” |
值得注意的是, extern “C” 是不可以用在函数内部的, 且如果函数有多个声明, 可以全都加上 extern “C”, 也可以只加一个, 后续声明会遵循第一个链接指示符的规则.
Static 的用法
static 的用法其实只有三个: 隐藏, 保持变量持久, 默认初始化为0
Static 的主要功能:隐藏
当我们同时编译多个源文件时, 普通的全局变量和函数 (即未加 static 的) 都是全局可访问的.
例如, 对于多源文件main.cpp和test.cpp如下所示
1 | //main source |
test.cpp结构如下
1 |
|
上述可以运行是因为在test.cpp文件中的全局变量并没有加static,因此其余文件可以随意的利用修饰符extern来调用全局变量和函数. 如果加上static, 那么他就会对其余的源文件隐藏, 只能在本文件中使用. 因此利用这一特性我们可以在不同的源文件定义同名变量与同名函数, 不会出现命名冲突, 也不会出现数据混用的情况. static 可用于修饰函数也可以用来修饰变量; 如果针对函数, 那么 static 的作用仅限于隐藏; 而对于变量, 那么 static 就有接下来的两个作用.
保持变量内容的持久
与自动变量存储在栈区不同, static变量会存储在静态存储区. 虽然全局变量也会存储在静态存储区, 但是与全局变量相比, static变量主要的特性是对变量的隐藏.
如果static用于声明内部变量, 那么static类型的内部变量和函数内部的自动变量一样,只能在函数内部使用, 但不同的一点是, 函数内部的自动变量会在函数运行结束自动释放内存, 下一次运行需要重新声明, 重新分配内存; 而static类型变量则不会, 她只在第一次声明时分配内存且完成初始化, 不会随着函数的返回而释放空间, 一直等到重新调用函数时,其值为上次结束时的值.
1 |
|
默认初始化为 0
实际上, 全局变量也有这个特性, 这是因为全局变量和静态变量都存储在静态存储区, 静态存储区的字节默认为 0x00. 善用这个特性, 可以极大简便程序的编写复杂度, 如果我们考虑一个稀疏矩阵, 如果我们设置局部变量, 那么我们需要先将变量全部置为0, 而后一个个添加非零元素; 实际上我们可以直接用静态变量, 这样的话, 我们只需要添加非零元素即可. 这同样对字符串有相同的操作.
Const 的用法
const 用来修饰一个变量, 那么他的值从某种角度来说就是不能被改变的. 这其实很类似与C语言中的#define的宏命令, 这两个之间还是有较大的差异:
define 是预处理指令, 其在预处理阶段就会完成文本替换. 而const则是对变量的修饰,因此他需要在后续的语法编译的时期, 程序还要检查其类型是否正确, 对比更加安全.
- const 可以保护被修饰的内容, 防止意外修改, 从而来增强程序的鲁棒性.
- const 常量是不通过编译器分配内存空间的, 而是将其纳入符号表; 这样使得他在运行过程中不需要存储和读取内存的时间, 提高了运行效率.
修饰局部变量
1 | const int a=1; |
这两个语句表示的是同一条命令, 即定义整型const常量a并初始化为1. 由于const常量后续将不再允许被修改, 因此我们在定义const常量的同时一定要对其进行初始化. 其实对于这种基本类型的const修饰十分简单, 我们考虑const字符串,可以知道他的一些优势.
1 | const char* ptr="Hello world!"; |
如果这里我们不加上const限定符, 那在程序中可能出现 ptr[3]=’L’ 的命令, 他会因为对只读区域的写入而报错, 也就是在程序运行时报错; 而如果我们用 const 限制符, 那么在程序编译期间, 就会因为试图修改const常量而报错.
常量指针和指针常量
值得注意的是 const 的使用在此处将会十分灵活, 因为他涉及到指针的内容, 因此比较复杂.
首先, 我们先考虑常量指针, 其声明方式如下所示,
1 | const int* ptr; |
常量指针的含义为指针指向的内容是常量. 不可以通过常量指针来修改对应的值. 需要注意的是,
这里的常量指针不能修改对应的值,只是不能通过常量指针修改; 但允许通过其他的引用来修改, 例如
1
2
3
4int a=10;
const int* pn=&a;
*pn=10; //报错,因为pn是常量指针
a=10; //允许运行, 因为这是其他的引用并没有常量的限制常量指针指向的值不能修改, 但是可以修改常量指针指向的对象, 也就是可以将常量指针指向不同的位置.
1
2
3
4int x=5;
int y=6;
const int* pn=&x;
pn=&y;
其次, 我们讨论指针常量, 其定义声明如下所示
1 | int* const pn; |
指针常量指的是这个指针本身就是一个常量, 我们在运行过程中不可以重新给该指针常量重新赋值, 但是我们可以用这个指针修改他指向的内容. 例如
1 | int a = 5; |
这两种const常量的定义的区别仅在于 const 限定符与 的位置关系, 如果const在 左边, 那么定义的就是常量指针; 如果const在* 右边, 那么定义的就是指针常量. 特殊的实际上, 我们可以将上面的两种定义合并, 得到指向常量的常指针的定义
1 | const int* const a=10; |
指针指向的位置不能改变并且也不能通过这个指针改变变量的值,但是依然可以通过其他的普通指针改变变量的值.
修饰函数的参数
根据前面讨论的常量指针和指针常量, const修饰函数的参数也为两种
防止修改指针指向的内容
1
void StringCopy(char *strDestination, const char *strSource);
其中函数内部是不能随意修改strSource指向的内容, 在编译期间就会报错.
防止修改指针的地址
1
void swap(int* const p1, int* const p2);
不能随意修改p1和p2的地址.
修饰函数的返回值
如果给以“指针传递”方式的函数返回值加 const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针.
1 | const char* Getstring(); |
修饰全局变量
全局变量的作用域是整个文件,我们应该尽量避免使用全局变量,因为一旦有一个函数改变了全局变量的值,它也会影响到其他引用这个变量的函数,导致除了bug后很难发现,如果一定要用全局变量,我们应该尽量的使用const修饰符进行修饰,这样防止不必要的人为修改,使用的方法与局部变量是相同的。
代码运行的内存分配
C/C++程序经过编译器完成编译链接等等处理工作以后得到的二进制文件, 其包含栈(stack), 堆(heap), 数据段, BSS段, 代码段. 其中数据段, BSS段和代码段是程序编译完成就已经分配完成了, 无需等代码运行; 而堆区和栈区需要程序被加载到内存开始运行时才会分配.
栈区(stack): 由编译器自动分配和释放内存. 该区一般用于存储函数参数, 局部变量等值. 其操作方式类似于数据结构中提到的栈.
堆区(heap): 程序员通过 new 命令和 malloc 命令, 可以动态申请某个大小的内存. 注意的是, 此区的内存和栈区的不同, 并不能由编译器自动释放, 而是由程序员通过 delete 命令和 free 命令来手动释放内存, 如果不释放, 长期使用下, 申请内存超过了堆区大小, 那么就会发生内存泄漏现象.
数据段: 数据段属于静态内存分配,所有有初值的全局变量和用static修饰的静态变量,常量数据都在数据段中。实际上可以认为其分为两块数据段:1.只读数据段 2. 读写数据段.
在只读数据段中, 一般用来存储程序使用时不会发生变化的数据.一般是用const修饰的变量或者程序中使用的文字常量存储在此处. 特殊的还有常量存储区(特殊的常量存储区,属于静态存储区)
1) 常量占用内存,只读状态,决不可修改
2) 常量字符串就是放在这里的,程序结束后由系统释放读写数据段:用来存储那些已经完成初始化的全局变量或者初始化的静态变量. 已初始化数据是在程序中声明,并且具有初值的变量,这些变量需要占用存储器的空间,在程序执行时它们需要位于可读写的内存区域内,并且有初值,以供程序运行时读写
BSS段: 存储未初始化的全局变量或者静态(全局)变量. 其是可读写的, 编译器给处理成0. 未初始化数据是在程序中声明, 但是没有初始化的变量, 这些变量在程序运行之前不需要占用存储器的空间. 与读写数据段类似, 它也属于静态数据区. 但是该段中数据没有经过初始化. 未初始化数据段只有在运行的初始化阶段才会产生, 因此它的大小不会影响目标文件的大小.
代码段: 存放函数体的二进制代码,所有语句经过编译后产生的CPU指令都会存放在此处.