《More Effective C++》读书笔记

一、基础议题(Basics)

1、仔细区别 pointers 和 references

当一定会指向某个对象,且不会改变指向时,就应该选择 references,其它任何时候,应该选择 pointers。 实现某一些操作符的时候,操作符由于语义要求使得指针不可行,这时就使用引用。

 

2、最好使用 C++ 转型操作符

为解决 C 旧式转型的缺点(允许将任何类型转为任何类型,且难以辨识),C++ 导入 4 个新的转型操作符(cast operators):

static_cast , const_cast , dynamic_cast , reinterpret_cast:分别是常规类型转换,去常量转换,继承转换,函数指针转换

使用方式都是形如: static_cast<type>(expression)  , 如: int d = static_cast<int>(3.14);

#include <iostream>
using namespace std;

struct B
{
    virtual void print(){}//想要使用 dynamic_cast ,基类中必须有虚函数
};
struct D : B
{
    void print(){}
};

int fun(){}

int main()
{
    int i = static_cast<int>(3.14); //i == 3

    const int j = 10;
    int *pj = const_cast<int*>(&j);
    //int *pj = (int*)(&j);     //等同于上面
    *pj = 20;
    //虽然 *pj的地址和 j 的地址是一样的,但是值却不一样。
    cout<<*pj<<endl;    //20
    cout<<j<<endl;      //10

    B *b;
    dynamic_cast<D*>(b);

    typedef void (*FunPtr)();
    reinterpret_cast<FunPtr>(&fun);     //尽量避免使用
}

const_cast :用于去除变量的const或者volatile属性。但目的绝不是为了修改 const 变量的内容,而是因为无奈,比如说有一个const的值,想代入一个参数未设为const的函数

synamic_cast:用来针对一个继承体系做向下的安全转换,目标类型必须为指针或者引用。基类中要有虚函数,否则会编译出错;static_cast则没有这个限制。原因是:存在虚函数,说明它有想要让基类指针或引用指向派生类对象的情况,此时转换才有意义。由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表中,只有定义了虚函数的类才有虚函数表。必须保证源类型跟目标类型本来就是一致的,否则返回 null 指针。这个函数使用的是RTTI机制,所以编译器必须打开这个选项才能编译。

reinterpret_cast: 不具有移植性,最常用的用途是转换函数指针类型,但是不建议使用它,除非迫不得已。

 

3、绝对不要以多态方式处理数组

#include <iostream>
using namespace std;

struct B
{
    virtual void print() const{cout<<"base print()"<<endl;}
};
struct D : B
{
    void print() const{cout<<"derived print()"<<endl;}
    int id;  //如果没有此句,执行将正确,因为基类对象和子类对象长度相同  
};

int fun(const B array[],int size)
{
    for(int i = 0;i<size;++i)
    {
        array[i].print();
    }
}

int main()
{
    B barray[5];
    fun(barray,5);
    D darray[5];
    fun(darray,5);
}

array[i] 其实是一个指针算术表达式的简写,它代表的其实是 *(array+i),array是一个指向数组起始处的指针。在 for 里遍历 array 时,必须要知道每个元素之间相差多少内存,而编译器则根据传入参数来计算得知为 sizeof(B),而如果传入的是派生类数组对象,它依然认为是 sizeof(B),除非正好派生类大小正好与基类相同,否则运行时会出现错误。但是如果我们设计软件的时候,不要让具体类继承具体类的话,就不太可能犯这种错误。(理由是,一个类的父类一般都会是一个抽象类,抽象类不存在数组)

 

4、避免无用的 default constructors

没有缺省构造函数造成的问题:通常不可能建立对象数组,对于使用非堆数组,可以在定义时提供必要的参数。另一种方法是使用指针数组,但是必须删除数组里的每个指针指向的对象,而且还增加了内存分配量。
提供无意义的缺省构造函数会影响类的工作效率,成员函数必须测试所有的部分是否都被正确的初始化。

 

二、操作符(Operators)

5、对定制的“类型转换函数”保持警觉

