一、先搞懂:为什么需要智能指针?
c++ 原生指针(裸指针)最大的问题是手动管理内存容易出错,比如:
- 忘记释放内存 → 内存泄漏;
- 提前释放内存 → 野指针(访问已释放的内存);
- 重复释放内存 → 程序崩溃;
- 异常场景下(比如函数中途抛异常),释放代码执行不到 → 内存泄漏。
智能指针的核心是利用 raii 机制(资源获取即初始化):把内存管理绑定到智能指针的生命周期,当智能指针超出作用域(如函数结束、对象销毁)时,自动调用析构函数释放内存,彻底避免手动管理的问题。
二、c++ 四大智能指针详解(附代码示例)
c++ 标准库(<memory> 头文件)提供了 4 种智能指针,其中 auto_ptr 已被废弃,重点掌握后 3 种:
| 智能指针 | 核心特性 | 适用场景 | 线程安全 |
|---|---|---|---|
| auto_ptr | 独占所有权,已废弃 | 无(c++11 后用 unique_ptr 替代) | 否 |
| unique_ptr | 独占所有权,不可拷贝 | 单个对象/数组的独占管理 | 否 |
| shared_ptr | 共享所有权,引用计数 | 多个对象共享同一块内存 | 引用计数线程安全,对象本身否 |
| weak_ptr | 弱引用,不增加引用计数 | 解决 shared_ptr 循环引用 | 否 |
1.auto_ptr(废弃!仅作了解)
- 问题:拷贝时会转移所有权(原指针变为空),极易导致野指针,c++11 已被 unique_ptr 取代。
- 反例(为什么废弃):
#include <memory> #include <iostream> using namespace std; int main() { auto_ptr<int> p1(new int(10)); auto_ptr<int> p2 = p1; // 拷贝后,p1 所有权转移给 p2,p1 变为空 cout << *p2 << endl; // 正常输出 10 // cout << *p1 << endl; // 崩溃!p1 是空指针 return 0; }
2.unique_ptr(最常用!独占式智能指针)
- 核心:保证同一时间只有一个
unique_ptr指向一块内存,不可拷贝,仅可移动(std::move)。 - 优势:轻量级(性能接近裸指针),支持管理数组(
unique_ptr<int[]>)。 - 示例:
#include <memory> #include <iostream> using namespace std; int main() { // 1. 基本使用:独占内存,超出作用域自动释放 unique_ptr<int> p(new int(10)); cout << *p << endl; // 输出 10 // 2. 不可拷贝(编译报错) // unique_ptr<int> p2 = p; // 3. 可移动(转移所有权) unique_ptr<int> p3 = move(p); // cout << *p << endl; // 崩溃!p 已无所有权 cout << *p3 << endl; // 输出 10 // 4. 管理数组(自动调用 delete[]) unique_ptr<int[]> arr(new int[3]{1,2,3}); cout << arr[1] << endl; // 输出 2 return 0; // p3、arr 超出作用域,自动释放内存 }
3.shared_ptr(共享式智能指针)
- 核心:通过引用计数管理内存:每多一个
shared_ptr指向同一块内存,计数+1;指针销毁时计数-1;计数为 0 时,自动释放内存。 - 注意:不能用裸指针初始化多个
shared_ptr(会导致重复释放),推荐用make_shared初始化(更高效)。 - 示例:
#include <memory> #include <iostream> using namespace std; int main() { // 1. 初始化(推荐 make_shared,避免内存碎片) shared_ptr<int> p1 = make_shared<int>(10); cout << p1.use_count() << endl; // 输出 1(引用计数) // 2. 拷贝:引用计数+1 shared_ptr<int> p2 = p1; cout << p1.use_count() << endl; // 输出 2 cout << *p2 << endl; // 输出 10 // 3. 销毁一个指针:引用计数-1 p1.reset(); // 手动释放 p1 的所有权 cout << p2.use_count() << endl; // 输出 1 return 0; // p2 超出作用域,计数变为 0,内存释放 }
4.weak_ptr(弱引用指针,解决循环引用)
核心:指向 shared_ptr 管理的内存,但不增加引用计数,不会阻止内存释放;需先转为 shared_ptr 才能访问对象。
解决的问题:shared_ptr 循环引用(如 a 指向 b,b 指向 a)会导致引用计数永远不为 0,内存泄漏,weak_ptr 可破解。
示例(循环引用问题 + 解决):
#include <memory>
using namespace std;
// 循环引用问题(内存泄漏)
struct a { shared_ptr<b> b; ~a() { cout << "a 析构" << endl; } };
struct b { shared_ptr<a> a; ~b() { cout << "b 析构" << endl; } };
// 用 weak_ptr 解决循环引用
struct c { shared_ptr<d> d; ~c() { cout << "c 析构" << endl; } };
struct d { weak_ptr<c> c; ~d() { cout << "d 析构" << endl; } };
int main() {
// 1. 循环引用:析构函数不会执行,内存泄漏
auto a = make_shared<a>();
auto b = make_shared<b>();
a->b = b;
b->a = a;
// 2. 用 weak_ptr 解决:析构函数正常执行
auto c = make_shared<c>();
auto d = make_shared<d>();
c->d = d;
d->c = c; // d 用 weak_ptr 指向 c,不增加引用计数
return 0;
}输出:
d 析构
c 析构
(a、b 的析构函数未执行,c、d 正常执行)
三、智能指针的核心使用原则
- 优先用
unique_ptr:性能最好,独占所有权,满足大部分场景; - 需要共享时用
shared_ptr:避免循环引用,搭配weak_ptr; - 禁止用裸指针初始化多个
shared_ptr:会导致重复释放; - 不要用智能指针管理非堆内存(如栈上的对象):析构时会调用
delete,导致崩溃; - 自定义删除器:如果管理的资源不是
new分配的(如malloc、文件句柄),需自定义删除器:// 示例:管理文件句柄(自定义删除器关闭文件) shared_ptr<file> fp(fopen("test.txt", "r"), [](file* f) { fclose(f); });
总结
- 智能指针的核心价值是自动管理内存,解决裸指针的内存泄漏、野指针等问题;
unique_ptr是首选(独占、高效),shared_ptr用于共享场景,weak_ptr解决循环引用;auto_ptr已废弃,切勿使用;- 本质是 raii 机制:把内存释放逻辑封装到析构函数,利用 c++ 自动调用析构的特性,实现“不用手动释放”。
智能指针是c++ raii(资源获取即初始化)机制的核心实现,旨在解决手动管理指针导致的内存泄漏、野指针、重复释放等问题。以下先深度解析shared_ptr的实现原理(基于参考回答优化),再讲解unique_ptr和weak_ptr的实现逻辑与核心差异。
一、shared_ptr 实现详解(带完整可运行代码)
shared_ptr的核心是引用计数:多个shared_ptr共享同一个底层指针,引用计数记录当前指针被引用的次数,仅当计数为0时,才真正释放底层内存。
1. 核心设计思路
| 核心成员 | 作用 |
|---|---|
| t* ptr | 指向底层资源的原始指针 |
| int* use_count | 引用计数的指针(所有共享对象指向同一块内存,保证计数共享) |
| 拷贝构造/赋值重载 | 增加引用计数 |
| 析构函数 | 减少引用计数,计数为0时释放资源+计数内存 |
2. 完整实现(修复参考回答的bug,补充线程安全)
#include <iostream>
#include <mutex> // 引入互斥锁保证线程安全
using namespace std;
template <typename t>
class sharedptr {
private:
t* ptr; // 底层资源指针
int* use_count; // 引用计数(指针形式,保证所有对象共享)
mutex* mtx; // 互斥锁(保证多线程下计数操作原子性)
// 私有辅助函数:释放当前资源
void release() {
lock_guard<mutex> lock(*mtx); // 加锁保证线程安全
if (--(*use_count) == 0) { // 引用计数减1,若为0则释放资源
delete ptr; // 释放底层内存
delete use_count; // 释放计数内存
delete mtx; // 释放锁内存
ptr = nullptr;
use_count = nullptr;
mtx = nullptr;
}
}
public:
// 1. 构造函数:接收原始指针
sharedptr(t* p = nullptr) : ptr(p) {
try {
use_count = new int(1); // 初始计数为1
mtx = new mutex(); // 初始化锁
} catch (...) { // 内存分配失败,释放资源避免泄漏
delete ptr;
ptr = nullptr;
throw; // 抛出异常,让上层处理
}
}
// 2. 拷贝构造函数:共享资源,计数+1
sharedptr(const sharedptr<t>& other) {
ptr = other.ptr;
use_count = other.use_count;
mtx = other.mtx;
// 加锁更新计数(多线程安全)
lock_guard<mutex> lock(*mtx);
++(*use_count);
}
// 3. 赋值运算符重载:先释放自己的资源,再共享对方的资源
sharedptr<t>& operator=(const sharedptr<t>& other) {
// 防止自赋值(p = p)
if (this == &other) return *this;
// 第一步:释放当前对象持有的资源
release();
// 第二步:共享新对象的资源,计数+1
ptr = other.ptr;
use_count = other.use_count;
mtx = other.mtx;
lock_guard<mutex> lock(*mtx);
++(*use_count);
return *this;
}
// 4. 析构函数:计数-1,计数为0则释放资源
~sharedptr() {
if (use_count != nullptr) { // 避免重复释放
release();
}
}
// 5. 重载解引用运算符:模拟原始指针行为
t& operator*() const {
if (ptr == nullptr) {
throw runtime_error("解引用空指针");
}
return *ptr;
}
// 6. 重载->运算符:模拟原始指针行为
t* operator->() const {
if (ptr == nullptr) {
throw runtime_error("访问空指针成员");
}
return ptr;
}
// 7. 获取引用计数
int get_count() const {
lock_guard<mutex> lock(*mtx);
return *use_count;
}
// 8. 获取原始指针(谨慎使用)
t* get() const {
return ptr;
}
// 禁用移动构造/赋值(简化版,c++11后可实现)
sharedptr(sharedptr<t>&&) = delete;
sharedptr<t>& operator=(sharedptr<t>&&) = delete;
};
// 测试代码
int main() {
sharedptr<int> p1(new int(10));
cout << "p1计数:" << p1.get_count() << endl; // 输出1
cout << "*p1 = " << *p1 << endl; // 输出10
sharedptr<int> p2 = p1;
cout << "p1计数:" << p1.get_count() << endl; // 输出2
cout << "p2计数:" << p2.get_count() << endl; // 输出2
*p2 = 20;
cout << "*p1 = " << *p1 << endl; // 输出20(共享资源)
sharedptr<int> p3(new int(30));
p2 = p3; // p2赋值给p3,p1计数变为1,p3计数变为2
cout << "p1计数:" << p1.get_count() << endl; // 输出1
cout << "p3计数:" << p3.get_count() << endl; // 输出2
return 0;
}3. 关键细节(修复参考回答的问题)
- 线程安全:
- 参考回答未处理多线程,实际
shared_ptr的引用计数操作必须加锁; - 每个
shared_ptr对象共享同一个互斥锁,保证计数增减的原子性。
- 参考回答未处理多线程,实际
- 赋值运算符重载:
- 参考回答的赋值逻辑错误(先加对方计数,再减自己计数),正确逻辑是“先释放自己的资源,再共享对方的资源”;
- 必须处理自赋值(
p = p),否则会导致计数错误。
- 异常安全:
- 构造函数中若
use_count分配失败,需释放ptr避免内存泄漏; - 解引用/访问成员时检查空指针,避免野指针访问。
- 构造函数中若
4. shared_ptr 核心特性
- 共享所有权:多个
shared_ptr可指向同一资源; - 自动释放:最后一个指向资源的
shared_ptr析构时,释放底层内存; - 线程安全:
- 引用计数的增减是原子操作(多线程安全);
- 但
shared_ptr的读写操作(如p = q)需手动加锁(非线程安全)。
二、unique_ptr 实现详解
unique_ptr的核心是独占所有权:同一时间只有一个unique_ptr能指向某个资源,禁止拷贝,仅支持移动(c++11)。
1. 核心设计思路
| 核心特性 | 实现方式 |
|---|---|
| 独占所有权 | 禁用拷贝构造/赋值运算符 |
| 移动语义 | 实现移动构造/赋值,转移资源所有权 |
| 自动释放 | 析构函数直接释放底层资源(无需计数) |
2. 完整实现
#include <iostream>
#include <utility> // 引入move
using namespace std;
template <typename t>
class uniqueptr {
private:
t* ptr; // 独占的底层资源指针
public:
// 1. 构造函数:接收原始指针
explicit uniqueptr(t* p = nullptr) : ptr(p) {}
// 2. 禁用拷贝构造(独占所有权,禁止拷贝)
uniqueptr(const uniqueptr<t>&) = delete;
// 3. 禁用赋值运算符
uniqueptr<t>& operator=(const uniqueptr<t>&) = delete;
// 4. 移动构造函数:转移资源所有权
uniqueptr(uniqueptr<t>&& other) noexcept {
ptr = other.ptr;
other.ptr = nullptr; // 源对象置空,避免重复释放
}
// 5. 移动赋值运算符:转移资源所有权
uniqueptr<t>& operator=(uniqueptr<t>&& other) noexcept {
if (this == &other) return *this;
// 释放当前资源
delete ptr;
// 转移所有权
ptr = other.ptr;
other.ptr = nullptr;
return *this;
}
// 6. 析构函数:直接释放资源(无计数,独占所以直接删)
~uniqueptr() {
delete ptr;
ptr = nullptr;
}
// 7. 重载解引用运算符
t& operator*() const {
if (ptr == nullptr) throw runtime_error("解引用空指针");
return *ptr;
}
// 8. 重载->运算符
t* operator->() const {
if (ptr == nullptr) throw runtime_error("访问空指针成员");
return ptr;
}
// 9. 释放资源(手动放弃所有权)
t* release() {
t* temp = ptr;
ptr = nullptr;
return temp;
}
// 10. 重置资源
void reset(t* p = nullptr) {
delete ptr;
ptr = p;
}
// 11. 获取原始指针
t* get() const {
return ptr;
}
};
// 测试代码
int main() {
uniqueptr<int> p1(new int(10));
cout << "*p1 = " << *p1 << endl; // 输出10
// uniqueptr<int> p2 = p1; // 编译错误:禁用拷贝构造
uniqueptr<int> p2 = move(p1); // 移动构造,p1置空
// cout << *p1 << endl; // 崩溃:p1已空
cout << "*p2 = " << *p2 << endl; // 输出10
p2.reset(new int(20)); // 重置资源
cout << "*p2 = " << *p2 << endl; // 输出20
return 0;
}
3. unique_ptr 核心特性
- 独占性:禁止拷贝,仅能通过
std::move转移所有权; - 轻量级:无引用计数,内存开销仅等于原始指针;
- 高效:析构时直接释放资源,无计数操作开销;
- 支持数组:
unique_ptr<int[]>会自动调用delete[]释放数组。
三、weak_ptr 实现详解
weak_ptr是为解决shared_ptr的循环引用问题而生的“弱引用”指针:
- 不拥有资源所有权,仅能观察
shared_ptr管理的资源; - 不增加引用计数,不会阻止资源释放;
- 需通过
lock()转换为shared_ptr后才能访问资源。
1. 核心设计思路
| 核心成员 | 作用 |
|---|---|
| t* ptr | 指向底层资源的指针(仅观察,不拥有) |
| int* use_count | 共享shared_ptr的引用计数(仅读取,不修改) |
| int* weak_count | 弱引用计数(记录weak_ptr的数量) |
| mutex* mtx | 共享互斥锁 |
2. 完整实现(依赖shared_ptr)
#include <iostream>
#include <mutex>
using namespace std;
// 先声明sharedptr,供weakptr使用
template <typename t> class sharedptr;
template <typename t>
class weakptr {
private:
t* ptr; // 底层资源指针(仅观察)
int* use_count; // 共享_ptr的强引用计数
int* weak_count; // 弱引用计数
mutex* mtx; // 共享锁
// 私有辅助函数:释放弱引用
void release_weak() {
lock_guard<mutex> lock(*mtx);
if (--(*weak_count) == 0) { // 弱引用计数为0时,释放计数内存
delete use_count;
delete weak_count;
delete mtx;
use_count = nullptr;
weak_count = nullptr;
mtx = nullptr;
}
}
public:
// 1. 构造函数:默认构造
weakptr() : ptr(nullptr), use_count(nullptr), weak_count(nullptr), mtx(nullptr) {}
// 2. 从shared_ptr构造
weakptr(const sharedptr<t>& sp) {
ptr = sp.get();
use_count = sp.use_count_ptr(); // 需要sharedptr暴露use_count指针(友元)
weak_count = sp.weak_count_ptr();
mtx = sp.get_mutex();
lock_guard<mutex> lock(*mtx);
++(*weak_count); // 弱引用计数+1
}
// 3. 拷贝构造
weakptr(const weakptr<t>& wp) {
ptr = wp.ptr;
use_count = wp.use_count;
weak_count = wp.weak_count;
mtx = wp.mtx;
lock_guard<mutex> lock(*mtx);
++(*weak_count);
}
// 4. 赋值运算符
weakptr<t>& operator=(const weakptr<t>& wp) {
if (this == &wp) return *this;
if (weak_count != nullptr) release_weak(); // 释放当前弱引用
ptr = wp.ptr;
use_count = wp.use_count;
weak_count = wp.weak_count;
mtx = wp.mtx;
lock_guard<mutex> lock(*mtx);
++(*weak_count);
return *this;
}
// 5. 析构函数:弱引用计数-1
~weakptr() {
if (weak_count != nullptr) {
release_weak();
}
}
// 6. 核心方法:lock()转换为shared_ptr
sharedptr<t> lock() const {
lock_guard<mutex> lock(*mtx);
if (*use_count > 0) { // 强引用计数>0,资源未释放
return sharedptr<t>(ptr, use_count, weak_count, mtx); // 自定义sharedptr构造函数
}
return sharedptr<t>(); // 资源已释放,返回空shared_ptr
}
// 7. 检查资源是否有效(未被释放)
bool expired() const {
lock_guard<mutex> lock(*mtx);
return *use_count == 0;
}
// 8. 获取强引用计数
int use_count() const {
lock_guard<mutex> lock(*mtx);
return *use_count;
}
// 禁用移动构造/赋值(简化版)
weakptr(weakptr<t>&&) = delete;
weakptr<t>& operator=(weakptr<t>&&) = delete;
// 友元声明:让sharedptr访问weakptr的私有成员
friend class sharedptr<t>;
};
// 补充sharedptr的扩展(支持weakptr)
template <typename t>
class sharedptr {
private:
t* ptr;
int* use_count;
int* weak_count; // 新增弱引用计数
mutex* mtx;
void release() {
lock_guard<mutex> lock(*mtx);
if (--(*use_count) == 0) {
delete ptr;
ptr = nullptr;
}
// 弱引用计数处理移交给weakptr
}
// 供weakptr::lock()调用的私有构造函数
sharedptr(t* p, int* uc, int* wc, mutex* m) : ptr(p), use_count(uc), weak_count(wc), mtx(m) {
lock_guard<mutex> lock(*mtx);
++(*use_count);
}
public:
// 原有构造函数扩展弱引用计数
sharedptr(t* p = nullptr) : ptr(p) {
try {
use_count = new int(1);
weak_count = new int(0); // 初始弱引用计数为0
mtx = new mutex();
} catch (...) {
delete ptr;
ptr = nullptr;
throw;
}
}
// 暴露给weakptr的接口
int* use_count_ptr() const { return use_count; }
int* weak_count_ptr() const { return weak_count; }
mutex* get_mutex() const { return mtx; }
// 其余方法(拷贝构造、赋值、析构等)需适配weak_count,此处省略...
t* get() const { return ptr; }
};3. weak_ptr 核心特性
- 弱引用:不增加强引用计数,仅观察资源;
- 解决循环引用:如链表节点互相持有
shared_ptr会导致计数无法归零,改用weak_ptr即可; - 安全访问:通过
lock()获取shared_ptr后才能访问资源,避免访问已释放的资源; - expired():检查资源是否已被释放,防止野指针。
四、三种智能指针核心对比
| 特性 | shared_ptr | unique_ptr | weak_ptr |
|---|---|---|---|
| 所有权 | 共享 | 独占 | 无(仅观察) |
| 引用计数 | 强引用计数(控制释放) | 无 | 弱引用计数(仅记录) |
| 拷贝/移动 | 支持拷贝,移动 | 禁用拷贝,支持移动 | 支持拷贝,禁用移动 |
| 内存开销 | 2个指针(资源+计数)+锁 | 1个指针(仅资源) | 2个指针(资源+计数)+锁 |
| 核心场景 | 多对象共享资源 | 单对象独占资源 | 解决shared_ptr循环引用 |
| 访问方式 | 直接解引用 | 直接解引用 | 先lock()转为shared_ptr |
五、总结(核心要点回顾)
- shared_ptr:
- 核心是引用计数,多线程下需加锁保证计数原子性;
- 拷贝/赋值时增加计数,析构时减少计数,计数为0释放资源;
- 缺点:有计数开销,易产生循环引用(需weak_ptr解决)。
- unique_ptr:
- 独占所有权,禁用拷贝,仅支持移动;
- 轻量级、高效,无计数开销,优先用于单所有权场景;
- 支持数组类型,自动适配delete/delete[]。
- weak_ptr:
- 弱引用,不影响资源释放,专门解决shared_ptr循环引用;
- 需通过lock()转为shared_ptr才能访问资源,保证访问安全;
- 核心方法:lock()(获取shared_ptr)、expired()(检查资源有效性)。
智能指针的实现核心是raii:将资源生命周期绑定到智能指针对象的生命周期,对象析构时自动释放资源,从根本上避免内存泄漏。实际开发中优先使用标准库的std::shared_ptr/std::unique_ptr,其实现更完善(支持自定义删除器、数组、线程安全等)。
到此这篇关于c++四个智能指针的使用小结的文章就介绍到这了,更多相关c++ 智能指针内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论