《More Effective C++》学习笔记(五)——第一部分

技术    Techniques, Idioms, Patterns

条款 25:将constructor 和 non-merber functions 虚化

    当手上有一个对象的 指针 或 引用,而不知道该对象的真正的类型是什么的时候,会调用虚函数以完成“因类型而异的行为”,当未获得对象,但已经知道需要什么类型的时候,会调用构造函数以构造对象,那么virtual constructors(虚拟构造器),其实就是设计模式的工厂模式;

    virtual constructors能够根据输入的类型来创建出不同的对象(构造函数本身是不能进行虚化的,这里的constructors实际上是概念性的构造并不是指构造函数),关键是在类定义的返回当前类类型新对象的虚函数;

class Base{ ... };                //基类
class A : public Base{ ... };     //派生类
class B : public Base{ ... };     //派生类
class BaseCreator{
public:
    virtual Base* create(){ return new Base; }    //(创建)虚函数
};
class ACreator : pubcli BaseCreator{              //A类创建器
public:
    virtual Base* create(){ return new A; }
};
class BCreator : pubclc BaseCreator{              //B类创建器
public:
    virtual Base* create(){ return new B; }
};
Base* f(BaseCreator* bc){
    bc.create();                                 //基类指针指向的类型的创建器会返回创建器所对应的类的对象
}

    Virtual copy constructor是一种特别的 virtual constructor, virtual copy constructor 会返回一个指针,指向其调用者(某对象)的一个新副本,基于这种行为,  virtual copy constructor 通常以 copySelf 或 cloneSelf 命名;

class NLComponet{
public:
    virtual NLComponet * clone() const = 0;    //声明虚拟拷贝构造函数
    ...
};

class TextBlock: public NLComponet{
public:
    virtual TextBlock * clone() const{
        return new TextBlock(*this);            //返回当前对象的一个拷贝
    }
};

class Graphic: public NLComponet{
public:
    virtual Graphic * clone() const {
        return new Graphic(*this);             //返回当前对象的一个拷贝
    }
};

    当派生类重新定义其基类的一个虚函数时,不再需要得声明与原本相同的返回类型。如果基类函数返回类型是一个指向一个基类的指针(或引用),那么派生类的函数可以返回指向该基类的派生类的一个指针(或引用);

    将非成员函数进行虚化的技巧是:定义做实际工作的虚函数,然后使非成员函数调用对应的虚函数(流运算符不能重载为成员函数);

条款 26:限制某个class所能产生的对象的数量

    允许一个或零个对象

    "阻止某个类产出对象"的最简单的方法就是将其构造函数声明为private;

class Printer{
private:
    Pinter();                    //默认构造函数
    Printer(const Printer& rhs); //默认拷贝构造函数
    ...
};

    可以选择地解除这项约束,令类只存在一个对象;

class PrintJob;        //前置声明
class Printer{
public:
    void submitJob();
    void reset();
    void performSelfTest();
    ...
friend Printer& thePrinter();    //友元声明,使函数不受private 构造函数的访问约束(还可以声明为类内的静态成员函数,静态的原因是因为可以不通过对象直接调用)
private:
    Printer();
    Printer(const Printer& rhs);
    ...
};

Printer& thePrinter(){            //友元函数返回唯一的 Printer 对象
     static Printer p;            //静态变量
     return p;
}
class PrintJob{
public:
    PrintJob(const string& whatToPrint);    //隐式转换,支持string 到 PrintJob 的转换
    ...
};

string buffer;
...
thePrinter().reset();                  //重置
thePrinter().submitJob(buffer);        //提交打印作业,发生隐式转换

    第一:形成一个Printer对象,是函数中的static对象而非类中的static对象,“类中拥有一个static对象”的意思是,即使未使用到,它也会被构造(及析构),相反地,“函数内拥有一个static对象”的意思是,此对象在函数第一次被调用时才产生,如果该函数从未被调用,这个对象也就绝不会诞生(编译器也需要插入代码检查函数每次调用时对象是否需要诞生);

    第二:可以确切知道一个函数static的初始化时机:在函数第一次被调用,并且在该static定义处,至于一个类static(或全局static),则不一定在什么时候初始化,c++对“编译单元内的statics”的初始化顺序是提出一定保证的,但对于“不同编译单元内的statics”的初始化顺序则是没有任何说明;

    或者用另外一种做法实现对象的限制,利用类静态变量记录类的对象的个数,并限制类的对象的个数;

