智能指针这个东西想必学过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;
}
它的输出结果为:
可以看到在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;
}
最终运行结果为:
可以看到在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;
}
最终运行结果就是正常的。
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的右值来初始化,不能通过其他的方式来构造,这一点是为了避免多个指针指向同一块内存。
由于unique_ptr始终保持单独地指向某块内存,在一些STL容器中如果存放的类型是unique_ptr,那么对容器作clear操作就会释放掉所有unique_ptr所指向的内存块,如果删掉容器中的某个unique_ptr,也会顺便释放掉相应的内存块。
智能指针与裸指针
如果确定要使用智能指针的话,就要避免所有的对裸指针的操作,所有的操作都在智能指针上进行。由于智能指针最终会对其包含的那个裸指针做一次delete,如果在对裸指针的操作过程中不小心把它delete了就会导致灾难性的后果。或许你会说谨慎一点仔细一点,不去delete它不就行了吗?但是要注意到使用智能指针就是因为你会忘记掉一些事情,如果你足够谨慎仔细,对裸指针的操作万无一失,那就没有必要用智能指针这个东西了,何必浪费内存和时间在这上面呢?智能指针都会提供一个Get接口来获取裸指针,但是这个接口只能说聊胜于无,尽量不要去用它。而如果说在某些情况下必须得对裸指针做非常精细的操作的话,那么在那些地方就不要用智能指针了,这样只会带来更多的麻烦。