【学习点滴】c++智能指针(手写一个),及线程安全性

目录

原始指针的问题

智能指针

问题:share_ptr是线程安全的吗?其底层实现是怎么样的?

手撸一个简单的智能指针


原始指针的问题

使用 C++的指针可以动态开辟存储空间,但若在使用完毕后忘记释放(或在释放之前,程序 throw 出错误,导致没有释放),导致该内存单元一直被占据直到程序结束,即发生了所谓的内存泄漏。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。

智能指针正好能够弥补这些问题,因为它本质是存放在栈的模板对象,只是在栈内部包了一层指针而栈在其生命周期结束时,其中的指针指向的堆内存也自然被释放了。因而实现了智能管理的效果,不需要考虑内存问题了,其实有点类似某种单例写法,程序运行结束,也不用考虑单例对象内存问题。

【注】内存泄漏是指堆内存的泄漏。堆,就是那些由 new 分配的内存块。

       情况1: int * p = new int(0);   忘记用delete释放,

       情况2:int * p = new int(0);

                     delete p;               p为置nullptr,会变成野指针

       情况3:int * p = new int(0);

                     delete p;

                     p=nullptr;              如果内存申请不成功,new会抛出异常,而我们却什么都没有做!

       情况4:int *ptr = new(nothrow) int(0);

      if(!ptr)

      {

          cout << "new fails."

         return 0;

       }

       if (hasException())               //若程序突然发生异常,就转到异常处理函数去了,最终导致程序终止,下面就没有释放

         throw exception();

     delete ptr;

     ptr = nullptr;

当然,我们可以在“hasException()”为真时释放内存:但,我们并不总会想到这么做。而且,这样子做也显得麻烦,不够人性化。

智能指针

因此智能指针的作用就是为了保证使用堆上对象的时候,对象一定会被释放,但只能释放一次,并且释放后指向该对象的指针应该马上归 0。

为什么要使用智能指针:我们知道c++的内存管理是让很多人头疼的事,当我们写一个new语句时,一般就会立即把delete语句直接也写了,但是我们不能避免程序还未执行到delete时就跳转了或者在函数中没有执行到最后的delete语句就返回了,如果我们不在每一个可能跳转或者返回的语句前释放资源,就会造成内存泄露。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。

我们使用智能指针的原因至少有以下三点:

1)智能指针能够帮助我们处理资源泄露问题;

2)它也能够帮我们处理空悬指针(野指针)的问题;

3)它还能够帮我们处理比较隐晦的由异常造成的资源泄露

智能指针在C++11版本之后提供,包含在头文件<memory>中,shared_ptr、unique_ptr、weak_ptr。

  1. unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。(不用手动去delete)
  2. shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。

几乎每一个有分量的程序都需要“在相同时间的多处地点处理或使用对象”的能力。为此,我们必须在程序的多个地点指向(refer to)同一对象。虽然C++语言提供引用(reference)和指针(pointer),还是不够,因为我们往往必须确保当“指向对象”的最末一个引用被删除时该对象本身也被删除,毕竟对象被删除时析构函数可以要求某些操作,例如释放内存或归还资源等等。

所以我们需要“当对象再也不被使用时就被清理”的语义。Class shared_ptr提供了这样的共享式拥有语义。也就是说,多个shared_ptr可以共享(或说拥有)同一对象。对象的最末一个拥有者有责任销毁对象,并清理与该对象相关的所有资源。

shared_ptr的目标就是,在其所指向的对象不再被使用之后(而非之前),自动释放与对象相关的资源。

weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。指针a指向类q,指针b指向类w,类q和类w中都有一个智能指针q.i和w.o,此时令a.i=b, b.o=a; 就形成了相互引用的死锁,可以看到fun函数中a,b之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针a,b析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(q,w的析构函数没有被调用)如果把其中一个改为weak_ptr就可以了。

auto_ptr是用于C++11之前的智能指针。由于 auto_ptr 基于排他所有权模式:两个指针不能指向同一个资源,复制或赋值都会改变资源的所有权。auto_ptr 主要有两大问题:

o 复制和赋值会改变资源的所有权,不符合人的直觉。
o 在 STL 容器中无法使用auto_ptr ,因为容器内的元素必需支持可复制(copy constructable)和可赋值(assignable)。

unique_ptr特性
o 拥有它所指向的对象
o 无法进行复制构造,也无法进行复制赋值操作(拷贝构造和赋值构造被=delete
o 保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。

unique_ptr可以:
o 为动态申请的内存提供异常安全
o 将动态申请内存的所有权传递给某个函数
o 从某个函数返回动态申请内存的所有权
o 在容器中保存指针
unique_ptr十分依赖于右值引用和移动语义。

 

例子1:unique_ptr

class test {
public:

	test() {

	}

	test(int id,const char*name) {
		this->id = id;
		this->name = new char[strlen(name) + 1];		//开辟空间
		strcpy(this->name, name);
	}
	void func() {
		cout << "success!"<< endl;
	}
private:
	int id;
	char*  name;
};

int main() {
	unique_ptr<test> uni(new test);
	//unique_ptr<test> b = uni;		//不能进行控制权转换,因为unique_ptr中的拷贝构造和赋值操作符delete了,所以也就意味着,他和auto_ptr有区别,控制权唯一
	unique_ptr<test>base2 = move(uni);//base1变成empty     但是使用move函数可以实现,把右值的对象(right)移动给左值(_myt&),并且右值清空。


	/*
	int main(){
		std::unique_ptr<int> uptr(new int(10));  //绑定动态对象
		//std::unique_ptr<int> uptr2 = uptr;  //不能賦值
		//std::unique_ptr<int> uptr2(uptr);  //不能拷貝
		std::unique_ptr<int> uptr2 = std::move(uptr); //轉換所有權
		uptr2.release(); //释放所有权
	}
	//超過uptr的作用域,內存釋放
	*/
}

例子2:share_ptr

	shared_ptr<test> s1(new test(1,"xiaoli"));
	shared_ptr<test> s2 = s1;
	shared_ptr<test> s3 = s2;
	cout << s1.use_count() << endl;			//输出此类被几个share指针使用了
	s3.reset();								//计数减1
	cout << s1.use_count() << endl;
	/*
		int a = 10;
		std::shared_ptr<int> ptra = std::make_shared<int>(a);
	*/

例子3:weak_ptr

	shared_ptr<int> sh_ptr = make_shared<int>(10);
	cout << sh_ptr.use_count() << endl;

	weak_ptr<int> wp(sh_ptr);				//提供类似观测器的功能,不能操作资源
	cout << wp.use_count() << endl;

	if (!wp.expired()) {					//判断此weak指针是否允许lock
		shared_ptr<int> sh_ptr2 = wp.lock(); //get another shared_ptr		lock后可产生一个由weak变成正常share的指针,从而操作资源
		*sh_ptr = 100;
		cout << wp.use_count() << endl;					//这时候use_count()就会计数了
	}

循环引用问题:

#include<iostream>
#include<memory>

using namespace std;

class B;
class A
{
public:
	//A();
	~A() {
		cout << "kill A\n";
	}
	shared_ptr<B> pb;
private:
	

};

class B
{
public:
	//B();
	~B() {
		cout << "kill B\n";
	}
	shared_ptr<A> pa;
private:
	
};


int main() {
	shared_ptr<A> sa(new A()); //new出来的A对象引用计数为1
	shared_ptr<B> sb(new B());
	if (sa && sb)
	{
		sa->pb = sb;
		sb->pa = sa;    //A对象引用计数变为2
	}
	cout << "sa use count:" << sa.use_count() << endl;
	cout << "sb use count:" << sb.use_count() << endl;


    //函数结束,sa析构,A的引用计数变为1,但永远不会为0。造成内存泄漏
    //同理B的引用计数也不会为0
	return 0;
}

如此一来,A和B都互相指着对方吼,“放开我的引用!“,“你先放我的我就放你的!”,于是悲剧发生了。

 

问题:share_ptr是线程安全的吗?其底层实现是怎么样的?

作者:陈硕 
原文:https://blog.csdn.net/solstice/article/details/8547547 

先说结论:因为 shared_ptr 有两个数据成员,读写操作不能原子化”使得多线程读写同一个 shared_ptr 对象需要加锁。

 

shared_ptr 的数据结构
shared_ptr 是引用计数型(reference counting)智能指针,几乎所有的实现都采用在堆(heap)上放个计数值(count)的办法(除此之外理论上还有用循环链表的办法,不过没有实例)。具体来说,shared_ptr<Foo> 包含两个成员,一个是指向 Foo 的指针 ptr,另一个是 ref_count 指针(其类型不一定是原始指针,有可能是 class 类型,但不影响这里的讨论),指向堆上的 ref_count 对象。ref_count 对象有多个成员,具体的数据结构如图 1 所示,其中 deleter 和 allocator 是可选的。

               图 1:shared_ptr 的数据结构。

为了简化并突出重点,后文只画出 use_count 的值:

以上是 shared_ptr<Foo> x(new Foo); 对应的内存数据结构。

如果再执行 shared_ptr<Foo> y = x; 那么对应的数据结构如下。

但是 y=x 涉及两个成员的复制,这两步拷贝不会同时(原子)发生。

中间步骤 1,复制 ptr 指针:

中间步骤 2,复制 ref_count 指针,导致引用计数加 1:

步骤1和步骤2的先后顺序跟实现相关(因此步骤 2 里没有画出 y.ptr 的指向),我见过的都是先1后2。

既然 y=x 有两个步骤,如果没有 mutex 保护,那么在多线程里就有 race condition。
 

多线程无保护读写 shared_ptr 可能出现的 race condition
考虑一个简单的场景,有 3 个 shared_ptr<Foo> 对象 x、g、n:

shared_ptr<Foo> g(new Foo); // 线程之间共享的 shared_ptr
shared_ptr<Foo> x; // 线程 A 的局部变量
shared_ptr<Foo> n(new Foo); // 线程 B 的局部变量


一开始,各安其事。

线程 A 执行 x = g; (即 read g),以下完成了步骤 1,还没来及执行步骤 2。这时切换到了 B 线程。

同时编程 B 执行 g = n; (即 write g),两个步骤一起完成了。

先是步骤 1:

再是步骤 2:

这时 Foo1 对象已经销毁,x.ptr 成了空悬指针!

最后回到线程 A,完成步骤 2:

多线程无保护地读写 g,造成了“x 是空悬指针”的后果。这正是多线程读写同一个 shared_ptr 必须加锁的原因。

当然,race condition 远不止这一种,其他线程交织(interweaving)有可能会造成其他错误。

思考,假如 shared_ptr 的 operator= 实现是先复制 ref_count(步骤 2)再复制 ptr(步骤 1),会有哪些 race condition?
 

从源码角度看:

shared_ptr继承了下面的模板类,用它来管理引用计数。其中有两个变量一个表示shared_ptr的引用数,另外一个表示weak_ptr的引用数,我们知道weak_ptr不会增加只能指针的引用数也就是说不持有对象,他的使用必须通过lock方法获取它指向的shared_ptr才能使用。

template<_Lock_policy _Lp = __default_lock_policy>
   class _Sp_counted_base
   : public _Mutex_base<_Lp>
   {
   public:  
     _Sp_counted_base() noexcept
     : _M_use_count(1), _M_weak_count(1) { }
     
     virtual
     ~_Sp_counted_base() noexcept
     { }
 
     //当_M_use_count为0时调用,是个纯虚函数(必须实现),这个函数的作用是释放指针指向的对象所持有的资源,即*this
     virtual void
     _M_dispose() noexcept = 0;
     
     // 当_M_weak_count为0时调用,释放自己本身的资源,即this
     //  _M_weak_count = _M_weak_count + (_M_use_count!= 0),当_M_weak_count和_M_use_count都为0时释放this
     virtual void
     _M_destroy() noexcept
     { delete this; }
     
     virtual void*
     _M_get_deleter(const std::type_info&) noexcept = 0;
 
    //增加一个引用
     void
     _M_add_ref_copy()
     { __gnu_cxx::__atomic_add_dispatch(&_M_use_count, 1); }
 
     void
     _M_add_ref_lock();
 
     bool
     _M_add_ref_lock_nothrow();
 
     void
     _M_release() noexcept
     {
       _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_use_count);
  //首先use_count减去1,并对比减操作之前的值,如果减之前是1,说明减后是0,a1没有任何shared_ptr指针指向它了将销毁对象
if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1)
  {
           _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_use_count);
    _M_dispose();
    //如果destory和dispose存在内存屏障,保证dispose函数的效果在destory函数的调用该线程的可见性
    if (_Mutex_base<_Lp>::_S_need_barriers)
      {
   __atomic_thread_fence (__ATOMIC_ACQ_REL);
      }
 
    //同时对a1的weak_count减去1,也对比减操作之前的值,如果减之前是1,说明减后是0,a1没有weak_ptr指向它了,
    //应该将管理对象销毁,于是调用_M_destroy()销毁了管理对象
           _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_weak_count);
    if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count,
                      -1) == 1)
             {
               _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_weak_count);
        _M_destroy();
             }
  }
     }
 
     void
     _M_weak_add_ref() noexcept
     { __gnu_cxx::__atomic_add_dispatch(&_M_weak_count, 1); }
 
     void
     _M_weak_release() noexcept
     {
       _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_weak_count);