class Printer(){
public:
    class TooManyObjects{ };                // 产生超过限定数量时抛出的异常
    Printer* makePrinter() const;           // 使用代理构造函数创建对象
    Printer* makePrinter(const Printer& rhs) const;
    ~Printer();
private:
    static size_t numObjects = 0;                         // 对象数量记录值
    static size_t maxObjectsNum = MAX_OBJECT_NUM;         // 最大对象数量
    Printer();                                            // private 构造函数限制产出对象
    Printer(const Printer& rhs);                          // private 拷贝构造函数
};
auto_ptr Printer::makePrinter() const{        // 代理构造函数,返回的指针用智能指针确保资源不会泄露
     return new Printer;
}
auto_ptr Printer::makePrinter(const Printer& rhs) const{   // 代理拷贝构造函数,对象数量为1的情况下,其实不应该存在的
    return new Printer(rhs);
}
Printer::Printer(){
    if(numObjects >= maxObjectsNum)    // 对象数量超过限制则抛出异常
        throw TooManyObjects();
    ...                                // 构造过程
    ++numObjects;
}
Printer::Printer(const Printer& rhs){
    if(numObjects >= maxObjectsNum)                    
        throw TooManyObjects();
    ...                                // 拷贝构造过程
    ++numObjects;                    
}
Printer::~Printer(){                   //析构函数,类对象数量减一
    --numObjects;
    ...
}

    使用pseudo的原因:类对象可于三种不同的状态下生存:(1)单独存在(声明对象),(2)派生类对象的“基类成分”,(3)内嵌于较大的对象中,后两种情况的存在把“追踪目前存在的对象的个数”的意义严重弄混了,所以“目前存在的对象个数”和编译器理解的并不一样;

    用来计算对象个数的Base Class(基类)

template                           //使用模板是为了区分不同的类使用此计数器不会被混淆(模板进一步描述了计数器的类型,使每个类有自己的计数器)
class Counted{
public:
    class TooManyObjects();                 // 对象数量超过限定值则抛出此异常
    static int objectCount(){ return numObjects; }
protected:
    Counted();
    Counted(const Counted& rhs);
    ~Counted(){ --numObjects; }
private:
    static size_t numObjects = 0;                // 对象数量记录值
    static const size_t maxObjects = MAXOBJECTS; // 对象最大数量记录值
    void init();                                 // 用以避免ctor码重复出现
};

template
Counted::Counted(){ init();}
template
Counted::Counted(const Counted& rhs){ init(); }

template
void Counted::init(){
    if(numObjects >= maxObjects)
        throw TooManyObjects();
    ++numObjects;
}

    如果用以上基类来记录类的对象个数量的话,在对象创建时(使用代理构造函数或代理拷贝构造函数创建时),基类部分的创建时(无论是拷贝创建或独立创建),会检查类对象的数量,因为在派生类构造开始之前,总是有个基类的构造先开始调用;

class Printer: private Counted{
public:
    static auto_ptr makePrinter();
    static auto_ptr makePrinter(const Printer& rhs);
    ~Printer();
    ...
    using Counted::objectCount;                //使原本privae继承的objectCount变得可见
    using Counted::TooManyObjects;             //同上
private:
    Printer();
    Printer(const Printer& rhs);
};

条款27:要求(或禁止)对象产生于heap中

   要求对象产生于heap中(Heap-Based Objects)

    要阻止clients(客端)不得使用new以外的方法产生对象,因为non-heap objects 会在其定义点自动构造,并在其生存期结束时自动析构,所以,可以令被隐式调用的构造动作或析构的动作不合法(使得在栈内存的构造或析构动作不合法),在编译器期报错;

    方法一:使析构函数声明为private:

