我的C++实践(16-18)

22 篇文章 0 订阅

我的C++实践(16):引用计数实现

转自 http://blog.csdn.net/zhoudaxia/article/details/4566914

    我们在做设计时,将接口与实现相分离是一个基本的策略。分离接口与实现主要有两种技术:
    (1)抽象基类。这个大家比较熟悉,在C++中是声明了纯虚函数的类,在Java、C#等语言中有现成的关键字。设计时将接口部分放在abstract base class中,它们是一个virtual析构函数和一组pure virtual函数。实现部分由各子类担当。这种类称为Interface Class,通过继承的方式来实现。
    (2)句柄类。即pimpl手法,把隶属对象的数据(即实现)从原对象中抽离出来,封装成一个称为impl的实现对象,在原对象中用一个指针成员指向它。pimpl即pointer to implementation,这种impl类称为Handle Class,它通过组合方式的来实现,主类的数据被抽离成一个独立的类(称为数据类,或值类),通过组合一个它的指针来完成工作。我们知道,在设计时应该优先使用组合而不继承,因为组合的耦合性更低。pimpl手法在C++的程序库设计中到处都是。
    用组合的方式来做设计时,如果多个主类对象的数据相同,我们经常可以让它们共享这一个值对象,以提高性能,这就需要对共享一个值对象的多个主对象进行引用计数。例如对字符串类String,它里面会有一个char* data的数据,我们可以把这个数据抽离出来设计成一个独立的值类StringValue,在String类中用一个StringValue*型指针指向数据。当进行拷贝或赋值时,比如s1="abcd",s2=s1,我们并不把s1的数据"abcd"深拷贝(对指针所指向的数据进行拷贝)给s2,而是让s2与s1共享这一份数据,这样可以大大提高性能。因此,我们需要对共享值对象"abcd"的主对象个数进行引用计数,要跟踪有多少个主对象引用了这一个值对象,引用计数一旦变成0就删除这个值对象。当我们需要写入时,比如s2[2]='x',这时就要执行真正的拷贝,把s1的"abcd"拷贝给s2,然后把s2的"abcd"修改为"abxd",这就是写时拷贝技术。从这可以看出,任何的组合方式都可以看作是一种pimpl手法,被组合的对象可以认为是主对象的值对象。智能指针类就是这样的,它所指向的对象可以认为是智能指针对象的数据。
    1、引用计数类。 在前面的“智能指针”介绍中我们实际上已经实现了引用计数,simplerefcount.hpp中的SimpleReferenceCount就是一个引用计数类,不过它是通过成员函数模板来对任意的指针类型进行计数,这实际上是相当于用模板的方式来实现引用计数的。现在我们需要对一般类型的对象进行引用计数,我们改用继承的方式来实现,把引用计数设计成抽象基类RCObject,值对象需要计数,它必须继承这个抽象基类,因此引用计数基类成为值对象的一部分

[cpp]  view plain copy
  1. //rcobject.hpp:引用计数类,是抽象类,因此只能作为基类使用,被计数的类要继承它  
  2. #ifndef RCOBJECT_HPP  
  3. #define RCOBJECT_HPP  
  4. #include <cstddef>  
  5. class RCObject{  
  6. private:  
  7.     std::size_t refCount; //计数变量  
  8.     bool shareable; //被计数对象(即值对象)是否可以共享的标识,默认可以共享  
  9. public:  
  10.     RCObject():refCount(0),shareable(true){  
  11.     }  
  12.     RCObject(RCObject const& rhs) : refCount(0),shareable(true){  
  13.     }  
  14.     RCObject& operator=(RCObject const& rhs){  
  15.         return *this;  
  16.     }  
  17.     virtual ~RCObject()=0; //作为基类使用,则析构函数一般要声明为vritual  
  18.                                  //注意纯虚的虚构函数必须提供实现  
  19.     void addReference(){  
  20.         ++refCount;  
  21.     }  
  22.     void removeReference(){  
  23.         if(--refCount==0)  
  24.             delete this//使用时继承RCObject的值对象(即被计数的对象)必须创建在堆上  
  25.     }  
  26.     void markUnshareable(){  
  27.         shareable=false;  
  28.     }  
  29.     bool isShareable() const//值对象是否可共享  
  30.         return shareable;  
  31.     }  
  32.     bool isShared() const//值对象是否已经被共享  
  33.         return refCount>1;  
  34.     }  
  35. };  
  36. RCObject::~RCObject(){ //纯虚的析构函数必须有定义  
  37. }  
  38. #endif  

    解释:
    (1)我们声明析构函数为纯虚的,因此RCObject是一个抽象类,不能实例化,只能由值类来继承。同时,RCObject类中不单单只是接口的声明,它也有计数功能的实现,它们是值类共性(都需要计数)的抽离,这样的类一般称为抽象混合基类。注意当把析构函数声明为纯虚的时,必须同时要有定义,因为子类一定会调用基类的析构函数,这样它就必须有定义,而不只是声明。
    (2)这是一个侵入式的计数器类,因为值对象继承自RCObject,计数器就成为了值对象的一部分,要占用值对象的内存空间。通过继承,值对象自身有了计数功能和共享开关。
    (3)这里增加了一个共享标志shareable,表示值对象能不能共享。如果值对象不想让多个主对象共享,就可以用markUnshareable()关闭这个标志。isShared()表示对象是否已经被共享。引入共享标志后就可以实现写时拷贝。比如对String类,访问可能会是读操作cout<<s2[2],也可能是写操作s2[2]='x'。如果是写操作,则在opeartor[]函数中可先判断值对象是否已经被共享,如果已经被共享,则必须拷贝一份出来进行写操作。
    (4)引用计数为0时,删除对象用delete this,这就要求值对象必须分配在堆上,可见,引用计数的抽象基类实现是有限制的。当然,我们用模板并且通过组合的方式来实现引用计数可以更完善,也更高效。这里主要是为了演示Interface Class和Handle Class的设计策略。
    2、封装值对象的智能指针。 由于我们需要在主类中组合一个值类(有引用计数功能)的指针,通过这个指针调用各个计数操作来完成值对象的计数功能,例如在String类中通过StringValue* data指针来调用各个计数操作,这会在主类中增加很多函数调用代码。为此,我们可以把这个值类指针data封装成智能指针,由智能指针类来完成所有对计数函数的调用,只要在主类中组合一个指向值类的智能指针对象(而不组合原始的值类指针)即可。当然,我们可以直接复用在“智能指针”介绍中开发的通用型智能指针,但这里的智能指针对所指类型有要求(是值类,有计数功能),具有特殊性,并不能直接用通用型的智能指针。因此,我们独立实现了一个简单的智能指针RCPtr:

[cpp]  view plain copy
  1. //rcptr.hpp:简单的智能指针,用于指向有计数功能的对象(值对象)  
  2. #ifndef RCPTR_HPP  
  3. #define RCPTR_HPP  
  4. template<typename T>  
  5. class RCPtr{ //T必须为值类,即T必须继承自RCObject  
  6. private:  
  7.     T* pointee; //哑指针  
  8.     void init(){  
  9.         if(pointee==0)  
  10.             return;  
  11.         //如果值对象不可共享  
  12.         if(pointee->isShareable()==false)  
  13.             pointee=new T(*pointee); //则只能对它进行拷贝,不能共享它  
  14.         pointee->addReference(); //现在有一个新的主对象(即组合了本智能指针的主对象)引用了  
  15.                                  //这个值对象,因此引用计数要加1  
  16.     }  
  17. public:  
  18.     RCPtr(T* realPtr=0):pointee(realPtr){ //智能指针的构造  
  19.         //产生一个智能指针,说明有了一个值对象  
  20.         //引用计数会加1  
  21.         init();  
  22.     }  
  23.     RCPtr(RCPtr const& rhs):pointee(rhs.pointee){ //智能指针的拷贝  
  24.         init();  
  25.     }  
  26.     RCPtr& operator=(RCPtr const& rhs){ //智能指针的赋值  
  27.         if(pointee==rhs.pointee)  
  28.             return *this//自我赋值情况  
  29.         T* oldPointee=pointee; //保存原来的指针  
  30.         pointee=rhs.pointee; //实行赋值,指向了新的值对象  
  31.         init(); //引用计数加1,如果值对象不能共享,则要进行拷贝  
  32.         if(oldPointee) //原来指向的值对象的引用计数减1,如果变成0,会销毁对象  
  33.             oldPointee->removeReference();  
  34.         return *this;  
  35.     }  
  36.     ~RCPtr(){  
  37.         if(pointee) //值对象引用计数减1,如果变成0,则销毁对象  
  38.             pointee->removeReference();  
  39.     }  
  40.     T* operator->() const{  
  41.         return pointee; //返回哑指针  
  42.     }  
  43.     T& operator*() const{  
  44.         return *pointee; //返回值对象  
  45.     }  
  46. };  
  47. #endif  

    解释:
    (1)RCPtr的构造。在根据传来的值对象指针构造RCPtr对象时,需要判断这个值对象是否可以共享,如果它设为不可共享,那就不能简单地增加引用计数了,而是只能只能对它进行拷贝,这样RCPtr就指向了一个独立值对象,而没有共享原来的那个值对象。因为RCPtr被组合在主对象中,说明这个独立值对象有一个主对象引用了它,因此引用计数要加1。如果可以共享,则直接增加引用计数即可。
    (2)RCPtr的拷贝也一样。把主对象a拷贝给主对象b时,a中的RCPtr成员也会拷贝给b。我们知道a和b需要共享a中的值对象,而值对象被封装在RCPtr中(让RCPtr成为主对象的一个成员,而不是赤裸裸的值对象),因此RCPtr的拷贝只要增加值对象的引用计数即可,并没有拷贝RCPtr指向的值对象(除非它不可共享,这时就必须深拷贝指向的值对象,使a和b拥有各自独立的值对象)。
    (3)RCPtr的赋值。主对象赋值a=b时,会导致里面的RCPtr成员的赋值。因为赋值时a可以共享b内部的值对象以提高性能,这需要先保存a中的RCPtr,然后把b的RCPtr指针(即内部的哑指针)赋给a,这样a就要使用b内部的值对象了,但接着要调用init(),或者是直接增加b内部的那个值对象的引用计数,或者是因为它不可共享,需要拷贝一份出来给a。最后a原来引用的值对象的计数要减1(因为主对象a已经不再引用它了)。可见,值对象拥有了共享否决权后使得引用计数的实现变得复杂了。因为值对象可以声明它不想在各个主对象之间被共享,因此我们需要增加额外的代码来判断它是否可共享,而不是在主对象拷贝或赋值时直接简单地增加值对象的引用计数。
    (4)解引用操作符operator*必须返回T&型引用,不能返回T型对象,因为哑指针pointee可能指向了派生类对象,如果返回T型对象,会导致对象切割问题。同理,箭头操作符opeator->也必须返回T*型指针。
    3、使用了引用计数的String类实现。 我们使用pimpl手法,把String内的char*型数据封装成独立的StringValue值类,在String类中用一个指针成员(这里使用封装StringValue的智能指针RCPtr<StringValue>)指向String的数据。StringValue类有引用计数功能,因此要继承自RCObject,多个String对象在 拷贝或赋值时可以共享一个StringValue值对象。

[cpp]  view plain copy
  1. //string.hpp:字符串类,对字符串对象的内容进行了引用计数,可使多个字符串  
  2. //对象共享同一个字符串值  
  3. #ifndef STRING_HPP  
  4. #define STRING_HPP  
  5. #include <iostream>  
  6. #include <cstring>  
  7. #include "rcobject.hpp"  
  8. #include "rcptr.hpp"  
  9. class String{  
  10. private:  
  11.     //表示字符串内容的内嵌类,实现了引用计数功能  
  12.     //这个值对象必须在堆上创建  
  13.     struct StringValue : public RCObject{  
  14.         char *data;  
  15.         void init(char const* initValue){  
  16.             data=new char[strlen(initValue)+1];  
  17.             strcpy(data,initValue); //对字符串进行拷贝  
  18.         }  
  19.         StringValue(char const *initValue){ //值对象的构造  
  20.             init(initValue);  
  21.         }  
  22.         StringValue(StringValue const& rhs){ //值对象的拷贝  
  23.             init(rhs.data);  
  24.         }  
  25.         ~StringValue(){  
  26.             delete[] data;  
  27.         }         
  28.     };  
  29.     RCPtr<StringValue> value; //String对象的内容,用智能指针RCPtr封装它  
  30.     friend std::ostream& operator<<(std::ostream&,String const&);  
  31. public:  
  32.     String(char const* initValue="")  
  33.         :value(new StringValue(initValue)){  
  34.     }  
  35.     char const& operator[](int index) const//const版本:只可能是读操作  
  36.         return value->data[index];  
  37.     }  
  38.     char& operator[](int index){ //非const版本:可能是读也可能是写  
  39.                                  //因此需要完成写时拷贝  
  40.         if(value->isShared()){ //如果已经被共享了  
  41.             value=new StringValue(value->data); //则必须拷贝一份出来进行写操作  
  42.         }  
  43.         value->markUnshareable(); //标记为不可共享  
  44.         return value->data[index];   
  45.     }  
  46. };  
  47. inline std::ostream& operator<<(std::ostream& os,String const& str){  
  48.     os<<(str.value)->data;  
  49.     return os;  
  50. }  
  51. #endif  

