C/C++程序中头文件(.h)与源文件(.c/.cpp)之疑问探索

在程序开发过程中,头文件放什么东西?源文件放什么东西?程序在编译过程中,头文件和源文件是如何编译的?为什么会有重复定义的错误……

针对这些问题,今天我们就来细细盘点一下头文件和源文件之间的种种疑问纠葛。

头文件,以“.h”为后缀(h为head(头)的首字母),如“animal.h”。头文件中一般存放:函数声明(即函数原型,function prototype)、宏定义(#define)、结构体类型定义(并非结构体类型变量定义)、类(class)的定义、类成员(函数,变量)的声明、全局变量的声明(extern)、const定义的常量等。

代码示例如下:

// animal.h 
// 在头文件中包含类的定义及类成员函数的声明
class animal
{
public:
	animal();
	~animal();
	void eat();
	void sleep();
    virtual void breathe();
};

源文件,以“.cpp”为后缀,如“main.cpp”。源文件中存放函数的实现,结构体类型变量的定义,类成员函数的实现,全局变量的定义等。

代码示例如下:

// animal.cpp
// 在源文件中包含类中成员函数的实现

#include "animal.h"  // 因为在编译animal.cpp时,编译器不知道animal到底是什么,所以要包含animal.h,这样编译器就知道animal是一种类的类型
#include <iostream.h>

animal::animal()
{}

animal::~animal()
{}

void animal::eat()  // 函数体虽然为空,但仍然是实现了这个函数
{}

void animal::sleep()
{}

void animal::breathe()  // 在头文件中加了virtual后,在源文件中就不必再加virtual了
{
	cout << "animal breathe" << endl;
}

进一步,当项目比较复杂,会有类的继承等,进而会有头文件的多次包含情况,代码示例如下:

// fish.h

#include "animal.h"  //  因fish类从animal类继承而来,要让编译器知道animal是一种类的类型,就要包含animal.h头文件

class fish::public animal
{
public:
	void breathe();
};
// fish.cpp

#include "fish.h"
#include <iostream.h>

void fish::breathe()
{
	cout << "fish bubble" << endl;
}
// main.cpp

#include "animal.h"
#include "fish.h"

void mian()
{	
    // TODO
}

从代码上看没有任何问题,但是当编译该工程后,就会出现类重复定义的错误。为什么会出现该错误呢?通过查看main.cpp文件,可以发现该文件包含了animal.h和fish.h这两个头文件。当编译器编译main.cpp文件时,因为在文件中包含了animal.h头文件,编译器会展开该头文件(头文件具体展开过程处理,见后文注1中(2)文件包含及处理部分),知道animal类的定义,接着展开fish.h头文件,而在fish.h头文件中也包含了animal.h,再次展开animal.h,于是animal类就重复定义了。

如何解决同一个文件中,包含多个头文件,引起的重复定义的问题呢?通常使用添加条件预处理指令(条件编译,属于宏定义,见注1)解决该问题。

添加条件编译宏定义后的代码如下:

// animal.h
// 在头文件中包含类的定义及类成员函数的声明

#ifndef ANIMAL_H  // 条件编译
#define ANIMAL_H  // 我们一般用#define定义一个宏,是为了在程序中使用,使程序更加简洁,维护更加方便,然而在此处,我们只是为了判断ANIMAL_H是否定义,以此来避免重复定义,因此我们没有为其定义某个具体的值

class animal
{
public:
	animal();
	~animal();
	void eat();
	void sleep();
	virtual void breathe();
};

#endif

再看前述中main.cpp的编译过程,当编译器展开animal.h头文件时,条件预处理指令判断ANIMAL_H没有定义,于是就定义它,然后继续执行,定义了animal这个类;接着展开fish.h头文件,而在fish.h头文件中也包含了animal.h,再次展开animal.h,这个时候条件预处理指令发现ANIMAL_H已经定义,于是跳转到#endif,执行结束。通过分析,我们发现这次的编译过程中,animal类只定义了一次。

因此,在项目开发过程中,为了避免头文件重复包含、类重复定义的问题,在头文件中,一般都会加上条件编译和宏定义,格式如下:

#ifndef ANIMAL_H

#define ANIMAL_H

……

#endif

而后,该工程即可进行编译,链接,运行了(具体过程见注2)。

但是,新的问题又来了......

在一个工程中,同一个头文件会在不同cpp文件之间的重复包含,头文件中的函数和变量的声明出现多次没有问题,但是其中的类定义也会在不同cpp文件中重复定义,即一个工程中的多个cpp文件中出现同一个类的重复定义,这难道不会导致错误吗?

通过添加条件编译宏定义,就可以避免在同一个.cpp文件中对.h文件的重复包含问题。但是如果仔细看注2中程序编译链接的过程就会发现,同一个头文件也会在不同的源文件中被包含,而宏定义的有效范围只是在定义命令后到本文件结束,是局部有效的,因此宏定义并不能防止两个源文件包含同一个头文件。为了避免这种情况下的重复包含重复定义,前文中也己经述说头文件中一般只存放函数声明,变量声明(函数和变量都是只能定义一次,但是可以多次声明),#define等宏定义已经在预处理时替换了(注1)等。但是类的定义const定义的常量一般也在头文件中,这是会引起不同源文件中重复包含重复定义问题的。

其实......

类可以在头文件中定义,这是因为遵守“单一定义规则”(One Definition Rule, ODR)。根据此规则,如果对同一个类的两个定义完全相同且出现在不同编译单位,会被当做同一个定义,即当包含类的头文件(animal.h)分别被几个不同的编译单位(main.cpp, fish.cpp, animal.cpp)包含,满足ODR规则,会被当做同一个定义,所以不会有冲突。此外,模板和inline函数也使用此规则。

const常量(const int bufSize = 512;)也能定义在头文件中,这是因为默认情况下,const对象仅在文件内有效。编译器将在编泽过程中把用到该变量的地方都替换成对应的值。也就是说,编译器会找到代码中所有用到bufSize的地方,然后用512替换。 为了执行上述替换,编译器必须知道变量的初始值:如果程序包含多个文件,则每个用了const对象的文件都必须能访问到它的初始值才行,要做到这一点,就必须在每个用到变量的文件中都有对它的定义(const常量只在定义的时候才有初始值,即要访问初始值就得包含该常量的定义)。为了支持这一用法,同时避免对同一变量的重复定义,默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。所以,通常情况下,const常量定义并初始化在头文件中,可以被多个源文件包含,并不会出错。


注1:预处理命令及处理过程

C/C++中加入的“预处理命令”是为了改进程序设计环境,提高编程效率。一般来说,预处理命令是统一规定的,但是并不是C/C++语言本身的组成部分,不能直接对他们进行编译,必须在对程序进行通常的编译之前,先对程序中这些特殊的命令进行“预处理”(如用#define命令定义的符号常量A,则在预处理时将程序中所有的A都置换成指定的字符串;若用#include命令包含头文件“stdio.h”,则在预处理时将stdio.h文件中的实际内容代替该命令)。预处理功能主要包括:(1)宏定义,(2)文件包含,(3)条件编译,(4)#pragma once,这些命令均以符号#开头;

    (1)宏定义

格式:#define 标识符 字符串

宏定义不是C语句,不必在行末加分号;

#define命令出现在程序中函数的外面,宏名的有效范围为定义命令之后到本源文件结束;通常,#define命令写在文件开头,函数之前,作为文件一部分,在此文件范围内有效;

    (2)文件包含及处理过程

文件包含及处理过程如图所示,图中(a)为文件animal.cpp,包含#include <animal.h>,及其他内容(以A表示);图中(b)为文件animal.h,内容以B表示;

在编译预处理时,要对#include命令进行“文件包含”处理:将animal.h的全部内容复制插入到#include<animal.h>命令处,即animal.h被包含到animal.cpp中,得到(c)所示的结果。在编译时,对将经编译预处理的animal.cpp,即(c),作为一个源文件单位进行编译;

    (3)条件编译

当程序中一部分内容只在满足一定条件时才进行编译,也就是说对该部分内容指定编译的条件,即条件编译;条件编译有三种格式,如下所示:

         第一种:若所指定的标识符已经被#define命令定义过,则在程序编译阶段编译程序段1,否则编译程序段2;#else部分可以没有;

#ifdef 标识符
      程序段1
#else
      程序段2
#endif

        第二种:若标识符未被定义过则编译程序段1,否则编译程序段2;

#ifndef 标识符
      程序段1
#else
      程序段2
#endif

        第三种:当指定的表达式值为真时就编译程序段1,否则编译程序段2;

#if 表达式
      程序段1
#else
      程序段2
#endif

(4)#pragma once

使用 #pragma once,和使用方式(3)条件编译,来避免多次包含头文件的效果是一样的,对比如下;

//方式(4):使用#progma once
#pragma once  
// Code placed here is included only once per translation unit(翻译单元) 
// 方式(3):使用宏定义方式
#ifndef HEADER_H_ 
#define HEADER_H_  
// Code placed here is included only once per translation unit  
#endif // HEADER_H_  

 注2:C/C++程序编译链接的原理与过程

首先,编译器对工程中的三个源文件main.cpp、fish.cpp、animal.cpp单独进行编译(compiling)。在编译时,先由预处理器对预处理指令(#include、#define和#if)进行处理,在内存中输出翻译单元(临时文件)。编译器接收预处理的输出,将源代码转换成包含机器语言指令的三个目标文件(扩展名为obj的文件):mian.obj,fish.obj,anima.obj。注意,在编译过程中,头文件不参与编译;接下来是链接过程(linking),链接器将目标文件和所用到的C++类库文件一起链接生成main.exe。整个编译链接过程如下图所示:

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值