C++ extern关键字

Share on:
300 Words | Read in about 1 Min | View times

Overview

在C++标准库中,我们经常可以看到extern关键字的使用。围绕声明与定义、externstaticextern "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->vint->ichar->cP代表指针,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*)
Prev Post: 『C++ volatile的作用』
Next Post: 『C++对象内存模型』