共计 3917 个字符,预计需要花费 10 分钟才能阅读完成。
导读 | 如果一个文件 (假设文件名 A) 要大量引用另一个文件 (假设文件名 B) 中定义的变量或函数,则使用头文件效率更高,程序结构也更规范。 |
用 #include 可以包含其他头文件中变量、函数的声明,为什么还要 extern 关键字?
如果我想引用一个全局变量或函数 a,我只要直接在源文件中包含 #include
这个问题一直也是似是而非的困扰着我许久,经过实践和查找资料,有如下总结:
首先说下头文件,其实头文件对计算机而言没什么作用,她只是在预编译时在 #include 的地方展开一下,没别的意义了,其实头文件主要是给别人看的。
我做过一个实验,将头文件的后缀改成 xxx.txt,然后在引用该头文件的地方用 #include “xxx.txt”, 编译,链接都很顺利的过去了,由此可知,头文件仅仅为阅读代码作用,没其他的作用了!
不管是 C 还是 C ++,你把你的函数,变量或者结构体,类啥的放在你的.c 或者.cpp 文件里。然后编译成 lib,dll,obj,.o 等等,然后别人用的时候,最基本的 gcc hisfile.cpp yourfile.o|obj|dll|lib 等等。
但对于我们程序员而言,他们怎么知道你的 lib,dll… 里面到底有什么东西?要看你的头文件。你的头文件就是对用户的说明。函数,参数,各种各样的接口的说明。
那既然是说明,那么头文件里面放的自然就是关于函数,变量,类的 ” 声明 ”(对函数来说,也叫函数原型)了。记着,是 ” 声明 ”,不是 ” 定义 ”。
那么,我假设大家知道声明和定义的区别。所以,最好不要傻嘻嘻的在头文件里定义什么东西。比如全局变量:
/*xx 头文件 */
#ifndef _XX_头文件.H
#define _XX_头文件.H
int A;
#endif
那么,很糟糕的是,这里的 int A 是个全局变量的定义,所以如果这个头文件被多次引用的话,你的 A 会被重复定义,显然语法上错了。只不过有了这个 #ifndef 的条件编译,所以能保证你的头文件只被引用一次,不过也许还是不会出岔子,但若多个 c 文件包含这个头文件时还是会出错的,因为宏名有效范围仅限于本 c 源文件,所以在这多个 c 文件编译时是不会出错的,但在链接时就会报错,说你多处定义了同一个变量:
Linking...
incl2.obj : error LNK2005: "int glb" (?glb@@3HA) already defined in incl1.obj
Debug/incl.exe : fatal error LNK1169: one or more multiply defined symbols found
这个关键字真的比较可恶,在定义变量的时候,这个 extern 居然可以被省略(定义时,默认均省略);在声明变量的时候,这个 extern 必须添加在变量前,所以有时会让你搞不清楚到底是声明还是定义。或者说,变量前有 extern 不一定就是声明,而变量前无 extern 就只能是定义。注:定义要为变量分配内存空间;而声明不需要为变量分配内存空间。
下面分变量和函数两类来说:
(1)变量
尤其是对于变量来说:
extern int a; // 声明一个全局变量 a
int a; // 定义一个全局变量 a
extern int a =0 ; // 定义一个全局变量 a 并给初值。int a =0; // 定义一个全局变量 a, 并给初值,
第四个等于第三个,都是定义一个可以被外部使用的全局变量,并给初值。
糊涂了吧,他们看上去可真像。但是定义只能出现在一处。也就是说,不管是 int a;还是 extern int a=0;还是 int a=0; 都只能出现一次,而那个 extern int a 可以出现很多次。
当你要引用一个全局变量的时候,你就必须要声明,extern int a; 这时候 extern 不能省略,因为省略了,就变成 int a; 这是一个定义,不是声明。注:extern int a; 中类型 int 可省略,即 extern a; 但其他类型则不能省略。
(2)函数
函数,对于函数也一样,也是定义和声明,定义的时候用 extern,说明这个函数是可以被外部引用的,声明的时候用 extern 说明这是一个声明。但由于函数的定义和声明是有区别的,定义函数要有函数体,声明函数没有函数体(还有以分号结尾),所以函数定义和声明时都可以将 extern 省略掉,反正其他文件也是知道这个函数是在其他地方定义的,所以不加 extern 也行。两者如此不同,所以省略了 extern 也不会有问题。
比如:
/* 某 cpp 文件 */
int fun(void)
{return 0;}
很好,我们定义了一个全局函数:
/* 另一 cpp 文件 */
int fun(void);
我们对它做了个声明,然后后面就可以用了, 加不加 extern 都一样, 我们也可以把对 fun 的声明放在一个头文件里,最后变成这样:
/*fun.h*/
int fun(void); // 函数声明,所以省略了 extern,完整些是 extern int fun(void);
/* 对应的 fun.cpp 文件 */
int fun(void)
{return 0;}// 一个完整的全局函数定义,因为有函数体,extern 同样被省略了。
然后,一个客户,一个要使用你的 fun 的客户,把这个头文件包含进去,ok,一个全局的声明。没有问题。
但是,对应的,如果是这个客户要使用全局变量,那么要 extern 某某变量;不然就成了定义了。
总结:对变量而言,如果你想在本源文件 (例如文件名 A) 中使用另一个源文件 (例如文件名 B) 的变量,方法有 2 种:(1)在 A 文件中必须用 extern 声明在 B 文件中定义的变量 (当然是全局变量);(2) 在 A 文件中添加 B 文件对应的头文件,当然这个头文件包含 B 文件中的变量声明,也即在这个头文件中必须用 extern 声明该变量,否则,该变量又被定义一次。对函数而言,如果你想在本源文件 (例如文件名 A) 中使用另一个源文件 (例如文件名 B) 的函数,方法有 2 种:(1)在 A 文件中用 extern 声明在 B 文件中定义的函数 (其实,也可省略 extern,只需在 A 文件中出现 B 文件定义函数原型即可);(2) 在 A 文件中添加 B 文件对应的头文件,当然这个头文件包含 B 文件中的函数原型,在头文件中函数可以不用加 extern。
对上述总结换一种说法:
(a)对于一个文件中调用另一个文件的全局变量,因为全局变量一般定义在原文件.c 中,我们不能用 #include 包含源文件而只能包含头文件,所以常用的方法是用 extern int a 来声明外部变量。另外一种方法是可以是在 a.c 文件中定义了全局变量 int global_num,可以在对应的 a.h 头文件中写 extern int global_num,这样其他源文件可以通过 include a.h 来声明她是外部变量就可以了。
(b)还有变量和函数的不同举例 int fun(); 和 extern int fun(); 都是声明(定义要有实现体)。用 extern int fun() 只是更明确指明是声明而已。而 int a; 是定义 extern int a; 是声明。
(3)此外,extern 修饰符可用于 C++程序中调用 c 函数的规范问题。
比如在 C++中调用 C 库函数,就需要在 C++程序中用 extern “C” 声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用 C 函数规范来链接。主要原因是 C++和 C 程序编译完成后在目标代码中命名规则不同。
C++语言在编译的时候为了解决的多态问题,会将名和参数联合起来生成一个中间的名称,而 c 语言则不会,因此会造成链接时找不到对应的情况,此时 C 就需要用 extern “C” 进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中间名。
这种联系也解决了最初提出的 2 个问题:
(a)用 #include 可以包含其他头文件中变量、函数的声明,为什么还要 extern 关键字?
(b)如果我想引用一个全局变量或函数 a,我只要直接在源文件中包含 #include (xxx.h 包含了 a 的声明)不就可以了么,为什么还要用 extern 呢??
答案:如果一个文件 (假设文件名 A) 要大量引用另一个文件 (假设文件名 B) 中定义的变量或函数,则使用头文件效率更高,程序结构也更规范。其他文件 (例如文件名 C、D 等) 要引用文件名 B 中定义的变量或函数,则只需用 #include 包含文件 B 对应的头文件 (当然,这个头文件只有对变量或函数的声明,绝不能有定义) 即可。
那是一个被遗忘的年代,那时,编译器只认识.c(或.cpp)文件,而不知道.h 是何物的年代。
那时的人们写了很多的.c(或.cpp)文件,渐渐地,人们发现在很多.c(或.cpp)文件中的声明变量或函数原型是相同的,但他们却不得不一个字一个字地重复地将这些内容敲入每个.c(或.cpp)文件。但更为恐怖的是,当其中一个声明有变更时,就需要检查所有的.c(或.cpp)文件,并修改其中的声明,啊~,简直是世界末日降临!
终于,有人(或许是一些人)再不能忍受这样的折磨,他(们)将重复的部分提取出来,放在一个新文件里,然后在需要的.c(或.cpp)文件中敲入 #include XXXX 这样的语句。这样即使某个声明发生了变更,也再不需要到处寻找与修改了 — 世界还是那么美好!
因为这个新文件,经常被放在.c(或.cpp)文件的头部,所以就给它起名叫做 ” 头文件 ”,扩展名是.h。
从此,编译器(其实是其中预处理器)就知道世上除了.c(或.cpp)文件,还有个.h 的文件,以及一个叫做 #include 命令。
![post-qrcode](https://www.xgss.net/wp-content/uploads/2023/07/二维码-微信公众号256x256.jpg)