1. 引言
在 C++ 的日常开发中,内存管理一直是开发者最头疼的问题之一。手动管理new和delete不仅繁琐,还容易引发内存泄漏、悬空指针、重复释放等难以排查的 bug。C++11 标准引入的智能指针(Smart Pointer)正是为了解决这些问题而生。
智能指针本质上是封装了原始指针的类模板,它利用 RAII(资源获取即初始化)机制,在对象生命周期结束时自动释放所管理的资源,从而让内存管理变得更加安全、高效。
本文将系统讲解 C++ 智能指针的核心概念、三种常用智能指针(unique_ptr、shared_ptr、weak_ptr)的原理与用法,并通过大量可运行的代码示例,帮助你从原理到实战全面掌握智能指针。
2. 为什么需要智能指针
2.1 传统裸指针的痛点
先看一个典型的裸指针使用场景:
voidprocess(){int*p=newint(42);// ... 中间可能抛出异常或提前 returndeletep;// 容易忘记,或提前 return 导致跳过}这段代码存在几个明显问题:
- 内存泄漏:忘记
delete,或函数中途return/ 抛异常,导致delete被跳过。 - 悬空指针:
delete之后指针仍指向已释放的内存,再次访问是未定义行为。 - 重复释放:多个指针指向同一块内存,各自
delete导致 double free。
2.2 RAII 思想
RAII 的核心思想是:资源在对象构造时获取,在对象析构时释放。智能指针正是 RAII 的典型应用——把裸指针包装进一个类对象,当这个对象离开作用域被销毁时,析构函数自动执行delete。
classSmartPtr{int*ptr;public:explicitSmartPtr(int*p):ptr(p){}~SmartPtr(){deleteptr;}// 自动释放int&operator*(){return*ptr;}};这样,无论函数正常结束还是抛出异常,智能指针对象都会在栈展开时被析构,资源必然得到释放。
3. 智能指针家族概览
C++11 标准库在<memory>头文件中提供了三种智能指针:
| 智能指针 | 所有权模型 | 适用场景 |
|---|---|---|
std::unique_ptr | 独占所有权 | 资源唯一归属,默认首选 |
std::shared_ptr | 共享所有权 | 多个对象共同持有同一资源 |
std::weak_ptr | 弱引用,不拥有资源 | 打破循环引用,观察资源 |
此外,C++98 时代的std::auto_ptr已被 C++11 废弃,切勿在新代码中使用。
4. unique_ptr:独占所有权
4.1 基本用法
std::unique_ptr独占管理一个对象,同一时刻只能有一个unique_ptr指向该资源。它不可拷贝,但可以移动。
#include<memory>#include<iostream>intmain(){// 推荐:使用 make_unique(C++14 起)std::unique_ptr<int>p=std::make_unique<int>(42);std::cout<<*p<<std::endl;// 42// 移动语义:所有权转移std::unique_ptr<int>q=std::move(p);// p 现在为空if(!p){std::cout<<"p 已为空"<<std::endl;}std::cout<<*q<<std::endl;// 42// 离开作用域时自动释放return0;}4.2 自定义删除器
unique_ptr支持自定义删除器,适用于管理非new分配的资源,如文件句柄、malloc内存等:
#include<memory>#include<cstdio>structFileCloser{voidoperator()(FILE*f)const{if(f){std::fclose(f);std::cout<<"文件已关闭"<<std::endl;}}};intmain(){std::unique_ptr<FILE,FileCloser>fp(std::fopen("test.txt","w"));// 离开作用域时自动调用 FileCloser 关闭文件return0;}4.3 作为函数返回值
unique_ptr可以作为函数返回值,此时会触发移动语义,所有权安全转移:
std::unique_ptr<int>createValue(){returnstd::make_unique<int>(100);}intmain(){autov=createValue();std::cout<<*v<<std::endl;// 100}5. shared_ptr:共享所有权
5.1 基本用法
std::shared_ptr允许多个智能指针共享同一资源,内部通过引用计数跟踪资源被多少个shared_ptr持有。当最后一个shared_ptr被销毁时,资源才被释放。
#include<memory>#include<iostream>intmain(){std::shared_ptr<int>p1=std::make_shared<int>(10);std::shared_ptr<int>p2=p1;// 拷贝:引用计数 +1std::shared_ptr<int>p3=p2;std::cout<<"use_count: "<<p1.use_count()<<std::endl;// 3p2.reset();// 引用计数 -1std::cout<<"use_count: "<<p1.use_count()<<std::endl;// 2// 最后一个 shared_ptr 销毁时自动释放内存return0;}5.2 引用计数原理
shared_ptr内部维护两个指针:
- 一个指向管理的对象;
- 一个指向控制块(control block),其中保存引用计数、删除器等信息。
当拷贝shared_ptr时,控制块中的引用计数加一;当shared_ptr被销毁或reset时,引用计数减一。计数归零时,控制块负责释放对象并销毁自身。
5.3 为什么优先使用 make_shared
std::shared_ptr<int>p1(newint(5));// 两次内存分配std::shared_ptr<int>p2=std::make_shared<int>(5);// 一次内存分配make_shared将对象和控制块分配在同一块内存中,只做一次分配,性能更好,且异常安全。因此优先使用make_shared。
6. weak_ptr:打破循环引用
6.1 循环引用问题
当两个对象通过shared_ptr互相持有对方时,会形成循环引用,导致引用计数永远不为零,内存无法释放:
#include<memory>#include<iostream>structNode{std::shared_ptr<Node>next;~Node(){std::cout<<"Node 析构"<<std::endl;}};intmain(){std::shared_ptr<Node>a=std::make_shared<Node>();std::shared_ptr<Node>b=std::make_shared<Node>();a->next=b;b->next=a;// 循环引用!// 离开作用域后,a 和 b 的引用计数都不为 0,Node 不会被析构return0;}运行这段代码,你会发现 “Node 析构” 永远不会打印——这就是内存泄漏。
6.2 用 weak_ptr 解决
std::weak_ptr是一种不增加引用计数的弱引用,它只能从shared_ptr创建,用于观察资源而不拥有资源:
#include<memory>#include<iostream>structNode{std::shared_ptr<Node>next;std::weak_ptr<Node>prev;// 用 weak_ptr 打破循环~Node(){std::cout<<"Node 析构"<<std::endl;}};intmain(){std::shared_ptr<Node>a=std::make_shared<Node>();std::shared_ptr<Node>b=std::make_shared<Node>();a->next=b;b->prev=a;// weak_ptr 不增加引用计数// 离开作用域后,a、b 正常析构return0;}6.3 使用 weak_ptr 访问资源
weak_ptr不能直接解引用,必须通过lock()临时提升为shared_ptr,并检查是否有效:
std::weak_ptr<int>w;{autos=std::make_shared<int>(99);w=s;std::cout<<"expired: "<<w.expired()<<std::endl;// 0(未失效)}// s 已销毁std::cout<<"expired: "<<w.expired()<<std::endl;// 1(已失效)if(autolocked=w.lock()){std::cout<<*locked<<std::endl;}else{std::cout<<"资源已释放"<<std::endl;}7. 智能指针的选择与最佳实践
7.1 如何选择
7.2 最佳实践清单
- 默认使用
unique_ptr:除非确实需要共享所有权,否则优先选择独占所有权。 - 优先使用
make_unique/make_shared:更安全、更高效。 - 不要混用裸指针和智能指针:避免把裸指针交给智能指针后又手动
delete。 - 用
weak_ptr打破循环引用:在双向链表、观察者模式等场景中尤其重要。 - 不要用
shared_ptr管理数组:除非提供自定义删除器,否则应使用std::vector或unique_ptr<T[]>。 - 避免在函数参数中传递
shared_ptr的拷贝:除非确实需要参与所有权,否则传引用或裸指针即可。
8. 常见陷阱与注意事项
8.1 不要用裸指针初始化多个 shared_ptr
int*raw=newint(5);std::shared_ptr<int>p1(raw);std::shared_ptr<int>p2(raw);// 错误!两个控制块,double free正确做法是只用一个shared_ptr接管,其余通过拷贝获得。
8.2 不要用 shared_ptr 管理栈上对象
intx=5;std::shared_ptr<int>p(&x);// 错误!析构时会 delete 栈上对象8.3 注意 this 指针的 shared_ptr 化
在类内部返回shared_ptr(this)会导致重复释放。正确做法是继承std::enable_shared_from_this:
#include<memory>structWidget:std::enable_shared_from_this<Widget>{std::shared_ptr<Widget>getShared(){returnshared_from_this();}};intmain(){autow=std::make_shared<Widget>();autow2=w->getShared();// 安全return0;}9. 总结
智能指针是 C++ 现代编程中内存管理的核心工具。回顾本文要点:
unique_ptr:独占所有权,轻量高效,是默认首选。shared_ptr:共享所有权,基于引用计数,适合多所有者场景。weak_ptr:弱引用,不增加计数,用于打破循环引用。
掌握智能指针,不仅能显著减少内存泄漏和悬空指针问题,还能让代码更简洁、更安全。建议在实际项目中养成「优先智能指针、避免裸指针」的习惯,配合make_unique/make_shared使用,写出更健壮的 C++ 代码。