第12章 动态内存

动态内存与智能指针

不同的内存管理方式

静态内存:保存局部static对象、类static数据成员、定义在任何函数之外的变量。

栈内存:保存定义在函数之内的非static对象。

内存池 | 自由空间(free store)| 堆:存储动态分配的对象。

PS:必须显式销毁不再使用的动态内存

 

C++11提供了智能指针:shared_ptr允许多个指针指向同一对象;unique_ptr“独占”所指对象;还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。(这三种类型定义在头文件memory中)

智能指针会自动释放所指的对象。

shared_ptr类与unique_ptr类都支持的操作

shared_ptr<T> sp   unique_ptr<T> up   空的智能指针,可以指向类型为T的对象

p   将p用作一个条件判断,若p指向任一对象则为true

*p   解引用获得p所指的对象

p->mem   等价于(*p).mem

p.get()   返回p中保存的指针;要谨慎使用,若智能指针释放了其对象,该方法返回的指针也失效了

swap(p,q)   p.swap(q)   交换p、q中的指针

 

shared_ptr独有的操作

make_shared<T>(args)   返回一个shared_ptr<T>,指向一个动态分配的T类型对象;使用args初始化该对象,若args为空,则进行值初始化

shared_ptr<T>p(q)   p是q的拷贝;此操作会递增q中的计数器;q中的指针必须能转换成T*

p=q   p、q都是shared_ptr,所保存的指针要能互相转换;此操作会递减p的引用计数、递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放

p.use_count()   返回与p共享对象的指针数量;可能很慢,主要用于调试

p.unique()   若use_count为1,返回true;否则返回false

 

shared_ptr的拷贝、赋值与引用计数

无论何时当拷贝一个shared_ptr,计数器都会递增。例如,当用一个shared_ptr初始化另一个shared_ptr,或者将它作为参数传递给一个函数,或者作为函数的返回值时,它所关联的计数器就会递增;

当给一个shared_ptr赋予新值,或者shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域时),计数器就会递减;

当shared_ptr的计数器变为0,它就自动释放自己所管理的对象。

 

直接管理内存

使用new动态分配和初始化对象

new返回一个指向自由空间中新创建对象的指针。

 

动态分配

T *p = new T;  //p指向一个动态分配的、未初始化的无名对象

注意:默认情况下,动态分配的对象是默认初始化的。若T是内置类型或组合类型,则对象的值是未定义的;若T是类类型,将用默认构造函数进行初始化。

 

初始化

可以使用直接初始化(第2章)方式来初始化一个动态分配的对象。

初始化的方式可以是传统的构造方式(圆括号),C++11标准下,也可以是列表初始化(花括号)。

int *pi = new int(1024); //pi指向对象的值为1024

string *ps = new string(3, 'a'); //*ps为"aaa"

vector<int> *pv = new vector<int>{0,1,2};

 

也可以对对动态分配的对象进行值初始化,在类型名之后跟一对空括号即可。

int *pi = new int; //默认初始化;*pi的值未定义

int *pi = new int(); //值初始化;*pi的值为0

string *ps = new string; //默认初始化;*ps为空string

string *ps = new string(); //值初始化;*ps为空string

PS:对于定义了自己构造函数的类类型,要求值初始化时实际也是通过默认构造函数来初始化;但对于内置类型则不同。

(此外,对于类中那些依赖于编译器合成的默认构造函数的内置类型成员,若它们未在类内被初始化,那么它们的值也是未定义的)

 

如果提供括号包围的、单一的初始化器,就可使用auto从此初始化器来自动推断要分配的对象类型。

auto p = new auto(obj); //p指向一个obj所属类型的对象,该对象用值obj进行初始化(例如,若obj是int,则p是int*)

auto p = new auto{a,b,c}; //非法,括号中只能有单个初始化器

 

动态分配的const对象

const T *p = new const T; //若T是定义了默认构造函数的类类型,允许隐式初始化

const T *p = new const T(val); //其它类型,必须显式初始化

 

内存耗尽

当new表达式失败时,默认情况下,会抛出bad_alloc异常;

当给new传递额外参数:一个标准库定义的名为nothrow的对象时,new不会抛出异常,而只是返回一个空指针;称此种形式的new为定位new(placement new)

int *p = new (nothrow) int;

 

释放动态内存

delete p; //p是一个指针;p必须指向一个动态分配的对象或是一个空指针

delete表达式执行两个动作:

(1)销毁给定指针指向的对象

(2)释放对应的内存

PS:虽然一个动态分配的const对象的值不能被改变,但它本身允许被销毁。

PS:由内置指针(而不是智能指针)管理的动态内存在被显式释放之前一直都会存在。

PS:在delete之后,指针指向的内存已经释放,但指针中仍然保存着原来的地址,即所谓空悬指针(dangling pointer);应在delete后将nullptr赋予指针。

 

shared_ptr与new结合使用

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

shared_ptr<int> p = new int(1024); //错误

