C++11特性:共享智能指针

在C++中没有垃圾回收机制,必须自己释放分配的内存,否则就会造成内存泄露。解决这个问题最有效的方法是使用智能指针(smart pointer)。智能指针是存储指向动态分配(堆)对象指针的类,用于生存期的控制,能够确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露。智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。

C++11中提供了三种智能指针,使用这些智能指针时需要引用头文件

1. std::shared_ptr:共享的智能指针
2. std::unique_ptr:独占的智能指针
3. std::weak_ptr:弱引用的智能指针,它不共享指针,不能操作资源,是用来监视shared_ptr的。

弱引用的智能指针是辅助共享的智能指针的。

共享的智能指针独占的智能指针区别

C++11特性:共享智能指针_第1张图片

假设new出来一块int类型的内存,使用共享的智能指针可以有多个共享的智能指针同时管理。

在每个智能指针的内部都有一个引用计数,记录的当前的这块内存被多少个智能指针共同管理着。

当这个智能指针被析构或者重置的时候,计数就会减一。注意在最后一个智能指针析构前,会把管理的内存一并析构掉。

由上述这个例子不难看出,共享智能指针可以多个指针同时管理,那么独占智能指针不允许多个指针同时管理一块内存。

后来的这个独占智能指针想要管理这块内存,就需要进行资源的转移,也就是使用move函数实现资源的转移,这样原来的独占智能指针就不能管理这块内存了。所以独占智能指针的计数永远为1

1. shared_ptr的初始化:

共享智能指针是指多个智能指针可以同时管理同一块有效的内存,共享智能指针shared_ptr 是一个模板类,如果要进行初始化有三种方式:通过构造函数std::make_shared辅助函数以及reset方法。共享智能指针对象初始化完毕之后就指向了要管理的那块堆内存,如果想要查看当前有多少个智能指针同时管理着这块内存可以使用共享智能指针提供的一个成员函数use_count,函数原型如下:

// 管理当前对象的 shared_ptr 实例数量,或若无被管理对象则为 0。
long use_count() const noexcept;

 1.1 通过构造函数初始化

// shared_ptr 类模板中,提供了多种实用的构造函数, 语法格式如下:
std::shared_ptr 智能指针名字(创建堆内存);

测试代码如下:

#include 
#include 
using namespace std;

int main()
{
    // 使用智能指针管理一块 int 型的堆内存
    shared_ptr ptr1(new int(520));
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
    // 使用智能指针管理一块字符数组对应的堆内存
    shared_ptr ptr2(new char[12]);
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
    // 创建智能指针对象, 不管理任何内存
    shared_ptr ptr3;
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
    // 创建智能指针对象, 初始化为空
    shared_ptr ptr4(nullptr);
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;

    return 0;
}

测试代码输出的结果如下:

ptr1管理的内存引用计数: 1
ptr2管理的内存引用计数: 1
ptr3管理的内存引用计数: 0
ptr4管理的内存引用计数: 0

注意如果智能指针被初始化了一块有效内存,那么这块内存的引用计数+1,如果智能指针没有被初始化或者被初始化为nullptr空指针,引用计数不会+1。另外,不要使用一个原始指针初始化多个shared_ptr。

int *p = new int;
shared_ptr p1(p);
shared_ptr p2(p);		// error, 编译不会报错, 运行会出错

1.2 通过拷贝和移动构造函数初始化 

当一个智能指针被初始化之后,就可以通过这个智能指针初始化其他新对象。在创建新对象的时候,对应的拷贝构造函数或者移动构造函数就被自动调用了。

#include 
#include 
using namespace std;

int main()
{
    // 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1
    shared_ptr ptr1(new int(520));
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
    //调用拷贝构造函数
    shared_ptr ptr2(ptr1);
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
    shared_ptr ptr3 = ptr1;
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
    //调用移动构造函数
    shared_ptr ptr4(std::move(ptr1));
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;
    std::shared_ptr ptr5 = std::move(ptr2);
    cout << "ptr5管理的内存引用计数: " << ptr5.use_count() << endl;

    return 0;
}

