C++ 面试复习:对象模型、资源管理与并发主线
这块我之前最容易复习散。对象模型看一点,智能指针看一点,并发再看一点,最后每个词都认识,但真要从头讲一遍 C++ 在管什么,又总觉得东一榔头西一棒。
现在更实用一点的记法是直接抓三件事:对象怎么活,资源怎么管,多线程下为什么会出错。
先说对象。
很多语法点最后都落在对象生命周期上。构造、拷贝、移动、析构这些东西拆开看很碎,但放回“一个对象从创建到销毁到底经历了什么”这条线上就顺很多。尤其是类型内部一旦自己管资源,问题马上就不是语法题了,而是拷贝到底是不是合法的吗,移动之后原对象还能不能处在一个合理状态,析构时会不会重复释放。
这也是为什么 rule of zero / three / five 虽然名字很像背诵材料,但本质上是在提醒一件很简单的事:你只要开始自己接管资源,就得把特殊成员函数一起想完。
然后是资源管理。
这块我现在基本不从 new/delete 开始想了。真正该先记住的还是 RAII。资源拿到手的时候就绑定到对象生命周期里,对象离开作用域自动释放,这样异常路径也比较自然,不需要到处补清理逻辑。
智能指针其实也是这个思路的延伸。unique_ptr 很适合表达独占所有权,shared_ptr 能解决一部分共享问题,但它也会把所有权关系弄复杂,还多一层引用计数开销。所以它不是“更高级的指针”,只是另一种更重的语义。之前我老把这个点答成“shared_ptr 更方便”,现在觉得这种答法就很虚。
再往后就是并发。
并发这里最容易犯的错是工具记得很多,主线却没立起来。比如知道 mutex、condition_variable、atomic,但一问为什么会错,就只会说“线程不安全”。更稳一点的讲法还是 data race。多个线程同时碰一份共享内存,只要有写,而且中间没有同步,那就是未定义行为,不是“偶尔会出 bug”那么简单。
这样再去看锁、条件变量、原子这些工具就不会乱。锁是拿来建立互斥的,条件变量是拿来等条件,不是拿来凑一个“高级同步原语”的名词,原子操作也只是保证某个对象的原子访问,不会自动让整段逻辑都正确。
我现在觉得 C++ 面试里比较稳的起手式,不是先背一堆术语,而是先说:这门语言给了你很强的对象语义和性能控制能力,但代价是生命周期、所有权和并发正确性都得自己负责任。后面不管面试官往移动语义、智能指针还是内存模型去追,基本都还能接得上。
顺手记几个容易被追问的点:
- 右值引用不是单纯为了“更快”,而是为了把资源从一个即将失效的对象里转出去。
vector扩容时为什么会尽量走移动而不是拷贝,和异常安全也有关系。shared_ptr的引用计数是线程安全的,不代表它管理的对象天然线程安全。
下面这个小例子我还是想留着,因为它刚好把几个点放在一起了:
#include <memory>
#include <mutex>
#include <vector>
class Buffer {
public:
explicit Buffer(std::size_t n) : data_(std::make_unique<int[]>(n)), size_(n) {}
Buffer(Buffer&&) noexcept = default;
Buffer& operator=(Buffer&&) noexcept = default;
Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
int read(std::size_t i) const {
std::lock_guard<std::mutex> lock(mu_);
return data_[i];
}
private:
std::unique_ptr<int[]> data_;
std::size_t size_;
mutable std::mutex mu_;
};
这里最值得看的也不是代码本身多复杂,而是:
unique_ptr把独占所有权写死了。- 拷贝直接禁掉,省得误拷资源。
- 就算资源释放这件事已经安全了,读共享数据还是得上锁。