首页 > 其他分享 >智能指针详解

智能指针详解

时间:2023-08-29 12:35:33浏览次数:42  
标签:cnt 对象 智能 详解 shared unique ptr 指针



文章目录

  • 一、智能指针背后的设计思想
  • 二、普通指针存在的问题
  • 三、shared_ptr类
  • 1、make_shared函数
  • 2、shared_ptr的拷贝和赋值
  • 3、shared_ptr自动销毁所管理的对象
  • 4、使用动态内存的原因:
  • 5、使用shared_ptr的一个例子:
  • 四、shared_ptr的实现和循环引用问题
  • 五、weak_ptr类
  • 1、weak_ptr详解
  • 2、weak_ptr的实现
  • 六、unique_ptr类
  • 七、unique_ptr类为何优于auto_ptr类
  • 八、使用智能指针需要注意的问题:
  • 九、如何选择智能指针?


一、智能指针背后的设计思想

我们先来看一个简单的例子:

void remodel(std::string & str)
{
    std::string * ps = new std::string(str);
    ...
    if (weird_thing())
        throw exception();
    str = *ps; 
    delete ps;
    return;
}

当出现异常时(weird_thing()返回true),delete将不被执行,因此将导致内存泄露。

如何避免这种问题?有人会说,这还不简单,直接在throw exception();之前加上delete ps;不就行了。是的,你本应如此,问题是很多人都会忘记在适当的地方加上delete语句(连上述代码中最后的那句delete语句也会有很多人忘记吧),如果你要对一个庞大的工程进行review,看是否有这种潜在的内存泄露问题,那就是一场灾难!

这时我们会想:当remodel这样的函数终止(不管是正常终止,还是由于出现了异常而终止),本地变量都将自动从栈内存中删除—因此指针ps占据的内存将被释放,如果ps指向的内存也被自动释放,那该有多好啊。

我们知道析构函数有这个功能。如果ps有一个析构函数,该析构函数将在ps过期时自动释放它指向的内存。但ps的问题在于,它只是一个常规指针,不是有析构凼数的类对象指针。如果它指向的是对象,则可以在对象过期时,让它的析构函数删除指向的内存。

这正是智能指针背后的设计思想。我简单的总结下就是:将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。

shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中。

二、普通指针存在的问题

我们来看看普通指针的悬垂指针问题。当有多个指针指向同一个基础对象时,如果某个指针delete了该基础对象,对这个指针来说它是明确了它所指的对象被释放掉了,所以它不会再对所指对象进行操作,但是对于剩下的其他指针来说呢?它们还傻傻地指向已经被删除的基础对象并随时准备对它进行操作。于是悬垂指针就形成了,程序崩溃也“指日可待”。我们通过代码+图来来探求悬垂指针的解决方法。

int * ptr1 = new int (1);
int * ptr2 = ptr1;
int * ptr3 = prt2;
        
cout << *ptr1 << endl;
cout << *ptr2 << endl;
cout << *ptr3 << endl;

delete ptr1;

cout << *ptr2 << endl;

智能指针详解_智能指针


智能指针详解_智能指针_02


智能指针详解_引用计数_03


  从图可以看出,错误的产生来自于ptr1的”无知“:它并不知道还有其他指针共享着它指向的对象。如果有个办法让ptr1知道,除了它自己外还有两个指针指向基础对象,而它不应该删除基础对象,那么悬垂指针的问题就得以解决了。如下图:

智能指针详解_智能指针_04


智能指针详解_引用计数_05

三、shared_ptr类

智能指针详解_赋值_06

智能指针详解_智能指针_07

1、make_shared函数

最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。

shared_ptr<int> p = make_shared<int>(42);//效率比下面的高  
shared_ptr<int> p2(new int(42));// 不推荐,为了避免智能指针与普通指针的混用,所以最后使用make_shared,这样在内存分配之后立刻与智能指针绑定到一起.

2、shared_ptr的拷贝和赋值

auto p = make_shared<int>(42);
auto q(p);
auto r = p; //p递增,r递减。

每个shared_ptr都有一个关联的计数器,通常称为引用计数

无论何时我们拷贝一个shared_ptr,计数器都会递增;当我们给shared_ptr赋予一个新值或者shared_ptr被销毁时,引用计数会递减。