[cpp]  view plain copy
  1. //stringtest.cpp:对String的测试  
  2. #include <iostream>  
  3. #include "string.hpp"  
  4. using namespace std;  
  5. int main(){  
  6.     String s1("abcd");  
  7.     cout<<s1<<endl;  
  8.     String s2("efgh");  
  9.     s2=s1; //s2和s1共享"abcd"  
  10.     cout<<s2<<endl;  
  11.     const String s3(s1); //s1,s2,s3共享“abcd"  
  12.     cout<<s3<<endl;  
  13.     cout<<s3[2]<<endl; //读操作,调用const版本的opeator[],  
  14.                        //不会进行拷贝  
  15.     cout<<s2[2]<<endl; //读操作,调用non-const版本的opeator[],  
  16.                        //会进行拷贝  
  17.     s2[2]='x'//写操作,调用non-const版本的opeator[],  
  18.                //会进行拷贝,即写时拷贝  
  19.     cout<<s2<<endl; //输出修改后的值  
  20.     return 0;  
  21. }                                  

    解释:
    (1)这里值类StringValue封装了String的数据和实现细节,可见我们把Stirng的接口与实现细节分离了。它被实现为内嵌类,继承自RCObject,有引用计数功能。它表示是专为String设计的,只在String内部使用。
    (2)这里使用了智能指针RCPtr来封装StringValue值对象,这样String的实现就非常简洁,没有任何的计数操作的调用代码。当String对象进行拷贝或赋值时,需要对里面的StringValue值对象进行引用计数操作,所有的这些操作都委托给了智能指针RCPtr<StringValue>对象,它既含有实际的StringValue值对象,又能调用StringValue的计数函数完成实际的计数操作。String里面没有了指针成员,都是对象成员,因此无需实现拷贝构造函数、赋值运算符、析构函数等,使用默认的就够了,一切都非常的简洁。
    (3)const版本的operator[]只能是读操作,因此不需要实现写时拷贝。而non-const版的operator[]可能是读cout<<s2[2],也可能是写s2[2]='x'。如果是写,而对象已经被共享了,则必须进行拷贝了,我们对拷贝出来的一份值对象进行写操作,就需要把它标记为不可共享,这种情况下使得这个拷贝出来的值对象永远不可共享了。另一方面,我们也没有区分出读和写操作,如果这个operator[]调用是读操作(比如cout<<s2[2]),那我们同样也进行了拷贝,而实际上在读的时候并不需要拷贝,可见这样的写时拷贝实现并不是完美的,它导致有时在读的时候也进行了拷贝。事实上,opeator[]运算并不能区分是读cout<<s2[2],还是写s2[2]='x',因为opeartor[]函数里只是返回一个值的引用,实际的写操作并不是在函数里面完成的。要区分读和写,我们需要使用代理类技术(即代理模式)。
    4、为既有类添加引用计数。 前面我们设计StringValue时,让它继承RCObject,使它有了引用计数功能。可见引用计数是一种通用的功能,任何类(不一定要作为值类)只要继承RCObject,就拥有了引用计数功能。现在如果程序库中已经存在一个类Widget,它不能被修改,但客户端又需要它具有引用计数功能,该怎么办呢?唯一的办法就是把引用计数功能委托给其他类的完成。我们可以对Widget进行包装,比如通过组合把Widget包装成RCWidget类,让RCWidget类组合一个指向Widget对象的RCPtr智能指针来完成Widget的所有功能,同时增加引用计数功能。我们把RCWidget提供给客户端使用就可以了。最直接的实现是让RCWidget继承RCObject,但这样的话RCWidget对象就只能创建在堆上了。其实我们可以让RCPtr来完成对Widget的引用计数。这就需要修改RCPtr的设计,其实并不难。只要在RCPtr中引入一个内嵌类,让它继承RCObject,用这个内嵌类来对作为模板实参传过去的Widget类进行引用计数,修改后的智能指针为RCIPtr类。

[cpp]  view plain copy
  1. //rciptr.hpp:指向值对象的智能指针,同时可以对值对象进行引用计数  
  2. #ifndef RCIPTR_HPP  
  3. #define RCIPTR_HPP  
  4. #include "rcobject.hpp"  
  5. template<typename T>  
  6. class RCIPtr{  
  7. private:  
  8.     //由内嵌类来持有值对象,并能对值对象进行引用计数  
  9.     struct CountHolder : public RCObject{  
  10.         T* pointee;  
  11.         ~CountHolder(){  
  12.             delete pointee;  
  13.         }  
  14.     };  
  15.     CountHolder *counter; //持有者:指向值对象,并且有引用计数功能  
  16.     void init(){  
  17.         //如果对象不能共享,则只能对它进行拷贝  
  18.         if(counter->isShareable()==false){  
  19.             T* oldValue=counter->pointee; //保存原来的指针  
  20.             counter=new CountHolder; //创建一个新的持有者(有引用计数功能)  
  21.             //对值对象进行拷贝  
  22.             counter->pointee=oldValue ? new T(*oldValue) : 0;  
  23.         }  
  24.         counter->addReference(); //引用计数加1  
  25.     }  
  26. public:  
  27.     RCIPtr(T* realPtr=0) : counter(new CountHolder){ //智能指针的构造  
  28.         //创建一个持有者  
  29.         counter->pointee=realPtr; //让持有者指向值对象  
  30.     }  
  31.     RCIPtr(RCIPtr<T> const& rhs) : counter(rhs.counter){ //智能指针的拷贝  
  32.         init();  
  33.     }  
  34.     RCIPtr<T>& operator=(RCIPtr<T> const& rhs){ //智能指针的赋值  
  35.         if(counter!=rhs.counter){  
  36.             counter->removeReference(); //原来引用计数减1  
  37.             counter=rhs.counter; //进行赋值,指向新的值对象  
  38.             init(); //引用计数加1,如果值对象不能共享,则要进行拷贝  
  39.         }  
  40.         return *this;  
  41.     }  
  42.     ~RCIPtr(){  
  43.         counter->removeReference();  
  44.     }  
  45.     T* operator->() const{  
  46.         return counter->pointee; //返回哑指针  
  47.     }  
  48.     T& operator*() const{  
  49.         return *(counter->pointee); //返回值对象  
  50.     }  
  51.     RCObject& getRCObject() const//返回值对象,客户可以判断它是否被共享  
  52.                                    //这里直接调用上面的operator*  
  53.         return *counter;  
  54.     }  
  55. };  
  56. #endif  

[cpp]  view plain copy
  1. //rcwidget.hpp:RCWidget对Widget进行包装,通过智能指针RCIPtr使之具有引用计数功能  
  2. #ifndef RCWIDGET_HPP  
  3. #define RCWIDGET_HPP  
  4. #include <iostream>  
  5. #include "string.hpp"  
  6. #include "rciptr.hpp"  
  7. class Widget{ //容器部件类:已有的不能修改的类  
  8. private:  
  9.     int isize;  
  10.     String stitle;  
  11.     friend std::ostream& operator<<(std::ostream&,Widget const&);  
  12. public:  
  13.     Widget(int size=0):isize(size){  
  14.         //...  
  15.     }  
  16.     Widget(String title="Untitled"):stitle(title){  
  17.         //...  
  18.     }  
  19.     void doThis(){ //非const操作  
  20.         //...  
  21.     }  
  22.     int showThat() const//const操作  
  23.         return isize;  
  24.         //...  
  25.     }  
  26.     //...  
  27. };  
  28. inline std::ostream& operator<<(std::ostream& os,Widget const& rhs){  
  29.     os<<"Size="<<rhs.isize<<",Title="<<rhs.stitle; //输出窗口大小和标题  
  30.     return os;  
  31. }  
  32.   
  33. //对Widget添加了引用计数功能后的包装类  
  34. class RCWidget{   
  35. private:  
  36.     RCIPtr<Widget> value; //指向Widget对象的智能指针,同时还能对Widget进行计数  
  37.     friend std::ostream& operator<<(std::ostream&,RCWidget const&);  
  38. public:  
  39.     RCWidget(int size=0) : value(new Widget(size)){  
  40.     }  
  41.     RCWidget(String title="Untitled"):value(new Widget(title)){  
  42.     }     
  43.     void doThis(){ //非const操作,说明有可能修改Widget对象的内容,需要写时拷贝  
  44.         if(value.getRCObject().isShared())  
  45.             value=new Widget(*value); //若已经被共享了,则要拷贝一份出来  
  46.         value->doThis(); //调用实际的操作  
  47.     }  
  48.     int showThat() const//const操作,不会修改Widget对象,因此直接转发调用  
  49.         return value->showThat();  
  50.     }  
  51.     //...  
  52. };  
  53. inline std::ostream& operator<<(std::ostream& os,RCWidget const& rhs){  
  54.     os<<*(rhs.value);  //输出窗口大小和标题  
  55.     return os;  
  56. }  
  57. #endif  
  58.       
  59.       

