C++11:智能指针

智能指针是存储指向动态分配(堆)对象指针的类,用于生存期控制,能够确保在离开指针所在作用域时,自动正确地销毁动态分配的对象,防止内存泄露。它的一种通用实现技术是使用引用计数。每使用它一次,内部的引用计数加1,每析构一次,内部引用计数减1,减为0时,删除所指向的堆内存。
C++11提供了3种智能指针: std:.shared_ptr、std::uniq _ptr和std::weak _ptr,使用时需要引用头文件,本章将分别介绍这3种智能指针。

目录

  • 1 shared_ptr共享的智能指针
    • 1.1shared_ptr的基本用法
    • 1.2 使用shared _ptr 需要注意的问题
  • 2 unique_ptr独占的智能指针
  • 3 weak_ptr弱引用的智能指针
    • 3.1 weak_ptr基本用法
    • 3.2 weak_ptr返回this指针
    • 3.3 weak_ptr解决循环引用问题
  • 4 通过智能指针管理第三方库分配的内存

1 shared_ptr共享的智能指针

std:.shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。在最后一个shared_ptr析构的时候,内存才会被释放。

1.1shared_ptr的基本用法

初始化:
可以通过构造函数、std::make_shared辅助函数和reset方法来初始化shared_ptr,代码如下:

std::shared_ptr<int> p(new int(1));
std::shared_ptr<int> p2 = p;
std::shared_ptr<int> ptr;
ptr.reset(new int (1)) ;
if (ptr)
{
	cout<< "ptr is not null";
}

我们应该优先使用make_shared来构造智能指针,因为它更加高效。
不能将一个原始指针直接赋值给一个智能指针,例如,下面这种方法是错误的:

std::shared_ptr<int> p = new int(1);	//编译报错,不允许直接赋值

可以看到智能指针的用法和普通指针的用法类似,只不过不需要自己管理分配的内存。shared_ptr不能通过直接将原始指针赋值来初始化,需要通过构造函数和辅助方法来初始化。对于一个未初始化的智能指针,可以通过reset方法来初始化,当智能指针中有值的时候,调用reset 会使引用计数减1。另外,智能指针可以通过重载的bool类型操作符来判断智能指针中是否为空(未初始化)。
获取原始指针:
当需要获取原始指针时,可以通过get方法来返回原始指针,代码如下:

std::shared_ptr<int> ptr (new int (1));
int* p=ptr.get () ;

指定删除器:
智能指针初始化可以指定删除器。

void DeleteIntPtr (int* p)
{
	delete p;
}
std::shared_ptr<int> p(newint, DeleteIntPtr);

当p的引用计数为0时,自动调用删除器DeleteIntPtr来释放对象的内存。删除器可以是一个 lambda表达式,因此,上面的写法还可以改为:

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

当我们用shared_ptr管理动态数组时,需要指定删除器,因为std::shared_ptr的默认删除器不支持数组对象,代码如下:

std::shared_ptr<int> p(new int[10], [](int* p){delete[] p;));	//指定delete[]

也可以将std::default_delete作为删除器。default_delete的内部是通过调用delete来实现功能的,代码如下:

std::shared_ptr<int> p(new int[10], std::default_delete<int[]>);

另外,还可以通过封装一个make_shared_array方法来让 shared_ptr支持数组,代码如下:

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

std::shared_ptr<int> p = make_shared_array<int>(10);
std::shared_ptr<char> p = make_shared_array<char>(10);

1.2 使用shared _ptr 需要注意的问题

智能指针虽然能自动管理堆内存,但它还是有不少陷阱,在使用时要注意。
1)不要用一个原始指针初始化多个shared_ptr,例如下面这些是错误的:

int* ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr) ; 	//logic error

2)不要在函数实参中创建shared_ptr。对于下面的用写法:

f(shared_ptr<int>(new int), g());	//有缺陷

