一 多态性的分类
编译时的多态
函数重载
运算符重载
运行时的多态
虚函数
1 运算符重载的引入
使用C++编写程序时,我们不仅要使用基本数据类型,还要设计新的数据类型-------类类型。
一般情况下,基本数据类型的运算都是运算符来表达,这很直观,语义也简单。
例如:
int a,b,c;a=b+c;
对于基本数据类型,就隐含着运算符重载的概念。
如果直接将运算符作用在类类型之上,情况又如何呢?
例如:
Complex ret,c1,c2;ret=c1+c2;
编译器将不能识别运算符的语义。
需要一种机制来重新定义运算符作用在类类型上的含义。
这种机制就是运算符重载。
二 两种重载函数的比较
多数情况下,运算符可以重载为类的成员函数,也可以重载为友元函数。但两种重载也有各自特点:
一般情况下,单目运算符重载为类的成员函数;双目元素重载为类的友元函数。
有些双目运算符不能重载为类的友元函数:=,(),[],->
类型转换函数只能定义为类的成员函数,而不能定义为友元函数。
若一个运算符的操作需要修改对象的状态,则重载为成员函数比较好;
若运算符所需要的操作数(尤其是第一个操作数)希望有隐式类型转换,则只能选择友元函数;
若运算符是成员函数,最左边的操作数必须是运算符类的对象(或者类对象的引用)。如果左边操作数必须是一个不同类的对象,或者是基本数据类型,则必须重载为友元函数;
当需要重载运算符的元素具有交换性时,重载为友元函数;
1 重载运算符的几点注意事项
大多数预定义的运算符可以被重载,重载后的优先级、结合级及所需的操作数都不变。
但少数的C++运算符不能重载:
不能重载非运算符的符号,例如:;
C++ 不运行重载不存在的运算符,如"?"、“**”等。
当运算符被重载时,它是被绑定在一个特定的类类型之上的。当此运算符不作用在特定类类型上时,它将保持原有的含义。
当重载运算符时,不能创造新的运算符符号,例如不能用"**"来表示求幕运算符。
应当尽可能保持重载运算符原有的语义。试想,如果在某个程序中用"+“表示减,”*"表示除,那么这个程序读起来将会非常别扭。
三 多态性的引入
1 虚函数和多态性
重载普通的成员函数的两种方式:
在同一个类中重载:重载函数是以参数特征区分的。
派生类重载基类的成员函数:
由于重载函数处在不同的类中,因此它们的原型可以完全相同。调用时使用“类名::函数名”的方式加以区分。
以上两种重载的匹配都是在编译的时候静态完成的。
重载是一种简单形式的多态。
C++提供另一种更加灵活的多态机制:虚函数。虚函数运行函数调用与函数体的匹配在运行时才确定。
虚函数提供的是一种动态绑定的机制。
2 赋值兼容规则
在公有派生方式下,派生类对象可以作为基类对象来使用,具体方式如下:
派生类拥有从基类继承过来的成员;
基类对象和派生类对象的内存布局方式;
当一个派生类对象直接赋值给基类对象时,不是所有的数据都赋给了基类对象,赋予的只是派生类对象的一部分。这部分叫做派生类对象的“切片(sliced)”。
注意
回忆一下不同的继承方式,子类对基类中成员的访问权限:
只有在公有派生的情况下,才有可能出现“基类的公有成员变成派生类的公有成员”的情况。
通过基类引用或指针所能看到的是一个基类对象,派生类中的成员对于基类引用或指针来说是“不可见的”。
我们能不能“通过基类引用或指针来访问派生类的成员”呢?
为了达到上述目的,我们可以利用C++的虚函数机制,将基类的Print说明为虚函数形式。这样就可以通过基类引用或指针来访问派生类中的Print。
3 虚函数
在基类中用virtual关键字声明的成员函数即为虚函数。
虚函数可以在一个或多个派生类中被重写定义,但要求重定义时虚函数的原型(包括返回值类型、函数名、参数列表)必须完全相同。
3 基类中的函数具有虚特性的条件
在基类中用virtual将函数说明为虚函数。
在公有派生类中原型一致地重载该虚函数。
定义基类引用或指针,使其引用或指向派生类对象。当通过该引用或指针调用需要函数时,该函数将体现出虚特性来。
C++中,基类必须指出希望派生类重定义哪些函数。定义为virtual的函数是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。
注意:
在派生类中重载虚函数时必须与基类中的函数原型相同,否则该函数将丢失虚特性。
仅返回类型不同,其他相同。C++编译器认为这种情况是不允许的。
函数原型不同,,仅函数名相同。C++编译器认为这是一般的函数重载,此时虚特性丢失。
四 虚函数与多态性
1 提供虚函数的意义
提升软件的重用性
基类使用虚函数提供一个接口,但派生类可以定义自己的实现版本。
虚函数调用的解释依赖于它的对象类型,这就实现了“一个接口,多种语义”的概念。
提供软件架构的合理性。
2 虚函数和虚指针
在编译时,为每个有虚函数的类建立一张虚函数表VTABLE,表中存放的时每一个虚函数的指针;同时用一个虚指针VPTR指向这张表的入口。
访问某个虚函数时,不是直接找到那个函数的地址,而是通过VPTR间接查到它的地址。
对象的内存空间除了保存数据成员外,还保存VPTR。VPTR由构造函数来初始化。
3 对虚函数的要求
虚函数必须是类的非静态成员函数。
不能将虚函数说明为全局函数。
不能将虚函数说明为静态成员函数。
不能将虚函数说明为友元函数。
本质的原因就是非静态成员函数隐含传递this指针,而通过this指针能够找到VPTR。
4 在成员函数中调用虚函数
在一个基类或派生类的成员函数中,可以直接调用类等级中的虚函数。此时需要根据成员函数中this指针所指向的对象来判断调用的时哪一个函数。
5 析构函数可以定义为虚函数
构造函数不能定义为虚函数。
而析构函数可以定义为虚函数。
若析构函数为虚函数,那么当使用delete释放基类指针所指向的派生类对象时,先调用派生类的析构函数,再调用基类的析构函数。
五 纯虚函数与抽象类
基类中的这些公共接口只需要有售卖而不需要有实现,即纯虚函数。纯虚函数刻画了派生类应该遵循的协议,这些协议的具体实现由派生类来决定。
将一个函数说明为纯虚hasn’t,就要求任何派生类都定义自己的实现。
拥有纯虚函数的类被称为抽象类。抽象类不能被实例化,只能作为基类被使用。
抽象类的派生类需要实现纯虚函数,否则该派生类也是一个抽象类。
当抽象类的所有函数成员都是纯虚函数时,这个类被称为接口类。
小结:
继承和动态绑定在两个方面简化了我们的程序:
能够容易地定义与其他类相似但又不相同的新类,能更容易地编写忽略这些相似类型之间区别的程序。
许多应用程序的特性可以用一些相关但略有不同的概率描述。面向对象编程与这种应用非常匹配。通过继承可以定义一些类型,可以模型不同冲类;通过动态绑定可以编写程序,使用这些类而又忽略与具体类型相关的差异。
继承和动态绑定的思想在概念上非常简单,但对于如何创建应用程序以及对于程序设计语言必须支持得特性,含义深远。
面向对象编程的关键思想是多态性。因为在需要情况下可以互换地使用派生类型或基类型的“许多形态”,所以称通过继承而相关联的类型为多态类型。C++中,多态型仅用于通过继承而相关性的类型的引用或指针。
我们称因继承而相关的类构成一个继承层次。其中一个类称为根,所有其他类直接或间接继承根类。