[cpp]  view plain copy
  1. //rcwidgettest.cpp:对RCWidget的测试  
  2. #include <iostream>  
  3. #include "rcwidget.hpp"  
  4. using namespace std;  
  5. int main(){  
  6.     RCWidget w1("Jack"); //标题为"Jack"  
  7.     cout<<w1<<endl; //输出窗口大小和标题内容  
  8.     RCWidget w2("Zhou"); //标题为"Zhou"  
  9.     w2=w1; //w2和w1共享一个Widget对象  
  10.     cout<<w2<<endl;  
  11.       
  12.     const RCWidget w3(w1); //w1,w2,w3共享一个Widget对象  
  13.     cout<<w3<<endl;  
  14.     return 0;  
  15. }                                  

    解释:
    (1)RCIPtr与RCPtr相比,只有两点不同,一是RCIPtr直接指向值对象,现在RCIPtr引入了一个中间层CountHolder类,CountHolder代理对象指向实际的值对象,同时还能对值对象进行引用计数(继承了RCObject)。二是提供一个友好的getRCObject()来返回指向的值对象,当然operator*也是直接返回值对象的,但getRCObject()对客户端而言更友好,因为客户端会经常使用这个函数。
    (2)RCIPtr中的CountHolder对象必须分配在堆上(因此用counter指针),它在计数值变成0时会自己销毁自己(delete this),因此并不需要在RCIPtr的析构函数中delete counter,只需要减少引用计数即可,这一点要特别注意。
    (3)RCWidget包装了Widget,具有Widget的功能,通过RCIPtr它还具有了对Widget的引用计数功能,因此多个RCWidget可以共享一个Widget。当调用非const操作时,说明有可能修改Widget对象的内容,需要写时拷贝。先用getRCObject()获得值对象,看看它是否被共享,若被共享了,则需要拷贝一份出来才能进行修改,然后调用实际的可能做修改动作的Widget操作。当调用const操作时,不会修改Widget对象内容,直接转发调用。RCWidget的实现非常简洁,没有指针成员,因此无需拷贝构造函数、赋值操作符、析构函数等,使用默认就可以了。
    这其实就是Decorator模式的应用。Decorator模式用于动态给一个对象添加一些额外的职责,就扩展功能而言,Decorator模式比生成子类方式更为灵活。我们用RCWidget对Widget进行装饰,动态地给它增加了引用计数功能。




=================================================================================


我的C++实践(17):代理类技术


转自 http://blog.csdn.net/zhoudaxia/article/details/4569792


    代理类其实就是代理模式的应用。Proxy模式为其他对象提供一种代理以控制这个对象的访问。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层,这个访问层也叫代理。Proxy模式是最常见的模式,在我们生活中处处可见。
    1、实现二维数组。 C++中数组各个维的大小必须在编译期确定。要在运行期确定数组大小,我们可以开发一个数组类来代替内建的数组,这样就可以在运行期指定数组的大小。例如对二维数组,开发一个Array2D<T>模板。对二维数组元素的访问用arr[4][6]的形式,但是类对象没有operator[][]这样的重载运算符,因此怎样才能使之与内建数组的行为一致呢?如果在Array2D<T>中直接存储二维数组,我们只有operator[]运算符,它只带一个索引参数,因此不能访问二维数组中的元素。但是一维数组可以通过operator[]直接访问数组的元素,而二维数组实际上是一个一维数组,其中每个元素又是一个一维数组。比如arr[4][6]实际上是(arr[4])[6],先取出arr的第4个元素arr[4],这个元素是一维数组,然后在arr[4]这个元素中取出第6个元素。可见,访问Array2D<T>中的元素分两步,我们可以先开发一个一维数组类Array1D<T>,它的operator[]可以直接访问元素,然后在Array2D<T>中存储一个Array1D<T>数组(而不是原始的二维数组数据),Array2D<T>的operator[]只是返回一个一维数组对象,这是第一步访问,第二步访问则直接代理给了这个一维数组对象,通过它的operator[]最终得到数组中的元素值。如下:

[cpp]  view plain copy
  1. //Array2D.hpp:二维数组类  
  2. #ifndef ARRAY_2D_HPP  
  3. #define ARRAY_2D_HPP  
  4. #include <cstddef>  
  5. template<typename T>  
  6. class Array1D{     //一维数组模板  
  7. private:  
  8.     class NotEqualLength{ }; //数组长度不相等时的异常类  
  9.     std::size_t length; //一维数组长度  
  10.     T* data;  
  11.     void copy(Array1D<T> const& rhs){  
  12.         for(std::size_t i=0;i<length;++i)  
  13.             data[i]=rhs.data[i];  
  14.     }  
  15. public:  
  16.     Array1D(std::size_t len):length(len),data(new T[len]){ //T必须要有默认构造函数  
  17.     }  
  18.     Array1D(Array1D<T> const& rhs):length(rhs.length),data(new T[rhs.length]){  
  19.         copy(rhs);  //深拷贝  
  20.     }  
  21.     Array1D<T>& operator=(Array1D<T> const& rhs){  
  22.         if(this==&rhs)  
  23.             return *this;  
  24.         if(length!=rhs.length)  
  25.             throw NotEqualLength(); //数组长度不相等,不能赋值  
  26.         else  
  27.             copy(rhs); //深拷贝  
  28.         return *this;  
  29.     }  
  30.     ~Array1D(){  
  31.         delete[] data;  
  32.     }  
  33.     T const& operator[](std::size_t index) const//const版本  
  34.         return data[index]; //直接返回数组中的元素  
  35.     }  
  36.     T& operator[](std::size_t index){ //非const版本  
  37.         return data[index];  
  38.     }  
  39.     std::size_t getLength() const//返回数组第1维的大小  
  40.         return length;  
  41.     }  
  42.     std::size_t getElemSum() const//返回数组中元素总个数  
  43.         return length;  
  44.     }  
  45. };  
  46. template<typename T>  
  47. class Array2D{   //二维数组模板  
  48. private:  
  49.     class NotEqualLength{ };  
  50.     std::size_t length2,length1; //数组各个维的大小  
  51.     Array1D<T>* data;  
  52. public:  
  53.     Array2D(std::size_t len2,std::size_t len1)  
  54.         :length2(len2),length1(len1),data(0){  
  55.         //为Array1D<T>数组分配原始内存  
  56.         void* raw=::operator new[](length2*sizeof(Array1D<T>));  
  57.         data=static_cast<Array1D<T>*>(raw);  
  58.           
  59.         //用placement new调用构造函数初始化各个元素的内存  
  60.         for(std::size_t i=0;i<length2;++i)  
  61.             new(data+i) Array1D<T>(length1);  
  62.     }  
  63.     Array2D(Array2D<T> const& rhs)  
  64.         :length2(rhs.length2),length1(rhs.length1),data(0){ //拷贝构造:深拷贝  
  65.             //为Array1D<T>数组分配原始的内存  
  66.             void* raw=::operator new[](length2*sizeof(Array1D<T>));  
  67.             data=static_cast<Array1D<T>*>(raw);  
  68.               
  69.             //用placement new调用拷贝构造函数来初始化各个元素的内存  
  70.             for(std::size_t i=0;i<length2;++i)  
  71.                 new(data+i) Array1D<T>(rhs.data[i]);  
  72.     }  
  73.     Array2D<T>& operator=(Array2D<T> const& rhs){ //赋值运算符:要深拷贝  
  74.         if(this==&rhs)  
  75.             return *this;  
  76.         //如果有一维不相等,则数组不能赋值,抛出异常  
  77.         if((length2!=rhs.length2)||(length1!=rhs.length1))  
  78.             throw NotEqualLength();  
  79.         else//否则进行深拷贝  
  80.             for(std::size_t i=0;i<length2;++i)  
  81.                 data[i]=rhs.data[i];  
  82.         }  
  83.         return *this;  
  84.     }  
  85.     ~Array2D(){ //没有用new来创建data数组,就不能直接用delete[]来删除data  
  86.         for(std::size_t i=0;i<length2;++i)  
  87.             data[i].~Array1D<T>(); //显式调用析构函数销毁各个对象  
  88.         ::operator delete[](static_cast<void*>(data)); //释放内存  
  89.     }  
  90.     Array1D<T> const& operator[](std::size_t index) const//const版本  
  91.         return data[index]; //返回索引处的一维数组对象  
  92.     }  
  93.     Array1D<T>& operator[](std::size_t index){ //非const版本  
  94.         return data[index]; //返回索引处的一维数组对象  
  95.     }   
  96.     std::size_t getLength2() const//返回数组第2维的大小  
  97.         return length2;  
  98.     }   
  99.     std::size_t getLength1() const//返回数组第1维的大小  
  100.         return length1;  
  101.     }   
  102.     long getElemSum() const//返回数组中的元素总个数  
  103.         return length1*length2;  
  104.     }  
  105. };  
  106. #endif  

