C++面试八股文:override和finial关键字有什么作用?
某日二师兄参加XXX科技公司的C++工程师开发岗位第22面: (二师兄好苦逼,节假日还在面试。。。)
面试官:C++的继承了解吗?
二师兄:(不好意思,你面到我的强项了。。)了解一些。
面试官:什么是虚函数,为什么需要虚函数?
二师兄:虚函数允许在基类中定义一个函数,然后在派生类中进行重写(
override)。二师兄:主要是为了实现面向对象中的三大特性之一多态。多态允许在子类中重写父类的虚函数,同样的函数在子类和父类实现不同的形态,简称为多态。
面试官:你知道
override和finial关键字的作用吗?二师兄:
override关键字告诉编译器,这个函数一定会重写父类的虚函数,如果父类没有这个虚函数,则无法通过编译。此关键字可省略,但不建议省略。二师兄:
finial关键字告诉编译器,这个函数到此为止,如果后续有类继承当前类,也不能再重写此函数。二师兄:这两个关键字都是C++11引入的,为了提升C++面向对象编码的安全性。
面试官:你知道多态是怎么实现的吗?
二师兄:(起开,我要开始装逼了!)C++主要使用了虚指针和虚表来实现多态。在拥有虚函数的对象中,包含一个虚指针(
virtual pointer)(一般位于对象所在内存的起始位置),这个虚指针指向一个虚表(virtual table),虚表中记录了虚函数的真实地址。
#include <iostream>
struct Foo
{
size_t a = 42;
virtual void fun1() {std::cout <<"Foo::fun1" << std::endl;}
virtual void fun2() {std::cout <<"Foo::fun2" << std::endl;}
virtual void fun3() {std::cout <<"Foo::fun3" << std::endl;}
};
struct Goo: Foo{
size_t b = 1024;
virtual void fun1() override {std::cout <<"Goo::fun1" << std::endl;}
virtual void fun3() override {std::cout <<"Goo::fun3" << std::endl;}
};
using PF = void(*)();
void test(Foo* pf)
{
size_t* virtual_point = (size_t*)pf;
PF* pf1 = (PF*)*virtual_point;
PF* pf2 = pf1 + 1; //偏移8字节 到下一个指针 fun2
PF* pf3 = pf1 + 2; //偏移16字节 到下下一个指针 fun3
(*pf1)(); //Foo::fun1 or Goo::fun1 取决于pf的真实类型
(*pf2)(); //Foo::fun2
(*pf3)(); //Foo::fun3 or Goo::fun3 取决于pf的真实类型
}
int main(int argc, char const *argv[])
{
Foo* fp = new Foo;
test(fp);
fp = new Goo;
test(fp);
size_t* virtual_point = (size_t*)fp;
size_t* ap = virtual_point + 1;
size_t* bp = virtual_point + 2;
std::cout << *ap << std::endl; //42
std::cout << *bp << std::endl; //1024
}

二师兄:当初始化虚表时,会把当前类override的函数地址写到虚表中(
Goo::fun1、Goo::fun3),对于基类中的虚函数但是派生类中没有override,则会把基类的函数地址写到虚表中(Foo::fun2),在调用函数的时候,会通过虚指针转到虚表,并根据虚函数的偏移得到真实函数地址,从而实现多态。面试官:不错。上图你画出了单一继承的内存布局,那多继承呢?
二师兄:多继承内存布局类似,只不过会多几个
virtual pointer。
#include <iostream>
struct Foo1
{
size_t a = 42;
virtual void fun1() {std::cout <<"Foo1::fun1" << std::endl;}
virtual void fun2() {std::cout <<"Foo1::fun2" << std::endl;}
virtual void fun3() {std::cout <<"Foo1::fun3" << std::endl;}
};
struct Foo2{
size_t b = 1024;
virtual void fun4() {std::cout <<"Foo2::fun4" << std::endl;}
virtual void fun5() {std::cout <<"Foo2::fun5" << std::endl;}
};
struct Foo3{
size_t c = 0;
virtual void fun6() {std::cout <<"Foo3::fun1" << std::endl;}
virtual void fun7() {std::cout <<"Foo3::fun3" << std::endl;}
};
struct Goo: public Foo1, public Foo2, public Foo3
{
virtual void fun2() override {std::cout <<"Goo::fun2" << std::endl;}
virtual void fun6() override {std::cout <<"Goo::fun6" << std::endl;}
};
int main(int argc, char const *argv[])
{
Goo g;
g.fun1(); //Foo1::fun1
g.fun2(); //Goo::fun2
g.fun3(); //Foo1::fun3
g.fun4(); //Foo2::fun4
g.fun5(); //Foo2::fun5
g.fun6(); //Goo::fun6
g.fun7(); //Foo3::fun7
}

