这是笔者最近面试一家量化投资公司的真题。
笔者当时面试时没有做出来,与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++多线程编程专栏 网络编程重难点分析专栏 高性能通信协议设计专栏 推荐一波新版优质 Modern C++书籍 高级 C++ 开发综合岗面试题,能挑战否? 大型开源 FTP 软件 FileZilla 源码分析 如何尽快适应大型 C++ 项目? 如果你想低成本的快速提高开发水平,推荐这个
小方目前在一家知名外企做 C++ 架构方面的工作。我建立 高质量 C++ 后端开发微信技术交流群,群里高手如云,不定期分享编程技术,也会提供一些求职和内推资源。
现在限时开放中,需要加群交流的同学可以加微信 cppxiaofang,备注“加群”。
关注我,更多的优质内容第一时间阅读