当前位置: 首页 > article >正文

【C++篇】智能指针

目录

智能指针的概念及使用

1,RAII和智能指针

2,C++标准库中的智能指针

3,智能指针的原理及模拟实现(shared_ptr)

4,shared_ptr循环引用问题

5,weak_ptr 

 6,shared_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;
}

 


http://www.kler.cn/a/539499.html

相关文章:

  • C++并发编程指南 09(共享数据)
  • 【kafka系列】Topic 与 Partition
  • 编程式路由
  • <论文>DeepSeek-R1:通过强化学习激励大语言模型的推理能力(深度思考)
  • 第六届MathorCup高校数学建模挑战赛-A题:淡水养殖池塘水华发生及池水自净化研究
  • Stability AI 联合 UIUC 提出单视图 3D 重建方法SPAR3D,可0.7秒完成重建并支持交互式用户编辑。
  • Objective-C语言的云计算
  • openssl使用
  • 【HeadFirst系列之HeadFirstJava】第2天之类与对象-拜访对象村
  • 使用golang wails写了一个桌面端小工具:WoWEB, 管理本地多前端项目
  • YOLOV8 OpenCV + usb 相机 实时识别
  • JMeter常用函数整理
  • 高并发读多写少场景下的高效键查询与顺序统计的方案思路
  • 【Spring Boot】Spring 事务探秘:核心机制与应用场景解析
  • Android studio怎么创建assets目录
  • 编程语言的深度剖析:从语法到性能优化
  • 【JavaScript】《JavaScript高级程序设计 (第4版) 》笔记-Chapter7-迭代器与生成器
  • 【自动化办公】基于WPF和阿里云API的高效识别PDF多个区域内容并保存至JSON文件,再将JSON文件转换解析为表格输出
  • pytest生成报告no tests ran in 0.01s
  • Java WORD和PDF互相转换以及数据填充示例
  • Windows Docker笔记-Docker容器操作
  • react 17 源码调试环境搭建(超简单)
  • springboot项目的单元测试
  • vue.js v-model实现原理
  • 编译QCefView时出现的setlocal命令错误
  • 【含文档+PPT+源码】基于微信小程序的校园志愿者管理系统的设计与实现