首页 > 编程语言 >C++面试八股文:override和finial关键字有什么作用?

C++面试八股文:override和finial关键字有什么作用?

时间:2023-06-22 23:44:14浏览次数:35  
标签:面试官 虚表 函数 多态 C++ override finial 师兄

某日二师兄参加XXX科技公司的C++工程师开发岗位第22面: (二师兄好苦逼,节假日还在面试。。。)

面试官:C++的继承了解吗?

二师兄:(不好意思,你面到我的强项了。。)了解一些。

面试官:什么是虚函数,为什么需要虚函数?

二师兄:虚函数允许在基类中定义一个函数,然后在派生类中进行重写(override)。

二师兄:主要是为了实现面向对象中的三大特性之一多态。多态允许在子类中重写父类的虚函数,同样的函数在子类和父类实现不同的形态,简称为多态。

面试官:你知道overridefinial关键字的作用吗?

二师兄: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
}

file

二师兄:当初始化虚表时,会把当前类override的函数地址写到虚表中(Goo::fun1Goo::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
}

file

面试官:你知道什么是菱形继承吗?菱形继承会引发什么问题?如何解决?

二师兄:菱形继承(Diamond Inheritance)是指在继承层次结构中,如果两个不同的子类B和C继承自同一个父类A,而又有一个类D同时继承B和C,这种继承关系被称为菱形继承。

file

二师兄:因为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,师兄
From: https://www.cnblogs.com/binarch/p/17498594.html

相关文章

  • C++面试八股文:什么是左值,什么是右值?
    C++面试八股文:什么是左值,什么是右值?某日二师兄参加XXX科技公司的C++工程师开发岗位第16面:面试官:什么是左值,什么是右值?二师兄:简单来说,左值就是可以使用&符号取地址的值,而右值一般不可以使用&符号取地址。inta=42; //a是左值,可以&aint*p=&a;int*p=&42; //42是右......
  • UE5 C++ TSet
    概念TSet是一种快速容器类,用于在排序不重要的情况下存储唯一元素TSet也是值类型,支持常规复制、赋值和析构函数操作,以及其元素较强的所有权TSet被销毁时,其元素也将被销毁。键类型也必须是值类型创建TSet<FString>FruitSet;添加元素Add将提供键加入setFruitSet.......
  • UE5 C++ TMap
    概述映射的元素类型为键值对,元素类型实际上是TPair<KeyType,ElementType>,只将键用于存储和获取TMap和TMultiMap两者之间的不同点是TMap中的键是唯一的,而TMultiMap可存储多个相同的键TMap是散列容器,这意味着键类型必须支持GetTypeHash函数,并提供运算符==来比较各个键是否......
  • C++入门教程
    C++入门教程----------------------------------------------------------一.初识C++---------------------------------------------------------1.什么是C++.c++是一种较为基础的编程语言,虽然没有Python,Scratch那么高级,但是它应用范围很广.不论是信息奥赛还是国......
  • UE5 C++ TArray
    概述TArray是UE4中最常用的容器类。其速度快、内存消耗小、安全性高TArray类型由两大属性定义:元素类型和可选分配器元素类型是存储在数组中的对象类型。TArray被称为同质容器。换言之,其所有元素均完全为相同类型。单个TArray中不能存储不同类型的元素。分配器常被省略,默......
  • c++ doctest 测试报告
    地址https://github.com/laolang2016/doctest-reports效果未完成事项状态筛选没做只做了dashboard,其他两个页面还是空白四栏确实有点多了,数据应该纵向展示jquery果然还是比较墨迹,下一个版本使用vue......
  • Delete vector contents and free up memory in C++
     DeletevectorcontentsandfreeupmemoryinC++Thispostwilldiscusshowtodeletethevector’scontentsandfreeupthememoryallocatedbythevectortostoreobjectsinC++.1.Using vector::clear functionWecanusethe vector::clear......
  • visual studio 2022 c++ doxygen风格注释
    设置工具/选项/文本编辑器/C/C++/代码样式/常规注释效果输入/**添加注释鼠标停靠注释预览......
  • C++面试八股文:用过STL吗?
    某日二师兄参加XXX科技公司的C++工程师开发岗位第21面:面试官:用过STL吗?二师兄:(每天都用好吗。。)用过一些。面试官:你知道STL是什么?二师兄:STL是指标准模板库(StandardTemplateLibrary),是C++区别于C语言的特征之一。面试官:那你知道STL的六大部件是什么?二师兄:分别是容器(container......
  • C++面试八股文:了解auto关键字吗?
    C++面试八股文:了解auto关键字吗?某日二师兄参加XXX科技公司的C++工程师开发岗位第15面:面试官:了解auto关键字吗?二师兄:嗯,了解一些(我很熟悉)。面试官:说一说auto的用法吧?二师兄:auto主要是为了编译器进行类型推导。比如:autoi=42; //i被推导位int型std::vector<int>vi......