一.多态基础
面向对象程序设计语言有封装、继承和多态三种机制,这三种机制能够有效提高程序的可读性、可扩充性和可重用性。
“多态(polymorphism)”指的是同一名字的事物可以完成不同的功能。多态可以分为编译时的多态和运行时的多态。前者主要是指函数的重载(包括运算符的重载)、对重载函数的调用,在编译时就能根据实参确定应该调用哪个函数,因此叫编译时的多态;而后者则和继承、虚函数等概念有关。
1.多态
我们之前知道基类的指针可以指向派生类对象,但是其中存在一个问题:通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数。大家可以自己写代码尝试确认,为了消除这种尴尬,让基类指针能够访问派生类的成员函数,C++增加了虚函数。使用虚函数非常简单,只需要在函数声明前面增加 virtual
关键字。
有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员。换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)。
#include <iostream>
using namespace std;//基类People
class People{
public:People(char *name, int age);virtual void display(); //声明为虚函数
protected:char *m_name;int m_age;
};
People::People(char *name, int age): m_name(name), m_age(age){}
void People::display(){cout<<m_name<<"今年"<<m_age<<"岁了,是个无业游民。"<<endl;
}//派生类Teacher
class Teacher: public People{
public:Teacher(char *name, int age, int salary);virtual void display(); //声明为虚函数
private:int m_salary;
};
Teacher::Teacher(char *name, int age, int salary): People(name, age), m_salary(salary){}
void Teacher::display(){cout<<m_name<<"今年"<<m_age<<"岁了,是一名教师,每月有"<<m_salary<<"元的收入。"<<endl;
}int main(){People *p = new People("王志刚", 23);p -> display();p = new Teacher("赵宏佳", 45, 8200);p -> display();return 0;
}
上面的代码中,同样是p->display();
这条语句,当 p 指向不同的对象时,它执行的操作是不一样的。同一条语句可以执行不同的操作,看起来有不同表现方式,这就是多态。多态是面向对象编程的主要特征之一,C++中虚函数的唯一用处就是构成多态。
C++提供多态的目的是:可以通过基类指针对所有派生类(包括直接派生和间接派生)的成员变量和成员函数进行“全方位”的访问,尤其是成员函数。如果没有多态,我们只能访问成员变量。
多态满足条件:
- 1、有继承关系
- 2、子类重写父类中的虚函数
多态使用:父类指针或引用指向子类对象
2.引用实现多态
引用在本质上是通过指针的方式实现的,既然借助指针可以实现多态,那么我们就有理由推断:借助引用也可以实现多态。
修改上例中 main() 函数内部的代码,用引用取代指针:
int main(){People p("王志刚", 23);Teacher t("赵宏佳", 45, 8200);People &rp = p;People &rt = t;rp.display();rt.display();return 0;
}
运行结果:
王志刚今年23岁了,是个无业游民。
赵宏佳今年45岁了,是一名教师,每月有8200元的收入。
由于引用类似于常量,只能在定义的同时初始化,并且以后也要从一而终,不能再引用其他数据,所以本例中必须要定义两个引用变量,一个用来引用基类对象,一个用来引用派生类对象。从运行结果可以看出,当基类的引用指代基类对象时,调用的是基类的成员,而指代派生类对象时,调用的是派生类的成员。
不过引用不像指针灵活,指针可以随时改变指向,而引用只能指代固定的对象,在多态性方面缺乏表现力,所以以后我们再谈及多态时一般是说指针。
3.多态剖析
我们知道一个空类在C++中的大小是1,如果一个类只含有成员函数,它近似于一个空类,如果使用sizeof()
关键字计算其大小,其结果是1。
那么就上面的例子,现在我们计算People
类的大小,应该为56字节,其中string
为40字节,而隐含的vfptr
在x64
环境下为8字节,分析以下Teachar
的大小为64字节,注意VS的默认情况下是对齐方式的。
4.虚函数注意事项
C++虚函数对于运行时多态具有决定性的作用,有虚函数才能构成多态。
-
只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加。
-
为了方便,你可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽关系的同名函数都将自动成为虚函数。
-
当在基类中定义了虚函数时,如果派生类没有定义新的函数来遮蔽此函数,那么将使用基类的虚函数。
-
只有派生类的虚函数覆盖基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)。例如基类虚函数的原型为
virtual void func();
,派生类虚函数的原型为virtual void func(int);
,那么当基类指针 p 指向派生类对象时,语句p -> func(100);
将会出错,而语句p -> func();
将调用基类的函数。 -
**构造函数不能是虚函数。**对于基类的构造函数,它仅仅是在派生类构造函数中被调用,这种机制不同于继承。也就是说,派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义。
-
析构函数可以声明为虚函数,而且有时候必须要声明为虚函数。
二.虚函数
1.纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数。
纯虚函数语法:
virtual 返回值类型 函数名 (函数参数) = 0;
当类中只要有了一个纯虚函数,这个类也称为抽象类。
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
2.虚析构
问题:多态使用时,如果在子类中有属性开辟到堆区,那么父类指针在释放时无法调用子类的析构代码。
方法:将父类的析构函数改为虚析构或纯虚析构。
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
3.虚函数表
编译器之所以能通过基类指针指向的对象找到虚函数,是因为在创建对象时额外地增加了虚函数表。
如果一个类包含了虚函数,那么在创建该类的对象时就会额外地增加一个数组,数组中的每一个元素都是虚函数的入口地址。不过数组和对象是分开存储的,为了将对象和数组关联起来,编译器还要在对象中安插一个指针,指向数组的起始位置。这里的数组就是虚函数表(Virtual function table),简写为vtable
。
其中某一多态的内存模型图,如图所示:
图中左半部分是对象占用的内存,右半部分是虚函数表 vtable
。在对象的开头位置有一个指针 vfptr
,指向虚函数表,并且这个指针始终位于对象的开头位置。
当通过指针调用虚函数时,先根据指针找到 vfptr
,再根据 vfptr
找到虚函数的入口地址。
三.多态案例练习1
1.要求
写一个简单计算器。
2.实验代码
#include <iostream>
#include <string>
using namespace std;//基类
class Calculate {
public:int m_num1;int m_num2;//虚函数virtual int getresult() {return 0;}
};//加法类计算器
class AddCalculate :public Calculate {int getresult() {return m_num1 + m_num2;}
};//减法计算器
class SubCalculate :public Calculate {int getresult() {return m_num1 - m_num2;}
};//乘法计算器
class MulCalculate :public Calculate {int getresult() {return m_num1 * m_num2;}
};//除法计算器
class DivCalculate :public Calculate {int getresult() {return m_num1 / m_num2;}
};
int main() {Calculate* p = new AddCalculate;p->m_num1 = 20;p->m_num2 = 10;cout << p->m_num1 << "+" << p->m_num2 << "=" << p->getresult() << endl;return 0;
}
3.总结
多态的优点
- 代码组织结构清晰
- 可读性强
- 利于前期和后期的扩展以及维护
四.多态案例练习2
1.题目
案例描述:
电脑主要组成部件为 CPU (用于计算) ,显卡 (用于显示) ,内存条 (用于存储)。将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件,例如intel
厂商和Lenovo
厂商,创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口。测试时组装三台不同的电脑进行工作 。
2.实验代码
#include<iostream>
using namespace std;//抽象CPU类
class CPU
{
public://抽象的计算函数virtual void calculate() = 0;
};//抽象显卡类
class VideoCard
{
public://抽象的显示函数virtual void display() = 0;
};//抽象内存条类
class Memory
{
public://抽象的存储函数virtual void storage() = 0;
};//电脑类
class Computer
{
public:Computer(CPU * cpu, VideoCard * vc, Memory * mem){m_cpu = cpu;m_vc = vc;m_mem = mem;}//提供工作的函数void work(){//让零件工作起来,调用接口m_cpu->calculate();m_vc->display();m_mem->storage();}//提供析构函数 释放3个电脑零件~Computer(){//释放CPU零件if (m_cpu != NULL){delete m_cpu;m_cpu = NULL;}//释放显卡零件if (m_vc != NULL){delete m_vc;m_vc = NULL;}//释放内存条零件if (m_mem != NULL){delete m_mem;m_mem = NULL;}}private:CPU * m_cpu; //CPU的零件指针VideoCard * m_vc; //显卡零件指针Memory * m_mem; //内存条零件指针
};//具体厂商
//Intel厂商
class IntelCPU :public CPU
{
public:virtual void calculate(){cout << "Intel的CPU开始计算了!" << endl;}
};class IntelVideoCard :public VideoCard
{
public:virtual void display(){cout << "Intel的显卡开始显示了!" << endl;}
};class IntelMemory :public Memory
{
public:virtual void storage(){cout << "Intel的内存条开始存储了!" << endl;}
};//Lenovo厂商
class LenovoCPU :public CPU
{
public:virtual void calculate(){cout << "Lenovo的CPU开始计算了!" << endl;}
};class LenovoVideoCard :public VideoCard
{
public:virtual void display(){cout << "Lenovo的显卡开始显示了!" << endl;}
};class LenovoMemory :public Memory
{
public:virtual void storage(){cout << "Lenovo的内存条开始存储了!" << endl;}
};void test01()
{//第一台电脑零件CPU * intelCpu = new IntelCPU;VideoCard * intelCard = new IntelVideoCard;Memory * intelMem = new IntelMemory;cout << "第一台电脑开始工作:" << endl;//创建第一台电脑Computer * computer1 = new Computer(intelCpu, intelCard, intelMem);computer1->work();delete computer1;cout << "-----------------------" << endl;cout << "第二台电脑开始工作:" << endl;//第二台电脑组装Computer * computer2 = new Computer(new LenovoCPU, new LenovoVideoCard, new LenovoMemory);;computer2->work();delete computer2;cout << "-----------------------" << endl;cout << "第三台电脑开始工作:" << endl;//第三台电脑组装Computer * computer3 = new Computer(new LenovoCPU, new IntelVideoCard, new LenovoMemory);;computer3->work();delete computer3;