这几天工作时碰到一个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)映射虚拟盘X为D:/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:)就能把大家的代码路径统一起来了。但是另一方面,有了虚拟盘,就为出现类型重定义提供了条件,以下是我得出的两个解决方法:
<!--[if !supportLists]-->
<!--[if !supportLists]-->1)<!--[endif]-->抛弃#pragma once使用古老但集稳定性与移植性于一身的
#ifndef _XXX_H
#define _XXX_H
...
#endif
来保证头文件只被编译一次。这样不管是包含两个相同的文件,还是包含两个不同的文件,或是包含两个文件相同但路径不同的文件,只要_XXX_H被定义过,就不会再编译那个编译(但这里我们要保证_XXX_H的唯一性,如果两个不同的头文件里用了同一_XXX_H,是会出问题的)
<!--[if !supportLists]-->2)<!--[endif]-->在包含头文件时,不要使用绝对路径,哪怕那是虚拟盘的绝对路径。