【C++高阶】:智能指针的全面解析
island1314 2024-08-19 16:35:02 阅读 87
✨ 落絮无声春堕泪,行云有影月含羞 🌏
📃个人主页:island1314
🔥个人专栏:C++学习
🚀 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞
目录
1.引言
1.1 动态内存与智能指针
🌸1.1.1 动态内存
🌸1.1.2 智能指针
1.2 智能指针的重要性
1.3 像指针一样使用
1.4 支持智能指针对象拷贝
2. C++标准库的智能指针
2.1 auto_ptr
🌈2.1.1 基本概念
🌈2.1.2 基本用法
2.2 unique_ptr
✨2.2.1 基本概念
✨2.2.2 基本用法
2.3 shared_ptr
🎈2.3.1 基本概念
🎈2.3.2 基本用法
🎈2.3.3 注意事项
2.4 weak_ptr
🧩2.4.1 基本概念
🧩2.4.2 基本用法
🧩2.4.3 对循环引用的解决
🧩2.4.4 与shared_ptr的关系
3. 智能指针的实现
3.1 auto_ptr模拟实现
3.2 unique_ptr模拟实现
3.3 shared_ptr模拟实现
3.4 weak_ptr模拟实现
📖总结
1.引言
到目前为止,我们编写的程序中所使用的对象都有着严格定义的生存期:
全局对象:程序启动时分配,在程序结束时销毁。局部对象:当我们进入其定义所在的程序块时被创建,在离开块时销毁。局部static对象:在第一次使用前分配,在程序结束时销毁。
我们的程序到目前为止只使用过静态内存或栈内存:
静态内存:保存局部static对象、类static数据成员以及定义在任何函数之外的变量。栈内存:保存定义在函数内的非static对象。分配在静态或栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在其定义的程序块运行时才存在;static对象在使用之前分配,在程序结束时销毁。
📒除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作自由空间(free store)或堆(heap)。程序用堆来存储动态分配(dynamically allocate)的对象。动态对象的生存期由程序来控制,也就是说,当动态对象不再使用时,我们的代码必须显式地销毁它们。
1.1 动态内存与智能指针
🌸1.1.1 动态内存
📒除了局部和static对象外,C++还支持动态分配对象。动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地被释放时,这些对象才会销毁。动态对象的正确释放是编程中极其容易出错的地方。为了更安全地使用动态对象,C++标准库定义了两个智能指针类型来管理动态分配的对象。当一个对象应该被释放时,指向它的智能指针可以确保自动地释放它。
new:在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete:在受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
但是动态内存的使用很容易出问题;
内存泄漏:有时我们会忘记释放内存,在这种情况下就会产生内存泄漏;非法指针:有时在尚有指针引用内存的情况下我们就释放了它,在这种情况下就会产生引用非法内存的指针。
📒C++11为了更容易(同时也更安全)地使用动态内存,新的标准库提供了智能指针(smart pointer)类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。具体说明如下:
🌸1.1.2 智能指针
从比较简单的层面来看,智能指针是 RAII(Resource Acquisition Is Initialization,资源获取即初始化) 机制对普通指针进行的一层封装,利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。这样使得智能指针的行为动作像一个指针,本质上却是一个对象,这样可以方便管理一个对象的生命周期。
智能指针本身: 智能指针是一个类模板的实例,通常作为局部变量存在于栈区(Stack)。当函数返回或者局部变量超出其作用域时,栈区的内存会被自动释放。智能指针管理的对象: 智能指针通常用来管理在堆区(Heap)上分配的内存。这是通过调用如new操作符来完成的。堆区的内存会一直存在,直到显式地释放它(使用delete操作符)或者当程序结束时才会被系统回收。
其实质如下:
由于C++中没有垃圾回收机制,必须自己释放掉分配的内存,否则就会造成内存泄露。比如对于普通的new或者malloc分配空间,需要及时的释放空间,否则容易造成内存泄漏,导致堆区空间不足。但是,随着代码量的增多,很容易忘记释放空间,解决这个问题最有效的方法是使用智能指针(smart pointer),其会自动释放。省去手动管理内存。智能指针是存储指向动态分配(堆)对象指针的类,用于生存期的控制,能够确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露。智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,计数器的大小等于指向对象的智能指针的数量,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。
注意:智能指针的头文件为<memory>
1.2 智能指针的重要性
📒在C++编程中,内存管理一直是一个不可或缺的话题。传统的C++程序员依赖new和delete(新建和删除)来手动管理内存,但是由于new和delete不能自动管理资源也不支持自定义删除器,导致使用该方式容易导致内存泄漏或是双重释放等问题。这就是智能指针(Smart Pointers)登场的原因。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效。
在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。
这种做法有两大好处:
不需要显式地释放资源。采用这种方式,对象所需的资源在其生命期内始终保持有效
📒智能指针不仅仅是一个指针,它是一个对象,拥有生命周期(Lifetime)。当智能指针的生命周期结束时,它会自动释放所拥有的资源。这种自动管理机制极大地减少了程序员的负担,也降低了出错的可能性。
“The best code is no code at all.” - Jeff Atwood
这句话在这里非常合适。【越少的代码用于管理内存,越少的地方会出错。】智能指针就是这样一种工具,让你能更专注于业务逻辑而非内存管理。
1.3 像指针一样使用
下面将用一个自定义智能指针SmartPtr为例:
// 模板
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr) //当智能指针未初始化时,赋予nullptr缺省值
: _ptr(ptr)
{}
~SmartPtr(){
if (_ptr)
delete _ptr;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
int main()
{
SmartPtr<int> p(new int(1));
cout << *p << endl;
return 0;
}
其中重载了*
和->
运算符,使得使用这个类就像使用指针一样。智能指针是一个模板类,以能够管理任何类型的指针引用的内存,如果模板参数是一个有公有成员的类,那么还能使用->
访问其成员。
1.4 支持智能指针对象拷贝
上面实现的智能指针SmartPtr
是极不完善的,如果想实现拷贝构造和拷贝赋值:
int main(){
SmartPtr<int> ap1(new int(1));
SmartPtr<int> ap2(ap1); //拷贝构造
SmartPtr<int> ap3(new int(2));
SmartPtr<int> ap4(new int(2));
ap4 = ap3; // 拷贝赋值
return 0;
}
错误(Clion):
malloc: *** error for object 0x600003c84030: pointer being freed was not allocated
造成程序崩溃的原因:是在这个类中没有实现拷贝构造函数和拷贝赋值函数,而编译器默认生成的全都是对内置类型的浅拷贝(值拷贝):相当于ap1和ap2、ap3和ap4共同管理同一块空间。当出了ap1的作用域后,调用析构函数,释放空间,ap2再次调用析构函数时导致这块已经被释放的空间再次被释放。ap3和ap4同理。
要解决浅拷贝造成的二次析构问题,就必须要去实现深拷贝的拷贝构造函数和拷贝赋值函数吗?
答案是不用的,智能指针的功能需求是模拟指针的使用,本质是帮指针托管资源,那么指针的拷贝或赋值操作就相当于两个指针指向同一块内存空间。资源管理权转移,通过不负责任的拷贝,会导致被拷贝对象悬空。虽然资源能得到释放,但是会造成垂悬指针。智能指针将内存资源的管理和对象的生命周期绑定在一起,如果只是像上面一样简单地满足RAII,那么一定会发生二次析构的问题,因为创建的智能指针对象一定会调用析构函数,且不论程序是否正常结束。
程序正常结束:对象出了作用域调用析构函数;
程序不正常结束:例如抛异常,跳转到catch块相当于跳转到另一个函数的栈帧中,也相当于出了作用域,依然调用析构函数。
后面以标准库中(C++98)智能指针auto_ptr
为例。
2. C++标准库的智能指针
在c++中,智能指针一共定义了4种:
auto_ptr、unique_ptr、shared_ptr 和 weak_ptr。
📝其中,auto_ptr 在 C++11已被摒弃,在C++17中已经移除不可用。
📙虽然auto_ptr有很多问题,但是也没有取消,因为可能有人在用,因此后面弄了一个
Boost库,属于C++扩展库(第三方库),不属于C++标准库(C++标准库是写了头文件就能用)
注意:本文主要讲的就是后面的 三种智能指针。
智能指针 | 类型 |
---|---|
unique_ptr | 独占的智能指针,该指针独占对象的所有权,每个对象智能有一个该指针 |
shared_ptr | 共享的智能指针,多个共享指针可以指向同一个对象 |
weak_ptr | 弱引用的智能指针,该指针是对象的一个非拥有性引用,不共享指针,不能操作资源,用来指向一个shared_ptr,主要用来避免shared_ptr循环引用导致的内存泄露 |
为了方便我们对下面智能之类的理解,我们定义如下的类
<code>// Date类
struct Date{
int _year;
int _month;
int _day;
Date (int year = 1, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
~Date() {
cout << "~Date()" << endl;
}
};
//指定删除器模板
template<class T>
class DeleteArray{
public:
void operator()(T* ptr){
delete[] ptr;
}
};
//文件删除的模板
class Fclose{
public:
void operator()(FILE* ptr){
cout << "fclose:" << ptr << endl;
fclose(ptr);
}
};
//Test类
class Test
{
public:
Test() : _num(0)
{
cout << "Test() 构造成功..." << endl;
}
Test(int x) : _num(0)
{
cout << "Test(int x) 构造成功, x = " << x << endl;
}
Test(string str) : _num(0)
{
cout << "Test(string str) 构造成功, str = " << str << endl;
}
~Test()
{
cout << "~Test() 析构成功..." << endl;
}
void setValue(int v)
{
this->_num = v;
}
void print()
{
cout << "_num: " << this->_num << endl;
}
private:
int _num;
};
2.1 auto_ptr
🌈2.1.1 基本概念
为了解决上面1.4 里面的问题,由于出现多次析构问题的本质是同一块内存空间被多个对象通过管理,因此如果将资源的管理权只交给一个对象,就不会出现多次析构问题。
🌈2.1.2 基本用法
<code>int main(){
auto_ptr<Date> ap1(new Date);
//拷贝时,管理权限转移,被拷贝悬空
auto_ptr<Date> ap2(ap1);
//下面这样会使程序会崩,因为auto_ptr的拷贝是管理权转移,这样就会导致ap1空了
ap1->_year++;
return 0;
}
然而,将一个对象对资源的管理权转移后,就意味着这个对象再对资源访问是一个非法操作,程序会因此崩溃。如果让不熟悉auto_ptr原理的人使用,因为拷贝操作而造成非法指针或内存泄漏是有可能的(拷贝悬空),而这也是致命的错误,因此许多公司明文规定禁止auto_ptr的使用,进而由C++11的unique_ptr和shared_ptr取代。
2.2 unique_ptr
✨2.2.1 基本概念
出现多次析构问题的本质是同一块内存空间被多个对象通过管理,如果将资源的管理权只交给一个对象,就不会出现多次析构问题。
C++98的auto_ptr因为拷贝和赋值操作而造成内存泄漏和悬垂指针的问题而饱受诟病,C++11引入的unique_ptr则粗暴地砍掉了它的拷贝和赋值功能。这通过C++11引入的关键字delete的新功能实现。
在C++11之前,可以通过将构造函数和拷贝赋值函数私有声明实现。
🌞它实现对对象的独占所有权语义。这意味着一个unique_ptr在任何时候都指向一个对象,而且这个对象只能由一个unique_ptr拥有。当unique_ptr被销毁(例如离开其作用域)时,它所指向的对象也会被自动删除。
✨2.2.2 基本用法
样例1:(初概)
int main() {
unique_ptr<Date> up1(new Date);
//不支持拷贝
//unique_ptr<Date> up2(up1);
//unique_ptr<Date> up2(new Date[6]); //error,因为类型不匹配,由于我们是用new []构造的,那么就要用delete[]去销毁
//定制删除器解决上述问题
unique_ptr<Date, DeleteArray<Date>> up2(new Date[6]); //用定制的删除器
//unique_ptr<Date[]> up2(new Date[5]);
unique_ptr<FILE, Fclose> up3(fopen("test.cpp", "r"));
return 0;
}
样例2:(详细)
1. 初始化
std::unique_ptr是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,可以通过它的构造函数初始化一个独占智能指针对象,但是不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr。
2. 指定删除器
unique_ptr指定删除器和shared_ptr指定删除器是有区别的,unique_ptr指定删除器的时候需要确定删除器的类型,所以不能像shared_ptr那样直接指定删除器
<code>int main()
{
/*-------------------------- 一,初始化智能指针unique_ptr ------------------------------*/
// 1.通过构造函数初始化
unique_ptr<int> up1(new int(3));
//unique_ptr<int> up = up1; // 编译错误,不能复制
// 2.通过移动函数初始化
unique_ptr<int> up2 = move(up1); // 现在up2独占资源,up1变为空
// 3.通过reset,释放资源并将指针置为空,然后再初始化
up1.reset(new int(7));
//对比reset,release会释放资源的所有权但不删除,返回原始指针
up1.release();
/*-------------------------- 二,unique_ptr的使用 ------------------------------*/
//1.方法一
unique_ptr<Test> up3(new Test(666));
Test* pt = up3.get();
pt->setValue(6);
pt->print();
//2.方法二
up3->setValue(777);
up3->print();
/*------------------------------------ 三,指定删除器 -----------------------------------*/
1.函数指针类型
//using ptrFunc = void(*)(Test*);
//unique_ptr<Test, ptrFunc> up4(new Test("hello"), [](Test* t) {
// cout << "-----------------------" << endl;
// delete t;
// });
//2.仿函数类型(利用可调用对象包装器)
unique_ptr<Test, function<void(Test*)>> up4(new Test("hello"), [](Test* t) {
cout << "-----------------------" << endl;
delete t;
});
/*---------- 四,独占(共享)的智能指针可以管理数组类型的地址,能够自动释放 ---------*/
unique_ptr<Test[]> up5(new Test[3]);
//在c++11中shared_ptr不支持下面的写法,c++11以后才支持的
shared_ptr<Test[]> up6(new Test[3]);
return 0;
}
2.3 shared_ptr
🎈2.3.1 基本概念
🌙shared_ptr是C++11的智能指针,通过引用计数的方式解决智能指针的拷贝问题。
🌙每个被管理的资源有有一个对应的引用计数,这个引用计数记录当前有多少对象在管理这块资源。
🌙每新增加一个对象管理这块资源则对该资源的引用计数++,当一个对象不在管理这块资源或对象析构时那么该资源对应的引用计数 – –,当一个资源的引用计数为0时那么就说明已经没有对象在管理这块资源了,这时候就可以进行释放了。
引用计数的方式能够支持多个对象一起管理一个资源,也就支持智能指针的拷贝,只有当资源的引用计数减为0时才会释放,保证了同一个资源不会被多次释放:
🌙实现了对象的共享所有权语义。多个shared_ptr可以指向同一个对象,并且每个shared_ptr持有一个引用计数。当最后一个指向某个对象的shared_ptr被销毁或重置时,该对
象才会被删除。
🎈2.3.2 基本用法
样例1:(初概)
<code>int main() {
//vector<shared_ptr<Date>> v;
shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1);
shared_ptr<Date> sp3(sp2);
cout << sp1.use_count() << endl; //查看其引用计数
//定制删除器
shared_ptr <Date[]> sp4(new Date[5]);
//shared_ptr<FILE, Fclose> up5(fopen("test.cpp", "r")); //不支持传这个模板参数
shared_ptr<FILE> up5(fopen("test.cpp", "r"), Fclose());
shared_ptr<Date> sp6 = make_shared<Date>(2024, 8, 5);
shared_ptr<int> sp7((int*)malloc(40), [](int* ptr)
{
cout << "free:" << ptr << endl;
free(ptr);
});
return 0;
}
1. 初始化
共享智能指针是指多个智能指针可以同时管理同一块有效的内存,共享智能指针shared_ptr 是一个模板类,如果要进行初始化有三种方式:通过构造函数、std::make_shared辅助函数以及reset方法。共享智能指针对象初始化完毕之后就指向了要管理的那块堆内存,如果想要查看当前有多少个智能指针同时管理着这块内存可以使用共享智能指针提供的一个成员函数use_count
shared_ptr通过一个指针保持对一个对象的共享所有权。多个shared_ptr对象可以拥有同一个对象。当以下情况之一发生时,对象被销毁并释放其内存:
拥有该对象的最后一个shared_ptr被销毁;通过reset()函数将shared_ptr赋值为另一个指针。
2. 获取原始指针
对应基础数据类型来说,通过操作智能指针和操作智能指针管理的内存效果是一样的,可以直接完成数据的读写。但是如果共享智能指针管理的是一个对象,那么就需要取出原始内存的地址再操作,可以调用共享智能指针类提供的get()方法得到原始地址
3. 指定删除器
当智能指针管理的内存对应的引用计数变为0的时候,这块内存就会被智能指针析构掉了。另外,我们在初始化智能指针的时候也可以自己指定删除动作,这个删除操作对应的函数被称之为删除器,这个删除器函数本质是一个回调函数,我们只需要进行实现,其调用是由智能指针完成的。
4. 引用计数
auto_ptr转移资源后造成内存泄漏和悬垂指针的主要原因就是每个auto_ptr智能指针对象管理的资源是各自独立的,非此即彼。shared_ptr共享同一个资源,内存资源只在最后一个智能指针解除引用时释放,这样就不会造成资源被单方面地接管造成的问题。引用计数使得一个空间可以被多个对象管理,当引用计数为0时,说明已经没有智能指针管理这块内存空间了,此时才能释放资源,弥补了auto_ptr的缺陷。要知道引用计数的值,只需要调用shared_ptr的成员函数use_count()即可。
样例2:(详细)
<code>int main()
{
/*---------------------------- 一,初始化智能指针shared_ptr ------------------------------*/
// 1.通过构造函数初始化
shared_ptr<int> sp1(new int(3));
cout << "sp1管理的内存引用计数:" << sp1.use_count() << endl;
// 2.通过移动和拷贝构造函数初始化
shared_ptr<int> sp2 = move(sp1);
cout << "sp1管理的内存引用计数:" << sp1.use_count() << endl;
cout << "sp2管理的内存引用计数:" << sp2.use_count() << endl;
shared_ptr<int> sp3 = sp2; //赋值
cout << "sp2管理的内存引用计数: " << sp2.use_count() << endl;
cout << "sp3管理的内存引用计数: " << sp3.use_count() << endl;
// 3.通过 std::make_shared初始化
shared_ptr<int> sp4 = make_shared<int>(8);
shared_ptr<Test> sp5 = make_shared<Test>(7);
shared_ptr<Test> sp6 = make_shared<Test>("Love Life");
// 4.通过reset初始化
sp6.reset();//重置sp6, ps6的引用基数为0
cout << "sp6管理的内存引用计数: " << sp6.use_count() << endl;
sp5.reset(new Test("hello")); //重置了指针的指向对象,原来的对象已经释放
cout << "sp5管理的内存引用计数: " << sp5.use_count() << endl;
cout << endl << endl;
/*----------------------------- 二,共享智能指针shared_ptr的使用 ------------------------------*/
// 1.方法一
Test* t = sp5.get();
t->setValue(1000);
t->print();
// 2.方法二
sp5->setValue(7777);
sp5->print();
cout << endl << endl;
///*------------------------------ 三,指定删除器 -----------------------------------*/
// 1.简单举例
shared_ptr<Test> ppp(new Test(100), [](Test* t) {
//释放内存
cout << "Test对象的内存被释放了......." << endl;
delete t;
});
printf("----------------------------------------------------------------------\n");
2.如果是数组类型的地址,就需要自己写指定删除器,否则内存无法全部释放
////shared_ptr<Test> p1(new Test[5], [](Test* t) {
//// delete[]t;
//// });
3.也可以使用c++给我们提供的 默认删除器函数(函数模板)
shared_ptr<Test> p2(new Test[3], default_delete<Test[]>());
4.c++11以后可以这样写 也可以自动释放内存
shared_ptr<Test[]> p3(new Test[3]);
return 0;
}
此外我们还可以封装一个函数模板make_shared_array方法来让shared_ptr支持数组:
<code>template <typename T>
shared_ptr<T> make_share_array(size_t size)
{
//返回匿名对象
return shared_ptr<T>(new T[size], default_delete<T[]>());
}
int main()
{
shared_ptr<int> ptr1 = make_share_array<int>(10);
cout << ptr1.use_count() << endl;
shared_ptr<string> ptr2 = make_share_array<string>(7);
cout << ptr2.use_count() << endl;
}
🎈2.3.3 注意事项
shared_ptr中的引用计数是存放在堆区,因为这样可以让所有指向同一个对象的shared_ptr。如果引用计数在栈区,那么当一个shared_ptr改变指向或者离开作用域时,就无法通知其他shared_ptr更新引用计数了。因此,引用计数也不能是静态成员,每个类型实例化的智能指针对象时共用静态成员,这会导致管理相同资源的对象和管理不同资源的对象共用同一个引用计数。
由于在堆区的引用计数和同一类型的智能指针是绑定在一起的,当智能指针释放资源时,也需要释放引用计数占用的内存。
2.4 weak_ptr
🧩2.4.1 基本概念
⭐弱引用智能指针std::weak_ptr可以看做是shared_ptr的助手,是对shared_ptr所管理对象的一个非拥有性引用,它不管理shared_ptr内部的指针。std::weak_ptr没有重载操作符*和->,因为它不共享指针,不能操作资源,所以它的构造不会增加引用计数,析构也不会减少引用计数。最后一个指向对象的shared_ptr被销毁时,无论是否还有weak_ptr指向该对象,对象都会被删除。它的主要作用就是作为一个旁观者监视shared_ptr中管理的资源是否存在,解决shared_ptr可能导致的循环引用问题。
在这部分利用率局部变量的特性,C++的局部变量存在栈中,当变量的生命周期结束后,那栈会自动释放空间。而智能指针同样为局部变量,存在栈中。
🧩2.4.2 基本用法
样例1:(初概)
// ———————— weadk_ptr ——————————
int main()
{
// weak_ptr不支持管理资源,不支持RAII
//std::weak_ptr<Date> wp1(new Date);
weak_ptr<Date> wp;
shared_ptr<Date> sp; //保证不过期
{
shared_ptr<Date> n1(new Date);
wp = n1;
cout << wp.expired() << endl;
n1->_day++;
//sp = wp.lock();
}
// 出了作用域就失效了
cout << wp.expired() << endl;
}
样例2:(详细)
std::weak_ptr底层与std::shared_ptr共享相同的引用计数机制,但不会增加计数。
<code>
int main()
{
/*---------------------------- 一,初始化智能指针weak_ptr ------------------------------*/
// weak_ptr不支持管理资源,不支持RAII
shared_ptr<int> sp(new int(2));
weak_ptr<int> wp1;; //构造了一个空weak_ptr对象
weak_ptr<int> wp2(wp1); //通过一个空weak_ptr对象构造了另一个空weak_ptr对象
weak_ptr<int> wp3(sp); //通过一个shared_ptr对象构造了一个可用的weak_ptr实例对象
weak_ptr<int> wp4;
wp4 = sp; //通过一个shared_ptr对象构造了一个可用的weak_ptr实例对象(这是一个隐式类型转换)
weak_ptr<int> wp5;
wp5 = wp3; //通过一个weak_ptr对象构造了一个可用的weak_ptr实例对象
/*---------------------------- 二,weak_ptr常用函数 ------------------------------*/
// 1.通过调用weak_ptr类提供的use_count()方法可以获得当前共享该资源的shared_ptr数量
cout << "wp1管理的shared_ptr内存引用计数:" << wp1.use_count() << endl;
cout << "wp2管理的shared_ptr内存引用计数:" << wp2.use_count() << endl;
cout << "wp3管理的shared_ptr内存引用计数:" << wp3.use_count() << endl;
cout << "wp4管理的shared_ptr内存引用计数:" << wp4.use_count() << endl;
cout << "wp5管理的shared_ptr内存引用计数:" << wp5.use_count() << endl;
cout << endl << endl;
// 2.通过调用std::weak_ptr类提供的expired()方法来判断
//判断指针所指的内存空间是否被释放掉 / 指针是否为空 / 是否还有shared_ptr指针指向weak_ptr指向的内存空间
shared_ptr<int> sp2 = make_shared<int>(10);
weak_ptr<int> wp6(sp2); // shared_ptr初始化weak_ptr
sp2.reset(new int); // 此时,已没有一个shared_ptr指针指向weak_ptr指向的内存区域
cout << "是否已没有shared_ptr指针指向该内存区域:" << wp6.expired() << endl;
shared_ptr<int> sp22 = nullptr;
weak_ptr<int> wp7(sp22);
cout << "weak_ptr指针是否为空:" << wp7.expired() << endl;
// 3.通过调用weak_ptr类提供的lock()方法来获取管理所监测资源的shared_ptr对象,返回一个shared_ptr,增加引用计数
shared_ptr<int> sp3 = wp3.lock(); // 利用返回的shared_ptr初始化
cout << "share_ptr的初始化为:" <<*sp3 << endl;
cout << "wp3管理的shared_ptr内存引用计数:" << wp3.use_count() << endl;
return 0;
}
🧩2.4.3 对循环引用的解决
假如存在这样的一个结构体
struct ListNode{
int _data;
shared_ptr<ListNode> _next;
shared_ptr<ListNode> _prev;
//用weak_ptr就可以避免内存泄露
//weak_ptr<ListNode> _next;
//weak_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
情况一:当我们在shaed_ptr的正常情况下未出现内存泄露
int main()
{
// 下面这就是循环引用 --> 内存泄露
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
n1->_next = n2;
//n2->_prev = n1; //互相指向,就会出现内存泄露
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
return 0;
}
情况二:当我们增加一句n2->_prev = n1; 到函数中,此时就出现了互相指向,导致了内存泄露
情况三:造成问题的本质是引用计数永不为0,那么只要将其中一个智能指针改为weak_ptr即可:
🧩2.4.4 与shared_ptr的关系
🌸由上面可知:weak_ptr是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期。也就是将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。
🌸它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造,它的构造和析构不会引起引用记数的增加或减少。弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。
也就是说,weak_ptr是为了弥补shared_ptr循环引用而生的,它没有RAII的特性,不直接管理资源,只是shared_ptr的跟班,这也是weak_ptr支持使用shared_ptr构造的原因。
在使用上,weak_ptr支持指针所有的操作。它不是一个功能型的智能指针,而是辅助型,它的使命是解决shared_ptr造成的循环引用问题。
🌸与 shared_ptr 不同,weak_ptr 不能直接访问所指向的对象。要访问对象,需要先调用 lock() 方法将其转换为 shared_ptr。如果所指向的对象已经被销毁,则 lock() 方法返回空指针。
3. 智能指针的实现
📚看完前面其智能指针的实现,相信大家对智能指针也有个大概的了解了,下面我们来实现部分的智能指针吧,如unique_ptr、shared_ptr、weak_ptr.
智能指针的实现,必须解决下面三个问题:
RAII,将资源交给对象的生命周期管理,即构造对象时开始获取(管理)资源,析构对象时释放资源;像真正的指针一样使用;支持智能指针对象的拷贝。
其中最容易实现的是像指针一样使用,只要重载使用指针的运算符即可。其次是实现RAII,最后是智能指针对象的拷贝。
3.1 auto_ptr模拟实现
auto_ptr的实现原理:管理权转移的思想
析构函数:需要对它管理的指针判空,只有指针非空时才能对其进行释放资源操作,释放资源以后对其置空。
拷贝构造函数:用传入对象管理的内存资源来构造当前对象,并将传入对象管理资源的指针置空。
拷贝赋值函数:先将当前对象管理的资源释放,然后再接管传入对象管理的资源,最后将传入对象管理资源的指针置空。
<code>namespace qian {
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
// 管理权转移
sp._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
// 检测是否为自己给自己赋值
if (this != &ap)
{
// 释放当前对象中资源
if (_ptr)
delete _ptr;
// 转移ap中资源到当前对象中
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
3.2 unique_ptr模拟实现
模拟实现的过程:去除auto_ptr中拷贝和赋值的函数。
📚注意:C++98中,delete的意思是不让编译器自动生成默认函数,而C++11为了实现这个智能指针,赋予delete一个新功能:不允许调用,因此我们在下面的拷贝和赋值中用到了delete。
namespace qian
{
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr != nullptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
}
unique_ptr(auto_ptr<T>& ap) = delete;
unique_ptr& operator=(auto_ptr<T>& ap) = delete;
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
int main()
{
qian::unique_ptr<int> up1(new int);
qian::unique_ptr<int> up2(new int);
up2 = up1; // error
return 0;
}
3.3 shared_ptr模拟实现
增加count成员变量,表示引用计数;构造函数:当获取到资源则设置count=1,表示当前只有一个智能指针对象管理此资源;拷贝构造函数:将传入的智能指针对象中的count++,表示新增了一个管理者;拷贝赋值函数:将本智能指针的count--,表示解除对当前资源的引用,然后再将传入的智能指针对象中的count++,表示管理新的资源;析构函数:count--,表示解除对当前管理资源的引用,如果count=0则释放资源;重载*和->运算符,使shared_ptr对象具有指针一样的行为。
其中,operator = 的重载需要注意两个问题:
内存泄漏:赋值时要把自己的引用计数给对方,赋值代表对方要共同接管自己管理的资源,所以对方的引用计数也要 - 1;自我赋值:本质也会造成内存泄漏,自我赋值后资源的管理权并未发生变化,但是引用计数却 + 1了,到真正最后一个对象时,引用计数仍不为0(如果自我赋值1次,那就是1),造成资源不能释放,内存泄漏。
namespace qian
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
{}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
if (_ptr != nullptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
delete _pcount;
_pcount = nullptr;
}
}
shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
(*_pcount)++;
}
shared_ptr<T>& operator=(shared_ptr<T>& sp)
{
if (_ptr != sp._ptr) // 管理同一资源的智能指针赋值无意义
{
if (--(*_pcount) == 0) // 将管理的资源的引用计数-1
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr; // 与传入的智能指针共享资源
_pcount = sp._pcount; // 将自己的引用计数和传入的智能指针同步
(*_pcount)++; // 引用计数+1,表示自己是新增的管理者
}
return *this;
}
// 获取引用计数
int use_count()
{
return *_pcount;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount; // 引用计数
};
}
int main(){
qian::shared_ptr<int> sp1(new int(1));
cout << sp1.use_count() << endl;
qian::shared_ptr<int> sp2(sp1);
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
qian::shared_ptr<int> sp3(new int(0));
cout << sp3.use_count() << endl;
qian::shared_ptr<int> sp4(new int(2));
sp3 = sp4;
cout << sp3.use_count() << endl;
cout << sp4.use_count() << endl;
return 0;
}
3.4 weak_ptr模拟实现
构造函数:无参构造拷贝构造:支持参数是shared_ptr类型和本身类型构造,同时接管shared_ptr管理的资源,但不增加引用计数拷贝赋值:同上。智能指针一般有
get()
接口,所以返回指针时可以调用像指针一样
namespace qian
{
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get()) //sp的get接口在share_pr中已实现,如上
{}
weak_ptr& operator=(const shared_ptr<T>& sp)
{
//_ptr = sp._shared_ptr;
_ptr = sp.get();
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
📖总结
以上就是智能指针的全部内容啦,后面我会单独出一篇关于自定义删除器的博客,敬请期待咯!!!
💞 💞 💞那么本篇到此就结束,希望我的这篇博客可以给你提供有益的参考和启示,感谢大家支持!!!祝大家天天开心。
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。