[C++11] : raii思想与智能指针
C++11 RAII 思想与智能指针 详细教程
头文件:
RAII 是 C++ 核心编程思想,智能指针则是基于 RAII 实现的内存自动管理工具,用来解决 C++ 原生裸指针内存泄漏、野指针、重复释放等经典问题,也是 C++11 及现代 C++ 必备知识点。
一、什么是内存泄漏 & 裸指针痛点
1. 裸指针的常见问题
C++ 原生 new / delete 手动管理堆内存,全程需要开发者自己把控,极易出错:
- 内存泄漏:
new开辟内存后,忘记调用delete,程序退出前内存无法回收。 - 重复释放:同一块内存多次
delete,直接导致程序崩溃。 - 野指针:内存释放后,指针指向随机值但是继续被使用,访问非法内存。
- 悬空指针:某内存空间的支配权还给操作系统后,依然访问该空间内存,这是一种无权访问
- 分支/异常导致漏释放:代码分支多、抛出异常时,
delete无法执行。
示例:典型内存泄漏场景
#include <iostream>
using namespace std;
void test()
{
// 手动在堆上开辟内存
int* p = new int(100);
// 模拟业务逻辑,中途 return,跳过 delete
if (true)
{
return;
}
// 永远执行不到,内存泄漏
delete p;
}
int main()
{
test();
return 0;
}
问题:函数提前返回,delete 未执行,堆内存无人回收。
二、RAII 核心思想
1. 全称
RAII:Resource Acquisition Is Initialization
中文直译:资源获取即初始化
2. 核心原理
利用 C++ 类的构造函数、析构函数的生命周期特性 管理资源:
- 构造函数:对象创建时,自动获取/申请资源(堆内存、文件、锁、网络句柄等)。
- 析构函数:对象生命周期结束(出作用域)时,自动释放资源。
核心精髓:资源的生命周期 和 栈对象的生命周期绑定,不用手动释放。
栈对象由编译器自动管理,出作用域必然调用析构,资源一定被释放。
3. 手写简易 RAII 内存管理类(看懂本质)
我们自己封装一个类,模拟 RAII 管理堆内存:
#include <iostream>
using namespace std;
// 基于 RAII 封装内存管理
class MemGuard
{
private:
int* ptr; // 托管的裸指针
public:
// 构造函数:创建对象 → 申请资源
MemGuard(int* p) : ptr(p)
{
cout << "构造:接管堆内存" << endl;
}
// 析构函数:对象销毁 → 自动释放资源
~MemGuard()
{
cout << "析构:自动释放堆内存" << endl;
delete ptr;
ptr = nullptr;
}
// 重载 * 运算符,像普通指针一样使用
int& operator*()
{
return *ptr;
}
};
void test()
{
int* raw_p = new int(999);
// 栈对象 guard,生命周期在当前函数内
MemGuard guard(raw_p);
if (true)
{
return; // 函数提前退出
}
// 无需手动 delete
}
int main()
{
test();
// guard 出作用域,析构函数自动执行,释放内存
return 0;
}
运行逻辑:
guard是栈对象,函数结束/提前 return 都会被销毁。- 销毁时自动执行析构函数,执行
delete,彻底避免内存泄漏。
智能指针,本质就是 C++ 标准库提前写好的、通用 RAII 资源管理类
三、智能指针总览
C++11 标准库提供三种主流智能指针,全部定义在 <memory> 头文件:
| 智能指针 | 作用 | 所有权规则 | C++ 版本 |
|---|---|---|---|
std::unique_ptr |
独占式智能指针(推荐首选) | 独占所有权,不能拷贝,只可移动 | C++11 |
std::shared_ptr |
共享式智能指针 | 引用计数,多指针共享同一块内存 | C++11 |
std::weak_ptr |
弱引用指针 | 配合 shared_ptr,不增加引用计数,解决循环引用 | C++11 |
通用特性:
- 重载了
*、->,用法和裸指针几乎一致。 - 基于 RAII,出作用域自动释放内存。
四、std::unique_ptr 独占智能指针(最常用)
1. 核心特点
- 独占所有权:同一堆内存,只能被一个 unique_ptr 托管。
- 禁止拷贝构造、拷贝赋值(防止多个指针共管同一块内存)。
- 支持移动语义(std::move 转移所有权)。
- 开销极小,性能接近裸指针,日常开发首选。
2. 基本用法
2.1 创建与初始化
#include <iostream>
#include <memory>
using namespace std;
int main()
{
// 方式1:直接接收 new 出来的裸指针
unique_ptr<int> p1(new int(100));
// 方式2(C++14 推荐):make_unique,更安全、简洁
auto p2 = make_unique<int>(200);
// 像裸指针一样解引用
cout << *p1 << endl; // 100
cout << *p2 << endl; // 200
return 0;
}
2.2 禁止拷贝,支持移动
int main()
{
unique_ptr<int> p1 = make_unique<int>(10);
// unique_ptr<int> p2 = p1; // 编译报错!禁止拷贝
// 使用 std::move 转移所有权(移动语义)
unique_ptr<int> p2 = move(p1);
// 转移后,原指针 p1 变为空(不再托管内存)
if (p1 == nullptr)
{
cout << "p1 已空" << endl;
}
cout << *p2 << endl; // 10
return 0;
}
2.3 常用成员函数
unique_ptr<int> p = make_unique<int>(666);
p.get(); // 获取内部托管的裸指针
p.release(); // 释放管理权(返回裸指针,智能指针置空,**不会 delete**)
p.reset(); // 释放当前内存,指针置空
p.reset(new int(888)); // 释放旧内存,托管新内存
3. 使用场景
- 绝大多数单个对象、数组内存管理。
- 函数局部变量、类成员指针(优先使用)。
- 容器中存储指针。
五、std::shared_ptr 共享智能指针
1. 核心特点
- 共享所有权:允许多个
shared_ptr同时托管同一块堆内存。 - 引用计数机制:内部维护一个计数器:
- 新增一个
shared_ptr→ 计数 +1 - 一个
shared_ptr销毁 → 计数 -1 - 计数变为 0 时,自动释放堆内存。
- 新增一个
- 支持拷贝、赋值、移动。
- 相比
unique_ptr有少量计数开销,适合多指针共享资源场景。
2. 基本用法
2.1 创建与引用计数变化
#include <iostream>
#include <memory>
using namespace std;
int main()
{
// 推荐写法:make_shared(效率更高)
shared_ptr<int> p1 = make_shared<int>(100);
cout << "计数:" << p1.use_count() << endl; // 计数 = 1
// 拷贝,共享内存,计数 +1
shared_ptr<int> p2 = p1;
cout << "计数:" << p1.use_count() << endl; // 计数 = 2
{
// 局部 shared_ptr,出作用域自动销毁
shared_ptr<int> p3 = p1;
cout << "计数:" << p1.use_count() << endl; // 计数 = 3
}
// p3 销毁,计数 -1
cout << "计数:" << p1.use_count() << endl; // 计数 = 2
return 0;
}
执行流程:函数结束,p1、p2 依次销毁,计数减为 0,内存自动释放。
2.2 常用成员函数
shared_ptr<int> p = make_shared<int>(10);
p.use_count(); // 获取当前引用计数
p.reset(); // 放弃托管,计数-1,指针置空
p.get(); // 获取内部裸指针
3. 致命问题:循环引用(内存泄漏)
shared_ptr 最大坑点:两个对象互相使用 shared_ptr 指向对方,造成循环引用,计数永远不为0,内存泄漏。
循环引用示例
#include <iostream>
#include <memory>
using namespace std;
class B;
class A
{
public:
shared_ptr<B> pb;
~A() { cout << "A 析构" << endl; }
};
class B
{
public:
shared_ptr<A> pa;
~B() { cout << "B 析构" << endl; }
};
int main()
{
shared_ptr<A> a = make_shared<A>();
shared_ptr<B> b = make_shared<B>();
// 互相指向对方:循环引用
a->pb = b;
b->pa = a;
// 函数结束,a、b 销毁,但内部互相引用,计数无法归0
// A、B 的析构函数永远不执行 → 内存泄漏
return 0;
}
解决方案:使用 std::weak_ptr 弱指针打破循环。
六、std::weak_ptr 弱引用指针
1. 核心特点
- 弱引用:专门配合
shared_ptr使用。 - 不增加引用计数:只观察资源,不拥有资源所有权。
- 可以检测
shared_ptr托管的内存是否已经释放。 - 不能直接解引用使用,必须先提升为 shared_ptr。
2. 作用
- 解决
shared_ptr循环引用问题。 - 做“旁观者”,判断资源是否有效。
3. 修复上面的循环引用
将其中一个 shared_ptr 改为 weak_ptr:
#include <iostream>
#include <memory>
using namespace std;
class B;
class A
{
public:
shared_ptr<B> pb;
~A() { cout << "A 析构" << endl; }
};
class B
{
public:
// 改为弱指针,不增加引用计数
weak_ptr<A> pa;
~B() { cout << "B 析构" << endl; }
};
int main()
{
shared_ptr<A> a = make_shared<A>();
shared_ptr<B> b = make_shared<B>();
a->pb = b;
b->pa = a;
// 正常执行析构,内存释放
return 0;
}
4. 常用用法
shared_ptr<int> sp = make_shared<int>(10);
weak_ptr<int> wp = sp;
// 1. 判断资源是否存在
if (!wp.expired())
{
// 2. 弱指针提升为 shared_ptr 再使用
shared_ptr<int> temp = wp.lock();
cout << *temp << endl;
}
expired():判断指向的内存是否已释放(true=已释放)。lock():尝试提升为shared_ptr,资源失效则返回空指针。
七、智能指针与裸指针对比 & 选择建议
1. 优缺点对比
| 类型 | 优点 | 缺点 |
|---|---|---|
| 裸指针 | 无开销、灵活 | 手动管理内存,易泄漏、野指针、重复释放 |
| unique_ptr | 零额外开销、安全、语法简单 | 不能拷贝,仅独占使用 |
| shared_ptr | 支持共享所有权 | 引用计数有性能开销,易出现循环引用 |
| weak_ptr | 打破循环引用、安全观察 | 不能单独使用,依赖 shared_ptr |
2. 开发选择原则
- 优先使用
unique_ptr:绝大多数场景首选,性能最优,安全。 - 必须多指针共享同一块内存时,使用
shared_ptr。 - 使用
shared_ptr且存在互相引用时,搭配weak_ptr打破循环。 - 尽量少用裸指针;迫不得已使用裸指针时,保证
new和delete成对出现。 - 推荐使用
make_unique/make_shared创建智能指针,比直接new更安全。
八、新手常见误区总结
-
RAII 理解误区
RAII 靠栈对象生命周期自动释放资源,智能指针必须定义在栈上,不要用new创建智能指针。 -
unique_ptr 拷贝误区
unique_ptr 禁止拷贝,只能用std::move转移所有权。 -
shared_ptr 循环引用
两个类互相持有 shared_ptr 必然内存泄漏,务必用 weak_ptr 解决。 -
不要混用裸指针和智能指针
同一块内存不要同时被裸指针和多个智能指针共管,极易重复释放。 -
weak_ptr 不能直接解引用
弱指针只是观察者,必须通过lock()转为 shared_ptr 才能使用。
九、全文精简总结
- RAII 思想:资源获取即初始化,利用栈对象构造/析构自动管理资源,是智能指针的底层原理。
- 智能指针本质:基于 RAII 封装的指针类,自动释放堆内存,规避裸指针缺陷。
- unique_ptr:独占模式,不可拷贝、可移动,性能最高,日常首选。
- shared_ptr:共享模式,引用计数管理,多指针共用资源。
- weak_ptr:弱引用,不增加计数,专门解决 shared_ptr 循环引用问题。
- 现代 C++ 规范:能不用裸指针就不用裸指针,优先全套智能指针。
更多推荐

所有评论(0)