热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【C++要笑着学】虚函数表(VBTL)|观察虚表指针|运行时决议与编译时决议|动态绑定与静态绑定|静态多态与动态多态|单继承与多继承关系的虚表

C表情包趣味教程👉《C要笑着学》💭写在前面虚表是编译器的实现,而非C的语言标准。上一章我们学习了多态的概念,本章我们

   C++ 表情包趣味教程 👉 《C++要笑着学》

💭 写在前面

虚表是编译器的实现,而非C++的语言标准。上一章我们学习了多态的概念,本章我们深入探讨一下多态的原理。文章开头先说虚表指针,观察编译器的查表行为。首次观察我们先从监视窗口观察美化后的虚表 _vfptr,再透过内存窗口观察真实的 _vfptr。我们还会探讨为什么对象也能切片却不能实现多态的问题。对于虚表到底存在哪?我们会带着大家通过一些打印虚表的方式进行比对!铺垫完虚表的知识后,会讲解运行时决议与编译时决议,穿插动静态的知识点。文章的最后我们会探讨单继承与多继承的虚表,多继承中的虚表神奇的切片指针偏移问题,这块难度较大,后续我们会考虑专门讲解一下,顺带着把钻石虚拟继承给讲了。

   本篇博客全站热榜排名:未上榜



Ⅰ. 虚函数表(VTBL)


0x00 引入:发现虚表的存在

❓ 我们首先来做一道题:sizeof(Base) 是多少(32位下)?

class Base {
public:virtual void Func1() {cout <<"Func1()" <private:int _b &#61; 1;
};int main(void)
{Base b;return 0;
}

&#x1f4a1; 答案&#xff1a;答案令人诧异&#xff0c;居然是 8。

 通过监视窗口我们发现除了 _b 成员外还有了一个 _vfptr b1 对象中&#xff1a;

  

不监视不知道&#xff0c;一监视吓一跳。这个 _vfptr 是个什么 √8 玩意&#xff1f;

对象中的这个 _vfptr 我们称之为虚表指针&#xff08;virtual function pointer&#xff09;&#xff0c;我们简称其为 虚表

一个含有虚函数的类中都至少有一个像这样的虚函数表指针&#xff0c;虚函数地址都会放到这个表里。

那么虚函数表中放了些什么呢&#xff1f;我们继续往下看。

&#x1f4ac; 为了方便演示&#xff0c;我们再多整点函数&#xff1a;

class Base {
public:void Func1() {cout <<"Func1()" <private:int _b &#61; 1;
};

通过监视窗口我们可以看到&#xff0c;虚函数 Func2Func3 都被存进了 _vfptr 中。

虚表虚表&#xff0c;自然是存虚函数的表了&#xff0c;Func1 不是虚函数&#xff0c;自然也就不会存入表中。


0x01 观察虚表指针 _vfptr 

❓ 思考&#xff1a;多态是怎么做到指向哪就调用哪的&#xff1f;对于父类的虚表又是什么样的呢&#xff1f;

&#x1f4ac; 代码&#xff1a;我们用的是 VS2013 &#43; 64位 环境去观测&#xff1a;

class Base {
public:virtual void Func1() {cout <<"Base::Func1()" <private:int _b &#61; 1;
};int main(void)
{cout <}

&#x1f50d; 监视&#xff1a;我们还是先用监视窗口去做一个简单的观察&#xff1a;

 监视窗口是为了方便我们观测优化过的&#xff0c;相当于是一种美化。

注意看&#xff0c;Func3 没有放在 _vfptr 中&#xff0c;又一次证明了这个表里只会存虚函数。

其实虚函数表也没搞什么特殊&#xff0c;也没什么了不起的&#xff0c;虚函数其实是和普通函数一样存在代码段的。

只是普通函数只会进符号表以方便链接&#xff0c;都是 "编译时决议"&#xff0c;

而虚函数的地址会被放进虚表&#xff0c;是为了 "运行时决议" 做准备&#xff0c;这个我们后面会细说。

