Rule of Three/Five/Zero:面试官让我写一个安全的String类,我漏掉了一个函数 上篇聊了RAII今天接着讲一个和RAII直接相关的类设计法则——Rule of Three/Five/Zero。面试的时候面试官说写一个简单的String类管理一个char*缓冲区。我写了构造函数、析构函数。面试官问拷贝构造呢我写了。面试官又问拷贝赋值呢我写了。面试官说移动构造和移动赋值呢我愣了。这就是Rule of Three和Rule of Five的区别。今天把这个法则讲清楚。Rule of Three三个特殊函数要一起写C98时代就有这个规则了如果你需要自定义析构函数、拷贝构造函数、拷贝赋值运算符中的任何一个你几乎肯定需要自定义全部三个。为什么因为它们三个通常都在做同一件事管理资源。class SensorData { double* buffer_; size_t size_; public: // 构造函数 SensorData(size_t size) : buffer_(new double[size]), size_(size) {} // 析构函数——释放资源 ~SensorData() { delete[] buffer_; } // 拷贝构造——深拷贝 SensorData(const SensorData other) : buffer_(new double[other.size_]), size_(other.size_) { std::copy(other.buffer_, other.buffer_ size_, buffer_); } // 拷贝赋值——先释放再拷贝 SensorData operator(const SensorData other) { if (this ! other) { // 自赋值检查 delete[] buffer_; size_ other.size_; buffer_ new double[size_]; std::copy(other.buffer_, other.buffer_ size_, buffer_); } return *this; } };如果你只写了析构函数没写拷贝构造——编译器会生成默认的拷贝构造做浅拷贝。两个对象的buffer_指向同一块内存。第一个对象析构时delete了buffer_第二个对象再访问就是野指针。经典的double-free bug。面试里考的就是这个。Rule of FiveC11加上移动语义C11引入了移动构造和移动赋值。Rule of Three升级为Rule of Five如果你自定义了析构、拷贝构造、拷贝赋值中的任何一个通常也需要自定义移动构造和移动赋值。class SensorData { double* buffer_; size_t size_; public: // ... 构造、析构、拷贝构造、拷贝赋值同上 ... // 移动构造——偷指针 SensorData(SensorData other) noexcept : buffer_(other.buffer_), size_(other.size_) { other.buffer_ nullptr; other.size_ 0; } // 移动赋值 SensorData operator(SensorData other) noexcept { if (this ! other) { delete[] buffer_; buffer_ other.buffer_; size_ other.size_; other.buffer_ nullptr; other.size_ 0; } return *this; } };为什么移动语义很重要因为没有移动构造的类在STL容器扩容时只能拷贝。对于大数据量的对象拷贝的开销远大于移动。注意移动函数要标记noexcept。之前讲过STL容器只有在移动构造是noexcept的时候才会选择移动而不是拷贝。写拷贝赋值有个技巧叫copy-and-swap先用拷贝构造创建一个临时对象然后交换临时对象和当前对象的数据。好处是天然处理了自赋值和异常安全。SensorData operator(SensorData other) { // 注意按值传递自动拷贝 swap(buffer_, other.buffer_); swap(size_, other.size_); return *this; // other析构时释放旧资源 }在机器人开发里自定义的传感器数据容器、点云缓冲区这类需要管理动态内存的类都逃不开Rule of Five。但说实话大部分时候用vector代替裸指针就完事了——Rule of Zero才是正道。Rule of Zero最好的选择Rule of Five听起来很复杂其实有个更简单的方案Rule of Zero。如果你的类不需要直接管理资源把资源管理交给RAII类型那你就不需要自定义任何特殊函数。编译器生成的默认版本就够了。// Rule of Zero的写法 class SensorData { std::vectordouble buffer_; // vector自己管理内存 // 不需要析构、拷贝构造、拷贝赋值、移动构造、移动赋值 // 编译器生成的默认版本全部正确 };vector、string、unique_ptr、shared_ptr——这些标准库类型都已经正确实现了五个特殊函数。你只要用它们来管理资源你的类就自动获得了正确的行为。在机器人开发里Rule of Zero应该是默认选择。大部分时候你不需要自己管理裸指针——用vector代替动态数组用unique_ptr代替裸指针用string代替char*。面试中的经典考点Rule of Three和Rule of Five有什么区别Rule of Three是C98时代的规则析构拷贝构造拷贝赋值Rule of Five是C11的扩展加上移动构造移动赋值。拷贝赋值运算符里为什么要检查自赋值如果不检查if (this ! other)自赋值时先delete了buffer_然后从已被释放的buffer_拷贝数据——未定义行为。什么时候用Rule of Five什么时候用Rule of Zero如果你的类直接管理裸资源裸指针、文件句柄等用Rule of Five。如果所有资源都用RAII类型管理用Rule of Zero。实际开发中Rule of Zero用得更多。给正在准备面试的你Rule of Three/Five/Zero本质上是告诉你类设计的时候想清楚谁来管理资源。要么用RAII类型让编译器帮你管Rule of Zero要么自己管但管完整Rule of Five。最忌讳的是半吊子——写了析构没写拷贝构造。面试的时候能讲清楚三个Rule的关系和适用场景说明你对C的类设计有体系化的理解。补充一个实战经验如果你用编译器生成的默认特殊成员函数建议用 default显式声明。这样代码的意图更清晰——告诉读代码的人我是故意让编译器生成的而不是我忘了写。在代码review时这个习惯能减少很多不必要的讨论也让Rule of Zero的意图更加明确。再补充一个面试中容易踩坑的点Rule of Five中移动构造函数和移动赋值运算符的正确实现。很多人在写移动构造时忘了把源对象的指针置为nullptr导致两个对象析构时double free。正确的写法是移动完指针后立刻把源对象的指针设为nullptr这样源对象析构时delete nullptr是安全的C保证delete nullptr是空操作。另外面试时经常被追问的还有为什么移动构造函数要标记noexcept——因为STL容器在扩容时只有移动构造是noexcept的才会使用移动否则回退到拷贝以保证异常安全。如果你写的移动构造没标记noexceptvector扩容时还是会走拷贝构造性能提升就白费了。这些细节在面试中区分度很高能答上来的候选人不多。下篇聊完美转发std::forward——模板编程的高频考点。如果这篇文章对你有帮助欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。「机器人软件开发面试·从入门到精通」连载系列上一篇第41篇 RAII惯用法——C资源管理的核心思想 下一篇预告第43篇 完美转发std::forward——模板编程的高频考点有任何问题欢迎评论区留言我会尽量回复。