引言
在C++中,最麻烦的就是内存管理。动态内存的使用很容易出问题,因为确保在正确的时间释放内存是非常困难的事情。
有时,我们会忘记释放内存,这种情况就会出现内存泄漏;有时在指针还在引用内存的情况释放了它,这种情况就会出现引用非法内存的指针。
为了能更加安全的使用动态内存,C11提供了两种智能指针类型管理动态对象 。
智能指针
- shared_ptr 允许多个指针指向一个对象。
- unique_ptr 则独占所指向的对象。
- weak_ptr 弱引用。
shared_ptr 和 unique_ptr都支持的操作
操作 | 功能 |
---|---|
shared_ptr<T> sp | 空智能指针,可以指向类型为T的对象 |
unique_ptr<T> sp | 同上 |
*p | 解引用p,获得它指向的对象 |
p.get() | 返回p中保存的指针。要小心使用,若智能指针释放了对象,返回的指针所指的对象也就消失了 |
swap(p, q) | 交换p和q的指针 |
p.swap(q) | 同上 |
shared_ptr独有的操作
操作 | 功能 |
---|---|
make_shared<T> (args) | 返回了一个动态分配的类型为T的对象。使用args初始化该对象 |
shared_ptr<T> p(q) | p是shared_ptr q的拷贝:此操作会递增q中的计数器。q中的指针必须转换为*T |
p.unique() | 若p.use_count()为1,返回true;否则返回false; |
p.use_count() | 返回与p共享对象的智能指针数量:可能很慢,主要用于调试 |
make_shared也定义在头文件memory中
shared_ptr 的拷贝和赋值
auto p = make_shared<int>(42);
auto q(p);
每个shared_ptr都有一个关联的计数器,通常称其为引用计数。
无论何时,我们拷贝一个shared_ptr,计数器都会递增,例如,当用一个shared_ptr初始化另一shared_ptr,或者让它作为参数传一个函数,以及做函数的返回值(用的是拷贝传参)。它所关联的计数器都会递增。
但我们给shared_ptr赋予一个新值,或者shared_ptr被销毁。计数器就会递减。
一旦一个shared_ptr的计数器变为0。它就会自动**释放自己所管理的对象。**这一特性使得动态内存的使用变得简单。
auto r = make_shared<int>(42);
r = q; //赋予新值。计数器减一
shared_ptr使用场景
现在我们要使用一个函数,它返回一个shared_ptr,指向一个Foo类型的动态分配的对象。
shared_ptr<Foo> factory(T arg){
return make_shared<Foo>(arg);
}
由于factory返回一个shared_ptr,所以我们可以确保它分配的对象会在恰当的时刻被释放掉。例如
void use_factory(T arg){
shared_ptr<Foo> p = factory(arg);
}
由于p是函数的局部变量,在use_factroy结束时,它将会被销毁。当p被销毁,将递减其计数器并检查它是否为0,如果p是唯一引用factory返回的内存的对象,那么该对象会被释放,否则继续保留。
当多其他地方需要这个shared_ptr指向这个内存,那么它就不会被释放掉。
void use_factory(T arg){
shared_ptr<Foo> p = factory(arg);
return p; //当我们返回p,引用计数进行了增量操作
}//p离开了作用域,但是它指向的内存没有被释放掉
shared_ptr和new混合使用
shared_ptr<double> p1 = new int(43); //错误:必须使用直接初始化式
shared_ptr<int> p2(new int(43)); //正确:使用了直接初始化式
shared_ptr不能支持隐式的转换(从内置指针到智能指针的转换),因此第一条的初始化语句是错的。
出于同种原因。下面的语句也是错的。
shared_ptr<int> clone(int p){
return new int(p); //错误
}
要改成
shared_ptr<int>
clone(int p){
return shared_ptr<int>(new int(p));
}
默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete来释放自己的内存。
定义和改变shared_ptr的方法 | 功能 |
---|---|
shared_ptr<T> p(q) | p 管理内置指针q所指向的对象;q必须指向new分配的内存,且能够转化为T类型 |
shared_ptr<T> p(u) | p从unique_ptr u那里接管了对象的所有权,将u设置为空 |
shared_ptr<T> p(q, d) | p从q那里接管了对象的所有权,并使用d代替delete操作 |
p.reset() | 若p是唯一指向对象的shared_ptr,reset将会释放此时的对象 |
p.reset(q) | 若传递了可选的参数内置指针q ,会令p指向q,否则将p置为空 |
p.reset(q, d) | 若传递了参数d,将会调用d,将会使用d代替delete操作来释放q |
注意事项
不要使用独立的智能指针指向同一个对象
这里使用p.get(), 返回p保存的指针给q,然后再某个函数体 又将另一个独立的shard_ptr保存
而函数体内的shared_ptr的count和函数体外的shared_ptr并不是同一个计数器,函数结束后,将指针指向的对象释放掉,那么shared_ptr p所指向的也释放掉,从而导致了错误。
shared_ptr<int> p(new(int(43));
int *q = p.get();
void func(){
shared_ptr<int>(q);
}
int foo = *p;//error
智能指针和异常
在使用智能指针,函数遇到了异常。
void f(){
shared_ptr<int> p(new int (43));
//函数体发生了异常,并且没有被函数f捕获。
//在函数体结束时,通过shared_ptr释放了内存。
}
在使用内置指针,函数遇到了异常
void f(){
int *p = new int(43);
//函数发生了异常
delete(p);//这时候由于函数发生了异常,导致new出来的内存一直都没法释放掉。
}
智能指针在哑类的使用
在标准库内有许多C++的类都定义了析构函数,负责清理对象使用的资源。但是不是所有的类都有这样良好的定义。
对于这种使用了资源,但是没有定义析构函数来释放资源的类,可能会遇到和使用动态内存同样的错误。
struct destination; //表示我们在连接什么
struct connection; //表示连接所需要的信息
connection connect("destination"); //打开连接
void disconnect(connection); //关闭给定的连接
void f(destination &d){
connection c = connect(&d); //使用连接,但是忘记关了
}
但是由于connection没有析构函数,我们可以使用shared_ptr来保证connection正确关闭。
默认情况下, shared_ptr指向的是动态内存,当一个shared_ptr被销毁时, 它默认给它管理的对象进行delete操作,而在这个案例中,我们通过修改delete操作,来实现对该类对象的智能管理。
void end_connection(connection *p ){disconnect(*p);}
替代之前的函数
void f(){
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
//使用连接
//当f退出,即便是异常退出,connection都会自动关闭。
//用lambda来写
shared_ptr<connection> p(&c, [](connection *p){ disconnect(p)});
}