一、VC编译原理
解决重定义问题,首先要明白VC的编译原理:
- VC只编译cpp文件,这些cpp文件构成将来的exe;
- 当VC编译A.cpp文件的时候,如果遇到了语句
,实质上是将"B.h"文件中的代码全部“复制”到A.cpp中,然后再继续编译A.cpp。#include "B.h"
- 当在B.h文件中定义全局变量a等,即使使用了避免文件重复包含的方法(如下节提到的两种方法),是不能避免“A.cpp中
,C.pp中#include"B.h"
,然后提示变量a重复定义”的问题,只能保证“A.cpp中多次出现#include"B.h"
而不会提示变量a重复定义”。#include"B.h”
- 全局变量、函数、结构体一定要在.cpp文件中定义,在.h文件中声明,一定不要在h文件中定义,否则会出现重复定义的问题。
二、头文件避免类重复包含的方法
1. 微软预编译控制
#if _MSC_VER > 1000
#pragma once
#endif
... ... // .h文件正文
在.h文件最开始的地方加上这段代码,即可避免头文件重复include。但是需要注意的是#pragma once 这条语句只有VC编译器大于1000才可以支持(反正VC++6.0及以上是可以的),_MSC_VER就是Microsoft的C编译器的版本。
#pragma once则由编译器提供保证:同一个文件不会被包含多次。注意这里所说的“同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件。带来的好处是,你不必再费劲想个宏名了,当然也就不会出现宏名碰撞引发的奇怪问题。对应的缺点就是如果某个头文件有多份拷贝,本方法不能保证他们不被重复包含。当然,相比宏名碰撞引发的“找不到声明”的问题,重复包含更容易被发现并修正。
2、宏定义方式
#ifndef __SOMEFILE_H__
#define __SOMEFILE_H__
... ... // .h文件正文
#endif
#ifndef的方式依赖于宏名字不能冲突,这不光可以保证同一个文件不会被包含多次,也能保证内容完全相同的两个文件不会被不小心同时包含。当然,缺点就是如果不同头文件的宏名不小心“撞车”,可能就会导致头文件明明存在,编译器却硬说找不到声明的状况 。不过如果是编译器自动生成的类一般都会自动根据编译次数(唯一)来自动生成唯一的宏名。
三、出现重复定义的变量的解决方法
在项目同时使用第三方库GuiLib和CJ609Lib,编译提示结构体
CMenuItemInfo
重定义,后来研究了一下,发现Guilib和CJ609Lib中都全局定义了同一个结构体名
CMenuItemInfo
,而解决方法有两种:使用宏定义规避和使用命名空间
1.宏定义规避
这种方法很简单,就是类似C++利用宏避免头文件重复的形式,直接给实例,在两个
关于CMenuItemInfo
结构体定义的位置加上一下的宏定义就行:
#ifndef __CMenuItemInfo_LOCAL_DEFINED//避免结构体CMenuItemInfo重复定义
#define __CMenuItemInfo_LOCAL_DEFINED
struct CMenuItemInfo : public MENUITEMINFO_LOCAL {
CMenuItemInfo()
{
memset(this, 0, sizeof(MENUITEMINFO_LOCAL));
cbSize = sizeof(MENUITEMINFO_LOCAL);
}
};
#endif//__CMenuItemInfo_LOCAL_DEFINED
这样做的好处是,用
CMenuItemInfo
定义具体对象时,直接使用
CMenuItemInfo
,程序会自动使用编译过程遇到的第一个
CMenuItemInfo
的定义。
但这种方法有一个前提:两个结构体的定义必须一致,也就是说上述关于
CMenuItemInfo
的两个定义必须一致,很巧的是Guilib和CJ609Lib关于
CMenuItemInfo
的定义确实一致。
2.使用命名空间
这种方法更加广泛,尤其适合于重名且定义的内容不一样的情况。这种方法等有时间了,我再在下边补充,本次使用了宏定义就解决了上述问题。
PS:最后补充一点,有的时候提示@.obj已经在@.obj中定义的时候,在项目属性配置中启用预编译头可能会解决这个问题,但不知道是什么原理。
四、关于全局变量
关于程序中使用的全局变量,最好在cpp文件中定义,然后在.h中声明,这样可以极大的概率避免重复定义
A.cpp文件定义全局变量
global_time
#include "A.h "
int global_time;
void main
{
...
}
在A.h文件中声明全局变量
externint global_time;
当B.cpp中要使用
global_time
,直接在B.h文件中声明
extern int global_time;
,就行了。此时即使已经在B.h或B.cpp文件中已经引入A.h文件,也不会冲突。
五、 C/C++中默认参数在函数声明(h文件)还是定义(cpp文件)
编译器一般是禁止在声明和定义同时定义缺省参数值。若声明时没有定义缺省参数值,那么在定义成员函数时可以定义缺省参数值。但这种情况下,使用函数的用户是看不见的,很可能会带来灾难性的后果。因此为了程序的可读性,我们最好在h文件中声明成员函数的时候定义缺省值,而不要在定义函数的时候定义缺省值。
六、C++类构造函数初始化列表
构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。例如:
class CStudent{ //为了好看,就将类的声明与定义写在一起,而不是分别放在h和cpp文件中去了
public:
int a;
float b;
//构造函数初始化列表
CStudent(int x, int y):a(x),b(y)
{}
}
当我们需要实例化对象的时候,直接这样即可
CStuden student(10,20);
上面的构造函数(使用初始化列表的构造函数)显式的初始化类的成员。和下面这样没使用初始化列表进行显式的初始化,而是内部赋值是一样的。
class CStudent{ //为了好看,就将类的声明与定义写在一起,而不是分别放在h和cpp文件中去了
public:
int a;
float b;
//构造函数内部赋值
CStuden(int x, int y)
{
a=x;
b=y;
}
}
初始化和赋值对内置类型的成员没有什么大的区别,像上面的任一个构造函数都可以。但有的时候必须用带有初始化列表的构造函数:
- 成员类型是没有默认构造函数的类。若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。
- const成员或引用类型的成员。因为const对象或引用类型只能初始化,不能对他们赋值。
一定要注意初始化列表的成员初始化顺序:C++初始化类成员时,是按照声明的顺序初始化的,而不是按照出现在初始化列表中的顺序。
class CStudent{
int a;
int b;
CStudent::CStudent(int x,int y):b(x),a(b)
{}
}
实例化的时候
CStudent stude(10,20);
你可能以为上面的代码将会首先做b=x,然后做a=b,最后它们有相同的值都是10。但是编译器先初始化a,然后是b,因为它们是按这样的顺序声明的。结果是a将有一个不可预测的值。有两种方法避免它,一个是总是按照你希望它们被初始化的顺序声明成员,第二个是,如果你决定使用初始化列表,总是按照它们声明的顺序罗列这些成员。这将有助于消除混淆。