class UPNumber{
public:
    UPNumber();
    UPNumber(int initValue);
    UPNumber(double initValue);
    UPNumber(const UPNumber& rhs);
    ...
    void destroy() const { delete this; }    // 代理析构函数
private:
    ~UPNumber();                   // private域内的析构函数,无法被类以外的访问
};
UPNumber n;                        // 错误,不合法
UPNumber *p = new UPNumber;        // 正确
...
delete p;                          // 错误,delete无法调用private函数
p->delete();                       // 正确

    方法二:将所有的构造函数都声明为private:这种方法缺点很明显,类常常有大量的构造函数,其中包括默认构造函数,编译产生的构造函数都为public,所以只能手动进行声明,而且还得配置相应的代理构造函数;

    但是,这两种方法把析构函数或构造函数都声明为private会妨碍了继承(inheritance)和内含(containment)

class UPNumber{ ... };        // 构造或析构函数声明为private
class NonNegativeUPNumber: public UPNumber{ ... };    // 继承错误
class Asset{                                          // 内含错误
private:
    UPNumber value;
    ...
};

    令类的析构函数成为protected(与派生类共享但是不能被公共访问的)(并保持构造函数为public),便可解决继承问题,至于内含的情况,可以修改为“内含一个指针,指向UPNumber对象”;

class UPNumber{ ... };
class NonNegativeUPNumber : public UPNumber{ ... };    // 派生类可以取用protected 成员
class Asset {
public:
    Asset(int initValue);
    ~Asset();
private
    UPNumber *value;
};
Asset::Asset(int initValue)
: value(new UPNumber(initValue))
{ ... }
Asset::~Asset()
{ value->destroy();}        //析构时调用代理析构函数    

    判断某个对象是否位于heap内

bool onHeap(const void *address){
    char onTheStack;
    return address < &onTheStack;
}
    在函数里面,onTheStack被置于stack内,当onHeap被调用,其stack frame 会被放在stack的顶端,而此时stack是向低地址成长,所以,onTheStack的地址一定比其他任何一个位于stack中的变量更低,因此,如果参数address比onTheStack的地址更低,就不可能位于stack内(这么做是基于系统的一个事实,程序的地址空间以线性序列组织而成,其中stack(栈)高地址往低地址成长,heap(堆)从低地址往高地址成长),但是如果把static对象(包括全局作用域和命名空间内的对象)加进来那就没办法分辨了;
class HeapTracked{
public;
    class MissingAddress{};            //异常类
    virtual ~HeapTracked() = 0;        //纯虚函数
    static void * operator new(size_t size);
    static void * operator delete(void *ptr);
    bool isOnHeap() const;
private:
    typedef const void* RawAddress;
    static list addresses;    // 记录new申请指针的链表
};

HeapTracked::~HeapTracked(){}

void * HeapTracked::operator new(size_t size){    // 重定义的 operator new
    void *memPtr = ::operaotr(siez);              // 调用全局的 operator new
    addresses.push_front(memPtr);
    return memPtr;
}

void HeapTracked::operator delete(void *ptr){      // 重定义的 operaotr delete
    list::iterator it = find(addresses.begin(), addresses.end(), ptr);
    if(it != addresses.end()){
        addresses.earse(it);
        ::operator delete(ptr);                    // 调用全局的 operator delete
    }else{
        throw MissingAddress();
    }
}

bool HeapTracked:: isOnHeap() const{                             // 判断当前对象的内存是否由new申请的
    const void *rawAddress = dynamic_cast(this);    // 令指针指向“原指针所指对象”的内存起始处(多重继承或虚基类的情况,对象的指针(引用)会有多个地址)
    list::iterator it = find(addresses.begin(), address,end(), rawAddress);
    return it!=addresses.end();
}
    C++以abstract mixin base class(抽象混合基类)(abstract class 是一个不能被实例化的基类,而mix-in类则提供一组定义完好的能力,能够让其派生类提供某种功能),可以让派生类获得判断类的对象是否由new得到的功能,HeapTracked定义析构纯虚函数成为抽象基类(实际上的意味是:该类不能单独存在,所以不能产生对象,因为单独存在是没有意义的),而在类内对全局的operator new 和 operator delete 进行了重定义,令继承于该类的类的operator new 和 operator delete具有定制的运行方法(该类的定制行为就是对申请到的指针进行记录,对析构的指针进行消除记录);

    禁止对象产生于heap之中

    如果要阻止client(客端)直接将对象实例化在heap中,那么可以使得client无法调用new,虽然不能影响new opearator的能力,但是可以利用一个事实:new opearator 总是调用operator new (见条款8),后者是可以自行声明的,所以可以声明为private;

