《Thinking in C++, 2nd Edition》笔记-第十二章(Operator Overloading)

运算符重载只是一种“语法修饰”,本质上也是函数,只不过它的参数不出现在括号之内,而是出现在运算符附近。因此除非重载运算符使程序变得易写,尤其是更易读,否则就不必去重载运算符。比如说,不能像重载 1<<4;使其有别意义,或者使1.414<<2有意义。

重载运算符就像定义一个函数,函数名字为operator@,其中@代表运算符。运算符参数表中的参数取决于:(1)运算符是一元(一个参数)还是二元(两个参数);(2)运算符是全局(对于一元是一个参数,对于二元是两个参数)还是成员函数(对象本身是第一个参数)。

虽然几乎可以重载C中所有可用的运算符,但使用它们是受限制的。特别地,不能结合C中当前没有意义的运算符(例如用**来求幂),不能改变运算符的优先级,不能改变运算符的参数个数。否则这样产生的运算符只会造成意思混淆而不是使之清楚。

重载运算符

一元运算符

下面是所有的一元运算符重载:

// Non-member functions: 
class Integer { 
  long i; 
  Integer* This() { return this; } 
public: 
  Integer(long ll = 0) : i(ll) {} 
  // No side effects takes const& argument: 
  friend const Integer& operator+(const Integer& a); 
  friend const Integer operator-(const Integer& a); 
  friend const Integer operator~(const Integer& a); 
  friend Integer* operator&(Integer& a); 
  friend int operator!(const Integer& a); 
  // Side effects have non-const& argument: 
  // Prefix: 
  friend const Integer& operator++(Integer& a); 
  // Postfix: 
  friend const Integer operator++(Integer& a, int); 
  // Prefix: 
  friend const Integer& operator--(Integer& a); 
  // Postfix: 
  friend const Integer operator--(Integer& a, int); 
}; 
 
// Global operators: 
const Integer& operator+(const Integer& a) { 
  cout << "+Integer\n"; 
  return a; // Unary + has no effect 
} 
const Integer operator-(const Integer& a) { 
  cout << "-Integer\n"; 
  return Integer(-a.i); 
} 
const Integer operator~(const Integer& a) { 
  cout << "~Integer\n"; 
  return Integer(~a.i); 
} 
Integer* operator&(Integer& a) { 
  cout << "&Integer\n"; 
  return a.This(); // &a is recursive! 
} 
int operator!(const Integer& a) { 
  cout << "!Integer\n"; 
  return !a.i; 
} 
// Prefix; return incremented value 
const Integer& operator++(Integer& a) { 
  cout << "++Integer\n"; 
  a.i++; 
  return a; 
} 
// Postfix; return the value before increment: 
const Integer operator++(Integer& a, int) { 
  cout << "Integer++\n"; 
  Integer before(a.i); 
  a.i++; 
  return before; 
} 
// Prefix; return decremented value 
const Integer& operator--(Integer& a) { 
  cout << "--Integer\n"; 
  a.i--; 
  return a; 
} 
// Postfix; return the value before decrement: 
const Integer operator--(Integer& a, int) { 
  cout << "Integer--\n"; 
  Integer before(a.i); 
  a.i--; 
  return before; 
} 
 
// Show that the overloaded operators work: 
void f(Integer a) { 
  +a; 
  -a; 
  ~a; 
  Integer* ip = &a; 
  !a; 
  ++a; 
  a++; 
  --a; 
  a--; 
}

下面是成员函数:

// Member functions (implicit "this"): 
class Byte { 
  unsigned char b; 
public: 
  Byte(unsigned char bb = 0) : b(bb) {} 
  // No side effects: const member function: 
  const Byte& operator+() const { 
    cout << "+Byte\n"; 
    return *this; 
  } 
  const Byte operator-() const { 
    cout << "-Byte\n"; 
    return Byte(-b); 
  } 
  const Byte operator~() const { 
    cout << "~Byte\n"; 
    return Byte(~b); 
  } 
  Byte operator!() const { 
    cout << "!Byte\n"; 
    return Byte(!b); 
  } 
  Byte* operator&() { 
    cout << "&Byte\n"; 
    return this; 
  } 
  // Side effects: non-const member function: 
  const Byte& operator++() { // Prefix 
    cout << "++Byte\n"; 
    b++; 
    return *this; 
  } 
  const Byte operator++(int) { // Postfix 
    cout << "Byte++\n"; 
    Byte before(b); 
    b++; 
    return before; 
  } 
  const Byte& operator--() { // Prefix 
    cout << "--Byte\n"; 
    --b; 
    return *this; 
  } 
  const Byte operator--(int) { // Postfix 
    cout << "Byte--\n"; 
    Byte before(b); 
    --b; 
    return before; 
  } 
}; 
 
