C++多态(中)

简介: C++多态(中)

下面程序输出什么?

#include <iostream>
using namespace std;
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;
}

首先,创建的是子类对象,子类对象去调用虚函数test(),然后里面是调用func(),这里要注意,是一个多态调用,因为test成员函数是属于A类的,调用func函数是通过this指针去调用(就算是test函数被子类继承了,内部的this指针也不会被更换,还是A类的this指针),并且func函数也进行了重写,在main函数中调用的也是子类对象,所以走向的是B类中的func函数。

这里最让我们疑惑的就是为什么是1不是0,这里就涉及到了只继承接口,所以val的缺省值还是1。

但是子类的缺省参数并不是一点用处都没有,当普通调用的时候这个缺省参数就可以使用了。

再看一个程序:选哪个?

#include <iostream>
using namespace std;
class Base1 
{ 
public:
  int _b1; 
};
class Base2 
{
public:
  int _b2; 
};
class Derive : public Base1, public Base2 
{
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

数据模型大概是这样的:

所以选C。

这里注意一下:其实继承的对象在内存里是从下面开始放,因为下面是低地址,上面是是高地址,我们经常能看到一个数组,用数组名+n就能到对应的位置,这就是为什么从低地址放的原因,加就代表要到高地址。

多态原理

虚函数表

先来研究一下这个类的大小:32位环境下

#include<iostream>
using namespace std;
class Base
{
public:
  virtual void Func1()
  {
    cout << "Func1()" << endl;
  }
private:
  int _b = 1;
};
int main()
{
  cout << sizeof(Base) << endl;
  return 0;
}

这里明明只有一个成员变量,之前说过成员函数并不在类中,可是为什么结果是8呢?

这里多出来了一个_vfptr,这个叫做虚表/虚函数表,里面储存的是虚函数的地址。

#include<iostream>
using namespace std;
class Base
{
public:
  virtual void Func1()
  {
    cout << "Func1()" << endl;
  }
  virtual void Func2()
  {
    cout << "Func2()" << endl;
  }
  void Func3()
  {
    cout << "Func3()" << endl;
  }
private:
  int _b = 1;
};
int main()
{
  cout << sizeof(Base) << endl;
  Base a;
  return 0;
}

原理与动静态绑定

多态的原理一定跟虚表有着千丝万缕的联系。

再来看看完成重写有什么区别;

#include<iostream>
using namespace std;
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;
};
class Derive : public Base
{
public:
  virtual void Func1()
  {
    cout << "Derive::Func1()" << endl;
  }
private:
  int _d = 2;
};
int main()
{
  Base b;
  Derive d;
  return 0;
}

这里虚表也变了,之前重写也可以叫做覆盖,这里就是覆盖的部分。

其实重写只是语法上的,继承了父类的接口,重写了实现部分。覆盖就是覆盖了父类继承过来重写的虚函数的地址。

那么我们这样调用试一下:

多态调用更长。

这里差别就在于,根本不在乎是指向哪里,因为有虚表的存在,如果指向父类就去父类的虚表中找,如果指向子类就去子类的虚表中找。

在汇编当中eax里面存的就是虚表指针数组。

1.静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。

2.动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

那么虚表是放在哪一个位置呢?

打印出来的地址和常量区非常接近,所以是在常量区。

单继承与多继承关系的虚函数列表

单继承的虚函数表

#include <iostream>
using namespace std;
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; }
  virtual void func4() { cout << "Derive::func4" << endl; }
private:
  int b;
};
int main()
{
  Base b;
  Derive d;
  return 0;
}

在VS当中其实并不能看到虚表当中所有的虚函数,这时VS编译器的一个优化,也可以看作是一个BUG。

这个时候我们可以用内存窗口去看。

这里也将func3和func4的函数地址给显示出来,顺便说一下,在VS编译器下,虚表是以空指针结尾的。

但是这样看有些麻烦,我们想个办法给他打印出来。

#include <iostream>
using namespace std;
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; }
  virtual void func4() { cout << "Derive::func4" << endl; }
private:
  int b;
};
typedef void(*p)();
void PrintVFTbale(p vft[])//打印虚表
{
  for (int i = 0; vft[i]; i++)
  {
    printf("[%d]:%p->", i, vft[i]);//打印虚表当中每个数组的内容,也就是每个虚函数的地址
    vft[i]();//调用对应的函数
  }
}
int main()
{
  Base b;
  Derive d;
  PrintVFTbale((p*)(*(int*)&b));//将虚表的地址传过去
  PrintVFTbale((p*)(*(int*)&d));
  return 0;
}

这里还可以改进,因为有时候是64位和32位,到时候64位就是取头8个字节了。

其实只需要将里面的变成二级指针就行了(任何类型的二级指针都可以),因为二级指针是储存一级指针的,解引用之后再去看解引用多大时,剩下的就是一级指针,一级指针就可以根据平台位数变化了,到时候就对应了64位和32位的平台大小了。

目录
打赏
0
0
0
0
21
分享
相关文章
c++--多态
上一篇文章已经介绍了c++的继承,那么这篇文章将会介绍多态。看完多态的概念,你一定会感觉脑子雾蒙蒙的,那么我们先以举一个例子,来给这朦胧大致勾勒出一个画面,在此之前,先介绍一个名词虚函数,(要注意与虚拟继承区分)重定义: 重定义(隐藏)只要求函数名相同(但要符合重载的要求,其实两者实际上就是重载);重定义下:在这种情况下,如果通过父类指针或引用调用函数,会调用父类的函数而不是子类。重定义(或称为隐藏)发生的原因是因为函数名相同但参数列表不同,导致编译器无法确定调用哪一个版本的函数。
49 0
【c++】多态(多态的概念及实现、虚函数重写、纯虚函数和抽象类、虚函数表、多态的实现过程)
本文介绍了面向对象编程中的多态特性,涵盖其概念、实现条件及原理。多态指“一个接口,多种实现”,通过基类指针或引用来调用不同派生类的重写虚函数,实现运行时多态。文中详细解释了虚函数、虚函数表(vtable)、纯虚函数与抽象类的概念,并通过代码示例展示了多态的具体应用。此外,还讨论了动态绑定和静态绑定的区别,帮助读者深入理解多态机制。最后总结了多态在编程中的重要性和应用场景。 文章结构清晰,从基础到深入,适合初学者和有一定基础的开发者学习。如果你觉得内容有帮助,请点赞支持。 ❤❤❤
508 0
【C++】多态
多态是面向对象编程中的重要特性,允许通过基类引用调用派生类的具体方法,实现代码的灵活性和扩展性。其核心机制包括虚函数、动态绑定及继承。通过声明虚函数并让派生类重写这些函数,可以在运行时决定具体调用哪个版本的方法。此外,多态还涉及虚函数表(vtable)的使用,其中存储了虚函数的指针,确保调用正确的实现。为了防止资源泄露,基类的析构函数应声明为虚函数。多态的底层实现涉及对象内部的虚函数表指针,指向特定于类的虚函数表,支持动态方法解析。
86 1
|
9月前
|
C++入门12——详解多态1
C++入门12——详解多态1
110 2
C++入门12——详解多态1
|
9月前
|
C++
C++入门13——详解多态2
C++入门13——详解多态2
146 1
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
AI助理
登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问

你好,我是AI助理

可以解答问题、推荐解决方案等