class UPNumber{
private:
    static void * opaertor new(size_t size);
    static void * operator delete(void *ptr);
    ...
};
UPNumber n1;        //正确
static UPNumber n2; //正确
UPNumber *n = new UPNumber;  //错误,无法访问private域的new

    如果也想禁止“由UPNumber对象组成的数组”位于heap内,可以将operator new[] 和 operator delete[] 也声明为private;

    将opeartor new声明为private,同时也会阻止UPNumber对象被实例化为heap-based 派生类对象的基类部分,因为 opertaor new 和operator delete 都会被继承,如果这些函数不在派生类内声明为public,派生类继承的就是其基类所声明的private版本,但是如果派生类定义了自己的operator new 且为public ,那么对象就能声明在heap上了(UPNumber作为基类实例化在heap就不能阻止了);

    同样,在内含的情况(UPNumber作为类成员的一部分),“UPNumber的operator new 仍为 private“这一事实并不会带来任何影响;    

class Asset{
public:
    Asset(int initValue);
    ...
private:
    UPNumber value;
};
Asset *pa = new Asset(100);    //没问题,因为调用的是Asset::operator new或::operator new,并非UPNumber::operator new

条款28:Smart Pointers(智能指针)

    SmartPointers像内建指针一样,所以必须有“强烈的类型性”(strongly typed),可以利用template参数表明其所指对象的类型;
template
class SmartPtr{
public:
    SmartPtr(T* realPtr= 0);
    SmartPtr(const Smart& rhs);
    ~SmartPtr();
    SmartPtr& operator=(const Smart& rhs);
    T* operator->() const;            // 解引用并获得其所指之物的一个成员
    T& operator*() const;             // 解引用
private:    
    T *ptr;                           //dumb pointer 实际进行指向的指针
};

    SmartPointers 的构造,赋值,析构

    智能指针的拷贝构造函数,赋值运算符,和析构函数的实现会因为“拥有权”的观念而变得复杂,如果一个智能指针拥有它所指向的对象,那么它就有责任在本身即将销毁时删除该对象,C++标准库的auto_ptr template中的“拥有权”则是“同一个对象只能被一个auto_ptr拥有”,当auto_ptr被复制或被赋值,其“对象拥有权”会转移;
template
class MyAutoPtr
{
public:
	MyAutoPtr(T* realPointer = 0);
	MyAutoPtr( MyAutoPtr& rhs);
	MyAutoPtr& operator=( MyAutoPtr& rhs);
	T& operator*()const;
	T* operator->() const;
	~MyAutoPtr();
private:
	T *pointee;
};
template
MyAutoPtr::MyAutoPtr(MyAutoPtr& rhs){
	pointee = rhs.pointee;            // 获得自变量的对象的拥有权
	rhs.pointee = 0;                  // 自变量放弃对对象的拥有权
}

template
MyAutoPtr& MyAutoPtr::operator=(MyAutoPtr& rhs){
	if (this == rhs)                  // 处理自赋值情况
		return *this;
	delete pointee;                   // 放弃拥有权并销毁指向的对象
	pointee = rhs.pointee;            // 获得自变量指向的对象的拥有权
	rhs.pointee = 0;                  
	return *this;
}

template
MyAutoPtr::~MyAutoPtr(){
	delete pointee;                   
}

template
T& MyAutoPtr::operator*()const{        // 实现解引用*
	return *pointee;
}

template
T* MyAutoPtr::operator->()const{        // 实现解引用->
	return pointee;
}

    实现Dereferencing Operators(解引操作符)

tempalte
T& MyAutoPtr::operator*()cosnt{
    ...
    return *pointee;
}

    返回值是一个引用类型,因为如果返回的是对象,返回的类型不一定和指针指向的对象的类型是一致的(因为,T类型的指针可以指向T类型以及其派生类类型的对象),返回的过程应存在切割(slicing)的问题;

MyAutoPtr n(new string("str"));
n->clear();

    其中的n->clear()会被解释为(n.opeartor->( ))->clear(),所以返回值是一个指针;

    测试SmartPointer是否为Null

