extern用法小结

extern

  • extern用于声明一个全局变量或函数,它表明这个变量或函数的定义在其它文件中,并允许在当前文件中引用它。

    我们一般把所有的全局变量和全局函数的实现放在一个 *.cpp 文件里面,然后用一个同名的 *.h 文件包含所有的函数和变量的声明。如:

    /*Demo.h*/
    extern int a;
    extern int b;
    int add(int a, int b);
    
    /*Demo.cpp*/
    #include "Demo.h"
    int a = 10;
    int b = 10;
    
    int add(int l, int r)
    {
        return l + r;
    }
    

    在其它的*.cpp文件里,可以通过extern来使用个的全局变量,如:

    /*main.cpp*/
    #include "Demo.h"
    
    int main()
    {
        // 如果没有extern,编译器会认为下面的变量是在当前源文件里定义的,这将导致链接错误
        extern int a;
        extern int b;
        return a + b;
    }
    

    如果将Demo.cpp写成了Demo.c,编译器会提示无法解析的外部符号。

    因为Demo.c里面的实现会被C编译器处理,然而C++和C编译器在编译函数时存在差异,所以会存在找不到函数的情况。

  • 全局函数的声明语句中,关键字extern可以省略,因为全局函数默认是extern类型的。

    extern char g_str[] = "123456"; // 此时隐式包含extern
    

    extern是严格的声明,如果在一个文件里定义了char g_str[] = "123456";,在另外一个文件中必须使用extern char g_str[];来声明,不能使用extern char* g_str;来声明。

extern "C"

所有函数类型,所有拥有外部链接的函数名,以及所有拥有外部链接的变量名,拥有一种称作语言链接的性质。语言链接封装与以另一程序语言编写的模块进行链接的要求的集合:如调用约定、名字重整的算法等。语如果不同编程语言间编写的模块想要互相调用,那么它们之间需要满足这样的语言链接约定。

extern xx 可以为不同语言编写的程序间提供链接,在C++标准中,只有"C"与"C++"保证受支持

extern "C" 包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的,在生成编译单元时,其中声明的函数或变量要满足C语言的调用要求。

extern "C" 常用的用法有两方面:

  • 第一,是在C++中引用C语言中的函数和变量,当C++引用C语言定义的头文件时,需要做以下处理:
extern "C"{
#include "cExample.h"
}

而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern"C"时会出现编译语法错误。

  • 第二,在C中引用C++语言中的函数和变量时,C++的头文件需添加extern"C",此处也需要注意C语言不能直接引用包含了extern "C"的头文件:

像这样以C的形式声明函数或变量:

extern "C" {
    int func(int);
    int var;
}

在上面的代码中,C++的名称修饰机制将不会起作用。如果单独声明某个函数或变量为C语言的符号,也可以用如下格式:

extern "C" int func(int);
extern "C" int var;

上面两个例子有一些区别:extern "C" int var; 等价于 extern "C" { extern int var; },只对 var 进行了声明而没有定义;而 extern "C" { int var; }var进行了声明和定义

有些头文件声明了一些C语言的函数和全局变量,但这个文件可能被C语言代码或C++代码包含。为了兼容C与C++,可以使用C++的宏__cplusplus来解决:

#ifdef __cplusplus
extern "C"{
#endif

int func(int);
int var;

#ifdef __cplusplus
}
#endif

extern 和 static

存储类说明符

人们在谈论extern时,通常会将它与static进行区分,这两个关键字都属于存储类说明符,存储类说明符会控制名字的两个性质:它的存储期和它的链接性质,下面是所有的存储类说明符:

  • auto 或无说明符:自动存储期
  • register:自动存储期,另提示编译器将此对象置于处理器的寄存器。(C++17弃用)
  • static:静态或线程存储期,内部链接
  • extern:静态或线程存储期,外部链接
  • thread_local:线程存储期(C++11)
  • mutable:虽然不影响存储期或链接,但它被归为存储类说明符

我个人的理解,static的主要目的是静态存储期,extern的主要目的是声明外部链接,而成为静态存储期是为了实现这样的功能顺带达成的。(瞎猜的,别信)

extern 与 static 的区别

**(1)**extern表明该变量在别的地方已经定义过了,在这里要使用那个变量。

**(2)**static 表示静态的变量,分配内存的时候,存储在静态区,不存储在栈上面。

static作用范围是内部连接的关系这和extern有点相反。它和对象本身是分开存储的,extern也是分开存储的,但是extern可以被其他的对象用extern引用,而static不可以,只允许对象本身用它。具体的说

  • extern和static不能同时修饰一个变量;
  • 其次,static修饰的全局变量声明与定义同时进行,extern则只声明变量,而变量的定义一般在其它地方;
  • 最后,static修饰全局变量的作用域只能是本身的编译单元,也就是说它的“全局”只对本编译单元有效,其他编译单元则看不到它,如:
// test.h
static char g_str[] = "123456";
void fun1();

// test1.cpp
#include "test.h"
void fun1() 
{
    cout << g_str << endl;
}

// test2.cpp
#include "test.h"
void fun2() 
{
    cout << g_str << endl;
}

以上两个编译单元可以连接成功,当打开test1.o时,我们可以在它里面找到字符串"123456",同时也可以在test2.o中找到它们,它们之所以可以链接成功而没有报重复定义的错误是因为虽然它们有相同的内容,但是存储的物理地址并不一样,就像是两个不同变量赋了相同的值一样,而这两个变量分别作用于它们各自的编译单元。

如果查看两个编译单元里的g_str的内存地址,我们会发现它们的内存地址是一样的,这是编译器优化的导致的:在使用了相同的字符串时,这个字符串在内存内只有一份拷贝;如果将其中一个字符串改变,那么在链接时就不会只存在一份内存了,请看下面一个例子:

// test1.cpp
#include "test.h"
void fun1() 
{
    g_str[0] = 'a';
    cout << g_str << endl;
}

// test2.cpp
#include "test.h"
void fun2() 
{
    cout << g_str << endl;
}

// main.cpp
void main()
{
    fun1(); // a23456
    fun2(); // 123456
}

这个时候你在跟踪代码时,就会发现两个编译单元中的g_str地址并不相同,因为你在一处修改了它,所以编译器被强行的恢复内存的原貌,在内存中存在了两份拷贝给两个模块中的变量使用。正是因为static有以上的特性,所以一般定义static全局变量时,都把它放在原文件中而不是头文件,这样就不会给其他模块造成不必要的信息污染。

​ C++中const修饰的全局常量具有跟static相同的特性,即它们只能作用于本编译模块中,且static修饰的是全局变量,但是const可以与extern连用来声明该常量可以作用于其他编译模块中,如extern const char g_str[];,此时g_str是一个常量,并且可以被其它编译模块使用。