如何防止头文件被重复包含或引用?

一、条件编译

#ifndef ***

#define ***

 

#endif

二、#pragma once 

只要在头文件的最开始加入这条指令就能够保证头文件被编译一次,这条指令实际上在VC6中就已经有了,但是考虑到兼容性并没有太多的使用。

#pragmaonce是编译相关,就是说这个编译系统上能用,但在其他编译系统不一定可以,也就是说移植性差,不过现在基本上已经是每个编译器都有这个定义了。

#pragmaonce这种方式,是微软编译器独有的,也是后来才有的,所以知道的人并不是很多,用的人也不是很多,因为他不支持跨平台。如果你想写跨平台的代码,最好使用上一种。这是一种由编译器提供支持的方式,防止同一文件的二次编译,这里的同一文件指的是物理文件。

第一种也是有弊端的:

假如你的某一个头文件有多份拷贝,那么这些文件虽然在逻辑上都是一样的,但是在物理上他们却是不同的,所以当你把这些文件包含的时候,就会发现真的都包含进来了,然后就是编译错误了。还有,当物理上的同一文件被嵌套包含的时候,使用第一种方法预处理会每一次打开该文件做判断的,但是第二种方法则不会,所以在此#pragma once会更快些。下面举例说明

   // Test1.h
    #ifndefine  TEST1_H
    #defineTEST1_H
    ...
    #endif
  
    // Test2.h
    #pragma once        
    ...
    
    // Test.cpp
    #include "Test1.h"     // line 1
    #include "Test1.h"     // line 2
    #include "Test2.h"     // line 3
    #include "Test2.h"     // line 4 这里的Test2.h是同一物理文件

预处理器在执行这四句的时候,先打开Test1.h然后发现里面的宏TEST1_H没有被定义,所以会包含这个文件,第二句的时候,同样还是会打开Test1.h的发现宏已定义,就不包含该文件按了。

  第三句时,发现之前没有包含Test2,h则会把该文件包含进来,执行第四句的时候,发现该文件已经被包含了,所以不用打开就直接跳过了


 

条件编译

#include"a.h"

#include"b.h"
看上去没什么问题。如果a.h和b.h都包含了一个头文件x.h。那么x.h在此也同样被包含了两次,只不过它的形式不是那么明显而已。
多重包含在绝大多数情况下出现在大型程序中,它往往需要使用很多头文件,因此要发现重复包含并不容易。要解决这个问题,我们可以使用条件编译。如果所有的头文件都像下面这样编写:
#ifndef _HEADERNAME_H
#define _HEADERNAME_H

...//(头文件内容)

#endif

#ifndef _ASC_H
#define _ASC_H

...//(头文件内容)

#endif
那么多重包含的危险就被消除了。当头文件第一次被包含时,它被正常处理,符号HEADERNAME_H被定义为1。如果头文件被再次包含,通过条件编译,它的内容被忽略。符号HEADERNAME_H按照被包含头文件的文件名进行取名,以避免由于其他头文件使用相同的符号而引起的冲突。

但是,你必须记住预处理器仍将整个头文件读入,即使这个头文件所有内容将被忽略。由于这种处理将托慢编译速度,所以如果可能,应该避免出现多重包含。


 

  问题:test--1.0使用#ifndef只是防止了头文件被重复包含(其实本例中只有一个头件,不会存在重复包含的问题),但是无法防止变量被重复定义。如以下代码:

 1 //vs 2012 : test.c
 2 
 3 #include <stdio.h>
 4 #include "test.h"
 5 
 6 extern i;
 7 extern void test1();
 8 extern void test2();
 9 
10 int main()
11 {
12    test1();
13    printf("ok/n");
14    test2();
15    printf("%d/n",i);
16    return 0;
17 }

 

 1 //vs 2012 : test.h
 2 
 3 
 4 #ifndef _TEST_H_
 5 #define _TEST_H_
 6 
 7 char add1[] = "www.shellbox.cn/n";
 8 char add2[] = "www.scriptbox.cn/n";
 9 int i = 10;
