[C++]关于重载运算符的一些建议

运算符

1. 谨慎定义类型转换函数

有两种函数允许编译器进行这些的转换:单参数构造函数(single-argument constructors)和隐式类型转换运算符。单参数构造函数是指只用一个参数即可以调用的构 造函数。该函数可以是只定义了一个参数,也可以是虽定义了多个参数但第一个参数以后的 所有参数都有缺省值。

第一个例子:

class Name {
public:
  Name(const string& s);
... }; 
class Rational {
public:
  Rational(int numerator = 0,
           int denominator = 1);
... 
// for names of things
// 转换 string 到 
// Name 
// 有理数类 
// 转换 int 到 
// 有理数类 
};

第二个例子:隐式类型转换运算符只是一个样子奇怪的成员函数:operator 关键字,其后跟一个类
型符号。

class Rational { 
public: ... 
  operator double() const;
};
// 在下面这种情况下,这个函数会被自动调用: Rational r(1, 2); 
double d = 0.5 * r;
// 转换 Rational 类成 
// double 类型 
// r 的值是 1/2 
// 转换 r 到double, 
// 然后做乘法 

隐式类型转换可能出现的问题:

#include <iostream>
using namespace std;
class rational {
public:
    rational(double a, double b) {
        val = a / b;
    }
    operator double() {
        return val;
    }
private:
    double val;
};
int main() {
    rational test(3, 4);
    cout << test << endl;
    return 0;
}

我们本以为没有定义operator <<,所以编译器会报错,但实际上编译器会把test隐式类型转换为double类型。这看起来很不错,实际上回出现很多不可预计的问题。它表明了隐式类型转换的缺点: ==它们的存在将导致错误的发生==。

解决方法是用不使用语法关键字的等同的函数来替代转换运算符。例如为了把 Rational 对象转换为 double,用 asDouble 函数代替 operator double 函数:


class Rational {
public: 
... 
  double asDouble() const;
};
// 这个成员函数能被显式调用: Rational r(1, 2); 
cout << r;
cout << r.asDouble();
//转变 Rational // 成 double 
// 错误! Rationa 对象没有 // operator<< 
// 正确, 用 double 类型 //打印 r 

在多数情况下,这种显式转换函数的使用虽然不方便,但是函数被悄悄调用的情况不再 会发生,这点损失是值得的。就好像在编写库时,string没有给出隐式类型转换为char*的操作符,而是给出了c_str()来转换就是这个道理。

以下讨论单参数构造函数进行隐式类型转换的问题。

template<class T>
class Array {
public: 
  Array(int lowBound, int highBound);
  Array(int size);
  T& operator[](int index);
  ...
};

第一个构造函数允许调用者确定数组索引的范围,例如从 10 到 20。它是一个两参数构造函数,所以不能做为类型转换函数。第二个构造函数让调用者仅仅定义数组元素的个数(使 用方法与内置数组的使用相似),不过不同的是它能做为类型转换函数使用,能导致无穷的痛苦。

例如比较 Array对象,部分代码如下:

bool operator==( const Array<int>& lhs, 
Array<int> a(10);
const Array<int>& rhs);
Array<int> b(10);
...
for (int i = 0; i < 10; ++i)
  if (a == b[i]) {
    do something for when
    a[i] and b[i] are equal;
} else { 
// 哎呦! "a" 应该是 "a[i]" 
    do something for when they're not;
  }

我们想用 a 的每个元素与 b 的每个元素相比较,但是当录入 a 时,我们偶然忘记了数组 下标。当然我们希望编译器能报出各种各样的警告信息,但是它根本没有。因为它把这个调 用看成用 Array参数(对于 a)和 int(对于 b[i])参数调用 operator==函数,然而没有 operator==函数是这样的参数类型,我们的编译器注意到它能通过调用 Array构造函 数能转换 int 类型到 Array类型,这个构造函数只有一个 int 类型的参数。然后编译器如此去编译,生成的代码就像这样:

for (int i = 0; i < 10; ++i)
  if (a == static_cast< Array<int> >(b[i]))   ...

每一次循环都把 a 的内容与一个大小为 b[i]的临时数组(内容是未定义的)比较。这 不仅不可能以正确的方法运行,而且还是效率低下的。因为每一次循环我们都必须建立和释 放 Array对象。

