news 2026/11/4 15:01:43

C++中复制构造函数和重载赋值操作符总结

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++中复制构造函数和重载赋值操作符总结

前言


复制构造函数(copy constructor)和拷贝赋值运算符(copy assignment operator)是 C++ 对象复制的两条路径。前者用已有对象初始化一个新对象,后者把一个已有对象的值赋给另一个已经存在的对象。两者都叫「拷贝」,但调用时机、实现方式、异常安全要求都不同。


最常见的 bug 是「浅拷贝」:类里持有裸指针,编译器生成的拷贝函数只复制指针值,于是两个对象指向同一块内存,析构时重复释放(double free)。解决办法是手写深拷贝,而一旦手写了析构、拷贝构造、拷贝赋值中的任何一个,通常三个都要写——这就是「规则三(Rule of Three)」。


另一个常见误解是「拷贝构造和赋值可以互相替代」。它们的调用场景不同,编译器不会用一个代替另一个;只实现其中一个,另一个仍会按默认规则生成,可能悄悄做浅拷贝。


现代 C++ 更进一步:优先用 RAII 成员(std::string、std::vector、智能指针),让编译器生成的版本就是正确的,这叫「规则零(Rule of Zero)」。只有需要管理裸资源时,才手写这些函数。


本文讲清调用时机、默认行为、深拷贝实现、自赋值与异常安全,以及移动语义加入后的「规则五」。


一、调用时机


1.1 复制构造函数


// 适用于 C++17
T a;
T b = a; // 拷贝构造(不是赋值)
T c(a); // 拷贝构造
void f(T v); // 按值传参,拷贝构造
T g() { return a; } // 返回值,可能被省略

复制构造函数的签名是T(const T&),也可以有额外带默认值的参数,但第一个参数必须是本类型的引用。参数必须是引用:如果写成T(T other),传参本身又要拷贝,会无限递归。


1.2 拷贝赋值


// 适用于 C++17
T a, b;
b = a; // 拷贝赋值,b 已存在

签名是T& operator=(const T&)。返回引用是为了支持链式赋值a = b = c,也符合标准库的惯例。




维度复制构造拷贝赋值



时机创建新对象时两个对象都已存在

签名T(const T&)T& operator=(const T&)

是否清理旧资源否,对象刚创建是,必须先释放原有资源

自赋值风险无有,a = a

返回值无T&,通常是*this



T b = a;虽然用了=,但因为是初始化,调用的是复制构造,不是赋值。区分方法:看对象是不是「正在被创建」。


二、编译器生成的版本


如果没声明,编译器会隐式生成复制构造和拷贝赋值,行为是逐成员拷贝:



  • 内置类型成员:按位复制。

  • 类类型成员:调用该成员的复制构造 / 拷贝赋值。

  • 数组成员:逐个元素复制。

  • 指针成员:只复制指针值,不复制所指对象。


对不管理资源的类,逐成员拷贝就是正确的。对持有裸指针的类,它就是浅拷贝,两个对象共享同一块内存,析构时重复delete,是未定义行为。


三、深拷贝与规则三/五/零


以管理字符缓冲的String为例:


// 适用于 C++17
class String {
public:
explicit String(const char* s = "")
: size_(std::strlen(s)), data_(new char[size_ + 1]) {
std::memcpy(data_, s, size_ + 1);
}

// 复制构造:分配新缓冲,复制内容
String(const String& other)
: size_(other.size_), data_(new char[other.size_ + 1]) {
std::memcpy(data_, other.data_, size_ + 1);
}

// 拷贝赋值:先构造副本,再交换
String& operator=(const String& other) {
if (this != &other) {
String tmp(other);
swap(tmp);
}
return *this;
}

~String() { delete[] data_; }

void swap(String& other) noexcept {
std::swap(size_, other.size_);
std::swap(data_, other.data_);
}

private:
std::size_t size_;
char* data_;
};

几个关键点:



  • 复制构造直接初始化成员,不需要先释放资源(对象是新建的)。

  • 拷贝赋值采用「拷贝并交换」(copy-and-swap):先用other构造一个临时副本tmp,再与*this交换。tmp析构时释放原来的资源。这样即使构造副本时抛异常,*this也没有被修改,满足强异常安全保证。

  • 自赋值检查this != &other不是必须的(copy-and-swap 本身对自赋值安全),但能省掉一次无谓的分配。注意:如果采用「先delete[] data_再分配」的写法,自赋值检查就是必须的,否则会释放掉自己要读的数据。

  • swap标noexcept,因为交换两个指针和长度不会失败。


