前段时间一个刚转到C语言的同事问我,为什么C会多一个头文件,而不是像Java和Python那样所有的代码都在源文件中。我当时回答的是C是静态语言很多东西都是需要事先定义的,所以按照惯例我们是将所有的定义都放在头文件中的。事后我再仔细想想,这个答案并不不能很好的说明这个问题。所以我在这将关于这个问题的相关内容写下来,希望给大家一点提示,也算是一个总结

include语句的本质

要回答这个问题,首先需要知道C语言代码组织问题,也就是我比较喜欢说的多文件,这个不光C语言有,几乎所有的编程语言都有,比如Python中使用import来导入新的模块,而C中我们可以简单的将include等效为import。那么问题来了,import后面的模块名称一般是相关类和对象的的的声明和实现模块,而include后面只能跟一个头文件,只有声明。其实这个认识是错误的,C语言并没有规定include只能包含头文件,include的本质是一个预处理指令它主要的工作是将它后面的相关文件整个拷贝并替换这个include语句,比如下面一个例子

//add.cpp
int add(int x, int y)
{
return x + y;
} //main.cpp
#include "add.cpp" int main()
{
int x = add(1, 2);
return 0;
}

在这个例子中我们在add.cpp文件中先定义一个add函数,然后在main文件中先包含这个源代码文件,然后在main函数中直接调用add函数,项目的目录结构如下:

在这里给大家说一个技巧,在VS中右击项目--->选择属性------>C++------>命令行,在编辑框中填入 /P,然后打开对应的文件点击编译(这里不能选生成,由于/P选项只会进行预处理并编译这一个文件,其余.cpp文件并没有编译,选生成一定会报错)

点击编译以后它会在项目的源码目录下生成一个与对应cpp同名的.i文件,这个文件是预处理之后生成的源文件。这个技巧对于调试检查和理解宏定义的代码十分重要,我们看到预处理之后的代码如下:

int add(int x, int y)
{
return x + y;
} int main()
{
int x = add(1, 2);
return 0;
}

这段代码中我把注释给删掉了,注释表示后面的代码段都是来自于哪个文件的,从代码文件来看,include被替换掉了,正是用add.cpp文件中的代码替换了,去掉之前添加的/P参数,再次点击编译,发现它报错了,报的是add函数重复定义。因为编译add.cpp时生成的add.obj中有函数add的定义,而在main文件中又有add函数的定义。我们将代码做简单的改变就可以解决这个问题,最终的代码如下:

//add.cpp
int add(int x, int y);
#ifndef __ADD_H__
int add(int x, int y)
{
return x + y;
} #endif // __ADD_H__ //main.cpp
#define __ADD_H__
#include "add.cpp" int main()
{
int x = add(1, 2);
return 0;
}

在这段代码中加了一个宏定义,如果没有定义这个宏则包含add的实现代码,否则不包含。然后在main文件中定义这个宏,表示在main中不包含它的实现,但是不管怎么样都需要在add.cpp中加上add函数的定义,否则在调用add函数时会报add函数未定义的变量或者函数

上述写法的窘境

上面只引入一个文件,我们来试试引入两个, 在这个项目中新增一个mul文件来编写一个乘法的函数

#define __ADD_H__
#include "add.cpp"
int mul(int x, int y);
#ifndef __MUL_H__
int mul(int x, int y)
{
int res = 0;
for(int i =0; i < y; i++)
{
res = add(res, x);
} return res;
} #endif

上面的乘法函数利用之前的add函数,乘法是多次累加的结果,在上面的代码中由于要使用add函数,所以先包含add.cpp文件,并定义宏保证没有重复定义,然后再写对应的算法。最后在main中引用这个函数

#define __ADD_H__
#define __MUL_H__
#include "add.cpp"
#include "mul.cpp" int main()
{
int x = add(1, 2);
x = mul(x, 2);
return 0;
}

注意这里对应宏定义和include的顺序,稍有不慎就可能会报错,一般都是报重复定义的错误,如果报错还请使用之前介绍的/P选项来排错