 所以这里我们可以这么理解&#xff1a;

&#x1f4da; 虚表的本质&#xff1a;虚表是一个 "存虚函数指针的指针数组" &#xff0c;一般情况这个数组最后面会放一个空指针。


0x02 虚函数的重写与覆盖

回忆一下&#xff0c;上一章我们介绍重写的时候还说过&#xff0c;"重写" 还可以称为 "覆盖"&#xff0c;

这是为什么呢&#xff1f;叫重写似乎更好去理解&#xff0c;覆盖好像很难去理解啊。

&#x1f4ac; 代码&#xff1a;现在我们增加一个子类 Derive 去继承 Base&#xff1a;

// 父类 Base
class Base {
public:virtual void Func1() {cout <<"Base::Func1()" <private:int _b &#61; 1;
};// 子类 Derive
class Derive : public Base {
public:virtual void Func1() {cout <<"Derive::Func1()" <private:int _d &#61; 2;
};int main(void)
{cout <}

&#x1f6a9; 运行结果&#xff1a;

 &#xff08;如果没有虚表这里会是8&#xff09;

&#x1f50d; 监视&#xff1a;我们再通过监视窗口观察

 和父类的相对比&#xff0c;冷静分析后不难发现&#xff1a; 

父类 b 对象和子类 d 对象虚表是不一样的&#xff0c;这里看我们发现 Func1 完成了重写&#xff0c;

所以 d 的虚表中存的是重写的 Derive::Func1&#xff0c;所以虚函数的重写也叫做覆盖。

就可以理解为&#xff1a;子类的虚表拷贝了父类的虚表&#xff0c;子类的 Func1 覆盖掉了父类上的 Func1。

&#xff08;覆盖指的是虚表中虚函数的覆盖&#xff09;

