Programming 版 (精华区)

发信人: zhangyan (我让模电拼了……), 信区: Programming
标  题: Effective STL Item 12
发信站: 哈工大紫丁香 (2001年06月09日09:44:34 星期六), 站内信件

如何有效地进行运算符重载 

重载运算符将使代码更清晰-只在合理使用它们. 

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/. 

--
※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: 61.158.69.80]
[百宝箱] [返回首页] [上级目录] [根目录] [返回顶部] [刷新] [返回]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:4.364毫秒