3.1 规则三、规则五、规则零


规则三:如果定义了析构函数、复制构造、拷贝赋值中的任何一个,通常三个都需要定义。原因是它们共同管理同一份资源,只定义一部分会导致语义不一致。


规则五:C++11 起加上移动构造和移动赋值,共五个。定义了拷贝操作通常也需要考虑移动操作,否则移动会被抑制(声明了拷贝构造或析构,移动操作不会隐式生成),容器操作可能退化为拷贝。


规则零:最好的做法是不手写任何这五个函数,用 RAII 成员管理资源:


// 适用于 C++17
class String {
public:
explicit String(std::string s = "") : data_(std::move(s)) {}
private:
std::string data_;
};

std::string自己正确管理内存,编译器生成的拷贝、移动、析构全部正确。这是现代 C++ 的首选。


四、移动语义


手写资源管理时,还应提供移动构造和移动赋值,避免不必要的深拷贝:


// 适用于 C++17
String(String&& other) noexcept
: size_(other.size_), data_(other.data_) {
other.size_ = 0;
other.data_ = nullptr;
}

String& operator=(String&& other) noexcept {
if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = other.data_;
other.size_ = 0;
other.data_ = nullptr;
}
return *this;
}

移动构造「窃取」源对象的指针,然后把源对象置空。置空是必须的:源对象析构时会delete[] data_,如果还指向同一块内存,就会重复释放。被移动后的对象处于「有效但未指定」状态,只能赋值或销毁,不应读取其内容。


移动操作应标noexcept。std::vector在扩容时,如果元素的移动构造不是noexcept,为了保证强异常安全,可能改用拷贝构造,性能会下降。


完整示例


// 适用于 C++17
#include <cstddef>
#include <cstring>
#include <iostream>
#include <utility>

class String {
public:
explicit String(const char* s = "")
: size_(std::strlen(s)), data_(new char[size_ + 1]) {
std::memcpy(data_, s, size_ + 1);
}

String(const String& other)
: size_(other.size_), data_(new char[other.size_ + 1]) {
std::memcpy(data_, other.data_, size_ + 1);
}

String& operator=(const String& other) {
if (this != &other) {
String tmp(other);
swap(tmp);
}
return *this;
}

String(String&& other) noexcept
: size_(other.size_), data_(other.data_) {
other.size_ = 0;
other.data_ = nullptr;
}

String& operator=(String&& other) noexcept {
if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = other.data_;
other.size_ = 0;
other.data_ = nullptr;
}
return *this;
}

~String() { delete[] data_; }

void swap(String& other) noexcept {
std::swap(size_, other.size_);
std::swap(data_, other.data_);
}

const char* c_str() const { return data_ != nullptr ? data_ : ""; }
std::size_t size() const { return size_; }

private:
std::size_t size_;
char* data_;
};

int main() {
String a("hello");
String b = a; // 复制构造
String c;
c = a; // 拷贝赋值
c = c; // 自赋值,必须安全

String d = std::move(a); // 移动构造

std::cout << b.c_str() << ' ' << c.c_str() << ' ' << d.c_str() << '\n';
std::cout << "a size after move: " << a.size() << '\n';
return 0;
}

按标准语义,输出应为hello hello hello,以及a size after move: 0。a被移动后指针为空,c_str()返回空串,size()为 0,但对象本身仍然可以安全析构。


c_str()里判断了data_ != nullptr,是因为移动后的对象指针为空。如果不做这个判断,返回空指针给std::cout是未定义行为。


常见坑点



  1. ❌ 浅拷贝导致重复释放:


``cpp class S { char* p; }; // 默认拷贝只复制指针`✅ 手写深拷贝,或改用std::string/std::vector`。



  1. ❌ 复制构造参数按值传递:


``cpp S(S other); // 无限递归`✅S(const S& other);`



  1. ❌ 拷贝赋值不检查自赋值,且先释放再读取:


``cpp delete[] data_; data_ = new char[other.size_ + 1]; // other 可能就是自己`✅ 用 copy-and-swap,或先判this != &other`。



  1. ❌ 拷贝赋值返回void:


