C++11——动态内存

智能指针

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

shared_ptr类

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

Code:
    shared_ptr p1;      // shared_ptr that can point at a string
    shared_ptr> 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 sp
unique_ptr 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 (args) 返回指向类型为T的动态分配对象的shared_ptr。使用args初始化该对象。
shared_ptr 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

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

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

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 *pv = new vector{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 u1
unique_ptr u2
生成一个指向类型T的空unique_ptr。.u1将使用delete来释放它的指针; u2将使用类型为D的可调用对象来释放其指针。
unique_ptr 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 p1;            // unique_ptr that can point at a double
    unique_ptr p2(new int(42));  // p2 points to int with value 42

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

Code:
    unique_ptr p1(new string("Stegosaurus"));
    unique_ptr p2(p1);    // error: no copy for unique_ptr
    unique_ptr 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 p2(p1.release());   // release makes p1 null
    unique_ptr 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

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 w 生成一个指向类型T对象的空weak_ptr
weak_ptr 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(42);
    weak_ptr 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 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

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

(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
虽然我们可以使用空括号来值初始化数组的元素,但是我们不能在括号内提供元素初始值设定项。事实上,我们不能在括号内提供初始值,这意味着我们不能使用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.

你可能感兴趣的:(C++11——动态内存)