[cpp]  view plain copy
  1. //Array2Dtest.cpp:对二组数组的测试  
  2. #include <cstddef>  
  3. #include <iostream>  
  4. #include "Array2D.hpp"  
  5. int main(){  
  6.     std::size_t a1=4;  
  7.     std::size_t a2=5;  
  8.     Array1D<int> myarr(a1); //数组的各个维数大小在运行期确定  
  9.     std::cout<<"myarr's length: "<<myarr.getLength()<<std::endl; //输出一维数组长度  
  10.     for(std::size_t i=0;i<myarr.getLength();++i)  
  11.         myarr[i]=i;  
  12.     std::cout<<"myarr[2]: "<<myarr[2]<<std::endl; //输出myarr[2]=2  
  13.     std::cout<<"myarr's elem-numbers: "<<myarr.getElemSum()<<std::endl; //输出元素总个数  
  14.       
  15.     Array1D<int> yourarr(myarr); //测试拷贝构造函数  
  16.     std::cout<<"yourarr[2]: "<<yourarr[2]<<std::endl;  
  17.       
  18.     Array1D<int> herarr(4);  
  19.     herarr=yourarr;  //测试赋值操作符  
  20.     std::cout<<"herarr[2]: "<<herarr[2]<<std::endl;  
  21.       
  22.           
  23.     Array2D<int> arr(a1,a2);   
  24.     std::cout<<"arr's length2: "<<arr.getLength2()<<std::endl; //输出二维数组各个维的长度  
  25.     std::cout<<"arr's length1: "<<arr.getLength1()<<std::endl;      
  26.     for(std::size_t i=0;i<arr.getLength2();++i)  
  27.         for(std::size_t j=0;j<arr.getLength1();++j)  
  28.             arr[i][j]=i+j; //下标访问与内置数组一样  
  29.     std::cout<<"arr[2][3]: "<<arr[2][3]<<std::endl;  //输出arr[2][3]=5  
  30.     std::cout<<"arr's elem-numbers: "<<arr.getElemSum()<<std::endl;  //输出元素总个数  
  31.       
  32.     Array2D<int> arr2(arr); //测试拷贝构造函数  
  33.     std::cout<<"arr2[2][3]: "<<arr2[2][3]<<std::endl;  
  34.       
  35.     Array2D<int> arr3(4,5);   
  36.     arr3=arr2; //测试赋值操作符  
  37.     std::cout<<"arr3[2][3]: "<<arr3[2][3]<<std::endl;  
  38.       
  39.     Array2D<int> arr4(3,5);  
  40.     arr4=arr3; //抛出异常  
  41.     return 0;  
  42. }  

    解释:
    (1)Array1D<T>是一维数组模板,它直接存储了一个指向数组的指针data,因此在拷贝和赋值时都要进行深拷贝(对data指向的数据进行拷贝),赋值时还要检查数组长度是否一致,若不一致,则不能赋值,抛出异常。由于要创建T类型的数组,因此T必须要有默认构造函数。当然我们可以用容器比如vector来存放数据,而不用data数组,这样就可以不要求T必须有默认构造函数。Array1D<T>的operator[]直接返回数组中元素的引用。
    (2)Array2D<T>是二维数组模板,它并没有存储二维数组数据,而存储了一个由一维数组对象组成的数组data。注意因为Array1D<T>没有默认的构造函数,它只有一个单参数的构造函数,因此不能直接用new Array1D<T>[len2]来初始化data。当要创建数组但没有默认构造函数时,我们可以用operator new[]来为数组分配原始的内存,然后用placement new表达式调用显式的构造函数来初始化各个元素的内存。对拷贝构造也类似,只不过调用的是拷贝构造函数。这里创建数组并没有用new操作符,因此在析构时不能用delete操作符(用delete是未定义行为,在Linux下出现segmentation fault错误),必须对数组的各个元素显式地调用析构函数来销毁对象,然后调用operator delete[]来释放整个数组内存。
    (3)Array2D<T>的operator[]只是返回下标处的一维数组对象的引用,相当于arr[2],这样对数组元素的访问被代理给了这个一维数组对象arr[2],用它的operator[]最终可以获取到元素的值,即arr[2][3],从测试代码中我们可以看出这个结果。可见,通过代理类Array1D<T>,我们最终实现了与内建行为一致的元素访问语法。这种思想可以推广到多维数组上去。
    2、区分operator[]的读操作和写操作。 对于前面“引用计数实现”中介绍的String类,我们通过共享开关实现了一定程度的写时拷贝,但并不完美,它导致有时在读的时候也进行了拷贝。这主要是由于共享开关并不能让operator[]区分读操作和写操作,opeartor[]函数里只是返回下标处字符的引用,然后我们才对这个字符进行读(作右值)或写(作左值)操作,如cout<<s2[2]是读操作,s2[2]='x'是写操作。可见读或写操作并不是在operator[]里面完成的,operator[]内部并不能区分是读还是写。通过代理类技术,我们可以将读或写的判断推迟到operator[]返回之后。修改operator[],让它返回一个字符的代理类对象(而不是字符本身),然后看看这个代理对象是被读(比如赋值其他对象),还是被写(比如被赋值),是写则需要写时拷贝。

