C++——重复编译与重复定义

来源引用:

https://blog.csdn.net/Tsinting/article/details/62232518(非常清楚)

https://www.cnblogs.com/jdxn/p/6970228.html

http://www.cnblogs.com/xuepei/p/4027946.html

https://blog.csdn.net/u014557232/article/details/50354127

https://blog.csdn.net/qq_34809033/article/details/80652116(非常清楚)

 

为什么要避免重复包含?


       1.在编译c或c++程序时候,编译器首先要对程序进行预处理,预处理其中一项工作便是将你源程序中#include的头文件完整的展开,如果你在(同一个.cpp下)有意或无意的多次包含相同的头文件,会导致编译器在后面的编译步骤多次编译该头文件,工程代码量小还好,工程量一大会使整个项目编译速度变的缓慢,后期的维护修改变得困难。
       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();} 

 

如何解决?(两种做法)

方法一:条件编译:

//条件编译
//test.h

#ifndef _TEST_H_
#define _TEST_H_                //一般是文件名的大写
//文件名全大写,前后加下划线,并把文件名中的“.”也变成下划线,
//………………………………
//………………………………
//………………………………
//………………………………
//………………………………

#endif

值得注意的是,#ifndef起到的效果是防止一个源文件(上例指main.c)两次包含同一个头文件(上例指a.h),而不是防止两个源文件包含同一个头文件。网上很多资料对这一细节的描述都是错误的。事实上,防止同一头文件被两个不同的源文件包含这种要求本身就是不合理的,头文件存在的价值就是被不同的源文件包含。也就是说对于A.cpp和B.cpp两个源文件都包含了head.h的头文件时,条件编译不会起到作用(如下例——b.cpp和c.cpp分别包含了a.h,a.h被重复包含甚至定义)。这也就意味着如果头文件中定义了类外的函数或者全局变量,那么当多个源文件同时包含这一头文件时,会发生重复定义的错误(如下例 int A =1)。

//a.h  
  
#include<stdio.h>  
#ifndef _A_H  
#define _A_H  
  
int A = 1;  
  
#endif;  
//b.h  
  
#include<stdio.h>  
#include "a.h"  
void f();  
  
//b.c  
  
#include"b.h"  
void f()  
{  
   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();  
}

条件编译只防止“(同一个.cpp)重复编译(导致的重复定义)”,而不是“重复定义
重复编译可能造成重复定义,但重复定义的来源不只有重复编译

从代码变成可执行的程序,需要两个步骤
编译和链接
编译开始时,将所有#include头文件的地方替换成该头文件的代码
在编译阶段,编译所有源文件成为模块,各模块中的每个变量与函数都得到了属于自己的空间
在链接阶段,各个模块被组合到一起

上例为什么会出错呢?按照条件编译,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的地址,产生了二义性,所以程序会出错。

#ifndef能够防止在编译阶段,(同一个.cpp中include的)一段代码被重复编译,并且由此可以避免一个变量被重复定义
但它不能防止链接阶段,各模块中都有叫某个名字的变量,于是报链接错误:变量重复定义

 

子问题:如何解决重复定义?

为了避免重复定义,一般头文件中不会存放定义,只存放函数声明和变量的声明。但也有例外,类、inline函数和编译时值已知的const对象可以在头文件中定义,这是因为遵守“单一定义规则”(One-Definition Rule, ODR)。根据此规则, 如果对同一个类的两个定义完全相同且出现在不同编译单位,会被当作同一个定义。当包含类的头文件分别被两个不同的编译单位(file1.cpp, file2.cpp)包含,满足ODR规则,会被当作同一个定义,所以不会有冲突。此外,模板和inline函数也适用此规则。

解决方法即不仅用#ifndef组合防止重复编译,而且将变量在源文件中定义,只在头文件里放extern声明
这样各模块在编译的时候,就知道“有这么个变量,但它的空间不在我这里”,链接的时候,这个变量虽然出现在所有包含这个头文件的模块里,但只有一个模块是它的真身所在

 

方法二:#pragma once

#pragma once这种方式,是微软编译器独有的,也是后来才有的,所以知道的人并不是很多,用的人也不是很多,因为他不支持跨平台。如果你想写跨平台的代码,最好使用条件编译。如果想使用#pragma once,只需在头文件开头加上#pragma once即可。
 

展开阅读全文

没有更多推荐了,返回首页