More Effective C++ 笔记 (一)
目录

01. 仔细区别pointers和reference

  • reference不能为null;pointer可以为null。
  • reference必须有初值;pointer可以不赋初值。因此使用reference之前不需要测试其有效性。
  • reference不能重新赋值,因此不能改变其所指对象;pointer可以重新赋值,改变其所指对象。

在有些情况下,如operator[], operator+=操作符,必须返回reference。

02. 最好使用C++转型操作符

  • static_cast
  • const_cast
  • dynamic_cast 在继承体系中基类pointer/reference转派生类pointer/reference,失败返回null或exception。无法应用在缺乏虚函数的类型上(#24)。
  • reinterpret_cast 平台依赖。最常用的是转换“函数指针”类型。

03. 绝对不要以多态(polymorphically)方式处理数组

class A {...};
class B : public A {...};

void printAry(const A array[], int len) {
for(int i=0; i<len; i++)
cout << array[i];
}

void deleteAry(A array[]) {
delete [] array;
}

对于函数printAry, array[i]等价于*(arary+i),每个元素之间的尺寸间隔是sizeof(A)。 因此如果数组中的元素是派生类对象(B),这种方式将产生错误。

对于函数deleteAry, 同样,如果array[]中存储派生类对象B,那么执行delete [] array时, 将对每一个元素执行A::~A(),这个操作执行在对象B上结果是未定义的。

04. 非必要不提供 default constructor

有需求才需提供default constructor。

P18页,针对placement new的使用方法例子。


05. 对定制的“类型转换函数”保持警觉

用户定义对象的两种隐式转换:

  1. 单自变量constructors
  2. 隐式类型转换操作符: 如operator double() const;

消除非预期的隐式转换方法是:

  1. 对constructors使用explicit关键字
  2. 显示转换为目标类型,如double asDouble() const;

对于C++,隐式类型转换有一条规则:隐式转换次数不能超过一次。 因此针对这个规则,使用proxy object也可以消除不必要的隐式转换。

06. 区别increment/decrement操作符的前置(prefix)和后置(postfix)形式

class UPInt {
UPInt& operator++();
const UPInt operator++();
};

UPInt* UPInt::operator++() {
*this += 1;
return *this;
}

const UPInt UPInt::operator++(int) {
UPInt oldValue = *this;
++(*this);
return oldValue;
}

UPint;
++i;           // i.operator++();
i++;           // i.operator++(0);

注意++i和i++返回值的区别。 造成这种情况的原因是:

  • ++++i是合法的
  • i++++错误的,i的最终值很难确定

07. 千万不要重载&&, ||和, 操作符

如果重载&&和||操作符,就没有办法提供骤死式语义,和标准规则相违背。

如果重载,操作符,则不能保证,左边的表达式比,右边的表达式先计算,和标准规则相违背。

更多可见C++ 操作符重载

08. 了解各种不同意义的new和delete

operator new: 只分配内存,不会调用constructor,相当于malloc

void * operator new(size_t size);

new operator: 隐式调用operator new, 取得operator new返回的内存并调用constructor将之初始化。

new Widget(WidgetSize)

placement new: new operator用法之一,在给定空间(buffer)上隐式调用operator new,取得operator new返回的内存并调用constructor将之初始化。

new (buffer) Widget(WidgetSize)

// 这时的operator new
void * operator new(size_t, void *buffer) {
return buffer;
}

operator delete: 释放内存,相当于free()

void operator delete(void *mem);

delete operator: 调用析构函数,调用operator delete释放内存

delete ps;

使用placement new产生的对象不能使用delete operator删除。 只能手动调用析构函数,然后使用指定函数释放。

pw->~Widget();
freeShared(pw);

operator new[]/operator delete[]: 给数组申请一块区域,针对每个对象调用一个constructor/destructor

operator new []
operator delete []

09. 利用destructors避免泄露资源

为了防止指针指向的对象因为忘记或者异常没有释放, 可以使用智能指针自动释放,或者把资源封装在对象内,在语句块退出时自动调用对象的destructors释放资源。

10. 在constructors内阻止资源泄露(resource leak)

要适当处理“构造过程constructors中可能发生的exception”。 处理方法有:

  • 在constructors中捕捉异常,释放已申请的资源并抛出异常
  • 使用auto_ptr封装资源(推荐)

11. 禁止异常(exception)流出destructors之外

理由:

  1. 避免terminate函数在exception传播过程的栈展开(stack-unwinding)机制中被调用
  2. 协助确保destructors完成其应该完成的所有事情。

12. 了解“抛出一个异常”、“传递一个参数”和“调用一个虚函数”的差异

  • 函数参数有by value, by pointer, by reference的方式。
  • 异常传递有by value和by reference的方式。

但是异常不论catch是以哪种方式捕捉,必定发生Exception对象的复制行为。 因为向上抛出会离开Exception对象的作用域,因此需要复制一个副本(临时对象)。 任何修改catch得到的异常对象都是修改的那个副本,即使是以by reference形式传递。

异常副本的创建是通过copy constructor。 需要注意:复制动作永远是以对象的静态类型为本

下面两个抛出是有差异的:

catch (Widget& w)
{
...
throw;   // 抛出当前异常,动态类型延续
}

catch (Widget& w)
{
...
throw w;  // 抛出当前异常的副本,转为静态类型
}

因此:

catch (Widget w) ...        // 两个副本的构造代价
catch (Widget& w) ...       // 一个副本的构造代价

异常捕捉也不会发生函数调用那样的隐式类型转换(比如catch(double)不会捕捉throw int)。 只允许以下两种:

  • 派生类到基类
  • 有型指针到无型指针(void*)

在异常匹配catch时,其处理机制是first fit最先吻合策略。 这和虚函数采用的best fit最佳吻合策略不同。

13. 以by reference方式捕捉exceptions

4个标准异常抛出的都是对象:

  • bad_alloc:operator new抛出
  • bad_cast:dynamic_cast作用于reference时抛出
  • bad_typeid:dynamic_cast作用于null指针时抛出
  • bad_exception:适用于未预期的异常

三种捕捉方式:

  • by pointer: 和上面不兼容、释放问题
  • by value: 效率,切割问题
  • by reference: 推荐!

14. 明智运用exception specifications

  1. 不应该将templates和exception specifications混合使用。
  2. 如果A函数内调用了B函数,而B函数无exception specifications, 那么A函数本身也不要设定exception specifications。
  3. 处理“系统”可能抛出的exceptions。

15. 了解异常处理(exception handling)的成本

编译器生成的代码中支持异常的运行成本,代码膨胀的成本。


16. 谨记80-20法则

一个程序80%的资源用于20%的代码身上。

17. 考虑使用lazy evaluation(缓式评估)

4种用途:

  • Reference Counting: 类似copy-on-write的技术
  • 区分读和写: 写时才要修改数据,写的代价更高
  • Lazy Fetching: 如果数据需要用从网络或磁盘上获取,可以等到最终使用时才获取
  • Lazy Expression Evaluation: 计算的结果等到最终使用时才计算

18. 分期摊还预期的计算成本

与lazy evaluation相反,over-eager evaluation(超急评估)指的是:在被要求之前就把事情完成。 over-eager evaluation的背后观念是:如果你的预期程序常常会用到某个计算,你可以降低每次计算的平均成本。 有以下几种做法:

  • caching: 使用cache将已计算好的而且有可能再用到的数值保留下来
  • prefetching: 使用预先取出做法,这常常用在硬件上,比如磁盘读取、指令读取

这些做法都可以看成是空间换时间。

19. 了解临时对象的来源

临时对象:没有命名的non-heap object。 通常发生于两种情况:

  • 隐式类型转换:在参数为by value或reference-to-const传递时会发生(在reference-to-non-const时不会发生,因为如果可以的话,会允许临时对象被修改)
  • 函数返回对象:通过RVO优化

20. 协助完成“返回值优化(RVO)”

有些函数一定要by-value返回(比如operator*),因此可以使用constructor arguments取代返回的对象,完成retrun value optimization(RVO)。

const Rational operator*(const Rational* lhs, const Rational& rhs) {
return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}

Rational a = 10;
Rational b(1, 2);
Rational c = a * b;

21. 利用重载技术(overload)避免隐式类型转换(implicit type conversions)

class UPInt {
public:
UPInt(int i);
...
const UPInt operator+(const UPInt& lhs, const UPInt& rhs);
};

upi3 = upi1 + upi2;
upi3 = upi1 + 10;          // 将产生隐式类型转换,int->UPInt,再调用operator+

每个“重载操作符”必须获得至少一个“用户自定义类型”的自变量:

class UPInt {
public:
UPInt(int i);
...
const UPInt operator+(const UPInt& lhs, const UPInt& rhs);
const UPInt operator+(const UPInt& lhs, int rhs);
const UPInt operator+(int lhs, int rhs);                  // error!
};

upi3 upi1 + 10;           // 通过重载调用operator+(const UPInt&, int)

22.考虑以操作符复合形式(op=)取代其独身形式(op)

CPP并不为复合形式(如operator+=)和独身形式(operator+)设立关系, 如果实现了operator+,也必须自己实现operator+=。

但是如果都要实现两者,我们可以先实现operator+=,再以此基础实现operator+

class A {
T& operator+=(const T&)
{
...
}

const T operator+(const T& rhs)                 // 成员函数
{
return T(*this) += rhs;                     // 匿名对象有助于返回值优化RVO
}
};

template<typename T>
const T operator+(const T& lhs, const T& rhs)       // 全局函数
{
return T(lhs) += rhs;
}

23. 考虑使用其它程序库

iostream比stdio有类型安全,可扩充等优点,但是效率上慢。 iostream编译器就决定了操作数的类型,而stdio在运行期才解析其格式字符串。

24. 了解virtual functions, multiple inheritance, virtual base classes, runtime type identification成本

  1. virtual functions需要通过vptr找到vtbl,再在vtbl中找出真正调用函数的指针。而且virtual function无法inlined。
  2. 在multiple inheritance下,一个对象会有多个vptrs(每个base class各一个),因此通过vtbls找到真正调用函数也更耗时一些。
  3. virtual base class中virtual继承通过再增加指向基类对象的隐藏指针来实现。
  4. RTTI必须通过typeid取得class的type_info对象。这可能通过在class的vtbl中增加一项来实现。

发表评论