[cpp]  view plain copy
  1. //string2.hpp:字符串类,使用代理模式来区分读操作和写操作  
  2. #ifndef STRING_HPP  
  3. #define STRING_HPP  
  4. #include <iostream>  
  5. #include <cstring>  
  6. #include "rcobject.hpp"  
  7. #include "rcptr.hpp"  
  8. class String{  
  9. private:  
  10.     //表示字符串内容的内嵌类,实现了引用计数功能  
  11.     //这个值对象必须在堆上创建  
  12.     struct StringValue : public RCObject{  
  13.         char *data;  
  14.         void init(char const* initValue){  
  15.             data=new char[strlen(initValue)+1];  
  16.             strcpy(data,initValue); //对字符串进行拷贝  
  17.         }  
  18.         StringValue(char const *initValue){ //值对象的构造  
  19.             init(initValue);  
  20.         }  
  21.         StringValue(StringValue const& rhs){ //值对象的拷贝  
  22.             init(rhs.data);  
  23.         }  
  24.         ~StringValue(){  
  25.             delete[] data;  
  26.         }         
  27.     };  
  28.     RCPtr<StringValue> value; //String对象的内容,用智能指针RCPtr封装它  
  29. public:  
  30.     //字符的代理类  
  31.     class CharProxy{  
  32.     private:  
  33.         String& theString; //代理字符所从属的String对象  
  34.         int charIndex;  //真正字符在String中的下标  
  35.     public:  
  36.         CharProxy(String& str,int index):theString(str),charIndex(index){  
  37.         }  
  38.         CharProxy& operator=(CharProxy const& rhs){ //代理对象之间的写操作:需要写时拷贝  
  39.             if(this==&rhs)  
  40.                 return *this;   
  41.             if(theString.value->isShared()) //若已经被共享,则写时需要拷贝  
  42.                 theString.value=new StringValue(theString.value->data);  
  43.                   
  44.             theString.value->data[charIndex]=  
  45.                 rhs.theString.value->data[rhs.charIndex]; //写入操作  
  46.             return *this;  
  47.         }  
  48.         CharProxy& operator=(char c){  //原始字符到代理对象的写操作:写时拷贝  
  49.             if(theString.value->isShared())  
  50.                 theString.value=new StringValue(theString.value->data);  
  51.                   
  52.             theString.value->data[charIndex]=c; //写入操作  
  53.             return *this;  
  54.         }  
  55.         operator char() const//对代理对象的读操作:直接转型为底部字符,无需拷贝  
  56.             return theString.value->data[charIndex];  
  57.         }  
  58.     };  
  59.       
  60.     String(char const* initValue="")  
  61.         :value(new StringValue(initValue)){ //构造函数  
  62.     }  
  63.     CharProxy const operator[](int index) const//const版本:对返回的代理对象只能进行读操作  
  64.                                                  //因为返回的对象是const的  
  65.         //对要获取的字符创建一个代理对象返回  
  66.         return CharProxy(const_cast<String&>(*this),index);  
  67.     }  
  68.     CharProxy operator[](int index){ //非const版本:对返回的代理对象可读可写  
  69.         return CharProxy(*this,index);  
  70.     }  
  71.       
  72.     friend class CharProxy; //要访问String的私有成员value  
  73.     friend std::ostream& operator<<(std::ostream&,String const&);  
  74. };  
  75. inline std::ostream& operator<<(std::ostream& os,String const& str){  
  76.     os<<(str.value)->data;  
  77.     return os;  
  78. }  
  79. #endif  

[cpp]  view plain copy
  1. //stringtest.cpp:对区分了读操作还是写操作的String类的测试  
  2. #include <iostream>  
  3. #include "string2.hpp"  
  4. using namespace std;  
  5. int main(){  
  6.     String s1("abcd");  
  7.     cout<<s1<<endl;  
  8.     String s2("efgh");  
  9.     s2=s1; //s2和s1共享"abcd"  
  10.     cout<<s2<<endl;  
  11.     const String s3(s1); //s1,s2,s3共享“abcd"  
  12.     cout<<s3<<endl;  
  13.     cout<<s3[2]<<endl; //读操作,s3[2]直接转型为底部字符,无拷贝动作  
  14.     cout<<s2[2]<<endl; //读操作,s2[2]直接转型为底部字符,无拷贝动作  
  15.     s2[2]='x'//写操作,会进行写时拷贝  
  16.     s2[1]=s2[2]; //写操作,会进行写时拷贝  
  17.     cout<<s2<<endl; //输出修改后的值  
  18.     return 0;  
  19. }                                  

    解释:
    (1)CharProxy类是字符的代理类,它记录了字符的下标和字符属于哪个String对象,用它来控制对字符的访问。String的const版本的operator[]返回const的CharProxy对象,这样我们对这个字符代理对象只能进行读操作。由于CharProxy构造函数的参数为String&,而operator[]中的*this是const的,因此要用const_cast去掉const属性。String的非const版本的operator[]返回非const的CharProxy对象(而不引用,因此不能通过operator[]来修改String的内容),这样我们对这个字符代理对象可读可写。现在,s2[2],s2[3]等操作得到的是CharProxy对象,而不是原始的字符。
    (2)现在来看对s2[2]的操作,它是一个CharProxy对象,而不是原始的字符,当进行读操作cout<<s2[2]时,直接用CharProxy中的转型运算符operator char,转型为底部字符,无需拷贝,并且是const的,不能修改String中的这个字符,因此读操作区分出来了,没有拷贝。当进行写操作s2[2]='x',调用CharProxy的第二个赋值运算符,把char型字符赋给CharProxy对象,这需要写时拷贝。对另一种写操作形式s2[2]=s2[3],则调用第一个赋值运算符,也进行了写时拷贝。赋值运算符的左边是从String的operator[]返回的CharProxy对象,作为左值肯定是写操作,因此写操作也区分出来了。可见,通过代理对象,我们区分出了String的operator[]是读操作还是写操作,并且在写操作时进行写时拷贝。注意代理类CharProxy要访问String的私有成员value,因此要声明为友元类。
    (3)使用代理类技术并不是没有缺点的。比如原来的s2[2]返回的是直接的字符,现在返回的是CharProxy对象。当然通过一次用户自定义的转型,s2[2]可以当作char型字符来用,但我们不能对s2[2]实施类似于char型的其他运算。比如不能把&s2[2]赋给char*型指针,因为CharProxy没有重载operator&,&s2[2]的结果是CharProxy*型指针。同理还有+=、++、<<=等很多运算,以及需要char&型作为函数参数而不能把s2[2]传过去等,你要使用这些操作,就必须在CharProxy中重载各个运算符。总之,代理对象不可能与它所代理的字符有完全相同的行为,而如果不使用代理对象的话,我们就不存在这样的问题。




==============================================================================



我的C++实践(18):多态的双重分派实现

转自 http://blog.csdn.net/zhoudaxia/article/details/4580438


    一般的多态是单重分派,即一个基类指针(或引用)直接到绑定到某一个子类对象上去,以获得多态行为。在前面“多态化的构造函数和非成员函数”介绍中,非成员函数函数operator<<实现了单重分派,它只有一个多态型的参数,即基类引用NLComponent&,通过在继承体系中定义一个统一的虚函数接口print来完成实际的功能,然后让operator<<的NLComponent&引用直接调用它即可,就可以自动地分派到某一个子类的print上去。
    但很多时候我们需要双重分派或多重分派。比如有一个外太空天体碰撞的视频游戏软件,涉及到宇宙飞船SapceShip、太空站SpaceStation、小行星Asteroid,它们都继承自GameObject。当天体碰撞时,需要调用processCollision(GameObject& obj1,GameObject& obj2)来进行碰撞处理,不同天体之间的碰撞产生不同的效果。这里有两个基类引用型的参数,它们的动态类型不同时需要做不同的碰撞处理,这就是双重分派。一种实现方案类似于前面的NLComponent,在各个天体类中定义统一的虚函数接口collide(GameObject&,GameObject&)来完成实际的碰撞处理,在processCollision中调用它即可。这样,在collide中我们要用一大堆的if/else来判断参数的动态类型(用typeid),根据不同的动态类型调用不同的碰撞处理函数,这种方法显然非常糟糕,它使得一个天体类需要知道它所有的兄弟类,特别地,如果增加一个新类(比如Satellite),那所有的类都需要修改collide,以增加对这个新类的判断,然后重新编译全部的代码。
    如果分析虚函数的实现机理,我们知道虚函数在编译器中通过虚函数表来实现,它是一个函数指针数组,数组的每个元素是一个函数指针,指向了实际要调用的虚函数,每个函数指针有一个唯一的下标索引,通过下标索引可以直接定位到该函数指针入口。这就启示我们,可以通过模拟虚函数表来实现双重分派。
    1、模拟虚函数表。我们把各个碰撞函数实现为非成员函数,参数的不同动态类型对应不同的碰撞函数。它们接受的参数都是两个GameObject&引用,这样所有的碰撞函数都具有相同的类型。定义一个map用来存放这种类型的函数指针,用函数参数的动态类型名称作为唯一的索引,由于有两个参数,因此把它们捆绑成一个pair对象来作为唯一的索引。这样,在processCollision中,直接根据两个参数的动态类型名称查找函数表,找到接受此参数的函数指针,然后调用这个碰撞函数进行处理即可。
    下面是天体类的继承体系:

