【C++篇】智能指针
目录
智能指针的概念及使用
1,RAII和智能指针
2,C++标准库中的智能指针
5,weak_ptr
智能指针的概念及使用
智能指针是C++11引入的自动化内存管理工具,通过RAII(资源获取立即初始化)机制自动释放内存,避免内存泄漏和指针悬空的问题。
1,RAII和智能指针
- RAII是Resource Acquisition Is Initialization的缩写,它是一种管理资源的类的设计思想。本质是一种利用对象生命周期来管理获取到的动态资源,避免资源泄漏。这里的资源可以是内存,文件,网络链接,互斥锁等。
- RAII在获取资源时把资源委托给一个对象,接着控制对资源的访问, 资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。
- 智能指针除了满足RAII的设计思路,还要方便资源的访问。所以智能指针类还会像迭代器类一样重载一些operator* / operator-> /operator[ ]等运算符,方便访问资源。
template<class T>
class SmartPtr
{
public:
//RAII
SmartPtr(T* ptr)
; _ptr(ptr)
{}
~SmartPtr()
{
cout << "delete[]" << _ptr << endl;
delete[] ptr;
}//重载运算符,模拟指针行为,方便资源的访问
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t i)
{
return _ptr[i];
}private:
T* _ptr;
};int main()
{
SmartPtr<int> sp1 = new int[10];
SmartPtr<pair<int, int>> sp2 = new pair<int, int>[10];sp1[5] = 10;
sp2->first = 1;
sp2->second = 2;
return 0;
}
2,C++标准库中的智能指针
- C++标准库中的智能指针都在<memory>这个头文件下面。
- auto_ptr是C++98设计出来的智能指针,它的特点是拷贝时把 拷贝对象的资源的管理权转移,这会导致 被拷贝对象悬空,访问报错的问题。该智能指针已废弃,用 unqiue_ptr取代。
#include <memory>
class Date
{
public:
Date() = default;
Date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)
{}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//本质是想实现ap1和ap2共同管理一份Date资源
auto_ptr<Date> ap1(new Date);
//拷贝时,管理权转移,被拷贝对象ap1悬空 访问会出错
auto_ptr<Date> ap2(ap1);
return 0;
}
- unique_ptr是C++11设计出来的智能指针,它的特点是不支持拷贝,只支持移动。
unique_ptr<Date> up1(new Date);
//不支持拷贝
//unique_ptr<Date> up2(up1);
//支持移动
unique_ptr<Date> up3(move(up1));
- shared_ptr是C++11设计出来的智能指针,是一个共享指针。它的特点是支持拷贝,也支持移动。如果需要拷贝可以使用 shared_ptr。底层使用引用计数的方式实现的。
引用计数是一种内存管理技术,通过跟踪对象的引用次数来决定何时释放其占用的资源。当引用计数降为0时,对象会被自动销毁。
计数器:每个对象关联一个整数计数器,记录当前有多少引用指向该对象。
计数增减:
引用创建(如复制指针、赋值) → 计数+1。
引用销毁(如指针离开作用域、被重置) → 计数-1。
资源释放:当计数归零时,立即释放对象内存或资源。
shared_ptr<Date> sp1(new Date);
//拷贝
shared_ptr<Date> sp2(sp1);
//移动
shared_ptr<Date> sp3(move(sp1));
智能指针析构时,默认使用delete释放资源,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。示例:
int main()
{shared_ptr<Date> sp(new Date[10]); //err
return 0;
}
上述代码运行会崩溃,对于这种情况,shared_ptr和unique_ptr都特化了一个[ ]版本。
shared_ptr<Date[ ]> sp(new Date[ ]) ; unique_ptr<Date[ ]> up(new Date[ ])。
这样就可以解决new[ ]的问题了。
但是这只是解决了new和new[ ]的问题,还有其他无法删除的情况。为了解决这个问题,智能指针支持在构造时给一个删除器,删除器本质是一个可调用 对象,比如仿函数,lambda表达式,函数指针等等。这个 可调用对象实现你想要释放资源的方式,当构造智能指针时,给了定制的删除器,在智能指针析构时,就会调用删除其释放资源。
库中shared_ptr的构造:
使用:
//仿函数做删除器
template<class T>
class DeleteArray
{
public:
void operator()(T* ptr)
{
delete[ ] ptr;
ptr = nullptr;
}
};
class Fclose
{
public:
void operator()(FILE* ptr)
{
fclose(ptr);
}
};
//函数指针做删除器
template<class T>
void DeleteArrayFunc(T* ptr)
{
delete[ ] ptr;
}int main()
{
//函数指针做删除器
shared_ptr<Date> sp1(new Date[10], DeleteArrayFunc<Date>);//lambda表达式做删除器
auto del = [ ](Date* ptr) {delete[] ptr; };
shared_ptr<Date> sp2(new Date[10], del);//仿函数做删除器
shared_ptr<FILE> sp3(fopen("test.cpp","r"), Fclose());
//lambda
shared_ptr<FILE> sp4(fopen("test.cpp", "r"), [ ](FILE* ptr) {fclose(ptr); });
return 0;
}
- weak_ptr 是C++11设计出来的智能指针,与上面几个智能指针不同,它不支持RAII,也就意味着他不能直接用来管理资源。它的作用是为了解决shared_ptr的一个循环引用导致内存泄漏的问题。(循环引用在下面讲解)
3,智能指针的原理及模拟实现(shared_ptr)
shared_ptr要想支持拷贝,也就是几个对象共同管理一份资源,需要用到引用计数。
引用计数是一种内存管理技术,通过跟踪对象的引用次数来决定何时释放其占用的资源。当引用计数降为0时,对象会被自动销毁。
计数器:每个对象关联一个整数计数器,记录当前有多少引用指向该对象。
计数增减:
引用创建(如复制指针、赋值) → 计数+1。
引用销毁(如指针离开作用域、被重置) → 计数-1。
资源释放:当计数归零时,立即释放对象内存或资源。
如果我们用count来记录一份资源被多少个对象管理,那么常见的是将count设为静态成员函数,新增一个管理对象count就++。如果这样设计,会造成下面的问题:
sp1和sp2共同管理资源1,引用计数为2,当定义一个新的对象sp3管理资源2时,引用计数会错误的为3。 所以这种方式是不行的。要使用堆上动态开辟的方式,构造智能指针对象时,就new一个引用计数。多个shared_ptr指向同一个资源时,++引用计数,析构时--引用计数,引用计数减为0,代表当前析构的shared_ptr是最后一个管理资源的对象,则析构资源。
以管理Date类为例:
class Date
{
public:
Date() = default;
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};template<class T>
class shared_ptr
{
public:
//构造函数
shared_ptr(T* ptr=nullptr)
:_ptr(ptr)
,_pcount(new int(1))
{}
//......//......
private:
T* ptr;
int* pcount;//引用计数器
};
shared_ptr拷贝构造的实现:
//拷贝构造
shared_ptr(const shared_ptr& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
(*_pcount)++; //引用计数++
}
shared_ptr赋值运算符的重载:
//赋值 sp1=sp2
shared_ptr<T>& operator=(shared_ptr<T>& sp)
{
//1,防止自己给自己赋值,//2,如果sp1和sp2管理同一份资源,也没必要赋值
if (_ptr != sp._ptr)
{
//sp1之前管理的资源引用计数--,如果等于0,就释放之前的资源
if (--(*_pcount) == 0)
{
delete _ptr;
delete _Pcount;
}
_pcount = sp.pcount;
_ptr = sp._ptr;
++(*_pcount);
}
return *this;
}
定制删除器的实现:
这里和标准库库里的实现保持一值,在构造时传一个删除器。在类中定义删除器的时候,因为我们无法判断它的类型,所以无法定义,我们可以用一个包装器function来包装这个删除器。
function<(void)T*> _del = [](T* ptr) {delete ptr; }; //不传删除器时,默认为delete
//含删除器的构造
shared_ptr(T* ptr,D del)
:_ptr(ptr)
, _pcount(new int(1))
,_del(del)
{}
shared_ptr模拟实现代码:
template<class T>
class shared_ptr
{
public:
//构造函数
shared_ptr(T* ptr=nullptr)
:_ptr(ptr)
,_pcount(new int(1))
{}
//含删除器的构造
shared_ptr(T* ptr,D del)
:_ptr(ptr)
, _pcount(new int(1))
,_del(del)
{}
//拷贝构造
shared_ptr(const shared_ptr& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
,_del(sp._del)
{
(*_pcount)++;
}
void release()
{
if (--(*_pcount) == 0)
{
//delete _ptr;
_del _ptr;
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
//赋值 sp1=sp2
shared_ptr<T>& operator=(shared_ptr<T>& sp)
{
//防止自己给自己赋值
if (_ptr != sp._ptr)
{
release();
_pcount = sp.pcount;
_ptr = sp._ptr;
++(*_pcount);
}
return *this;
}
//析构函数
~shared_ptr()
{
release();
}
int use_count()
{
return *_pcount;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;//引用计数器
function<void(T*)> _del = [](T* ptr) {delete ptr; }; //删除器
};
4,shared_ptr循环引用问题
shared_ptr大多数情况管理资源非常合适,支持RAII,也支持拷贝。但在循环引用场景下会导致内存泄漏。如下情节:
我们想实现一个链表结构,由shared_ptr管理。
struct ListNode
{
int _data;
ListNode* next;
ListNode* prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};int main()
{
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);//n1->next = n2; //err
return 0;
}
上面的写法无法形成链表,因为n2是shared_ptr类型的,而n1->next是ListNode* 类型的,不能赋值。需要改成shared_ptr<ListNode> prev; shared_ptr<ListNode> next;
struct ListNode
{
int _data;
shared_ptr<ListNode> next;
shared_ptr<ListNode> prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};int main()
{
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);n1->next = n2;
n2->prev = n1;return 0;
}
程序结束,析构时:
运行结果:
没有调用析构,节点资源没有释放。导致内存泄漏的问题。
解决办法,使用weak_ptr:
weak_ptr支持shared_ptr参数的构造,而且weak_ptr是不支持资源管理的,不支持RAII。所以用weak_ptr指向节点时,是不会增加 引用计数的。
struct ListNode
{
int _data;
weak_ptr<ListNode> next;
weak_ptr<ListNode> prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};int main()
{
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);n1->next = n2;
n2->prev = n1;return 0;
}
5,weak_ptr
weak_ptr不支持资源管理,如果它绑定的shared_ptr已经释放资源了,那么它去访问资源是很危险的。
weak_ptr支持expired检查指向的资源是否过期,use_count也可以获取shared_ptr的引用计数。
weaak_ptr想访问资源时,可以调用lock函数返回一个管理资源的,如果资源已经释放,返回的shared_ptr是一个空对象,如果没有释放,则通过shared_ptr访问资源时安全的。
示例:
int main()
{
shared_ptr<string> sp1(new string("1111111"));
shared_ptr<string> sp2(sp1);weak_ptr<string> wp(sp1);
cout << wp.expired() << endl; //false表示资源没有过期
cout << wp.use_count() << endl;
return 0;
}
6,shared_ptr
std::make_shared
用于创建一个 std::shared_ptr,
它比直接使用 std::shared_ptr
的构造函数更高效,因为它会同时分配对象和控制块(用于引用计数等管理信息)的内存,而不是分别分配。
#include <iostream>
#include <memory>class MyClass {
public:
int value;
MyClass(int v) : value(v) {
std::cout << "Object created with value: " << value << std::endl;
}
~MyClass() {
std::cout << "Object destroyed" << std::endl;
}
};int main() {
// 使用 make_shared 创建 shared_ptr
auto ptr = std::make_shared<MyClass>(42);// 使用 shared_ptr
std::cout << "Value: " << ptr->value << std::endl;return 0;
}
shared_ptr还重载了operator bool,可以判断一个shared_ptr对象是否管理着资源。没有管理资源返回false,否则返回true。
#include <iostream>
#include <memory>int main() {
std::shared_ptr<int> sp1;
std::shared_ptr<int> sp2(new int(34));if (sp1) std::cout << "sp1 points to " << endl;
else std::cout << "sp1 is null\n";if (sp2) std::cout << "sp2 points to " << endl;
else std::cout << "sp2 is null\n";return 0;
}