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++多态实现计算器
C++多态实现计算器
|
2月前
|
C++
9. C++虚函数与多态
9. C++虚函数与多态
32 0
|
2月前
|
存储 编译器 C++
C++:多态究竟是什么?为何能成为面向对象的重要手段之一?
C++:多态究竟是什么?为何能成为面向对象的重要手段之一?
51 0
|
2月前
|
存储 安全 算法
【C/C++ 数据发送结构设计】C++中的高效数据发送:多态、类型擦除与更多解决方案
【C/C++ 数据发送结构设计】C++中的高效数据发送:多态、类型擦除与更多解决方案
79 0
|
2月前
|
算法 Java 编译器
【C++ 关键字 virtual 】C++ virtual 关键字(将成员函数声明为虚函数实现多态
【C++ 关键字 virtual 】C++ virtual 关键字(将成员函数声明为虚函数实现多态
27 0
|
11天前
|
编译器 C++
c++的学习之路:22、多态(1)
c++的学习之路:22、多态(1)
21 0
c++的学习之路:22、多态(1)
|
2月前
|
存储 编译器 C++
【C++练级之路】【Lv.13】多态(你真的了解虚函数和虚函数表吗?)
【C++练级之路】【Lv.13】多态(你真的了解虚函数和虚函数表吗?)
|
4天前
|
C++
深入理解 C++ 中的多态与文件操作
C++中的多态是OOP核心概念,通过继承和虚函数实现。虚函数允许对象在相同操作下表现不同行为,提高代码可重用性、灵活性和可维护性。例如,基类`Animal`声明`makeSound()`虚函数,派生类如`Cat`、`Dog`和`Bird`可重写该函数实现各自叫声。C++也提供多种文件操作,如`fstream`库的`ofstream`、`ifstream`用于读写文件,C++17引入的`&lt;filesystem&gt;`库提供更现代的文件操作接口。
11 0
|
10天前
|
存储 C++
【C++进阶(九)】C++多态深度剖析
【C++进阶(九)】C++多态深度剖析
|
11天前
|
编译器 C++
c++的学习之路:23、多态(2)
c++的学习之路:23、多态(2)
21 0