C++ —— 智能指针
C++ ——智能指针
- 智能指针存在的必要性
- 1. 解决内存泄漏问题
- 2. 避免悬垂指针(Dangling Pointer)
- 3. 异常安全性
- std::unique_ptr (独占所有权)
- 代码功能说明
- 关键点解析
- 内存管理流程
- 对比传统指针
- 为何使用 `make_unique`?
- unique_ptr的特点
- 独占资源所有权,不可复制
- 移动语义允许转移所有权
- 智能指针数组
- 模拟实现unique_ptr
- shared_ptr
- shared_ptr模拟实现
- 循环引用
- 程序输出
- 内存状态图示
- 引用计数变化全过程
- **内存状态图示(函数结束后)**
- weak_ptr
- weak_ptr的特性
- 1. 非占有性观察
- 2. 解决循环引用
- 3. 安全访问机制
- 4. 生命周期监控
- 5. 控制块分离
- 6. 内存管理规则
- 7. 线程安全性
- 8. 典型应用场景
- 9. 性能特点
- 10. 限制与注意事项
- 完整工作流程示例
- 与 `shared_ptr` 的关键区别
在学习C++智能指针之前,我们要先学习一下,为啥要有智能指针:
智能指针存在的必要性
C++ 引入智能指针主要是为了解决传统裸指针(raw pointer)在内存管理中存在的诸多问题,通过自动化资源管理来提升代码的安全性和可维护性。以下是智能指针存在的核心原因及其价值:
1. 解决内存泄漏问题
- 传统指针的缺陷:
手动new
/delete
容易因忘记释放或执行路径异常(如抛出异常)导致内存泄漏。void riskyFunction() { int* raw_ptr = new int(42); if (some_condition) throw std::runtime_error("Oops"); // 内存泄漏! delete raw_ptr; // 可能永远不会执行 }
2. 避免悬垂指针(Dangling Pointer)
- 传统指针的问题:
指针指向的对象被释放后,指针仍可能被误用(访问已释放内存)。int* dangling_ptr; { int x = 10; dangling_ptr = &x; } // x 被销毁 std::cout << *dangling_ptr; // 未定义行为!
3. 异常安全性
- 传统代码的风险:
在多个new
和delete
之间抛出异常会导致资源泄漏。void unsafe() { int* p1 = new int(1); int* p2 = new int(2); // 如果此处抛出异常,p1 泄漏! delete p1; delete p2; }
再这样的背景之下,我们提出了智能指针:
std::unique_ptr (独占所有权)
我们首先来看看unique_ptr,这应该是用的最多的智能指针之一:
int main()
{
//unique_ptr 的使用
std::unique_ptr<int> ptr = make_unique<int>(10);
std::cout << *ptr << std::endl;
}
这段代码演示了 std::unique_ptr
的基本用法,其功能是创建一个独占所有权的智能指针来管理一个动态分配的整数。以下是详细解析:
代码功能说明
#include <memory> // 必需的头文件
#include <iostream>
int main() {
// 1. 创建一个独占指针,管理动态分配的 int 值 10
std::unique_ptr<int> ptr = std::make_unique<int>(10);
// 2. 解引用指针并输出值
std::cout << *ptr << std::endl; // 输出: 10
// 3. main() 结束时,ptr 自动释放内存(无需手动 delete)
}
关键点解析
-
std::make_unique<int>(10)
- 动态分配一个
int
类型的内存,初始化为10
。 - 返回一个
std::unique_ptr<int>
对象,独占该内存的所有权。 - 这是 C++14 引入的工厂函数,比直接
new
更安全(避免内存泄漏)。
- 动态分配一个
-
std::unique_ptr<int>
- 独占所有权:该指针唯一拥有其指向的内存,不可复制(但可移动)。
- 自动释放:当
ptr
离开作用域(如main
函数结束)时,自动调用delete
释放内存。
-
*ptr
- 通过解引用操作访问指针指向的值(此处为
10
)。
- 通过解引用操作访问指针指向的值(此处为
内存管理流程
+-------------------+ +-----+
| std::unique_ptr | --> | 10 |
| (栈内存) | +-----+
+-------------------+ ↑
动态分配的内存(堆)
对比传统指针
操作 | 传统指针 (int* ) | std::unique_ptr<int> |
---|---|---|
初始化 | int* p = new int(10); | auto p = make_unique<int>(10); |
释放内存 | 需手动 delete p; | 自动释放 |
异常安全性 | 可能泄漏 | 安全 |
所有权语义 | 不明确 | 明确独占 |
为何使用 make_unique
?
-
避免显式
new
// 不推荐(潜在内存泄漏风险) std::unique_ptr<int> p1(new int(10)); // 推荐(异常安全) auto p2 = std::make_unique<int>(10);
-
性能优化
make_unique
将内存分配和构造合并,可能减少代码生成开销。
unique_ptr的特点
unique_ptr主要有以下几个特点:
独占资源所有权,不可复制
unique_ptr对于自己的资源是自己享有的,其他人无权接手。
移动语义允许转移所有权
但是如果move一下,移动语义可以把所有权转交:
智能指针数组
C++14支持数组的创建:
int main()
{
std::unique_ptr<int[]> ptr = make_unique<int[]>(5);
for (int i = 0; i < 5; i++)
{
ptr[i] = i;
std::cout << ptr[i] << " ";
}
}
如果是C++11,还需要使用new:
std::unique_ptr<int[]> arr(new int[10]); // C++11 方式
模拟实现unique_ptr
template<class T>
class my_unique_ptr
{
public:
//构造函数
my_unique_ptr(T* p = nullptr)
:_ptr(p)
{
}
//禁止拷贝
my_unique_ptr(const my_unique_ptr&) = delete;
my_unique_ptr& operator=(const my_unique_ptr&) = delete;
//移动拷贝
my_unique_ptr(my_unique_ptr&& other)
:_ptr(other._ptr)
{
other._ptr = nullptr;
}
//移动赋值拷贝
my_unique_ptr&& operator= (my_unique_ptr&& other)
{
if (this != other)
{
delete _ptr;
_ptr = other._ptr;
other._ptr = nullptr;
}
return *this;
}
~my_unique_ptr() //析构函数
{
delete _ptr;
}
T& operator*() const
{
return *_ptr;
}
T* operator->() const
{
return _ptr;
}
//释放所有权
T* release()
{
T* p = _ptr;
_ptr = nullptr;
return p;
}
//重置指针
void reset(T* p = nullptr)
{
delete _ptr;
_ptr = p;
}
//交换指针
void swap(my_unique_ptr& other)
{
std::swap(_ptr, other._ptr);
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
};
int main()
{
my_unique_ptr<int> p1(new int(42));
std::cout << *p1 << std::endl; // 输出: 42
my_unique_ptr<int> p2 = std::move(p1); // 所有权转移
if (!p1.get()) {
std::cout << "p1 is now null" << std::endl;
}
}
shared_ptr
shared_ptr 最大的特点就是所有权可以共享:
int main()
{
std::shared_ptr<int> ptr1 = std::make_shared<int>(67);
auto ptr2 = ptr1;
//引用计数
std::cout << ptr2.use_count() << std::endl; //use_count()查看引用计数
std::cout << *ptr1 << std::endl;
std::cout << *ptr2 << std::endl;
}
shared_ptr模拟实现
template<class T>
class my_shared_ptr
{
public:
//构造函数
my_shared_ptr(T* p = nullptr)
:_ptr(p)
,_count(new size_t(1))
{
}
//拷贝构造
my_shared_ptr(const my_shared_ptr& other)
:_ptr(other._ptr)
,_count(other._count)
{
if (_count) (*_count)++;
}
//拷贝赋值
my_shared_ptr& operator=(const my_shared_ptr& other)
{
if (this != &other)
{
release();
_ptr = other._ptr;
_count = other._count;
if (_count) (*_count)++;
}
return *this;
}
//移动构造
my_shared_ptr(const my_shared_ptr&& other)
:_ptr(other._ptr)
,_count(other._count)
{
other._ptr = nullptr;
other._count = nullptr;
}
//移动赋值
my_shared_ptr& operator=(const my_shared_ptr&& other)
{
if (this != &other)
{
release();
_ptr = other._ptr;
_count = other._count;
other._ptr = nullptr;
other._count = nullptr;
}
}
//析构函数
~my_shared_ptr()
{
release();
}
//解引用
T& operator*() const
{
return *_ptr;
}
T* operator->() const
{
return _ptr;
}
//获取引用计数
size_t use_count() const
{
return _count ? *_count : 0;
}
//获取原始指针
T* get()
{
return _ptr;
}
private:
//释放资源
void release()
{
if (_count)
{
(*_count)--;
if (*_count == 0)
{
delete _ptr;
delete _count;
}
}
}
T* _ptr;
size_t* _count = nullptr;
};
int main()
{
my_shared_ptr<int> p1(new int(42));
std::cout << "p1 value: " << *p1 << ", count: " << p1.use_count() << std::endl;
{
my_shared_ptr<int> p2 = p1;
std::cout << "p1 count: " << p1.use_count() << ", p2 count: " << p2.use_count() << std::endl;
} // p2 析构,计数减1
std::cout << "p1 count after p2 destroyed: " << p1.use_count() << std::endl;
return 0;
}
循环引用
下面是一个的循环引用示例,展示父子对象相互持有 shared_ptr
导致的内存泄漏:
#include <memory>
#include <iostream>
class Child; // 前向声明
class Parent {
public:
std::shared_ptr<Child> child; // 父对象持有子对象的shared_ptr
Parent() { std::cout << "Parent created\n"; }
~Parent() { std::cout << "Parent destroyed\n"; }
};
class Child {
public:
std::shared_ptr<Parent> parent; // 子对象也持有父对象的shared_ptr
Child() { std::cout << "Child created\n"; }
~Child() { std::cout << "Child destroyed\n"; }
};
void createFamily() {
auto parent = std::make_shared<Parent>();
auto child = std::make_shared<Child>();
// 建立相互引用
parent->child = child;
child->parent = parent;
std::cout << "Parent ref count: " << parent.use_count() << std::endl; // 2
std::cout << "Child ref count: " << child.use_count() << std::endl; // 2
} // 函数结束时,parent和child的引用计数只减1,不会变为0!
int main() {
createFamily();
std::cout << "检查内存是否泄漏...\n";
// 这里应该看到Parent和Child的析构函数没有被调用!
return 0;
}
程序输出
注意:没有看到 “Parent destroyed” 和 “Child destroyed” 的输出,证明内存泄漏了!
内存状态图示
+-------------------+ +-------------------+
| Parent | | Child |
| [shared_ptr]─────┼────>| [shared_ptr] |
| | | |
| child ───────────┼────>| parent ───────────┼──┐
+-------------------+ +-------------------+ │
▲ │
└─────────────────────────────────────────┘
详细解释为什么对象不会被析构:
引用计数变化全过程
-
对象创建时:
auto parent = std::make_shared<Parent>(); // parent引用计数=1 auto child = std::make_shared<Child>(); // child引用计数=1
-
建立相互引用后:
parent->child = child; // child被parent.child引用 → child计数=2 child->parent = parent; // parent被child.parent引用 → parent计数=2
-
函数结束时:
- 局部变量
child
析构 → child引用计数从2减到1(因为parent->child
还引用它) - 局部变量
parent
析构 → parent引用计数从2减到1(因为child->parent
还引用它) - 结果:
- parent的最终引用计数=1(由
child->parent
持有) - child的最终引用计数=1(由
parent->child
持有) - 两者都无法释放!
- parent的最终引用计数=1(由
- 局部变量
内存状态图示(函数结束后)
[Parent对象] [Child对象]
+-------------+ +-------------+
| child ──────┼──────>| |
| | | parent ─────┼──┐
+-------------+ +-------------+ │
▲ │
└────────────────────────────────┘
- 两个对象互相持有对方的
shared_ptr
,形成闭环 - 没有外部引用,但引用计数永远不会归零
那么如何解决这个问题呢?我们会使用weak_ptr
weak_ptr
class Child; // 前向声明
class Parent {
public:
std::weak_ptr<Child> child; // 换成weak_ptr
Parent() { std::cout << "Parent created\n"; }
~Parent() { std::cout << "Parent destroyed\n"; }
};
class Child {
public:
std::shared_ptr<Parent> parent; // 子对象也持有父对象的shared_ptr
Child() { std::cout << "Child created\n"; }
~Child() { std::cout << "Child destroyed\n"; }
};
void createFixedFamily() {
auto parent = std::make_shared<Parent>();
auto child = std::make_shared<Child>();
parent->child = child;
child->parent = parent; // weak_ptr不会增加引用计数
std::cout << "Fixed Parent ref count: " << parent.use_count() << std::endl; // 1
std::cout << "Fixed Child ref count: " << child.use_count() << std::endl; // 2
} // parent引用计数变为0 → 释放parent → 释放child
int main() {
createFixedFamily();
std::cout << "现在应该看到析构消息了!\n";
return 0;
}
weak_ptr的特性
weak_ptr
是 C++ 智能指针体系中的重要组件,它与 shared_ptr
配合使用,主要解决共享所有权模型中的循环引用问题。以下是其核心特性的详细说明:
1. 非占有性观察
- 不增加引用计数
weak_ptr
观察shared_ptr
管理的对象,但不会增加其引用计数。auto shared = std::make_shared<int>(42); std::weak_ptr<int> weak = shared; // shared.use_count() 仍为 1
2. 解决循环引用
- 经典场景
当两个对象互相持有shared_ptr
时会导致内存泄漏:struct Node { std::shared_ptr<Node> next; // 循环引用! };
- 解决方案
将单向引用改为weak_ptr
:struct Node { std::weak_ptr<Node> next; // 打破循环 };
3. 安全访问机制
- 必须通过
lock()
提升为shared_ptr
检查对象是否存在并获取访问权:if (auto shared = weak.lock()) { std::cout << *shared; // 对象存活 } else { std::cout << "对象已释放"; }
4. 生命周期监控
expired()
快速检查
判断关联的shared_ptr
是否已释放:if (!weak.expired()) { // 对象尚未释放 }
5. 控制块分离
-
与
shared_ptr
共享控制块
weak_ptr
和shared_ptr
共同维护控制块,包含:- 强引用计数(
shared_ptr
使用) - 弱引用计数(
weak_ptr
使用) - 原始指针
控制块结构: +---------------------+ | 强引用计数 (use_count) | | 弱引用计数 (weak_count) | | 原始指针 (ptr) | +---------------------+
- 强引用计数(
6. 内存管理规则
- 强引用归零时
释放托管对象内存,但控制块保留(直到弱引用也归零)。 - 弱引用归零时
释放控制块内存。
7. 线程安全性
- 引用计数操作是原子的
可安全跨线程传递weak_ptr
,但lock()
后对对象的访问需额外同步。
8. 典型应用场景
场景 | 说明 |
---|---|
打破循环引用 | 父子关系、双向链表等相互引用的场景 |
缓存系统 | 持有缓存对象的弱引用,当内存不足时自动释放 |
观察者模式 | 主题对象持有观察者的弱引用,避免观察者延长主题生命周期 |
跨模块对象访问 | 模块间传递非拥有性引用,避免资源管理冲突 |
9. 性能特点
- 低开销
仅维护弱引用计数,不参与对象生命周期管理。 lock()
的代价
需要原子操作检查强引用计数,比直接使用shared_ptr
略慢。
10. 限制与注意事项
- 不能直接解引用
必须通过lock()
转换为shared_ptr
后使用。 - 不适用于单例模式
无法阻止对象被shared_ptr
释放。 - 控制块内存延迟释放
即使对象已释放,控制块会保留到最后一个weak_ptr
析构。
完整工作流程示例
// 创建共享对象
auto shared = std::make_shared<std::string>("Hello");
// 创建弱引用
std::weak_ptr<std::string> weak = shared;
// 使用对象
if (auto locked = weak.lock()) {
std::cout << *locked << std::endl; // 输出: Hello
}
// 释放对象
shared.reset();
// 检查对象状态
std::cout << "Expired: " << weak.expired() << std::endl; // 输出: 1 (true)
与 shared_ptr
的关键区别
特性 | shared_ptr | weak_ptr |
---|---|---|
所有权 | 拥有对象 | 仅观察对象 |
引用计数影响 | 增加强引用计数 | 增加弱引用计数 |
内存释放时机 | 强引用归零释放对象 | 弱引用归零释放控制块 |
直接访问 | 支持 operator* /-> | 必须通过 lock() |
weak_ptr
通过这种设计,既保持了资源管理的安全性,又提供了解决循环引用的灵活手段,是现代 C++ 资源管理体系中不可或缺的部分。