一旦引用计数变为0,shared_ptr就会自动释放自己所管理的对象。

3、shared_ptr自动销毁所管理的对象

shared_ptr的析构函数会递减它所指向的对象的引用计数,当引用计数变为0时,shared_ptr就会通过析构函数自动释放自己所管理的对象。当动态对象不再使用时,shared_ptr会自动释放对象,这一特性使得动态内存的使用变得非常容易(尽量使用智能指针管理动态内存)。如果将shared_ptr放于容器中,而后不再需要全部元素,而只是使用其中一部分,要记得用erase删除不再需要的那些元素。

在多线程程序中,一个对象如果被多个线程访问,一般使用shared_ptr,通过引用计数来保证对象不被错误的释放导致其他线程访问出现问题。

4、使用动态内存的原因:

  • 程序不知道自己需要多少对象。
  • 程序不知道所需对象的准确类型。
  • 允许多个对象共享相同的状态。

5、使用shared_ptr的一个例子:

#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;
class StrBlob
{
public:
	typedef vector<string>::size_type st;
	StrBlob():data(make_shared<vector<string>>()){}
	StrBlob(initializer_list<string> il):data(make_shared<vector<string>>()){}
	inline st size() const { return data->size(); } 
	inline bool empty() const { return data->empty(); }
	inline void push_back(const string &t) { data->push_back(t); }
	void pop_back();
	const string & front();
	const string & back();
private:
	shared_ptr<vector<string>> data;
	void check(st i,const string &msg) const;
};
void StrBlob::check(st i,const string &msg) const
{
	if(i >= data->size())
		throw out_of_range(msg);
}
const string & StrBlob::front()
{
	StrBlob::check(0,"front on empty StrBlob");
	return data->front();
}
const string & StrBlob::back()
{
	StrBlob::check(0,"back on empty StrBlob");
	return data->back();
}
void StrBlob::pop_back()
{
	StrBlob::check(0,"pop_back on empty StrBlob");
	return data->pop_back();	
}
int main(int argc, char const *argv[])
{
    StrBlob b1;
    StrBlob b2 = {"a","an","the"};
    b1 = b2;
    b2.push_back("about");
    cout << b1.size() << endl;
    cout << b2.size() << endl;
    return 0;  
	return 0;
}

四、shared_ptr的实现和循环引用问题

template<typename T>
class SmartPtr {
private:
    T *_ptr;    // 指向对应的对象
    int *_cnt;   // 计数
public:
    SmartPtr(T *ptr) : _ptr(ptr), _cnt(new int(1)) {}
    // 拷贝
    SmartPtr(const SmartPtr &p) : _ptr(p._ptr), _cnt(p._cnt) { (*_cnt)++; }
    // 赋值
    SmartPtr & operator=(const SmartPtr &p) {
        (*(p._cnt))++; // 给右侧的对象的计数++
        (*_cnt)--;     // 给左侧的对象的计数--
        if (*_cnt == 0) {
            delete _ptr;
            delete _cnt;
        }
        _ptr = p._ptr;
        _cnt = p._cnt;
        return *this;
    }
    ~SmartPtr() {
        (*_cnt)--;
        if (*_cnt == 0) {
            delete _cnt;
            delete _ptr;
        }
    }
};

但是这里还有一个严重的问题,就是关于循环引用(会引起内存泄漏) 的问题。对于什么是循环引用?我们用下面这个测试用例来解释:

class B;
class A
{
public:
  shared_ptr<B> m_b;
};

class B
{
public:
  shared_ptr<A> m_a;
};

void fun()
{
	shared_ptr<A> pa(new A); // new出来的A的引用计数此时为1
	shared_ptr<B> pb(new B); // new出来的B的引用计数此时为1
	pa->m_b = b; // B的引用计数增加为2
	pb->m_a = a; // A的引用计数增加为2
}

int main()
{
	fun();
	return 0;
}

智能指针详解_赋值_08


  分析class A对象的引用情况,该对象被main函数中的pa和class B对象中的ptr管理,因此pa引用计数是2,class B对象同理。在这种情况下,在fun函数结束的时候,pa和pb的析构函数被调用,但是class A对象和class B对象仍然被一个智能指针管理,pa和pb引用计数变成1,于是这两个对象的内存无法被释放,造成内存泄漏,如下图所示:

智能指针详解_赋值_09


  因此,在这里标准库就引用了weak_ptr,将类里面的shared_ptr换成weak_ptr即可,由于weak_ptr并不会增加引用计数use的值,所以这里就能够打破shared_ptr所造成的循环引用问题。但是这里要注意一点,就是weak_ptr并不能单独用来管理空间。

五、weak_ptr类

1、weak_ptr详解

由于在shared_ptr(强引用:每创建一个变量引用该对象时,该对象的计数就增加1)的析构函数中,只有当use=1,进行减减之后为0,才会释放_ptr所指向的空间,所以在这里a和b都不会被释放,因此也不会调用析构函数,所以这里就出现了内存泄漏。

由于在shared_ptr单独使用的时候会出现循环引用的问题,造成内存泄漏,所以标准库又从boost库当中引入了weak_ptr(弱引用:不更改引用计数,类似普通指针)。对上面的测试用例进行修改:

class B;
class A
{
public:
  weak_ptr<B> m_b;
};

class B
{
public:
  weak_ptr<A> m_a;
};

void fun()
{
	shared_ptr<A> pa(new A); // new出来的A的引用计数此时为1
	shared_ptr<B> pb(new B); // new出来的B的引用计数此时为1
	pa->m_b = b; // B的引用计数增加为2
	pb->m_a = a; // A的引用计数增加为2
}

int main()
{
	fun();
	return 0;
}

解决方法很简单,把class A或者class B中的shared_ptr改成weak_ptr即可,由于weak_ptr不会增加shared_ptr的引用计数,所以pa和pb中有一个的引用计数为1,在pa和pb析构时,会正确地释放掉内存。

智能指针详解_赋值_10


  weak_ptr是一种不控制所指向对象生存周期的智能指针,他指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr,不会改变shared_ptr的引用计数。

auto p = make_shared<int>(10); // 使用shared_ptr来初始化。
    weak_ptr<int> wp(p);  
      
    if(shared_ptr<int> np = wp.lock()) // 访问对象必须调用lock()
    {  
      //使用np访问共享对象  
    }

2、weak_ptr的实现

weak_ptr的作为弱引用指针,其实现依赖于counter的计数器类和share_ptr的赋值,构造

class Counter
{
	public:
		Counter():s(0),w(0){};
		int s;
		int w;
};

s是share_ptr的引用计数,w是weak_ptr的引用计数,当w为0时,删除Counter对象。当weak_ptr是由share_ptr构造或者赋值时,不会增加share_ptr的引用计数,只会增加自身的引用计数。

template<class T>
class WeakPtr
{
public:
    WeakPtr()
    {
        _ptr=0;
        cnt=0;
    }
    WeakPtr(SharePtr<T>& s): _ptr(s._ptr), cnt(s.cnt)
    {
        cout<<"w con s"<<endl;
        cnt->w++;
    }
    WeakPtr(WeakPtr<T>& w): _ptr(w._ptr), cnt(w.cnt)
    {
        cnt->w++;
    }
    ~WeakPtr()
    {
        release();  
    }
    WeakPtr<T>& operator =(WeakPtr<T> & w)
    {
        if(this != &w)
        {
            release();
            cnt=w.cnt;
            cnt->w++;
            _ptr=w._ptr;
        }
        return *this;
    }
    WeakPtr<T>& operator =(SharePtr<T> & s)
    {
        cout<<"w = s"<<endl;
        release();
        cnt=s.cnt;
        cnt->w++;
        _ptr=s._ptr;
        return *this;
    }
    SharePtr<T> lock()
    {
        return SharePtr<T>(*this);
    }
    bool expired()
    {
        if(cnt)
        {
            if(cnt->s >0)
            {
                cout<<"empty "<<cnt->s<<endl;
                return false;
            }
        }
        return true;
    }
    friend class SharePtr<T>;//方便weak_ptr与share_ptr设置引用计数和赋值。
private:
    void release()
    {
        if(cnt)
        {
            cnt->w--;
            cout<<"weakptr release"<<cnt->w<<endl;
            if(cnt->w <1&& cnt->s <1)
            {
                //delete cnt;
                cnt=NULL;
            }
        }
    }
    T* _ptr;
    Counter* cnt;
};

六、unique_ptr类