定义类似功能的函数,而抛弃隐式类型转换,使得类型转换必须显示调用。例如 String类没有定义对Char*的隐式转换,而是用c_str函数来实施这个转换。拥有单个参数(或除第一个参数外都有默认值的多参数)构造函数的类,很容易被隐式类型转换,最好加上 explicit 防止隐式类型转换。

 

6、区别 increment/decrement 操作符的前置和后置形式

#include <iostream>
using namespace std;

class A
{
    public:
        A(int i):id(i){}
        A& operator++()
        {
            this->id += 1;
            return *this;
        }
        //返回值为 const ,以避免 a++++这种形式
        //因为第二个 operator++ 所改变的对象是第一个 operator++ 返回的对象
        //最终结果其实也只是累加了一次,a++++ 也还是相当于 a++,这是违反直觉的
        const A operator++(int)
        {
            A a = *this;
            this->id += 1;
            return a;
        }
        int id;
};
int main()
{
    A a(3);
    cout<<++a.id<<endl; //++++a;   也是允许的,但 a++++ 不允许。
    cout<<a.id<<endl;
    cout<<a++.id<<endl;
    cout<<a.id<<endl;
}

后置operator++(int) 的叠加是不允许的,原因有两个:一是与内建类型行为不一致(内建类型支持前置叠加);二是其效果跟调用一次 operator++(int) 效果一样,这是违反直觉的。另外,后置式操作符使用 operator++(int),参数的唯一目的只是为了区别前置式和后置式而已,当函数被调用时,编译器传递一个0作为int参数的值传递给该函数

处置用户定制类型时,尽可能使用前置式,因为后置式会产生一个临时对象。

 

7、千万不要重载 &&, || 和 , 操作符

int *pi = NULL;
if(pi != 0 && cout<<*pi<<endl) { }

上面的代码不会报错,虽然 pi 是空指针,但 && 符号采用"骤死式"评估方式,如果 pi == 0 的话,不会执行后面的语句。

不要重载这些操作符,是因为我们无法控制表达式的求解优先级,不能真正模仿这些运算符。操作符重载的目的是使程序更容易阅读,书写和理解,而不是来迷惑其他人。如果没有一个好理由重载操作符,就不要重载。而对于&&,||和“,”,很难找到一个好理由。

 

8、了解各种不同意义的 new 和 delete

new 操作符的执行过程:
  (1). 调用operator new分配内存 ;  //这一步可以使用 operator new 或 placement new 重载。
  (2). 调用构造函数生成类对象;
  (3). 返回相应指针。

函数 operator new 通常声明如下: 

void * operator new(size_t size);  //第一个参数必须为 size_t,表示需要分配多少内存。

返回值为void型指针,表示这个指针指向的内存中的数据的类型要由用户来指定。比如内存分配函数malloc函数返回的指针就是void *型,用户在使用这个指针的时候,要进行强制类型转换,如(int *)malloc(1024)。任何类型的指针都可以直接赋给 void * 变量,而不必强制转换。如果函数的参数可以为任意类型的指针,则可以声明为 void * 了。

void 有两个地方可以使用,第一是函数返回值,第二是作为无参函数的参数。(因为在C语言中,可以给无参函数传任意类型的参数,而且C语言中,没有指定函数返回值时,默认返回为 int 值)

#include <iostream>

using namespace std;
class User
{
    public:
    void * operator new(size_t size)
    {
        std::cout<<"size: "<<size<<std::endl;
    }
    void * operator new(size_t size,std::string str)
    {
        std::cout<<"size: "<<size <<"\nname: " << str<< std::endl;
    }
    int id;
};

int main()
{
    User* user1 = new User;
    User* user2 = new ("JIM")User;
    void *pi = operator new(sizeof(int));
    int i = 3;
    int *p = &i;
    pi = p;
    cout<<*(int*)pi<<endl;
}

 

三、异常(Exceptions)

9、利用 destructors 避免泄漏资源

#include <iostream>
#include <stdexcept>

void exception_fun()
{
    throw std::runtime_error("runtime_error");
}

