文章目录
1.多态实现的底层
1.1初识多态原理
cpp
class Dad
{
public:
virtual void Cook()
{
cout << "佛跳墙" << endl;
}
virtual void Work()
{
cout << "Work" << endl;
}
int _a = 0;
};
class Son : public Dad
{
public:
virtual void Cook()
{
cout << "方便面" << endl;
}
int _b = 0;
};
void Test(Dad& p)
{
p.Cook();
}
int main()
{
Dad dad;
Test(dad);
Son son;
Test(son);
return 0;
}
1.2深入理解虚函数表
1.单继承虚函数表
同类型对象共用一个虚表
若子类不重写 父类虚表指向父类的虚函数 子类虚表也指向父类的虚函数但是vs下 不管是否重写 子类跟父类虚表都不是同一个
这样实现的理由:即便子类没有重写 但是子类有自己的虚函数时 单独创建一个虚表和父类分隔开 更有条理
子类虚函数表存储:重写的父类虚函数 没有重写的父类虚函数 自己的虚函数
2.探究虚函数表存储数据
cpp
class Dad
{
public:
virtual void BuyCar()
{
cout << "Dad::买车-宾利" << endl;
}
virtual void Func1()
{
cout << "Dad::Func1()" << endl;
}
};
class Son : public Dad
{
public:
virtual void BuyCar()
{
cout << "Son::买车-奔驰" << endl;
}
virtual void Func2()
{
cout << "Son::Func2()" << endl;
}
};
typedef void(*vftptr)();
void PrintVftable(vftptr* pt) //void PrintVftable(vftptr pt[])
{
for (size_t i = 0; *(pt + i) != nullptr; ++i)
{
printf("vft[%d]:%p->", i, pt[i]);
//1.直接访问
pt[i]();
//2.间接访问
//vftptr pf = pt[i];
//pf();
}
cout << endl;
}
int main()
{
Dad p1;
Dad p2;
Son s1;
Son s2;
//打印子类虚表
PrintVftable((vftptr*)*(int*)&s1);
PrintVftable((*(vftptr**)&s1));//解释见下
//打印父类虚表
PrintVftable((vftptr*)*(int*)&p1);
PrintVftable((*(vftptr**)&p1));//解释见下
return 0;
}
3.知识点金
- 虚表在编译阶段生成。
- 类实例化的对象中的虚表指针在构造函数的初始化列表初始化。
- 虚表存在于代码段。
cpp
int x = 0;
static int y = 0;
int* z = new int;
const char* p = "xxxxxxxxxxxxxxxxxx";
printf("栈对象:%p\n", &x);
printf("堆对象:%p\n", z);
printf("静态区对象:%p\n", &y);
printf("常量区对象:%p\n", p);
printf("s对象虚表:%p\n", *((int*)&s));
printf("d对象虚表:%p\n", *((int*)&d1));
4.多继承虚函数表
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <list>
#include <vector>
#include <algorithm>
#include <array>
#include <time.h>
#include <queue>
using namespace std;
class Dad1
{
public:
virtual void func1()
{
cout << "Dad1::func1" << endl;
}
virtual void func2()
{
cout << "Dad1::func2" << endl;
}
private:
int a1 = 1;
};
class Dad2
{
public:
virtual void func1()
{
cout << "Dad2::func1" << endl;
}
virtual void func2()
{
cout << "Dad2::func2" << endl;
}
private:
int a2 = 2;
};
class Son : public Dad1, public Dad2
{
public:
virtual void func1()
{
cout << "Son::func1" << endl;
}
virtual void func3()
{
cout << "Son::func3" << endl;
}
private:
int aa = 3;
};
typedef void(*vftptr)();
void PrintVftable(vftptr* pt) //void PrintVftable(vftptr pt[])
{
for (size_t i = 0; *(pt + i) != nullptr; ++i)
{
printf("vft[%d]:%p->", i, pt[i]);
//1.直接访问
pt[i]();
//2.间接访问
//vftptr pf = pt[i];
//pf();
}
cout << endl;
}
int main()
{
Dad1 d1;
Dad2 d2;
Son s;
cout << "d1所占字节数为" << sizeof(d1) << endl;//8
cout << "d2所占字节数为" << sizeof(d2) << endl;//8
cout << "s所占字节数为" << sizeof(s) << endl;//20
//显示虚表Ⅰ
PrintVftable((vftptr*)(*(int*)&s)); //int只能访问4个字节 在64位下不再适用1
//PrintVftable((*(vftptr**)&s)); 高级写法
//显示虚表Ⅱ法一:
PrintVftable((vftptr*)(*(int*)((char*)&s+sizeof(Dad1))));
//PrintVftable((*(vftptr**)((char*)&s + sizeof(Dad1))));高级写法
//显示虚表tⅡ法二:
//Dad2* ptr = &s;
//PrintVftable((vftptr*)(*(int*)(ptr)));
//PrintVftable((*(vftptr**)ptr)); 高级写法
cout << "单独调用Son中的func1->" ;
printf("%p\n", &Son::func1); //成员函数需要加&才能取到地址 普通函数名就可作为地址
//普通调用
s.func1();
//多态调用
Dad1* ptr1 = &s;
ptr1->func1();
Dad2* ptr2 = &s;
ptr2->func1();
return 0;
}
问题:这三次调用的func1是不是同一个函数?答案是肯定的,从运行结果最三行可以看出。但是为什么这三次调用的地址都不一样???答案见下
图中可以看出 调用ptr2时执行了为什么呢?答案见下。
以上汇编代码仅供参考。解读:调用ptr2时,先执行了目的是使得此时的this指针能够指向s对象的首地址。为什么ptr1调用时没有此动作?因为ptr1调用时,this指针指向Dad1部分,恰好就是s对象的首地址。而ptr2调用时,是s中间的某个位置。需要修正this指针到s对象的首地址。
所以有。也就解释了为什么从监视窗口看到两个func1函数地址不同。实际上是同一个函数,只不过其中一个要到另一个地方,做一些特定的事情。
2.题目讲解
- 什么是多态?
- 什么是重载、重写(覆盖)、重定义(隐藏)?
- 多态的实现原理?
- inline函数可以是虚函数吗?
- 静态成员可以是虚函数吗?
- 构造函数可以是虚函数吗?
- 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
- 对象访问普通函数快还是虚函数更快?
- 虚函数表是在什么阶段生成的,存在哪的?
- C++菱形继承的问题?虚继承的原理?
- 什么是抽象类?抽象类的作用?
答案:
-
多态是指同一种行为(方法)在不同对象上产生不同的结果。在面向对象编程中,多态是通过继承和重写(覆盖)实现的。子类可以重写父类的方法,从而产生不同的行为。
-
重载(Overload)是指在同一个作用域内,使用相同的函数名,但参数类型或个数不同的多个函数。
重写(Override/覆盖)是指在派生类中重新定义(覆盖)基类中定义的虚函数,使其能够根据具体的派生类对象来执行对应的操作。
重定义(Hide)是指在派生类中定义与基类中相同函数名的非虚函数,该函数会屏蔽基类中的同名函数,无法通过基类指针或引用调用派生类中重新定义的函数。
-
多态的实现原理是通过基类的指针或引用来访问派生类的对象,在运行时确定具体调用哪个类的函数。这是因为基类中的虚函数使用了虚函数表的机制,每个对象都有一个指向对应虚函数表的指针。当通过基类指针或引用调用虚函数时,根据对象的实际类型,在虚函数表中查找需要调用的函数,并执行相应的操作。
-
inline函数可以是虚函数,但编译器会忽略inline属性,将该函数从inline函数列表中移除,因为虚函数需要放在虚函数表中。
-
静态成员函数不能是虚函数,因为静态成员函数没有this指针,使用 类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放入虚函数表。
-
构造函数不能是虚函数,因为构造函数中的虚函数表指针是在构造函数初始化列表阶段才初始化的,此时对象尚未完全建立。
-
析构函数可以是虚函数,并且最好将基类的析构函数定义为虚函数。这样当通过基类指针或引用来删除一个派生类对象时,会调用正确的析构函数并避免内存泄漏。虚析构函数通常用于处理多态对象的释放问题。
-
对象访问普通函数和访问虚函数的速度相同,对于普通对象,直接调用函数就可以了,不需要查找虚函数表。而对于指针对象或引用对象,由于可能存在多态性,需要根据实际类型查找虚函数表,稍微慢一些。
-
虚函数表是在编译阶段生成的,一般情况下存储在代码段(常量区)。每个类有一个独立的虚函数表,其中存储了该类及其基类的虚函数信息。对象在创建时会分配一块内存用来存储动态分派所需的虚函数表指针,通过这个指针来访问虚函数表并执行对应的函数。
-
C++中的菱形继承问题是指一个派生类同时继承了两个共同基类,而这两个基类又继承了同一个虚基类,造成了二义性和资源浪费的问题。为了解决这个问题,可以使用虚继承(virtual inheritance)来共享同一个虚基类,避免重复继承。
-
抽象类是指含有纯虚函数(只有函数声明,没有函数体)的类,无法实例化对象。抽象类一般用作基类,强制派生类重写纯虚函数,从而达到接口继承的目的。抽象类的作用是定义一组接口(纯虚函数),规范具体派生类的行为。