C++11——动态内存

智能指针

为了使管理动态内存更容易、更安全,新标准库提供了两种管理动态对象的智能指针类型。智能指针的作用类似于常规指针,但格外重要的是它会自动删除它指向的对象。新标准库定义了两种智能指针,它们在管理底层指针方面有所不同:shared_ptr(允许多个指针引用同一个对象)和unique_ptr(“拥有”它指向的对象)。该库还定义了一个名为weak_ptr的伴随类,它是对shared_ptr管理的对象的弱引用。这三个类都在头文件<memory>中定义。
关于头文件<memory>的更多信息请参考header <memory>

shared_ptr类

vector一样,智能指针也是模板。因此,当我们创建智能指针时,我们必须提供附加信息,即在这种情况下,指针可以指向的类型。与vector一样,我们在尖括号内提供该类型,它遵循我们定义的智能指针类型的名称:

Code:
    shared_ptr<string> p1;      // shared_ptr that can point at a string
    shared_ptr<list<int>> p2;   // shared_ptr that can point at a list of ints

默认初始化智能指针为空指针。我们也可以使用其他初始化摸板类的方法对其进行初始化。
我们使用智能指针的方式类似于使用指针。解引用智能指针会返回指针指向的对象。当我们在条件中使用智能指针时,效果是测试指针是否为空:

Code:
    // if p1 is not null, check whether it's the empty string
    if (p1 && p1->empty())
        *p1 = "hi";  // if so, dereference p1 to assign a new value to that string

下表中列出了shared_ptrunique_ptr共有的操作:

操作 功能
shared_ptr<T> sp
unique_ptr<T> up
生成一个可以指向T类型对象的空指针。
p 用p作为条件,如果p指向一个对象则返回true。
*p 解引用p来获得p指向的对象
p->mem 等效于(*p).mem
p.get() 返回p中的指针。请谨慎使用;当智能指针要删除它时,返回的指针指向的对象将消失,这样返回的指针就是一个空指针。
swap(p, q)
p.swp(q)
交换p和q中的指针。

下表中列出了shared_ptr独有的操作:

操作 功能
make_shared<T> (args) 返回指向类型为T的动态分配对象的shared_ptr。使用args初始化该对象。
shared_ptr<T> p(q) pshared_ptr对象q的副本; 增加q中的计数。q中的指针必须可转换为T *
p = q pqshared_ptr,它们包含可以相互转换的指针。减少p的引用计数并增加q的计数; 如果p的计数变为0,则删除p的现有内存。
p.unique() 如果p.use_count()是1则返回true,否则返回false。
p.use_count() 返回与p共享的对象数;这可能是一个慢速的操作,主要用于调试目的。

关于shared_ptr更多信息请参考header <shared_ptr>

动态分配对象的列表初始化

我们可以使用直接初始化的方式来初始化动态分配的对象。我们也可以使用传统构造的构造方式(使用括号)进行初始化。在新标准下,我们也可以使用列表初始化的方式(带花括号)进行初始化:

Code:
    int *pi = new int(1024); // object to which pi points has value 1024
    string *ps = new string(10, '9');  // *ps is "9999999999"
    // vector with ten elements with values from 0 to 9
    vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};

我们还可以通过使用类型名称后跟一对空括号来初始化动态分配的对象:

Code:
    string *ps1 = new string;    // default initialized to the empty string
    string *ps = new string();   // value initialized to the empty string
    int *pi1 = new int;          // default initialized; *pi1 is undefined
    int *pi2 = new int();        // value initialized to 0; *pi2 is 0

对于定义了自己的构造函数的类类型(如string),类型名称后面是否后跟一对空括号无关紧要;无论形式如何,对象都由默认构造函数进行初始化。在内置类型的情况下,类型名称后面是否后跟一对空括号的差异是显着的;内置类型的值初始化对象具有明确定义的值,但默认初始化对象不具有。同样,对于那些依赖于编译器默认生成的构造函数的类,类中的内置类型的成员没有在类体中初始化,那么这些成员也将未初始化。
与我们通常初始化变量的原因相同,初始化动态分配的对象也是一个好主意。

