某日二师兄加入 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++!(狗头)