C++ 多态的相关问题

目录

1. 第一题

2. 第二题

3. inline 函数可以是虚函数吗

4. 静态成员函数可以是虚函数吗

5. 构造函数可以是虚函数吗

6. 析构函数可以是虚函数吗

7. 拷贝构造和赋值运算符重载可以是虚函数吗

8. 对象访问普通函数快还是访问虚函数快

9. 虚函数表是什么阶段生成的?存在哪里的?


1. 第一题

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()
{B* ptr = new B;ptr->Test();return 0;
}// A: A->0
// B: B->1
// C: A->1
// D: B->0
// E: 编译出错
// F: 以上都不正确

答案是什么呢? 

分析过程:

  • 首先,派生类 B 继承 A类,会将B类的方法继承下来,但注意,继承是派生类有访问基类方法的权限,而并不是说基类的方法在派生类中也有一份,继承后的基类方法依旧属于基类;
  • 其次,多态的条件以及注意多态是接口继承;
  • 最后,根据多态判别调用什么方法,即指向的什么对象,就调用谁的方法。

如下:

2. 第二题

class A{
public:A(char *s) { std::cout << s << std::endl; }~A(){}
};
class B :virtual public A
{
public:B(char *s1, char*s2) :A(s1) { std::cout << s2 << std::endl; }
};
class C :virtual public A
{
public:C(char *s1, char*s2) :A(s1) { std::cout << s2 << std::endl; }
};
class D :public B, public C
{
public:D(char *s1, char *s2, char *s3, char *s4) :B(s1, s2), C(s1, s3), A(s1){std::cout << s4 << std::endl;}
};int main() {D *p = new D("class A", "class B", "class C", "class D");delete p;return 0;
}// A:class A class B class C class D
// B:class D class B class C class A
// C:class D class C class B class A
// D:class A class C class B class D

分析过程如下:

第一个问题:为什么D类的实例化对象要显示调用A的构造呢?并且此时我们发现如果不调用A的构造还会报错,如下:

class D :public B, public C
{
public:D(char *s1, char *s2, char *s3, char *s4) :B(s1, s2), C(s1, s3) /*  A(s1) */{std::cout << s4 << std::endl;}
};

现象如下:

原因是因为这是一个菱形虚拟继承 ,如图所示:

而菱形虚拟继承带来的结果就是A只有一份,既然只有一份(B和C类共享),在B和C类进行初始化是不合适的。因此需要在D类调用A的构造函数。

但是我们发现,B和C类也在初始化列表中显式调用了A的构造函数。那这是为什么呢?因为有些情况下,我们可能会单独实例化B和C的对象,此时就需要在B和C中初始化A类了。因此B和C类也需要显示调用A的构造函数。

但是对于D实例化的对象来说,只会在D中对A类的资源进行初始化。

清楚了这个问题 ,接下来就简单了,之前说过,初始化列表的初始化顺序是由继承的先后顺序决定的。谁先继承,就先初始化谁。而在这里,继承的先后顺序:A、B、C;

因此,初始化列表的初始化顺序:A、B、C

即最后的答案就是 class A class B class C class D

3. inline 函数可以是虚函数吗

我们之前学习过 inline 函数,内联函数的特点就是:

会在调用的地方展开,潜台词就是没有函数地址,而虚函数的地址会进入虚函数表,那么既然内联函数都没有地址了,也就不可能是虚函数了。

因此我们的结论就是:inline不可以是虚函数。

但是,结果不是这样:

class A
{
public:inline virtual void Func(){std::cout << "haha" << std::endl;}
};void Test22()
{A a;a.Func();
}

现象如下: 

当我们用A实例化的对象a去调用 Func() 时,发现不仅没有编译报错,还能正常调用 Func()。

那是不是我们分析错了呢?

  • 首先,内联函数我们当初学的时候说过,inline 只是一个建议,具体这个函数最后会不会是一个内联函数是由编译器决定的;
  • 具体就是,如果编译器认为这个函数是符合需求的 (如没有递归,且代码量很少) 那么编译器就会将这个函数声明为 inline 函数,会在调用的地方展开该函数。

因此,最后的结论就是,inline函数可以是虚函数,但是这个 inline 是否会有效,即 inline 函数是否会在调用的地方展开,就不一定了,测试 demo 如下

class A
{
public:inline virtual void Func(){std::cout << "haha" << std::endl;}
};class B
{
public:virtual void Func() { std::cout << "hehe" << std::endl; }
};void Test23(void)
{A* ptr = new B;// 多态调用ptr->Func();A a;// 普通调用a.Func();
}

 现象如下:

多态调用,但此时函数未被展开,即 inline 无效。

 普通调用,此时函数就被展开了,inline 有效。

