本文共 4105 字,大约阅读时间需要 13 分钟。
本节书摘来自异步社区出版社《C++面向对象高效编程(第2版)》一书中的第4章,第4.7节,作者: 【美】Kayshav Dattatri,更多章节内容可以访问云栖社区“异步社区”公众号查看。
C++面向对象高效编程(第2版)
显然,赋值和复制是类的设计者和实现者必须考虑的重要问题。另一个相关问题也同等重要,即对象相等(object equality)的概念。当我们说两个对象相等时,即一个对象和另一个对象相等到底意味着什么?要牢记,两个不同对象相等和两个名称代表相同对象(也就是对象之间等价的概念),这两个概念是有区别的。对象相等要比较对象的结构和状态,而对象等价(object equivalence)则要比较对象的地址。两个不同的对象可能相等,但是不允许它们是同一个对象。
4.11
图4-11
以下是一个示例:
main(){ TPerson person1(“Bugs Bunny”, “Toon Town”, 414235056, “12-30-56”); TPerson* person2 = new TPerson (“Daffy”, “TV Land”, 418325156, “6-6-55”); // 动态对象 TPerson person3(“Goofy”, “Toon Town”, 418235057, “11-30-60”); TPerson* person4 = 0; // 尚未指向对象 TPerson person5(“Bugs Bunny”, “Toon Town”, 414235056, “12-30-56”); int i1 = 100 // ① int i2 = 200; // ② person4 = &person3;}```根据图4-11,我们可以提出一些简单的问题:person1 和person5是否相等?person4所指定的对象和person3所指定的对象是否相同?当然,person1和person5并不是相同的对象,但它们都具有相同的状态。另一方面,person4和person3并不是相互独立的对象,它们指定了相同的对象。很明显,person3和person4是等价的。参考上面程序中的①和②,如果我们提出这样一个问题:i1和i2是否相等,则答案非常简单它们不相等。在这种情况下,我们只需比较i1和i2中的值,便可立即推断,因为它们没有相同的值,所以不相等。但是,对于对象,我们不能作如此简单的判断,因为很难将对象看成一个简单的值。显而易见,我们还需要了解更多细节。比较对象是非常重要的操作,特别是包含复杂数据结构的对象,例如链表(list)、队列(queue)和树(tree)1,相关操作包括查找储存在内的特定对象、基于key排序对象等。所有这些操作都需要比较对象来判断对象是否相等。进一步而言,判断对象是否相等避免了意外删除对象,同时也避免了重复操作。比较两个链表之间或两个队列之间是否相等,涉及定义包含其他聚集对象的对象之间是否相等,更为复杂。###4.7.1 对象相等和对象等价在处理对象时,不同的语言定义对象相等的方式不同。例如,C++对于对象等价并未定义任何默认的含义,而Eiffel和Smalltalk则定义了默认含义。再者,不同程序员对对象间相等的解释也不同。接下来,先介绍基于引用的语言如何表示对象等价和对象相等。Smalltalk:Smalltalk为等价判断提供了==方法,所有对象都可以使用该方法。如果==方法返回值为true,则待比较的两个对象是相同的对象(它们等价)。换言之,这两个对象是对相同对象的不同引用。为了判断对象是否相等,Smalltalk提供了=方法。该方法通过比较两个对象中相应实例变量的值来实现。任何加入新实例变量的类都需要重新实现这个方法。例如,比较链表对象要涉及比较链表的长度,以及比较链表中的每个元素是否相等。这与递归导航整个对象树的深复制操作非常类似。与==对应的是~~,它用于判断两个对象是否不等价。与此类似,=对应的是~=,即不相等操作符。Eiffel:在Eiffel中,等价的语义与Smalltalk类似。简单类型的比较操作符是=,基于简单类型变量中所包含的值作比较。对于对象引用(object reference),比较操作符使用引用本身的值作比较。当两个对象引用都引自相同的对象时,则比较它们是否相等,以此判断两者是否等价。Equal方法用于比较当前对象和另一个对象是否相等,可用此方法比较对象(不是引用)。但是,Equal是一个浅比较操作,它不会递归地遍历对象引用,以判断两对象是否相等。在需要用到深比较语义时,程序员必须编写自定义的方法。另外,该语言中不相等操作符是/=。C++:C++与Smalltalk和Eiffel完全不同,这可以理解。C中定义的比较操作符是==,它用于比较值(C中不使用==操作符来比较结构)。C++中并未定义默认的比较机制。在需要使用类的比较语义时,由设计者负责实现操作符== 和在重载操作符==函数中提供正确的比较语义。比较指针与比较整数类似,而且也是语言的一部分。例如,p.146示例中,person3和person4两个指针的比较的结果显示它们包含相同的地址。比较引用和比较变量(或对象,如果它们是对象引用)相同。例如:
int i = 10;
int &ir = i;int j = 100;int &jr = j;int k = 100;int &kr = k;if (kr == ir) { } // 该判断为false – 比较k和i的值if (kr == jr) { } // 该判断为 true – 比较k和j的值`
比较引用kr和引用jr就是比较j和k的值(因为引用就是现有实体的别名)。 为了比较TPerson类对象,我们需要实现比较操作符。
class TPerson { public: // 其他细节如前所述,已省略 bool operator==(const TPerson& other) const; bool operator!=(const TPerson& other) const; void SetName(const char theNewName []); void Print() const; // 为简化起见,省略细节 private: // 如其他细节如前所述,已省略};bool TPerson::operator==(const TPerson& other) const{ if (this == &other) { return true; } // 比较别名 if (this->ssn == other._ssn && this->_birthDate == other._birthDate) { // 现在比较人名 if (strcmp(this->_name, other._name) == 0) // strcmp是一个库函数 return true; } return false; // 它们不相等}```这里的假设是,如果社会安全号码、出生日期和人名都相等,则必定是同一个人。事实上,社会安全号码本身就是唯一的,而且可作为比较的唯一根据。另一个需要实现的操作符是不相等操作符!=。如果类实现了==,则最好也实现!=。成对实现操作符可以保证在比较对象时,两操作符中只有其中之一(==或!=)为真。如果缺少其中一个,类的接口则看起来就不完整,而且即使使用另一个操作符2更加切合实际,客户也只能被迫使用类所提供的不成对的操作符。
bool TPerson::operator!=(const TPerson& other) const
{ return ! (*this == other); // 还可写成: // return ! (this->operator==(other));}`
记住: 如果对象需要比较语义,要实现==操作符。
如果实现==操作符,记住要实现!=操作符。还需注意,==操作符可以是虚函数(将在第5章中讨论),而!=操作符通常是非虚函数,如上代码所示。回顾TCar类,如果需要比较TCar类对象,将涉及比较诸如车牌号、车辆识别码等细节。从这个角度来说,真正的比较操作和深复制操作非常类似。对于任何复杂对象,都不得不通过该对象内部所有的对象、指针和引用递归地调用比较操作符来判断是否相等。然而,C++中的等价就相对简单得多,它只涉及比较地址。注意:
Smalltalk有一个与对象散列值(hash value)相关的概念。每个类都支持hash方法,作为类本身基本运算的一部分。该方法为每个对象都返回一个唯一的整数。任何相等的两个对象都会返回相同的散列值。但是,不相等的对象也可以(或不可以)返回相同的散列值。通常,该方法用于链表、队列等中的快速查找对象。即使C++和Eiffel都未提供这样的方法作为语言的一部分,但许多商业软件产品仍提供hash成员函数。而且,在许多实现中,系统中的每个类都要求提供hash方法的实现。语言结构被用于强制执行这样的限制。有时,某些方法也要求强制执行这样的限制,如isEqual()和Clone()方法。决定哪些固有方法需要所有类的支持是设计的难点,任何设计团队都应在早期设计阶段处理这些问题。1也称为类集(或容器),将在第12章中讨论。2操作符成对实现可以扩展至许多其他的操作符,如<=
和>=
、<
和>
等。本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。