``cpp void operator=(const S&);`✅ 返回S&,末尾return *this;`。



  1. ❌ 析构用delete释放new[]的数组:


``cpp ~S() { delete data_; } // 必须是 delete[]`✅delete[] data_;`



  1. ❌ 只写复制构造,不写拷贝赋值:


``cpp S(const S&); // 赋值仍走默认浅拷贝`` ✅ 按规则三补齐,或按规则零用 RAII 成员。



  1. ❌ 移动后不把源对象置空:


``cpp data_ = other.data_; // other.data_ 仍指向同一内存`✅ 移动后把源对象的指针设为nullptr`。



  1. ❌ 移动构造没标noexcept,容器扩容退化。


✅ 不抛异常的移动操作标noexcept。


总结




函数签名关键点



复制构造T(const T&)新建对象,分配并复制资源

拷贝赋值T& operator=(const T&)先释放旧资源,注意自赋值

移动构造T(T&&) noexcept窃取资源,源对象置空

移动赋值T& operator=(T&&) noexcept释放旧资源后窃取

析构~T()new[]配delete[]



拷贝控制的本质是「谁拥有资源、何时释放」。能用 RAII 成员解决,就不要手写这五个函数(规则零);必须手写时,五个一起考虑(规则五),并用 copy-and-swap 保证赋值的安全。记住这两条,double free 和自赋值问题基本不会出现。




版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/4 15:01:27

TypeScript 从零搭建 RAG 知识获取管道:AI Agent 检索增强生成实战

1. 为什么知识获取管道是 AI Agent 的分水岭 做 AI Agent 开发的人&#xff0c;绕不开一个尴尬的现实&#xff1a;模型本身很聪明&#xff0c;但它对你私有的东西一无所知。你问它公司内部某个接口的鉴权逻辑&#xff0c;它只能编&#xff1b;你问它三个月前那场故障的根因&…

作者头像 李华
网站建设 2026/11/4 15:00:17

PADS、AD、Cadence三套EDA工具互转实操指南:四条链路与避坑清单

简介&#xff1a;面向电子设计从业者与初学者的EDA工具互转实操文档&#xff0c;聚焦PADS、Altium Designer与Cadence三大平台之间设计文件的跨工具迁移难题。内容覆盖PADS转AD、AD转Cadence、AD转PADS、Cadence转PADS四大方向&#xff0c;并细分原理图与PCB两条路径&#xff0…

作者头像 李华
网站建设 2026/11/4 14:57:35

基于GNN的新冠疫情时空预测模型实践

1. 项目背景与核心价值 去年疫情最严重的时候&#xff0c;我在医院工作的朋友经常抱怨床位和呼吸机调配困难。当时我就在想&#xff0c;如果能提前几天预测疫情发展趋势&#xff0c;医疗资源的调度会不会更从容&#xff1f;这个想法促使我开始了"新冠预测"这个深度学…

作者头像 李华
网站建设 2026/11/4 14:56:47

FlyEnv:轻量级本地开发环境解决方案

1. 为什么我们需要更轻量的本地开发环境&#xff1f; 作为一名长期奋战在开发一线的程序员&#xff0c;我深知环境配置的痛苦。记得去年接手一个老项目&#xff0c;光是搭建本地开发环境就花了两天时间——安装特定版本的PHP、配置Nginx虚拟主机、折腾MySQL权限&#xff0c;最…

作者头像 李华
网站建设 2026/11/4 14:56:48

Spring Boot 外部 YAML 解析:SnakeYAML 绑定与安全

接手一个偏数据侧的 Spring Boot 服务时&#xff0c;需求看着不复杂&#xff1a;从一份几十行的 YAML 里读出若干条清洗规则&#xff0c;跑批时按规则处理数据。Spring Boot 对 YAML 的支持已经够好了&#xff0c;application.yml 写得飞起&#xff0c;可真到「在业务代码里读一…

作者头像 李华
网站建设 2026/11/4 14:56:10

5分钟用pnpm搭建React原型:依赖管理与Vite实践

pnpm 装 React 原型这件事&#xff0c;说起来简单&#xff0c;但我见过太多人卡在第一步——敲完命令盯着终端里滚动的进度条发呆&#xff0c;或者第二天发现 C 盘少了好几个 G。原型项目的特点很明确&#xff1a;起得快、扔得也快、数量还多&#xff0c;一个人手里同时挂着三五…

作者头像 李华