一、基础议题
Item 1. 区分指针和引用
指针是整数,代表的是内存地址,通过*操作符访问该内存地址上的值
而引用是一个变量的别名,可以有空指针,没有空引用
引用使用之前不需要判空,因为没有所谓的空引用
但是指针使用之前通常得判空
引用永远指向它最初获得的那个对象
应该总是令operator []返回reference
Item 2. 优先考虑C++风格的类型转换
(type)expression这样的类型转换是以前C风格的,可以将任何类型转换为任何其他类型,而且难以辨识。应该使用C++风格的static_cast等4个转型操作符,十分容易辨识出来
Static_cast与C旧式转型类似
Const_cast主要是改变表达式中的常量性或者变异性
Dynamic_cast主要用以将指向基类的指针或者引用转型为指向子类的指针或者引用,转型失败返回空指针或者异常。
Reinterpret_cast常用语转换“函数指针“类型,不具移植性,不推荐使用。
Item 3. 决不要把多态用于数组
问题一:一个函数参数是基类数组,并且访问数组中的对象,如果传入的是子类数组,由于array[i] = * (array + i), 而每一个偏移量是根据
sizeof(数组存储对象)来计算的,传入的是子类数组,但是参数类型是基类数组,因此使用的偏移量是基于sizeof(基类),这样访问会导致未定义行为。
问题二:在一个数组中delete数组时,也会调用每一个数组元素的析构函数,那么仍然会使用指针算术。
多态和指针算术不能混用,数组对象几乎总是会涉及指针的算数运算,所以数组和多态不要混用。不过问题的根源是:不要以一个具体类去继承一个具体类(Item 33).
Item 4. 避免不必要的默认构造函数
当显式指定构造函数时,系统便不会自动生成默认构造函数。
如果某件商品通过标识号唯一标识,那么构造一个没有标识号的商品对象显然就是错误的,所以default constructor就该被禁止,而要提供一个可以初始化商品标识号的constructor。但是没有default constructor有时也会带来不便,声明对象数组时,使用STL容器时(STL容器要求放入其中的对象能够提供default constructor)等等,然而提供default constructor也可能会带来不便,比如可能由于提供了default constructor而在制作一些成员函数时需要检查某些成员变量是否为初始变量或为其他值。所以是否需要默认构造函数得需要再三权衡。
二、运算符
Item 5. 小心用户自定义的转换函数
定义类似功能的函数,而抛弃隐式类型转换,使得类型转换必须显示调用。例如 string类没有定义对char*的隐式转换,而是用c_str函数来实施这个转换。拥有单个参数(或除第一个参数外都有默认值的多参数)构造函数的类,很容易被隐式类型转换,最好加上 explicit 防止隐式类型转换。
Item 6. 区分自增运算符和自减运算符的前缀形式与后缀形式
前置是先算后用,后置是先用后算,会产生一个内存副本
重载前缀运算符和后缀运算符的例子:
Class& operator++(); //前置的++
const Class operator++(int); //后置的++,返回常量避免 i++++的合法化
后置式操作符使用 operator++(int),参数的唯一目的只是为了区别前置式和后置式而已,当函数被调用时,编译器传递一个0作为int参数的值传递给该函数。
Item 7. 不要重载"&&"、"||"和","
还有很多个操作符是最好不要重载的,只会让别人更迷惑,而且容易出错
Item 8. 理解new和delete在不同情形下的含义
new 操作符的执行过程:
(1). 调用operator new分配内存 ;
通常声明为如下: void * operator new(size_t size)
//这一步可以使用 operator new 或 placement new 重载。
(2). 调用构造函数生成类对象;
(3). 返回相应指针。
如果希望将对象产生于heap, 使用new operator。它不但分配内存而且为该对象调用一个constructor.
只打算分配内存,调用operator new, 没有任何constructor被调用。
如果打算在heap objects产生时自己决定内存分配方式,自己重载operator new,再使用new operator生成。如果要在已分配并拥有指针的内存中构造对象,使用placement new
delete也类似,分为2个步骤:
(1).调用指针指向的对象的析构函数
(2).释放该对象占用的内存
三、异常
Item9.使用析构函数防止资源泄漏
析构函数时,要负责释放类内申请的一些资源,否则会造成内存泄漏
这个问题是和异常相关的:
一个函数在堆里申请内存到释放内存的过程中,如果发生异常,如果自己不处理而只交给调用程序处理,则可能由于未调用 delete 导致内存泄漏。
比较好的解决方法是将资源封装在对象内,析构函数内释放资源,那么当对象离开作用域时就可以自动释放资源,比如说智能指针auto_ptr,自动锁也是这样的一个原理:
/** @class CCriticalSectionLock
* @see CCriticalSection
* @brief 临界区锁类\n
* 提供了对临界区对象的一系列操作
*/
class CCriticalSectionLock
{
CCriticalSection *m_pCriticalSection;
void Unlock() { m_pCriticalSection->Leave(); }
public:
CCriticalSectionLock(CCriticalSection &object): m_pCriticalSection(&object) {m_pCriticalSection->Enter(); }
~CCriticalSectionLock() { Unlock(); }
};
Item10. 防止构造函数里的资源泄漏
首先,复习一点:C++保证“删除NULL”是安全的:
void SomeClass::UnInit()
{
if (ms_pSome != NULL)
{
delete ms_pSome;
ms_pSome = NULL;
}
}
堆栈辗转开解(stack-unwinding):如果一个函数中出现异常,在函数内即通过 try..catch 捕捉的话,可以继续往下执行;如果不捕捉就会抛出(或通过 throw 显式抛出)到外层函数,则当前函数会终止运行,释放当前函数内的局部对象(局部对象的析构函数就自然被调用了),外层函数如果也没有捕捉到的话,会再次抛出到更外层的函数,该外层函数也会退出,释放其局部对象……如此一直循环下去,直到找到匹配的 catch 子句,如果找到 main 函数中仍找不到,则退出程序。
C++拒绝为没有完成构造函数的对象调用析构函数,那么如果在构造函数中在堆上申请了内存,随后某些操作抛出了异常,则没法调用析构函数释放该内存。
Item11. 阻止异常传递到析构函数以外
一是防止程序调用 terminate 终止(这里有个名词叫:堆栈辗转开解 stack-unwinding);在抛出异常如果一直往顶层传递时,会不停地有局部对象调用析构函数,如果此时又有异常抛出,双重异常,会直接terminate。第二是析构函数内如果发生异常,则异常后面的代码将不执行,无法确保我们完成我们想做的清理工作。
Item12. 理解抛出异常与传递参数或者调用虚函数之间的不同
传递参数是往栈顶传递,异常是往逻辑上的顶层(应该也就是栈的底层传递)
异常处理中,支持的类型转换只有两种,一种是上面例子中演示的从"有型指针"转为"无型指针",所以用 const void* 可以捕捉任何指针类型的 exception。另一种是继承体系中的类转换。
不要抛出一个指向局部对象的指针,因为该局部对象会在exception离开作用域时被销毁,catch会获得一个指向“已被销毁的对象”的指针。
Exception objects必定会造成复制行为,throw出的异常总是其他对象的副本,即使是引用,catch端也只能修改到副本。
异常处理可以出现多个 catch 子句,而匹配方式是按先后顺序来匹配的,而虚函数则是根据虚函数表来的。
Item13.通过引用捕获异常
用指针方式来捕捉异常,效率很高,没有产生临时对象。但是这种方式只能运用于全局或静态的对象(如果是 new 出来的堆中的对象也可以,但是该何时释放呢?),否则的话由于对象离开作用域被销毁,catch中的指针指向不复存在的对象。
请尽量使用引用方式来捕捉异常,它可以避免 new 对象的删除问题,也可以正确处理继承关系的多态问题,还可以减少异常对象的复制次数。
Item14. 明智运用exception specifications
void fun(void) throw(int,double);
可能会带来的麻烦:非预期的异常运行期被检测出来,导致unexpected函数的调用。
Item15. 理解异常处理所付出的代价
这一点也没有考虑过,只考虑到它可以处理一些非预料内的情况。
Exception会带来一些性能成本,只有在确实需要用它的地方才去用,也不能因此放弃一些必须的异常处理情况。