【C++】浅谈智能指针
C++智能指针是管理动态内存的重要工具,主要包括unique_ptr、shared_ptr和weak_ptr三种类型。unique_ptr实现独占所有权,shared_ptr通过引用计数实现共享所有权,weak_ptr则用于解决shared_ptr的循环引用问题。智能指针遵循RAII原则,确保资源自动释放。创建时应优先使用make_unique/make_shared,避免直接new。shared
文章目录
1. 指针基础
指针全名为指针变量,是C++中用于存储内存地址的变量。计算机在存储数据时采用有序存放的方式,为了能够准确访问每个数据的位置,需要使用地址来区分,指针变量就是专门用来存放这些地址的变量。
2. 智能指针的本质与实现原理
智能指针本质上是封装了原始C++指针的类模板,是为了确保动态内存安全性而产生的。其实现原理是通过一个对象存储需要被自动释放的资源,然后依靠对象的析构函数来释放资源。
智能指针是C++11引入的最重要特性之一,用于自动管理动态分配的内存,防止内存泄漏和悬空指针问题。它们遵循RAII(Resource Acquisition Is Initialization)原则,确保资源在不再需要时被正确释放。
3. 智能指针类型
C++标准库提供了三种主要的智能指针:
3.1 std::unique_ptr(独占所有权指针)
- 特点:独占所指向的对象,不允许复制,但可以移动
- 用途:用于管理独占所有权的资源,替代原始指针的首选
- 内存开销:几乎为零(通常与原始指针大小相同)
3.2 std::shared_ptr(共享所有权指针)
- 特点:多个shared_ptr可以共享同一个对象的所有权,使用引用计数
- 用途:用于需要多个所有者共享同一资源的场景
- 内存开销:需要存储引用计数(通常是指针大小的两倍)
3.3 std::weak_ptr(弱引用指针)
- 特点:不增加引用计数,用于解决shared_ptr的循环引用问题
- 用途:观察shared_ptr管理的对象,但不拥有所有权
- 内存开销:与shared_ptr类似
4. 关键知识点
4.1 创建智能指针
优先使用std::make_unique
和std::make_shared
:
//(C++14起)
auto ptr1 = std::make_unique<int>(42);
auto ptr2 = std::make_shared<std::string>("Hello");
// 传统方式
std::unique_ptr<int> ptr3(new int(42));
std::shared_ptr<std::string> ptr4(new std::string("Hello"));
4.2 所有权转移
// unique_ptr 的所有权转移
std::unique_ptr<int> source = std::make_unique<int>(42);
std::unique_ptr<int> destination = std::move(source); // source 变为空
// shared_ptr 的所有权共享
std::shared_ptr<int> shared1 = std::make_shared<int>(42);
std::shared_ptr<int> shared2 = shared1; // 引用计数增加
5. shared_ptr的所有权共享机制
当多个shared_ptr共享同一对象时:
std::shared_ptr<int> shared1 = std::make_shared<int>(42);
std::shared_ptr<int> shared2 = shared1; // 共享同一对象
底层机制详解:
-
第一行:
std::make_shared<int>(42)
- 在堆上分配内存并构造一个值为42的int对象
- 同时创建一个控制块,其中包含引用计数(初始为1)和其他管理信息
- shared1包含两个指针:一个指向int对象,一个指向控制块
-
第二行:
shared2 = shared1
- shared2复制了shared1的两个指针:指向同一int对象和同一控制块
- 控制块中的引用计数增加(从1变为2)
6. 循环引用问题及解决方案
6.1 什么是循环引用?
循环引用发生在两个或多个对象通过shared_ptr相互引用时,形成一个闭环,导致它们的引用计数永远不会降为零,从而无法被自动销毁。
6.2 基本示例
#include <iostream>
#include <memory>
class B; // 前向声明
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed\n"; }
};
class B {
public:
std::shared_ptr<A> a_ptr;
~B() { std::cout << "B destroyed\n"; }
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
// 形成循环引用
a->b_ptr = b;
b->a_ptr = a;
std::cout << "A use count: " << a.use_count() << std::endl;
std::cout << "B use count: " << b.use_count() << std::endl;
return 0;
// 程序结束时不会打印"A destroyed"和"B destroyed"
}
6.3 循环引用的工作原理
- 创建a时,A对象的引用计数为1
- 创建b时,B对象的引用计数为1
- 当执行
a->b_ptr = b
时:- B对象的引用计数增加为2
- 现在有两个shared_ptr指向B对象:b和a->b_ptr
- 当执行
b->a_ptr = a
时:- A对象的引用计数增加为2
- 现在有两个shared_ptr指向A对象:a和b->a_ptr
- 当main函数结束时:
- 局部变量a和b被销毁,它们的引用计数减1
- A对象的引用计数变为1(来自b->a_ptr)
- B对象的引用计数变为1(来自a->b_ptr)
- 由于引用计数不为零,A和B对象都不会被销毁
- 但没有任何外部指针可以访问这些对象了,导致内存泄漏
6.4 解决方案:使用std::weak_ptr
std::weak_ptr
是一种不控制对象生命周期的智能指针,它指向一个由shared_ptr管理的对象,但不会增加引用计数。
修复循环引用:
#include <iostream>
#include <memory>
class B; // 前向声明
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed\n"; }
};
class B {
public:
std::weak_ptr<A> a_ptr; // 使用weak_ptr而不是shared_ptr
~B() { std::cout << "B destroyed\n"; }
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a; // weak_ptr不会增加引用计数
std::cout << "A use count: " << a.use_count() << std::endl;
std::cout << "B use count: " << b.use_count() << std::endl;
return 0;
// 程序结束时会打印"B destroyed"和"A destroyed"
}
7. weak_ptr的计数方式与内存分配
7.1 weak_ptr真的不计数吗?
- weak_ptr不参与强引用计数:不会阻止对象被销毁
- weak_ptr参与弱引用计数:用于管理控制块的生命周期
7.2 内存分配方式
- 使用
std::make_shared
:对象和控制块在同一内存块中分配 - 直接使用new:对象和控制块分开分配
7.3 控制块内容
控制块包含:
- 强引用计数
- 弱引用计数
- 其他管理信息
7.4 生命周期管理
- 对象生命周期:当强引用计数变为0时,对象被销毁(调用析构函数)
- 控制块生命周期:当强引用计数和弱引用计数都变为0时,控制块被释放
7.5 可视化示例
时间线:
1. shared_ptr 创建: [控制块: strong=1, weak=0] → [对象存在]
2. weak_ptr 创建: [控制块: strong=1, weak=1] → [对象存在]
3. shared_ptr 重置: [控制块: strong=0, weak=1] → [对象销毁!]
4. weak_ptr 检查: 对象已销毁,但控制块存在
5. weak_ptr 销毁: [控制块: strong=0, weak=0] → [控制块销毁]
7.6 形象比喻
想象一个热气球(对象)和它的锚点(控制块):
shared_ptr
是抓住气球绳子的人- 每个shared_ptr都用力拉着绳子(强引用计数+1)
- 只要还有人拉着,气球就不会飞走(对象不会被销毁)
weak_ptr
是旁边看气球的人- 他们只观察气球,并不拉绳子(不增加强引用计数)
- 但他们需要知道这个锚点位置是否存在(弱引用计数+1)
发生了什么?
当最后一个shared_ptr松手(强引用数归零):
- 气球立刻飞走销毁(对象内存被释放)
- 但锚点还在地上(控制块还在)
- 因为还有weak_ptr观察者需要知道"这里曾经有个气球"
当最后一个weak_ptr也离开:
- 锚点被移除(控制块内存最终释放)
weak_ptr不阻止对象被销毁(不计强引用数),但需要控制块活着来告诉你对象是否还存在(计弱引用数)。这就是它既能解决循环引用,又能安全检查对象状态的原因
本文基于学习笔记整理
更多推荐
所有评论(0)