因为C++的函数参数的计算顺序在不同的编译器不同的调用约定下可能是不一样的,一般是从右到左,但也有可能是从左到右,所以,可能的过程是先new int,然后调g(),如果恰好g()发生异常,而shared_ptr还没有创建,则int 内存泄露了,正确的写法应该是先创建智能指针,代码如下:

shared_ptr<int> p(new int());
f(p, g());

3)通过shared_from_this()返回this指针。不要将this指针作为shared_ptr返回出来,因为this 指针本质上是一个裸指针,因此,这样可能会导致重复析构,看下面的例子:

struct A
{
	shared_ptr<A>GetSelf()
	{
		return shared_ptr<S>(this);	//don't do this!
	}
};

int main ()
{
	shared_ptr<A> spl(new A);
	shared_ptr<A> sp2 = sp1->Getself();
	return0;
}

在这个例子中,由于用同一个指针( this)构造了两个智能指针sp1和 sp2,而它们之间是没有任何关系的,在离开作用域之后this将会被构造的两个智能指针各自析构,导致重复析构的错误。
正确返回this的 shared_ptr 的做法是:让目标类通过派生std::enable_shared_from_this类,然后使用基类的成员函数shared_from_this来返回this的 shared_ptr,看下面的示例:

class A: public std::enable_shared_from_this<A>
{
	std::shared_ptr<A> Getself()
	{
		return shared_from_this();
	}
};

std::shared_ptr<A> spy(newA)
std::shared_ptr<A> p = spy->GetSelf();	//OK

至于用shared_from_this()的原因,将在4.3.3节中给出解释。
4)要避免循环引用。智能指针最大的一个陷阱是循环引用,循环引用会导致内存泄露。下例是一个典型的循环引用的场景。

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

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

void TestPtr()
{
	std::shared_ptr<A> ap(new A);
	std::shared_ptr<B> bp(new B);
	ap->bptr = bp;
	bp->aptr = ap;
}

测试结果是两个指针A和B都不会被删除,存在内存泄露。循环引用导致ap和 bp 的引用计数为2,在离开作用域之后,ap和 bp 的引用计数减为1,并不会减为0,导致两个指针都不会被析构,产生了内存泄露。解决办法是把A和B任何一个成员变量改为weak_ptr。

2 unique_ptr独占的智能指针

unique_ptr是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,不允许通过赋值将一个unique_ptr赋值给另外一个unique_ptr。下面这样是错误的:

unique_ptr<T> myPtr(new T);
unique_ptr<T> myotherPtr = myPtr;	//错误,不能复制

unique_ptr不允许复制,但可以通过函数返回给其他的unique_ptr,还可以通过std::move来转移到其他的unique_ptr,这样它本身就不再拥有原来指针的所有权了。例如:

unique_ptr<T> myPtr(new T);	//正确
unique_ptr<T> myotherPtr = std::move(myPtr);	//正确
unique_ptr<T> ptr = myPtr;	//错误,只能移动,不可复制

unique_ptr和 shared_ptr相比,unique_ptr除了独占性这个特点之外,还可以指向一个数组,代码如下:

std::unique_ptr<int []> ptr(new int [10]);
ptr[9] = 9;	//设置最后一个元素值为9

std::shared_ptr ptr(new int[10]);是不合法的。
unique_ptr指定删除器和 std::shared_ptr是有差别的,比如下面的写法:

std::shared_ptr<int> ptr(new int(1), [](int*p){delete p;});	//正确
std::unique_ptr<int> ptr(new int(1), [](int*p){delete p;});	//错误

std:.unique_ptr指定删除器的时候需要确定删除器的类型,所以不能像shared_ptr那样直接指定删除器,可以这样写:

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

上面这种写法在 lambda没有捕获变量的情况下是正确的,如果捕获了变量,则会编译报错:

std::unique ptr<int, void(*)(int*)> ptr(new int(1), [&](int*p){delete p;});	//错误,捕获了变量

