基础议题

条款1:仔细区别pointers和references

  • 首先你必须认知一点,没有所谓的 null reference。
  • 由于reference一定得代表某个对象,C++因此要求references必须有初值。
  • Pointers和references之间的另一个重要差异就是,pointers可以被重新赋值,指向另一个对象,reference却总是指向(代表)它最初获得的那个对象。
  • 结论:当你知道你需要指向某个东西,而且绝不会改变指向其他东西,或是当你实现一个操作符而其语法需求无法由pointers达成,你就应该选择references。任何其他时候,请采用pointers。

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

  • C++提供的4种新的转型操作符,相比于C风格的转型,尽管其语法看起来又臭又长。但它提供了严谨的意义以及更强的辨识度。
  • 如果你在程序中使用新式转型法,比较容易被解析(不论是对人类还是对工具而言),编译器也因此得以诊断转型错误(那是旧式转型法侦测不到的)。
  • 让转型动作既丑陋又不易键入(typing),或许未尝不是件好事。

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

  • 因为基类和派生类大小不一样,函数传参时如果是形参是基类数组,实参是派生类的数组,这时在函数内循环遍历基类数组的结果就是不可预期的。
  • 简单地说,多态和指针算术不能混用。数组对象几乎总是会设计指针的算术运算,所以数组和多态不要混用。

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

class EquipmentPiece {
public:
  EquipmentPiece(int IDNumber = UNSPECIFIED);
  ...
private:
  static const int UNSPECIFIED;  //一个某数数字,意味没有被指定ID值。
};
  • 这就是一个添加了default constructor的class,这样我们就可以直接通过

EquipmentPiece e;
这样的语句来产生一个EquipmentPiece的object。虽然方便了使用,但是这几乎总是会造成class内的其他member functions变得复杂。

  • 添加无意义的default constructor,会影响classes的效率。如果member functions必须测试字段(指的是default constructor被赋了默认值的必备字段)是否真的被初始化了,其调用者便必须为测试行为付出时间代价,并为测试代码付出空间代价,因为可执行文件和程序库都变大了。万一测试结果为否定,对应的处理又需要一些空间代价。如果class constructors可以确保对象的所有字段都被正确地初始化,上述所有成本便都可以免除。如果default constructor无法提供这种保证,那么最好避免让default constructor出现。虽然这可能会对classes的使用方式带来某种限制,但同时也带来一些保证:当你真的使用了这样的classes,你可以预期它们所产生的对象会被完全地初始化,实际上亦富有效率。

操作符

条款5:对定制的“类型转换函数”保持警觉

  • 单自变量constructors和隐式类型转换操作符可以让自定义类型实参隐式转换。
  • 重载类型转换操作符可能会导致错误(非预期)的函数被调用。解决办法就是以功能对等的另一个函数取代重载的类型转换操作符(参考std::string的c_str)。
  • 将constructors声明为explicit,使得编译器不能因隐式转换的需要而调用它们,只能显示的调用。通过这种方式可以消除单自变量constructors引起的隐式转换。

条款6:区别increment/decrement操作符的前置和后置形式

  • 重载increment前置式和后置式操作符的范式(decrement同理):
// 前置式:累加然后取出(increment and fetch)
UPInt& UPInt::operator++()
{
*this +=1;
return *this;
}
// 后置式:取出然后累加(fetch and increment)
const UPInt UPInt::operator++(int)
{
UPInt oldValue = *this;
++(*this);
return oldValue;
}
  • 为了使类似i++++这样的调用不合法,所以重载后置式increment操作符的返回值必须是const的。
  • 后置式increment和decrement操作符的实现应以其前置式兄弟为基础。如此一来你就只需维护前置式版本。
  • 因后置式increment(decrement)函数会返回一个临时对象,所以它天生效率没有其前置式兄弟高(主要对自定义类型)。

条款7:千万不要重载&&,||和,操作符

  • C++对于“真假值表达式”采用所谓的“骤死式”评估方式。意思是一旦该表达式的真假值确定,即使表达式中还有部分尚未检验,整个评估工作仍告结束。
  • 而如果重载了&&或||操作符则他们变成了函数调用,函数调用则没办法实现类似的“骤死式”语法,因为函数调用必须所有参数都评估完成。而且也无法保证表达式的评估顺序,因为C++语言规范并未明确定义函数调用动作中各参数的评估顺序。
  • 逗号操作符也有类似问题。表达式如果内含逗号,那么逗号左侧会先被评估,然后逗号的右侧再被评估;最后,整个逗号表达式的结果以逗号右侧的值为代表。而如果重载它,无法模仿这些行为。

条款8:了解各种不同意义的new和delete

  • 如果你希望将对象产生于heap,请使用new operator。它不但分配内存而且为该对象调用一个constructor。如果你只是打算分配内存,请调用operator new,那就没有任何constructor会被调用。如果你打算在heap objects产生时自己觉得内存分配方式,请写一个自己的operator new,并使用new operator,它将会自动调用你所写的operator new。如果你打算在已分配(并拥有指针)的内存中构造对象,请使用placement new。
  • 总结关系如下:
new <->delete
operator new <-> operator delete //分配、释放内存
placement new <-> placement delete //在指定内存上构造、析构对象
new[] <-> delete[]
  • operator new调用示范:
void *rawMemroy = operator new(sizeof(string));
  • plcement new示范:
class Widget {
public:
  Widget(int widgetSize);
  ...
};
Widget* constructWidgetInBuffer(void* buffer, int widgetSize)
{
  return new (buffer) Widget(widgetSize);
}

吴尼玛
32 声望12 粉丝

记问之学