More effective c++ (一)

来源:互联网 发布:扬琴淘宝网 编辑:程序博客网 时间:2024/05/18 15:51

Item M1:指针与引用的区别

引用必须被初始化,且不能改变它本身使其成为另一个变量的别名。

Item M2:尽量使用C++风格的类型转换
这样的类型转换不论是对人工还是对程序都很容易识别。它们允许编译器检测出原来不能发现的错误。
你能用dynamic_cast把指向基类的指针或引用转换成指向其派生类或其兄弟类的指针或引用,而且你能知道转换是否成功。失败的转换将返回空指针(当对指针进行类型转换时)或者抛出异常(当对引用进行类型转换时)。dynamic_cast只能在有继承关系的类之间进行转换。
reinterpret_cast的最普通的用途就是在函数指针类型之间进行转换。
reinterpret_cast<type>(expression) 这个expression必须是指针或引用!!!

Item M3:不要对数组使用多态
因为派生类的大小一般不与基类大小相同,而数组的存储方式在内存中是连续的。 所以一般Base[i]与Derive[i]并不是指向相同的位置。
语言规范中说通过一个基类指针来删除一个含有派生类对象的数组,结果将是不确定的。
多态和指针算法不能混合在一起来用,所以数组与多态也不能用在一起。

Item M4:避免无用的缺省构造函数
提供无意义的缺省构造函数也会影响类的工作效率。因为缺省构造函数一般不能保证所有的成员被正确的初始化,所以在其它成员函数里面都要对数据成员进行判断,以确保进行了有效的初始化。会使可执行程序变大。
但如果不使用缺省构造函数时:
建立对象数组的时候,会有问题。
它们无法在许多基于模板(template-based)的容器类里使用。因为实例化一个模板时,模板的类型参数应该提供一个缺省构造函数,这是一个常见的要求。

Item M5:谨慎定义类型转换函数
有两种函数允许编译器进行这些的转换:单参数构造函数(single-argument constructors)和隐式类型转换运算符。
class Rational {
public:
...
operator double() const; // 转换Rational类成double类型
};
Rational r(1, 2);
cout<<r;
如果没有重载<<运算符,也不会提示错误。因为r可以用operator double()转为浮点型。
单参构造函数造成的隐式转换的问题甚至比隐式类型转换运算符的问题都大。
不声明运算符(operator)的方法可以克服隐式类型转换运算符的缺点。通过给单参构造函数加explicit关键字,可以解决隐式类型转换问题。

Item M6:自增(increment)、自减(decrement)操作符前缀形式与后缀形式的区别
后缀自增自减都返回const对象,如果不是const对象,则会允许i++++;的语句出现。不允许这样做!!!

Item M7:不要重载“&&”,“||”, 或“,”
C++使用布尔表达式短路求值法. 如if (p!=null && p++) p为null时后面的p++就不会计算
如果用operator &&重载了&&,就会用函数调用法替代了短路求值法
if (expression1 && expression2) 就会变为:
if (expression1.operator&&(expression2)) //operaotr &&为成员函数时
if (operator&&(expression1, expression2)) //operaotr &&为全局函数时
它会运算它所有的参数,而且运算的顺序不能确定。
一个包含逗号的表达式首先计算逗号左边的表达式,然后计算逗号右边的表达式;整个表达式的结果是逗号右边表达式的值。
重载逗号运算符也没办法保证计算次序

Item M8:理解各种不同含义的new和delete
new操作符(new operator)与new操作(operator new)的区别
string *ps = new string("Memory Management");
你使用的new是new操作符。这个操作符就象sizeof一样是语言内置的,你不能改变它的含义,它的功能总是一样的。它要完成的功能分成两部分。第一部分是分配足够的内存以便容纳所需类型的对象。第二部分是它调用构造函数初始化内存中的对象。new操作符总是做这两件事情,你不能以任何方式改变它的行为。
delete操作符(delete operator)与new操作符相反,先调用析构函数,然后再删除内存。
new操作符为分配内存所调用函数的名字是operator new。所以可以通过重载new操作(operator new)来更改new操作符申请内存的行为。
void * operator new(size_t size);
使用:
void *buffer = operator new(50);    //50为size大小
operator delete(buffer);

placement new (一种特殊的operator new)
用来为一些已经分配,但是尚未处理的(raw)内存中构造对象。
定义:
void * operator new(size_t, void *location)
{
    return location;
}
使用:
new (buffer) Widget(widgetSize);  //buffer是已经申请好的内存空间地址
placement new是标准C++库的一部分,为了使用placement new,你必须使用语句#include <new>。
operator new[]和operator delete[] 也可以被重载,用来操作对象数组。

调用相应的new,就要使用相对应的delete,不要混用。

Item M9:使用析构函数防止资源泄漏
应该对操纵局部资源的指针说再见。让智能指针来代替。(使用指针指针简洁,对产生异常的情况也适用)

