《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新特性。