条款 21 - 条款 30 | 《Effictive C++》笔记
Effictive C++ 条款 21 - 条款 30。
条款 21:必须返回对象时,别妄想返回其 reference
不要返回指针或引用指向一个局部静态对象。
条款 22:将成员变量声明为 private
protected 并不比 public 更具封装性。
假设我们有一个 protected 成员变量,而我们最终取消了它,所有使用它的派生类都会被破坏,所以 protected 成员变量就像 public 成员变量一样缺乏封装性。其实只有两种访问权限: private(提供封装)和其他(不提供封装)。
条款 23:宁以 non-member non-friend 替换 member 函数
non-member non-friend 函数比 member 函数封装性更好。
对于一个类成员,我们以可以访问到这个成员的函数数量来衡量其封装性。如果要在一个 member 函数和一个 non-member non-friend 函数之间做抉择,而且两者提供相同机能,那么导致较大封装性的是 non-member non-friend 函数,因为它并不增加“可以访问到类内 private 成员”的函数数量。
比较自然的做法是让该函数成为一个 non-member 函数并且位于目标类所在的同一个命名空间内。
条款 24:若所有参数皆需类型转换,请为此采用 non-member 函数
当重载双目运算符的时候,如果在类内部重载,则该双目运算符无法支持混合运算,解决方法是将该重载函数设成 non-member 形式。
1 | class Rational{ |
如果进行混合运算:
1 | result = oneHalf * 2; // 正确,相当于 oneHalf.operator*(2) |
这是因为 2 不是 Rational,不能作为左操作数。oneHalf*2 会把 2 隐式转换为 Rational 类型。上面两种做法,第一种可以发生隐式转换,第二种却不可以,这是因为只有当参数被列于参数列(parameter list)内,这个参数才是隐式类型转换的合格参与者。第二种做法,还没到到“参数被列于参数列内”,2 不是 Rational,不会调用operator*。如果要支持混合运算,可以让 operator* 成为一个 non-member 函数,这样编译器可以在实参身上执行隐式类型转换。
1 | const Rational operator*(const Rational& lhs, const Rational& rhs); |
条款 25:考虑写出一个不抛异常的 swap 函数
由于 p_impl 是 Widget 的私有成员,所以为了实现 swap,我们令 Widget 声明一个名为 swap 的 public 成员函数做真正的 swap,然后将 std::swap 特化,令它调用该成员函数:
1 | class Widget { |
如果 Widget 是模板类,由于 C++ 不允许对模板函数进行偏特化,通常做法是简单地位其添加一个重载版本:
1 | namespace std { |
虽然以上代码可以编译运行,但是最好不要往 std 命名空间中添加任何东西,所以应该在 Widget 类所在的命名空间定义一个 non-member swap 函数:
1 | namespace WidgetStuff { |
在使用时,我们希望优先调用 Widget 专属版本,并且在该版本不存在的情况下调用 std 内的通用版本:
1 | template<typename T> |
总结一下:
- 提供一个更加高效的,不抛异常的公有成员函数(如 Widget::swap)
- 在类(或类模板)的同一命名空间下提供非成员函数 swap,调用类的成员函数
- 如果你写的是类而不是类模板,请偏特化 std::swap,同样应当调用类的成员函数
- 调用时,请首先用 using 使 std::swap 可见,然后直接调用 swap
条款 26:尽可能延后变量定义式的出现时间
不仅仅应该延后变量的定义到非得使用该变量的前一刻为止,甚至还应该尝试延后这份定义直到能够给它初值实参为止。这样不仅能够避免构造和析构非必要对象,还可以避免无意义的默认构造行为。
条款 27:尽量少做转型动作
- 如果可以,尽量避免转型,特别是在注重效率的代码中避免 dynamic_cast,如果有个设计需要转型动作,试着发展无需转型的替代设计
- 如果转型是必要的,试着将它隐藏于某个函数背后,客户随后可以调用该函数,而不需将转型放进他们自己的代码内
- 使用新式转型,不要使用旧式转型
诸如 int(float_num)
或者 (int)float_num
的形式为“旧式转型”,新式转型有如下四种:
const_cast<int>(const_var)
:移除变量的 const 或 volatile 限定符dynamic_cast<Derived*>(base_ptr)
:将基类的指针或引用安全地转换成派生类的指针或引用,并用派生类的指针或引用调用非虚函数reinterpret_cast<char*>(unsigned_char_ptr)
:用来处理无关类型之间的转换,会产生一个新的值,这个值会有与原始参数有完全相同的比特位;实际动作及结果可能取决于编译器,这也就意味着不可移植static_cast<int>(float_var)
:强制隐式转换
条款 28:避免返回 handles 指向对象内部成分
避免返回 handles (包括引用、指针、迭代器)指向对象内部。
条款 29:为“异常安全”而努力是值得的
异常安全函数提供以下三个保证之一:
- 基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下
- 强烈保证:如果异常被抛出,程序状态不改变,如果函数成功,就是完全成功,如果函数失败,程序会回复到“调用函数之前”的状态
- 不抛掷保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能
“强烈保证”往往能够以 copy-and-swap 实现出来:为你打算修改的对象做出一份副本,然后在副本上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换。
但是如果在修改副本和完成置换这两个操作之间有一些无法进行强烈保证的函数,那么这整个函数的强烈保证也就无从谈起。
条款 30:透彻了解 inlining 的里里外外
即使拥有虚内存,内联造成的代码膨胀亦会导致额外的换页行为,降低指令高速缓存命中率,以及伴随这些而来的效率损失。换个角度说,如果内联函数的本体很小,编译器针对“函数本体”所产出的码可能比针对“函数调用”所产出的码更小,将函数内联确实可能导致较小的目标码和较高的指令高速缓存命中率。
将函数定义与类定义式内是函数内联的隐喻方式。
内联函数要在头文件内定义,因为内联发生在编译期,这就要求所有使用内联函数的文件都要包含有内联函数,内联函数放在头文件内就不用复制代码了。
模板的定义也要放在头文件内,因为模板在没有确定具体类型的时候是无法编译的,所以声明和实现分开的做法也是不可取的,必须把实现全部写在头文件里面。
大部分编译器拒绝将太过复杂(例如带有循环或递归)的函数内联,而所有对虚函数的调用也会使内联声明失效。如果程序内要对某个内联函数取地址,那么编译器也会生成一个非内联函数本体。
有时候编译器会生成构造函数和析构函数的非内联函数副本,如此一来它们就可以获得指针指向那些函数,在数组内部元素的构造和析构过程中使用。
条款 21 - 条款 30 | 《Effictive C++》笔记