一、裸指针的风险与智能指针的诞生
1. 传统内存管理的痛点
在 C++98 时代,手动内存管理存在三大核心问题:
- 内存泄漏:
new分配的内存未被delete释放 - 双重释放:多个指针指向同一内存,多次
delete导致崩溃 - 悬空指针:对象已被释放,但仍有指针引用它
典型案例:
void process() {
int* ptr = new int(42);
// 业务逻辑...
if (condition) return; // 直接返回导致内存泄漏
delete ptr; // 若condition为true,此句不会执行
}
2. RAII(资源获取即初始化)范式
智能指针通过 RAII 原则解决上述问题:
- 在构造时获取资源(如分配内存)
- 在析构时释放资源(如
delete内存) - 生命周期结束时自动触发析构,确保资源释放
二、std::unique_ptr:独占所有权的智能指针
1. 核心特性
- 独占性:同一时刻只能有一个
unique_ptr指向该对象 - 不可复制:禁用拷贝构造和赋值运算符
- 可移动:通过
std::move转移所有权
#include <memory>
void demo_unique_ptr() {
// 创建方式1:直接构造
std::unique_ptr<int> ptr1(new int(10));
// 创建方式2(推荐):使用make_unique(C++14)
auto ptr2 = std::make_unique<int>(20);
// 转移所有权
std::unique_ptr<int> ptr3 = std::move(ptr2); // ptr2变为空
// 访问对象
std::cout << *ptr3 << std::endl; // 输出20
// 释放所有权
int* raw_ptr = ptr3.release(); // ptr3变为空,raw_ptr需手动管理
delete raw_ptr;
}
2. 实战技巧
- 自定义删除器:
// 使用lambda作为删除器
auto deleter = [](int* p) {
std::cout << "Custom deleting..." << std::endl;
delete p;
};
std::unique_ptr<int, decltype(deleter)> ptr(new int(42), deleter);
- 作为容器元素:
std::vector<std::unique_ptr<int>> vec;
vec.push_back(std::make_unique<int>(1));
vec.push_back(std::make_unique<int>(2));
- 返回值优化:
std::unique_ptr<MyClass> create_object() {
return std::make_unique<MyClass>(); // 自动移动语义
}
三、std::shared_ptr:共享所有权的智能指针
1. 引用计数原理
- 每个
shared_ptr关联一个引用计数 - 拷贝 / 赋值时计数 + 1,析构时计数 - 1
- 计数为 0 时自动释放对象
#include <memory>
struct MyClass {
~MyClass() { std::cout << "Destroying MyClass" << std::endl; }
};
void demo_shared_ptr() {
auto ptr1 = std::make_shared<MyClass>(); // 计数=1
{
auto ptr2 = ptr1; // 计数=2
} // ptr2析构,计数=1
// ptr1析构,计数=0,对象被销毁
}
2. 循环引用问题
当两个对象通过shared_ptr互相引用时,会导致内存泄漏:
struct B;
struct A {
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed" << std::endl; }
};
struct B {
std::shared_ptr<A> a_ptr;
~B() { std::cout << "B destroyed" << std::endl; }
};
void cyclic_reference() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a; // 循环引用:a和b的引用计数永远不会为0
} // 内存泄漏!
3. 性能考量
- 每个
shared_ptr比裸指针大(通常为 2 个指针大小) - 引用计数操作需要原子性保证,存在性能开销
- 优先使用
unique_ptr,仅在必要时使用shared_ptr
四、std::weak_ptr:解决循环引用的利器
1. 弱引用特性
- 不增加引用计数,不控制对象生命周期
- 用于观察
shared_ptr管理的对象 - 通过
lock()方法获取临时shared_ptr
struct B;
struct A {
std::weak_ptr<B> b_ptr; // 使用weak_ptr打破循环引用
~A() { std::cout << "A destroyed" << std::endl; }
};
struct B {
std::shared_ptr<A> a_ptr;
~B() { std::cout << "B destroyed" << std::endl; }
};
void fix_cyclic_reference() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b; // weak_ptr不增加引用计数
b->a_ptr = a; // 只有a的引用计数=2
// 使用weak_ptr
if (auto shared_b = a->b_ptr.lock()) {
// shared_b是临时的shared_ptr,确保对象存在
}
} // a和b均正确释放
2. 典型应用场景
- 缓存系统:
weak_ptr观察缓存对象,避免阻止对象被回收 - 观察者模式:观察者持有被观察对象的
weak_ptr - 树形结构:子节点用
weak_ptr指向父节点
五、智能指针使用最佳实践
-
优先使用智能指针:
- 避免手动
new/delete,减少内存管理负担
- 避免手动
-
选择合适的智能指针:
- 独占场景用
unique_ptr - 共享场景用
shared_ptr - 打破循环引用用
weak_ptr
- 独占场景用
-
避免混合使用裸指针和智能指针:
int* raw = new int; std::shared_ptr<int> ptr(raw); // 危险!多个所有者可能释放同一内存 -
使用 make 系列函数创建智能指针:
std::make_unique(C++14)和std::make_shared- 提供更好的异常安全性和性能优化
-
避免在函数参数中使用智能指针:
- 除非明确需要转移所有权或共享所有权
- 优先传递对象引用或裸指针
六、智能指针与多线程
1. 线程安全特性
- 引用计数的增减是原子操作(线程安全)
- 对象的读写操作需要额外同步(与裸指针相同)
2. 跨线程传递智能指针
#include <thread>
#include <memory>
void worker(std::shared_ptr<int> data) {
// 使用data...
}
int main() {
auto data = std::make_shared<int>(42);
std::thread t(worker, data); // 安全传递shared_ptr
t.join();
return 0;
}
七、C++20 新增功能
1. std::make_shared_for_overwrite
// 无需值初始化的内存分配(适合POD类型)
auto ptr = std::make_shared_for_overwrite<int[]> (1000);
2. 智能指针的分配器支持
// 使用自定义分配器的shared_ptr
auto alloc = MyAllocator<int>();
auto ptr = std::allocate_shared<int>(alloc, 42);
总结
智能指针是现代 C++ 最核心的特性之一,通过 RAII 原则和引用计数机制,彻底改变了内存管理方式。合理使用unique_ptr、shared_ptr和weak_ptr,不仅能消除内存泄漏风险,还能提升代码的健壮性和可维护性。
1446

被折叠的 条评论
为什么被折叠?