这是因为lambda在没有捕获变量的情况下是可以直接转换为函数指针的,一旦捕获了就无法转换了。
如果希望unique_ptr的删除器支持lambda,可以这样写:

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

还可以自定义unique_ptr的删除器,比如下面的代码:

#include 
#include 
using namespace std;

struct MyDeleter
{
	void operator() (int*p)
	{
		cout<<"delete"<<endl;
		delete p;
	}
};

int main()
{
	std::unique_ptr<int, MyDeleter> p(new int(1));
	return 0;
}

关于shared_ptr和 unique_ptr的使用场景要根据实际应用需求来选择,如果希望只有一个智能指针管理资源或者管理数组就用unique_ptr,如果希望多个智能指针管理同一个资源就用shared_ptr。

3 weak_ptr弱引用的智能指针

弱引用指针weak_ptr是用来监视shared_ptr 的,不会使引用计数加1,它不管理shared_ptr内部的指针,主要是为了监视 shared_ptr的生命周期,更像是 shared_ptr 的一个助手。weak_ptr没有重载操作符*和->,因为它不共享指针,不能操作资源,主要是为了通过shared_ptr获得资源的监测权,它的构造不会增加引用计数,它的析构也不会减少引用计数,纯粹只是作为一个旁观者来监视 shared_ptr中管理的资源是否存在。weak_ptr还可以用来返回this指针和解决循环引用的问题。

3.1 weak_ptr基本用法

1)通过use_count()方法来获得当前观测资源的引用计数,代码如下:

shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp) ;
cout<<wp.use_count()<<endl;	//结果将输出1

2)通过expired()方法来判断所观测的资源是否已经被释放,代码如下:

shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp) ;
if(wp.expired())
	std::cout <<"weak_ptr无效,所监视的智能指针已被释放\n";
else
	std::cout<<"weak_ptr有效\n";	//结果将输出:weak_ptr有效

3 )通过lock方法来获取所监视的shared_ptr,代码如下:

std::weak_ptr<int> gw ;
void f ()
{
	if (gw.expired ()) 	//所监视的shared_ptr是否释放
	{
		std::cout << "gw is expired\n";
	}
	else
	{
		auto spt = gw.lock();
		std::cout << * spt <<" \n";
	}
}

int main ()
{
	{
		auto sp = std::make_shared<int>(42);
		gw = sp;
		f();
	}
	f();
}

输出如下:
42
gw is expired

3.2 weak_ptr返回this指针

之前提到不能直接将this指针返回为shared_ptr,需要通过派生std::enable_shared_from_this类,并通过其方法 shared_from_this来返回智能指针,原因是std:enableshared_from_this类中有一个weak _ptr,这个weak _ptr用来观测this智能指针,调用sharedfrom_this()方法时,会调用内部这个weak _ptr的 lock()方法,将所观测的shared_ptr返回。再看一下前面的例子。

struct A: public std::enable_shared_from_this<A>
{
	std::shared_ptr<A> GetSelf()
	{
		return shared_from_this();
	}
	~s ()
	{
		cout <<"A is deleted"<< endl;
	}
};
std::shared_ptr<A> spy(newA) ;
std::shared ptr<A> p = spy->Getself(); 	//OK

输出结果如下:
A is deleted
在外面创建A对象的智能指针和通过该对象返回this的智能指针都是安全的,因为shared_from_this()是内部的weak_ptr调用lock()方法之后返回的智能指针,在离开作用域之后,spy的引用计数减为0,A对象会被析构,不会出现A对象被析构两次的问题。
需要注意的是,获取自身智能指针的函数仅在shared_ptr的构造函数被调用之后才能使用,因为enable_shared_from_this内部的weak _ptr只有通过shared _ptr才能构造。

3.3 weak_ptr解决循环引用问题

智能指针的循环引用会导致内存泄露,再看一下前面的例子。

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

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