if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count, -1) == 1)
  {
           _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_weak_count);
    if (_Mutex_base<_Lp>::_S_need_barriers)
      {
   __atomic_thread_fence (__ATOMIC_ACQ_REL);
      }
    _M_destroy();
  }
     }
 
    //获取引用计数 
     long
     _M_get_use_count() const noexcept
     {
       return __atomic_load_n(&_M_use_count, __ATOMIC_RELAXED);
     }
 
   private:  
     _Sp_counted_base(_Sp_counted_base const&) = delete;
     _Sp_counted_base& operator=(_Sp_counted_base const&) = delete;
 
     _Atomic_word  _M_use_count;   
     _Atomic_word  _M_weak_count;   
   };

这里的智能指针的引用计数在手段上使用了atomic原子操作,只要在shared_ptr在拷贝或赋值时增加引用,析构时减少引用就可以了。

虽然通过原子操作解决了引用计数的计数的线程安全问题, 但是智能指针指向的对象的线程安全问题,智能指针没有做任何的保证。  首先智能指针有两个变量,一个是指向的对象的指针,还有一个就是我们上面看到的引用计数管理对象, 当智能指针发生拷贝的时候,标准库的实现是先拷贝智能指针,再拷贝引用计数对象(拷贝引用计数对象的时候,会使use_count加一),这两个操作并不是原子的,隐患就出现在这里。
 