void g(Byte b) { 
  +b; 
  -b; 
  ~b; 
  Byte* bp = &b; 
  !b; 
  ++b; 
  b++; 
  --b; 
  b--; 
}
对于自增或自减运算符,当编译器看到++a(先自增)时,它就调用operator++(a) ;但当编译器看到a++时,它就调用operator++(a, int )。即编译器通过调用不同的函数区别这两种形式。int参数只是给后缀操作符产生不同的署名,而不会实际用到。

二元操作符

// Member overloaded operators 
// Member functions (implicit "this"): 
class Byte {  
  unsigned char b; 
public: 
  Byte(unsigned char bb = 0) : b(bb) {} 
  // No side effects: const member function: 
  const Byte operator+(const Byte& right) const { 
    return Byte(b + right.b); 
  } 
  const Byte operator-(const Byte& right) const { 
    return Byte(b - right.b); 
  } 
  const Byte operator*(const Byte& right) const { 
    return Byte(b * right.b); 
  } 
  const Byte operator/(const Byte& right) const { 
    require(right.b != 0, "divide by zero"); 
    return Byte(b / right.b); 
  } 
  const Byte operator%(const Byte& right) const { 
    require(right.b != 0, "modulo by zero"); 
    return Byte(b % right.b); 
  } 
  const Byte operator^(const Byte& right) const { 
    return Byte(b ^ right.b); 
  } 
  const Byte operator&(const Byte& right) const { 
    return Byte(b & right.b); 
  } 
  const Byte operator|(const Byte& right) const { 
    return Byte(b | right.b); 
  } 
  const Byte operator<<(const Byte& right) const { 
    return Byte(b << right.b); 
  } 
  const Byte operator>>(const Byte& right) const { 
    return Byte(b >> right.b); 
  } 
  // Assignments modify & return lvalue. 
  // operator= can only be a member function: 
  Byte& operator=(const Byte& right) { 
    // Handle self-assignment: 
    if(this == &right) return *this; 
    b = right.b; 
    return *this; 
  } 
  Byte& operator+=(const Byte& right) { 
    if(this == &right) {/* self-assignment */} 
    b += right.b; 
    return *this; 
  } 
  Byte& operator-=(const Byte& right) { 
    if(this == &right) {/* self-assignment */} 
    b -= right.b; 
    return *this; 
  } 
  Byte& operator*=(const Byte& right) { 
    if(this == &right) {/* self-assignment */} 
    b *= right.b; 
    return *this; 
  } 
  Byte& operator/=(const Byte& right) { 
    require(right.b != 0, "divide by zero"); 
    if(this == &right) {/* self-assignment */} 
    b /= right.b; 
    return *this; 
  } 
  Byte& operator%=(const Byte& right) { 
    require(right.b != 0, "modulo by zero"); 
    if(this == &right) {/* self-assignment */} 
    b %= right.b; 
    return *this; 
  } 
  Byte& operator^=(const Byte& right) { 
    if(this == &right) {/* self-assignment */} 
    b ^= right.b; 
    return *this; 
  } 
  Byte& operator&=(const Byte& right) { 
    if(this == &right) {/* self-assignment */} 
    b &= right.b; 
    return *this; 
  } 
  Byte& operator|=(const Byte& right) { 
    if(this == &right) {/* self-assignment */} 
    b |= right.b; 
    return *this; 
  } 
  Byte& operator>>=(const Byte& right) { 
    if(this == &right) {/* self-assignment */} 
    b >>= right.b; 
    return *this; 
  } 
  Byte& operator<<=(const Byte& right) { 
    if(this == &right) {/* self-assignment */} 
    b <<= right.b; 
    return *this; 
  } 
  // Conditional operators return true/false: 
  int operator==(const Byte& right) const { 
      return b == right.b; 
  } 
  int operator!=(const Byte& right) const { 
      return b != right.b; 
  } 
  int operator<(const Byte& right) const { 
      return b < right.b; 
  } 
  int operator>(const Byte& right) const { 
      return b > right.b; 
  } 
  int operator<=(const Byte& right) const { 
      return b <= right.b; 
  } 
  int operator>=(const Byte& right) const { 
      return b >= right.b; 
  } 
  int operator&&(const Byte& right) const { 
      return b && right.b; 
  } 
  int operator||(const Byte& right) const { 
      return b || right.b; 
  } 
};  
所有赋值操作符都需要先进行自赋值检查,尤其是复杂的类型。“=”只能作为成员函数。

参数和返回值

操作符的参数传递方法和返回方法有时是不能随意选择的,大部分情况下:

