目录
引言
接下来我们继续学习多态。
没有阅读多态(上)的可以点击下面的链接哦~
多态
4.多态的原理
4.1 虚函数表指针
我们先来看看这段代码:
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
Base b;
cout << sizeof(b) << endl;
return 0;
}
输出结果为:
我们通过监视来观察一下:
通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。它指向的对象就是虚函数表,一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
对于大多数现代编译器,在32位系统上,虚指针通常占用4个字节,而在64位系统上则占用8个字节。
编译器可能会为对象添加一些填充以确保内存对齐,从而提高访问速度。因此,实际的对象大小可能会比这些值的简单相加要大。
这就解释了为什么上面的代码输出结果为16。
针对上面的代码我们对其进行改造:
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;
}
通过监视来观察一下:
1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
2.基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
5. 总结一下派生类的虚表生成:
a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
以下是对几个名词的补充:
虚函数: 虚函数是C++类中的成员函数,它们被声明为virtual,用于支持多态性。虚函数本身和普通函数一样,都是存储在程序的代码段中的。当我们调用一个虚函数时,实际上是在调用存储在代码段中的函数体。
虚表(vtable): 虚表是一个全局的或静态的表,它包含了指向类中所有虚函数的指针。这个表并不是存储在对象中的,而是存储在程序的某个全局或静态数据段中。每个包含虚函数的类都有一个与之对应的虚表。
虚表指针(vptr): 虚表指针是存储在对象实例中的一个特殊指针,它指向该对象所属类的虚表。这个指针是对象的一部分,通常位于对象的起始位置(但这也取决于编译器的具体实现和对象的内存布局)。当我们通过对象的指针或引用来调用虚函数时,编译器会使用这个虚表指针来查找并调用正确的虚函数。
4.2 多态的原理
在C++中,多态性允许我们通过基类指针或引用来调用派生类中的重写方法。为了实现这一点,编译器为每个包含虚函数的类生成一个虚函数表(vtable)。这个表包含了指向类中所有虚函数的指针。
每个对象实例中都含有一个指向其所属类的虚函数表的指针(虚表指针,vptr)。当通过基类指针或引用来调用虚函数时,编译器会使用这个虚表指针来查找并调用正确的虚函数。
(1)对于父类对象,其虚表指针指向基类的虚函数表。
(2)对于派生类对象,其虚表指针指向派生类的虚函数表。如果派生类重写了基类的虚函数,那么派生类的虚函数表中相应位置将指向派生类的重写函数;如果派生类没有重写某个虚函数,那么该位置将指向基类中的原始函数。
因此,当通过基类指针或引用来调用虚函数时,实际调用的函数取决于对象的实际类型(即对象的虚表指针指向的虚函数表)。这种机制允许C++在运行时根据对象的实际类型来确定应该调用哪个虚函数,从而实现多态性。
简单来说就是:
C++通过为每个包含虚函数的类生成一个虚函数表,该表存储了指向类中所有虚函数的指针。每个对象实例内部都含有一个指向其所属类虚函数表的指针。当使用基类指针或引用来调用虚函数时,编译器会利用这个虚表指针,在运行时查找并调用与对象实际类型相匹配的虚函数。
多态实现的两个条件:
(1) 继承(或基类与派生类的关系):继承(或基类与派生类的关系)是多态的基础。它允许一个类(派生类)继承另一个类(基类)的属性和方法。通过继承,派生类可以获取基类的所有公有和保护成员(注意,私有成员对派生类是不可见的,但它们在派生类对象的内存布局中仍然存在,只是不可直接访问)。在C++等语言中,继承还涉及虚函数表的继承。每个包含虚函数的类都有一个虚函数表,该表存储了类中所有虚函数的地址。派生类继承基类时,会继承基类的虚函数表,并可能根据需要对某些虚函数进行重写。
(2) 虚函数重写(动态绑定/后期绑定):虚函数是C++等语言中实现多态的关键机制。虚函数允许在运行时(而不是编译时)根据对象的实际类型来确定调用哪个函数。当派生类重写了基类的虚函数时,派生类对象的虚函数表中对应虚函数的地址会被更新为派生类实现的地址。这样,当通过基类指针或引用指向派生类对象并调用虚函数时,实际调用的是派生类实现的版本。虚函数重写是多态性的核心,它使得相同的函数调用可以根据对象的实际类型产生不同的行为。
综上所述,要实现多态,必须满足两个条件:一是通过继承(或基类与派生类的关系)建立类之间的层次关系;二是在派生类中重写基类的虚函数,以实现动态绑定(在运行时根据对象的实际类型调用正确的方法)。这两个条件共同作用,使得程序能够根据对象的实际类型来调用相应的方法,从而实现多态性。
5.单继承和多继承关系的虚函数表
5.1 单继承中的虚函数表
我们来看看以下代码:
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;
}
通过调试来观察一下:
我们可以发现:监视窗口并没有显示fun3和fun4。我们要如何查看d的虚表呢?我们可以通过下面的代码实现需求:
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Base b;
Derive d;
// 思路:取出b、d对象的头8bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数
// 指针的指针数组,这个数组最后面放了一个nullptr
// 1.先取b的地址,强转成一个int*的指针
// 2.再解引用取值,就取到了b对象头8bytes的值,这个值就是指向虚表的指针
// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
// 4.虚表指针传递给PrintVTable进行打印虚表
// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最
// 后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再
// 编译就好了。
// 这里我使用了reinterpret_cast来进行类型转换,这是更安全的做法,
// 因为它明确指出了我们正在执行低级别的、可能依赖于实现的转换。
VFPTR* vTableb = reinterpret_cast<VFPTR*>(*reinterpret_cast<intptr_t*>(&b));
PrintVTable(vTableb);
VFPTR* vTabled = reinterpret_cast<VFPTR*>(*reinterpret_cast<intptr_t*>(&d));
PrintVTable(vTabled);
return 0;
}
输出结果为:
5.2 多继承中的虚函数表
我们来了解多继承的虚函数表,来看看这段代码:
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;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
VFPTR* vTableb1 = reinterpret_cast<VFPTR*>(*reinterpret_cast<intptr_t*>(&d));
PrintVTable(vTableb1);
VFPTR* vTableb2 = *reinterpret_cast<VFPTR**>(reinterpret_cast<char*>(&d) + sizeof(Base1));
PrintVTable(vTableb2);
return 0;
}
输出结果为:
多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。
结束语
把有关多态的一些基础内容写了一下。
感谢各位大佬的支持!!!
希望这篇文章对您理解C++多态有所帮助!!!
求点赞收藏评论关注!!!