如何有效地进行运算符重载
重载运算符将使代码更清晰-只在合理使用它们.
by Bill Wagner
译者:黄森堂(vcmfc)
C++初学者(特别是从其他语言“叛逃”而来的)往往视运算符重载为一大绊脚石,害怕改变内建运算符的原意(参阅“What is Operator Overloading?”)。殊不知,这却能使得代码清晰,比如"C=A+B"和"C.assign(A.plus(B))",您更喜欢哪一个呢?若不重载运算符,您只能选择累赘的后者了。
考虑重载运算符时,您应站在客户的角度,为代码的使用者想想。不妨先自问一下,我这样做是提高还是降低代码的可读性,再由此做出恰当的决定。而其中的关键并不在于实现函数功能,而是由于每种运算符都有其约定俗成的含义,重载它们应是在保留原有含义的基础上对功能的扩展,而非改变。所以应时刻站在使用者的角度来审视,比如为扩展的算术类型而重载算术运算符就是很自然的,以复数为例:
Complex c1( 1.0, -4.0 );
Complex c2( 15.0, 1.0 );
Complex c3 = c1 * c2;
c2 *= c1;
那么对于非算术类(例如一个Document类),自然就不应当重载算术运算符,这会让我们面对一群陌生的运算符而不知所措。从这里我们可以得到一点重载运算符的心得:对于特定的类,如果一眼能看出某个运算符在此处的含义,那么函数实现往往也很容易;若连您自己对其作用都有所迟疑,那么请罢手,连您自己都不能很快反应过来,何况别人呢?
I/O组: <<, >>
这是通常IO流运算符重载的规则,你应该这样写,在实际上,大部分的客户端预期在你的类已实现这些运算符来支持io流,如何你的类支持序列化的话,你应该实现这样功能,除了在MFC中支持CArchive之外应该实现的功能。你可以在你的类中始终实现这些功能,除非你明确不支持存储你的对象到文件与从文件中读取。例如:MFC的CString支持序列化,但CDC不支持,CString声明一个string,它将能进行存储到文件与从文件中恢复,CDC是设备,它是操作系统资源但不能恢复。
以下运算符实现了在任何支持序列化的类中实现提取与插入: ostream& operator <<
(ostream& o, const MyClass& c);
istream& operator >>
(istream& I, MyClass& c);
注释:以上两个运算符是公共函数,非成员函数,第一个参数对每一个函数来说是相同的,都是IO流。如果两个运算符是成员函数,它们需要内部的iostream类,iostream类是标准C++库的一部分,追加新的定义,你不能在内部实现流运算符,他们必须是公共的,在你的类中友元你写的函数,那么就可以存取所有的内部变量。
比较与赋值:=, !=, ==
这些运算符是实现在任何类中实现你想拷贝值的赋值运算,如果在你的类中写了拷贝构造,那么你得实现这些功能,再一次强调,这些功能是新增到你的类的公共部分,在公共部分包含了这些运算符并显露它们。
所有的比较运算符都有关系到赋值运算,你通常在你的类中提供了拷贝构造与赋值操作,然后,如果你支持赋值,那么你也要支持同等的比较对象的能力,公共实现了!=与==操作,在实际上,如果你明确隐含了赋值操作,比较操作仍然值得做
MFC的CRect类提供了这些所有运算符,尽管你设想有两个矩形相等的理由.MFC的CBrush类不提供,将赋值操作创建两个
brush来指向系统资源或创建新的系统资源?任何一个都可以,程序员在读客端的CBrush赋值声明将不明白如何管理下面的系统资源,客户端程序员可能需要知道 // as global operators:
bool operator == (const MyClass& l,
const MyClass& r)
{
bool isEqual;
//... test each member,
// set isEqual.
Return isEqual;
};
bool operator != (const MyClass& l,
const MyClass& r)
{
return ! (l == r);
}
// As member functions:
bool operator == (const MyClass& r)
const
{
bool isEqual;
//... test each member,
// set isEqual.
Return isEqual;
};
bool operator != (const MyClass& r)
const
{
return ! (*this == r);
}
次序关系:<, >, <=, >=
标准模板库在它所有的查找与排序算法中使用次序关系,如果你实现这些功能那么你随时都可以在你的类中使用自然的次序比较,没有一个MFC对象包装GDI对象来支持这些操作,然而,CString类却支持所有的。
STL使用小于运算符来进行排序与查找的比较,且STL算法只需要小于运算符,不需要其它。STL通过限制这种需求小于运算符,不要在你的类中把这种限制强加于所有用户上。我更喜欢给予客户端存取所有的次序比较,同样,如果类已定义了次序关系,所有同等的运算符都得定义,你得避免在其它条件中用函数实现它们,你得到避免在小组的其它地方使用同等的函数。注意:如果我在小组中实现了>=,我就会实现<、<=、>。一般认为只要实现>与<就可以,这是错误的,因为当两个对象相等的时候。 bool operator < (const MyClass& r)
const
{
// ... test each member.
};
bool operator >= (const MyClass& r)
const
{
return !(*this < r)
}
bool operator > (const MyClass& r)
const
{
// ... test each member.
};
bool operator <= (const MyClass& r)
const
{
return !(*this > r);
}
递增与递减:++, --
这些运算符是递增与递减类对象,在以下三种情形你需要重载它们:类型指示器(译者:像STL的Iterator),顺序存取与整型类型。类型的整数模型的支持要有明确的意义。
类型指示器是个指向vector类型,数据库光标,似类的指示器,标准C++库提供少量有关这方面的例子,在STL中iterator支持递增与与递减,在一些场合,这些操作进行向前与向后移动对象集,增加那些函数在类来包装数据的存取;通过递增与递减来向前与向后移动数的行数。
我通过使用顺序存取来循环取得color,通过递增与递减来移动到向后与向前的color上(译者:我觉得作者的意思:如果你要提供像操纵数据库中的数据表的上一笔与下一笔等功能的话,你就需要重载它)。
只要你想重载递增与递减运算符,你要记住每个操作符有前递增与后递增的版本,以下是递增的例子: int i=5;
int j = i++; // j == 5, i == 6.
j = ++i; // i == 7, j == 7.
注释:返回值有对于前递增与后递增是不同的,C++语言定义了前递增版本是无参数,后递增是有一个整数参数,你的类应该象如下定义: //前递增,例:++i
MyClass& operator++ ()
{
// ... whatever is necessary.
return *this;
}
//后递增,例:i++
MyClass operator ++ (int)
{
// Make a copy.
MyClass rval (*this);
// ... whatever is necessary.
// to increment *this.
// Return the old value.
return rval;
}
在通常驻,你应该支持前递增与后递增的两个版本,但有时候,只需要前递增版本,后递增版本需要你拷类对象;有时,拷贝对象是昂贵的代价,所以只支持前递增;当对象使用大多的内存来进行拷贝构造的时候,我不赞成支持后递增,类的使用者不知道没有拷贝构造的支持是无法支持后递补增的。
加法,减法:+, -, +=, -=
这些运算符执行通常驻的算术运算,有两种情形你需要重载它们:首先,你的类需要模拟数学模型,第二是当你的类是要模拟指针.通常的错误是认为只需要重载少量的运算符,而我认为应该成组重载,因为用户在使用你的类时认为它们已经全部重载了。
+=与-=比二元运算符+能更好工作,二元运算符版本在返回结果创建了你的类的拷贝,当函数执行时候它们创建了临时对象。 MyClass operator + (const MyClass& r)
const
{
MyClass rVal;
// perform operations, storing the
// result in rVal.
return rVal;
}
MyClass& operator += (const
MyClass& r)
{
// perform operations, storing the
// result in this.
return *this;
}
乘法: *, /, *=, /= 整除:%, %=
这些运算符执行通常的算术乘法与除法,像加法与减法运算符,只有在你的类中需要模拟处理这些类型时才需要重载它们,我分开整数乘法与除法是因为它们只有在整数类型才有意义。
相似的其它运算符,如果你的类重载过一个,那么你应该重载相关的全部运算符,这些函数的原型与加法与减法是一样的,例如:*、/相似于+,它们都有更多效的一元运算符(如:*=,/=),因此,当你重载其中的一个,那么人就得重载全部。
内存管理组:new delete
C++ new与delete运算符是在用户需要在公共内存中产生新对象或删除已存在对象而被调用,你可以写类的专有版本函数或公共版本;只有在你的类对象或派生类的创建中使用类的专有版本。
只有一个理由来重载这些函数:重载它们来改善你的应用程序的性能,如果你重载这些函数,你必须提供相同的界面,相同的外部行为与相同的功能。前面所说的,很少重载new和delete运算符的全局的form。这么做会产生很严重的后果,因为它使你的库与跟已经与new和delete运算符的标准form相连接的库产生冲突。而且,它使你的代码与所有可能产生同样结论的库相冲突。例如,MFC使用全局的new和delete运算符来避免内存的泄漏。
我创建了类的专有版本的new和delete,在我运行了我的一个应用程序的配置文件并发现内存的分配和释放是瓶颈的时候。一般来说,在程序运行过程中有很多小的对象被创建和删除,例如一个简单的自由手绘工具。鼠标的每次移动都在图形的一系列点中增加一个新点,而每个点都包含两个整数。无论何时使用者创建了一个新的图形,可能会增加成百上千的点。如果使用者删除了一个图形,可能有成百上千的点被删除。
你可以通过很多的途径调用new和delete。无论何时你重载new和delete,你要保证在所有的form中重载是有意义的。首先,在标准的form里重载new和delete,无论何时生成一个对象都得调用类的专有版本。第二,重载new的数组form版:运算符new[]。如果你重载了这两个form,只有当用户需要一个对象而不是对象的数组的时候,你才可以得到你改良版的new和delete。你需要为你写的每个new的form写一个delete。
要认识到new和delete是继承的,即使它们是静态的函数。这意味着你的专有版本的new和delete是被你自己的类派生出来的类所调用。偶然的是,你的类的专有版本的new和delete函数是基于对象的大小的,并且是你的专有的函数 class MyClass
{
public:
// Two forms of operator new:
static void* operator new(size_t
size); // Regular new
static void* operator new
[](size_t size); // array new
// Similar forms of operator
// delete:
static void operator delete (void*
rawMemory, size_t size);
static void operator
delete[](void*
rawMemory, size_t size);
}
在重载它们之前你需要充分理解所有标准版本newdelete的内部行为,记得你重载了new或delete,你必须重载两者;同样,new与delete在许多form是不同的,你要确信你能解决所有的问题。
指针引用标识符:->, *
使用这些运算符来操纵指向内存中自已的引用,如果p是指向整数,那么*p的值就是整数;如果pRect是指向CRect对象,pRect->left是矩形的左值。
只有当你要模拟指针的时候才需要重载这些运算符,标准库auto_ptr定义了它们与STL中所有的iterator,当你需要自已的版本函数,你得遵守相同的应用规则,我建议写两个版本:const对象版与随机存取对象版,如果你不写这两个版本,在你的新类中适当使用const方式,你需要非const版本。因此,当客户端程序员使用的智能指针去修改指向对象,你需要cons版本来处理你的类的const对象,如果你没有定义这个运算符,你就不能使用const智能指针去存取指向的对象。 class MyClass
{
public:
_Ty* operator -> ();
const _Ty* operator->() const;
_Ty& operator* ();
const _Ty& operator*()const;
};
函数调用标识符:()
使用函数运算符来处理你的类的函数类型,当类支持函数运算符来调用你的函数对象,在两种情形下你需要重载函数运算符:以前,程序员使用这种调用来提供两维或更多给的数组类: class TwoDArray {
public:
float operator () (int x, int y)
const;
float& operator () (int x, int y);
};
但现在,在更多的场合里在你使用这个函数来提供函数对象来使用STL算法(在这个文档里它不能完全代替函数对象,可参考其它信息),例如:排序函数使用函数对象来比较对象类型:class CompareObjects {
public:
// Function call operator:
bool operator () (
const MyClass& l,
const MyClass& r) const
{
bool rVal = false;
// various tests.
return rVal;
};
};
// usage:
extern vector < MyClass > array;
sort (array.begin (), array.end (),
CompareObjects ());
注解:在两个示例里,你可以自由定义数字与其它参数类型的函数,你可以在任何地方调用运算符。
数组存取标识符:[]
使用[]来存取数组中的单个元素,你需要重载它们的唯一情形:模拟数组;当你重载这个运算符你需要明白两点:首先,[]是二元运算符,这意味着这个运算符只有一个参数,你不能使用这个运算符来使用多维数组;第二,你想在数组中使用任意类型的数据,这允许你结合数组来创建任何类型,例如:class MyDictionary {
public:
// Array operator:
string operator [] (const string&
word) const
{
string definition =
"who knows";
// find definiton.
return rVal;
};
// To change a definition:
string& operator [] (const string&
word);
};
// usage:
extern MyDictionary Websters;
string def = Websters["alligator"];
位操作:^, &, |, ~, ^=, &=, |=
它们都是整数值的位运算符,我从未到达使用重载来应用位运算,它们只定义了整数类型,但我找不到更好的理由来模拟整数类型。
不能使用:!, <<=, >>=, &&, ||
!是逻辑非运算符,它不能在表达式的右边(译者:可参阅Guru of the week#26 Boolean),<<=与>>=是赋值移位运算符,它们执行了在对象中进行左移或右移并返回结果,使用公共运算符来混合使用;&&是逻辑AND运算符,与||是逻辑OR运算符。
我看到!运算符来进行任何对象的有效性测试,这在你不记得它的时候,这是个好主意,如果对象是true,那么对象是有效的,至少到目前为止我只看到过这种用法;>>=与<<=不能使用是因为它们只能进行位运算,除非你的类是整数值,否则重载移位是没有意义的。
&&与||不能重载是因为它们与正常的表达式计算规则冲突,逗号是不能重载的是因为那是没有意义的
运算符重载是有用的技术,但你需要恰当地使用应用它们,你也需要确定在客户端什么时候使用你重载的运算符,使得代码更简练与明了,始终要求你自已实现重载运算符比用正常的函数更加清楚描绘你的意图。
感谢babysloth(小懒虫虫)的校验,albert_ywy(北方的狼)帮助。
Bill Wagner is the president/software architect for Midwest Seas Software Development Inc. Bill has several years of experience in Windows and C++ programming and specializes in C++, Java, and COM-based enterprise-programming solutions. Reach him at wwagner@midwestseas.com, or through http://www.midwestseas.com/.