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

来源:互联网 发布:贴片机工作原理及编程 编辑:程序博客网 时间:2024/06/05 15:03

运算符

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 functionif (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)

1 0