史上最全面的C++面试题多态篇(内含详细解析)

本文详细介绍了C++中的多态性,包括什么是多态、虚析构函数的作用、虚表和虚表指针的概念,以及纯虚函数和抽象类接口。此外,还讨论了移动语义、完美转发以及内联函数、静态成员和构造函数不能作为虚函数的原因。最后提到了使用`final`和`override`关键字来控制虚函数的重写。
摘要由CSDN通过智能技术生成

目录

1. 多态篇

1.1 什么是多态

1.2 虚析构函数

1.3 虚表和虚表指针

1.4 纯虚函数

1.5 抽象类接口

1.6 移动语义和完美转发

1-7 为什么默认的析构函数不是虚函数?

1-8 内联(inline)函数可以是虚函数吗?

1-9 静态成员可以是虚函数吗?

1-10 构造函数可以是虚函数吗?

1-11 如何让父类的虚函数无法被重写–final

1-12 override


1. 多态篇

1.1 什么是多态

所谓多态性,顾名思义就是“多个性态”。更具体一点的就是,用一个名字定义多个函数,这些函数执行不同但相似的工作。最简单的多态性的实现方式就是函数重载和模板,这两种属于静态多态性。还有一种是动态多态性,其实现方式就是继承虚函数重写。

1.2 虚析构函数

如果没有虚析构函数,父类指针delete对象就只会调用父类的析构函数,如果加上虚析构函数的话,那么,析构父类指针指向的子类对象时候会先调用子类的析构函数,再调用父类的构造函数。如果一个类会被其他类继承,那么我们有必要将被继承的类(基类)的析构函数定义成虚函数。这样,释放基类指针指向的派生类实例时,清理工作才能全面进行,才不会发生内存泄漏。

如果严格遵守多态的构成条件,那么子类析构函数就算定义成虚函数也无法完成重写了,因为父子类的析构函数是不可能同名的。

编译器做了一件事,凡是父子类的析构函数,都将父子类的析构函数名变成destructor(),其目的就是为了父子类的析构函数可以构成多态。

1.3 虚表和虚表指针

多态的本质就是虚表和虚表指针,虚表本质上是一个数组,存放着所有虚函数的指针。如果父类的虚函数没有被子类改写,那么子类虚函数表的指针就是父类对应的虚函数的指针;否则,虚表的指针是子类虚函数的指针。这个过程在程序运行过程中执行,被称为“动态绑定”;

1.4 纯虚函数

虚函数和纯虚函数两者的区别在于纯函数尚未被实现,定义纯虚函数是为了实现一个接口。在基类中定义纯虚函数的方法是在函数原型后加=0

virtual void function() = 0;

1.5 抽象类接口

含有虚函数的类叫做抽象类,设计抽象类的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。如果试图实例化一个抽象类的对象,会导致编译错误。

1.6 移动语义和完美转发

移动语义:移动语义是为了浅拷贝和深拷贝产生的,浅拷贝会出现两个对象占用一地址,会出现内存泄露。深拷贝开辟两个内存分别存放,造成资源浪费。移动语义则采用的是偷窃的方法,占为己有。为了保证移动语义, 必须记得用std::move 转化左值对象为右值,以避免调用复制构造函数.

万能引用:C++11 标准中规定,通常情况下右值引用形式的参数只能接收右值,不能接收左值。但对于函数模板中使用右值引用语法定义的参数来说,它不再遵守这一规定,既可以接收右值,也可以接收左值在函数模板中的右值引用又被称为万能引用。

template <typename T>
void function(T&& t) {
    otherdef(t);
}

引用折叠:当实参为左值或者左值引用(A&)时,函数模板中 T&& 将转变为 A&(A& && = A&);当实参为右值或者右值引用(A&&)时,函数模板中 T&& 将转变为 A&&(A&& && = A&&)。

完美转发:无论调用 function() 函数模板时传递给参数 t 的是左值还是右值,对于函数内部的参数 t 来说,它有自己的名称,也可以获取它的存储地址,因此它永远都是左值,也就是说,传递给 otherdef() 函数的参数 t 永远都是左值。无论从那个角度看,function() 函数的定义都不“完美”。完美转发这样严苛的参数传递机制,C++98标准中几乎不会用到,但 C++11 标准为 C++ 引入了右值引用和移动语义,因此很多场景中是否实现完美转发,直接决定了该参数的传递过程使用的是拷贝语义(调用拷贝构造函数)还是移动语义(调用移动构造函数)。C++11新标准还引入了一个模板函数 forword<T>(),我们只需要调用该函数,就可以很方便地将函数模板接收到的形参连同其左、右值属性,一起传递给被调用的函数

1-7 为什么默认的析构函数不是虚函数?

那么既然基类的析构函数如此有必要被定义成虚函数,为何类的默认析构函数却是非虚函数呢?
原来是因为,虚函数不同于普通成员函数,当类中有虚成员函数时,类会自动进行一些额外工作。这些额外的工作包括生成虚函数表和虚表指针,虚表指针指向虚函数表。每个类都有自己的虚函数表,虚函数表的作用就是保存本类中虚函数的地址,我们可以把虚函数表形象地看成一个数组,这个数组的每个元素存放的就是各个虚函数的地址。这样一来,就会占用额外的内存,当们定义的类不被其他类继承时,这种内存开销无疑是浪费的。

这样一说,问题就不言而喻了。当我们创建一个类时,系统默认我们不会将该类作为基类,所以就将默认的析构函数定义成非虚函数,这样就不会占用额外的内存空间。同时,系统也相信程序开发者在定义一个基类时,会显示地将基类的析构函数定义成虚函数,此时该类才会维护虚函数表和虚表指针。

1-8 内联(inline)函数可以是虚函数吗?

答:不能,因为内联(inline)函数没有地址,无法把地址放到虚函数表中。

1-9 静态成员可以是虚函数吗?

答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

1-10 构造函数可以是虚函数吗?

答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

从存储空间角度
虚函数对应一个vtable,可是这个vtable其实是存储在对象的内存空间的。
那么问题来了,如果构造函数是虚函数,就要通过vtable来调用,可是对象空间还没有实例化,也就是内存空间还没有,无法找到vtable,所以构造函数不能是虚函数。

从使用角度
虚函数主要用于在信息不全的情况下,能够使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义。另外,虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数,从而实现多态,也就是实现“一个接口,多种方法”。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此规定构造函数不能是虚函数。

1-11 如何让父类的虚函数无法被重写–final

class Car 
{
public:
    virtual void Drive() final {}
};
​
class Benz :public Car
{
public:
    virtual void Drive() { cout << "Benz-舒适" << endl; }
};

当我们在虚函数的后面加上final关键字时,这个虚函数是无法被子类的虚函数重写的。
​所以上述代码会报错! 

1-12 override

如何强制要求完成虚函数的重写–override
如果C语言和C++看作是两兄弟,那么C语言更像是一个自由自在的孩子,而C++更像一个有规矩的孩子,例如C++中有封装,不能随便访问类的成员变量
而override就像一个监督你有没有完成虚函数的重写的老师一样,如果在函数后面加了override又没有完成虚函数的重写,程序就会报错

有待补充,敬请期待!

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值