一、基础议题(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 == 3const int j = 10;int *pj = const_cast<int*>(&j);//int *pj = (int*)(&j);     //等同于上面*pj = 20;//虽然 *pj的地址和 j 的地址是一样的,但是值却不一样。cout<<*pj<<endl;    //20cout<<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();      //10b3+=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;   //8std::cout<<sizeof(B2)<<std::endl;   //4std::cout<<sizeof(B3)<<std::endl;   //4std::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   D2it++;}
}

在这里,即使给每一个继承类单独实现友元的 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;    //ErrorB2* 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新特性。

《More Effective C++》读书笔记相关推荐

  1. 读书笔记 | 墨菲定律

    1. 有些事,你现在不做,永远也不会去做. 2. 能轻易实现的梦想都不叫梦想. 3.所有的事都会比你预计的时间长.(做事要有耐心,要经得起前期的枯燥.) 4. 当我们的才华还撑不起梦想时,更要耐下心来 ...

  2. 读书笔记 | 墨菲定律(一)

    1. 有些事,你现在不做,永远也不会去做. 2. 能轻易实现的梦想都不叫梦想. 3.所有的事都会比你预计的时间长.(做事要有耐心,要经得起前期的枯燥.) 4. 当我们的才华还撑不起梦想时,更要耐下心来 ...

  3. 洛克菲勒的38封信pdf下载_《洛克菲勒写给孩子的38封信》读书笔记

    <洛克菲勒写给孩子的38封信>读书笔记 洛克菲勒写给孩子的38封信 第1封信:起点不决定终点 人人生而平等,但这种平等是权利与法律意义上的平等,与经济和文化优势无关 第2封信:运气靠策划 ...

  4. 股神大家了解多少?深度剖析股神巴菲特

    股神巴菲特是金融界里的传奇,大家是否都对股神巴菲特感兴趣呢?大家对股神了解多少?小编最近在QR社区发现了<阿尔法狗与巴菲特>,里面记载了许多股神巴菲特的人生经历,今天小编简单说一说关于股神 ...

  5. 2014巴菲特股东大会及巴菲特创业分享

     沃伦·巴菲特,这位传奇人物.在美国,巴菲特被称为"先知".在中国,他更多的被喻为"股神",巴菲特在11岁时第一次购买股票以来,白手起家缔造了一个千亿规模的 ...

  6. 《成为沃伦·巴菲特》笔记与感想

    本文首发于微信公众帐号: 一界码农(The_hard_the_luckier) 无需授权即可转载: 甚至无需保留以上版权声明-- 沃伦·巴菲特传记的纪录片 http://www.bilibili.co ...

  7. 读书笔记002:托尼.巴赞之快速阅读

    读书笔记002:托尼.巴赞之快速阅读 托尼.巴赞是放射性思维与思维导图的提倡者.读完他的<快速阅读>之后,我们就可以可以快速提高阅读速度,保持并改善理解嗯嗯管理,通过增进了解眼睛和大脑功能 ...

  8. 读书笔记001:托尼.巴赞之开动大脑

    读书笔记001:托尼.巴赞之开动大脑 托尼.巴赞是放射性思维与思维导图的提倡者.读完他的<开动大脑>之后,我们就可以对我们的大脑有更多的了解:大脑可以进行比我们预期多得多的工作:我们可以最 ...

  9. 读书笔记003:托尼.巴赞之思维导图

    读书笔记003:托尼.巴赞之思维导图 托尼.巴赞的<思维导图>一书,详细的介绍了思维发展的新概念--放射性思维:如何利用思维导图实施你的放射性思维,实现你的创造性思维,从而给出一种深刻的智 ...

  10. 产品读书《滚雪球:巴菲特和他的财富人生》

    作者简介 艾丽斯.施罗德,曾经担任世界知名投行摩根士丹利的董事总经理,因为撰写研究报告与巴菲特相识.业务上的往来使得施罗德有更多的机会与巴菲特亲密接触,她不仅是巴菲特别的忘年交,她也是第一个向巴菲特建 ...

最新文章

  1. 153和154.寻找旋转排序数组中的最小值
  2. 2022年六大值得关注的边缘计算趋势
  3. 【SpringCloud】Hystrix-实例
  4. windows下php7安装redis扩展
  5. Android 应用程序消息处理机制(Looper、Handler)分析
  6. IDEA使用技巧整理
  7. 姑娘,你为什么要编程?
  8. linux es数据库 head,Elasticsearch 5.3.x 使用 Head 插件
  9. 蚂蚁(挑战程序设计竞赛)
  10. 苹果x Android,中国发明安卓iPhoneX 安卓iPhoneX有何特别之处?
  11. oracle查看登录失败次数,Oracle取消用户连续登录失败次数限制
  12. 地铁口多了一堆小卡片,事情并不简单?
  13. 搜索及代码在GitHub上查重小技巧
  14. [HTML/CSS]Flex 布局中space-evenly 的兼容性
  15. matalab三维画图
  16. SAP系统管理的常用T-CODE
  17. D2D加载图片资源(2)
  18. Latex 制作斜线表头、合并行列单元格
  19. dolphinscheduler v2.0.1 master和worker执行流程分析(一)
  20. python进阶:yield与yield from

热门文章

  1. 现在没点硬核技术都不敢卖货了
  2. 华为自研搜索引擎曝光:「花瓣」虽小,但要替代谷歌
  3. STL priority_queue sort 自定义比较终极模板
  4. var s=+newDate();
  5. 139.00.005 Git学习-分支管理
  6. static用法解析---java语言
  7. Linux如何解决动态库的版本控制
  8. WebLogic使用总结(一)——WebLogic安装
  9. 网站的线下活动如何组织
  10. QEMU — VirtIO 的网络实现