可以看到,如果一个虚函数被声明为 inline 时:

  • 如果这个函数是多态调用,inline 就会失效;
  • 如果这个函数是普通调用,inline 就会有效,但最后该函数会不会被展开 (inline是否有效) 是由编译器决定的。

总而言之,inline 函数可以是虚函数。

4. 静态成员函数可以是虚函数吗

测试 demo 如下:

class A
{
public:static virtual void Func(){std::cout << "haha" << std::endl;}
};void Test24(void)
{A a;a.Func();
}

现象如下:

 可以看到,静态成员函数不可以是虚函数,为什么呢?

  • 首先,静态成员函数是没有 this 指针的 (因为它属于整个类,而不属于某个对象),没有 this 指针就无法访问对象中的虚表指针,也就无法找到虚表;
  • 而虚函数存在的价值就是为了构成多态,而静态成员函数都无法访问虚表,怎么能构成多态呢? 因此,将虚函数声明为静态函数是无意义的,编译器进行了强制检查,如果一个虚函数是静态的,那么会编译报错。

总而言之,静态成员函数不可以是虚函数。

5. 构造函数可以是虚函数吗

测试 demo 如下:

class A
{
public:virtual A() { std::cout << "A()" << std::endl; }
};void Test25(void)
{A a;
}

现象如下: 

可以看到,发生了编译报错,构造函数不可以是虚函数,为什么呢?

首先我们需要搞明白一个问题:对象中的虚表指针是在什么时候创建好的呢? 测试 demo 如下:

class A
{
public:A() { std::cout << "A()" << std::endl; }virtual void Func()  { std::cout << "haha" << std::endl; }
};void Test25(void)
{A a;
}

启动进程,调出监视窗口,如下: 

可以看到,当 A 实例化的对象 a 还没有进入构造函数之前,具体在初始化列表之前,虚表指针是没有被初始化的。 

可以看到对象中的虚表的指针是在初始化列表阶段中才进行初始化的。

那么也就是说先在初始化列表中初始化虚表指针,但如果此时将构造函数声明为虚函数,而虚函数的多态调用,需要到虚表去找,但是此时虚表指针都没有被初始化,怎么找到虚表呢?此时就出问题了。

因此如果将构造函数定义为虚函数,那么此时构造函数无法进入虚表 (找不到虚表),换言之,构造函数不可以是虚函数。

6. 析构函数可以是虚函数吗

可以,并且最好是将析构函数定义为虚函数。

因为这样就可以做到,如果我指向的是一个基类,调用的就是基类的析构;如果我指向的是一个派生类,调用的是派生类的析构,可以做到合理释放资源。

7. 拷贝构造和赋值运算符重载可以是虚函数吗

拷贝构造不可以是虚函数,因为拷贝构造函数也是一个构造函数,原因与构造函数类似;

赋值运算符重载可以是虚函数,因为调用赋值的两个对象是已经存在的对象,既然已经存在的对象,如果有虚函数,那么虚表的指针是被初始化过了的,也就是说赋值运算符重载可以进入虚表,虽然赋值运算重载可以是虚函数,但是赋值运算符重载实现多态是没有实际价值的。

8. 对象访问普通函数快还是访问虚函数快

  • 如果符合多态调用,访问普通函数快,因为此时调用虚函数是一个运行时决议,需要去虚表中找虚函数的地址;
  • 如果符合普通调用,且此时调用虚函数是一个编译时决议,那么一样快。

9. 虚函数表是什么阶段生成的?存在哪里的?

构造函数中的初始化列表阶段初始化的是虚函数表的指针(虚表指针是存于对象中的),不是虚函数表,虚函数表是编译阶段时生成的。

那虚函数表存在哪里呢?

首先看看虚拟进程地址空间,具体如下:

我们用下面的 demo 验证下虚表的大概位置:

class A
{
public:A() {}virtual void Func() { std::cout << "Func()" << std::endl; }
};int global_val = 10;int main()
{A a;// 代码段的地址printf("code address: %p\n", main);// 字符常量区的地址const char* str = "haha\n";printf("string address: %p\n", str);// 静态区的地址static int i = 0;printf("static address: %p\n", &i);// 全局变量的地址printf("global address: %p\n", &global_val);// 虚表的地址printf("vft_ptr: %p\n", *(int*)(&a));return 0;
}

运行结果如下: 

可以看到, 虚表指针是在代码段和字符常量区之间的,事实上,菱形虚拟继承中的虚基表也是在这个范围之间的。

最后,再补充一句:

  • 对象中只有虚表指针,而无虚表;
  • 虚表指针是在类的构造函数中初始化的,而虚表是在编译阶段就生成了的。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/701476.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

宿舍管理系统代码详解(主页面)

本篇将对管理系统的主页面的代码进行详细的介绍。 目录 一、主页面前端代码 1.样式展示 2.代码详解 &#xff08;1&#xff09;template部分 &#xff08;2&#xff09;script部分 &#xff08;3&#xff09;路由导航守卫 &#xff08;4&#xff09;在vue中引用vue 一、主页…

