More Effective C++ 第五部分 技术

来源:互联网 发布:linux创建新文件 编辑:程序博客网 时间:2024/05/24 05:05


本部分内容比较高深,涉及很多较高端的技术。笔记会有许多不足的地方,若复习可以在看一遍书。只看笔记恐怕无法彻底理解。

25.将constructor和non-member function虚化

virtual constructor

virtual constructor是并非是constructor,其根据输入可产生不同类型的对象(以多态的方式返回),常用的使用情况如在磁盘读取数据,产生一个或为音乐,或为视频的对象。

virtual copy constructor

virtual copy constructor 返回一个指针,指向其调用者的一个新副本,通常命名为colne().使用virtual copy constructor可以用一个基类指针调用clone()得到一个派生类的返回值。
virtual函数返回类型可与基类中虚函数的类型不一致。

#include <iostream>#include <vector>class Base{public:    virtual Base* clone() const = 0;};class Derived1: public Base{public:    //virtual copy constructor返回类型不为基类类型    virtual Derived1* clone() const{        return new Derived1(*this);    }    int a = 1;};class Derived2: public Base{public:    virtual Derived2* clone() const{        return new Derived2(*this);    }    int b = 2;};int main(int argc, const char * argv[]) {    std::vector<Base*> v;    v.push_back(new Derived1());    v.push_back(new Derived2());    std::vector<Base*> v2;    for (auto i = v.begin(); i!=v.end(); i++) {        v2.push_back((*i)->clone());//调用virtual copy constructor     }    //此时v2第一个内容应该是derived1类型的,第二个是derived2类型的    printf("%d %d",           dynamic_cast<Derived1*>(*(v2.begin()))->a,           dynamic_cast<Derived2*>(*(v2.begin()+1))->b           );    return 0;}输出:1 2Program ended with exit code: 0

将non-member function的行为虚化

指在一个non-member function中调用一个虚函数。不做其他的事情。出现情况例如下边的例子:
若使用虚函数operator<<将会导致t << cout;这样的奇怪写法,因为operator<<接受一个类型为ostream&的右边的值。而不能接受左边的值。

