More Effective C++ 第五部分 技术

Posted zhangqixiang5449

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了More Effective C++ 第五部分 技术相关的知识,希望对你有一定的参考价值。


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

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 function
inline
ostream& 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 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加上处理的函数,在 initializeCollisionMap()中添加对应的map即可。

缺点

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

以上是关于More Effective C++ 第五部分 技术的主要内容,如果未能解决你的问题,请参考以下文章

《More Effective C++》总结笔记

More Effective C++ 第四部分 效率

More Effective C++ 第六部分 杂项讨论

《More Effective C++》总结笔记——异常

《More Effective C++》阅读笔记

More Effective C++ 第一部分 基础议题