shared_ptr
对象需要在异步回调中安全获取自身的 shared_ptr
- 错误: shared_ptr(this) -> 新建独立控制块 -> 双重释放!
- 正确: 继承 enable_shared_from_this,调用 shared_from_this()
classTcpSession:publicstd::enable_shared_from_this<TcpSession>{std::string id_;public:explicitTcpSession(conststd::string&id):id_(id){std::cout<<" [ctor] TcpSession("<<id_<<")\n";}~TcpSession(){std::cout<<" [dtor] ~TcpSession("<<id_<<")\n";}voidcorrect_way(){autoself=shared_from_this();std::cout<<" [correct] shared_from_this(), use_count = "<<self.use_count()<<"\n";}voidwrong_way(){std::shared_ptr<TcpSession>self(this);std::cout<<" [WRONG] shared_ptr<T>(this), use_count = "<<self.use_count()<<"\n";std::cout<<" >>> This self has an INDEPENDENT control block!\n";std::cout<<" >>> When self goes out of scope, it will delete 'this'!\n";}};voidtask3_enable_shared(){std::cout<<"\n=== Task 3: enable_shared_from_this ===\n";autosession=std::make_shared<TcpSession>("sess-001");std::cout<<"outer shared_ptr use_count: "<<session.use_count()<<"\n\n";std::cout<<"--- correct way ---\n";session->correct_way();std::cout<<" outer use_count: "<<session.use_count()<<" (correct: use_count temporarily +1, prevents premature delete)\n";std::cout<<"\n--- wrong way (uncomment below to see crash) ---\n";std::cout<<" shared_ptr<T>(this) creates an independent control block -> double free\n";// session->wrong_way(); // <- UNCOMMENT to crash! self inside wrong_way()// will delete 'this' when it goes out of scope, then outer session// destructor will delete again -> double free!std::cout<<"\nWithout enable_shared_from_this, a member function\n";std::cout<<"cannot safely obtain a shared_ptr to itself.\n";std::cout<<"\n--- scenario: lambda capture this vs shared_from_this ---\n";std::cout<<" WRONG: [this] captures raw pointer, lambda may outlive object\n";std::cout<<" RIGHT: [self = shared_from_this()] captures shared_ptr, extends lifetime\n";}输出:
能看到执行到为wrong_way就导致程序崩溃,后续逻辑无法执行。
现在能看清区别了:shared_from_this()vsshared_ptr<T>(this)
[正确]shared_from_this() | [错误]shared_ptr<T>(this) | |
|---|---|---|
| use_count | 2(与外部 session 共享控制块) | 1(创建了独立的控制块) |
| 离开作用域 | 计数回 1,对象存活 ✅ | 计数归 0 →delete this❌ |
| 最终结果 | 安全 | 外层 session 析构时再次 delete →double free 崩溃 |
如果取消注释session->wrong_way(),运行流程是:
1. make_shared<TcpSession> → 控制块A: refcount=1 2. correct_way() 调用 shared_from_this() → 控制块A: refcount=2 → 离开后回到1 ✅ 3. wrong_way() 调用 shared_ptr<T>(this) → 控制块B: refcount=1 (独立的!) 离开 wrong_way() → refcount=0 → delete this 💥 4. task3 结束, session 析构 → 控制块A: refcount=0 → 再次 delete → CRASH 💥💥核心原理:enable_shared_from_this 内部维护了一个 weak_ptr,make_shared 时会自动初始化它。调用 shared_from_this() 时,通过这个 weak_ptr 创建 shared_ptr,从而共享同一个控制块,不会产生分裂。而 shared_ptr(this) 对同一个裸指针创建了第二个控制块,两个控制块互不知情,最终各自 delete 一次。
指针声明使用
原则:优先用make_*,但有例外
✅ 应该用make_shared/make_unique的理由
cpp
// ❌ 直接 new — 3 个问题std::shared_ptr<Foo>p1(newFoo(1,2,3));// 写两次 Foostd::unique_ptr<Foo>p2(newFoo(1,2,3));// ✅ make 函数 — 简洁、安全、高效autop1=std::make_shared<Foo>(1,2,3);// Foo 只写一次autop2=std::make_unique<Foo>(1,2,3);| 优势 | 说明 |
|---|---|
| 异常安全 | foo(shared_ptr<T>(new T), bar())— 若bar()在shared_ptr构造前抛异常,new T泄漏。make_shared不会 |
| 一次分配 | make_shared对象+控制块合并,少一次 malloc |
| 代码简洁 | 不重复写类型名,auto友好 |
❌ 什么时候必须用new
1. 自定义删除器(shared_ptr)
cpp
// make_shared 无法传删除器autop=std::shared_ptr<FILE>(fopen("a.txt","r"),fclose);2. 对象很大 + weak_ptr 长期存在
cpp
// make_shared: 对象析构了但 400MB 内存还被 weak_ptr 锁着// new: weak_ptr 存在时, 400MB 早已归还系统autobig=std::shared_ptr<BigData>(newBigData(400*1024*1024));3.unique_ptr数组(C++14 前)
cpp
// C++14 才支持 make_unique<T[]>autoarr=std::unique_ptr<int[]>(newint[100]);4. 私有构造函数 + 工厂模式
cpp
classFoo{Foo()=default;// 私有friendstd::shared_ptr<Foo>std::make_shared<Foo>();// ← 但 make_shared 需要友元};总结
make_shared | make_unique | |
|---|---|---|
| 推荐程度 | ⭐⭐⭐ 默认首选 | ⭐⭐⭐ 默认首选 |
| 唯一硬伤 | weak_ptr 延迟释放 | 不能传删除器 |
| 何时用 new | 自定义删除器、大对象+weak_ptr | C++14 前数组、私有构造 |
一句话:能用make_*就用,只有在上述例外场景才退回到new。这是 Scott Meyers《Effective Modern C++》Item 21 的核心建议。
move
工作原理
voidtask1_understand_move(){std::cout<<"=== 任务 1: std::move 的本质 ===\n";std::string a="hello world";std::string b=std::move(a);// std::move(a) 得到 string&&, 触发移动构造std::cout<<"a = \""<<a<<"\" (移动后通常为空)\n";std::cout<<"b = \""<<b<<"\" (拿到了 a 的内容)\n";std::cout<<"注意: std::move 之后不要再使用 a,它处于 valid but unspecified 状态\n\n";}第一步:std::move(a)返回什么?
std::move(a)把a(左值)cast成string&&(右值引用)。它返回的是一个引用类型,不是一个新对象。
std::move(a)// 类型是 string&&,它仍然指向 a第二步:用这个右值引用初始化b
std::string b=std::move(a);// ↑ 这里传入的是 string&& (右值引用)编译器看到初始化器是string&&,就去匹配std::string的移动构造函数:
string(string&&other)noexcept;// 移动构造移动构造函数内部偷走了a的内部资源(指针),然后b就变成了一个完完整整的、正常的std::string对象。
图解
移动前: a ──→ [h][e][l][l][o][ ][w][o][r][l][d][\0] (堆上的字符数组) b ──→ (未初始化) std::string b = std::move(a); ↓ 匹配 string(string&&),偷走 a 的指针 移动后: a ──→ (空字符串,内部指针为 nullptr) b ──→ [h][e][l][l][o][ ][w][o][r][l][d][\0] (同一块堆内存!)b的类型自始至终都是std::string,不是什么string&&。string&&只是用来选择调用哪个构造函数的一个"标签"。一旦构造完成,b就是一个普通的std::string对象,当然可以正常输出。
移动构造函数
classStringBuffer{std::string data_;staticintcopy_count;staticintmove_count;public:StringBuffer()=default;// 拷贝构造: 深拷贝StringBuffer(constStringBuffer&other):data_(other.data_){++copy_count;}StringBuffer(StringBuffer&&other)noexcept:data_(std::move(other.data_)){++move_count;}// 拷贝赋值StringBuffer&operator=(constStringBuffer&other){if(this!=&other){data_=other.data_;++copy_count;}return*this;}为什么移动构造要加 noexcept?
这不仅仅是一个"好习惯",它有实际的性能影响。最关键的场景是 std::vector 扩容:
- std::vector 扩容时需要把旧内存中的元素搬移到新内存。
- vector 面临一个选择:用移动还是用拷贝?
- 如果用移动,但移动过程中抛了异常,旧内存已经被破坏,新内存也不完整 → 数据丢失,无法恢复。
- 所以 vector 的策略是:只有当移动构造函数标记了 noexcept 时,才敢用移动;否则退化为拷贝。
NRVO优化
C++17 起按值返回大对象是安全的
核心概念:NRVO(命名返回值优化)
编译器足够聪明,它知道 buf 最终是要返回给调用者的,所以它直接在调用者的栈空间上构造 buf,跳过所有拷贝/移动。
没有 NRVO 时(理论上): ┌─────────────────────┐ copy/move ┌─────────────────────┐ │ create_buffer 内部 │ ──────────→ │ main 中的 buf │ │ 局部变量 buf │ │ │ └─────────────────────┘ └─────────────────────┘ 有 NRVO 时(实际上): ┌───────────────────────────────────────┐ │ create_buffer 的 buf 直接在主函数 │ │ (调用者) 的内存位置上构造 │ │ → 零拷贝、零移动 │ └───────────────────────────────────────┘StringBuffercreate_buffer(conststd::string&content){StringBuffer buf;// ① 局部变量buf.set(content);// ② 设置内容returnbuf;// ③ 返回局部变量// std::move(buf) // 显示会阻止NRVO}voidtask4_return_value(){std::cout<<"=== 任务 4: 按值返回 ===\n";StringBuffer::reset_counters();autobuf=create_buffer("returned data");std::cout<<"返回后: copies="<<StringBuffer::copies()<<" moves="<<StringBuffer::moves()<<"\n";std::cout<<"期望: copies=0 moves=0 (NRVO 优化) 或 moves=1 (移动)\n";std::cout<<"内容: \""<<buf.get()<<"\"\n\n";}NRVO优化后的输出
显式移动后的输出
移动语义陷阱
陷阱 1: move 了 const 对象
const T&& 不会被移动构造函数匹配, 会退化到拷贝构造
voidpitfall1_const_move(){std::cout<<"=== 陷阱 1: move const 对象 ===\n";StringBuffer::reset_counters();constStringBuffer buf;StringBuffer target=std::move(buf);// buf 是 const, std::move 得到 const StringBuffer&&// 实际调用拷贝构造! 不是移动!std::cout<<"const 对象的 std::move 会退化到拷贝, 不是移动\n\n";std::cout<<"copies: "<<StringBuffer::copies()<<"\n";std::cout<<"moves: "<<StringBuffer::moves()<<"\n";}陷阱2:move后还使用旧变量
voidpitfall3_use_after_move(){std::cout<<"=== 陷阱 3: move 后继续使用 ===\n";std::string s="important";std::string t=std::move(s);}触及了 C++ 移动语义中最核心的安全边界,接下来讲解下使用旧变量会有什么问题
风险操作
std::string s="important";std::string t=std::move(s);// 这些结果不确定:std::cout<<s;// 可能输出空串,也可能是乱码s[0];// 如果 s 内部指针已被置空,这是未定义行为!s.at(0);// 可能抛出 std::out_of_range危险操作
std::string s="important";std::string t=std::move(s);// 灾难:if(s=="important"){...}// 很可能不成立s.append("more");// 从"不确定内容"追加,结果未知for(charc:s){...}// 范围不确定安全使用
std::string s="important";std::string t=std::move(s);// 模式 1: 重新赋值后再用s="new value";std::cout<<s;// 安全,输出 "new value"// 模式 2: 调用 clear() 建立确定状态s.clear();s+="hello";// s 现在确定是 "hello"// 模式 3: 放在新的作用域,自动销毁{std::string temp=std::move(s);// 用 temp...}// temp 析构,s 随之外层也不用了多线程
unique_lock和lock_guard有什么区别
学习消费者-生产者模式时,看到消费者中需要使用unique_lock,而生产者则使用lock_guard,对这两个使用条件产生的疑问
先看代码里两者的使用位置
voidpush(std::string task){std::lock_guard<std::mutex>lock(mtx_);// ✅ 加锁 → 干活 → 自动解锁,够了queue_.push(std::move(task));cv_.notify_one();}std::stringpop(){std::unique_lock<std::mutex>lock(mtx_);// ← 为什么这里换成了 unique_lock?cv_.wait(lock,[this]{return!queue_.empty()||stopped_;});// ↑↑↑ 原因就在这里!wait() 的参数类型是 unique_lock,不是 lock_guard...}核心区别:unique_lock能"临时解锁",lock_guard不能
| 能力 | lock_guard | unique_lock |
|---|---|---|
| 构造时自动加锁 | ✅ | ✅ |
| 析构时自动解锁 | ✅ | ✅ |
手动unlock() | ❌ | ✅ |
手动lock() | ❌ | ✅ |
| 临时解锁再重新加锁 | ❌ | ✅← 这是关键! |
可以传给cv_.wait() | ❌ | ✅ |
| 可以移动所有权 | ❌ | ✅ |
| 内存开销 | 1 个指针 | 1 个指针 + 1 个 bool |
为什么wait()必须用unique_lock?
因为wait()在内部要做一件事:先解锁,再睡觉。而lock_guard根本不让你手动解锁:
std::lock_guard<std::mutex>lock(mtx_);lock.unlock();// ❌ 编译错误!lock_guard 没有 unlock() 方法unique_lock就可以:
std::unique_lock<std::mutex>lock(mtx_);lock.unlock();// ✅ 可以!lock.lock();// ✅ 重新加锁也可以!cv_.wait()内部做的事情等价于:
// wait() 内部伪代码template<typenamePredicate>voidwait(unique_lock<mutex>&lock,Predicate pred){while(!pred()){lock.unlock();// ① 把锁还回去,让生产者能拿到// ... 线程阻塞睡觉 ...// ... 被 notify 叫醒 ...lock.lock();// ② 重新拿锁}// 此时锁是拿着的,继续执行}图解对比
push() — lock_guard 够用: lock(mtx_) 🔒 queue_.push(...) ← 干活 cv_.notify_one() ← 通知 离开作用域 → unlock(mtx_) 🔓 全程拿着锁,不需要"临时放手",lock_guard 完美。 pop() — 必须用 unique_lock: lock(mtx_) 🔒 cv_.wait(lock, ...) │ ├─ 队列为空? │ ├─ unlock(mtx_) 🔓 ← 必须把锁还回去! │ └─ 睡觉... ← 否则生产者永远拿不到锁 → 死锁 │ └─ 被叫醒 └─ lock(mtx_) 🔒 ← 重新拿锁 queue_.front() ← 安全取数据 离开作用域 → unlock(mtx_) 🔓一句话总结
lock_guard = "我拿到锁就不放手,干完活自动放" unique_lock = "我拿到锁,但中间可能需要临时放手再拿回来"生产者(push)加锁 → 放数据 → 通知 → 解锁,一条直线走到底,不需要临时放手,所以lock_guard就行。
消费者(pop)加锁 → 发现没数据 →必须临时放手让生产者进来放数据→ 睡觉等通知 → 醒来重新拿锁,这个"放手再拿"的动作只有unique_lock支持,所以必须用它。
模板
特化版和通用版
template<typenameT>intcompare(constT&a,constT&b){// TODO: 实现// 返回: -1 (a<b), 0 (a==b), 1 (a>b)if(a<b)return-1;if(a>b)return1;return0;}// 特化版本: std::string 比较长度而不是字典序template<>intcompare<std::string>(conststd::string&a,conststd::string&b){if(a.length()<b.length())return-1;if(a.length()>b.length())return1;return0;}匹配机制
参数类型 → 有没有特化版? ├── 有 → ✅ 用特化版(最精准) └── 没有 → ✅ 用通用版(保底)历史包袱
变量声明的3种方式
// 方式 1: = (拷贝初始化)intcounter=0;// 方式 2: () (直接初始化)std::threadt1(print_message,"Hello",1);// 方式 3: {} (列表初始化 / 统一初始化,C++11 引入)std::atomic<int>processed{0};优先用 {},传构造参数用 (),简单 int 用 = 也无所谓。