class NLComponent {public:    // 对输出操作符的不寻常的声明    virtual ostream& operator<<(ostream& str) const = 0; ...};class TextBlock: public NLComponent {public:    // 虚拟输出操作符(同样不寻常)    virtual ostream& operator<<(ostream& str) const;};class Graphic: public NLComponent {public:    // 虚拟输出操作符     virtual ostream& operator<<(ostream& str) const;};TextBlock t;Graphic g;...t << cout;//不能写cout<<t,因为类内声明的operator<<接受一个右边的ostream&g << cout;

其解决办法是

class NLComponent {public:    virtual ostream& print(ostream& s) const = 0;    };class TextBlock: public NLComponent {public:    virtual ostream& print(ostream& s) const;};class Graphic: public NLComponent {public:    virtual ostream& print(ostream& s) const;    };//虚化的non-member functioninlineostream& operator<<(ostream& s, const NLComponent& c){    //仅调用虚函数    return c.print(s);}    TextBlock t;    Graphic g;    cout << t;    cout << g;//可以使用正常的写法    return 0;

26.限制某个class所能产生的对象数量

阻止产出对象

将各constructor声明为private

仅产生一个对象

使用单例模式,但书中提出一个观点是将static对象声明在function内。

class Printer{public:    static Printer& thePrinter();private:    Printer();    Printer(const Printer&);};Printer& Printer::thePrinter(){    static Printer p;//使用Function static    return p;}

使用Function static的好处是:
1.Function static在函数调用时才会构造对象,而class static则在程序开始时被创建。也就意味着即使没有使用到也会被创建。
2.C++对不同编译单元内的static对象初始化顺序不提供任何说明,使用class static可能会导致一些问题。

使用Function static不可将其inline,因为目标代码可能会复制多份local static对象。

不同的对象构造状态

在三种情况下会调用构造函数:
1.正常的构造。
2.派生类的base class部分
3.内嵌于其他对象内。
在控制对象产生的数量时应注意。

限制对象数量的方法

在class内声明一个static成员,用来指定生成对象最多的数量,在产生对象的函数内进行判断,若超过最大限制,则抛出异常:例如tooManyObj.
我们可以将这部分功能分离出来形成一个base class,将其template化,在使用的时候使用private继承,利用base class的构造函数在derived class构造函数内被调用的特性实现自动的管理。实现如下:

template<class BeingCounted> class Counted { public:     class TooManyObjects{};    //用来抛出异常                    static int objectCount() { return numObjects; } protected:    Counted();     Counted(const Counted& rhs);      ~Counted(){ --numObjects;}private:    static int numObjects;     static const size_t maxObjects;      void init();                   // 避免构造函数的代码重复 };template<class BeingCounted> Counted<BeingCounted>::Counted(){ init(); }template<class BeingCounted> Counted<BeingCounted>::Counted(const Counted<BeingCounted>&){ init(); }template<class BeingCounted> void Counted<BeingCounted>::init(){     if(numbObjects >= maxObjects) throw TooManyObjects();    ++numObjects;}//初始化static对象template<class BeingCounted> int Counted<BeingCounted>::numObjects;

使用方法:

class Printer: private Counted<Printer>{public://若Printer类的用户想知道当前有多少个对象,可以把让objectCount成为public的。    using Counted<Printer>::objectCount;};//初始化static对象const size_t Counted<Printer>::maxObjects = 10;

27.要求(或禁止)对象产生于heap之中

要求对象产生于heap之中

产生于heap 对象必须使用new.
限制constructors和destructor其中之一为非public即可,选用限制destructor的原因是一个class有多个constructor,若由编译器合成便为public,所以我们选用限制destructor。将destructor声明为protected而非private的原因是在继承和内涵的情况下,不可以声明private。若要调用destructor需要一个member function间接调用。

class UPNumber{public:    void destory() const{ delete this; }protected:    ~UPNumber();};

若以产生非heap对象将会在自动析构时报错,因为无法访问destructor.

阻止对象产生于heap之中

使用new产生的对象需要调用operator new,可以把operator new声明为private以阻止。

class UPNumber{private:    static void* operator new(size_t size);    static void operator delete(void* ptr);//最好声明为与new同一访问层级.    //还可以声明operator new[]};

这样继承自该类的对象也无法产生heap内的对象,但内含该class的可以,因为其调用的是自身的operator new.


28.智能指针

智能指针包含一个普通指针数据,通过构造析构重载操作符等方法实现“智能”并且与原指针使用方法类似。

实现解引操作符

解引操作符包含operator*和operator->
operator*需要返回一个引用。若返回T类型对象,若装入的是T类型的派生类则会发生切割问题。

template<class T>T& SmarePtr<T>::operator*() const{    ....    return *pointee;//pointee是原始指针}

operator->返回一个普通指针即可,因为

//pt是智能指针,重载了operator->pt->fun();//会变为(pt.operator->())->fun();

所以operator->实现如下

template<class T>T* SmarePtr<T>::operator->() const{    ....    return pointee;//pointee是原始指针}

测试智能指针是否为空

希望可以使用传统指针判空的方式:

if(pt == 0);if(pt);if(!pt);

采用的办法是重载operator void*(),提供隐式转换为void指针

template<class T>class SmartPtr{public:    operator void*();//如果普通指针为空返回0,否则返回非零值。};

其缺点是可以比较两个不同类型的智能指针。因为都将其转为void*.

不要提供对普通指针的隐式转换

提供对普通指针的隐式转换将会导致可以轻易的对普通指针操作,从而回避了智能指针的设计目的。

智能指针与“与继承有关的”类型转换

使用template function实现

template<class newType>operator SmartPtr<newType>(){    return SmartPtr<newType>(pointee);}

该方法的缺点是若有一个函数接收基类的智能指针,还有一个重载版本,接收派生类的智能指针。将会导致二义性,不能通过编译。


29.引用计数Reference counting

引用计数带来两个好处:
1.实现垃圾回收机制
2.使等值对象共享一份实值

等值对象共享一份实值

使等值对象共享一份实质,需要记录被共享的实值被共享的次数。
做法是使用一个struct将引用计数与实值关联起来

class String {public:    String(const char *initValue = "")    : value(new StringValue(initValue))    {}    String(const String& rhs)    :value(rhs.value)    {        ++value->refCount;    }    ~String()    {        if (--value->refCount == 0) delete value;    }    String& operator=(const String& rhs)    {        if (value == rhs.value) {            return *this;        }        if (--value->refCount == 0) {        delete value;        }        value = rhs.value;        ++value->refCount;        return *this;    }    //const string的operator[]假设只读,详细见下章    const char& operator[](int index) const    {       return value->data[index];    }    //非const string的operator[]可读可写    char& operator[](int index)    {          if (value->refCount > 1) {            --value->refCount;//减少引用1个            value = new StringValue(value->data);//创建一个新的stringvalue        }        return value->data[index];    }private:    struct StringValue {        int refCount;        char *data;        StringValue(const char *initValue);        ~StringValue();    };    StringValue *value;};String::StringValue::StringValue(const char *initValue):refCount(1){    data = new char[strlen(initValue) + 1];    strcpy(data, initValue);}String::StringValue::~StringValue(){    delete [] data;}

这个做法有一个缺点:

    string s = "fuck";    char* p = &s[1];    auto s2 = s;//s2和s共享一个实值    *p = 'b';//检测不到实值被改变。s2也被改变

解决方法是使用一在StringValue内设置一个shareable标志,一旦non-const operator[]作用于对象值身上便将此标志设为false,并且不可改变。所有其他的member function都应检查sharedable。实现将在下面的基类给出。

引用计数基类

class RCObject{public:    RCObject();    RCObject(const RCObject& rhs);    RCObject& operator=(const RCObject& rhs);    virtual ~RCObject() = 0;//设计为base class的析构函数为virtual    void addReference();    void removeReference();    void markUnshareable();    bool isShareable() const;    bool isShared() const;private:    int refCount;    bool shareable;};RCObject::RCObject(): refCount(0), shareable(true) {}//当RCObject被复制的时候会产生一个新的实值RCObject::RCObject(const RCObject&): refCount(0), shareable(true) {}RCObject& RCObject::operator=(const RCObject&){ return *this; }RCObject::~RCObject() {}void RCObject::addReference() { ++refCount; }void RCObject::removeReference(){   if (--refCount == 0) delete this; }void RCObject::markUnshareable(){ shareable = false; }bool RCObject::isShareable() const{ return shareable; }bool RCObject::isShared() const{ return refCount > 1; }class String {private:    struct StringValue: public RCObject {    char *data;    StringValue(const char *initValue);    StringValue();    };     ...};

自动引用计数

各对象通过一个指针共享一个实值,我们可以修改指针(28章)以侦测指针的构造copy等操作,来自动设置共享了实值的对象。
被共享的对象生成在heap内,需要使用27章的方法强制其生成在heap中。

整合

将所有部分整合在一起,结果如图:
这里写图片描述
各部分详细代码和整合代码参考书203-209页

使用这种技术会导致代码维护难度极大的增加,应确保这样做可以大幅优化程序性能才使用这种方法。


30.替身类、代理类 Proxy classes

Proxy classes用来代表一个观念上并不存在的类。可以完成一些十分困难的任务,但需要从与真实的对象合作,转变到与替身对象合作,其行为有些差异。
例如希望实现一个二维数组,可通过A[9][8]这样的方式访问。但是并没有operator[][]。所以我们建立一个替身类,二维数组重载operator[],返回该替身类的对象,而该替身类也重载operator[],返回在二维数字内的真正的值。这样operator[][]便看起来的合法的。

Proxy classes区分operator[]左值运用和右值运用

在上一章我们假设const char& operator[](int index) const只做读操作,在这章我们用Proxy classes解决区分左值运用和右值运用。
我们令string的operator[]返回一个proxy class对象,然后等待其被运用,在确定operator[]是读还是写操作。若对proxy object做赋值动作将会是写操作,其余为读操作。

class String {public:    class CharProxy {    public:        CharProxy(String& str, int index);        CharProxy& operator=(const CharProxy& rhs);//左值运用        CharProxy& operator=(char c);//左值运用        operator char() const;//右值运用    private:        String& theString;        int charIndex;    };    const CharProxy operator[](int index) const;   // 返回替身对象    CharProxy operator[](int index);  // 返回替身对象    friend class CharProxy;private:    RCPtr<StringValue> value;};

考虑以下调用方式

String s1,s2;cout<<s1[5];//s1[5]产生一个CharProxy对象,但该对象并无<<操作符,//但是可以提供到char的隐式转换,所有可通过该隐式转换判定为右值运用s2[5] = 'x';//s2[5]产生一个CharProxy对象,调用operator=,可判定为左值运用

限制

1.在判断左值与右值引用时,可能不止一种赋值方式,可以使用+=,-=,++……这意味着若希望proxy class能和返回其对象的类合作,便要支持这些operator。

2.通过proxy class调用原对象member function将会失败,这将导致下边这样的代码报错。除非重载。

s[4].memberFun();

3.无法返回reference to non-const objects对象。

void swap(char& a, char& b);String s = "+C+";swap(s[0], s[1]);

String::operator[]返回一个 CharProxy 对象,但 swap()函数要求它所参数是 char & 类型。一个 CharProxy 对象可以印式地转换为一个 char,但没有转换为 char &的转换函数。 而它可能转换成的 char 并不能成为 swap 的 char &参数,因为这个 char 是一个临时对象。

4.无法提供原对象的隐式类型转换。


31.让函数根据一个以上的对象类型来决定如何虚化

传统虚函数只依据一个对象的动态类型决定调用的函数是哪个,本章将以2或以上个参数的动态类型来决定调用哪个函数,也就是说根据1个以上的对象类型来决定如何虚化函数。
例如构造一个星体碰撞的程序,碰撞的结果由碰撞的两个对象的类型决定。

class GameObject{...};class SpaceShip: public  { ... }; class SpaceStation: public GameObject { ... }; class Asteroid: public GameObject { ... }; processCollision(GameObject& object1,GameObject& object2);//处理碰撞结果的函数

虚函数+运行期类型识别RTTI

在base class内定义虚函数,在派生类改写的虚函数内利用RTTI和if else判断两个object的类型

class GameObject { public:   virtual void collide(GameObject& otherObject) = 0; };void SpaceShip::collide(GameObject& otherObject) {     const type_info& objectType = typeid(otherObject);     if (objectType == typeid(SpaceShip)) {     SpaceShip& ss = static_cast<SpaceShip&>(otherObject);     //处理碰撞    }else if((objectType == typeid(SpaceStaion)){    }//下面省略    //若不是三种类型之一就抛出一个异常}

使用这种方法导致代码维护性增加,类之间的耦合性增大。

只用虚函数

做法是在base class内重载虚函数。

class GameObject { public:  virtual void collide(GameObject& otherObject);      virtual void collide(SpaceShip& otherObject);  vrtuall void collide(SpaceSttion& otherObject);  virtual void collide(Asteroid& otherObject);  ... }; //处理非三种类型的参数,不像第一种会抛出异常void SpaceShip::collide(GameObject& otherObject) {   otherObject.collide(*this); }

仿真虚函数表virtual function table

在class内声明一个函数指针,并实现一个函数lookup,其功能是根据不同的输入对象类型,使函数指针指向处理与该类型对象碰撞的函数。

class SpaceShip: pulic GameObject{private:     typedef void (SpaceShip::*HitFunctionPtr)(GameObject&);    typedef map<string,HitFunctionPtr> HitMap;//虚函数表的typedef    static HitFunctionPtr lookup(const GameObject& whatWeHit);    static HitMap initializeCollisionMap();//初始化map  //接受GameObject&类型参数。在每个函数的实现内将其dynamic——cast为正确的类型    virtual void hitSpaceShip(GameObject& spaceShip);    virtual void hitSpaceStation(GameObject& spaceStation);    virtual void hitAsteroid(GameObject& asteroid);};//base class定义的纯虚函数void SpaceShip::collide(GameObject& otherObject){     HitFunctionPtr hfp = lookup(otherObject);                    if (hfp) {                                (this->*hfp)(otherObject);            } else {     throw CollisionWithUnknownObject(otherObject);     } } SpaceShip::HitFunctionPtr SpaceShip::lookup(const GameObject& whatWeHit){    static auto_ptr<HitMap> collisionMap(initializeCollisionMap());//初始化,见下面    HitMap::iterator mapEntry = collisionMap.find(typeid(whatWeHit).name());    if(mapEntry == collisionMap.end()) return 0;    return (*mapEntry).second;}SpaceShip::HitMap * SpaceShip::initializeCollisionMap(){    HitMap *phm = new HitMap;    (*phm)["SpaceShip"] = &hitSpaceShip;    (*phm)["SpaceStation"] = &hitSpaceStation;    (*phm)["Asteroid"] = &hitAsteroid;    return phm;}

使用non-member函数

使用non-member函数的好处是当修改时(增加或删除等)不必修改类,直接修改本文件即可。

#include "SpaceShip.h"#include "SpaceStation.h"#include "Asteroid.h"namespace { // 使用你们namespace,使得其内的内容对编译单元为私有的    // 主要的碰撞处理函数    void shipAsteroid(GameObject& spaceShip,                      GameObject& asteroid);    void shipStation(GameObject& spaceShip,                     GameObject& spaceStation);    void asteroidStation(GameObject& asteroid,                         GameObject& spaceStation);    //次要的碰撞处理函数,只为了实现对称性,将参数位置对调。    void asteroidShip(GameObject& asteroid,                      GameObject& spaceShip)    { shipAsteroid(spaceShip, asteroid); }    void stationShip(GameObject& spaceStation,                     GameObject& spaceShip)    { shipStation(spaceShip, spaceStation); }    void stationAsteroid(GameObject& spaceStation,                         GameObject& asteroid)    { asteroidStation(asteroid, spaceStation); }    //HitFunctionPtr现在是指向non-member function的指针    typedef void (*HitFunctionPtr)(GameObject&, GameObject&);    //HitMap变为pair<string,string>, HitFunctionPtr型    typedef map< pair<string,string>, HitFunctionPtr > HitMap;    //可使用标准库make_pair代替    pair<string,string> makeStringPair(const char *s1,                                       const char *s2);    //需要修改以适应新HitMap类型,原理不变    HitMap * initializeCollisionMap();    //需要修改以适应新HitMap类型,原理不变    HitFunctionPtr lookup(const string& class1,                          const string& class2);} // end namespacevoid 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加上处理的函数,在 initializeCollisionMap()中添加对应的map即可。

缺点

上述的所有方法都是静态的,没有解耦合,若想增加或删除都需要更改已将写好的函数。使用动态方法的思路是:
我们可以将存储撞击函数的map放入一个类,并由它提供动态修改映射关系的成员函数。

0 0
原创粉丝点击