十二、运算符重载
操作符重载只是“语法糖”,这意味着它只是你进行函数调用的另一种方式。
不同之处在于,这个函数的参数没有出现在括号内,而是出现在您一直认为是不可变操作符的字符周围或旁边。
运算符的使用和普通的函数调用有两个不同之处。语法不同;一个操作符经常被放在参数之间,有时放在参数之后。第二个区别是编译器决定调用哪个“函数”。例如,如果您将运算符+与浮点参数一起使用,编译器会“调用”该函数来执行浮点加法(“调用”通常是插入内联代码的动作,或者浮点处理器指令)。如果将 operator +与浮点数和整数一起使用,编译器会“调用”一个特殊函数将 int 转换为 float,然后“调用”浮点加法代码。
但是在 C++ 中,可以定义新的操作符来处理类。这个定义就像一个普通的函数定义,除了函数名由关键字 operator 后跟运算符组成。这是唯一的区别,它变成了一个像其他函数一样的函数,编译器在看到合适的模式时会调用它。
警告和保证
操作符过载很容易让人变得过于热情。起初,这是一个有趣的玩具。但是请记住它的 only 语法糖,调用函数的另一种方式。从这个角度来看,你没有理由重载一个操作符,除非它能让涉及你的类的代码更容易编写,尤其是更容易被阅读。(记住,读代码比写代码多得多。)如果不是这样,那就不用麻烦了。
对运算符重载的另一个常见反应是恐慌;突然,C 运算符不再有熟悉的含义了。"一切都变了,我所有的 C 代码将做不同的事情!"这不是真的。在只包含内置数据类型的表达式中使用的所有运算符都不能更改。你永远不能重载操作符
1 << 4;
行为不同,或者
1.414 << 2;
有意义。只有包含用户定义类型的表达式才能有重载运算符。
语法
定义一个重载的操作符就像定义一个函数,但是这个函数的名字是operator@
,其中@
代表被重载的操作符。重载运算符的参数列表中的参数数量取决于两个因素:
- 无论是一元运算符(一个参数)还是二元运算符(两个参数)。
- 运算符是定义为全局函数(一元有一个参数,二元有两个参数)还是成员函数(一元有零个参数,二元有一个参数——对象成为左边的参数)。
清单 12-1 包含了一个小类,显示了操作符重载的语法。
清单 12-1 。阐释运算符重载的语法
//: C12:OperatorOverloadingSyntax.cpp
#include <iostream>
using namespace std;
class Integer {
int i;
public:
Integer(int ii) : i(ii) {}
const Integer
operator+(const Integer& rv) const {
cout << "operator+" << endl;
return Integer(i + rv.i);
}
Integer&
operator+=(const Integer& rv) {
cout << "operator+=" << endl;
i += rv.i;
return *this;
}
};
int main() {
cout << "built-in types:" << endl;
int i = 1, j = 2, k = 3;
k += i + j;
cout << "user-defined types:" << endl;
Integer ii(1), jj(2), kk(3);
kk += ii + jj;
} ///:∼
这两个重载操作符被定义为内联成员函数,它们在被调用时进行声明。对于二元运算符,单个参数出现在运算符的右侧。一元运算符在定义为成员函数时没有参数。对运算符左侧的对象调用成员函数。
对于非条件操作符(条件操作符通常返回一个布尔值),如果两个参数是相同的类型,那么您几乎总是希望返回一个与您正在操作的类型相同的对象或引用。
注意如果它们不是同一个类型,那么它应该产生什么样的解释就由你决定了。
通过这种方式,可以构建复杂的表达式,如:
kk += ii + jj;
operator+
产生一个新的Integer
(临时的),用作operator+=
的rv
参数。一旦不再需要这个临时文件,它就会被销毁。
过载运算符
尽管您可以重载 C # 中几乎所有可用的运算符,但运算符重载的使用相当有限。特别是不能组合 C 中目前没有意义的运算符(比如**
表示取幂),不能改变运算符的求值优先级,也不能改变一个运算符所需的参数个数。这是有道理的——所有这些行为都会产生混淆含义而不是澄清含义的操作符。
接下来的两个小节给出了所有“常规”操作符的例子,以您最可能使用的形式重载。
一元运算符
清单 12-2 显示了重载所有一元运算符的语法,既有全局函数(非成员friend
函数)的形式,也有成员函数的形式。这些将扩展前面显示的Integer
类,并添加一个新的byte
类。您的特定操作符的含义将取决于您希望使用它们的方式,但是在做一些意想不到的事情之前,请考虑客户端程序员。
清单 12-2 。阐释重载一元运算符的语法
//: C12:OverloadingUnaryOperators.cpp
#include <iostream>
using namespace std;
// 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--;
}
int main() {
Integer a;
f(a);
Byte b;
g(b);
} ///:∼
这些函数根据其参数的传递方式进行分组。后面给出了如何传递和返回参数的准则。上面的形式(和将在下一节中介绍)通常是您将使用的形式,所以在重载您自己的操作符时,以它们作为模式开始。
增量和减量和
重载的++
和– –
操作符提出了一个难题,因为您希望能够调用不同的函数,这取决于它们是出现在它们所作用的对象之前(前缀)还是之后(后缀)。解决方案很简单,但人们有时会觉得一开始有点困惑。例如,当编译器看到++a
(一个前增量),它生成一个对operator++(a)
的调用;但是当它看到a++
时,它生成一个对operator++(a, int)
的调用。也就是说,编译器通过调用不同的重载函数来区分这两种形式。在OverloadingUnaryOperators.cpp
(清单 12-2)中,对于成员函数版本,如果编译器看到++b
,就会生成对B::operator++( )
的调用;如果它看到b++
,它就调用B::operator++(int)
。
用户看到的只是前缀和后缀版本调用了不同的函数。然而,在底层,两个函数调用具有不同的签名,因此它们链接到两个不同的函数体。编译器为参数int
传递一个伪常量值(因为该值从未被使用,所以从未被赋予标识符),以便为后缀版本生成不同的签名。
二元运算符
清单 12-3 & 12-4 对二元运算符重复OverloadingUnaryOperators.cpp
的例子,这样你就有了一个你可能想要重载的所有运算符的例子。同样,全局版本(见清单 12-3 )和成员函数版本(见清单 12-4 )都被显示出来。
清单 12-3 。说明重载二元操作符的语法(对于非成员重载操作符
//: C12:Integer.h
// Non-member overloaded operators
#ifndef INTEGER_H
#define INTEGER_H
#include <iostream>
// Non-member functions:
class Integer {
long i;
public:
Integer(long ll = 0) : i(ll) {}
// Operators that create new, modified value:
friend const Integer
operator+(const Integer& left,
const Integer& right);
friend const Integer
operator-(const Integer& left,
const Integer& right);
friend const Integer
operator*(const Integer& left,
const Integer& right);
friend const Integer
operator/(const Integer& left,
const Integer& right);
friend const Integer
operator%(const Integer& left,
const Integer& right);
friend const Integer
operator^(const Integer& left,
const Integer& right);
friend const Integer
operator&(const Integer& left,
const Integer& right);
friend const Integer
operator|(const Integer& left,
const Integer& right);
friend const Integer
operator<<(const Integer& left,
const Integer& right);
friend const Integer
operator>>(const Integer& left,
const Integer& right);
// Assignments modify & return lvalue:
friend Integer&
operator+=(Integer& left,
const Integer& right);
friend Integer&
operator-=(Integer& left,
const Integer& right);
friend Integer&
operator*=(Integer& left,
const Integer& right);
friend Integer&
operator/=(Integer& left,
const Integer& right);
friend Integer&
operator%=(Integer& left,
const Integer& right);
friend Integer&
operator^=(Integer& left,
const Integer& right);
friend Integer&
operator&=(Integer& left,
const Integer& right);
friend Integer&
operator|=(Integer& left,
const Integer& right);
friend Integer&
operator>>=(Integer& left,
const Integer& right);
friend Integer&
operator<<=(Integer& left,
const Integer& right);
// Conditional operators return true/false:
friend int
operator==(const Integer& left,
const Integer& right);
friend int
operator!=(const Integer& left,
const Integer& right);
friend int
operator<(const Integer& left,
const Integer& right);
friend int
operator>(const Integer& left,
const Integer& right);
friend int
operator<=(const Integer& left,
const Integer& right);
friend int
operator>=(const Integer& left,
const Integer& right);
friend int
operator&&(const Integer& left,
const Integer& right);
friend int
operator||(const Integer& left,
const Integer& right);
// Write the contents to an ostream:
void print(std::ostream& os) const { os << i; }
};
#endif // INTEGER_H ///:∼
//: C12:Integer.cpp {O}
// Implementation of overloaded operators
#include "Integer.h" // TO be INCLUDED from Header FILE above
#include "../require.h" // TO be INCLUDED From Header FILE in *Chapter 9*
const Integer
operator+(const Integer& left,
const Integer& right) {
return Integer(left.i + right.i);
}
const Integer
operator-(const Integer& left,
const Integer& right) {
return Integer(left.i - right.i);
}
const Integer
operator*(const Integer& left,
const Integer& right) {
return Integer(left.i * right.i);
}
const Integer
operator/(const Integer& left,
const Integer& right) {
require(right.i != 0, "divide by zero");
return Integer(left.i / right.i);
}
const Integer
operator%(const Integer& left,
const Integer& right) {
require(right.i != 0, "modulo by zero");
return Integer(left.i % right.i);
}
const Integer
operator^(const Integer& left,
const Integer& right) {
return Integer(left.i ^ right.i);
}
const Integer
operator&(const Integer& left,
const Integer& right) {
return Integer(left.i & right.i);
}
const Integer
operator|(const Integer& left,
const Integer& right) {
return Integer(left.i | right.i);
}
const Integer
operator<<(const Integer& left,
const Integer& right) {
return Integer(left.i << right.i);
}
const Integer
operator>>(const Integer& left,
const Integer& right) {
return Integer(left.i >> right.i);
}
// Assignments modify & return lvalue:
Integer& operator+=(Integer& left,
const Integer& right) {
if(&left == &right) {/* self-assignment */}
left.i += right.i;
return left;
}
Integer& operator-=(Integer& left,
const Integer& right) {
if(&left == &right) {/* self-assignment */}
left.i -= right.i;
return left;
}
Integer& operator*=(Integer& left,
const Integer& right) {
if(&left == &right) {/* self-assignment */}
left.i *= right.i;
return left;
}
Integer& operator/=(Integer& left,
const Integer& right) {
require(right.i != 0, "divide by zero");
if(&left == &right) {/* self-assignment */}
left.i /= right.i;
return left;
}
Integer& operator%=(Integer& left,
const Integer& right) {
require(right.i != 0, "modulo by zero");
if(&left == &right) {/* self-assignment */}
left.i %= right.i;
return left;
}
Integer& operator^=(Integer& left,
const Integer& right) {
if(&left == &right) {/* self-assignment */}
left.i ^= right.i;
return left;
}
Integer& operator&=(Integer& left,
const Integer& right) {
if(&left == &right) {/* self-assignment */}
left.i &= right.i;
return left;
}
Integer& operator|=(Integer& left,
const Integer& right) {
if(&left == &right) {/* self-assignment */}
left.i |= right.i;
return left;
}
Integer& operator>>=(Integer& left,
const Integer& right) {
if(&left == &right) {/* self-assignment */}
left.i >>= right.i;
return left;
}
Integer& operator<<=(Integer& left,
const Integer& right) {
if(&left == &right) {/* self-assignment */}
left.i <<= right.i;
return left;
}
// Conditional operators return true/false:
int operator==(const Integer& left,
const Integer& right) {
return left.i == right.i;
}
int operator!=(const Integer& left,
const Integer& right) {
return left.i != right.i;
}
int operator<(const Integer& left,
const Integer& right) {
return left.i < right.i;
}
int operator>(const Integer& left,
const Integer& right) {
return left.i > right.i;
}
int operator<=(const Integer& left,
const Integer& right) {
return left.i <= right.i;
}
int operator>=(const Integer& left,
const Integer& right) {
return left.i >= right.i;
}
int operator&&(const Integer& left,
const Integer& right) {
return left.i && right.i;
}
int operator||(const Integer& left,
const Integer& right) {
return left.i || right.i;
} ///:∼
//: C12:IntegerTest.cpp
//{L} Integer
#include "Integer.h"
#include <fstream>
using namespace std;
ofstream out("IntegerTest.out");
void h(Integer& c1, Integer& c2) {
// A complex expression:
c1 += c1 * c2 + c2 % c1;
#define TRY(OP) \
out << "c1 = "; c1.print(out); \
out << ", c2 = "; c2.print(out); \
out << "; c1 " #OP " c2 produces "; \
(c1 OP c2).print(out); \
out << endl;
TRY(+) TRY(-) TRY(*) TRY(/)
TRY(%) TRY(^) TRY(&) TRY(|)
TRY(<<) TRY(>>) TRY(+=) TRY(-=)
TRY(*=) TRY(/=) TRY(%=) TRY(^=)
TRY(&=) TRY(|=) TRY(>>=) TRY(<<=)
// Conditionals:
#define TRYC(OP) \
out << "c1 = "; c1.print(out); \
out << ", c2 = "; c2.print(out); \
out << "; c1 " #OP " c2 produces "; \
out << (c1 OP c2); \
out << endl;
TRYC(<) TRYC(>) TRYC(==) TRYC(!=) TRYC(<=)
TRYC(>=) TRYC(&&) TRYC(||)
}
int main() {
cout << "friend functions" << endl;
Integer c1(47), c2(9);
h(c1, c2);
} ///:∼
清单 12-4 。说明重载二元运算符的语法(对于成员重载运算符)
//: C12:Byte.h
// Member overloaded operators
#ifndef BYTE_H
#define BYTE_H
#include "../require.h"
#include <iostream>
// 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;
}
// Write the contents to an ostream:
void print(std::ostream& os) const {
os << "0x" << std::hex << int(b) << std::dec;
}
};
#endif // BYTE_H ///:∼
//: C12:ByteTest.cpp
#include "Byte.h" // To be INCLUDED from Header FILE above
#include <fstream>
using namespace std;
ofstream out("ByteTest.out");
void k(Byte& b1, Byte& b2) {
b1 = b1 * b2 + b2 % b1;
#define TRY2(OP) \
out << "b1 = "; b1.print(out); \
out << ", b2 = "; b2.print(out); \
out << "; b1 " #OP " b2 produces "; \
(b1 OP b2).print(out); \
out << endl;
b1 = 9; b2 = 47;
TRY2(+) TRY2(-) TRY2(*) TRY2(/)
TRY2(%) TRY2(^) TRY2(&) TRY2(|)
TRY2(<<) TRY2(>>) TRY2(+=) TRY2(-=)
TRY2(*=) TRY2(/=) TRY2(%=) TRY2(^=)
TRY2(&=) TRY2(|=) TRY2(>>=) TRY2(<<=)
TRY2(=) // Assignment operator
// Conditionals:
#define TRYC2(OP) \
out << "b1 = "; b1.print(out); \
out << ", b2 = "; b2.print(out); \
out << "; b1 " #OP " b2 produces "; \
out << (b1 OP b2); \
out << endl;
b1 = 9; b2 = 47;
TRYC2(<) TRYC2(>) TRYC2(==) TRYC2(!=) TRYC2(<=)
TRYC2(>=) TRYC2(&&) TRYC2(||)
// Chained assignment:
Byte b3 = 92;
b1 = b2 = b3;
}
int main() {
out << "member functions:" << endl;
Byte b1(47), b2(9);
k(b1, b2);
} ///:∼
可以看到operator=
只允许是成员函数。这个后面解释。
注意,所有的赋值操作符都有检查自赋值的代码;这是一个总的指导方针。在某些情况下,这是不必要的;例如,对于operator+=
,你经常希望让说出A+=A
,并让它把A
加到自己身上。检查自我分配最重要的地方是operator=
,因为对于复杂的对象,可能会出现灾难性的结果。(在某些情况下这是可以的,但是在写operator=
的时候你应该一直记在心里。)
前面三个清单(即清单 12-2、12-3 和 12-4)中显示的所有操作符都被重载以处理单一类型。也可以重载操作符来处理混合类型,例如,你可以把苹果加到橘子上。然而,在开始详尽的操作符重载之前,应该先看看本章后面的自动类型转换一节。通常,在正确的位置进行类型转换可以为您节省许多重载操作符。
参数和返回值和
当你看着OverloadingUnaryOperators.cpp
、Integer.h
和Byte.h
并看到参数传递和返回的所有不同方式时,一开始可能会有点困惑。尽管你可以用你想要的任何方式传递和返回参数,这些例子中的选择并不是随机选择的。它们遵循一种逻辑模式,与你在大多数选择中想要使用的模式相同。
- 与任何函数参数一样,如果您只需要读取参数而不需要更改它,默认情况下会将其作为
const
引用传递。普通算术运算(如+
、–
、、等)。)和布尔值不会改变它们的参数,所以你主要使用const
引用。当函数是一个类成员时,这就转化成了一个const
成员函数。只有使用运算符赋值(如+=
和operator=
)来改变左边的参数,左边的参数不是常量,但它仍然作为地址传入,因为它将被改变。 - 您应该选择的返回值类型取决于运算符的预期含义。(同样,您可以对参数和返回值做任何您想做的事情。)如果操作符的效果是产生一个新值,那么就需要生成一个新的对象作为返回值。例如,
Integer::operator+
必须产生一个作为操作数总和的Integer
对象。该对象通过值作为const
返回,因此结果不能作为左值修改。 - 所有的赋值操作符都修改左值。为了允许赋值的结果被用在链式表达式中,比如
a=b=c
,你应该返回一个对刚刚被修改的左值的引用。但是这个参考应该是一个const
还是非const
?尽管你从左向右读a=b=c
,编译器从右向左解析它,所以你不会被迫返回一个非const
来支持赋值链接。然而,人们有时确实希望能够对刚刚被赋值的对象执行操作,比如在将b
赋值给它之后,在a
上调用func( )
。因此,所有赋值操作符的返回值应该是对左值的非const
引用。 - 对于逻辑运算符,每个人都期望在最坏的情况下得到一个
int
,在最好的情况下得到一个bool
。(在大多数编译器支持 C++ 的内置bool
之前开发的库将使用int
或等效的typedef
。)
由于前缀和后缀版本的不同,递增和递减运算符呈现出两难的局面。两个版本都改变了对象,因此不能将对象视为const
。前缀版本返回对象被更改后的值,因此您期望得到被更改的对象。因此,使用 prefix,您可以返回*this
作为引用。postfix 版本应该在值被改变之前返回值*,所以您被迫创建一个单独的对象来表示该值并返回它。因此,使用 postfix,如果想保留预期的含义,就必须通过值返回。(注意,你有时会发现递增和递减操作符返回一个int
或bool
来指示,例如,一个被设计来在列表中移动的对象是否在列表的末尾。)现在的问题是:这些应该被返回为const
还是非const
?如果你允许对象被修改,有人写(++a).func( )
,func( )
会对a
本身进行操作,但是有了(a++).func( )
,func( )
会对后缀operator++
返回的临时对象进行操作。临时对象是自动const
的,所以这将被编译器标记,但是为了一致性的缘故,将它们都设为const
可能更有意义,正如这里所做的。或者你可以选择让前缀版本不使用const
而使用后缀版本const
。由于您可能希望赋予递增和递减运算符多种含义,因此需要根据具体情况来考虑它们。*
以常量 的形式按值返回
作为一个const
通过值返回起初看起来有点微妙,所以它值得更多的解释。考虑二进制operator+
。如果在像f(a+b)
这样的表达式中使用它,a+b
的结果就变成了一个临时对象,用于调用f( )
。因为是临时的,所以自动const
,所以你显式的做不做返回值const
都没有影响。
然而,您也可以向a+b
的返回值发送一条消息,而不仅仅是将它传递给一个函数。比如你可以说(a+b).g( )
,其中g( )
是Integer
的某个成员函数,在这种情况下。通过生成返回值const
,您声明只有一个const
成员函数可以被调用来获取返回值。这是正确的,因为它防止你在一个很可能丢失的对象中存储潜在的有价值的信息。
返回优化
当创建新对象以按值返回时,请注意使用的形式。例如,在operator+
中,表格是
return Integer(left.i + right.i);
乍一看,这可能像是对构造器的函数调用,但事实并非如此。语法是临时对象的语法;语句说“创建一个临时的Integer
对象并返回它。”因此,您可能认为结果与创建一个命名的本地对象并返回它是一样的。然而,这是完全不同的。如果你说
Integer tmp(left.i + right.i);
return tmp;
会发生三件事。首先,tmp
对象被创建,包括其构造器调用。其次,复制构造器将tmp
复制到外部返回值的位置。第三,在作用域的末尾为tmp
调用析构函数。
相比之下,“返回临时”方法的工作方式完全不同。当编译器看到你这样做时,它知道你除了返回它所创建的对象之外,没有其他需要。编译器通过将对象直接构建到外部返回值的位置来利用这一点。这只需要一个普通的构造器调用(不需要复制构造器),也没有析构函数调用,因为你从来没有真正创建一个本地对象。因此,尽管除了程序员的认知之外,它没有任何成本,但它的效率显著提高。这通常被称为返回值优化。
异常运算符
几个额外的运算符对于重载有稍微不同的语法。
下标operator[ ]
必须是一个成员函数,它需要一个参数。因为operator[ ]
意味着被调用的对象的行为类似于一个数组,所以您通常会从这个操作符返回一个引用,所以它可以方便地用在等号的左边。该运算符通常是重载的;你会在本书的其余部分看到例子。
操作符new
和delete
控制动态存储分配,并且可以以多种不同的方式重载。这个话题在第十三章中有所涉及。
运算符逗号
当逗号运算符出现在为其定义逗号的类型的对象旁边时,将调用该运算符。然而,operator
是而不是对函数参数列表的调用,只对公开的对象,用逗号分隔。这个运算符似乎没有太多实际用途;这是为了语言的一致性。清单 12-5 展示了一个例子,展示了当逗号出现在一个对象的前面和后面时,如何调用逗号函数。
清单 12-5 。重载逗号运算符
//: C12:OverloadingOperatorComma.cpp
#include <iostream>
using namespace std;
class After {
public:
const After& operator,(const After&) const {
cout << "After::operator,()" << endl;
return *this;
}
};
class Before {};
Before& operator,(int, Before& b) {
cout << "Before::operator,()" << endl;
return b;
}
int main() {
After a, b;
a, b; // Operator comma called
Before c;
1, c; // Operator comma called
} ///:∼
全局函数允许将逗号放在所讨论的对象之前。所显示的用法相当模糊和可疑。虽然您可能会在更复杂的表达式中使用逗号分隔的列表,但在大多数情况下使用它太微妙了。
操作员->
当你想让一个对象看起来像一个指针时,通常使用operator–>
。由于这种对象比典型的指针具有更多的内置“智能”,因此这种对象通常被称为智能指针。如果您想在指针周围“包装”一个类以使指针安全,或者在迭代器的常见用法中,这些特别有用,迭代器是一个对象,它遍历其他对象的集合 / 容器,并一次选择一个,而不提供对容器实现的直接访问。
指针取消引用运算符必须是成员函数。它有额外的、非典型的约束:它必须返回一个也有指针取消引用操作符的对象(或对对象的引用),或者它必须返回一个指针,该指针可用于选择指针取消引用操作符箭头所指向的对象。一个简单的例子见清单 12-6 。
清单 12-6 。智能指针示例
//: C12:SmartPointer.cpp
#include <iostream>
#include <vector>
#include "../require.h"
using namespace std;
class Obj {
static int i, j;
public:
void f() const { cout << i++ << endl; }
void g() const { cout << j++ << endl; }
};
// Static member definitions:
int Obj::i = 47;
int Obj::j = 11;
// Container:
class ObjContainer {
vector<Obj*> a;
public:
void add(Obj* obj) { a.push_back(obj); }
friend class SmartPointer;
};
class SmartPointer {
ObjContainer& oc;
int index;
public:
SmartPointer(ObjContainer& objc) : oc(objc) {
index = 0;
}
// Return value indicates end of list:
bool operator++() { // Prefix
if(index >= oc.a.size()) return false;
if(oc.a[++index] == 0) return false;
return true;
}
bool operator++(int) { // Postfix
return operator++(); // Use prefix version
}
Obj* operator->() const {
require(oc.a[index] != 0, "Zero value "
"returned by SmartPointer::operator->()");
return oc.a[index];
}
};
int main() {
const int sz = 10;
Obj o[sz];
ObjContainer oc;
for(int i = 0; i < sz; i++)
oc.add(&o[i]); // Fill it up
SmartPointer sp(oc); // Create an iterator
do {
sp->f(); // Pointer dereference operator call
sp->g();
} while(sp++);
} ///:∼
类Obj
定义了在这个程序中被操作的对象。函数f( )
和g( )
使用static
数据成员简单地打印出感兴趣的值。使用add( )
函数将指向这些对象的指针存储在ObjContainer
类型的容器中。看起来像一个指针数组,但是你会注意到没有办法再把指针取出来。然而,SmartPointer
被声明为一个friend
类,所以它有权限查看容器内部。SmartPointer
类看起来非常像一个智能指针;您可以使用operator++
向前移动它(您也可以定义一个operator– –
),它不会超过它所指向的容器的末尾,并且它(通过指针解引用操作符)产生它所指向的值。请注意,SmartPointer
是为其创建的容器定制的;与普通指针不同,没有“通用”智能指针。在main( )
中,一旦容器oc
被Obj
对象填满,就会创建一个SmartPointer sp
。智能指针调用发生在表达式中,如:
sp->f(); // Smart pointer calls
sp->g();
这里,即使sp
实际上没有f( )
和g( )
成员函数,指针解引用操作符也会自动为SmartPointer::operator–>
返回的Obj*
调用这些函数。编译器执行所有检查以确保函数调用正常工作。
尽管指针取消引用操作符的底层机制比其他操作符更复杂,但目标是完全一样的:为类的用户提供更方便的语法。
一个嵌套的迭代器
更常见的是,智能指针或迭代器类嵌套在它所服务的类中。在清单 12-7 中,清单 12-6 中的代码被重写以将SmartPointer
嵌套在ObjContainer
中。
清单 12-7 。嵌套的智能指针/迭代器
//: C12:NestedSmartPointer.cpp
#include <iostream>
#include <vector>
#include "../require.h"
using namespace std;
class Obj {
static int i, j;
public:
void f() { cout << i++ << endl; }
void g() { cout << j++ << endl; }
};
// Static member definitions:
int Obj::i = 47;
int Obj::j = 11;
// Container:
class ObjContainer {
vector<Obj*> a;
public:
void add(Obj* obj) { a.push_back(obj); }
class SmartPointer;
friend class SmartPointer;
class SmartPointer {
ObjContainer& oc;
unsigned int index;
public:
SmartPointer(ObjContainer& objc) : oc(objc) {
index = 0;
}
// Return value indicates end of list:
bool operator++() { // Prefix
if(index >= oc.a.size()) return false;
if(oc.a[++index] == 0) return false;
return true;
}
bool operator++(int) { // Postfix
return operator++(); // Use prefix version
}
Obj* operator->() const {
require(oc.a[index] != 0, "Zero value "
"returned by SmartPointer::operator->()");
return oc.a[index];
}
};
// Function to produce a smart pointer that
// points to the beginning of the ObjContainer:
SmartPointer begin() {
return SmartPointer(*this);
}
};
int main() {
const int sz = 10;
Obj o[sz];
ObjContainer oc;
for(int i = 0; i < sz; i++)
oc.add(&o[i]); // Fill it up
ObjContainer::SmartPointer sp = oc.begin();
do {
sp->f(); // Pointer dereference operator call
sp->g();
} while(++sp);
} ///:∼
除了类的实际嵌套之外,这里只有两个不同之处。第一个是在类的声明中,这样它就可以是一个friend
,如:
class SmartPointer;
friend SmartPointer;
编译器必须首先知道这个类的存在,然后才能被告知它是一个friend
。
第二个不同是在ObjContainer
成员函数begin( )
中,它产生一个指向ObjContainer
序列开始的SmartPointer
。虽然这只是为了方便,但它很有价值,因为它遵循了标准 C++ 库中使用的部分形式。
运算符->*
operator–>*
是一个二元操作符,其行为类似于所有其他二元操作符。它是为那些想要模仿内置的成员指针语法所提供的行为的情况而提供的,如前一章所述。
就像operator->
一样,指向成员的指针解引用操作符通常与某种表示智能指针的对象一起使用,尽管这里显示的例子会更简单一些,所以容易理解。定义operator->*
的诀窍在于,它必须返回一个对象,这个对象的operator( )
可以用你正在调用的成员函数的参数来调用。
函数调用operator( )
必须是一个成员函数,它的独特之处在于它允许任意数量的参数。它让你的对象看起来像是一个函数。尽管您可以用不同的参数定义几个重载的operator( )
函数,但它通常用于只有一个操作的类型,或者至少是一个特别突出的操作。
要创建一个operator->*
,你必须首先创建一个带有operator( )
的类,这是operator->*
将返回的对象类型。这个类必须以某种方式捕获必要的信息,以便当调用operator( )
时(这是自动发生的),指向成员的指针将被对象解引用。在清单 12-8 中,FunctionObject
构造器捕获并存储指向对象的指针和指向成员函数的指针,然后operator( )
使用它们进行实际的指向成员的指针调用。
清单 12-8 。指向成员运算符的指针
//: C12:PointerToMemberOperator.cpp
#include <iostream>
using namespace std;
class Dog {
public:
int run(int i) const {
cout << "run\n";
return i;
}
int eat(int i) const {
cout << "eat\n";
return i;
}
int sleep(int i) const {
cout << "ZZZ\n";
return i;
}
typedef int (Dog::*PMF)(int) const;
// operator->* must return an object
// that has an operator():
class FunctionObject {
Dog* ptr;
PMF pmem;
public:
// Save the object pointer and member pointer
FunctionObject(Dog* wp, PMF pmf)
: ptr(wp), pmem(pmf) {
cout << "FunctionObject constructor\n";
}
// Make the call using the object pointer
// and member pointer
int operator()(int i) const {
cout << "FunctionObject::operator()\n";
return (ptr->*pmem)(i); // Make the call
}
};
FunctionObject operator->*(PMF pmf) {
cout << "operator->*" << endl;
return FunctionObject(this, pmf);
}
};
int main() {
Dog w;
Dog::PMF pmf = &Dog::run;
cout << (w->*pmf)(1) << endl;
pmf = &Dog::sleep;
cout << (w->*pmf)(2) << endl;
pmf = &Dog::eat;
cout << (w->*pmf)(3) << endl;
} ///:∼
Dog
有三个成员函数,它们都接受一个int
参数并返回一个int
。PMF
是一个typedef
,用于简化定义指向Dog
成员函数的指针。
由operator->*
创建并返回一个FunctionObject
。注意,operator->*
知道成员指针被调用的对象(this
)和成员指针,并将它们传递给存储值的FunctionObject
构造器。当调用operator->*
时,编译器立即返回并调用operator( )
获取operator->*
的返回值,并传入给operator->*
的参数。FunctionObject::operator( )
接受参数,然后使用其存储的对象指针和成员指针解引用“真正的”成员指针。
请注意,您在这里所做的事情,就像使用operator->
一样,是将您自己插入到对operator->*
的调用中。这允许您在必要时执行一些额外的操作。
这里实现的operator->*
机制只对采用int
参数并返回int
的成员函数有效。这是限制性的,但是如果您试图为每种不同的可能性创建重载机制,这似乎是一个禁止性的任务。幸运的是,C++ 的template
机制(在第十六章中讨论)就是为处理这样的问题而设计的。
运营商你不能霸王
可用集合中有某些运算符不能重载。限制的一般原因是安全。如果这些操作符过载,它会以某种方式危及或破坏安全机制,使事情变得更困难,或者混淆现有的实践。
- 成员选择
operator.
。目前,点对类中的任何成员都有意义,但是如果你允许它被重载,那么你就不能以正常的方式访问成员;相反,你必须使用指针和箭头operator->
。 - 指向成员解引用的指针
operator.*
,原因与operator.
相同。 - 没有指数运算符。对此最流行的选择是 Fortran 中的
operator**
,但是这带来了困难的解析问题。此外,C 没有指数运算符,所以 C++ 似乎也不需要,因为您可以随时执行函数调用。取幂运算符会增加一个方便的符号,但是没有新的语言功能来解释编译器增加的复杂性。 - 没有用户定义的运算符。也就是说,您不能创建当前不在集合中的新运算符。问题的一部分是如何确定优先级,问题的一部分是没有足够的必要来考虑必要的麻烦。
- 您不能更改优先规则。如果不让人们玩它们,它们就很难被记住。
非成员操作符
在前面的一些例子中,操作者可能是成员,也可能不是成员,这似乎没有太大的区别。这通常会引发选择哪个的问题。一般来说,如果没什么区别;它们应该是成员,以强调操作符和它的类之间的关联。当左边的操作数总是当前类的一个对象时,这样做很好。
但是,有时您希望左边的操作数是某个其他类的对象。当操作符<<
和>>
为iostream
重载时,你会经常看到这种情况。因为iostream
是一个基本的 C++ 库,你可能想为你的大多数类重载这些操作符,所以这个过程值得记忆;参见清单 12-9 。
清单 12-9 。Iostream 运算符重载
//: C12:IostreamOperatorOverloading.cpp
// Example of non-member overloaded operators
#include "../require.h"
#include <iostream>
#include <sstream>
// "String streams"
#include <cstring>
using namespace std;
class IntArray {
enum { sz = 5 };
int i[sz];
public:
IntArray() { memset(i, 0, sz* sizeof(*i)); }
int& operator[](int x) {
require(x >= 0 && x < sz,
"IntArray::operator[] out of range");
return i[x];
}
friend ostream&
operator<<(ostream& os, const IntArray& ia);
friend istream&
operator>>(istream& is, IntArray& ia);
};
ostream&
operator<<(ostream& os, const IntArray& ia) {
for(int j = 0; j < ia.sz; j++) {
os << ia.i[j];
if(j != ia.sz -1)
os << ", ";
}
os << endl;
return os;
}
istream& operator>>(istream& is, IntArray& ia){
for(int j = 0; j < ia.sz; j++)
is >> ia.i[j];
return is;
}
int main() {
stringstream input("47 34 56 92 103");
IntArray I;
input >> I;
I[4] = -1; // Use overloaded operator[]
cout << I;
} ///:∼
这个类还包含一个重载的operator [ ]
,它返回对数组中合法值的引用。因为返回了引用,所以表达式
I[4] = -1;
不仅看起来比使用指针文明得多,而且也达到了预期的效果。
重要的是,重载的 shift 操作符通过引用传递并返回*,因此这些操作将影响外部对象。在函数定义中,像*
os << ia.i[j];
调用现有的重载运算符函数(即<iostream>
中定义的函数)。在这种情况下,被调用的函数是ostream& operator<<(ostream&, int)
,因为ia.i[j]
解析为int
。
一旦在istream
或ostream
上执行了所有的动作,它就会被返回,因此可以在更复杂的表达式中使用。
在main( )
中,使用了一种新型的iostream
(在<sstream>
中声明)。这是一个接受一个string
(它可以从一个char
数组中创建,如此处所示)并将其转换成一个iostream
的类。在清单 12-9 的例子中,这意味着不用打开文件或在命令行上输入数据就可以测试移位操作符。
本示例(清单 12-9 )中所示的插入器和提取器的形式是标准的。如果您想为自己的类创建这些操作符,请复制上面的函数签名和返回类型,并遵循主体的形式。
基本指南
表 12-1 中的指南推荐用于在成员和非成员之间进行选择。
表 12-1 。选择成员的准则
操作员 | 推荐用途 |
---|---|
All Unary Operators | 成员 |
= ( ) [ ] –> –>* | 必须是会员吗 |
+= –= /= *= ^= &= |= %= >>= <<= | 成员 |
All Other Binary Operators | 非成员 |
重载赋值
新 C++ 程序员的一个常见困惑是赋值。这是毫无疑问的,因为=
符号是编程中的一个基本操作,甚至可以复制机器级别的寄存器。此外,当使用=
符号时,复制构造器(在第十一章中描述)有时也会被调用,例如:
MyType b;
MyType a = b;
a = b;
在第二行中,对象a
正在被定义。一个新的对象正在以前不存在的地方被创建。因为现在你已经知道 C++ 编译器对对象初始化有多谨慎,所以你知道构造器必须总是在定义对象的地方被调用。但是哪个构造者?a
是从一个现有的MyType
对象(等号右边的上的b
)创建的,所以只有一个选择:复制构造器。即使涉及到等号,也要调用复制构造器。
第三行,事情就不一样了。等号的左边是一个先前初始化的对象。显然,你不会为一个已经创建的对象调用构造器。在这种情况下,调用MyType::operator=
来调用a
,将出现在右侧的任何内容作为参数。
注意你可以有多个operator=
函数来接受不同类型的右边参数。
这种行为不限于复制构造器。任何时候你使用一个=
而不是构造器的普通函数调用形式来初始化一个对象,编译器会寻找一个接受右边任何东西的构造器;见清单 12-10 。
清单 12-10 。复制与初始化
//: C12:CopyingVsInitialization.cpp
class Fi {
public:
Fi() {}
};
class Fee {
public:
Fee(int) {}
Fee(const Fi&) {}
};
int main() {
Fee fee = 1; // Fee(int)
Fi fi;
Fee fum = fi; // Fee(Fi)
} ///:∼
当处理=
符号时,重要的是记住这个区别:如果对象还没有被创建,需要初始化;否则使用赋值operator=
。
避免编写使用=
进行初始化的代码更好;相反,总是使用显式构造器形式。带有等号的两个结构就变成了
Fee fee(1);
Fee fum(fi);
操作员的行为=
在Integer.h
和Byte.h
中,你看到了operator=
只能是一个成员函数。它与=
左侧的物体紧密相连。如果有可能全局定义operator=
,那么您可能会尝试重新定义内置的=
符号,如下所示:
int operator=(int, MyType); // Global = not allowed!
编译器通过强迫你将operator=
变成一个成员函数来回避这个问题。
当您创建一个operator=
时,您必须将所有必需的信息从右边的对象复制到当前对象(也就是说,operator=
被调用的对象)中,以执行您认为对您的类“分配”的任何事情。对于简单的对象,这是显而易见的,正如你在清单 12-11 中看到的。
清单 12-11 。简单赋值
//: C12:SimpleAssignment.cpp
// Simple operator=()
#include <iostream>
using namespace std;
class Value {
int a, b;
float c;
public:
Value(int aa = 0, int bb = 0, float cc = 0.0)
: a(aa), b(bb), c(cc) {}
Value& operator=(const Value& rv) {
a = rv.a;
b = rv.b;
c = rv.c;
return *this;
}
friend ostream&
operator<<(ostream& os, const Value& rv) {
return os << "a = " << rv.a << ", b = "
<< rv.b << ", c = " << rv.c;
}
};
int main() {
Value a, b(1, 2, 3.3);
cout << "a: " << a << endl;
cout << "b: " << b << endl;
a = b;
cout << "a after assignment: " << a << endl;
} ///:∼
这里,=
左边的对象复制了右边对象的所有元素,然后返回一个对自身的引用,这允许创建一个更复杂的表达式。
这个例子包括一个常见的错误。当你分配两个相同类型的对象时,你应该总是首先检查自分配:对象是否被分配给它自己?在某些情况下,比如这种情况,无论如何执行赋值操作都是无害的,但是如果对类的实现进行了更改,就会有所不同,如果您没有养成这样做的习惯,您可能会忘记并导致难以发现的错误。
类中的指针
如果对象没那么简单会怎么样?例如,如果对象包含指向其他对象的指针呢?简单地复制一个指针意味着你将得到两个指向相同存储位置的对象。在这种情况下,你需要自己记账。
解决这个问题有两种常见的方法。最简单的技术是当你做赋值或复制构造时,复制指针所指的任何东西。这很简单,如清单 12-12 所示。
清单 12-12 。用指针复制
//: C12:CopyingWithPointers.cpp
// Solving the pointer aliasing problem by
// duplicating what is pointed to during
// assignment and copy-construction.
#include "../require.h"
#include <string>
#include <iostream>
using namespace std;
class Dog {
string nm;
public:
Dog(const string& name) : nm(name) {
cout << "Creating Dog: " << *this << endl;
}
// Synthesized copy-constructor & operator= are correct.
// Create a Dog from a Dog pointer:
Dog(const Dog* dp, const string& msg)
: nm(dp->nm + msg) {
cout << "Copied dog " << *this << " from "
<< *dp << endl;
}
∼Dog() {
cout << "Deleting Dog: " << *this << endl;
}
void rename(const string& newName) {
nm = newName;
cout << "Dog renamed to: " << *this << endl;
}
friend ostream&
operator<<(ostream& os, const Dog& d) {
return os << "[" << d.nm << "]";
}
};
class DogHouse {
Dog* p;
string houseName;
public:
DogHouse(Dog* dog, const string& house)
: p(dog), houseName(house) {}
DogHouse(const DogHouse& dh)
: p(new Dog(dh.p, " copy-constructed")),
houseName(dh.houseName
+ " copy-constructed") {}
DogHouse& operator=(const DogHouse& dh) {
// Check for self-assignment:
if(&dh != this) {
p = new Dog(dh.p, " assigned");
houseName = dh.houseName + " assigned";
}
return *this;
}
void renameHouse(const string& newName) {
houseName = newName;
}
Dog* getDog() const { return p; }
∼DogHouse() { delete p; }
friend ostream&
operator<<(ostream& os, const DogHouse& dh) {
return os << "[" << dh.houseName
<< "] contains " << *dh.p;
}
};
int main() {
DogHouse fidos(new Dog("Fido"), "FidoHouse");
cout << fidos << endl;
DogHouse fidos2 = fidos; // Copy construction
cout << fidos2 << endl;
fidos2.getDog()->rename("Spot");
fidos2.renameHouse("SpotHouse");
cout << fidos2 << endl;
fidos = fidos2; // Assignment
cout << fidos << endl;
fidos.getDog()->rename("Max");
fidos2.renameHouse("MaxHouse");
} ///:∼
Dog
是一个简单的类,只包含一个保存狗的名字的string
。然而,你通常会知道什么时候Dog
发生了什么,因为构造器和析构函数在被调用时会打印信息。请注意,第二个构造器有点像复制构造器,只是它采用了一个指向Dog
的指针而不是引用,并且它有一个第二个参数,这是一条连接到参数Dog
名称的消息。这用于帮助跟踪程序的行为。
您可以看到,每当成员函数打印信息时,它并不直接访问该信息,而是将*this
发送到cout
。这又叫ostreamoperator<<
。这样做是有价值的,因为如果你想重新格式化Dog
信息的显示方式(就像通过添加[ and ]所做的那样),你只需要在一个地方完成。
一个DogHouse
包含一个Dog*
,并演示了当你的类包含指针时,你总是需要定义的四个函数:所有必要的普通构造器、复制构造器、operator=
( 要么定义它,要么不允许它),以及一个析构函数。operator=
检查自我分配是理所当然的,尽管在这里并没有严格的必要。这实际上消除了你忘记检查自我赋值的可能性,如果你确实改变了代码以使它变得重要。
参考计数
在清单 12-12 的中,复制构造器和operator=
为指针指向的内容创建了一个新的副本,析构函数删除了它。但是,如果您的对象需要大量内存或很高的初始化开销,您可能希望避免这种复制。解决这个问题的一种常见方法叫做引用计数。你赋予被指向的对象智能,让它知道有多少对象指向它。那么复制构造或赋值意味着将另一个指针附加到一个现有的对象上,并增加引用计数。销毁意味着减少引用计数,如果引用计数变为零,则销毁对象。
但是如果你想写对象(清单 12-12 中的中的Dog
)呢?可能不止一个对象在使用这个Dog
,所以你会修改别人的Dog
和你的,这看起来不太友好。为了解决这个“混叠”问题,使用了一种称为写时复制的附加技术。在写入内存块之前,您要确保没有其他人在使用它。如果引用计数大于 1,那么在写入之前,您必须为自己创建一个该块的个人副本,这样您就不会打扰到其他人。参见清单 12-13 中引用计数和写时复制的简单例子。
清单 12-13 。说明引用计数和写入时复制
//: C12:ReferenceCounting.cpp
// Reference count, copy-on-write
#include "../require.h"
#include <string>
#include <iostream>
using namespace std;
class Dog {
string nm;
int refcount;
Dog(const string& name)
: nm(name), refcount(1) {
cout << "Creating Dog: " << *this << endl;
}
// Prevent assignment:
Dog& operator=(const Dog& rv);
public:
// Dogs can only be created on the heap:
static Dog* make(const string& name) {
return new Dog(name);
}
Dog(const Dog& d)
: nm(d.nm + " copy"), refcount(1) {
cout << "Dog copy-constructor: "
<< *this << endl;
}
∼Dog() {
cout << "Deleting Dog: " << *this << endl;
}
void attach() {
++refcount;
cout << "Attached Dog: " << *this << endl;
}
void detach() {
require(refcount != 0);
cout << "Detaching Dog: " << *this << endl;
// Destroy object if no one is using it:
if(--refcount == 0) delete this;
}
// Conditionally copy this Dog.
// Call before modifying the Dog, assign
// resulting pointer to your Dog*.
Dog* unalias() {
cout << "Unaliasing Dog: " << *this << endl;
// Don't duplicate if not aliased:
if(refcount == 1) return this;
--refcount;
// Use copy-constructor to duplicate:
return new Dog(*this);
}
void rename(const string& newName) {
nm = newName;
cout << "Dog renamed to: " << *this << endl;
}
friend ostream&
operator<<(ostream& os, const Dog& d) {
return os << "[" << d.nm << "], rc = "
<< d.refcount;
}
};
class DogHouse {
Dog* p;
string houseName;
public:
DogHouse(Dog* dog, const string& house)
: p(dog), houseName(house) {
cout << "Created DogHouse: "<< *this << endl;
}
DogHouse(const DogHouse& dh)
: p(dh.p),
houseName("copy-constructed " +
dh.houseName) {
p->attach();
cout << "DogHouse copy-constructor: "
<< *this << endl;
}
DogHouse& operator=(const DogHouse& dh) {
// Check for self-assignment:
if(&dh != this) {
houseName = dh.houseName + " assigned";
// Clean up what you're using first:
p->detach();
p = dh.p; // Like copy-constructor
p->attach();
}
cout << "DogHouse operator= : "
<< *this << endl;
return *this;
}
// Decrement refcount, conditionally destroy
∼DogHouse() {
cout << "DogHouse destructor: "
<< *this << endl;
p->detach();
}
void renameHouse(const string& newName) {
houseName = newName;
}
void unalias() { p = p->unalias(); }
// Copy-on-write. Anytime you modify the
// contents of the pointer you must
// first unalias it:
void renameDog(const string& newName) {
unalias();
p->rename(newName);
}
// ... or when you allow someone else access:
Dog* getDog() {
unalias();
return p;
}
friend ostream&
operator<<(ostream& os, const DogHouse& dh) {
return os << "[" << dh.houseName
<< "] contains " << *dh.p;
}
};
int main() {
DogHouse
fidos(Dog::make("Fido"), "FidoHouse"),
spots(Dog::make("Spot"), "SpotHouse");
cout << "Entering copy-construction" << endl;
DogHouse bobs(fidos);
cout << "After copy-constructing bobs" << endl;
cout << "fidos:" << fidos << endl;
cout << "spots:" << spots << endl;
cout << "bobs:" << bobs << endl;
cout << "Entering spots = fidos" << endl;
spots = fidos;
cout << "After spots = fidos" << endl;
cout << "spots:" << spots << endl;
cout << "Entering self-assignment" << endl;
bobs = bobs;
cout << "After self-assignment" << endl;
cout << "bobs:" << bobs << endl;
// Comment out the following lines:
cout << "Entering rename(\"Bob\")" << endl;
bobs.getDog()->rename("Bob");
cout << "After rename(\"Bob\")" << endl;
} ///:∼
类Dog
是由一个DogHouse
指向的对象。它包含一个引用计数以及控制和读取引用计数的函数。这里有一个复制构造器,所以你可以从现有的 ?? 中创建一个新的。
attach( )
函数增加一个Dog
的引用计数,表示有另一个对象正在使用它,而detach( )
减少引用计数。如果引用计数变为零,那么就没有人再使用它了,所以成员函数通过说delete this
来销毁自己的对象。
在您进行任何修改(比如重命名一个Dog
)之前,您应该确保您没有更改某个其他对象正在使用的Dog
。你可以通过调用DogHouse::unalias( )
来实现,后者又调用Dog::unalias( )
。如果引用计数为 1(意味着没有其他人指向那个Dog
),后一个函数将返回现有的Dog
指针,但是如果引用计数大于 1,将复制Dog
。
复制构造器不是创建自己的内存,而是将Dog
分配给源对象的Dog
。然后,因为现在有一个额外的对象在使用那个内存块,所以它通过调用Dog::attach( )
来增加引用计数。
operator=
处理一个已经在=
左侧创建的对象,所以它必须首先通过为那个Dog
调用detach( )
来清理它,如果没有其他人在使用它,这将销毁旧的Dog
。然后operator=
重复复制构造器的行为。请注意,它首先检查您是否将同一个对象分配给了它自己。
析构函数调用detach( )
有条件地析构Dog
。
要实现写入时复制,您必须控制写入内存块的所有操作。例如,renameDog( )
成员函数允许你改变内存块中的值。但是首先,它使用unalias( )
来防止修改一个别名Dog
(一个有不止一个DogHouse
对象指向它的Dog
)。如果你需要从一个DogHouse
中产生一个指向一个Dog
的指针,你首先需要unalias( )
这个指针。
main( )
测试必须正确工作以实现引用计数的各种函数:构造器、复制构造器、operator=
和析构函数。它还通过调用renameDog( )
来测试写入时复制。
下面是输出(经过一点重新格式化):
Creating Dog: [Fido], rc = 1
Created DogHouse: [FidoHouse]
contains [Fido], rc = 1
Creating Dog: [Spot], rc = 1
Created DogHouse: [SpotHouse]
contains [Spot], rc = 1
Entering copy-construction
Attached Dog: [Fido], rc = 2
DogHouse copy-constructor:
[copy-constructed FidoHouse]
contains [Fido], rc = 2
After copy-constructing bobs
fidos:[FidoHouse] contains [Fido], rc = 2
spots:[SpotHouse] contains [Spot], rc = 1
bobs:[copy-constructed FidoHouse]
contains [Fido], rc = 2
Entering spots = fidos
Detaching Dog: [Spot], rc = 1
Deleting Dog: [Spot], rc = 0
Attached Dog: [Fido], rc = 3
DogHouse operator= : [FidoHouse assigned]
contains [Fido], rc = 3
After spots = fidos
spots:[FidoHouse assigned] contains [Fido],rc = 3
Entering self-assignment
DogHouse operator= : [copy-constructed FidoHouse]
contains [Fido], rc = 3
After self-assignment
bobs:[copy-constructed FidoHouse]
contains [Fido], rc = 3
Entering rename("Bob")
After rename("Bob")
DogHouse destructor: [copy-constructed FidoHouse]
contains [Fido], rc = 3
Detaching Dog: [Fido], rc = 3
DogHouse destructor: [FidoHouse assigned]
contains [Fido], rc = 2
Detaching Dog: [Fido], rc = 2
DogHouse destructor: [FidoHouse]
contains [Fido], rc = 1
Detaching Dog: [Fido], rc = 1
Deleting Dog: [Fido], rc = 0
通过研究输出、跟踪源代码和试验程序,您将加深对这些技术的理解。
自动运算符=创建
因为将一个对象分配给同类型的另一个对象是大多数人期望可能的活动,所以如果你不创建一个type::operator=(type)
,编译器会自动创建一个。这个操作符的行为模仿了自动创建的复制构造器的行为;如果该类包含对象(或者是从另一个类继承的),那么这些对象的operator=
将被递归调用。这称为成员式分配。参见清单 12-14 中的示例。
清单 12-14 。阐释成员式分配
//: C12:AutomaticOperatorEquals.cpp
#include <iostream>
using namespace std;
class Cargo {
public:
Cargo& operator=(const Cargo&) {
cout << "inside Cargo::operator=()" << endl;
return *this;
}
};
class Truck {
Cargo b;
};
int main() {
Truck a, b;
a = b; // Prints: "inside Cargo::operator=()"
} ///:∼
为Truck
自动生成的operator=
调用Cargo::operator=
。
一般来说,你不想让编译器为你做这些。任何复杂程度的类(尤其是当它们包含指针时!)你想显式创建一个operator=
。如果你真的不想让人们执行赋值,就把operator=
声明为一个private
函数。
注意你不需要定义它,除非你在类内部使用它。
自动类型转换
在 C 和 C++ 中,如果编译器发现一个表达式或函数调用使用的类型不是它所需要的类型,它通常会自动执行从它所拥有的类型到它想要的类型的类型转换。在 C++ 中,通过定义自动类型转换函数,您可以为用户定义的类型实现同样的效果。这些函数有两种类型:一种特定类型的构造器和一种重载运算符。
构造器转换
如果您定义一个构造器,该构造器将另一种类型的对象(或引用)作为其单个参数,则该构造器允许编译器执行自动类型转换。参见清单 12-15 中的示例。
清单 12-15 。说明自动类型转换
//: C12:AutomaticTypeConversion.cpp
// 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
} ///:∼
当编译器看到用一个One
对象调用f( )
时,它会查看f( )
的声明,并注意到它需要一个Two
。然后,它会查看是否有办法从一个One
获取一个Two
,并找到构造器Two::Two(One)
,它会悄悄地调用它。得到的Two
对象被交给f( )
。
在这种情况下,自动类型转换让你免去了定义两个重载版本f( )
的麻烦。然而,代价是对Two
的隐藏构造器调用,如果你关心对f( )
调用的效率,这可能很重要。
阻止构造器转换
有时,通过构造器进行自动类型转换会导致问题。要关闭它,您可以通过以关键字explicit
开头来修改构造器(它只适用于构造器)。清单 12-16 使用这个关键字修改清单 12-15 中Two
类的构造器。
清单 12-16 。说明显式关键字的使用
//: C12:ExplicitKeyword.cpp
// 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
} ///:∼
通过使Two
的构造器显式化,编译器被告知不要使用该特定构造器执行任何自动转换(该类中的其他非explicit
构造器仍然可以执行自动转换)。如果用户想要进行转换,必须写出代码。在清单 12-16 的中,f(Two(one))
从one
创建了一个Two
类型的临时对象,就像编译器在清单 12-15 的中所做的一样。
运算符转换
产生自动类型转换的第二种方法是通过运算符重载。您可以创建一个成员函数,该函数接受当前类型,并使用关键字operator
后跟您想要转换的类型,将其转换为所需的类型。这种形式的操作符重载是独特的,因为你似乎没有指定返回类型——返回类型是你重载的操作符的名称。参见清单 12-17 中的示例。
清单 12-17 。阐释运算符重载转换
//: C12:OperatorOverloadingConversion.cpp
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);
g(1); // Calls Three(1,0)
} ///:∼
使用构造器技术,目标类执行转换,但是使用操作符,源类执行转换。构造器技术的价值在于,您可以在创建新类时向现有系统添加新的转换路径。然而,创建一个单参数构造器总是定义一个自动的类型转换(即使它有不止一个参数,如果其余的参数是默认的),这可能不是你想要的(在这种情况下你可以使用explicit
关闭它)。此外,没有办法使用从用户定义类型到内置类型的构造器转换;这只有在运算符重载的情况下才有可能。
反身性
使用全局重载操作符而不是成员操作符的一个最方便的原因是,在全局版本中,自动类型转换可以应用于任一操作数,而对于成员对象,左边的操作数必须已经是正确的类型。如果你希望两个操作数都被转换,全局版本可以节省大量的代码;参见清单 12-18 。
清单 12-18 。说明重载中的反身性
//: C12:ReflexivityInOverloading.cpp
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
} ///:∼
类Number
有一个成员operator+
和一个friendoperator–
。因为有一个接受单个int
参数的构造器,所以一个int
可以自动转换成一个Number
,但是只有在正确的条件下。在main( )
中,您可以看到向另一个Number
添加一个Number
工作正常,因为它与重载操作符完全匹配。此外,当编译器看到一个Number
后跟一个+
和一个int
时,它可以匹配到成员函数Number::operator+
,并使用构造器将int
参数转换为Number
。但是当它看到一个int
、一个+
和一个Number
时,它不知道该怎么做,因为它只有Number::operator+
,这要求左操作数已经是一个Number
对象。因此,编译器会发出一个错误。
有了friendoperator–
,事情就不一样了。编译器需要尽可能地填充它的两个参数;它并不局限于用一个Number
作为左边的参数。因此,如果它看到
1 – a
它可以使用构造器将第一个参数转换成一个Number
。
有时,您希望能够通过使操作符成为成员来限制它们的使用。例如,当矩阵乘以向量时,向量必须在右边。但是如果您希望您的操作符能够转换任何一个参数,那么就让操作符成为一个friend
函数。
幸运的是,编译器不会接受1 – 1
并将两个参数都转换成Number
对象,然后调用operator–
。这意味着现有的 C 代码可能会突然开始以不同的方式工作。编译器首先匹配“最简单”的可能性,这是表达式1 – 1
的内置操作符。
类型转换示例
自动类型转换非常有用的一个例子是任何封装字符串的类(在这种情况下,您只需使用标准的 C++ string
类实现该类,因为它很简单)。如果没有自动类型转换,如果你想使用标准 C 库中所有现有的字符串函数,你必须为每个函数创建一个成员函数,如清单 12-19 所示。
清单 12-19 。不使用自动类型转换
//: C12:Strings1.cpp
// No auto type conversion
#include "../require.h"
#include <cstring>
#include <cstdlib>
#include <string>
using namespace std;
class Stringc {
string s;
public:
Stringc(const string& str = "") : s(str) {}
int strcmp(const Stringc& S) const {
return ::strcmp(s.c_str(), S.s.c_str());
}
// ... etc., for every function in string.h
};
int main() {
Stringc s1("hello"), s2("there");
s1.strcmp(s2);
} ///:∼
这里只创建了strcmp( )
函数,但是您必须为可能需要的<cstring>
中的每个人创建一个相应的函数。幸运的是,您可以提供一个自动的类型转换,允许访问<cstring>
中的所有函数,如清单 12-20 中的所示。
清单 12-20 。使用自动类型转换
//: C12:Strings2.cpp
// With auto type conversion
#include "../require.h"
#include <cstring>
#include <cstdlib>
#include <string>
using namespace std;
class Stringc {
string s;
public:
Stringc(const string& str = "") : s(str) {}
operator const char*() const {
return s.c_str();
}
};
int main() {
Stringc s1("hello"), s2("there");
strcmp(s1, s2); // Standard C function
strspn(s1, s2); // Any string function!
} ///:∼
现在任何带有char*
参数的函数也可以带有Stringc
参数,因为编译器知道如何从Stringc
生成char*
。
自动类型转换中的陷阱
因为编译器必须选择如何安静地执行类型转换,所以如果您没有正确地设计转换,它会遇到麻烦。一个简单而明显的情况发生在一个类X
上,它可以将自己转换成一个带有operator Y( )
的类Y
的对象。如果类Y
有一个接受类型X
的单个参数的构造器,这表示相同的类型转换。编译器现在有两种从X
到Y
的方法,所以当转换发生时,它会产生一个模糊错误;参见清单 12-21 。
清单 12-21 。说明自动类型转换中的二义性
//: C12:TypeConversionAmbiguity.cpp
class Orange; // Class declaration
class Apple {
public:
operator Orange() const; // Convert Apple to Orange
};
class Orange {
public:
Orange(Apple); // Convert Apple to Orange
};
void f(Orange) {}
int main() {
Apple a;
//! f(a); // Error: ambiguous conversion
} ///:∼
这个问题最明显的解决方法就是不去做。只需为从一种类型到另一种类型的自动转换提供一条路径。
当您提供到多种类型的自动转换时,会出现一个更难发现的问题。这有时被称为扇出;参见清单 12-22 。
清单 12-22 。说明“扇出”
//: C12:TypeConversionFanout.cpp
class Orange {};
class Pear {};
class Apple {
public:
operator Orange() const;
operator Pear() const;
};
// Overloaded eat():
void eat(Orange);
void eat(Pear);
int main() {
Apple c;
//! eat(c);
// Error: Apple -> Orange or Apple -> Pear ???
} ///:∼
类Apple
自动转换为Orange
和Pear
。关于这一点的阴险之处在于,直到有人无意中出现并创建了两个过载版本的eat( )
时,才出现问题。(只有一个版本,main( )
中的代码运行良好。)
同样,解决方案——以及自动类型转换的通用口号——是只提供一个从一种类型到另一种类型的自动转换。您可以转换为其他类型;它们不应该是自动的。您可以用像makeA( )
和makeB( )
这样的名字创建显式函数调用。
隐藏活动
自动类型转换可能会引入比您想象的更多的底层活动。作为一个小脑筋急转弯,看看前面对CopyingVsInitialization.cpp
程序的如下修改(清单 12-23 )。
清单 12-23 。阐释自动类型转换中的隐藏活动
//: C12:CopyingVsInitialization2.cpp
class Fi {};
class Fee {
public:
Fee(int) {}
Fee(const Fi&) {}
};
class Fo {
int i;
public:
Fo(int x = 0) : i(x) {}
operator Fee() const { return Fee(i); }
};
int main() {
Fo fo;
Fee fee = fo;
} ///:∼
没有从Fo
对象创建Fee fee
的构造器。然而,Fo
可以自动转换成Fee
。没有从Fee
创建Fee
的复制构造器,但是这是编译器可以为你创建的特殊函数之一。(默认构造器、复制构造器、 operator=
和析构函数可以由编译器自动合成。)所以对于相对无伤大雅的说法
Fee fee = fo;
调用自动类型转换运算符,并创建一个复制构造器。
小心使用自动类型转换。和所有的操作符重载一样,当它显著地减少了编码任务时,它是非常好的,但是通常不值得免费使用。
审查会议
- 1.存在操作符重载的全部原因是为了那些使生活变得更容易的情况。没什么特别神奇的;重载操作符只是名字有趣的函数,当编译器发现正确的模式时,它会为你调用函数。
- 2.但是如果操作符重载没有给你(类的创建者)或类的用户提供显著的好处,不要通过添加它来混淆这个问题。