文章目录
- 智能指针的原理
- auto_ptr和unique_ptr的模拟实现
- boost
- shared_ptr(重点)
- 拷贝赋值
- shared_ptr底层实现的代码
- 智能指针的使用
- 定制删除器
- shared_ptr
- unique_ptr
- 模拟实现定制删除器
- shared_ptr和weak_ptr
- shared_ptr循环引用问题(重点)
- weak_ptr
- shared_ptr的线程安全问题
- 内存泄漏
智能指针的原理
智能指针就像是两个指针管理同一块资源,智能指针可以解决多次析构的问题
auto_ptr和unique_ptr的模拟实现
1. RAII构造函数保存资源,析构函数释放资源
2.这两个智能指针不太重要
auto_ptr
namespace wbc
{
template<class T>
class auto_ptr
{
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& p)
:_ptr(p._ptr)
{
// 管理权的转移
p._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& p)
{
if (this != &p)
{
// 释放当前指针中的资源
if (_ptr)
delete _ptr;
// 转移资源,浅拷贝
_ptr = p._ptr;
p._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;
};
unique_ptr
template<class T>
class unique_ptr
{
public:
explicit unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "delete" << _ptr << endl;
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr(const unique_ptr<T>& p) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& p) = delete;
// 移动构造
unique_ptr(unique_ptr<T>&& p)
:_ptr(p._ptr)
{
p._ptr = nullptr;
}
unique_ptr<T>& operator=(unique_ptr<T>&& p)
{
delete _ptr;
_ptr = p._ptr;
p._ptr = nullptr;
}
private:
T* _ptr;
};
boost
Boost库是为C++语言标准库提供扩展的一些C++程序库的总称,C++11智能指针都是从boost库中出来的
1.C++ 98 中产了第⼀个智能指针auto_ptr
2.C++ boost给出了更实用的scoped_ptr/scoped_arrayshared_ptr/shared_array和weak_ptr等
3.C++ TR1,引入了shared_ptr等,不过注意的是TR1并不是标准版
4.C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。
shared_ptr(重点)
1. 重点要看看shared_ptr是如何设计的,尤其是引用计数的设计,主要这里一份资源就需要一个引用计数,所以引用计数才用静态成员的方式是无法实现的(静态的成员是所有对象所共享的)
2. 一块资源对应一块引用计数
3.要使用堆上动态开辟的方式,构造智能指针对象时来一份资源,就要new一个引用计数出来。多个shared_ptr指向资源时就++引用计数,shared_ptr对象析构时就–引用计数,引用计数减到0时代表当前析构的shared_ptr是最后一个管理资源的对象,则析构资源。
template<class T>
class shared_ptr
{
public:
// 构造引用计数初始化为1
shared_ptr(T* ptr)
:_ptr(ptr),
_count(new int(1))
{}
// 拷贝构造引用计数交给另一个指针管理
// 引用计数++
shared_ptr(const shared_ptr<T>& p)
:_ptr(p._ptr),
_count(p._count)
{
(*_count)++;
}
~shared_ptr()
{
if (--(*_count) == 0)
{
delete _count;
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _count;
};
int main()
{
wbc::shared_ptr<Date> p1(new Date);
wbc::shared_ptr<Date> p2(p1);
wbc::shared_ptr<Date> p3(p2);
// cout << p1.use_count << endl;
// p4和p1,p2,p3各自有一份自己的引用计数
wbc::shared_ptr<Date> p4(new Date);
return 0;
}
说明sp1,sp2,sp3浅拷贝,共同管理同一块资源
sp4管理另一份资源
拷贝赋值
1.拷贝赋值分两种情况:
1、有多个指针指向同一块资源,引用计数减减即可,另一份引用计数加加
2、只有一个指针指向同一块资源,引用计数减到0,销毁这块指针,然后指向新的资源,引用计数加加
2.自己给自己赋值的问题:
1、(p4 = p4)如果引用计数是1的话,减减为0,会delete,就产生了野指针
2、(p1 = p2,p1和p2指向同一块资源,对象的地址是不一样的),走一遍拷贝赋值相当于做无用功
应该比较同一块资源的地址或者是引用计数的地址
// 浅拷贝 + 引用计数
// p1 = p4
shared_ptr<T>& operator=(shared_ptr<T>& p)
{
if (_ptr != p._ptr)
{
if (--(*_count) == 0)
{
delete _count;
delete _ptr;
}
_ptr = p._ptr;
_count = p._count;
(*_count)++;
}
return *this;
}
// 赋值拷贝
p1 = p4;
p1 = p2;
shared_ptr底层实现的代码
template<class T>
class shared_ptr
{
public:
// 构造引用计数初始化为1
shared_ptr(T* ptr)
:_ptr(ptr),
_count(new int(1))
{}
// 拷贝构造引用计数交给另一个指针管理
// 引用计数++
shared_ptr(const shared_ptr<T>& p)
:_ptr(p._ptr),
_count(p._count)
{
(*_count)++;
}
~shared_ptr()
{
if (--(*_count) == 0)
{
delete _count;
delete _ptr;
}
}
// 浅拷贝 + 引用计数
// p1 = p4
shared_ptr<T>& operator=(shared_ptr<T>& p)
{
if (_ptr != p._ptr)
{
if (--(*_count) == 0)
{
delete _count;
delete _ptr;
}
_ptr = p._ptr;
_count = p._count;
(*_count)++;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _count;
};
int main()
{
wbc::shared_ptr<Date> p1(new Date);
// 拷贝构造
wbc::shared_ptr<Date> p2(p1);
wbc::shared_ptr<Date> p3(p2);
// cout << p1.use_count << endl;
// p4和p1,p2,p3各自有一份自己的引用计数
wbc::shared_ptr<Date> p4(new Date);
// 赋值拷贝
// p1 = p4;
p1 = p2;
return 0;
}
智能指针的使用
定制删除器
1.智能指针析构时默认是进行delete释放资源,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。
2. 因为new[]经常使用,所以为了简洁一点,unique_ptr和shared_ptr都特化了一份[]的版本,就可以管理new[]的资源了
3. 智能指针支持在构造时给一个删除器,所谓删除器本质就是一个可调用对象,这个可调用对象中实现你想要的释放资源的方式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源
int main()
{
// 智能指针底层都是new出来的,最后都是delete的
// 会出现new[] 和 delete不匹配的问题
std::shared_ptr<Date> p1(new Date);
std::shared_ptr<Date> p2(new Date[10]);
// Date[]会被识别为指针
std::shared_ptr<Date[]> p3(new Date[10]);
std::unique_ptr<Date[]> p4(new Date[10]);
return 0;
}
shared_ptr
这种文件指针没有特化,需要用删除器解决
shared_ptr的删除器
可调用对象可以是仿函数,lambda,函数指针
shared_ptr删除器建议用lambda
构造的时候传定制删除器
还是用lambda更方便,就不需要写仿函数了
删除器可以解决一切问题
// 仿函数
class Fclose
{
public:
void operator()(FILE* ptr)
{
cout << "fclose:" << ptr << endl;
fclose(ptr);
}
};
// 函数指针
template<class T>
void DeleteArrayFunc(T* ptr)
{
delete[] ptr;
}
int main()
{
// 仿函数
std::shared_ptr<FILE> p5(fopen("test.cpp", "r"), Fclose());
// 函数指针
// DeleteArrayFunc必须实例化,加上<Date>
std::shared_ptr<Date> p5(new Date[10], DeleteArrayFunc<Date>);
// lambda
std::shared_ptr<FILE> p5(fopen("test.cpp", "r"), [](FILE* ptr)
{
fclose(ptr);
});
std::shared_ptr<Date> p2(new Date[10], [](Date* ptr)
{delete[] ptr; });
return 0;
}
unique_ptr
类声明的时候传定制删除器
unique_ptr删除器建议用仿函数
class Fclose
{
public:
void operator()(FILE* ptr)
{
cout << "fclose:" << ptr << endl;
fclose(ptr);
}
};
// 仿函数
// 模版的位置要传类型
std::unique_ptr<FILE, Fclose> up1(fopen("test.cpp","r"));
// lambda
// decltype可以推导类型
auto fcloseFunc = [](FILE* ptr) {fclose(ptr); };
// 用lambda的uid要生成删除器的对象,生成不出来
// lambda禁用了默认构造,
// 所以后面参数要传对象去拷贝构造
// 这样才可以
std::unique_ptr<FILE, decltype(fcloseFunc)> up3(fopen("test.cpp", "r"),fcloseFunc);
// 函数指针
unique_ptr<Date, void(*)(Date*)> up3(new Date[10], DeleteArrayFunc<Date>);
1. shared_ptr 除了支持用指向资源的指针构造,还支持 make_shared 用初始化资源对象的值
直接构造。
2. 了解一下即可make_shared是模版要传类型实例化,make_shared比shared_ptr更加高效一些,make_shared开一次空间,shared_ptr开两次空间,因为创建一个shared_ptr就需要一个引用计数,非常多个shared_ptr就会产生多个引用计数的碎片,影响效率,而make_shared会把资源和引用计数开到一起,就跟之前的new那里的引用计数一样开到资源的前面
shared_ptr<Date> sp3(new Date(2024, 1, 5));
shared_ptr<Date> sp4 = make_shared<Date>(2024, 1, 5);
3. shared_ptr 和 unique_ptr 都支持了operator bool的类型转换(自定义类型转为内置类型),如果智能指针对象是一个空对象没有管理资源,则返回false,否则返回true,意味我们可以直接把智能指针对象给if判断是否为空。
int main()
{
std::shared_ptr<Date> sp1(new Date);
std::shared_ptr<Date> sp2(new Date[10], [](Date* ptr) {delete[] ptr; });
shared_ptr<Date> sp3(new Date(2024, 1, 5));
shared_ptr<Date> sp4 = make_shared<Date>(2024, 1, 5);
shared_ptr<Date> sp5;
// if(sp1.operator bool())
if (sp1)
{
cout << "sp1 is not nullptr" << endl;
}
// if(!sp5.operator bool())
if (!sp5)
{
cout << "sp5 is nullptr" << endl;
}
return 0;
}
- shared_ptr 和 unique_ptr 都得构造函数都使用explicit 修饰(不支持隐式类型转化),防止普通指针隐式类型转换成智能指针对象。
// Date是Date*隐式类型转化为临时对象,临时对象拷贝构造sp6
// shared_ptr<Date> sp6 = new Date(2025,1,5);
// unique_ptr<Date> sp7 = new Date(2025, 1, 5);
模拟实现定制删除器
#include<functional>
#include<memory>
template<class T>
class shared_ptr
{
public:
// 构造引用计数初始化为1
// 没传定制删除器会走默认的function
// 所以new的这个要写默认的function
shared_ptr(T* ptr)
:_ptr(ptr),
_count(new int(1))
{}
template<class D>
shared_ptr(T* ptr,D del)
:_ptr(ptr),
_count(new int(1)),
_del(del)
{}
// 拷贝构造引用计数交给另一个指针管理
// 引用计数++
shared_ptr(const shared_ptr<T>& p)
:_ptr(p._ptr),
_count(p._count)
{
(*_count)++;
}
~shared_ptr()
{
if (--(*_count) == 0)
{
delete _count;
_del(_ptr);
// delete _ptr;
}
}
// 浅拷贝 + 引用计数
// p1 = p4
shared_ptr<T>& operator=(shared_ptr<T>& p)
{
if (_ptr != p._ptr)
{
if (--(*_count) == 0)
{
delete _count;
delete _ptr;
}
_ptr = p._ptr;
_count = p._count;
(*_count)++;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _count;
function<void(T*)> _del = [](T* p) {delete[] p; };
};
shared_ptr和weak_ptr
shared_ptr循环引用问题(重点)
1. shared_ptr大多数情况下管理资源非常合适,支持RAII,也支持拷贝。但是在循环引用的场景下会导致资源没得到释放内存泄漏,所以我们要认识循环引用的场景和资源没释放的原因,并且学会使用weak_ptr解决这种问题。
极端场景会出现循环引用问题
struct ListNode
{
int _data;
ListNode* _next;
ListNode* _prev;
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
// 循环引用 -- 内存泄露
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
n1->_next = n2;
n2->_prev = n1;
return 0;
}
n1和n2析构后,n1和n2的引用计数减到1,会出现循环引用的问题
使用weak_ptr解决了循环引用的问题
struct ListNode
{
int _data;
// ListNode* _next;
// ListNode* _prev;
// std::shared_ptr<ListNode> _next;
// std::shared_ptr<ListNode> _prev;
// 这里改成weak_ptr,当n1->_next = n2;绑定shared_ptr时
// 不增加n2的引用计数,不参与资源释放的管理,就不会形成循环引用了
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
// 循环引用 -- 内存泄露
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
// use_count打印引用计数的个数
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
return 0;
}
weak_ptr
1. weak_ptr不支持RAII,也不支持访问资源,所以我们看问档发现weak_ptr构造时不支持绑定到资源,只支持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引用计数,那么就可以解决上述的循环引用问题。
2. 问题:weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的shared_ptr已经释放了资源,那么他去访问资源就是很危险的。
3. 解决方法:weak_ptr支持expired检查指向的资源是否过期,use_count也可获取shared_ptr的引用计数,weak_ptr想访问资源时,可以调用lock返回一个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是一个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。
weak_ptr解决悬空和野指针的问题,遇到了循环引用的时候
用引用计数检查shared_ptr资源是否已经被释放了,wp1指向的引用计数减为0,但是不释放引用计数
expired
use_count == 0 返回true 过期
use_count != 0 返回false 没有过期
int main()
{
std::shared_ptr<string> sp1(new string("111111"));
std::shared_ptr<string> sp2(sp1);
std::weak_ptr<string> wp = sp1;
// expired检查资源没有过期返回false
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
// sp1和sp2都指向了其他资源,则weak_ptr就过期了
sp1 = make_shared<string>("222222");
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
sp2 = make_shared<string>("333333");
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
return 0;
}
weak_ptr的底层:
如果有weak_ptr的话,引用计数减到0,还会继续套一层引用计数;
引用计数减到0,如果没有weap_ptr就直接释放引用计数
lock:锁住资源即使是引用计数减到0后,weak_ptr还要保住这份资源
使用一个sp3自立一个大哥锁住这份资源,即使是sp1不指向这块资源了
wp = sp1;
//std::shared_ptr<string> sp3 = wp.lock();
auto sp3 = wp.lock();
cout << wp.expired() << endl;// 0
cout << wp.use_count() << endl;// 2
sp1 = make_shared<string>("55555");
cout << wp.expired() << endl;// 0
cout << wp.use_count() << endl;// 1
shared_ptr的线程安全问题
1. 引用计数++,加多了,然后减少了,就会没有析构
2. 引用计数- -,减多了,减到了负数,就会提前析构
3. 引用计数加多了,减少了(引用计数不是0)就需要使用锁
解决上述问题有两种方法:
可以解决同时加的问题,可以让线程一个加完了另外一个再加,按顺序地加
- 用锁的方法,这个要学了多线程后更好理解
- 用atomic< int >* _count; 原子操作
1. shared_ptr的引用计数对象在堆上,如果多个shared_ptr对象在多个线程中,进行shared_ptr的拷贝析构时会访问修改引用计数,就会存在线程安全问题,所以shared_ptr引用计数是需要加锁或者原子操作保证线程安全的。
2.shared_ptr指向的对象也是有线程安全的问题的,但是这个对象的线程安全问题不归shared_ptr管,它也管不了,应该有外层使用shared_ptr的人进行线程安全的控制。
3. 下面的程序会崩溃或者AA资源没释放,wbc::shared_ptr引用计数从int*改成atomic< int >*就可以保证引用计数的线程安全问题,或者使用互斥锁加锁也可以。
智能指针本身是线程安全的,指向的资源不是线程安全的
#include<iostream>
#include<functional>
#include<memory>
#include<atomic>
#include<mutex>
#include<thread>
namespace wbc
{
template<class T>
class shared_ptr
{
public:
// 构造引用计数初始化为1
// 没传定制删除器会走默认的function
// 所以new的这个要写默认的function
shared_ptr(T* ptr)
:_ptr(ptr),
_pcount(new atomic<int>(1))
{
}
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr),
_pcount(new atomic<int>(1)),
_del(del)
{
}
// 拷贝构造引用计数交给另一个指针管理
// 引用计数++
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr),
_pcount(sp._pcount)
{
(*_pcount)++;
}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
_del(_ptr);
delete _pcount;
// delete _ptr;
}
}
// 浅拷贝 + 引用计数
// p1 = p4
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_pcount = sp._pcount;
_ptr = sp._ptr;
++(*_pcount);
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int use_count()
{
return *_pcount;
}
private:
T* _ptr;
// int* _count;
atomic<int>* _pcount;// 原子操作
function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
}
struct AA
{
int _a1 = 0;
int _a2 = 0;
~AA()
{
cout << "~AA()" << endl;
}
};
int main()
{
wbc::shared_ptr<AA> p(new AA);
const size_t n = 100000;
mutex mtx;
auto func = [&]()
{
for (size_t i = 0; i < n; ++i)
{
// 这里智能指针拷贝会++计数
wbc::shared_ptr<AA> copy(p);
/*{
unique_lock<mutex> lk(mtx);
copy->_a1++;
copy->_a2++;
}*/
}
};
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << p->_a1 << endl;
cout << p->_a2 << endl;
cout << p.use_count() << endl;
return 0;
}
内存泄漏
1. 什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存,一般是忘记释放或者发生异常释放程序未能执行导致的。
2. 内存泄漏的危害:普通程序运行一会就结束了出现内存泄漏问题也不大,进程正常结束,页表的映射关系解除,物理内存也可以释放。
服务器等内存泄漏会导致内存不断变少,导致卡死。
int main()
{
// 申请⼀个1G未释放,这个程序多次运⾏也没啥危害
// 因为程序⻢上就结束,进程结束各种资源也就回收了
char* ptr = new char[1024 * 1024 * 1024];
cout << (void*)ptr << endl;
return 0;
}
解决方法:
1.尽量使用智能指针来管理资源,如果自己场景比较特殊,采用RAII思想自己造个轮子管理。
2. 定期使用内存泄漏工具检测,尤其是每次项目快上线前,不过有些工具不够靠谱,或者是收费
3.总结一下:
内存泄漏非常常见,解决方案分为两种:
1、事前预防型,如智能指针等。(公司比较倾向于第一种)
2、事后查错型,如泄漏检测工具。