美文网首页C++
[C++ Primer Note11] 动态内存

[C++ Primer Note11] 动态内存

作者: 梦中睡觉的巴子 | 来源:发表于2018-11-26 18:45 被阅读5次

    除了自动和static对象外,C++还支持动态分配对象。动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地被释放时,这些对象才会销毁。

    1. 静态内存用来保存局部static对象,类static数据成员,以及定义在任何函数之外的变量。栈内存用来保存定义在函数内的非static对象。分配在静态内存或栈内存的对象由编译器自动创建和销毁。
    2. 除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作自由空间(free store)堆(heap)。程序用堆来存储动态分配的对象——即程序运行时分配的对象。动态对象的生存期由程序而不是编译器来控制。
    3. C++中,动态内存的管理是通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针;delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
    4. 为了更容易,更安全地使用动态内存,新的标准库提供了两种智能指针(smart pointer)类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象,这三种类型都定义在memory头文件中。
    5. 类似vector,智能指针也是模板,因此当我们创建智能指针时,必须提供额外信息——指针可以指向的类型。
    shared_ptr<string> p1;
    shared_ptr<list<int>> p2;
    

    默认初始化的智能指针保存着一个空指针
    智能指针的使用方式与普通指针类似,解引用返回它指向的对象,如果在条件判断中使用智能指针,效果就是检测它是否为空

    image
    1. 最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。该函数同样定义在memory中。
    shared_ptr<int> p3=make_shared<int>(42);
    shared_ptr<string> p4=make_shared<string>(10,'9);
    shared_ptr<int> p5=make_shared<int>();
    

    如果我们不传递参数,对象会进行值初始化

    1. 当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象:
    auto p=make_shared<int>(42);
    auto q(p);  //p和q指向相同对象,此对象有两个引用者
    

    我们可以认为每个shared_ptr都有一个关联的计数器,通常称为引用计数(reference count),无论我们何时拷贝一个shared_ptr,计数器都会递增。当我们给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开作用域),计数器就会递减。

    1. 当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象。它是通过析构函数实现的。
    2. 程序使用动态内存出于以下三种原因之一
    • 程序不知道自己需要使用多少对象
    • 程序不知道所需对象的准确类型
    • 程序需要在多个对象间共享数据
    class SharedVector{
    public:
        SharedVector(initializer_list<string> l):data(make_shared<vector<string>>(l)){}
        shared_ptr<vector<string>> data;
    
    
    };
    

    当我们拷贝SharedVector对象时,会使用默认版本的拷贝,因此Shared_ptr也被拷贝,从而实现了资源的对象间共享。

    1. C++定义了两个运算符newdelete来分配和释放动态内存。相对于智能指针,使用这两个运算符管理内存非常容易出错。
    2. 在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针:
    int *pi=new int;
    

    默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的值将是未定义的,而类类型对象将使用*默认构造函数进行初始化:

    string *ps=new string;     //初始化为空string
    int *pi=new int;   //pi指向一个未初始化的int
    

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

    int *pi=new int(1024);
    string *ps=new string(10,'9');
    vector<int> *pv=new vector<int>{0,1,2,3,4,5,6,7,8};
    

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

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

    对于定义了自己的构造函数的类类型来说,要求值初始化没有意义(对象都会通过默认构造函数来初始化),但对于内置类型,值初始化意味着对象有良好定义的值。

    1. 用new分配const对象是合法的,一个动态const对象必须进行初始化
    const int *pci=new const int(1024);
    
    1. 一旦一个程序用光了它所有可用的内存,new表达式就会失败。默认情况下,它会抛出一个类型为bad_alloc的异常,我们可以改变new的方式来阻止抛出异常:
    int *pw=new (nothrow) int;   //如果分配失败,返回一个空指针
    

    我们称这种形式的new为定位new(placement new),定位new允许我们向new传递额外的参数。此处我们传递了一个nothrow对象。它与bad_alloc都定义在头文件new中。

    1. 为了防止内存耗尽,我们通过delete 表达式来将动态内存归还给系统。delete接受一个指针,指向我们想要释放的对象:
    delete p;  //p必须指向一个动态分配的对象或是一个空指针
    
    1. 释放一块并非new的内存,或者将相同的指针释放多次,其行为是未定义的。
    2. 由内置指针管理的动态内存在被显式释放前会一直存在,如果不注意这一点很容易造成内存泄漏
    3. 当我们delete一个指针后,指针值就变为无效了,该指针就变成了所谓的空悬指针(dangling pointer),即指向一块曾经保存数据对象但现在已经无效的内存的指针。如果我们需要保留指针,可以在delete之后赋予它nullptr的字面量。但实际上,可能有多个指针指向相同的内存,这仅仅提供了很有限的保护。
    4. 如前所述,我们如果不初始化一个智能指针,它就会被初始化为一个空指针。我们可以用new返回的指针来初始化智能指针:
    shared_ptr<int> p1(new int(42));
    

    接受指针参数的智能指针构造函数是explicit的,所以必须使用直接初始化的形式。同理,一个返回shared_ptr的函数不能返回一个普通指针。
    默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放关联的对象。但是我们可以提供操作替代delete使得智能指针可以绑定到一个指向其他类型资源的指针上

    image
    1. 当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。一旦这么做了,就不应该再使用内置指针来访问shared_ptr所指向的内存了。
    2. 如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放。而直接管理的内存如果在new之后delete之前发生了异常,则内存不会释放
    3. 我们还可以利用智能指针来管理不具有良好定义的析构函数的类。
    4. 为了正确使用智能指针,我们必须坚持一些基本规范
    • 不使用相同的内置指针值初始化多个智能指针
    • 不delete get()返回的指针
    • 不使用get()初始化另一个智能指针
    • 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了
    • 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器
    1. 一个unique_ptr ”拥有“它所指向的对象,某个时刻只能有一个unique_ptr指向一个给定对象。当它被销毁时,指向的对象也被销毁。
      没有类似make_shared的函数返回一个unique_ptr。当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上,初始化也同样必须采用直接初始化
    unique_ptr<double> p1;
    unique_ptr<int> p2(new int(10));
    

    由于一个unique_ptr独占它指向的对象,因此不支持普通的拷贝赋值操作。

    image
    1. 虽然不能拷贝或赋值unique_ptr,但是可以通过releasereset将指针的所有权转移:
    p2.reset(p3.release());
    
    1. 不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr。比如,从函数返回一个unique_ptr
    2. 与shared_ptr一样,我们可以重载一个unique_ptr中默认的删除器,此处不赘述
    3. weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使有weak_ptr指向对象。
      image
    4. 当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它:
    auto p=make_shared<int>(42);
    weak_ptr<int> wp(pP;
    
    1. 由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock,此函数检查weak_ptr指向的对象是否仍存在。如果存在,则返回一个shared_ptr
    2. C++和标准库提供了两种一次分配一个对象数组的方法。C++定义了另一种new表达式语法,可以分配并初始化一个对象数组。标准库中包含一个名为allocator的类,允许我们将分配和初始化分离。
    3. 大多数应用应该使用标准库容器而不是动态分配的数组。使用容器更为简单,更不容易出现内存管理错误并且可能有更好的性能。
    4. 为了让new分配一个对象数组,我们要在类型名之后跟一对方括号,在其中指明要分配的对象的数目,new返回指向第一个对象的指针
    int *p=new int[size];
    
    1. 虽然我们通常称new T[]分配的内存为“动态数组”,但实际上我们只是得到一个数组元素类型的指针动态数组并不是数组类型,因此前文所述的begin和end函数,以及范围for语句,通通不适用
    2. 默认情况下,new分配的对象,都是默认初始化的。可以对数组中的元素进行值初始化,方法是在大小之后跟一对空括号:
    int *p=new int[10]();   //10个值初始化为0的int
    

    新标准中,我们还可以提供一个元素初始化器的花括号列表:

    int *p=new int[2]{1,2};
    
    1. 可以用任意表达式来确定要分配的对象的数目:
    size_t n=get_size();
    int *p=new int[n];
    
    1. 当我们用new分配一个大小为0的数组,new返回一个合法的非空指针。但此指针不能解引用。
    2. 为了释放动态数组,我们使用一种特殊形式的delete——指针前加上一个空括号对
    delete [ ] p;
    

    数组中的元素按逆序销毁。如果没有方括号,行为是未定义的

    1. 标准库提供了一个可以管理new分配的数组的unique_ptr版本,为了用一个unique_ptr管理动态数组,我们必须在对象类型后面跟一对空方括号
    unique_ptr<int[]> up(new int[10]);
    up.release();    //自动用delete[]销毁其指针
    
    image
    1. shared_ptr不直接支持管理动态数组,必须提供自己定义的删除器,此处不展开。
    2. new有一些灵活性上的局限,其中一方面表现在它将内存分配和对象构造组合在了一起。标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方式,它分配的内存是原始的,未构造的。
      image
    allocator<string> alloc;  // 可以分配 string 的 allocator 对象
    auto const p = alloc.allocate(n);  // 分配 n 个未初始化的 string
    
    auto q = p;  // q 指向最后构造的元素之后的位置
    alloc.construct(q++);  // *q 为空字符串
    alloc.construct(q++, 10, 'c');  // *q 为 cccccccccc
    alloc.construct(q++, "hi");  // *q 为 hi
    
    while(q != p)
        alloc.destroy(--q);  // 释放我们真正构造的 string
    
    alloc.deallocate(p, n);  // 释放内存
    
    1. 为了使用allocator返回的内存,必须用construct构造对象,使用位构造的内存,行为未定义。
      使用完,必须对每个构造的元素调用destroy来销毁,destroy接受一个指针,对指向的对象执行析构函数。
      销毁后,可重新使用这部分内存保存其他 string, 也可以释放内存还给系统
    2. 拷贝和填充未初始化内存的算法


      拷贝和填充未初始化内存的算法
    vector<int> vi{1, 2, 3};
    allocator<int> alloc;
    auto p = alloc.allocate(vi.size() * 2);  // 分配比 vi 中元素所占空间大一倍的动态内存
    auto q = alloc.unintialized_copy(vi.begin(), vi.end(), p); //拷贝vi中元素构造从p开始的元素
    uninitialized_fill_n(q, vi.size(), 42);  // 将剩余元素初始化为42
    

    相关文章

      网友评论

        本文标题:[C++ Primer Note11] 动态内存

        本文链接:https://www.haomeiwen.com/subject/psleqqtx.html