为什么C/C++要分为头文件和源文件?-无法读源文件或磁盘
喜欢的可以收藏转发加关注
这是否和外部调用有关?为什么现在大多数语言都没有采用这种设计?为什么调用dll有时需要使用Windows提供的API导出函数或者结构,而不能直接include xxxx.h或者像C#写的dll那样在项目中添加引用然后直接using xxxx。
我试着从C/C++历史演变的角度回答下这个问题。
上世纪70年代初,C语言初始版本被设计出来时,是没有头文件的。这一点与后世的Java只有 .java 文件,C#只有 .cs 文件很相似。即使是现代的C编译器,头文件也不是必须的。我使用下面这个例子说明:
// alpha.c
int main() {
print_hello();
}
// beta.c
void print_hello() {
puts("hello");
}
上例只有两个源文件,alpha.c 与 beta.c 。其中 alpha.c 使用了一个自定义函数 print_hello ,beta.c 中使用了标准库函数 puts 。注意:alpha.c 与 beta.c 都没有包含任何头文件。
你可以使用MS CL编译器来编译:
cl /Fe:program.exe alpha.c beta.c
或者 GCC 以及 Clang:
clang -o program alpha.c beta.c
这样会得到一个名为 program 的可执行文件,并且它可以正常工作。
以 beta.c 为例:当 beta.c 被编译时,编译器解析到名为 puts 的符号,虽然它是未定义的,但从语法上可以判断 puts 是一个函数,故而将其认定为函数,作为外部符号等待链接就可以了(倘若 alpha ,beta 是 C++ 源文件,编译无法通过,这个后文会做解释)。
下面我用ASCII字符绘制的“编译”与“链接”流程图:
alpha.c -> alpha.obj
\
program.exe
/
beta.c -> beta.obj
相信这个流程作为基础知识已广为人知,我就不再赘述了。问题在于:当初为什么要采用这样的设计 ?将“编译”、“链接”两个步骤区分开,并让用户可知是什么意图 ?
其实这是上世纪60、70年代各语言的“套路”做法,因为各个 obj 文件可能并不是同一种语言源文件编译得到的,它们可能来自于 C,可能是汇编、也可能是 Fortran 这样与 C 一样的高级语言。即是说“编译”、“链接”的流程其实是这样的:
alpha.c -> alpha.obj
\
beta.asm -> beta.obj --> program.exe
/
gamma.f -> gamma.obj
所以,编译阶段C源文件(当然也包括其它语言的源文件)是不与其它源文件产生关系的,因为编译器(这里指的是狭义的编译器,不包括链接器)本身有可能并不能识别其它源。
说到这里,定然有人要问:连函数参数和返回值都不知道,直接链接然后调用,会不会出现问题。答案是:不会,至少当时不会。因为当时的C只有一种数据类型,即“字长”(同时代的大多数语言也一样)。
我们考虑这样一个函数调用:
n = add(1, 2, 3, 4);
[1] 首先,add函数的调用者,将4个参数自右向左压入栈,即是说压栈完成后 1 在栈顶,4在栈底;[2] 然后,add被调用,对于被调用者(也就是 add)而言,栈长度是不可知的,但第一个参数在栈顶,往下一个字长就是第二个参数,以此类推,所以栈长度不可知并不会带来问题;[3] add 处理完成后,将返回值放入数据寄存器,并返回;[4] 调用者弹栈,因为压栈操作是调用者实施的,故而栈长度、压栈前栈顶位置等信息调用者是可知的,可以调用者有能力保持栈平衡。
这里说一个题外话:倘若 调用者 压栈的参数不够,那会如何?答案是 被调用者 会在栈上读到垃圾数据;又问:倘若 被调用者 没有返回值,那会如何?答案是 调用者 会在寄存器得到垃圾数据;再问:如此在代码维护上不会有问题吗?答案是从后来的实践上看,问题不大,其实可以对比下如今python、lua等弱类型语言。
通过上面的论述,我们得知C语言设计之初是没有头文件的,调用某个函数也不需要提前声明。
不过好景不长,后来出现了不同的数据类型。例如出于可移植性和内存节省的考虑,出现了 short int 、long int ;为了加强对块处理的 IO 设备的支持,出现了 char 。如此就带来了一个问题,即函数的调用者不知道压栈的长度。例如有函数调用:
add(x, y);
调用者知道 add 是一个函数,也知道需要将 x、y 压栈,但应该是先压2个字节、再压4个字节喃,还是先压4个字节,再压2个字节喃;还是连续压2个4字节喃?
这里需要说明一下,在上世纪80年代intel 8084系的处理器普及以前,并没有公认的“字节(byte)”概念,以上只是我举例方便。
紧接着结构体等特性陆续引入,问题变得更复杂。在这种情况下,函数调用需要提前声明,以便让调用者得知函数的参数与返回值尺寸(结构体使用也需要提前声明,以便让调用者知道其成员、尺寸、内存对其规则等,这里不赘述了)。
于是,头文件就出现了。这里有人可能就会问了:为什么在编译一个源文件时,不去其它源文件查找声明,就如后世的Java、C#一样。主要原因上文已经说过:C源文件在编译时不与其它源产生关系,因为其它源可能根本就不是C;此外使用 include 将声明插入到源文件中,技术实现毕竟很简单,也可以说是一种技术惯性。
又后来出现了C++,由于函数重载、模板等特性,当编译器识别到一个函数,不仅是参数与返回值尺寸,连调用哪一个函数都无法从函数名辨别了(即上文的“倘若 alpha ,beta 是 C++ 源文件,编译无法通过,这个后文会做解释”一语)。函数与数据结构需要提前声明才能使用更是不可或缺。
以上。共勉。
学习从来不是一个人的事情,要有个相互监督的伙伴,工作需要学习C/C++或者为了入行、转行学习C/C++的伙伴可以私信回复小编“学习”领取全套免费C/C++学习资料、
相关内容
-
硬盘阵列设置|磁盘阵列配置全程解(图)
硬盘阵列设置|磁盘阵列配置全程解(图),,1. 磁盘阵列配置全程解...
-
为什么我的电脑里不显示磁盘了|我的电脑怎么不
为什么我的电脑里不显示磁盘了|我的电脑怎么不显示磁盘,,1. 我...
-
怎么快速整理电脑硬盘|电脑如何磁盘整理
怎么快速整理电脑硬盘|电脑如何磁盘整理,,电脑如何磁盘整理1、...
-
如何加密文件Win8Win8磁盘加密教程_windows8
如何加密文件Win8Win8磁盘加密教程_windows8,,核心提示:对于很...
-
电脑硬盘分区重装|电脑重装磁盘分区
电脑硬盘分区重装|电脑重装磁盘分区,,1. 电脑重装磁盘分区1、...
-
探探语言设置|探探怎么设置语言
探探语言设置|探探怎么设置语言,,1. 探探怎么设置语言打开探探...
-
磁盘读取错误发生错误计算机无法启动(解决方案)
磁盘读取错误发生错误计算机无法启动(解决方案),,故障现象: 计算...
-
git设置编码|git语言设置
git设置编码|git语言设置,,git设置编码点击cap4j搜索从git直接...
-
Win10系统可以修改磁盘分区图标吗?|win10系统可
Win10系统可以修改磁盘分区图标吗?|win10系统可以修改磁盘分...
-
电脑本地磁盘怎么共享|如何共享本地磁盘
电脑本地磁盘怎么共享|如何共享本地磁盘,,1. 如何共享本地磁盘...
-
老电脑硬盘整理|硬盘磁盘整理
老电脑硬盘整理|硬盘磁盘整理,,1. 硬盘磁盘整理个人移动硬盘文...
-
电脑语言怎么切换不了|电脑哪里设置语言切换
电脑语言怎么切换不了|电脑哪里设置语言切换,,电脑哪里设置语...
-
电脑磁盘名改不了名|电脑硬盘改名字不能用了怎
电脑磁盘名改不了名|电脑硬盘改名字不能用了怎么解决,,1. 电脑...
-
我的电脑本地磁盘打不开|本地磁盘打不开怎么办
我的电脑本地磁盘打不开|本地磁盘打不开怎么办,,1. 本地磁盘打...
-
Win10系统电脑磁盘清理垃圾文件的解决方法
Win10系统电脑磁盘清理垃圾文件的解决方法,解决方法,磁盘, ...