ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

cpp特性记录

cpp特性记录

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_count2(与外部 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_sharedmake_unique
推荐程度⭐⭐⭐ 默认首选⭐⭐⭐ 默认首选
唯一硬伤weak_ptr 延迟释放不能传删除器
何时用 new自定义删除器、大对象+weak_ptrC++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 扩容:

  1. std::vector 扩容时需要把旧内存中的元素搬移到新内存。
  2. vector 面临一个选择:用移动还是用拷贝?
  3. 如果用移动,但移动过程中抛了异常,旧内存已经被破坏,新内存也不完整 → 数据丢失,无法恢复。
  4. 所以 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_guardunique_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 用 = 也无所谓。

返回列表