《More Effective C++》读书笔记
http://www.cnblogs.com/tianyajuanke/archive/2012/11/29/2795131.html
一、基础议题(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的函数
void update(SpecialWidget *psw);
SpecialWidget sw; // sw 是一个非const 对象。
const SpecialWidget& csw = sw; // csw 是sw的一个引用
update(const_cast<SpecialWidget*>(&csw));
Dynamic_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异常语句永远不会执行),而虚函数则是根据虚函数表来的。
三个主要区别:
第一,异常对象在传递时总被进行拷贝当通过传值方式捕获时,异常对象被拷贝了两次,对象作为参数传递给函数时不需要被拷贝;
第二,对象作 为异常被抛出与作为参数传递给函数相比,前者类型转换比后者少(前者只有两种转换形式:继承类与基类的转换,类型化指针到无类型指针的转换)
最后一点, catch子句进行异常类型匹配的顺序是它们在源代码中出现的顺序,第一个类型匹配成功的擦他处将被用来执行当一个对象调用一个虚函数时,被选择的函数 位于与对象类型匹配最佳的类里,急事该类不是在源代码的最前头
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新特性。