面向对象程序设计及C++实验指导(第3版)
上QQ阅读APP看书,第一时间看更新

第6章 多态性

1.例6-2的思考题:

①将li06_02.h文件中的第12行改为“Complex operator+(Complex &a);”,将li06_02.cpp文件中的第19行改为“Complex Complex::operator+(Complex &a)”,将常引用形参改为引用形参,重新运行程序,结果有变化吗?为什么?

②将li06_02.h文件中的第12行改为“Complex operator+(Complex a);”,将li06_02.cpp文件中的第19行改为“Complex Complex::operator+(Complex a)”,也就是将常引用形参改为值形参,重新运行程序,结果有变化吗?二者的工作原理有区别吗?

③将li06_02_main.cpp文件中的第13行改为“c3=5.32f+c3;”,重新编译程序,会有什么现象?请解释原因。

【分析与解答】①将常引用形参改为引用形参,重新运行程序,结果没有变化,因为常引用和引用形参都是对应实参对象的别名,都不另外分配空间,常引用形参是从语法上保证了如果试图修改参数a将会报错,从而保护对应实参对象;而引用形参如果函数中试图修改参数a不会报错。现在本函数中没有修改参数a,所以结果不变。但是对于本例仍然建议用常引用形参。

②将常引用形参改为值形参,重新运行程序,结果没有变化。但是这两种不同的形参其工作原理完全不同。常引用形参是对应实参对象的别名,不另外分配空间,也就不调用构造函数,因此时间、空间效率都高;而值形参系统为之另外分配存储空间,在调用之初用实参对象初始化形参对象,需要调用复制构造函数,因此有一定的时间和空间开销,本函数中的加法运算符只是将最初的对象值传进来而不需要修改,所以用值形参不会影响运行结果。对于本例,用哪种形式的形参都可以,但是仍然建议用常引用形参。

③将li06_02_main.cpp文件中的第13行改为“c3=5.32f+c3;”,重新编译程序,会出现若干个error报错,其中一条为“error C2677:二进制“+”:没有找到接受“Complex”类型的全局运算符(或没有可接受的转换)”,原因是,程序中以成员函数形式重载“operator+”运算符函数,所以第一实参只能是本类的对象,不能是5.32f,参数不匹配。所以类似这种用法,两个运算对象,只有一个是本类对象,又以成员函数形式重载,则另一个运算对象只能作为第二运算对象,调用的时候保证实参类型与形参类型一致。

2.例6-3的思考题:

①将li06_03.h文件中的第14行改为“friend Complex operator++(Complex a);”,将li06_03.cpp文件中的第29行改为“Complex operator++(Complex a)”,将引用形参改为值形参,重新运行程序,结果有变化吗?试分析原因。

②恢复li06_03.h文件中的第14行和li06_03.cpp文件中的第29行原来的代码,仍然用引用形参,将li06_03.cpp文件中的第31行和第32行代码改为“a.real++;a.imag++;”,将这两句中的前置++改为后缀++,重新运行程序,结果有变化吗?试分析原因。

【分析与解答】①将引用形参改为值形参,重新运行程序,结果有变化,程序运行结果的前4行代码不变,最后2行代码如下。

也就是第5行结果由“6+11 i”变成了“5+10 i”,因为值形参的改变不能影响对应实参对象,所以c2的值不会发生改变,但是改变后的值形参放在return后返回了,这样系统生成一个无名的临时对象,调用复制构造函数,用改变后的值形参a的值6+11 i初始化该临时对象,然后该对象的值赋值给了对象c4,所以c4的值为6+11 i。

②将li06_03.cpp文件中的第31和第32行的代码的前置“++”改为后缀“++”,对结果没有影响。因为如果只是对a对象的数据成员本身自增,而不将这个表达式的结果再用于其他运算,那么前置“++”和后缀“++”没有区别,总体效果都是a对象的两个数据成员的值都增加了1。尽管如此,还是建议用原来的前置“++”,以便与运算符重载保持一致的顺序。

3.例6-4的思考题:

①比较li06_04.h文件中新增的赋值运算符函数的代码和3.6节中复制构造函数的代码,二者有什么相同和不同之处?试分析解释。

