声明和定义
前言
很多编程语言的语法中都有关于声明和定义的概念,这种概念一般会应用于函数或变量的创建和使用中,但是为什么要这么做?
以C语言为例,一些书籍或教程会要求读者在程序文件开头写上函数和变量的声明,然后再在后面对其进行定义(对于变量也可以叫初始化)。这不免让人感到有一丝疑惑,为什么要这样做?我能不能先定义再声明?或者我能否不声明直接定义?
事实上这样做很多时候也是可以的,但是为什么呢?
为此我们需要明确一些基本的概念。
编译和链接
以C和C++为例,如果你正在使用某个IDE进行编程,那么你应该可以发现,在你写好一个程序后需要将它运行之前,往往需要先进行构建(有的IDE里面写的是生成或build),然后经过一段时间的等待后,你就会获得一个可执行程序,之后你就可以运行这个程序文件得到想要的效果。
这个过程中发生了什么呢?一般情况下这个过程分为了三步——预处理、编译、链接。预处理过程一般是C和C++的特色,你所见到的那些带#
的语句一般被称为宏,而这些宏将会被预处理器进行预处理。通常而言,你只需要知道#define
(将被定义的内容用定义的内容进行替换)和#include
(将被引用的文件全部原样复制到此)两个宏就够了。
不过预处理并不是我们讨论的焦点。
编译一般发生于预处理之后,一般而言,当预处理结束后,所有有用的.h
文件都已经通过#include
被原样复制到了.c
文件中了,所以整个工程现在就只剩下一堆.c
文件。而编译器会检测每个.c
文件然后将它们编译称为对应的汇编文件,再然后将汇编文件翻译成二进制文件。不过一般而言,翻译成为汇编文件这个过程都是被隐藏的,至少如果不单独设置你是无法看到生成汇编文件这个过程。不过没关系,这样也可以简化我们的思路,所以直接将编译过程理解为从.c
文件到二进制文件(这个文件后缀根据编译器和系统的不同可能有所差异,一般是.o
或.obj
,当然具体是什么后缀都不重要)。
编译结束后,显然我们会得到一堆二进制文件,它们和之前写的.c
文件一般是一一对应的。但是很显然,这些二进制文件还不是可执行程序,而且它们太分散了。这个时候就需要链接器进行工作,链接器会根据链接表将所有二进制文件进行重组和拼装,最后形成一个完整的可执行文件。
声明和定义
那么,声明和定义在这里有什么用呢?
为什么要声明
现在我们假设一个工程中有两个.c
文件,一个是main.c
,里面放了一个空的main函数,此外什么也没有。另一个是hello.c
,里面放了一个void SayHello()
函数并引用了stdio.h
文件,这个函数的功能是输出一个Hello World
。
显然,hello.c
里面实现了对SayHello
函数的定义,也就是说我们的工程中已经实现了SayHello
这个函数,此时我们是否能够在main
函数中调用它呢?
如果你尝试直接在main
函数中调用SayHello
,然后分别对main.c
和hello.c
进行编译(一般IDE会有单独的编译选项,右键单击某个c文件就可以选择,而且这个编译事实上还是包括了预处理过程的),你会发现对main.c
进行编译的时候会出现报错。
编译器会提示你,没有对SayHello
函数进行定义。
但是我们明明在hello.c
中进行定义了啊?难道是我需要先编译一下hello.c
然后再编译main.c
?事实上你换一下顺序结果也是一样。
因为编译器是没有记忆的,当他编译完一个文件,就会马上忘记它在这个文件里都发现了什么东西,所以哪怕它刚刚才在hello.c
里面编译了SayHello
的定义,但是现在它在编译main.c
,它就已经忘记了这回事了。
那么我们有什么办法可以解决这种情况呢?
第一种方法就是,在main.c
的前面加上对SayHello
的定义,注意尤其一定要在main
函数的前面。有人会问,为什么一定要在前面,后面不行吗?事实上确实不行,因为当编译器发现一个它从来没见过的函数调用时,就会马上报错,不会那么智能地再在后面找一遍你是不是定义在后面了,所以唯有在调用之前就定义好,他才知道你有好好定义这个函数,然后它就不会闹了。
但是显然,如果我希望在多个文件里面调用SayHello
函数,那么这个方法就不起效了。
所以我们就有了第二种方法,在main.c
文件前面加上一句对SayHello
的声明。声明只需要写上函数返回值、函数名和函数参数表就可以了,不需要写完整的实现。声明其实是程序员对编译器的保证,意思就是“我保证我在其它某个地方肯定会写这个函数的定义的”,然后编译器会相信程序员,虽然你并没有实际在这个文件中定义这个函数,但是当你调用这个函数的时候,编译器不会报错。注意,声明也需要写在调用的前面,逻辑和第一种方法一样。
现在你大概理解了声明的意思了,而一般情况下,声明会被写在一个.h
文件中,要使用某个函数之前只要#include
这个文件并且将它对应的.c
文件包含在工程内即可。
注意,除了#include
,你一定也要将它对应的.c
文件包含在工程内,因为.c
文件里包含了对这些函数的定义。
为什么要定义
没有定义显然是不行的,否则这个函数就是一个空有名字和输入输出的不明物,很显然这是程序员的疏忽(或像我们故意)造成的错误,是需要被发现检测出来的。
现在,我们做个简单的小实验,还是刚才的工程,但是我们将hello.c
里的SayHello
注释掉(如果你不怕麻烦想要更好的体验,可以直接删掉hello.c
或将它移出工程),此时我们再次编译hello.c
和main.c
,你会发现,编译器仍然没有报错。
是不是有点疑惑,现在明显没有SayHello
的定义,但是为什么编译器不报错呢?
很显然,是因为编译器非常相信你,你在main.c
中声明了,你一定会定义SayHello
的,所以编译器就理所当然地相信了你,而没有记忆的它显然是无法发现自己被骗了。而hello.c
文件里什么都没有,那么自然也不会有什么错(如果你直接将其移除了工程就更加不可能报错了)。
但是这并不代表你能一直骗它,我们知道编译结束后还得链接才能得到可执行程序,不过IDE一般没有单独的链接按钮,所以我们得使用构建,当按下这个按钮后整个工程都将完整经历完前面说的三个过程——预处理、编译和链接。其中预处理和编译仍然是刚才我们的编译器进行的,它和刚才一样,没有发现任何问题。
但是你骗不了链接器,因为它会对程序进行重新组装,但是在组装的过程中它会发现,有一个本来应该出现的函数定义却在此刻不见了,然后它会发出一个L
字母开头的报错,唯有你在工程中重新创建了这个函数的定义并重新构建才可以消除。
总结
这里用C语言的函数声明与定义解释了为什么要声明和定义以及它们有什么用,用相同的逻辑它也可以扩展到变量以及其它语言的声明与定义中。