我的编程空间,编程开发者的网络收藏夹
学习永远不晚

C++的多态和虚函数你真的了解吗

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

C++的多态和虚函数你真的了解吗

一、C++的面试常考点

阿里虽然是国内Java的第一大厂但是并非所有的业务都是由Java支撑,很多服务和中下层的存储,计算,网络服务,大规模的分布式任务都是由C++编写。在阿里所有部门当中对C++考察最深的可能就是阿里云。

阿里对C++的常考点:

1.STL 容器相关实现

2.C++新特性的了解

3.多态和虚函数的实现

4.指针的使用

二、阿里真题

2.1 真题一

现在假设有一个编译好的C++程序,编译没有错误,但是运行时报错,报错如下:你正在调用一个纯虚函数(Pure virtual function call error),请问导致这个错误的原因可能是什么?

纯虚函数调用错误一般由以下几种原因导致:

  • 从基类构造函数直接调用虚函数。(直接调用是指函数内部直接调用虚函数)
  • 从基类析构函数直接调用虚函数。
  • 从基类构造函数间接调用虚函数。(间接调用是指函数内部调用其他的非虚函数,其内部直接或间接地调用了虚函数)
  • 从基类析构函数间接调用虚函数。
  • 通过悬空指针调用虚函数。

注意:其中1,2编译器会检测到此类错误。3,4,5编译器无法检测出此类情况,会在运行时报错。

(1)虚函数表vtbl

编译器在编译时期为每个带虚函数的类创建一份虚函数表

实例化对象时, 编译器自动将类对象的虚表指针指向这个虚函数表

(2)构造一个派生类对象的过程

1.构造基类部分:

  • 构造虚表指针,将实例的虚表指针指向基类的vtbl
  • 构造基类的成员变量
  • 执行基类的构造函数函数体

2.递归构造派生类部分:

  • 将实例的虚表指针指向派生类vtbl
  • 构造派生类的成员变量
  • 执行派生类的构造函数体

(3)析构一个派生类对象的过程

1.递归析构派生类部分:

  • 将实例的虚表指针指向派生类vtbl
  • 执行派生类的析构函数体
  • 析构派生类的成员变量(这里的执行函数体,析构派生类成员变量,两者的顺序和构造的步骤是相反的)

2.析构基类部分:

  • 将实例的虚表指针指向基类的vtbl
  • 执行基类的析构函数函数体
  • 析构基类的成员变量

构造函数和析构函数执行函数体时,实例的虚函数表指针,指向构造函数和析构函数本身所属的类的虚函数表,此时执行的虚函数,即调用的本身的该类本身的虚函数,下面是一个【间接调用】的栗子:基类中的析构函数中,调用纯虚函数(该虚函数就在基类中定义)。

#include <iostream>
using namespace std;

class Parent {
public:
	//纯虚函数
    virtual void virtualFunc() = 0;
    void helper() {
        virtualFunc();
    }
    virtual ~Parent(){
        helper();
    }
};

class Child : public Parent{
    public:
    void virtualFunc() {
        cout << "Child" << endl;
    }
    virtual ~Child(){}
};


int main() {
    Child child;
	//system("pause");
    return 0;
}

运行时报错libc++abi.dylib: Pure virtual function called

在这里插入图片描述

2.2 真题二

在构造实例过程当中一部分是初始化列表一部分是在函数体内,你能说一下这些的顺序是什么?差别是什么和this指针构造的顺序

顺序:

(1)初始化列表中的先初始化。

(2)执行函数体代码。

  • 执行类中函数体,如执行构造函数时,所有成员已经初始化完毕了;
  • this指针属于对象,而对象还没构造完成前,若使用this指针,编译器会无法识别。在初始化列表中显然不能使用this指针,注意:在构造函数体内部可以使用this指针。

构造函数的执行可以分成两个阶段:

  • 初始化阶段:所有类类型的成员都会在初始化阶段初始化,即使该成员没有出现在构造函数的初始化列表中。
  • 计算赋值阶段:一般用于执行构造函数体内的赋值操作。
#include <iostream>
using namespace std;

class Test1 {
public:
    Test1(){
    	cout << "Construct Test1" << endl;
    }
	//拷贝构造函数
    Test1& operator = (const Test1& t1) {
    	cout << "Assignment for Test1" << endl;
        this->a = t1.a;
        return *this;
    }
    int a ;
};

class Test2 {
public:
    Test1 test1;
	//Test2的构造函数
    Test2(Test1 &t1) {
    	cout << "构造函数体开始" << endl;
        test1 = t1 ;
        cout << "构造函数体结束" << endl;
    }
};

