纯粹记录个人知识盲区,并系统性的做个学习
# C++ 中的 extern
extern 可以应用于「变量」「函数」和「模板」,用于只声明不定义。
这里有个比较严格的规则:所有常规「变量」「函数」「模板」可以多次声明,只能有一次定义;这个很好理解,如果定义多次的情况下,编译器也不清楚改用哪次的定义作为最终的结果。
这里先做个简单的测试:
// test_extern.h | |
#pragma once | |
int a; | |
void func() {} | |
template<typename T1> | |
void func(T1 t){} | |
// test_extern1.h | |
#pragma once | |
int a; | |
void func() {} | |
template<typename T1> | |
void func(T1 t){} | |
// main.cpp | |
#include "test_extern.h" | |
#include "test_extern1.h" | |
int main(int argc, char **argv) | |
{ | |
return 0; | |
} |
g++ -c main.cpp
/* ----------------------- output ----------------------- */ | |
In file included from main.cpp:2:0: | |
test_extern1.h:2:5: 错误:‘int a’ 重定义 | |
int a; | |
^ | |
test_extern.h:2:5: 附注:‘int a’ previously declared here | |
int a; | |
^ | |
In file included from main.cpp:2:0: | |
test_extern1.h: 在函数‘void func()’中: | |
test_extern1.h:3:6: 错误:‘void func()’ 重定义 | |
void func() {} | |
^~~~ | |
test_extern.h:3:6: 附注:‘void func()’ previously defined here | |
void func() {} | |
^~~~ | |
In file included from main.cpp:2:0: | |
test_extern1.h: 在全局域: | |
test_extern1.h:5:6: 错误:‘template<class T1> void func(T1)’ 重定义 | |
void func(T1 t){} | |
^~~~ | |
test_extern.h:5:6: 附注:‘template<class T1> void func(T1)’ previously declared here | |
void func(T1 t){} | |
^~~~ |
这里提示「变量」、「函数」和「模板函数」都重定义了。比较反直觉的是: int a;
被视作定义而非声明。
我们都知道,如果需要声明一个函数,可以使用如下写法:
// 函数声明 | |
void func() | |
// 模板函数声明 | |
template<typename T1> | |
void func(T1 t) |
由于 func()
本身没有二义性,虽然编译器会自动加上 extern 将其视为如下代码,但两者其实是等效的:
// 函数声明 | |
extern void func() | |
// 模板函数声明 | |
template<typename T1> | |
extern void func(T1 t) |
那么变量呢?这里因为变量比较特殊 int a;
不但会声明变量 a
,还会初始化变量 a
为 0
,因此就需要单独用关键字标识变量告诉编译器这里仅声明不定义:
extern int a; |
这便是 extern 的第一个作用。
# GCC 中做了什么
之前介绍了 extern 的作用,但都比较偏向理论,下面来看看 GCC 编译过程中是如何处理的,这里以 GCC 7.3.1 版本为例:
首先 GCC 会根据词法分析规则,获取到声明中附带的一些关键字,这里我们暂且只看 extern 关键字,这里给声明打上了 csc_extern
的标记
// c-decl.c | |
case RID_EXTERN: | |
n = csc_extern; | |
/* Diagnose "__thread extern". */ | |
if (specs->thread_p && specs->thread_gnu_p) | |
error ("%<__thread%> before %<extern%>"); | |
break; |
specs->thread_p && specs->thread_gnu_p
则表示这个声明之前不允许存在关键字 _thread
,但可以用 thread_local
:
struct c_declspecs { | |
... | |
/* Whether "__thread" or "_Thread_local" was specified. */ | |
BOOL_BITFIELD thread_p : 1; | |
/* Whether "__thread" rather than "_Thread_local" was specified. */ | |
BOOL_BITFIELD thread_gnu_p : 1; | |
... | |
}; |
可以做一个简单的测试,分别用 __thread
和 thread_local
:
// main.cpp | |
int main(int argc, char **argv) | |
{ | |
thread_local extern int a; | |
__thread extern int b; | |
return 0; | |
} | |
// gcc -c main.cpp | |
/* ----------------------- output ----------------------- */ | |
main.cpp: 在函数‘int main(int, char**)’中: | |
main.cpp:6:5: 警告:‘__thread’出现在‘extern’之前 | |
__thread extern int b; | |
^~~~~~~~ |
接下来我们再看看 csc_extern
都有哪些限制规则:
# thread_local 声明必须同时是 static 或者 extern
if (n != csc_extern && n != csc_static && specs->thread_p) | |
{ | |
error ("%qs used with %qE", specs->thread_gnu_p ? "__thread" : "_Thread_local", scspec); | |
specs->thread_p = false; | |
} |
# 不支持非函数声明 extern 并初始化
针对声明在不同的作用域会有不同的提示:
else if (storage_class == csc_extern | |
&& initialized | |
&& !funcdef_flag) | |
{ | |
/* 'extern' with initialization is invalid if not at file scope. */ | |
if (current_scope == file_scope) | |
{ | |
/* It is fine to have 'extern const' when compiling at C | |
and C++ intersection. */ | |
if (!(warn_cxx_compat && constp)) | |
warning_at (loc, 0, "%qE initialized and declared %<extern%>", name); | |
} | |
else | |
error_at (loc, "%qE has both %<extern%> and initializer", name); | |
} |
简单的测试一下:
// main.cpp | |
extern int a = 1; | |
int main(int argc, char **argv) | |
{ | |
extern int b = 1; | |
return 0; | |
} | |
// gcc -c main.cpp | |
/* ----------------------- output ----------------------- */ | |
main.cpp:3:12: 警告:‘a’已初始化,却又被声明为‘extern’ | |
extern int a = 1; | |
^ | |
main.cpp: 在函数‘int main(int, char**)’中: | |
main.cpp:6:16: 错误:‘b’既有‘extern’又有初始值设定 | |
extern int b = 1; | |
^ |
# 嵌套函数不允许声明 extern
这个好像 C++ 不支持嵌套,只能用 lambda 来实现因此不会有这种问题。
if (storage_class == csc_extern && funcdef_flag) | |
error_at (loc, "nested function %qE declared %<extern%>", name); |
此外还有很多其他的限制规则,这里就不一一展开。
# 总结
总的来说 extern 用于在使用前对对象进行声明,对象的定义是否存在会在链接的时候通过符号表进行查找。
# 参考资料
- 理解 C 中的 “extern” 关键字
- gcc7 源码
- Microsoft-extern(C++)