auto和动态分配

当我们在括号内提供初始化器时,我们可以使用auto推导出我们想要从该初始化器中生成的对象的类型。但是,因为编译器使用初始化器的类型来推断要分配的类型,所以我们只能在括号内使用auto和一个初始化器:

Code:
    auto p1 = new auto(obj);    // p points to an object of the type of obj
                                // that object is initialized from obj
    auto p2 = new auto{a,b,c};  // error: must use parentheses for the initializer

p1是一个指向由obj通过auto推导出的类型的指针。如果objint,则p1int *;如果objstring,则p1string *;依此类推。新分配的对象由obj的值进行初始化。

unique_ptr类

unique_ptr“拥有”它指向的对象。与shared_ptr不同,一次只能有一个unique_ptr指向给定对象。当unique_ptr被销毁时,unique_ptr指向的对象将被销毁。下表列出了unique_ptr特有的操作。

操作 功能
unique_ptr<T> u1
unique_ptr<T, D> u2
生成一个指向类型T的空unique_ptr。.u1将使用delete来释放它的指针; u2将使用类型为D的可调用对象来释放其指针。
unique_ptr<T, D> u(d) 生成一个指向类型T且使用d的空unique_ptrd必须是类型D的对象而不是delete
u = nullptr
u.release()
删除u指向的对象,并将u置位空。
u.reset()
u.reset(q)
u.reset(nullptr)
删除u指向的对象。如果提供了内置指针q,则指向该对象。 否则使u为空。

shared_ptr不同,没有与make_shared相类似的库函数返回unique_ptr。相反,当我们定义unique_ptr时,我们将它绑定到new返回的指针,与shared_ptr一样,我们必须使用直接初始化形式对其进行初始化:

Code:
    unique_ptr<double> p1;            // unique_ptr that can point at a double
    unique_ptr<int> p2(new int(42));  // p2 points to int with value 42

因为unique_ptr拥有它指向的对象,所以unique_ptr不支持普通拷贝或赋值:

Code:
    unique_ptr<string> p1(new string("Stegosaurus"));
    unique_ptr<string> p2(p1);    // error: no copy for unique_ptr
    unique_ptr<string> p3;
    p3 = p2;    // error: no assign for unique_ptr

