More Effective C++ 基础议题(条款1-4)总结

More Effective C++ 基础议题(条款1-4)总结

条款1:仔细区别pointers和references

  • 如果有一个变量,其目的是用来指向(代表)另一个对象,但是也有可能它不指向(代表)这个变量,那么应该使用pointer,因为可将pointer设为null,反之设计不允许变量为null,那么使用reference
  • 以下这是有害的行为,其结果不可预期(C++对此没有定义),编译器可以产生任何可能的输出
    char *pc = 0;       // 将 pointer 设定为null
    char& rc = *pc;     // 让 refercence 代表 null pointer 的 解引值
  • 没有null reference, 使用reference可能比pointers更有效率,在使用reference之前不需要测试其有效性
    void printDouble(const double& rd)
    {
        cout < < rd; // 不需要测试rd,它
    } // 肯定指向一个double值
    //相反,指针则应该总是被测试,防止其为空:
    void printDouble(const double *pd)
    {
        if (pd) // 检查是否为NULL
        {
            cout < < *pd;
        }
    }
  • pointers可以被重新赋值,指向另一个对象,reference却总是指向(代表)它最初获得的哪个对象
  • 实现某些操作符。如operator[],操作符应返回某种“能够被当作assignment赋值对象”

总结

当你知道你需要指向某个东西,而且绝不会改变指向其他东西,或是当你实现一个操作符而其语法需求无法由pointers达成,你就应该选择reference。任何其他时候,请采用pointers

条款2:最好使用C++转型操作符

  • 旧式的C转型方式,它几乎允许你将任何类型转换为任何其他类型,这是十分拙劣的

旧式转型存在的问题:

  • 例如将pointer-to-const-object转型为一个pointer-to-non-const-object(只改变对象的常量性),和将一个pointer-to-base-class-object转型为一个pointer-to-derived-class-object(完全改变一个对象的类型),其间有很大的差异。但是传统的C转型动作对此并无区分
  • 难以辨识,旧式转型由一小对小括号加上一个对象名称(标识符)组成,而小括号和对象名称在C++的任何地方都有可能被使用

staic_cast:

  • static_cast基本上拥有与 C 旧式转型相同的威力与意义,以及相同的限制(如不能将struct转型为int)。
  • 不能移除表达式的常量性,由const_cast专司其职
  • 其他新式 C++ 转型操作符适用于更集中(范围更狭窄)的目的
    (type) expression               //  原先 C 的转型写码形式
    static_cast<type>(expression)   //  使用 C++ 转型操作符

const_cast:

  • const_cast用来改变表达式的常量性(constness)变易性(volatileness),使用const_cast,便是对人类(编译器)强调,通过这个转型操作符,你唯一打算改变的是某物的常量性或变易性。这项意愿将由编译器贯彻执行。如果将const_cast应用于上述以外的用途,那么转型动作会被拒绝
#include <iostream>
using namespace std;

class Widget {};
class SpecialWidget : public Widget {};
void update(SpecialWidget* psw);
SpecialWidget sw;                           // sw是个 non-const 对象
const SpecialWidget& csw = sw;              // csw 确实一个代表sw的 reference
                                            // 并视之为一个const对象

update(&csw);                               // 错误!不能及那个const SpecialWidget*
                                            // 传给一个需要SpecialWidget* 的函数

update(const_cast<SpecialWidget*>(&csw));   // 可!&csw的常量性被去除了

update((SpecialWidget*)&csw);               // 可!但较难识别 C 旧式转型语法
  • const_cast最常见的用途就是将某个对象的常量性去除掉

dynamic_cast:

  • 用来转型继承体系重“安全的向下转型或跨系转型动作”。也就是说你可以利用dynamic_cast,将“指向base ckass objectspointersreferences”转型为“指向derived(或sibling base)class objectspointersreferences”,并得知转型是否成功。如果转型失败,会以一个null指针或一个exception(当转型对象是reference)表现出来:
Widget *pw;

update(dynamic_cast<SpecialWidget*>(pw));           // 很好,传给update()一个指针,指向pw所指的
                                                    // pw所指的SpecialWidget--如果pw
                                                    // 真的指向这样的东西;否则传过去的
                                                    // 将是一个 null 指针
void updateViaRef(SpecialWidegt& rsw);
updateViaRef(dynamic_cast<SpecialWidegt&>(*pw));    // 很好,传给updateViaRef()的是
                                                    // pw所指的SpecialWidget--如果
                                                    // pw真的指向这样的东西;否则
                                                    // 抛出一个exception
  • dynamic_cast只能用来协助你巡航于继承体系之中。它无法应用在缺乏虚函数(请看条款24)的类型身上,也不能改变类型的常量性(constness)
  • 如果不想为一个不涉及继承机制的类型执行转型动作,可使用static_cast;要改变常量性(constness),则必须使用const_cast

