文章目录
C++智能指针
智能指针的使用
内存泄漏一直是困扰困扰程序猿的一大难题,在编写代码过程中很容易因为疏忽或者错误导致我们的程序存在内存泄漏。并且伴随着C++异常的出现,更是雪上加霜,智能指针应运而生
内存泄漏
内存泄漏是指因为程序猿的疏忽或者错误,导致程序没有释放已经不再使用的内存资源,比如在C++异常中我们写的
struct myexception final : public std::exception {
const char* what() const noexcept override { return "i'm clx_exception"; }
};
void func2() {
int* arr = new int[10]; //在堆上申请空间
myexception e;
throw e; // 抛异常
delete[] arr; // 空间没有释放
}
void func1() {
try {
func2();
} catch (std::exception& e) {
std::cout << e.what() << std::endl;
}
}
int main() {
func1();
return 0;
}
执行上述代码,就会出现内存泄漏的现象,在执行fun2()发生异常后,会跳到func1()去处理异常,导致func函数中申请的内存资源没有得到释放
使用异常的重新捕获
对于这种情况,我们可以在func2函数中先将异常捕获,处理好内存资源后再将异常原封不动的抛出,达到目的。但是这样比较麻烦
void func2() {
int* arr = new int[10];
try {
myexception e;
throw e;
delete[] arr;
} catch (...){
delete[] arr;
throw;
}
}
void func1() {
try {
func2();
} catch (std::exception& e) {
std::cout << e.what() << std::endl;
}
}
使用智能指针
上述问题也可以使用智能指针来解决,我们可以实现一个简易的智能指针
struct myexception final : public std::exception {
const char* what() const noexcept override { return "i'm clx_exception"; }
};
template<typename T>
class SmartPtr{ // 简单的智能指针
public:
SmartPtr(T* _ptr) :ptr(_ptr){}
~SmartPtr() { std::cout << "delete : " << ptr << std::endl; delete ptr;}
T& operator*() {return *ptr;}
T* operator->() { return ptr; }
private:
T* ptr;
};
void func2() {
SmartPtr<int> sp(new int);
myexception e;
throw e;
}
void func1() {
try {
func2();
} catch (std::exception& e) {
std::cout << e.what() << std::endl;
}
}
int main() {
func1();
return 0;
}
我们将申请的内存空间交给了一个SmartPtr对象进行管理
- 在构造SmartPtr对象时,SmartPtr将传入的需要被管理的内存空间保存起来
- 在Smart对象西沟时,SmartPtr的析构函数会自动将管理的内存空间进行释放
- 此外,为了让SmartPtr对象能够像原生指针一样使用,还需要*和-> 运算符重载
这样一来,虽然程序即使中途折返,抛异常返回,只要SmartPtr的生命周期结束,离开了其所属于的函数栈帧,那么就会调用析构函数完成内存资源的释放
智能指针的原理
实现智能指针需要关注三方面的问题:
1、在对象构造获取资源,在对象析构释放资源,利用对象的生命周期来控制程序资源,即RALL特性
2、对 * 和->运算符进行重载,让该对象具有像指针一样的行为
3、智能指针对象的拷贝问题
RALL:(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(内存、文件描述符、句柄、互斥量)的简单技术
智能指针的拷贝
对于一个SmartPtr对象如果使用拷贝构造另一个SmartPtr对象,那么第一个对象销毁后,第二对象销毁时就会对释放过的空间再次释放,导致程序崩溃
int main() {
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(sp1);
return 0;
}
因为编译器默认生成的拷贝函数对内置类型进行浅拷贝,因此这两个智能指针共同管理一块空间。赋值操作同理。需要注意的是我们想让智能指针可以模拟原生指针的行为,当一个指针赋值给另外一个指针时,目的就是为了让两个指针指向同一块内存空间,所以这里应该进行浅拷贝,但是单纯的浅拷贝又会让空间多次被释放。所以根据解决智能指针的拷贝问题方式的不同,从而衍生出不同版本的智能指针
C++ 中的智能指针
std::auto_ptr
auto_ptr时C++98时就引入的智能指针,auto_ptr通过管理权转移的方式解决了智能指针的拷贝问题,保证资源在任何时刻都只有一个对象在进行管理,这样一个对象就不会被多次释放了
int main() {
std::auto_ptr<int> ap1(new int(1));
std::auto_ptr<int> ap2(ap1);
*ap2 = 10;
std::auto_ptr<int> ap3(new int(1));
std::auto_ptr<int> ap4(new int(2));
return 0;
}
但是一个对象转移了管理权限也就意味着其不能管理原来的资源并进行访问了,否则程序就会崩溃,因此使用auto_ptr前必须了解其的机制,否则程序很容易出问题,很多公司明确禁止使用auto_ptr
auto_ptr 的模拟实现
实现步骤
- 在构造函数中获取资源,在析构函数中释放资源,利用对象生命周期控制资源
- 对 * 和 -> 运算符进行重载
- 在拷贝构造函数中,将原先管理资源的对象指针置空
- 在拷贝赋值函数中,先将当前对象管理的资源进行释放,然后再接管传入对象管理的资源,最后将传入对象的指针置空
template<typename T>
class clx_auto_ptr {
public:
clx_auto_ptr(T* _ptr) : ptr(_ptr) {}
T& operator*() { return *ptr; };
T* operator->() { return ptr; };
~clx_auto_ptr();
clx_auto_ptr(clx_auto_ptr& ap);
clx_auto_ptr& operator=(clx_auto_ptr& ap);
private:
T* ptr;
};
template<typename T>
clx_auto_ptr<T>::~clx_auto_ptr() {
if (ptr != nullptr) {
std::cout << "delete : " << ptr << std::endl;
delete ptr;
ptr = nullptr;
}
}
template<typename T>
clx_auto_ptr<T>::clx_auto_ptr(clx_auto_ptr& ap)
: ptr(ap.ptr) { ap.ptr = nullptr; }
template<typename T>
clx_auto_ptr<T>& clx_auto_ptr<T>::operator=(clx_auto_ptr& ap) {
if (this != &ap) { // 防自拷贝
delete ptr; // 清理资源
ptr = ap->ptr; // 转移资源
ap->ptr = nullptr; // 剥夺权限
}
return *this;
}
int main() {
clx_auto_ptr<int> p1(new int);
std::cout << "*p1 = " << *p1 << std::endl;
clx_auto_ptr<int> p2 = p1;
std::cout << "*p2 = " << *p2 << std::endl;
return 0;
}
// 输出
*p1 = 0
*p2 = 0
delete : 0x600003360030
std::unique_ptr
unique_ptr是C++11中引入的智能指针,unique_ptr通过防拷贝的方式解决了智能指针的拷贝问题。简单粗暴
int main() {
std::unique_ptr<int> up1(new int(0));
std::unique_ptr<int> up2(up1); // 错误
std::unique_ptr<int> up3 = up1; // 错误
return 0;
}
但是放拷贝也并不是很好的方法,因为总有一些场景需要拷贝
unique_ptr 的模拟实现
实现步骤
1、在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源
2、对* 和 -> 运算符进行重载,时unique_ptr对象具有像指针一样的行为
3、使用C++98方式将拷贝函数和赋值运算符重载函数声明为私有,或者用C++11的delete操作符
template<typename T>
class clx_unique_ptr{
public:
clx_unique_ptr(T* _ptr) : ptr(_ptr) {}
~clx_unique_ptr(){ if (ptr) delete ptr; }
T& operator*() { return *ptr; };
T* operator->() { return ptr; };
private:
clx_unique_ptr(clx_unique_ptr& ap) = delete;
clx_unique_ptr& operator=(clx_unique_ptr& ap) = delete;
T* ptr;
};
std::shared_ptr
shared_ptr 也是C++11引入的智能指针,shared_ptr 通过引用计数的方式解决了智能指针拷贝问题,只有一个资源对应的引用计数减为0才会释放资源,因此保证了同一个资源不会被释放多次
实现步骤
1、在shared_ptr 类中增减一个成员变量count,表示智能指针管理的资源的引用计数
2、构造时引用计数置成一,表示只有一个对象在管理这个资源
3、拷贝构造时获取传入指针,然后++引用计数
4、赋值函数拷贝时,将当前对象管理的引用计数减减(若引用计数为1直接调用析构函数),然后再将数据和传入指针同步,同时将传入对象的引用计数++
5、析构函数中将引用计数–,当减为0时就会将资源释放
template<typename T>
class clx_shared_ptr {
public:
clx_shared_ptr(T* _ptr) : ptr(_ptr), pcount(new int(1)) {}
T& operator*() { return *ptr; };
T* operator->() { return ptr; };
~clx_shared_ptr();
clx_shared_ptr(clx_shared_ptr& ap);
clx_shared_ptr& operator=(clx_shared_ptr& ap);
int use_count() { return *pcount; };
private:
T* ptr;
int* pcount;
};
template<typename T>
clx_shared_ptr<T>::~clx_shared_ptr() {
*pcount -= 1;
if (*pcount == 0) {
if (ptr != nullptr) {
std::cout << "delete : " << ptr << std::endl;
delete ptr; ptr = nullptr;
}
delete pcount; pcount = nullptr;
}
}
template<typename T>
clx_shared_ptr<T>::clx_shared_ptr(clx_shared_ptr& ap)
: ptr(ap.ptr), pcount(ap.pcount) { (*pcount)++; } // 注意优先级
template<typename T>
clx_shared_ptr<T>& clx_shared_ptr<T>::operator=(clx_shared_ptr<T>& ap) {
if (ap.ptr != ptr) {
if (*pcount == 1) {
this->~clx_shared_ptr();
} else {
*pcount -= 1;
}
ptr = ap.ptr;
pcount = ap.pcount;
*pcount += 1;
}
return *this;
}
int main() {
clx_shared_ptr<int> sp1(new int(1));
std::cout << sp1.use_count() << std::endl;
clx_shared_ptr<int> sp2(sp1);
*sp1 = 10;
*sp2 = 20;
std::cout << sp1.use_count() << std::endl;
clx_shared_ptr<int> sp3(new int(1));
clx_shared_ptr<int> sp4(sp3);
sp3 = sp1;
std::cout << sp1.use_count() << std::endl;
std::cout << sp4.use_count() << std::endl;
return 0;
}
1
2
3
1
delete : 0x600001d2c050
delete : 0x600001d2c030
++ 和 * 操作符的优先级 ++ > *
template<typename T>
clx_shared_ptr<T>::clx_shared_ptr(clx_shared_ptr& ap)
: ptr(ap.ptr), pcount(ap.pcount) { (*pcount)++; }
由于优先级原因不能将(*pcount)++
写成*pcount++
后面写法是错误的,并且编译器还不会报错,注意注意⚠️
引用计数为何要放在堆区
因为引用计数并不是单单属于一个对象的,所以不能使用int类型的成员变量。而引用计数也并不是属于类的,所以也不能使用static静态成员变量,引用计数是属于所有管理同一块区域的对象的,我们想让这些对象可以看到同一个引用计数最简单的方法就是开辟一块空间,让所有符合条件的对象都看见
std::shared_ptr 线程安全问题
当前模拟实现的clx_shared_ptr存在线程安全问题,由于管理同一个资源的多个对象的引用计数是共享的,所以多线程可能存在同时对一个引用计数进行自增或者自减操作,而这些操作都不是原子操作,需要进行加锁保护
线程安全问题验证
使用两个线程,分别对一个智能指针拷贝析构100000次,按照道理来说最后输出的值应该是1,我在Clion跑出来是96.根本原因就是引用计数的自增和自减不是原子操作
void func(clx_shared_ptr<int>& sp, size_t n) {
for (size_t i = 0; i < n; i++) {
clx_shared_ptr<int> copy(sp);
}
}
int main() {
clx_shared_ptr<int> p(new int(1));
const size_t n = 100000;
std::thread t1 (func, std::ref(p), n);
std::thread t2 (func, std::ref(p), n);
t1.join();
t2.join();
std::cout << p.use_count() << std::endl;
}
解决方案1 : 使用原子类atomic对引用计数进行封装
这种方法非常简单,只需要将原来的int类型替换成我们的原子类型std::atomic即可
template<typename T>
class clx_shared_ptr {
public:
clx_shared_ptr(T* _ptr) : ptr(_ptr), pcount(new std::atomic<int>(1)) {} // 修改1
T& operator*() { return *ptr; };
T* operator->() { return ptr; };
~clx_shared_ptr();
clx_shared_ptr(clx_shared_ptr& ap);
clx_shared_ptr& operator=(clx_shared_ptr& ap);
int use_count() { return *pcount; };
private:
T* ptr;
std::atomic<int>* pcount; // 修改2
};
修改完成后就可以跑一跑了,可以观察到输出是1
解决方案2 : 加锁
因为是所有管理同一份资源的对象共用一把锁,所以这把锁也要放到内存中
template<typename T>
class clx_shared_ptr {
public:
clx_shared_ptr(T* _ptr)
: ptr(_ptr), pcount(new int(1))
, pmtx(new std::mutex){}
T& operator*() { return *ptr; };
T* operator->() { return ptr; };
~clx_shared_ptr();
clx_shared_ptr(clx_shared_ptr& ap);
clx_shared_ptr& operator=(clx_shared_ptr& ap);
int use_count() { return *pcount; };
private:
T* ptr;
int* pcount;
std::mutex* pmtx;
};
template<typename T>
clx_shared_ptr<T>::~clx_shared_ptr() {
std::unique_lock<std::mutex> ul(*pmtx);
int flag = false;
*pcount -= 1;
if (*pcount == 0) {
flag = true;
if (ptr != nullptr) {
std::cout << "delete : " << ptr << std::endl;
delete ptr; ptr = nullptr;
}
delete pcount; pcount = nullptr;
}
ul.unlock();
if (flag) {
ul.~unique_lock();
delete pmtx;
}
}
template<typename T>
clx_shared_ptr<T>::clx_shared_ptr(clx_shared_ptr& ap) {
std::unique_lock<std::mutex> ul(*(ap.pmtx));
ptr = ap.ptr;
pcount = ap.pcount;
pmtx = ap.pmtx;
(*pcount)++;
}
template<typename T>
clx_shared_ptr<T>& clx_shared_ptr<T>::operator=(clx_shared_ptr<T>& ap) {
if (ap.ptr != ptr) {
std::unique_lock<std::mutex> ul(*pmtx);
if (*pcount == 1) {
ul.unlock();
this->~clx_shared_ptr();
ul.lock();
} else {
*pcount -= 1;
}
ptr = ap.ptr;
pcount = ap.pcount;
pmtx = ap.pmtx;
*pcount += 1;
}
return *this;
}
注意在析构函数中我们必须在最后才析构锁,因为不能在临界区内释放锁,因为后面还需要进行解锁操作,因此代码中截住了一个flag变量,通过flag变量判断解锁后是否需要释放锁资源
template<typename T>
clx_shared_ptr<T>::~clx_shared_ptr() {
std::unique_lock<std::mutex> ul(*pmtx);
int flag = false;
*pcount -= 1;
if (*pcount == 0) {
flag = true;
if (ptr != nullptr) {
std::cout << "delete : " << ptr << std::endl;
delete ptr; ptr = nullptr;
}
delete pcount; pcount = nullptr;
}
ul.unlock();
if (flag) {
ul.~unique_lock();
delete pmtx;
}
}
shared_ptr只需要保证引用计数的线程安全问题,而不需要保证管理的资源的线程安全问题,就像原生指针管理一块内存空降一样,原生指针只需要指向这块空间,而这块空间的线程安全问题应该由操作者来考虑
std::shared_ptr 定制删除器
当智能指针对象的生命周期结束时,都是默认以delete的方式将资源释放的,这是不太合适的,因为智能指针并不是只管理new方式申请到的内存空间,智能指针的管理也可能是new[]的方式申请到的空间,或者是管理一个文件指针所以我们需要其拥有new[] 和 delete[]的申请和释放方式
template<class T>
class clx_shared_ptr;
struct ListNode{
ListNode* prev;
ListNode* next;
int val;
~ListNode() { std::cout << "~ListNode() " << std::endl;}
};
int main() {
std::shared_ptr<ListNode> sp1(new ListNode[10]);
std::shared_ptr<FILE> sp2(fopen("test.cpp", "r"));
return 0;
}
// 输出
~ListNode()
samrt_point(33728,0x200fec280) malloc: *** error for object 0x600001b9c008: pointer being freed was not allocated
samrt_point(33728,0x200fec280) malloc: *** set a breakpoint in malloc_error_break to debug
当智能指针生命周期结束时,使用delete的方式释放管理的资源会导致程序崩溃,因为new[]方式申请的空间必须要以delete[]方式进行释放,而文件指针必须要通过调用fclose函数进行释放
这时候就需要定制删除起来控制资源的释放方式,C++标准库中的shared_ptr提供了如下构造函数
template<class U, class D>
shared_ptr(U* p, D del);
- p:需要让智能指针管理的资源
- del:删除器,这个删除器需要是一个可调用对象,如函数指针,仿函数,lambda表达式以及被包装器包装后的可调用对象
当shared_ptr对象的生命周期结束时就会调用传入的删除器完成资源的释放工作,调用该删除器会将shared_ptr 管理的资源当作参数传入
template<class T>
class clx_shared_ptr;
template<class T> // 定制的删除器
struct DelArr{
void operator()(const T* ptr) {
std::cout << "delete[]" << ptr << std::endl;
delete[] ptr;
}
};
struct ListNode{
ListNode* prev;
ListNode* next;
int val;
~ListNode() { std::cout << "~ListNode() " << std::endl;}
};
int main() {
std::shared_ptr<ListNode> sp1(new ListNode[10], DelArr<ListNode>());
std::shared_ptr<FILE> sp2(fopen("test.cpp", "r"), [](FILE* ptr) { // 使用lambda表达式
std::cout << "fclose: " << ptr << std::endl;
fclose(ptr);
});
return 0;
}
定制删除器的模拟实现
- C++标准库中实现shared_ptr时是分成了很多类的,因此C++标准库中可以将删除器的类型设置为构造函数的模版参数,让删除器的类型在各个类之间传递
- 如今我们使用一个类来模拟实现shared_ptr,不能将删除器的类型设置为构造函数的模版参数。因为删除器并非在构造函数中调用,而是要在析构函数中调用,所以我们需要一个成员变量将删除器保存下来,而定义这个成员变量时需要指定删除器的类型,因此这里模拟实现的时候不能将删除器的类型设置成构造函数的模版参数
- 所以我们只需要再给shared_ptr增加一个模版参数,在构造对象时制定删除器的类型,然后增加一个支持传入删除器的构造函数,在构造时将删除器保存下来,需要释放
template<typename T, typename D = Delete<T>>
class clx_shared_ptr {
public:
clx_shared_ptr(T* _ptr, D _del)
: ptr(_ptr), pcount(new int(1))
, pmtx(new std::mutex), del(_del){}
T& operator*() { return *ptr; };
T* operator->() { return ptr; };
~clx_shared_ptr();
clx_shared_ptr(clx_shared_ptr& ap);
clx_shared_ptr& operator=(clx_shared_ptr& ap);
int use_count() { return *pcount; };
private:
T* ptr;
int* pcount;
std::mutex* pmtx;
D del; // 管理资源的删除器
};
template<typename T, typename D>
clx_shared_ptr<T, D>::~clx_shared_ptr() {
std::unique_lock<std::mutex> ul(*pmtx);
int flag = false;
*pcount -= 1;
if (*pcount == 0) {
flag = true;
if (ptr != nullptr) {
std::cout << "delete : " << ptr << std::endl;
del(ptr); ptr = nullptr; // 使用定制删除器删除资源
}
delete pcount; pcount = nullptr;
}
ul.unlock();
if (flag) {
ul.~unique_lock();
delete pmtx;
}
}
这时我们实现的shared_ptr 就支持定制删除器了,但是使用起来没有C++标准库那么方便
#include "clx_shared_ptr.hpp"
#include <functional>
template<class T>
struct DelArr{
void operator()(const T* ptr) {
std::cout << "delete[]:" << ptr << std::endl;
delete[] ptr;
}
};
struct ListNode{
ListNode() {};
~ListNode() { std::cout << "~LinstNode" << std::endl; }
ListNode* prev;
ListNode* next;
int val;
};
int main() {
// 普通删除
clx_shared_ptr<ListNode> sp1(new ListNode);
// 仿函数删除
clx_shared_ptr<ListNode, DelArr<ListNode>> spl2(new ListNode[10], DelArr<ListNode>());
// lambda 表达式1
clx_shared_ptr<FILE, std::function<void(FILE*)>> sp3(fopen("test.cpp", "r"), [](FILE* ptr){
std::cout << "fclose : " << ptr << std::endl;
});
// lambda 表达式2
auto f = [](FILE* ptr) {
std::cout << "fclose: " << ptr << std::endl;
fclose(ptr);
};
clx_shared_ptr<FILE, decltype(f)> sp4(fopen("test.cpp", "r"), f);
return 0;
}
关于lambda表达式的使用
如果传入的删除器是一个lambda表达式,因为lambda表达式的类型不太容易获取到,所以我们可以使用包装器将其包装一下,让编译器传参时自动推演,也可以先用auto接受表达式,然后用decltype来声明删除器的类型
std::weak_ptr
Shared_ptr看似非常成功,但是任然存在循环引用问题
Shared_ptr的循环引用问题在特定的场景下才会产生。比如
struct s_ListNode{
std::shared_ptr<s_ListNode> next;
std::shared_ptr<s_ListNode> prev;
int val;
~s_ListNode() { std::cout << "~ListNode() " << std::endl; }
};
void test3() {
std::shared_ptr<s_ListNode> node1(new s_ListNode);
std::shared_ptr<s_ListNode> node2(new s_ListNode);
node1->next = node2;
node2->next = node1;
}
为了让节点在中途返回或者抛异常情况下未能被释放,我们将这两个节点交给shared_ptr进行管理,为了让链接节点的赋值操作能够执行,就需要将ListNode的next和prev两个成员变量的类型也改成智能指针的
运行这段代码发现两个节点都没有被释放,但如果去掉链接节点的两句代码中的任意一句,那么这两个节点就都正确释放了,根本原因是因为链接这两个节点导致了循环引用。
双向链接
node1和node2->next在管理第一块空间,node2和node1->next在管理第二块空间,两个的引用计数都为2,而每一个智能指针只会调用一次析构函数导致最终他们的引用计数还是1,导致本应该释放的空间没有释放
单向链接
Node2 和 node1->next 共同管理第二块空间,而只有node1在管理第一块空间,函数结束调用node1和node2的析构函数,node1引用计数由1变零调用析构清理资源,由于next和prev都是自定义类型就会调用他们自己的析构函数来析构,node1->next调用析构函数导致第一块空间的引用计数为1,node2再调用其的析构函数就能将空间全部释放了。这就是为什么只进行一个链接操作时两个节点都能够释放的原因
std::weack_ptr解决循环引用问题
Weak_ptr是C++11中引入的智能指针,weak_ptr不是用来管理资源的释放的,主要用来解决shared_ptr的循环引用问题
- weak_ptr支持用shared_ptr对象来构造weak_ptr对象,构造出来的weak_ptr和shared_ptr对象共同管理同一个资源,但不会增加这块资源的引用计数
将s_ListNode中的next和prev类型替换成weak_ptr就不会导致循环引用问题了,当node1和node2两个生命周期结束时引用计数都为零,进而释放两个节点的资源
#include <iostream>
#include <memory>
struct ListNode{
std::weak_ptr<ListNode> next;
std::weak_ptr<ListNode> prev;
int val;
~ListNode() {
std::cout << "~ListNode" << std::endl;
}
};
void test1 () {
std::shared_ptr<ListNode> node1(new ListNode);
std::shared_ptr<ListNode> node2(new ListNode);
node1->next = node2;
node2->prev = node1;
}
int main() {
test1();
return 0;
}
weak_ptr的模拟实现
1、weak_ptr支持无参构造
2、weak_ptr支持通过拷贝以及赋值获取shared_ptr的资源(shared_ptr会提供一个get函数用于获取其管理的资源)
#pragma once
#include <iostream>
#include <memory>
template<class T>
class clx_weak_ptr{
public:
clx_weak_ptr() : ptr(nullptr) {}
clx_weak_ptr(const std::shared_ptr<T>& sp) : ptr(sp.get()){}
clx_weak_ptr& operator=(const std::shared_ptr<T>& sp) {
ptr = sp.get();
return *this;
}
T& operator*() { return *ptr; }
T* operator->() { return ptr; }
private:
T* ptr;
};