0%

2 操作符

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

  1. C++允许编译器在不同类型之间执行隐式转换implicit conversions)。它继承C的传统,允许将char转换为int,将short转换为double。然而还有令人害怕的转型,包括将int转换为short,将double转换为char,这些类型转换会遗失信息。所以我们可以使用自己的类型。两种函数允许编译器执行这样的转换:单自变量constructors隐式类型转换操作符。所谓的单自变量constructor是指能够以单一自变量成功调用的constructors:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Name
    {
    public:
    // string to Name
    Name(const std::string &s);
    ...
    };
    class Rational
    {
    public:
    ...
    // Rational to double
    operator double() const;
    }
    ,而隐式类型转换操作符,是一个拥有奇怪名称的member function:关键词operator后面加上一个类型名称,你不能为此函数指定返回值类型,因为其返回值类型已经表现在函数名称上了:
    1
    2
    3
    4
    5
    6
    7
    8
    class Rational 
    {
    public:
    ...
    operator double() const;
    };
    Rational r(1, 2); // r == 1/2
    double d = 0.5 * r; // 将r转为double,然后执行乘法运算
  2. 假设有一个class用来表现分数(rational numbers)。你希望像内建类型一样地输出Rational objects内容。
    1
    2
    Rational r(1, 2);
    std::cout << r;
    假设忘了给Rational写一个operator<<,上述打印不但不会出错,编译器在面对上述动作,它会想尽各种办法(包括找出一系列可接受的隐式类型转换)让函数调用动作成功。而之间的代码发现,只要调用了Rational::oeprator double,将r转换为double,调用动作就能成功,却又有隐式类型转换操作符的缺点:它的出现可能到导致错误(非预期)的函数被调用。于是有了解决办法:
    1
    2
    3
    4
    5
    6
    7
    8
    class Rational
    {
    public:
    ...
    double asDouble() const;
    };
    std::cout << r; // error
    std::cout << r.asDouble(); // ok, 以double形式输出r
    必须明白调用类型转换函数虽然有点不便,却可以避免默默调用了不想调用的函数。这也就是为什么标准程序库的string类型并未含有从string object到C-style char*的隐式转换函数,而是提供了一个显式的member function c_str
    在单自变量constructors,这些函数造成的隐式类型转换的情况更难对付:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    template<class T>
    class Array
    {
    public:
    Array(int lowBound, int highBound);
    Array(int size);
    T& operator[](int index);
    ...
    };
    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]) // 如果打算写a[i] == b[i]
    {
    do something for when a[i] and b[i] are equal;
    }
    else
    do something for when they're not;
    这样的错误编译器并没有提醒,它一声不吭,编译器注意到它,发现只要调用Array<int> constructor(需要一个int自变量)就可以将int转为Array<int> object。于是它放手去做。
    有两种办法阻止编译器这么做:一个是简易法,另一个可在编译器不支持简易法的情况下使用。
    简易法是使用C++特征:关键词explicit。只要将constructors声明为explicit,编译器就能因隐式转换而调用它,不过显式类型转换是允许的(没发现)。有个解决方案:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    template<class T>
    class Array
    {
    public:
    class ArraySize
    {
    public:
    ArraySize(int numElements) : theSize(numElements) {}
    int size() const { return theSize; }
    private:
    int theSize;
    };
    Array(int lowBound, int highBound);
    Array(ArraySize size);
    ...
    };
    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]) // error
    编译器需要一个Array<int>的对象在==右边,但此时没有这样隐式转换的constructor。更不能将int转换一个临时性的ArraySize对象,再根据这个临时对象产生必要的Array<int>对象,所以报错。类似ArraySize这样的classes,往往被称为proxy classes,因为它的每一个对象都是为了其他对象而存在的,好像其他对象的代理人proxy)一样。这是一个很值得学习的技术。

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

  1. 重载函数是以其参数类型来区分彼此的,然而不论increment或decrement操作符的前置式或后置式,都没有参数,为了填平这个语言学上的漏洞,只好让后置式有一个int自变量,并且让它被调用时,编译器默默地为该int指定一个0值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class UPInt
    {
    public:
    UPInt& operator++(); // 前置式++
    const UPInt operator++(int); // 后置式++
    UPInt& operator--(); // 前置式--
    const UPInt operator--(int); // 后置式--
    UPInt& operator+=(int); // +=操作符
    ...
    };
    UPInt i;
    ++i; // i.operator++();
    i++; // i.operator++(0);
    --i; // i.operator--();
    i--; // i.operator--(0);
  2. 所谓的increment操作符的前置式意义“increment and fetch“(积累然后取出),后置式意义”fetch and increment“(取出然后累加):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    UPInt& UPInt::operator++()
    {
    *this += 1;
    return *this;
    }
    const UPInt UPInt::operator++(int)
    {
    UPInt oldValue = *this;
    ++(*this);
    return oldValue;
    }
    为什么后置increment操作符返回的对象是const呢?以下:
    1
    2
    3
    UPInt i;
    i++++; // 后置increment操作符两次
    i.operator(0).operator++(0);

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

  1. 操作符重载也不是任何都能用来重,它也是有底线的,你不能重载以下操作符:
    1
    2
    3
    .       .*      ::      ?:      new 
    delete sizeof typeid static_cast
    dynamic_cast const_cast reinterpret_cast
    可以重载的有:
    1
    2
    3
    4
    5
    6
    7
    operator new        operator delete
    operator new[] operator delete[]
    + - * / % ^ & | ~
    ! = < > += -= *= /= %=
    ^= &= |= << >> >>= <<= == !=
    <= >= && || ++ -- , ->* ->
    () []

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

  1. new operator和operator new之间是有差别的。

    1
    string *ps = new string("Memory Management");

    所使用的new是所谓的new operator。这个操作符是语言内建的,跟sizeof一样不能改变意义。它的分为两方面:分配足够的内存用来存放某类型的对象而后调用一个constructor为刚才分配的内存中的那个对象设定初值
    我们能够重写改函数,改变其行为。这个函数名称叫operator new。它的函数原型是:

    1
    void * operator new(size_t size);

    size_t参数表示需要分配多少内存。然后返回一块原始的内存。和malloc一样,operator new唯一任务是分配内存。但它不知道什么是constructors。当你想要调用一个constructor,有个特殊版本的operator new,称为placement new,允许你这么做:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Widget
    {
    public:
    Widget(int widgetSize);
    ...
    };
    Widget* constructorWidgetInBuffer(void *buffer, int widgetSize)
    {
    return new(buffer) Widget(widgetSize);
    }

    这是new operator的用法之一,指定一个额外自变量(buffer)作为new operator隐式调用operator new时所用。于是,被调用的operator new除了几首size_t变量外,还接受了一个void*参数,这就是所谓的placement new,看起来像这样:

    1
    2
    3
    4
    void* operator new(size_t, void *location)
    {
    return location;
    }

    如果你希望将对象产生于heap,请使用new operator,它不但分配内存而且为对象调用一个constructor。如果只是打算分配内存,调用operator new,那没有任何constructor被调用。如果打算在heap objects产生时决定内存分配方式,写一个自己的operator new。

  2. 为了避免resource leaks资源泄露),每一个动态分配行为都必须匹配一个相应但相反的释放动作。内存释放动作是由operator delete执行:

    1
    2
    3
    4
    string *ps;
    ...
    delete ps;
    void operator delete(void *memoryToBeDeallocated);

    因此,delete动作会产生类似代码:

    1
    2
    ps->~string();
    operator delete(ps);

    这里呈现的暗示是,如果只打算处理原始的、未设初值的内存,应该回避new operator和delete operators。如果使用了placement new产生对象,避免对该内存使用delete operator。因为delete operator会调用operator delete来释放内存,但该内存并非由operator new分配得来的。placement new只是返回它接受的指针而已,所以未了抵消该对象的constructor的影响,应该直接调用该对象的destructor。

  3. 面对数组情况,也有所不同:

    1
    string *ps = new string[10];

    上述的new仍然是new operator,但由于诞生的是数组,所以new operator的行为不再以operator new分配,而是由一个名为operator new[]负责分配(通常称为array new)。同样道理,当delete operator被用于数组,它会针对数组中的每个元素调用其destructor,然后调用operator delete[]释放内存。
    总结以下,new operator和delete operator都是内建操作符,无法为你所控制,但是它们所调用的内存分配/释放函数则不然。