文章目录
-
- C++智能指针详解(二):原理剖析与高级话题
- 一、智能指针的实现原理
-
- [1.1 auto_ptr的实现](#1.1 auto_ptr的实现)
- [1.2 unique_ptr的实现](#1.2 unique_ptr的实现)
- [1.3 shared_ptr的实现](#1.3 shared_ptr的实现)
- [1.4 支持自定义删除器](#1.4 支持自定义删除器)
- 二、shared_ptr的循环引用问题
-
- [2.1 什么是循环引用](#2.1 什么是循环引用)
- [2.2 循环引用的原理分析](#2.2 循环引用的原理分析)
- [2.3 weak_ptr:打破循环引用](#2.3 weak_ptr:打破循环引用)
- [2.4 weak_ptr的使用](#2.4 weak_ptr的使用)
- [2.5 weak_ptr的简单实现](#2.5 weak_ptr的简单实现)
- 三、智能指针的线程安全
-
- [3.1 两个层面的线程安全](#3.1 两个层面的线程安全)
- [3.2 引用计数的线程安全](#3.2 引用计数的线程安全)
- [3.3 指向对象的线程安全](#3.3 指向对象的线程安全)
- 四、C++11与Boost的关系
-
- [4.1 Boost库简介](#4.1 Boost库简介)
- [4.2 智能指针的演进历史](#4.2 智能指针的演进历史)
- 五、内存泄漏的检测与预防
-
- [5.1 什么是内存泄漏](#5.1 什么是内存泄漏)
- [5.2 内存泄漏的检测与预防](#5.2 内存泄漏的检测与预防)
- 六、智能指针使用的注意事项
-
- [6.1 不要混用智能指针和裸指针](#6.1 不要混用智能指针和裸指针)
- [6.2 不要用get()返回的指针构造新智能指针](#6.2 不要用get()返回的指针构造新智能指针)
- [6.3 小心移动后的对象](#6.3 小心移动后的对象)
- [6.4 避免循环引用](#6.4 避免循环引用)
- [6.5 智能指针不是万能的](#6.5 智能指针不是万能的)
- 七、总结与展望
-
- [7.1 全系列回顾](#7.1 全系列回顾)
- [7.2 核心要点总结](#7.2 核心要点总结)
- [7.3 实践建议](#7.3 实践建议)
- [7.4 继续学习](#7.4 继续学习)
- 八、常见问题解答
C++智能指针详解(二):原理剖析与高级话题
💬 欢迎讨论:本文是C++智能指针系列的第二篇,将深入剖析智能指针的实现原理,并探讨循环引用、线程安全等高级话题。如果你在学习过程中有任何疑问,欢迎在评论区留言交流!
👍 点赞、收藏与分享:这是系列的完结篇,建议结合第一篇一起学习。如果觉得有帮助,请分享给更多的朋友!
🚀 系列回顾:在第一篇中,我们学习了智能指针的使用场景、RAII设计思想以及标准库智能指针的使用方法。本篇将继续深入学习。
一、智能指针的实现原理
1.1 auto_ptr的实现
虽然auto_ptr已被废弃,但了解它的实现有助于理解其设计缺陷。
核心思想:拷贝转移所有权
cpp
namespace bit
{
template<class T>
class auto_ptr
{
public:
explicit auto_ptr(T* ptr = nullptr)
: _ptr(ptr)
{}
// 拷贝构造:转移所有权
auto_ptr(auto_ptr<T>& ap)
: _ptr(ap._ptr)
{
// 被拷贝对象失去所有权
ap._ptr = nullptr;
}
// 拷贝赋值:转移所有权
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
if (this != &ap)
{
// 释放当前资源
if (_ptr)
delete _ptr;
// 转移ap的资源
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete: " << _ptr << endl;
delete _ptr;
}
}
// 模拟指针行为
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
}
为什么这种设计不好?
cpp
int main()
{
bit::auto_ptr<int> ap1(new int(10));
bit::auto_ptr<int> ap2(ap1); // ap1被悬空
// cout << *ap1 << endl; // 崩溃!ap1已经是nullptr
cout << *ap2 << endl; // OK
return 0;
}
拷贝后原对象悬空,这违反了拷贝语义的直觉,容易导致错误。
1.2 unique_ptr的实现
unique_ptr通过禁用拷贝来解决auto_ptr的问题。
核心思想:独占所有权,只支持移动
cpp
namespace bit
{
template<class T>
class unique_ptr
{
public:
explicit unique_ptr(T* ptr = nullptr)
: _ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "delete: " << _ptr << endl;
delete _ptr;
}
}
// 禁用拷贝
unique_ptr(const unique_ptr<T>&) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;
// 支持移动
unique_ptr(unique_ptr<T>&& up)
: _ptr(up._ptr)
{
up._ptr = nullptr;
}
unique_ptr<T>& operator=(unique_ptr<T>&& up)
{
if (this != &up)
{
if (_ptr)
delete _ptr;
_ptr = up._ptr;
up._ptr = nullptr;
}
return *this;
}
// 模拟指针行为
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
T* get() const { return _ptr; }
private:
T* _ptr;
};
}
使用示例
cpp
int main()
{
bit::unique_ptr<int> up1(new int(10));
// 编译错误:拷贝被禁用
// bit::unique_ptr<int> up2(up1);
// OK:支持移动
bit::unique_ptr<int> up3(std::move(up1));
// 移动后up1为空,必须显式使用std::move
// 这样就不容易出错
return 0;
}
1.3 shared_ptr的实现
shared_ptr是最复杂的智能指针,核心是引用计数机制。
引用计数的设计难点
cpp
// 错误的设计
template<class T>
class shared_ptr
{
private:
T* _ptr;
static int _count; // 静态成员?所有对象共享一个计数!
};
这样不行!不同的资源需要不同的引用计数。
正确的设计:引用计数也在堆上
cpp
#include <iostream>
using namespace std;
namespace bit
{
template<class T>
class shared_ptr
{
public:
// 构造函数
explicit shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pcount(nullptr)
{
if (_ptr)
{
_pcount = new int(1); // 只有非空资源才分配计数
}
cout << "shared_ptr ctor: " << _ptr << endl;
}
// 拷贝构造:共享资源,增加引用计数
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pcount(sp._pcount)
{
if (_pcount)
{
++(*_pcount);
}
}
// 拷贝赋值
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (this != &sp)
{
// 先释放当前资源
release();
// 再共享新资源
_ptr = sp._ptr;
_pcount = sp._pcount;
if (_pcount)
{
++(*_pcount);
}
}
return *this;
}
// 析构函数
~shared_ptr()
{
release();
}
// 释放资源
void release()
{
if (_pcount)
{
--(*_pcount);
if (*_pcount == 0)
{
cout << "delete: " << _ptr << endl;
delete _ptr;
delete _pcount;
}
}
_ptr = nullptr;
_pcount = nullptr;
}
// 模拟指针行为
T& operator*() const
{
return *_ptr;
}
T* operator->() const
{
return _ptr;
}
// 获取裸指针
T* get() const
{
return _ptr;
}
// 获取引用计数
int use_count() const
{
return _pcount ? *_pcount : 0;
}
// 判空
explicit operator bool() const
{
return _ptr != nullptr;
}
private:
T* _ptr; // 被管理的资源
int* _pcount; // 堆上的引用计数
};
}
关键点解析
- 引用计数在堆上:每个资源对应一个独立的计数
- 拷贝时增加计数:多个shared_ptr共享同一个计数
- 析构时减少计数:计数归零时释放资源
- 赋值运算符:先释放旧资源,再共享新资源
⚠️说明:这是一个教学版 shared_ptr,省略了控制块、weak_ptr 支持、线程安全等复杂细节,但完整体现了 "引用计数在堆上 +RAII 自动释放" 这一核心思想。标准库的实现会更复杂,但基本原理一致。
测试代码
cpp
int main()
{
bit::shared_ptr<int> sp1(new int(10));
cout << "引用计数: " << sp1.use_count() << endl; // 1
{
bit::shared_ptr<int> sp2(sp1);
cout << "引用计数: " << sp1.use_count() << endl; // 2
cout << "引用计数: " << sp2.use_count() << endl; // 2
bit::shared_ptr<int> sp3 = sp2;
cout << "引用计数: " << sp1.use_count() << endl; // 3
}
cout << "引用计数: " << sp1.use_count() << endl; // 1
return 0;
}
1.4 支持自定义删除器
实际的shared_ptr还支持自定义删除器。
实现思路:使用function包装可调用对象
cpp
#include <functional>
namespace bit
{
template<class T>
class shared_ptr
{
public:
explicit shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pcount(ptr ? new int(1) : nullptr)
, _del([](T* p) { delete p; })
{}
// ✅ 支持自定义删除器
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _pcount(ptr ? new int(1) : nullptr)
, _del(del)
{}
// 拷贝构造:共享资源,增加引用计数
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pcount(sp._pcount)
, _del(sp._del)
{
if (_pcount)
{
++(*_pcount);
}
}
// 拷贝赋值
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (this != &sp)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
_del = sp._del;
if (_pcount)
{
++(*_pcount);
}
}
return *this;
}
~shared_ptr()
{
release();
}
void release()
{
if (_pcount)
{
--(*_pcount);
if (*_pcount == 0)
{
_del(_ptr);
delete _pcount;
}
}
_ptr = nullptr;
_pcount = nullptr;
}
T& operator*() const { return *_ptr; }
T* operator->() const { return _ptr; }
T* get() const { return _ptr; }
int use_count() const
{
return _pcount ? *_pcount : 0;
}
explicit operator bool() const
{
return _ptr != nullptr;
}
private:
T* _ptr = nullptr;
int* _pcount = nullptr;
std::function<void(T*)> _del;
};
}
使用自定义删除器
cpp
int main()
{
// 管理数组
bit::shared_ptr<int> sp1(new int[10], [](int* p) {
cout << "delete[] " << p << endl;
delete[] p;
});
// 管理FILE*
bit::shared_ptr<FILE> sp2(fopen("test.txt", "r"), [](FILE* fp) {
cout << "fclose " << fp << endl;
fclose(fp);
});
return 0;
}
⚠️说明:本文为了演示"shared_ptr 能携带自定义删除策略",把删除器作为成员存到了 shared_ptr 本体中。 但标准库
std::shared_ptr 的删除器存放在"控制块(control block)"里,多个 shared_ptr共享同一个控制块,因此删除器也只存一份;拷贝 shared_ptr 只增加引用计数,不会复制删除器对象
二、shared_ptr的循环引用问题
2.1 什么是循环引用
循环引用是shared_ptr最容易遇到的陷阱,会导致内存泄漏。
经典场景:双向链表
cpp
struct ListNode
{
int _data;
shared_ptr<ListNode> _next;
shared_ptr<ListNode> _prev;
ListNode(int data = 0)
: _data(data)
{}
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> n1(new ListNode(1));
shared_ptr<ListNode> n2(new ListNode(2));
cout << "n1引用计数: " << n1.use_count() << endl; // 1
cout << "n2引用计数: " << n2.use_count() << endl; // 1
// 建立双向连接
n1->_next = n2;
n2->_prev = n1;
cout << "n1引用计数: " << n1.use_count() << endl; // 2
cout << "n2引用计数: " << n2.use_count() << endl; // 2
return 0;
}
// 程序结束,但~ListNode()永远不会被调用!
输出
bash
n1引用计数: 1
n2引用计数: 1
n1引用计数: 2
n2引用计数: 2
注意:析构函数没有被调用!这就是内存泄漏。
2.2 循环引用的原理分析
让我们分析为什么会发生内存泄漏:
初始状态
bash
n1 (引用计数1) → [节点1]
n2 (引用计数1) → [节点2]
建立连接后
bash
n1 (引用计数1) → [节点1] ←─┐
↓ _next │ _prev
[节点2] ─────┘
↑
n2 (引用计数1) ───┘
节点1的引用计数:1(n1) + 1(_prev) = 2
节点2的引用计数:1(n2) + 1(_next) = 2
main函数结束时
- n1析构,节点1的引用计数:2 - 1 = 1
- n2析构,节点2的引用计数:2 - 1 = 1
- 节点1要释放,需要等_next析构
- _next是节点2的成员,节点2要释放才会析构_next
- 节点2要释放,需要等_prev析构
- _prev是节点1的成员,节点1要释放才会析构_prev
- 形成循环依赖,谁都不会释放!
逻辑上的死锁
bash
节点1释放 ← 依赖 ← _next析构 ← 依赖 ← 节点2释放
↑ ↓
└──── 依赖 ←── _prev析构 ←── 依赖 ──────┘
2.3 weak_ptr:打破循环引用
weak_ptr是专门设计来解决这个问题的。
weak_ptr的特点
- 不增加引用计数:绑定到shared_ptr时不影响计数
- 不管理资源:不参与资源的释放
- 可以检测资源是否有效:通过expired()检查
解决方案
cpp
struct ListNode
{
int _data;
shared_ptr<ListNode> _next;
weak_ptr<ListNode> _prev; // 改用weak_ptr
ListNode(int data = 0) : _data(data) {}
~ListNode()
{
cout << "~ListNode(" << _data << ")" << endl;
}
};
int main()
{
shared_ptr<ListNode> n1(new ListNode(1));
shared_ptr<ListNode> n2(new ListNode(2));
cout << "n1引用计数: " << n1.use_count() << endl; // 1
cout << "n2引用计数: " << n2.use_count() << endl; // 1
n1->_next = n2;
n2->_prev = n1; // weak_ptr绑定,不增加引用计数
cout << "n1引用计数: " << n1.use_count() << endl; // 1
cout << "n2引用计数: " << n2.use_count() << endl; // 2
return 0;
}
输出
bash
n1引用计数: 1
n2引用计数: 1
n1引用计数: 1
n2引用计数: 2
~ListNode(1)
~ListNode(2)
完美!析构函数被正确调用了。
原理分析
bash
n1 (引用计数1) → [节点1]
↓ _next
[节点2] ─weak_ptr─> [节点1]
↑
n2 (引用计数1) ───┘
节点1的引用计数:1(n1)(_prev不计数)
节点2的引用计数:1(n2) + 1(_next) = 2
当main结束:
- n1析构,节点1引用计数归零,节点1释放
- 节点1释放时,_next析构,节点2引用计数减1
- n2析构,节点2引用计数归零,节点2释放
- 成功打破循环!
2.4 weak_ptr的使用
基本操作
cpp
int main()
{
shared_ptr<int> sp1(new int(10));
// weak_ptr绑定shared_ptr
weak_ptr<int> wp1 = sp1;
weak_ptr<int> wp2(sp1);
// 检查是否过期
cout << wp1.expired() << endl; // 0 (false)
// 获取引用计数
cout << wp1.use_count() << endl; // 1
// wp不增加引用计数
cout << sp1.use_count() << endl; // 1
return 0;
}
安全访问资源:lock()
weak_ptr不能直接访问资源(没有*和->运算符),需要通过lock()获取shared_ptr:
cpp
int main()
{
shared_ptr<string> sp1(new string("hello"));
weak_ptr<string> wp = sp1;
// 方式1:使用lock()获取shared_ptr
if (auto sp2 = wp.lock())
{
cout << *sp2 << endl; // OK
*sp2 += " world";
cout << *sp1 << endl; // hello world
}
else
{
cout << "资源已释放" << endl;
}
return 0;
}
检测资源是否有效
cpp
int main()
{
weak_ptr<string> wp;
{
shared_ptr<string> sp(new string("test"));
wp = sp;
cout << "内层作用域" << endl;
cout << "expired: " << wp.expired() << endl; // 0
cout << "use_count: " << wp.use_count() << endl; // 1
}
cout << "外层作用域" << endl;
cout << "expired: " << wp.expired() << endl; // 1 (过期)
cout << "use_count: " << wp.use_count() << endl; // 0
auto sp2 = wp.lock();
if (!sp2)
{
cout << "资源已释放,lock返回空shared_ptr" << endl;
}
return 0;
}
2.5 weak_ptr的简单实现
cpp
namespace bit
{
template<class T>
class weak_ptr
{
public:
weak_ptr() : _ptr(nullptr) {}
// 绑定shared_ptr
weak_ptr(const shared_ptr<T>& sp)
: _ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
// weak_ptr之间可以拷贝
weak_ptr(const weak_ptr<T>& wp)
: _ptr(wp._ptr)
{}
weak_ptr<T>& operator=(const weak_ptr<T>& wp)
{
_ptr = wp._ptr;
return *this;
}
T* get() const { return _ptr; }
private:
T* _ptr;
};
}
⚠️提示:真正的
weak_ptr必须与shared_ptr共享同一个"控制块(control block)",通过控制块中的
weak_count/shared_count才能实现expired()/lock()的正确语义。只保存T*无法判断对象是否已析构,因此这里只是"演示接口形状",不是可用实现。
三、智能指针的线程安全
3.1 两个层面的线程安全
使用智能指针时,需要考虑两个层面的线程安全:
- 引用计数的线程安全:多线程同时拷贝/析构智能指针
- 指向对象的线程安全:多线程同时访问智能指针指向的对象
它们是两个独立的问题!
3.2 引用计数的线程安全
问题演示
cpp
#include <thread>
#include <mutex>
#include <atomic>
struct AA
{
int _a1 = 0;
int _a2 = 0;
~AA()
{
cout << "~AA()" << endl;
}
};
int main()
{
bit::shared_ptr<AA> p(new AA);
const size_t n = 100000;
auto func = [&]()
{
for (size_t i = 0; i < n; ++i)
{
// 拷贝构造会++引用计数
bit::shared_ptr<AA> copy(p);
// copy析构会--引用计数
}
};
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << "引用计数: " << p.use_count() << endl;
return 0;
}
可能的问题
- 程序崩溃:引用计数操作不是原子的
- 内存泄漏:AA对象没有被释放
解决方案:使用原子操作
cpp
#include <atomic>
namespace bit
{
template<class T>
class shared_ptr
{
public:
explicit shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pcount(ptr ? new std::atomic<int>(1) : nullptr)
{}
// 拷贝构造:共享资源,原子递增计数
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pcount(sp._pcount)
{
if (_pcount)
{
_pcount->fetch_add(1, std::memory_order_relaxed);
}
}
// 拷贝赋值
shared_ptr& operator=(const shared_ptr& sp)
{
if (this != &sp)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
if (_pcount)
{
_pcount->fetch_add(1, std::memory_order_relaxed);
}
}
return *this;
}
~shared_ptr()
{
release();
}
void release()
{
if (_pcount)
{
if (_pcount->fetch_sub(1, std::memory_order_acq_rel) == 1)
{
delete _ptr;
delete _pcount;
}
}
_ptr = nullptr;
_pcount = nullptr;
}
T& operator*() const { return *_ptr; }
T* operator->() const { return _ptr; }
T* get() const { return _ptr; }
int use_count() const
{
return _pcount ? _pcount->load(std::memory_order_relaxed) : 0;
}
explicit operator bool() const { return _ptr != nullptr; }
private:
T* _ptr = nullptr;
std::atomic<int>* _pcount = nullptr;
};
}
标准库的实现
标准库的shared_ptr保证了引用计数操作的线程安全性,使用了原子操作或其他同步机制。
补充:标准库只保证对同一个控制块的引用计数增减是线程安全的;但如果多个线程同时读写同一个 shared_ptr
对象本身(例如同时对同一个 shared_ptr 赋值/reset),仍需要外部同步。
3.3 指向对象的线程安全
shared_ptr不保证指向对象的线程安全!
cpp
#include <thread>
#include <mutex>
#include <atomic>
int main()
{
shared_ptr<AA> p(new AA);
const size_t n = 100000;
auto func = [&]()
{
for (size_t i = 0; i < n; ++i)
{
shared_ptr<AA> copy(p);
// 多线程同时访问AA对象
copy->_a1++; // 数据竞争!
copy->_a2++; // 数据竞争!
}
};
thread t1(func);
thread t2(func);
t1.join();
t2.join();
// 预期:200000,实际:基本小于200000
cout << p->_a1 << endl;
cout << p->_a2 << endl;
return 0;
}
解决方案:使用互斥锁
cpp
int main()
{
shared_ptr<AA> p(new AA);
const size_t n = 100000;
mutex mtx;
auto func = [&]()
{
for (size_t i = 0; i < n; ++i)
{
shared_ptr<AA> copy(p);
{
unique_lock<mutex> lock(mtx);
copy->_a1++;
copy->_a2++;
}
}
};
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << p->_a1 << endl; // 200000
cout << p->_a2 << endl; // 200000
return 0;
}
总结
| 方面 | 是否安全 | 负责人 |
|---|---|---|
| 引用计数操作 | ✓ | shared_ptr保证 |
| 指向的对象 | ✗ | 使用者负责 |
四、C++11与Boost的关系
4.1 Boost库简介
Boost库是什么?
- C++标准库的扩展和补充
- 高质量、可移植、开源的C++库集合
- 很多C++11/14/17的特性都来自Boost
Boost与C++标准的关系
- Boost社区为C++标准化提供参考实现
- 很多Boost库被纳入C++标准
- C++标准委员会成员参与Boost开发
4.2 智能指针的演进历史
C++98时代
- 标准库:
auto_ptr(唯一的智能指针) - Boost库:
scoped_ptr、shared_ptr、weak_ptr等
C++ TR1时代
- 技术报告引入了Boost的
shared_ptr - TR1不是正式标准,但被广泛支持
C++11时代
unique_ptr(对应Boost的scoped_ptr)shared_ptr(参考Boost实现)weak_ptr(参考Boost实现)- 废弃
auto_ptr
对应关系
| Boost | C++11 | 说明 |
|---|---|---|
| scoped_ptr | unique_ptr | 独占所有权 |
| scoped_array | unique_ptr<T[]> | 数组版本 |
| shared_ptr | shared_ptr | 共享所有权 |
| shared_array | shared_ptr<T[]> | 数组版本 |
| weak_ptr | weak_ptr | 弱引用 |
五、内存泄漏的检测与预防
5.1 什么是内存泄漏
定义
程序在运行过程中动态分配了内存,但在不再使用时未能正确释放 ,
导致该内存无法再次被程序访问或使用 ,从而造成内存浪费,这种现象称为内存泄漏。
内存泄漏 vs 内存溢出
-
内存泄漏(Memory Leak)
已分配的内存没有被释放,内存占用持续增加,但单次分配可能是成功的。
-
内存溢出(Out Of Memory)
程序在申请内存时,所需内存超过系统可提供的内存,导致分配失败甚至程序崩溃。
👉 二者关系:长期内存泄漏很容易最终导致内存溢出。
常见原因
new / malloc后忘记delete / free- 异常或提前
return,导致释放代码未执行 - 智能指针 循环引用 (如
shared_ptr相互引用) - 指针重新赋值前未释放原内存
- 容器中存放裸指针,清空容器时未释放指针指向的内存
典型内存泄漏示例
cpp
void MemoryLeak1()
{
int* p = new int(10);
// 函数结束前忘记 delete
}
再看一个更隐蔽的例子:
cpp
void MemoryLeak2()
{
int* p = new int(10);
p = new int(20); // 原来指向的内存丢失,发生泄漏
}
⚠️ 指针重新赋值前,如果不释放旧内存,就会造成泄漏。
5.2 内存泄漏的检测与预防
1️⃣ 事前预防(最重要)
- 尽量避免使用裸指针,优先使用 RAII 思想
- 使用
std::unique_ptr、std::shared_ptr - 明确资源的所有权和生命周期
- 遵循"谁申请,谁释放"的原则
2️⃣ 事中检测
- Code Review:重点检查资源的申请与释放是否成对
- 使用静态分析工具(如 clang-tidy、cppcheck)
- 编写单元测试,覆盖异常路径和边界条件
3️⃣ 事后查错
-
使用内存检测工具定位泄漏位置
- Linux:
valgrind - Windows:Visual Studio 内存诊断工具
- Linux:
-
观察程序运行过程中内存占用是否持续增长
-
修复后进行回归测试,防止问题反复出现
完整示例:使用智能指针避免内存泄漏
cpp
// 错误的做法
void BadCode()
{
int* p1 = new int(10);
int* p2 = new int(20);
if (some_condition)
{
throw "error"; // p1 和 p2 未释放,发生内存泄漏
}
delete p1;
delete p2;
}
cpp
// 正确的做法
void GoodCode()
{
auto p1 = std::make_unique<int>(10);
auto p2 = std::make_unique<int>(20);
if (some_condition)
{
throw "error"; // 栈展开时自动释放,无内存泄漏
}
// 不需要手动 delete
}
✅ 总结一句话:
让对象管理资源,而不是让人去记得释放资源。
六、智能指针使用的注意事项
6.1 不要混用智能指针和裸指针
错误示例
cpp
int main()
{
int* p = new int(10);
shared_ptr<int> sp1(p);
shared_ptr<int> sp2(p); // 危险!
return 0;
} // sp1和sp2都会delete p,导致double free
正确做法
cpp
int main()
{
shared_ptr<int> sp1(new int(10));
shared_ptr<int> sp2(sp1); // 正确:拷贝构造
return 0;
}
6.2 不要用get()返回的指针构造新智能指针
错误示例
cpp
int main()
{
shared_ptr<int> sp1(new int(10));
// 危险!两个独立的shared_ptr管理同一资源
shared_ptr<int> sp2(sp1.get());
return 0;
} // double free
正确做法
cpp
int main()
{
shared_ptr<int> sp1(new int(10));
shared_ptr<int> sp2 = sp1; // 正确
return 0;
}
6.3 小心移动后的对象
cpp
int main()
{
unique_ptr<int> up1(new int(10));
unique_ptr<int> up2(std::move(up1));
// 危险!up1已经是nullptr
// *up1 = 20; // 崩溃
if (up1) // 应该先检查
{
*up1 = 20;
}
return 0;
}
6.4 避免循环引用
cpp
// 错误:循环引用
class Node
{
public:
shared_ptr<Node> next;
shared_ptr<Node> prev; // 会导致内存泄漏
};
// 正确:使用weak_ptr打破循环
class Node
{
public:
shared_ptr<Node> next;
weak_ptr<Node> prev; // 使用weak_ptr
};
6.5 智能指针不是万能的
不适合用智能指针的场景
- 需要自定义内存分配器
- 对性能要求极高的场景
- 需要与C接口交互
cpp
// C接口
extern "C" void c_function(int* data);
void CallCFunction()
{
shared_ptr<int> sp(new int(10));
// 传递给C函数
c_function(sp.get()); // OK,但要确保函数不会保存指针
}
七、总结与展望
7.1 全系列回顾
通过两篇文章,我们全面学习了C++智能指针:
第一篇:基础与使用
- 智能指针的使用场景
- RAII设计思想
- auto_ptr、unique_ptr、shared_ptr的使用
- 自定义删除器
- make_shared等实用技巧
第二篇:原理与高级话题
- auto_ptr、unique_ptr、shared_ptr的实现原理
- 循环引用问题与weak_ptr解决方案
- 智能指针的线程安全
- C++11与Boost的关系
- 内存泄漏的检测与预防
7.2 核心要点总结
智能指针的本质
智能指针是基于RAII思想设计的资源管理工具,它利用对象的生命周期自动管理动态资源。
三种智能指针的选择
| 场景 | 推荐 | 原因 |
|---|---|---|
| 单一所有权 | unique_ptr | 零开销,明确语义 |
| 共享所有权 | shared_ptr | 引用计数,自动释放 |
| 观察资源 | weak_ptr | 不增加引用计数 |
循环引用的解决
使用weak_ptr打破循环引用,它不增加引用计数,不参与资源管理。
线程安全
- 引用计数操作是线程安全的(标准库保证)
- 指向的对象不是线程安全的(需要自己保证)
- 指针本身不是线程安全的(需要自己保证)
内存泄漏预防
- 优先使用智能指针
- 遵循RAII原则
- 定期使用检测工具
- 注意循环引用
7.3 实践建议
1. 默认使用智能指针
cpp
// 推荐
auto ptr = make_unique<MyClass>();
// 不推荐(除非有特殊需求)
MyClass* ptr = new MyClass();
2. 优先使用unique_ptr
cpp
// 如果不需要共享,使用unique_ptr
unique_ptr<Data> CreateData()
{
return make_unique<Data>();
}
3. 必要时使用shared_ptr
cpp
// 需要共享时才使用shared_ptr
class Image { /*...*/ };
class Button
{
shared_ptr<Image> _icon; // 多个按钮共享同一图标
};
4. 用weak_ptr解决循环引用
cpp
// 双向关系中,一侧使用weak_ptr
class Parent;
class Child
{
weak_ptr<Parent> _parent; // 避免循环引用
};
5. 使用make_shared/make_unique
cpp
// 推荐:一次内存分配
auto sp = make_shared<Data>(args);
// 不推荐:两次内存分配
shared_ptr<Data> sp(new Data(args));
7.4 继续学习
推荐阅读
- 《Effective Modern C++》
- 《C++ Primer》
- 《More Effective C++》
深入主题
- 自定义内存分配器
- 智能指针与STL容器
- 智能指针的性能优化
- 引用计数的实现细节
实践项目
- 实现一个完整的智能指针类库
- 使用智能指针重构现有项目
- 分析开源项目中的智能指针使用
八、常见问题解答
Q1: unique_ptr和shared_ptr如何选择?
A: 默认使用unique_ptr。只有在明确需要共享所有权时才使用shared_ptr。
Q2: 为什么要废弃auto_ptr?
A: auto_ptr的拷贝语义会导致被拷贝对象悬空,容易产生bug,unique_ptr通过禁止拷贝解决了这个问题。
Q3: weak_ptr有什么用?
A: 主要用于打破shared_ptr的循环引用,以及在不增加引用计数的情况下观察资源。
Q4: 智能指针有性能开销吗?
A: unique_ptr几乎零开销。shared_ptr有引用计数的开销,但通常可以接受。
Q5: 可以把this指针给智能指针管理吗?
A: 不能直接这样做!需要使用enable_shared_from_this机制(这是一个高级话题)。
通过这两篇文章的学习,我们全面掌握了C++智能指针的使用和原理。智能指针是现代C++的核心特性,它让内存管理变得简单和安全。希望这个系列对你有所帮助!
C++智能指针系列到此完结!感谢你的阅读,如果对你有帮助,记得点赞、收藏、分享!期待在评论区看到你的学习心得和使用经验!❤️
