C++ 头文件的包含

 作为初学者,经常会对对c/c++头文件的包含很纠结。这两天和实验室的小伙伴们花了点时间研究了一下,在这里做个总结。


1) 首先,头文件不是必不可少的,可以用直接写声明来取代。

对于小规模项目,只需要一个源文件,所有声明和定义都放在这一个源文件里面即可,因此不需要头文件。但是要遵循一个原则,即先声明后调用。所以应把所有声明(如对全局变量和函数的声明)都放在主函数之前。但是函数的定义允许放在主函数之后。

对于含有两个或两个以上源文件的项目,也不一定需要头文件。例如有一个项目,其中含有两个源文件,a.cpp和m.cpp。a.cpp里定义了一个函数void f(), 而m.cpp需要调用该函数,这时应该怎么办呢?其实很简单,还是遵循先声明后调用的原则,只要在m.cpp主函数前加入一个声明void f();即可(这里要注意函数的声明是默认extern的,如果是变量声明,必须在声明前加上extern字样,以告之定义在另一文件处)。编译器会先编译成a.obj和m.obj两个目标文件,然后连接器根据m.obj中的声明接口去a.cpp找定义,连接生成可执行文件。所以说,头文件是可以完全不要的。如果非要用头文件(假设命名为a.h),其实就是把m.cpp里的声明void f();转移到该头文件中,然后在m.cpp里面添加#inlcude "a.h"。这个效果和不用头文件是一模一样的,即达到在m.cpp里先声明后调用的目的。


2)其次,头文件的存在,一是为了少打字,二是为了封装。

一,少打字。如上所述头文件的作用和声明的功能是一样的。但是我们可以把经常用到的声明放在一个头文件里,做成一个声明的集合,方便一次性调用多个声明。这些声明的定义可以位于不同的源文件,比如有些声明的定义位于标准库,而有些声明的定义位于自己写的源文件。从这里也可以看出头文件的命名理论上是任意的,它可以和任意源文件都不重名(不包含扩展名)。并非有一个头文件就必须有一个对应的源文件。并且如果某个声明的函数或变量并未被真正调用,是不要求存在对应的定义的。

二,实现封装。我们经常会把某些具有共性的函数定义放在同一个源文件里(如果是面向对象编程,就可以把这些函数做成一个类),并且该源文件具有一般性,会被其他几个不同的源文件同时调用,或者在以后的人生中可能还会被再次调用。此时我们就可以把该源文件里的所有函数声明封装到一个头文件里,这样就不用每次调用的时候都去查看一下源文件里定义了哪些函数,不用每次都写一遍声明。这时头文件的命名习惯上和源文件重名,便于理解,但是不做强求。

以下两种情况,头文件是没有价值的。1,头文件里只声明了一个函数。此时写一遍声明和写一遍包含头文件所做的功差不多,那么也就没有必要再弄个头文件了。2,如果一个源文件不具有任何一般性,这辈子估计就只在一个源文件里调用一次,以后打死也不用了,那么头文件对于该源文件也是没有价值的,因为把声明放在头文件里和直接放在调用该源文件的另一源文件里所做的功是一样的,都是一辈子只写一遍。当然,人还是给自己留条后路比较好,别把自己往死里逼,还是建议都使用头文件。


3)如果用了头文件就要注意避免重复定义。声明可以重复,但是定义不可以。

对于变量,什么是声明什么是定义,c和c++的标准不同。对于c,只要不赋值,就认为是声明,所以一个文件里面同一作用域可以出现多个int i,但是只能有一个int i=5。但是对于c++,int i既是声明也是定义,因此只能出现一次int i,只有加了extern字样的才是单纯的声明。