Item M10:在构造函数中防止资源泄漏
当在构造函数执行的时候有异常发生,则已经分配的内存空间无法释放。因为这时候不会去执行析构函数。
可以用异常处理,在构造函数里面捕获到异常之后,在catch块里面对内存进行释放。
成员指针变量用智能指针auto_ptr来代替。当对象消失的时候,会被自动调用。

Item M11:禁止异常信息(exceptions)传递到析构函数外
禁止异常传递到析构函数外有两个原因,第一能够在异常转递的堆栈辗转开解(stack-unwinding)的过程中,防止terminate被调用。第二它能帮助确保析构函数总能完成我们希望它做的所有事情。 (如果异常从析构函数中传递到外面的话,terminate会被自动调用,彻底终止你的程序)
一般通过在析构函数中用try catch来捕获它本身产生的任何异常。
(堆栈辗转开解:也即堆栈退栈。如果某个函数中发生了异常,并执行了throw来抛出异常对象,则压在栈中的对象都会被自动析构,直到碰到catch。)

Item M12:理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异
传递函数参数与异常的途径均可以是传值、传递引用或传递指针,这是相同的。当你传递参数和异常时,系统所要完成的操作过程则是完全不同的。产生这个差异的原因是:你调用函数时,程序的控制权最终还会返回到函数的调用处,但是当你抛出一个异常时,控制权永远不会回到抛出异常的地方。
在参数中传引用的时候,引用指向实际的对象。而异常传递的时候,不管是通过传值还是传引用,都会进行对象的拷贝操作。(因为产生异常后,会使原来的异常对象离开生存空间,其析构函数也会被调用。)即使异常对象是静态的,不会被释放,也会进行拷贝。
因为异常总是会复制对象,所以抛出异常运行速度稍慢一些。
对象拷贝的时候,会使用静态类型拷贝构造函数,而不是动态类型的。
如:
class Base { ... };
class Derive: public Base { ... };
Derive d;
Base &b = d;
在throw b的时候,只会调用Base的拷贝构造。
另一个区别是:在函数调用中不允许转递一个临时对象到一个非const引用类型的参数里,但是在异常中却被允许。
通过指针来捕获异常的话,异常对象必须是全局或堆中的!因为指针传递异常时,只有指针的拷贝被传递。
函数调用时,可能有隐式的类型转换。但在捕获异常的时候,不会有类型转换。 如catch(double d),并不会捕获产生的int型异常。(不过有继承类与基类间可能转换。如捕获基类的异常可以捕获派生类类型的异常。 还有,允许从一个类型化指针(typed pointer)转变成无类型指针(untyped pointer),所以带有const void* 指针的catch子句能捕获任何类型的指针类型异常)
catch子句进行异常类型匹配的顺序是它们在源代码中出现的顺序,第一个类型匹配成功的catch将被用来执行。当一个对象调用一个虚拟函数时,被选择的函数位于与对象类型匹配最佳的类里。

Item M13:通过引用(reference)捕获异常
四个标准的异常:
bad_alloc: 当operator new不能分配足够的内存时,被抛出
bad_cast:  当dynamic_cast针对一个引用(reference)操作失败时,被抛出
bad_typeid: 当dynamic_cast对空指针进行操作时,被抛出
bad_exception: 用于unexpected异常
通过指针方式捕获异常,处理完异常之后,无法确定是否要删除这个异常对象。(因为 如果是全局对象,则不用删除,如果是堆对象,则要删除。)
通过传值捕获异常,当它们被抛出时系统将对异常对象拷贝两次。而且会有slicing problem,即即派生类的异常对象被做为基类异常对象捕获时,那它的派生类行为就被切掉了(sliced off)。
通过引用捕获异常,不会有对象删除的问题。没有slicing problem (可以通过父类引用调用虚函数,实现多态)。异常对象只被拷贝一次。(所以尽量选用“引用捕获异常”)
(引用是除指针外另一个可以产生多态效果的手段。)

Item M14:审慎使用异常规格(exception specifications)
如果一个函数抛出一个不在异常规格范围里的异常,系统在运行时能够检测出这个错误,然后一个特殊函数unexpected将被自动地调用。unexpected缺省的行为是调用函数terminate。
void f1(); //没有声明异常规格,可以抛出任意的异常
void f2() throw(int); //抛出int型的异常
void f2() throw(int)
{
   f1();   //f1抛出的异常不是int,但是合法
}
但是,其抛出的异常与发出调用的函数的异常规格不一致,并且这样的调用可能导致你的程序执行被终止

Item M15:了解异常处理的系统开销
为了在运行时处理异常,程序要记录大量的信息。

Item M16:牢记80-20准则(80-20 rule)
大约20%的代码使用了80%的程序资源。
尽量使用工具来找出系统的瓶径,如profiler,而不要人为观察或凭直觉判断。