[cpp]  view plain copy
  1. //GameObject.hpp:太空游戏的框架  
  2. #ifndef GAME_OBJECT_HPP  
  3. #define GAME_OBJECT_HPP  
  4. class GameObject{ //表示天体的抽象基类  
  5. public:  
  6.     //...  
  7.     virtual ~GameObject()=0;  
  8. };  
  9. GameObject::~GameObject(){ //纯虚的析构函数必须有定义  
  10. }  
  11. class SpaceShip : public GameObject{ //飞船类  
  12. public:  
  13.     //...  
  14. };  
  15. class SpaceStation : public GameObject{ //空间站类  
  16. public:  
  17.     //...  
  18. };  
  19.   
  20. class Asteroid : public GameObject{ //小行星类  
  21. public:  
  22.     //...  
  23. };  
  24.   
  25. #endif  

    下面是碰撞处理的实现:

[cpp]  view plain copy
  1. //collision.hpp:碰撞处理  
  2. #ifndef COLLISION_HPP  
  3. #define COLLISION_HPP  
  4. #include <string>  
  5. #include <utility>   //用到了pair及auto_ptr  
  6. #include <map>  
  7. #include "GameObject.hpp"  
  8. namespace{  
  9.     //主要的碰撞处理函数   
  10.     void shipStation(GameObject& spaceShip,GameObject& spaceStation){  
  11.         //处理SpaceShip-SpaceStation碰撞:比如让双方遭受与碰撞速度成正比的损坏  
  12.     }     
  13.     void shipAsteroid(GameObject& spaceShip,GameObject& asteroid){  
  14.         //处理SpaceShip-Asteroid碰撞  
  15.     }     
  16.     void stationAsteroid(GameObject& spaceStation,GameObject& asteroid){  
  17.         //处理SpaceStation-Asteroid碰撞  
  18.     }     
  19.     void shipShip(GameObject& spaceShip1,GameObject& spaceShip2){  
  20.         //处理SpaceShip-SpaceShip碰撞  
  21.     }  
  22.     void stationStation(GameObject& spaceStation1,GameObject& spaceStation2){  
  23.         //处理SpaceStation-SpaceStation碰撞  
  24.     }   
  25.     void asteroidAsteroid(GameObject& asteroid1,GameObject& asteroid2){  
  26.         //处理Asteroid-Asteroid碰撞  
  27.     }   
  28.       
  29.     //对称的版本  
  30.     void stationShip(GameObject& spaceStation,GameObject& spaceShip){  
  31.         shipStation(spaceShip,spaceStation);  
  32.     }  
  33.     void asteroidShip(GameObject& asteroid,GameObject& spaceShip){  
  34.         shipAsteroid(spaceShip,asteroid);  
  35.     }  
  36.     void asteroidStation(GameObject& asteroid,GameObject& spaceStation){  
  37.         stationAsteroid(spaceStation,asteroid);  
  38.     }  
  39.           
  40.     class UnknownCollision{ //不明天体碰撞时的异常类  
  41.     public:  
  42.         UnknownCollision(GameObject& object1,GameObject& object2){ }  
  43.     };  
  44.       
  45.     typedef void (*HitFunctionPtr)(GameObject&,GameObject&); //指向碰撞函数的函数指针    
  46.     typedef std::pair<std::string,std::string> StringPair; //关联碰撞函数两个参数的动态类型    
  47.     //函数表的类型:每项关联了碰撞函数两个参数的动态类型名和碰撞函数本身  
  48.     typedef std::map<StringPair, HitFunctionPtr> HitMap;  
  49.       
  50.     HitMap* initializeCollisionMap(); //初始化函数表  
  51.     HitFunctionPtr lookup(std::string const& class1,  
  52.                         std::string const& class2); //在函数表中查找需要的碰撞函数  
  53. }  //end namespace  
  54. void processCollision(GameObject& object1,GameObject& object2){  
  55.     根据参数的动态类型查找相应碰撞函数  
  56.     HitFunctionPtr phf=lookup(typeid(object1).name(),typeid(object2).name());  
  57.       
  58.     if(phf)   
  59.         phf(object1,object2); //调用找到的碰撞处理函数来进行碰撞处理  
  60.     else  
  61.         throw UnknownCollision(object1,object2); //没有找到则抛出异常  
  62. }  
  63. namespace{  
  64.     HitMap* initializeCollisionMap(){  //创建并初始化虚函数表  
  65.         HitMap *phm=new HitMap; //创建函数表  
  66.         //初始化函数表  
  67.         (*phm)[StringPair(typeid(SpaceShip).name(),  
  68.                         typeid(SpaceStation).name())]=&shipStation;  
  69.         (*phm)[StringPair(typeid(SpaceShip).name(),  
  70.                         typeid(Asteroid).name())]=&shipAsteroid;  
  71.         (*phm)[StringPair(typeid(SpaceStation).name(),  
  72.                         typeid(Asteroid).name())]=&shipAsteroid;  
  73.         //要包含所有的碰撞函数  
  74.         //...  
  75.         (*phm)[StringPair(typeid(Asteroid).name(),  
  76.                         typeid(SpaceStation).name())]=&asteroidStation;  
  77.           
  78.         return phm;  
  79.     }  
  80. }  
  81. namespace{  
  82.      //根据参数类型名在函数表中查找需要的碰撞函数  
  83.      HitFunctionPtr lookup(std::string const& class1,  
  84.                         std::string const& class2){  
  85.         //用智能指针指向返回的函数表,为静态,表示只能有一个函数表  
  86.         static std::auto_ptr<HitMap> collisionMap(initializeCollisionMap());  
  87.           
  88.         HitMap::iterator mapEntry=collisionMap->find(make_pair(class1,class2));  
  89.         if(mapEntry==collisionMap->end())  
  90.             return 0; //没找到,则返回空指针  
  91.         return (*mapEntry).second; //找到则返回关联的碰撞函数  
  92.     }  
  93. }  
  94.           
  95. #endif    

