【C++】非常重要的——多态
创始人
2025-05-28 10:00:00
0

         凡是面向对象的语言,都有三大特性,继承,封装和多态,但并不是只有这三个特性,是因为者三个特性是最重要的特性,那今天我们一起来看多态!


目录

1.多态的概念

1.1虚函数

1.2虚函数的重写

1.3虚函数重写的两个例外

1.子类的虚函数可以不加virtual

2. 协变(基类与派生类虚函数返回值类型不同)

1.4如何实现一个不能被继承的类

2. 多态的定义及实现

2.1多态调用

2.2普通调用:

2.3析构函数建议加virtual吗? 

2.4抽象类

2.5接口继承和实现继承

3.多态的实现原理

3.1.虚表(虚函数表)

3.2多态的实现

3.3静态多态和动态多态

3.4单继承的多态实现

3.5多继承的多态实现

4.一些常考的多态的问题

总结:


1.多态的概念

多态,就是不同对象去完成某一种行为时,产生的不同状态。

举例说明:日常生活中,我们去买票,尤其买火车票时,总会有不同的结果。当成人买的时候,就是原价,学生就是半价,军人就是优先买票,这都体现了多态。不同对象完成某一行为,产生不同状态。

那实现多态前,我们首先得清楚一些概念:

1.1虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数。
class A
{
public:virtual void func(){cout << "A" << endl;}
};

1.2虚函数的重写

我们之前在继承的时候,学习过重定义(隐藏),现在在多态这一节,又出现了重写,那到底是什么呢?我们来看一看

重定义(隐藏):首先在两个不同的类中(父类,子类)(构成继承),只要函数名相同,就会构成重定义(隐藏)。

重写(覆盖):在重定义的基础上,除了函数名要相同,还有返回值,参数都得相同,这才构成重写。

举个例子:

class A
{
public:virtual void func(){cout << "A" << endl;}
};
class B:public A
{
public:virtual void func()  //重写{cout << "B" << endl;}
};

总结就是:虚函数的重写条件:子类和父类都是虚函数,且函数名,返回值,参数都必须相同(三同),这才能构成虚函数的重写。

1.3虚函数重写的两个例外

1.子类的虚函数可以不加virtual

因为是继承关系,父类的虚函数也被继承下来,所以子类的可以不加virtual。(建议还是都写上)

class A
{
public:virtual void func(){cout << "A" << endl;}
};
class B:public A
{
public:void func(){cout << "B" << endl;}
};

2. 协变(基类与派生类虚函数返回值类型不同)

意思是:三同中,返回值可以不同,但要求返回值必须是父子类关系的指针或引用。(其他父子类关系的指针或者引用也可以)

class person
{
public:virtual person* func()  //本父子类指针或引用{return this;}
};class student:public person
{
public:virtual student* func() //本父子类指针或引用{return this;}
};

其他父子类关系的指针或者引用也可以:

class A
{};
class B:public A
{};class person
{
public:virtual A* func(){return nullptr;}
};class student:public person
{
public:virtual A* func(){return nullptr;}
};

但是父类的返回值不可以为子类的指针。

1.4如何实现一个不能被继承的类

方法一:是需要把它的构造函数写为私有即可,无法构造,就不可能被继承;

方法二:类定义时,加final(c++11),最终类,不能被继承


class A final
{};
class B:public A
{};

若final给虚函数,虚函数则不能被重写

class A
{virtual void func()final{}
};
class B:public A
{virtual void func(){}
};

override是来判断是否已经重写(检查重写)

class A
{virtual void func(int){}
};
class B:public A
{virtual void func()override{}
};

2. 多态的定义及实现

首先多态实现的前提必须是继承!

多态实现的两个条件:

1.必须使用父类(基类)的指针或者引用调用虚函数;

2.被调用的函数必须是虚函数,且子类(派生类)必须对虚函数进行重写;

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了 Person。Person对象买票全价,Student对象买票半价。

2.1多态调用

class Person
{
public:virtual void Buyticket(){cout << "Person:全价" << endl;}
};class Student:public Person
{
public:virtual void Buyticket(){cout << "Student:半价" << endl;}
};void func(Person& p) //切片
{p.Buyticket();
}int main()
{Person p;Student s;func(p);func(s);
}

2.2普通调用:

不符合多态条件即可:

void func(Person p)//不是指针或者引用,就是对象
{p.Buyticket();
}int main()
{Person p;Student s;func(p);func(s);
}

 

那么我们可以发现:

普通调用跟调用对象的类型有关;

