理解面向对象的三大特性 -- 多态(详解)
基于32位操作系统
多态概念
概念
多态:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。在运行时,可以通过指向基类的指针,来调用实现派生类中的方法。
C++中,实现多态有以下方法:虚函数,抽象类,覆盖,模板(重载和多态无关)。
举例
直接说明多态是什么可能不太好理解, 举个例子来说明一下。
问题
一个主人养了猫和狗,猫和狗都有自己爱吃的东西,主人在喂它们的时候,如果既要判断是猫还是狗,再判断他们分别爱吃什么,就显得很麻烦。如果主人养了很多种动物,这样的重复判断,就会浪费很多时间。
解决
多态就是我们让每种食物和动物联系起来, 就相当于在动物的脸上贴上的动物喜欢吃的食物,在食物上面贴上哪种动物爱吃。这样让动物和植物有机的结合在了一起。提高了喂养的效率。
定义实现
构成条件
构成多态条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
虚函数
概念
我们将被virtual修饰的函数叫做虚函数(与虚继承使用同一个关键字,但是没有其他关系)
virtual void BuyTicket() {
std::cout << "Student Buy" << std::endl;
}
虚函数的重写
虚函数的重写(覆盖)就是派生类中有一个跟基类完全相同的虚函数称子类的虚函数重写了基类的虚函数。(协变允许返回值不同)
我们用一个没有重写的例子和重写的例子分别进行探讨。
代码如下:
没有virtual关键字
namespace test1 {
class Person {
public:
void BuyTicket() {
std::cout << "Person Buy" << std::endl;
}
};
class Student : public Person {
public:
void BuyTicket() {
std::cout << "Student Buy" << std::endl;
}
};
void Func(Person& p) {
p.BuyTicket();
}
void mytest() {
Person p;
Student s;
Func(p);
Func(s);
}
};
运行结果
无法访问派生类的同名函数
加上virtual关键字:
namespace test2 {
class Person {
public:
virtual void BuyTicket() {
std::cout << "Person Buy" << std::endl;
}
};
class Student : public Person {
public:
virtual void BuyTicket() {//叫做覆盖或者重写
std::cout << "Student Buy" << std::endl;
}
};
void Func(Person& p) {//必须通过指针或者引用调用虚函数
p.BuyTicket();
}
void mytest() {
Person p;
Student s;
Func(p);
Func(s);
}
};
运行结果:
实现多态
协变
协变是一种允许重写的函数返回值不同的操作,派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时称为协变。
namespace test3 {
//协变 允许返回值不同
class A{};
class B : public A{};
class Person {
public :
virtual A* f() {
return new A;
}
};
class Student : public Person {
public:
virtual B* f() {
return new B;
}
};
};
析构函数的特殊处理
概念
析构函数的特出处理就是析构函数的重写
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
意义
我们可以观察下面代码:
namespace test4 {//析构函数的重写
class Person {
public:
~Person() {
std::cout << "~Person" << std::endl;
}
};
class Student : public Person {
~Student() {//如果不加的话又资源泄漏的危险
std::cout << "~Student" << std::endl;
}
};
void mytest() {
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
}
};
运行结果:
我们发现不重写的析构函数是不会调用子类的析构函数的,也就是说当我们开辟一个子类对象,赋值给父类时,当其声明周期结束的时候,子类中的资源是不会释放的,这很可能造成一定的危险。
实现重写的虚构函数:
namespace test4 {//析构函数的重写
class Person {
public:
virtual ~Person() {
std::cout << "~Person" << std::endl;
}
};
class Student : public Person {
virtual ~Student() {//如果不加的话又资源泄漏的危险
std::cout << "~Student" << std::endl;
}
};
void mytest() {
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
}
};
运行结果:
资源完全释放,系统根据指针指向的对象进行析构函数的调用,而不是根据类型调用。这也是多态的一个特点。
抽象类 override final
抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(接口类)。因此抽象类是一种接口继承(与普通的实现继承区分对待)
注意
- 抽象类不能实例化出对象。
- 派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
为什么要有抽象类
- 为子类提供一个公共的类型;
- 封装子类中重复内容(成员变量和方法);
- 定义有抽象方法,子类虽然有不同的实现,但该方法的定义是一致的。
个人认为,抽象类更像是一种规章制度,一定程度上增加了代码的复用性与鲁棒性,写程序的时候更加不容易“跑偏”。
代码实现:
namespace test6 {//抽象类
class Car {
public:
virtual void Drive() = 0;
};
class Benz : public Car {
public:
virtual void Drive() {
std::cout << "This is Benz" << std::endl;
}
};
class BWM : public Car {
public:
virtual void Drive() {
std::cout << "This is BWM" << std::endl;
}
};
//class NoNO : public Car {
//};
void mytest() {
Car* b1 = new Benz;
Car* b2 = new BWM;
b1->Drive();
b2->Drive();
//Car* c1 = new Car;//不能实现
//NoNO* n1 = new NoNO;//不能实现
}
};
运行结果:
override与final
C++11提供了override和final两个关键字,可以帮助用户检测是否可以继承与重写。
- final 修饰虚函数,表示该虚函数不能被继承
- override 修饰虚函数,强制要求用户重写
注意的是override只能修饰子类的虚函数。
final代码
namespace test5 {
//final 虚函数不能被继承
class Car {
public:
virtual void Drive() final {}
};
class FiveStars : public Car {
public:
virtual void Drive() {//这样会报错
}
};
override代码
class House {
public:
virtual void BigHouse() {}
};
class NewHouse : public House{
virtual void BigHouse() override { //如果不重写会报错 不能放在基类只能放在派生类
}
};
//override 虚函数必须实现重写
多态的原理
原理介绍
多态可以让同名函数,因为函数指向对象的不同,而去调用该对象中该名称的函数。
其实底层就是因为虚表的一些神奇操作。
操作系统为构成多态的每个类增加了一个虚函数表。这个虚函数表中存放的就是virtual关键词修饰的虚函数的首地址。编译器运行的时候通过虚表中存储的函数首地址去调用对应的函数。从而达到我们多态的目的。
验证
验证虚表的存在
打印两个一模一样的类的大小
一个实现多态,一个没有实现。
namespace test7 {
//求一个正常的class大小
class Person {
public:
void test() {};
private:
int _a;
};
class Student {
public:
virtual void test() {};
private:
int _a;
};
void mytest() {
Person s1;
Student s2;
std::cout << sizeof(s1) << std::endl;
std::cout << sizeof(s2) << std::endl;
}
//求一个有虚表的class的大小
};
运行结果:
我们发现实现多态的类比没实现的大四个字节(一个指针大小)
对虚表中存储的函数地址进行打印
虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。利用这个特性我们进行类型强转,于是可以打印出这个续表中存储的各个函数指针的数值。
namespace test8 {
//student 继承person 打印出指针 深入验证虚表存储的是什么
class Person {
public:
virtual void Example1() {
std::cout << "pex1" << std::endl;
}
virtual void Example2() {
std::cout << "pex2" << std::endl;
}
virtual void Example3() {
std::cout << "pex3" << std::endl;
}
virtual void Example4() {
std::cout << "pex4" << std::endl;
}
private:
int _a;
};
class Student : public Person {
public:
virtual void Example1() {
std::cout << "sex1" << std::endl;
}
virtual void Example2() {
std::cout << "sex2" << std::endl;
}
virtual void Example3() {
std::cout << "sex3" << std::endl;
}
virtual void Example5() {
std::cout << "sex5" << std::endl;
}
private:
int _b;
};
typedef void(*VFPTR) ();
void MyPrint(VFPTR vTable[])
{
// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
std::cout << " 虚表地址>" << vTable << std::endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
std::cout << std::endl;
}
int mytest() {
Person s1;
Student s2;
//打印出虚函数表的数值
VFPTR* vTableb = (VFPTR*)(*(int*)&s1);
MyPrint(vTableb);
VFPTR* vTabled = (VFPTR*)(*(int*)&s2);
MyPrint(vTabled);
return 0;
}
};
运行结果:
于是发现,父子类中都有一张虚表,用来存放虚函数的地址,子类重写了父类中的虚函数时,子类的虚表会指向新的地址,该地址是存放重写的虚函数的。(函数都在代码段)
当没有重写时候,父子的虚表都指向同一个地址。
单继承和多继承的虚函数表
单继承的虚表如上面所示,父子类各有一张。而出现多继承的时候,子类中就会存在1张虚表包含了n个父类虚表的虚表,每张虚表都来自于不同的父类。
代码所示:
using namespace std;
namespace test9 {
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
void mytest()
{
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
PrintVTable(vTableb2);
}
};
运行结果:
先继承的类虚表在前面,后继承的类的虚表在后边。
子类中父类没有的虚函数会默认放在第一张虚表中。
其他
-
首先要区分好重载,重写,重定义的区别。
- 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,虚表也存在代码端。
-
明白虚表如何生成的
3.1. 先将基类中的虚表内容拷贝一份到派生类虚表中
3.2.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
3.3.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。 -
动态绑定与静态绑定
4.1 在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
4.2 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。 - inline函数不能是虚函数,inline函数没有地址,无法把地址放到虚函数表中,不能是虚函数。
- 静态成员函数不能是虚函数,静态成员函数没有this指针,不能用使用类型::成员函数的调用方式无法访问虚函数表
- 虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。