编译器的行为以 gcc/g++ 9.4.0 为准。
目录
2.2.4 C 变量同时使用 extern 和初始化器(initializer)
1 前置知识
1.1 声明、定义与初始化
声明(声明式)= declaration
extern int x;
std::size_t numDigits(int number);
class Widget;
template<typename T>
class GraphNode;
定义(定义式)= definition
int x;
std::size_t numDigits(int number) {
}
class Widget {
public:
};
template<typename T>
class GraphNode {
public:
};
(函数的)签名(签名式/类型) = signature(函数参数的类型列表,或函数参数的类型列表及函数的返回类型)
头文件 = header file (*.h, *.hpp)
源文件 = source file (*.c, *.cpp)
初始化 = initialization(类的初始化即调用类的构造函数)
声明告诉编译器被声明的数据在其它地方已被定义,链接器会链接各处的声明与定义。例如,在编译多个文件时,两个源文件需要同一个函数 foo(),在其中一个源文件中写上该函数的定义,在另一个函数写上该函数的声明;或在一个头文件中写上该函数的声明,并在要用到该函数的源文件中包含该头文件。如果编译器找到了该函数的定义但未在使用该函数的文件找到其声明会警告 implicit declaration of function 'foo' [-Wimplicit-function-declaration];如果编译器找不到该函数的定义会错误 undefined reference to 'foo'。
定义包含了声明的作用;一个变量、函数、类或对象(对象的含义有时包含变量)可以被多次声明,但只能被定义一次,否则会产生重复定义错误(例如在编译多个文件时,在两个源文件的全局定义了同一个符号,或在头文件和包含该头文件的源文件的全局同时定义了同一个符号)。编译器不在声明处为声明的变量或对象分配内存,而在定义处为定义的变量或对象分配内存。
1.2 作用域
C:
作用域 | 应用 | 范围 |
---|---|---|
块作用域(block scope) | 任何在复合语句,包含函数体或出现于 if、switch、for、while 或 do-while 语句中的任何表达式、声明或语句,或在函数定义内的参数列表中声明的标识符的作用域 | 在声明点开始,在声明于其中的块或语句的结尾结束 |
文件作用域(file scope) | 在任何块或参数列表外声明的任何标识符的作用域 | 在声明点开始,翻译单元尾结束 |
函数作用域(function scope) | 声明于函数内部的标号(且只有标号) | 该函数中的所有位置(所有嵌套块中,其自身声明前后) |
函数原型作用域(function prototype scope) | 非函数定义的函数声明的参数列表中引入的名称的作用域 | 在函数声明器的结尾结束 |
C++:
作用域 | 应用 | 范围 |
---|---|---|
块作用域(block scope) | 块(复合语句)中的声明所引入的变量的潜在作用域 | 开始于其声明点,终止于该块末尾 |
函数形参作用域(function parameter scope) | 函数形参(包括 lambda 表达式的形参)或函数局部预定义变量的潜在作用域 | 开始于其声明点,若最内层的外围函数声明符不是函数定义的声明符,则其潜在作用域终止于该函数声明符的结尾,否则终止于函数 try 块的最后异常处理块的末尾或函数末尾 |
命名空间作用域(namespace scope) | 命名空间中声明的任何实体的潜在作用域 | 开始于其声明点,包括其后的同一命名空间名的所有命名空间定义、using 指令引入到的该作用域的剩余部分 |
类作用域(class scope) | 类中声明的名字的潜在作用域 | 包含类体的剩余部分和所有函数体(无论是否定义于类定义外或在该名字的声明之前)、默认实参、异常规定、类内花括号或等号初始化器,并递归地包括嵌套类中的所有这些内容 |
枚举作用域(enumeration scope) | 枚举项(enumerator)的潜在作用域 | 无作用域枚举的枚举项的潜在作用域开始于其声明点,并终止于外层作用域的末尾;有作用域枚举的枚举项的潜在作用域开始于其声明点,并终止于 enum 说明符的末尾 |
模板形参作用域(template parameter scope) | 模板形参名的潜在作用域 | 开始于其声明点,持续到于其中引入了它的最小模板声明的末尾 |
注意:C++ 的函数作用域在 P1787 中被移除(Contribution: https://zh.cppreference.com/mwiki/index.php?title=cpp/language/scope&oldid=73396)
注意:翻译单元的顶层作用域(“文件作用域”或“全局作用域”)亦为命名空间,且被正式称作“全局命名空间作用域”(类比于 C 的文件作用域)。任何声明于全局命名空间作用域的实体的潜在作用域均开始于其声明,并持续到翻译单元的结尾【 C 与 C++ 的行为不同的重要原因之一】。
Contribution: https://zh.cppreference.com/mwiki/index.php?title=cpp/language/scope&oldid=73397, https://zh.cppreference.com/mwiki/index.php?title=cpp/language/scope&oldid=73398.
1.3 链接
C:
链接 | 应用 | 行为 |
---|---|---|
无链接(no linkage) | 函数参数、非 extern 声明的块作用域对象 | 只能从其所在的作用域指代该标识符(非 extern 的块作用域内的对象只能在块作用域内被使用;函数参数只能在函数/块作用域内使用) |
内部链接(internal linkage) | static 函数和对象 | 能从当前翻译单元(translation unit,或称“编译单元” compilation unit)的所有作用域指代该标识符(静态函数或变量只能在所在文件内被使用,而不能在其它文件中使用) |
外部链接(external linkage) | 非 static 函数、extern 对象、文件作用域的非 static 对象 | 能从整个程序的任何其他翻译单元指代该标识符(非 static 函数默认为 extern 可以在其它文件被使用;extern 对象和文件作用域非 static 对象可以被其它文件使用) |
翻译单元一般可视为经过 C 预处理器处理(#include 头文件、#define 宏、#ifdef/#ifndef/#endif)过的单个源文件。
C++:
链接 | 应用 | 行为 |
---|---|---|
无链接(no linkage) | 块作用域内未显式声明为 extern 的变量、局部类及其成员函数、声明于块作用域的其它名字(typedef、枚举、枚举项);未指定为拥有外部、模块或内部链接的名字 | 名字只能从其所在的作用域使用(一般为块作用域) |
内部链接(internal linkage) | 命名空间作用域内static 变量、变量模板、函数、函数模板、匿名联合体(Anonymous Union)的数据成员、非 volatile 非模板非内联非 exported 非 extern 声明且未声明有外部链接的 const 变量(包括 constexpr);所有声明于无名命名空间或无名命名空间内的命名空间中的名字 | 名字可从当前翻译单元中的所有作用域使用 |
外部链接(external linkage) | 命名空间作用域内以上未列出的变量与函数(非 static 函数、非 static 且非 const 变量、extern 声明的变量)、枚举、类及其成员函数及静态数据成员、嵌套类、首次以类体内的友元声明引入的函数的名字、以上未列出的模板(非 static 函数模板)、首次声明于块作用域的 extern 声明的变量名与函数名;除非它们在无名命名空间内声明或在具名模块内声明且非 exported | 名字能从其他翻译单元中的作用域使用 |
模块链接(module linkage) | 声明于命名空间作用域内附着到具名模块,非 exported 且无内部链接的名字 | 名字只能从同一模块单元或同一具名模块中的其他翻译单元的作用域指代 |
具有外部链接的变量和函数自动具有语言链接(Language linkage,见下);无名命名空间、所有直接或间接在无名命名空间内声明的命名空间、在无名命名空间内声明的所有名字都具有内部链接,即使它们被声明为拥有外部链接,其它翻译单元也不能访问它们。
1.4 存储期
C:
存储期 | 应用 | 范围 |
---|---|---|
自动存储期(automatic storage duration) | 函数参数、非 static 块作用域对象、块作用域的复合字面量 | 进入声明对象于其中的块(块作用域)时分配内存,离开块作用域(抵达结尾、goto、return)时释放内存。 |
静态存储期(static storage duration) | static 对象、带内部或外部链接且非 _Thread_local 的对象 | 整个程序执行过程中拥有内存 |
线程存储期(thread storage duration) | _Thread_local 声明的对象 | 创建对象的线程的整个执行过程中拥有内存 |
分配存储期(allocated storage duration) | 动态内存分配函数所用 | 动态分配内存函数管理内存 |
C++:
存储期 | 应用 | 范围 |
---|---|---|
自动存储期(automatic storage duration) | 未声明为 static、extern 或 thread_local 的所有局部对象 | 在外围代码块开始时分配,在结束时解分配 |
静态存储期(static storage duration) | 所有声明于命名空间(包含全局命名空间)作用域的对象、声明带有 static 或 extern 的对象 | 在程序开始时分配,在程序结束时解分配 |
线程存储期(thread storage duration) | 声明为 thread_local 的对象 | 在线程开始时分配,在线程结束时解分配 |
动态存储期(dynamic storage duration) | 动态分配内存函数管理的对象 | 动态内存分配函数分配和解分配 |
2 C 的 extern
具有内部或(通常为)外部链接之一的静态存储期的存储类说明符。存储类说明符用于声明。
2.1 C 函数的 extern
C 语言的函数默认为 extern。这就是为什么联合编译时,如果在一个源文件中使用另一个源文件中定义的函数,即使不声明也可以成功编译正常运行并只报警告而不报错误的原因。
2.2 C 变量的 extern
C 语言文件作用域中的变量的声明默认为 extern(见星号注释及参考文献 [2],“若不提供存储类说明符,则默认为:对在文件作用域的对象为 extern”),也就是说,只要文件作用域中变量声明的 extern 均可以省略。
2.2.1 声明还是定义?
(1) 在文件作用域写
int x;
编译器会根据不同情况确认这条语句是一条声明还是一条定义(见下文),在一些情况下,为了避免编译器认为这条语句是一条定义,使用 extern 保证其修饰的语句是数据的声明而不是定义:
extern int x;
(2) 在块作用域中写
int x;
必然意味着这是一条定义;
(3) 不管是在文件作用域还是块作用域,使用初始化器
int x = 5;
必然意味着这是一条定义。
2.2.2 外部与试探性定义
对于 2.2.1 (1) 的情况而言,它的正式名称叫做试探性定义(tentative definition):没有初始化器且没有说明符或有 static 说明符的外部声明。
对于单个翻译单元:
a. 若在同一翻译单元的前方或后方能找到实际的外部定义(即头文件或本源文件的全局/文件作用域中带初始化器的定义),则试探性定义仅表现为声明;
b. 若在同一翻译单元中无外部定义,则试探性定义表现为拥有初始化器 = 0 (对于数组、结构、联合类型则是 = {0} )的实际定义。
由于头文件和源文件全局作为一个翻译单元的文件作用域,所以二者可以等同考虑。
头文件和源文件全局 | 行为 |
---|---|
一个 int x | 定义,值为 0 |
两个及以上 int x | 在翻译单元中如同 int x = 0 定义 |
一个及以上 int x 和一个 int x = 1 | int x = 1 为定义 int x 为声明 |
零个及以上 int x 和 两个及以上 int x = 1 | 重复定义 |
对于共同编译的多个翻译单元:
一个翻译单元内使用一个全局符号必须:
i. 在该翻译单元的全局有一条声明
ii. 在整个程序的全局有一条定义
a. 若在所有翻译单元的前方或后方能找到实际的外部定义(即头文件或本源文件的全局/文件作用域中带初始化器的定义),则试探性定义仅表现为声明;
b. 若在所有翻译单元中无外部定义,则试探性定义表现为拥有初始化器 = 0 (对于数组、结构、联合类型则是 = {0} )的实际定义。
头文件和源文件 1 全局 | 头文件和源文件 2 全局 | 行为 |
---|---|---|
/ 或 一个及以上 int x 和/或一个 int x = 1 | / | 如果源文件 2 使用 x 则 源文件 2 会找不到定义 |
/ | 一个及以上 int x | int x 为定义 |
一个及以上 int x | 一个及以上 int x | 在翻译单元中如同 int x = 0 定义 |
零个及以上 int x 及一个 int x = 1 | 一个及以上 int x | int x = 1 为定义 int x 为声明 |
任一文件有零个及以上 int x 和两个及以上 int x = 1 | 重复定义 |
以上可概括为单个翻译单元或共同编译的多个翻译单元的全局/文件作用域:
a. 如果编译器找到了一条(带初始化的)定义,那么它表现为一条声明;
b. 编译器找到了多条试探性定义,那么在翻译单元中如同初始化为 0 的定义;
c. 如果只有一条试探性定义,那么它表现为一条定义。
以上重复定义的问题是违反了一个定义规则(One Definition Rule)的第 3 条:
整个程序可以拥有每个具有外部链接的标识符的零或一个外部定义。
注意:以上我们默认重声明(Redeclaration)是允许的,但是情况并非总是如此,只有以下 3 种情况的重声明是允许的:
a. 有链接对象(外部或内部)的声明
b. 指明同一类型的非 VLA typedef
c. struct 和 union 声明
例如,如果在函数体内部即无链接的情况下重声明会报错误 redeclaration of 'x' with no linkage。
2.2.3 C 变量的 extern 的行为
C 变量的 extern 应在头文件或源文件全局(函数外部)使用,若在函数内部使用会错误 declaraction of 'x' with no linkage follows extern declaraction。以下默认讨论的是文件作用域(全局/函数外部),因为函数内部不允许使用 extern 声明(函数内部是块作用域内部,而块作用域内部默认为 auto 说明符限定,即自动存储期、无链接),而函数内部的定义会屏蔽在外部(头文件、其它源文件或本源文件的函数外部)的声明。
* 如果在头文件或源文件中声明
extern int x;
那么不能直接初始化 x,因为这时还没有 x 的定义,需要先定义 x。
可以以以下几种方式使用 extern:
(1) 在单个源文件中使用 extern 声明并定义 x(没有意义)
(2) 在头文件中定义(即定义并初始化) x 并在单个包含该头文件的源文件中使用 extern 声明 x(如果多个包含会导致重复定义错误)
(3) 在一个源文件中定义 x,在另一个源文件中使用 extern 声明 x(并使用 x),联合编译
(4) 在头文件中使用 extern 声明 x,在其中一个包含或不包含(因为定义本身已经作了声明,无需重复声明)该头文件的源文件中定义 x,在另一个包含该头文件的源文件中直接使用 x,联合编译,与 (3) 的实质是一样的
由于不同文件使用的实际是同一个 x,所以在其中一处更改 x 的值会导致在程序接下来的执行顺序中 x 的值被更改。如前所述,只要编译器通过的情况 extern 都可以省略,起到的作用是相同的,例如:
** 对于 (3) 起到相同作用的做法是:在一个源文件中全局定义(定义并初始化)x,在另一个源文件中的函数外部写:
int x;
联合编译,编译器会将该语句视作一条声明而非定义(即与 extern int x 的效果相同);但不能在另一个源文件的函数外部也初始化 x,否则编译器会将该语句视作一条定义而非声明,会导致重复定义错误。
*** 对于 (4) 起到相同作用的做法是:在头文件中写
int x;
在其中一个包含或不包含该头文件的源文件中定义(定义并初始化) x,在另一个包含该头文件的源文件中使用 x,联合编译,同样,编译器会将该语句视作一条声明而非定义(即与 extern int x 的效果相同)。对于 (4),也可以在使用 x 的源文件再用 extern 声明一遍 x 或再写一遍 int x(仍然是默认 extern 的声明),但是没有必要。
**** 不能直接在源文件的全局(函数外部)直接初始化变量。如果试图直接初始化 x 为 1,那么会警告 data definition has no type or storage class 以及 type defaults to 'int' in declaration of 'x' [-Wimplicit-int]。
2.2.4 C 变量同时使用 extern 和初始化器(initializer)
extern int x = 5
初始化器代表该语句是一条定义。
(1) 在头文件中同时使用并在单个源文件中包含该头文件:不报错
(2) 在单个源文件中全局(函数外部)同时使用:警告 'x' initialized and declared 'extern'
(3) 在单个源文件中函数内部使用:错误 'x' has both 'extern' and initializer
(4) 在多个源文件中同时使用(例如在头文件中同时使用并在多个源文件中包含该头文件或在头文件中使用但在源文件中又定义了一遍):警告 'x' initialized and declared 'extern' 错误 multiple definition of 'x' (即重复定义错误)
如果在函数内部定义同名变量并使用该变量遮蔽掉全局的变量,则不会产生重复定义问题,因为使用的变量是函数内部定义的局部变量,不是全局定义的外部变量。
如果直接在头文件中定义并初始化
int x = 5;
则 (1)(4) 行为与前述一致。
3 C++ 的 extern
1. 具有外部链接的静态存储期的存储类说明符(可以修饰 const 和 constexpr)
2. 语言链接说明(即 extern "C" 与 extern "C++")
3. 显式模板实例化声明
Contribution: https://zh.cppreference.com/mwiki/index.php?title=cpp/keyword/extern&oldid=73391
3.1 静态存储期的存储类说明符
由于前述 C 与 C++ 作用域不同,所以其行为与 C 并不完全相同(比 C 简单)。
1. C++ 的函数和全局变量在全局命名空间作用域中,仅对当前文件可见,因此联合编译时一个源文件不能直接使用另一个源文件的函数或全局变量,否则会直接错误,必须在使用函数的源文件中声明该函数或用 extern 声明该全局变量,或在头文件中声明再在使用函数的源文件中包含该头文件。
2. 不同于 C,C++ 中的
int x;
一定是一条定义(不管是在头文件中还是源文件中,不管是在命名空间作用域中还是在块作用域中),不存在外部定义与试探性定义的说法。
3.2 语言链接
extern "C++" 为默认的语言链接
extern "C" 将其应用的函数类型、具有外部链接的函数名和变量与 C 语言编写的函数相链接,使得 C 模块可以调用 C++ 程序定义的函数
extern "C" 可以声明全局变量或函数:
extern "C"
{
int open(const char *pathname, int flags);
}
extern "C" int errno;
extern "C" int printf(const char *fmt, ...);
也可以定义:
extern "C" char ShowChar(char ch) {
putchar(ch);
return ch;
}
还可以包含头文件:
extern "C" {
#include <stdio.h>
}
例如,在上述代码中用 extern "C" 声明了具有 C 链接的 open 函数后,就可以在 C++ 程序中调用该函数;同样,C++ 程序也可以调用 ShowChar 函数;否则,编译器会错误 'open' was not declared in this scope; did you mean 'popen'?。
常见用法是在头文件中作如下定义以区分 C 和 C++:
#ifndef EXAMPLE_H
#define EXAMPLE_H
#ifdef __cplusplus
extern "C" {
#endif
int f();
#ifdef __cplusplus
}
#endif
#endif
在 C 源文件(作为库文件)中包含该头文件写出函数 f 的定义,然后在 C++ 源文件(作为客户文件)中包含该头文件并使用函数 f。如果不加 extern "C",C++ 源文件将无法识别符号 f(即 undefined reference)。如果是在 C++ 源文件(作为库文件)中包含该头文件写出函数 f 的定义,然后在 C 源文件(作为客户文件)中包含该头文件并使用函数 f,那么要注意为 C++ 的重载分别定义函数包装器。作为特例,对于 C++ 版本的 C 头文件如 <cstdio.h>,GCC 在编译 C++ 时隐式将所有系统头文件包含在 extern "C" 块内,这是受到 C++ 标准确定的,虽然可以将它们视作全局名称空间内,但新标准认为应把它们放入标准命名空间(std)内;POSIX 头文件如 <unistd.h> 已经包含了上述区分 C 和 C++ 的 extern "C" 保护,但这并不包含在 C 标准内。
3.3 显式模板实例化声明(extern 模板)
当一个模板会在其它地方被初始化时,应当用 extern 要求编译器不在此处重复对模板初始化以减少编译时间和编译出的文件大小。
3.3.1 类模板
template
class-key template-name <
argument-list >
;
extern template
class-key template-name <
argument-list >
;
MWE 如下:
namespace N
{
template<class T>
class Y
{
void mf() {}
};
}
template class N::Y<char*>;
template void N::Y<double>::mf();
3.3.2 函数模板
template
return-type name <
argument-list >
(
parameter-list )
;
return-type name
template(
parameter-list )
;
return-type name
extern template<
argument-list >
(
parameter-list )
;
return-type name
extern template(
parameter-list )
;
MWE 如下:
template<typename T>
void f(T s)
{
std::cout << s << '\n';
}
template void f<double>(double);
4 References
[1] MEYERS S. Effective C++: 55 Specific Ways to Improve Your Programs and Designs,3rd Edition[M].London:Pearson Education,2005.
[2] GeeksforGeeks. Understanding “extern” keyword in C[EB/OL].Noida:GeeksforGeeks,2020-11-16[2022-01-04].https://www.geeksforgeeks.org/understanding-extern-keyword-in-c.
[3] Wikipedia contributors. External variable[EB/OL].United States:Wikimedia Foundation,2021-10-22[2022-01-04].https://en.wikipedia.org/wiki/External_variable.
[4] cppreference.com. Storage-class specifiers[EB/OL].Cppreference.com,2021-09-30[2021-01-05].https://en.cppreference.com/w/c/language/storage_duration.
[5] cppreference.com. Storage-class specifiers[EB/OL].Cppreference.com,2021-10-13[2021-01-05].https://en.cppreference.com/w/cpp/language/storage_duration.
[6] cppreference.com. extern[EB/OL].Cppreference.com,2021-04-24[2021-01-05].https://en.cppreference.com/w/cpp/keyword/extern.
[7] cppreference.com. Language linkage[EB/OL].Cppreference.com,2021-12-07[2021-01-05].https://en.cppreference.com/w/cpp/language/language_linkage.
[8] ROBERTSON C, TAO J. extern (C++)[EB/OL].Redmond:Microsoft,2021-12-04[2021-01-05].https://docs.microsoft.com/en-us/cpp/cpp/extern-cpp?view=msvc-170.
[9] Litherum et al. What is the effect of extern "C" in C++?[EB/OL].StackOverflow,2020-10-05[2021-01-05].https://stackoverflow.com/questions/1041866/what-is-the-effect-of-extern-c-in-c.
[10] cppreference.com. Class template[EB/OL].Cppreference.com,2021-12-07[2021-01-05].https://en.cppreference.com/w/cpp/language/class_template.
[11] cppreference.com. Function template[EB/OL].Cppreference.com,2021-12-07[2021-01-05].https://en.cppreference.com/w/cpp/language/function_template.
[12] Wikipedia contributors. C++11[EB/OL].United States:Wikimedia Foundation,2021-12-24[2022-01-05].https://en.wikipedia.org/wiki/C++11.
[13] codekiddy et al. using extern template (C++11)[EB/OL].StackOverflow,2014-10-27[20212-01-05].https://stackoverflow.com/questions/8130602/using-extern-template-c11.
[14] cppreference.com. Scope[EB/OL].Cppreference.com,2021-09-15[2021-01-06].https://en.cppreference.com/w/c/language/scope.
[15] cppreference.com. Scope[EB/OL].Cppreference.com,2022-01-01[2021-01-06].https://en.cppreference.com/w/cpp/language/scope.
[16] cppreference.com. External and tentative definitions[EB/OL].Cppreference.com,2020-08-18[2021-01-07].https://en.cppreference.com/w/c/language/extern.
[17] cppreference.com. Declarations[EB/OL].Cppreference.com,2021-06-23[2021-01-07].https://en.cppreference.com/w/c/language/declarations.