【C++】非常重要的——多态(三)

简介: 【C++】非常重要的——多态


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<p1.


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


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,它强制必须重写子类的虚函数,不写就不可以实例化出对象,另外抽象类体现出了接口继承关系。

总结:

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

目录
相关文章
|
5月前
|
C++
C++中的封装、继承与多态:深入理解与应用
C++中的封装、继承与多态:深入理解与应用
123 1
|
2月前
|
存储 编译器 C++
|
3月前
|
存储 编译器 C++
【C++】深度解剖多态(下)
【C++】深度解剖多态(下)
48 1
【C++】深度解剖多态(下)
|
3月前
|
存储 编译器 C++
|
2月前
|
存储 编译器 C++
C++多态实现的原理:深入探索与实战应用
【8月更文挑战第21天】在C++的浩瀚宇宙中,多态性(Polymorphism)无疑是一颗璀璨的星辰,它赋予了程序高度的灵活性和可扩展性。多态允许我们通过基类指针或引用来调用派生类的成员函数,而具体调用哪个函数则取决于指针或引用所指向的对象的实际类型。本文将深入探讨C++多态实现的原理,并结合工作学习中的实际案例,分享其技术干货。
55 0
|
3月前
|
机器学习/深度学习 算法 C++
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
|
3月前
|
Java 编译器 C++
【C++】深度解剖多态(上)
【C++】深度解剖多态(上)
46 2
|
3月前
|
程序员 C++
【C++】揭开C++多态的神秘面纱
【C++】揭开C++多态的神秘面纱
|
3月前
|
机器学习/深度学习 PyTorch 算法框架/工具
C++多态崩溃问题之在PyTorch中,如何定义一个简单的线性回归模型
C++多态崩溃问题之在PyTorch中,如何定义一个简单的线性回归模型
|
4月前
|
C++
C++一分钟之-继承与多态概念
【6月更文挑战第21天】**C++的继承与多态概述:** - 继承允许类从基类复用代码,增强代码结构和重用性。 - 多态通过虚函数实现,使不同类对象能以同一类型处理。 - 关键点包括访问权限、构造/析构、菱形问题、虚函数与动态绑定。 - 示例代码展示如何创建派生类和调用虚函数。 - 注意构造函数初始化、空指针检查和避免切片问题。 - 应用这些概念能提升程序设计和维护效率。
34 2
下一篇
无影云桌面