②例6-4主函数中如果有语句“CMessage Mes3(Mes1);”,则文件li06_04.h需要做怎样的改变?

【分析与解答】①li06_04.h文件中新增的赋值运算符函数的代码与3.6节中复制构造函数的代码相比,相同之处为:根据常引用形参的pmessage指针所指向的字符串的长度+1,通过当前对象的pmessage指针申请这个容量的动态空间,然后将常引用形参的pmessage指针所指向的字符串复制到当前对象的pmessage所指向的动态空间中。不同之处有两处:赋值运算符函数的一开始,在申请新的动态空间之前需要先释放当前对象pmessage指针所指向的动态空间;而复制构造函数没有这一步。这是因为赋值运算符的第一运算对象是一个已经存在的对象,必然已经申请过动态空间,所以在申请新的动态空间之前需要释放原来的空间,而复制构造函数是定义新对象的时候自动调用的,显然还不存在已有动态空间,所以当然没有释放原空间再申请新空间的说法。第二处不同是,赋值运算符函数的最后有“return*this;”语句,因为该函数需要返回当前类的引用,返回被改变后的当前对象;而构造函数没有返回值,所以无需返回任何结果。

②本题主函数中如果有语句“CMessage Mes3(Mes1);”,那么对象Mes3是通过已有对象Mes1作初始化的,根据第3章的知识,此时文件li06_04.h需要增加主教材3.6节中的复制构造函数的完整定义,否则程序运行时同样会因为浅复制问题而出现指针悬挂的现象,导致意外终止的错误。

4.例6-7的思考题:

①后缀“++”和后缀“--”的实现代码中首先定义了一个临时对象保存未改变的原对象,最后返回这个临时对象,为什么要这么做?

②文件li06_07.cpp文件中的第10行、第11行、第17行、第18行的“--”以及第23行、第24行、第30行、第31行的“++”运算符的位置放前面或后面,对程序的结果有没有影响?为什么?

【分析与解答】①后缀“++”和后缀“--”中首先需要定义一个临时对象保存未改变的原对象,最后也是返回这个临时对象,这样做是为了保证后缀“++”和后缀“--”作用于本类对象与作用于标准类型变量的意义是一致的:先用未改变的值参与运算,最后再改变自身的值。如果不通过临时对象这样处理,就无法返回未改变的对象值了。运算符重载的原则,就是不改变运算符本身的功能和意义,所以此处需要这样处理。

②对于对象的数据成员real和imag的改变,“--”和“++”的位置放前或放后对结果没有影响,因为如果只是改变变量本身,不参与其他运算,前后的效果是一样的。

5.例6-9的思考题:

①将文件li06_09_main.cpp代码中的第5行、第6行分别改为“B bb(5000);”和“A*a=&bb;”,重新运行程序,结果是什么?解释这一结果。

②在上一步修改的基础上,再将文件li06_09.h中第7行的“virtual”关键字删除,重新运行程序,结果是什么?解释这一现象。

【分析与解答】①修改代码后的运行结果与原来的结果一样,还是依次调用B类和A类的析构函数。因为修改后,定义B类对象bb,所以最后该对象析构肯定调用B类析构函数,继而调用A类析构函数。

②若将“virtual”关键字删除,运行结果不变,还是依次调用B类和A类的析构函数。这里A类指针只是获得了B类对象的地址,不涉及动态和静态多态性问题。

对照思考题修改后的代码和教材原来的代码,请读者再次理解一下析构函数什么时候必须声明为虚析构函数。基类指针用new方式申请公有派生类的对象空间,而不是简单地将派生类对象地址赋值给基类指针。不过,无论何种情况,声明为虚析构函数总是没问题的。

6.例6-10的思考题:

将文件li06_10.h代码中第15行“void f3()”改为“virtual void f3()”,重新编译链接、运行程序,观察结果并对不同之处作解释。

【分析与解答】将文件li06_10.h代码中第15行改为“virtual void f3()”之后,由于派生类中f3的函数原型与基类f3完全一致,此时f3函数就成为虚函数。于是运行结果的第8行输出,由原来的“f3 function of base”改为“f3 function of derive”,因为此时基类指针p指向了派生类对象ob2,所以根据动态多态性的特点,通过p调用的虚函数f3就是派生类中的版本了。