一、智能指针概述
智能指针是C++11引入的RAII(资源获取即初始化)机制,用于自动管理动态内存的生命周期,避免内存泄漏。
核心原理
- RAII机制:在构造函数中获取资源,在析构函数中释放资源
- 重载运算符:重载
*和->运算符,使其行为像普通指针 - 自动释放:智能指针离开作用域时自动调用析构函数释放内存
二、unique_ptr - 独占所有权
2.1 基本概念
- 独占所有权:同一时刻只能有一个
unique_ptr指向某个对象 - 不可复制:删除了拷贝构造函数和拷贝赋值运算符
- 可移动:支持移动语义,所有权可以转移
2.2 核心实现原理
template<typename T>
class unique_ptr {
private:
T* ptr;
public:
// 构造函数
explicit unique_ptr(T* p = nullptr) : ptr(p) {}
// 析构函数 - 自动释放内存
~unique_ptr() {
delete ptr;
}
// 删除拷贝构造函数
unique_ptr(const unique_ptr&) = delete;
// 删除拷贝赋值运算符
unique_ptr& operator=(const unique_ptr&) = delete;
// 移动构造函数
unique_ptr(unique_ptr&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr;
}
// 移动赋值运算符
unique_ptr& operator=(unique_ptr&& other) noexcept {
if (this != &other) {
delete ptr;
ptr = other.ptr;
other.ptr = nullptr;
}
return *this;
}
// 重载解引用运算符
T& operator*() const {
return *ptr;
}
// 重载箭头运算符
T* operator->() const {
return ptr;
}
// 获取原始指针
T* get() const {
return ptr;
}
// 释放所有权
T* release() {
T* temp = ptr;
ptr = nullptr;
return temp;
}
// 重置指针
void reset(T* p = nullptr) {
delete ptr;
ptr = p;
}
};
2.3 使用示例
#include <memory>
#include <iostream>
class MyClass {
public:
MyClass() { std::cout << "构造函数\n"; }
~MyClass() { std::cout << "析构函数\n"; }
void doSomething() { std::cout << "执行操作\n"; }
};
int main() {
// 创建unique_ptr
std::unique_ptr<MyClass> ptr1(new MyClass());
ptr1->doSomething();
// 移动所有权
std::unique_ptr<MyClass> ptr2 = std::move(ptr1);
// ptr1现在为nullptr
if (!ptr1) {
std::cout << "ptr1已释放所有权\n";
}
// 使用make_unique创建(C++14)
auto ptr3 = std::make_unique<MyClass>();
// 自定义删除器
std::unique_ptr<FILE, decltype(&fclose)> file(fopen("test.txt", "w"), fclose);
return 0; // 自动调用析构函数
}
2.4 关键点总结
- ✅ 理解unique_ptr独占所有权:同一时间只能有一个unique_ptr拥有对象
- ✅ 性能最优:没有引用计数开销,与裸指针性能相当
- ✅ 线程安全:单个unique_ptr的读写不是线程安全的,但不同unique_ptr之间是独立的
三、shared_ptr - 共享所有权
3.1 基本概念
- 共享所有权:多个
shared_ptr可以指向同一个对象 - 引用计数:维护一个引用计数器,记录有多少个
shared_ptr指向该对象 - 自动释放:当引用计数降为0时,自动释放对象
3.2 引用计数机制
引用计数工作流程:
┌─────────────┐
│ shared_ptr1 │ ──┐
└─────────────┘ │
├──→ [对象] ←── [控制块: ref_count=3]
┌─────────────┐ │ [weak_count=0]
│ shared_ptr2 │ ──┘
└─────────────┘
┌─────────────┐
│ shared_ptr3 │ ──┘
└─────────────┘
当某个shared_ptr析构时:
1. ref_count--
2. 如果ref_count == 0,释放对象
3.3 核心实现原理
template<typename T>
class shared_ptr {
private:
T* ptr;
int* ref_count; // 引用计数
public:
// 构造函数
explicit shared_ptr(T* p = nullptr)
: ptr(p), ref_count(new int(1)) {}
// 析构函数
~shared_ptr() {
(*ref_count)--;
if (*ref_count == 0) {
delete ptr;
delete ref_count;
}
}
// 拷贝构造函数 - 增加引用计数
shared_ptr(const shared_ptr& other)
: ptr(other.ptr), ref_count(other.ref_count) {
(*ref_count)++;
}
// 拷贝赋值运算符
shared_ptr& operator=(const shared_ptr& other) {
if (this != &other) {
// 减少当前对象的引用计数
(*ref_count)--;
if (*ref_count == 0) {
delete ptr;
delete ref_count;
}
// 复制新对象
ptr = other.ptr;
ref_count = other.ref_count;
(*ref_count)++;
}
return *this;
}
// 移动构造函数
shared_ptr(shared_ptr&& other) noexcept
: ptr(other.ptr), ref_count(other.ref_count) {
other.ptr = nullptr;
other.ref_count = nullptr;
}
// 重载解引用运算符
T& operator*() const {
return *ptr;
}
// 重载箭头运算符
T* operator->() const {
return ptr;
}
// 获取引用计数
int use_count() const {
return ref_count ? *ref_count : 0;
}
// 获取原始指针
T* get() const {
return ptr;
}
};
3.4 使用示例
#include <memory>
#include <iostream>
class MyClass {
public:
MyClass(int id) : id_(id) {
std::cout << "构造函数: " << id_ << "\n";
}
~MyClass() {
std::cout << "析构函数: " << id_ << "\n";
}
void show() {
std::cout << "对象ID: " << id_ << "\n";
}
private:
int id_;
};
int main() {
// 创建shared_ptr
std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(1);
std::cout << "引用计数: " << ptr1.use_count() << "\n"; // 1
// 拷贝构造 - 引用计数增加
std::shared_ptr<MyClass> ptr2 = ptr1;
std::cout << "引用计数: " << ptr1.use_count() << "\n"; // 2
{
std::shared_ptr<MyClass> ptr3 = ptr1;
std::cout << "引用计数: " << ptr1.use_count() << "\n"; // 3
} // ptr3离开作用域,引用计数减为2
std::cout << "引用计数: " << ptr1.use_count() << "\n"; // 2
// reset操作
ptr2.reset();
std::cout << "引用计数: " << ptr1.use_count() << "\n"; // 1
return 0; // ptr1离开作用域,引用计数减为0,对象被释放
}
3.5 关键点总结
- ✅ 理解shared_ptr引用计数:通过控制块维护引用计数,计数为0时释放对象
- ✅ 线程安全:引用计数的增减是原子操作,线程安全
- ✅ 性能开销:比unique_ptr有额外的引用计数开销
四、weak_ptr - 解决循环引用
4.1 基本概念
- 弱引用:不增加引用计数,不影响对象生命周期
- 观察者:用于观察shared_ptr管理的对象
- 解决循环引用:打破shared_ptr之间的循环引用
4.2 循环引用问题
// 循环引用示例 - 内存泄漏!
class Node {
public:
std::shared_ptr<Node> next;
~Node() { std::cout << "析构函数\n"; }
};
int main() {
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2; // node2引用计数: 2
node2->next = node1; // node1引用计数: 2
// 离开作用域时,node1和node2的引用计数都为1,不会释放!
return 0; // 内存泄漏
}
4.3 weak_ptr解决方案
// 使用weak_ptr解决循环引用
class Node {
public:
std::weak_ptr<Node> next; // 使用weak_ptr
~Node() { std::cout << "析构函数\n"; }
};
int main() {
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2; // node2引用计数: 1
node2->next = node1; // node1引用计数: 1
// 离开作用域时,node1和node2的引用计数都为0,正常释放
return 0; // 正常释放
}
4.4 weak_ptr核心方法
#include <memory>
#include <iostream>
int main() {
auto sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp;
// 检查对象是否还存在
if (!wp.expired()) {
std::cout << "对象仍然存在\n";
}
// lock()获取shared_ptr
if (auto locked = wp.lock()) {
std::cout << "值: " << *locked << "\n";
std::cout << "引用计数: " << locked.use_count() << "\n";
}
sp.reset(); // 释放对象
if (wp.expired()) {
std::cout << "对象已被释放\n";
}
return 0;
}
4.5 关键点总结
- ✅ 理解weak_ptr循环引用解决:weak_ptr不增加引用计数,可以打破循环引用
- ✅ lock()方法:将weak_ptr转换为shared_ptr,如果对象已释放则返回空
- ✅ expired()方法:检查对象是否已被释放
五、三种智能指针对比
| 特性 | unique_ptr | shared_ptr | weak_ptr |
|---|---|---|---|
| 所有权 | 独占 | 共享 | 弱引用 |
| 引用计数 | 无 | 有 | 无 |
| 可复制 | 否 | 是 | 是 |
| 可移动 | 是 | 是 | 是 |
| 性能 | 最优 | 有开销 | 有开销 |
| 用途 | 单一所有权 | 多个所有者 | 观察者/解决循环引用 |
六、最佳实践
6.1 优先使用unique_ptr
// 推荐
auto ptr = std::make_unique<MyClass>();
// 不推荐
auto ptr = std::unique_ptr<MyClass>(new MyClass());
6.2 需要共享时使用shared_ptr
// 推荐
auto ptr = std::make_shared<MyClass>();
// 不推荐
auto ptr = std::shared_ptr<MyClass>(new MyClass());
6.3 避免循环引用
// 父子关系:父持有shared_ptr,子持有weak_ptr
class Parent;
class Child {
public:
std::weak_ptr<Parent> parent; // weak_ptr避免循环引用
};
class Parent {
public:
std::vector<std::shared_ptr<Child>> children;
};
6.4 不要混用裸指针和智能指针
// 危险!
int* raw = new int(42);
std::shared_ptr<int> sp(raw);
delete raw; // 重复释放!
// 正确做法
auto sp = std::make_shared<int>(42);
七、常见面试题
Q1: shared_ptr的线程安全性如何?
A:
- 引用计数的增减是原子操作,线程安全
- 指向的对象本身不是线程安全的,需要额外同步
- 多个线程读写同一个shared_ptr对象需要加锁
Q2: make_shared比直接构造有什么优势?
A:
- 性能更好:一次性分配内存(对象+控制块)
- 异常安全:不会出现内存泄漏
- 缓存友好:对象和控制块在连续内存中
Q3: weak_ptr如何知道对象是否被释放?
A:
- weak_ptr通过控制块中的weak_count和expired标志
- 当shared_ptr引用计数为0时,对象被释放,但控制块保留
- 当weak_ptr引用计数也为0时,控制块才被释放