天天看点

C++:关于类以及h/cpp文件的一些实用知识

一、VC编译原理

解决重定义问题,首先要明白VC的编译原理:

  • VC只编译cpp文件,这些cpp文件构成将来的exe;
  • 当VC编译A.cpp文件的时候,如果遇到了语句

    #include "B.h"

    ,实质上是将"B.h"文件中的代码全部“复制”到A.cpp中,然后再继续编译A.cpp。
  • 当在B.h文件中定义全局变量a等,即使使用了避免文件重复包含的方法(如下节提到的两种方法),是不能避免“A.cpp中

    #include"B.h"

    ,C.pp中

    #include"B.h"

    ,然后提示变量a重复定义”的问题,只能保证“A.cpp中多次出现

    #include"B.h”

    而不会提示变量a重复定义”。
  • 全局变量、函数、结构体一定要在.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将有一个不可预测的值。有两种方法避免它,一个是总是按照你希望它们被初始化的顺序声明成员,第二个是,如果你决定使用初始化列表,总是按照它们声明的顺序罗列这些成员。这将有助于消除混淆。

继续阅读