多态调用必须是父类的指针或者引用,无论是是哪个对象传,他都会指向该对象中父类的那一部分(切片),进而调用该对象中的虚函数。一句话,多态调用跟 指针/引用 指向的对象有关

2.3析构函数建议加virtual吗? 

我们看一个例子:

class Person
{
public:~Person(){cout << "Person delete" << endl;delete _p;}
protected:int* _p = new int[10];
};class Student :public Person
{
public:~Student(){cout << "Student delete" << endl;delete _s;}
protected:int* _s = new int[20];
};int main()
{//Person p;//Student s;Person* ptr1 = new Person;Person* ptr2 = new Student;delete ptr1;delete ptr2;
}

我们都知道,析构函数自动调用,在继承中,子类会先析构,调用子类的析构函数以后,自动再调用父类的析构函数。

但这用情况还适用吗?

先看一下结果:

我们发现,居然调用了两次父类的析构函数 !!!

这种情况就会造成子类对象中的成员变量没有释放,导致内存泄露!!

我们知道:

delete有两种行为:1.使用指针调用析构函数;2.operator delete(ptr)

所以使用指针调用析构函数是普通调用(不满足多态调用的条件),普通调用是跟调用的对象类型有关,类型都是Person,所以只会调用person的析构函数

但此时我们更希望的是多态调用,所以建议加virtual,指针指向的对象是哪个,就调用哪个的析构函数。但此时我们会想,析构函数名字都不一样,这能构成重写吗?当然可以,那是因为编译器会自动把父类子类的析构函数名字换成一样的:ptr->destructor()。

那么就可以实现我们预期的效果:

所以我们建议:再写析构函数时,可以无脑给父类的析构函数加virtual,防止出现上面的情况,导致内存泄露 。

普通调用时,时普通调用;父类的指针或者引用调用时,时多态调用,互不影响! 

2.4抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生 类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class Car
{
public:virtual void Drive() = 0;
};class BMW :public Car
{
public:virtual void Drive(){cout << "BMW-操控" << endl;}
};
void Test()
{Car* pBMW = new BMW;pBMW->Drive();
}int main()
{Test();
}

总结:有些类不需要类的对象,可以在写成纯虚函数。

2.5接口继承和实现继承

接口继承针对虚函数;实现继承针对普通函数。

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实 现。 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成 多态,继承的是接口。 学了这么多,来做一道题温习一下:(很坑)

class A
{
public:virtual void func(int val = 1){ std::cout << "A->" << val << std::endl; }virtual void test(){ func(); }
};class B : public A
{
public:void func(int val = 0){ std::cout << "B->" << val << std::endl; 
};int main(int argc, char* argv[])
{B*p = new B;p->test();  return 0;
}A: A->0    B: B->1    C: A->1    D: B->0    E: 编译出错      F: 以上都不正确若是ptr->func(),就是B类对象直接调用,就是普通调用,普通调用跟对象类型有关。
普通调用在编译时就会静态绑定,在编译时调用的函数以及函数的默认值就已经确定,子类调用子类自己的函数,跟父类没有任何关系,函数都是子类编译时就已经静态绑定的,所以缺省值依然是0。最终结果是B->0

答案选哪个??

首先我们了解的第一点是,继承父类的成员,会原封不动的继承到子类;

我们接下来看:创建了一个B对象的指针,指针来调用p->test(),这时候,会直接调用父类中的test,再this->func(),此时的this的类型是A*,因为test处于A类中,继承到B中,也会原封不动的继承过去,this依然是A*,所以父类的指针调用虚函数,满足多态的调用,多态调用是看指针指向的对象,又因为p调用的test,所以指针指向B对象,所以会调用B的重写的func虚函数,所以最终答案是B->1.(其实多态调用一直是调的父类的接口,再根据指向的对象去调用具体的实现,后面会详细讲到)

当B对象自己调用函数func时,当不是多态调用时,就会直接调用自己的func(),缺省值还是自己的val=0.


3.多态的实现原理

3.1.虚表(虚函数表)

来先看一道题:

class Base1
{
public:virtual void Func1(){cout << "Func1()" << endl;}};class Base2
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};
sizeof(Base1),sizeof(Base2),它所占的字节数是多少?

通过之前学习的内容,我们可以了解到,如果类中没有成员变量,只有成员函数,会留一个字节进行占位,因为成员函数在代码段,所以Base1的大小是1吗?

原来不是我们想象的那样子,是事实上,来看:

凡是有虚函数的,都会有一个虚函数表指针来存虚函数,简称虚表指针,存虚函数的表叫做虚函数表,简称虚表。

