【智能指针】
1、为什么要使用智能指针???
int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}
void Func()
{// 1、如果p1这里new 抛异常会如何?// 2、如果p2这里new 抛异常会如何?// 3、如果div调用这里又会抛异常会如何?int* p1 = new int;int* p2 = new int;cout << div() << endl;delete p1;delete p2;
}
int main()
{try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}
p1分配后,p2分配时抛异常,p1内存泄漏
p1、p2分配后,div函数调用抛异常,p1、p2均内存泄漏
内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内
存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。内存泄漏分类:
1、堆内存泄漏:堆内存是指必须使用malloc,calloc,new,realloc等从堆中分配的内存,用完之后必须调用free/delete释放,不然就会造成堆内存泄漏。
2、系统资源泄漏:程序使用系统分配的资源,如套接字、管道、文件描述符如果没有使用对应的函数释放,就会导致系统资源浪费,严重可导致系统效能减少,系统执行不稳定。
2、智能指针的使用及原理
2.1、RAII
利用对象生命周期来控制资源(内存、文件句柄、网络连接、互斥量)的简单技术
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在 对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做 法有两大好处:不需要显式地释放资源。采用这种方式,对象所需的资源在其生命期内始终保持有效。
template<class T>
class SmartPtr {
public:SmartPtr(T* ptr = nullptr): _ptr(ptr){}~SmartPtr(){if(_ptr)delete _ptr;}private:T* _ptr;
};
int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}
void Func()
{ShardPtr<int> sp1(new int);ShardPtr<int> sp2(new int);cout << div() << endl;
}
int main()
{try {Func();}catch(const exception& e){cout<<e.what()<<endl;}return 0;
}
- 如果
div()
抛出异常,sp1
和sp2
会自动调用析构函数释放内存,不会泄漏。指针可以解引用,也可 以通过->去访问所指空间中的内容,因此, 将* 、 -> 重载下,才可让其 像指针一样去使用 。
template<class T>
class SmartPtr {
public:SmartPtr(T* ptr = nullptr) : _ptr(ptr) {}~SmartPtr() {if(_ptr)delete _ptr;}T& operator*() { return *_ptr; } // 解引用运算符重载T* operator->() { return _ptr; } // 箭头运算符重载
private:T* _ptr;
};
struct Date {int _year;int _month;int _day;
};
int main() {SmartPtr<int> sp1(new int);*sp1 = 10; // 通过解引用运算符赋值cout << *sp1 << endl; // 输出: 10SmartPtr<Date> sparray(new Date); sparray->_year = 2018; // 等价于 (sparray.operator->())->_year = 2018;sparray->_month = 1;sparray->_day = 1;
}
sp1管理一个整数,通过*sp1赋值和解引用
sparray管理一个Date对象
语法糖:sparray->_year其实是 (sparray.operator->())->_year函数的简写
编译器会自动展开
->
运算符,直到返回一个普通指针
总结一下智能指针的原理:1. RAII特性2. 重载operator*和opertaor->,具有像指针一样的行为。
2.2、std::unique_ptr
-----简单粗暴的防拷贝。智能指针是一种用于管理动态分配内存的工具,其中unique_ptr
是实现独占资源所有权的智能指针。
1、unique_ptr确保在任何时候都只有一个智能指针实例可以拥有对特定资源(如动态分配的内存)的所有权。例如,当创建一个
unique_ptr
来管理一个动态分配的int
对象时,这个unique_ptr
就是该int
对象的唯一所有者.2、sp1在此处被销毁,其所管理的资源被自动释放
std::unique_ptr<int> sp1(new int(5));
3、unique_ptr
禁用了拷贝构造函数和拷贝赋值运算符。因为不能通过拷贝构造函数创建sp2
来共享sp1
所管理的资源。std::unique_ptr<int> sp1(new int(5));
// std::unique_ptr<int> sp2(sp1); // 编译错误,拷贝构造被禁用
namespace bit {template<typename T>class unique_ptr {public:// 构造函数:接收原始指针并管理其生命周期explicit unique_ptr(T* ptr = nullptr) : _ptr(ptr) {}// 析构函数:自动释放资源~unique_ptr() {if (_ptr) {std::cout << "delete: " << _ptr << std::endl;delete _ptr;}}// 禁用拷贝构造和赋值,确保独占性unique_ptr(const unique_ptr&) = delete;unique_ptr& operator=(const unique_ptr&) = delete;// 指针运算符重载T& operator*() const { return *_ptr; }T* operator->() const { return _ptr; }private:T* _ptr; // 管理的原始指针};
}// 测试代码(已注释)
/*
int main() {// bit::unique_ptr<int> sp1(new int);// bit::unique_ptr<int> sp2(sp1); // 编译错误:拷贝构造被禁用std::unique_ptr<int> sp1(new int);// std::unique_ptr<int> sp2(sp1); // 同样错误:标准库的unique_ptr也禁用拷贝return 0;
}
*/
为什么要使用explicit防止隐式类型转换???
void func(unique_ptr<int> ptr) {// 使用ptr管理资源
}int main() {func(new int(42)); // 隐式转换:int* → unique_ptr<int>
}
unique_ptr智能指针的核心是独占资源所有权。若允许隐式转换,可能意外地将同一原始指针交给多个智能指针管理,导致双重释放
func(new int(42)); // 错误:无法隐式转换
func(unique_ptr<int>(new int(42))); // 合法:显式调用构造函数
强制开发者显式地将原始指针交给智能指针管理,减少内存泄漏和双重释放的风险
std::unique_ptr<int> p = new int(42); // 错误:不允许隐式转换
std::unique_ptr<int> p(new int(42)); // 正确:显式构造
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;防止拷贝和赋值:
每个类创建出来之后,编译器默认生成以下成员函数:拷贝构造函数、拷贝赋值运算符。
拷贝构造函数:用已经创建出的对象去构造新的对象
class MyClass { public:MyClass(int value):data(value){}MyClass(const MyClass& other):data(other.data)//拷贝构造函数 用已存在的类对象初始化新的类对象{} private:int data; }
拷贝赋值运算符:将一个对象赋值给一个已存在的对象,需要重载operator=
class MyClass { public:MyClass(int value) : data(value) {}// 拷贝赋值运算符MyClass& operator=(const MyClass& other) {if (this != &other) {data = other.data;}return *this;}private:int data; };
= delete
是 C++11 引入的特性,用于显式删除某个特殊成员函数。当一个函数被标记为delete
后:
- 任何尝试调用该函数的代码都会导致编译错误。
- 编译器不会再隐式生成该函数。
1、unique_ptr<int> a(new int(42));
unique_ptr<int> b(a); // 错误:拷贝构造函数被删除
2、unique_ptr<int> a(new int(42));
unique_ptr<int> b(new int(10));
b = a; // 错误:拷贝赋值运算符被删除
防止多个
unique_ptr
指向同一资源,避免双重释放
2.3、std::shared_ptr
原理:通过引用计数的方式实现多个shared_ptr之间共享资源。
1、在shared_ptr内部,每个资源都维护着一份计数,用来记录这份资源被几个对象共享。
2、在对象被销毁的时候(也就是调用析构函数的时候),就说明这个对象不使用这份资源了,对象的引用计数减一。
3、如果引用计数是0,就说明自己是最后一个使用这份资源的对象,就必须释放资源。
4、如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
每多一个
shared_ptr
指向某个对象,该对象的引用计数 +1。当引用计数 归零 时,对象才会被销毁。
支持多个拷贝管理同一份资源。
#include <iostream>
#include <mutex>namespace bit {template<class T>class shared_ptr {public:// 构造函数,初始化指针、引用计数和互斥锁shared_ptr(T* ptr = nullptr) : _ptr(ptr), _pRefCount(new int(1)), _pmtx(new std::mutex) {}// 拷贝构造函数,增加引用计数并复制相关指针shared_ptr(const shared_ptr<T>& sp) : _ptr(sp._ptr), _pRefCount(sp._pRefCount), _pmtx(sp._pmtx) {AddRef();}// 释放资源的函数,处理引用计数和资源释放void Release() {_pmtx->lock();bool flag = false;if (--(*_pRefCount) == 0 && _ptr) {std::cout << "delete:" << _ptr << std::endl;delete _ptr;delete _pRefCount;flag = true;}_pmtx->unlock();if (flag == true) {delete _pmtx;}}// 增加引用计数的函数,加锁保护void AddRef() {_pmtx->lock();++(*_pRefCount);_pmtx->unlock();}// 拷贝赋值运算符,处理资源释放、指针复制和引用计数增加shared_ptr<T>& operator=(const shared_ptr<T>& sp) {//if (this != &sp)if (_ptr != sp._ptr) {Release();_ptr = sp._ptr;_pRefCount = sp._pRefCount;_pmtx = sp._pmtx;AddRef();}return *this;}// 获取引用计数的函数int use_count() {return *_pRefCount;}// 析构函数,调用Release释放资源~shared_ptr() {Release();}// 重载解引用运算符,返回资源的引用T& operator*() {return *_ptr;}// 重载箭头运算符,返回资源的指针T* operator->() {return _ptr;}// 获取原始指针的函数T* get() const {return _ptr;}private:T* _ptr; //指向管理的资源int* _pRefCount;//引用计数,有多少个shared_ptr共享同一份资源std::mutex* _pmtx;//指向互斥锁,保护引用计数的线程安全};// 简化版本的weak_ptr实现template<class T>class weak_ptr {public:// 默认构造函数,初始化指针为nullptrweak_ptr() : _ptr(nullptr) {}// 从shared_ptr构造weak_ptr,复制指针weak_ptr(const shared_ptr<T>& sp) : _ptr(sp.get()) {}// 赋值运算符,从shared_ptr赋值指针weak_ptr<T>& operator=(const shared_ptr<T>& sp) {_ptr = sp.get();return *this;}// 重载解引用运算符,返回资源的引用T& operator*() {return *_ptr;}// 重载箭头运算符,返回资源的指针T* operator->() {return _ptr;}private:T* _ptr;};
}// 主函数,用于测试shared_ptr和weak_ptr(当前注释掉了测试代码)
int main() {bit::shared_ptr<int> sp1(new int);//创建一个shared_ptr对象sp1,来管理动态分配的int对象//new int 动态分配了一个int类型的对象,shared_ptr的构造函数接受了该指针,初始化为sp1bit::shared_ptr<int> sp2(sp1);//使用sp1构造sp2,使用拷贝构造函数从sp1复制。这会是sp2也指向sp1所管理的资源,同时引用计数+1,现在该资源的引用计数为2,sp1和sp2共享该资源的所有权。bit::shared_ptr<int> sp3(sp1);//此时该资源的引用计数变为 3,sp1、sp2 和 sp3 都共享对该资源的所有权bit::shared_ptr<int> sp4(new int);bit::shared_ptr<int> sp5(sp4);//这会使 sp5 指向 sp4 所管理的资源,引用计数加 1,//现在该资源的引用计数为 2,sp4 和 sp5 共享对该资源的所有权sp1 = sp1;sp1 = sp2;sp1 = sp4;sp2 = sp4;//尝试将 sp4 的值赋给 sp2。调用拷贝赋值运算符,//sp2 会释放原来管理的资源(如果引用计数减为 0),然后 sp2 开始管理 sp4 所管理的资源,引用计数相应调整。sp3 = sp4;*sp1 = 2;//对 sp1 所管理的资源进行解引用并赋值为 2。//由于 sp1 是 shared_ptr,*sp1 会返回所管理资源的引用,这里将该资源的值设置为 2*sp2 = 3;//对 sp2 所管理的资源进行解引用并赋值为 3。//因为 sp2 和 sp1 共享同一资源(之前通过拷贝构造),这里赋值操作会改变共享资源的值。return 0;
}
shared_ptr的循环引用:
#include <iostream>
#include <memory>// 定义链表节点结构体
struct ListNode {int _data;std::shared_ptr<ListNode> _prev;std::shared_ptr<ListNode> _next;~ListNode() {std::cout << "~ListNode()" << std::endl;}
};int main() {// 创建两个shared_ptr<ListNode>对象,每个对象管理一个ListNode对象std::shared_ptr<ListNode> node1(new ListNode);std::shared_ptr<ListNode> node2(new ListNode);// 输出node1的引用计数std::cout << node1.use_count() << endl;// 输出node2的引用计数std::cout << node2.use_count() << endl;// 将node1的_next指针指向node2node1->_next = node2;// 将node2的_prev指针指向node1node2->_prev = node1;// 输出node1的引用计数std::cout << node1.use_count() << endl;// 输出node2的引用计数std::cout << node2.use_count() << endl;return 0;
}
当资源对应的引用计数减为0时对应的资源才会被释放,因此资源1的释放取决于资源2当中的prev成员,而资源2的释放取决于资源1当中的next成员。而资源1当中的next成员的释放又取决于资源1,资源2当中的prev成员的释放又取决于资源2,于是这就变成了一个死循环,最终导致资源无法释放。
node1和node2是shared_ptr的实例。
1、现在main函数刚开始,node1和node2分别有一个shared_ptr指向,所有node1和node2的引用计数都是1.
2、node1->_next = node2; node1->next现在也持有node2。node2的引用计数+1
node2->_prev = node1;
node2
->next 也持有 node1,node1的引用计数+13、main函数结束之后,node1和node2是局部变量,出作用域自动析构,(shared_ptr析构),但是不会释放资源(shared_ptr析构不等于对象析构),只有
shared_ptr
的析构逻辑运行了(减少引用计数),由于引用计数未归零,~ListNode()
并未执行(所以没有输出~ListNode()
)对象析构只有在计数器为0时才会被调用,因为此时计数器不为0,所有只是计数器减一,现在计数器为1。循环引用导致引用计数无法归0,它们的内存 永远不会被释放(内存泄漏)
引用计数是记录有多少个shared_ptr实例指向同一个对象。
使用weak_ptr就不会造成内存泄漏:
struct ListNode {int _data;std::weak_ptr<ListNode> _prev; // 改为 weak_ptrstd::shared_ptr<ListNode> _next;~ListNode() { std::cout << "~ListNode()" << std::endl; }
};int main() {auto node1 = std::make_shared<ListNode>();//node1:1auto node2 = std::make_shared<ListNode>();//node2:1node1->_next = node2;//node2:2node2->_prev = node1; // weak_ptr 不增加引用计数 //node1:1// main 结束时:// 1. node1 析构,引用计数归零,销毁 node1// 2. node1->_next 释放(node1释放之后,没有指向node2的指针了,加上shared_ptr出作用域之后析构,node2 的引用计数 -1(归零),销毁 node2return 0;
}