void fun()
{
    int *pi = new int[10000];
    std::cout<<pi<<std::endl;
    try
    {
        exception_fun();    //如果此处抛出异常而未处理,则无法执行 delete 语句,造成内存泄漏。
    }
    catch(std::runtime_error& error)
    {
        delete pi;
        throw;
    }
    delete pi;
}

main()
{
    for(;;)
    {
        try { fun(); } catch(std::runtime_error& error) { }
    }
}

 一个函数在堆里申请内存到释放内存的过程中,如果发生异常,如果自己不处理而只交给调用程序处理,则可能由于未调用 delete 导致内存泄漏。上面的方法可以解决这一问题,不过这样的代码使人看起来心烦且难于维护,而且必须写双份的 delete 语句。函数返回时局部对象总是释放(调用其析构函数),无论函数是如何退出的。(仅有的一种例外是当调用 longjmp 时,而 longjmp 这个缺点也是C++最初支持异常处理的原因)

所以这里使用智能指针或类似于智能指针的对象是比较好的办法:

#include <iostream>
#include <stdexcept>

void exception_fun()
{
    throw std::runtime_error("runtime_error");
}

void fun()
{
    int *pi = new int[10000];
    std::auto_ptr<int> ap(pi);    //用 auto_ptr 包装一下
    std::cout<<pi<<std::endl;
    exception_fun();
}

main()
{
    for(;;)
    {
        try { fun(); } catch(std::runtime_error& error) { }
    }
}

上面的代码看起来简洁多了,因为 auto_ptr 会在离开作用域时调用其析构函数,析构函数中会做 delete 动作。

  

10、在 constructors 内阻止资源泄漏

这一条讲得其实是捕获构造函数里的异常的重要性。

堆栈辗转开解(stack-unwinding):如果一个函数中出现异常,在函数内即通过 try..catch 捕捉的话,可以继续往下执行;如果不捕捉就会抛出(或通过 throw 显式抛出)到外层函数,则当前函数会终止运行,释放当前函数内的局部对象(局部对象的析构函数就自然被调用了),外层函数如果也没有捕捉到的话,会再次抛出到更外层的函数,该外层函数也会退出,释放其局部对象……如此一直循环下去,直到找到匹配的 catch 子句,如果找到 main 函数中仍找不到,则退出程序。

#include <iostream>
#include <string>
#include <stdexcept>

class B
{
    public:
        B(const int userid_,const std::string& username_ = "",const std::string address_ = ""):
        userid(userid_),
        username(0),
        address(0)
        {
            username = new std::string(username_);
            throw std::runtime_error("runtime_error");  //构造函数里抛出异常的话,由于对象没有构造完成,不会执行析构函数
            address = new std::string(address_);
        }
        ~B()    //此例中不会执行,会导致内存泄漏
        {
            delete username;
            delete address;
            std::cout<<"~B()"<<std::endl;
        }
    private:
        int userid;
        std::string* username;
        std::string* address;
};

main()
{
    try { B b(1); } catch(std::runtime_error& error) { }
}

 C++拒绝为没有完成构造函数的对象调用析构函数,原因是避免开销,因为只有在每个对象里加一些字节来记录构造函数执行了多少步,它会使对象变大,且减慢析构函数的运行速度。

一般建议不要在构造函数里做过多的资源分配,而应该把这些操作放在一个类似于 init 的成员函数中去完成。这样当 init 成员函数抛出异常时,如果对象是在栈上,析构函数仍会被调用(异常会自动销毁局部对象,调用局部对象的析构函数,见下面),如果是在堆上,需要在捕获异常之后 delete 对象来调用析构函数。

 

11、禁止异常流出 destructors 之外

这一条讲得其实是捕获析构函数里的异常的重要性。第一是防止程序调用 terminate 终止(这里有个名词叫:堆栈辗转开解 stack-unwinding);第二是析构函数内如果发生异常,则异常后面的代码将不执行,无法确保我们完成我们想做的清理工作。

