C++ Primer 5th学习笔记11 动态内存

动态内存

1 动态内存和智能指针

  在C++中,动态内存的管理是通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针。delete则是接受一个动态对象的指针,销毁该对象,并释放与之相关联的内存。
  动态内存的使用很容易出问题。若忘记释放内存,则会产生内存泄露;若在尚有指针引用内存的情况下就释放内存,则会产生引用非法内存的指针。
  因此新标准库提供了两种智能指针类型来管理动态对象:一是shared_ptr允许多个指针指向一个对象;二是unique_ptr则“独占”所实现的对象;标准库还定义一个名为weak_ptr的伴随类,该类是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中。

1.1 shared_ptr类

  类似vector,智能指针也是模板。智能指针的使用方式和普通指针类似,解引用一个智能指针返回它指向的对象。

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的对象。使用args初始化此对象
shared_ptr<T>p (q) p是shared_ptr的拷贝;此操作会递增q中的计数器。q中的指针必须能转换为T*
p = q p和q都是shared_ptr,所保存的指针必须能相互转换,此操作会递减p的引用计数,递增q的引用计数
p.unique() 若p.use_count()为1,返回true;否则返回false
p.use_count() 返回与p共享对象的智能指针数量

make_shared
  最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数,此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。其使用示例如下:

//指向一个值为42的int的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);
//利用auto来定义 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的计数器变为0,它就会自动释放自己所管理的对象:

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

shared_ptr自动释放相关联的内存
  当动态对象不再被使用时,shared_ptr类会自动释放动态对象。如以下例子所示:

//factory返回一个shared_ptr,指向一个动态分配的对象
shared_ptr<Foo> factory(T arg)
{
    //恰当处理arg
    //shared_ptr负责释放内存
    //return make_shared<Foo>(arg);
}

//局部变量p在use_factory使用结束时,将被自动销毁
void use_factory(T arg)
{
    shared_ptr<Foo> p = factory(arg);
    //使用p
}    //p离开了作用域,其指向的内存会被自动释放掉

shared_ptr<Foo> use_factory(T arg)
{
    shared_ptr<Foo> p = factory(arg);
    //使用p
    return p;
}    //p离开了作用域,其指向的内存会不会被释放掉

  因为在最后一个use_factory中的return语句向函数的调用者返回一个p的拷贝,拷贝一个shared_ptr会增加所管理对象的引用计数值。对于一块内存,shared_ptr类保证只要有任何shared_ptr对象引用它,就不会被释放掉。

使用动态生存期的资源的类
  程序使用动态内存处于以下三种原因之一:

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

1.2 直接管理内存——new和delete

使用new动态分配和初始化对象,可使用直接初始化和列表初始化,来初始化一个动态分配的对象。示例如下:

int *pi = new int(1024);    //值初始化
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};    //列表初始化

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

动态分配的const对象
  用new分配const对象是合法的:

//分配并初始化一个const int
const int *pci = new const int(1024);
//分配并默认初始化一个const的空string
const string *pcs = new const string;

释放动态内存
  通过delete表达式来将动态内存归还给系统。传递给delete的指针必须指向动态分配的内存或者是一个空指针,示例如下:

double *pd = new double(33),
delete pd;

使用new和delete管理动态内存存在的问题:

  • 忘记delete内存
  • 使用已经释放掉的对象
  • 同一块内存释放两次
    若只使用智能指针,可以避免所有这些问题

delete之后重置指针值
  在delete之后,指针就变成了空悬指针,即指向一块曾经保存数据对象但现在已经无效的内存的指针。避免上述问题:在指针即将要离开作用域之前释放掉其所关联的内存

  动态内存的一个基本问题是:可能有多个指针指向相同的内存。如下例子:

int *p(new int(42));    //p指向动态内存
auto q = p;    //p和q均指向相同的内存
delete p;    //p和q均变为无效
p = nullptr;    //指出p不再绑定到任何对象

1.3 shared_ptrnew结合使用

  用new返回的指针来初始化智能指针,同时必须使用直接初始化形式来初始化一个智能指针:

shared_ptr<int> p1(new int(42));     //正确:使用了直接初始化形式
shared_ptr<int> p2 = new int(1024);    //错误:必须使用直接初始化形式
定义和改变`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所指向的对象的所有权。p使用可调用对象d来代替delete
shared_ptr<T> p(p2, d) p是shared_ptr p2的拷贝,p将用可调用对象d来代替delete
p.reset() 若p是唯一指向其对象的shared_ptr,reset会释放此对象
p.reset(q) 若传递了可选的参数内置指针q,会令p指向q,否则会将p置为空
p.reset(q, d) 若传递了参数d,将会调用d而不是delete来释放q

不要混合使用普通指针和智能指针
  考虑下面对shared_ptr进行操作的函数:

void process(shared_ptr<int> ptr)
{
    //使用ptr
}    //ptr离开作用域,被销毁

由于process的参数是传值方式传递,因此实参会被拷贝到ptr中,而拷贝一个shared_ptr会递增其引用计数。当局部变量ptr被销毁时,ptr指向的内存不会被释放。使用此函数的正确方法是传递一个shared_ptr:

shared_ptr<int> p(new int(42));    //引用计数为1
process(p);   //拷贝p会递增其引用计数;在process中引用计数值为2
int i = *p;    //引用计数只为1

不要使用get初始化另一个智能指针或为智能指针赋值
智能指针类型定义的get函数的功能:需要向不能使用智能指针的代码传递一个内置指针,使用get返回的指针的代码不能delete此指针,注意:永远不要用get初始化另一个智能指针或者为另一个智能指针赋值

1.4 智能指针和异常

使用智能指针的一些规范:

  • 不使用相同的内置指针值初始化(或reset)多个智能指针
  • 不使用get()初始化或reset另一个智能指针
  • 若使用智能指针管理的资源部署new分配的内存,一定要传递给它应该删除器
  • 使用get()返回的指针,当最后一个对应的智能指针销毁后,指针就变为无效了。

1.5 unique_ptr

  与shared不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,他所实现的对象也被销毁,与shared_ptr不同,定义unique_ptr时,需要将其绑定到一个new返回的指针上。初始化unique_ptr必须采用直接初始化形式,示例如下:

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

unique_ptr不支持普通的拷贝和赋值操作

`unique_ptr`操作
操作 描述
unique_ptr<T> u1 unique_ptr,可以指向类型为T的对象。u1会使用delete来释放它的指针;
unique_ptr<T, D> u2 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指向的对象
u.reset(nullptr)u.reset(q) 如果提供了内置指针q,令u指向这个对象;否则将u置为空

通过上述表格中的操作,虽然不能拷贝或赋值unique_ptr,但可以调用release或reset将指针的所有从一个(非const)unique_ptr转移给另外一个unique,示例如下:

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

在上述程序中,如果不用另外一个智能指针来保存release返回的指针,则就需要程序来释放资源:

p2.release();    //错误,p2不会释放内存,而且会丢失了指针
auto p = p2.release();    //正确,但必须记得delete(p)

传递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;
}

1.6 weak_ptr

  是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。

weak_ptr
操作 描述
weak_ptr<T> w weak_ptr可以指向类型为T的对象
weak_ptr<T> w(sp) shared sp指向相同对象的weak_ptr。T必须能转换为sp指向的类型
w = p p可以是一个shared_ptr或一个weak_ptr,赋值后w与p共享对象
w.reset() 将w置为空
w.use_count() 与w共享对象的shared_ptr的数量
w.expired() 若w.use_count()为0,返回true,否则返回false
w.lock() 如果expired为true。返回一个空shared_ptr;否则返回一个指向w的对象的shared_ptr

当创建一个weak_ptr时,要用一个shared_ptr来初始化它:

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

wp和p指向相同的对象,由于是弱共享,创建wq不会改变p的引用计数;wp指向的对象可能被释放掉。==由于对象可能不存在,因此不能直接使用weak_ptr直接访问对象,而必须调用lock,此函数检查weak_ptr指向的对象是否仍然存在。==调用示例如下:

if (shared_ptr<int> np = wp.lock())    //若np不为空则条件成立
{
    //此处np与p共享对象
}

2 动态数组

2.1 new和数组

  new分配一个对象数组,在其中指明要分配的对象的数目,示例如下:
int *pia = new int[get_size()]; //pia指向第一个int
方括号中的大小必须是整数,但不必是常量

初始化动态分配对象的数组,示例如下:

int *pia = new int[10];    //10个未初始化的int
int *pia2 = new int[10]();    //10个值初始化为0的int

释放动态数组
  特殊形式的delete——在指针前加上一个空方括号对:

delete p;    //p必须指向一个动态分配的对象或为空
delete [] pa;    //pa必须指向一个动态分配的数组或为空

在第二条语句中,销毁pa指向的数组中的元素,是按逆序销毁的,即,最后一个元素首先被销毁,然后是倒数第二个。方括号是必须的

智能指针和动态数组
  使用unique_ptr管理动态数组,必须在对象类型后面跟一对空方括号:

//up指向一个包含10个未初始化int的数组
unique_ptr<int[]> up(new int[]);
up.release();    //自动用delete[] 销毁其指针
指向数组的unique_ptr
操作 描述
unique_ptr<T[]> u u可以指向一个动态分配的数组,数组元素类型为T
unique_ptr<T[]> u(p) u指向内置指针p所指向的动态分配的数组,p必须能转换为类型T*
u[i] 返回u拥有的数组中位置i处的对象,u必须指向一个数组

指向数组的unique_ptr不支持成员访问运算符(点和箭头运算符)

2.2 allocator类

  allocator类定义在头文件memory中,下表介绍了allocator的操作

标准库allocator类及其算法
操作 描述
allocator<T> a 定义了一个名为a的allocator对象,可以为类型为T的对象分配内存
a.allocate(n) 分配一段原始的,为构造的内存,保存n个类型为T的对象
a.deallocate(p, n) 释放从T*指针中p中地址开始的内存,这块内存保存了n个类型为T的对象;在调用deallocate之前,必须对每个这块内存中创建的对象调用的destroy
a.construct(p, args) p必须是一个类型T*的指针,指向一块原始内存;arg被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象
a.destroy(p) p为T*类型的指针,对p指向的对象执行析构函数

拷贝进而填充未初始化内存的算法

allocator算法
算法 描述
uninitialized_copy(b, e, b2) 从迭代器b和e指出的输入范围拷贝元素到迭代器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个对象

猜你喜欢

转载自blog.csdn.net/qq_18150255/article/details/89032906