手撸一个简单的智能指针

原文链接:https://blog.csdn.net/u013611405/article/details/88047741

写的很棒,做了一点点修改

一句话介绍shared_ptr智能指针:多个shared_ptr中的T *ptr能指向同一个内存区域(同一个对象),并共同维护同一个引用计数器。

一般来说,智能指针的实现需要以下步骤:
1.一个模板指针T* ptr,指向实际的对象。
2.一个引用次数(必须new出来的,不然会多个shared_ptr里面会有不同的引用次数而导致多次delete)。

3.重载operator*和operator->,使得能像指针一样使用shared_ptr。
4.重载copy constructor,使其引用次数加一。
5.重载operator=,如果原来的shared_ptr已经有对象,则让其引用次数减一并判断引用是否为零(是否调用delete)。
 然后将新的对象引用次数加一。
6.重载析构函数,使引用次数减一并判断引用是否为零(是否调用delete)。
 

#include<iostream>
#include <string>
using namespace std;

template <typename T>
class Shared_ptr {
public:
	// 空参构造 空指针
	Shared_ptr() :count(0), _ptr((T*)0) {};
	// 构造函数 count必须new出来
	Shared_ptr(T* p) : count(new int(1)), _ptr(p) {};
	// 拷贝构造函数 使其引用次数加一
	Shared_ptr(Shared_ptr<T>& another) :count(&(++ *another.count)), _ptr(another._ptr) {};
	// 重载 operator*和operator-> 实现指针功能
	T* operator->() { return _ptr; };
	T& operator*() { return *_ptr; };
	// 重载operator=
	// 如果原来的Shared_ptr已经有对象,则让其引用次数减一并判断引用是否为零(是否调用delete)。
	// 然后将新的对象引用次数加一。
	Shared_ptr<T>& operator=(Shared_ptr<T>& another) {
		if (this == &another)
			return *this;

		++ *another.count;
		if (this->_ptr && 0 == --*this->count) {	//本指针原有对象计数为1,赋值之后应该析构
			delete count;
			delete _ptr;
			cout << "delete ptr in =" << endl;
		}

		this->_ptr = another._ptr;
		this->count = another.count;
		return *this;
	}

