转自 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,值对象需要计数,它必须继承这个抽象基类,因此引用计数基类成为值对象的一部分
- //rcobject.hpp:引用计数类,是抽象类,因此只能作为基类使用,被计数的类要继承它
- #ifndef RCOBJECT_HPP
- #define RCOBJECT_HPP
- #include <cstddef>
- class RCObject{
- private:
- std::size_t refCount; //计数变量
- bool shareable; //被计数对象(即值对象)是否可以共享的标识,默认可以共享
- public:
- RCObject():refCount(0),shareable(true){
- }
- RCObject(RCObject const& rhs) : refCount(0),shareable(true){
- }
- RCObject& operator=(RCObject const& rhs){
- return *this;
- }
- virtual ~RCObject()=0; //作为基类使用,则析构函数一般要声明为vritual
- //注意纯虚的虚构函数必须提供实现
- void addReference(){
- ++refCount;
- }
- void removeReference(){
- if(--refCount==0)
- delete this; //使用时继承RCObject的值对象(即被计数的对象)必须创建在堆上
- }
- void markUnshareable(){
- shareable=false;
- }
- bool isShareable() const{ //值对象是否可共享
- return shareable;
- }
- bool isShared() const{ //值对象是否已经被共享
- return refCount>1;
- }
- };
- RCObject::~RCObject(){ //纯虚的析构函数必须有定义
- }
- #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:
- //rcptr.hpp:简单的智能指针,用于指向有计数功能的对象(值对象)
- #ifndef RCPTR_HPP
- #define RCPTR_HPP
- template<typename T>
- class RCPtr{ //T必须为值类,即T必须继承自RCObject
- private:
- T* pointee; //哑指针
- void init(){
- if(pointee==0)
- return;
- //如果值对象不可共享
- if(pointee->isShareable()==false)
- pointee=new T(*pointee); //则只能对它进行拷贝,不能共享它
- pointee->addReference(); //现在有一个新的主对象(即组合了本智能指针的主对象)引用了
- //这个值对象,因此引用计数要加1
- }
- public:
- RCPtr(T* realPtr=0):pointee(realPtr){ //智能指针的构造
- //产生一个智能指针,说明有了一个值对象
- //引用计数会加1
- init();
- }
- RCPtr(RCPtr const& rhs):pointee(rhs.pointee){ //智能指针的拷贝
- init();
- }
- RCPtr& operator=(RCPtr const& rhs){ //智能指针的赋值
- if(pointee==rhs.pointee)
- return *this; //自我赋值情况
- T* oldPointee=pointee; //保存原来的指针
- pointee=rhs.pointee; //实行赋值,指向了新的值对象
- init(); //引用计数加1,如果值对象不能共享,则要进行拷贝
- if(oldPointee) //原来指向的值对象的引用计数减1,如果变成0,会销毁对象
- oldPointee->removeReference();
- return *this;
- }
- ~RCPtr(){
- if(pointee) //值对象引用计数减1,如果变成0,则销毁对象
- pointee->removeReference();
- }
- T* operator->() const{
- return pointee; //返回哑指针
- }
- T& operator*() const{
- return *pointee; //返回值对象
- }
- };
- #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值对象。
- //string.hpp:字符串类,对字符串对象的内容进行了引用计数,可使多个字符串
- //对象共享同一个字符串值
- #ifndef STRING_HPP
- #define STRING_HPP
- #include <iostream>
- #include <cstring>
- #include "rcobject.hpp"
- #include "rcptr.hpp"
- class String{
- private:
- //表示字符串内容的内嵌类,实现了引用计数功能
- //这个值对象必须在堆上创建
- struct StringValue : public RCObject{
- char *data;
- void init(char const* initValue){
- data=new char[strlen(initValue)+1];
- strcpy(data,initValue); //对字符串进行拷贝
- }
- StringValue(char const *initValue){ //值对象的构造
- init(initValue);
- }
- StringValue(StringValue const& rhs){ //值对象的拷贝
- init(rhs.data);
- }
- ~StringValue(){
- delete[] data;
- }
- };
- RCPtr<StringValue> value; //String对象的内容,用智能指针RCPtr封装它
- friend std::ostream& operator<<(std::ostream&,String const&);
- public:
- String(char const* initValue="")
- :value(new StringValue(initValue)){
- }
- char const& operator[](int index) const{ //const版本:只可能是读操作
- return value->data[index];
- }
- char& operator[](int index){ //非const版本:可能是读也可能是写
- //因此需要完成写时拷贝
- if(value->isShared()){ //如果已经被共享了
- value=new StringValue(value->data); //则必须拷贝一份出来进行写操作
- }
- value->markUnshareable(); //标记为不可共享
- return value->data[index];
- }
- };
- inline std::ostream& operator<<(std::ostream& os,String const& str){
- os<<(str.value)->data;
- return os;
- }
- #endif
- //stringtest.cpp:对String的测试
- #include <iostream>
- #include "string.hpp"
- using namespace std;
- int main(){
- String s1("abcd");
- cout<<s1<<endl;
- String s2("efgh");
- s2=s1; //s2和s1共享"abcd"
- cout<<s2<<endl;
- const String s3(s1); //s1,s2,s3共享“abcd"
- cout<<s3<<endl;
- cout<<s3[2]<<endl; //读操作,调用const版本的opeator[],
- //不会进行拷贝
- cout<<s2[2]<<endl; //读操作,调用non-const版本的opeator[],
- //会进行拷贝
- s2[2]='x'; //写操作,调用non-const版本的opeator[],
- //会进行拷贝,即写时拷贝
- cout<<s2<<endl; //输出修改后的值
- return 0;
- }
解释:
(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类。
- //rciptr.hpp:指向值对象的智能指针,同时可以对值对象进行引用计数
- #ifndef RCIPTR_HPP
- #define RCIPTR_HPP
- #include "rcobject.hpp"
- template<typename T>
- class RCIPtr{
- private:
- //由内嵌类来持有值对象,并能对值对象进行引用计数
- struct CountHolder : public RCObject{
- T* pointee;
- ~CountHolder(){
- delete pointee;
- }
- };
- CountHolder *counter; //持有者:指向值对象,并且有引用计数功能
- void init(){
- //如果对象不能共享,则只能对它进行拷贝
- if(counter->isShareable()==false){
- T* oldValue=counter->pointee; //保存原来的指针
- counter=new CountHolder; //创建一个新的持有者(有引用计数功能)
- //对值对象进行拷贝
- counter->pointee=oldValue ? new T(*oldValue) : 0;
- }
- counter->addReference(); //引用计数加1
- }
- public:
- RCIPtr(T* realPtr=0) : counter(new CountHolder){ //智能指针的构造
- //创建一个持有者
- counter->pointee=realPtr; //让持有者指向值对象
- }
- RCIPtr(RCIPtr<T> const& rhs) : counter(rhs.counter){ //智能指针的拷贝
- init();
- }
- RCIPtr<T>& operator=(RCIPtr<T> const& rhs){ //智能指针的赋值
- if(counter!=rhs.counter){
- counter->removeReference(); //原来引用计数减1
- counter=rhs.counter; //进行赋值,指向新的值对象
- init(); //引用计数加1,如果值对象不能共享,则要进行拷贝
- }
- return *this;
- }
- ~RCIPtr(){
- counter->removeReference();
- }
- T* operator->() const{
- return counter->pointee; //返回哑指针
- }
- T& operator*() const{
- return *(counter->pointee); //返回值对象
- }
- RCObject& getRCObject() const{ //返回值对象,客户可以判断它是否被共享
- //这里直接调用上面的operator*
- return *counter;
- }
- };
- #endif
- //rcwidget.hpp:RCWidget对Widget进行包装,通过智能指针RCIPtr使之具有引用计数功能
- #ifndef RCWIDGET_HPP
- #define RCWIDGET_HPP
- #include <iostream>
- #include "string.hpp"
- #include "rciptr.hpp"
- class Widget{ //容器部件类:已有的不能修改的类
- private:
- int isize;
- String stitle;
- friend std::ostream& operator<<(std::ostream&,Widget const&);
- public:
- Widget(int size=0):isize(size){
- //...
- }
- Widget(String title="Untitled"):stitle(title){
- //...
- }
- void doThis(){ //非const操作
- //...
- }
- int showThat() const{ //const操作
- return isize;
- //...
- }
- //...
- };
- inline std::ostream& operator<<(std::ostream& os,Widget const& rhs){
- os<<"Size="<<rhs.isize<<",Title="<<rhs.stitle; //输出窗口大小和标题
- return os;
- }
- //对Widget添加了引用计数功能后的包装类
- class RCWidget{
- private:
- RCIPtr<Widget> value; //指向Widget对象的智能指针,同时还能对Widget进行计数
- friend std::ostream& operator<<(std::ostream&,RCWidget const&);
- public:
- RCWidget(int size=0) : value(new Widget(size)){
- }
- RCWidget(String title="Untitled"):value(new Widget(title)){
- }
- void doThis(){ //非const操作,说明有可能修改Widget对象的内容,需要写时拷贝
- if(value.getRCObject().isShared())
- value=new Widget(*value); //若已经被共享了,则要拷贝一份出来
- value->doThis(); //调用实际的操作
- }
- int showThat() const{ //const操作,不会修改Widget对象,因此直接转发调用
- return value->showThat();
- }
- //...
- };
- inline std::ostream& operator<<(std::ostream& os,RCWidget const& rhs){
- os<<*(rhs.value); //输出窗口大小和标题
- return os;
- }
- #endif
- //rcwidgettest.cpp:对RCWidget的测试
- #include <iostream>
- #include "rcwidget.hpp"
- using namespace std;
- int main(){
- RCWidget w1("Jack"); //标题为"Jack"
- cout<<w1<<endl; //输出窗口大小和标题内容
- RCWidget w2("Zhou"); //标题为"Zhou"
- w2=w1; //w2和w1共享一个Widget对象
- cout<<w2<<endl;
- const RCWidget w3(w1); //w1,w2,w3共享一个Widget对象
- cout<<w3<<endl;
- return 0;
- }
解释:
(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进行装饰,动态地给它增加了引用计数功能。
=================================================================================
转自 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[]最终得到数组中的元素值。如下:
- //Array2D.hpp:二维数组类
- #ifndef ARRAY_2D_HPP
- #define ARRAY_2D_HPP
- #include <cstddef>
- template<typename T>
- class Array1D{ //一维数组模板
- private:
- class NotEqualLength{ }; //数组长度不相等时的异常类
- std::size_t length; //一维数组长度
- T* data;
- void copy(Array1D<T> const& rhs){
- for(std::size_t i=0;i<length;++i)
- data[i]=rhs.data[i];
- }
- public:
- Array1D(std::size_t len):length(len),data(new T[len]){ //T必须要有默认构造函数
- }
- Array1D(Array1D<T> const& rhs):length(rhs.length),data(new T[rhs.length]){
- copy(rhs); //深拷贝
- }
- Array1D<T>& operator=(Array1D<T> const& rhs){
- if(this==&rhs)
- return *this;
- if(length!=rhs.length)
- throw NotEqualLength(); //数组长度不相等,不能赋值
- else
- copy(rhs); //深拷贝
- return *this;
- }
- ~Array1D(){
- delete[] data;
- }
- T const& operator[](std::size_t index) const{ //const版本
- return data[index]; //直接返回数组中的元素
- }
- T& operator[](std::size_t index){ //非const版本
- return data[index];
- }
- std::size_t getLength() const{ //返回数组第1维的大小
- return length;
- }
- std::size_t getElemSum() const{ //返回数组中元素总个数
- return length;
- }
- };
- template<typename T>
- class Array2D{ //二维数组模板
- private:
- class NotEqualLength{ };
- std::size_t length2,length1; //数组各个维的大小
- Array1D<T>* data;
- public:
- Array2D(std::size_t len2,std::size_t len1)
- :length2(len2),length1(len1),data(0){
- //为Array1D<T>数组分配原始内存
- void* raw=::operator new[](length2*sizeof(Array1D<T>));
- data=static_cast<Array1D<T>*>(raw);
- //用placement new调用构造函数初始化各个元素的内存
- for(std::size_t i=0;i<length2;++i)
- new(data+i) Array1D<T>(length1);
- }
- Array2D(Array2D<T> const& rhs)
- :length2(rhs.length2),length1(rhs.length1),data(0){ //拷贝构造:深拷贝
- //为Array1D<T>数组分配原始的内存
- void* raw=::operator new[](length2*sizeof(Array1D<T>));
- data=static_cast<Array1D<T>*>(raw);
- //用placement new调用拷贝构造函数来初始化各个元素的内存
- for(std::size_t i=0;i<length2;++i)
- new(data+i) Array1D<T>(rhs.data[i]);
- }
- Array2D<T>& operator=(Array2D<T> const& rhs){ //赋值运算符:要深拷贝
- if(this==&rhs)
- return *this;
- //如果有一维不相等,则数组不能赋值,抛出异常
- if((length2!=rhs.length2)||(length1!=rhs.length1))
- throw NotEqualLength();
- else{ //否则进行深拷贝
- for(std::size_t i=0;i<length2;++i)
- data[i]=rhs.data[i];
- }
- return *this;
- }
- ~Array2D(){ //没有用new来创建data数组,就不能直接用delete[]来删除data
- for(std::size_t i=0;i<length2;++i)
- data[i].~Array1D<T>(); //显式调用析构函数销毁各个对象
- ::operator delete[](static_cast<void*>(data)); //释放内存
- }
- Array1D<T> const& operator[](std::size_t index) const{ //const版本
- return data[index]; //返回索引处的一维数组对象
- }
- Array1D<T>& operator[](std::size_t index){ //非const版本
- return data[index]; //返回索引处的一维数组对象
- }
- std::size_t getLength2() const{ //返回数组第2维的大小
- return length2;
- }
- std::size_t getLength1() const{ //返回数组第1维的大小
- return length1;
- }
- long getElemSum() const{ //返回数组中的元素总个数
- return length1*length2;
- }
- };
- #endif
- //Array2Dtest.cpp:对二组数组的测试
- #include <cstddef>
- #include <iostream>
- #include "Array2D.hpp"
- int main(){
- std::size_t a1=4;
- std::size_t a2=5;
- Array1D<int> myarr(a1); //数组的各个维数大小在运行期确定
- std::cout<<"myarr's length: "<<myarr.getLength()<<std::endl; //输出一维数组长度
- for(std::size_t i=0;i<myarr.getLength();++i)
- myarr[i]=i;
- std::cout<<"myarr[2]: "<<myarr[2]<<std::endl; //输出myarr[2]=2
- std::cout<<"myarr's elem-numbers: "<<myarr.getElemSum()<<std::endl; //输出元素总个数
- Array1D<int> yourarr(myarr); //测试拷贝构造函数
- std::cout<<"yourarr[2]: "<<yourarr[2]<<std::endl;
- Array1D<int> herarr(4);
- herarr=yourarr; //测试赋值操作符
- std::cout<<"herarr[2]: "<<herarr[2]<<std::endl;
- Array2D<int> arr(a1,a2);
- std::cout<<"arr's length2: "<<arr.getLength2()<<std::endl; //输出二维数组各个维的长度
- std::cout<<"arr's length1: "<<arr.getLength1()<<std::endl;
- for(std::size_t i=0;i<arr.getLength2();++i)
- for(std::size_t j=0;j<arr.getLength1();++j)
- arr[i][j]=i+j; //下标访问与内置数组一样
- std::cout<<"arr[2][3]: "<<arr[2][3]<<std::endl; //输出arr[2][3]=5
- std::cout<<"arr's elem-numbers: "<<arr.getElemSum()<<std::endl; //输出元素总个数
- Array2D<int> arr2(arr); //测试拷贝构造函数
- std::cout<<"arr2[2][3]: "<<arr2[2][3]<<std::endl;
- Array2D<int> arr3(4,5);
- arr3=arr2; //测试赋值操作符
- std::cout<<"arr3[2][3]: "<<arr3[2][3]<<std::endl;
- Array2D<int> arr4(3,5);
- arr4=arr3; //抛出异常
- return 0;
- }
解释:
(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[],让它返回一个字符的代理类对象(而不是字符本身),然后看看这个代理对象是被读(比如赋值其他对象),还是被写(比如被赋值),是写则需要写时拷贝。
- //string2.hpp:字符串类,使用代理模式来区分读操作和写操作
- #ifndef STRING_HPP
- #define STRING_HPP
- #include <iostream>
- #include <cstring>
- #include "rcobject.hpp"
- #include "rcptr.hpp"
- class String{
- private:
- //表示字符串内容的内嵌类,实现了引用计数功能
- //这个值对象必须在堆上创建
- struct StringValue : public RCObject{
- char *data;
- void init(char const* initValue){
- data=new char[strlen(initValue)+1];
- strcpy(data,initValue); //对字符串进行拷贝
- }
- StringValue(char const *initValue){ //值对象的构造
- init(initValue);
- }
- StringValue(StringValue const& rhs){ //值对象的拷贝
- init(rhs.data);
- }
- ~StringValue(){
- delete[] data;
- }
- };
- RCPtr<StringValue> value; //String对象的内容,用智能指针RCPtr封装它
- public:
- //字符的代理类
- class CharProxy{
- private:
- String& theString; //代理字符所从属的String对象
- int charIndex; //真正字符在String中的下标
- public:
- CharProxy(String& str,int index):theString(str),charIndex(index){
- }
- CharProxy& operator=(CharProxy const& rhs){ //代理对象之间的写操作:需要写时拷贝
- if(this==&rhs)
- return *this;
- if(theString.value->isShared()) //若已经被共享,则写时需要拷贝
- theString.value=new StringValue(theString.value->data);
- theString.value->data[charIndex]=
- rhs.theString.value->data[rhs.charIndex]; //写入操作
- return *this;
- }
- CharProxy& operator=(char c){ //原始字符到代理对象的写操作:写时拷贝
- if(theString.value->isShared())
- theString.value=new StringValue(theString.value->data);
- theString.value->data[charIndex]=c; //写入操作
- return *this;
- }
- operator char() const{ //对代理对象的读操作:直接转型为底部字符,无需拷贝
- return theString.value->data[charIndex];
- }
- };
- String(char const* initValue="")
- :value(new StringValue(initValue)){ //构造函数
- }
- CharProxy const operator[](int index) const{ //const版本:对返回的代理对象只能进行读操作
- //因为返回的对象是const的
- //对要获取的字符创建一个代理对象返回
- return CharProxy(const_cast<String&>(*this),index);
- }
- CharProxy operator[](int index){ //非const版本:对返回的代理对象可读可写
- return CharProxy(*this,index);
- }
- friend class CharProxy; //要访问String的私有成员value
- friend std::ostream& operator<<(std::ostream&,String const&);
- };
- inline std::ostream& operator<<(std::ostream& os,String const& str){
- os<<(str.value)->data;
- return os;
- }
- #endif
- //stringtest.cpp:对区分了读操作还是写操作的String类的测试
- #include <iostream>
- #include "string2.hpp"
- using namespace std;
- int main(){
- String s1("abcd");
- cout<<s1<<endl;
- String s2("efgh");
- s2=s1; //s2和s1共享"abcd"
- cout<<s2<<endl;
- const String s3(s1); //s1,s2,s3共享“abcd"
- cout<<s3<<endl;
- cout<<s3[2]<<endl; //读操作,s3[2]直接转型为底部字符,无拷贝动作
- cout<<s2[2]<<endl; //读操作,s2[2]直接转型为底部字符,无拷贝动作
- s2[2]='x'; //写操作,会进行写时拷贝
- s2[1]=s2[2]; //写操作,会进行写时拷贝
- cout<<s2<<endl; //输出修改后的值
- return 0;
- }
解释:
(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中重载各个运算符。总之,代理对象不可能与它所代理的字符有完全相同的行为,而如果不使用代理对象的话,我们就不存在这样的问题。
==============================================================================
转自 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中,直接根据两个参数的动态类型名称查找函数表,找到接受此参数的函数指针,然后调用这个碰撞函数进行处理即可。
下面是天体类的继承体系:
- //GameObject.hpp:太空游戏的框架
- #ifndef GAME_OBJECT_HPP
- #define GAME_OBJECT_HPP
- class GameObject{ //表示天体的抽象基类
- public:
- //...
- virtual ~GameObject()=0;
- };
- GameObject::~GameObject(){ //纯虚的析构函数必须有定义
- }
- class SpaceShip : public GameObject{ //飞船类
- public:
- //...
- };
- class SpaceStation : public GameObject{ //空间站类
- public:
- //...
- };
- class Asteroid : public GameObject{ //小行星类
- public:
- //...
- };
- #endif
下面是碰撞处理的实现:
- //collision.hpp:碰撞处理
- #ifndef COLLISION_HPP
- #define COLLISION_HPP
- #include <string>
- #include <utility> //用到了pair及auto_ptr
- #include <map>
- #include "GameObject.hpp"
- namespace{
- //主要的碰撞处理函数
- void shipStation(GameObject& spaceShip,GameObject& spaceStation){
- //处理SpaceShip-SpaceStation碰撞:比如让双方遭受与碰撞速度成正比的损坏
- }
- void shipAsteroid(GameObject& spaceShip,GameObject& asteroid){
- //处理SpaceShip-Asteroid碰撞
- }
- void stationAsteroid(GameObject& spaceStation,GameObject& asteroid){
- //处理SpaceStation-Asteroid碰撞
- }
- void shipShip(GameObject& spaceShip1,GameObject& spaceShip2){
- //处理SpaceShip-SpaceShip碰撞
- }
- void stationStation(GameObject& spaceStation1,GameObject& spaceStation2){
- //处理SpaceStation-SpaceStation碰撞
- }
- void asteroidAsteroid(GameObject& asteroid1,GameObject& asteroid2){
- //处理Asteroid-Asteroid碰撞
- }
- //对称的版本
- void stationShip(GameObject& spaceStation,GameObject& spaceShip){
- shipStation(spaceShip,spaceStation);
- }
- void asteroidShip(GameObject& asteroid,GameObject& spaceShip){
- shipAsteroid(spaceShip,asteroid);
- }
- void asteroidStation(GameObject& asteroid,GameObject& spaceStation){
- stationAsteroid(spaceStation,asteroid);
- }
- class UnknownCollision{ //不明天体碰撞时的异常类
- public:
- UnknownCollision(GameObject& object1,GameObject& object2){ }
- };
- typedef void (*HitFunctionPtr)(GameObject&,GameObject&); //指向碰撞函数的函数指针
- typedef std::pair<std::string,std::string> StringPair; //关联碰撞函数两个参数的动态类型
- //函数表的类型:每项关联了碰撞函数两个参数的动态类型名和碰撞函数本身
- typedef std::map<StringPair, HitFunctionPtr> HitMap;
- HitMap* initializeCollisionMap(); //初始化函数表
- HitFunctionPtr lookup(std::string const& class1,
- std::string const& class2); //在函数表中查找需要的碰撞函数
- } //end namespace
- void processCollision(GameObject& object1,GameObject& object2){
- 根据参数的动态类型查找相应碰撞函数
- HitFunctionPtr phf=lookup(typeid(object1).name(),typeid(object2).name());
- if(phf)
- phf(object1,object2); //调用找到的碰撞处理函数来进行碰撞处理
- else
- throw UnknownCollision(object1,object2); //没有找到则抛出异常
- }
- namespace{
- HitMap* initializeCollisionMap(){ //创建并初始化虚函数表
- HitMap *phm=new HitMap; //创建函数表
- //初始化函数表
- (*phm)[StringPair(typeid(SpaceShip).name(),
- typeid(SpaceStation).name())]=&shipStation;
- (*phm)[StringPair(typeid(SpaceShip).name(),
- typeid(Asteroid).name())]=&shipAsteroid;
- (*phm)[StringPair(typeid(SpaceStation).name(),
- typeid(Asteroid).name())]=&shipAsteroid;
- //要包含所有的碰撞函数
- //...
- (*phm)[StringPair(typeid(Asteroid).name(),
- typeid(SpaceStation).name())]=&asteroidStation;
- return phm;
- }
- }
- namespace{
- //根据参数类型名在函数表中查找需要的碰撞函数
- HitFunctionPtr lookup(std::string const& class1,
- std::string const& class2){
- //用智能指针指向返回的函数表,为静态,表示只能有一个函数表
- static std::auto_ptr<HitMap> collisionMap(initializeCollisionMap());
- HitMap::iterator mapEntry=collisionMap->find(make_pair(class1,class2));
- if(mapEntry==collisionMap->end())
- return 0; //没找到,则返回空指针
- return (*mapEntry).second; //找到则返回关联的碰撞函数
- }
- }
- #endif
- //GameTest.cpp:对游戏框架的测试
- #include <iostream>
- #include "GameObject.hpp"
- #include "Collision.hpp"
- int main(){
- SpaceShip a;
- SpaceStation b;
- Asteroid c;
- processCollision(a,b);
- processCollision(a,c);
- processCollision(b,c);
- return 0;
- }
解释:
(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只能创建一个函数表。
- //CollisionMap.hpp:碰撞处理函数的映射表,实现了单例模式
- #ifndef COLLISION_MAP_HPP
- #define COLLISION_MAP_HPP
- #include <string>
- #include <utility> //用到了pair及auto_ptr
- #include <map>
- #include "GameObject.hpp"
- class CollisionMap{ //碰撞函数映射表
- public:
- typedef void (*HitFunctionPtr)(GameObject&,GameObject&); //指向碰撞函数的函数指针
- typedef std::pair<std::string,std::string> StringPair; //关联碰撞函数两个参数的动态类型
- typedef std::map<StringPair, HitFunctionPtr> HitMap; //函数表的类型
- //根据参数类型名称在函数映射表中查找需要的碰撞函数
- HitFunctionPtr lookup(std::string const& type1,
- std::string const& type2){
- HitMap::iterator mapEntry=collisionMap->find(make_pair(type1,type2));
- if(mapEntry==collisionMap->end())
- return 0; //没找到,则返回空指针
- return (*mapEntry).second; //找到则返回关联的碰撞函数
- }
- //根据参数类型名称向映射表中加入一个碰撞函数
- void addEntry(std::string const& type1,
- std::string const& type2,
- HitFunctionPtr collisionFunction){
- if(lookup(type1,type2)==0) //映射表中没找到时插入相应条目
- collisionMap->insert(make_pair(make_pair(type1,type2),collisionFunction));
- }
- //根据参数类型名称从映射表中删除一个碰撞函数
- void removeEntry(std::string const& type1,
- std::string const& type2){
- if(lookup(type1,type2)!=0) //若找到,则删除该条目
- collisionMap->erase(make_pair(type1,type2));
- }
- private:
- std::auto_ptr<HitMap> collisionMap; //函数映射表,用智能指针存储
- //构造函数声明为私有,以避免创建多个碰撞函数映射表
- CollisionMap() : collisionMap(new HitMap){
- }
- CollisionMap(CollisionMap const&); //不会调用,无需定义
- friend CollisionMap& theCollisionMap();
- };
- inline CollisionMap& theCollisionMap(){ //返回唯一的一个碰撞函数映射表
- static CollisionMap co;
- return co;
- }
- #endif
解释:
(1)CollisionMap的实现是很直接的,它维护一个collisionMap表来模拟虚函数表。碰撞函数的添加、删除、搜索都比较容易。theCollisionMap返回唯一的一个函数映射表。
(2)现在游戏开发者就不再需要initializeCollisionMap、lookup这样的函数了,直接用theCollisionMap()来动态地添加和删除碰撞函数,在processCollision直接用theCollisionMap()来搜索给定索引的碰撞函数即可。可见,这种模拟虚函数表的方法还可以推广到多重分派的情况。