动态内存与智能指针

2019-10-30  本文已影响0人  toMyLord

在C/C++中,动态内存的使用很容易出问题,因为确保在正确的时间释放内存是极其困难的。有时忘记释放内存就会出现内存泄露的问题;有时在尚有指针引用内存的情况下我们就释放了它,在这种情况下就会产生引用非法内存问题。
新标准库提供了两种智能指针类型来管理动态对象,智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象;unique_str则“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。

1、shared_ptr类

智能指针是模板,因此我们创建一个智能指针的时候,必须提供额外的信息——指针可以指向的类型:

shared_ptr<string> p1;      //指向string
shared_ptr<list<int>> p2;   //指向int的list

默认初始化的智能指针中保存着一个空指针。智能指针的使用方式与普通指针类似。解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用中智能指针,效果就是检测它是否为空:

//如果p1不为空,检查它是否指向一个空string
if (p1 && p1->empty())
    *p1 = "hi";     //如果p1指向一个空string,则解引用p1,将一个新值赋予string

shared_ptrunique_str都支持的操作:

shared_ptr<T> sp        //空智能指针,可以指向类型为T的对象
unique_ptr<T> up
p                       //将p作为一个条件判断,若p指向一个对象,则为true
*p                      //解引用p,获得它指向的对象
p->mem                  //等价于(*p).mem
p.get()                 //返回p中保存的指针,若智能指针释放了其对象,返回的指针所指向的对象也就消失了
swap(p, q)              //交换p、q中的指针
p.swap(q)

share_ptr独有的操作:

make_shared<T>(args)    //返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化此对象
shared_ptr<T>p (q)      //p是shared_ptr的拷贝,q中的指针必须能转化为T*
p = q                   //p和q都是shared_ptr,所保存的指针必须能互换。此操作会递减p的引用计数器,递增q的引用计数器;若p的引用计数器变为0,则会将其管理的原内存释放
p.unique()              //若p.use_count()为1,返回true,否则返回false
p.use_count()           //返回与p共享对象的智能指针数;可能很慢,主要用于调试

make_shared函数

最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。定义在头文件memory中。

//指向一个值为42的int的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);
//指向一个值为"999"的string
shared_ptr<string> p4 = make_shared<string>(3, '9');
//指向一个值初始化的int,即值为0
shared_ptr<int> p5 = make_shared<int>();

类似顺序容器的emplace成员,make_shared用其参数来构造给定类型的对象。例如,调用make_shared<string>时传递的参数必须与string的某个构造函数相匹配。如果不传递任何参数,对象就会进行值初始化。
auto定义一个对象来保存make_shared的结果,这种方式比较简单:

//p6指向一个动态分配的空vector<string>
auto p6 = make_shared<vector<string>>();

shared_ptr的拷贝与赋值

当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象:

auto p = make_shared<int>(42);  //p指向的对象只有p一个引用者
auto q(p);                      //p和q指向相同对象,此对象有两个引用者

我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数。拷贝一个shared_ptr时,计数器就会递增。例如,当我们用一个shared_ptr初始化另一个shared_ptr,或将他作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增。当我们给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减。一旦一个shared_ptr的计数器变为0,它就会自动释放字节所管理的对象:

auto r = make_shared<int>(42);      //r指向的int只有一个引用者
r = q;         //给r赋值,令它指向另一个地址;递增q指向的对象的引用计数;递减r原来指向的对象的引用计数;r原来指向的对象已没有引用者,会自动释放。

shared_ptr自动销毁所管理的对象

当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象。它是通过析构函数来完成销毁工作的。shared_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会自动销毁对象,并释放它占用的内存。

shared_ptr自动释放相关联的内存

void use_factory(T arg) {
    shared_ptr<Foo> p = factory(arg);
    ......
}   //p离开了作用域,它指向的内存会被自动释放掉

此例中,p是唯一引用factory返回的内存的对象。由于p将要销毁,p指向的这个对象也会被销毁,所占用的内存将会释放。但如果有其他的shared_ptr也指向这块内存,他就不会被释放掉:

shared_ptr<Foo> use_factory(T arg) {
    shared_ptr<Foo> p = factory(arg);
    ......
    return p;       //当我们返回p时,引用计数器进行了递增操作
}   //p离开了作用域,但它指向的内存不会被释放掉

由于在最后一个shared_ptr销毁钱内存都不会释放,保证shared_ptr在无用之后就不在保留就非常重要了。
程序使用动态内存一般处于一下三种原因之一:

2、shared_ptr和new结合使用

接受指针参数的智能指针构造函数是explicit的,因此,我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化的形式来初始化一个指针。

shared_ptr<int> p1 = new int(1024);     //错误:必须使用直接初始化形式
shared_ptr<int> p1(new int(1024));      //正确:使用了直接初始化形式

