第十二章 动态内存

前言

静态内存用来保存局部static对象,类static数据成员以及定义在任何函数之外的变量。栈内存用来保存定义在函数内的非static对象。分配在静态或栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在其定义的程序块运行时才存在;static对象在使用之前分配,在程序结束时销毁。

动态内存

一. 动态内存与智能指针

  1. 新标准提供的两种智能指针区别在于,管理底层指针的方式:

shared_ptr 允许多个指针指向同一个对象;
unique_ptr 独占所指向的对象;

标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象;

1. shared_ptr类

  1. 默认初始化的智能指针中保存着一个空指针;
  2. 函数unique:若指针的use_count为1,返回true,否则false;
    函数use_count:返回与指针共享对象的智能指针数量;可能很慢,主要用于调试;
  3. 最安全的分配和使用动态的方法是调用make_shared 的标准函数,通常和auto一起使用。
  4. 一旦一个shared_ptr 的计数器变为0的时候,它就会自动释放自己所管理的对象;

其中利用了析构函数,其会递减它所指向的对象的引用计数,如果引用计数为0,其就会销毁对象,并释放它所占用的内存。

对于一块内存,shared_ptr 类保证只要有任何shared_ptr 对象引用它,它就不会释放掉;

如果你将shared_ptr 存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase 删除不再需要的元素;

一般而言,如果两个对象共享底层的数据,当某个对象被销毁时,我们不能单方面的销毁底层数据;

2. 直接管理内存

  1. 默认情况下,动态内存是默认初始化的,这意味着内置类型或组合类型(比如 int ,string,vector等等)的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化;
  2. 如果提供了一个括号包围的初始化器,就可以使用auto,从此初始化器来推断想要分配的对象的类型,但是,由于编译器要用初始化器的类型推断要分配的类型,只有当括号中仅有单一初始化器时才可以使用auto。

比如代码:

auto p1 = new auto(1); //正确
    
auto p2 = new auto(2, "a", 'c'); //报错
  1. 类似其他任何const 对象,一个动态分配的const 对象必须进行初始化;
    例如:
    // 显式初始化
    const int * p1 = new const int(12);
    // 隐式初始化
    const string * p2 = new const string;
  1. 默认情况下,如果new不能分配所要求的内存空间,它就会抛出一个类型为bad_alloc 的异常;
    改变new的方式来阻止它抛出的异常,即定位new:
// 如果分配失败,不会抛异常,而是返回一个空指针
    int * p1 = new (nothrow) int;
  1. delete掉的必须是指向动态分配的内存,或者是一个空指针;

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

  3. 空悬指针,指向一块曾经保存数据对象但现在已经无效的内存的指针;

避免空闲指针,可以在指针离开作用域后,释放掉所关联的内存,如果需要保留指针,可以在delete 之后将nullptr 赋予指针;

但是即使释放自己所关联的内存,但是可能存在多个指针指向相同的内存,都变无效了,其并没有对它们进行重置,

  1. 接受指针参数的智能指针构造函数是explicit的,因此,不能将一个内置指针隐式转换为一个智能指针,必须直接初始化形式来初始化一个智能指针;
    比如:
    shared_ptr<int> p1 = new int(1024); // 报错
    
    shared_ptr<int> p2(new int(1024)); // 正确
  1. 使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时被销毁;

  2. get返回一个内置指针,指向智能指针管理的对象;

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

  1. reset函数会更新引用计数,会导致释放掉之前的指向的对象;
    reset 成员经常与unique 一起使用,来控制多个shared_ptr 共享的对象;

  2. 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器;

  3. 由于一个unique_str 拥有它所指向的对象,因此unique_str 不支持普通的或赋值操作,但是我们可以通过调用release 或 reset 将指针的所有权从一个(非const)unique_str 转移给另一个unique;

比如:

// 将所有权从p1转移到p2
    unique_ptr<string> p1(new string("Hello"));
    unique_ptr<string> p2(p1.release()); // 将p1置为空
    
    // 将所有权从p3转移到p2
    unique_ptr<string> p3(new string("World"));
    p2.reset(p1.release());
    
    unique_ptr<string> p4(p1); // 报错

  1. 不能拷贝 unique_ptr 的规则有一个例外:可以拷贝或赋值一个将要被销毁的 unique_ptr ,最常见的例子就是,从函数返回一个unique_ptr。

  2. weak_ptr 是一种不控制所指向对象生存期的智能指针,他指向一个 shared_ptr 管理的对象;将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数。一旦最后一个指向指向对象的 shared_ptr 被销毁,对象就会释放,即使有 weak_ptr 指向对象,对象也还是会释放;

  3. use_count:共享对象的shared_ptr的数量;
    expired:若use_count 为0,返回true,否则返回false;
    lock:如果expired为true,返回一个空的shared_ptr,否则返回一个指向自己的shared_ptr;

  4. 对象可能不存在,所以不能使用weak_ptr 直接访问对象,而必须调用lock。
    比如:

int main(int argc, const char * argv[]) {
    auto p = make_shared<int>(42);
    weak_ptr<int> wp(p); //用shared_ptr进行初始化
    
    if (shared_ptr<int> np = wp.lock()) { // 访问对象安全
        cout << "np != null" << endl;
    }
    
    return 0;
}

二. 动态数组

  1. 使用容器的类可以使用默认版本的拷贝、赋值和析构操作,分配动态数组的类则必须定义自己版本的操作,在拷贝、赋值以及销毁对象时管理所关联的内存;因此使用容器更为简单、更不容易出现内存管理错误并且可能有更好的性能;

  2. 当用new分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针;

  3. 如果初始化器数目小于元素数目,剩余元素将进行值初始化;如果初始化器数目大于元素数目,则new表达式失败,不会分配任何内存;

  4. 当用new 分配一个大小为0的数组时,new返回一个合法的非空指针;对于长度为0的数组而言,此指针就像尾后指针一样,可以像使用尾后迭代器一样使用这个指针。但是,此指针不能解引用,毕竟它不指向任何元素。

  5. 释放动态数组时,需要加上方括号[],其释放内存时,按照逆序销毁数组中元素;

  6. 标准库提供了一个可以管理new分配的数组的unique_ptr 的版本;

代码:

// up指向一个包含10个未初始化int的数组
unique_ptr<int[]> up(new int[10]);
up.release(); // 自动用delete[]销毁其指针

当一个unique_ptr指向一个数组时,不能使用点和箭头,即不支持成员访问运算符,但是可以通过下标运算符来访问数组中元素;

  1. 和unique_ptr不同,shared_ptr不直接支持管理动态数组,如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器;
    必须使用get获取一个内置指针,然后用它来访问数组元素;

  2. allocator类帮助我们将内存分配和对象构造分离开;提供一种类型感知的内存分配方法,它分配的内存是原始的,未构造的;其类似vector,是个模版,必须指明可以分配的对象类型,其会根据给定的对象类型来确定恰当的内存大小和对齐位置;
    代码:

allocator<string> alloc;
auto const p = alloc.allocate(10); //分配10个未初始化的string
  1. 为了使用allocate返回的内存,必须使用construct构造对象,使用未构造的内存,其行为是未定义的;
    当用完对象后,必须对每个构造的元素调用destroy来销毁它们,函数destroy接受一个指针,对指向的对象执行析构函数;(前提是已经构造了)
    传递给deallocate的指针不能为空,它必须指向由allocate分配的内存,而且,传递给deallocate的大小参数必须与调用allocated分配内存时提供的大小参数具有一样的值。

  2. allocator类中还有定义了伴随算法,可以在未初始化内存中创建对象。

uninitialized_copy(b, e, b2)
uninitialized_copy_n(b, n, b2)
uninitialized_fill(b, e, t)
uninitialized_fill_n(b, n, t)

posted @ 2016-09-21 17:17  banananana  阅读(180)  评论(0编辑  收藏  举报