1) 对于任何函数参数,如果只需要从参数中读取而不改变它,缺省地应当按const引用来传递它。普通算术运算符(像+和-号等)和布尔运算符不会改变参数,所以以const引用传递是使用的主要方式。当函数是一个类成员的时候,就转换为const成员函数。只是对于会改变左侧参数的赋值运算符( operator-assignment,像+=)和运算符‘=’,左侧参数才不是常量( constant ),但因为参数将被改变,所以参数仍然按地址传递。

2) 应该选择的返回值取决于运算符所期望的类型。(可以对参数和返回值做任何想做的事)如果运算符的效果是产生一个新值,将需要产生一个作为返回值的新对象。例如,
interger::operator+必须生成一个操作数之和的integer对象。这个对象作为一个const通过传值方式返回,所以作为一个左值结果不会被改变。

3) 所有赋值运算符改变左值。为了使得赋值结果用于链式表达式(像A = B = C),应该能够返回一个刚刚改变了的左值的引用。但这个引用应该是const还是nonconst呢?虽然我们是从左向右读表达式A = B = C,但编译器是从右向左分析这个表达式,所以并非一定要返回一个
nonconst值来支持链式赋值。然而人们有时希望能够对刚刚赋值的对象进行运算,例如(A = B). foo(),这是B赋值给A后调用foo()。因此所有赋值运算符的返回值对于左值应该是
nonconst引用。

4) 对于逻辑运算符,人们希望至少得到一个int返回值,最好是bool返回值。(在大多数编译器支持C++内置bool类型之前开发的库函数使用int或typedef等价物)。

5)对于自增和自减,有前缀和后缀版本,前缀版本返回改变后的对象,因此可以返回*this作为引用,后缀版本则要创建一个对象保存原值,因此应该以传值方式返回。那么 是否应该是const呢?如果允许对象改变,那么可以写(++A).foo(),foo()作用在A上,但对于(A++).foo(),临时对象自动定义为const,所以为了保持一致,都使用const更有意义。

考虑二元运算符+,如果f(A+B)来使用它,A+B的结果变为临时对象,因此自动定义为const,返回值是否是const没有影响。但是如果(A+B).g()这样使用,通过将返回值设为const,规定了返回值只有const函数可以被调用,可以防止丢失对象中贮存的重要信息。

编译器在返回时会进行优化,return integer (left.i + right.i) ;表示创建一个临时对象并返回它,编译器直接地把这个对象创建在返回值外面的内存单元。而如果使用

Integer tmp(left.i + right.i); 
return tmp; 
首先tmp对象通过构造函数被创建,然后拷贝构造函数把tmp拷贝到返回值外部的存储单元中,最后销毁tmp。显然,“ 返回临时对象”效率会更高。

其它运算符

下标运算符‘[ ]’必须是成员函数并且它需要单个参数。因为它暗示对象像数组一样动作,可以经常从这个运算符返回一个引用,所以它可以被很方便地用于等号左侧。

当逗号出现在逗号运算对象左右时,逗号运算符被调用。然而,逗号运算符在函数参数表中出现时不被调用,此时, 逗号仅在对象中起分割作用。除了使语言保持一致性外,这个运算符似乎没有许多实际用途。

运算符( )的函数调用必须是成员函数,它是唯一的允许在它里面有任意个参数的函数。这使得对象看起来像一个真正的函数名,因此,它最好为仅有单一运算的类型使用,或至少是特别优先的一种类型。

运算符new和delete控制动态内存分配,也可被重载。

运算符- > *是其行为像所有其他二元运算符的二元运算符。它是为模仿内部数据类型的成员指针行为的情形而提供的。

不能重载的运算符

在可用的运算符集合里存在一些不能重载的运算符。这样限制的通常原因是出于对安全的考虑:如果这些运算符也可以被重载的话,将会造成危害或破坏安全机制,使得事情变得困难或混淆现有的习惯。
现在,成员选择运算符‘.’在类中对任何成员都有一定的意义。但如果允许它重载,就不能用普通的方法访问成员,只能用指针和指针运算符- >访问。
成员指针逆向引用的运算符‘.* ’因为与运算符‘.’同样的原因而不能重载。
没有求幂运算符。大多数通常的选择是从Fotran语言引用运算符**,但这出现了难以分析的问题。C也没有求幂运算符,C++似乎也不需要,因为这可以通过函数调用来实现。求幂运算符增加了使用的方便,但没有增加新的语言功能,反而给编译器增加了复杂性。
不存在用户定义的运算符,即不能编写目前运算符集合中没有的运算符。不能这样做的部分原因是难以决定其优先级,另一部分原因是没有必要增加麻烦。
不能改变优先级规则。否则人们很难记住它们。