之前我们知道,析构函数被调用,会发生在对象被删除时,如栈对象超出作用域或堆对象被显式 delete (还有继承体系中,virtual 基类析构函数会在子类对象析构时调用)。除此之外,在异常传递的堆栈辗转开解(stack-unwinding)过程中,异常处理系统也会删除局部对象,从而调用局部对象的析构函数,而此时如果该析构函数也抛出异常,C++程序是无法同时处理两个异常的,就会调用 terminate()终止程序(会立即终止,连局部对象也不释放)。另外,如果异常被抛出,析构函数可能未执行完毕,导致一些清理工作不能完成。

所以不建议在析构函数中抛出异常,如果异常不可避免,则应在析构函数内捕获,而不应当抛出。 场景再现如下:

#include <iostream>

struct T
{
    T()
    {
        pi = new int;
        std::cout<<"T()"<<std::endl;
    }
    void init(){throw("init() throw");}
    ~T()
    {
        std::cout<<"~T() begin"<<std::endl;
        throw("~T() throw");
        delete pi;
        std::cout<<"~T() end"<<std::endl;
    }
    int *pi;
};

void fun()
{
    try{
        T t;
        t.init();
    }catch(...){}

//下面也会引发 terminate
    /*
    try
    {
        int *p2 = new int[1000000000000L];
    }catch(std::bad_alloc&)
    {
        std::cout<<"bad_alloc"<<std::endl;
    }
    */
}

void terminate_handler()
{
    std::cout<<"my terminate_handler()"<<std::endl;
}

int main()
{
    std::set_terminate(terminate_handler);
    fun();
}

 

12、了解 "抛出一个 exception ”  与 “传递一个参数” 或 “调用一个虚函数”之间的差异

抛出异常对象,到 catch 中,有点类似函数调用,但是它有几点特殊性:

 1 #include <iostream>
 2 
 3 void fun1(void)
 4 {
 5     int i = 3;
 6     throw i;
 7 }
 8 void fun2(void)
 9 {
10     static int i = 10;
11     int *pi = &i;
12     throw pi; //pi指向的对象是静态的,所以才能抛出指针
13 }
14 
15 main()
16 {
17     try{
18         fun1();
19     }catch(int d)
20     {
21         std::cout<<d<<std::endl;
22     }
23     try{
24         fun2();
25     } catch(const void* v)
26     {
27         std::cout<<*(int*)v<<std::endl;
28     }
29 }

如果抛出的是 int 对象的异常,是不能用 double 类型接收的,这一点跟普通函数传参不一样。异常处理中,支持的类型转换只有两种,一种是上面例子中演示的从"有型指针"转为"无型指针",所以用 const void* 可以捕捉任何指针类型的 exception。另一种是继承体系中的类转换,可见下一条款的例子。

另外,它跟虚拟函数有什么不同呢?异常处理可以出现多个 catch 子句,而匹配方式是按先后顺序来匹配的(所以如 exception 异常一定要写在 runtime_error异常的后面,如果反过来的话,runtime_error异常语句永远不会执行),而虚函数则是根据虚函数表来的。

 

13、以 by reference 方式捕捉 exceptions

 1 #include <iostream>
 2 #include <stdexcept>
 3 
 4 class B
 5 {
 6     public:
 7         B(int id_):id(id_){}
 8         B(const B& b){id = b.id;std::cout<<"copy"<<std::endl;}
 9         int id;
10 };
11 
12 void fun(void)
13 {
14     static B b(3);  //这里是静态对象
15     throw &b;   //只有该对象是静态对象或全局对象时,才能以指针形式抛出
16 }
17 main()
18 {
19     try{
20         fun();
21     }catch(B* b)    //这里以指针形式接收
22     {
23         std::cout<<b->id<<std::endl;    //输出3
24     }
25 }

用指针方式来捕捉异常,上面的例子效率很高,没有产生临时对象。但是这种方式只能运用于全局或静态的对象(如果是 new 出来的堆中的对象也可以,但是该何时释放呢?)身上,否则的话由于对象离开作用域被销毁,catch中的指针指向不复存在的对象。接下来看看对象方式和指针方式:

