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*)