10 void test1();
11 void test2();
12 
13 #endif

 

 1 //vs 2012 : test1.c
 2 
 3 --
 4 #include <stdio.h>
 5 #include "test.h"
 6 
 7 extern char add1[];
 8 
 9 void test1()
10 {
11    printf(add1);
12 }

 

 1 //vs 2012 : test2.c
 2 
 3 #include<stdio.h>
 4 #include "test.h"
 5 
 6 extern char add2[];
 7 extern i;
 8 
 9 void test2()
10 {
11    printf(add2);
12    for (; i > 0; i--) 
13        printf("%d-", i);
14 }

 

 错误分析:
由于工程中的每个.c文件都是独立的解释的,即使头文件有
#ifndef _TEST_H_

#define _TEST_H_

....   //其中内部变量是以定义的形式而不是外部声明的形式存放在这的

#enfif 
在其他文件中只要包含了test.h就会独立的解释,然后每个.c文件生成独立的变量标示符。

在编译器链接时,就会将工程中所有的符号整合在一起,由于文件中有重名变量标识符,编译器连接时不知道连接哪一个,于是就出现了重复定义的错误。
解决方法:
  在.c文件中定义变量,然后再建一个头文件(.h文件),在所有的变量声明前加上extern,注意这里不要对变量进行的初始化,加上外部声明在对.h文件展开时全是声明变量而不是定义,在编译器连接时只会去找定义的那个变量,外部声明的同名变量不会对其产生影响。

  然后在其他需要使用全局变量的.c文件中包含.h文件。编译器会为.c生成目标文件,然后链接时,如果该.c文件使用了全局变量,链接器就会链接到定义变量的.c文件 。

 1 //vs 2012 : test.h
 2 
 3 //-------------------------------
 4 
 5 #ifndef _TEST_H_
 6 
 7 #define _TEST_H_
 8 
 9 
10 extern int i;
11 
12 extern char add1[];
13 
14 extern char add2[];
15 
16 
17 void test1();
18 
19 void test2();
20  
21 
22 #endif

 

 1 //vs 2012 : test.c
 2 
 3 //-------------------------------
 4 
 5 #include <stdio.h>
 6 
 7 #include "test.h"
 8 
 9 
10 int i = 10;
11 
12 char add1[] = "www.shellbox.cn/n";
13 
14 char add2[] = "www.scriptbox.cn/n";
15 
16 extern void test1();
17 
18 extern void test2();
19 
20  
21 int main()
22 
23 {
24 
25    test1();
26 
27   printf("ok/n");
28 
29    test2();
30 
31   printf("%d/n",i);
32 
33    return 0;
34 
35 }

 

 

 1 //vs 2012 : test1.c
 2 
 3 //-------------------------------
 4 
 5 #include <stdio.h>
 6 
 7 #include "test.h"
 8 
 9 
10 extern char add1[];
11 
12 
13 void test1()
14 
15 {
16 
17    printf(add1);
18 
19 }

 

 1 //vs 2012 : test2.c
 2 
 3 //-------------------------------
 4 
 5 #include <stdio.h>
 6 
 7 #include "test.h"
 8 
 9  
10 extern char add2[];
11 
12 extern int i;

 

 1 void test2()
 2 
 3 {
 4 
 5    printf(add2);
 6 
 7    for (; i > 0;i--)
 8 
 9        printf("%d-",i);
10 
11 }

问题扩展: 变量的声明有两种情况:

    (1) 一种是需要建立存储空间的(定义、声明)。例如:int a在声明的时候就已经建立了存储空间。 
    (2) 另一种是不需要建立存储空间的(外部声明)。例如:extern int a其中变量a是在别的文件中定义的。
    前者是"定义性声明(defining declaration)"或者称为"定义(definition)",而后者是"引用性声明(referncingdeclaration)"。从广义的角度来讲声明中包含着定义,但是并非所有的声明都是定义,例如:

  int a它既是声明,同时又是定义。

  然而对于extern a来讲它只是声明不是定义。一般的情况下我们常常这样叙述,把建立空间的声明称之为"定义",而把不需要建立存储空间称之为"声明"。很明显我们在这里指的声明是范围比较窄的,也就是说非定义性质的声明。

