C++中的类型重定义

本文详细解析了C++中常见的类型重定义问题,并重点介绍了因从不同路径包含同一头文件而导致的重定义现象,提供了实用的解决方案。

    这几天工作时碰到一个C++的编译错误(我使用的是Visual C++ 7.0),说是有一个类重复定义,仔细想想我们的这个项目也是做了好几个Release了, 内部代码应该不会有这样的低级错误, 真把类型给重复定义了,检查结果正如我预料的一样。 就这样, 我左右没找到原因,被一个编译错误给卡在那里了。(在我的概念中, 程序错误的等级为:编译错误->链接错误->逻辑错误, 此错误属于最低级 )。这时我仔细看了一下错误提示, 发现重复定义是由于从两个不同的路径包含了同一个头文件而引起的,同事也建议从另外一个路径打开工程试试, 这才慢慢发现了原因。这个原因可能有些拗口,而事实上要出现这种错误也有些曲折 让我从不同情况下的类型重定义来解释一下吧。

  

C++中的三种类型重定义

我总结的类型重定义情况有三。

1 没有在文件头加#pragma once指示符。

Type1.h:

//#pragma once

class Type

{ 

};

 

Main.cpp:

#include "Type1.h"

#include "Type1.h"

int main(int argc, char *argv[])

{

   return 1;

}

#pragma once的作用是保证本文件只被编译一次,如果没有在Type1.h中加这句话那么在main.cpp里面包含了两次Type1.h 就相当于在main.cpp里面定义了两次Type类, 自然就是类型重定义了。

 

2 两个不同的头文件中定义了相同的类型(均有#pragma once

Type1.h:

#pragma once

class Type

{

     

};

 

Type2.h:

#pragma once

class Type

{

      

};

 

Main.cpp:

#include "Type1.h"

#include "Type2.h"

 

int main(int argc, char *argv[])

{

   return 1;

}

    这里main.cpp中同时包含了Type1.h, Type2.h两个头文件, 虽然其文件头都有#pragma once,但因为是不同的文件, 预编译器还是会两次把Type类的定义放在Main.cpp中, 所以也会出现了重定义。

 

3 从两个不同的路径包含了同一个头文件

    前面两种是比较常见, 也是比较容易解决的情况, 而这里要讲的第三种情况, 比较少见, 而且一般出现在有虚拟映射盘的时候。(这样才能做到从两个不同的路径包含同一个头文件), 其他会在什么时候出现, 我还没想到, 知道的朋友顶一下:)。下面我来分析一下:

1 VC工程在D:/Test目录下。

2 映射虚拟盘XD:/Test.

不熟悉的网友可以按此操作: 开始->运行->在运行窗口输入:cmd->cmd窗口输入:

Subst X: D:/Test->回车。

3 该工程有文件Type1.h, main.cpp

Type1.h:

#pragma once

class Type

{

     

};

 

Main.cpp:

#include "Type1.h"

#include "X:/Type1.h"

 

int main(int argc, char *argv[])

{

   return 1;

}

这里我们在main.cpp这样包含了两个头文件, 从本质上来讲, 它们都对应于物理盘D:/Test下的文件Type1.h, 是同一个文件。但在不同的操作下, VC对其有不同的解释。#include "X:/Type1.h"用的是绝对路径, 自然没有什么异议, #include "Type1.h"却有些变化:

* 假如我从D:/Test/下打开工程, 那么#include "Type1.h"其实就是#include "D:/Test/Type1.h"

* 假如从X:/下打开工程,那么#include "Type1.h"就解释为#include "X:/Type1.h"

 

4 D:/Test下打开工程, 编译, 出现类型Type重复定义错误

这种情况下,main.cpp预编译为:

Main.cpp:

#include "D:/Test/Type1.h"

#include "X:/Type1.h"

 

int main(int argc, char *argv[])

{

   return 1;

}

#pragma once只保证本文件被编译一次, 这里VC将其认为是两个不同的文件, 所以都要编译, 出现编译错误自然也就不奇怪了。

当然, 这里如果从X:/ 下打开工程的话,VC就会认为都是从X:/Type1.h下包含这个文件,#pragma once起到了作用, 也就不会出现类型重定义了

 

总结

我在VC7, VC8,Dev C++中都测试了第三种情况, 发现只有Dev C++是可以通过编译的。这可能是微软VC#pragma once还不够智能吧,轻易的被Windows的虚拟盘给蒙蔽了双眼, 看不到其本质(只是猜测, 或许VC这么处理是有其他用意的)。

