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位的平台大小了。

相关文章
|
1月前
|
编译器 C++
C++入门12——详解多态1
C++入门12——详解多态1
38 2
C++入门12——详解多态1
|
6月前
|
C++
C++中的封装、继承与多态:深入理解与应用
C++中的封装、继承与多态:深入理解与应用
150 1
|
1月前
|
C++
C++入门13——详解多态2
C++入门13——详解多态2
79 1
|
3月前
|
存储 编译器 C++
|
4月前
|
存储 编译器 C++
【C++】深度解剖多态(下)
【C++】深度解剖多态(下)
54 1
【C++】深度解剖多态(下)
|
4月前
|
存储 编译器 C++
|
3月前
|
存储 编译器 C++
C++多态实现的原理:深入探索与实战应用
【8月更文挑战第21天】在C++的浩瀚宇宙中,多态性(Polymorphism)无疑是一颗璀璨的星辰,它赋予了程序高度的灵活性和可扩展性。多态允许我们通过基类指针或引用来调用派生类的成员函数,而具体调用哪个函数则取决于指针或引用所指向的对象的实际类型。本文将深入探讨C++多态实现的原理,并结合工作学习中的实际案例,分享其技术干货。
75 0
|
4月前
|
机器学习/深度学习 算法 C++
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
|
4月前
|
Java 编译器 C++
【C++】深度解剖多态(上)
【C++】深度解剖多态(上)
54 2
|
4月前
|
程序员 C++
【C++】揭开C++多态的神秘面纱
【C++】揭开C++多态的神秘面纱