C++类包含问题(重复包含和相互包含)

一.重复包含头文件

头文件重复包含,可能会导致的错误包括:变量重定义,类型重定义及其他一些莫名其妙的错误。C++提供两种解决方案,分别是#ifndef和#pragma once

#ifndef _SOME_FILE_H_                      #pragma once

#define _SOME_FILE_H_                      ...//一些声明语句

...//一些声明语句

#endif

第一种方式:通过这种预处理实现唯一检查。预处理首先测试_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提供的解决方案

第二种方式:#pragma once

这种方式一般由编译器提供,只要在头文件的最开始加入这条指令就能够保证头文件只被编译一次。#pragma once用来防止某个头文件被多次include,#ifndef方式用来防止某个宏被多次定义。#pragma once是编译相关,即在这个编译系统上使用,但在其它编译系统上不一定能用,也就是说移植性差,不过现在基本上已经是每个编译器都有这个定义了。

针对#pragma once,GCC已经取消对其的支持了,而微软的VC++却依然支持

如果写的程序需要跨平台,最好使用#ifndef方式,而避免使用#pragma once方式

二.相互包含问题

CA类包含CB类的实例,而CB类也包含CA类的实例。代码如下

//A.h实现CA类的定义                        //B.h实现CB类的定义        

#include "B.h"                           #include "A.h"               int main()

class CA                              class CB                  {

{                                  {                        CA instanceA;

  public:                               public:                    return 0;

    int iData;                            int iData;                }

    CB instaceB;                           CA instaceA;

};                                 };

编译出错分析:

CA类定义时使用CB类的定义,CB类定义时使用CA类的定义,递归定义

A.h包含了B.h,B.h包含了A.h,也存在递归包含的问题

其实,无论是结构体的递归定义,还是类的递归定义,最后都归于一个问题,C/C++采用静态编译模型,在程序运行时,结构或类大小都会在编译后确定。程序要正确编译,编译器必须知道一个结构或结构所占用的空间大小,否则编译器就会报出奇怪的编译错误。

解法:

1.向前声明实现

//A.h实现CA类的定义                        //B.h实现CB类的定义

class CB//前向声明CB类                      #include "A.h"                #include "B.h"

class CA                              class CB                    int main()

{                                  {                        {

  public:                               public:                      CA instaceA;

    int iData;                            int iData;                    return 0;

    CB* pinstaceB;                         CA instaceA;                }

};                                 };

前向声明实现方式的主要实现原则

主函数只需要包含B.h就可以,因为B.h中包含了A.h

A.h中不需要包含B.h,但要声明class CB,在避免死循环的同时也成功引用了CB

包含class CB声明,而没有包含头文件B.h,这样只能声明CB类型的指针,而不能实例化

2.friend声明定义

//A.h实现CA类的定义                        //B.h实现CB类的定义

class CB//前向声明CB类                      #include "A.h"                #include "B.h"

class CA                              class CB                    int main()

{                                  {                        {

  public:                               public:                      CA instaceA;

    friend classCB;  //友元类声明

    int iData;                            int iData;                    return 0;

    CB* pinstaceB;                         CA instaceA;                }

};                                 };

friend友元声明实现说明:

主函数只需要包含B.h就可以,因为B.h中包含了A.h

A.h不需要包含B.h,但要声明class CB,在避免死循环的同时也成功引用了CB

class CA包含class CB友元声明,而没有包含头文件B.h,这样只能声明CB类型的指针,而不能实例化

无论是前向声明还是friend友元实现,有一点是肯定的,即最多只能有一个类可以定义实例。同样头文件包含也是一件很麻烦的事情,再加上头文件中常常出现的宏定义,各种宏定义的展开是非常耗时间的

类或结构体递归定义实现应遵循两个原则

1.如果可以不包含头文件,那就不要包含了。这时候前置声明可以解决问题的。如果使用的仅仅是一个类的指针,没有使用这个类的具体对象,也没有访问到类的具体成员,那么前置声明就可以了。因为指针这一数据类型的大小是特定的,编译器可以获知。

2.尽量在cpp文件中包含头文件,而非在头文件。假设类A的一个成员是一个指向类B的指针,在类A的头文件中使用了类B的前置声明并编译成功,那么在A的实现中需要访问B的具体成员,因此需要包含头文件,那么我们应该在类A的实现部分包含类B的头文件而非声明部分。

上一篇:(转)Libevent(3)— 基础库


下一篇:TCP/IP 笔记 - TCP连接管理