到这里是不是觉得这么写很麻烦?其实我在准备这些例子的时候也是这样,很多时候没有注意相关代码的顺序导致报错,而针对重复定义的报错很难排查。而这还仅仅只引入了两个文件,一般的项目中几时上百个文件那就更麻烦了

头文件的诞生

从上面的两个例子来看,其实我们只需要包含对应的声明,不需要也不能包含它的实现。很自然的就想到专门编写一个文件来包含所有的定义,这样要使用对应的函数或者变量的时候直接包含这个文件就可以了,这个就是我们所说的头文件了。至于为什么叫做头文件,这只是一个约定俗成的叫法,而以.h来命名也只是一个约定而已,我们经常看到C++的开源项目中将头文件以.hpp命名。这个真的只是一个约定而已,我们也看到了上面的例子都包含的是cpp文件,它也能编译过。

其实针对所有的变量、类、函数可以都在统一的头文件中声明,但是这么做又带来一个问题,如果我要看它的实现怎么办,那么多个文件我不可能一个个的找吧。所以这里又有一条约定,每个模块都放在统一的cpp文件中而该文件中相关内容的声明则放到与之同名的头文件中

其实我觉得这个原则在所有静态的、需要区分声明和实现的语言应该是都适用的,像我知道的汇编语言,特别是win32 的宏汇编,它也有一个头文件的思想。

C语言编译过程

在上面我基本上回答了为什么需要一个头文件,但是本质的问题还是没有解决,为什么像Python这类动态语言也有对应模块、多文件,但是它不需要像C那样要先声明才能使用?

要回答这个问题需要了解一点C/C++的编译过程。

C/C++编译的时候先扫描整个文件有没有语法错误,然后将C语句转化为汇编,当碰到不认识的变量、类、函数、对象的命名时,首先查找它有没有声明,如果没有声明直接报错,如果有,则根据对应的定义空出一定的存储空间并进行相关的指令转化:比如给变量赋值时会转化为mov指令并将、调用函数时会使用call指令。这样就解释了为什么在声明时指定变量类型,如果编译器不知道类型就不知道该用什么指令来替换C代码。同时会将对应的变量名作为符号保留。然后在符号表(这个符号表时每个代码文件都有一个)中填入该文件中定义的相关内容的符号以及它所在的首地址。最终如果未发生错误就生成了一个对应的.obj文件,这就是编译的基本过程。

编译完成之后进行链接,首先扫描所有的obj文件,先查找main函数,然后根据main函数中代码的执行流程来一一组织代码结构,当碰到之前保留的符号时,去所有的obj中的符号表中根据变量符号查找对应的地址,当它发现找到多个地址的时候就会报重复定义的错误。如果未找到对应的符号就会报函数或者变量已经声明但是未定义。找到之后会将之前obj中的符号替换为地址,比如将 mov eax num 替换成 mov eax, 0x00ff7310这样的指令。最终生成一个PE文件。

根据上面的编译过程来看,它事先会扫描文件中所有的变量定义,所以必须让编译器知道这个变量是什么。而Python是边解释边执行,所以事先不需要声明,只要执行到该处能找到定义即可。它们这点区别就解释了为什么C/C++需要声明而Python不用。


