【C++ 】智能指针:内存管理的 “自动导航仪”
目录
一、引入
二、智能指针的两大特性:
1、RAII
特点:
好处:
2、行为像指针
三、智能指针起初的缺陷:拷贝问题
四、几种智能指针的介绍。
1、C++98出现的智能指针——auto_ptr
auto_ptr解决上述拷贝构造的问题:
2、boost库
3、unique_ptr
引用计数的实现:
赋值运算符的问题:(循环引用)
5、weak_ptr
特点:
解决循环引用问题:
五、C++智能指针的基本框架:
六、定制删除器,以及包装器的使用场景之一
七、内存泄漏:
1、什么是内存泄漏,内存泄漏的危害:
2、内存泄漏的分类
八、关于C++智能指针的相关代码:
std::unique_ptr
std::weak_ptr
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家
点击跳转到网站
一、引入
首先通过一个使用场景来引入智能指针,如下:
class HF { public: HF(int a, int b) :_a(a) ,_b(b) { cout << "HF(int a, int b)" << endl; } ; ~HF() { cout << "~HF()" << endl; } private: int _a = 0; int _b = 1; }; void fun() { HF* h1 = new HF(1, 1); HF* h2 = new HF(1, 1); HF* h3 = new HF(1, 1); delete h1; delete h2; delete h3; } int main() { try { fun(); } catch (exception& e) { cout << e.what() << endl; } return 0; }
这里有一个类HF,一个子函数fun,在fun里面new了三个HF对象,然后delete,正常情况下delete会先调用析构函数,然后再释放相应的资源:
二、智能指针的两大特性:
智能指针的两大特性:
1、RAII
2、行为像指针
1、RAII
是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。(通俗来讲,就是将资源交给一个类对象来管理,通过该类的构造函数交给对象。)
特点:
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。
好处:
(1)、不需要显式地释放资源,而是通过智能指针间接帮忙释放(2)、采用这种方式,对象所需的资源在其生命期内始终保持有效
2、行为像指针
智能指针实际也是一个类,要是行为像一个指针,即要重载解引用(*),箭头(->),甚至有时还要重载方括号([ ])。
三、智能指针起初的缺陷:拷贝问题
首先我们实现一个简易版智能指针:
new了一个日期类对象交给智能指针管理,智能指针对象存在期间,资源都是存在的,最后智能指针对象生命周期结束,调用析构函数释放,同时释放掉资源(delete);
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; SmartPtr<Date> sparray(new Date); // 需要注意的是这里应该是sparray.operator->()->_year = 2018; // 本来应该是sparray->->_year这里语法上为了可读性,省略了一个-> sparray->_year = 2018; sparray->_month = 1; sparray->_day = 1; return 0; }
但是当我们用对象sp1去拷贝构造sp2时:
此时就会报错:
原因在于我们没有实现拷贝构造,此时默认拷贝构造就是浅拷贝,这样两个对象的成员变量都会指向这份资源,最后调用析构函数时,就会对这份资源delete两次,从而造成野指针的问题。如何解决这个问题,在第四点进行介绍。
四、几种智能指针的介绍。
1、C++98出现的智能指针——auto_ptr
头文件:memory
具体信息可以查看官网文档。
auto_ptr解决上述拷贝构造的问题:
auto_ptr是直接将资源的管理权转移,用对象sp1去拷贝构造sp2,那么就会将sp1的资源的管理权交给sp2管理,而sp1被置空。
大致处理方法如下:拷贝后将sp1置空就行了:
注意,C++11的移动语义也是资源的转移,但和这里是不一样的,移动语义是针对将亡值去转移资源,而这里sp1不是将亡值。
这样做是有些问题的,这里资源转移后,sp1就悬空了,此时拷贝后就不能去访问sp1,否则就会出现空指针的问题,所以很多公司都禁止使用auto_ptr;
2、boost库
提到智能指针,就得提一下boost库,boost库是C++第三方库,里面就有智能指针,而C++的智能指针就是从这个库里面引入的,然后进行了略微修改。
3、unique_ptr
该智能指针解决拷贝构造的问题的方法就是:简单粗暴,禁止拷贝,适用于不需要拷贝的场景。
底层实际就是将拷贝构造给delete了:
同时,赋值运算符重载也要禁掉,默认生成的赋值运算符重载也是浅拷贝。
4、shared_ptr
当遇到需要拷贝构造的场景时,就需要使用shared_ptr,shared_ptr解决拷贝构造的问题的方法是:引用计数,去解决多次析构的问题。
引用计数的实现:
引用计数:记录当前有几个对象参与管理这个资源,在某个对象析构时,就将引用计数减1,当最后一个对象析构时才去释放资源。
要实现引用计数,就需要一份资源对应一个计数,有人会想到定义一个静态成员count,但实则不行,因为静态成员是属于整个类的,属于所有对象。管理一个资源的时候是可以解决的,但当第二个资源出现时,就不能适用了,因为不同资源之间的引用计数都是同一个静态成员变量,所以会相互影响。
实际上的实现如下:
增加一个成员变量*pcount,即指向引用计数的指针,在构造的时候(即资源来了),就new一个计数给该指针,在拷贝构造发生的时候,除了使两个对象指向同一个资源外,两个对象的引用计数也要指向同一个,并且要记得把引用计数++一下,在某个对象析构时,就将引用计数减1,然后判断是否为最后一个对象的析构,如果是的话就释放资源。
赋值运算符的问题:(循环引用)
shared_ptr虽然解决了拷贝构造的问题,但正因为引用计数的这样实现,又会造成赋值运算符重载后出现问题。
赋值运算符简单重载:
为了分析这里的缺陷,我们引入一个场景:双向链表的赋值:
这是双向链表的简单实现
因为会将链表资源交给智能指针管理,如下:
所以链表的定义中,成员next和prev的类型也应该是智能指针,不然在赋值的时候会出现类型不同的问题,正因为需要这样设计,问题就来了。
一般情况上述实现是没有问题的,但当执行下面两句代码后,问题就来了:
这是在链接两个节点,链接完后就会这样:
首先出现两个节点分别由n1和n2指向,此时两个节点的引用计数分别都是1,当执行n1->next = n2时,n2指向的节点的引用计数就会变成2;当执行n2->prve = n1时,n1指向的节点的引用计数就会变成2。
最后当析构链表时:
这样就形成了一个闭环,导致这两个节点的内存泄漏,这个问题也叫循环引用。当两个shared_ptr互相引用就会出现循环引用的问题。
5、weak_ptr
为了解决shared_ptr的循环引用问题,所以引入了weak_ptr。
特点:
weak_ptr的特点:没有引用计数,支持默认构造,构造函数的形参没有指针,因为该智能指针不参与资源管理,但自身成员变量会有一个指针,但会被置空,weak_ptr的重点在于拷贝构造和赋值。
解决循环引用问题:
这里的不同是将链表的成员变量_next和_prev的类型变为weak_ptr,正因为weak_ptr没有增加引用计数,所以在节点链接的时候,引用计数不会增加,所以节点会正常释放。
五、C++智能指针的基本框架:
六、定制删除器,以及包装器的使用场景之一
七、内存泄漏:
1、什么是内存泄漏,内存泄漏的危害:
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
2、内存泄漏的分类
C++中我们一般关心两种分类:(1)、堆内存泄漏(Heap leak)堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。(2)、系统资源泄漏指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
八、关于C++智能指针的相关代码:
此小点内容来源于:豆包AI
#include <iostream> #include <memory> // 自定义类 class MyClass { public: MyClass() { std::cout << "MyClass constructor" << std::endl; } ~MyClass() { std::cout << "MyClass destructor" << std::endl; } void doSomething() { std::cout << "Doing something..." << std::endl; } }; class B; class A { public: std::shared_ptr<B> bPtr; ~A() { std::cout << "A destructor" << std::endl; } }; class B { public: std::weak_ptr<A> aPtr; // 使用 weak_ptr 避免循环引用 ~B() { std::cout << "B destructor" << std::endl; } }; void uniquePtrExample() { // 创建一个 unique_ptr 指向 MyClass 对象 std::unique_ptr<MyClass> uniquePtr = std::make_unique<MyClass>(); // 调用对象的成员函数 uniquePtr->doSomething(); // unique_ptr 不能被复制,但可以转移所有权 // std::unique_ptr<MyClass> anotherPtr = uniquePtr; // 错误,不能复制 std::unique_ptr<MyClass> anotherPtr = std::move(uniquePtr); if (!uniquePtr) { std::cout << "uniquePtr is empty after move" << std::endl; } if (anotherPtr) { anotherPtr->doSomething(); } } void sharedPtrExample() { // 创建一个 shared_ptr 指向 MyClass 对象 std::shared_ptr<MyClass> sharedPtr1 = std::make_shared<MyClass>(); std::cout << "Shared pointer 1 use count: " << sharedPtr1.use_count() << std::endl; // 复制 shared_ptr,引用计数增加 std::shared_ptr<MyClass> sharedPtr2 = sharedPtr1; std::cout << "Shared pointer 1 use count after copy: " << sharedPtr1.use_count() << std::endl; std::cout << "Shared pointer 2 use count: " << sharedPtr2.use_count() << std::endl; // 调用对象的成员函数 sharedPtr2->doSomething(); // 释放一个 shared_ptr,引用计数减少 sharedPtr2.reset(); std::cout << "Shared pointer 1 use count after reset sharedPtr2: " << sharedPtr1.use_count() << std::endl; } void weakPtrExample() { std::shared_ptr<A> a = std::make_shared<A>(); std::shared_ptr<B> b = std::make_shared<B>(); a->bPtr = b; b->aPtr = a; std::cout << "A use count: " << a.use_count() << std::endl; std::cout << "B use count: " << b.use_count() << std::endl; // 当 a 和 b 离开作用域时,对象会被正确销毁 } int main() { std::cout << "=== Unique Ptr Example ===" << std::endl; uniquePtrExample(); std::cout << std::endl; std::cout << "=== Shared Ptr Example ===" << std::endl; sharedPtrExample(); std::cout << std::endl; std::cout << "=== Weak Ptr Example ===" << std::endl; weakPtrExample(); std::cout << std::endl; return 0; }
在 C++ 里,手动管理动态分配的内存容易引发内存泄漏、悬空指针等问题。智能指针作为一种类模板,能有效管理动态分配的内存,避免这些问题的出现。C++ 标准库提供了三种主要的智能指针:
std::unique_ptr
、std::shared_ptr
和std::weak_ptr
。
std::unique_ptr
std::unique_ptr
属于独占式智能指针,它对所指向的对象拥有唯一的所有权。一旦std::unique_ptr
被销毁,其指向的对象也会随之被自动销毁。在
uniquePtrExample
函数中:
- 借助
std::make_unique
创建了一个std::unique_ptr
,它指向MyClass
的一个对象。- 调用
doSomething
方法来使用这个对象。- 尝试复制
std::unique_ptr
会引发编译错误,因为它不允许复制,不过可以使用std::move
转移所有权。- 转移所有权之后,原
std::unique_ptr
变为空。
std::shared_ptr
std::shared_ptr
是共享式智能指针,多个std::shared_ptr
能够指向同一个对象。它采用引用计数来管理对象的生命周期,当引用计数变为 0 时,对象就会被销毁。在
sharedPtrExample
函数中:
- 利用
std::make_shared
创建了一个std::shared_ptr
,它指向MyClass
的一个对象。- 通过
use_count
方法可以查看当前的引用计数。- 复制
std::shared_ptr
会使引用计数增加。- 调用
reset
方法可以释放std::shared_ptr
,从而使引用计数减少。
std::weak_ptr
std::weak_ptr
是弱引用智能指针,它不拥有对象的所有权,只是对std::shared_ptr
所管理的对象进行弱引用。std::weak_ptr
主要用于解决std::shared_ptr
的循环引用问题。在
weakPtrExample
函数中:
- 定义了
A
和B
两个类,其中A
类包含一个std::shared_ptr<B>
成员,B
类包含一个std::weak_ptr<A>
成员。- 创建了
A
和B
的std::shared_ptr
对象,并相互引用。- 由于
B
类使用了std::weak_ptr
,所以不会出现循环引用,当a
和b
离开作用域时,对象能够被正确销毁。