shared_ptr<int> p(new int(1024)); //正确

默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象;

也可以将智能指针绑定到一个指向其它类型资源的指针上,但此时必须提供自己的操作来替代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所指对象的所有权;q必须能转换成T*类型;p将使用可调用对象d来代替delete

shared_ptr<T> p(p2,d)   p是shared_ptr p2的拷贝;p将用可调用对象d来代替delete

p.reset()   p.reset(q)   p.reset(q,d)  若p是唯一指向其对象的shared_ptr,reset会释放此对象;若传递了一个内置指针q,会令p指向q,否则会将p置为空;若还传递了参数d,将会调用d而不是delete来释放q

 

PS:reset会更新引用计数,若需要的话,会释放p所指的对象。reset经常与unique一起使用,来控制多个shared_ptr共享的对象。在改变底层对象之前,应该检查自己是否是当前对象仅有的用户,若不是,在改变之前要制作一份新的拷贝。

例如:

  if(!p.unique())

    p.reset(new string(*p)); //不是唯一用户,分配新的拷贝

  *p += newVal; //现在是唯一用户了,可以改变对象的值

 

不要混合使用普通指针和智能指针

当将一个智能指针绑定到一个普通指针时,再使用一个普通指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被智能指针销毁。

 

不要使用get初始化另一个智能指针或为智能指针赋值

智能指针的get函数用来将指针的访问权限传递给代码,只有在确定代码不会delete指针的情况下,才能使用get。特别是,不要使用get初始化另一个智能指针或为智能指针赋值。

 

智能指针使用基本规范

(1)不使用相同的内置指针值初始化(或reset)多个智能指针

(2)不delete get函数返回的指针

(3)不使用get函数初始化或reset另一个智能指针

(4)若使用get返回的指针,要记住当最后一个对应的智能指针销毁后,你的指针就无效了

(5)若使用智能指针管理的资源并非new分配的内存,记住传递给它一个删除器(deleter)

 

unique_ptr

一个unique_ptr独占它所指的对象,不支持普通的拷贝或赋值操作。

与shared_ptr不同,没有类似make_shared的标准库函数返回一个unique_ptr。

定义一个unique_ptr时,必须将其绑定到一个new返回的指针上。(初始化unique_ptr必须采用直接初始化方式)

 

unique_ptr特有的操作

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

unique_ptr<T, D> u(d)   空unique_ptr,指向类型为T的对象,用类型为D的对象d代替的delete

u = nullptr   释放u指向的对象,将u置为空

u.release()   u放弃对指针的控制权,返回指针,并将u置空

u.reset()  u.reset(nullptr)   u.reset(q)   释放u所指的对象;或令u指向内置指针q所指的对象

 

例子:虽然不能拷贝或赋值unique_ptr,但可以通过release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique_ptr

  unique_ptr<string> p2(p1.release()); //p1会被置空

  unique_ptr<string> p3(new string("hi"));

  p2.reset(p3.release()); //p3被置空,p2原来指向的内存被释放

 

PS:release返回的指针通常被用来初始化另一个智能指针或者给另一个智能指针赋值。如果不用另一个智能指针来保存release返回的指针,就要注意对资源的释放。

  p1.release(); //错误;p1不会释放内存,而且我们丢失了指针

  auto p = p1.release(); //合法;但我们必须记得delete(p)

注:p1是一个智能指针,p是一个普通指针。

 

传递unique_ptr参数和返回unique_ptr

“不能拷贝unique_ptr”的规则有一个例外:允许拷贝或赋值一个将要被销毁的unique_ptr(编译器执行一种特殊的“拷贝”,这将在之后讨论)

常见的例子是在函数中返回unique_ptr。

 

weak_ptr

weak_ptr是一种不控制所指对象生存期的智能指针,它指向一个shared_ptr管理的对象。

将weak_ptr绑定到shared_ptr不会改变shared_ptr的引用计数。当最后一个指向对象的shared_ptr被销毁,即使有weak_ptr指向该对象,对象还是会被释放。

 

weak_ptr的操作

weak_ptr<T> w   空weak_ptr可以指向类型为T的对象

weak_ptr<T> w(sp)   与shared_ptr sp指向相同对象的weak_ptr;T必须能转换为sp指向的类型

w = p    p可以是weak_ptr或shared_ptr;赋值后w与p共享对象

w.reset()   将w置空

w.use_count()   与w共享对象的shared_ptr数量

w.expired()   若use_count为0,返回true,否则返回false

w.lock()   若expired为true,返回一个空shared_ptr;否则返回一个指向w的对象的shared_ptr

 

PS:由于weak_ptr指向的对象可能不存在(对象是由对应的shared_ptr管理的),因而最好不要使用weak_ptr直接访问对象,而是先调用lock函数,检查weak_ptr指向的对象是否仍然存在。(lock返回非空的shared_ptr即说明底层对象存在)

  if(shared_ptr<T> p = wp.lock()) { /**/ } //在if语句块中,p与wp共享对象

 

