C++ 智能指针详解
一、简介
C++ 智能指针是 C++11 标准引入的重要特性,用于解决传统 C++ 中手动内存管理带来的各种问题,包括内存泄漏、野指针、重复释放等。智能指针本质上是封装了原始指针的类模板,通过 RAII(Resource Acquisition Is Initialization)技术实现自动内存管理。
主要优势:
- 自动内存释放:当智能指针离开作用域时,自动释放其管理的内存
- 所有权明确:清晰表达资源的所有权关系
- 异常安全:即使发生异常,资源也能被正确释放
- 线程安全:部分智能指针提供线程安全的引用计数
C++11 提供了三种主要的智能指针:
unique_ptr:独占所有权的智能指针shared_ptr:共享所有权的智能指针weak_ptr:不拥有所有权的弱引用指针
二、unique_ptr
unique_ptr 是一种独占所有权的智能指针,同一时间只能有一个 unique_ptr 指向某个对象。当 unique_ptr 被销毁时,它所指向的对象也会被自动释放。
基本特性:
- 独占所有权:不能复制,只能移动
- 零开销:与原始指针相比几乎没有性能损失
- 支持自定义删除器
基本用法:
#include <memory>
#include <iostream>
using namespace std;
// 创建并返回 unique_ptr
unique_ptr<int> create_unique() {
// 方式1:使用 new 直接初始化
unique_ptr<int> uptr(new int(10));
// 方式2:使用 make_unique (C++14 推荐)
// auto uptr = make_unique<int>(10);
cout << "函数内值: " << *uptr << endl;
return uptr; // 通过移动语义返回
}
int main() {
// 接收返回的 unique_ptr
unique_ptr<int> uptr = create_unique();
// 访问对象
cout << "main 中值: " << *uptr << endl;
// 获取原始指针(但不应该手动释放)
int* raw_ptr = uptr.get();
cout << "原始指针值: " << *raw_ptr << endl;
// 释放所有权(返回原始指针并释放管理权)
int* released_ptr = uptr.release();
delete released_ptr; // 需要手动释放
// 检查是否为空
if (!uptr) {
cout << "uptr 已释放所有权" << endl;
}
return 0;
}
运行结果:
函数内值: 10
main 中值: 10
原始指针值: 10
uptr 已释放所有权
注意事项:
- 不能复制,只能移动:
unique_ptr删除了拷贝构造函数和拷贝赋值运算符 - 可以使用
std::move()转移所有权 - 优先使用
make_unique(C++14) 而不是直接 new,更安全高效 - get() 只是临时借用地址,所有权仍归智能指针
- release() 是彻底移交所有权,之后必须手动释放内存
三、move
由于 unique_ptr 独占所有权的特性,不能直接复制,只能通过 std::move() 转移所有权。
#include <memory>
#include <iostream>
using namespace std;
int main() {
// 创建 unique_ptr
unique_ptr<int> uptr1(new int(42));
cout << "uptr1 值: " << *uptr1 << endl;
cout << "uptr1 地址: " << uptr1.get() << endl;
// 转移所有权(使用 std::move)
unique_ptr<int> uptr2 = move(uptr1);
cout << "转移后:" << endl;
cout << "uptr1 地址: " << uptr1.get() << endl; // 变为 nullptr
cout << "uptr2 值: " << *uptr2 << endl;
cout << "uptr2 地址: " << uptr2.get() << endl; // 与原 uptr1 相同
// 检查 uptr1 是否为空
if (!uptr1) {
cout << "uptr1 已变为空" << endl;
}
return 0;
}
运行结果:
uptr1 值: 42
uptr1 地址: 0x55a95eb762b0
转移后:
uptr1 地址: 0
uptr2 值: 42
uptr2 地址: 0x55a95eb762b0
uptr1 已变为空
重要原则:
- 移动后源指针变为
nullptr,不再拥有对象所有权 - 确保不会同时有多个
unique_ptr管理同一对象 - 在函数间传递时,优先使用移动语义而非原始指针
四、share_ptr
shared_ptr 实现了共享所有权的智能指针,多个 shared_ptr 可以指向同一个对象,通过引用计数机制管理内存。当最后一个 shared_ptr 被销毁时,对象才会被释放。
引用计数机制:
- 每个
shared_ptr都会增加引用计数 - 拷贝赋值会增加引用计数
- 析构或重置会减少引用计数
- 当引用计数归零时,自动删除管理对象
基本用法:
#include <memory>
#include <iostream>
using namespace std;
// 创建并返回 shared_ptr
shared_ptr<int> create_shared() {
// 方式1:使用 new 直接初始化
shared_ptr<int> sptr(new int(10));
// 方式2:使用 make_shared (推荐,更高效)
// auto sptr = make_shared<int>(10);
cout << "函数内值: " << *sptr << endl;
cout << "函数内引用计数: " << sptr.use_count() << endl;
return sptr;
}
int main() {
// 创建 shared_ptr
shared_ptr<int> sptr1 = create_shared();
cout << "sptr1 引用计数: " << sptr1.use_count() << endl;
// 共享所有权(拷贝构造)
shared_ptr<int> sptr2 = sptr1;
cout << "创建 sptr2 后引用计数: " << sptr1.use_count() << endl;
// 再次共享
shared_ptr<int> sptr3 = sptr2;
cout << "创建 sptr3 后引用计数: " << sptr1.use_count() << endl;
// 获取原始指针
int* raw_ptr = sptr1.get();
cout << "原始指针值: " << *raw_ptr << endl;
// 释放一个 shared_ptr
sptr1.reset();
cout << "释放 sptr1 后引用计数: " << sptr2.use_count() << endl;
// 再次释放
sptr2.reset();
cout << "释放 sptr2 后引用计数: " << sptr3.use_count() << endl;
// 最后一个 shared_ptr 释放时,对象自动销毁
sptr3.reset();
cout << "释放 sptr3 后引用计数: 0" << endl;
// 注意:此时 raw_ptr 已成为悬空指针,不应再使用
// cout << *raw_ptr << endl; // 危险行为!
return 0;
}
运行效果:
函数内值: 10
函数内引用计数: 1
sptr1 引用计数: 1
创建 sptr2 后引用计数: 2
创建 sptr3 后引用计数: 3
原始指针值: 10
释放 sptr1 后引用计数: 2
释放 sptr2 后引用计数: 1
释放 sptr3 后引用计数: 0
循环引用问题:
shared_ptr 可能存在循环引用问题,导致内存泄漏:
#include <memory>
#include <iostream>
using namespace std;
struct Node {
shared_ptr<Node> next;
shared_ptr<Node> prev;
int value;
Node(int val) : value(val) {
cout << "Node " << val << " 创建" << endl;
}
~Node() {
cout << "Node " << value << " 销毁" << endl;
}
};
int main() {
// 创建循环引用
shared_ptr<Node> node1 = make_shared<Node>(1);
shared_ptr<Node> node2 = make_shared<Node>(2);
node1->next = node2; // node2的引用计数变为2(node2本身和node1->next都指向它)
node2->prev = node1; // node1的引用计数变为2(node1本身和node2->prev都指向它)
// node1离开作用域,引用计数从 2 减为 1(不是 0)
// node2离开作用域,引用计数从 2 减为 1(不是 0)
// 即使离开作用域,引用计数也不会归零,导致内存泄漏
return 0;
}
注意事项:
- 避免循环引用,这会导致引用计数无法归零,造成内存泄漏:
- 可以使用 std::make_shared 更安全高效地创建 shared_ptr;
五、make_shared
简介:
std::make_shared 是 C++11 引入的模板函数,定义在 <memory> 头文件中,用于创建 std::shared_ptr 智能指针。它是推荐创建 shared_ptr 的方式,相比直接使用 shared_ptr 构造函数有显著优势。
基本用法:
#include <memory>
#include <iostream>
using namespace std;
// 尾置返回类型
// 完全等价 shared_ptr<int> func()
auto func() -> shared_ptr<int> {
// 使用 make_shared 动态分配内存
auto sptr = make_shared<int>(10);
cout << "函数中: " << *sptr << endl;
return sptr; // 返回 shared_ptr
}
int main() {
// 接收返回的 shared_ptr
shared_ptr<int> sptr1 = func();
// 共享所有权
shared_ptr<int> sptr2 = sptr1;
// 获取原始指针
int* ptr = sptr1.get();
// 输出信息
cout << "*sptr1: " << *sptr1 << endl;
cout << "*sptr2: " << *sptr2 << endl;
// 释放一个 shared_ptr
sptr1 = nullptr;
cout << "sptr1释放后,*ptr: " << *ptr << endl; // 此时对象仍存在
// 释放最后一个 shared_ptr
sptr2 = nullptr;
cout << "sptr2释放后,*ptr: " << *ptr << endl; // 危险!对象已释放
return 0;
}
与自定义类结合使用:
#include <memory>
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass(int value) : data(value) {
cout << "MyClass 构造函数" << endl;
}
~MyClass() {
cout << "MyClass 析构函数" << endl;
}
void print() {
cout << "数据: " << data << endl;
}
private:
int data;
};
int main() {
// 使用 make_shared 创建 shared_ptr,传递构造函数参数
auto ptr = make_shared<MyClass>(42);
// 调用对象方法
ptr->print();
// 不需要手动释放内存,shared_ptr 会自动管理
return 0;
}
优势:
- 效率更高:
make_shared会一次性分配对象内存和引用计数所需的内存,而直接使用shared_ptr构造函数需要两次内存分配(一次给对象,一次给引用计数)。 - 异常安全:避免了在构造函数抛出异常时可能导致的内存泄漏。
- 代码更简洁:
- 不需要显式指定模板参数类型(可通过自动类型推导)
- 不需要使用
new操作符 - 减少代码重复
注意事项:
make_shared不支持自定义删除器,如需自定义删除器,仍需使用shared_ptr构造函数- 对象内存可能延迟释放:由于对象和引用计数在同一内存块中,即使对象引用计数归零,只要还有
weak_ptr存在,内存就不会被释放
六、weak_ptr
简介:
weak_ptr 是一种弱引用智能指针,它不拥有对象的所有权,主要用于解决 shared_ptr 的循环引用问题。它必须从一个 shared_ptr 或另一个 weak_ptr 构造。
循环引用问题:
循环引用是使用 shared_ptr 时最常见的陷阱之一,会导致内存泄漏。当两个或多个 shared_ptr 相互引用形成闭环时,它们的引用计数永远无法减为 0,从而导致指向的对象无法被释放。
- 示例:循环引用导致内存泄漏
#include <memory>
#include <iostream>
using namespace std;
class B; // 前向声明
class A {
public:
shared_ptr<B> b_ptr; // A 持有 B 的 shared_ptr
~A() { cout << "A 被销毁" << endl; }
};
class B {
public:
shared_ptr<A> a_ptr; // B 持有 A 的 shared_ptr
~B() { cout << "B 被销毁" << endl; }
};
int main() {
auto a = make_shared<A>();
auto b = make_shared<B>();
a->b_ptr = b; // a 引用 b
b->a_ptr = a; // b 引用 a,形成循环引用
return 0;
// 离开作用域时,a 和 b 的引用计数仍为 1,不会被销毁
// A 和 B 的内存被泄漏,析构函数不会被调用
}
问题分析:
当 a 和 b 离开作用域时,它们的引用计数各减 1(从 2 变为 1)。由于 a->b_ptr 仍引用 b,b->a_ptr 仍引用 a,引用计数无法归零,最终导致内存泄漏。
使用 weak_ptr 解决循环引用:
将其中一个 shared_ptr 改为 weak_ptr 即可打破循环引用:
#include <memory>
#include <iostream>
using namespace std;
class B; // 前向声明
class A {
public:
shared_ptr<B> b_ptr; // A 持有 B 的 shared_ptr
~A() { cout << "A 被销毁" << endl; }
};
class B {
public:
weak_ptr<A> a_ptr; // 将 shared_ptr 改为 weak_ptr
~B() { cout << "B 被销毁" << endl; }
};
int main() {
auto a = make_shared<A>();
auto b = make_shared<B>();
a->b_ptr = b; // a 引用 b
b->a_ptr = a; // b 弱引用 a,不增加引用计数
cout << "程序结束" << endl;
return 0;
// 离开作用域时:
// 1. a 的引用计数从 1 减为 0 → A 被销毁
// 2. A 销毁后,b_ptr 被销毁,b 的引用计数从 2 减为 1
// 3. b 的引用计数从 1 减为 0 → B 被销毁
}
weak_ptr 的常用操作:
lock(): 尝试获取一个指向对象的shared_ptr,如果对象已被释放,则返回空的shared_ptrexpired(): 检查对象是否已被释放use_count(): 返回与weak_ptr共享对象的shared_ptr的数量- 示例:使用 lock() 安全访问对象
#include <memory>
#include <iostream>
using namespace std;
int main() {
shared_ptr<int> sptr = make_shared<int>(42);
weak_ptr<int> wptr = sptr;
// 使用 lock() 获取 shared_ptr
if (auto shared = wptr.lock()) {
cout << "值: " << *shared << endl; // 输出 42
}
sptr.reset(); // 释放对象
if (auto shared = wptr.lock()) {
cout << "值: " << *shared << endl; // 不会执行
} else {
cout << "对象已释放" << endl; // 输出此内容
}
return 0;
}
wptr.lock()是弱引用访问对象的唯一安全方式:- 如果对象还存在(未被释放),返回一个指向该对象的
shared_ptr(此时引用计数会临时加 1) - 如果对象已被释放,返回一个空的
shared_ptr
- 如果对象还存在(未被释放),返回一个指向该对象的
- 这里对象还存在(
sptr还在管理它),所以shared是有效的,会输出42
常见使用场景
- 双向链表:节点的 prev 或 next 指针使用
weak_ptr - 观察者模式:观察者对被观察者的引用使用
weak_ptr,避免影响被观察者的生命周期 - 缓存场景:缓存对象对源对象的引用使用
weak_ptr,避免源对象无法释放
建议:
- 当两个对象需要互相引用时,至少一方应使用
weak_ptr,以防止循环引用导致的内存泄漏 - 使用
weak_ptr访问对象前,总是先使用lock()方法获取shared_ptr,并检查其有效性 - 优先使用
make_shared创建shared_ptr,然后再创建相关的weak_ptr
更多推荐

所有评论(0)