动态内存与智能指针

在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在无用之后就不在保留就非常重要了。
程序使用动态内存一般处于一下三种原因之一:

  • 1、程序不知道自己需要使用多少对象
  • 2、程序不知道所需对象的准确类型
  • 3、程序需要在多个对象间共享数据

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此指针。
为了正确使用智能指针,我们必须坚持一些基本规范:

  • 不使用相同的内置指针初始化多个智能指针
  • 不delete get()返回的指针
  • 不使用get()初始化或reset另一个智能指针
  • 如果使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变得无效了
  • 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。

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共享对象
    ......
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • C++动态内存 了解动态内存在 C++ 中是如何工作的是成为一名合格的 C++ 程序员必不可少的。C++ 程序中的...
    Cor9阅读 305评论 0 1
  • 智能指针 智能指针的类型 shared_ptr实现共享式拥有的概念(shared ownership)。多个智能指...
    Catcher07阅读 361评论 0 0
  • 1. 什么是智能指针? 智能指针是行为类似于指针的类对象,但这种对象还有其他功能。 2. 为什么设计智能指针? 引...
    MinoyJet阅读 633评论 0 1
  • 12.1 智能指针 智能指针行为类似普通指针,但它负责自动释放所知的对象。 #include <memory> s...
    龙遁流阅读 352评论 0 1
  • 子曰:“温故而知新,可以为师矣。”本人已使用VUE一段时间,官方文档写的非常的详细,本次整理主要是为了温习,加深对...
    杰尼龟七号阅读 497评论 0 0