头文件重复包含和变量重复定义

---------------------
版权声明:本文为CSDN博主「printfnothing」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u014557232/article/details/50354127

在c或c++中,头文件重复包含问题是程序员必须避免的问题,也是很多新手容易犯错的问题。
为什么要避免头文件重复包含呢?
       1.我们知道在编译c或c++程序时候,编译器首先要对程序进行预处理,预处理其中一项工作便是将你源程序中#include的头文件完整的展开,如果你有意或无意的多次包含相同的头文件,会导致编译器在后面的编译步骤多次编译该头文件,工程代码量小还好,工程量一大会使整个项目编译速度变的缓慢,后期的维护修改变得困难。
       2.第一点讲的头文件重复包含的坏处其实还能忍,毕竟现在计算机运算能力已经不是早些时候那么慢了。但是头文件重复包含带来的最大坏处是会使程序在编译链接的时候崩溃,这是我们无法容忍的。

来看个例子:

  

    //a.h
    #include<stdio.h>
    int A=1;
     
     
    //b.h
    #include "a.h"
    void f(){printf("%d",A);}
     
    //main.c
    #include<stdio.h>
    #include"a.h"
    #include"b.h"
    void main(){f();}

此时输入gcc -c main.c进行编译,会提示A重复定义,程序崩溃:


然后输入gcc -E main.c -o main.i看下预处理内容:


可以看到6015行和6021行重复出现int A=1;的定义,违背了一次定义的原则,所以会出错。

了解了头文件重复包含的坏处,那么如何避免它呢?
通常有两种做法:条件编译#pragma once
条件编译就是通常的
#ifndef _XXX
#define _XXX
...
#endif
具体怎么用,可以google。这里介绍下#pragma once:#pragma once这种方式,是微软编译器独有的,也是后来才有的,所以知道的人并不是很多,用的人也不是很多,因为他不支持跨平台。如果你想写跨平台的代码,最好使用条件编译。如果想使用#pragma once,只需在头文件开头加上#pragma once即可。
两者的联系与区别:
 联系是都可以避免头文件重复包含,区别主要在于两者避免头文件重复包含的实现方式上。
再看上面的例子:
暂不考虑b.h,将main.c中变为:

   

    #include<stdio.h>
    #include "a.h"
    #include "a.h"
     
    void main()
    {
       printf("%d",A);
    }

1.在a.h中加入条件编译:

   

    //a.h
     
    #include<stdio.h>
    #ifndef _A_H
    #define _A_H
     
    int A = 1;
     
    #endif;

2.在a.h 中加入#pragma once:

   

    //a.h
     
    #pragma once
    #include<stdio.h>
    int A = 1;

考虑情况1条件编译:
编译main.c时,预处理阶段遇到①,编译器打开a.h,发现_A_H未定义,于是将 #define到#endif之间的内容包含进main.c;当遇到②时,编译器再次打开a.h,发现_A_H已经定义,于是直接关闭a.h,a.h没有再次包含进main.c,从而避免了重复包含。
考虑情况2#pragma once:
预处理阶段遇到①时,打开a.h,将#pragma once后面的内容包含进main.c中,关闭a.h。遇到②时,编译器直接跳过该语句,执行后面的语句,从而避免重复包含。

讲完了文件的重复包含,让我们来思考一个问题:如前所说,避免头文件的重复包含可以有效地避免变量的重复定义,其实不光是变量的重复定义,也可以避免函数和类、结构体的重复定义。但是
避免头文件的重复包含是否一定可以避免变量、函数、类、结构体的重复定义?
答案当然是否!
让我们再看下面的例子:

   

     //a.h
     
    #include<stdio.h>
    #ifndef _A_H
    #define _A_H
     
    int A = 1;
     
    #endif;

   

     //b.h
     
    #include<stdio.h>
    #include "a.h"
    void fb();
      //b.c
     
    #include"b.h"
    void fb()
    {
       printf("%d",A+1);
    }

 
     

     //c.h
     
    #include<stdio.h>
    #include "a.h"
    void fc();
     //c.c
     
    #include"c.h"
    void fc()
    {
       printf("%d",A+2);
    }

    
      

     //main.c
     
    #include<stdio.h>
    #include "b.h"
    #include "c.h"
    void main()
    {
        fb();
        fc();
    }

然后分别编译gcc -c b.c -o b.o和gcc -c main.c -o main.o,并未提示任何错误。
但是当生成可执行文件时候gcc b.o main.o -o main,编译器提示出错:


 为什么会出错呢?按照条件编译,a.h并没有重复包含,可是还是提示变量A重复定义了。
在这里我们要注意一点,变量,函数,类,结构体的重复定义不仅会发生在源程序编译的时候,在目标程序链接的时候同样也有可能发生。我们知道c/c++编译的基本单元是.c或.cpp文件,各个基本单元的编译是相互独立的,#ifndef等条件编译只能保证在一个基本单元(单独的.c或.cpp文件)中头文件不会被重复编译,但是无法保证两个或者更多基本单元中相同的头文件不会被重复编译,不理解?没关系,还是拿刚才的例子讲:
gcc -c b.c -o b.o :b.c文件被编译成b.o文件,在这个过程中,预处理阶段编译器还是会打开a.h文件,定义_A_H并将a.h包含进b.c中。
gcc -c c.c -o c.o:c.c文件被编译成c.o文件,在这个过程中,请注意预处理阶段,编译器依旧打开a.h文件,此时的_A_H是否已被定义呢?前面提到不相关的.c文件之间的编译是相互独立的,自然,b.c的编译不会影响c.c的编译过程,所以c.c中的_A_H不会受前面b.c中_A_H的影响,也就是c.c的_A_H是未定义的!!于是编译器再次干起了相同的活,定义_A_H,包含_A_H。
到此,我们有了b.o和c.o,编译main.c后有了main.o,再将它们链接起来生成main时出现问题了:
编译器在编译.c或.cpp文件时,有个很重要的步骤,就是给这些文件中含有的已经定义了的变量分配内存空间,在a.h中A就是已经定义的变量,由于b.c和c.c独立,所以A相当于定义了两次,分配了两个不同的内存空间。在main.o链接b.o和c.o的时候,由于main函数调用了fb和fc函数,这两个函数又调用了A这个变量,对于main函数来说,A变量应该是唯一的,应该有唯一的内存空间,但是fb和fc中的A被分配了不同的内存,内存地址也就不同,main函数无法判断那个才是A的地址,产生了二义性,所以程序会出错。

讲了这么多,那么到底怎么样才能避免重复定义呢?
其实避免重复定义关键是要避免重复编译,防止头文件重复包含是有效避免重复编译的方法,但是最好的方法还是记住那句话:头文件尽量只有声明,不要有定义。这么做不仅仅可以减弱文件间的编译依存关系,减少编译带来的时间性能消耗,更重要的是可以防止重复定义现象的发生,防止程序崩溃
 

发布了55 篇原创文章 · 获赞 29 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/ruibin_cao/article/details/98631372