MyAutoPtr ptn;
...
if(ptn == 0) ...      // 错误
if(ptn) ...           // 错误
if(!ptn) ...          // 错误
    可以对类进行生存,复制,销毁,解引用,但是没办法和原生指针一样就是判断是否为null;
tempalte
class MyAutoPtr{
public:
    ...
    operator void* ();        //如果dumb ptr 是null,返回零,否则返回非零数
    ...
};
    可以提供一个隐式类型转换操作符,允许以上的动作得以编译通过,这类转换的传统目标是void*(Null 其实是((void*)0));
    
MyAutoPtr s;
MyAutoPtr n;
...
if(n == s) ...     // 允许通过编译,因为会被转换为同一类型
    并不存在相应的operator==函数时,但是由于存在两个对象都能被隐式转换为void*,所以到处错误得以通过编译,这种行为使得隐式转换非常的危险 (参考条款5);
tempalte
class MyAutoPtr{
public:
    ...
    bool operator!();        // 只有是null时才返回true
    ...
};
MyAutoPtr s;         
MyAutoPtr n;
if(!s) ... if(!n) ...        //  正常通过编译
if(!n == !s) ...             //  这种使用情况比较少见,但是依然是错误的

    可以声明!操作符函数令正确编译得以通过,但是这正确性依赖于一种不常用的写法(如果这种所谓不常用的写法写出来了那依然还是会出现意料之中的错误);

    将Smart Pointer 转换为 Dumb Pointers

template
class MyAutoPtr{
    ...
    operator T*() { return pointee; }    // 添加隐式转换   
    ...
};
void f(string s&);
MyAutoPtr ptr;
f(ptr);             // 没问题,可以隐式转换为T类型
...
if(ptr == 0) ...    // nullness测试也没有问题
if(ptr) ...         // 同上
if(!ptr) ...        // 同上

    允许这种转换,会带来一定问题,这种转换可以使客端(client)得以直接对dumb pointer做动作(失去了封装性);

void f(MyAutoPtr& pt){
     string* s = pt;                // 隐式转换,将dumb指针直接暴露出来了
     use s to modify the tuple;
}

    这种转换也并没有起到方便的作用,在某些原本对T类型存在入参隐式转换进行调用的函数,也并不能通过这个隐式转换起作用(因为两次的隐式转换是被C++禁止的,一次从智能指针转换到T类型,一次从T类型转换到参数调用类型),而且还会发生严重的错误,所以,不要提供对dumb pointers的隐式转换操作符;

MyAutoPtr s = new string("str");
...
delete s;        // 原本这不应该通过编译的,但是存在隐式转换所以调用成功了,但是会发生对dumb pointer两次的delete动作(这是一次,还有一次是对象s析构时)

    Smart Pointers 和 “与继承有关的”类型转换

    假设存在以下的继承体系:

《More Effective C++》学习笔记(五)——第一部分_第1张图片

void displayAndPlay(const MusicProduct* pmp, int numTimes);    // 函数接受基类指针类型的自变量
Cassette *funMusic = new Cassette("tittle");
CD *nightmareMusic = new CD("tittle");
displayAndPlay(funcMusic, 10);            // 正常调用,发生派生类到基类的转换
dispalyAndPlay(nightmarMusic, 0);         // 正常调用,发生派生类到基类的转换
void displayAndPlay(const MyAutoPtr& pmp, int numTimes);    // 函数接受基类智能指针类型的自变量
MyAutoPtr Music_Ca(new Cassette("tittle"));    // 智能指针对象
MyAutoPtr Music_CD(new CD("tilttle"));               // 智能指针对象
dispalyAndPlay(Music_Ca, 10);             // 错误
displayAndPlay(nightmareMusic, 0);        // 错误

    之所以错误,是因为没办法将MyAutoPtr 和 MyAutoPtr转换为MyAutoPtr,这是智能指针和指针的区别之一,智能指针无法得知自己哑指针指向的对象的继承体系方面的信息(无法执行编译器支持的类继承体系内的转换),但是可以在模板类定义时提供相应的类型转换:

    第一种:提供相应的隐式转换