  • 虚函数重写&#xff1a;语法层的概念&#xff0c;子类对继承父类虚函数实现进行了重写。
  • 虚函数覆盖&#xff1a;原理层的概念&#xff0c;子类的虚表&#xff0c;拷贝父类虚表进行了修改&#xff0c;覆盖重写那个虚函数。

&#x1f53a; 总结&#xff1a;虚函数的重写与覆盖&#xff0c;重写是语法层的叫法&#xff0c;覆盖是原理层的叫法。


0x03 编译器的查表行为

❓ 思考&#xff1a; 是如何做到指针指向谁就调用谁的虚函数的&#xff1f;好像非常的听♂话&#xff1a;

class Base {
public:virtual void Func1() {cout <<"Base::Func1()" <private:int _b &#61; 1;
};class Derive : public Base {
public:virtual void Func1() {cout <<"Derive::Func1()" <private:int _d &#61; 2;
};int main(void)
{Base b;Derive d;Base* ptr &#61; &b; ptr->Func1(); // 调用的是父类的虚函数ptr &#61; &d;ptr->Func1(); // 调用的是子类的虚函数return 0;
}

&#x1f6a9; 运行结果&#xff1a;

 能不能猜到是跟虚表有关系&#xff1f;它到底要调用哪个函数不是按类型去定的&#xff0c;

如果是按类型去定的那这里调的应该都是父类&#xff0c;结果会都是 Base::Func1() &#xff0c;所以显然不是。

这里会去 ptr 指针指向的对象里去查表&#xff0c;其实对它自己而言它自己都也不知道调用的是谁&#xff0c;

因为子类切个片&#xff0c;它自己也只能看到父类对象&#xff0c;它根本就没法知道&#xff0c;但是他会查表&#xff01;

&#x1f4da; 具体行为如下&#xff1a;

编译器会从指向的对象里去找&#xff0c;先在父类对象里找到了 Base::Func1&#xff0c;

Base* ptr &#61; &b; // 指向是b&#xff0c;是父类Base的ptr->Func1(); // 调用的是父类的虚函数

然后指向变为 &d&#xff0c;它就从子类对象里找&#xff0c;从而找到了 Derive::Func1

ptr &#61; &d; // 指向变成d了&#xff0c;是子类Derive的
ptr->Func1(); // 这时调用的就是子类的虚函数了

所以&#xff0c;多态调用实现是依靠运行时去指向对象的虚表中查&#xff0c;调用函数地址。


0x04 探讨&#xff1a;对象也能切片&#xff0c;为什么不能实现多态&#xff1f;

 既然指针和引用可以实现多态&#xff0c;那父类赋值给子类对象也可以切片&#xff0c;

为什么实现不了多态&#xff1f;搞歧视&#xff1f;

Base* ptr &#61; &d; ✅
Base& ref &#61; d; ✅ Base b &#61; d; ❓ 为什么不行&#xff1f;都是支持切片的&#xff0c;为什么对象就不行&#xff1f;

从编译器的角度&#xff0c;编译器实现时会判断构不构成多态&#xff0c;不满足规则不构成多态就找到地址&#xff0c;call。

 至于为什么实现不了多态&#xff0c;因为实现出来会出现混乱状态。

"即使你是一门语言的设计者&#xff0c;遇到这种问题也很难解决 "

根本原因是&#xff1a;对象切片时&#xff0c;子类对象只会拷贝成员给父类对象&#xff0c;并不会拷贝虚表指针。

 因为拷贝了就混乱了&#xff0c;父类对象中到底是父类的虚表指针&#xff1f;还是子类的虚表指针&#xff1f;

那下面的调用是调用父类的虚函数还是子类的虚函数&#xff1f;就不确定了&#xff1a;

ptr &#61; &b;
ptr->func1(); // ?????????? 父类的func1&#xff0c;还是子类的func1&#xff1f;

对象实现多态又不得不去拷贝虚表&#xff0c;因为它肯定是需要去对象里的虚表里找&#xff0c;

问题是拷贝虚表后就乱了套了。最大问题是 —— 这时候到底调用的是谁的问题。

 如果一个父类对象切片拷贝给子类后&#xff0c;切片前指向子类&#xff0c;没切片前指向父类。

"这让人头大"

所以对象不能实现多态&#xff0c;想实现也不行&#xff0c;实现了就乱了套了&#xff01;

&#x1f53a; 总结&#xff1a;

  • 一个类对象中的 __vptr 是恒定的&#xff0c;它永远都会指向其所属类的虚表。
  • 而当使用指针或引用时&#xff0c;__vptr 会指向继承类的虚表&#xff08;从而达成多态的效果&#xff09;

0x05 透过内存窗口仔细观察 _vfptr

&#x1f4ac; 打开监视窗口观察下列代码的虚表&#xff1a;

class Base {
public:virtual void Func1() {cout <<"Base::Func1()" <private:int _b &#61; 1;
};class Derive : public Base {
public:virtual void Func1() {cout <<"Derive::Func1()" <private:int _d &#61; 2;
};

从监视窗口观察&#xff0c;有时候会发现&#xff0c;好像有些虚函数再监视窗口显示的虚表里不存在。

 这时候千万不能动摇 "只要是虚函数都会存入虚表" 这个事实。 

这是监视窗口的锅&#xff0c;我们前面就说了 —— 监视窗口是美化过的&#xff01;

想要看到真实的样子&#xff0c;我们可以打开内存去查看&#xff1a;

但是这内存看的很让人迷糊&#xff0c;这谁看得懂&#xff0c;知道谁是谁&#xff1f;有什么办法可以把虚表打印出来&#xff1f;

&#x1f4ac; 只要取到虚表指针&#xff0c;想打印虚表就很简单了&#xff1a;

虚表是个函数指针数组&#xff0c;该数组里的每个元素存放的是一个函数指针。

typedef void(*V_FUNC)();/* 打印虚表 */
void Print_VFTable(V_FUNC* arr) {printf("vfptr:%p\n", arr);for (size_t i &#61; 0; arr[i] !&#61; nullptr; i&#43;&#43;) {printf("[%d]: %p\n", i, arr[i]);V_FUNC foo &#61; arr[i];foo();}
}int main(void)
{Derive d;Print_VFTable((V_FUNC*)(*((int*)&d)) // 指针之间是可以互相转换的);/* 语法有规定&#xff1a;完全没有关系的类型强转也不行。至少得有一点关系&#xff1a;比如指针和int因为指针虽然是地址但是也是表示地址的编号&#xff0c;第几个编号的地址指针之间可以随意转换&#xff0c;我想取4个字节&#xff0c;&d 是个 Derive*&#xff0c;接引用后就是 Derive&#xff0c;此时强转成 int* 解引用就是取4个字节了。由于是 int* 要调用 Print_VFTable 函数传不过去&#xff0c;所以我们最外层又强转回 V_FUNC* 了&#xff0c;这是一个函数指针数组的地址指针。“内线转外线再转内线”*/return 0;
}

&#x1f6a9; 运行结果&#xff1a;

&#x1f53a; 结论&#xff1a;VS 监视窗口看到的虚函数表不一定是真实的&#xff0c;可能被处理过。


0x06 虚表的存储位置

 虚表&#xff0c;一个类型共用一个类型虚表吗&#xff1f;虚表到底存在哪&#xff1f;

int main(void)
{Base b1;Base b2;Base b3;Base b4;Print_VFTable((V_FUNC*)(*((int*)&b1)));Print_VFTable((V_FUNC*)(*((int*)&b2)));Print_VFTable((V_FUNC*)(*((int*)&b3)));Print_VFTable((V_FUNC*)(*((int*)&b4)));return 0;
}

&#x1f6a9; 运行结果&#xff1a;

&#x1f53a; 结论&#xff1a;同一个类型它们的虚表内存地址都是一样的&#xff0c;同一类型的对象共用一份虚表。

现在我们知道了同一类型的对象公用一张虚表了&#xff0c;我们再来思考虚表存在哪里的问题。

❓ 思考&#xff1a;虚表到底存在哪里&#xff1f;

虚表放在栈上合理吗&#xff1f;显然不合理&#xff0c;放在栈上虚表跟着这个对象走跟着那个对象走&#xff0c;太不稳定了。

虚表最好能够永久存储&#xff0c;我们希望虚表稳稳地存着。

我们说的对象在构造的时候初始化虚表&#xff0c;实际上不是建立虚表&#xff0c;

按理来说编译的时候就已经把虚表建立好了&#xff0c;会在构造函数的初始化列表阶段把地址存进虚表。

此外&#xff0c;不仅要将虚表放置到永久的区域&#xff0c;不能因为某个对象销毁了这个虚表就没了&#xff0c;

那其他对象住哪&#xff1f;他们可是要共用同一张虚表的&#xff01;&#xff01;&#xff01;所以这个虚表要保证一直都在。

并且还要很容易就能找到&#xff0c;那存在堆上可以吗&#xff1f;

不太行&#xff01;堆要动态申请&#xff0c;虽然让第一个实例化的对象申请似乎也是可以的&#xff0c;但是堆释放啊&#xff01;

谁去释放&#xff1f;让最后一个走的对象释放&#xff1f;那不还得加引用计数&#xff0c;所以虚表放堆上也不太可能。

那现在栈也不能存&#xff0c;堆也不能存&#xff0c;就只剩下常量区和数据段了。

静态区和常量区存放好像也很合理&#xff0c;当你实在不确定它到底在哪里的时&#xff0c;

这时候就需要一种 "验证问题的逆向精神"&#xff0c;就比如刚才打印虚表指针&#xff0c;正是这种精神。

当然&#xff0c;这很依赖丰富的基础知识&#xff0c;是需要大量练习和实际锻炼的。

&#x1f4ac; 比对&#xff1a;

int c &#61; 2; // 全局变量
int main(void)
{Base b1;Base b2;Base b3;Base b4;Print_VFTable((V_FUNC*)(*((int*)&b1)));Print_VFTable((V_FUNC*)(*((int*)&b2)));Print_VFTable((V_FUNC*)(*((int*)&b3)));Print_VFTable((V_FUNC*)(*((int*)&b4)));int a &#61; 0;static int b &#61; 1; // 静态区const char* str &#61; "Hello,World!\n"; // str在栈上&#xff0c;但指向的空间在常量区int* p &#61; new int[10]; // p在栈上&#xff0c;但p指向的空间在堆上printf("栈: %p\n", &a);printf("静态区/数据段: %p\n", &b);printf("静态区/数据段: %p\n", &c);printf("常量区代码段: %p\n", str);printf("代码段: %p\n", str);printf("堆: %p\n", p);printf("虚表: %p\n", (*((int*)&b4)));printf("函数&#xff1a;%p\n", Derive::Func3);printf("函数&#xff1a;%p\n", Derive::Func2);printf("函数&#xff1a;%p\n", Derive::Func1);return 0;
}

&#x1f6a9; 运行结果&#xff1a;

 最合适的地方似乎就是数据段了。

想一想一下虚表是什么&#xff0c;是一个函数指针数组&#xff0c;放到数据段上是再合适不过的了。

&#x1f53a; 总结&#xff1a;虚表存储在数据段上。


Ⅱ. 多态的原理


0x00 运行时决议与编译时决议

我们刚才知道了&#xff0c;多态调用实现是靠运行时查表做到的&#xff0c;我们再看一段代码。

&#x1f4ac; 在刚才代码基础上&#xff0c;让父类子类分别多调用一个 Func3&#xff0c;注意 Func3 不是虚函数&#xff1a;

class Base {
public:virtual void Func1() {cout <<"Base::Func1()" <private:int _b &#61; 1;
};class Derive : public Base {
public:virtual void Func1() {cout <<"Derive::Func1()" <private:int _d &#61; 2;
};int main(void)
{Base b;Derive d;Base* ptr &#61; &b; ptr->Func1(); // 调用的是父类的虚函数ptr->Func3();ptr &#61; &d;ptr->Func1(); // 调用的是子类的虚函数ptr->Func3();return 0;
}

&#x1f6a9; 运行结果&#xff1a;

❓ 问题&#xff1a;这里 Func3 为什么不是 Derive 的&#xff1f;

&#x1f4a1; 解答&#xff1a;因为 Func3 不是虚函数&#xff0c;它没有进入虚表。

如果我们从更深的角度 —— 汇编层面去看&#xff0c;就可以牵扯出编译时决议和运行时决议。

&#xff08;这个我们前面一直再提&#xff0c;我们现在就来好好讲讲~ 乖♂乖♂站♂好 &#xff09;

决议的意思就是如何去确定函数的地址&#xff0c;一个是在运行时确定&#xff0c;一个是在编译时确定。

&#x1f4da; 多态调用&#xff1a;运行时决议&#xff0c;即运行时确定调用函数的地址。【通过查虚函数表】

&#xff08;编译完后通过指令&#xff0c;去对象中虚表里去找虚函数运行&#xff0c;是运行时去找&#xff0c;找到了才调用&#xff09;

&#x1f4da; 普通调用&#xff1a;编译时决议&#xff0c;编译时确定调用函数的地址。【通过类型】

&#xff08;所有的编译时确定都是看 ptr 是什么类型&#xff0c;跟对象没有关系&#xff0c;不看指向的对象&#xff0c;自己是什么类型&#xff0c;就去哪里找 Func1&#xff09;

 &#xff08;查看反汇编&#xff09;

这正是多态底层实现的原理&#xff0c;编译器去检查&#xff0c;如果满足多态的条件了&#xff0c;它就按运行时决议的方式。


0x01 动态绑定与静态绑定

静态库&#xff1a;指的是链接的那个阶段链接的库。

动态库&#xff1a;程序运行起来后才加载&#xff0c;去动态库里找。

静态绑定&#xff1a;又称为前期绑定&#xff08;早绑定&#xff09;&#xff0c;在程序编译期间确定了程序的行为&#xff0c;也称为静态多态。比如函数重载。

动态绑定&#xff1a;又称后期绑定&#xff08;晚绑定&#xff09;&#xff0c;在程序运行期间&#xff0c;根据具体拿到的类型确定程序的具体行为&#xff0c;调用具体的函数&#xff0c;也成为动态多态。


0x02 静态的多态和动态的多态

 多态在有些书上还细分了静态的多态和动态的多态。

静态的多态&#xff08;编译时&#xff09;&#xff1a;指的是函数重载。

int x &#61; 0, y &#61; 1;
double a &#61; 0.0, b &#61; 1.1;swap(x, y);
swap(a, b);这两个 swap 让人感觉是同一个函数&#xff0c;
但实际不是。实际编译链接根据函数名修饰规则找到不同的函数。

动态的多态&#xff08;运行时&#xff09;&#xff1a;指的是本节内容讲的这个。

void Func(Person& p) {p.BuyTicket();
}Person Mike;
Func(Mike);Student Jack;
Func(Jack);

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


0x00 单继承中的虚函数表

&#xff08;需要注意的是&#xff0c;在单继承和多继承关系中&#xff0c;下面我们去关注的是子类对象的虚表模型&#xff0c;因为父类的虚表模型我们前面已经看过了&#xff0c;没什么需要特别研究的地方&#xff09;

&#x1f4ac; 代码&#xff1a;单继承中的虚函数表&#xff1a;

class Base {
public:virtual void func1() { cout <<"Base::func1" <private:int a;
};class Derive :public Base {
public:virtual void func1() { cout <<"Derive::func1" <private:int b;
};

我们还是用刚才介绍的方法打印虚表&#xff1a;

typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[]) {// 依次取虚表中的虚函数指针打印并调用&#xff0c;调用就可以看出存的是哪个函数cout <<" 虚表地址>" <", i, vTable[i]);VFPTR f &#61; vTable[i];f();}cout <}int main()
{Base b;Derive d;
}

&#x1f4ac; 代码&#xff1a;我们在把虚函数表打印出来看看&#xff08;32位取头上4个字节&#xff0c;64位需要取头上8个字节&#xff09;&#xff1a;

int main()
{Base b;Derive d;PrintVTable((VFPTR*)(*(int*)&d));return 0;
}

&#x1f6a9; 运行结果&#xff1a;


0x01 多继承中的虚函数表

刚才我们看的是单继承&#xff0c;我们现在再看复杂一点的多继承。

&#x1f4ac; 代码&#xff1a;Base1 和 Base2 都进行了重写

class Base1 {
public:virtual void func1() { cout <<"Base1::func1" <private:int b1;
};class Base2 {
public:virtual void func1() { cout <<"Base2::func1" <private:int b2;
};class Derive : public Base1, public Base2 {
public:virtual void func1() { cout <<"Derive::func1" <private:int d1;
};typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[]) {cout <<" 虚表地址>" <", i, vTable[i]);VFPTR f &#61; vTable[i];f();}cout <}int main()
{Derive d;VFPTR* vTableb1 &#61; (VFPTR*)(*(int*)&d);PrintVTable(vTableb1);VFPTR* vTableb2 &#61; (VFPTR*)(*(int*)((char*)&d &#43; sizeof(Base1)));PrintVTable(vTableb2);return 0;
}

这里 Derive 明显会有两张虚表&#xff0c;我们先透过监视简单看一下&#xff1a;

我们的 func3 是放哪一个虚表里&#xff1f;是两张都放一份&#xff0c;还是选择一份放呢&#xff1f;

func1 的两个地址好像不一样&#xff0c;0X0911ae 和 0X901249&#xff0c;因为它们都不是真正的函数的地址。

我们来看看 Derive 中的 func1 真正的地址&#xff1a;

printf("%p\n", &Derive::func1);

这里可能就是多套了一层&#xff0c;是一种保护机制。虽然不一样但是最后都跳到了函数上面去。

&#x1f53a; 结论&#xff1a;Derive 对象 Base2 虚表中 func1 时&#xff0c;是 Base2 指针 ptr2 取调用&#xff0c;但是这时 ptr2 发生切片指针偏移&#xff0c;需要修正。中途就需要修正存储 this 指针 ecx 的值。

❓ 问题&#xff1a;这里还有一个指针偏移的问题&#xff0c;在多继承中这三个指针的值是一样的吗&#xff1f;

Base1* ptr1 &#61; &d;
Base2* ptr2 &#61; &d;
Derive* ptr3 &#61; &d;cout <cout <cout <

&#x1f6a9; 运行结果&#xff1a;0073FBA0   0073FBA8   0073FBA0

&#x1f4a1; 答案&#xff1a;不一样。给人第一感觉好像是一样的&#xff0c;因为赋过去的值都是 &d&#xff0c;但实际上并不一样。

因为这里要发生切片&#xff0c;切片后赋值兼容&#xff0c;所以它们的地址就不一样了。


0x02 多态的一些题目


1. 什么是多态&#xff1f;

2. 什么是重载、重写(覆盖)、重定义(隐藏)&#xff1f;

3. 多态的实现原理&#xff1f;答&#xff1a;参考本节课件内容

4. inline函数可以是虚函数吗&#xff1f;答&#xff1a;可以&#xff0c;不过编译器就忽略inline属性&#xff0c;这个函数就不再是inline&#xff0c;因为 虚函数要放到虚表中去。

5. 静态成员可以是虚函数吗&#xff1f;答&#xff1a;不能&#xff0c;因为静态成员函数没有this指针&#xff0c;使用类型::成员函数的调用方式 无法访问虚函数表&#xff0c;所以静态成员函数无法放进虚函数表。

6. 构造函数可以是虚函数吗&#xff1f;答&#xff1a;不能&#xff0c;因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

7. 析构函数可以是虚函数吗&#xff1f;什么场景下析构函数是虚函数&#xff1f;答&#xff1a;可以&#xff0c;并且最好把基类的析构函数定义 成虚函数。参考本节课件内容

8. 对象访问普通函数快还是虚函数更快&#xff1f;答&#xff1a;首先如果是普通对象&#xff0c;是一样快的。如果是指针对象或者是 引用对象&#xff0c;则调用的普通函数快&#xff0c;因为构成多态&#xff0c;运行时调用虚函数需要到虚函数表中去查找。

9. 虚函数表是在什么阶段生成的&#xff0c;存在哪的&#xff1f;答&#xff1a;虚函数表是在编译阶段就生成的&#xff0c;一般情况下存在代码 段(常量区)的。

10. C&#43;&#43;菱形继承的问题&#xff1f;虚继承的原理&#xff1f;



&#x1f4cc; [ 笔者 ]   王亦优
&#x1f4c3; [ 更新 ]   2022.9.5
❌ [ 勘误 ]   /* 暂无 */
&#x1f4dc; [ 声明 ]   由于作者水平有限&#xff0c;本文有错误和不准确之处在所难免&#xff0c;本人也很想知道这些错误&#xff0c;恳望读者批评指正&#xff01;

&#x1f4dc; 参考资料 

C&#43;&#43;reference[EB/OL]. []. http://www.cplusplus.com/reference/.

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

百度百科[EB/OL]. []. https://baike.baidu.com/.

比特科技. C&#43;&#43;[EB/OL]. 2021[2021.8.31]. 


推荐阅读
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • 本文介绍了如何在给定的有序字符序列中插入新字符,并保持序列的有序性。通过示例代码演示了插入过程,以及插入后的字符序列。 ... [详细]
  • 本文介绍了P1651题目的描述和要求,以及计算能搭建的塔的最大高度的方法。通过动态规划和状压技术,将问题转化为求解差值的问题,并定义了相应的状态。最终得出了计算最大高度的解法。 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
  • Linux环境变量函数getenv、putenv、setenv和unsetenv详解
    本文详细解释了Linux中的环境变量函数getenv、putenv、setenv和unsetenv的用法和功能。通过使用这些函数,可以获取、设置和删除环境变量的值。同时给出了相应的函数原型、参数说明和返回值。通过示例代码演示了如何使用getenv函数获取环境变量的值,并打印出来。 ... [详细]
  • 本文介绍了PE文件结构中的导出表的解析方法,包括获取区段头表、遍历查找所在的区段等步骤。通过该方法可以准确地解析PE文件中的导出表信息。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 本文介绍了一个题目的解法,通过二分答案来解决问题,但困难在于如何进行检查。文章提供了一种逃逸方式,通过移动最慢的宿管来锁门时跑到更居中的位置,从而使所有合格的寝室都居中。文章还提到可以分开判断两边的情况,并使用前缀和的方式来求出在任意时刻能够到达宿管即将锁门的寝室的人数。最后,文章提到可以改成O(n)的直接枚举来解决问题。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • 本文详细介绍了GetModuleFileName函数的用法,该函数可以用于获取当前模块所在的路径,方便进行文件操作和读取配置信息。文章通过示例代码和详细的解释,帮助读者理解和使用该函数。同时,还提供了相关的API函数声明和说明。 ... [详细]
  • 电话号码的字母组合解题思路和代码示例
    本文介绍了力扣题目《电话号码的字母组合》的解题思路和代码示例。通过使用哈希表和递归求解的方法,可以将给定的电话号码转换为对应的字母组合。详细的解题思路和代码示例可以帮助读者更好地理解和实现该题目。 ... [详细]
  • 本文介绍了UVALive6575题目Odd and Even Zeroes的解法,使用了数位dp和找规律的方法。阶乘的定义和性质被介绍,并给出了一些例子。其中,部分阶乘的尾零个数为奇数,部分为偶数。 ... [详细]
author-avatar
高--洁
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有