C++ extern关键字
300 Words | Read in about 1 Min | View times
Overview
在C++标准库中,我们经常可以看到extern关键字的使用。围绕声明与定义、extern和static、extern "C",本节内容将尝试一次性讲清楚。
本系列文章将包括以下领域:
本章其他内容请见 《现代C++》
声明和定义
- 
声明,declaration,用于向程序表明变量的类型和名字,不会实际分配内存空间。变量可以被声明多次。 
- 
定义,definition,用于为变量分配内存空间,还可以为变量指定初始值。定义同时也是声明。变量只能被定义一次。 
extern表示声明,而不是定义,extern关键字常置于变量或函数前,以表示变量或函数的定义在其他文件中,提示编译器在链接阶段到其他模块寻找该符号的定义。
extern还可以用来进行链接指定,后面的extern "C"我们再展开。
1extern int a; //声明一个全局变量a,其定义可能在别的文件
2
3int a; //定义一个全局变量a
4
5int a = 0; //定义一个全局变量a,并初始化为0
6
7extern int a = 0; //定义一个全局变量a,并初始化为0。相当于int a = 0;
当要引用一个全局变量时,就必须要声明,extern关键字就不能省略,否则int a;就变成一句定义了。
而对于函数,定义和声明是有区别的,定义函数要有函数体,声明函数不需要函数体(且以;结尾),所以函数定义和声明都可以把extern省略,也不会造成问题:
1//.h文件
2//声明一个函数,其中extern是可以省略的
3extern int foo();
4
5//.cpp文件
6//定义一个函数,可以省略extern
7int foo() {
8    return 0;
9}
extern和static
- 
extern表示声明的变量定义在其他文件,在本文件要使用这个变量(已初始化的全局变量在.data段,而未初始化的全局变量在.bss段)
- 
static表示静态变量,分配内存的时候存储在静态区(已初始化的静态变量在.data段,而未初始化的静态变量在.bss段),不存储在栈上。
static的作用范围是本编译单元,其他文件不能使用该静态变量,作用跟extern刚好相反。两者不能同时用来修饰一个变量。
static的声明和定义是同时的,在头文件中声明一个静态变量的同时,它也被定义了。static修饰的全局变量的作用域只能是本编译单元,其他编译单元是不可见的。
extern “C”
前面说到,extern还可以用来进行链接指定。简单来说,就是通过extern "C"指定链接方式以C语言的规约进行,而不是以C++的规约进行。
当我们在C++代码中使用C函数时,经常会出现编译器无法找到obj模块中的C函数定义,从而导致链接失败,这是为什么呢?
C++是一门支持多态和重载的语言,编译器在编译时会将函数名和参数按一定规则组合起来,生成一个中间命名,而C语言则不会。因此使用extern "C"的目的,就是告诉编译器,此时我要使用的函数按照C的方式链接,不要生成C++的中间命名。
每个变量或函数可以单独使用extern "C"来声明;当有多个变量或函数都在使用extern "C"声明时,可以将它们用花括号包裹起来:
 1extern "C" int foo;
 2extern "C" void func();
 3extern "C" void bar();
 4
 5//等价于
 6extern "C" {
 7    int foo;
 8    void func();
 9    void bar();
10}
通常我们会在标准库的头文件里看到如下的写法:
 1//.h文件
 2#ifndef __SOME_MODULE_H__
 3#define __SOME_MODULE_H__
 4
 5#ifdef __cplusplus
 6extern "C" {
 7#endif
 8
 9    /*...*/
10
11#ifdef __cplusplus
12}
13#endif
14
15#endif
当代码作为C++编译时,__cplusplus宏有定义,可以用来区别当前的编译环境是否C++。如果是C++,则声明接下来的函数或变量采用C的方式进行链接。
C++函数重载的底层原理是基于编译器的名称修饰机制,即name mangling机制。不同编译器name mangling的实现不同,下面介绍gcc编译器的修饰规则。
gcc编译器的name mangling规则
对于一个C++函数:
- 
编码后符号由 _Z开头
- 
如果有作用域符,在 _Z后加上N
- 
[命名空间名长度 + 命名空间名 + 类名长度 + 类名 + ] 函数名长度 + 函数名 
- 
如果有作用域符,以 E结尾
- 
最后加上函数形参的符号, void->v,int->i,char->c,P代表指针,R代表引用,K代表const,有几个参数就写几个符号。
可以看到name mangling与函数返回值无关。
举个例子:
 1namespace zhxilin {
 2class Test {
 3public:
 4    void func();
 5};
 6}
 7
 8void zhxilin::Test::func() {} //_ZN7zhxilin4Test4funcEv
 9
10void func() {} //_Z4funcv
11
12void func(int) {} //_Z4funci
13
14void func(int, int) {} //_Z4funcii
15
16void func(zhxilin::Test*) {} //_Z4funcPN7zhxilin4TestE
17
18void func(zhxilin::Test const&) {} //_Z4funcRKN7zhxilin4TestE
更详细的规则可以参阅gcc_cpp_mangling_documentation。
Linux提供了一个工具c++filt可以用来将编码后的符号还原成易读的格式
1$ c++filt _Z4funcPN7zhxilin4TestE
2func(zhxilin::Test*)