C++中类的前置声明

来源:互联网 发布:股票盈亏软件 编辑:程序博客网 时间:2024/05/19 06:48

转载:http://www.cnblogs.com/King-Gentleman/p/5081159.html

一、类嵌套的疑问

假设我们有两个类A和B,定义在一个.H文件中,但是在A中要用到B,B中也要用到A,但是这样的写法当然是错误的:

#ifndef _TEST__#define _TEST__class B;class A{private:    B b;};class B{private:    A a;};#endif

编译错误:error C2079: ‘A::b’ uses undefined class ‘B’

为什么会这样呢?因为C++编译器自上而下编译源文件的时候,对每一个数据的定义,总是需要知道定义的数据类型的大小,在预先声明语句class B之后,编译器已经知道B是一个类,但是其中的数据却是未知的(需要定义B之后才知道),因此B类型的大小也不知道(无法分配内存空间);
解决:
在这里我们只需要把其中的一个A类中的B类型成员改成指针形式就可以避免

#ifndef _TEST__#define _TEST__class B;class A{private:    B *b;};class B{private:    A a;};#endif

因为在特定的平台上,给指针分配的内存空间是一定的(Win32平台上是4字节(4字节32bit可以寻址整个内存空间),Win64平台是6字节),编译器在编译时,就可以给b分配内存空间;

二、不同头文件中的类的嵌套

在实际编程中,不同的类一般是放在不同的相互独立的头文件中的,这样两个类在相互引用时又会有不一样的问题,重复编译是问题出现的根本原因。为了保证头文件仅被编译一次,在C++中常用的办法是使用条件编译命令在头文件中我们常常会看到以下语句段:

#ifndef _TEST__#define _TEST__#endif

意思是如果没有定义过这个宏,那么就定义它,然后执行直到#endif的所有语句如果下次在与要这段代码,由于已经定义了那个宏,因此重复的代码不会被再次执行这实在是一个巧妙而高效的办法在高版本的VC++上,还可以使用这个命令来代替以上的所有:

#pragma once

但是不要以为使用了这种机制就全部搞定了,比如在以下的代码中:

//    A.h#ifndef _A__#define _A__#include <B.h>class A{private:    B *b;};#endif//   B.h#ifndef _B__#define _B__#include <A.h>class B{private:    A *a;};#endif

这里两者都使用了指针成员,因此嵌套本身不会有什么问题,在主函数前面使用#include “A.h”之后,主要编译错误如下:

  error C2501: 'A' : missing storage-class or type specifiers

如果在主函数前面使用#include “B.h”之后,主要编译错误如下:

  error C2501: 'B' : missing storage-class or type specifiers   

(不难理解,可以根据头文件线索,依次展开头文件,如果之前用到(已展开),就不会再展开)
解决:
仍然是类型不能找到的错误。其实这里仍然需要前置声明,分别添加前置声明之后,可以成功编译了;

#ifndef _A__#define _A__#include <B.h>class B;class A{private:    B *b;};#endif#ifndef _B__#define _B__#include <A.h>class A;class B{private:    A *a;};#endif

(两个头文件分别都加,目的在于,在主函数中,无论包含A.h还是B.h都可以;如果在主函数中,包含A.h,那么A.h中的class B没有必要写,因为在这之前,编译器已经知道B已定义)

头文件包含代替不了前置声明,有的时候只能依靠前置声明来解决问题,我们还要思考一下,有了前置声明的时候头文件包含还是必要的吗?我们尝试去掉A.h和B.h中的#include行,发现没有出现新的错误那么究竟什么时候需要前置声明,什么时候需要头文件包含呢?
两点原则:
第一个原则: 如果可以不包含头文件,那就不要包含,这时候前置声明可以解决问题,如果使用的仅仅是一个类的指针(A *a),没有使用这个类的具体对象(A a),也没有访问到类的具体成员,那么前置声明就可以了,因为指针这一数据类型的大小是特定的,编译器可以获知.
第二个原则: 尽量在CPP文件中包含头文件,而不要在头文件中包含。假设类A的一个成员是一个指向类B的指针,在类A的头文件中使用了类B的前置声明,那么在A的实现中我们需要访问B的具体成员,因此需要包含头文件,那么我们应该在类A的实现部分(CPP文件)包含类B的头文件而非声明部分(H文件)。

三、C++的前置声明
在前置声明时,我们只能使用的就是类的指针和引用(因为引用也是居于指针的实现的)(具体讲解部分见链接),像我们这样前置声明类A:

class A;

一种不完整的声明,只要类B中没有执行需要了解类A的大小或者成员的操作,则这样的不完整声明允许声明指向A的指针和引用,而在前一个代码中的语句

A a;

是需要了解A的大小的,不然编译器是不可能知道给类B分配内存的大小,因此不完整的前置声明就不行,必须要包含A.h来获得类A的大小(也就是先定义,编译器获得类A的所有信息)

