[C++]关于重载运算符的一些建议
Posted stary_yan
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[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 delete?operator 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++]关于重载运算符的一些建议的主要内容,如果未能解决你的问题,请参考以下文章