进一步理解 extern

首先,要区分全局变量和局部变量。

全局变量和局部变量

在 C 和 C++ 中,局部变量意义相同;全局变量略有出入。

在《C++ Primer》中很明确的表示:(p44)

名字 main 定义于所有花括号之外,它和其他大多数定义在函数体之外的名字一样拥有全局作用域。一旦声明之后,全局作用域内的名字在整个程序的范围内都可使用。

C语言中文网-C语言局部变量和全局变量 中指出:

在所有函数外部定义的变量称为全局变量(Global Variable),它的作用域是整个源程序

这一点和 C语言中文网-C语言变量作用域和生存期 中的描述是统一的:

extern (外部的) 这是在函数外部定义的变量的缺省存储方式extern 变量的作用域是整个程序。

但是,在 维基百科词条-全局变量 中则有另一番表述:

需要指出的是,C 语言不存在真正意义上的“全局变量”。被习惯性误称为“全局变量”的,一般是文件作用域对象。

文件作用域变量,作用域从声明开始一直到文件末尾。

在实际的测试中(使用的 gcc 编译器),C 中的全局变量作用域为整个程序,而不是到文件末尾。因为会报错“重复定义”,但想来和 gcc/g++ 作为 C++ 编译器也是有关的。

然后,我们来区分声明和定义。

变量声明和定义

在《C++ Primer》中指出:p41

为了允许把程序拆分成多个逻辑部分来编写,C++ 语言支持分离式编译机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。

如果将程序分为多个文件,则需要有在文件间共享代码的方法。

为了支持分离式编译,C++ 语言将声明和定义区分开来。声明(declaration)使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义(definition)负责创建与名字关联的实体。

变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。

如果想声明一个变量而非定义它,就在变量名前添加关键字 extern,而且不要显示地初始化变量,任何包含了显示初始化的声明即成为定义。

变量能且只能被定义一次,但是可以被多次声明。

如果要在多个文件中使用同一个变量,就必须将声明和定义分离。此时,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。

C语言中文网-关于C++变量的声明和定义 中描述如下:

外部变量定义和外部变量声明的含义是不同的。外部变量的定义只能有一次,它的位置在所有函数之外,而同一文件中的外部变量的声明可以有多次,它的位置可以在函数之内,也可以在函数之外。

系统根据外部变量的定义分配存储单元。对外部变量的初始化只能在定义时进行,而不能在声明中进行。

所谓声明,其作用是向编译系统发出一个信息,声明该变量是一个在后面定义的外部变量,仅仅是为了提前引用该变量而作的声明。extern 只用作声明,而不用于定义。

其中的讲解和“分离式编译”、“文件间共享代码”毫无关系,重在关联函数的声明/定义变量的声明/定义,以函数的声明/定义来讲变量的声明/定义。比如

  • 如果未在 fun2() 之前定义 fun1(),则在 fun2() 中调用 fun1() 时必须先声明 fun1(),相似的,使用外部变量时若未定义,则必须先声明。
  • 声明(函数声明/变量声明)一般放在头文件中,这样就可以在多个源文件间共享函数/变量。

extern 和 static

至此,引出externstatic关键字。

static

参考 浅谈C/C++中的static和extern关键字,其中关于extern的讲解不够精细,关于static的足够。

static 对局部变量进行修饰过后,其生命周期以及存储空间发生了变化,但是其作用域并没有改变,其仍然是一个局部变量,作用域仅限于该语句块。

对于一个全局变量,它既可以在本源文件(定义此全局变量的源文件)中被访问到,也可以在同一个工程的其它源文件中被访问(其他源文件只需用 extern 进行声明即可)。

在本源文件中(定义时)用 static 对全局变量进行修饰改变了其作用域的范围,由原来的整个工程可见变为本源文件可见。

引用另一篇帖子中的表述:

全局变量本身就是静态存储方式, 静态全局变量当然也是静态存储方式。这两者在存储方式上并无不同。这两者的区别虽在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。

把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围。

Tip A.若全局变量仅在单个 C 文件中访问,则可以将这个变量修改为静态全局变量,以降低模块间(文件间)的耦合度;

Tip B.若全局变量仅由单个函数访问,则可以将这个变量改为该函数的静态局部变量,以降低模块间(函数间)的耦合度;

Tip E.函数中必须要使用 static 变量情况:比如当某函数的返回值为指针类型时,则必须是 static 的局部变量的地址作为返回值,若为 auto 类型,则返回为错指针。

extern

在 C 语言中,修饰符 extern 用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。

extern 声明的位置对其作用域也有关系,如果是在 main 函数中进行声明的,则只能在 main 函数中调用,在其它函数中不能调用。

看到这里,应该也理解了 extern 关键字了。如果还要进一步理解可以参考 C/C++中extern关键字详解,但其实不建议通过这篇帖子学习,因为其中的逻辑层次、语言表述很让人难受啊。

二进制兼容

C++ 项目中的 extern “C” {}

不同语言编写的代码互相调用是困难的,甚至是同一种编写的代码但不同的编译器编译的代码。

为了使它们遵守统一规则,可以使用 extern 指定一个编译和连接规约。

extern "C" 指令非常有用,因为 C 和 C++ 的近亲关系。注意:extern "C" 指令中的 C,表示的一种 编译和连接规约,而不是一种语言。C 表示符合 C 语言的编译和连接规约的任何语言,如 Fortran、assembler 等。

还有要说明的是,extern "C" 指令仅指定编译和连接规约,但不影响语义。例如在函数声明中,指定了 extern "C",仍然要遵守 C++ 的类型检测、参数转换规则。

其他

在弄清楚 extern 关键字的过程中,还额外学到了一些知识:

  1. 头文件中定义实体(不只是声明)

    不能在头文件中定义变量。但是有三个例外。头文件可以定义类、值在编译时就知道的 const 对象和 inline 函数。

  2. extern 和 const

    C++const 修饰的全局常量据有跟 static 相同的特性,即它们只能作用于本编译模块中,但是 const 可以与 extern 连用来声明该常量可以作用于其他编译模块中

    我只是想提醒你,const char* g_str = "123456"const char g_str[] ="123465"是不同的, 前面(指向常量的普通指针 pointer to const)那个 const 修饰的是 char * 而不是 g_str,它的 g_str 并不是常量,它被看做是一个定义了的全局变量(可以被其他编译单元使用), 所以如果你想让 char* g_str 遵守 const 的全局常量的规则,最好这么定义 const char* const g_str="123456"(指向常量的常量指针 const pointer to const)。

    强调,C++ 和C 是不同的!