void TestPtr()
{
	{
		std::shared_ptr<A> ap (new A);
		std::shared_ptr<B> bp (new B);
		ap->bptr = bp;
		bp->aptr = ap;
	}	//objects should be destroyed .
}

在这个例子中,由于循环引用导致ap和 bp 的引用计数都为2,离开作用域之后引用计数减为1,不会去删除指针,导致内存泄露。通过weak_ptr就可以解决这个问题,只要将A或B的任意一个成员变量改为weak_ptr即可。

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

struct B
{
	std::weak_ptr<A> aptr; 	//改为weak_ptr
	~B() { cout <<"B is deleted!" <<endl; }
};

void TestPtr()
{
	{
		std::shared_ptr<A> ap (new A);
		std::shared_ptr<B> bp (new B);
		ap->bptr = bp;
		bp->aptr = ap;
	}	//objects should be destroyed .
}

输出如下:
A is deleted !
B is deleted !
这样在对B的成员赋值时,即执行bp->aptr=ap;时,由于aptr是 weak_ptr,它并不会增加引用计数,所以ap 的引用计数仍然会是1,在离开作用域之后,ap 的引用计数会减为0A指针会被析构,析构后其内部的 bptr的引用计数会减为1,然后在离开作用域后bp引用计数又从1减为0,B对象也将被析构,不会发生内存泄露。

4 通过智能指针管理第三方库分配的内存

智能指针可以很方便地管理当前程序库动态分配的内存,还可以用来管理第三方库分配的内存。第三方库分配的内存一般需要通过第三方库提供的释放接口才能释放,由于第三方库返回的指针一般都是原始指针,在用完之后如果没有调用第三方库的释放接口,就很容易造成内存泄露。比如下面的代码:

void* p = GetHandle ( ) ->create();
//do something. ..
GetHandle()->Release(p);

这段代码实际上是不安全的,在使用第三方库分配的内存的过程中,可能忘记调用Release接口,还有可能中间不小心返回了,还有可能中间发生了异常,导致无法调用Release接口。这时用智能指针来管理第三方库的内存就很合适了,只要离开作用域内存就会自动释放,不用显式去调用释放接口了,不用担心中途返回或者发生异常导致无法调用释放接口的问题。例如:

void* p = GetHandle ()->Create() ;
std::shared_ptr<void> sp(p, [this](void*p) { GetHandle()->Release(p); });

上面这段代码就可以保证任何时候都能正确释放第三方库分配的内存。虽然能解决问题,但还是有些烦琐,因为每个第三方库分配内存的地方都要调用这段代码。我们可以将这段代码提炼出来作为一个公共函数,简化调用:

std::shared_ptr<void> Guard(void* p)
{
	return std::shared_ptr<void> sp (p, [this](void*p){GetHandle()->Release(p);});
}
void* p = GetHandle()->Create();
auto sp = Guard(p) ;
//do something. ..

上面的代码通过Guard 函数做了简化,用起来比较方便,但仍然不够安全,因为使用者可能会这样写:

void* p = GetHandle()->Create() ;
Guard(p); //危险,这句结束之后p就被释放了
//do something...

这样写是有问题的,会导致访问野指针,因为Guard§;是一个右值,如果不将这个右值赋值给一个指针,Guard§;这句结束之后,就会释放,导致p提前释放了,后面就会访问野指针的内容。auto sp=Guard§;需要一个赋值操作,忘记赋值就会导致指针提前释放,这种写法仍然不够安全。我们可以定义一个宏来解决这个问题,代码如下:

#define GUARD(P) std::shared_ptr<void> p##p(p, [](void*p){GetHandle()->Release(p);});
void* p = GetHandle()->Create();
GUARD(p); //安全

也可以用unique_ptr来管理第三方的内存,代码如下:

#define GUARD(P) std::unique_ptr<void,void(*)(int*)> p##p(p, [](void*p){GetHandle ()->Release(p);});

你可能感兴趣的:(C++,c++,算法,数据结构)