裸指针最大的麻烦其实不是「会忘写delete」而是所有权含糊不清这段内存归谁谁来释放读代码的人只能猜。std::unique_ptr把答案钉死在类型里同一时刻只有一个unique_ptr拥有这块内存要转移就必须显式写std::move。代价几乎为零既不比裸指针多占内存也不比它多花时间。下面把常用接口和几个容易踩的点一起讲清。指针身上的「所有权标签」看一段老式写法voidprocess(){int*pnewint(42);// ... 中途可能 return、可能抛异常 ...deletep;// 任何一条提前退出的路径忘了删就是泄漏}p归谁、什么时候释放全靠人脑盯着。换成std::unique_ptrint析构时自动释放提前 return 也好、抛异常也好从哪条路径退出都安全。这就是 RAII资源获取即初始化Resource Acquisition Is Initialization。官方文档std::unique_ptr — cppreference · RAII — cppreference独占所有权不可拷贝只能移动unique_ptr删除了拷贝构造和拷贝赋值只保留移动。试图拷贝会直接编译失败这正是它「独占」的体现。所有权转移必须用std::move显式表达。// 片段无 main展示独占语义#includememory#includeutilitystd::unique_ptrintastd::make_uniqueint(1);// std::unique_ptrint b a; // 编译错误拷贝被删除std::unique_ptrintbstd::move(a);// OK所有权从 a 转移到 ba 变空核心接口make_unique / get / release / reset / swapstd::make_uniqueC14 引入是现在唯一推荐的创建方式。它把new包在内部既避免裸new也消掉了「已经 new 出来、却还没进智能指针」这个危险窗口。其余几个接口分工如下接口作用是否释放原对象make_uniqueT(args)创建并拥有新对象—get()取裸指针不转移所有权否release()放弃所有权不释放返回裸指针否你要自己管reset()释放原对象可接手新指针是swap(other)交换两人的所有权否官方文档std::make_unique — cppreferencerelease() 与 reset() 的区别一个天一个地这两个名字最容易混语义却正相反。release()是「我不要了而且不销毁把指针还给你」reset()是「销毁当前对象顺便可以接手一个新的」。跑一遍就清楚了// up_release_reset.cpp — 编译: g -stdc17 -Wall -O2 up_release_reset.cpp -o ur#includecstdio#includememoryintmain(){std::unique_ptrintpstd::make_uniqueint(42);std::printf(拥有对象, *p%d\n,*p);int*rawp.release();// 放弃所有权但不释放p 变空std::printf(release 后: p 为空? %s, raw%d\n,p?否:是,*raw);deleteraw;// 现在轮到你负责释放std::unique_ptrintqstd::make_uniqueint(7);q.reset();// 释放对象q 变空std::printf(reset 后: q 为空? %s\n,q?否:是);}拥有对象, *p42 release 后: p 为空? 是, raw42 reset 后: q 为空? 是记住一句话就行release()交出但不销毁有泄漏风险reset()直接销毁。日常绝大多数场合该用reset()或者干脆什么都不写、交给析构函数别碰release()。除非你要把裸指针交给一个不支持智能指针的旧接口那时候没得选。自定义删除器对 sizeof 的影响实测unique_ptr的第二个模板参数用来定制删除器。这里有个值得记住的零开销细节无状态删除器empty deleter会被空基类优化EBOEmpty Base Optimization吃掉一个字节也不多占。换成函数指针做删除器就不一样了它必须在对象里存一个指针sizeof因此多出一个机器字。// up_sizeof.cpp — 编译: g -stdc17 -Wall -O2 up_sizeof.cpp -o us#includecstdio#includememorystructStatelessDeleter{voidoperator()(int*p)constnoexcept{deletep;}// 无状态空类型};voidfn_deleter(int*p)noexcept{deletep;}// 函数指针删除器intmain(){usingP1std::unique_ptrint;// 默认删除器无状态usingP2std::unique_ptrint,StatelessDeleter;// 自定义无状态删除器usingP3std::unique_ptrint,void(*)(int*);// 函数指针删除器携带值std::printf(unique_ptrint(默认删除器) sizeof %zu\n,sizeof(P1));std::printf(无状态自定义删除器 sizeof %zu\n,sizeof(P2));std::printf(函数指针删除器(携带值) sizeof %zu\n,sizeof(P3));std::printf(裸指针 int* sizeof %zu\n,sizeof(int*));// 必须真的给删除器赋一个函数指针它才会作为成员被存储std::unique_ptrint,void(*)(int*)q(newint(1),fn_deleter);std::printf(q 拥有对象, *q%d\n,*q);}unique_ptrint(默认删除器) sizeof 8 无状态自定义删除器 sizeof 8 函数指针删除器(携带值) sizeof 16 裸指针 int* sizeof 8 q 拥有对象, *q1对比表64 位平台1 机器字 8 字节形式删除器存储方式sizeof相对裸指针unique_ptrint默认无状态删除器EBO 优化掉8相等零开销无状态自定义删除器EBO 优化掉8相等零开销函数指针删除器对象内嵌一个指针成员16多 1 个机器字裸指针int*—8基准原因在对象布局上unique_ptrT, D内部嵌了一个D类型的删除器成员。当D无状态时默认的default_delete或者上面的StatelessDeleter编译器用 EBO 把它压没了大小自然等于一个裸指针。函数指针不是这么回事它本身就是个有值的东西必须实实在在占一个机器字于是sizeof变成「指针 函数指针」也就是 16。结论也就清楚了默认删除器和无状态删除器都是零开销这和标准库的承诺一致只有把函数指针当删除器类型时才多存一个机器字。日常写make_unique完全不用担心体积膨胀。官方文档std::unique_ptr 析构与删除器 — cppreference数组特化unique_ptrT[]管理动态数组用std::unique_ptrT[]不是裸new[]。make_uniqueint[](n)从 C14 起可用会调用new T[n]并在析构时delete[]。// up_array.cpp — 编译: g -stdc17 -Wall -O2 up_array.cpp -o ua#includecstdio#includememoryintmain(){autoarrstd::make_uniqueint[](4);// C14 起支持for(inti0;i4;i)arr[i]i*10;for(inti0;i4;i)std::printf(arr[%d]%d\n,i,arr[i]);}arr[0]0 arr[1]10 arr[2]20 arr[3]30注意unique_ptrT[]重载了operator[]但不支持*和-普通unique_ptrT则相反。别混用。作为函数参数/返回值用类型表达所有权unique_ptr出现在参数或返回值的位置时是在用类型本身告诉调用方所有权怎么流转按值返回std::unique_ptr意思是「我把所有权交出去了」按值接收std::unique_ptr参数意思是「这个函数接管所有权用完即释放」实参那边必须写std::move又一次靠类型系统逼你把转移写明白。// up_ownership.cpp — 编译: g -stdc17 -Wall -O2 up_ownership.cpp -o uo#includecstdio#includememory#includeutilitystd::unique_ptrintmake(){returnstd::make_uniqueint(99);// 返回即移交所有权}voidtake(std::unique_ptrintp){// 参数接管所有权std::printf(take 拿到 *p%d\n,*p);}// 离开作用域对象被释放intmain(){std::unique_ptrintamake();std::printf(make 返回后 *a%d\n,*a);take(std::move(a));// 必须显式 move所有权转移std::printf(take 之后 a 为空? %s\n,a?否:是);}make 返回后 *a99 take 拿到 *p99 take 之后 a 为空? 是所有权流转画成图是一条单向链同一份资源在任何时刻只有一个人拿着所有权流转同一时刻只有一个持有者 make() ──返回──► a ──std::move──► take(p) ──离开作用域──► 释放 创建 持有 接管 delete a 转移后变为空nullptr不能再解引用如果只想「借用」而不转移所有权参数就传const std::unique_ptrT或裸指针T*明确表达「我不拥有」这正是 C Core Guidelines 推荐的「裸指针只表示不拥有」。官方文档C Core Guidelines · R.32/R.33/R.34 —— 借用用引用/裸指针转移用unique_ptr把前面要点串起来// up_full.cpp — 编译: g -stdc17 -Wall -O2 up_full.cpp -o uf#includecstdio#includememory#includeutilitystd::unique_ptrintcreate(){returnstd::make_uniqueint(5);}intmain(){autopcreate();std::printf(p 拥有 %d\n,*p);int*borrowedp.get();// 借用不转移所有权std::printf(借用读到 %d\n,*borrowed);autoqstd::move(p);// 转移所有权std::printf(转移后 p 为空? %s, q 拥有 %d\n,p?否:是,*q);q.reset();// 释放std::printf(reset 后 q 为空? %s\n,q?否:是);}p 拥有 5 借用读到 5 转移后 p 为空? 是, q 拥有 5 reset 后 q 为空? 是延伸阅读std::unique_ptr — cppreference —— 全部接口与特化说明std::make_unique — cppreference —— 为什么优先用它不是裸newC Core Guidelines · R.30–R.34 —— 所有权传递的规范Compiler Explorer —— 对比unique_ptr与裸指针生成的汇编验证零开销收个尾接口其实就那几个真要养成的是一个阅读习惯看到unique_ptr就默认它独占看到get()就默认这只是借用。release()大多时候不该出现在业务代码里它把 delete 的责任又塞回了人手。零开销这个说法在默认用法下确实成立前提是你没往里面塞函数指针当删除器。那一个机器字花得挺冤。
阅读完成 · 觉得有帮助?