class MyAutoPtr{
public:
    operator MyAutoPtr();
    { return MyAutoPtr(pointee);}
    ...
private:
    Cassette *pointee;
}
class MyAutoPtr{
public:
    operaotr MyAutoPtr()
    { return MyAutoPtr<(pointee); }
private:
    CD *pointee;
};

    这种做法有两个缺点:第一,必须为每个智能指针模板类的实例加入特有的隐式转换,第二,如果是一个继承层数比较多的类的话,必须为对象直接继承或间接继承的每一个基类提供一个转换操作符;

    第二种:使用新的特性,将nonvirtual member function 声明为 templates:

template
class MyAutoPtr{
public:
    MyAutoPtr(T* realPtr = 0);
    MyAutoPtr(MyAutoPtr& rhs);
    T* operator->() const;
    T& operaotr&() const;
    template
    operaotr MyAutoPtr(){
        return MyAutoPtr(pointee);
    }
private;
    T *pointee;
};

    假设编译器有个smart pointer-to-T(指向派生类的智能指针)对象,而需要把此对象转换为smart pointer-to-base-calss-of-T(指向基类的智能指针)对象,编译器先在基类智能指针(MyAutoPtr<"BaseClass">)定义内存在是否存在一个人“单一自变量”且其自变量类型为MyAutoPtr构造函数参见条款5),然后检查类内部对派生类智能指针(MyAutoPtr)的定义,看看其中是否声明有必要的转换操作符方法一),接下来,编译器再试图寻找一个“可实例化以导出合宜之转换函数”的成员函数模板,然后在MyAutoPtr定义内找到了,当函数被实例化时并令newType绑定到基类类型时,产生了所需的函数,于是编译器将函数实例化,导出以下代码:

MyAutoPtr:: operator MyAutoPtr(){
    return MyAutoPtr(pointee);
}

    不只可以用于继承体系的向上转换,还能用于指针类型之间的任何合法隐式类型转换,但是这个方法不能用于继承层次较多的时候某种情况;

《More Effective C++》学习笔记(五)——第一部分_第2张图片

void displayAndPlay(const MyAutoPtr& pmp, int howMany);
void displayAndPlay(const MyAutoPtr& pc ,int howMany);
MyAutoPtr dumbMusic(new CasSingle("tillte"));
displayAndPlay(dumbMusic, 1);            // 错误

    displayAndPlay被重载,函数接受MyAutoPtr或MyAutoPtr,预期调用的是MyAutoPtr版本的,但是,对于c++来说,对任何转换函数的调用动作哦,都是一样“好”的,于是displayAndPlay成为一种模棱两可的行为;

    Smart Pointers 与 const

    赋予智能指针区分指向对象具有const 或 non-const的能力,以及在non-const到const之间的转换;
CD goodCD("Flood");
const AutoPtr p = &goodCD;            // 常量智能指针
MyAutoPtr p = &goodCD;          // 指向常量的智能指针
const MyAutoPtr p = &goodCD;    // 指向常量的常量智能指针
CD *pCD = new CD("Movie");
const CD * PConstCD = pCD;                // 可以,发生 non-const 转换到 const
MyAutoPtr pCD = new CD("Movie");      
MyAutoPtr PConstCD = pCD;       // 错误,不存在 MyAutoPtr 到 MyAutoPtr

    如果要赋予智能指针类似于原生指针的(non-const 到 const )转换,可以存在多种方法;

    第一种: 使用成员模板函数( member funciton template ),产生相应的转换函数;

    第二种: 从non-const 到 const 是安全的,从 const 到 non-const 是不安全,这是类似于继承体系(派生类到基类的继承体系的转换),实现智能指针时可以利用这种性质,令每一个指向T类指针公开继承一个对应的指向常量T类智能指针,至于盲指针的实现可以使用union来(因为const智能指针实现中会使用到const T*, non-const智能指针会使用到T*)来节省空间的浪费;

template
class MyAutoPtrToConst{
    ...
functions
protected:                            // 取消 private 域得以让派生类对union可见 
    union{
        const T* constPointee;        // 该类使用到的成员
        T* pointee;                   // 派生类下使用的成员
    };
};
template
class MyAutoPtr : public MyAutoPtrToConst{
    ...
};

    缺点是:类的成语函数都要使用适当的成员;

你可能感兴趣的:(学习笔记)