下面是一个使用成员还是非成员函数的建议表:

赋值运算符

MyType b; 
MyType a = b; 
a = b; 
在上面的第二行代码中,a被定义,a在定义时构造函数总是被调用,但是应该用哪个构造函数?a是从现有的b对象创建的,所以只有一个选择:拷贝构造函数。虽然这里有“=”,但拷贝构造函数仍被调用。

而在第三行中,A是已经被初始化的对象,这种情况下,会调用MyType::Operator=来赋值。其实第二行可以用MyType a(b);来避免混淆读者。

一个数据结构自动创建的赋值运算符,会模仿自动创建的拷贝构造函数的行为:如果类包含对象,对于这些对象,“=”被递归调用。这被称为“成员赋值(memberwise assignment)”。

自动类型转换

C和C++中,如果编译器看到一个表达式或函数调用使用了一个不合适的类型,它经常会执行一个自动类型转换。在C+ +中,可以通过定义自动类型转换函数来为用户定义类型达到相同效果。这些函数有两种类型:特殊类型的构造函数和重载的运算符。

构造函数

下面是一个构造函数来自动转型的例子:

// Type conversion constructor 
class One { 
public: 
  One() {} 
}; 
 
class Two { 
public: 
  Two(const One&) {} 
}; 
 
void f(Two) {} 
 
int main() { 
  One one; 
  f(one); // Wants a Two, has a One 
}
当编译器看到f()以为对象one参数调用时,编译器检查f( )的声明并注意到它需要一个two对象作为参数。然后,编译器检查是否有从对象 one 到two的方法。它发现了构造函数two::two(one),two::two(one)被悄悄地调用,结果对象two被传递给f( )。在这个情况里,自动类型转换避免了定义两个f( )重载版本的麻烦。然而,代价是隐藏了构造函数对two的调用,如果我们关心f( )的调用效率的话,那就不要使用这种方法。为了阻止这种自动转换,可以在构造函数前加explicit(只能用于构造函数)。

// Using the "explicit" keyword 
class One { 
public: 
  One() {} 
}; 
 
class Two { 
public: 
  explicit Two(const One&) {} 
}; 
 
void f(Two) {} 
 
int main() { 
  One one; 
//!  f(one); // No auto conversion allowed 
  f(Two(one)); // OK -- user performs conversion 
} 

重载操作符来类型转换

创建一个成员函数,这个函数通过在关键字operator(后跟随着想要转到的类型)的方法,将当前类型转换为希望的类型。这种形式的运算符重载是独特的,因为没有指定一个返回类型——返回类型就是我们正在重载的运算符的名字。例子如下:
class Three { 
  int i; 
public: 
  Three(int ii = 0, int = 0) : i(ii) {} 
}; 
 
class Four { 
  int x; 
public: 
  Four(int xx) : x(xx) {} 
  operator Three() const { return Three(x); } 
}; 
 
void g(Three) {} 
 
int main() { 
  Four four(1); 
  g(four); //Calls operator Three() const
  g(1);  // Calls Three(1,0) 
} 
用构造函数技术,目的类执行转换(one转为two,two来执行转换)。然而使用运算符技术,是源类执行转(four转为Three, four执行转换)。构造函数技术的价值是在创建一个新类时为现有系统增加了新的转换途径。然而,创建一个单一参数的构造函数总是定义一个自动类型转换(即使它有不止一个参数也是一样,因为其余的参数将被缺省处理),这可能并不是我们所想要的。另外,使用构造函数技术没有办法实现从用户定义类型向内置类型转换,这只有运算符重载可能做到。

下面的例子体现了全局重载操作符的意义:

class Number { 
  int i; 
public: 
  Number(int ii = 0) : i(ii) {} 
  const Number 
  operator+(const Number& n) const { 
    return Number(i + n.i); 
  } 
  friend const Number operator-(const Number&, const Number&); 
}; 
 
const Number operator-(const Number& n1, const Number& n2) { 
    return Number(n1.i - n2.i); 
} 
 
int main() { 
  Number a(47), b(11); 
  a + b; // OK 
  a + 1; // 2nd arg converted to Number 
  
//! 1 + a; // Wrong! 1st arg not of type Number 
  a - b; // OK 
  a - 1; // 2nd arg converted to Number 
  1 - a; // 1st arg converted to Number 
}
编译器不会把表达式1 - 1的两个参数转换为number对象,然后调用运算符-号。那将意味着现有的C代码可能突然执行不同的工作了。编译器首先匹配“最简单的”可能性,
对于表达式“1 - 1”将优先使用内部运算符。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值