智能指针这个东西想必学过C++的人都应该听说过或者使用过,但是应该有一些人和我一样对其只有一点浅显的认识,今天查了一些关于智能指针的资料,下面对其做一些整理:

为什么要有智能指针
在用C++动态内存分配的时候,使用new操作获取了一块内存,那么一定要在合适的地方将其delete掉,否则就会造成内存泄漏,这一点都明白,但是实际操作的时候总是会忘记这个delete操作,而智能指针就是为了避免这样一个问题。智能指针通常都是一个类,它包含一个指针成员p,这个指针指向new得到的内存块,最简单的智能指针会在自己的析构函数处调用delete p以实现自动delete。

C++11中智能指针包括四种:auto_ptr、weak_ptr、shared_ptr、unique_ptr,其中auto_ptr就是上面所说的那样一种最简单的智能指针,但是它存在很大的缺陷:当有两个不同的auto_ptr指向相同的一块内存的时候,由于auto_ptr在析构的时候会delete掉内存块,那么这两个auto_ptr必然会按先后顺序对这块内存做delete,也就是说一块内存会被delete两次,这会导致很严重的后果。因此通常是不考虑使用auto_ptr的,后面的讨论也不会涉及到auto_ptr。

shared_ptr
为了避免auto_ptr的问题,shared_ptr采用引用计数的方法,当一个shared_ptr指向一块内存的时候,这块内存的引用计数就会+1;当一个shared_ptr的作用域结束的时候,这块内存的引用计数就会-1。当内存的引用变为0的时候才会去调用delete方法,看一个具体的例子:

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

class A
{
public:
	A() { cout << " Create A ! " << endl;  };
	~A() { cout << " Delete A ! " << endl; };
};

void Test( shared_ptr<A> p2 )
{
	shared_ptr<A> p1( p2 );
}

int main()
{
	shared_ptr<A> p(new A);
	Test(p);
	cout << " Test Finished ! " << endl;
	return 0;
}

它的输出结果为:
智能指针内容整理-LMLPHP
可以看到在Test结束之后,p1就被销毁掉了,但是此时还并没有对A做delete,因为在main函数中还有一个p享有A的内存,只有在p也被销毁之后,才会对A做delete。
对于shared_ptr的使用有一些需要注意的地方:
1.shared_ptr的初始化问题:从上面就可以看出shared_ptr可以通过一个裸指针或者另一个shared_ptr生成,这两者有很大的区别。shared_ptr用到了引用计数,如果是通过裸指针来初始化shared_ptr的话,会创建一个新的引用计数,而如果是通过另一个shared_ptr初始化的话,就不会生成新的引用计数,而是在这个已知的shared_ptr的引用计数上+1。比如说将上面的例子改为:

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

class A
{
public:
	A() { cout << " Create A ! " << endl;  };
	~A() { cout << " Delete A ! " << endl; };
};

void Test(A * a )
{
	shared_ptr<A> p1( a );
}

int main()
{
	A * a = new A;
	shared_ptr<A> p(a);
	Test(a);
	cout << " Test Finished ! " << endl;
	return 0;
}

运行这个实例程序会崩溃,尽管p和p1指向的都是相同的内存块,但是它们并没有做到共享,由于它们都是通过裸指针来实现,因此各自的引用计数都是1,那么在Test函数结束的时候,p1被销毁掉,p1的引用计数变为0,这个时候会调用delete a,之后p会再delete a,这样就崩溃掉了。
因此在使用shared_ptr的时候,如果多个shared_ptr共享同一块内存,就需要确保只有一个shared_ptr是通过裸指针的方式构造,其余的所有shared_ptr都需要通过其他的shared_ptr来构造。

2.new的问题:可以看到在使用裸指针构造的时候,都用到了new操作,尽管通过智能指针会自动的调用delete操作,但还是有一种不协调的感觉,看到new却看不到delete,总让人觉得不太舒服。为了避免这一点可以使用make_shared函数,
可以将代码中的shared_ptr<A> p(new A);
改为shared_ptr<A> p(make_shared<A>());
这样就可以避免使用new造成一种没有delete的错觉。除了这一点,用make_shared还能提升性能:如果使用前一种方式,至少会分配两次内存,一次创建A,一次创建智能指针的控制块;而使用make_shared就可以在一个大的内存块上通过一次分配内存完成,减少了内存分配的次数。
如果A的构造函数需要实参的话,就可以在make_shared< A >()后面的这个括号中填上对应的参数。

3.this指针的问题:考虑下面这样一个情形:

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

class A;
void Test(A * a);


class A
{
public:
	A() { cout << " Create A ! " << endl;  };
	~A() { cout << " Delete A ! " << endl; };
	void Do()
	{
		Test(this);
	}
};

void Test( A * a )
{
	//Do Something
}

int main()
{
	A * a = new A;
	a->Do();
	return 0;
}

这里使用裸指针传this指针到Test函数中,没有任何问题。但是如果说要让所有的参数都变成智能指针该怎么做?

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

class A;
void Test(shared_ptr<A> a);


class A
{
public:
	A() { cout << " Create A ! " << endl;  };
	~A() { cout << " Delete A ! " << endl; };
	void Do()
	{
		Test(shared_ptr<A>(this));
	}
};

void Test(shared_ptr<A> a)
{
	//Do Something
}

int main()
{
	shared_ptr<A> a(new A);
	a->Do();
	return 0;
}

