阿里云的一道有趣的面试题(关于纯虚函数)

条款09:绝不在构造和析构过程中调用virtual函数    ——《Effective C++》 昨 …

2019年3月23日

条款09:绝不在构造和析构过程中调用virtual函数    ——《Effective C++》

昨天有大神面完阿里云(据说面试地狱难度),发了几道面试的题目,不得不说,确实有新意,能把老掉牙的问题挖的如此之深。

有道题是这样的。

一个程序编译没问题,没报错,但是运行的时候崩溃了,原因是调用了纯虚函数,你觉得是啥原因。

其他题看到基本都有思路,这道题懵了很久。

刚开始想的是可能是利用虚函数指针找到虚函数表然后通过虚函数槽调用这个纯虚函数,但是一想,不对啊,拥有纯虚函数的类不能被实例化,那么去哪里找虚函数指针呢?

然后就开始无尽的google之旅。下面是我认为可能的一种答案。

首先,纯虚函数是可以有函数体并且是可以调用的。如下:

class base
{
public:
  virtual void foo() = 0;
};

void base::foo()
{
  cout << "base" << endl;
}

class derive: public base
{
public:
  void foo()
  {
    cout << "derive" << endl;
  }
};

int main(int argc, char const *argv[])
{
  derive* b = new derive();
  b->base::foo();
  return 0;
}

没错,上述代码将调用base的纯虚函数foo(),打印”base”(注意,此处的foo()是静态调用)。到这里又开始感慨C++真是一门神奇的语言。

长久以来受教科书的影响(这里并不是说教科书不好,纯虚函数确实是按照这种思维设计的),一直认为纯虚函数只是一个接口,不能拥有函数体且无法被调用。

之后的知识点倒是在我的认知范围内。在子类的构造函数和析构函数里在父类的构造时调用虚函数会有意想不到的结果。看下面这段代码

class base
{
public:
  base() {foo();}
  virtual void foo() { n = 1;}
  int n;
};


class derive: public base
{
public:
  derive(): base() {}
  virtual void foo() { n = 2;}
  void getn() {cout << n << endl;}
};

int main(int argc, char const *argv[])
{
    derive* d=new derive();
    d->getn();
    return 0;
}

n的值将被赋值为1,尽管foo()是一个虚函数,尽管是在derive的构造函数里。但其实也很好理解,子类的构造之前需要对构造并初始化基类的成员,于是调用基类的构造函数,此时虚函数的调用将不再绑定在子类中,而是回归到基类的函数实现。

到这里其实离上面那个问题的答案已经不远了,但是深吸一口气,最为难以理解的地方来了(至少我认为是这样)。

class Base
{
private:
  virtual void foo() = 0;
public:
  Base()
  {
    //Base * bptr = (Base*)this;
    //bptr->foo();
    this->foo();
  }
};

void
Base::foo()
{
  cout << "Base::foo()=0" << endl;
}

class Derive : public Base
{
public:
  Derive() {}
public:
  void foo()
  {
    cout << "Derive::foo()" << endl;
  }
};

int main()
{
  Derive d;
}

注意8、9行和第10行的区别,在之前看到Base的构造函数里this指针肯定也是指向的Base,因此第10行的纯虚函数调用将是一个静态调用,这在之前的讨论中是允许的,但是如果把第10行注释掉,换成8、9行,则编译不会出错,但是运行时会报pure virtual method called error,即纯虚函数调用错误。

乍一看这个替换没有啥区别,其实原因在于第8行的转换是动态类型的转换,是在运行时才识别的转换类型,此时再调用纯虚函数自然会失败,因为纯虚函数不允许动态调用。

到这里这个面试题就算解决了,其实《Effective c++》条款9明细了在构造或析构函数里调用虚函数的一系列后果(尽管是可以的,而且没有任何警告),而对于纯虚函数,在调用时编译器就会发出警告,手册里也指出在构造函数里使用纯虚函数的行为是UB(undefined-behavior,未定义行为)。但对于程序员而言,理解其内部的实现机制是避不开的,这里又感慨c++给面试出题提供了太多便利,随便找一个特性深入一下就能挖到不少东西。

最后的感想,想把简历上的c++熟练度改成入门了…

共有 0 条评论