重复定义有两种情况,一种是同一源文件内的重定义,另一种是不同源文件之间的重定义。前者会在编译阶段报错,后者会在连接阶段报错。这里插播一段编译器和连接器的工作过程,虽然前面已经提过。首先编译器会把所有cpp源文件编译成一个个对应的.obj目标文件(因此cpp扩展名不可任意改动),如果发现某一源文件里同一标识符(同一作用域内)出现了两次或多次定义(同一源文件内的重定义),则宣告编译失败。如果每个源文件都不存在重定义,则编译成功。但编译成功并不代表连接成功。接着连接器把所有obj文件连接成一个可执行exe文件,如果发现某两个obj文件里各自对同一标识符进行了一次定义(不同源文件之间的重定义),则宣告连接失败,反之则成功。

那么在包含头文件时怎么避免这两种重定义呢?对于第一种情况,如果一个头文件被同一个源文件包含了两次或两次以上就会有重定义的风险。例如,b.h包含了a.h,m.cpp既包含了a.h又包含了b.h,这就等于m.cpp包含了a.h两次,那么头文件a.h里如果出现了定义,会导致m.cpp源文件内部的重定义,无法编译成m.obj文件。这种同一源文件内的重定义有三种方式避免,一是直接去掉b.h里包含a.h的指令,这样m.cpp便只包含了a.h一次;二是把a.h里的定义移到cpp文件里,只留声明;三是加入条件编译语句(如#ifndef, #define, #endif, 或者#pragma once),强制只编译一次。对于第二种情况(不同源文件之间的重定义),来看一个例子,a.cpp包含了a.h,m.cpp也包含了a.h,即a.cpp和m.cpp包含了同一个头文件,这里不存在同一源文件内部的重定义,因此编译可以通过,得到两个目标文件a.obj和m.obj。但是如果a.h里有定义,会导致在连接阶段发现a.obj和m.obj里出现重定义,连接失败。解决方法是把a.h里的定义移到cpp里去,只留声明。

这里有三个特殊情况要注意,一是常量的声明和定义,二是内联函数的声明和定义,三是类的声明和定义。

一,常量的声明和定义。常量的声明也是以extern开头,但不能赋值,如extern const double pi。常量的定义是const double pi=3.14。同一源文件里不允许出现两次定义,否则编译失败,避免方法如上所述。但是不同源文件之间允许重定义,这和一般的变量很不同。c++标准之所以如此规定,其中一个原因是为了方便数组的定义。假如我们把数组的大小作为参数放在头文件里,那么该参数必须是const,而且头文件里必须有该参数的定义(赋值)。如果头文件里只放声明,而把定义放在另一源文件里,那么在编译阶段会找不到数组的大小,无法通过编译。因此,头文件里必须允许常量定义的存在。作为妥协,还得允许不同源文件都能包含该头文件。

二,内联函数的声明和定义。内联函数可以在头文件里定义,并且不导致重定义。这是因为内联函数的内容在编译时会在源程序中原地扩展,而非在运行时调用,也就是说在目标文件里内联函数其实并不存在,所以也就无所谓重定义了。

三,类的声明和定义。类是一种数据类型(抽象数据类型),它和函数一样,不调用(确切的说是声明类对象)就不会占据内存(只占代码区内存,不占其他三区内存)。习惯上会把类的声明和定义都放在头文件里(这是必须的),而把成员函数的定义(实现)放在源文件里(但是允许在类定义里直接定义小规模成员函数,并将其默认为内联函数)。因此对类头文件进行包含时也要注意避免同一源文件内的重定义。对小规模项目,可以尽量只包含一次,对于大项目,特别是多人合作项目,就要加入条件编译语句,避免他人重复包含导致的重定义。但是,和常量一样,允许不同源文件都能对同一声明(并定义)类的头文件进行包含,并不导致重定义。这也是因为在编译阶段,编译器必须知道类的定义,否则无法编译。关于类的声明有一个有趣的现象请参考[2]。


如有理解错误的地方,欢迎指正。


参考资料:
[1] C++程序设计教程,钱能主编
[2] http://blog.csdn.net/eclipser1987/article/details/7516968



  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值