测试程序输入的结果:

ptr1管理的内存引用计数: 1
ptr2管理的内存引用计数: 2
ptr3管理的内存引用计数: 3
ptr4管理的内存引用计数: 3
ptr5管理的内存引用计数: 3

通过拷贝构造函数的初始化: 

上述代码中的调用拷贝构造函数初始化,首先用ptr1来初始化ptr2,然后调用拷贝构造来初始化ptr3,这时候ptr2和ptr3都指向相同的一块内存,并且引用计数加一。 

 通过移动构造函数的初始化: 

 移动构造就需要右值引用了,右值引用就需要右值来进行初始化了。右值分为纯右值和将亡值,这里使用的是将亡值。使用move函数就可得到将亡值了。上述代码中通过转移的方式将ptr2的资源转移给了ptr5,引用计数并不能加一

1.3 通过std::make_shared初始化

通过C++提供的std::make_shared() 就可以完成内存对象的创建并将其初始化给智能指针,函数原型如下:

template< class T, class... Args >
shared_ptr make_shared( Args&&... args );

1. T:模板参数的数据类型 

 2.  Args&&... args :要初始化的数据,如果是通过make_shared创建对象,需按照构造函数的参数列表指定

通过make_shared就能创建一块普通类型的内存,也可以去创建某一个对象对应的内存


测试代码如下:

#include 
#include 
#include 
using namespace std;

class Test
{
public:
    Test() 
    {
        cout << "construct Test..." << endl;
    }
    Test(int x) 
    {
        cout << "construct Test, x = " << x << endl;
    }
    Test(string str) 
    {
        cout << "construct Test, str = " << str << endl;
    }
    ~Test()
    {
        cout << "destruct Test ..." << endl;
    }
};

int main()
{
    // 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1
    shared_ptr ptr1 = make_shared(520);
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;

    shared_ptr ptr2 = make_shared();
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;

    shared_ptr ptr3 = make_shared(520);
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;

    shared_ptr ptr4 = make_shared("我是要成为海贼王的男人!!!");
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;
    return 0;
}

使用std::make_shared()模板函数可以完成内存地址的创建,并将最终得到的内存地址传递给共享智能指针对象管理。如果申请的内存是普通类型,通过函数的()可完成地址的初始化,如果要创建一个类对象,函数的()内部需要指定构造对象需要的参数,也就是类构造函数的参数。

1.4 通过 reset方法初始化

共享智能指针类提供的std::shared_ptr::reset方法函数原型如下:

void reset() noexcept;

template< class Y >
void reset( Y* ptr );

template< class Y, class Deleter >
void reset( Y* ptr, Deleter d );

template< class Y, class Deleter, class Alloc >
void reset( Y* ptr, Deleter d, Alloc alloc );

1. ptr:指向要取得所有权的对象的指针
2. d:指向要取得所有权的对象的指针
3. aloc:内部存储所用的分配器 

测试代码如下:

#include 
#include 
#include 
using namespace std;

int main()
{
    // 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1
    shared_ptr ptr1 = make_shared(520);
    shared_ptr ptr2 = ptr1;
    shared_ptr ptr3 = ptr1;
    shared_ptr ptr4 = ptr1;
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;

    ptr4.reset();
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;

    shared_ptr ptr5;
    ptr5.reset(new int(250));
    cout << "ptr5管理的内存引用计数: " << ptr5.use_count() << endl;

    return 0;
}

测试代码输入的结果:

ptr1管理的内存引用计数: 4
ptr2管理的内存引用计数: 4
ptr3管理的内存引用计数: 4
ptr4管理的内存引用计数: 4
    
ptr1管理的内存引用计数: 3
ptr2管理的内存引用计数: 3
ptr3管理的内存引用计数: 3
ptr4管理的内存引用计数: 0
    
ptr5管理的内存引用计数: 1

对于一个未初始化的共享智能指针,可以通过reset方法来初始化,当智能指针中有值的时候,调用reset会使引用计数减1。

 简单来说reset有两个功能

1. 使指向某块内存的智能指针解除对这块内存的管理。

