Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

My Blog

从此烟雨落金城,一人撑伞两人行。

C++ 多态

简介

编译时的多态:函数的重载(包括运算符的重载)、对重载函数的调用,在编译时就能根据实参确定应该调用哪个函数,因此叫编译时的多态。

运行时多态:继承、虚函数等概念有关。

虚函数

通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数。

让基类指针能够访问派生类的成员函数,C++ 增加了虚函数(Virtual Function)

1
2
3
4
5
6
7
8
9
//基类People
class People{
public:
People(char *name, int age);
virtual void display(); //声明为虚函数
protected:
char *m_name;
int m_age;
};

有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员。换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)

C++中虚函数的唯一用处就是构成多态。

C++提供多态的目的是:可以通过基类指针对所有派生类(包括直接派生和间接派生)的成员变量和成员函数进行“全方位”的访问,尤其是成员函数。如果没有多态,我们只能访问成员变量。

虚函数是根据指针的指向来调用的,指针指向哪个类的对象就调用哪个类的虚函数。

构成多态的条件

虚函数使用条件:

  1. 只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加。
  1. 可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽关系的同名函数都将自动成为虚函数。
  2. 当在基类中定义了虚函数时,如果派生类没有定义新的函数来遮蔽此函数,那么将使用基类的虚函数。
  3. 只有派生类的虚函数覆盖基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)。
  4. 派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义。
  5. 析构函数可以声明为虚函数,而且有时候必须要声明为虚函数。

构成多态的条件:

  • 必须存在继承关系;
  • 继承关系中必须有同名的虚函数,并且它们是覆盖关系(函数原型相同)。
  • 存在基类的指针,通过该指针调用虚函数。

虚析构函数

析构函数用于在销毁对象时进行清理工作,可以声明为虚函数,而且有时候必须要声明为虚函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//基类
class Base{
public:
Base();
~Base();
protected:
char *str;
};
Base::Base(){
str = new char[100];
cout<<"Base constructor"<<endl;
}
Base::~Base(){
delete[] str;
cout<<"Base destructor"<<endl;
}
//派生类
class Derived: public Base{
public:
Derived();
~Derived();
private:
char *name;
};
Derived::Derived(){
name = new char[100];
cout<<"Derived constructor"<<endl;
}
Derived::~Derived(){
delete[] name;
cout<<"Derived destructor"<<endl;
}
int main(){
Base *pb = new Derived();
delete pb;
cout<<"-------------------"<<endl;
Derived *pd = new Derived();
delete pd;
return 0;
}

第一条语句delete pb;只调用了基类的析构函数(上转型对象)。

第二条语句delete pd;同时调用了派生类和基类的析构函数(先调用派生类的析构函数,后调用基类的析构函数)。

将基类的析构函数声明为虚函数后,派生类的析构函数也会自动成为虚函数。这个时候编译器会忽略指针的类型,而根据指针的指向来选择函数。

就是说,大部分情况下都应该将基类的析构函数声明为虚函数。

纯虚函数和抽象类(接口)

将虚函数声明为纯虚函数,语法格式为:

1
virtual 返回值类型 函数名 (函数参数) = 0;

包含纯虚函数的类称为抽象类(Abstract Class)。之所以说它抽象,是因为它无法实例化,也就是无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。

抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。

纯虚函数说明:

  1. 一个纯虚函数就可以使类成为抽象基类,抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数和成员变量。

  2. 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数。

虚函数表

当通过指针访问类的成员函数时:

  • 如果该函数是非虚函数,那么编译器会根据指针的类型找到该函数;指针是哪个类的类型就调用哪个类的函数。
  • 如果该函数是虚函数,并且派生类有同名的函数遮蔽它,那么编译器会根据指针的指向找到该函数;指针指向的对象属于哪个类就调用哪个类的函数。这就是多态。

编译器之所以能通过指针指向的对象找到虚函数,是因为在创建对象时额外地增加了虚函数表。

如果一个类包含了虚函数,那么在创建该类的对象时就会额外地增加一个数组,数组中的每一个元素都是虚函数的入口地址。不过数组和对象是分开存储的,为了将对象和数组关联起来,编译器还要在对象中安插一个指针,指向数组的起始位置。这里的数组就是虚函数表(Virtual function table),简写为vtable

img

图中左半部分是对象占用的内存,右半部分是虚函数表 vtable。在对象的开头位置有一个指针 vfptr,指向虚函数表,并且这个指针始终位于对象的开头位置。

基类的虚函数在 vtable 中的索引(下标)是固定的,不会随着继承层次的增加而改变,派生类新增的虚函数放在 vtable 的最后。如果派生类有同名的虚函数遮蔽(覆盖)了基类的虚函数,那么将使用派生类的虚函数替换基类的虚函数,这样具有遮蔽关系的虚函数在 vtable 中只会出现一次。

评论