(C++) More Effective C++ 笔记:操作符(Operators)

Posted by

on

C++ 中提供了强大的操作符重载功能。More Effective C++ 第二章操作符重点介绍了一些使用 C++ 操作符的条款。

操作符(Operators)章节介绍了以下条款:

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

条款六:区别 increment/decrement 操作符的前置(prefix)和后置(postfix)形式

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

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

条款五:尽量避免隐式类型转换(Implicit Conversions)

我们在书写程序的时候不可避免地会遇到隐式类型转换。最简单的例子如下:

int iNumber = 65537;
short sNumber = iNumber;    // iNumber 的数字将作为 short 类型提供给 sNumber。编译器可能会对此行为给出警告,因为这么做会丢失精度。

这个例子经常在代码中可以遇到,所以辨识度极高。但如果是自己书写的类,就不会这么容易发现了。自定义的数据格式(比如一个类)参与运算的过程中,编译器知道此类型数据(比如书中提到的 Rational class, 有理数类)无法直接参与运算(比如与 double 数据做乘法),编译器便会想尽一切办法(包括隐式转换,调用内部转换函数等)让函数调用成功。这么做会使程序的目的显得比较隐晦,不利于直接暴露出开发者的意图。这体现了它的缺点:一旦误用,他会导致非预期的错误或结果。

书中还提到一个典型例子,那就是将类数据直接与内建数据类型比较。这一定会触发隐式类型转换(如果条件满足)。

bool operator==(const Array<int>& lhs, const Array<int>& rhs);
Array<int> a(10);
Array<int> b(10);
// ...
for(int i = 0; i < 10; i++) {
    if(a == b[i]) {
    // 如果 Array 类中有构造函数允许将 int 初始化一个新的 Array 类,那么编译器就会将这条语句解释为:
    if(a == Array(b[i])) {
    // 这么做不仅效率低下,而且很难达到预期效果
    }
}

解决方法:公开一个显式的成员函数供客户(Clients)使用。最典型的例子莫过于 std::string::c_str() 了。当一个函数接受 const char* 类型数据的时候,如果直接传入 std::string 便会报错。正确的做法便是使用 c_str() 方法。

对于类的构造函数,可以使用 C++ 的新特性:给构造器(constructors)函数前加 explicit 修饰。这样就可以避免构造函数被隐式调用。

条款六:仔细区分自增(++)自减(–)操作符的放置位置

i++ 代表先用值再自增,++i 代表先自增再用新值。C++ 分别允许重载 ++ 和 — 这两种操作符的前置式(prefix)与后置式(postfix)。

重载这两种操作符时,一定要注意重载函数的返回值。

class UPInt {
    public:
        UPInt& operator++();         // 前置++
        const UPInt operator++(int); // 后置++

        UPInt& operator--();         // 前置--
        const UPInt operator--(int); // 后置--
};

前置式最好返回自身的引用,后置式最好返回 const 修饰的不可修改的原始数值的临时类。否则,我们便会遇到 UPInt i; i++++; 的问题。

当我们无法确定我们的行为是否正确的时候,设计 classes 的宝典就是:看看内建 int 类型怎么做,并尊从它。 

条款七:禁止重载 “&&”  “||” “,” 操作符

在使用 if 语句的时候,我们经常会利用 && 与 || 带来便利性。例如(假定 p 是一个 const char *):if( p && strlen(p) ) ,如果 p 是 nullptr 那么后面的 strlen(p) 便不会执行也不会因此导致出现非法内存访问错误。|| 则是相反的动作。原书将此行为定义为:骤死式语义

如果我们重载了 && 或 ||,那么:

if( CMyClass1 && CMyClass2 )
// 将会被编译器理解为
if( CMyClass1.operator&&(CMyClass2) )
// 或
if( operator&&(CMyClass1, CMyClass2) )

那么骤死式语义将因此失效。同理,逗号表达式的结果以逗号右侧值为代表,我们无法保证逗号表达式左侧右侧操作数是否被检测。

因此,我们禁止重载 “&&”  “||” “,” 操作符。

条款八:了解 new 与 delete

书中提出了 new operatoroperator new 这两种 new 法,越看越容易混淆。我们常常直接书写 new,因为我们这里统一讨论。

其实 new 与 delete 的职责是分配/回收内存并调用构造/析构函数(如果是类)。

Placement New

普通的 new 会自动分配内存空间,而 placement new 允许在已有的内存上直接构建一个类。

用法:

#include <new>
new (buffer) CLASS_NAME(...);

注意:使用 placement new 构建的类,不可以直接使用 delete 删除。因为这个类的内存不是通过 new 分配的,而是原来通过其他手段获得的。解决方案是手动调用析构函数,并利用原有渠道返还这段既有的内存。


Leave a Reply

Your email address will not be published. Required fields are marked *