2. 让这个指针管理另外一块内存,相当于做了智能指针的重新初始化。

1.5 获取原始指针

通过智能指针可以管理一个普通变量或者对象的地址,此时原始地址就不可见了。当我们想要修改变量或者对象中的值的时候,就需要从智能指针对象中先取出数据的原始内存的地址再操作,解决方案是调用共享智能指针类提供的get()方法,其函数原型如下:

T* get() const noexcept;

当一个智能指针管理一块内存的时候,获取的原始指针是什么类型,这个T*返回的就是什么类型。 也就是智能指针管理的这块内存的类型

测试代码如下:

#include 
#include 
#include 
using namespace std;

int main()
{
    int len = 128;
    shared_ptr ptr(new char[len]);
    // 得到指针的原始地址
    char* add = ptr.get();
    memset(add, 0, len);
    strcpy(add, "我是要成为海贼王的男人!!!");
    cout << "string: " << add << endl;
    
    shared_ptr p(new int);
    *p = 100;
    cout << *p.get() << "  " << *p << endl;
    
    return 0;
}

 接下来通过一个示例程序来更加清楚的了解上述的前四个用法:

代码如下:

#include
#include
using namespace std;

class Test
{
public:
	Test()
	{
		cout << "construct Test..." << endl;
	}

	Test(int x) : m_num(x)
	{
		cout << "construct Test,x = " << x << endl;
	}

	Test(string str)
	{
		cout << "construct Test,str = " << str << endl;
	}

	~Test()
	{
		cout << "destruct Test..." << endl;
	}

	void setValue(int v)
	{
		m_num = v;
	}

	void print()
	{
		cout << "m_num:" << m_num << endl;
	}

private:
	int m_num;
};

int main()
{
	// 通过构造函数初始化
	shared_ptr ptr1(new int(3));
	cout << "ptr1 use_count:" << ptr1.use_count() << endl;

	// 移动构造和拷贝构造函数初始化
	// 移动构造:
	shared_ptr ptr2 = move(ptr1);
	cout << "ptr1 use_count:" << ptr1.use_count() << endl;
	cout << "ptr2 use_count:" << ptr2.use_count() << endl;
	// 拷贝构造:
	// 需要一个实例对象来初始化,所以要使用ptr2来初始化
	// 因为ptr1的资源已经转移到ptr2中了
	shared_ptr ptr3 = ptr2;
	cout << "ptr3 use_count:" << ptr3.use_count() << endl;
	cout << "ptr2 use_count:" << ptr2.use_count() << endl;

	// 通过std::make_shared初始化
	shared_ptr ptr4 = make_shared(8);// ()中的数就是给给new出来的这个int类型内存进行初始化
	shared_ptr ptr5 = make_shared(8);
	shared_ptr ptr6 = make_shared("luffy");

	// 通过reset初始化
	ptr6.reset();// 指针重置,引用计数变为0
	cout << "ptr6 use_count:" << ptr6.use_count() << endl;

	ptr5.reset(new Test("ace"));// 管理另一块内存
	cout << "ptr5 use_count:" << ptr5.use_count() << endl;

	// 获取原始指针




	return 0;
}

输出结果为:

ptr1 use_count:1
ptr1 use_count:0
ptr2 use_count:1
ptr3 use_count:2
ptr2 use_count:2
construct Test,x = 8
construct Test,str = luffy
destruct Test...
ptr6 use_count:0
construct Test,str = ace
destruct Test...
ptr5 use_count:1
destruct Test...

对输出结果进行分析:

当ptr1创建成功之后,他的引用计数就是1。然后通过移动拷贝构造函数转移ptr1的资源到ptr2,所以ptr1的引用计数变为0,ptr2的引用计数变为1。紧接着,ptr3进行的是一个拷贝构造,所以他的引用计数和ptr2一样为2,也就是说这块内存由ptr2和ptr3共同管理着。

