头文件的作用

头文件的作用主要表现为以下两个方面:

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

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

区别

一、函数实现究竟放在哪里?

一般来说,什么时候需要把实现放在头文件里,什么时候又需要把实现放在源文件里?

不把实现放在头文件中,往往是出于以下几种顾虑:

  1. 暴露了实现细节
  2. 头文件被包含到不同的源文件中,会导致链接冲突
  3. 头文件被包含到不同的源文件中,会导致有多份实现被编译出来,增大可执行体的体积

如果有顾虑 1 ,那很显然应该在第一时间抛弃完全在头文件中实现的念头。

至于顾虑 2和3 的,我们举例如下。例如有以下头文件 c_function.h:

1
2
3
4
int integer_add(const int a, const int b)
{
         return a + b;
}

如果在同一工程中,有 a.c 和 b.c 两个(或两个以上)源文件包含了此头文件,则在链接时期就会发生冲突,因为在两个源文件编译得到的目标文件中都有一份 integer_add 的函数实现,导致链接器不知道对于调用了此函数的调用者,应该使用哪一个副本。解决冲突办法有两个,一个是加上 inline ,另一个是加上 static 。使用这两个关键字的任意一个来修饰 integer_add 函数,然而本质却大不相同。

如果使用 inline ,则意味着编译器会在调用此函数的地方把函数的目标代码直接插入,而不是放置一个真正的函数调用,实际作用就是这个函数事实上已经不再存在,而是像宏一样被就地展开了。使用 inline 的副作用,首先在于毋庸置疑地,代码的体积变大了;其次则是,这个关键字严格算起来并不是 C 语言的关键字,使用它多少会带来一些移植性方面的风险。而且inline不对编译器做强制要求,编译器有权把它实现为非inline的状态(可能的原因有,函数太大或者复杂度过高)。这样的后果是不确定的。

如果是使用static,那么包含此头文件的源文件中都会存在此函数的一份副本。因为 static 关键字保证了该函数为单个源文件之内可见,所以不会产生冲突问题。虽然代码也有一定程度的膨胀,但至少结果是可预料的。

另外,应该避免使用extern关键字,如果在两个文件中重复定义了一个函数,并且在其中一个文件中对这个函数使用了extern关键字进行修饰,那么就会发生连接错误。

所以把实现放在头文件里,似乎不是一个很好的办法,但并不是不能这么做。 虽然这些讨论主要聚焦在 C 语言上,但由于 C++ 是 C 语言的超集,并且在这些方面并没有做太多的修改,因此讨论结果同样也适用于 C++ 。

二、源文件与头文件的关系

接下来,我们谈一下头文件和源文件在编译与组建的过程中的关系。

编译器就将源文件(.cpp)编译成目标文件(.obj),目标文件就是编译单元。一个程序可以由一个编译单元组成,也可以有多个编译单元组成。一个函数不能放到两个编译单元里面,但两个函数或以上就可以分别放在一个单元里面。那么就是一个源文件对应一个目标文件,然后通过链接器组成一个.exe,也就是程序了。

在C++中,使用函数或者变量之前必须要进行声明。那么如果一个源文件要用到另一个源文件定义的函数,只需在这个源文件中写上他的函数声明就可以了,其余工作由链接器帮你完成。但是当多个文件都需要使用同一个函数时,那么就要在多份源文件中进行声明。而且如果要修改这个函数时,就必须逐个修改每个源文件。

头文件(.h)就是为了解决这个问题而诞生,他包含了这些公共的函数定义,而且如果需要修改,也只修改头文件中的内容即可。对于商业C++程序库,一般把头文件随二进制的库文件发布,而把源代码保留,这也是上面所说的顾虑1。

所有需要使用该函数的源文件只需要用#include语句将相应的头文件包含进去便可。预处理器发现#include指令后,就会寻找指令后面的文件名并把这个文件的内容包含到当前文件中。被包含文件中的文本将替换源代码文件中的#include指令,就像你把被包含文件中的全部内容键入到源文件中的这个位置一样。头文件是没有编译意义的,编译器只编译源文件生成目标文件,而头文件不参与编译过程。

