zoukankan      html  css  js  c++  java
  • C++ 类的前置声明

    http://www.2cto.com/kf/201311/260705.html
     
     今天在研究C++”接口与实现分离“的时候遇到了一个问题,看似很小,然后背后的东西确值得让人深思!感觉在学习的过程中有太多的为什么,而每一个为什么背后都隐藏着一些原理和目的,所以得多问自己”为什么“,这样才不仅知其然,更知其所以然,才能举一反三、融会贯通,这也是我以前学习不够重视的一个问题,好在”亡羊补牢,犹未晚也!“。
            C++中将”接口与实现分离“的两个重要目的就是”降低文件间的编译依存关系“和”隐藏对象的实现细节“,都是考虑到C++的高效和安全。而实现这一目的的关键技术就是”Pimpl模式(pointer to implementation)”,也即是”把一个类所有的实现细节都“代理”给另一个类来完成,而自己只负责提供接口“,而实现”Pimpl模式”的关键又是“依赖对象的声明(declaration)而非定义(definition)”,这样就引出了今天的话题:为什么通过“依赖对象的声明”可以实现Pimpl模式“,进而实现”接口与实现分离“?我们一步步来抽丝剥茧吧!
     
     
    什么是“前置声明”?
     
            ”前置声明“和”include“就是一对冤家!我们先看一个例子:
    [cpp]  
    // A.h  
    #include "B.h"  
    class A  
    {  
      
    public:  
        A(void);  
        virtual ~A(void);  
    private:  
        B b;  
    };  
      
    // B.h  
    #include "A.h"  
    class B  
    {  
    private:  
        A a;  
    public:  
        B(void);  
        ~B(void);  
    };  
            一编译,就出现了一个互包含的问题了,A中有B类型的成员变量所以需要include<b.h>,而B中又有A类型的成员变量也需要include<a.h>,这就导致了循环include,编译是肯定通过不了的!
            解决办法就是使用”前置声明“,在A.h中加上class B的声明:
    [cpp] 
    // A.h  
    #include "B.h"  
      
    class B;  
    class A  
    {  
      
    public:  
        A(void);  
        virtual ~A(void);  
    private:  
        B b;  
    };  
      
    // B.h  
    #include "A.h"  
    class B  
    {  
    private:  
        A a;  
    public:  
        B(void);  
        ~B(void);  
    };  
            但是,有人知道问题是为什么就被解决的吗,也就是说,加了个前置声明为什么就解决了这样的问题。下面,让我来探讨一下这个前置声明。
            前置声明是什么?举个形象点的例子,就是我要盖一个屋子(CHOuse),光有屋子还不行啊,我还得有床(CBed)。但是屋子还没盖好,总不能先买床吧,床的大小我定了,改天买。先得把房子盖好,盖房子的时候我先给床留个位置,等房子盖好了,我再决定买什么样的床。前置声明就是我在声明一个类(CHouse)的时候,用到了另外一个类的定义(CBed),但是CBed还没有定义呢,而且我还先不需要CBed的定义,只要知道CBed是一个类就够了。那好,我就先声明类CBed,告诉编译器CBed是一个类(不用包含CBed的头文件):
     
            class CBed;
     
            然后在CHouse中用到CBed的,都用CBed的指针类型代(因为指针类型固定大小的,但是CBed的大小只用知道了CBed定义才能确定)。等到要实现CHouse定义的时候,就必须要知道CBed的定义了,那是再包好CBed的头文件就行了。
    [cpp] 
    // House.h  
    class CBed; // 盖房子时:现在先不买,肯定要买床的  
    class CHouse  
    {  
        CBed& bed; // 我先给床留个位置  
        // CBed bed; // 编译出错  
    public:  
        CHouse(void);  
        CHouse(CBed& bedTmp);  
        virtual ~CHouse(void);  
        void GoToBed();  
    };  
    // House.cpp  
    #include "Bed.h"  
    #include "House.h" // 等房子开始装修了,要买床了  
    CHouse::CHouse(void)  
        : bed(*new CBed())  
    {  
        CBed* bedTmp = new CBed(); // 把床放进房子  
        bed = *bedTmp;  
    }  
    CHouse::CHouse(CBed& bedTmp)  
        : bed(bedTmp)  
    {  
    }  
    CHouse::~CHouse(void)  
    {  
        delete &bed;  
    }  
    void CHouse::GoToBed()  
    {  
        bed.Sleep();  
    }  
     
    “前置声明”的作用?
     
            “前置声明”的作用有2:
            (1)解决两个class的相互依赖问题,也就是两个文件相互include,减少头文件的包含层次。比如以上的几个例子!
            (2)降低文件之间的“编译依存关系”,从第一个例子我们看到,当我们在类A使用类B的前置声明时,我们修改类B时,只需要重新编译类B,而不需要重新编译a.h的(当然,在真正使用类B时,必须包含b.h)。如果使用include的话,一个文件改变,所有include这个文件的文件都得重新编译,这是非常耗时的!
            (3)通过“前置声明”可以实现“接口与实现分离“。我们将需要提供给客户的类分割为两个classes:一个只提供接口,另一个负责实现!例如以下实例:
    [cpp]  
    //XYZ.h  
      
    class X{  
    public:  
        X();  
        void Print();  
    };  
      
    class Y{  
    public:  
        Y();  
        void Print();  
    };  
      
    class Z{  
    public:  
        Z();  
        void Print();  
    };  
      
    //XYZ.cpp  
      
    #include "stdafx.h"  
    #include "XYZ.h"  
    #include <iostream>  
      
    X::X(){}  
    void X::Print(){  
        std::cout<<"X::Print"<<std::endl;  
    }  
      
    Y::Y(){}  
    void Y::Print(){  
        std::cout<<"Y::Print"<<std::endl;  
    }  
      
    Z::Z(){}  
    void Z::Print(){  
        std::cout<<"Z::Print"<<std::endl;  
    }  
      
    // AImpl.h  
    #include "stdafx.h"  
    #include "XYZ.h"  
      
    class AImpl    
    {    
    public:    
        AImpl();  
        ~AImpl();  
        X* getX();    
        Y* getY();    
        Z* getZ();    
        void doSth();  
    private:    
        X* x;    
        Y* y;    
        Z* z;    
    };   
      
      
       
    //AImpl.cpp  
    #include "stdafx.h"  
    #include "Aimpl.h"  
    #include <iostream>  
      
    AImpl::AImpl(){  
        x = new X();  
        y = new Y();  
        z = new Z();  
    }  
    AImpl::~AImpl(){  
        if (x)  
        {  
            delete x;  
        }  
        if (y)  
        {  
            delete y;  
        }  
        if (z)  
        {  
            delete z;  
        }  
    }  
    X* AImpl::getX(){  
        return x;  
    }  
    Y* AImpl::getY(){  
        return y;  
    }  
    Z* AImpl::getZ(){  
        return z;  
    }  
    void AImpl::doSth(){  
        x->Print();  
        y->Print();  
        z->Print();  
    }  
      
      
    // A.h    
    #include "stdafx.h"  
    #include <memory>  
      
    class AImpl;    
    class X;  
    class Y;  
    class Z;  
      
    class A    
    {    
    public:    
        A();  
        X* getX();  
        Y* getY();  
        Z* getZ();     
        void doSth();  
    private:    
        std::shared_ptr<AImpl> pImpl;    
    };   
      
      
    //A.cpp  
    #include "stdafx.h"  
    #include "A.h"  
    #include "Aimpl.h"  
      
    A::A(){  
        pImpl = std::shared_ptr<AImpl>(new AImpl());  
    }  
      
    X* A::getX(){  
        return pImpl->getX();  
    }  
      
    Y* A::getY(){  
        return pImpl->getY();  
    }  
      
    Z* A::getZ(){  
        return pImpl->getZ();  
    }  
      
    void A::doSth(){  
        pImpl->doSth();  
    }  
      
      
    //PreDeclaration.cpp  
      
    #include "stdafx.h"  
    #include "A.h"  
      
    int main()  
    {  
        A a;  
        a.doSth();  
        return 0;  
    }  
     
            在这个例子中,A.h是提供给客户使用的头文件,在该文件中定义了class A,其中只含有一个指针成员(pImpl),指向其实现类(AImpl)。在这样的设计之下,使用class A的客户端就完全与X、Y、Z以及AImpl的实现细节分离了,同时实现了“实现细节”的隐藏,这样,无论这些classes的任何实现修改都不需要A的客户端重新编译。
     
    使用“前置声明”的注意事项
     
            (1)在前置声明时,我们只能使用的就是类的指针和引用,自然也就不能调用对象的方法了!
              像我们这样前置声明类A:
              class A;
              是一种不完整的声明,只要类B中没有执行"需要了解类A的大小或者成员"的操作,则这样的不完整声明允许声明指向A的指针和引用。
              而在前一个代码中的语句
              A a;
              是需要了解A的大小的,不然是不可能知道如果给类B分配内存大小的,因此不完整的前置声明就不行,必须要包含A.h来获得类A的大小,同时也要重新编译类B。
              再回到前面的问题,使用前置声明只允许的声明是指针或引用的一个原因是:只要这个声明没有执行“需要了解类A的大小或者成员”的操作就可以了,所以声明成指针或引用是没有“执行需要了解类A的大小或者成员的操作”的。
              我们将上面这个例子的A.cpp稍作修改:
    [cpp]  
    //A.cpp  
    #include "stdafx.h"  
    #include "A.h"  
    #include "Aimpl.h"  
      
    A::A(){  
        pImpl = std::shared_ptr<AImpl>(new AImpl());  
    }  
      
    X* A::getX(){  
        return pImpl->getX();  
    }  
      
    Y* A::getY(){  
        return pImpl->getY();  
    }  
      
    Z* A::getZ(){  
        return pImpl->getZ();  
    }  
      
    void A::doSth(){  
        //pImpl->doSth();  
        getX()->Print();  
        getY()->Print();  
        getZ()->Print();  
    }  
  • 相关阅读:
    python的filter函数的使用方法详解以及使用案例,是否以什么结尾,是否大于什么(判断是True,则留下来)
    python的reduce函数的使用方法详解以及使用案例,相加,相乘(处理一个序列,然后把序列进程合并操作)
    python的map函数的使用方法详解以及使用案例(处理每个元素的自增、自减、平方等)
    python的匿名函数 lambda的使用方法详解以及使用案例
    python函数的 全局变量与局部变量
    python的函数介绍 位置参数 关键字参数 默认参数 参数组 *args **kwargs
    python set() 集合的添加删除、交集、并集、差集、交叉补集、集合的方法介绍以及使用案例
    python的dict()字典数据类型的方法详解以及案例使用
    python的tuple()元组数据类型的使用方法以及案例
    MATLAB分类与预测算法函数
  • 原文地址:https://www.cnblogs.com/584709796-qq-com/p/5005256.html
Copyright © 2011-2022 走看看