为什么C语言会有头文件的更多相关文章

  1. 转:C语言中的头文件可以自己写吗?

    转自:http://www.eefocus.com/computer00/blog/08-09/155791_9ebdc.html 一些初学C语言的人,不知道头文件(*.h文件)原来还可以自己写的. ...

  2. C语言中的头文件

    1.头文件#include <> :表示引用标准库头文件,编译器会从系统配置的库环境中去寻找 2.头文件#include "":一般表示用户自己定义使用的头文件,编译器 ...

  3. C语言笔记 11_头文件&强制类型转换&错误处理&递归

    头文件 头文件是扩展名为 .h 的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享.有两种类型的头文件:程序员编写的头文件和编译器自带的头文件. 在程序中要使用头文件,需要使用 C 预处理指 ...

  4. 正确使用c语言中的头文件

    我们在使用c编程的时候经常会遇到头文件,前段时间我自己做了个小项目的时候,也遇到了关于头文件的问题. 预处理器发现#include 指令后,就会寻找后跟的文件名并把这个文件包含的内容包含到当前文件中. ...

  5. 整理一下C++语言中的头文件

    对于每一个像我一样的蒟蒻来说,C++最重要的东西就是头文件的使用了.由于初学,直到现在我打代码还是靠一些事先写好的的头文件,仍然不能做到使用自己需要的.最近看了几位大佬打代码,心中突然闪过要把自己冗长 ...

  6. C语言库函数,头文件

    参看:https://zhidao.baidu.com/question/328173842.html 该文件包含了的C语言标准库函数的定义 stdlib.h里面定义了五种类型.一些宏和通用工具函数. ...

  7. C语言之在头文件中定义全局变量

    通常情况下,都是在C文件中定义全局变量,在头文件中声明,但是,如果我们定义的全局变量需要被很多的C文件使用的话,那么将全局变量定义在头文件里面会方便很多,那到底是如何实现的? os_var.c文件内容 ...

  8. C语言之标准头文件模板

    /*======================================================================================* * 版权 : xxx ...

  9. C语言头文件

    最近在工作当中遇到了一点小问题,关于C语言头文件的应用问题,主要还是关于全局变量的定义和声明问题.学习C语言已经有好几年了,工作使用也近半年了,但是对于这部分的东西的确还没有深入的思考过.概念上还是比 ...

随机推荐

  1. 浏览器HTTP_USER_AGENT汇总——Firefox、Chrome、IE9、IE8、IE7、IE6

    结论:  浏览器 \ OS XP(IE6) XP(IE7) XP(IE8) Win7 x64(IE9) 猎豹浏览器2.0急速模式     Chrome/21 猎豹浏览器2.0兼容模式 IE6     ...

  2. 【python】10分钟教你用python一行代码搞点大新闻

    准备 相信各位对python的语言简洁已经深有领会了.那么,今天就带大家一探究竟.看看一行python代码究竟能干些什么大新闻.赶紧抄起手中的家伙,跟我来试试吧. 首先你得先在命令行进入python. ...

  3. P4854 MloVtry的咸鱼树 状压+最短路

    $ \color{#0066ff}{ 题目描述 }$ 俗话说种瓜得瓜,种豆得豆,MloVtry把自己砍掉一半埋进了土里,于是它得到了一颗n个点的咸鱼树. 但是问题是由于MloVtry只舍得埋下一半的自 ...

  4. P4855 MloVtry的idea

    $ \color{#0066ff}{ 题目描述 }$ MloVtry是一个脑洞很大的人,它总会想出一些奇奇怪怪的idea. 可问题是,MloVtry作为一个蒟蒻,很多时候都没办法解决自己提出的问题,所 ...

  5. docker 安装 postgresql

    docker拉去镜像以及配置生成容器的步骤几乎和之前的nginx安装一样,直接写下面的命令了 1. docker pull postgres 2. docker run --name xf-postg ...

  6. Tensorflow基础-mnist数据集

    MNIST数据集,每张图片包含28*28个像素,把一个数组展开成向量,长度为28*28=784,故数据集中mnist.train.images是一个形状为[60000,784]的张量,第一个维度数字用 ...

  7. computed 和 watch 组合使用,监听数据全局数据状态

    我要实现的就是,当接口返回数据时,我在任何组件中都能感知到到该数据的变化,然后根据业务逻辑进行处理.展示. 实现这个效果的方式很多,比如当接口返回数据后,就emit这数据,在另外组件中on接收渲染即可 ...

  8. BZOJ - 4260 01字典树+前后缀

    题意:给定\(a[1...n]\),求\((a_i⊕a_i+1⊕...⊕a_j)+(a_p⊕a_{p+1}⊕...⊕a_{q})\)的最大值,其中\(1≤i≤j<p≤q≤n\) 前后缀最优解预处 ...

  9. an concreate example

    Step 1: Creating Parts 1. Split the geometry 2. Create the INLET part. 3. Create the OUTLET part. 4. ...

  10. lambda 表达式定制操作

    泛型算法中的定制操作 许多算法都会比较输入序列中的元素以达到排序的效果,通过定制比较操作,可以控制算法按照编程者的意图工作. 普通排序算法: template<class RandomItera ...