	// 析构函数 使引用次数减一并判断引用是否为零(是否调用delete)。
	~Shared_ptr()
	{
		if (_ptr && 0 == --*count) {
			delete count;
			delete _ptr;
			cout << "delete ptr in ~" << endl;
		}
	}

	int getRef() { return *count; }

private:
	T* _ptr;
	int* count; // should be int*, rather than int
};

 

添加测试:

int main() {

	Shared_ptr<string> p1(new string("abc"));
	cout << "p1's ref: " << p1.getRef() << " ,p1's obj: " << *p1 << endl;

	Shared_ptr<string> p2(p1);
	cout << "after p2 share with p1: " << endl;
	cout<< "p1's ref: " << p1.getRef() << " ,p1's obj: " << *p1 << endl;
	cout << "p2's ref: " << p2.getRef() << " ,p2's obj: " << *p2 << endl;

	cout << "creat p3 = hello" << endl;
	Shared_ptr<string> p3(new string("hello"));
	cout << "p3's ref: " << p3.getRef() << " ,p3's obj: " << *p3 << endl;

	cout << "after p3 = p2:" << endl;
	p3 = p2;
	cout << "p1's ref: " << p1.getRef() << " ,p1's obj: " << *p1 << endl;
	cout << "p2's ref: " << p2.getRef() << " ,p2's obj: " << *p2 << endl;
	cout << "p3's ref: " << p3.getRef() << " ,p3's obj: " << *p3 << endl;

	system("pause");
	return 0;
}

结果如下:

 

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ActiViz是一个基于C#的开源数据可视化库,它提供了一系列用于创建和呈现2D和3D图形的功能。如果你想学习ActiViz,以下是一些学习点滴: 1. 理解ActiViz的基本概念:开始学习之前,了解ActiViz的基本概念是很重要的。了解ActiViz的工作原理、主要组件和使用方式,可以帮助你更好地理解和应用它。 2. 安装和配置ActiViz:在开始使用ActiViz之前,你需要将其安装到你的开发环境中。阅读官方文档或教程,按照指示进行安装和配置。 3. 学习ActiViz的API:ActiViz提供了丰富的API,用于创建和操作图形对象。学习这些API的用法和功能,可以帮助你更好地使用ActiViz来实现你的需求。 4. 创建基本图形对象:开始学习ActiViz时,从创建一些基本的图形对象开始是一个不错的选择。尝试创建点、线、多边形等基本图形对象,并学习如何对它们进行操作和渲染。 5. 了解数据可视化技术:ActiViz最常用的用途之一是数据可视化。学习如何使用ActiViz来可视化不同类型的数据,如二维数据、三维数据、图像数据等,可以帮助你更好地应用ActiViz来分析和展示数据。 6. 阅读官方文档和示例代码:ActiViz有详细的官方文档和示例代码,可以帮助你更深入地了解和使用ActiViz。阅读官方文档和运行示例代码,可以帮助你学习一些高级功能和技巧。 7. 参与开源社区:ActiViz是一个开源项目,有一个活跃的社区。参与到ActiViz的开发和讨论中,可以帮助你与其他开发者交流和学习,同时也可以为ActiViz的发展做出贡献。 希望这些学习点滴对你有帮助!祝你学***

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值