面试官:你知道什么是菱形继承吗?菱形继承会引发什么问题?如何解决?
二师兄:菱形继承(
Diamond Inheritance)是指在继承层次结构中,如果两个不同的子类B和C继承自同一个父类A,而又有一个类D同时继承B和C,这种继承关系被称为菱形继承。

二师兄:因为B和C各继承了一份A,当D继承B和C的时候就会有2份A;
#include <iostream>
struct A
{
int val = 42;
virtual void fun(){std::cout <<"A::fun" << std::endl;}
};
struct B: public A{ void fun() override{std::cout <<"B::fun" << std::endl;}};
struct C: public A{ void fun() override{std::cout <<"C::fun" << std::endl;}};
struct D: public B, public C{void fun() override{std::cout <<"D::fun" << std::endl;}};
int main(int argc, char const *argv[])
{
D d;
std::cout << d.val << std::endl; //编译失败,不知道调用从哪个类中继承的val变量
d.fun(); //编译失败,不知道调用从哪个类中继承的fun函数
}
二师兄:解决的办法有两种,一种是在调用符之前加上父类限定符:
std::cout << d.B::val << std::endl; //42
d.C::fun(); //C::fun
二师兄:但这里并没有解决数据冗余的问题,因为D中有B和C,而B和C各有一个虚表和一个int类型的成员变量,所以
sizeof(D)的大小是32(x86_64架构,考虑到内存对齐)。二师兄:所幸在C++11引入了虚继承(
Virtual Inheritance)机制,从源头上解决了这个问题:
#include <iostream>
struct A
{
int val = 42;
virtual void fun(){std::cout <<"A::fun" << std::endl;}
};
struct B: virtual public A{ void fun() override{std::cout <<"B::fun" << std::endl;}};
struct C: virtual public A{ void fun() override{std::cout <<"C::fun" << std::endl;}};
struct D: public B, public C{void fun() override{std::cout <<"D::fun" << std::endl;}};
int main(int argc, char const *argv[])
{
D d;
std::cout << d.val << std::endl; //42
d.fun(); //D::fun
}
二师兄:此时在对象
d中,只包含了一个val和两个虚指针,成员变量的冗余问题得到解决。面试官:一般我们认为多态会影响性能,你举得为什么影响性能?
二师兄:大多数人认为,虚函数的调用会先通过虚指针跳到虚函数表,然后通过偏移确定函数真实地址,再跳转到地址执行,是间接调用导致了性能损失。
二师兄:但实际上无法内联才是虚函数性能低于正常函数的主要原因。由于多态是运行时特征,在编译时编译器并不知道指针指向的函数地址,所以无法被内联。同时跳转到特定地址执行函数可能引发的
L1 cache miss(空间局部性不好),这也会影响性能。面试官:虚函数的调用一定是非内联的吗?
二师兄:不是。现代编译器很聪明,如果编译器能够在编译时推断出真实的函数,可能会直接内联这个虚函数。虚函数的调用是否内联取决于编译器的实现和上下文。
面试官:你觉得多态在安全性上有没有什么问题?
二师兄:的确是有的。当我们把类中的虚函数定义为
private的时候,虽然我们不能通过类的对象去访问这个函数,但我们知道这个函数就在虚函数表中,可以通过特殊的方法(上文中已经给出示例)访问它:
#include <iostream>
struct Foo
{
private:
virtual void fun() {std::cout << "Foo::fun" << std::endl;}
};
int main(int argc, char const *argv[])
{
Foo f;
//f.fun(); //编译错误
using Fun = void(*)();
size_t* virtual_point = (size_t*)&f;
Fun* fun = (Fun*)*virtual_point;
(*fun)();
}
面试官:好的,今天的面试到这里就结束了,请回去等通知吧。
今天二师兄表现很不错,加个肉粽。感谢小伙伴的耐心阅读,祝各位小伙伴端午节牛逼(端午快乐->没文化,端午安康->跟风狗,好吧我祝各位端午牛逼)。二师兄的C++面试之旅,明天不见不散
关注我,带你21天“精通”C++!(狗头)
C++面试八股文:override和finial关键字有什么作用?的更多相关文章
- 使用 Override 和 New 关键字进行版本控制
使用 Override 和 New 关键字进行版本控制 C# 语言经过专门设计,以便不同库中的基类与派生类之间的版本控制可以不断向前发展,同时保持向后兼容. 这具有多方面的意义.例如,这意味着在基类中 ...
- 第2节 Scala中面向对象编程:7、继承的概念以及override和super关键字;8、isInstanceOf 和 asInstanceOf关键字
6.3. Scala面向对象编程之继承 6.3.1. Scala中继承(extends)的概念 Scala 中,让子类继承父类,与 Java 一样,也是使用 extends 关键字: 继承 ...
- 《面试八股文》之kafka21卷
微信公众号:moon聊技术 关注选择" 星标 ", 重磅干货,第一 时间送达! [如果你觉得文章对你有帮助,欢迎关注,在看,点赞,转发] 大家好,我是moon,最新一篇面试八股文系 ...
- 《面试八股文》之 Redis 16卷
微信公众号:moon聊技术 关注选择" 星标 ", 重磅干货,第一 时间送达! [如果你觉得文章对你有帮助,欢迎关注,在看,点赞,转发] 大家好,我是 moon. redis 作为 ...
- 《面试八股文》之 JVM 20卷
微信公众号:moon聊技术 关注选择" 星标 ", 重磅干货,第一 时间送达! [如果你觉得文章对你有帮助,欢迎关注,在看,点赞,转发] 大家好,我是 moon. <面试八股 ...
- java中关键字volatile的作用
用在多线程,同步变量. 线程为了提高效率,将某成员变量(如A)拷贝了一份(如B),线程中对A的访问其实访问的是B.只在某些动作时才进行A和B的同步.因此存在A和B不一致的情况.volatile就是用来 ...
- 转!!java中关键字volatile的作用
用在多线程,同步变量. 线程为了提高效率,将某成员变量(如A)拷贝了一份(如B),线程中对A的访问其实访问的是B.只在某些动作时才进行A和B的同步.因此存在A和B不一致的情况.volatile就是用来 ...
- #import和#include的区别 关键字@class的作用
一.#import和#include的区别当我们在代码中使用两次#include的时候会报错:因为#include相当于拷贝头文件中的声明内容,所以会报重复定义的错误但是使用两次#import的话,不 ...
- java中关键字volatile的作用(转载)
转载:http://blog.csdn.net/orzorz/article/details/4319055 用在多线程,同步变量. 线程为了提高效率,将某成员变量(如A)拷贝了一份(如B),线程中对 ...
- static 关键字有什么作用
static关键字的含义及使用场景 static是Java50个关键字之一.static关键字可以用来修饰代码块表示静态代码块,修饰成员变量表示全局静态成员变量,修饰方法表示静态方法.(注意:不能修饰 ...
随机推荐
- [ACM]STL-dfs
#include<iostream> using namespace std; int book[101],sum,n,e[101][101]; void dfs(int cur) { c ...
- 《HelloTester》第4期
1.前言 终于到了谈面试的部分了! 我在这也说明一下,有同学说之前简历篇的时候一直在说项目的介绍,而面试官真正关心的是技术啊?我在这做个解释,因为我写的这些文章主要针对的是软件测试的同学,所以其他职位 ...
- pandas之loc/iloc操作
在数据分析过程中,很多时候需要从数据表中提取出相应的数据,而这么做的前提是需要先"索引"出这一部分数据.虽然通过 Python 提供的索引操作符"[]"和属性操 ...
- windows如何拉取一个文件夹下的所有文件名
问题描述:遇到一个问题,是说一个文件夹下的文件太多了,如何去批量的拉去文件名呢,今天用CMD+DIR的方式拉取 1.文件目录也很深,就从文件导航栏进入CMD窗口 2.在当前目录中输入cmd,然后回车 ...
- 深谈Spring如何解决Bean的循环依赖
1. 什么是循环依赖 Java循环依赖指的是两个或多个类之间的相互依赖,形成了一个循环的依赖关系,这会导致程序编译失败或运行时出现异常.下面小岳就带大家来详细分析下Java循环依赖. 简单来讲就是:假 ...
- Gateway服务网关+过滤器
为什么需要网关 Gateway网关是我们服务的守门神,所有微服务的统一入口. 网关的核心功能特性: 请求路由 权限控制 限流 架构图: 权限控制:网关作为微服务入口,需要校验用户是是否有请求资格,如果 ...
- 绝对强大的三大linux指令:ar, nm, objdump
前言 如果普通编程不需要了解这些东西,如果想精确控制你的对象文件的格式或者你想查看一下文件对象里的内容以便作出某种判断,刚你可以看一下下面的工具:objdump, nm, ar.当然,本文不可能非常详 ...
- 深度相机(TOF)的工作原理
文章目录 深度相机(TOF)的工作原理 TOF由什么组成? 一.TOF相机采用主动光探测,通常包括以下几个部分: 二.TOF是如何测距的呢? 三.TOF会受什么影响? 四.那TOF相机最后输出的是什么 ...
- Python OOP面向对象编程
OOP 思想: 以模块思想解决工程问题 面向过程 VS 面向对象 由面向过程转向面向对象 例子,我要开一个学校,叫XXX 讲师 学生 班主任 教室 学校 常用名词 OO:面向对象 OOA: 分析 OO ...
- 大话AI绘画技术原理与算法优化
引子 博主很长一段时间都没有发文,确实是在忙一些技术研究. 如标题所示,本篇博文主要把近段时间的研究工作做一个review. 看过各种相关技术的公关文章,林林总总,水分很多. 也确实没有多少人能把一些 ...