C#
、Java
、python
和go
等语言中都有垃圾自动回收机制,在对象失去引用的时候自动回收,而且基本上没有指针的概念,而C++
语言不一样,C++
充分信任程序员,让程序员自己去分配和管理堆内存,如果管理的不好,就会很容易的发生内存泄漏问题,而C++11
增加了智能指针(Smart Pointer)。主要分为shared_ptr
、unique_ptr
和weak_ptr
三种,使用时需要引用头文件
。c++98
中还有auto_ptr
,基本被淘汰了,不推荐使用。而c++11
中shared_ptr
和weak_ptr
都是参考的boost
库中实现的。
本文有很多代码片段,如果不喜欢的代码风格,请移步我的SegmentFault博客。
原始指针
c
语言中最常使用的是malloc()
函数分配内存,free()
函数释放内存,而c++
中对应的是new
、delete
关键字。malloc()
只是分配了内存,而new
则更进一步,不仅分配了内存,还调用了构造函数进行初始化。使用示例:
int main()
{
// malloc返回值是 void*
int* argC = (int*)malloc(sizeof(int));
free(argC);
char*c = (char*)malloc(100);
free(c);
char *age = new int(25); //做了两件事情 1.分配内存 2.初始化
int* height = new int(160);
delete height;
delete age;
char* arr = new int[100];
delete[] arr;
/*delete数组需要使用delete[],事实上,c++原始支持的数据结构组成的
数组不需要[]也可以,但 自定义的数据类型组成的数组必须使用delete[]*/
}
new
和delete
必须成对出现,有时候是不小心忘记了delete
,有时候则是很难判断在这个地方自己是不是该delete
,这个和资源的生命周期有关,这个资源是属于我这个类管理的还是由另外一个类管理的,如果是我管理的,就由我来delete
,由别人管理的就由别人delete
,我就算析构了也不影响该资源的生命周期。例如:
// 情况1: 需要自己delete
const char* getName() {
char *valueGroup = new char[1000];
// do something
return valueGroup;
}
// 情况2: 不需要自己delete
const char* getName2() {
static char valueGroup[1000];
// do something
return valueGroup;
}
只通过函数签名来看,这两个函数没有什么区别,但是由于实现的不同,有时候需要自己管理内存,有时候不需要,这个时候就需要看文档说明了。这就是使用一个"裸"指针不好的地方。
一点改进是,如果需要自己管理内存的话,最好显示的将自己的资源传递进去,这样的话,就能知道是该资源确实应该由自己来管理。
char *getName(char* v, size_t bufferSize) {
//do something
return v;
}
上面还是小问题,自己小心一点,再仔细看看文档,还是有机会避免这些情况的。但是在c++
引入异常的概念之后,程序的控制流就发生了根本性的改变,在写了delete的时候还是有可能发生内存泄漏。如下例:
void badThing(){
throw 1;// 抛出一个异常
}
void test() {
char* a = new char[1000];
badThing();
// do something
delete[] a;
}
int main() {
try {
test();
}
catch (int i){
cout << "error happened " << i << endl;
}
}
上面的new
和delete
是成对出现的,但是程序在中间的时候抛出了异常,由于没有立即捕获,程序从这里退出了,并没有执行到delete
,内存泄漏还是发生了。
C++
中的构造函数和析构函数十分强大,可以使用构造和析构解决这种问题,比如:
class SafeIntPointer {
public:
explicit SafeIntPointer(int v) : m_value(new int(v)) { }
~SafeIntPointer() {
delete m_value;
cout << "~SafeIntPointer" << endl;
}
int get() { return *m_value; }
private:
int* m_value;
};
void badThing(){
throw 1;// 抛出一个异常
}
void test() {
SafeIntPointer a(5);
badThing();
}
int main() {
try {
test();
}
catch (int i){
cout << "error happened " << i << endl;
}
}
// 结果
// ~SafeIntPointer
// error happened 1
可以看到,就算发生了异常,也能够保证析构函数成功执行!这里的例子是这个资源只有一个人使用,我不用了就将它释放掉。但还有种情况,一份资源被很多人共同使用,要等到所有人都不再使用的时候才能释放掉,对于这种问题,就需要对上面的SafeIntPointer
增加一个引用计数,如下:
class SafeIntPointer {
public:
explicit SafeIntPointer(int v) : m_value(new int(v)), m_used(new int(1)) { }
~SafeIntPointer() {
cout << "~SafeIntPointer" << endl;
(*m_used) --; //引用计数减1
if(*m_used <= 0){
delete m_used;
delete m_value;
cout << "real delete resources" << endl;
}
}
SafeIntPointer(const SafeIntPointer& other) {
m_used = other.m_used;
m_value = other.m_value;
(*m_used)++; //引用计数加1
}
SafeIntPointer& operator= (const SafeIntPointer& other) {
if (this == &other) // 避免自我赋值!!
return *this;
m_used = other.m_used;
m_value = other.m_value;
(*m_used)++; //引用计数加1
return *this;
}
int get() { return *m_value; }
int getRefCount() {
return *m_used;
}
private:
int* m_used;
int* m_value;
};
int main() {
SafeIntPointer a(5);
cout << "ref count = " << a.getRefCount() << endl;
SafeIntPointer b = a;
cout << "ref count = " << a.getRefCount() << endl;
SafeIntPointer c = b;
cout << "ref count = " << a.getRefCount() << endl;
}
/*
ref count = 1
ref count = 2
ref count = 3
~SafeIntPointer
~SafeIntPointer
~SafeIntPointer
real delete resources
*/
可以看到每一次赋值,引用计数都加一,最后每次析构一次后引用计数减一,知道引用计数为0,才真正释放资源。要写出一个正确的管理资源的包装类还是蛮难的,比如上面那个上面例子就不是线程安全的,只能属于一个玩具,在实际工程中简直没法用。而到了C++11
,终于提供了一个共享的智能指针解决这个问题。
shared_ptr共享的智能指针
shared_ptr的基本使用
shared_ptr
的基本使用很简单,看几个例子就明白了:
#include
#include
class Object {
public:
Object(int id) : m_id(id) {
std::cout << "init obj " << std::endl;
}
~Object() {
std::cout << "bye bye" << m_id << std::endl;
}
int id() const {
return m_id;
}
private:
int m_id;
};
// 取个别名 让写起来更方便
typedef std::shared_ptr
给shared_ptr指定删除器
大部分用法都基本上在上面的例子中体现出来了,当没有人引用这个资源的时候,智能指针的默认行为是调用 delete销毁这个资源,而我们也可以人为指定这个步骤,因为有些资源不一定是new
出来的,所以不应该使用默认的delete
行为,还有一个情况是,在用智能指针管理动态数组的时候,需要自己指定删除器函数。
#include
#include
class Object {
public:
Object(int id) : m_id(id) {
std::cout << "init obj " << std::endl;
}
~Object() {
std::cout << "bye bye" << m_id << std::endl;
}
int id() const {
return m_id;
}
private:
int m_id;
};
// 让写起来更方便
typedef std::shared_ptr
shared_ptr
主要就是利用变量出了作用域之后析构函数一定能被调用到,哪怕是出现了异常。
不要用一个原始的指针初始化多个shared_ptr
例如下面的例子:
#include
#include
class Object {
public:
Object(int id) : m_id(id) {
std::cout << "init obj " << std::endl;
}
~Object() {
std::cout << "bye bye " << m_id << std::endl;
}
int id() const {
return m_id;
}
private:
int m_id;
};
// 让写起来更方便
typedef std::shared_ptr
可以发现,虽然是用的同一个指针初始化了两个shared_ptr
,但是这两个shared_ptr
却没有关联,它们的引用计数都是1
,然后问题就发生了,p2
先析构,于是引用计数变为了0
,就开始删除它管理的资源obj
,于是obj
就被析构了,这是还算正常,接着析构p1
,引用计算也变成了0
,它也开始删除自己管理的资源obj
,相当于多次delete
了同一个对象,m_id
成为了随机数,这还算好的情况,如果Object
内部还有指针,或者obj
的地址被其他变量占据了,delete
掉这块内存就会发生严重的错误!而且不好发现原因。
将this指针正确的传递给shared_ptr
其实就是由于上面的原因,我们不可能传递this
指针给shared_ptr
,因为用同一个指针初始化两个shared_ptr
,它们之间并没有关联,如下面的例子:
#include
#include
class Y
{
public:
std::shared_ptr f(){
return std::shared_ptr(this);
}
};
int main()
{
std::shared_ptr p1(new Y());
std::shared_ptr p2 = p1->f(); // p2是由this构造共享智能指针
std::cout << p1.use_count() << " " << p2.use_count() << std::endl; // 1 1
}
从上面的例子可以看出,返回由this
构造的shared_ptr
并没有用,返回还可能造成严重错误(由于可能多次delete
)!解决办法是继承std::enable_shared_from_this
,然后使用shared_from_this()
构造shared_ptr
。
#include
#include
class Y : public std::enable_shared_from_this
{
public:
std::shared_ptr f(){
return shared_from_this();
}
};
int main()
{
std::shared_ptr p1(new Y());
std::shared_ptr p2 = p1->f(); // p2是由p1的thiss构造共享智能指针
std::cout << p1.use_count() << " " << p2.use_count() << std::endl; // 2 2
std::shared_ptr p3(new Y());
std::shared_ptr p4 = p3->f(); // p4是由p3的this构造的构造共享智能指针
std::cout << p1.use_count() << " " << p2.use_count() << " "
<< p3.use_count() << " " << p4.use_count() << std::endl; // 2 2 2 2
}
可以发现引用计数确实增加了。并且由p1
得到的shared_from_this()
增加的就是p1
的引用计数,p3
得到的shared_from_this()
增加的就是p3
的引用计数,这和this
的含义是一样的。所以我们在类内部需要传递this
指针给shared_ptr
时,需要继承自std::enable_shared_from_this
,并且使用shared_from_this()
替代this
。而shared_from_this()
就是借助了weak_ptr
。原理在后面再讲。
shared_ptr的正确构造方式
其实上面使用的智能指针构造方式有一点点问题,ObjectPtr obj(new Object(1));
这一个语句其实调用了两次new
,一次是new Object(1)
,另一次是构造内部的引用计数变量的时候,那有没有办法只掉用一次new
呢,答案就是使用make_shared
模板函数,它将资源和引用计数变量一起new
出来,例如:
#include
#include
#include
class Object {
public:
Object(int id) : m_id(id) {
std::cout << "init obj " << std::endl;
}
~Object() {
std::cout << "bye bye " << m_id << std::endl;
}
int id() const {
return m_id;
}
private:
int m_id;
};
// 让写起来更方便
typedef std::shared_ptr
然而,这个函数也有失效的时候,如果管理的资源对象的构造函数是私有的他就没有办法了。
weak_ptr弱引用的智能指针
循环引用问题的引出
在有些情况下,shared_ptr
也会遇见很尴尬、不能处理的情况,那就是循环引用,看下面的例子:
#include
#include
class Parent; //Parent类的前置声明
typedef std::shared_ptr ParentPtr;
class Child {
public:
ParentPtr father;
~Child() {
std::cout << "bye child" << std::endl;
}
};
typedef std::shared_ptr ChildPtr;
class Parent {
public:
ChildPtr son;
~Parent() {
std::cout << "bye parent" << std::endl;
}
};
void testParentAndChild() {
ParentPtr p(new Parent()); // 1 资源A
ChildPtr c(new Child()); // 2 资源B
p->son = c; // 3 c.use_count() == 2 and p.use_count() == 1
c->father = p; // 4 c.use_count() == 2 and p.use_count() == 2
}
int main() {
testParentAndChild();
std::cout << "finished" << std::endl;
}
/*
// 没有调用Parent 和 Child 的析构函数
finished
*/
很惊讶的发现,用了shared_ptr
管理资源,资源最后还是没有释放!内存泄漏还是发生了。
分析:
- 执行编号
1
的语句时,构造了一个共享智能指针p
,称呼它管理的资源叫做资源A
(new Parent()
产生的对象)吧, 语句2
构造了一个共享智能指针c
,管理资源B
(new Child()
产生的对象),此时资源A
和B
的引用计数都是1
,因为只有1
个智能指针管理它们,执行到了语句3
的时候,是一个智能指针的赋值操作,资源B
的引用计数变为了2
,同理,执行完语句4
,资源A
的引用计数也变成了2
。 - 出了函数作用域时,由于析构和构造的顺序是相反的,会先析构共享智能指针
c
,资源B
的引用计数就变成了1
;接下来继续析构共享智能指针p
,资源A
的引用计数也变成了1
。由于资源A
和B
的引用计数都不为1
,说明还有共享智能指针在使用着它们,所以不会调用资源的析构函数! - 这种情况就是个死循环,如果资源
A
的引用计数想变成0
,则必须资源B
先析构掉(从而析构掉内部管理资源A
的共享智能指针),资源B
的引用计数想变为0
,又得依赖资源A
的析构,这样就陷入了一个死循环。
要想解决这个问题,只能引入新的智能指针weak_ptr
,顾名思义,弱引用,也就是不增加引用计数,它不管理shared_ptr
内部管理的指针,他只是起一个监视的作用。它监视的不是shared_ptr
本身,而是shared_ptr
管理的资源!!!weak_ptr
没有重载操作符*
和->
,它不能直接操作资源,但是它可以获取所监视的shared_ptr
(如果资源还没有被析构的话)。
weak_ptr的基本用法
weak_ptr
使用示例:
#include
#include
class Object {
public:
Object(int id) : m_id(id) {
std::cout << "init obj " << std::endl;
}
~Object() {
std::cout << "bye bye" << m_id << std::endl;
}
int id() const {
return m_id;
}
private:
int m_id;
};
// 取个别名 让写起来更方便
typedef std::shared_ptr ObjectPtr;
void sharedPtrWithWeakPtr() {
ObjectPtr obj(new Object(1));
typedef std::weak_ptr WeakObjectPtr;
WeakObjectPtr weakObj(obj); //使用共享指针 初始化 弱引用指针
//weakObj 仅仅是一个监听者,不会增加引用计数
std::cout << "obj use count is " << obj.use_count() << std::endl; // 1
{
// lock() 方法返回一个 它对应的共享指针
// 下面这句话的结果是 2, 而不是1,
// 说明weakObj.lock() 内部也得到了一个新的共享指针,所以引用计数+1
// 在执行完这句话后就析构掉了,引用计数-1
std::cout << "weakObj.lock().use_count() is " << weakObj.lock().use_count() << std::endl; // 2
// 由于发生了一次 赋值 ,所以 引用次次数 +1
// auto === ObjectPtr
auto p = weakObj.lock(); //如果weakObj监视的资源存在, p就存在
std::cout << "obj use count is " << obj.use_count() << std::endl; // 2
if ( p ) {
// do what you want to do
} else {
}
}
// 共享指针不再管理任何资源的时候,weakObj的行为
// 注意:如果在obj.reset前,还存在共享指针管理它的资源
// 如 :ObjectPtr obj1(obj); weakObj.lock();还是有效的
obj.reset();
{
auto p = weakObj.lock();
if( p ) {
//不应该到这里来
std::cout << "weakObj is not null 1" << std::endl;
} else {
std::cout << "weakObj is null 1" << std::endl;
}
}
// 共享指针管理其他资源的时候,weakObj的行为
// 注意:weak_ptr.lock()
// 只有在 存在某一个shared_ptr管理的资源和该weak_ptr一样 的时候才有效果!
obj.reset(new Object(2));
{
auto p = weakObj.lock();
if( p ) {
//不应该到这里来
std::cout << "weakObj is not null 2" << std::endl;
} else {
std::cout << "weakObj is null 2" << std::endl;
}
}
weakObj = obj; // 重新监视 obj
// 用weakObj 判断管理的资源是否过期
if(weakObj.expired()) {
} else {
}
}
int main() {
sharedPtrWithWeakPtr();
std::cout << "finished" << std::endl;
}
/*
init obj
obj use count is 1
weakObj.lock().use_count() is 2
obj use count is 2
bye bye1
weakObj is null 1
init obj
weakObj is null 2
bye bye2
finished
*/
由上面的例子可以看出,weak_ptr
和初始化它的share_ptr
没有关系,而是和share_ptr
管理的资源有关系。假如WeakObjectPtr weakObj(obj);
,如果obj.reset()
,weakObj.lock()
的返回值就是空,如果obj.reset(new Object(2));
,替换了管理对象,则一起的资源就被析构了,weakObj.lock()
的返回值同样为空,同样可以推断,如果除了obj
以外还有其他共享智能指针一起管理资源,也就是说obj.reset()
的时候资源不会被析构,weakObj.lock();
的返回值就不会为空了。不明白的话自己写个简单的测试用例就知道了,如:
void sharedPtrWithWeakPtr() {
ObjectPtr obj(new Object(1));
typedef std::weak_ptr WeakObjectPtr;
WeakObjectPtr weakObj(obj); //使用共享指针 初始化 弱引用指针
ObjectPtr obj1 = obj; //注释掉这句话打印的就是error, 加上这句话打印的就是ok
obj.reset();
auto p = weakObj.lock();
if( p ) {
std::cout << "ok" << std::endl;
} else {
std::cout << "error" << std::endl;
}
}
weak_ptr解决循环引用
用weak_ptr
可以解决上面的循环引用问题,将Child
内部的parent
指针换成weak_ptr
管理:
#include
#include
class Parent; //Parent类的前置声明
typedef std::shared_ptr ParentPtr;
typedef std::weak_ptr WeakParentPtr;
class Child {
public:
WeakParentPtr father;
~Child() {
std::cout << "bye child" << std::endl;
}
};
typedef std::shared_ptr ChildPtr;
//typedef std::weak_ptr WeakChildPtr;
class Parent {
public:
//WeakChildPtr son;
ChildPtr son;
~Parent() {
std::cout << "bye parent" << std::endl;
}
};
void testParentAndChild() {
ParentPtr p(new Parent()); // 1 资源A
ChildPtr c(new Child()); // 2 资源B
p->son = c; // 3 c.use_count() == 2 and p.use_count() == 1
c->father = p; // 4 c.use_count() == 2 and p.use_count() == 1
}
int main() {
testParentAndChild();
std::cout << "finished" << std::endl;
}
/*
bye parent //成功调用析构函数
bye child
finished
*/
修改为弱引用后,成功的释放了资源,只要将任意一个shared_ptr
换成weak_ptr
,就可以解决问题。当然,也可以两个都换成weak_ptr
,至于这三种方案谁更好,就暂时不清楚了。
shared_from_this()实现原理
std::enable_shared_from_this
模板类中有一个weak_ptr
,这个weak_ptr
用来观测this
智能指针,调用shared_from_this()
函数的时候,会在内部调用weak_ptr
的lock()
方法,将所观测的shared_ptr
返回。这个设计要依赖于当前对象已经有了一个相应的控制块。为此,必须已经存在一个指向当前对象的shared_ptr
(比如在调用过shared_from_this()
成员函数之外已经有了一个)。假如没有这样shared_ptr
存在,那么shared_from_this()
会抛异常。 那么这个weak_ptr
在什么时候赋值的呢?答案就是在外部第一次构造shared_ptr
的时候(如之前的std::shared_ptr
),对std::enable_shared_from_this
进行了赋值(具体实现有点复杂,还不太懂。。),这也就为什么在调用shared_from_this()
时,必须存在一个指向当前对象的shared_ptr
的原因了。由于这个原因,不要在构造函数中调用shared_from_this()
,如:
#include
#include
class Y : public std::enable_shared_from_this
{
public:
Y() {
std::shared_ptr p = shared_from_this();
}
std::shared_ptr f(){
return shared_from_this();
}
};
int main()
{
std::shared_ptr p1(new Y());
}
// 会抛出异常!
/*
terminate called after throwing an instance of 'std::bad_weak_ptr'
what(): bad_weak_ptr
*/
unique_ptr独占的智能指针
unique_ptr
相对于其他两个智能指针更加简单,它和shared_ptr
使用差不多,但是功能更为单一,它是一个独占型的智能指针,不允许其他的智能指针共享其内部的指针,更像原生的指针(但更为安全,能够自己释放内存)。不允许赋值和拷贝操作,只能够移动。
#include
#include
#include
class Object {
public:
Object(int id) : m_id(id) {
std::cout << "init obj " << std::endl;
}
~Object() {
std::cout << "bye bye " << m_id << std::endl;
}
int id() const {
return m_id;
}
private:
int m_id;
};
// 让写起来更方便
typedef std::shared_ptr ObjectPtr;
typedef std::unique_ptr UniqueObjectPtr;
// 只能传递引用 不能传值
void print(const UniqueObjectPtr& obj) {
std::cout << obj->id() << std::endl;
}
int main() {
UniqueObjectPtr obj(new Object(1));
// UniqueObjectPtr obj1 = obj; // 编译错误,不允许赋值
// 获取原生指针
auto p = obj.get();
if( p ) {
} else {
}
// better 重载了 operator bool
if(obj) {
} else {
}
// 重载了 operator -> 和 operator *
std::cout << obj->id() << " " << (*obj).id() << std::endl;
print(obj);
// 释放管理的指针,由其他东西处理
p = obj.release();
// delete p; 自己负责处理
obj.reset(p); //析构之前负责管理的对象,重新管理 p指针
obj.reset(); // 析构之前负责管理的对象, 不再管理任何资源
// 允许 移动操作
UniqueObjectPtr obj1(new Object(1));
// obj1已经部管理任何资源 obj2开始管理obj1之前的资源
UniqueObjectPtr obj2 = std::move(obj1);
assert(obj1 == nullptr);
std::cout << obj2->id() << std::endl;
// 将unique_ptr管理的内容给 shared_ptr
ObjectPtr obj3(std::move(obj2));
assert(obj2 == nullptr);
}
/*
init obj
1 1
1
bye bye 1
init obj
1
bye bye 1
*/
unique_ptr
管理数组资源不需要指定删除器:
std::shared_ptr p(new int[10], [](int* p){
std::cout << "delete[] p" << std::endl;
delete[] p; //需要使用delete[]
});
std::unique_ptr p2(new int[10]); //不需要指定删除器
性能与安全的权衡
使用智能指针虽然能够解决内存泄漏问题,但是也付出了一定的代价。以shared_ptr
举例:
-
shared_ptr
的大小是原始指针的两倍,因为它的内部有一个原始指针指向资源,同时有个指针指向引用计数。 - 引用计数的内存必须动态分配。虽然一点可以使用
make_shared()
来避免,但也存在一些情况下不能够使用make_shared()
。 - 增加和减小引用计数必须是原子操作,因为可能会有读写操作在不同的线程中同时发生。比如在一个线程里有一个指向一块资源的
shared_ptr
可能调用了析构(因此所指向的资源的引用计数减一),同时,在另一线程里,指向相同对象的一个shared_ptr
可能执行了拷贝操作(因此,引用计数加一)。原子操作一般会比非原子操作慢。但是为了线程安全,又不得不这么做,这就给单线程使用环境带来了不必要的困扰。
我觉得还是分场合吧,看应用场景来进行权衡,我也没啥经验,但我感觉安全更重要,现在硬件已经足够快了,其他例如java
这种支持垃圾回收的语言不还是用的很好吗。
总结
- 智能指针主要是使用构造和析构来管理资源的。
-
shared_ptr
很好用也很难用,有两种构造方式,使用引用计数实现多人同时管理一份资源。使用this
的时候要格外注意。 -
weak_ptr
可以解决shared_ptr
的循环引用问题。 -
unique_ptr
最像裸指针,但更为安全,保证资源的释放,不能复制只能移动。 - 智能指针带来了性能问题,在不同场合可以选择不同的解决方案。优先使用类的实例(如果内存足够),其次
unique_ptr
,最后才是shared_ptr
。
参考
- 大部分内容出自c++游戏服务器编程
- 深入应用C++11:代码优化与工程级应用
- Effective Modern C++