[cpp]  view plain copy
  1. //GameTest.cpp:对游戏框架的测试  
  2. #include <iostream>  
  3. #include "GameObject.hpp"  
  4. #include "Collision.hpp"  
  5. int main(){  
  6.     SpaceShip a;  
  7.     SpaceStation b;  
  8.     Asteroid c;  
  9.     processCollision(a,b);  
  10.     processCollision(a,c);  
  11.     processCollision(b,c);  
  12.     return 0;  
  13. }  

    解释:
    (1)各个碰撞处理函数的类型相同,都是void(GameObject&,GameObject&),因此在函数映射表中可以统一存放它们的指针。碰撞处理具有对称性,对称的版本直接交换一下参数来调用原来的版本即可。需要一个异常类,当没有找到对应的碰撞函数时,抛出异常。
    (2)把函数的两个参数的动态类型名称捆绑成pair对象,它的类型定义为StringPair,函数映射表的类型定义为HitMap。
    (3)主要有两个函数实现,在前面的匿名空间中进行了声明,然后在后面的匿名空间中进行了定义。一个初始化函数表initializeCollisionMap(),它创建实际的函数表,并把各个子类的名称和碰撞函数指针填入函数表中,返回函数表的指针。一个是查找碰撞函数指针的lookup(),它用静态的智能指针指向initializeCollisionMap()返回的函数表,表示创建唯一的一个函数。然后根据参数的动态类型名称查找函数表,找到则返回关联的碰撞函数指针。
    (4)这里使用了匿名的命名空间。匿名空间中所有的东西都局部于当前编译单元(本质上说就是当前文件),与其他文件中的同名实体无关系,它们的不同的实体。有了匿名命名空间,我们就无需使用文件作用域内的static变量(它也是局部于文件的),应该尽量使用匿名的命名空间。注意initializeCollisionMap()和lookup()在前面的匿名空间中声明了,因此后面的定义也必须放在匿名空间中,这样就保证了它们的声明和定义在同一编译单元内,链接器就能正确地将声明与本编译单元内的实现关联起来,而不会去关联别的编译单元内的同名实现。
    (5)全局的processCollision中,根据两个参数的动态类型名称查找函数表,找到接受此参数的函数指针,然后直接调用这个碰撞函数即可。
    (6)这里碰撞函数都是非成员函数。当增加新的GameObject子类时,原来的各个子类无需重新编译,也无需再维护一大堆的if/else。只需增加相应的碰撞函数,在initializeCollisionMap中增加相应的映射表项即可。
    2、函数表的改进。上面每增加一个碰撞函数时,都需要在initializeCollisionMap中静态地注册一个条目。我们可以把函数映射表的功能抽离出来,开发成一个独立的类CollisionMap,提供addEntry,removeEntry,lookup来动态地对函数表添加条目、删除条目、或者搜索指定的碰撞函数。我们还可以实现单例模式,让CollisionMap只能创建一个函数表。

[cpp]  view plain copy
  1. //CollisionMap.hpp:碰撞处理函数的映射表,实现了单例模式  
  2. #ifndef COLLISION_MAP_HPP  
  3. #define COLLISION_MAP_HPP  
  4. #include <string>  
  5. #include <utility>   //用到了pair及auto_ptr  
  6. #include <map>  
  7. #include "GameObject.hpp"  
  8. class CollisionMap{ //碰撞函数映射表  
  9. public:   
  10.     typedef void (*HitFunctionPtr)(GameObject&,GameObject&); //指向碰撞函数的函数指针    
  11.     typedef std::pair<std::string,std::string> StringPair; //关联碰撞函数两个参数的动态类型    
  12.     typedef std::map<StringPair, HitFunctionPtr> HitMap;  //函数表的类型      
  13.       
  14.     //根据参数类型名称在函数映射表中查找需要的碰撞函数  
  15.     HitFunctionPtr lookup(std::string const& type1,  
  16.                         std::string const& type2){  
  17.         HitMap::iterator mapEntry=collisionMap->find(make_pair(type1,type2));  
  18.         if(mapEntry==collisionMap->end())  
  19.             return 0; //没找到,则返回空指针  
  20.         return (*mapEntry).second; //找到则返回关联的碰撞函数  
  21.     }  
  22.     //根据参数类型名称向映射表中加入一个碰撞函数  
  23.     void addEntry(std::string const& type1,  
  24.                 std::string const& type2,  
  25.                 HitFunctionPtr collisionFunction){  
  26.         if(lookup(type1,type2)==0) //映射表中没找到时插入相应条目       
  27.             collisionMap->insert(make_pair(make_pair(type1,type2),collisionFunction));  
  28.     }  
  29.     //根据参数类型名称从映射表中删除一个碰撞函数  
  30.     void removeEntry(std::string const& type1,  
  31.                     std::string const& type2){  
  32.         if(lookup(type1,type2)!=0)  //若找到,则删除该条目  
  33.             collisionMap->erase(make_pair(type1,type2));  
  34.     }  
  35. private:  
  36.     std::auto_ptr<HitMap> collisionMap; //函数映射表,用智能指针存储  
  37.       
  38.     //构造函数声明为私有,以避免创建多个碰撞函数映射表  
  39.     CollisionMap() : collisionMap(new HitMap){  
  40.     }         
  41.     CollisionMap(CollisionMap const&); //不会调用,无需定义  
  42.     friend CollisionMap& theCollisionMap();   
  43. };  
  44. inline CollisionMap& theCollisionMap(){ //返回唯一的一个碰撞函数映射表  
  45.     static CollisionMap co;  
  46.     return co;  
  47. }  
  48. #endif  

    解释:
    (1)CollisionMap的实现是很直接的,它维护一个collisionMap表来模拟虚函数表。碰撞函数的添加、删除、搜索都比较容易。theCollisionMap返回唯一的一个函数映射表。
    (2)现在游戏开发者就不再需要initializeCollisionMap、lookup这样的函数了,直接用theCollisionMap()来动态地添加和删除碰撞函数,在processCollision直接用theCollisionMap()来搜索给定索引的碰撞函数即可。可见,这种模拟虚函数表的方法还可以推广到多重分派的情况。



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LeNet-5是一种经典的卷积神经网络,它是深度学习的先驱之一。在这里,我将向您展示如何使用C语言来实现LeNet-5模型。 在C语言中实现卷积神经网络需要用到一些基本的线性代数运算,例如矩阵乘法、向量加法和卷积等。您可以使用像OpenBLAS或MKL这样的优化库来执行这些运算。然后,您需要定义LeNet-5模型的架构和参数。最后,您可以编写代码来实现前向传递和反向传播算法,以及使用随机梯度下降法进行训练。 以下是一份示例代码,它演示了如何在C语言中实现LeNet-5: ```c #include <stdio.h> #include <stdlib.h> #include <math.h> // 定义神经网络层的参数 typedef struct Layer { int in_size; // 输入数据的大小 int out_size; // 输出数据的大小 int filter_size; // 过滤器的大小 int stride; // 步长 int padding; // 填充 double *weights; // 权重矩阵 double *biases; // 偏差向量 double *out_data; // 输出数据 } Layer; // 定义神经网络模型 typedef struct Model { int input_size; // 输入数据的大小 int output_size; // 输出数据的大小 Layer conv1; // 第一层卷积层 Layer conv2; // 第二层卷积层 Layer fc1; // 第一层全连接层 Layer fc2; // 第二层全连接层 } Model; // 定义ReLU激活函数 double relu(double x) { return fmax(0, x); } // 定义softmax激活函数 void softmax(double *x, int n) { double sum = 0; for (int i = 0; i < n; i++) { x[i] = exp(x[i]); sum += x[i]; } for (int i = 0; i < n; i++) { x[i] /= sum; } } // 定义卷积操作 void conv(double *in_data, double *out_data, double *weights, double *biases, int in_size, int out_size, int filter_size, int stride, int padding) { for (int i = 0; i < out_size; i++) { out_data[i] = biases[i]; for (int j = 0; j < in_size; j++) { for (int k = 0; k < filter_size; k++) { int index = i * stride + k - padding; if (index >= 0 && index < in_size) { out_data[i] += weights[j * filter_size + k] * in_data[index]; } } } out_data[i] = relu(out_data[i]); } } // 定义全连接操作 void fc(double *in

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值