unique_ptr 独占所指对象,某个时刻只能有一个unique_ptr 指向一个给定对象。当unique_ptr 被销毁时,它所指向的对象也被销毁。unique_ptr 不支持拷贝赋值等操作(因此要实现unique_ptr可以将其拷贝构造函数和赋值构造函数定义为private,禁止拷贝和赋值),除非这个unique_ptr将要被销毁,这种情况,编译器执行一种特殊的"拷贝"。

unique_ptr<int> p1(new int(42));//必须直接初始化。
unique_ptr<int> p2(p1);//error
unique_ptr<int> p3 = p1;/error

unique_ptr<int> clone(int p)
{
  unique_ptr<int> ret(new int(p));
  return ret; //ok
}

智能指针详解_赋值_11


  虽然不能拷贝或者赋值unique_ptr,但是通过调用release或者reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique_ptr。调用release会切断unique_ptr和它原来管理对象间的联系。release返回的指针通常用来初始化另一个智能指针或者给另一个智能指针赋值。

#include <iostream>
#include <memory>
using namespace std;
unique_ptr<int> clone(int p);
int main(int argc, char const *argv[])
{
	unique_ptr<double> p;//
	unique_ptr<string> p1(new string("ABC"));//使用new返回的指针初始化。
	cout << *p1 << endl;

	unique_ptr<string> p2(p1.release());//放弃对p1的控制权,返回指针并置空,然后初始化另一个指针。
	cout << *p2 << endl;

	unique_ptr<string> p3(new string("abc"));
	p2.reset(p3.release());//释放p2的对象,并将p3的所有权转移给p2。
	cout << *p2 << endl;

	cout << *clone(10) << endl;
	return 0;
}
unique_ptr<int> clone(int p)
{
	return unique_ptr<int>(new int(p));//unique_ptr不能拷贝或者赋值,但是可以返回一个unique_ptr。
}

七、unique_ptr类为何优于auto_ptr类

可能大家认为前面的例子已经说明了unique_ptr为何优于auto_ptr,也就是安全问题,下面再叙述的清晰一点。请看下面的语句:

auto_ptr<string> p1(new string ("auto") ; // #1
auto_ptr<string> p2;                      // #2
p2 = p1;                                  // #3

在语句#3中,p2接管string对象的所有权后,p1的所有权将被剥夺。前面说过,这是好事,可防止p1和p2的析构函数试图刪同—个对象;但如果程序随后试图使用p1,这将是件坏事,因为p1不再指向有效的数据。下面来看使用unique_ptr的情况:

unique_ptr<string> p3 (new string ("auto");   // #4
unique_ptr<string> p4;                       // #5
p4 = p3;                                      // #6

编译器认为语句#6非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。但unique_ptr还有更聪明的地方。有时候,会将一个智能指针赋给另一个并不会留下危险的悬挂指针。假设有如下函数定义:

unique_ptr<string> demo(const char * s)
{
    unique_ptr<string> temp (new string (s)); 
    return temp;
}

unique_ptr<string> ps;
ps = demo('Uniquely special");

demo()返回一个临时unique_ptr,然后ps接管了原本归返回的unique_ptr所有的对象,而返回时临时的 unique_ptr 被销毁,也就是说没有机会使用 unique_ptr 来访问无效的数据,换句话来说,这种赋值是不会出现任何问题的,即没有理由禁止这种赋值。实际上,编译器确实允许这种赋值,这正是unique_ptr更聪明的地方。

总之,当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:

unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1;                                      // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You"));   // #2 allowed

其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。

八、使用智能指针需要注意的问题:

智能指针详解_智能指针_12

九、如何选择智能指针?

在掌握了这几种智能指针后,应使用哪种智能指针呢?

  • 如果程序要使用多个指向同一个对象的指针,应选择shared_ptr。这样的情况包括:有一个指针数组,并使用一些辅助指针来标示特定的元素,如最大的元素和最小的元素;两个对象包含都指向第三个对象的指针;STL容器包含指针。很多STL算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr(编译器发出warning)和auto_ptr(行为不确定)。如果你的编译器没有提供shared_ptr,可使用Boost库提供的shared_ptr。
  • 如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。如果函数使用new分配内存,并返还指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。这样,所有权转让给接受返回值的unique_ptr,而该智能指针将负责调用delete。可将unique_ptr存储到STL容器在那个,只要不调用将一个unique_ptr复制或赋给另一个算法(如sort())。


标签:cnt,对象,智能,详解,shared,unique,ptr,指针
From: https://blog.51cto.com/u_6526235/7274787

相关文章

  • Web服务器项目详解
    文章目录一、新连接到来的处理流程二、Channel、TcpConnection、TcpServer、Poller、EventLoop类详解1、Channel类2、TcpConnection类3、TcpServer类4、Poller类5、EventLoop类三、这几个类之间的关系一、新连接到来的处理流程一个新的连接到来后,首先被MainReactor接收,然后通过轮......
  • 纯水设备智能运维管理系统,远程监控,故障预警
    纯水设备适用于生产纯净水的设备,一般包括原水处理设备、海水淡化设备、地下水处理设备、超滤设备等,在电子、化工、食品、医药、海水淡化等领域应用广泛。为增强纯水设备的生产管理与故障预警能力,数之能提供设备智能运维管理系统,旨在提高纯水设备的管理效率和可靠性。包括以下功能: ......
  • 智能菜谱系统-计算机毕业设计源码+LW文档
    1.1研究背景自古以来,烹饪食品一直是人类的基本需求之一,烹饪技术的不断发展和创新,为人们带来了不同的美食体验。科技进步的同时又在不断地加快人们的生活节奏,越来越忙碌的生活节奏使得人们能够花费在制作美食上的时间越来越少;同时,随着生活水平的提高,人们对健康饮食的需求也日益增长......
  • C语言指针进阶
    目录字符指针指针数组数组指针数组指针的定义&数组名VS数组名数组指针的使用数组参数、指针参数一维数组传参二维数组传参一级指针传参二级指针传参一级指针二级指针数组指针函数指针函数指针数组指向函数指针数组的指针回调函数编码的三种境界:1.看代码就是代码2.看代码就是内......
  • ByteBuf用法详解文档
    来源:http://www.taodudu.cc/news/show-3638306.html?action=onClick_____________________________________________________________________________________________ ByteBufbytebuf文档点这里基本信息:ByteBuf类java.lang.Objectio.netty.buffer.ByteBuf所有已实......
  • 软件测试|SQL中的UNION和UNION ALL详解
    简介在SQL(结构化查询语言)中,UNION和UNIONALL是用于合并查询结果集的两个关键字。它们在数据库查询中非常常用,但它们之间有一些重要的区别。在本文中,我们将深入探讨UNION和UNIONALL的含义、用法以及它们之间的区别。UNION操作UNION用于合并两个或多个查询的结果集,并返回一个唯一的......
  • 智能存储控制器行业市场调查趋势分析报告2023-2029
    2023-2029全球智能存储控制器行业调研及趋势分析报告2022年全球智能存储控制器市场规模约亿元,2018-2022年年复合增长率CAGR约为%,预计未来将持续保持平稳增长的态势,到2029年市场规模将接近亿元,未来六年CAGR为%。从核心市场看,中国智能存储控制器市场占据全球约%的市场份额,为全......
  • 软件测试|Python中的变量与关键字详解
    简介在Python编程中,变量和关键字是非常重要的概念。它们是构建和控制程序的基本要素。本文将深入介绍Python中的变量和关键字,包括它们的定义、使用方法以及一些常见注意事项。变量变量的定义变量是用于存储数据值的名称。在Python中,变量无需声明,可以直接赋值使用。变量可以存储不同......
  • 释放人工智能的力量:彻底改变保险单的风险评估
    人工智能(AI)的新兴技术正在改变保险世界。风险评估是保险政策的关键部分,正在发生革命性的变化。借助AI,公司可以以无与伦比的精度、速度和效率分析和预测风险。这场技术革命并非遥不可及的未来。它正在发生,改变保险公司的运营方式并改变客户体验。了解保单中的风险评估风险评估是......
  • 共用体详解
    共用体同结构体的定义形式上相同,只是把关键字struct改为union。有时需要把几种不同类型的变量放在同有一内存区域中,见图12-6,把一个整型变量,一个字符变量,一个实型变量放在同一内存区域中,尽管三个变量占用字节数各不相同,但起始地址都一样(例如1000)它要用“覆盖’’技术,使多个变量互相......