前置声明的目的是避免在某个.h
文件中include
其他头文件,取而代之的是用class/struct
声明;
类的前置声明就是告诉编译器有这么一个类,它的名字是XXX
,甚至不需要知道它具有哪些成员;
注意,这里只是声明类,没有分配空间,实例化成对象的时候,编译器才会分配内存;
前置声明的好处:
- 算法封装后的接口类头文件中,可能会使用到其他头文件或第三方库中定义的数据类型,为了减少头文件之间的相互依赖(从而避免算法调用层除接口头文件之外,还要依赖更多的头文件,甚至还要配置第三方库的开发环境),可以使用前置声明;
- 当两个头文件交叉引用时(例如
A.h
中包含了B.h
,B.h
中又包含了A.h
),无法通过编译,必须使用前置声明; - 节省编译时间;修改某个头文件后,依赖该头文件的其他头文件和源程序都要重新编译,而前置声明隐藏了部分依赖关系,从而减少不必要的编译工作;
1 一般类的前置声明
举例:
首先在Person.h
中定义一个CPerson
的类型,如下:
// Person.h
#pragma once
#include <iostream>
class CPerson
{
public:
CPerson():miAge(0) {};
~CPerson() {};
void PrintAge()
{
std::cout << miAge << std::endl;
};
int miAge; // 目前只有一个成员变量
};
现在要开发一个新的类CEarth
,CEarth
中有个成员变量是CPerson
类型,一般情况下的做法如下:
在Earth.h
中定义一个CEarth
的类型,并包含Person.h
文件,如下:
// Earth.h
#pragma once
#include "Person.h"
class CEarth
{
public:
CEarth();
~CEarth();
void Run();
CPerson mPerson; // 有个CPerson类型的成员变量,因此include了"Person.h"
};
实现如下:
// Earth.cpp
#include "Earth.h"
CEarth::CEarth() {}
CEarth::~CEarth() {}
void CEarth::Run()
{
mPerson.miAge = 18;
mPerson.PrintAge();
}
如此带来的影响有:
- 如果别人要使用
CEarth
,不仅要给他提供Earth.h
文件,还要提供Person.h
文件,如果Person.h
依赖了其他头文件,也要提供,如此递归下去,一方面别人调用的时候需要依赖更多的文件(如果涉及到第三方库还要配置开发环境),另一方面也影响底层算法的私密性; - 如果类型
Person
发生了改变(例如添加属性“性别”),Person.h
必然发生改变,由于Earth.h
中包含了Person.h
,这就意味着Earth.h
也发生了改变,那么那些包含Earth.h
的文件都得重新编译,而如果Earth.h
中不包含Person.h
,这就可以减少不必要的编译时间,在大型项目工程中影响比较大;
将CEarth
的定义和实现改成前置声明的写法,如下:
// Earth.h
#pragma once
//#include "Person.h"
class CPerson; // 前置声明,告诉编译器有个类名字叫CPerson
class CEarth
{
public:
CEarth();
~CEarth();
void Run();
CPerson* mPerson1; // 只能使用指针或引用
};
在Earth.h
中引入类的前置声明,而不用包含Person.h
,告诉编译器有个类叫CPerson
,CPerson
中有哪些成员在Earth.h
中不需要知道;
由于类的前置声明不知道类中具体的成员,因此也就不能直接使用类型来定义类的对象(因为实例化对象时需要给对象分配内存,既然都不知道类中有几个成员,编译器也就无法分配内存),自然也就不能调用对象中的方法,因此CEarth
中的成员变量只能使用Person
的指针或引用,因为指针或者引用类型变量的大小是确定的(4或8字节);
这也正是为什么使用前置声明后,不管
CPerson
自身的定义会发生什么样的改变,CEarth
也不会发生改变,因为CEarth
中跟CPerson
类型相关的成员变量始终只占一个指针大小的字节;
// Earth.cpp
#include "Earth.h"
#include "Person.h"
CEarth::CEarth() :mPerson1(nullptr) {}
CEarth::~CEarth() {}
void CEarth::Run()
{
mPerson1 = new CPerson;
mPerson1->miAge = 18;
mPerson1->PrintAge();
delete mPerson1;
mPerson1 = nullptr;
}
Earth.cpp
中需要包含CPerson.h
文件,因为Earth.cpp
中通常是类成员函数的实现部分,既然要用到CPerson
类型,肯定是要用CPerson
实例化对象,并使用CPerson
对象中的成员变量和方法(否则CEarth
中就没必要使用CPerson
);
因此,不管是在.h
还是.cpp
文件中,判断能否使用CPerson类的前置声明代替包含Person.h
文件的关键是,看该文件中是否一定要用到CPerson
类的实例(一般来说,就是要通过实例访问CPerson
类中的成员变量或方法),通常我们在封装一个类时(如CEarth
),会拆成.h
和.cpp
两个文件,.h
文件用来进行类声明,不需要用到CPerson
类的实例,可以使用前置声明,不用包含Person.h
,而.cpp
文件用来进行类方法实现,需要用到CPerson
类的实例,必须包含Person.h
;
如果封装的类将声明和实现都写在
.h
文件中,那也不能使用前置声明,必须包含Person.h
;
另外,如前所述,使用CPerson
类的前置声明后,CEarth
中的成员变量只能使用CPerson
类的指针或引用,但如果CPerson
只是用作CEarth
成员函数的参数或返回值,或只是作为标准STL
模板容器的参数类型,指针或引用不是必须的,如下:
// Earth.h
#pragma once
//#include "Person.h"
#include <vector>
class CPerson; // 前置声明,告诉编译器有个类名字叫CPerson
class CEarth
{
public:
CEarth();
~CEarth();
void Run();
CPerson GetPerson1(CPerson a);
CPerson GetPerson2(CPerson& a);
CPerson* mPerson1; // 只能使用指针或引用
std::vector<CPerson> mvecPerson;
std::vector<CPerson*> mvecpPerson;
};
使用CPerson
类的前置声明后(不包含Person.h
文件),判断是否一定要使用指针或者引用的关键是,CPerson
修饰的变量是否会改变CEarth
实例化后的大小,成员函数或标准STL
模板容器都不会改变CEarth
的大小,因此,可以直接使用CPerson
,而成员变量会改变CEarth
的大小,因此,必须使用指针或引用(使用指针或引用后,成员变量的大小就始终只占1个指针大小的内存);
标准
STL
模板容器创建的对象,其内存大小与模板参数类型无关,也不会随着容器中的元素数量的改变而改变,此处不展开;
2 模板类及其实例的前置声明
以OpenCV
库中的Point2f
类型为例,定义如下:
// opencv2/core/types.hpp
namespace cv
{
template<typename _Tp> class Point_
{
public:
// ...
}
typedef Point_<float> Point2f;
}
也就是说,OpenCV
中,Point2f
是类模板Point_
参数类型为float
的实例,别名为Point2f
;
前置声明语法如下:
// Data.h
/*
* brief: 前置声明
*/
namespace cv
{
template<typename T>
class Point_;
typedef Point_<float> Point2f;
}
class CData
{
public:
CData() {};
~CData() {};
private:
std::vector<cv::Point2f> mvecPts2f;
}