例如:在主函数中 
int main()
{
    extern int A; //这是个声明而不是定义,声明A是一个已经定义了的外部变量
                 //注意:声明外部变量时可以把变量类型去掉如:extern A;
    dosth();      //执行函数
}

int A;            //是定义,定义了A为整型的外部变量(全局变量) 
    外部变量(全局变量)的"定义"与外部变量的"声明"是不相同的,外部变量的定义只能有一次,它的位置是在所有函数之外,而同一个文件中的外部变量声明可以是多次的,它可以在函数之内(哪个函数要用就在那个函数中声明)也可以在函数之外(在外部变量的定义点之前)。系统会根据外部变量的定义(而不是根据外部变量的声明)分配存储空间的对于外部变量来讲,初始化只能是在"定义"中进行,而不是在"声明"中。所谓的"声明",其作用,是声明该变量是一个已定义过的外部变量,仅仅是在为了引用该变量而作的"声明"而已。extern只作声明,不作定义。 
    用static来声明一个变量的作用有二:
    (1) 对于局部变量用static声明,则是为该变量分配的空间在整个程序的执行期内都始终存在
    (2) 外部变量用static来声明,则该变量的作用只限于本文件模块

(此部分参考自:如何防止头文件被重复包含、嵌套包含

三、前置声明:

在编写C++程序的时候,偶尔需要用到前置声明(Forward declaration)。下面的程序中,带注释的那行就是类B的前置说明。这是必须的,因为类A中用到了类B,

而类B的声明出现在类A的后面。如果没有类B的前置说明,下面的程序将不同通过编译,编译器将会给出类似“缺少类型说明符”这样的出错提示。

 1 // A.h  
 2 
 3 #include "B.h"  
 4 
 5 class A  
 6 
 7 {  
 8 
 9     B b;  
10 
11 public:  
12 
13     A(void);  
14 
15     virtual ~A(void);  
16 
17 };  

 

 1 //A.cpp  
 2 
 3 #include "A.h"  
 4 
 5 A::A(void)  
 6 
 7 {  
 8 
 9 }  
10 
11   
12 A::~A(void)  
13 
14 {  
15 
16 }  

 

 1 // B.h  
 2 
 3 #include "A.h"  
 4 
 5 class B  
 6 
 7 {  
 8 
 9     A a;  
10 
11 public:  
12 
13     B(void);  
14 
15     ~B(void);  
16 
17 };  

 

 1 // B.cpp  
 2 
 3 #include "B.h"  
 4 
 5 B::B(void)  
 6 
 7 {  
 8 
 9 }  
10 
11 
12 B::~B(void)  
13 
14 {  
15 
16 }

  编译一下A.cpp,不通过。再编译B.cpp,还是不通过。编译器去编译A.h,发现包含了B.h,就去编译B.h。编译B.h的时候发现包含了A.h,但是A.h已经编译过了(其实没有编译完成,可能编译器做了记录,A.h已经被编译了,这样可以避免陷入死循环。编译出错总比死循环强点),就没有再次编译A.h就继续编译。后面发现用到了A的定义,这下好了,A的定义并没有编译完成,所以找不到A的定义,就编译出错了。

 

这时使用前置声明就可以解决问题:

 1 // A.h  
 2 
 3 #include "B.h" 
 4 
 5 class B; //前置声明
 6 
 7 class A  
 8 
 9 {
10     private:  
11         B  b;  
12 
13     public:  
14         A(void);  
15         virtual ~A(void);  
16 
17 };  

 

 1 //A.cpp 
 2 
 3 #include "A.h"  
 4 
 5 A::A(void)  
 6 
 7 {  
 8 
 9 }  
10 
11 
12 A::~A(void)  
13 
14 {  
15 
16 }  

 

 1 // B.h  
 2 
 3 #include "A.h"  
 4 
 5 class B  
 6 
 7 {
 8     private:   
 9     A a;  
10 
11     public:  
12     B(void);  
13     ~B(void);  
14 
15 };  

 

 1 // B.cpp 
 2 
 3 #include "B.h"  
 4 
 5 B::B(void)  
 6 {  
 7 
 8 }  
 9  
10 
11 B::~B(void)  
12 {  
13 
14 }

 

 1 test.cpp 
 2 
 3 int main()
 4 {
 5     B* b = new B();
 6 
 7     A* a = new A();
 8 
 9     delete a;
10 
11     delete b;
12 
13     return 0;
14 }

类的前置声明是有许多的好处的。

我们使用前置声明的一个好处是,从上面看到,当我们在类A使用类B的前置声明时,我们修改类B时,只需要重新编译类B,而不需要重新编译a.h的(当然,在真正使用类B时,必须包含b.h)。

另外一个好处是减小类A的大小,上面的代码没有体现,那么我们来看下:

 1 //a.h  
 2 
 3 class B;  
 4 
 5 class A  
 6 
 7 {  
 8     ....  
 9 
10 private:  
11 
12     B *b;  
13 ....  
14 
15 };  

 

 1 //b.h  
 2 
 3 class B  
 4 
 5 {  
 6 ....  
 7 
 8 private:  
 9 
10     int a;  
11 
12     int b;  
13 
14     int c;  
15 
16 }; 

我们看上面的代码,类B的大小是12(在32位机子上)。

如果我们在类A中包含的是B的对象,那么类A的大小就是12(假设没有其它成员变量和虚函数)。如果包含的是类B的指针*b变量,那么类A的大小就是4,所以这样是可以减少类A的大小的,

特别是对于在STL的容器里包含的是类的对象而不是指针的时候,这个就特别有用了。在前置声明时,我们只能使用的就是类的指针和引用(因为引用也是居于指针的实现的)。

为什么我们前置声明时,只能使用类型的指针和引用呢?

看下下面这个类:

 1 class A  
 2 
 3 {  
 4 
 5 public:  
 6 
 7     A(int a):_a(a),_b(_a){} // _b is new add  
 8 
 9       
10 
11     int get_a() const {return _a;}  
12 
13     int get_b() const {return _b;} // new add  
14 
15 private:  
16 
17     int _b; // new add  
18 
19     int _a;  
20 
21 };  

上面定义的这个类A,其中_b变量和get_b()函数是新增加进这个类的。

改变:

第一个改变当然是增加了_b变量和get_b()成员函数;

第二个改变是这个类的大小改变了,原来是4,现在是8。

第三个改变是成员_a的偏移地址改变了,原来相对于类的偏移是0,现在是4了。

上面的改变都是我们显式的、看得到的改变。还有一个隐藏的改变。

隐藏的改变是类A的默认构造函数和默认拷贝构造函数发生了改变。

由上面的改变可以看到,任何调用类A的成员变量或成员函数的行为都需要改变,因此,我们的a.h需要重新编译。

如果我们的b.h是这样的:

 1 //b.h  
 2 
 3 #include "a.h"  
 4 
 5 class B  
 6 
 7 {  
 8 ...  
 9 
10 private:  
11 
12     A a;  
13 
14 };  

那么我们的b.h也需要重新编译。

如果是这样的:

 1 //b.h  
 2 
 3 class A;  
 4 
 5 class B  
 6 
 7 {  
 8 
 9 ... 
10 
11 private:  
12 
13    A *a;  
14 
15 };  

那么我们的b.h就不需要重新编译。

像我们这样前置声明类A:

classA;

是一种不完整的声明,只要类B中没有执行需要了解类A的大小或者成员的操作,则这样的不完整声明允许声明指向A的指针和引用。

而在前一个代码中的语句

Aa;

是需要了解A的大小的,不然是不可能知道如果给类B分配内存大小的,因此不完整的前置声明就不行,必须要包含a.h来获得类A的大小,同时也要重新编译类B。

再回到前面的问题,使用前置声明只允许的声明是指针或引用的一个原因是只要这个声明没有执行需要了解类A的大小或者成员的操作就可以了,所以声明成指针或引用是没有

执行需要了解类A的大小或者成员的操作的

 

前置声明解决两个类的互相依赖

 1 // A.h 
 2 
 3 class B; 
 4 
 5 class A 
 6 
 7 { 
 8     B* b; 
 9 public: 
10 
11     A(B* b):b(b)
12     {}  
13 
14     void something()
15     {
16         b->something();
17     }
18 
19 };  

   

 1  //A.cpp 
 2 
 3     #include "B.h" 
 4 
 5     #include "A.h" 
 6 
 7     A::A(B * b) 
 8 
 9     { 
10         b= new B; 
11     } 
12 
13 
14     A::~A(void) 
15     { 
16          delete b;
17     } 

 

 1 // B.h 
 2 
 3  class A; 
 4 
 5  class B 
 6 { 
 7 
 8      A a; 
 9 
10     public: 
11 
12      B(void); 
13     void something()
14     {
15         cout<<"something happend ..."<<endl; 
16     }
17    
18      ~B(void); 
19 };     

 

 1  // B.cpp 
 2 
 3 #include "A.h" 
 4 #include "B.h" 
 5 
 6 B::B(void) 
 7 { 
 8      a= New A; 
 9 } 
10 
11 
12 B::~B(void) 
13 { 
14 
15 }

 

 1 test.cpp
 2 
 3 int main()
 4 {
 5     B * n = new B();
 6     A *a = new A(b);
 7     delete a;
 8     delete b;
 9     return 0;
10 
11 }

编译之后发现错误:使用了未定义的类型B;

     ->something 的左边必须指向类/结构/联合/类型 

原因:

1.       (1)处使用了类型B的定义,因为调用了类B中的一个成员函数。前置声明class B;仅仅声明了有一个B这样的类型,而并没有给出相关的定义,类B的相关定义,是在类A后面出现的,因此出现了编译错误;

2.       代码一之所以能够通过编译,是因为其中仅仅用到B这个类型,并没有用到类B的定义。

解决办法是什么?

将类的声明和类的实现(即类的定义)分离。如下所示:

 1 // A.h 
 2 
 3 class B; 
 4 
 5 class A 
 6 
 7 { 
 8 
 9     B* b; 
10 
11 public: 
12 
13     A(B* b):b(b)
14     {}  
15 
16      void something();
17 
18 ~A(void)
19 
20 }; 

      

 1  // B.h 
 2  class A; 
 3 
 4 class B 
 5 { 
 6 
 7      A a; 
 8 
 9     public: 
10 
11         B(void); 
12 
13     void something();
14 
15        ~B(void); 
16 
17 }; 

 

 1     //A.cpp 
 2 
 3     #include "B.h" 
 4 
 5 #include "A.h" 
 6 
 7 A::A(B * b) 
 8 
 9 { 
10 
11      b= new B; 
12 
13 }     
14 
15 
16 void something()
17 {
18 
19 b->something();
20 
21 }   
22 
23  A::~A(void) 
24 {  } 

 

   

 1 // B.cpp 
 2 
 3 #include "A.h" 
 4 
 5 #include "B.h" 
 6 
 7 B::B(void) 
 8 
 9 { 
10 
11     a= New A; 
12 
13 } 
14 
15 void B::something()
16 
17 {
18 
19 cout<<"something happend ..."<<endl; 
20 
21 }
22 
23 
24 B::~B(void) 
25  {   }

 

 1 test.cpp
 2  
 3 
 4 int main()
 5 
 6 {
 7     B * n = new B();
 8 
 9     A *a = new A(b);
10 
11     delete a;
12 
13     delete b;
14 
15     return 0;
16 }

结论:

前置声明只能作为指针或引用,不能定义类的对象,自然也就不能调用对象中的方法了。

而且需要注意,如果将类A的成员变量B* b;改写成B& b;的话,必须要将b在A类的构造函数中,采用初始化列表的方式初始化,否则也会出错。

posted @ 2020-12-24 15:29  北极星!  阅读(1811)  评论(0编辑  收藏  举报