当前位置:主页 > 软件编程 > C代码 >

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

时间:2022-08-27 09:39:54 | 栏目: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.构造基类部分:

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

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

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

2.析构基类部分:

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

#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)执行函数体代码。

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

#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指向这个子类对象,这两个指针的值:

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

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

三、小结

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

总结

您可能感兴趣的文章:

相关文章