C++ 智能指针

在之前的一篇博客《c++智能指针用法》中简单介绍过智能指针

与Java等具有垃圾回收机制的语言相比,C++语言没有垃圾回收机制,必须自己去释放分配的内存,否则就会存在内存泄露的问题。而解决查找内存泄漏需要花费大量的时间和精力,最有效的解决办法就是使用智能指针(Smart Pointer)。智能指针能够自动删除分配的内存,它与普通指针的是使用方法类似,但是不需要手动释放。它会自己管理内存的释放,这样就不必担心因为忘记释放内存而导致内存泄漏。

智能指针是指向动态分配(heap)对象的指针,用于生存周期控制,它能够确保离开指针所在作用域时,正确自动的销毁动态分配的对象,它的一种通用实现技术实采用引用计数,每使用它一次,内部的引用计数加一,每析构一次,内部的引用计数减一,当引用计数为0时,删除所指向的堆内存。

C++11提供了三种智能指针:位于头文件<memory>中

1. shared_ptr

共享的智能指针,使用引用计数,每一个拷贝的shared_ptr都指向相同的内存。当新的 shared_ptr 对象与指针关联时,则在其构造函数中,将与此指针关联的引用计数增加1。当任何 shared_ptr 对象超出作用域时,则在其析构函数中,它将关联指针的引用计数减1。如果引用计数变为0,则表示没有其他 shared_ptr 对象与此内存关联,在这种情况下,它使用delete函数删除该内存。

基本使用方法:

对于一个未初始化的智能指针,可以通过reset()方法进行初始化。当智能指针有值的时候,reset()方法实际上相当于使指针与关联的原始指针分离,同时会使得引用计数减一。

// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <memory>


int main()
{
	// 智能指针的初始化
	std::shared_ptr<int> p(new int(1));  // 智能指针必须用构造函数进行初始化
	std::shared_ptr<int> p1 = p;
	std::shared_ptr<int> ptr;
	ptr.reset(new int(8));

	std::cout << *p << *p1 << std::endl;
	std::cout << p.use_count() << std::endl;
	std::cout << ptr.use_count() << std::endl;  // 获取指针的引用计数

	// 智能指针通过重载的bool运算符来判断真假
	if (ptr)
	{
		std::cout << "ptr is not null" << std::endl;
	}

	ptr.reset();   // 分离ptr指针与new int(8)的关联 reset使得ptr的引用计数减一,此时ptr被释放
	if (!ptr)
	{
		std::cout << "ptr is null" << std::endl;
	}

    return 0;
}



运行结果:

创建新的shared_ptr对象的最佳方法是使用std :: make_shared:,std::make_shared 一次性为int对象和用于引用计数的数据都分配了内存,而new操作符只是为int分配了内存,所以make_shared的效率更高:

// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <memory>


int main()
{
	std::shared_ptr<int> p1 = std::make_shared<int>(20);   // 创建并初始化
	std::shared_ptr<int> p2 = std::make_shared<int>();     // 创建一个指针
	*p2 = 12;   // 初始化

	std::cout << *p1 << " | " << *p2 << std::endl;
	p2.reset(new int(14));   // p2指向新的对象

	std::cout << *p2 << std::endl;
    
	std::shared_ptr<int> p3(p2);    // 利用智能指针初始化一个智能指针
	if (p2 == p3)     // 智能指针重载了operator==()
	{
		std::cout << "P2 and p3 point to the same element" << std::endl;
	}

	p3 = nullptr;   // 重置指针
	if (!p3)
	{
		std::cout << "p3 is null" << std::endl;
	}

	return 0;
}

运行结果:

获取原始指针:

可以通过get方法来获取原始指针:

int main()
{
	std::shared_ptr<int> p1 = std::make_shared<int>(20);   // 创建并初始化
	int* pr = p1.get();

	std::cout << *pr << std::endl;
	return 0;
}

自定义删除器:

智能指针在初始化的时候可以指定删除器,当智能指针的引用计数为0的时候,会自动调用定义的删除器释放对象的内存:

// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <memory>

void DeleteIntPointer(int* p)
{
	delete p;   // 删除
}

int main()
{
	std::shared_ptr<int> p1(new int(34), DeleteIntPointer);   // 指定删除器
	// 也可以用lambda表达式定义删除器
	std::shared_ptr<int> p2(new int(2), [](int* p) {delete p; });
	return 0;
}

当用shared_ptr管理数组的时候,需要指定删除器,因为shared_ptr默认的删除器不支持数组对象:

1. 自定义删除器

2. 使用default_delete作为删除器,它内部是通过调用delete实现的

3. 可以封装一个make_shared_array

// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <memory>

template<typename T>
std::shared_ptr<T> make_shared_array(size_t size)
{
	return std::shared_ptr<T>(new T[size], std::default_delete<T[]>());
}

int main()
{
	// 自定义删除器
	std::shared_ptr<int> p1(new int[10], [](int* p) {delete[] p; });

	// 使用delete_default
	std::shared_ptr<int> p2(new int[10], std::default_delete<int[]>());

	// 自定义函数
	std::shared_ptr<int> p3 = make_shared_array<int>(10);
	for (int i = 0; i < 10; i++)
	{
		p3.get()[i] = 9;
	}

	for (int i = 0; i < 10; i++)
	{
		std::cout << p3.get()[i] << std::endl;
	}

	return 0;
}

1, 使用shared_ptr需要注意的问题:
a. 不能用一个原始指针初始化多个shared_ptr

int* p = new int;
std::shared_ptr<int> p1(p);
std::shared_ptr<int> p2(p);    // invalid

b. 不要在函数参数中创建shared_ptr,应该先创建std::shared_ptr,再传入函数

c. 避免循环引用,造成死锁

具体例子如下所示:

// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <memory>

struct A;
struct B;

struct A
{
	std::shared_ptr<B> bptr;
	~A()
	{
		std::cout << "A is deleted" << std::endl;
	}
};

struct B
{
	std::shared_ptr<A> aptr;
	~B()
	{
		std::cout << "B is deleted" << std::endl;
	}
};


int main()
{
	std::shared_ptr<A> ap(new A);
	std::shared_ptr<B> bp(new B);
	ap->bptr = bp;    // 导致bp的引用计数为2
	bp->aptr = ap;    // 导致ap的引用计数为2

	return 0;
}

循环引用会导致ap 和bp 的引用计数都为2,在离开作用域之后,ap,bp的引用计数都会减为1,但不会是0,导致两个指针都不会被析构,产生了内存泄漏。

后面会介绍关于这一问题的解决方法。

2. unique_ptr

unique_ptr是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,不允许将一个unique_ptr赋值给另一个unique_ptr。但是,可以通过std::move来转移unique_ptr对内存的所有权,例如:

// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <memory>

int main()
{
	std::unique_ptr<int> p1(new int(4));
	std::unique_ptr<int> p2 = std::move(p1);  // 转移unique对内存的所有权
	std::unique_ptr<int> p2 = p1;   // 这样的赋值不合法
	return 0;
}

unique_ptr指向一个数组的时候和shared_ptr有所差别:

std::shared_ptr<int []> ptr(new int[10]);

当unique_ptr自定义删除器的时候,需要制定删除器的类型,而shared_ptr则不需要:

std::unique_ptr<int, void(*) (int*)> p(new int(1), [](int* p) {delete p; });

3. weak_ptr

弱引用指针weak_ptr是用来监视shared_ptr的,不会使其引用计数加一,weak_ptr不管理shared_ptr内部的指针,它的作用主要是为了监测shared_ptr的生命周期,weak_ptr没有重载* ->操作符,所以他不能操作资源,主要是通过它获取shared_ptr的监测权。weak_ptr的构造i函数不会增加引用计数,析构函数也不会减少引用计数。可以用来解决shared_ptr遇到的循环引用问题。

例如:

// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <memory>

