头文件重复包含,是每个程序员都遇到过的问题。头文件重复包含,可能会导致的错误包括:变量重定义,类型重定义及其他一些莫名其妙的错误。现在我们就讨论如何避免这些问题。
假设,我们的工程中存在a.h,b.h及c.cpp等3个文件。其中b.h中包含了a.h,而c.cpp又包含了a.h和b.h两个文件。文件代码如下所示。
/* 测试头文件重复包含问题 */
// File : a.h
#include <iostream.h>
// func_1 实现func_1函数名称打印。
void func_1()
{
std::cout << "this is" << __FUNCTION__ << endl;
}
// File : b.h
#include "a.h"
// Func_2 实现Func_2函数名称打印。并调用func_1。
void Func_2()
{
std::cout << "this is " << __FUNCTION__ << endl;
std::cout << "this function called " << endl;
func_1();
}
// File : c.cpp
#include "a.h"
#include "b.h"
int main()
{
// ....
return 0;
}
如果,你在VC++编译器中编译上述代码。在编译过程中,编译器会抛出“重复定义”错误。因为a.h被重复包含了2次。为了避免同一个文件被重复包含多次。C++提出了2种解决方案。他们是#ifndef方式和#pragman once方式。
方式1:
#ifndef __SOME_FILE_H__
#define __SOME_FILE_H__
... // 一些声明语句
#endif
方式2:
#pragma once
... // 一些声明语句
1. #ifndef方式
这种实现方式,通过预处理实现唯一检查。预处理器首先测试__SOME_FILE_H__预处理器变量是否未定义。如果__SOME_FILE_H__未定义,那么#ifndef测试成功,跟在#ifndef 后面的所有行都被执行,直到发现 #endif。相反,如果__SOME_FILE_H__已定义,那么#ifndef 指示测试为假,该指示和#endif指示间的代码都被忽略。
为了保证头文件在给定的源文件中只处理过一次,我们首先检测#ifndef。第一次处理头文件时,测试会成功,因为__SOME_FILE_H__还未定义。下一条语句定义了__SOME_FILE_H__。那样的话,如果我们编译的文件恰好又一次包含了该头文件。#ifndef 指示会发现__SOME_FILE_H__已经定义,并且忽略该头文件的剩余部分。
头文件应该含有保护符,即使这些头文件不会被其他头文件包含。编写头文件保护符并不困难,而且如果头文件被包含多次,它可以避免难以理解的编译错误。
当没有两个头文件定义和使用同名的预处理器常量时,这个策略相当有效。当有两个文件使用同一个宏,这个策略就失效了。当遇到这种问题时,一般有两种解决方案。
方案1:可以为定义在头文件里的实体(如类)命名预处理器变量,来避免预处理器变量重名的问题。例如:一个程序只能含有一个名为Sales_item的类。通过使用类名来组成头文件和预处理器变量的名字,可以使得很可能只有一个文件将会使用该预处理器变量。
方案2:为了保证宏的唯一性,我们可以采用google提供的解决方案,在定义宏时,宏名基于其所在的项目源代码数的全路径命名。宏命名格式为:
_<PROJECT>_<PATH>_<FILE>_H_
最佳实践
- 在定义#ifndef测试宏时,宏名最好采用全大写字母表示。
- 在定义测试宏时,最好采用google提供的解决方案。
2. #pragma once
这种方式一般又编译器提供,只要在头文件的最开始加入这条指令就能够保证头文件被编译一次#pragma once用来防止某个头文件被多次include,#ifndef方式用来防止某个宏被多次定义。
#pragma once是编译相关,就是说这个编译系统上能用,但在其他编译系统不一定可以,也就是说移植性差,不过现在基本上已经是每个编译器都有这个定义了。
#ifndef,#define,#endif这是C++语言相关的,是C++语言中的宏定义,通过宏定义避免文件多次编译。所以在所有支持C++语言的编译器上都是有效的,如果写的程序要跨平台,最好使用这种方式。
小心陷阱
- 针对#pragma once,GCC已经取消对其的支持了。而微软的VC++却依然支持。
- 如果写的程序需要跨平台,最好使用#ifndef方式,而避免使用#progma once方式。
3. #pragma once与 #ifndef的区别
#ifndef的方式依赖于宏名字不能冲突,这不光可以保证同一个文件不会被包含多次,也能保证内容完全相同的两个文件不会被不小心同时包含。当然,缺点就是如果不同头文件的宏名不小心“撞车”,可能就会导致头文件明明存在,编译器却硬说找不到声明的窘况。
#pragma once则由编译器提供保证:同一个文件不会被包含多次。注意这里所说的“同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件。带来的好处是,你不必再费劲想个宏名了,当然也就不会出现宏名碰撞引发的奇怪问题。对应的缺点就是如果某个头文件有多份拷贝,本方法不能保证他们不被重复包含。当然,相比宏名碰撞引发的“找不到声明”的问题,重复包含更容易被发现并修正。
请谨记
- 为了避免重复包含头文件,建议在每个头文件时采用“头文件卫士”加以保护。头文件卫士有两种形式一种是#progma once,一种是#ifndef。
- 如果你的程序需要跨平台,建议你使用#ifndef方式。因为这种方式仅与C++语言有关。