本来准备学习C语言中的作用域,但是在学习的过程中,发现了新的名词,函数原型。
一时间就产生好奇,函数还有原型?什么是函数原型?藏着太多疑惑,心里痒痒,就先学习一下函数原型。
函数原型
这个名称看着似乎有点抽象,但它的另一个叫法或许更好理解,函数声明。
前面学过的变量是需要声明的,需要提供变量的类型、变量名称,编译器才知道怎么使用这个变量。
函数声明也是一样的,需要提供这个函数的返回值类型、函数名称、参数列表(重点是参数的类型)等相关信息。
目的是为了告诉编译器,这家伙长什么样,怎么用。具体他会做什么事,我稍后再跟你说,你别不认识就把人赶跑了。
声明格式:
int add(int a, int b);
// 或者
int add(int, int);
看到这个,或许有的人就有疑惑了,这个不就和函数定义差不多,就是少了函数实现的具体代码块。为什么还需要这个函数原型呢?
这就不得不说一下C语言编译了。
编译问题
C语言的程序是自上而下,顺序执行的。如果遇到未知的东西,程序就会报错。
就比如没有声明变量,就直接 a=3 这样赋值,那程序就会直接报错。因为编译器从上读到这里,不知道 a 是什么东西,突然就冒出来,就会以为是什么脏东西混进来了,然后马上终止程序。
函数也是相似的。
往往我们都是主函数写在前面,而其它函数定义是写在主函数之后。那么在程序执行的时候,编译器运行到主函数中的函数调用,就发现这又是个什么脏东西跑进来。
就比如这个例子:
#include<stdio.h>
int main(){
helloWorld();
return 0;
}
void helloWorld(){
printf("Hello World!");
}
运行结果:
可以看到,结果弹出了一堆警告,但是也有运行结果。这是为什么呢?
一开始,我也不太注意这些警告,以为有结果就行了,有问题也只是编辑器的问题。
但看了函数原型之后,才发现编辑器没问题,是我寄几的问题。
来看一下编译器给出的警告都是什么意思:
在 main 函数中:
第3行第5列:警告:函数 helloWorld 的隐式声明
...
第6行第6列:警告:helloWorld 类型冲突
...
第3行第5列:注意:之前隐含的 helloWorld 声明在这里
...
在C语言当中,如果程序在前面没有遇到所调用函数的声明或者定义,就会默认生成一个隐式声明。
int helloWorld();
所以不影响函数的最终运行,但是这样是存在隐患的。
隐式声明的函数是 int 类型的,但是我们最终定义的函数是 void 类型。所以就出现了第二个警告,类型发生了冲突。
那么也有人说,我把所有函数定义都写在函数原型的位置不就好了。
如果只有主函数调用这些函数,这样做无可厚非,但是不建议。
如果有多个函数,它们之前互相调用会发生什么情况呢?
来看一个案例:
#include<stdio.h>
int func1(int a){
if(a > 3){
func2(a);
}
else{
return 0;
}
}
int func2(int a){
if(a<3){
func1(a);
}
else{
return 1;
}
}
int main(){
int x=1, y;
y = func1(x);
printf("%d", y)
return 0;
}
这里定义的两个函数,无论哪一个在前,都会出现问题。
但是有了函数声明,哪个先定义,哪个先调用,这些问题就不用考虑了。
一般建议,书写C语言程序是将函数声明放在主函数前头,例如头文件(stdio.h),函数定义放在主函数后头。这样一来,可以让代码的结构看起来清晰明了。
而且使用者往往是不在意你函数具体是怎么实现的,只在乎你的函数有什么功能,怎么用,好用吗。
所以,把函数声明放在前面,是为了说明函数的重要信息。至于函数定义里面复杂的内容,别人不想看,也不想知道,就放在后面。
且在实际开发中,一个项目往往都是成千上万行,甚至百万级别的,都是常见的。
要是把所有内容都放在一个文件里,可想而知,内容是多么的庞大。一来检索起来非常麻烦,二来文件越来越大,打开的速度也就会越来越缓慢。
因此,就需要将代码进行拆分,归类到不同的文件里。
通常是将函数声明放在头文件(.h),函数定义放在源文件(.c),使用函数的时候只需要引入头文件,编译器就会在链接阶段找到函数文件。
或许对此,大家还是有点疑惑,函数声明和函数定义为什么可以拆分成两个文件?
这就涉及到C语言编译的过程,下面简单说一下C语言编译过程。
编译过程
C语言程序从我们写下,到运行出结果,一共经历了四个步骤。
第一步:预处理。
通过预处理器,完成删除注释、宏扩展、文件包含等。
注释只是人为的添加解释,为了方便阅读代码。对程序本身没有任何影响,所以会在这一部将注释删除掉。
宏扩展,使用 #define 指令定义的一些常量或表达式。
文件包含,即 #include 指令,将另一个文件引用到我们自己的文件当中。
预处理之后,会生成一个临时文件(.i)。
第二步:编译
这个阶段是由编译器完成的,帮助我们完成语法和语义的解析,生成由汇编代码组成的文件(.s)。
这个步骤会将我们源代码中的任何语法错误和警告,通过端口窗口告诉我们。
第三步:汇编
计算机是看不懂我们的文字,什么是英文,什么是数字,一概不懂。它只能识别机器语言,二进制码。
这个阶段就是帮我们将生成的汇编文件(.s)翻译成机器码指令,然后将这些指令打包形成目标文件(.o)。
第四步:链接
这一步帮我们完成了文件中所调用的各种函数、静态库和动态库的链接。也解释了函数原型和函数定义可以分开到两个文件里的问题。
完成后,会生成一个可执行文件(.exe),运行这个文件也就能获得我们想看到的结果。
最后
函数原型到编译过程的内容结束了。其中关于编译过程的内容,只是简单的涉猎,内容或多或少有些不准确的地方,或者看不明白的地方。希望大家可以在评论区指出,一起讨论下!