一、多态性
静态联编所支持的多态性称为编译时的多态性,当调用重载函数时,编译器可以根据调用时所使用的实参在编译时就确定应该调用哪个函数;动态联编所支持的多态性称为运行时的多态性,这由虚函数来支持。虚函数类似于重载函数,但与重载函数的实现策略不同,即对虚函数的调用使用动态联编。
1、静态联编中的赋值兼容性及名字支配规律
派生一个类的原因并非总是为了添加新的数据成员或成员函数,有时是为了重新定义基类的成员函数。先看一个示例如下:
const double PI = 3.14159;
class Point {
private:
double x, y;
public:
Point(double a, double b) :
x(a), y(b) {
}
double area() {
return 0;
}
};
class Circle: public Point {
private:
double radius;
public:
Circle(double a, double b, double c) :Point(a, b) {
this->radius = c;
}
double area() {
return PI * radius * radius;
}
};
#include "Example1.h"
void example1();
int main() {
example1();
return 0;
}
void example1() {
Point a(10.5, 12.3);
cout << a.area() << endl; //0 名字支配规律决定它们只调用自己的area()函数
Circle c(10.5, 12.3, 13.5);
cout << c.area() << endl; //572.555 同上
Point *p1 = &c;
cout << p1->area() << endl; //0 根据赋值兼容规则,Point类的指针指向的是基类Point的area()
Point &p2 = c;
cout << p2.area() << endl; //0 根据赋值兼容规则,Point类的引用跟指针一样
}
对象的内存地址空间中只包含数据成员,并不存储有关成员函数的信息,这些成员函数的地址翻译过程与其对象的内存地址无关,编译器只根据数据类型来翻译成员函数的地址并判断其调用的合法性,这是由静态联编决定的。
声明的基类指针只能指向基类,派生类指针只能指向派生类,它们的原始类型决定它们只能调用各自的同名函数,除非派生类没有基类的同名函数,派生类的指针才根据继承调用基类的成员函数。
2、动态联编的多态性
如果让编译器进行动态联编,这就需要使用到关键字virtual来声明虚函数。当编译系统编译含有虚函数的类时,将为它建立一个虚函数表,表中的每一个元素都指向一个虚函数的地址。此外,编译器也为类增加一个数据成员,这个数据成员是一个指向该虚函数表的指针,通常称为vptr。
虚函数的地址翻译取决于对象的内存地址,编译器为含有虚函数的对象首先建立一个入口地址,这个地址用来存放指向虚函数表的指针vptr,然后按照类中虚函数的声明次序,一一填入函数指针。当调用虚函数时,先通过vptr找到虚函数表,然后再找出虚函数的真正地址。
派生类可以继承基类的虚函数表,而且只要和基类同名的成员函数,无论是否使用virtual声明,它们都自动成为虚函数。如果派生类没有改写继承基类的虚函数,则函数指针调用基类的虚函数;如果派生类改写了基类的虚函数,编译器将重新为派生类的虚函数建立地址,函数指针会调用改写过的虚函数。
虚函数的调用规则是:根据当前对象,优先调用对象本身的虚成员函数。
示例如下:
const double PI1 = 3.14159;
class Point1 {
private:
double x, y;
public:
Point1(double a, double b) :
x(a), y(b) {
}
virtual double area() {
return 0;
}
};
class Circle1: public Point1 {
private:
double radius;
public:
Circle1(double a, double b, double c) :Point1(a, b) {
this->radius = c;
}
virtual double area() {
return PI1 * radius * radius;
}
};
#include "Example2.h"
void example2();
int main() {
example2();
return 0;
}
void example2(){
Point1 a(10.5, 12.3);
cout << a.area() << endl; //0 名字支配规律决定它们只调用自己的area()函数
Circle1 c(10.5, 12.3, 13.5);
cout << c.area() << endl; //572.555
Point1 *p1 = &c;
cout << p1->area() << endl; //572.555 虚函数优先调用对象本身的成员函数
Point1 &p2 = c;
cout << p2.area() << endl; //572.555 同上
}
二、虚函数
一旦基类定义了虚函数,该基类的派生类的同名函数也自动成为虚函数。
1、虚函数的定义
虚函数只能是类中的一个成员函数,但不能是静态成员,关键字virtual用于类中该函数的声明。如下:
class A{
public:
virtual void func(); //声明虚函数
};
当在派生类中定义了一个同名的成员函数时,只要该成员函数的参数个数和函数类型以及它的返回类型同基类中同名虚函数的一样,则无论是否为该成员函数使用virtual,它都将成为一个虚函数。
2、虚函数实现多态性的条件
关键字virtual指示C++编译器对调用虚函数使用动态联编,这种多态性是程序运行到需要的语句处才动态确定的,所以称为运行时的多态性。产生运行时的多态性有如下三个前提:
① 类之间的继承关系满足赋值兼容性规则;
② 改写了同名虚函数;
③ 根据赋值兼容性规则使用指针或引用。
下面设计一个外部函数,把指针或引用作为函数参数来实现动态联编,示例如下:
const double PI2 = 3.14159;
class Point2 {
private:
double x, y;
public:
Point2(double a, double b) :
x(a), y(b) {
}
virtual double area() {
return 0;
}
};
class Circle2: public Point2 {
private:
double radius;
public:
Circle2(double a, double b, double c) :Point2(a, b) {
this->radius = c;
}
virtual double area() {
return PI2 * radius * radius;
}
};
void display(Point2 &p){
cout << p.area() << endl;
}
void display(Point2 *p){
cout << p->area() << endl;
}
#include "Example3.h"
void example3();
int main() {
example3();
return 0;
}
void example3(){
Point2 a(10.5, 12.3);
Circle2 c(10.5, 12.3, 13.5);
Point2 *p1 = &c;
Point2 &p2 = c;
display(a); //0
display(p1); //572.555
display(p2); //572.555
}
3、构造函数和析构函数调用虚函数
在构造函数和析构函数中调用虚函数采用静态联编,即它们所调用的虚函数是自己的类或基类中定义的函数,但不是任何在派生类中重定义的虚函数。示例如下:
class A {
public:
A() {
}
virtual ~A() {
}
virtual void fun1() {
cout << "创建类A的对象" << endl;
}
virtual void fun2() {
cout << "销毁类A的对象" << endl;
}
};
class B: public A {
public:
B() {
fun1();
}
~B() {
fun2();
}
void fun3() {
cout << "程序执行到这里然后...";
fun1();
}
};
class C: public B {
public:
C() {
}
~C() {
fun2();
}
void fun1(){
cout << "这是类C" << endl;
}
void fun2(){
cout << "销毁类C的对象" << endl;
}
};
#include "Example4.h"
void example4();
int main() {
example4();
return 0;
}
void example4(){
C c;
/**
* 创建类C的对象时,会首先创建基类的对象,然后再创建派生类的对象,A类的构造函数没有任何输出,
* B类的构造函数调用了A类的虚函数,这里输出的是:“创建类A的对象”
*/
c.fun3();
/**
* 类C的对象调用fun3函数,而自己没有就去调用基类的fun3函数,首先输出:“程序执行到这里然后...”,
* 然后调用fun1函数,由于类C中有定义这个函数,所以会调用自己的fun1函数输出:“这是类C”
*/
}
/**
* 程序结束之后,会按照创建对象相反的顺序,即先创建后析构的原则析构对象,则它会首先调用类C的析构函数,
* 类C的析构函数调用了fun2函数,则会先输出:“销毁类C的对象”,然后调用类B的析构函数,类B的析构函数
* 也调用了fun2函数,但类B中并没有定义这个函数,所以它调用了基类A中的fun2函数,输出了:“销毁类A的对象”,
* 紧接着类A的析构函数被调用,但类A的析构函数并没有进行任何输出
*/
//执行结果如下:
//创建类A的对象
//程序执行到这里然后...这是类C
//销毁类C的对象
//销毁类A的对象
目前推荐的C++标准并不支持虚构造函数,但是它支持虚析构函数。由于析构函数不允许有参数,因此一个类只能有一个虚析构函数。delete运算符和析构函数一起工作,当使用delete删除一个对象时,delete隐含着对析构函数的一次调用,如果析构函数为虚函数,则这个调用采用动态联编。一般来说,一个类如果定义了虚函数,析构函数也应说明为虚函数,尤其是在析构函数要完成一些有意义的任务时,例如释放内存等。
只要基类中的析构函数定义为虚函数,则派生类中的析构函数无论是否声明都自动成为虚函数。
4、纯虚函数与抽象类
在有些情况下,不能在基类中为虚函数给出一个有意义的定义,这时可以将它说明为纯虚函数,将其定义留给派生类去做。说明纯虚函数的形式如下:
class 类名{
virtual 函数类型 函数名(参数列表) = 0;
};
//示例
class Square{
virtual double area() = 0;
};
一个类可以说明多个纯虚函数,包含有纯虚函数的类称为抽象类。一个抽象类只能作为基类来派生新类,不能说明抽象类的对象,但可以说明指向抽象类对象的指针或引用。如下示例:
Point &p1;
Point *p2;
从一个抽象类派生的类必须提供纯虚函数的实现代码,或在派生类中仍将它说明为纯虚函数,否则编译器将会报错。这说明了纯虚函数的派生类仍是抽象类,如果派生类中给出了所有基类中纯虚函数的实现,则该派生类不再是抽象类。抽象类至少含有一个虚函数,而且至少有一个虚函数是纯虚函数,以便将它与空的虚函数区分开来。下面是虚函数两种不同的表示方法:
virtual void area() = 0; //纯虚函数
virtual void area() {} //空的虚函数
下面是一个示例,计算正文形和长方形的面积:
class Shape{//抽象类:包含一个纯虚函数
public:
virtual double area() = 0;
virtual ~Shape(){};
};
class Square:public Shape{
private:
double length;
public:
Square(double a):length(a){
}
~Square(){
cout << "析构Square类的对象" << endl;
}
virtual double area(){
return length*length;
}
};
class Rectangle:public Shape{
private:
double width,height;
public:
Rectangle(double a,double b):width(a),height(b){
}
~Rectangle(){
cout << "析构Rectangle类的对象" << endl;
}
virtual double area(){
return width*height;
}
};
#include "Example5.h"
void example5();
int main() {
example5();
return 0;
}
void example5(){
Shape *s[2];
s[0] = new Square(10);
s[1] = new Rectangle(5,10);
cout << "面积之和是:" << s[0]->area() + s[1]->area() << endl;
delete s[0];
delete s[1];
}
三、多重继承与虚函数
多重继承可以视为多个单一继承的组合。示例如下:
class AA{
public:
virtual void fun(){
cout << "this is AA" << endl;
}
};
class BB{
public:
virtual void fun(){
cout << "this is BB" << endl;
}
};
class CC:public AA,public BB{
public:
void fun(){
cout << "this is CC" << endl;
}
};
#include "Example6.h"
void example6();
int main() {
example6();
return 0;
}
void example6(){
AA *a;
BB *b;
CC *c1,c2;
a = &c2;
b = &c2;
c1 = &c2;
a->fun();
b->fun();
c1->fun();
// this is CC
// this is CC
// this is CC
}