【C++ 面试真题】聊聊 C++ 的拷贝构造与拷贝赋值 📅 2026/8/14 3:47:39 【C 面试真题】聊聊 C 的拷贝构造与拷贝赋值上一篇我们讲了构造析构的生与死。本篇聚焦其中最坑的一对——拷贝构造和拷贝赋值。看似就是复制一份可一旦你的类管着资源指针、文件句柄、socket浅拷贝就会给你送上 double free、悬空指针、内存泄漏三连。还有那个面试必问的三五法则。本文把拷贝这件事一次讲透。一、先说结论拷贝 “复制一份独立的”❓ 拷贝构造和拷贝赋值有什么区别✅ 一句话都是复制一份但触发的时机和对象状态不同。维度拷贝构造拷贝赋值形式T(const T)T operator(const T)触发对象一个还没诞生的对象一个已经存在的对象典型场景按值传参/返回、用同类型初始化a b;赋值string s1hi;// 构造非拷贝strings2(s1);// 拷贝构造s2 新生string s3s1;// 也是拷贝构造语法糖s2s3;// 拷贝赋值s2 已存在判别口诀等号左边是新对象 → 拷贝构造左边是老对象 → 拷贝赋值。string s2 s1;看着是但s2刚诞生所以是构造不是赋值。二、拷贝何时被触发❓ 哪些代码会触发拷贝✅ 主要三类voidf(string s);// 1. 按值传参stringg(){string tx;returnt;// 2. 按值返回}string ay;stringb(a);// 3. 用同类型初始化⚠️注意现代 C 里第 1、2 类经常被优化掉拷贝消除 RVO/NRVO、C17 起部分场景强制省略。本文先讲清拷贝本该发生时该怎么写对第 8 节会专门提优化。三、浅拷贝的灾难核心考点❓ 编译器默认合成的拷贝为什么常常出事✅ 因为默认拷贝是逐成员 bitwise 复制——成员是指针时它只复制地址不复制指向的内容。两个对象就指向同一块内存析构时 double free。经典翻车现场structBuffer{Buffer(intn):data_(newint[n]),n_(n){}~Buffer(){delete[]data_;}// 没写拷贝构造/赋值// 用编译器合成的逐成员复制int*data_;intn_;};{Buffera(10);Buffer ba;// 拷贝构造// a.data_ b.data_指向同一块}// a、b 离开作用域// 析构 bdelete[] data_;// 析构 adelete[] 同一块// ❌ double free程序崩溃⚠️更隐蔽的版本不止崩溃——两个对象里一个改了data_另一个也跟着变它们共享同一块内存。这种幽灵修改在大型项目里极难排查。只要类管着裸资源指针、句柄就不能用默认拷贝。四、深拷贝怎么写❓ 浅拷贝这么坑正确的拷贝该怎么做✅深拷贝不只复制指针而是重新分配一块新内存把内容复制过去让两个对象各持一份。structBuffer{Buffer(intn):data_(newint[n]),n_(n){}// 拷贝构造分配 复制Buffer(constBuffero):data_(newint[o.n_]),n_(o.n_){// 把内容逐个复制过去for(inti0;in_;i)data_[i]o.data_[i];}~Buffer(){delete[]data_;}int*data_;intn_;};Buffera(10);Buffer ba;// ✅ b 有自己的 10 个 int// 改 b.data_[0] 不影响 a核心区别浅拷贝复制的是门牌号地址深拷贝复制的是房子内容 给你盖一栋新门牌号。五、拷贝赋值自赋值与三步走❓ 拷贝赋值比拷贝构造多了什么坑✅ 多两件事目标对象已存在要先释放旧资源还要防自赋值。Bufferoperator(constBuffero){if(thiso)return*this;// 1. 自赋值检查delete[]data_;// 2. 释放旧的data_newint[o.n_];// 3. 分配新的n_o.n_;for(inti0;in_;i)data_[i]o.data_[i];// 复制内容return*this;}Buffera(10);aa;// 没有自赋值检查的话// delete[] data_; 然后 new ...// 但 o.data_ 就是 a.data_// 复制的是已释放的内存⚠️自赋值不写正常代码里a a很少直接出现但通过别名间接发生很常见a[i] a[j]、两个引用指向同一对象。所以拷贝赋值永远要先防自赋值。加分写法——copy-and-swap把赋值拆成先拷贝构造一份临时对象再和自己的内部资源交换自赋值、异常安全一次性解决。这个写法也是移动语义的天然搭档移动构造配合 swap 尤其优雅。六、三五法则Rule of Three/Five❓ 面试总听到的三五法则是什么✅ 一句话一旦你需要自定义析构、拷贝构造、拷贝赋值三者之一另外两个通常也得自己写。structBuffer{~Buffer(){delete[]data_;}// 既然你要管析构释放资源// 拷贝构造、拷贝赋值几乎肯定也要管// 否则就用默认的 bitwise 复制 → 出事};法则涉及函数适用Rule of Three析构 拷贝构造 拷贝赋值C98Rule of Five上面三个 移动构造 移动赋值C11 起本质这三个函数都和资源管理绑定。你需要自定义其中一个说明这个类管着需要特殊处理的资源——那复制它、“移动它”、释放它自然都得特殊处理。反过来如果类全是普通成员int、string、vector 这些会自己管资源就一个都别写让编译器合成的更好、更安全。七、什么时候默认拷贝就够用❓ 那些用着指针的类才要写拷贝那普通类呢✅只要类里没有自己裸管的资源默认拷贝就完全够用——标准库的容器、string、智能指针都自己管好了自己把它们当成员类就白嫖了它们的正确拷贝。structGood{std::vectorintv;std::string name;std::unique_ptrFILEfp;// 一个拷贝函数都不用写// vector/string/unique_ptr 各自正确处理};这就是现代 C 的方向把资源托管给 RAII 类型容器、智能指针让你的类rule of zero零法则——一个特殊成员函数都不写编译器合成的最优。需要手写三大/五大法则的基本是还在用裸 new/delete 的老式类。八、拷贝消除RVO/NRVO❓ 我明明按值返回了局部对象怎么没看到拷贝✅ 因为编译器把那次拷贝优化掉了——这叫拷贝消除copy elision分两种RVOReturn Value Optimization返回一个临时对象NRVO返回一个有名字的局部对象。stringmake(){string thello;returnt;// NRVOt 直接在调用方构造}string smake();// 可能 0 次拷贝C17 起RVO 在特定条件下是强制的guaranteed copy elisionNRVO 仍是可选优化但几乎所有主流编译器都开。⚠️重要推论正因为拷贝常被消除别为了省拷贝而乱写奇怪的代码比如返回std::move(局部对象)——这反而会阻止 NRVO这是高频坑。九、面试高频追问❓ Q1拷贝构造的参数为什么必须是引用✅ 因为按值传参会触发拷贝构造——而你现在定义的正是拷贝构造会造成无限递归。所以参数必须是引用通常const T避免再次拷贝。❓ Q2拷贝构造为什么常加const✅ 两点①不修改源对象拷贝本应只读②能接收常量对象和右值非 const 引用绑不了右值。❓ Q3三五法则反过来成立吗写了析构就一定得写拷贝✅ 几乎一定。编译器看到你写了析构说明类管着资源默认的 bitwise 拷贝大概率是错的。虽然 C11 后编译器看到自定义析构时会把合成拷贝标为 deprecated但最稳妥还是要么三个都写要么一个都不写rule of zero。❓ Q4 delete禁用拷贝和把拷贝设为 private 有什么区别✅ delete是编译期明确报错任何代码包括成员/友元调都报private只挡外部类自己/友元还能调。C11 后推荐 deletestd::mutex、std::unique_ptr就这么干。❓ Q5拷贝赋值为什么不返回void而返回*this✅ 为了支持链式赋值(a b) c和a b c。标准约定拷贝赋值返回T指向左操作数自己遵循这个约定才能和内置类型、标准库混用不出错。十、总结速查表考点一句话结论构造 vs 赋值左边新对象→构造左边老对象→赋值默认拷贝逐成员 bitwise 复制指针成员会出事深拷贝重新分配 复制内容拷贝赋值三步自赋值检查 → 释放旧 → 分配复制三五法则自定义析构/拷贝之一其余通常都要写Rule of Zero用 RAII 成员一个都不写RVO/NRVO按值返回的拷贝常被优化掉参数为什么是引用按值会无限递归一句话回顾管着裸资源的类拷贝构造和拷贝赋值得自己写成深拷贝尽量用容器/智能指针当成员享受零法则——让编译器合成的又快又对。如果您觉得本篇内容对你有帮助欢迎点赞 、收藏 ⭐、转发 。下期我们继续——讲拷贝的升级版移动语义。std::move到底 move 了什么、为什么移动构造要noexcept、vector 扩容为什么会偷偷退回拷贝敬请关注