#include <iostream>
#include <stdexcept>

class B
{
    public:
        B(){}
        B(const B& b){std::cout<<"B copy"<<std::endl;}
        virtual void print(void){std::cout<<"print():B"<<std::endl;}
};

class D : public B
{
    public:
        D():B(){}
        D(const D& d){std::cout<<"D copy"<<std::endl;}
        virtual void print(void){std::cout<<"print():D"<<std::endl;}
};

void fun(void)
{
    D d;
    throw d;
}
main()
{
    try{
        fun();
    }catch(B b) //注意这里
    {
        b.print();
    }
}

上面的例子会输出:

可是如果把 catch(B b) 改成 catch(B& b) 的话,则会输出:

该条款的目的就是告诉我们,请尽量使用引用方式来捕捉异常,它可以避免 new 对象的删除问题,也可以正确处理继承关系的多态问题,还可以减少异常对象的复制次数。 

 

14、明智运用 exception specifications

C++提供了一种异常规范,即在函数后面指定要抛出的异常类型,可以指定多个:

#include <iostream>

void fun(void) throw(int,double);    //必须这样声明,而不能是 void fun(void);

void fun(void) throw(int,double)    //说明可能抛出 int 和 double 异常
{
    int i = 3;
    throw i;
}

main()
{
    try{
        fun();
    }catch(int d)
    {
        std::cout<<d<<std::endl;
    }
}

 

15、了解异常处理的成本

 大致的意思是,异常的开销还是比较大的,只有在确实需要用它的地方才去用。

 

四、效率(Efficiency)

16、谨记 80-20 法则

大致的意思是说,程序中80%的性能压力可能会集中在20%左右的代码处。那怎么找出这20%的代码来进行优化呢?可以通过Profiler分析程序等工具来测试,而不要凭感觉或经验来判断。

 

17、考虑使用 lazy evaluation(缓式评估)

除非确实需要,否则不要为任何东西生成副本。当某些计算其实可以避免时,应该使用缓式评估。

 

18、分期摊还预期的计算成本

跟上一条款相对的,如果某些计算无可避免,且会多次出现时,可以使用急式评估。 

 

19、了解临时对象的来源

C++真正所谓的临时对象是不可见的——只要产生一个 non-heap object 而没有为它命名,就产生了一个临时对象。它一般产生于两个地方:一是函数参数的隐式类型转换,二是函数返回对象时。 任何时候,只要你看到一个 reference-to-const 参数,就极可能会有一个临时对象被产生出来绑定至该参数上;任何时候,只要你看到函数返回一个对象,就会产生临时对象(并于稍后销毁)。

 

20、协助完成“返回值优化(RVO)”

不要在一个函数里返回一个局部对象的地址,因为它离开函数体后就析构了。不过在GCC下可以正常运行,无论是否打开优化;而在VS2010中如果关闭优化,就会看到效果。

这个条款想说的是:const Test fun(){ return Test(); } 比 const Test fun(){Test test; return test; }  好,更能使编译器进行优化。

不过现在看来,在经过编译器优化之后,这两个好像已经没有什么区别了。

 

21、利用重载技术避免隐式类型转换

#include <iostream>

using namespace std;

struct B
{
    B(int id_):id(id_){}
    int id;
};

const B operator+(const B& b1,const B& b2)
{
    return B(b1.id + b2.id);
}

//const B operator+(const B& b1,int i)    //如果重载此方法,就不会产生临时对象了
//{
//  return B(b1.id + i);
//}
int main()
{
    B b1(3),b2(7);
    B b3 = b1+ b2;
    B b4 = b1 + 6;    //会把 6 先转换成B对象,产生临时对象
}

 

22、考虑以操作符复合形式(op=)取代其独身形式(op)

使用 operator+= 的实现来实现 operator= ,其它如果 operator*=、operator-= 等类似。

#include <iostream>

