目录
🌈前言
本篇文章进行C++11中智能指针的学习!!!
🚁1、C++11为什么要引入智能指针?
下面先分析一下下面这段程序有没有什么内存方面的问题?
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
else
return a / b;
}
// 异常的缺陷
void Exception_defect()
{
// 如果在初始化p2时,new抛异常,被外层catch捕捉后
// 没有释放p1就结束了程序,那么就会导致内存泄漏
int* p1 = new int;
int* p2 = new int;
// 如果div函数发生除0错误,p1,p2没有得到释放,导致内存泄漏
cout << div() << endl;
delete p1;
delete p2;
}
int main()
{
try
{
Exception_defect();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch(const char* errmsg)
{
cout << errmsg << endl;
}
return 0;
}
问题分析:上面的代码我们发现有什么问题?
内存泄露的问题!!!
🚂2、内存泄漏
🚃2.1、什么是内存泄漏?有什么危害?
什么是内存泄漏:
-
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况
-
内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费
内存泄漏的危害:
-
长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死
-
内存泄漏只能通过重启电脑/服务器才能得到解决 ,可见危害极大
void Exception_defect()
{
// 1.内存申请了忘记释放
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
// 2.异常安全问题 -- 申请内存超出堆区的上限,抛异常(std::bad_alloc)
int* p3 = new int[10];
// 假如Func函数抛异常,则会导致 delete[] p3未执行,p3没被释放.
Func();
delete[] p3;
}
🚄2.2、内存泄漏分类
C/C++程序中一般我们关心两种方面的内存泄漏:
- 堆内存泄漏(Heap Leak):
-
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 释放掉
-
假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak
- 系统资源泄漏:
- 指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定
🚅2.3、检测内存泄漏的方法
-
在linux下内存泄漏检测:Linux下几款内存泄漏检测工具
-
在windows下使用第三方工具:VLD工具说明
-
其他工具:内存泄漏工具比较
🚆2.4、如何避免内存泄漏
-
工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放
-
采用"RAII思想"或者"智能指针"来管理资源
-
有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项
-
出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵
总结:
内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具
🚇3、智能指针
🚈3.1、RAII思想
RAII( Resource Acquisition Is Initialization ):是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术
-
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源
-
借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
-
不需要显式地释放资源
-
采用这种方式,对象所需的资源在其生命期内始终保持有效
简单来说就是:开辟一块空间后,将指向这块空间的指针交给类对象管理,该对象释放后,自动调用析构
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
// 将指向开辟内存的指针交给类对象进行管理
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
// 对象生命周期将要销毁时,调用析构自动释放开辟的空间
cout << "~SmartPtr()" << endl;
if (_ptr)
{
delete _ptr;
}
}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
else
return a / b;
}
void Func()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(new int);
cout << div() << endl;
}
int main()
{
try
{
Func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
🚉3.2、智能指针的原理
-
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为,并且RAII只是一种思想
-
智能指针可以像指针一样解引用,也可以通过->去访问所指空间中的内容,因此:SmartPtr模板类中还得需要将* 、-> 、++ 和-- 重载下,才可让其像指针一样去使用
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
cout << "~SmartPtr()" << endl;
if (_ptr)
{
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return &(*_ptr);
}
SmartPtr& operator++()
{
++_ptr;
return *this;
}
SmartPtr& operator--()
{
--_ptr;
return *this;
}
private:
T* _ptr;
};
struct Date
{
int _year = 0;
int _month = 0;
int _day = 0;
};
int main()
{
SmartPtr<int> sp1(new int);
*sp1 = 10;
cout << *sp1 << endl;
SmartPtr<Date> sp2(new Date);
// 需要注意的是这里应该是sparray.operator->()->_year = 2018;
// 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->
sp2->_year = 2018;
sp2->_month = 1;
sp2->_day = 1;
return 0;
}
总结一下智能指针的原理:
- RAII特性
- 重载operator* 、opertaor->、operator++ 和 operator–,具有像指针一样的行为
🚐3.3、std::auto_ptr(C++98)
-
C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题
-
auto_ptr的实现原理:管理权转移思想(解决拷贝构造指向同一块空间问题)
#pragma once
namespace Smart_Ptr
{
// 智能指针是遵循RAII思想来进行实现的,RAII即:资源获取即刻初始化
// 是一种利用"对象生命周期来控制程序资源",对象初始化后将该对象交予类对象管理
// auto_ptr是C++98的智能指针,底层:管理权转移
template <typename T>
class auto_ptr
{
public:
auto_ptr(T* _ptr)
: ptr(_ptr)
{}
// 管理权转移:将被拷贝对象的指针拷贝给新对象,原指针置为空
auto_ptr(auto_ptr<T>& ap)
: ptr(ap.ptr)
{
ap.ptr = nullptr;
}
~auto_ptr()
{
delete ptr;
}
T& operator*()
{
return *ptr;
}
T* operator->()
{
return &(*ptr);
}
auto_ptr& operator++()
{
++ptr;
return *this;
}
auto_ptr& operator--()
{
--ptr;
return *this;
}
private:
T* ptr;
};
void Auto_ptr()
{
Smart_Ptr::auto_ptr<int> ptr1(new int);
*ptr1 = 1;
Smart_Ptr::auto_ptr<int> ptr2(ptr1);
}
}
结论:auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr
🚑3.4、C++11和boost中智能指针的关系
【boost】
-
Boost是为C++语言标准库提供扩展的一些C++程序库的总称。Boost库是一个可移植、提供源代码的C++库,作为标准库的后备,是C++标准化进程的开发引擎之一,是为C++语言标准库提供扩展的一些C++程序库的总称
-
boost主要是为了C++标准库提前探路用的,boost中很多人用的库都会被拉到C++标准库中
-
C++ 98 中产生了第一个智能指针auto_ptr.
-
C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
-
C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版
-
C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boostscoped_ptr.并且这些智能指针的实现原理是参考boost中的实现的
🚒3.5、std::unique_ptr
【unique_ptr文档】:不支持拷贝构造和拷贝赋值
unique_ptr的实现原理:简单粗暴的防拷贝,防止拷贝构造和赋值拷贝
#pragma once
namespace Smart_Ptr
{
// 智能指针是遵循RAII思想来进行实现的,RAII即:资源获取即刻初始化
// 是一种利用"对象生命周期来控制程序资源",对象初始化后将该对象交予类对象管理
// unique_ptr是C++11引入的智能指针,作用:防止拷贝和赋值
template <typename T>
class unique_ptr
{
public:
unique_ptr(T* _ptr)
: ptr(_ptr)
{}
// C++11使用delete关键字删除成员函数
unique_ptr(unique_ptr<T>& ap) = delete;
unique_ptr& operator=(const unique_ptr<T>& up) = delete;
~unique_ptr()
{
if (ptr != nullptr)
{
D del;
del(ptr);
}
}
T& operator*()
{
return *ptr;
}
T* operator->()
{
return &(*ptr);
}
unique_ptr& operator++()
{
++ptr;
return *this;
}
unique_ptr& operator--()
{
--ptr;
return *this;
}
private:
T* ptr;
};
void Unique_ptr()
{
unique_ptr<int> ptr1(new int(1));
// unique_ptr<int> ptr2(ptr1); // 防止拷贝
unique_ptr<int> ptr2(new int(2));
// ptr2 = ptr1; // 防止赋值拷贝
}
}
🚓3.6、std::shared_ptr(重点)
shared_ptr的原理:是通过"引用计数的方式"来实现多个shared_ptr对象之间共享资源
例如:老师每次放学的时候都会说:让最后走的学生记得把门锁下
-
shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享
-
在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一
-
如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源
-
如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了
#pragma once
namespace Smart_Ptr
{
// 智能指针是遵循RAII思想来进行实现的,RAII即:资源获取即刻初始化
// 是一种利用"对象生命周期来控制程序资源",对象初始化后将该对象交予类对象管理
// shared_ptr是C++11引入的智能指针,允许拷贝和赋值 -- 底层使用"引用计数法"
template <typename T>
class shared_ptr
{
public:
shared_ptr(T* _ptr)
: ptr(_ptr)
, pCount(new int(1))
{}
// 引用计数法 --- 增加一个指针用于计数,如果发生拷贝则计数++,如果发生释放则计数--,计数为0则释放
shared_ptr(shared_ptr<T>& sp)
: ptr(sp.ptr)
, pCount(sp.pCount)
{
++(*pCount);
}
// 赋值拷贝原理:不是自赋值时,释放(自减左值计数)左值,拷贝右值的的数据到左值中
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
// 判断ptr是否相等,相等说明自赋值(不做处理),不相等说明不是自赋值
if (ptr != sp.ptr)
{
// 左操作数计数自减(左值与右值不同,所以要自减),如果计数为0,则释放该指针开辟的空间
if (--(*pCount) == 0 && ptr != nullptr)
{
delete ptr;
ptr = nullptr;
delete pCount;
pCount = nullptr;
}
// 拷贝右操作数的内容,自增计数值
ptr = sp.ptr;
pCount = sp.pCount;
++(*pCount);
}
return *this;
}
~shared_ptr()
{
// 计数自减,如果计数为0,则释放该指针开辟的空间
if (--(*pCount) == 0 && ptr)
{
delete ptr;
ptr = nullptr;
delete pCount;
pCount = nullptr;
}
}
T& operator*()
{
return *ptr;
}
T* operator->()
{
return &(*ptr);
}
shared_ptr& operator++()
{
++ptr;
return *this;
}
shared_ptr& operator--()
{
--ptr;
return *this;
}
int use_count() const
{
return *pCount;
}
// 返回私有成员
T* get() const
{
return ptr;
}
private:
T* ptr;
int* pCount;
};
void Shared_ptr()
{
Smart_Ptr::shared_ptr<int> ptr1(new int(1));
Smart_Ptr::shared_ptr<int> ptr2(ptr1);
Smart_Ptr::shared_ptr<int> ptr3(new int);
ptr3 = ptr1;
}
}
赋值构造就不画图了:原理就是如果不是相同的对象就自减左值的引用计数,然后将新的拷贝对象拷贝给自己;如果是相同的对象进行赋值,那么什么都不做,左值有右值相等
🚓3.6.1、循环引用问题
首先看以下这段代码有什么问题:
struct ListNode
{
// 会导致循环引用问题
Smart_Ptr::shared_ptr<ListNode> _next = nullptr;
Smart_Ptr::shared_ptr<ListNode> _prev = nullptr;
int val = 0;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void Test()
{
{
Smart_Ptr::shared_ptr<ListNode> p1(new ListNode); // p1引用计数:1
Smart_Ptr::shared_ptr<ListNode> p2(new ListNode); // p2引用计数:1
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
// weak_ptr = shared_ptr --> weak_ptr(shared_ptr)
p1->_next = p2; // p2引用计数自增 -- 引用计数:2(shared_ptr)
p2->_prev = p1; // p1计数自增 -- 引用计数:2 (shared_ptr)
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
}
}
没有释放开辟的空间,释放的话shared_ptr会打印
注意:node1就是p1,node2就是p2
循环引用分析:
-
node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete
-
node1的_next指向node2,node2的_prev指向node1,引用计数变成2
-
node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点
-
也就是说_next析构了,node2就释放了
-
也就是说_prev析构了,node1就释放了
-
但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放
解决方案:只要不让类成员使用引用计数就行了,类成员生命周期到了自动会释放,不会影响类对象的释放:
使用weak_ptr智能指针解决
- weak_ptr原理:拷贝构造接收一个shared_ptr类,主要是为了解决循环引用而诞生的类
#pragma once
#include "Shared_ptr.h"
namespace Smart_Ptr
{
// weak_ptr不遵循RAII思想,也就是说它不对对象进行管理
// weak_ptr是为了解决循环引用(Circular reference)而设计的
template <typename T>
class weak_ptr
{
public:
weak_ptr(T* _ptr = nullptr)
: ptr(_ptr)
{}
// 循环引用是"双向链表链接时,左右二个节点里面的成员释放时,导致循环,
// 成员无法释放,间接导致引用计一直不能降到0,对象无法释放"
weak_ptr(const Smart_Ptr::shared_ptr<T>& sp)
: ptr(sp.get()) // 拷贝构造不对引用计数自增就可以完美的解决循环引用的问题
{}
// 可以像指针一样使用
T& operator*()
{
return *ptr;
}
T* operator->()
{
return &(*ptr);
}
weak_ptr& operator++()
{
++ptr;
return *this;
}
weak_ptr& operator--()
{
--ptr;
return *this;
}
private:
T* ptr;
};
}
解决循环引用问题:
// 循环引用问题 -- 使用weak_ptr解决
struct ListNode
{
// 会导致循环引用问题
//Smart_Ptr::shared_ptr<ListNode> next = nullptr;
//Smart_Ptr::shared_ptr<ListNode> pre = nullptr;
// 完美解决循环引用问题
Smart_Ptr::weak_ptr<ListNode> next = nullptr;
Smart_Ptr::weak_ptr<ListNode> pre = nullptr;
int val = 0;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void Test()
{
{
Smart_Ptr::shared_ptr<ListNode> p1(new ListNode); // p1引用计数:1
Smart_Ptr::shared_ptr<ListNode> p2(new ListNode); // p2引用计数:1
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
// weak_ptr = shared_ptr --> weak_ptr(shared_ptr)
p1->next = p2; // p2引用计数自增 -- 引用计数:2(shared_ptr)
p2->pre = p1; // p1计数自增 -- 引用计数:2 (shared_ptr)
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
}
}
🚔3.6.2、删除定制器
- 如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这个问题(ps:删除器这个问题我们了解一下)
这里了解unique_ptr就行了,shared_ptr是再构造函数中传参的
#pragma once
#include <iostream>
#include <cstdlib>
#include <cstdio>
namespace Smart_Ptr
{
// 定制删除器,处理特殊的情况 -- 默认是delete
template <typename T>
struct Delete
{
void operator()(T* ptr)
{
std::cout << "delete ptr" << std::endl;
delete ptr;
}
};
// 处理开辟数组形式的空间
template <typename T>
struct DeleteArray
{
void operator()(T* ptr)
{
std::cout << "delete[] ptr" << std::endl;
delete[] ptr;
}
};
// 处理malloc的情况
template <typename T>
struct Free
{
void operator()(T* ptr)
{
std::cout << "free(ptr)" << std::endl;
std::free(ptr);
}
};
// 处理关闭文件的情况
template <typename T>
struct Fclose
{
void operator()(T* ptr)
{
std::cout << "fclose(ptr)" << std::endl;
std::fclose(ptr);
}
};
}
后面我们只需要将它套到智能指针的类模板形参中就行了
#pragma once
#include "Deleter.h"
namespace Smart_Ptr
{
// 智能指针是遵循RAII思想来进行实现的,RAII即:资源获取即刻初始化
// 是一种利用"对象生命周期来控制程序资源",对象初始化后将该对象交予智能指针管理
// unique_ptr是C++11引入的智能指针,作用:防止拷贝和赋值
template <typename T, typename D = Delete<T>> // 增加定制删除其模板类型参数,默认为delete析构
class unique_ptr
{
public:
unique_ptr(T* _ptr)
: ptr(_ptr)
{}
// C++11使用delete关键字删除成员函数
unique_ptr(unique_ptr<T>& ap) = delete;
unique_ptr& operator=(const unique_ptr<T>& up) = delete;
~unique_ptr()
{
if (ptr != nullptr)
{
D del;
del(ptr);
}
}
private:
T* ptr;
};
void Delete1()
{
// 模板实参传对应的类就行了
Smart_Ptr::unique_ptr<int, DeleteArray<int>> ptr1(new int(1));
Smart_Ptr::unique_ptr<int, Free<int>> ptr2((int*)std::malloc(sizeof(int) * 10));
}
}