iOS--底层学习--GCD的简单认识

iOS--底层学习--GCD的简单认识 前言什么是GCDGCD的优点GCD中的任务和队列任务队列 GCD的使用队列的创建和获取任务的创建队列嵌套任务和队列中的一些要点 GCD线程间的通信从后台线程切换到主线程通过队列传递数据使用Dispatch Group进行线程间协调 GCD的方法dispatch_barrier_a…

长事务的理解和预防

我们常常听说数据库发生了“长事务”而导致很严重的后果。那么何为长事务&#xff1f;长事务是如何产生的&#xff1f;长事务对数据库有什么影响&#xff1f;如何防止长事务的产生&#xff1f;以下对这几方面进行阐述和说明&#xff0c;以加深对SinoDB长事务的理解。 1&#x…

Oracle 流stream将删除的数据保存

Oracle 流stream将删除的数据保存 --实验的目的是捕获hr.employees表的删除行&#xff0c;将删除行插入到emp_del表中。 --设置初始化参数 AQ_TM_PROCESSES1 COMPATIBLE9.2.0 LOG_PARALLELISM1 --查看数据库的名称&#xff0c;我的为ora9,将以下的ora9全部替换为你的数据库名称…

怎么给视频加水印?2招轻松搞定

在数字媒体时代&#xff0c;视频水印作为一种有效的版权保护手段&#xff0c;被广泛应用于各种场景。给视频添加水印不仅可以防止内容被恶意盗用&#xff0c;还能增加视频的辨识度&#xff0c;提升品牌形象。本文将为您介绍2种简单易行的方法&#xff0c;教您怎么给视频加水印&…

Cartographer前后端梳理

0. 简介 最近在研究整个SLAM框架的改进处&#xff0c;想着能不能从Cartographer中找到一些亮点可以用于参考。所以这一篇博客希望能够梳理好Cartographer前后端优化&#xff0c;并从中得到一些启发。carto整体是graph-based框架&#xff0c;前端是scan-map匹配&#xff0c;后端…

SpringBoot 3.2.5 + ElasticSearch 8.12.0 - SpringData 开发指南

目录 一、SpringData ElasticSearch 1.1、环境配置 1.2、创建实体类 1.3、ElasticSearchTemplate 的使用 1.3.1、创建索引库&#xff0c;设置映射 1.3.2、创建索引映射注意事项 1.3.3、简单的 CRUD 1.3.4、三种构建搜索条件的方式 1.3.5、NativeQuery 搜索实战 1.3.6…

鸿蒙开发之跨设备文件访问

分布式文件系统为应用提供了跨设备文件访问的能力&#xff0c;开发者在多个设备安装同一应用时&#xff0c;通过基础文件接口&#xff0c;可跨设备读写其他设备该应用分布式文件路径&#xff08;/data/storage/el2/distributedfiles/&#xff09;下的文件。 例如&#xff1a;多…

一文读懂通用漏洞评分系统CVSS4.0:顺带理清CVE、CWE及其与CVSS之间的关系

事件响应和安全团队论坛 (FIRST&#xff0c;Forum of Incident Response and Security Teams) 于 2023 年 11 月 1 日正式推出第四版通用漏洞评分系统 (CVSS 4.0&#xff0c;Common Vulnerability Scoring System version 4.0)。CVSS 4.0 是评估计算机系统安全漏洞严重性的行业…

申请免费的必应搜索API

申请免费的必应搜索API 文章目录 申请免费的必应搜索API前言一、原理1.1 登录1.2 进入1.3 获取密钥1.4 申请VISA信用卡1.5 创建必应自定义搜索资源 二、创建成功 前言 准备条件&#xff1a; 1、outlook邮箱 2、招商银行全币种VISA信用卡【建议之前就有一张招商银行信用卡&…

服务器通的远程桌面连接不上,关于服务器通畅但远程桌面连接不上问题的专业分析

在日常的企业IT管理中&#xff0c;服务器远程桌面连接是一个重要的操作功能。然而&#xff0c;有时会出现服务器网络通畅&#xff0c;但远程桌面无法连接的情况。 问题分析 1. 防火墙或安全组设置问题&#xff1a;服务器的防火墙或安全组可能阻止了远程桌面连接的端口&#xf…

QCC30xx 开发板如何测试待机电流

高通的通用蓝牙开发板底CF376上&#xff0c;有各种各样的外围电路与芯片&#xff0c;组成一整套完整的开发板平台&#xff0c;但客户通常只关心其中蓝牙芯片的各个状态下的工作电流&#xff0c;本文就介绍如何在CF376底板上&#xff0c;通过断开其它非必要电路 &#xff0c;去测…