多态的介绍
多态的概念
多态的概念:简单来讲,多态的意思就是多种形态,或者说,去完成某个行为,当不同的对象去完成时会产生出不同的状态
也可以理解为我们人去完成同一件事情,但是每一个人去完成,都会产生不一样的结果
这里我们简单的演示一下
class Base {
public:
virtual void print() {
cout << "我是基类" << endl;
}
};
class Derive :public Base{
public:
virtual void print() {
cout << "我是派生类" << endl;
}
};
void func(Base& a) {
a.print();
}
int main() {
Base base;
Derive derive;
func(base);
func(derive);
}
这里便是不同的对象调用会产生不同的结果
对比我们上一节继承来讲,这里的print并没有构成隐藏关系,而是构成重写关系,什么是重写呢?我们后面说
多态的定义及实现
多态构成的条件:
1.必须通过父类的指针或引用去调用虚函数
2.子类重写父类的虚函数
虚函数:即函数名前面加上了virtual(和上节的虚继承并没有关系)
虚函数的重写
虚函数的重写:子类中有一个跟基类完全相同的虚函数(即子类虚函数和父类虚函数的返回值类型,函数名,参数列表完全相同),此时即为重写,代码可以看上面演示的代码
但是!!!
虚函数重写有两个例外:
1.协变(父子类虚函数返回值类型可以不同)返回值类型可以不同,但必须是父子类类型指针或引用
class A{};
class B : public A{};
class Person{
public:
virtual A* BuyTicket(){
cout << "正常排队-全价买票" << endl;
return new A;
}
};
class Student:public Person{
public:
virtural B* Buyticket(){
cout<< "正常排队-半价买票" <<endl;
return new B;
}
}
void Func(Person* a){
a->Buyticket();
}
int main(){
Person ps;
Student st;
Func(&ps);
Func(&st);
}
此时便会正常打印,这叫做协变
2.析构函数的重写
如果父类的析构函数为虚函数,此时子类的析构函数只要定义,无论是否加virtual关键字,都构成了与父类析构函数的重写,即使函数名字不同。但实际上,看似两者名字不同,但是可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名字统一处理成destructor。
class Person {
public:
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
~Student() { cout << "~Student()" << endl; }
};
// 不是虚函数,他们是隐藏关系
// 是虚函数,他们是重写关系
int main()
{
// 普通场景下面,虚函数是否重写都是ok的
Person p;
Student s;//子类析构函数先调用自己,在调用父类
// new对象特殊场景
Person* p1 = new Person;
Person* p2 = new Student;
delete p1; // -> p1->destructor() + operator delete(p1)
delete p2; //如果此时不构成多态,那么析构便是隐藏关系,只
//会调用父类的析构函数,而子类析构函数没调用,这样可能会造成内存泄露问题(隐藏关系,我们可以简单认为两个类析构函数同名)
//打印出~Person(),~Student(),~Person()
return 0;
}
最后,析构函数满足了上面构成虚函数的两个条件,即同名(destruct),和为虚函数(因为此时我们需要析构函数构成多态的行为,所以编译器把析构函数处理成同名))
我们子类虚函数可以不加virtual关键字,可以认为子类把父类的虚函数继承下来,但是父类必须加virtual
C++11 override和final
为了避免函数名字母次序写反而无法构成重载,此时编译器不会报错,所以11提供了两个关键字,可以帮助用户检测是否重写
final:修饰虚函数,表示该虚函数不能被重写,也可以修饰类,表示这个类不能被继承
override:检查派生类虚函数是否重现了基类某个虚函数,如果没有重写编译报错
都加在函数体前面
重载、覆盖(重写)、隐藏(重定义)的对比
名字 | 概念 |
---|---|
重载 | 1.两个函数都在同一个作用域 2.函数名相同,参数不同(个数,类型,顺序) |
重写 | 1.两个函数分别在父类和子类的作用域 2.函数名,参数,返回值都必须相同(协变例外) 3.两个都必须是虚函数 |
重定义 | 1.两个函数分别在父类和子类的作用域 2.函数名相同 3.此时两个同名函数不构成重写就是重定义 |
抽象类
在虚函数的后面写上=0,则这个函数为纯虚函数,包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象,同样派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象
class Person {
public:
virtual void fun() =0;//这便是纯虚函数
//不需要实现
};
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,子类继承的是父类虚函数的接口,目的是为了重写,达成多态
⭐多态的原理
虚函数表
我们可以对一个有虚函数的类进行sizeof,大家可以发现出什么呢?
希望大家可以自己去把以下代码里实验一下哦
class Base
{
public:
virtual void Func1(){
cout << "Func1()" << endl;
}
private:
int _b=1;
};
此时我们发现b对象是8字节,为什么呢?
除了_b成员,还多一个_vfptr放在对象的前面
当一个类有了虚函数以后,这个类的对象会增加4个字节在头上,_vfptr是一个指针,叫做虚函数表指针(虚表的地址),虚函数表里面存的是虚函数的地址(和我们之前学的虚基表指针要学会进行区分),表是一个函数指针数组,里面存的是函数指针
⭐注:虚函数被编译成指令后,还是跟普通函数一样存在代码段,只是他的地址放到了虚表
class Base{
public:
virtual void Func1(){
cout << "Func1()" << endl;
}
virtual void Func2(){
cout << "Func2()" << endl;
}
void Func3(){
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
char _ch = 'a';
};
class Derive : public Base{
public:
virtual void Func1(){
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main(){
// 1、父子类无论是否完成虚函数重写,都有各自的独立的虚表
// 2、一个类的所有对象,共享一张虚表
Base bb1;
//Base bb2;//同一个虚表,内容一样
Derive dd1;
}
上式代码我们可以看一下监视器中是怎么样的
我们可以仔细看上面监视器的图
图中我们可以看出,
父类和子类不是同一张虚表(子类父类不存在公用一个虚表的情况)
Func3不是虚函数,所以也没有进入虚表当中
子类没有对Func2进行重写,所以子类虚表中存的是父类Func2虚函数的地址
子类把父类的虚表拷贝过来,谁完成了重写,就把重写的那个位置覆盖成重写的虚函数
此时我们知道,只有子类的将虚函数完成重写,子类的虚表里面才会覆盖成子类的虚函数,那么是为什么呢?
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
int _a = 0;
string _b = "hello world";
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p){
p.BuyTicket();
}
void f(){
cout << "hello world" << endl;
}
int main(){
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
//Person p = Johnson;显然,这里p和Mike的虚表是同一张
return 0;
}
上面的代码,我们使用父类的指针或者引用来完成多态,很显然,我们通过父类指针去指向父类对象或者子类对象,指向子类对象调子类,指向父类对象调父类。使用Func函数的参数是父类引用,所以我们将父类或者子类对象传进去,便调用的是其本身的Buyticket函数。因为我们传过去的对象不同,所以调用的是各自虚表指针指向的虚表不同,调用了不同的函数
为什么必须是指针或者引用呢??
如果是对象话,父类会对子类对象进行切片,但不会把子类的虚表一同切过去,因为很显然,如果把子类虚表一同切过去,那么我们在进行赋值的时候,就无法确定此时我们使用的是父类虚表还是子类虚表,导致一个父类对象指向的是子类的虚函数,而且切片仅仅只会对子类的成员有效
只有指针或引用,才能既能指向父类对象或者子类对象,对子类切片后,子类的虚表仍然是子类的
通过上面的图片我们可以很清楚的看见_vfptr的相同与不同,父类对象和子类对象都共用自己的虚表
虚表的创建和虚表指针的创建
普通函数的调用是编译或链接时确定他的地址,多态的调用是运行时去确定地址->去指向对象的虚函数表中找到虚函数的地址,会造成一定的效率损失
⭐⭐虚表创建时机是在编译期间。编译期间编译器就为每个类确定好了对应的虚函数表里的内容,虚表指针_vptr跟着对象走,所以对象什么时候创建出来,vptr就什么时候创建出来,也就是运行的时候(更详细的说是在构造函数初始化列表时创建的)
自探虚表
我们在编写当中,我们创建父类虚函数和子类虚函数,但子类独立创建了一个虚函数,并没有进行重写,此时我们通过监视器无法查到子类独立创建的虚函数
class Base{
public:
virtual void Func1(){
cout << "Base::Func1()" << endl;
}
virtual void Func2(){
cout << "Base::Func2()" << endl;
}
void Func3(){
cout << "Base::Func3()" << endl;
}
};
class Derive : public Base{
public:
virtual void Func1(){
cout << "Derive::Func1()" << endl;
}
virtual void Func4(){//监视器中无法观察到func4
cout << "Derive::Func4()" << endl;
}
};
我们可以自行取虚表指针
// 打印虚表
//typedef 返回类型(*新类型)(参数表)
typedef void(*VFPTR)();//为了调用函数
void PrintVFT(void* vft[]){//上面的虚函数返回值都得时void
printf("%p\n", vft);
for (size_t i = 0; vft[i] != nullptr; ++i){//虚表内最后一个位置会存放nullptr,以此结束
printf("vft[%d]:%p->", i, vft[i]);
VFPTR f = (VFPTR)vft[i];//当打印对时才会调用对的函数
f();
}
printf("\n");
}
//拿虚表的地址!!
//...略
//Base b;
// PrintVFT((void**)(*((int*)&b)));先取类的地址,在用int*强转(即取头上四个字节,_vfptr),再解引用获得地址,但我们还需要从int类型强转成void**类型传给形参(即函数指针数组)
PS:我自行补充的,因为我也不是很好。
函数指针定义
函数返回值类型 (* 指针变量名) (函数参数列表);
动态绑定与静态绑定
在程序编译期间确定了程序的行为,也称静态多态,如:函数重载
在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态
多继承中的虚表
当进行多继承时,子类会有父类各自的虚表,两个父类就有两个虚表
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;
};
上面代码我们可以通过监视器发现子类有两个虚表,并且子类两个虚表中的func1的地址都不一样,多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中,直接的讲,子类的func3放在了子类关于Base1的虚表中
需要两张虚表是为了实现各自的多态,并不会造成浪费
总结与补充
一、虚函数编译出来函数指令跟普通函数一样,存在代码段(常量区)(类对象共用,不会存在栈上把虚表不断的创建销毁)
二、虚函数地址又被放在虚函数表中,所以构成了多态会造成效率降低
三、多态是为了让我们能够实现不同对象完成同一个任务时有不同的结果
四、内联函数可以是虚函数,因为编译器会自动忽略inline(如果不忽略,内联函数没有地址,是无法放入到虚表中的)
五、静态成员函数不能是虚函数,因为静态成员函数没有this指针,无法访问虚表
六、构造函数不可以是虚函数,因为对象的虚函数表指针是在构造函数初始化列表阶段才初始化的
注:结合我们上篇继承的内存地址,在存放虚基表地址的地方还会多存放一个地址,即虚表的地址
部分代码来自比特杭哥QAQ
感谢大家,各位感觉有收获的话可以点个赞嘛,十分感谢❤❤
前篇继承内容网址:本人前一篇继承:继承的详细介绍与理解,看了就懂