如果像这样直接将裸指针全部换为智能指针,这个程序也是会崩掉的。原因很简单,Do()函数中生成另一个通过裸指针this创建的shared_ptr,在Do结束之后,它必然会调用delete this,之后在main结束的时候又delete了一次,因此程序崩溃。
那么很容易想到Do()函数中的shared_ptr应该由main()函数中的a来创建,但是现在的问题是没有办法在Do()中获取a。像这样一个类的实例已经被一个智能指针所指,而又需要在这个类的内部创建一个新的智能指针的时候,就需要用到shared_from_this函数,要用这个函数首先要让这个类继承enable_shared_from_this类,即将类A改为:

class A : public enable_shared_from_this<A>
{
public:
	A() { cout << " Create A ! " << endl;  };
	~A() { cout << " Delete A ! " << endl; };
	void Do()
	{
		Test(shared_from_this());
	}
};

为什么这样就可以了呢?enable_shared_from_this这个基类中有一个weak_ptr指针,在第一个shared_ptr指向继承自这个类的子类的实例的时候,这个weak_ptr会通过这个shared_ptr来初始化。在类中调用shared_from_this()函数又会通过这个weak_ptr来获取这个shared_ptr,这样就完成了在类中对类外的shared_ptr的访问。

4.循环引用问题:

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

class A;
class B;
void Test();


class A
{
public:
	A() { cout << " Create A ! " << endl;  };
	~A() { cout << " Delete A ! " << endl; };
	shared_ptr<B> m_B;
};

class B
{
public:
	B() { cout << " Create B ! " << endl; };
	~B() { cout << " Delete B ! " << endl; };
	shared_ptr<A> m_A;
};

void Test()
{
	shared_ptr<A> a = make_shared<A>();
	shared_ptr<B> b = make_shared<B>();
	a->m_B = b;
	b->m_A = a;
	cout << a.use_count() << endl;
	cout << b.use_count() << endl;
}

int main()
{
	Test();
	return 0;
}

最终运行结果为:
智能指针内容整理-LMLPHP
可以看到在Test()赋值语句结束后,a和b的引用计数都为2。这就是循环引用,a和b的成员都互相包含一个指向对方的智能指针,在Test函数结束后,尽管a和b的作用域已经结束,引用计数都减1,但是a和b所指向的那块内存的引用计数仍旧不为0,不会去调用它们的析构函数,这依旧会导致内存泄漏。
解决这一问题的方法是用后面讨论的weak_ptr,但这提醒我们,在使用shared_ptr的时候一定要避免循环引用。

weak_ptr
为什么要用weak这个词呢?因为weak_ptr并不会对指向的内存块做任何的delete操作,它的表现更多的像是一种监控,可以用来判断当前的这个内存块是否已经被delete掉了。
weak_ptr只能通过shared_ptr或者已有的weak_ptr初始化,不能由裸指针直接初始化。
如果将上面的例子改为:

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

class A;
class B;
void Test();


class A
{
public:
	A() { cout << " Create A ! " << endl;  };
	~A() { cout << " Delete A ! " << endl; };
	weak_ptr<B> m_B;
};

class B
{
public:
	B() { cout << " Create B ! " << endl; };
	~B() { cout << " Delete B ! " << endl; };
	weak_ptr<A> m_A;
};

void Test()
{
	shared_ptr<A> a = make_shared<A>();
	shared_ptr<B> b = make_shared<B>();
	a->m_B = b;
	b->m_A = a;
	cout << a.use_count() << endl;
	cout << b.use_count() << endl;
}

int main()
{
	Test();
	return 0;
}

智能指针内容整理-LMLPHP
最终运行结果就是正常的。
weak_ptr的机制也是引用计数,但是它的计数和shared_ptr的计数是分开的。shared_ptr的计数为0时会调用delete,但是weak_ptr的计数为0时则不会调用delete。如果weak_ptr的计数大于0,那么通过weak_ptr的Lock函数就可以获取到它所关联的一个shared_ptr,如果weak_ptr的计数为0,那么通过Lock函数只能得到一个空指针。通过这一点可以通过weak_ptr来获取shared_ptr,例如

bool Deleted(weak_ptr<A> wp)
{
	if (auto shardPtr = wp.lock())
	{
		return false;
	}
	return true;
}

通过这样的语句就可以判断weak_ptr所指向的内存块是否已经被释放掉。

unique_ptr
shared_ptr和weak_ptr都可以做到多指针指向同一块内存。但是如果一块内存被一个unique_ptr所指,那么就不能再有任何其他的智能指针指向这块内存,即unique_ptr使用的是一一对应的关系。
unique_ptr的初始化只能通过裸指针或者unique_ptr的右值来初始化,不能通过其他的方式来构造,这一点是为了避免多个指针指向同一块内存。
智能指针内容整理-LMLPHP
由于unique_ptr始终保持单独地指向某块内存,在一些STL容器中如果存放的类型是unique_ptr,那么对容器作clear操作就会释放掉所有unique_ptr所指向的内存块,如果删掉容器中的某个unique_ptr,也会顺便释放掉相应的内存块。

智能指针与裸指针
如果确定要使用智能指针的话,就要避免所有的对裸指针的操作,所有的操作都在智能指针上进行。由于智能指针最终会对其包含的那个裸指针做一次delete,如果在对裸指针的操作过程中不小心把它delete了就会导致灾难性的后果。或许你会说谨慎一点仔细一点,不去delete它不就行了吗?但是要注意到使用智能指针就是因为你会忘记掉一些事情,如果你足够谨慎仔细,对裸指针的操作万无一失,那就没有必要用智能指针这个东西了,何必浪费内存和时间在这上面呢?智能指针都会提供一个Get接口来获取裸指针,但是这个接口只能说聊胜于无,尽量不要去用它。而如果说在某些情况下必须得对裸指针做非常精细的操作的话,那么在那些地方就不要用智能指针了,这样只会带来更多的麻烦。

10-03 17:37