智能指针
持续更新,补充C++新增feature,目录结构遵循《C++ Primer》
动态内存
- 静态内存:保存局部static对象,类static数据成员,以及定义在任何函数之外的变量
- 栈内存:保存定在函数内非static对象,
- 堆内存:动态分配与回收
对于静态内存和栈内存中的对象,编译器负责销毁,对栈对象,栈被回收后即被销毁,static对象在使用之前分配,在程序结束时销毁。对于堆上的对象,由程序来控制,当对象不再使用时,需要代码显式将其删除。
对于动态内存的管理,C++ 11中引入了三类智能指针,定义在<memory>
头文件
- shared_ptr: 允许多个指针指向同一对象
- unique_ptr: “独占”所指向的对象
- weak_ptr: 弱引用,指向
shared_ptr
所管理的对象
当然也可以直接使用new
和delete
来直接控制内存
new & delete
new
在对象上开辟一个具有某类型大小的空间,并且可以对该块内存进行初始化,默认情况下,动态分配的对象时默认初始化的,即调用类的默认构造函数来初始化。
如果在执行new
的时候,内存耗尽,则系统会抛出std::bad_alloc
的异常,也可以向new
传参控制其行为
//pi指向一个动态分配的,未初始化的无名对象
int *p1 = new int;
//定义并初始化
int *p2(new int(100));
string *p2 = new string(10,'9');
//如果内存不足,不抛异常,返回一个空指针
int *p3 = new (nothrow) int;
对于delete
,在实际应用中也会有很多问题,比如下面的情况:
int *p(new int(42));
auto q = p;
delete p;
p = nullptr;
当p
被delete后,其内存被释放掉,对于q
来说,却不知道,仍会继续访问,从而出现异常。类似的问题还有指针的double free,以及将指针作为函数的返回值返回后,使用者忘记delete该指针(或许根本不清楚这个指针怎么来的),而造成内存泄漏
unique_ptr
当需要使用智能指针时,可以首先考虑使用unique_ptr
,使用它几乎和使用原始指针一样,并且性能上几乎没有损耗。不同的是一个unique_ptr
独占一个资源的控制权(exclusive ownership
),指针,你不能用两个unique_ptr
来控制同一份资源,即某个时刻只能有一个unique_ptr
指向一个给定的对象。使用unique_ptr
另一个方便之处是,你不需要担心资源的释放,当unique_ptr
对象销毁时,被管理的资源也将自动被销毁。
- 创建
创建unique_ptr
必须采用直接初始化的形式,并且不支持拷贝与赋值操作
//创建一个int指针
unique_ptr<int> p1(new int(42));
//创建一个string[]数组
unique_ptr<string[]> ps(new string[3]);
//不支持拷贝构造和赋值操作
unique_ptr<int> p2(p1); //wrong;
auto p3 = p1; //wrong
make_unique
C++ 14引入了make_unique
函数,可以用来创建unique_ptr
,官方文档也推荐使用这种方式以取代new
的方式,但是这种方式也有局限性,其中之一是它不支持自定义的delete
函数(后面会介绍)
//使用new
auto ptr = unique_ptr<widget> (new widget(params));
//使用make
auto ptr = make_unique<widget>(params);
- 控制权转移
一个unique_ptr
可以通过release()
方法将对某个内置指针的控制权转移给另一个unique_ptr
对象,
unique_ptr<string> p1(new string("abc")); //交出控制权后,p1=nullptr
unique_ptr<string> p2(p1.release());
unique_ptr<string> p3(p2.release());
//p1 == nullptr & p2 == nullptr
注意,release
操作是切断了unique_ptr
和其内部指针之间的联系,release
返回的指针通常用来初始化另一个智能指针
p2.release(); //wrong,p2不会释放内存,却丢失了内置指针
auto p = p2.release(); //正确,但是要手动delete p
- 作为返回值
不能拷贝unique_ptr
的规则有一个例外,我们可以拷贝或赋值一个将要销毁的unique_ptr
unique_ptr<int> clone(int p){
return unique_ptr<int>(new int(p));
}
- 释放资源
unique_ptr
支持自定义析构函数,其析构函数必须将其类型用于构造unique_ptr
。
//p指向一个类型为objT的对象,并使用一个类型为delT的函数释放资源
unique_ptr<objT, delT> p (new objT, fcn);
void f(destination &d){
connnection c = connect(&d);
unique_ptr<connection, decltype(end_connection)* >p(&c,end_connection);
}
注意上述代码中在decltype(end_connection)
后必须加一个*
表示函数指针。
shared_ptr
最安全的分配和使用动态内存的方法是使用make_shared<T>
函数,头文件定义在<memory>
中,make_shared
参数和待构造对象的构造函数参数相同,make_shared
会将其参数透传给类的构造函数。
//指向一个值为32的int型指针r
shared_ptr<int> p3 = make_shared<int>(42);
cout<<*p3 <<endl; //42
//创建指向值为“ccc”的字符串指针
shared_ptr<string> p4= make_shared<string>(3,'c');
//使用auto
auto p5 = make_shared<vector<string>>();
除了使用make_shared
以外,还可以使用new
来创建指针,当时用new
时,必须使用其explicit
的构造函数,而不能通过隐式的类型转换
// /shared_ptr<int> p1 = new int(10); //wrong!
shared_ptr<int> p1(new int(10)); //correct, 使用直接初始化形式
shared_ptr<int> clone(int p){
//return new int(p); //worng
return shared_ptr<int>(new int(p)); //correct
}
这里需要注意一种情况,不要混用普通指针和shared_ptr
,在创建shared_ptr
时就new对象或者使用make_shared
,考虑下面情况
shared_ptr<int> p = new(int(10));
int *ptr = p.get(); //返回p管理的指针
{
shared_ptr<int>q (ptr);
}
//q被释放,导致`ptr`指向的内存被释放,此时p并不知情
int foo = *p; //crash
当使用一个已经初始化的指针赋给q
时,会导p
和q
管理同一片内存,并且这种情况下,且各自的引用计数均为1,二者中任何一个被销毁会导致该内存被释放,会有野指针访问或者double free的风险
避免将一个已经初始化的指针来构造`shared_ptr`对象
- 拷贝和赋值
当对shared_ptr
对象进行拷贝时,会影响到其引用计数,每个shared_ptr
都关联一个引用计数,来记录有多少个其它的shared_ptr
指向相同的对象。当发生拷贝时,引用计数+1,拷贝可以发生在构造函数,函数传参以及函数返回值。
auto p = make_shared<int>(20); //p所指向对象的引用计数 = 1
//可以使用unique()查看被管理的资源是否是自己独有
cout<<p.unique()<<endl; //true;
auto q(p); //q执行拷贝构造,引用计数+1
cout<<p.unique()<<endl; //false, 因为p和q共享该资源
cout<<q.user_count()<<endl; //引用计数为2
当给一个shared_ptr
对象赋一个新值或是该对象被销毁时,其管理的引用计数会-1。一旦引用计数为0,则shared_ptr
会自动释放对象内存。
auto r = make_shared<int>(19);
r = q;
//q指向对象的引用计数+1
//r指向对象的引用计数-1
//r的引用计数为0,自动释放内存
使用shared_ptr
的一个好处是不需要惦记何时去释放对象,一个例子是用shared_ptr
对象做返回值
shared_ptr<Foo> factory(T arg){
return mark_shared<Foo>(arg);
}
按照以往的经验,使用这种方式来返回一直指针是有风险的,原因在于使用者很有可能不知道怎么处理这个指针;如果使用shared_ptr
则可以让使用者无需关心这个问题
void use_factory(T arg){
auto p = factor(arg);
return p;
}
//p离开作用于,内存自动释放
再看一个函数传参的例子
void process(shared_ptr<int> ptr){
//ptr被copy,引用计数+1
do_some_thing();
//ptr被释放,引用计数-1
}
shared_ptr<int> p(new int(42));
process(p);
int i=*p;
- 使用
reset
reset
的作用是使shared_ptr
指针指向一个新的对象:
auto p6 = make_shared<DynamicArray>(10);
p6.reset(new DynamicArray(11));
这里需要注意一个问题,如果p6
是当前内存对象的唯一持有者,那么当p6
被reset时,内存对象被释放,但是如果p6
不是唯一持有者的时,如果p6
想要修改该内存对象,右不影响其它持有者,则需要单独拷贝一份内存
if(!p6.unique()){
p6.reset(new string(*p));
}
*p += newVal;
- 释放资源
默认情况下当shared_ptr
对象析构时,会调用delete
来释放内部管理的对象的内存。shared_ptr
也提供了自定义释放的方法,可以在构造时,传入一个lambda表达式,函数指针或者Functor来自定义析构逻辑
//使用lambda表达式清理int数组
shared_ptr<int> sp(new int[10],[](*p){
delete []p;
})
- API整理
shared_ptr<T> p(q) |
q是内置指针 |
shared_ptr<T> p(u) |
u是unique_ptr,p从u那里接管了对象的所有权,将u置为空 |
shared_ptr<T> p(q,d) |
q是内置指针,d是自定义析构函数 |
shared_ptr<T> p(p2,d) |
p是shared_ptr |
reset |
若p是唯一指向其对象的shared_ptr, reset会释放此对象 |
reset(q) |
p指向内置指针q |
weak_ptr
weak_ptr
是一种不控制所指对象生命周期的智能指针,它指向由一个shared_ptr
管理的对象,将weak_ptr
绑定到shared_ptr
不会影响改变后者的引用计数。一旦最后一个指向对象的shared_ptr
被销毁,对象内存就会被释放,即是有weak_ptr
存在,对象依旧会被释放。
创建一个weak_ptr
对象,需要传入shared_ptr
。使用weak_ptr
时,不能直接访问其内置指针,因为对象可能已经被释放,需要先使用lock
检查对象是否存在,进而访问
auto p = make_shared<int>(42);
weak_ptr<int> wp(p);
if(shared_ptr<int> np = wp.lock()){ //lock返回一个shared_ptr对象
cout<<*np<<endl;
}
动态数组
new
和数组
int *pia = new int[10]; //10个未初始化的int
int *pia2 = new int[10](); //10个初值为0的int
int *pia3 = new int[10] {1,23,3,5,6,7,7,8,9};
int *psa = new string[10]; //10个空string
int *psa2 = new string[10](); //10个空string
//delete
delete [] pia;
在销毁数组中的对象时,析构顺序按照逆序进行,即最后一个元素先被销毁
- 使用智能指针
与new
对应,C++ 11中刻意使用std::unique_ptr<int[]>
类型来包装动态数组
unique<int[]> up(new int[10]);
up.release(); //自动delete []销毁其指针
由于unique_ptr
指向一个数组,因此没有点或者箭头成员运算符,如果想要遍历数组,可以还是用for
循环
for(int i=0;i<10;i++){
up[i] = i; //unique_ptr支持下标运算
}
如果想要使用shared_ptr
则需要为其指定析构函数
shared_ptr<int> sp(new int[10], [](int *p){ delete [] p;});
另外,shared_ptr
并不支持下标运算,因此想要遍历数组需要通过get()
得到指针周再遍历,这不是一种好的选择。
使用allocator
new
有一些灵活上的局限性,其中一方面是它将内存分配和对象构造结合在了一起,类似的,delete
也将对象析构和内存释放结合在了一起。对于单个对象,这种策略是可行的,但是当分配一大块内存时,我们希望将内存的分配和对象的构造进行分离。C++ 标准库中的allocator
提供了可以帮助我们完成这个工作
allocator<string> alloc;
auto const p = alloc.allocate(n);