解决的方法是利用一个最新编译器的特性,explicit 关键字。为了解决隐式类型转换 而特别引入的这个特性,它的使用方法很好理解。构造函数用 explicit 声明,如果这样做, 编译器会拒绝为了隐式类型转换而调用构造函数。显式类型转换依然合法:

template<class T> 
class Array {
public:
...
explicit Array(int size); // 注意使用"explicit" 
... }; 
Array<int> a(10);
Array<int> b(10);
if (a == b[i]) ...
if (a == Array<int>(b[i])) ...
// 正确, explicit 构造函数 // 在建立对象时能正常使用 
// 也正确
// 错误! 没有办法 
// 隐式转换
// int 到 Array<int> 
// 正确,显式从 int 到 // Array<int>转换 
// (但是代码的逻辑 
// 不合理) 
if (a == static_cast< Array<int> >(b[i])) ... 
// 同样正确,同样 
// 不合理

if (a == (Array<int>)b[i]) ... //C 风格的转换也正确, 
// 但是逻辑 
// 依旧不合理

关于explicit:(不允许参数隐式类型转换!)

class Test1
{
public:
    Test1(int n)
    {
        num=n;
    }//普通构造函数
private:
    int num;
};
class Test2
{
public:
    explicit Test2(int n)
    {
        num=n;
    }//explicit(显式)构造函数
private:
    int num;
};
int main()
{
    Test1 t1=12;//隐式调用其构造函数,成功
    Test2 t2=12;//编译错误,不能隐式调用其构造函数
    Test2 t2(12);//显式调用成功
    return 0;
}

2. 自增和自减

重载函数间的区别决定于它们的参数类型上的差异,但是不 论是 increment 或 decrement 的前缀还是后缀都只有一个参数。为了解决这个语言问题,C++ 规定后缀形式有一个int类型参数,当函数被调用时,编译器传递一个0做为int参数的值 给该函数:


class UPInt {
public: 
  UPInt& operator++();
  const UPInt operator++(int);
  UPInt& operator--();
  const UPInt operator--(int);
  UPInt& operator+=(int);
... }; 
UPInt i;
++i;
i++;
--i;
i--;

值得注意的是,==前缀返回的是引用,后缀返回的是const对象==。(很容易通过前缀自增和后缀自增的区别来判读合理性。)

UPInt& UPInt::operator++() {
  *this += 1;
  return *this;
}
const UPInt UPInt::operator++(int) {
  UPInt oldValue = *this;
++(*this); // 增加 return oldValue;
} 

如果后缀的increment不是const对象,那么以下代码就是正确的:

UPInt i;
i++++; // 两次 increment 后缀 这组代码与下面的代码相同: 
i.operator++(0).operator++(0);

3. 不要重载&&, ||或 “,”

C++使用==布尔表达式短路求值法==(short-circuit evaluation)。这表示一旦 确定了布尔表达式的真假值,即使还有部分表达式没有被测试,布尔表达式也停止运算。

char *p;
...if ((p != 0) && (strlen(p) > 10)) ...
// 这里不用担心当 p 为空时 strlen 无法正确运行,因为如果 p 不等于 0 的测试失败,strlen 不会被调用。同样:

int rangeCheck(int index)
{
if ((index < lowerBound) || (index > upperBound)) ......
}

C++允许根据用户定义的类型,来定制&&和||操作符。方法是重载函数 operator&& 和 operator||,你能在全局重载或每个类里重载。但是你就失去了短路求值的特性。

if (expression1 && expression2) ... 
// 对于编译器来说,等同于下面代码之一: 
if (expression1.operator&&(expression2)) ...
                              // when operator&& is a
                              // member function
if (operator&&(expression1, expression2)) ...
                              // when operator&& is a
// global function 

这好像没有什么不同,但是函数调用法与短路求值法是绝对不同的。首先当函数被调用时,需要运算其所有参数,所以调用函数 functions operator&& 和 operator||时,两个 参数都需要计算,换言之,没有采用短路计算法。第二是 C++语言规范没有定义函数参数的 计算顺序,所以没有办法知道表达式1与表达式2哪一个先计算。完全可能与具有从左参数 到右参数计算顺序的短路计算法相反。

不能重载的部分:

. .* :: ?:
new delete sizeof typeid 
static_cast dynamic_cast const_cast reinterpret_cast 

能重载的部分:

operator new operator deleteoperator new[] operator delete[] +-*/%^&|~ 
! =<>+=-=*=/=%= ^=&=|=<<>> >>=<<=== != <=>=&&||++ -- , ->*-> () [] 

操作符重载的目的是使程序更容易阅读,书 写和理解,而不是用你的知识去迷惑其他人。如果你没有一个好理由重载操作符,就不要重 载。在遇到&&, ||, 和 ,时,找到一个好理由是困难的,因为无论你怎么努力,也不能让它 们的行为特性与所期望的一样。

4. 理解各种不同含义的new和delete

string *ps = new string(“Memory Management”);

你使用的 new 是 new 操作符。这个操作符就象 sizeof 一样是语言内置的,你不能改变它的 含义,它的功能总是一样的。它要完成的功能分成两部分。第一部分是分配足够的内存以便 容纳所需类型的对象。第二部分是它调用构造函数初始化内存中的对象。new 操作符总是做 这两件事情,你不能以任何方式改变它的行为。

new operator

你所能改变的是如何为对象分配内存。new 操作符调用一个函数来完成必需的内存分 配,你能够重写或重载这个函数来改变它的行为。new 操作符为分配内存所调用函数的名字 是 operator new。
函数 operator new 通常这样声明:

void * operator new(size_t size); 

void *rawMemory = operator new(sizeof(string));
操作符 operator new 将返回一个指针,指向一块足够容纳一个 string 类型对象的内存。
就象 malloc 一样,operator new 的职责只是分配内存。它对构造函数一无所知。 operator new 所了解的是内存分配。把 operator new 返回的未经处理的指针传递给一个对 象是 new 操作符的工作。


placement new

但是有时你有一些已经被分配但是尚未处理的(raw)内存,你需要在这些内存中构造一个对象。你可以 使用一个特殊的 operator new ,它被称为 placement new。

void * operator new(size_t, void *location)
{ 
  return location;
}

operator new 的目的是为对象分配内存然后返回指向该内存的指针。在使用 placement new 的情况下,调 用者已经获得了指向内存的指针,因为调用者知道对象应该放在哪里。placement new 必须 做的就是返回转递给它的指针。(没有用的(但是强制的)参数 size_t 没有名字,以防止编 译器发出警告说它没有被使用。)

delete memory deallocation

Operator delete 用来释放内存,它被这样声明:

void operator delete(void *memoryToBeDeallocated); 

因此,

delete ps; 

导致编译器生成类似于这样的代码:


ps->~string(); // call the object's dtor operator 
delete(ps); // deallocate the memory  // the object occupied 

这有一个隐含的意思是如果你只想处理未被初始化的内存,你应该绕过 new 和 delete
操作符,而调用 operator new 获得内存和 operator delete 释放内存给系统:

void *buffer =
  operator new(50*sizeof(char));
  // 分配足够的// 内存以容纳 50 个 char 
...
operator delete(buffer);
//没有调用构造函数 
// 释放内存 // 没有调用析构函数 

如果你用 placement new 在内存中建立对象,你应该避免在该内存中用 delete 操作符。
因为 delete 操作符调用 operator delete 来释放内存,但是包含对象的内存最初不是被 operator new 分配的,placement new 只是返回转递给它的指针。谁知道这个指针来自何方? 而你应该显式调用对象的析构函数来解除构造函数的影响:


// 在共享内存中分配和释放内存的函数 
void * mallocShared(size_t size);
void freeShared(void *memory);
void *sharedMemory = mallocShared(sizeof(Widget));
Widget *pw = // 如上所示, 
constructWidgetInBuffer(sharedMemory, 10); // 使用 
...
delete pw;
pw->~Widget();
freeShared(pw);
// 结果不确定! 共享内存来自// mallocShared, 而不是 operator new 
// 正确。 析构 pw 指向的 Widget,// 但是没有释放//包含 Widget 的内存// 正确。 释放 pw 指向的共享内存// 但是没有调用析构函数 

new 和 delete 操作符是内置的,其行为不受你的控制,凡是它们调用的内存分配和释放函数则可以控制。当你想定制 new 和 delete 操作符的行为时,请记住你不能真的做到这 一点。你只能改变它们为完成它们的功能所采取的方法,而它们所完成的功能则被语言固定 下来,不能改变。(You can modify how they do what they do, but what they do is fixed by the language)

你可能感兴趣的:([C++]关于重载运算符的一些建议)