VFptr(全程vftable)是一个指针, 指向虚表,虚表中存的是虚函数的地址。

所以我们知道,原来只要有虚函数,就会有虚表指针,所以Base1的字节大小是,4字节;

Base2的字节大小是,加上内存对齐,_b占四字节,vtf占四字节,8字节。

对于同一类实例化出的不同的对象,他们的虚表是公用的:

class A
{public:virtual void func(){}
}int main()
{A b;A c;
}

我们了解虚表和虚表指针以后,那么多态到底如何实现呢?

3.2多态的实现

来看一段代码:

class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}private:int _b = 1;char _ch;
};class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}void Func3(){cout << "Derive::Func3()" << endl;}
private:int _d = 2;
};int main()
{Base b;Derive d;//普通调用Base* ptr = &b;ptr->Func3();ptr = &d;ptr->Func3();
//多态调用ptr = &b;ptr->Func1();ptr = &d;ptr->Func1();
}

多态调用:

ptr是父类的指针,无论指向哪个对象都只能看到该对象父类的部分(切片),那么多态调用怎么调用呢?通过虚表指针来调用虚函数,完成重写的虚函数会在虚表对应的位置进行覆盖,变成重写后的虚函数,进而调用。(一句话,我也不知道我调用谁,我指向谁,就调用谁的虚函数,进而完成动态绑定,完成多态调用)

 静态绑定:编译时,通过类型就确定调用函数的地址,然后直接call完成调用

通过反汇编可以看到:

静态绑定,一步完成;动态绑定得很多步完成。 

3.3静态多态和动态多态

1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为也称为静态多态, 比如:函数重载 2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数,也称为动态多态

总结:多态调用就是依靠虚表实现,指向谁,就调用谁的虚函数

虚表是存在代码段中的。

3.4单继承的多态实现

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};class Derive :public Base {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }void func4() { cout << "Derive::func4" << endl; }
private:int b;
};int main()
{Base b;Derive d;}

我们知道Base对象中的虚表有func1和func2,子类对虚函数进行重写,func1重写,func2不变:

  

那么子类自己的虚函数func3在不在虚表里面呢?

为了更方便观察,我们可以实现一个打印虚表的函数:

typedef void(*VFTptr)();  //函数指针,重命名必须写到里面
void Print(VFTptr VFT[])  //函数指针数组
{int i = 0;while (VFT[i])  //虚表中,vs默认以空结束。{printf("[%d]%p->", i, VFT[i]);VFT[i]();i++;}cout << endl;
}
int main()
{Base b;Derive d;Print((VFTptr*)(*(int*)&b)); //先取地址,再强转VFTptr的地址,然后解引用取到地址,再强转为VFT*类型,进而传参调用Print((VFTptr*)(*(void**)&d));//换为void**原因是因为,机器若是32位,指针大小就是4字节,若是64位,就是8字节//所以换为void**更普适,先取地址,再强转void**,void*解引用,那么这就根据机器的位数来决定指针的大小了
}

我们可以发现,虚函数func3也会存在虚表中。

 3.5多继承的多态实现

