C++真题,面试现场翻车......

文摘   2024-07-16 12:36   上海  

这是笔者最近面试一家量化投资公司的真题。

笔者当时面试时没有做出来,与offer失之交臂。

最近面试实在是找不到方向。有的公司考C++语法,工程方向;有的公司考算法题;有的公司考CUDA,追问的比较深,只会入门的那点皮毛知识通不过面试;有的公司考模型量化的原理,唉!我一般就是会用现成的东西,极少去探究内部的深层原理。

本篇文章用来记录这道题目。

这道题目的经过是这样的。面试官让我在开发环境里写一个C++类的继承。我写一个父类A,和一个继承自类A的类B,并且实现析构函数,在析构函数里打印一些信息。写完后,面试官让我创建一个A类指针和new一个B类对象,并且将A类指针指向B类对象。然后,让我delete掉A类指针,看看输出什么。当时忘了加virtual关键字,结果没有调用类B的析构函数。

还原一下写的代码。

class A
{
public:
    ~A() { cout << "A" << endl; }
}

class B : public A
{
public:
    ~B() { cout << "B" << endl; }
}

int main()
{
    A* ptr = new B;
    delete A;
    return 0;
}

写出如上代码时,面试官问我输出什么?我说B占一行A占一行。他问我为什么。我说析构时先析构子对象的数据再析构父对象的数据。因此,先执行子类的析构函数,再执行父类的析构函数。

然后,面试官让我执行代码看输出。结果只输出了一个A。我当时就傻眼了。不过,很快恢复镇定。发现是忘了加virtual关键字。(平时写C++代码几乎用不到继承。对这个语法有印象但是没用过不熟悉。继承这个语法)

在类A的析构函数前加了virtual关键字后,再次执行程序。程序的输出是B占一行A占一行。

紧接着,面试官让我改成智能指针。

class A
{
public:
    ~A() { cout << "A" << endl; }
}

class B : public A
{
public:
    ~B() { cout << "B" << endl; }
}

int main()
{
    shared_ptr<A> ptr(new B);
    return 0;
}

看到这里,不妨停下来,思考下上述代码会输出什么。

类A的析构函数是非虚的,输出肯定是A。

如果你的想法和我的想法是一样的,恭喜你,你我都错了。

面试官让我运行一下,运行的结果再次让我傻眼了。

输出的是B占一行A占一行。

也就是说,智能指针对象,销毁的时候,在父类析构函数非虚的情况下,能够先执行子类的析构函数,再执行父类的析构函数。

好了,接下来,有意思的面试来了。面试官让我实现一个类,类似智能指针,能够持有父类指针,析构时调用的是指针指向的类的析构函数。

面试时,我的思考方向是从指针指向的对象提取对象的信息。一直在对指针解引用,或者强制类型转换成char或void,看能不能提取出指针所指向的真实对象的信息,通过真实对象找到其析构函数的地址,再执行这个析构函数。

面试结束后,我研究了shared_ptr的底层实现。我发现,我在面试过程中思考的这个方向就错了。

类A没有虚函数,对象在内存中,根本就没有虚指针这个信息,更没有虚函数表。所以,通过对象的底层信息找到虚函数的地址,这个想法就错了。

这个题没做出来。直接告诉面试官不会了。

很遗憾,面试结束了。未通过。

class A
{
public:
    ~A() { cout << "A" << endl; }
}

class B : public A
{
public:
    ~B() { cout << "B" << endl; }
}

template <class T>
class manager_ptr : public manager
{
public:
    manager_ptr(T p): ptr(p) {}
    ~manager_ptr() { delete ptr; }
private:
    T ptr;
};

template <class T>
class _shared_ptr
{
public:
    template<class Y>
    _shared_ptr(Y* p) { ptr_manager = new manager_ptr<Y*>(p); }
    ~_shared_ptr() { delete ptr_manager; }
private:
    manager* ptr_manager;
};

int main() {
    _shared_ptr<A> ptr(new B);
    return 0;
}

面试结束后,学习了shared_ptr的实现,我写出了上述代码。在父类的析构函数非虚的情况下,能够正确析构。如果父类指针指向子类对象,在父类的析构函数非虚的情况下,仍能够正确析构,即先析构子类的数据成员,再析构父类的数据成员。换句话说,先执行子类的析构函数,再执行父类的析构函数。

实际上,在底层,虽然_shared_ptr的类型参数是A,但是_shared_ptr构造函数的类型参数,是由编译器推导出来的。_shared_ptr构成函数的类型参数Y,是由new B这条语句推导出来的。推导出来的Y是类型B。

也就是说,manager_ptr这个类的对象,持有的指针类型是B*,在_shared_ptr销毁的时候,执行delete ptr_manager,将访问manager的析构函数,由于这个析构函数是虚函数,执行manager_ptr的析构函数。由于manager_ptr持有的指针类型是B*,因此,执行的是类B的析构函数。执行子类的析构函数会自动调用父类的析构函数。所以,我们看到的输出会是B占一行A占一行。

我的这个解法,参考了C++标准库中shared_ptr的实现。

整个思考的方向,应该是使用C++的模板编程。由编译器推导出类型参数。执行delete语句时,delete后接的指针变量的类型,是其所指对象的真实类型对应的指针类型。这个思考方向才是正确的。之前的思考方向错误的原因就是类中没有虚函数,类的对象不会持有虚指针,也没有虚函数表这些信息。思考方向错了,即使再熬半个小时也做不出来的。

在写出上述代码后,我在想能否不借助manager和manager_ptr,而是直接在_shared_ptr类中,将构造函数的类型参数Y,直接保存在_shared_ptr类的数据成员中。

但是,目前还没找到不借助manager和manager_ptr的方法。

后来有读者看到此贴后,提供了不借助manager和manager_ptr的写法。例如:

template <typename T>
class MySharedV2
{
public:
    template <typename T2>
    MySharedV2(T2 *p)
    {
        data_ = p;
        deleter_ = [p](){ delete p;};
    }
    ~MySharedV2()
    {
        deleter_();
    }
    T* operator->()
    {
        return data_;
    }
private:
    std::function<void()> deleter_;
    T* data_ = nullptr;
};

原文链接:https://zhuanlan.zhihu.com/p/662637642?utm_psn=1796140397371719680 文中我非小方本人,乃是原文作者。

小方的话

这道面试题属于初中级C++开发面试题,考察的知识点有C++继承关系中父子类的析构顺序、C++虚表的实现原理、stl智能指针如何析构有继承关系的父子类对象。题目不难,但是却考察了C++的基本功,如果平常学习C++的过程中不注意基础原理的学习,面试和编码时也可能给自己遗留下坑。



相关阅读


小方目前在一家知名外企做 C++ 架构方面的工作。我建立 高质量 C++ 后端开发微信技术交流群,群里高手如云,不定期分享编程技术,也会提供一些求职和内推资源。


现在限时开放中,需要加群交流的同学可以加微信 cppxiaofang,备注“加群”。


关注我,更多的优质内容第一时间阅读

CppGuide
专注于高质量高性能C++开发,站点:cppguide.cn
 最新文章