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

一、#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没有被定义,所以会包含这个文件,第二句的时候,同样还是会打开Test2.h的发现宏已定义,就不包含该文件按了。第三句时,发现之前没有包含Test2,h则会把该文件包含进来,执行第四句的时候,发现该文件已经被包含了,所以不用打开就直接跳过了

 

二、条件编译

 

#include"a.h"

#include"b.h"

看上去没什么问题。如果a.h和b.h都包含了一个头文件x.h。那么x.h在此也同样被包含了两次,只不过它的形式不是那么明显而已。

多重包含在绝大多数情况下出现在大型程序中,它往往需要使用很多头文件,因此要发现重复包含并不容易。要解决这个问题,我们可以使用条件编译。如果所有的头文件都像下面这样编写:
#ifndef _HEADERNAME_H
#define _HEADERNAME_H

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

#endif

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

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

 

 

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

 

//vs 2012 : test.c


#include <stdio.h>
#include "test.h"

extern i;
extern void test1();
extern void test2();

int main()
{
   test1();
   printf("ok/n");
   test2();
   printf("%d/n",i);
   return 0;
}



//vs 2012 : test.h


#ifndef _TEST_H_
#define _TEST_H_

char add1[] = "www.shellbox.cn/n";
char add2[] = "www.scriptbox.cn/n";
int i = 10;
void test1();
void test2();

#endif




//vs 2012 : test1.c

--
#include <stdio.h>
#include "test.h"

extern char add1[];

void test1()
{
   printf(add1);
}




//vs 2012 : test2.c

#include<stdio.h>
#include "test.h"

extern char add2[];
extern i;

void test2()
{
   printf(add2);
   for (; i > 0; i--) 
       printf("%d-", i);
}

 

 错误分析:
由于工程中的每个.c文件都是独立的解释的,即使头文件有
#ifndef _TEST_H_ #define _TEST_H_ .... #enfif
在其他文件中只要包含了test.h就会独立的解释,然后每个.c文件生成独立的标示符。在编译器链接时,就会将工程中所有的符号整合在一起,由于文件中有重名变量,于是就出现了重复定义的错误。

解决方法:
在.c文件中定义变量,然后再建一个头文件(.h文件),在所有的变量声明前加上extern,注意这里不要对变量进行的初始化。然后在其他需要使用全局变量的.c文件中包含.h文件。编译器会为.c生成目标文件,然后链接时,如果该.c文件使用了全局变量,链接器就会链接到定义变量的.c文件 。

//vs 2012 : test.h

//-------------------------------

#ifndef _TEST_H_

#define _TEST_H_

 

extern int i;

extern char add1[];

extern char add2[];

 

void test1();

void test2();

 

#endif

 

 

//vs 2012 : test.c

//-------------------------------

#include <stdio.h>

#include "test.h"

 

 

int i = 10;

char add1[] = "www.shellbox.cn/n";

char add2[] = "www.scriptbox.cn/n";

extern void test1();

extern void test2();

 

int main()

{

   test1();

  printf("ok/n");

   test2();

  printf("%d/n",i);

   return 0;

}

 

//vs 2012 : test1.c

//-------------------------------

#include <stdio.h>

#include "test.h"

 

extern char add1[];

 

void test1()

{

   printf(add1);

}

 

 

//vs 2012 : test2.c

//-------------------------------

#include <stdio.h>

#include "test.h"

 

extern char add2[];

extern int i;

 

void test2()