p1的初始化隐式的要求编译器用一个new返回的int*来创建一个shared_ptr。由于我们不能进行内置指针到智能指针间的隐式转换,因此这条语句是错误的。出于相同的原因,一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针:

shared_ptr<int> clone(int p) {
    return new int(p);                  //错误:隐式转换为shared_ptr<int>
    return shared_ptr<int>(new int(p)); //正确:显式的用int*创建shared_ptr<int>
}

不要混合使用普通指针和智能指针,也不要用get初始另一个智能指针或为智能指针赋值

智能指针类型定义了一个名为get的函数,它返回一个内置指针,指向智能指针管理的对象。此函数是为了这样一种情况而设计的:我们需要想不能使用智能指针的代码传递一个内置指针。使用get返回的指针的代码不能delete此指针。
为了正确使用智能指针,我们必须坚持一些基本规范:

3、unique_ptr

shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。当我们定一个unique_ptr时,需要将其绑定到一个new返回的指针上。类似shared_ptr,初始化unique_ptr必须采用直接初始化形式:

unique_ptr<double> p1;      //可以指向一个double的unique_str
unique_ptr<int> p2(new int(42));    //p2指向一个值为42的int

由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作:

unique_ptr<string> p1(new string("hello world"));
unique_ptr<string> p2(p1);          //错误:unique_ptr不支持拷贝
unique_ptr<string> p3;
p3 = p2;            //错误:unique_ptr不支持赋值

unique_ptr独有的操作:

//空unique_ptr,可以指向类型为T的对象。u1使用delete来释放他的指针;u2会使用一个类型为D的可调用对象来释放他的指针。
unique_ptr<T> u1
unique_ptr<T, D> u2

unique_ptr<T, D> u(d)   //空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete
u = nullptr             //释放u指向的对象,将u置为空
u.release()             //u放弃对指针的控制权,将u置为空
u.reset()               //释放u指向的对象
u.reset(q)              //如果提供内置指针q,令u指向这个对象;否则将u置为空
u.reset(nullptr)

虽然我们不能拷贝或赋值unique_ptr,但是可以通过releasereset将指针的所有权从一个(非constunique_ptr转移给另一个unique_ptr

//将所有权从p1(指向string "hello world")转移给p2
unique_ptr<string> p2(p1.reslease());       //release将p1置空
unique_ptr<string> p3(new string("Trex"));
//将所有权从p3转移给p2
p2.reset(p3.release());                     //reset释放了p2原来指向的内存

release成员返回unique_ptr当前保存的指针并将其置为空。因此,p2被初始化为p1原来保存的指针,而p1被置空。
reset成员接收一个可选的指针参数,令unique_str重新指向给定的指针。如果unique_ptr不为空,它原来指向的对象被释放。因此,对p2调用reset释放了用hello world初始化的string所使用的内存,将p3对指针的所有权转移给p2,并将p3置空。

传递unique_ptr参数和返回unique_ptr

不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr。最常见的例子是从函数返回一个unique_ptr

unique_ptr<int> clone(int p) {
    //正确:从int*创建一个unique_ptr<int>
    return unique_ptr<int>(new int(p));
}

还可以返回一个局部对象的拷贝:

unique_ptr<int> clone(int p) {
    unique_ptr<int> ret(new int(p));
    ......
    return ret;
}

对于两段代码,编译器都知道要范湖IDE对象将要被销毁,在此情况下,编译器执行一种特殊的“拷贝”。

向unique_ptr传递删除器

unique_ptr默认情况下用delete释放它指向的对象,但是我们可以重载unique_ptr中默认的删除器。与重载关联容器的比较操作类似,我们必须尖括号中unique_ptr指向类型之后提供删除器类型。创建或reset一个这种unique_ptr类型的对象时,必须提供一个指定类型的可调用对象(删除器):

//p指向一个类型为objT的对象,并使用一个类型为delT的对象释放objT对象
//它会调用一个名为fcn的delT类型对象
unique_ptr<objT, delT> p(new objT, fcn);

//下面是个更具体的例子
void f(destination &d /*需要其他的参数*/) {
    connection c = connect(&d);     //打开连接
    //当p被销毁时,连接将会关闭
    unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);
    //使用连接
    //当f退出时(即使是由于异常而退出),connection会被正确关闭。
}

4、weak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也会被释放,因此,weak_ptr的名字抓住了这种智能指针“弱”共享对象的特点。当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它:

auto p = make_shared<int>(42);
weak_ptr<int> wp(p);            //弱共享p,p的引用计数未改变

由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock。此函数检查weak_ptr指向的对象是否仍存在。如果存在,lock返回一个指向共享对象的shared——ptr。与任何其他shared_ptr类似,只要此shared_ptr存在,它所指向的底层对象也就会一直存在:

if (shared_ptr<int> np = wp.lock()) {
    //在if中,np与p共享对象
    ......
}
上一篇下一篇

猜你喜欢

热点阅读