虽然我们无法拷贝或对unique_ptr进行赋值,但我们可以通过调用releasereset将所有权从一个(nonconstunique_ptr转移到另一个(nonconstunique_ptr

Code:
    // transfers ownership from p1 (which points to the string Stegosaurus) to p2
    unique_ptr<string> p2(p1.release());   // release makes p1 null
    unique_ptr<string> p3(new string("Trex"));
    // transfers ownership from p3 to p2
    p2.reset(p3.release());   // reset deletes the memory to which p2 had pointed

release成员函数返回当前存储在unique_ptr中的指针,并使unique_ptrnull。因此,p2由存储在p1中的指针值初始化,p1变为空。
reset成员接收一个可选指针,并重新定位unique_ptr以指向给定指针。如果unique_ptr不为null,则删除unique_ptr指向的对象。因此,对p2进行的reset调用释放了从“Stegosaurus”初始化的字符串所使用的内存,将p3的指针传递给p2,并使p3为空。
调用release会破坏unique_ptr与其管理的对象之间的连接。通常由release返回的指针用于初始化或分配另一个智能指针。在这种情况下,管理内存的责任只是从一个智能指针转移到另一个智能指针。但是,如果我们不使用另一个智能指针来保存从release返回的指针,我们的程序将负责释放该资源:

Code:
    p2.release();   // WRONG: p2 won't free the memory and we've lost the pointer
    auto p = p2.release();   // ok, but we must remember to delete(p)

关于unique_str更多信息请参考header <unique_str>

weak_ptr类

weak_ptr是一个智能指针,它不控制它指向的对象的生命周期。相反,weak_ptr指向由shared_ptr管理的对象。将weak_ptr绑定到shared_ptr不会更改该shared_ptr的引用计数。一旦指向该对象的最后一个shared_ptr消失,该对象本身将被删除。即使有指向它的weak_ptr,该对象也将被删除。因此名称为weak_ptr,它强调了weak_ptr“弱”共享其对象的想法。
下表列出了weak_ptr的相关操作:

操作 功能
weak_ptr<T> w 生成一个指向类型T对象的空weak_ptr
weak_ptr<T> w(sp) weak_ptr指向与shared_ptr对象sp相同的对象。类型T必须可以转换为sp指向的类型。
w = p p可以是shared_ptrweak_ptr。赋值后wp共享所有权。
w.reset() 使w为空。
w.use_count() 返回与w共享所有权的shared_ptr数。
w.expired() 如果w.use_count()为0,则返回true;否则返回false。
w.lock() 如果w.expired为true,则返回一个空shared_ptr;否则将返回一个指向w指向的对象的shared_ptr

当我们创建一个weak_ptr时,我们用shared_ptr初始化它:

Code:
    auto p = make_shared<int>(42);
    weak_ptr<int> wp(p);    // wp weakly shares with p; use count in p is unchanged

这里wpp都指向同一个对象。由于共享较弱,创建wp不会改变p的引用计数;wp点指向的对象可能会被删除。
因为对象可能不再存在,所以我们不能使用weak_ptr直接访问其对象。要访问该对象,我们必须调用locklock函数检查weak_ptr指向的仍然存在的对象。如果存在,lock会将shared_ptr返回给共享对象。与任何其他shared_ptr一样,我们保证shared_ptr指向的底层对象至少在shared_ptr存在的情况下仍然存在。例如:

Code:
    if (shared_ptr<int> np = wp.lock()) { // true if np is not null
        // inside the if, np shares its object with p
    }

这里,我们只在调用lock成功时才进入if的主体。在if中,使用np访问该对象是安全的。
关于weak_str更多信息请参考header <weak_str>

for范围声明不适用动态分配的数组

\color{red}{我们所谓的动态数组不具有数组类型}\(array type)!所以不能对其使用for范围声明!

动态数组的列表初始化

新标准下,我们可以使用列表初始化器来初始化一个动态数组:

Code:
    // block of ten ints each initialized from the corresponding initializer
    int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
    // block of ten strings; the first four are initialized from the given initializers
    // remaining elements are value initialized
    string *psa3 = new string[10]{"a", "an", "the", string(3,'x')};

当我们列出初始化内置数组类型的对象时,初始化器用于初始化数组中的第一个元素。如果初始值设定项的个数少于元素个数,则其余元素是值初始化的。如果初始值设定项多于给定的大小,那么new表达式将失败,并且不会分配任何内存。在这种情况下,new会抛出bad_array_new_length类型的异常。与bad_alloc一样,此类型在new头中定义。关于头文件new更多的信息可参考header <new>
虽然我们可以使用空括号来值初始化数组的元素,但是我们不能在括号内提供元素初始值设定项。事实上,我们不能在括号内提供初始值,这意味着我们不能使用auto来申请分配数组。

动态分配空数组是合法的

我们可以使用任意表达式来确定要分配的对象数量:

Code:
    size_t n = get_size(); // get_size returns the number of elements needed
    int* p = new int[n];  // allocate an array to hold the elements
    for (int* q = p; q != p + n; ++q)
        /* process the array*/ ;

一个有趣的问题出现了:如果get_size返回0会发生什么?答案是我们的代码正常工作。即使我们不能创建大小为0的数组变量,使用n等于0调用new [n]仍然是合法的:

Code:
    char arr[0];            // error: cannot define a zero-length array
    char *cp = new char[0]; // ok: but cp can't be dereferenced

当我们使用new来分配一个大小为零的数组时,new返回一个有效的非零指针。该指针与new返回的任何其他指针不同。此指针用作零元素数组的非结束指针(off-the-end pointer)。我们可以通过一个非结束迭代器(off-the-end iterator)来使用这个指针。可以像在上述代码中的for循环那样比较指针。我们可以在这样的指针上加零(或从中减去零),也可以减去其自身,得到零。 这种指针不能被解引用。毕竟,它指向的数组中没有元素。
在上述代码中,如果get_size返回0,则n也为0。对new的调用将分配一个大小为零的数组。for中的判定条件将失败(p等于q + n,因为n为0)。因此,不执行for循环体。

allocator::construct可以使用任何构造函数

allocator分配的内存是未构造的。我们通过在该内存中构造对象来使用此内存。在新标准库中,construct成员接收一个指针和零个或多个附加参数,它在给定位置构造一个元素,附加参数用于初始化正在构造的对象。与make_shared的参数一样,这些附加参数必须是正在构造的类型的对象的有效初始值设定项。特别是,如果对象是类类型,则这些参数必须与该类的构造函数匹配:

Code:
    auto q = p;   // q will point to one past the last constructed element
    alloc.construct(q++);           // *q is the empty string
    alloc.construct(q++, 10, 'c');  // *q is cccccccccc
    alloc.construct(q++, "hi");     // *q is hi!

在早期版本的标准库中,construct只接收两个参数:构造对象的指针和元素类型的值。因此,我们只能将一个元素复制到未构造的空间中,我们不能对元素类型使用任何其他构造函数。
使用尚未构造对象的原始内存是错误的:

Code:
    cout << *p << endl;  // ok: uses the string output operator
    cout << *q << endl;  // disaster: q points to unconstructed memory!

注:我们必须construct对象以便使用allocate返回的内存。以其他方式使用未构造的内存是未定义的。
当我们完成对象的使用时,我们必须销毁我们构造的元素,我们通过在每个构造的元素上调用destroy来实现。destroy函数接受一个指针并在指向的对象上运行析构函数:

Code:
    while (q != p)
        alloc.destroy(--q);    // free the strings we actually allocated

在循环开始时,q指向最后一个构造元素的后一位置。我们在调用destroy之前递减q。因此,在第一次调用调用destroy时,q指向最后构造的元素。我们在最后一次迭代中destroy第一个元素,之后q将等于p并且循环结束。
一旦元素被destroy,我们可以重用内存来保存其他字符串或将内存返回给系统。我们通过调用deallocate来释放内存:

    alloc.deallocate(p, n);

我们传递给deallocate的指针不能为null;它必须指向由allocate分配的内存。而且,传递给deallocatesize参数必须与调用allocate中使用的大小相同才能获得指针所指向的内存。
关于allocator::construct的更多信息可参考std::allocator::construct

参考文献

[1] Lippman S B , Josée Lajoie, Moo B E . C++ Primer (5th Edition)[J]. 2013.

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

推荐阅读更多精彩内容

  • 12.1 智能指针 智能指针行为类似普通指针,但它负责自动释放所知的对象。 #include <memory> s...
    龙遁流阅读 359评论 0 1
  • 除了自动和static对象外,C++还支持动态分配对象。动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式...
    梦中睡觉的巴子阅读 513评论 0 0
  • C#、Java、python和go等语言中都有垃圾自动回收机制,在对象失去引用的时候自动回收,而且基本上没有指针的...
    StormZhu阅读 3,726评论 1 15
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,513评论 1 51
  • 我好像踩在时间的轮轴上 晃晃悠悠回到那个破旧的小城 不期而遇的你 亦如当初的模样 即使时间冲淡了我的欢喜 也曾爱你...
    沧海一声哭阅读 136评论 1 1