• C语言中头文件和cpp文件解析


    务必提前预读这里的内容:http://www.cnblogs.com/stemon/p/3999844.html

    回到cpp文件与头文件各写什么内容的话题上:

    理论上来说cpp文件与头文件里的内容,只要是C语言所支持的,无论写什么都可以的,比如你在头文件中写函数体实现,任何一个cpp文件包含此头文件就可以将这个函数编译成目标文件的一部分(编译是以cpp文件为单位的,如果不在任何cpp文件中包含此头文件的话,这段代码就形同虚设),你可以在cpp文件中进行函数声明、变量声明、结构体声明,这也不成问题!!!

    那为何一定要分成头文件与cpp文件呢?

    又为何一般都在头件中进行函数、变量声明,宏声明,结构体声明呢?而在cpp文件中去进行变量定义,函数实现呢??

    原因如下

      1.如果在头文件中实现一个函数体,那么如果在多个cpp文件中引用它,而且又同时编译多个cpp文件,将其生成的目标文件连接成一个可执行文件,在每个引用此头文件的cpp文件所生成的目标文件中,都有一份这个函数的代码,如果这段函数又没有定义成局部函数,那么在连接时,就会发现多个相同的函数,就会报错,函数重复定义。

      2.如果在头文件中定义全局变量,势必会对此全局变量赋初值,那么在多个引用此头文件的cpp文件中同样存在相同变量名的拷贝,关键是此变量被赋了初值,所以编译器就会将此变量放入DATA段,最终在连接阶段,会在DATA段中存在多个相同的变量,它无法将这些变量统一成一个变量,统一变量的意思也就是仅为此变量分配一个空间,而不是多份空间。但是对于声明一个变量,这个变量在头文件没有赋初值,编译器就会将之放入 BSS段,连接器会对BSS段的多个同名变量仅分配一个存储空间。

      3.如果在cpp文件中声明宏、结构体、函数等,那么我要在另一个cpp文件中引用相应的宏、结构体、函数,就必须再做一次重复的工作(意思是说如果不去#include),如果我改了一个cpp文件中的一个声明,那么又忘了改其它cpp文件中的声明,这不就出了大问题了,程序的逻辑就变成了你不可想象的了,如果把这些公共的东东放在一个头文件中,想用它的cpp文件就只需要引用一个就OK了!!!这样岂不方便,要改某个声明的时候,只需要动一下头文件就行了。

      4.在头文件中声明结构体,函数等,当你需要将你的代码封装成一个库,让别人来用你的代码,你又不想公布源码,那么人家如何利用你的库呢?也就是如何利用你的库中的各个函数呢??一种方法是公布源码,别人想怎么用就怎么用,另一种是提供头文件,别人从头文件中看你的函数原型,这样人家才知道如何调用你写的函数,就如同你调用printf函数一样,里面的参数是怎样的?你是怎么知道的?还不是看人家的头文件中的相关声明啊。当然这些东东都成了C标准,就算不看人家的头文件,你一样可以知道怎么使用。

    c语言中cpp文件和头文件的困惑:

    本质上没有任何区别。 只不过一般:

    .h文件是头文件,内含函数声明、宏定义、结构体定义等内容。

    .cpp文件是程序文件,内含函数实现,变量定义等内容

    而且是什么后缀也没有关系,只不过编译器会默认对某些后缀的文件采取某些动作。你可以强制编译器把任何后缀的文件都当作cpp文件来编。

    这样分开写成两个文件是一个良好的编程风格。

    而且,比方说我在aaa.h里定义了一个函数的声明,然后我在aaa.h的同一个目录下建立aaa.c ,aaa.c里定义了这个函数的实现,然后在main函数所在.cpp文件里#include这个aaa.h 然后我就可以使用这个函数了。 main在运行时就会找到这个定义了这个函数的aaa.cpp文件。

    这是因为:

      首先:main函数为标准C/C++的程序入口,编译器会先找到main函数所在的文件

    假定编译程序编译myproj.c(其中含main())时,发现它include了mylib.h(其中声明了函数void test()),那么此时编译器将按照事先设定的路径(include路径列表及代码文件所在的路径)查找与之同名的实现文件(扩展名为.cpp或.c,此例中为mylib.c),如果找到该文件,并在其中找到该函数(此例中为void test())的实现代码,则继续编译;如果在指定目录找不到实现文件,或者在找到的实现文件及后续的各include文件中未找到实现代码,则返回一个编译错误。其实include的过程完全可以"看成"是一个文件拼接的过程,将声明和实现分别写在头文件及cpp文件中,或者将二者同时写在头文件中,理论上没有本质的区别。

    注意:之所以去查找与之同名的实现文件,是默认的Makefile文件设定的。当然也可以在Makefile中设定其他cpp文件,或者是

    以上是所谓动态方式。

    对于静态方式,基本所有的C/C++编译器都支持一种链接方式被称为Static Link,即所谓静态链接。

    在这种方式下,我们所要做的,就是写出包含函数、类等声明的头文件(a.h,b.h,...),以及他们对应的实现文件(a.cpp,b.cpp,...)。编译程序会将实现文件编译为静态的库文件(a.lib,b.lib,...)。在随后的代码重用过程中,我们只需要提供相应的头文件(.h)和相应的库文件(.lib),就可以使用过去的代码了。

    相对动态方式而言,静态方式的好处是实现代码的隐蔽性,即C++中提倡的"接口对外,实现代码不可见",有利于库文件的转发。

    提示:
    如果说难题最难的部分是基本概念,可能很多人都会持反对意见,但实际上也确实如此。我高中的时候学物理,老师抓的重点就是概念--概念一定要搞清,于是难题也成了容易题。如果你能分析清楚一道物理难题存在着几个物理过程,每一个过程都遵守那一条物理定律(比如动量守恒、牛II定律、能量守恒),那么就很轻松的根据定律列出这个过程的方程,N个过程必定是N个N元方程,难题也就迎刃而解。即便是高中的物理竞赛难题,最难之处也不过在于:
    
    (1)、混淆你的概念,让你无法分析出几个物理过程,或某个物理过程遵循的那条物理定律;
    
    (2)、存在高次方程,列出方程也解不出。而后者已经是数学的范畴了,所以说,最难之处还在于掌握清晰的概念;

    程序设计也是如此,如果概念很清晰,那基本上没什么难题(会难在数学上:比如算法的选择、时间空间与效率的取舍、稳定与资源的平衡上)。但是,要掌握清晰的概念也没那么容易。比如下面这个例子,看看你有没有很清晰透彻的认识。

    a.h文件中:

    void foo(); 

    a.cpp文件中:

    #include "a.h"
    void foo() 
    { 
        return; 
    }

    我的问题出来了:#include "a.h"这句话是要还是不要?  

    main.cpp文件中:

    #include "a.h" 
    
    int main(int argc, char *argv[]) 
    { 
        foo(); 
        return 0; 
    }

    针对上面的代码,请回答三个问题:

    (1)a.c 中#include "a.h"这句话是不是多余的?(不一定)

    (2)为什么经常见 xx.c 里面 include 对应的 xx.h?

    (3)如果 a.c 中不写#include "a.h",那么编译器是不是会自动把 .h 文件里面的东西跟同名的 .c 文件绑定在一起?(不会)

    (请针对上面3道题仔细考虑10分钟,莫要着急看下面的解释。:) 考虑的越多,下面理解的就越深。)

    好了,时间到!请忘掉上面的3道题,以及对这三道题引发出的你的想法,然后再听我慢慢道来。

    正确的概念是:从C编译器角度看,.h和.c皆是浮云,就是改名为.txt、.doc也没有大的分别。换句话说,就是.h和.c没啥必然联系。.h中一般放的是同名.cpp文件中定义的变量、数组、函数的声明等需要让.c外部使用的声明。这个声明有啥用?只是让需要用这些声明的地方方便引用(因为使用前必须声明)。因为 #include "xx.h" 这个宏其实际意思就是把当前这一行删掉,把 xx.h 中的内容原封不动的插入在当前行的位置。由于想写这些函数声明的地方非常多(每一个调用 xx.c 中函数的地方,都要在使用前声明一下),所以用 #include "xx.h" 这个宏就简化了许多行代码--让预处理器自己替换好了。也就是说,xx.h 其实只是让需要写 xx.c 中函数声明的地方调用(可以少写几行字),至于 include 这个 .h 文件是谁,是 .h 还是 .c,还是与这个 .h 同名的 .c,都没有任何必然关系。

    这样你可能会说:啊?那我平时只想调用 xx.c 中的某个函数,却 include了 xx.h 文件,岂不是宏替换后出现了很多无用的声明?没错,确实引入了很多垃圾,但是它却省了你不少笔墨,并且整个版面也看起来清爽的多。鱼与熊掌不可得兼,就是这个道理。反正多些声明(.h一般只用来放声明,而放不定义,参见拙著"过马路,左右看")也无害处,又不会影响编译,何乐而不为呢?

    翻回头再看上面的3个问题,很好解答了吧?

    1.答:不一定。这个例子中显然是多余的。但是如果.c中的函数也需要调用同个.c中的其它函数,那么这个.c往往会include同名的.h,这样就不需要为声明和调用顺序而发愁了(C语言要求使用之前必须声明,而include同名.h一般会放在.c的开头)。有很多工程甚至把这种写法约定为代码规范,以规范出清晰的代码来。

    3.答:不会。问这个问题的人绝对是概念不清,要不就是想混水摸鱼。非常讨厌的是中国的很多考试出的都是这种烂题,生怕别人有个清楚的概念了,绝对要把考生搞晕。

    提示:
    搞清楚语法和概念说易也易,说难也难。窍门有三点: 不要晕着头工作,要抽空多思考思考,多看看书;
    
    看书要看好书,问人要问强人。烂书和烂人都会给你一个错误的概念,误导你;
    
    勤能补拙是良训,一分辛苦一分才;

    (1)通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口怎么实现的。编译器会从库中提取相应的代码。

    (2)头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。

      头文件用来存放函数原型。

      头文件如何来关联源文件?

    这个问题实际上是说,已知头文件"a.h"声明了一系列函数(仅有函数原型,没有函数实现),"b.cpp"中实现了这些函数,那么如果我想在"c.cpp"中使用"a.h"中声明的这些在"b.cpp"中实现的函数,通常都是在"c.cpp"中使用#include "a.h",那么c.cpp是怎样找到b.cpp中的实现呢?

    其实.cpp和.h文件名称没有任何直接关系,很多编译器都可以接受其他扩展名。

    谭浩强老师的《C程序设计》一书中提到,编译器预处理时,要对#include命令进行"文件包含处理":将headfile.h的全部内容复制到#include "headfile.h"处。这也正说明了,为什么很多编译器并不care到底这个文件的后缀名是什么----因为#include预处理就是完成了一个"复制并插入代码"的工作。

      程序编译的时候,并不会去找b.cpp文件中的函数实现,只有在link的时候才进行这个工作。我们在b.cpp或c.cpp中用#include "a.h"实际上是引入相关声明,使得编译可以通过,程序并不关心实现是在哪里,是怎么实现的。源文件编译后成生了目标文件(.o或.obj文件),目标文件中,这些函数和变量就视作一个个符号。在link的时候,需要在makefile里面说明需要连接哪个.o或.obj文件(在这里是b.cpp生成的.o或.obj文件),此时,连接器会去这个.o或.obj文件中找在b.cpp中实现的函数,再把他们build到makefile中指定的那个可以执行文件中。

     (非常重要)

    在VC中,一般情况下不需要自己写makefile,只需要将需要的文件都包括在project中,VC会自动帮你把makefile写好。

    通常,连接器会在每个.o或.obj文件中都去找一下所需要的符号,而不是只在某个文件中找或者说找到一个就不找了。因此,如果在几个不同文件中实现了同一个函数,或者定义了同一个全局变量,链接的时候就会提示"redefined"。

    文章转载地址:http://www.cnblogs.com/laojie4321/archive/2012/03/30/2425015.html

  • 相关阅读:
    2、容器初探
    3、二叉树:先序,中序,后序循环遍历详解
    Hebbian Learning Rule
    论文笔记 Weakly-Supervised Spatial Context Networks
    在Caffe添加Python layer详细步骤
    论文笔记 Learning to Compare Image Patches via Convolutional Neural Networks
    Deconvolution 反卷积理解
    论文笔记 Feature Pyramid Networks for Object Detection
    Caffe2 初识
    论文笔记 Densely Connected Convolutional Networks
  • 原文地址:https://www.cnblogs.com/stemon/p/4000266.html
Copyright © 2020-2023  润新知