Sealessland logo Sealessland
Interview Prep

C++ 面试复习:对象模型、资源管理与并发主线

先把 C++ 面试里最常见的三条线讲顺:对象怎么活、资源怎么管、并发代码为什么会错。

C++ 面试复习:对象模型、资源管理与并发主线

这块我之前最容易复习散。对象模型看一点,智能指针看一点,并发再看一点,最后每个词都认识,但真要从头讲一遍 C++ 在管什么,又总觉得东一榔头西一棒。

现在更实用一点的记法是直接抓三件事:对象怎么活,资源怎么管,多线程下为什么会出错。

先说对象。

很多语法点最后都落在对象生命周期上。构造、拷贝、移动、析构这些东西拆开看很碎,但放回“一个对象从创建到销毁到底经历了什么”这条线上就顺很多。尤其是类型内部一旦自己管资源,问题马上就不是语法题了,而是拷贝到底是不是合法的吗,移动之后原对象还能不能处在一个合理状态,析构时会不会重复释放。

这也是为什么 rule of zero / three / five 虽然名字很像背诵材料,但本质上是在提醒一件很简单的事:你只要开始自己接管资源,就得把特殊成员函数一起想完。

然后是资源管理。

这块我现在基本不从 new/delete 开始想了。真正该先记住的还是 RAII。资源拿到手的时候就绑定到对象生命周期里,对象离开作用域自动释放,这样异常路径也比较自然,不需要到处补清理逻辑。

智能指针其实也是这个思路的延伸。unique_ptr 很适合表达独占所有权,shared_ptr 能解决一部分共享问题,但它也会把所有权关系弄复杂,还多一层引用计数开销。所以它不是“更高级的指针”,只是另一种更重的语义。之前我老把这个点答成“shared_ptr 更方便”,现在觉得这种答法就很虚。

再往后就是并发。

并发这里最容易犯的错是工具记得很多,主线却没立起来。比如知道 mutexcondition_variableatomic,但一问为什么会错,就只会说“线程不安全”。更稳一点的讲法还是 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 把独占所有权写死了。
  • 拷贝直接禁掉,省得误拷资源。
  • 就算资源释放这件事已经安全了,读共享数据还是得上锁。