智能指针是存储指向动态分配(堆)对象指针的类。用于生存期控制,能够确保在离开指针所在作用域时,自动正确的销毁动态分配的对象,防止内存泄漏。它的一种通用实现技术是使用引用计数。美使用它一次,内部的引用计数加1,每析构一次,内部引用计数减1,减为0时,删除所指向的堆内存。
shared_ptr共享的智能指针
std::shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。在最后一个shared_ptr析构的时候,内存才会被释放。
shared_ptr的基本用法
初始化
可以通过构造函数,std::make_shared<T>辅助函数和reset方法来初始化shared_ptr,代码如下:
#include <memory>
#include <iostream>
using namespace std;int main()
{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" << endl;}///编译报错,不允许直接赋值///std::shared_ptr<int> p4 = new int(1);return 0;
}
我们应该优先使用make_shared来构造智能指针,因为它更加高效。
不能将一个原始指针直接赋值给一个指针指针,例如,下面这种方法是错误的:
///编译报错,不允许直接赋值
std::shared_ptr<int> p = new int(1);
可以看到智能指针的用法和普通指针的用法类似,只不过不需要自己管理分配的内存。shared_ptr不能通过直接将原始指针赋值来初始化,需要通过构造函数和辅助方法来初始化。对于一个未初始化的智能指针,可以通过reset方法来初始化,当智能指针中有值的时候,调用reset会使引用计数减1。另外,智能指针可以通过重载的bool类型符来判断智能智能中是否为空(未初始化)。
获取原始指针
当需要获取原始指针时,可以通过get方法来返回原始指针,代码如下:
#include <memory>
#include <iostream>
#include <string>
using namespace std;int main()
{std::shared_ptr<int> p(new int(1));int* pp = p.get();cout << *pp << endl;std::shared_ptr<string> pstr(new string("hello world"));auto ppstr = pstr.get();cout << *ppstr << endl;return 0;
}
指定删除器
智能指针初始化可以指定删除器,代码如下:
#include <memory>
#include <iostream>
#include <string>
#include <string.h>
using namespace std;typedef struct Message
{char* ptr;int i;double db;
}Message;void DeletePtr(Message* pm)
{cout << pm->i << endl;cout << pm->db << endl;cout << pm->ptr << endl;if (pm->ptr){delete [] pm->ptr;pm->ptr = nullptr;}
}int main()
{Message* m = new Message;m->db = 2.8;m->i = 1;m->ptr = new char[100];memset(m->ptr, 0, 100);char* ptr = (char*)"hello world";strncpy(m->ptr, ptr, strlen(ptr));std::shared_ptr<Message> p(m, DeletePtr);///std::shared_ptr<Message> pp(m, [](Message* pm){if (pm->ptr) { cout << pm->ptr << endl; delete [] pm->ptr;}});return 0;
}
当p的引用计数为0时,自动调用删除器DeletePtr来释放对象的内存。删除器可以是一个lambda表达式,因此,上面的写法还可以改为:
std::shared_ptr<Message> pp(m, [](Message* pm){if (pm->ptr) { cout << pm->ptr << endl; delete [] pm->ptr;}});
使用shared_ptr需要注意的问题
智能指针虽然能自动管理堆内存,但是它哟不少缺陷,在使用时需要注意。
1、不要用一个原始指针初始化多个shared_ptr,例如下面这些是错误的:
int* ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr); ///logic error
2、不要在函数实参中创建shared_ptr,对于下面的写法是错误的:
function(shared_ptr<int>(new int), g());
因为C++的函数参数的计算顺序在不同的编译器不同的调用约定下可能是不一样的,一般是从右到左,但也有可能是从左到右,所以,可能的过程是先new int,然后调用g(),如果恰好g()发生异常,而shared_ptr<int>还没有创建,则int内存泄漏,正确的写法是先创建智能指针,代码如下:
shared_ptr<int> p(new int);
f(p, g());
3、通过shared_from_this()返回this指针。不要将this指针作为shared_ptr返回出来,因为this指针本质上是一个裸指针,因此,这样可能会导致重复析构,看下面的代码:
#include <memory>
#include <iostream>
#include <string>
#include <string.h>
using namespace std;struct A
{shared_ptr<A> GetSelf(){return shared_ptr<A>(this);}///析构函数调用了2次~A(){cout << "delete A" << endl;}
};int main()
{shared_ptr<A> sp1(new A);shared_ptr<A> sp2 = sp1->GetSelf();return 0;
}
在这个例子中,由于用同一个指针(this)构造了两个智能指针sp1和sp2,而他们之间是没有任何关系的,在离开作用域之后this将会被构造的两个智能指针各自析构,导致重复析构的错误。
正确返回this的shared_ptr的做法是:让目标类通过派生std::enable_shared_this<T>类,然后使用基类的成员函数shared_from_this来返回this的shared_ptr,看下面的代码:
#include <memory>
#include <iostream>
#include <string>
#include <string.h>
using namespace std;struct A : public std::enable_shared_from_this<A>
{shared_ptr<A> GetSelf(){return shared_from_this();}///析构函数调用了1次~A(){cout << "delete A" << endl;}
};int main()
{shared_ptr<A> sp1(new A);shared_ptr<A> sp2 = sp1->GetSelf();return 0;
}
4、要避免循环引用。指针指针最大的一个陷阱是循环引用,循环引用会导致内存泄漏。下面是一个典型的循环引用的场景。
#include <memory>
#include <iostream>
#include <string>
#include <string.h>
using namespace std;struct A;
struct B;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;}
};int main()
{{///析构函数没有被调用shared_ptr<A> ap(new A);shared_ptr<B> bp(new B);ap->bptr = bp;bp->aptr = ap;}return 0;
}
测试结果是两个指针A和B都不会被删除,存在内存泄漏。循环引用导致ap和bp引用计算器为2,在离开作用域之后,ap和bp的引用计数器减为1,并不会为0,导致两个指针不会被析构,产生了内存泄漏。