{

   printf(add2);

   for (; i > 0;i--)

       printf("%d-",i);

}

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

   (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的前置说明,下面的程序将不同通过编译,编译器将会给出类似“缺少类型说明符”这样的出错提示。

 

// A.h  

#include "B.h"  

class A  

{  

    B b;  

public:  

    A(void);  

    virtual ~A(void);  

};  

  

//A.cpp  

#include "A.h"  

A::A(void)  

{  

}  

  

  

A::~A(void)  

{  

}  

  

// B.h  

#include "A.h"  

class B  

{  

    A a;  

public:  

    B(void);  

    ~B(void);  

};  

  

// B.cpp  

#include "B.h"  

B::B(void)  

{  

}  

  

  

B::~B(void)  

{  

}



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

 

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

// A.h  

#include "B.h" 

class B; //前置声明

class A  

{



private:  

    B  b;  

public:  

    A(void);  

    virtual ~A(void);  

};  

  

//A.cpp 

#include "A.h"  

A::A(void)  

{  

}  

  

  

A::~A(void)  

{  

}  

  

// B.h  

#include "A.h"  

class B  

{

private:   

    A a;  

public:  

    B(void);  

    ~B(void);  

};  

  

// B.cpp 

#include "B.h"  

B::B(void)  

{  

}  

  

  

B::~B(void)  

{  

}


test.cpp


int main()

{

B* b = new B();

A* a = new A();

delete a;

delete b;

return 0;

}

 

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

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

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

 

//a.h  

class B;  

class A  

{  

    ....  

private:  

    B *b;  

....  

};  

//b.h  

class B  

{  

....  

private:  

    int a;  

    int b;  

    int c;  

};  

 

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

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

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

 

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

看下下面这个类:

 

class A  

{  

public:  

    A(int a):_a(a),_b(_a){} // _b is new add  

      

    int get_a() const {return _a;}  

    int get_b() const {return _b;} // new add  

private:  

    int _b; // new add  

    int _a;  

};  

 

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

改变:

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

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

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

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

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

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

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

 

//b.h  

#include "a.h"  

class B  

{  

...  

private:  

    A a;  

};  

 

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

如果是这样的:

 

//b.h  

class A;  

class B  

{  

... 

private:  

   A *a;  

};  

 

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

像我们这样前置声明类A

classA;

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

而在前一个代码中的语句

Aa;

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

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

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

 

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

    // A.h 

class B

class A 

    B* b; 

public

    A(B* b):b(b)

{}  

        void something()

{

b->something();

}

    }; 

     

    //A.cpp 

    #include "B.h" 

    #include "A.h" 

    A::A(B * b

    { 

        b= new B

    } 

     

     

    A::~A(void

    { 

     delete b;

    } 

     

    // B.h 

    class A

    class B 

    { 

        A a; 

    public

        B(void); 

void something()

{

cout<<"something happend ..."<<endl; 

}

       ~B(void); 

    }; 

     

    // B.cpp 

    #include "A.h" 

    #include "B.h" 

    B::B(void

    { 

        a= New A; 

    } 

     

     

    B::~B(void

    { 

    }


test.cpp


int main()

{

B * n = new B();

A *a = new A(b);


delete a;

delete b;

return 0;

}


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

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

原因:

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

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

 

解决办法是什么?

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


    // A.h 

class B

class A 

    B* b; 

public

    A(B* b):b(b)

{}  

        void something();

~A(void)

    }; 

         

    // B.h 

    class A

    class B 

    { 

        A a; 

    public

        B(void); 

void something();

       ~B(void); 

    }; 


    //A.cpp 

    #include "B.h" 

    #include "A.h" 

    A::A(B * b

    { 

        b= new B

    }     

        void something()

{

b->something();

}   

    A::~A(void

    {  } 


     

    // B.cpp 

    #include "A.h" 

    #include "B.h" 

    B::B(void

    { 

        a= New A; 

    } 

     

void B::something()

{

cout<<"something happend ..."<<endl; 

}

     

    B::~B(void

    {   }


test.cpp


int main()

{

B * n = new B();

A *a = new A(b);


delete a;

delete b;

return 0;

}


结论:

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

 

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


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/459401.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何解决类模板的分离编译问题?

一模板&#xff1a; 模板不是数据类型&#xff0c;只能算是一种行为集合的表示。编译器在使用模板时&#xff0c;通过更换模板参数来创建数据类型。这个过程就是模板实例化(Instantiation)&#xff0c; 从模板类创建得到的类型称之为特例(specialization)&#xff0c;说白了就是…

结对项目 刘静 201303014059 计科高职13-2

结对&#xff1a;人&#xff1a;孙帅 博客地址&#xff1a; http://www.cnblogs.com/s3366181/p/4509260.html一、 题目简介 1.所选题目&#xff1a;输出圆的面积 2.编程工具&#xff1a;Eclipse 3、实现功能&#xff1a;用户给定一圆的半径运行程序系统会给出给定半径圆的面…

弹出div或者弹出新窗口的固定位置、固定大小

2019独角兽企业重金招聘Python工程师标准>>> js代码&#xff1a; //打开一个新窗口&#xff0c;固定的位置&#xff0c;固定的大小 //window.open("push_add.html",newwindow, height550, width1000, top200, left500, toolbarno, menubarno, scro…

NAT(网络地址转换)技术与代理服务器原理

一、 Nat技术&#xff1a; NAT英文全称是“Network Address Translation”&#xff0c;中文意思是“网络地址转换”&#xff0c;它是一个IETF(Internet Engineering Task Force,Internet工程任务组)标准&#xff0c;允许一个整体机构以一个公用IP&#xff08;Internet Prot…

原 Linux搭建SVN 服务器2

原 Linux搭建SVN 服务器 发表于1年前(2014-08-05 17:55) 阅读&#xff08;12257&#xff09; | 评论&#xff08;3&#xff09; 31人收藏此文章, 我要收藏赞3摘要 Linux搭建SVN 服务器目录[-] Linux搭建SVN 服务器1 安装SVN2 使用客户端连接2.1 使用…

网络层核心:路由和路由生成算法

一、路由和路由算法简介&#xff1a; 路由就是通过互连的网络把信息从源地址传送到目的地址的活动。路由发生在OSI网络参考模型的第三层即网络层。 路由引导封包转送&#xff0c;经过一些中间的节点后&#xff0c;到达目的地。把该功能做成硬件的话称为路由器。路由通常根据路…

网络:常见的端口号及分类

一、端口号概念 在网络技术中&#xff0c;端口&#xff08;Port&#xff09;包括逻辑端口和物理端口两种类型。物理端口指的是物理存在的端口&#xff0c;如ADSL Modem、集线器、交换机、路由器上用 于连接其他网络设备的接口&#xff0c; 如RJ-45端口、SC端口等等。逻辑端口…

EditPlus 技巧大全:[1]怎么配置PHP编译环境

editplus是一款小巧但功能强大易扩展的文本编辑器&#xff0c;可以通过设置用户工具将其作为C,Java,Php等等语言的一个简单的IDE。 工具/原料 EditPlus v3.3.1 php 5.3.14 方法/步骤 1.打开editplus 2.点击菜单栏“工具” 3.选择下拉菜单的“配置用户工具”&#xff0c;进入配置…

网络:传输层 TCP报文格式解析

一、TCP报文格式 1、为了提供可靠的数据传输&#xff0c;TCP报文首部字段有较多的字段&#xff0c;TCP报文格式如下图&#xff1a; 图2 TCP报文格式 16位源和目标端口&#xff08;16位&#xff09;&#xff1a;用于多路复用/多路分解来自或送至上层应用的数据&#xff0c;可以…

MATLAB图像小波变换

为什么80%的码农都做不了架构师&#xff1f;>>> 小波变换与小波包变换 人脸图像f(x,y) 的一层小波变换如下图所示&#xff1a; 图中L 和H 分别表示低通滤波器和高通滤波器&#xff0c;l(n) 和h(n) 分别表示它们相应的脉冲响应&#xff0c;2↓1表示降2采样fLL和fHH分…

grunt之Gruntfile(1)

grunt 执行的时候&#xff0c;他会找该目录下的Gruntfile文件&#xff0c;所以&#xff0c;要在目录下创建Gruntfile文件。 下面我demo一个copy任务&#xff1a; 执行copy&#xff0c;首先我们要一个copy的模块&#xff0c;那么我们先安装下copy模块 首先&#xff0c;我到H盘&a…

MyEclipse从数据库反向生成实体类之Hibernate方式 反向工程

2019独角兽企业重金招聘Python工程师标准>>> 开发项目涉及到的表太多&#xff0c;一个一个的写JAVA实体类很是费事。MyEclipse提供简便的方法&#xff1a;反向数据库 步骤大致如下: 第一步&#xff1a; window-->open Perspective-->MyEclipse Java Persisten…

开始nodejs+express的学习+实践(8)

为什么80%的码农都做不了架构师&#xff1f;>>> 1.session使用 介绍的非常详细&#xff1a; http://www.cnblogs.com/chenchenluo/p/4197181.html 对比我们的app.js需要引入express-session模块和使用这个模块&#xff0c;在package依赖&#xff0c;并加载。 我们修…

maven项目部署到linux上的奇葩问题

2019独角兽企业重金招聘Python工程师标准>>> 经常会遇到这样子的问题&#xff0c;maven项目在本地的eclipse配置的好好的&#xff0c;结果一到服务器就运行不起来。 当然遇到这种情况&#xff0c;我们首先会想到环境变量和相关的路径问题&#xff0c;但是当这两个条…

网络:TCP通讯之 time_wait 状态

基于TCP协议的通讯流程1、TCP建立连接2、TCP断开连接3、TCP状态转换TCP状态解释&#xff1a; SYN-RECVD&#xff1a;再收到和发送一个连接请求后等待对方对连接请求的确认 ESTABLISHED&#xff1a;代表一个打开的连接 FIN-WAIT-1&#xff1a;等待远程TCP连接中断请求&#xff0…

linux下echo与time服务的程序实现

一、针对ECHO服务的TCP客户软件的实现 1.网络拓扑结构&#xff1a; 2.源码&#xff1a; 1 #include <stdio.h>2 #include <stdlib.h>3 #include <unistd.h>4 #include <string.h>5 #include <stdarg.h>6 #include <sys/types.h>7 #include…

Linux高性能服务器编程:进程池和线程池原理及应用(有图有代码有真相!!!)

一、问题引入 在前面编写多进程、多线程服务器时通过动态创建子进程和子线程来实现并发服务器&#xff0c;这样做有以下缺点&#xff1a; 1&#xff09;动态创建进程、线程将会比较耗费时间&#xff0c;将导致较慢的客户响应。 2&#xff09;动态创建的子进程只为一个客户服…

Linux:多进程、多线程服务器的实现解析(有图有代码有真相!!!)

一、问题引入 阻塞型的网络编程接口 几乎所有的程序员第一次接触到的网络编程都是从 listen()、send()、recv()等接口开始的。使用这些接口可以很方便的构建服务器 /客户机的模型。 我们假设希望建立一个简单的服务器程序&#xff0c;实现向单个客户机提供类似于“一问一答”的…

数据结构:神奇的B树实现解析(有图有代码有真相!!!)

一、B树引入 二叉搜索树、平衡二叉树、红黑树都是动态查找树&#xff0c;典型的二叉搜索树结构&#xff0c;查找的时间复杂度和树的高度相关O(log2N)。 1&#xff09;数据杂乱无章-------线性查找--O&#xff08;n&#xff09; 2&#xff09;数据有序-------二分查找 ---O(lo…

Linux:dup/dup2 文件描述符重定向函数(有图有代码有真相!!!)

一、dup/dup2 有时我们希望把标准输入重定向到一个文件&#xff0c;或者把标准输出重定向到一个网络连接。系统调用dup和dup2能够复制文件描述符。dup返回新的文件文件描述符&#xff08;没有用的文件描述符最小的编号&#xff09;。 dup2可以让用户指定返回的文件描述符的值…