目录
-
- [1. 为什么必须抛弃裸指针?](#1. 为什么必须抛弃裸指针?)
- [2. 智能指针发展](#2. 智能指针发展)
-
- [2.1 auto_ptr (C++98)](#2.1 auto_ptr (C++98))
- [2.2 unique_ptr (C++11)](#2.2 unique_ptr (C++11))
- [2.3 shared_ptr (C++11)](#2.3 shared_ptr (C++11))
- [3. 深度解析 shared_ptr](#3. 深度解析 shared_ptr)
-
- [3.1 内存优化:make_shared 与 直接 new 的区别](#3.1 内存优化:make_shared 与 直接 new 的区别)
- [3.2 定制删除器(Deleter)的必要性](#3.2 定制删除器(Deleter)的必要性)
- [4. 线程安全陷阱](#4. 线程安全陷阱)
- [5. 循环引用灾难与 weak_ptr](#5. 循环引用灾难与 weak_ptr)
-
- [5.1 循环引用是如何发生的?](#5.1 循环引用是如何发生的?)
- [5.2 weak_ptr 解决循环引用](#5.2 weak_ptr 解决循环引用)
- [6. 总结](#6. 总结)
在现代 C++ 开发中,内存管理是核心痛点。结合 CSDN 优质技术博客的深度解析与底层代码拆解,本文将全面剖析 C++ 智能指针的演进、底层原理、内存布局以及多线程安全陷阱。
1. 为什么必须抛弃裸指针?
传统 C/C++ 内存泄漏通常分为堆内存泄漏 (忘记释放 new/malloc 内存)和系统资源泄漏 (未关闭句柄、套接字等)。除了疏忽导致外,最大的隐患是异常安全问题。
cpp
void Func() {
int* p1 = new int;
// 若后续逻辑(或函数调用)抛出异常,直接跳转至 catch 块,delete p1 将永远无法执行
cout << div() << endl;
delete p1;
}
工程实践中,事前预防远胜于事后检测。C++ 采用 RAII(Resource Acquisition Is Initialization,资源获取即初始化)思想彻底解决这一问题。其核心机制是:在对象构造时获取并接管资源,在生命周期内保持有效,在对象析构时由编译器自动调用析构函数释放资源。
2. 智能指针发展
C++ 智能指针经历了从 C++98 到 C++11 的重大演进,标准库大量吸收了 Boost 库的核心实现原理。
2.1 auto_ptr (C++98)

auto_ptr 采用"管理权转移"机制:拷贝或赋值时,原对象的指针会被强行置空,资源完全交由新对象管理。 原对象内部指针被强行置空。
cpp
#include <memory>
using namespace std;
int main() {
auto_ptr<Date> ap1(new Date);
// 拷贝时,管理权限转移,ap1 被悬空
auto_ptr<Date> ap2(ap1);
// ap1->_year++; // 致命错误:此时 ap1 为空指针,解引用将导致程序崩溃
return 0;
}
- 致命缺陷:拷贝行为隐式剥夺了原对象的所有权,若后续代码无意中访问原对象,会导致对空指针的解引用,直接引发 Core Dump(崩溃)。目前已被诸多公司明令禁止,并在 C++11 后被正式废弃。
2.2 unique_ptr (C++11)
针对 auto_ptr 的痛点,unique_ptr 采用了简单粗暴的防拷贝机制:直接 delete 拷贝构造函数和赋值运算符,强制实现独占所有权语义。
cpp
#include <memory>
using namespace std;
int main() {
unique_ptr<Date> up1(new Date);
// unique_ptr<Date> up2(up1); // 编译报错:不支持拷贝
// 若业务确实需要转移管理权,必须显式使用 std::move
unique_ptr<Date> up3 = std::move(up1);
// 此时 up1 为空,资源完全由 up3 独占接管
return 0;
}
- 特性:由于没有额外的维护开销,它与裸指针的性能几乎一致(零开销抽象)。
- 转移所有权 :必须显式调用
std::move()才能将管理权转移给另一个unique_ptr,语义极其清晰。
2.3 shared_ptr (C++11)
shared_ptr 允许多个智能指针管理同一块内存资源。它在内部维护了一个引用计数 ,新对象拷贝时计数 +1,对象销毁析构时计数 -1。当计数归零时,说明自己是最后一个使用者,触发 delete 释放底层资源。 并允许传入定制删除器以管理非 new 申请的资源
cpp
#include <memory>
#include <iostream>
using namespace std;
// 自定义文件句柄删除器
class Fclose {
public:
void operator()(FILE* ptr) {
cout << "fclose:" << ptr << endl;
fclose(ptr);
}
};
int main() {
// 1. 基础共享与引用计数测试
shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1);
shared_ptr<Date> sp3(sp2);
cout << sp1.use_count() << endl; // 输出 3
// 2. 高效初始化:make_shared 一次性分配内存,性能更优且避免内存碎片
shared_ptr<Date> sp6 = make_shared<Date>(2024, 8, 5);
// 3. 定制删除器:管理数组与文件
shared_ptr<Date[]> sp4(new Date[5]); // 内部正确调用 delete[] 释放数组
shared_ptr<FILE> sp5(fopen("test.cpp", "r"), Fclose()); // 析构时自动调用 fclose
return 0;
}
3. 深度解析 shared_ptr
标准的 shared_ptr 并非只有一个指针那么简单,其内部通常包含两个指针:
- 指向所管理对象内存的数据指针。
- 指向控制块(Control Block)的指针。控制块中存储了**强引用计数(use_count)、弱引用计数(weak_count)以及自定义删除器(Deleter)**等。
结合提供的底层模拟代码,分析核心机制:
cpp
namespace bit {
template<class T>
class shared_ptr {
public:
// 构造:分配控制块(此处用 _pcount 模拟)
shared_ptr(T* ptr = nullptr) : _ptr(ptr), _pcount(new int(1)) {}
// 支持定制删除器
template<class D>
shared_ptr(T* ptr, D del) : _ptr(ptr), _pcount(new int(1)), _del(del) {}
// 拷贝:引用计数累加
shared_ptr(const shared_ptr<T>& sp) : _ptr(sp._ptr), _pcount(sp._pcount) {
++(*_pcount);
}
// 赋值:处理自我赋值,解除旧引用,绑定新引用
shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
if (_ptr != sp._ptr) {
release(); // 递减旧计数,归零则释放
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
~shared_ptr() { release(); }
T* get() const { return _ptr; }
int use_count() const { return *_pcount; }
private:
void release() {
if (--(*_pcount) == 0) {
_del(_ptr); // 调用删除器
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
T* _ptr;
int* _pcount; // 控制块(引用计数)
std::function<void(T*)> _del = [](T* ptr) { delete ptr; }; // 默认删除器
};
}
3.1 内存优化:make_shared 与 直接 new 的区别
在标准的 std::shared_ptr<int> p(new int(10)) 初始化中,存在两次内存分配:一次为 new int(10) 分配数据内存,一次为控制块分配内存。
而使用 std::make_shared:
- 优势:只需进行一次内存分配,将数据对象与控制块放置在一块连续内存中。这不仅提升了性能(避免碎片化、提高缓存命中率),还避免了在两次分配之间若抛出异常导致的数据内存泄漏风险。
3.2 定制删除器(Deleter)的必要性
智能指针默认使用 delete 释放内存。但若资源是 new [] 出来的数组、malloc 的堆空间,或者是 fopen 产生的文件句柄,必须使用自定义删除器防止程序崩溃或系统资源泄漏。
cpp
// 仿函数删除文件句柄
class Fclose {
public:
void operator()(FILE* ptr) {
cout << "fclose:" << ptr << endl;
fclose(ptr);
}
};
// 业务应用
bit::shared_ptr<FILE> sp5(fopen("test.cpp", "r"), Fclose());
// 使用 Lambda 表达式释放 malloc 内存
bit::shared_ptr<int> sp6((int*)malloc(40), [](int* ptr) { free(ptr); });
4. 线程安全陷阱
shared_ptr 的线程安全性需要分为两个维度严格界定:
- 控制块(引用计数)是线程安全的 :标准库通过原子操作(或加锁机制)保护引用计数的
++和--。因此,多线程同时对同一个shared_ptr对象进行拷贝析构是安全的。 - 管理的底层对象资源不是线程安全的 :智能指针不管堆上数据的线程安全问题。如果多线程中同时通过
shared_ptr修改其指向的数据资源(如copy->_year++),必须由开发者在业务层使用mutex加锁同步,否则会导致数据错乱。
5. 循环引用灾难与 weak_ptr
5.1 循环引用是如何发生的?
在构建双向链表或图结构时,若节点内使用 shared_ptr 互指,将引发灾难性后果。
cpp
struct ListNode {
bit::shared_ptr<ListNode> _next;
bit::shared_ptr<ListNode> _prev;
~ListNode() { cout << "~ListNode()" << endl; }
};
// 互相链接后,n1 和 n2 的内部引用计数皆为 2
n1->_next = n2;
n2->_prev = n1;
离开作用域时,n1 和 n2 析构,引用计数减为 1。但此时 n1 被 n2._prev 持有,n2 被 n1._next 持有。因为外部对象无法释放,内部成员也不会被析构,双方互相钳制,造成无法挽回的内存泄漏。
5.2 weak_ptr 解决循环引用
weak_ptr 是专门为解决 shared_ptr 循环引用而生的观察者指针。 weak_ptr 不支持 RAII 管理资源,主要用作 shared_ptr 的辅助工具。它不增加引用计数,可完美打破双向链表中的循环引用死锁。
cpp
#include <memory>
#include <iostream>
using namespace std;
// 场景一:打破循环引用
struct ListNode {
int _data;
// 使用 weak_ptr 替代 shared_ptr 作为成员,避免互相钳制
weak_ptr<ListNode> _next;
weak_ptr<ListNode> _prev;
~ListNode() { cout << "~ListNode()" << endl; }
};
// 场景二:资源状态探测与安全提升
int main() {
std::weak_ptr<ListNode> wp;
std::shared_ptr<ListNode> sp;
{
std::shared_ptr<ListNode> n1(new ListNode);
wp = n1; // weak_ptr 观测 shared_ptr,但不增加强引用计数
cout << wp.expired() << endl; // 输出 0 (false),表示资源存活
n1->_data++;
// 安全访问数据:使用 lock() 尝试将 weak_ptr 提升为 shared_ptr
sp = wp.lock();
} // 离开作用域,n1 和 sp 析构,资源被彻底释放
cout << wp.expired() << endl; // 输出 1 (true),表示底层资源已被释放
return 0;
}
- 原理 :
weak_ptr指向shared_ptr管理的资源,但绝不会增加控制块中的强引用计数(use_count)。 - 底层实现逻辑:
cpp
namespace bit {
template<class T>
class weak_ptr {
public:
weak_ptr() = default;
weak_ptr(const shared_ptr<T>& sp) : _ptr(sp.get()) {}
weak_ptr<T>& operator=(const shared_ptr<T>& sp) {
_ptr = sp.get();
return *this;
}
private:
T* _ptr = nullptr; // 不干预强引用计数的递增
};
}
- 状态探测与资源访问 :由于
weak_ptr不保证资源存活,不能直接重载operator*或operator->(不支持 RAII)。使用标准库时,必须通过wp.expired()判断资源是否已被释放,并通过wp.lock()原子的将weak_ptr提升(Promote)为一个有效的shared_ptr,才能安全访问数据。
6. 总结
- 优先使用
std::unique_ptr,只在需要绝对共享所有权时才使用std::shared_ptr。 - 尽可能使用
std::make_unique和std::make_shared进行初始化,以提升性能并保障异常安全性。 - 警惕双向链表、树的父子节点互指以及观察者模式中的循环引用,强制采用
weak_ptr作为反向指针打破闭环。