总结了何时使用前置声明以及何时应该包括头文件的各种情况,给出一些示例代码:
首先,我们为什么要包括头文件?问题的回答很简单,通常是我们需要获得某个类型的定义(definition),那么接下来的问题就是,在什么情况下我们才需要类型的定义,在什么情况下我们只需要声明就足够了?问题的回答是当我们需要知道这个类型的大小或者需要知道它的函数签名的时候,我们就需要获得它的定义
假设我们有类型A和类型C,在A的头文件A.h中有如下情况是否需要包含C.h头文件:
1.A继承至C
2.A有一个类型为C的成员变量
3.A有一个类型为C的指针的成员变量
4.A有一个类型为C的引用的成员变量
5.A有一个类型为std::list< c >的成员变量
6.A有一个函数,它的签名中参数和返回值都是类型C
7.A有一个函数,它的签名中参数和返回值都是类型C,它调用了C的某个函数,代码在头文件中
8.A有一个函数,它的签名中参数和返回值都是类型C(包括类型C本身,C的引用类型和C的指针类型),并且它会调用另外一个使用C的函数,代码直接写在A的头文件中
9.C和A在同一个名字空间里面
10.C和A在不同的名字空间里面
解答:
1.需要,没有任何办法,必须要获得C的定义,因为我们必须要知道C的成员变量,成员函数
2.需要C的定义,因为我们要知道C的大小来确定A的大小,但是可以使用Pimpl惯用法来改善这一点,详情请看Hurb的Exceptional C++。
3/4.不需要,前置声明就可以了,其实3和4是一样的,引用在物理上也是一个指针,它的大小根据平台不同,可能是32位也可能是64位,反正我们不需要知道C的定义就可以确定这个成员变量的大小。
5.不需要,有可能老式的编译器需要。标准库里面的容器像list, vector,map,
在包括一个list< C >,vector< C >,map< C, C >类型的成员变量的时候,都不需要C的定义。因为它们内部其实也是使用C的指针作为成员变量,它们的大小一开始就是固定的了,不会根据模版参数的不同而改变。
6.不需要,只要我们没有使用到C,但是实现部分(.cpp) 文件中需要包含C.h头文件
7.需要,我们需要知道调用函数的签名
8.8的情况比较复杂,直接看代码会比较清楚一些

C& doToC(C&);C& doToC2(C& c) {return doToC(c);};

从上面的代码来看,A的一个成员函数doToC2调用了另外一个成员函数doToC,但是无论是doToC2,还是doToC,它们的的参数和返回类型其实都是C的引用(换成指针,情况也一样),引用的赋值跟指针的赋值都是一样,无非就是整形的赋值,所以这里即不需要知道C的大小也没有调用C的任何函数,实际上这里并不需要C的定义。但是,我们随便把其中一个C&换成C,比如像下面的几种示例:

1.C& doToC(C&);C& doToC2(C c) {return doToC(c);};2.C& doToC(C);C& doToC2(C& c) {return doToC(c);};3.C doToC(C&);C& doToC2(C& c) {return doToC(c);};4.C& doToC(C&);C doToC2(C& c) {return doToC(c);};

无论哪一种,其实都隐式包含了一个拷贝构造函数的调用,比如1中参数c由拷贝构造函数生成,3中doToC的返回值是一个由拷贝构造函数生成的匿名对象。因为我们调用了C的拷贝构造函数,所以以上无论那种情形都需要知道C的定义。
9/10.9和10都一样,我们都不需要知道C的定义,只是10的情况下,前置声明的语法会稍微复杂一些。
最后给出一个完整的例子,我们可以看到在两个不同名字空间的类型A和C,A是如何使用前置声明来取代直接包括C的头文件的:

A.h#pragma once#include <list>#include <vector>#include <map>#include <utility>    //不同名字空间的前置声明方式namespace test1{          class C;}namespace test2{          //用using避免使用完全限定名    using test1::C;    class A     {    public:              C   useC(C);            C& doToC(C&);            C& doToC2(C& c) {return doToC(c);};    private:            std::list<C>    _list;            std::vector<C>  _vector;            std::map<C, C>  _map;            C*              _pc;            C&              _rc;    };}C.h#ifndef C_H#define C_H#include <iostream>namespace test1{    class C    {    public:           void print() {std::cout<<"Class C"<<std::endl;}    };}#endif // C_H

实际编程中遇到的问题

//      StrBlob.h#ifndef _STRBLOB__#define _STRBLOB__class StrBlobPtr;class StrBlob{    public:    StrBlobPtr begin(); //编译通过    //StrBlobPtr begin(){return StrBlobPtr(*this);} //编译通过不过    StrBlobPtr end();private:};//      StrBlobPtr.h#ifndef _STRBLOBPTR__#define _STRBLOBPTR__#include <StrBlob.h>class StrBlobPtr{public:    StrBlobPtr() : curr(0) {}    StrBlobPtr(StrBlob &a, std::size_t sz = 0) : wptr(a.data), curr(sz) {} //必须要头文件StrBlob.h};

编译通不过的原因:函数体返回时,用到了StrBlobPtr类中的构造函数
在StrBlobPtr.h中必须要包含StrBlob.h头文件的原因是:构造函数的参数列表wptr(a.data)中,a访问了其数据成员导致;

0 0