class B
{
    public:
        B(int id_):id(id_){}
        B& operator+=(const B& b)
        {
            id +=  b.id;
            return *this;
        }
        int print_id(){std::cout<<id<<std::endl;}
    private:
        int id;
};

B operator+(const B& b1,const B& b2)    //不用声明为 B 的 friend 函数,而且只需要维护 operator+= 即可。
{
    return const_cast<B&>(b1) += b2;    //这里要去掉b1的const属性,才能带入operator+= 中的 this 中
}

int main()
{
    B b1(3),b2(7),b3(100);
    (b1+b2).print_id(); //10    这里进行 operator+ 操作,会改变 b1 的值,这个不应该吧
    b1.print_id();      //10
    b3+=b1;
    b3.print_id();      //110
}

 

23、考虑使用其它程序库

提供类似功能的程序库,可能在效率、扩充性、移植性和类型安全方面有着不同的表现。比如说 iostream 和 stdio 库,所以选用不同的库可能会大幅改善程序性能。

 

24、了解 virtual functions、multiple inheritance、virtual base classes、runtime type identification 的成本

在使用虚函数时,大部分编译器会使用所谓的 virtual tables 和 virtual table pointers ,通常简写为 vtbls 和 vptrs 。vtbl 通常是由 "函数指针" 架构而成的数组,每一个声明(或继承)虚函数的类都有一个 vtbl ,而其中的条目就是该 class 的各个虚函数实现体的指针。

虚函数的第一个成本:必须为每个拥有虚函数的类耗费一个 vtbl 空间,其大小视虚函数的个数(包括继承而来的)而定。不过,一个类只会有一个 vtbl 空间,所以一般占用空间不是很大。

不要将虚函数声明为 inline ,因为虚函数是运行时绑定的,而 inline 是编译时展开的,即使你对虚函数使用 inline ,编译器也通常会忽略。

虚函数的第二个成本:必须为每个拥有虚函数的类的对象,付出一个指针的代价,即 vptr ,它是一个隐藏的 data member,用来指向所属类的 vtbl。

调用一个虚函数的成本,基本上和通过一个函数指针调用函数相同,虚函数本身并不构成性能上的瓶颈。

虚函数的第三个成本:事实上等于放弃了 inline。(如果虚函数是通过对象被调用,倒是可以 inline,不过一般都是通过对象的指针或引用调用的)

#include <iostream>

struct B1 { virtual void fun1(){} int id;};
struct B2 { virtual void fun2(){} };
struct B3 { virtual void fun3(){} };
struct D : virtual B1, virtual B2, virtual B3 {virtual void fun(){}  void fun1(){}  void fun2(){}   void fun3(){}};

int main()
{
    std::cout<<sizeof(B1)<<std::endl;   //8
    std::cout<<sizeof(B2)<<std::endl;   //4
    std::cout<<sizeof(B3)<<std::endl;   //4
    std::cout<<sizeof(D)<<std::endl;    //16
}

//D 中只包含了三个 vptr ,D和B1共享了一个。

 

五、技术(Techniques,Idioms,Patterns)

25、将 constructor 和 non-member functions 虚化

这里所谓的虚拟构造函数,并不是真的指在构造函数前面加上 virtual 修饰符,而是指能够根据传入不同的参数建立不同继承关系类型的对象。 

被派生类重定义的虚函数可以与基类的虚函数具有不同的返回类型。所以所谓的虚拟复制构造函数,可以在基类里声明一个 virtual B* clone() const = 0 的纯虚函数,在子类中实现 virtual D* clone() const {return new D(*this);}

同样的,非成员函数虚化,这里也并不是指使用 virtual 来修饰非成员函数。比如下面这个输出 list 中多态对象的属性:

#include <iostream>
#include <list>
#include <string>

using namespace std;

class B
{
    public:
        B(string str):value(str){}
        virtual ostream& print(ostream& s) const = 0;
    protected:
        string value;
};