ptr4和ptr5分别是整型和字符串类型,然后就是重置ptr6,ptr6管理的这个Test对象就被析构了,所以输出析构语句,引用计数也变为0。然后是让ptr5管理另一块内存,输出构造函数中的语句,所以ptr5之前管理的那块内存也析构了,输出析构语句。又因为ptr5管理着新的内存,所以引用计数仍然为1。最后,程序结束,ptr5管理的这块内存也析构了。

 那么怎么去使用这个初始化得到的共享智能指针呢?(有两种方式)

1. 通过智能指针取出原始地址:

// 获取原始指针
Test* t = ptr5.get();
t->setValue(1000);
t->print();

2.  通过智能指针对象直接进行操作 :

// 通过智能指针对象直接进行操作
// 将智能指针对象当作指针去使用
ptr5->setValue(999);
ptr5->print();

 2. 指定删除器

当智能指针管理的内存对应的引用计数变为0的时候,这块内存就会被智能指针析构掉了。另外,我们在初始化智能指针的时候也可以自己指定删除动作,这个删除操作对应的函数被称之为删除器,这个删除器函数本质是一个回调函数,我们只需要进行实现,其调用由智能指针完成的。(智能指针都是在栈上的)

// 第二个参数是指定删除器,默认情况下可以不指定
// 删除器可以在外面创建一个或者写一个匿名函数(lambda表达式)
shared_ptr ppp(new Test(100), [](Test* t) {
	// 释放内存的操作 
	// 需要释放的是Test类型的指针
	// t指向的就是第一个参数
	cout << "----------------------" << endl;
	delete t;
});

输出结果为:

----------------------
destruct Test...
destruct Test...

由此可以看出这个删除器确定被调用了。

那么在什么时候必须要指定删除器函数呢?

 通过智能指针对象去管理一块数组内存的时候,如果不是数组内存,智能指针对象提供的默认删除器函数就能删除掉这块内存。如果管理的是一块数组内存就不能删除。

接下来用一段代码来演示:

#include
#include
using namespace std;

class Test
{
public:
	Test()
	{
		cout << "construct Test..." << endl;
	}

	Test(int x) : m_num(x)
	{
		cout << "construct Test,x = " << x << endl;
	}

	Test(string str)
	{
		cout << "construct Test,str = " << str << endl;
	}

	~Test()
	{
		cout << "destruct Test..." << endl;
	}

	void setValue(int v)
	{
		m_num = v;
	}

	void print()
	{
		cout << "m_num:" << m_num << endl;
	}

private:
	int m_num;
};

int main()
{

	shared_ptr p1(new Test[5]);

	return 0;
}

输出结果为:

construct Test...
construct Test...
construct Test...
construct Test...
construct Test...
destruct Test...

由输出结果可以看出默认的删除器函数不能删除数组内存。

手动指定删除器:

shared_ptr p1(new Test[5], [](Test* t) {
	cout << "----------------------" << endl;
	delete[] t;
});

输出结果为:

construct Test...
construct Test...
construct Test...
construct Test...
construct Test...
----------------------
destruct Test...
destruct Test...
destruct Test...
destruct Test...
destruct Test...

在删除数组内存时,除了自己编写删除器,也可以使用C++提供的std::default_delete()函数作为删除器,这个函数内部的删除功能也是通过调用delete来实现的,要释放什么类型的内存就将模板类型T指定为什么类型即可。具体处理代码如下:

int main()
{
    shared_ptr p2(new Test[5], default_delete());
    return 0;
}

注意:函数类型必须写[]不能写* 。而自己指定删除器中的参数类型可以也写成Test []t。

 输出结果和自己指定一样。

如果我们在程序中需要经常对数组类型的内存进行释放,我们可以去封装一个函数模板

代码如下:

#include 
#include 
using namespace std;

template 
shared_ptr make_share_array(size_t size)
{
    // 返回匿名对象
    return shared_ptr(new T[size], default_delete());
}

int main()
{
    shared_ptr ptr1 = make_share_array(10);
    cout << ptr1.use_count() << endl;
    shared_ptr ptr2 = make_share_array(128);
    cout << ptr2.use_count() << endl;
    return 0;
}

 本文参考:共享智能指针 | 爱编程的大丙 (subingwen.cn)

你可能感兴趣的:(c++,开发语言,笔记)