typedef void(*VFTptr)();
void Print(VFTptr VFT[])
{int i = 0;while (VFT[i]){printf("[%d]%p->", i, VFT[i]);VFT[i]();i++;}cout << endl;
}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;
};int main()
{Base1 b1;Base2 b2;Print((VFTptr*)(*(void**)&b1));Print((VFTptr*)(*(void**)&b2));Derive d;Print((VFTptr*)(*(void**)&d));//Base1的虚表Print((VFTptr*)(*(void**)((char*)&d + sizeof(Base1))));//Base2的虚表//Base2* ptr=&d;//Print((VFTptr*)(*(void**)ptr)); //也可以这样找到虚表指针}

 我们知道多继承下多态的实现,子类继承多个父类,只有当父类有虚函数,多继承时才有虚表。

当子类也有虚函数时,这时子类的虚函数放到第一个继承的父类的虚表中,我们可以从上面代码结果看出。


再来练习题目:

下列输出的结果是什么?

class A{
public:A(char *s) { cout << s << endl; }~A(){}
};class B :public A
{
public:B(char *s1, char*s2) :A(s1) { cout << s2 << endl; }
};class C : public A
{
public:C(char *s1, char*s2) :A(s1) { cout << s2 << endl; }
};class D :public C, public B
{
public:D(char *s1, char *s2, char *s3, char *s4) : B(s1, s2), C(s1, s3),A(s1){cout << s4 << endl;}
};int main() {D *p = new D("class A", "class B", "class C", "class D");delete p;return 0;
}
A:class A class B class C class D 
B:class D class B class C class A
C:class D class C class B class A
D:class A class C class B class D

答案是哪个呢?

首先D肯定是最后一个才被初始化的,构造函数先走初始化列表,B,C,A,那肯定是A先被初始化,因为B,C中都有A,A不初始化,B,C没办法初始化;其次要看继承的顺序,D先继承C,再继承B,所以先初始化C,再初始化B.最终答案就是D

第二题:

class Base1 {public:  int _b1;};
class Base2 { public:  int _b2; };
class Derive : public Base2, public Base1 
{ public: int _d; };int main(){Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}
A:p1 == p2 == p3              B:p1 > p2 == p3         C:p1 == p3 != p2        D:p1 != p2 != p3

答案选哪个呢?

子类首先继承了Base2,再继承了Base1,所以模型应该是这样的:

所以没有答案,答案应该是:p3==p2

所以通过上面这两个例子,我们可以看的出,其实实现继承时,继承的顺序是非常重要的,有关谁先被创建。


4.一些常考的多态的问题

1. 什么是多态? 多态分为静态多态和动态多态; 静态多态是在编译时,自动和所调用的类型所绑定,从而确定函数地址,直接调用 动态多态是在运行时,根据父类指针所指向的对象,指向父类调父类的虚函数,指向子类调子类中父类那部分重写以后的虚函数。 2. 什么是重载、重写(覆盖)、重定义(隐藏)? 重载:同一作用域,只有函数名相同,参数不同的函数 重定义(隐藏):在两个不同的类中(两个不同的作用域),只要函数名相同就构成了重定义 重写:在构成重定义(隐藏)的基础上,函数得是虚函数,且函数名,参数,返回值必须相同 3. 多态的实现原理? 简而言之:虚表的重要性,离不开虚表,和虚函数的重写;指向谁就调用谁 4. inline函数可以是虚函数吗? 可以(语法角度上看),不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。如果inline函数不是虚函数,就还会有inline这个属性 5. 静态成员可以是虚函数吗? 不能,因为静态成员函数没有this指针,静态成员函数在类没有实例化对象之前就已经分配空间了,不用实例化对象也可以调用,但是对于virtual虚函数,它的调用恰恰使用this指针。在有虚函数的类实例中,this指针调用vptr指针,指向的是vtable(虚函数列表),通过虚函数列表找到需要调用的虚函数的地址。总体来说虚函数的调用关系是:this指针->vptr(4字节)->vtable ->virtual虚函数。所以说,static静态函数没有this指针,也就无法找到虚函数了。所以静态成员函数不能是虚函数。他们的关键区别就是this指针。 6. 构造函数可以是虚函数吗? 不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的,你连虚表指针都没有,还怎么调用构造函数?? 7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数? 可以,并且最好把基类的析构函数定义成虚函数。防止多态调用析构函数时,重复调用一个对象的虚函数,发生内存泄漏。 8. 对象访问普通函数快还是虚函数更快? 我们得分具体情况: 普通调用时:当然是普通函数和虚函数都是一样的; 多态调用时:当然普通函数更快,虚函数的调用会先去找虚表指针,找到虚表,再去调用虚函数 9. 虚函数表是在什么阶段生成的,存在哪的? 虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。 10. C++菱形继承的问题?虚继承的原理? 注意这里不要把虚函数表和虚基表搞混了: 菱形继承为了避免数据冗余,会用虚基表来解决,虚基表是用来存偏移量,进而通过偏移量来找到虚基类; 虚继承是虚函数的重写,通过虚表指针找到虚表,进而调用虚表中的虚函数 11. 什么是抽象类?抽象类的作用? 抽象类是在虚函数后面写上=0,它强制必须重写子类的虚函数,不写就不可以实例化出对象,另外抽象类体现出了接口继承关系。

总结:

这一节,我们完完整整把多态的全部内容都讲了一遍,当然途中大家肯有会有不懂的地方,因为这是难点,我在编写这边文章的时候,也是反反复复思考和学习,所以大家需要反复思考观看,不懂得可以在评论区回复,或者私信我哦!​​​​​​​

大家加油!!! 

相关内容

热门资讯

AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWR报告解读 WORKLOAD REPOSITORY PDB report (PDB snapshots) AW...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
群晖外网访问终极解决方法:IP... 写在前面的话 受够了群晖的quickconnet的小水管了,急需一个新的解决方法&#x...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
Azure构建流程(Power... 这可能是由于配置错误导致的问题。请检查构建流程任务中的“发布构建制品”步骤,确保正确配置了“Arti...