拷贝构造函数、赋值运算符
今天写线程池的时候发现,标准做法禁用了拷贝构造函数和赋值操作符
cpp
ThreadPool(const ThreadPool&) = delete; //禁用拷贝构造函数
ThreadPool& operator=(const ThreadPool&) = delete; //禁用赋值
当不写这俩禁用的时候,c++会默认生成
cpp
ThreadPool(const ThreadPool& other) // 逐成员拷贝
ThreadPool& operator=(const ThreadPool& other) // 逐成员赋值
拷贝构造函数会将成员统统再复制一份------而线程池的成员 std::thread、std::mutex、std::condition_variable 本身都禁止拷贝,编译器会报错;
即使能拷贝,也会出现两个对象管理同一批线程、重复 join、重复 notify 的未定义行为。
显式 = delete 的效果
cpp
ThreadPool a(4);
ThreadPool b(a); // ❌ 编译错误:use of deleted function
a = b; // ❌ 编译错误:use of deleted function
在编译期就强制拒绝任何复制语义,把错误从"运行期崩溃"提前到"编译不过"。
与移动语义的关系
删除的是拷贝,并未禁止移动;
如果以后想支持"转移所有权",可以再手动添加:
cpp
ThreadPool(ThreadPool&&) noexcept = default;
ThreadPool& operator=(ThreadPool&&) noexcept = default;
目前单例或全局唯一池,直接把拷贝封死即可。
拷贝和移动的关系
-
"移动"在语言层面只是允许你把资源从源对象搬出来;
-
标准库的常见实现把它做成"指针置空",于是看起来源对象被"搬空";

二者通过重载函数签名区分,
cpp
T(const T& other); // 拷贝构造
T& operator=(const T& other); // 拷贝赋值
T(T&& other) noexcept; // 移动构造
T& operator=(T&& other) noexcept; // 移动赋值
cpp
T a;
T b = a; // a 是左值 → 拷贝
T c = std::move(a); // 转成右值 → 移动
| 维度 | 拷贝 | 移动 |
|---|---|---|
| 源对象状态 | 保持不变 | 被"搬空"(合法但未指定),源值不保证是原样 |
| 资源开销 | 深复制,O(n) 内存/系统调用 | 仅交换指针,O(1) |
| 异常安全 | 可能抛(内存不足) | 通常 noexcept |
| 默认生成规则 | 若用户未声明任何拷贝控制成员,编译器自动生成拷贝 ;不会自动生成移动 | |
| 相互影响 | 一旦用户显式声明拷贝成员 ,移动成员不会自动生成;反之亦然 | |
| 经典用途 | 传值返回、容器扩容、需要保留原对象 | 传 unique_ptr、fstream、thread 等"不可拷贝"对象 |
注意这里被"搬空"的说法容易有歧义
标准库对移动构造函数的做法
左值右值
左值(lvalue)≈ 有名字、有持久地址;右值(rvalue)≈ 没名字、即将销毁的"临时"值。

cpp
int a = 1; // a 是左值
int* p = &a; // 能取地址 → 左值
std::string s = "hi";
s // s 也是左值
cpp
int x = 3 + 4; // 3+4 产生临时量 → 右值
std::string("tmp") // 临时 string → 右值
42 // 字面量 → 右值
std::move(a) // 把左值"转成"右值引用,本质还是右值 这里a是任意类型变量
详解拷贝和移动的区别
cpp
T a; // 1. 默认构造,a 里可能申请了资源(堆内存、文件句柄...)
T b = a; // 2. 拷贝构造:T::T(const T&)
T c = std::move(a); // 3. 移动构造:T::T(T&&)
左值引用和c++11右值引用
T& 和 T&& 都是"引用",但一个是传统左值引用,一个是C++11 引入的右值引用。
它们最大的差别在于能绑定到什么东西、用来干什么。
能绑谁 ------ 重载决议的"筛选器"
cpp
int x = 42; // 左值
int f(); // 返回 int 右值
void g(int& lref); // 1. 左值引用
void g(int&& rref); // 2. 右值引用
g(x); // 调用 1,因为 x 是左值
g(f()); // 调用 2,因为 f() 返回的是右值
T& 只能绑到左值(有名字、有地址的对象)。
T&& 只能绑到右值(临时对象、字面量、std::move 后的东西)。
类型系统里的"真面目"
引用折叠规则(模板/类型推导时)
cpp
T& & → T&
T& && → T&
T&& & → T&
T&& && → T&&
只有 T&& && 才会保留成 T&&,其余全部塌成左值引用。
这让"万能引用"(转发引用)auto&& / template<class T> void foo(T&&) 可以同时接受左值和右值。
生命周期延长 ------ 都干一样的事
cpp
const std::string& s1 = std::string("tmp"); // 左值引用 const 也能延长临时量
std::string&& s2 = std::string("tmp"); // 右值引用同样延长
两句都把临时 string 的生命周期拉到与引用自身相同 ,差别只是s2 后来还能被移动走。

| 代码 | 实际调用的重载 | 理由 |
|---|---|---|
std::string t1 = s1; |
string(const string&) 拷贝 |
s1 的类型是 const string&,只能匹配到 const 左值引用,没有移动接口 |
std::string t2 = s2; |
string(string&&) 移动 |
s2 的类型是 string&&,可以绑定到移动构造函数 |
注意,一个变量可以同时是左值,然后类型又是右值引用
用途分工
| 引用类型 | 典型场景 |
|---|---|
T& |
传参避免拷贝、运算符重载(operator=)、普通别名 |
const T& |
只读大对象入参、绑定右值临时量 |
T&& |
移动构造/移动赋值 、完美转发 (std::forward<T>)、资源窃取 |
unique_ptr的用法
这里 unique_ptr是个独占指针,
| 特性 | 说明 |
|---|---|
| 独占所有权 | 不允许拷贝,只能移动(move) |
| 自动释放内存 | 当 unique_ptr 被销毁时,它所指向的对象也会被自动删除 |
| 轻量级 | 几乎没有额外开销,和裸指针大小相同 |
| 支持自定义删除器 | 可以指定删除方式(如 delete[]、文件关闭等) |
cpp
#include <memory>
#include <iostream>
int main() {
std::unique_ptr<int> ptr = std::make_unique<int>(42);
std::cout << *ptr << std::endl; // 输出 42
// 转移所有权
std::unique_ptr<int> ptr2 = std::move(ptr);
if (!ptr) {
std::cout << "ptr 已为空" << std::endl;
}
return 0; // ptr2 销毁时自动释放内存
}
当使用 move将 ptr的所有权移动给 ptr2后,ptr就变成了空指针。