前言
接下来介绍一下多态的知识点,多态可以使程序变得更加灵活,继承可以使程序变得富有,通过调用基类的虚函数,来产生不同的行为,举个栗子:我们一群人在抢一个红包,会产生不同的结果,类似于我们这一群人去调用同一个基类的虚函数,而我们就是抢红包的结果就是多态的一种结果。
1.多态的概念和定义
1.1多态的概念
具体点就是去完成某个行为,当不同的对象去完成时会产生出不同 的状态或者说不同的结果。有些书籍会把多态进行细分:静态的多态-------函数重载 调用同一个函数,使用不同的参数,有不同的行为动态的多态-------调用一个虚函数,不同对象去调用,就有不同的行为
1.2多态的定义
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如 Student 继承了 Person 。Person 对象买票全价, Student 对象买票半价。在继承中构成多态的条件:
- 必须通过基类的指针或者引用来调用虚函数
- 被调用的必须是虚函数,且派生类对基类的虚函数进行了重写
#include<iostream>
using namespace std;
class Person
{
public:
virtual void buy()
{
cout << "全价-全票" << endl;
}
};
//派生类继承基类
class Student:public Person
{
public:
virtual void buy()//对基类的虚函数进行重写
{
cout << "半价-全票" << endl;
}
};
//用引用调用父类的虚函数
void buyone(Person&p)
{
p.buy();
}
int main()
{
Person p;
Student s;
buyone(p);
buyone(s);
}
1.3虚函数及其虚函数的重写
被virtual修饰的类成员函数称为虚函数。类似于上面的buy函数就是虚函数。这里跟我们继承里面的虚继承里的virtual关键字是不一样的意思,两者并没有什么联系,
虚函数的重写(覆盖):重写是语法上的概念,覆盖是底层实现原理
派生类中有一个跟 基类完全相同的虚函数 ( 即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同 ) ,称子类的虚函数重写了基类的虚函数。
1.4虚函数的例外
1. 协变 ( 基类与派生类虚函数返回值类型不同 )派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变
class A {};
class B : public A {};
class Person {
public:
virtual A* f() {
cout << "全价-全票" << endl;
return new A; }
};
class Student : public Person {
public:
virtual B* f() {
cout << "半价-全票" << endl;
return new B; }
};
void buyone(Person&p)
{
p.f();
}
int main()
{
Person p;
Student s;
buyone(p);
buyone(s);
}
2. 析构函数的重写 ( 基类与派生类析构函数的名字不同 )如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写 ,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理, 编译后析构函数的名称统一处理成destructor。这点跟继承里的析构函数规则一样。
举个栗子:
class Person {
public:
~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
~Student() { cout << "~Student()" << endl; }
};
int main()
{
//当不写virtual的,p2的析构函数和person的析构函数构成隐藏关系,这里调用的是~person
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
我们加上virtual,或者给父类加上virtual,这个时候才能实现多态
3.支持子类不加virtual,父类的函数必须加virtual,也构成重写,实现多态 ,和第二点例外紧密重合。这一点和上面的第2点紧密重合。
1.5 C++11给多态定义了两个新功能
final:修饰虚函数,表示该虚函数不能再被重写,继承中可以将构造函数定义为private,或者也适用于关键字final
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() { cout << "Benz-舒适" << endl; }
};
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class Car
{
public:
virtual void Drive() {}
};
class Benz :public Car
{
public:
virtual void Drive()override { cout << "Benz-舒适" << endl; }
};
1.6 重载、覆盖、隐藏定义的区别
2.抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。 包含纯虚函数的类叫做抽象类(也叫接口类),抽象类 不能实例化出对象 。派生类继承后也不能实例化出对象,只有重写纯虚数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()override { cout << "Benz-舒适" << endl; }
};
int main()
{
Car* b1 = new Benz;
b1->Drive();
}
注意点:
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的 继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。 所 以如果不实现多态,不要把函数定义成虚函数
3.多态的原理
介绍多态的原理的时候,我们先来看一个例子
class Car
{
public:
virtual void Drive() {};
protected:
int _i;
char _s;
};
class Benz :public Car
{
public:
virtual void Drive()override { cout << "Benz-舒适" << endl; }
};
int main()
{
//Car* b1 = new Benz;
//b1->Drive();
Car c;
cout << sizeof(c) << endl;
}
这个时候我们觉得输出结果是什么呢?来看看吧
12,这是为什么呢?对齐规则不是应该是8吗?注意看这里引入了虚函数,这就涉及到虚函数表了。
通过观察测试我们发现c 对象是12 bytes , 除了成员变量,还多一个__vfptr放在对象的前面(注意有些平台可能会 放到对象的最后面,这个跟平台有关) ,对象中的这个指针我们叫做虚函数表指针 (v 代表 virtual , f 代表 function) 。 一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中 ,虚函数表也简称虚表
3.1虚函数表
我们多写几个虚函数,然后在派生类中观察虚表。
总结:
- 先将基类中的虚表内容拷贝一份到派生类虚表中
- 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
- 派生类自己新增加的虚函数按其在 派生类中的声明次序增加到派生类虚表的最后
- 每个类的虚表都是独立存在的。独立开辟一块空间
注意点:
虚函数存在哪的?虚表存在哪的?答:虚函数存在虚表,虚表 存在对象中。注意上面的回答的错的 。但是很多人都是这样深以为然的。注意 虚表存的是虚函数指 针,不是虚函数 , 虚函数和普通函数一样的,都是存在代码段 的,只是他的指针又存到了虚表中。另外 对象中存的不是虚表,存的是虚表指针。那么 虚表存在哪的呢?实际我们去验证一下会发现vs下是存在 代码段的。
3.2为什么多态必须要指针或者引用来调用呢?
我们想一下如果用类对象直接来调用,会不会实现多态呢?我们来实现一下
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
很明显是不行的,他只会调用person的虚表(赋值会造成他的虚表指针的内存地址指向父类,而不会指向子类自己虚表里面的地址),而不会去调用子类的虚表,这里就不能实现多态。
3.3打印虚函数表的地址
这里介绍虚函数的地址,一方面是为了解释一下下面的多继承,还有一方面是为了让我们对多态虚表的结构更加熟悉,也是为了更加透彻的理解多态的逻辑。
void PrintVFT(void* vft[])
{
printf("%p\n",vft);
for (size_t i = 0; vft[i] != nullptr; ++i)
{
printf("vtf[%d]:%p\n", i, vft[i]);
}
printf("\n");
}
对b的虚表里的地址进行打印,同时打印出虚表地址。
PrintVFT((void**)(*((int*)& b)));
这里并不能很好的指明是哪一个虚函数的地址,我们再改进一下,
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%p,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
VFPTR* vTableb1 = (VFPTR*)(*(int*)& d);
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)& d + sizeof(Base1)));
PrintVTable(vTableb2);
return 0;
4.单继承和多继承关系的虚函数表
4.1单继承中的虚函数表
上面的例子已经介绍过了,不过需要注意的是在编译器中子类的虚函数可能不会在监视窗口里面表示出来,不过自己可以定义一个函数来验证上面的猜想。
4.2多继承中的虚函数表
多继承的虚函数表,两个的来自父类的子类虚表虽然不一样,但也只是jmp一下本质上还是和单继承的方式是一样的。
5.总结