另外,使用#include指令包含源文件也是可行的,编译器完全能够正常处理,甚至可以使用#include指令包含任意扩展名的文件。因此从设计角度上讲,源代码区分为.h和.c,仅仅是为了接口与实现的分离,实际上两者没什么本质的差别。头文件只是工具,但不是必须的。

三、在这个场景下的结论

经过以上的讨论,最终的结果是,似乎在开源的工具方法中,把实现放在头文件里是个非常不错的选择。主要的有点有三个:

  1. 没有隐藏实现细节的要求,因为这是一份开源的、用作学习与练习的代码。

  2. 头文件不参与编译,再单独使用卫兵宏(#ifndef… #define… #endif),就可以避免多个文件引用本工具方法时,可能带来的重复编译(即链接错误,比如说LNK2005)。

  3. 不需要像商业程序库一样,把实现部分单独打包生成lib或dll等二进制文件,所以代码体积小,结构也简单。

推荐写法

用C++编写比较大型的项目时,文件的分割管理确实确实是非常必要的 。下面就非常简洁明了地谈谈头文件(.h)和源文件(.cc)应该怎么写。

头文件(.h):

写类的声明(包括类里面的成员和方法的声明)、函数原型、#define常数等,但一般来说不写出具体的实现。

在写头文件时需要注意,在开头和结尾处必须按照如下样式加上预编译语句(如下):

1
2
3
4
5
6
#ifndef CIRCLE_H
#define CIRCLE_H

//你的代码写在这里

#endif

这样做是为了防止重复编译,不这样做就有可能出错。至于CIRCLE_H这个名字实际上是无所谓的,你叫什么都行,只要符合规范都行。原则上来说,非常建议把它写成这种形式,因为比较容易和头文件的名字对应。

源文件(.cc):

源文件主要写实现头文件中已经声明的那些函数的具体代码。需要注意的是,开头必须#include一下实现的头文件,以及要用到的头文件。那么当你需要用到自己写的头文件中的类时,只需要#include进来就行了。

Example:

下面举个最简单的例子来描述一下,咱就求个圆面积。

第1步,建立一个空工程。

第2步,在头文件的文件夹里新建一个名为Circle.h的头文件,它的内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#ifndef CIRCLE_H
#define CIRCLE_H

class Circle
...{
private:
    double r;//半径
public:
    Circle();//构造函数
    Circle(double R);//构造函数
    double Area();//求面积函数
};

#endif

注意到开头结尾的预编译语句。在头文件里,并不写出函数的具体实现。

第3步,要给出Circle类的具体实现,因此,在源文件夹里新建一个Circle.cc的文件,它的内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include "Circle.h"

Circle::Circle()
...{
    this->r=5.0;
}

Circle::Circle(double R)
...{
    this->r=R;
}

double Circle:: Area()
...{
    return 3.14*r*r;
}

需要注意的是:开头处包含了Circle.h,事实上,只要此cpp文件用到的文件,都要包含进来!这个文件的名字其实不一定要叫Circle.cc,但非常建议cc文件与头文件相对应。

最后,我们建一个test.cc来测试我们写的Circle类,它的内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <iostream>
#include "Circle.h"
using namespace std;

int main()
...{
    Circle c(3);
    cout<<"Area="<<c.Area()<<endl;
    return 1;
}

注意到开头时有#include “Circle.h"的声明,证明我们使用到了我们刚才写的Circle类。

#include <filename.h>与#include “filename.h”

对于#include <filename.h> ,编译器先从标准库路径开始搜索filename.h,使得系统文件调用较快。而对于#include “filename.h”,编译器先从用户的工作路径开始搜索filename.h,然后去寻找系统路径,使得自定义文件较快。

转载:
http://www.cnblogs.com/superpig0501/p/3967578.html
http://www.cnblogs.com/sylar5/p/6702483.html