reinterpret_cast:

  • 最后一个转型操作符是reinterpret_cast。这个操作符的转换结果几乎总是与编译平台息息相关。所以reinterpret_cast不具移植性
  • reinterpret_cast的最常用用途是转换”函数指针”类型。
typedef void (*FuncPtr)();      // FuncPtr是个指针,指向某个函数
                                // 后者无须任何自变量,返回值为voids
FuncPtr funcPtrArray[10];       // funcPtrArray 是个数组
                                // 内有10个FuncPtrs

假设由于某种原因,希望将以下函数的一个指针放进funcPtrArray中

int doSomething();

如果没有转型,不可能办到,因为doSomething的类型与funcPtrArray所能接受的不同。funcPtrArray内各函数指针所指函数的返回值是void,但doSomething的返回值却是int

funcPtrArray[0] = &doSomething;                             //错误!类型不符
funcPtrArray[0] = reinterpret_cast<FuncPtr>(&doSomething);  //这样便可通过编译

某些情况下这样的转型可能会导致不正确的结果(如条款31),所以你应该尽量避免将函数指针转型。

补充:

  • More Effective C++没有过多的对reinterpret_cast操作符进行解释,但我觉得应该对它进行更多说明,因为它实在是太强大了,也应该对使用规则做出足够多的说明
  • reinterpret_cast通过重新解释底层位模式在类型之间进行转换。它将expression的二进制序列解释成new_type,函数指针可以转成void*再转回来。reinterpret_cast很强大,强大到可以随便转型。因为他是编译器面向二进制的转型,但安全性需要考虑。当其他转型操作符能满足需求时,reinterpret_cast最好别用。
  • 更多了解可看cpp reference reinterpret_cast

总结:

在程序中使用新式转型法,比较容易被解析(不论是对人类还是对工具而言),编译器也因此得以诊断转型错误(那是旧式转型法侦测不到的)。这些都是促使我们舍弃C旧式转型语法的重要因素

条款3:绝对不要以多态(polymorphically)方式处理数组

假设你有一个class BST及一个继承自BST的class BalancedBST;

class BST {};
class BalancedBST : public BST {};

现在考虑有个函数,用来打印BSTs数组中的每一个BST的内容

void printBSTArray(ostream& s, const BST array[], int numElements)
{
    for (int i = 0 ; i < numElements; ++i)
    {
        s << array[i];      // 假设BST objects 有一个
                            // operator<< 可用
    }
}

当你将一个由BST对象组成的数组传给此函数,没问题:

BST BSTArray[10];
printBSTArray(cout, BSTArray, 10);      // 运行良好

然而如果你将一个BalancedBST对象所组成的数组交给printBSTArray函数,会发生什么事?

BalancedBST bBSTArray[10];
printBSTArrat(cout, bBSTArray, 10);     // 可以正常运行吗?
  • 此时就会发生错误,因为array[i]代表的时*(array+i),编译器会认为数组中的每个元素时BST对象,所以array和array+i之间的距离一定是i*sizeof(BST)
  • 然后当传入由BalancedBST对象组成的数组,编译器会被误导。它仍假设数组中每一元素的大小是BST的大小,但其实每一元素的大小是BalancedBST的大小。因此当BalancedBST的大小不等于BST的大小时,会产生未定义的行为
  • 当尝试通过一个·base class·指针,删除一个由derived class objects组成的数组,上述的问题还会再次出现,下面是你可能做出的错误尝试

void deleteArray(ostream& os,BST array[])
{
	os << "Delete array,at address" << 
		static_cast<void*>(array) << 'n';
	delete []array;
}

编译器看到这样的句子

delete[] array;

会产生类似这样的代码,问题也就跟之前一样出现了

for(int i = the number of elements in the array-1; i >= 0; --i)
{
    array[i].BST::~BST();       // 调用array[i]的 destructor
}

总结:

  • 多态和指针算术不能混用,数组对象几乎总是涉及指针的算术运算,数组和多态不要混用

条款4:非必要不提供default constructor

后续看过条款43,再回头来补充

总结:

  • 添加无意义的default constructors,也会影响classes的效率。如果class constructors可以确保对象的所有字段都会被正确地初始化,为测试行为所付出的时间和空间代价都可以免除。如果default constructors无法提供这种保证,那么最好避免让default constructors出现。虽然这可能会对classes的使用方式带来某种限制,但同时也带啦一种保证:当你真的使用了这样的classes,你可以预期它们所产生的对象会被完全地初始化,实现上亦富有效率