int main() {
    Test1 t1;
    Test2 test(t1);
	system("pause");
    return 0;
}

在这里插入图片描述

分析上面的结果:

(1)第一行结果即Test t1实例化对象时,执行Test1的构造函数;

(2)第二行代码,实例化Test2对象时,在执行Test2构造函数时,正如上面所说的,构造函数的第一步是初始化阶段:所有类类型的成员都会在初始化阶段初始化,即使该成员没有出现在构造函数的初始化列表中。所以Test2在构造函数体执行之前已经使用了Test1的默认构造函数初始化好了t1。打印出Construct Test1

这里的拷贝构造函数中可以使用this指针,指向当前对象。

(3)第三四五行结果:执行Test2的构造函数。

2.3 真题三

初始化列表的写法和顺序有没有什么关系?

构造函数的初始化列表中的前后位置,不影响实际标量的初始化顺序。成员初始化的顺序和它们在类中的定义顺序一致。

必须使用初始化列表的情况:数据成员是const、引用,或者属于某种未提供默认构造函数的类类型。

2.4 真题四

在普通的函数当中调用虚函数和在构造函数当中调用虚函数有什么区别?

普调函数当中调用虚函数是希望运行时多态。而在构造函数当中不应该去调用虚函数因为构造函数当中调用的就是本类型当中的虚函数,无法达到运行时多态的作用。

2.5 真题五

成员变量,虚函数表指针的位置是怎么排布?

如果一个类带有虚函数,那么该类实例对象的内存布局如下:

  • 首先是一个虚函数指针,
  • 接下来是该类的成员变量,按照成员在类当中声明的顺序排布,整体对象的大小由于内存对齐会有空白补齐。
  • 其次如果基类没有虚函数但是子类含有虚函数:
    • 此时内存子类对象的内存排布也是先虚函数表指针再各个成员。

如果将子类指针转换成基类指针此时编译器会根据偏移做转换。在visual studio,x64环境下测试,下面的Parent p = Child();是父类对象,由子类来实例化对象。

#include <iostream>
using namespace std;

class Parent{
public:
    int a;
    int b;
};

class Child:public Parent{
public:
    virtual void test(){}
    int c;
};

int main() {
    Child c = Child();
    Parent p = Child();
    cout << sizeof(c) << endl;//24
    cout << sizeof(p) << endl;//8

    Child* cc = new Child();
    Parent* pp = cc;
    cout << cc << endl;//0x7fbe98402a50
    cout << pp << endl;//0x7fbe98402a58
	cout << endl << "子类对象abc成员地址:" << endl;
    cout << &(cc->a) << endl;//0x7fbe98402a58
    cout << &(cc->b) << endl;//0x7fbe98402a5c
    cout << &(cc->c) << endl;//0x7fbe98402a60
	system("pause");
    return 0;
}

结果如下:

24
8
0000013AC9BA4A40
0000013AC9BA4A48

子类对象abc成员地址:
0000013AC9BA4A48
0000013AC9BA4A4C
0000013AC9BA4A50
请按任意键继续. . .

分析上面的结果:

(1)第一行24为子类对象的大小,首先是虚函数表指针8B,然后是2个继承父类的int型数值,还有1个是该子类本身的int型数值,最后的4是填充的。

(2)第二行的8为父类对象的大小,该父类对象由子类初始化,含有2个int型成员变量。

(3)子类指针cc指向又new出来的子类对象(第三个),然后父类指针pp指向这个子类对象,这两个指针的值:

  • 父类指针pp值:0000013AC9BA4A48
  • 子类指针cc值:0000013AC9BA4A40

即发现如之前所说的:如果将子类指针转换成基类指针此时编译器会根据偏移做转换。我测试环境是64位,所以指针为8个字节。转换之后pp和cc相差一个虚表指针的偏移。

(4)&(cc->a)的值即 0000013AC9BA4A48,和pp值是一样的,注意前面的 0000013AC9BA4A40到0000013AC9BA4A47其实就是子类对象的虚函数表指针了。

三、小结

阿里常考的C++的问题集中在以下几点:

  • 虚函数的实现
  • 虚函数使用出现的问题原因
  • 带有虚函数的类对象的构造和析构过程
  • 对象的内存布局
  • 虚函数的缺点:相比普通函数,虚函数调用需要2次跳转(即需要先找到对象的虚函数表,再查找该表项,即虚函数指针,即真正的虚函数地址),会降低CPU缓存的命中率。运行时绑定,编译器不好优化。

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注编程网的更多内容!    

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

C++的多态和虚函数你真的了解吗

下载Word文档到电脑,方便收藏和打印~

下载Word文档

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录