750 likes | 930 Views
C++ 面向对象程序设计 第一章. 本章导读. 所谓多态性是指发出的消息被不同的对象接受时会产生完全不同的行为。多态性是面向对象程序设计的重要特性之一,多态性机制不仅增加了面向对象软件系统的灵活性,而且显著提高了软件的可重用性和可扩充性。在 C++ 中, C++ 中的多态性可以分为四类:参数多态、包含多态、重载多态和强制多态。运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据导致不同类型的行为。. 本章要点. 理解静态联编机制和动态联编机制 虚函数的使用 运算符重载函数的规则及其两种形式 单目运算符重载和双目运算符重载. 第六章 目录.
E N D
C++面向对象程序设计 第一章
本章导读 • 所谓多态性是指发出的消息被不同的对象接受时会产生完全不同的行为。多态性是面向对象程序设计的重要特性之一,多态性机制不仅增加了面向对象软件系统的灵活性,而且显著提高了软件的可重用性和可扩充性。在C++中,C++中的多态性可以分为四类:参数多态、包含多态、重载多态和强制多态。运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据导致不同类型的行为。
本章要点 • 理解静态联编机制和动态联编机制 • 虚函数的使用 • 运算符重载函数的规则及其两种形式 • 单目运算符重载和双目运算符重载
第六章 目录 • 6.1 多态性 • 6.2 虚函数 • 6.3 纯虚函数和抽象类 • 6.4 运算符重载 • 6.5 运算符重载函数的形式 • 6.6 程序举例 • 6.7 本章小结 • 习题六
6.1 多态性 • 6.1.1通用多态和专用多态 • 6.1.2多态的实现
6.1.1通用多态和专用多态 • C++中的多态性可以分为四类:参数多态、包含多态、重载多态和强制多态。前面两种统称为通用多态,而后面两种统称为专用多态。 • 参数多态与类属函数和类属类相关联,本书中讲到的函数摸板和类摸板就属于这种类型。由类摸板实例化的各个类都有相同的操作,而操作对象的类型却可以各不相同。同样地,由函数摸板实例化的各个参数也都具有相同的操作,但这些函数的参数类型也是可以各不相同的。
包含多态是研究类族中定义于不同类中的同名成员函数的多态行为,主要是通过本章中要讲的虚函数来实现的。包含多态是研究类族中定义于不同类中的同名成员函数的多态行为,主要是通过本章中要讲的虚函数来实现的。 • 重载多态如函数重载、运算符重载等。前面我们讲的普通函数及类的成员函数的重载都属于这一类型。运算符重载我们会在以后的学习中学到。
强制多态是指将一个变元的类型加以变化,以符合一个函数或操作的要求。例如,加法运算符在进行浮点数与整型数相加时,要进行类型强制转换,要把整型数转换为浮点数之后再进行相加。强制多态是指将一个变元的类型加以变化,以符合一个函数或操作的要求。例如,加法运算符在进行浮点数与整型数相加时,要进行类型强制转换,要把整型数转换为浮点数之后再进行相加。
6.1.2多态的实现 • C++语言支持两种多态性:编译时的多态和运行时的多态。多态的实现和联编这一概念有关。所谓联编就是把函数名与函数体的程序代码连接在一起的过程。联编又可分为静态联编和动态联编。系统用实参与形参进行匹配,对于同名的重载函数便根据参数上的差异进行区分,然后进行联编,从而实现多态。
1.静态联编 • 静态联编就是在编译阶段完成的联编。编译时的多态就是通过静态联编实现的。 • 2.动态联编 • 动态联编就是在程序运行阶段完成的联编。动态联编是运行阶段完成的联编。
运行时的多态就是用动态联编来完成的,当程序调用到某一函数名时,才去寻找和连接其程序代码。对面向对象程序而言,就是当对象接受到某一消息时,才去寻找和连接相应的方法。运行时的多态就是用动态联编来完成的,当程序调用到某一函数名时,才去寻找和连接其程序代码。对面向对象程序而言,就是当对象接受到某一消息时,才去寻找和连接相应的方法。
静态联编要求在程序编译时就知道调用函数的全部信息,因此,这种联编类型的函数调用速度很快,效率很高,但缺乏灵活性;而动态联编则恰好相反,采用动态联编时,一直要到程序运行时才能确定调用哪个函数,它降低了程序的运行效率,但提高了程序的灵活性。纯粹的面向对象程序语言因为其执行机制是消息传递,所以只能采用动态联编的方式。这就给基于C语言的C++带来了麻烦。因为为了保持C语言的高效性,C++仍是编译型的,仍采用静态联编。静态联编要求在程序编译时就知道调用函数的全部信息,因此,这种联编类型的函数调用速度很快,效率很高,但缺乏灵活性;而动态联编则恰好相反,采用动态联编时,一直要到程序运行时才能确定调用哪个函数,它降低了程序的运行效率,但提高了程序的灵活性。纯粹的面向对象程序语言因为其执行机制是消息传递,所以只能采用动态联编的方式。这就给基于C语言的C++带来了麻烦。因为为了保持C语言的高效性,C++仍是编译型的,仍采用静态联编。
好在C++的设计者想出了“虚函数”机制,利用虚函数机制,C++可部分地采用动态联编。也就是说,C++实际上是采用了静态联编和动态联编相结合的联编方法。运行时的多态性主要是通过虚函数来实现的。好在C++的设计者想出了“虚函数”机制,利用虚函数机制,C++可部分地采用动态联编。也就是说,C++实际上是采用了静态联编和动态联编相结合的联编方法。运行时的多态性主要是通过虚函数来实现的。
6.2 虚函数 • 6.2.1虚函数的作用和定义 • 6.2.2虚析构函数 • 6.2.3虚函数与重载函数的关系 • 6.2.4多继承与虚函数
虚函数允许函数调用与函数体之间的联系在运行时才建立,也就是在运行时才决定如何动作,这也就是所谓的动态联编。虚函数允许函数调用与函数体之间的联系在运行时才建立,也就是在运行时才决定如何动作,这也就是所谓的动态联编。
6.2.1虚函数的作用和定义 • 1.虚函数的作用 • 虚函数首先是基类中的成员函数,在这个成员函数前面缀上关键字virtual,并在派生类中被重载。虚函数与派生类的结合可使C++支持运行时的多态性,而多态性对面向对象的程序设计又是非常重要的,实现了在基类定义派生类所拥有的通用接口,而在派生类定义具体的实现方法,即通常所说的“同一接口,多种方法”,它能帮助程序员处理越来越复杂的程序。
2.虚函数的定义 • 虚函数的定义是在基类中进行的,它是在需要定义为虚函数的成员函数的声名中冠以关键字virtual,并要在派生类中重新定义。所以虚函数为它的派生类提供了一个公共的界面,而派生类对虚函数的重定义则指明函数的具体操作。在基类中的某个成员函数被声明为虚函数后,此虚函数就可以在一个或多个派生类中被重新定义。在派生类中重新定义时,其函数原型包括返回类型、函数名、参数个数、参数类型的顺序等都必须与基类中的原型完全相同。 • 虚函数定义的一般格式为: • virtual 函数类型 函数名(形参表) • { • 函数体 • }
如果在派生类中的函数满足以下三个条件则可以判断该函数是虚函数:如果在派生类中的函数满足以下三个条件则可以判断该函数是虚函数: • (1)该函数与基类的虚函数有相同的名称。 • (2)该函数与基类的虚函数有相同的参数个数及相同的对应参数类型。 • (3)该函数与基类的虚函数有相同的返回类型或者满足赋值兼容规则的指针、引用型
3.虚函数说明 • (1)派生类应该从它的基类公有派生。一个虚函数无论被公有继承多少次,它仍然保 • 持虚函数的特性。 • (2)必须首先在基类中定义虚函数。在实际应用中,应该在类等级内需要具有动态多 • 态性的几个层次中的最高层类内首先声明虚函数。 • (3)在派生类对基类声明的虚函数进行重定义时,关键字virtual可以写也可以不写。 • 但在容易引起混乱的情况下,最好在对派生类的虚函数进行重定义时也加上关键字virtual。
(4)使用对象名和点运算符的方式也可以调用虚函数,但这在编译时进行的是静态联遍,它没有充分利用虚函数的特性。只有通过基类指针访问虚函数时才能获得运行时的多态性。(4)使用对象名和点运算符的方式也可以调用虚函数,但这在编译时进行的是静态联遍,它没有充分利用虚函数的特性。只有通过基类指针访问虚函数时才能获得运行时的多态性。 • (5)虚函数必须是其所在类的成员函数,而不能是友员函数,也不能是静态成员函数, • 因为虚函数调用要靠特定对象来激活对应的函数。但是虚函数可以在另一个类中被声明为友员函数。
(6)内联函数不能是虚函数,因为内联函数是不能在运行时动态确定其位置的。即使(6)内联函数不能是虚函数,因为内联函数是不能在运行时动态确定其位置的。即使 • 虚函数是在类的内部定义,编译是仍将其看作是非内联的;构造函数不能是虚函数。因为虚函数作为运行过程中多态的基础,主要是针对对象的,而构造函数是在对象产生之前运行的,所以虚构造函数是没有意义的。析构函数可以是虚函数,而且通常被声明为虚函数。
6.2.2虚析构函数 • 虚析构函数的声明方法为: • virtual ~类名(); • 如果一个类的析构函数是虚函数,那么由它派生而来的所有派生类的析构函数不管他是否用virtual进行说明也是析构函数。析构函数被声明为虚函数后,在使用指针引用时可以动态联编,实现运行时的多态,保证使用时基类类型的指针能够调用适当的析构函数针对不同的对象进行清理工作。
6.2.3虚函数与重载函数的关系 • 在一个派生类中重新定义基类的虚函数其实是函数重载的一种形式,但它又不同与普通的函数重载。普通的函数重载时,其函数的参数或参数类型必须有所不同,函数的返回类型也可以不同。但是,当重载一个虚函数时,即当在派生类中重新定义虚函数时,要求函数名、参数个数、参数类型和顺序及返回类型与基类中的虚函数原型完全相同。如果返回类型不同,其余均相同,系统会给出错误信息;如果函数名相同,而参数个数、类型或顺序不同,系统将会把它作为普通的函数重载,这样将会丢失虚函数的特性。
6.2.4多继承与虚函数 • 多继承可以看成多个单继承的组合。 • 【例6.5】多继承情况下的虚函数的调用 • #include<iostream> • using namespace std; • class A • { • public: • virtual void show() //定义虚函数show • {cout<<"this is A"<<endl;} • };
class B • { • public: • void show() //此处不是虚函数 • {cout<<"this is B"<<endl;} • }; • class C:public A,public B • { • public: • void show() • {cout<<"this is C"<<endl;} • };
void main() • { • A a,*p1; • B *p2; • C c; • p1=&a; //指针p1指向对象a • p1->show(); //调用基类A的show() • p1=&c; //指针p1指向对象c 此处实现了多态性 • p1->show(); //此处show()为虚函数,调用派生类C中的show() • p2=&c; //指针p2指向对象c • p2->show(); //此处show()不是虚函数,而p2又是B的指针,因此调用派生类B • //中的show() • } • 程序的运行结果如下: • this is A • this is C • this is B
6.3 纯虚函数和抽象类 • 6.3.1纯虚函数 • 6.3.2抽象类
6.3.1纯虚函数 • 在前面讲过,虚函数为派生类提供了一个公共的界面,而派生类对虚函数的重定义则指明函数的具体操作。但在许多情况下,基类只是一个框架,它并没有虚函数功能的定义,而是希望所有的派生类都自己给出函数的定义。为处理这种要求,C++用与虚函数紧密相关的概念——纯虚函数来实现。
纯虚函数是一个在基类中说明的虚函数,但它在基类中没有定义。纯虚函数是一个在基类中说明的虚函数,但它在基类中没有定义。 • 纯虚函数的声明格式如下: • virtual 函数类型 函数名(参数表)=0; • 这种格式与一般的虚函数说明格式相比,纯虚函数被声明为“=0”,即在该类中没有该虚函数的函数体,或说其函数体为空。 • 纯虚函数是虚函数的特殊形式,对它的调用也是通过动态联编来实现的,不同的是纯虚函数在基类中完全是空的,调用的总是派生类中的定义。
6.3.2抽象类 • 如果一个类至少有一个纯虚函数,那么就称该类为抽象类。因此上程序中定义的类shape就是一个抽象类。对于抽象类的使用有以下几点规定: • (1)由于抽象类中至少包含了一个没有定义功能的纯虚函数。因此,抽象类只能作为 • 其他类的基类使用,不能建立抽象类对象,其纯虚函数的实现由派生类给出。 • (2)抽象类不能用作参数类型、返回类型或显式转换的类型。 • (3)不允许从具体类派生抽象类。所谓具体类,就是不包含纯虚函数的普通类。
(4)可以声明指向抽象类的指针或引用,此指针可以指向它的派生类,进而实现多态(4)可以声明指向抽象类的指针或引用,此指针可以指向它的派生类,进而实现多态 • 性。 • (5)如果派生类中没有重定义纯虚函数,而派生类只是继承基类的纯虚函数,则这个派生类仍然是一个抽象类。如果派生类中给出了基类纯虚函数的实现,该派生类就不再是抽象类了,它就是一个可以建立对象的具体类了。 • (6)类中也可以定义普通成员函数或虚函数,虽然不能为抽象类声明对象,但仍然可 • 以通过派生类对象来调用这些不是纯虚函数的函数。
6.4 运算符重载 • 6.4.1 运算符重载概述 • 6.4.2 运算符重载规则
6.4.1 运算符重载概述 • 重载是面向对象设计的重要特征,运算符重载是对已有的运算符赋予多重含义,使用同一个运算符作用于不同类型的数据导致不同的行为。 • C++中经重载后的运算符能直接对用户自定义的数据进行操作运算,这就是C++语言中的运算符重载所提供的功能。运算符重载进一步提高了面向对象的灵活性、可扩充性和可读性。
如我们要求两个复数的相加,当我们定义一个复数类complex:如我们要求两个复数的相加,当我们定义一个复数类complex: • class complex { • public: • double real,imag; • complex(double r=0,double i=0) • { real=r; imag=i;} • };
如果想实现把类complex的两个对象com1和com2加在一起,下面的语句则不能实现该功能 : • main() • { • complex com1(1.2,2.3),com2(3.4,4.5),total; • total=com1+com2; //错误,不能实现两个对象相加 • //… • return 0; • }
无法实现的原因是complex类是用户自己定义的数据类型,而不是预定义的基本类型,C++知道如何将两个int、float型数据相加,知道怎样把两个不同数据类型的数据相加,但C++无法直接将两个complex类对象相加。无法实现的原因是complex类是用户自己定义的数据类型,而不是预定义的基本类型,C++知道如何将两个int、float型数据相加,知道怎样把两个不同数据类型的数据相加,但C++无法直接将两个complex类对象相加。
complex operator+(complex om1,complex om2) • { • complex temp; • temp.real=om1.real+om2.real; • temp.imag=om1.imag+om2.imag; • return temp; • } • 这样,我们就能方便地使用语句“total=com1+com2;”, 来实现两个complex类对象的相加。在程序中也可以使用“total=operator+(com1,com2);”语句,来实现两个complex类对象的相加。
6.4.2 运算符重载规则 • .4.2 运算符重载规则 • C++语言对运算符重载制定了以下一些规则: • (1)运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造完成的, • 一般来讲,重载的功能应当与原有的功能相类似。 • (2)C++中只重载原先已有定义的运算符,程序员不能臆造新的运算符来扩充C++ • 语言。
(3)在ANSI C++中几乎所有的运算符都可以被重载,其中包括: • 算术运算符:+ 、- 、* 、/ 、% 、++ 、-- • 位操作运算符:& 、| 、~ 、^、<< 、>> • 逻辑运算符: ! 、&& 、|| • 比较运算符: < 、> 、 <= 、>= 、== 、!= • 赋值运算符:= 、+= 、-= 、*= 、/= 、%= 、&= 、|= 、^= 、<<= 、>>= • 其他运算符:[] 、 () 、-> 、, 、new 、delete 、->* • 以下5个运算符不能重载:
类属关系运算符“.”、成员指针运算符“*”、作用域分辨符“::”、和三目运算符“?:”。类属关系运算符“.”、成员指针运算符“*”、作用域分辨符“::”、和三目运算符“?:”。 • (4)不能改变运算符的操作数个数;不能改变运算符原有的优先级和结合特性。 • (5)不能改变运算符对预定义类型数据的操作方式。
6.5 运算符重载函数的形式 • 6.5.1 成员运算符函数 • 6.5.2 友员运算符函数 • 6.5.3 成员运算符函数与友元运算符函数的比较
6.5.1 成员运算符函数 • 1. 成员运算符函数定义的语法形式 • 成员运算符函数的原型在类的内部声明格式为: • class A { • //… • 返回类型 operator运算符(形参表); • //… • } • 在类外定义成员运算符函数的格式为: • 返回类型 A::operator运算符(形参表) • { • 函数体 • }
A是重载此运算符的类名,返回类型指定了运算符重载函数的运算结果类型;operator是定义运算符重载函数的关键字;运算符即是要重载的运算符名称;形参表中给出重载运算符所需要的参数和类型。A是重载此运算符的类名,返回类型指定了运算符重载函数的运算结果类型;operator是定义运算符重载函数的关键字;运算符即是要重载的运算符名称;形参表中给出重载运算符所需要的参数和类型。 • 注意:在成员运算符函数的形参表中,若运算符是单目的则形参表为空;若运算符是双目的则形参表中有一个操作数。
2.双目运算符重载 • 对于双目运算符,成员运算符函数的形参表中仅有一个参数,它作为运算符的右操作数,当前对象作为运算符的左操作数,它是通过this指针隐含地传递给函数的。
3.单目运算符重载 • 对单目运算符而言,成员运算符函数的参数表中没有参数,此时当前对象作为运算符的一个操作数。
6.5.2 友员运算符函数 • 1.友元运算符函数定义的语法形式 • 友元运算符函数的原型在类的内部声明格式为: • class A { • //… • friend 返回类型 operator运算符(形参表); • //… • } • 在类外定义友元运算符函数的格式为: • 返回类型 operator运算符(形参表) • { • 函数体 • }
是定义运算符函数的关键字;运算符即是要重载的运算符名称,但是必须是C++中可重载的运算符;形参表给出重载运算符所需要的参数和类型;关键字friend表明这是一个友员运算符函数。是定义运算符函数的关键字;运算符即是要重载的运算符名称,但是必须是C++中可重载的运算符;形参表给出重载运算符所需要的参数和类型;关键字friend表明这是一个友员运算符函数。 • 同友员函数一样,友员运算符函数也不是该类的成员函数,在类外定义时也不需要缀上类名;没有this指针,若友员运算符函数重载的是双目运算符,则参数表汇总有两个操作数;若重载的是单目运算符,则参数表中只有一个操作数。
2.双目运算符重载 • 我们知道两个复数a+bi和c+di进行乘、除的方法为: • 乘法:(a+bi)*(c+di)=(ac-bd)+(ab+bc)i • 除法:(a+bi)/(c+di)=((a+bi)*(c-di))/(c2+d2) • C++中要实现复数的乘、除运算,可由定义2个友员运算符函数,通过重载*、/运算符来实现。当用友元函数重载双目运算符时,两个操作数都要传递给运算符函数。
3.单目运算符重载 • 用友元函数重载单目运算符时,需要一个显式的操作数。