目录
[1.1 智能指针产生的原因](#1.1 智能指针产生的原因)
[1.2 RAII思想:智能指针诞生的基础](#1.2 RAII思想:智能指针诞生的基础)
[1.3 C++标准库的智能指针](#1.3 C++标准库的智能指针)
[1.3.1 auto_ptr](#1.3.1 auto_ptr)
[1.3.2 unique_ptr](#1.3.2 unique_ptr)
[1.3.3 shared_ptr](#1.3.3 shared_ptr)
[1.3.4 weak_ptr](#1.3.4 weak_ptr)
[1.3.5 定制删除器](#1.3.5 定制删除器)
[1.3.6 make_shared](#1.3.6 make_shared)
[1.3.7 智能指针的补充](#1.3.7 智能指针的补充)
[1.4 shared_ptr 和 weak_ptr](#1.4 shared_ptr 和 weak_ptr)
[1.4.1 shared_ptr 循环引用问题](#1.4.1 shared_ptr 循环引用问题)
[1.4.2 weak_ptr](#1.4.2 weak_ptr)
[1.5 shared_ptr的线程安全问题](#1.5 shared_ptr的线程安全问题)
[1.6 补充知识 ---- 内存泄漏](#1.6 补充知识 ---- 内存泄漏)
[1.6.1 什么是内存泄漏](#1.6.1 什么是内存泄漏)
[1.6.2 内存泄漏的原因和危害](#1.6.2 内存泄漏的原因和危害)
[1.6.3 避免内存泄漏的方法](#1.6.3 避免内存泄漏的方法)
[1.6.4 如何检测内存泄漏](#1.6.4 如何检测内存泄漏)
1.1 智能指针产生的原因
cpp
#include <iostream>
#include <string>
double Divide(double x, double y)
{
if(y == 0)
{
throw std::string("发生除0错误");
}
return x / y;
}
void Func()
{
int* arr = new int[10];
int* brr = new int[10];
Divide(2, 0);
std::cout << "delete[] arr" << std::endl;
delete[] arr;
std::cout << "delete[] brr" << std::endl;
delete[] brr;
}
int main()
{
try
{
Func();
}
catch (std::string& e)
{
std::cout << e << std::endl;
}
catch (...)
{
std::cout << "未知异常" << std::endl;
}
return 0;
}
运行结果:
发生除0错误
在上述程序中,我们不仅申请了内存,也释放了内存。但是因为抛异常导致执行流发生改变,后面的delete没有得到执行,就会造成内存泄漏的风险。
第一种解决方案:在即将抛出异常的时候,释放内存。但是new本身也会抛异常,我们也需要额外处理,就会让我们处理起来很麻烦。如下述程序所示:
cpp
#include <iostream>
#include <string>
double Divide(double x, double y)
{
if (y == 0)
{
throw std::string("发生除0错误");
}
return x / y;
}
void Func()
{
int *arr = nullptr;
try
{
arr = new int[10];
}
catch (const std::exception &e)
{
std::cerr << e.what() << '\n';
}
int *brr = nullptr;
try
{
brr = new int[10];
}
catch (const std::exception &e)
{
std::cout << "delete[] arr" << std::endl;
delete[] arr;
std::cerr << e.what() << '\n';
}
try
{
Divide(2, 1);
}
catch (const std::string &e)
{
std::cout << "delete[] arr" << std::endl;
delete[] arr;
std::cout << "delete[] brr" << std::endl;
delete[] brr;
throw; // 异常的重新抛出
}
std::cout << "delete[] arr" << std::endl;
delete[] arr;
std::cout << "delete[] brr" << std::endl;
delete[] brr;
}
int main()
{
try
{
Func();
}
catch (const std::string &e)
{
std::cout << e << std::endl;
}
catch (...)
{
std::cout << "未知异常" << std::endl;
}
return 0;
}
可以看出,这个方案能够解决问题,但随着资源越来越多,维护成本就会迅速上升,所以它不是一个好的工程化方案。
第二种解决方案:智能指针应运而生
1.2 RAII思想:智能指针诞生的基础
RAII 是 Resource Acquisition Is Initialization 的缩写,意思为获得资源立即初始化 。它是一种管理资源的类的设计思想 ,核心机制就是抛异常中栈展开的核心机制:栈销毁时,已经构造完成的局部对象一定会被自动析构,它的本质是一种利用类对象生命周期来管理获取到的资源,避免资源泄漏,这里的资源可以是内存、文件指针、网络连接、互斥锁等等。RAII 在获取资源时把资源交给一个类对象,通过类对象对资源进行访问,资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,保障了资源的正常释放,避免资源泄漏问题。
智能指针就是一种基于RAII设计思路实现出来的一种类 。但智能指针为了方便资源的访问,所以智能指针会像迭代器一样,重载operator*/operator-> 等运算符,对于管理数组的智能指针,还会提供operator\[\],使其像使用原生指针一样访问资源。
接下来,我们从最简单的智能指针实现开始,逐步理解指针指针是如何利用 RAII 自动管理资源。
cpp
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~SmartPtr()
{
delete _ptr;
}
private:
T* _ptr;
};
1.3 C++标准库的智能指针
C++标准库中的智能指针都在 <memory> 头文件里。
C++标准库中的智能指针有四种:auto_ptr、unique_ptr、shared_ptr、weak_ptr ,除了weak_ptr以外的智能指针都符合RAII和像原生指针一样访问的行为,这三种智能指针主要是解决智能指针拷贝的思路不同。
1.3.1 auto_ptr
auto_ptr 是 C++98 中设计出来的智能指针,它的特点是拷贝时,把被拷贝对象的资源的管理权交给拷贝对象(类似移动语义,掠夺别人的资源),但这是一个非常糟糕的设计,因为它会导致被拷贝对象悬空,后续代码使用它来访问资源时报错,在 C++11 设计出新的智能指针后,强烈建议不要使用 auto_ptr。在 C++11 出来之前很多公司明令禁止使用 auto_ptr。
cpp
#include <iostream>
#include <memory>
int main()
{
std::auto_ptr<int> ap1(new int(5));
printf("ap1中存放的地址: %p, ap1中地址指向的值: %d\n", ap1.get(), *ap1);
std::auto_ptr<int> ap2(ap1);
printf("ap2中存放的地址: %p, ap2中地址指向的值: %d\n", ap2.get(), *ap2);
printf("ap1中存放的地址: %p\n", ap1.get());
return 0;
}
运行结果:
ap1中存放的地址: 0x5623ce21beb0, ap1中地址指向的值: 5
ap2中存放的地址: 0x5623ce21beb0, ap2中地址指向的值: 5
ap1中存放的地址: (nil)
模拟实现:
cpp
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
// 拷贝构造和拷贝赋值的参数均不能用const修饰
auto_ptr(auto_ptr& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
auto_ptr& operator=(auto_ptr& ap)
{
if(this != &ap)
{
delete _ptr;
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
T* get() const
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~auto_ptr()
{
delete _ptr;
}
private:
T* _ptr;
};
1.3.2 unique_ptr
unique_ptr 是 C++11 中设计出来的智能指针,它的名字翻译出来是唯一指针 ,它的特点是不支持拷贝,支持移动。如果不需要拷贝,就非常建议使用它。
unique_ptr 和 auto_ptr 的区别是 unique_ptr不允许拷贝,如果发生拷贝,就会编译错误;auto_ptr允许拷贝,被拷贝对象悬空,不会发生编译错误。
cpp
#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<int> up1(new int(5));
// 编译错误,不支持拷贝
// std::unique_ptr<int> up2(up1);
// 编译错误, 不支持赋值
// std::unique_ptr<int> up2;
// up2 = up1;
// 支持移动构造和移动拷贝,需注意
// 被移动的对象的指针悬空,后续访问会发生错误
std::cout << "up1: " << up1.get() << std::endl;
std::unique_ptr<int> up2(std::move(up1));
std::cout << "up1: " << up1.get() << std::endl;
std::cout << "up2: " << up2.get() << std::endl;
std::unique_ptr<int> up3;
up3 = std::move(up2);
return 0;
}
运行结果:
up1: 0x55b78b948eb0
up1: 0
up2: 0x55b78b948eb0
模拟实现:
cpp
template <class T>
class unique_ptr
{
public:
unique_ptr(T *ptr = nullptr)
: _ptr(ptr)
{
}
unique_ptr(const unique_ptr &up) = delete;
unique_ptr &operator=(const unique_ptr &up) = delete;
unique_ptr(unique_ptr &&up)
: _ptr(up._ptr)
{
up._ptr = nullptr;
}
unique_ptr &operator=(unique_ptr &&up)
{
if (this != &up)
{
delete _ptr;
_ptr = std::move(up._ptr);
up._ptr = nullptr;
}
return *this;
}
T* get()
{
return _ptr;
}
// 运算符重载... 这里不再模拟实现,可以参考 auto_ptr 的模拟实现
~unique_ptr()
{
delete _ptr;
}
private:
T *_ptr;
};
1.3.3 shared_ptr
shared_ptr 是 C++11 设计出来的智能指针,它的名字翻译出来是共享指针 ,它的特点是支持拷贝,也支持移动 。如果需要拷贝的场景就使用它。它的底层是用引用计数的方式实现的。
cpp
#include <iostream>
#include <memory>
int main()
{
std::shared_ptr<int> sp1(new int(2));
std::shared_ptr<int> sp2(sp1);
std::shared_ptr<int> sp3;
sp3 = sp1;
std::cout << "sp1: " << sp1.get() << std::endl;
std::cout << "sp2: " << sp2.get() << std::endl;
std::cout << "sp3: " << sp3.get() << std::endl;
std::cout << sp1.use_count() << std::endl;
return 0;
}
运行结果:
sp1: 0x55b8c2123eb0
sp2: 0x55b8c2123eb0
sp3: 0x55b8c2123eb0
3
模拟实现:
cpp
template <class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
{
if(_ptr) // _ptr != nullptr
{
_count = new int(1);
}
}
shared_ptr(const shared_ptr& sp)
:_ptr(sp._ptr)
,_count(sp._count)
{
if(_ptr)
++(*_count);
}
shared_ptr& operator=(const shared_ptr& sp)
{
if(this != &sp)
{
if(_ptr && --(*_count) == 0)
{
delete _ptr;
delete _count;
}
_ptr = sp._ptr;
_count = sp._count;
if(_ptr)
++(*_count);
}
return *this;
}
T& operator*() const
{
return *_ptr;
}
T* operator->() const
{
return _ptr;
}
T* get() const
{
return _ptr;
}
size_t use_count() const
{
return _count == nullptr ? 0 : *_count;
}
~shared_ptr()
{
if(_ptr && --(*_count) == 0)
{
delete _ptr;
delete _count;
}
}
private:
T* _ptr = nullptr;
int* _count = nullptr;
};
1.3.4 weak_ptr
weak_ptr 是 C++11 设计出来的智能指针,它的名字翻译出来是弱指针,它不支持 RAII,意味着不能用它直接管理资源,weak_ptr 的产生本质是要解决 shared_ptr 的一个循环引用导致内存泄漏的问题,具体细节下面会讲。
1.3.5 定制删除器
对于标准库中的智能指针析构时默认是进行delete释放资源 ,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。所以智能指针支持在构造 时给一个删除器,所谓的删除器本质是一个可调用对象,这个可调用对象中实现你想要的释放资源的方式,当构造智能指针时,给了定制删除器,那么智能指针在析构时就会调用删除器来释放资源。
由于new\[\]经常使用,unique_ptr 和 shared_ptr 都特化了一份 \[\] 的版本,使用
unique_ptr<int\[\]> up1(new int5);
shared_ptr<int\[\]> sp1(new int5);
就可以管理 new\[\] 的资源。
cpp
#include <iostream>
#include <memory>
template <class T>
void DeleteArrayFunc(T* ptr)
{
std::cout << "DeleteArrayFunc" << std::endl;
delete[] ptr;
}
template <class T>
class DeleteArray
{
public:
void operator()(T* ptr)
{
std::cout << "DeleteArray" << std::endl;
delete[] ptr;
}
};
class Fclose
{
public:
void operator()(FILE* pf)
{
std::cout << "文件关闭" << std::endl;
fclose(pf);
}
};
int main()
{
// 程序运行崩溃
// std::unique_ptr<std::string> up1(new std::string[10]);
// std::shared_ptr<std::string> sp1(new std::string[10]);
// 解决方案一 ---- 模板特化
std::unique_ptr<std::string[]> up1(new std::string[10]);
std::shared_ptr<std::string[]> sp1(new std::string[10]);
// 解决方案二 ---- 定制删除器
// 1. 仿函数对象做删除器
// unique_ptr和shared_ptr支持删除器的方式有所不同
// unique_ptr是在类模板参数支持的,shared_ptr是构造函数参数支持的
// 这里没有使用相同的方式还是挺坑的
// 使用仿函数unique_ptr可以不在构造函数传递,因为仿函数类型构造的对象直接就可以调用
// 但是下面的函数指针和lambda的类型不可以
std::unique_ptr<std::string[], DeleteArray<std::string>> up2(new std::string[10]);
std::shared_ptr<std::string[]> sp2(new std::string[10], DeleteArray<std::string>());
// 2. 函数指针做删除器
std::unique_ptr<std::string[], void(*)(std::string*)> up3(new std::string[10], DeleteArrayFunc<std::string>);
std::shared_ptr<std::string[]> sp3(new std::string[10], DeleteArrayFunc<std::string>);
// 3. lambda表达式做删除器
auto delArr = [](std::string* ptr)
{
std::cout << "lambda" << std::endl;
delete[] ptr;
};
std::unique_ptr<std::string[], decltype(delArr)> up4(new std::string[10], delArr);
std::shared_ptr<std::string[]> sp4(new std::string[10], delArr);
// 实现其他资源管理的删除器
std::shared_ptr<FILE> sp5(fopen("log.txt", "w"), Fclose());
std::shared_ptr<FILE> sp6(fopen("log.txt", "w"), [](FILE* ptr)
{
std::cout << "文件关闭" << std::endl;
fclose(ptr);
});
return 0;
}
对于 unique_ptr 推荐仿函数对象传定制删除器,对于 shared_ptr 推荐仿函数对象或者lambda对象传定制删除器。


1.3.6 make_shared

shared_ptr 除了支持用指向资源的指针构造,还支持 make_shared 用初始化资源对象的值直接构造。
cpp
#include <iostream>
#include <memory>
int main()
{
std::shared_ptr<std::string> sp1(new std::string("1111"));
std::shared_ptr<std::string> sp2 = std::make_shared<std::string>("2222");
auto sp3 = std::make_shared<std::string>("3333");
return 0;
}
对于 new 出来的对象来构造 shared_ptr ,内存分两次申请,一次申请string对象,一次申请控制块(计数器的对象),对于 make_shared 出来的对象构造 shared_ptr,只做一次内存申请,同时申请 string 对象和 控制块,底层是把对象指针和控制块放到了一个结构体中。
make_shared 的优点
少一次堆申请 -> 提高效率
cache 更友好 -> 控制块和对象绑定,缓冲命中率高
异常安全更强 -> new T 成功,但 new 控制块失败,存在内存泄漏风险,make_shared一次性分配,不存在半成功状态
内存碎片更少 -> 控制块和对象绑定,只需在内存中申请一个struct的空间
make_shared 的缺点不支持定制删除器 -> 需要定制删除器的场景不能使用
控制块和对象绑定 -> weak_ptr 不会影响对象生命周期,但会延长控制块的生命周期,在make_shared场景下,由于控制块和对象绑定,即使对象得到了释放,也会导致整个结构体内存无法释放。
1.3.7 智能指针的补充
shared_ptr 和 unique_ptr 都支持了 operator bool 的类型转换,如果智能指针对象是一个空对象,没有管理资源,则返回 false,否则返回 true。意味着我们可以把智能指针对象使用 if 判断是否为空。

cpp
#include <iostream>
#include <memory>
int main()
{
std::shared_ptr<int> sp1(new int(2));
std::shared_ptr<int> sp2;
if (sp1)
{
std::cout << "sp1 is not empty object" << std::endl;
}
if (!sp2)
{
std::cout << "sp2 is empty object" << std::endl;
}
return 0;
}
shared_ptr 和 unique_ptr 的构造函数 均使用 explicit 修饰,防止普通指针隐式类型转换成智能指针对象。


cpp
// 编译错误
std::shared_ptr<int> sp1 = new int(2);
// 这样写的本质是先隐式类型转化构造成一个智能指针对象
// 再将其移动构造sp1
// 而第一步是不被允许的,所以编译错误
1.4 shared_ptr 和 weak_ptr
1.4.1 shared_ptr 循环引用问题
shared_ptr 在大多数情况下管理资源非常合适,支持RAII,也支持拷贝。但是在循环引用的场景下会导致资源没得到释放,产生内存泄漏,所以我们要认识循环引用的场景和资源没释放的原因,并且学会使用 weak_ptr 解决这种问题。
cpp
#include <iostream>
#include <memory>
struct ListNode
{
int _data;
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
~ListNode()
{
std::cout << "~ListNode()" << std::endl;
}
};
int main()
{
// 循环引用 -- 内存泄露
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
std::cout << n1.use_count() << std::endl;
std::cout << n2.use_count() << std::endl;
n1->_next = n2;
n2->_prev = n1;
std::cout << n1.use_count() << std::endl;
std::cout << n2.use_count() << std::endl;
return 0;
}
运行结果:
1
1
2
2
从运行结果可以看出,申请的堆内存并没有释放,原因:如果释放了内存,那么智能指针的析构函数会调用节点的析构函数。这就是典型的循环引用产生的内存泄漏问题,接下来让我们一步一步地理解这个过程。



右边节点什么时候释放? 左边节点的 next 析构后,右边节点就释放了。
next 什么时候析构呢? 左边节点释放,next 就析构了。
左边节点什么时候释放?右边节点的 prev 析构后,左边节点就释放了。
prev 什么时候析构呢? 右边节点释放,_prev 就析构了。
至此,逻辑上成功形成循环引用,导致内存泄漏。
解决方案:weak_ptr
把ListNode结构体中的_next和_prev改成weak_ptr,由于weak_ptr绑定到shared_ptr时不会增加它的引用计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引用,解决了这里的问题
cpp
#include <iostream>
#include <memory>
struct ListNode
{
int _data;
/*std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;*/
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
~ListNode()
{
std::cout << "~ListNode()" << std::endl;
}
};
int main()
{
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
std::cout << n1.use_count() << std::endl;
std::cout << n2.use_count() << std::endl;
n1->_next = n2;
n2->_prev = n1;
std::cout << n1.use_count() << std::endl;
std::cout << n2.use_count() << std::endl;
return 0;
}
运行结果:
1
1
1
1
~ListNode()
~ListNode()
1.4.2 weak_ptr
weak_ptr 不支持RAII,也不支持访问资源,weak_ptr构造时不支持绑定到资源,只支持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引用计数,那么就可以解决上述的循环引用问题。
weak_ptr也没有重载operator*和operator->等,因为它不参与资源管理,如果它绑定的shared_ptr已经释放了资源,那么它去访问资源就是很危险的。weak_ptr支持 expired 检查指向的资源是否过期,use_count也可获取shared_ptr的引用计数 ,weak_ptr想访问资源时,可以调用lock返回一管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是一个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。
cpp
#include <iostream>
#include <memory>
int main()
{
std::shared_ptr<int> sp1(new int(2));
std::shared_ptr<int> sp2(sp1);
std::weak_ptr<int> wp = sp1;
// 检测 wp 指向的资源是否被释放,释放返回true,否则返回false
std::cout << wp.expired() << std::endl;
// 获取 shared_ptr 指向 wp 所指向的资源的个数
std::cout << wp.use_count() << std::endl;
sp1 = std::make_shared<int>(1);
sp2 = std::make_shared<int>(3);
std::cout << wp.expired() << std::endl;
std::cout << wp.use_count() << std::endl;
wp = sp1;
// lock 获取 wp 指向资源的指针交给 shared_ptr 对象
auto sp3 = wp.lock();
std::cout << wp.expired() << std::endl;
std::cout << wp.use_count() << std::endl;
*sp1 *= 10;
std::cout << *sp1 << std::endl;
return 0;
}
运行结果:
0
2
1
0
0
2
10
1.5 shared_ptr的线程安全问题
shared_ptr的引用计数对象在堆上,如果多个shared_ptr对象在多个线程中,进行shared_ptr的拷贝析构时会访问修改引用计数,就会存在线程安全问题,所以shared_ptr引用计数是需要加锁或者原子操作保证线程安全的。我们上面的模拟实现是存在此问题的,我们可以将引用计数从int*改成atomic<int>* 就可以保证引用计数的线程安全问题。但是标准库里面是不存在此问题的。
**shared_ptr指向的对象是有线程安全的问题的(标准库也有该问题),但是这个对象的线程安全问题不归shared_ptr管,它也管不了,应该有外层使用shared_ptr的人进行线程安全的控制。**
1.6 补充知识 ---- 内存泄漏
1.6.1 什么是内存泄漏
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存,一般是忘记释放或者发生异常释放程序未能执行导致的。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
1.6.2 内存泄漏的原因和危害
对于不是长时间运行的普通程序,运行一段时间结束了,它如果出现内存泄漏的问题,形成的危害并不大。因为进程结束,页表的映射关系解除,将占用的物理内存还给操作系统,操作系统可以将其继续给其他进程使用。对于长期运行的程序,如果出现内存泄漏,危害很大,如操作系统、后台服务、长时间运行的客户端等等,不断出现内存泄漏会导致可用内存不断变少,各种功能响应越来越慢,最终卡死。
对于僵尸状态的进程,它的进程控制块得不到释放,就会一直占用物理内存空间,进而导致内存泄漏,产生操作系统卡顿,后台服务效率慢等问题。
1.6.3 避免内存泄漏的方法
-
工程前期良好的设计规范,养成良好的编码规范,申请的内存空间要释放,父进程等待子进程。
-
尽量使用智能指针来管理资源,如果自己场景比较特殊,采用RAII思想自己造个轮子管理。
-
定期使用内存泄漏工具检测,尤其是每次项目快上线前,不过有些工具不够靠谱,或者是收费。
总结一下:内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错
型。如泄漏检测工具。
1.6.4 如何检测内存泄漏
linux下内存泄漏检测:Linux下几款C++程序中的内存泄露检查工具_c++内存泄露工具分析-CSDN博客文章浏览阅读6w次,点赞48次,收藏308次。本文介绍了几种常用的Linux内存泄露检测工具,包括valgrind、mtrace、dmalloc和Kmemleak。文章详细阐述了每种工具的特点、安装方法及使用步骤,并提供了具体的示例程序。https://blog.csdn.net/gatieme/article/details/51959654windows下内存泄漏检测:
