联想 C++ 一面,问得比想象中深很多
投的是联想的 C++ 软件开发岗,一面是视频面试,面试官看起来是个技术 lead,全程很平和,没有刁难的感觉,但问题一个接一个,基本没有冷场的时间。
整体考察方向偏基础扎实度,C++ 语言特性问得比较细,也有一道设计题和一道手撕。项目部分聊了大概十分钟,他主要关注你在项目里遇到了什么问题、怎么解决的,不太关心项目本身做了什么。
总时长约六十分钟,体验不错,面试官会给你思考时间,答不上来他也会给提示。
1. new 和 malloc 的区别是什么?placement new 是什么,什么时候会用到?
答:malloc 是 C 标准库函数,只负责分配一块指定大小的原始内存,返回 void*,不做任何初始化,不调用构造函数。new 是 C++ 运算符,在分配内存之后还会调用对象的构造函数完成初始化,返回对应类型的指针,不需要手动强转。失败处理上也不同,malloc 失败返回 NULL,new 失败抛出 std::bad_alloc 异常。对应的释放操作,free 只释放内存,delete 会先调用析构函数再释放内存,两者不能混用。
placement new 是 new 的一种特殊形式,它不分配内存,而是在一块已有的内存地址上直接构造对象。主要用在内存池场景里,预先分配一大块内存,需要对象时用 placement new 在池里构造,避免频繁调用系统分配器带来的开销和碎片化问题。另外在共享内存场景里,需要在特定地址构造对象供多个进程访问,也会用到。需要注意的是,用 placement new 构造的对象销毁时要手动调用析构函数,不能直接 delete。
2. 左值引用和右值引用的区别是什么?什么是万能引用,std::forward 解决了什么问题?
答:左值是有名字、有持久地址的表达式,可以取地址,比如变量、数组元素。右值是临时的、即将销毁的值,不能取地址,比如字面量、函数返回的临时对象。左值引用只能绑定左值,右值引用只能绑定右值,右值引用的核心用途是实现移动语义,允许"偷走"临时对象的资源而不是复制一份。
万能引用是指在模板参数推导上下文中出现的 T&&,它既能绑定左值也能绑定右值。传入左值时 T 推导为左值引用,传入右值时 T 推导为普通类型。
std::forward 解决的问题是:函数参数一旦有了名字就变成了左值,右值信息在传递过程中丢失。比如你写了一个包装函数,接收万能引用参数再转发给内部函数,如果不用 forward,不管外部传进来的是左值还是右值,内部函数收到的都是左值,移动语义失效。std::forward 的作用就是根据模板参数 T 的推导结果,把参数还原成原来的值类别再传递出去,实现完美转发。
3. vector 和 list 的底层结构分别是什么?各自适合什么场景,迭代器失效的情况有哪些?
答:vector 底层是一块连续的堆内存,支持随机访问,尾部插入均摊 O(1),中间插入删除需要移动元素是 O(n)。list 底层是双向链表,节点分散在堆上,不支持随机访问,但任意位置插入删除是 O(1),前提是你已经有了指向那个位置的迭代器。
适用场景上,vector 适合随机访问多、尾部增删多、对缓存友好性有要求的场景,是绝大多数情况下的默认选择。list 适合频繁在中间插入删除、对迭代器稳定性要求高的场景,但实际上因为链表节点分散、缓存命中率低,很多时候性能不如 vector,要根据实际测量决定。
迭代器失效方面,vector 在触发扩容时所有迭代器全部失效,即使没有扩容,插入位置之后的迭代器也会失效,因为元素发生了移动。list 的迭代器稳定性很好,插入操作不会使任何迭代器失效,删除操作只使被删除元素的迭代器失效,其他迭代器完全不受影响。
4. explicit 关键字的作用是什么?不加它会有什么隐患?
答:explicit 用于修饰构造函数或类型转换运算符,禁止编译器进行隐式类型转换,只允许显式构造。
不加 explicit 的隐患在于,编译器会在需要的时候自动调用单参数构造函数做隐式转换,这种转换往往是无意的,容易引入难以发现的 bug。比如一个接受某个类对象的函数,如果你不小心传了一个整数进去,编译器不会报错,而是悄悄地用那个整数构造了一个临时对象,程序行为可能完全不是你预期的。
加了 explicit 之后,这种隐式转换会在编译期报错,强迫调用者明确写出构造过程,代码意图更清晰,也更安全。一般来说,单参数构造函数几乎都应该加 explicit,除非你明确需要隐式转换,比如 std::string 允许从字符串字面量隐式构造,这是有意为之的设计。C++11 之后 explicit 也可以用于转换运算符,同样禁止隐式转换。
5. 多线程下 std::shared_ptr 是线程安全的吗?
答:这个问题要分两个层面来回答。
引用计数本身是线程安全的。shared_ptr 内部的引用计数操作是原子操作,多个线程同时复制或销毁同一个对象的不同 shared_ptr 副本,引用计数不会出错,对象也会在最后一个副本销毁时被正确释放。
但 shared_ptr 对象本身不是线程安全的。如果多个线程同时读写同一个 shared_ptr 变量,比如一个线程在给它赋值、另一个线程在读取它,这是数据竞争,需要加锁保护。
另外,shared_ptr 对它所管理的对象不提供任何线程安全保证。多个线程通过不同的 shared_ptr 访问同一个对象,如果有写操作,需要自己在对象层面加锁,shared_ptr 不会帮你做这件事。
所以简单来说:引用计数安全,shared_ptr 变量本身不安全,指向的对象也不安全,后两者都需要自己处理同步。
6. C++ 的四种类型转换分别是什么,各自用在什么场景?
答:static_cast 是最常用的,编译期做类型检查,用于基本类型之间的转换、有继承关系的指针转换、void* 转具体指针等。向上转型(派生类转基类)是安全的,向下转型不做运行时检查,如果类型不对会产生未定义行为。
dynamic_cast 专门用于含虚函数的类层次,在运行时检查实际类型,向下转型失败时指针返回 nullptr,引用抛出异常,比 static_cast 安全但有运行时开销,需要 RTTI 支持。
const_cast 是唯一能去掉或添加 const/volatile 修饰的转换,常用于调用不接受 const 参数的旧接口。需要注意的是,去掉 const 后修改原本是 const 的对象是未定义行为,只有当原对象本身不是 const 时才安全。
reinterpret_cast 是最危险的,把内存直接重新解释为另一种类型,不做任何检查,结果高度依赖平台。用于底层操作比如把指针转成整数、内存映射寄存器访问等,能不用就不用,用了要加注释说明原因。
7. 什么是对象切片问题,如何避免?
答:对象切片发生在派生类对象被赋值给基类值类型变量的时候。因为基类变量只有基类的大小,派生类特有的成员没有地方存放,会被"切掉
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++技术面试核心考点,涵盖语言基础、面向对象、内存管理、STL容器、模板编程及经典算法。从引用指针、虚函数表、智能指针等底层原理,到继承多态、运算符重载等OOP特性从const、static、inline等关键字辨析,到动态规划、KMP算法、并查集等手写实现。每个知识点以面试答题形式呈现,注重原理阐述而非冗长代码,帮助你快速构建完整知识体系,从容应对面试官提问,顺利拿下offer。