class D1 : public B
{
    public:
        D1(int id_):B("protect value"),id(id_){}    //子类构造函数中,要先调用基类构造函数初始化基类
        ostream& print(ostream& s) const{cout<<value<<"\t"<<id;;return s;}  //如果基类虚函数是 const 方法,则这里也必须使用 const 修饰
    private:
        int id;
};

class D2 : public B
{
    public:
        D2(int id_):B("protect value"),id(id_){}    //子类构造函数中,要先调用基类构造函数初始化基类
        ostream& print(ostream& s) const{cout<<value<<"\t"<<id;return s;}
    private:
        int id;
};

ostream& operator<<(ostream& s,const B& b)
{
    return b.print(s);
}

int main()
{
    list<B*> lt;
    D1 d1(1);
    D2 d2(2);
    lt.push_back(&d1);
    lt.push_back(&d2);

    list<B*>::iterator it = lt.begin();
    while(it != lt.end())
    {
        cout<<*(*it)<<endl;     //D1   D2
        it++;
    }
}

在这里,即使给每一个继承类单独实现友元的 operator<< 方法,也不能实现动态绑定,只会调用基类的方法。那么,在基类里写 operator<< 用 virtual 修饰不就行了吗?遗憾的,虚函数不能是友元。

 

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

类中的静态成员总是被构造,即使不使用,而且你无法确定它什么时候初始化;而函数中的静态成员,只有在第一次使用时才会建立,但你也得为此付出代价,每次调用函数时都得检查一下是否需要建立对象。(另外该函数不能声明为内联,非成员内联函数在链接的时候在目标文件中会产生多个副本,可能造成程序的静态对象拷贝超过一个。)这个已经由标准委员会在1996年把 inline 的默认连接由内部改为外部,所以问题已经不存在了,了解一下即可。 限制对象个数:建立一个基类,构造函数和复制构造函数中计数加一,若超过最大值则抛出异常;析构函数中计数减一。

 

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

析构函数私有,有一个致命问题:妨碍了继承和组合(内含)。

#include <iostream>
#include <string>

using namespace std;

class B1    //禁止对象产生于 heap 中
{
    public:
        B1(){cout<<"B1"<<endl;};
    private:
        void* operator new(size_t size);
        void* operator new[](size_t size);
        void operator delete(void* ptr);
        void operator delete[](void* ptr);
};

class B2    //要求对象产生于 heap 中
{
    public:
        B2(){cout<<"B2"<<endl;};
        void destroy(){delete this;}  //模拟的析构函数
    private:
        ~B2(){}
};
int main()
{
    //B1* b1  = new B1; //Error!
    B1 b1;
    //B2 b2;    //Error
    B2* b2 = new B2;
    b2->destroy();
}

 

28、Smart Pointer(智能指针)

可以参考 auto_ptr 和 share_ptr(源于boost,已被收录进c++11标准)源码。 

 

29、Reference counting(引用计数)

同上。

 

30、Proxy classes(替身类、代理类)

参考《可复用面向对象软件基础》结构型模式之代理模式。

 

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

 

 

六、杂项讨论(Miscellany)

32、在未来时态下发展程序

要用语言提供的特性来强迫程序符合设计,而不要指望使用者去遵守约定。比如禁止继承,禁止复制,要求类的实例只能创建在堆中等等。处理每个类的赋值和拷贝构造函数,如果这些函数是难以实现的,则声明它们为私有。

所提供的类的操作和函数有自然的语法和直观的语义,和内建类型(如 int)的行为保持一致。

尽可能写可移植性的代码,只有在性能极其重要时不可移植的结构才是可取的。

多为未来的需求考虑,尽可能完善类的设计。

 

33、将非尾端类设计为抽象类

只要不是最根本的实体类(不需要进一步被继承的类),都设计成抽象类。

 

34、如何在同一个程序中结合 C++ 和 C

等有时间看看 C语言的经典书籍后再说。

 

35、让自己习惯于标准 C++ 语言

可以参考《C++标准程序库》,另外可以使用最新编译器,尝试c++11新特性。

 

 

posted @ 2012-12-05 14:26  轻典  阅读(4159)  评论(1编辑  收藏  举报