动态数组

new和数组

在类型名后的方括号中指定要分配的对象数量;返回指向第一个对象的指针

  int *p = new int[get_size()]; //方括号内必须是整数,但不必是常量

也可以使用表示数组类型的类型别名

  typedef int arrT[10];

  int *p = new arrtT;

  //编译器执行的是:int *p = new int[10]

 

动态数组实际上并不是数组类型

当用new分配一个数组时,并非得到一个数组类型的对象,而是得到一个元素类型的指针。(即使是使用类型别名定义了一个数组类型,new也不会分配一个数组类型的对象)

PS:由于分配的内存并非一个数组类型,因此不能对动态数组调用begin或end(它们依赖一个数组类型的维度);也不能对动态数组使用范围for语句。

 

初始化动态数组

默认情况下,new分配的对象都是默认初始化的,可以对数组中的元素进行值初始化,方法是在大小之后跟一对空括号。

  int *pi = new int[10]; //未初始化

  int *pi = new int[10](); //值初始化为0

  string *ps = new string[10]; //空string

  string *ps = new string[10](); //空string

PS:不支持在空括号内给出初始化器(这意味着不能用auto分配数组)

C++11下,还可以提供一个元素初始化器的花括号列表:

  int *pi = new int[10]{0,1,2}; //靠后的值进行值初始化

PS:如果初始化器数目大于元素数目,new表达式失败,不会分配任何内存,抛出bad_array_new_length的异常,此类型定义在头文件new中。

 

释放动态数组

delete [] pa;

 

智能指针和动态数组

指向动态数组的unique_ptr

unique_ptr<T[]> u   u可以指向一个动态分配的数组,数组元素类型为T

unique_ptr<T[]> u(p)   u指向内置指针p所指的动态分配的数组;p必须能转换为类型T*

u[i]   可以使用下标运算符来访问数组中的元素

PS:指向数组的unique_ptr不支持成员访问运算符(. ->)

 

与unique_ptr不同,shared_ptr不直接支持管理动态数组;

若希望使用shared_ptr管理动态数组,必须提供自定义的删除器。

例如:

  shared_ptr<int> sp(new int[10], [] (int *p) {delete [] p;});

  sp.reset();

注解:本例中传递给shared_ptr一个lambda作为删除器,它使用delete[]释放数组。

此外,shared_ptr未定义下标运算符,而且智能指针不支持指针算术运算。因此为了访问数组元素,必须使用get获取内置指针,然后用它访问数组元素。

例如:

  for(size_t i = 0; i != 10; ++i)

    *(sp.get() + i) = i;

 

allocator类

标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,他分配的内存是原始的、未构造的。

 

为什么要有allocator类?

【原因1】new有一些灵活性上的局限:它将内存分配和对象构造组合在一起。(类似的,delete将对象析构和内存释放组合在一起)

  当分配单个对象时,这是正好是我们所期望的;但当分配一大块内存时,我们通常计划在这块内存上按需构造对象。

  分配一大块内存时,一般情况下,将内存分配和对象构造组合在一起可能导致不必要的浪费

  (1)new创建并初始化了一些后来没用到的对象(空间浪费)

  (2)对于使用到的对象,它们都被赋值了两次:第一次是在默认初始化时,第二次是在赋值时(时间效率浪费)

【原因2】没有默认构造函数的类不能动态分配数组。

 

allocator类及其算法

allocator<T> a   定义一个名为a的allocator对象,它可以为类型T的对象分配内存

a.allocate(n)   分配一段原始的、未构造的内存,保存n个类型为T的对象;返回指向第一个元素的指针

a.deallocate(p,n)   释放从T*指针p中地址开始的内存,这块内存保存了n个类型为T的对象;p必须是一个先前由allocate返回的指针,且n必须是p创建时所要求的大小;在调用deallocate之前,用户还必须先对每个在这块内存中创建的对象调用destroy

a.construct(p,args)   p必须是T*类型的指针,指向一块原始内存;args被传递给类型T的构造函数,用来在p指向的内存中构造一个对象

a.destroy(p)   p为T*类型的指针,对p指向的对象执行析构函数

allocator的伴随算法

以下函数在给定目的位置创建元素,而不是由系统分配内存给它们。

uninitialized_copy(b,e,b2)   从迭代器范围[b,e)拷贝元素到迭代器b2指定的未构造的原始内存中;b2指向的内存必须足够大

uninitialized_copy_n(b,n,b2)   从迭代器b指向的元素开始拷贝n个元素到b2开始的范围中

uninitialized_fill(b,e,t)   在[b,e)指定的原始内存范围中创建对象,对象的值都为t的拷贝

uninitialized_fill_n(b,n,t)   从迭代器b指向的内存地址开始创建n个对象,b必须指向足够大的未构造的原始内存

以上算法会返回一个迭代器(指针),指向最后一个构造元素之后的位置。

内容来源于网络如有侵权请私信删除
你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!