与C/C++关键字extern有关的原理
关键字有一定的语义,但是用法不唯一。
对于C/C++语言的预编译、编译、汇编、链接。我相信大家在接触C++一年不到就背的滚瓜烂熟,但是其中的细节,是后来才慢慢想明白的。为什么我不讲extern关键字呢?extern关键字的渊源深着呢,耐心学完前面的内容,extern的神秘面纱自然就解开了。
众所周知,C语言的出现先于C++,而汇编语言的出现又先于C。但是不管你用它们中任何一门语言编写程序。编译后都生成一个可执行的程序(前提是代码没有语法错误)。
对于使用汇编写好的程序,我们只需要把汇编源代码交给汇编器,汇编器就输出可执行文件给我们。
对于C语言写好的程序,我们把C源代码交给预处理器,预处理器把C源代码中的宏“消化”掉,生成纯C源代码,然后把纯C代码交给编译器,编译器输出相应的汇编代码,之后的处理方式就跟汇编写的一样了,交给汇编器,汇编代码与最后的可执行代码可以说是一一对应的。使用C比使用汇编方便,因为编译器为我们做了很多固定的事情,但是前提是你必须明白编译器的原理,不然就乱套了。
随着软件规模的发展,后来的人们发现 有的软件功能太多了,全部放在一个main.c文件中真的太大了,逻辑混乱,不好读。另外还有更重要的一点,在A程序中写过的一个功能,在B程序中又要使用同样的功能,需要去A的源代码里面找到,复制过来,复制错了还麻烦。所以出现了一个重要的概念,“多文件编译”:每一个.c文件经过一个编译流程都能生成一个目标文件。即使.c文件没有main函数,也能生成一个目标文件。这里我们约定,没有main函数的.c文件编译出来的叫做目标文件,含有main函数的.c文件编译出来的叫做待链接可执行文件。一个目标文件可以调用其他任何一个目标文件中定义的函数代码、变量,前提是另外一个目标文件“允许”,如何允许或者拒绝呢?这里就靠我们使用extern和static、auto这类关键词控制了,如何控制我们之后再细谈。
链接器就是这个时候产生的,一个待链接可执行文件+n个相关的目标文件=可执行文件,这里的n可以等于0。这样就形成了多文件编译,虽然过程复杂了点,但是把一个main.c分割成多个源文件,解决了上面说到的两个严重问题。当然你不喜欢链接也可以不链接,把所有内容写在main.c里就不需要链接过程了。我们想要编写应用程序时,第一件事就是创建一个.c文件,在里面写一个main函数,然后开始编写main函数。我们在main函数中可以调用一些其他的函数,这些函数可以不跟main函数在一个源文件里,注意,不在同一个源文件也就意味着不在同一个目标文件里了,不在同一个目标文件,就需要跨目标文件调用,这是链接器的工作,我们只需要把main函数可能用到的所有目标文件丢给链接器,链接器会根据函数名和调用关系把他们链接生成一个完整的可执行文件。所有的目标文件都可以互相调用,但是我们在编写程序时,大多数这个调用关系是单向的,最后汇总到main函数。如果一个函数(不管在哪个文件里)没有被main函数直接或者间接的使用,那他就没有被链接的必要,链接器在链接的时候就不把他链接进可执行文件里。
多文件编译 全靠 链接器 支持,链接器怎么这么神奇?它为什么知道要怎么链接?哈哈,其实链接器并没有你想的那么神奇,它并不是凭空就把目标文件链接起来了。在它链接之前,我们需要给它留下一些记号、给它一些提示。要是提示的不明显、有二义性,它就立即报错让你链接失败,这一点想必经历过多文件编译的人都深有体会。有语法错误编译器会提示我语法错误在第几行。但是有时明明没有语法错误,链接器还给我报满满一屏幕的编译错误,而且还不告诉我在第几行,这个就是链接错误。链接错误很简单,所有的链接错误都是链接器在说:根据你给我留下的提示,我找不到你想要调用的函数在哪,或者我找到了好多个可行选择,不知道选哪个。。。
接下来重点来了,我告诉你如何提示链接器正确的链接。接下来我会用到3个关键词extern,static,auto。
纯C语言源代码由全局函数和全局变量组成(局部变量总是包含在全局函数里,算是全局函数的子集,就不作数了),对于函数一共有这3种写法:int fun(); static int fun(); extern int fun()。对于变量一共有4种写法:int val; auto int val; static int val; extern int val;
额,为什么不对称呢,函数为什么不能auto int fun();呢?
接下来我说几个C链接器的游戏规则:
写int fun();和写extern int fun();是一样的意思。意思是:这个函数可以让其他目标文件调用。而static int fun();的意思相反,不让其他目标文件看到这个函数,让他们在链接时找不到,只有在本文件内才能调用static修饰的函数。对于函数,就这么两个规则。
对于全局变量,写int val;和auto int val;意思一样,都是让其他源文件可以访问。而写static int val;意思就是只能在本文件内部使用。写extern int val;的意思就是:val变量不是在这里产生的,而是其他文件定义的,我想调用它,事先声明一下,这就叫做扩展声明。而函数就不需要扩展声明,因为没有函数体的函数都是函数声明,所以不需要留有关键词做提示。
现在大概明白这三个关键词的语义了吧。static最直当明了,沾到它别的文件就别想访问了。而auto几乎没什么用,几乎没人会在这里用auto(c++11中auto关键词可以用来让编译器自动推导出类型,使代码简洁)。
再后来,有了C++这门语言,编译器为我们做了更多的事情,比C编译器做的还多。最简单的,重载函数,为了支持重载函数,编译器会在编译时为它们重命名。这时如果你不知道C++的游戏规则,可能又会使链接器找不到链接对象了。