C++每日一问:智能指针的类别及原理实现

智能指针

1 智能指针的作用

      智能指针是一个类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源

STL一共给我们提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr和weak_ptr。模板auto_ptr是C++98提供的解决方案,C+11已将将其摒弃,并提供了另外两种解决方案。然而,虽然auto_ptr被摒弃,但它已使用了好多年:同时,如果您的编译器不支持其他两种解决力案,auto_ptr将是唯一的选择。

2 智能指针的种类

        shared_ptr、unique_ptr、weak_ptr、auto_ptr 

(1) shared_ptr 

        实现原理:采用引用计数器的方法,允许多个智能指针指向同一个对象,每当多一个指针指向该对象时,指向该对象的所有智能指针内部的引用计数加1,每当减少一个智能指针指向对象时,引用计数会减1,当计数为0的时候会自动的释放动态分配的资源。 

        1) 智能指针将一个计数器与类指向的对象相关联,引用计数器跟踪共有多少个类对象共享同一指针;

         2) 每次创建类的新对象时,初始化指针并将引用计数置为1;

         3) 当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;

         4) 对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;

         5) 调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。

(2) unique_ptr 

       unique_ptr采用的是独享所有权语义,一个非空的unique_ptr总是拥有它所指向的资源。转移一个unique_ptr将会把所有权全部从源指针转移给目标指针,源指针被置空;所以unique_ptr不支持普通的拷贝和赋值操作,不能用在STL标准容器中;局部变量的返回值除外(因为编译器知道要返回的对象将要被销毁);如果你拷贝一个unique_ptr,那么拷贝结束后,这两个unique_ptr都会指向相同的资源,造成在结束时对同一内存指针多次释放而导致程序崩溃。

(3) weak_ptr 

      weak_ptr:弱引用。 引用计数有一个问题就是互相引用形成环(环形引用),这样两个指针指向的内存都无法释放。需要使用weak_ptr打破环形引用。weak_ptr是一个弱引用,它是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是说,它只引用,不计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前使用函数lock()检查weak_ptr是否为空指针。

(4) auto_ptr 

       auto_ptr不支持拷贝和赋值操作,不能用在STL标准容器中。STL容器中的元素经常要支持拷贝、赋值操作,在这过程中auto_ptr会传递所有权,auto_ptr采用的是独享所有权语义,一个非空的unique_ptr总是拥有它所指向的资源。转移一个auto_ptr将会把所有权全部从源指针转移给目标指针,源指针被置空。

智能指针代码实现: 用两个类来实现智能指针的功能,一个是引用计数类,另一个则是指针类。

(1) 引用计数类

//  引用计数器类  用于存储指向同一对象的指针数
template<typename T>
class Counter
{
    
    
private:
	//  数据成员
	T *ptr;    //  对象指针
	int cnt;   //  引用计数器
 
	//  友元类声明
	template<typename T>
	friend class SmartPtr;
 
	//  成员函数
	//  构造函数
	Counter(T *p)   //  p为指向动态分配对象的指针
	{
    
    
		ptr = p;
		cnt = 1;
	}
	//  析构函数
	~Counter()
	{
    
    
		delete ptr;
	}
};

(2) 指针类

//  智能指针类  
template<typename T>
class SmartPtr
{
    
    
private:
	//  数据成员
	Counter<T> *ptr_cnt;  //  
 
public:
 
	//  成员函数
	//  普通构造函数  初始化计数类
	SmartPtr(T *p)
	{
    
    
		ptr_cnt = new Counter<T>(p);
	}
	//  拷贝构造函数
	SmartPtr(const SmartPtr &other)
	{
    
    
		ptr_cnt = other.ptr_cnt;
		ptr_cnt->cnt++;
	}
	//  赋值运算符重载函数
	SmartPtr &operator=(const SmartPtr &rhs)
	{
    
    
		ptr_cnt = rhs->ptr_cnt;
		rhs.ptr_cnt->cnt++;
		ptr_cnt->cnt--;
		if (ptr_cnt->cnt == 0)
			delete ptr_cnt;
		return *this;
	}
	//  解引用运算符重载函数
	T &operator*()
	{
    
    
		return *(ptr_cnt->cnt);
	}
 
	//  析构函数
	~SmartPtr()
	{
    
    
		ptr_cnt->cnt--;
		if (ptr_cnt->cnt == 0)
			delete ptr_cnt;
		else
			cout << "还有" << ptr_cnt->cnt << "个指针指向基础对象" << endl;
	}
};

(3) 使用与测试

#include<iostream>
 
using namespace std;
 
 
//  测试函数
void test()
{
    
    
	int *p = new int(42);
	{
    
    
		SmartPtr<int> sptr1(p);  //  出了作用域,计数器减1
		{
    
    
			SmartPtr<int> sptr2(sptr1);  //  出了作用域,计数器减1
			{
    
    
				SmartPtr<int> sptr3(sptr1);  //  出了作用域,计数器减1
			}
		}
	}
	cout << *p << endl;  //  动态分配的对象已被释放,故输出垃圾值
}
 
//  主函数
int main()
{
    
    
	test();
	return 0;
}

运行结果如下:

        

      如图所示,在离开大括号后,共享基础对象的指针计数从3->2->1->0变换,最后计数为0时,指针p指向的动态分配对象被delete,此时使用*p已经获取不到原来的值,故而产生垃圾值。

注意:引用计数问题或循环引用

3. 如何选择智能指针?

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

智能指针

猜你喜欢

转载自blog.csdn.net/m0_37251750/article/details/121027699