多态性1.docx
《多态性1.docx》由会员分享,可在线阅读,更多相关《多态性1.docx(26页珍藏版)》请在冰点文库上搜索。
多态性1
多态性
10.1知识要点
1.多态性:
多态是指同样的消息被不同类型的对象接收后导致完全不同的行为。
2.面向对象的多态性可以分为4类:
重载多态、强制多态、包含多态和参数多态。
3.多态从实现的角度来讲可以划分为两类:
编译时的多态和运行时的多态。
4.运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据导致不同类型的行为。
5.运算符重载的规则如下:
1)C++语言中的运算符除了少数几个之外,全部可以重载,而且只能重载C++语言中已有的运算符。
2)重载之后运算符的优先级和结合性都不会改变。
3)运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造。
一般来讲,重载的功能应当与原有功能相类似,不能改变原运算符的操作对象个数,同时至少要有一个操作对象是自定义类型。
不能重载的运算符只有5个,它们是类属关系运算符“.”、成员指针运算符“*”、作用域分辨符“:
:
”、sizeof运算符和三目运算符“?
:
”。
前面两个运算符保证了C++语言中访问成员功能的含义不被改变。
作用域分辨符和sizeof运算符的操作数是类型,而不是普通的表达式,也不具备重载的特征。
6.运算符的重载形式有两种,重载为类的成员函数和重载为类的友元函数。
运算符重载为类的成员函数的一般语法形式为:
函数类型operater运算符(形参表)
{函数体;}
运算符重载为类的友元函数的一般语法形式为:
friend函数类型operater运算符(形参表)
{函数体;}
7.虚函数
说明虚函数的一般格式如下:
virtual <函数返回类型说明符> <函数名>(<参数表>)
在定义虚函数时要注意:
(1)虚函数是非静态的、非内联的成员函数,而不能是友元函数,但虚函数可以在另一个类中被声明为友元函数。
(2)虚函数声明只能出现在类定义的函数原型声明中,而不能在成员函数的函数体实现的时候声明。
(3)一个虚函数无论被公有继承多少次,它仍然保持其虚函数的特性。
(4)若类中一个成员函数被说明为虚函数,则该成员函数在派生类中可能有不同的实现。
当使用该成员函数操作指针或引用所标识的对象时,对该成员函数调用可采用动态联编。
(5)定义了虚函数后,程序中声明的指向基类的指针就可以指向其派生类。
在执行过程中,该函数可以不断改变它所指向的对象,调用不同版本的成员函数,而且这些动作都是在运行时动态实现的。
虚函数充分体现了面向对象程序设计的动态多态性。
8.虚析构函数
在析构函数前加上关键字virtual进行说明,则该析构函数就称为虚析构函数。
虚析构函数的说明格式如下:
virtual ~<类名>()
在使用虚析构函数时要注意以下几点:
(1)只要基类的析构函数被声明为虚函数,则派生类的析构函数,无论是否使用virtual关键字进行声明,都自动成为虚函数。
(2)如果基类的析构函数为虚函数,则当派生类未定义析构函数时,编译器所生成的析构函数也为虚函数;
(3)当使用delete运算符删除一个对象时,隐含着对析构函数的一次调用,如果析构函数为虚函数,则这个调用采用动态联编。
动态联编可以保证析构函数被正确执行。
(4)子类型化要求析构函数应被声明为虚函数,特别是在析构函数完成一些有意义的工作时。
因此,当不能决定是否应将析构函数声明为虚函数时,就将析构函数声明为虚函数。
9.纯虚函数
当在基类中不能为虚函数给出一个有意义的实现时,可以将其声明为纯虚函数,其实现留待派生类完成。
纯虚函数的作用是为派生类提供一个一致的接口。
纯虚函数的声明格式如下:
virtual <函数返回类型说明符> <函数名>(<参数表>)=0;
10.抽象类
带有纯虚函数的类称为抽象类。
抽象类具有下述一些特点:
(1)抽象类只能作为基类使用,其纯虚函数的实现由派生类给出;但派生类仍可不给出纯虚函数的定义,继续作为抽象类存在。
(2)抽象类不能定义对象,一般将该类的构造函数说明为保护的访问控制权限。
(3)可以声明一个抽象类的指针和引用。
通过指针和引用,可以指向并访问派生类对象,进而访问派生类的成员,这种访问是具有多态特征的。
10.2典型例题分析与解答
例题1:
指出下列对定义重载函数的要求中,哪些是错误的提法。
A. 要求参数的个数不同。
B.要求参数中至少有一个类型不同。
C. 求函数的返回值不同。
D.要求参数的个数相同时,参数类型不同。
答案:
AC
分析:
将函数体与函数调用相联系称为捆绑或约束,与多态性称之为静态联编。
因为静态联编的函数调用是在编译阶段就确定了的,所以对重载的要求就是在编译阶段能够产生唯一的内部标识符号,以使编译器能够根据这个标识符号来确定到底要调用哪个函数。
不必要求参数个数相同,但是如果参数个数相同时,参数类型一定不能全部相同。
这也是选项B表达的意思,为什么可以通过参数来重载,而不能通过返回值来重载?
这是因为在调用一个函数时会忽略它的返回值,所以不能通过返回值来重载。
所以答案AD是错误的提法。
例题2:
下列运算符中,()运算符在C++中不能重载。
A.?
:
B.[]C.newD.&&
答案:
A
例题3:
下面关于友元的描述中,错误的是()。
A. 友元函数可以访问该类的私有数据成员
B. 一个类的友元类中的成员函数都是这个类的友元函数
C. 友元可以提高程序的运行效率
D. 类与类之间的友元关系可以继承
答案:
D
分析:
友元关系不能被继承,并且是单向的,不可交换的。
不清楚友元函数可以直接访问类的私有成员,误选答案A。
不清楚友元类的含义,误选答案B。
当一个类作为另一个类的友元时,这个类的所有成员函数都是另一个类的友元函数,即友元类中的所有成员函数都可以访问另一个类中的私有成员。
不清楚引入友元函数的目的,误选答案C。
例题4:
下述静态成员的特性中,()是错误的。
A. 静态成员函数不能利用this指针
B. 静态数据成员要在类体外进行初始化
C. 引用静态数据成员时,要在静态数据成员名前加<类名>和作用域运算符
D. 静态数据成员不是所有对象所共有的
答案:
D
分析:
静态数据成员是类的所有对象共享的成员,同一个类的不同对象拥有一个共同的静态数据成员。
不清楚静态成员函数的特点,误选答案A。
静态成员函数属于整个类,是类的所有对象共享的成员函数,它与一般成员函数不同,没有指向调用该成员函数对象的this指针。
不理解静态数据成员如何初始化,误选答案B。
静态数据成员的初始化应在类体外进行。
不清楚静态数据成员的使用方法,误选答案C。
静态数据成员是属于整个类的,因此可以不通过对象名,而直接使用类名和作用域运算符表明其所属的类即可。
例题5:
关于虚函数的描述中,()是正确的。
A. 虚函数是一个静态成员函数
B. 虚函数是一个非成员函数
C. 虚函数既可以在函数说明时定义,也可以在函数实现时定义
D. 派生类的虚函数与基类中对应的虚函数具有相同的参数个数和类型
参考答案:
D
分析:
派生类的虚函数与基类中对应的虚函数具有相同的函数名、相同的参数个数和类型。
返回值类型或者相同,或者都返回指针或引用,并且派生类虚函数所返回的指针或引用的基类型是基类中的虚函数所返回的指针或引用的基类型的子类型。
不清楚虚函数必须是一个非静态的成员函数,误选答案A。
不清楚虚函数必须是一个普通的成员函数,误选答案B。
虚函数是非内联的,也不能是友元函数。
不清楚虚函数的说明方法,误选答案C。
虚函数声明只能出现在类定义的函数原型声明中,而不能在成员函数的函数体实现的时候。
分析:
类属关系运算符“.”、成员指针运算符“.*”、作用域运算符“:
:
”、sizeof运算符和三目运算符“?
:
”在C++中不能重载。
例题6:
利用成员函数对二元运算符重载,其左操作数为 ,右操作数为 。
答案:
(1)this指针
(2)成员函数参数
分析:
将双目运算符重载为类的成员函数时,由于this指针在每次非静态成员函数操作对象时都作为第一个隐式参数传递给对象,因此它充当了二元运算符的左操作数,而该成员函数的形参则表示二元运算符的右操作数。
容易错误分析:
(1)不了解this指针的作用,所以不知道二元运算符的左操作数如何表示;
(2)误将成员函数的参数作为二元运算符的左操作数,而不知道右操作数如何表示;(3)混淆了成员函数和友元函数两种方式,误认为二元运算符的所有的操作数都必须通过函数的形参进行传递,函数的参数与操作数自左至右一一对应;
例题7:
可以用pow()表示幂,也能创造符号**来表示幂运算符。
描述是否正确?
答:
错误。
分析:
重载运算符应限制在C++语言中已有的运算符范围内的允许重载的运算符之中,不能创建新的运算符。
例题8:
分析下列程序的输出结果。
#include
#include
#include
#include
classSales
{public:
voidInit(charn[]){strcpy(name,n);}
int&operator[](intsub);
char*GetName(){returnname;}
private:
charname[25];
intdivisionTotals[5];
};
int&Sales:
:
operator[](intsub)
{if(sub<0||sub>4)
{cerr<<"Badsubscript!
"<abort();
}
returndivisionTotals[sub];
}
voidmain()
{inttotalSales=0,avgSales;
Salescompany;
company.Init("SwissCheese");
company[0]=123;
company[1]=456;
company[2]=789;
company[3]=234;
company[4]=567;
cout<<"Herearethesalesfor"<"<for(inti=0;i<5;i++)
cout<for(i=0;i<5;i++)
totalSales+=company[i];
cout<avgSales=totalSales/5;
cout<<"Theaveragesalesare"<}
运行结果:
HerearethesalesforSwissCheese'sdivisions:
123456789234567
Thetotalsalesare2169
Theaveragesalesare433
分析:
(1)在上述程序中,并没有创建company的对象数组。
程序中的下标被重载,以便在使用company时用一种特殊的方式工作。
下标总是返回和下标对应的那个部门的销售额divisionTotals[];
(2)重载下标运算符"[]"时,返回一个int型引用,可使重载的"[]"用在赋值语句的左边,因而在main()中,可对每个部门的销售额divisionTotals[]赋值。
这样,虽然divisionTotals[]是私有的,main()还是能够直接对其赋值,而不需要使用函数Init();(3)在上述程序中,设有对下标的检验,以确保被赋值的数组元素存在。
当程序中一旦向超出所定义的数组下标范围的数组元素进行赋值时,便会自动终止程序,以免造成不必要的破坏;(4)与函数调用运算符"()"一样,下标运算符"[]"不能用友元函数重载,只能采用成员函数重载。
例题9:
定义Point类,有数据成员X和Y,重载++和--运算符,要求同时重载前缀方式和后缀方式。
答案:
#include
classPoint
{public:
Point(){X=Y=0;}
intGetX(){returnX;}
intGetY(){returnY;}
Point&operator++();
Pointoperator++(int);
Point&operator--();
Pointoperator--(int);
voidPrint(){cout<<"Thepointis("<private:
intX,Y;
};
Point&Point:
:
operator++()
{X++;
Y++;
return*this;
}
PointPoint:
:
operator++(int)
{Pointtemp=*this;
++*this;
returntemp;
}
Point&Point:
:
operator--()
{X--;
Y--;
return*this;
}
PointPoint:
:
operator--(int)
{Pointtemp=*this;
--*this;
returntemp;
}
voidmain()
{Pointobj;
obj.Print();
obj++;
obj.Print();
++obj;
obj.Print();
obj--;
obj.Print();
--obj;
obj.Print();
}
分析:
注意重载前缀单目运算符和后缀单目运算符的区别。
前缀单目运算符重载为类的成员函数时,不需要显式说明参数,即函数没有形参;而后缀单目运算符重载为类的成员函数时,函数要带有一个整型形参。
例题10:
指出下列程序中的错误,并说明错误原因。
classX//1
{public:
//2
intreadme()const{returnm;}//3
voidwriteme(inti){m=i;}//4
private:
//5
intm;//6
};//7
voidf(X&x1,constX&x2)//8
{x1.readme();//9
x1.writeme
(1);//10
x2.readme();//11
x2.writeme
(2);//12
}//13
答案:
行12出错,删除。
分析:
常对象不能调用一般成员函数,因此
(1)类X中定义了两个成员函数:
常成员函数readme和一般成员函数writeme;
(2)函数f中定义了两个参数:
常参数x2和一般参数x1。
两个对象分别调用了两个成员函数;(3)成员函数与对象之间的操作关系是:
常对象和一般对象都可以调用常成员函数,而一般成员函数只能由一般对象调用,常对象调用它时将产生错误;(4)根据以上分析,一般对象x1对常成员函数readme和一般成员函数writeme的调用正确,常对象x2对常成员函数readme的调用正确,但对一般成员函数writeme的调用错误。
不清楚函数的调用者和被调用者,颠倒了成员函数和对象二者之间的关系,导致判断错误,认为行11出错。
例题11:
分析下列程序的输出结果。
#include
classA{
public:
A(){cout<<"A'scons."<virtual~A(){cout<<"A'sdes."<virtualvoidf(){cout<<"A'sf()."<voidg(){f();}
};
classB:
publicA{
public:
B(){f();cout<<"B'scons."<~B(){cout<<"B'sdes."<};
classC:
publicB{
public:
C(){cout<<"C'scons."<~C(){cout<<"C'sdes."<voidf(){cout<<"C'sf()."<};
voidmain()
{A*a=newC;
a->g();
deletea;
}
运行结果:
A'scons.
A'sf().
B'scons.
C'scons.
C'sf().
C'sdes.
B'sdes.
A'sdes.
分析:
(1)类B从类A公有继承,类C从类B公有继承,B是A的子类型,C是B的子类型,也是A的子类型。
因此可以使用类C的对象去初始化基类A的指针;
(2)类A中定义了两个虚函数:
虚成员函数f和虚析构函数。
由于类C中的f函数与基类A中的f函数的参数和返回类型相同,因此类C中的成员函数f也是虚函数;由于基类A中定义了虚析构函数,因此派生类B和C的析构函数也都是虚析构函数;(3)主程序中A*a=newC;隐含了两步操作,即:
首先建立一个派生类C的对象,然后使用该派生类对象去初始化基类A的指针a;(4)用new运算符创建C对象时,要调用C的构造函数。
C是一个派生类,它必须负责对其直接基类的构造函数的调用,因此执行顺序是:
先执行直接基类B的构造函数,再执行类C的构造函数体;(5)类B也是一个派生类,它也必须负责调用它的直接基类A的构造函数,其执行顺序是:
先执行直接基类A的构造函数,再执行类B的构造函数体;(6)执行类A的构造函数,输出:
A'scons.;(7)类B的构造函数体中,首先调用了虚函数f,但由于是在构造函数中调用虚函数,所以对它的调用采用静态联编。
因为类B中没有对f函数进行定义,因此调用基类A中的f函数,首先输出A'sf().;继续执行类B的构造函数体,输出B'scons.;(8)执行类C的构造函数体,输出:
C'scons.;(9)成员函数g中调用了虚函数f,此时满足动态联编的条件:
C是A的子类型;存在虚函数f;在成员函数中调用了虚函数,因此,通过指针a访问g函数时,采用动态联编,即a->g()调用的是派生类C中的成员函数f,输出:
C'sf().;(10)deletea将调用析构函数。
由于整个类族中都定义了虚析构函数,因此此处将进行动态联编,即调用的是基类指针a当前正在指向的派生类C的析构函数。
由于C是一个派生类,所以它必须负责调用它的基类的析构函数,其执行顺序是:
先调用派生类C的析构函数,再调用直接基类B的析构函数。
而直接基类B也是一个派生类,所以它也必须负责调用它的直接基类A的析构函数。
因此,析构的顺序是:
C、B、A,输出:
C'sdes.;B'sdes.;A'sdes.。
析构的顺序与构造的顺序完全相反。
容易发生误解是,不清楚用new运算符建立对象时将自动调用构造函数;不清楚派生类构造函数的执行顺序,误认为先调用派生类构造函数,再调用基类构造函数,导致先输出:
C'scons.;不清楚派生类只负责对其直接基类构造函数的调用,在建立C类的对象时,错误地既调用了直接基类B的构造函数,又调用了间接基类A的构造函数,导致错误输出:
B'scons.;A'scons.;不清楚在构造函数和析构函数中调用虚函数时只能进行静态联编,误认为此处也是动态联编,导致错误输出:
C'sf().;不清楚动态联编的实现条件,不知道在成员函数中调用虚函数是进行动态联编,导致对g函数的调用产生错误输出:
A'sf().;不清楚用delete运算符释放对象时将自动调用析构函数;不清楚对虚析构函数的调用采用的是动态联编,误认为释放的是基类A的对象,导致错误输出:
A'sdes.;不清楚派生类的析构函数的执行顺序,误认为与构造时的顺序相同,导致错误输出:
A'sdes.;B'sdes.;C'sdes.。
10.3教材习题解答
1.选择题
(1)下列关于动态联编的描述中,错误的是()。
A.动态联编是以虚函数为基础
B.动态联编是运行时确定所调用的函数代码的
C.动态联编调用函数操作是指向对象的指针或对象引用
D.动态联编是在编译时确定操作函数的
答案:
D
(2)关于虚函数的描述中,正确的是()。
A.虚函数是一个静态成员函数
B.虚函数是一个非成员函数
C.虚函数即可以在函数说明定义,也可以在函数实现时定义
D.派生类的虚函数与基类中对应的虚函数具有相同的参数个数和类型
答案:
D
(3)下面4个选项中,()是用来声明虚函数的。
A.virtualB.publicC.usingD.false
答案:
A
(4)编译时的多态性可以通过使用()获得。
A.虚函数和指针B.重载函数和析构函数C.虚函数和对象D.虚函数和引用
答案:
A
(5)关于纯虚函数和抽象类的描述中,错误的是()。
A.纯虚函数是一种特殊的虚函数,它没有具体的实现
B.抽象类是指具体纯虚函数的类
C.一个基类中说明有纯虚函数,该基类派生类一定不再是抽象类
D.抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出
答案:
B
(6)下列描述中,()是抽象类的特征。
A.可以说明虚函数B.可以进行构造函数重载C.可以定义友元函数D.不能说明其对象
答案:
D
(7)以下()成员函数表示虚函数。
A.virtualintvf(int);B.voidvf(int)=0;
C.virtualvoidvf()=0;D.virtualvoidvf(int){};
答案:
D
(8)如果一个类至少有一个纯虚函数,那么就称该类为(A)。
A.抽象类B.虚函数C.派生类D.以上都不对
答案:
A
(9)要实现动态联编,必须通过()调用虚函数。
A.对象指针B.成员名限定C.对象名D.派生类名
答案:
A
(10)下面描述中,正确的是(A)。
A.virtual可以用来声明虚函数
B.含有纯虚函数的类是不可以用来创建对象的,因为它是虚基类
C.即使基类的构造函数没有参数,派生类也必须建立构造函数
D.静态数据成员可以通过成员初始化列表来初始化
答案:
A
2.什么叫做多态性?
在C++语言中是如何实现多态的?
答:
多态是指同样的消息被不同类型的对象接收时导致完全不同的行为,是对类的特定成员函数的再抽象.c十+支持的多态有多种类型.重载(包括函数重载和运算符重载)和虚函数是其中主要的方式.
3.什么