全局变量处理不当会出现重复定义的原因是,普通全局变量是跨文件可见的,而C/C++仅允许一个单元有相同符号。常量全局默认仅当前可见,就不容易出现重复定义的问题。
一、extern的定义及其含义extern在中文里代表外部的、对外的。该关键字常常被用在全局变量、函数或者模板声明中,表示该符号具有外部链接属性。
1.1 翻译单元翻译单元是由源文件及头文件(间接或者直接包含)组成,每一个翻译单元都将会被独立编译,当编译完成后,由连接器合并这些独立的已编译的翻译单元,所有违背仅一次定义规则(One Definition Rule,ODR)的都将抛出连接错误。个人觉得,编译单元在编译时不需要知道所有的实现,链接才会最终查找具体实现。
翻译单元规则不允许我们重复定义同一对象,否则将会出现链接错误。比如两个cpp文件都定义了int a=444;
编译时,不是所有编译器都会报告发现这样的错误,但是在链接时这些错误总会被暴露出来。
为什么我们需要用到全局变量?一个最常见的用途就是实现源文件间数据共享,使用全局变量要特别小心:
- 不正确的修改导致其他使用者数据脏读
- 不应修改的却被使用者修改
对于后者,我们常常会采用const
关键字进行避免,前者是数据共享必然会出现的问题,这是程序设计上应该多加考虑的。如果可能,尽可以避免使用全局变量,因为不好管理而且会出现命名空间污染。
对于普通的变量,默认全部文件可见;对于常量变量,默认当前文件可见。注意,编译器认为定义在全局的“声明”是定义:
//a.cpp
int a;//是定义,而不是声明
void fun()
{
}
于是区分,这个全局变量是声明还是定义的“重任”就交给了extern关键字。
//a.cpp
extern int a;//是声明,而不是定义
void fun()
{
}
除此,extern关键字还具备改变常量对象的链接属性的功能。
const int pi=3.14159265;//默认当前文件可见
extern const int pi=3.14159265;//所有文件均可见
1.4 extern关键字含义
一个extern关键字的含义取决于具体的语境:
- 全局作用域中的非常量声明,extern用于指示该常量在其他翻译单元(translation unit)定义,而不是在此文件中定义。(普通全局仅在当前可见)
- 全局作用域中的常量声明,表示这个文件不局限于当前文件可见,其他文件也可见
- extern "C"表示这个函数定义在其他地方并且使用C语言的调用规范。这个extern "C"可以在一个块中定义多个函数。
- 模板声明中,extern表示这个模板已经在其他地方实例化了,编译器可以直接复用这个实例,而不是创建一个新的实例。
通常而言,实现变量在多个文件共享的最佳方法是在头文件声明这个变量,同时在另一个源文件中定义这个变量。每当一个源文件需要使用这个变量时只需要引用这个头文件即可,为了避免重复包含,需要用宏定义处理这个声明。
二、具体例子普通的全局变量默认是所有文件可见的,声明处和使用处都应该用extern表示它们属于同一个对象;常量修饰的全局变量是仅当前文件可见,假如确实我们需要多文件共享一个不变的变量,必须使用extern表示它属于同一变量。
2.1 改变const变量的默认链接属性如果没有特殊声明,对于一个全局变量用const约束,编译器将会自动将其设置为当前文件可见,如果你要让所有文件可见,那么就可以用到extern修饰以修改默认连接属性。
//fileA.cpp
extern const int i = 42; // extern const definition
//fileB.cpp
extern const int i; // declaration only. same as i in FileA
这样一来,const变量的链接属性是文件作用域,也就是所有文件均可看见这个全局常量。
2.2 改变非const变量默认链接属性一个非常量默认的连接属性是外部的(external),但是仍然要符合ODR规则。使用extern修饰变量声明才不会被认为是一个定义。
//fileA.cpp
int i = 42; // declaration and definition
//fileB.cpp
extern int i; // declaration only. same as i in FileA
//fileC.cpp
extern int i; // declaration only. same as i in FileA
//fileD.cpp
int i = 43; // LNK2005! 'i' already has a definition.
extern int i = 43; // same error (extern is ignored on definitions)
2.3 extern constexpr 链接属性取决于编译器
在Visual Studio 2017 V15.3之前编译器默认将constexpr理解为内部链接,尽管显示用extern标记,在Visual Studio 2017 V15.3之后,extern表现正常。
2.4 extern "C"和extern "C++"函数声明// Declare printf with C linkage.
extern "C" int printf(const char *fmt, ...);
// Cause everything in the specified
// header files to have C linkage.
extern "C" {
// add your #include statements here
#include
}
// Declare the two functions ShowChar
// and GetChar with C linkage.
extern "C" {
char ShowChar(char ch);
char GetChar(void);
}
// Define the two functions
// ShowChar and GetChar with C linkage.
extern "C" char ShowChar(char ch) {
putchar(ch);
return ch;
}
extern "C" char GetChar(void) {
char ch;
ch = getchar();
return ch;
}
// Declare a global variable, errno, with C linkage.
extern "C" int errno;
默认情况下,以下对象具有内部链接:
- const对象
- constexpr对象
- typedef对象
- static 命名空间范围中的 对象
[1] https://docs.microsoft.com/en-us/cpp/cpp/extern-cpp?view=msvc-170 [2] https://docs.microsoft.com/zh-cn/cpp/cpp/program-and-linkage-cpp?view=msvc-170
【20220623】翻译了英文部分描述。