int main()
{
	std::shared_ptr<int> shared_ptr = std::make_shared<int>(10);
	std::weak_ptr<int> wp(shared_ptr);   // 监测shared_ptr

	std::cout << wp.use_count() << std::endl;

	// expire可以用来判断被观测的资源是否已经被释放
	if (wp.expired())
	{
		std::cout << "资源已经被释放" << std::endl;
	}
	else
	{
		std::cout << "资源有效" << std::endl;
	}

	shared_ptr.reset();    // 释放资源
	if (wp.expired())
	{
		std::cout << "资源已经被释放" << std::endl;
	}
	else
	{
		std::cout << "资源有效" << std::endl;
	}

	// 通过lock方法获取所监视的shared_ptr
	std::shared_ptr<int> p1  = std::make_shared<int>(10);
	std::weak_ptr<int> wp1(p1);     // 创建一个weak_ptr监测p1

	if (wp1.expired())
	{
		std::cout << "资源已经被释放" << std::endl;
	}
	else
	{
		std::cout << "资源有效" << std::endl;
		auto p = wp1.lock();
		std::cout << *p << std::endl;
	}

	return 0;
}

weak_ptr:

expire方法可以判断所监测的资源是否被释放

lock方法可以获取weak_ptr所监测的shared_ptr

使用weak_ptr解决循环引用中出现的死锁问题

在前面介绍shared_ptr的时候,提到循环引用会导致能存泄露的问题,例如:

// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <memory>

struct A;
struct B;

struct A
{
	std::shared_ptr<B> bptr;
	~A()
	{
		std::cout << "A is deleted" << std::endl;
	}
};

struct B
{
	std::shared_ptr<A> aptr;
	~B()
	{
		std::cout << "B is deleted" << std::endl;
	}
};


int main()
{
	std::shared_ptr<A> ap(new A);
	std::shared_ptr<B> bp(new B);
	ap->bptr = bp;    // 导致bp的引用计数为2
	bp->aptr = ap;    // 导致ap的引用计数为2

	return 0;
}

通过weak_ptr可以解决这个问题:

// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <memory>

struct A;
struct B;

struct A
{
	std::weak_ptr<B> bptr;
	~A()
	{
		std::cout << "A is deleted" << std::endl;
	}
};

struct B
{
	std::shared_ptr<A> aptr;
	~B()
	{
		std::cout << "B is deleted" << std::endl;
	}
};


int main()
{
	std::shared_ptr<A> ap(new A);
	std::shared_ptr<B> bp(new B);
	ap->bptr = bp;    // 导致bp的引用计数为2
	bp->aptr = ap;    // 导致ap的引用计数为2

	return 0;
}

如何通过智能指针管理第三方库分配的内存:
智能指针可以管理当前程序动态分配的内存,还可以用来管理第三方库分配的内存。一般第三方库分配的内存需要通过第三方提供的释放接口进行释放,第三方库返回的指针一般是原始指针,如果在使用完之后没有调用释放接口,会导致内存泄漏。

例如:

void* p = GetHandle()->create();
// do something
...
...
GetHandle()->release();    // 释放内存

上面的代码是不安全的,因为可能会发生下面的问题:

1.在使用第三方库分配内存后,忘记调用release接口

2.程序在执行的过程中返回了,导致无法调用release接口

3. 程序在执行过程中发生了异常,导致无法调用release接口

如果使用智能指针来管理内存,就可以避免上述的问题,只要离开作用域就会自动释放内存。

按照如下的写法就可以保证任何时候都可以正确释放分配的内存:

void* p = GetHandle()->create();
std::shared_ptr<void> sp(p, [this](void* p) {GetHandle()->release(p); });
// do something
...
...

在自定义的删除器中调用第三方库的内存释放接口即可

4. auto_ptr

auto_ptr的详细介绍可以参考之前的博客:https://blog.csdn.net/zj1131190425/article/details/90446954

 

posted @ 2019-08-14 14:36  Alpha205  阅读(126)  评论(0编辑  收藏  举报