C++之类的前置声明

什么是前置声明

前置声明(Forward Declaration),顾名思义,就只是一个声明,并不包含其定义。

为什么要引入前置声明

试想一下,如果需要在头文件A.h中使用另一个头文件B.h中的类B,有哪些做法?
1.把类B直接挪到A.h中(完全不推荐)
2.在A.h中包含B.h(写法为#include “B.h”)
3.在A.h中前置声明类B(写法为class B;)
4.······

实现的方法有很多,但其实常规的做法就两种:包含头文件或者前置声明。
用到了就包含似乎更符合规律,其实不然,直接包含头文件其实会引入一些问题,看一下下边这种情况:


// A.h
#include "B.h"
class A
{
    B* b;
};

// A.cpp
#include "A.h"
......

// B.h
#include "A.h"
class B
{
    A* a;
};

// B.cpp
#include "B.h"
......

这时编译就会报错,编译器去编译A.h,发现包含了B.h,就去编译B.h,编译B.h的时候又发现包含了A.h,再去编译A.h,死循环,报错如下:
error
使用类的前置声明让上例编译通过:


// A.h
class B;
class A
{
    B* b;
};

// A.cpp
#include "A.h"
#include "B.h"
......

// B.h
class A;
class B
{
    A* a;
};

// B.cpp
#include "B.h"
#include "A.h"
......

前置声明的应用场景

1.编译出错时一定要用:
由上例可以看出,在嵌套调用时一定要使用前置声明,否则编译都通不过,在多人合作的项目里,这种嵌套调用确实是会发生的,一旦发生,改起来就比较烦了,所以一开始就使用前置声明可以避免这一问题。

2.编译太慢时考虑使用:
想象一下,如果A.h包含了B.h,B.h包含了C.h,C.h包含了D.h,…,Y.h包含了Z.h,那么,只改动Z.h,从A.h到Z.h都需要重新编译,不止如此,所有包含了A.h~Z.h的文件以及所有子孙都需要重新编译,如果采用了分布式编译还好一点,如果没有分布式编译,就靠自己那一台电脑去编,不使用前置声明时改动一个Z.h所增加的编译时间,够喝杯咖啡再吃个下午茶的了,当然这一点各自情况不同要具体情况具体看待。

怎么使用前置声明

在使用类的前置声明时,需要注意以下几点:

1.当前置声明的类作为成员变量时,只能以指针或引用的形式,不能定义该类的对象,因为编译器申请空间时,对象形式的成员变量需要其定义,而指针形式的成员变量需要的空间是固定的(引用的实现也基于指针)。

2.不能使用前置声明类的实现,如果想调用前置声明类的具体某个成员变量或者成员函数,一定要包含它的头文件。这一点其实不难理解,因为前置声明就只是一个声明并不包含定义,你想用它的定义,但是这会儿它自己也不知道自己是怎么定义的。

3.当前置声明的类作为某个成员函数的返回值或参数时,这个成员函数的定义如果操作了前置声明类的成员函数或变量,就和上边的第二点一样,就会报错,而绝大多数情况,函数的具体实现都会操作这个前置声明类的,不然把它当做参数或者返回值是图啥,当然也会有这种情况,但是一种好的习惯是:这个成员函数如果参数或者返回值用到了前置声明类,那么只声明不定义,具体实现放到cpp文件中去。

4.前置声明的类不能作为父类,以下写法是错误的:

class Parent;
class Child : public Parent{};

前置声明的优点

上述应用场景可以看出,使用类的前置声明有这些优点:

1.避免了重复包含引发的循环

2.节省了编译时间,在代码量庞大的项目中尤为明显,避免改动了祖宗头文件导致全族狂欢

3.······

前置声明的缺点

其实也算不上是缺点,应该说是使用前置声明时可能会存在的问题:

1.修改类名字时,为了兼容已有的,如果是包含头文件,就只需要在要改的头文件里,通过别名来改就好了,但是如果是前置声明,就需要把所有前置声明这个类的地方都改一下,可能需要改动很多个头文件就有可能漏改,而且git的提交记录可能改动了几十个头文件,冲突啊编译这些头文件的耗时啊,会被同事喷吧,没事瞎改什么名字…不过起名字时注意点,其实问题不大

2.还有一点是继承关系在类的前置声明中是体现不出来的,所以如果类Child继承自Parent,在类A中有如下三个函数:

void func(Parent*);
void func(void*);
void callFunc(Child* x) { func(x); }

在调用callFunc时应该是想调用func(Parent*)的,用包含头文件的方法可以知道继承关系就不会有问题,但是如果改用前置声明,调用callFunc实际执行的就是func(void*)了。
不过这一点个人认为其实还好,写了这样三个函数就一定会去检查是不是前置声明,要对自己写的代码负责,如果用到的类是前置声明,就把callFunc的实现放到.cpp文件中就好了,没必要非在头文件里去实现,但这点也算是存在的一点隐患,也还是列了出来,仅供参考。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值