因为在稍大一点的工程开发中, 我们一般都会用虚拟盘来方便工作, 一是访问快捷,简化了路径, 二是因为多人协同开发,我们一般希望大家源代码路径相同,但我们不应强制要求大家都把源代码放死在某一目录下, 这时把你放源代码的路径映射为一个虚拟盘(比如说统一为X:)就能把大家的代码路径统一起来了。但是另一方面,有了虚拟盘, 就为出现类型重定义提供了条件, 以下是我得出的两个解决方法:

1) 抛弃#pragma once使用古老但集稳定性与移植性于一身的

#ifndef _XXX_H

#define _XXX_H

...

#endif

来保证头文件只被编译一次。这样不管是包含两个相同的文件,还是包含两个不同的文件,或是包含两个文件相同但路径不同的文件, 只要_XXX_H被定义过, 就不会再编译那个编译(但这里我们要保证_XXX_H的唯一性, 如果两个不同的头文件里用了同一_XXX_H,是会出问题的)

2) 在包含头文件时,不要使用绝对路径, 哪怕那是虚拟盘的绝对路径。

C++中,类型重复定义(如类、结构体、枚举等)通常发生在多个源文件或头文件中引入了相同的定义,这会导致链接器报错。为了避免此类问题,可以采取以下方法进行预防和解决。 ### 使用头文件包含守卫(Header Guards) 头文件包含守卫是最常见的解决方案之一。通过预处理指令 `#ifndef`、`#define` 和 `#endif`,可以确保头文件的内容仅被编译一次,从而避免重复定义问题。例如: ```cpp // my_class.h #ifndef MY_CLASS_H #define MY_CLASS_H class MyClass { public: void doSomething(); }; #endif // MY_CLASS_H ``` 这种机制确保即使头文件被多次包含,其内容也只会被处理一次 [^3]。 --- ### 使用 `#pragma once` 另一种更简洁的方法是使用非标准但广泛支持的 `#pragma once` 指令。该指令在大多数现代编译器中都支持,并且可以替代传统的头文件包含守卫: ```cpp // my_class.h #pragma once class MyClass { public: void doSomething(); }; ``` 这种方法不仅减少了代码量,还避免了因宏名冲突导致的问题 [^3]。 --- ### 前向声明(Forward Declaration) 当两个头文件相互依赖时(例如类 A 引用类 B,而类 B 又引用类 A),可以使用前向声明来避免循环依赖问题。前向声明告诉编译器某个类型的存在,而不需要其完整定义。例如: ```cpp // forward.h #ifndef FORWARD_H #define FORWARD_H class B; // 前向声明 class A { public: void func(B* b); }; #endif // FORWARD_H ``` ```cpp // back.h #ifndef BACK_H #define BACK_H class A; // 前向声明 class B { public: void func(A* a); }; #endif // BACK_H ``` 这种方式可以有效避免因头文件相互包含而导致的重复定义问题 [^5]。 --- ### 避免在头文件中定义变量或函数的实现 如果在头文件中定义了变量或函数的具体实现(而非仅仅是声明),则在多个源文件中包含该头文件时,链接器会报告重复定义错误。正确的做法是将变量或函数的声明放在头文件中,并在一个源文件中定义其实现: ```cpp // global.h #ifndef GLOBAL_H #define GLOBAL_H extern int globalVar; // 声明 #endif // GLOBAL_H ``` ```cpp // global.cpp #include "global.h" int globalVar = 0; // 定义 ``` 通过这种方式,可以确保变量或函数仅在链接时被定义一次 [^4]。 --- ### 使用命名空间(Namespace) 在多个模块中定义相同名称的类或结构体时,可以通过命名空间来区分它们,从而避免命名冲突: ```cpp // module_a.h #ifndef MODULE_A_H #define MODULE_A_H namespace ModuleA { class Utility { public: void doSomething(); }; } #endif // MODULE_A_H ``` ```cpp // module_b.h #ifndef MODULE_B_H #define MODULE_B_H namespace ModuleB { class Utility { public: void doSomething(); }; } #endif // MODULE_B_H ``` 通过使用不同的命名空间,可以在不同模块中定义相同名称的类型而不会发生冲突 [^2]。 --- ### 综合建议 - **始终使用头文件保护**(无论是传统的 `#ifndef` 守护宏还是 `#pragma once`)。 - **避免在头文件中定义变量或函数的实现**,仅进行声明。 - **使用前向声明** 来解决类之间的循环依赖问题。 - **合理使用命名空间** 来组织代码,避免全局命名冲突。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值