纯粹记录个人知识盲区,并系统性的做个学习

# 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 ,还会初始化变量 a0 ,因此就需要单独用关键字标识变量告诉编译器这里仅声明不定义:

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;
  ...
};

可以做一个简单的测试,分别用 __threadthread_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;
}

image-20220623113659361


# 不支持非函数声明 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++)