美文网首页C++
第12章:动态内存

第12章:动态内存

作者: MrDecoder | 来源:发表于2019-01-09 18:34 被阅读24次
    • #1.动态内存与智能指针
      • 1.1 shared_ptr类
      • 1.2 直接管理内存
      • 1.3 shared_ptr和new结合使用
      • 1.4 智能指针和异常
      • 1.5 unique_ptr
      • 1.6 weak_ptr
    • #2. 动态数组
      • 2.1 new和数组
      • 2.2 allocator类

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

    动态对象的正确释放被证明是编程中极其容易出错的地方。为了更安全地使用动态对象,标准库定义了两个智能指针类型来管理动态分配的对象。当一个对象应该被释放时,指向它的智能指针可以确保自动地释放它。

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

    除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作自由空间。程序用堆来存储动态分配的对象——即,那些在程序运行时分配的对象。

    ==虽然动态内存有时是必要的,但众所周知,正确地管理动态内存是非常棘手的。==

    #.1 动态内存和智能指针

    C++中,动态内存管理是通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。

    为了更容易地使用动态内存,新的标准库提供了两种智能指针类型来管理动态对象。新的标准库提供的这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。

    1.1 shared_ptr类

    类似vector,智能指针也是模板。因此,当我们创建一个智能指针时,必须提供额外的信息——指针可以指向的类型。与vector一样,我们在尖括号内给出类型,之后是所定义的这种智能指针的名字:

    shared\_ptr<string> p1;//shared_ptr,可以指向string
    shared\_ptr<list<string>> p2; //shared_ptr,可以指向int的list
    

    默认初始化的智能指针保存着一个空指针。

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

    if(p1 && p1->empty()) {
        *p1 = "hi"; //如果p1指向一个空string,解引用p1,将一个新值赋予string
    }
    
    make_shared函数

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

    当要用make_shared时,必须指定想要创建的对象的类型。定义方式与模板类相同,在函数名之后跟一个尖括号,在其中给出类型:

    //指向一个值为42个int的shared_ptr
    shared_ptr<int> p3 = make_shared<int>(42);
    //p1指向一个值为"9999999999"的string
    shared_ptr<string> p4 = make_shared<string>(10,'9');
    //p5指向一个值初始化的int,即,值为0
    shared_ptr<int> p5 = make_shared<int>();
    

    类似顺序容器的emplace成员,make_shared用其参数来构造给定类型的对象。

    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)离开其作用域时,计数器就会递减。

    一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象:

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

    此例中我们分配了一个int,将其指针保存在r中。接下来,我们将一个新值赋予r。在此情况下,r是唯一指向此int的shared_ptr,在把q赋给r的过程中,此int被自动释放。

    使用了动态生存期的资源的类

    程序使用动态内存出于以下三种原因之一:

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

    ==使用动态内存的一个常见原因是允许多个对象共享相同的状态。==

    1.2 直接管理内存

    C++语言定义了两个运算符来分配和释放动态内存。运算符new分配内存,delete释放new分配的内存。

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

    在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针:

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

    此new表达式在自由空间构造一个int型对象,并返回指向该对象的指针。

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

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

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

    int *pi = new int(1024); //pi指向的对象的值为1024
    string *ps = new string(10,'9'); //*ps为“9999999999”
    //vector有10个元素,值依次从0到9
    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
    int *pi1 = new int;//默认初始;*pi1的值未定义
    int *pi2 = new int();//值初始化为0,*pi2为0
    

    ==出于与变量初始化相同的原因,对动态分配的对象进行初始化通常是个好主意。==

    动态分配的const对象

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

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

    类似其他任何const对象,一个动态分配的const对象必须进行初始化。对于一个定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型的对象就必须显示初始化。由于默认分配的对象是const的,new返回的指针是一个指向const的指针。

    内存耗尽

    一旦一个程序用光了它所有可用的内存,new表达式就会失败。默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。我们可以改变使用new的方式来阻止它抛出异常:

    //如果分配失败,new抛出std::bad_alloc
    int *p1 = new int;
    int *p2 = new (nothrow)int; //如果分配失败,new返回一个空指针
    

    我们称这种形式的new为定位new。定位new表达式允许我们向new传递额外的参数。在此例中,我们传递给它一个由标准库定义的名为nothrow的对象。如果将nothrow传递给new,我们的意图是告诉它不能抛出异常。如果这种形式的new不能分配所需内存,它会返回一个空指针。bad_alloc和nothrow都定义在头文件new中。

    释放动态内存

    为了防止内存耗尽,在动态内存使用完毕后,必须归还给系统。我们通过delete表达式来将动态内存归还给系统。delete表达式接受一个指针,指向我们想要释放的对象:

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

    与new类型类似,delete表达式也执行两个动作:销毁给定指针指向的对象;释放对应的内存。

    指针值和delete

    我们传递给delete的指针必须指向动态分配的内存,或者是一个空指针。释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的:

    int i, *pi1 = &i, *pi2 = nullptr;
    double *pd = new double(33), *pd2 = pd;
    delete i; //错误:i不是一个指针
    delete pi1; //未定义:pi1指向一个局部变量
    delete pd; //正确
    delete pd2; //未定义:pd2指向的内存已经被释放了
    delete pi2; //正确:释放一个空指针总是没有错误的
    
    动态对象的生存期直到被释放时为止

    由shared_ptr管理的内存在最后一个shared_ptr销毁时会被自动释放。但对于通过内置指针类型来管理的内存,就不是这样了。对于一个由内置指针管理的动态对象,直到被显示释放之前它都是存在的。

    返回指向动态内存的指针(而不是智能指针)的函数给其调用者增加了一个额外负担——调用者必须记得释放内存:

    //factory返回一个指针,指向一个动态分配的对象
    Foo* factory(T arg) {
        return new Foo(arg);//调用者负责释放此内存
    }
    

    此处,use_factory函数调用factory,后者分配一个类型为Foo的新对象。当use_factory返回时,局部变量p被销毁。此变量是一个内置指针,而不是一个智能指针。

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

    delete之后重置指针值

    在delete之后,指针就变成了人们所说的空悬指针,即,指向一个块曾经保存数据对象但现在已经无效的内存的指针。

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

    重置p对q没有任何作用,在我们释放p所指向的内存时,q也变为无效了。

    1.3 shared_ptr和new结合使用

    如前所述,如果我们不初始化一个智能指针,它就会被初始化为一个空指针。我们可以用new返回的指针来初始化智能指针:

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

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

    shared_ptr<int> p1 = new int(42); //错误:必须使用直接初始化形式
    shared_ptr<int> p1(new int(42)); //正确:使用了直接初始化形式
    

    p1的初始化隐式地要求编译器用一个new返回的int*来创建一个shared_ptr。由于我们不能进行内置指针到智能指针的隐式转换,因此这条初始化语句是错误的。出于相同的原因,一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针:

    shared_ptr<int> clone(int p) {
        return new int(p);
    }
    

    我们必须将shared_ptr显式绑定到一个想要返回的指针上:

    shared_ptr<int> clone(int p) {
        //正确:显式地用int*创建shared_ptr<int>
        return new shared_ptr(new int(p));
    }
    

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

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

    shared_ptr可以协调对象的析构,但这仅限于其自身的拷贝(也是shared_ptr)之间。这也是为什么我们推荐使用make_shared而不是new的原因。这样,我们就能在分配对象的同时将shared_ptr与之绑定,从而避免了无意中将同一块内存绑定到多个独立创建的shared_ptr上。

    考虑下面对shared_ptr进行操作的函数:

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

    process的参数是传值方式传递的,因此实参会被拷贝到ptr中。拷贝一个shared_ptr会递增其引用计数,因此,在process运行过程中,引用计数至少为2。当process接受时,ptr的引用计数会递减,但不会为0。因此,当局部变量ptr被销毁时,ptr指向的内存不会被释放。

    使用此函数的正确方法是传递给它一个shared_ptr:

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

    虽然不能传递给process一个内置指针,但可以传递给它一个(临时的)shared_ptr,这个shared_ptr是用一个内置指针显式构造的。但是,这样做很肯能会导致错误:

    int *x(new int(1024)); //危险:x是一个普通指针,不是一个智能指针
    process(x); //错误:不能讲int*转换为一个shared_ptr<int>
    process(shared_ptr<int>(x)); //合法,但内存会被释放
    int j = *x; //未定义的:x是一个空悬指针
    

    当将一个shared_ptr绑定到一个普通指针时,我们就将内存管理责任交给了这个shared_ptr。一旦这样做了,我们就不应该使用内置指针来访问shared_ptr所指向的内存。

    ==使用内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时被销毁==。

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

    智能指针定义了一个名为get的函数,它返回一个内置指针,指向智能指针管理的对象。此函数是为了这样一种情况而设计的:我们需要向不能使用智能指针的代码传递一个内置指针。使用get返回的指针的代码不能delete此指针。

    虽然编译器不会给出错误信息,但将另一个智能指针也绑定到get返回的指针上是错误的:

    shared_ptr<int> p(new int(42)); //引用计数为1
    int *q = p.get(); //正确:但使用q时要注意,不要让它管理的指针被释放
    {//新程序块
        //未定义:两个独立的shared_ptr指向相同的内存
        shared_ptr<int>(q);
    }//程序块结束,q被销毁,它指向的内存被释放
    int foo = *p;
    

    在本例中,p和q指向相同的内存。由于它们是相互独立创建的,因此各自的引用计数都是1。当q所在的程序块结束时,q被销毁,这会导致q指向的内存被释放。从而p变成一个空悬指针,意味着当我们试图使用p时,将发生未定义的行为。而且,当p被销毁时,这块内存会被第二次delete。

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

    其他shared_ptr操作

    shared_ptr还定义了其他一些操作,我们可以用reset来将一个新的指针赋予一个shared_ptr:

    p = new int(1024); //错误:不能将一个指针赋予shared_ptr
    p.reset(new int(1024)); //正确:p指向一个新对象
    

    与赋值类似,reset会更新引用计数,如果需要的话,会释放p指向的对象。reset成员经常与unique一起使用,来控制多个shared_ptr共享的对象。在改变底层对象之前,我们检查自己是否是当前对象仅有的用户。如果不是,在改变之前要制作一份新的拷贝:

    if(!p.unique()) {
        p.reset(new string(*p)); //我们不是唯一的用户;分配新的拷贝
    }
    *p += newVal; //现在我们知道自己是唯一的用户,可以改变对象的值
    

    1.4 智能指针和异常

    使用异常处理的程序能在异常发生后令程序流程继续,我们注意到,这种程序需要确保在异常发生后资源能被正确的释放。一个简单的确保资源被释放的方法是使用智能指针。

    如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放:

    void f() {
        shared_ptr<int> sp(new int(42));//分配一个对象
        //这段代码抛出一个异常,且在f中未被捕获
    }//在函数结束时shared_ptr自动释放内存
    

    与之相对的,当发生异常时,我们直接管理的内存是不会自动释放的。如果使用内置指针管理内存,且在new之后在对应的delete之前发生了异常,则内存不会被释放:

    void f() {
        int *ip = new int(42); //动态分配一个新对象
        //这段代码抛出一个异常,且在f中未被捕获。
        delete ip;  //在退出之前释放内存
    }
    

    如果在new和delete之间发生异常,且异常未在f中被捕获,则内存就永远不会被释放了。在函数f之外没有指针指向这块内存,因此就无法释放它了。

    注意:智能指针陷阱
    • 不使用相同的内置指针值初始化(或reset)多个智能指针。
    • 不delete get()返回的指针。
    • 不适用get()初始化或reset另一个智能指针。
    • 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。
    • 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器(deleter)。

    1.5 unique_ptr

    一个unique_ptr“拥有”它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。我们不能拷贝和赋值unique_ptr。

    与shared_ptr不同,没有类似make_shared的标准库函数返回一个unique_ptr。当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。类似shared_ptr,初始化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<string> p1(new string("Stegosaurus"));
    unique_ptr<string> p2(p1); //错误:unique_ptr不支持拷贝
    unique_ptr<string> p3;
    p3 = p2; //错误:unique_ptr不支持赋值
    

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

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

    release成员返回unique_ptr当前保存的指针并将其置为空。因此,p2被初始化为p1原来保存的指针,而p1置为空。

    reset成员接受一个可选的指针参数,令unique_ptr重新指向给定的指针。如果unique_ptr不为空,它原来指向的对象被释放。因此,对p2调用reset释放了用“Stegosaurus”初始化的string所使用的内存,将p3对指针的所有权转移给p2,并将p3置为空。

    调用release会切断unique_ptr和它原来管理的对象间的联系。release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。在本例中,管理内存的责任简单地从一个智能指针转移到另一个。但是,如果我们不用另一个智能指针来保存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;
    }
    

    对于两段代码,编译器都知道要返回的对象将要被销毁。在此情况下,编译器执行一种特殊的“拷贝”。

    向unique_ptr传递删除器

    类似shared_ptr,unique_ptr默认情况下用delete释放它指向的对象。与shared_ptr一样,我们可以重载一个unique_ptr中默认的删除器。但是,unique_ptr管理删除器的方式与shared_ptr不同。

    重载一个unique_ptr中的删除器会影响到unique_ptr类型以及如何构造(或reset)该类型的对象。与重载关联容器的比较操作类似,我们必须在尖括号中unique_ptr指向类型之后提供删除器类型。在创建或reset一个这种unique_ptr类型的对象时,必须提供一个指定类型的可调用对象(删除器):

    //p指向一个类型为objT的对象,并使用一个类型为delT的对象释放objT对象
    //它会调用一个名为fcn的delT类型对象
    unique_ptr<objT,delT> p(new objT,fcn);
    

    1.6 weak_ptr

    weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放,因此,weak_ptr的名字抓住了这种智能指针“弱”共享对象的特点。

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

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

    本例中wp和p指向相同的对象。由于是弱共享,创建wp不会改变p的引用计数;wp指向的对象可能被释放掉。

    由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock。此函数检查weak_ptr指向的对象是否仍存在。如果存在,lock返回一个指向共享对象的shared_ptr。与任何其他shared_ptr类似,只要此shared_ptr存在,它所指向的底层对象也就会一直存在。例如:

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

    #2. 动态数组

    new和delete运算符一次分配/释放一个对象,但某些应用需要一次为很多对象分配内存的功能。例如,vector和string都是在连续内存中保存它们的元素,因此,当容器需要重新分配内存时,必须一次性为很多元素分配内存。

    为了支持这种需求,C++语言和标准库提供了两种一次分配一个对象数组的方法。C++语言定义了另一种new表达式语法,可以分配并初始化一个对象数组。标准库中包含一个名为allocator的类,允许我们将分配和初始化分离。使用allocator通常会提供更好的性能和更灵活的内存管理能力。

    ==大多数应用应该使用标准库容器而不是动态分配的数组。使用容器更为简单、更不容易出现内存管理错误并且可能有更好的性能。==

    2.1 new和数组

    为了让new分配一个对象数组,我们要在类型名之后跟一对方括号,在其中指明要分配的对象的数目。在下例中,new分配要求数量的对象并(假定分配成功后)返回指向第一个对象的指针:

    //调用get_size()确定分配多少个int
    int *pia = new int[get_size()]; //pia指向第一个int
    

    方括号中大小必须是整型,但不必是常量。

    也可以用一个表示数组类型的类型别名来分配一个数组,这样,new表达式就不需要方括号了:

    typedef int arrT[42]; //arrT表示42个int的数组类型
    int *p = new arrT; //分配一个42个int的数组;p指向第一个int
    

    在本例中,new分配一个int数组,并返回指向第一个int的指针。即使这段代码中没有方括号,编译器执行这个表达式还是会用new[]。即,编译器执行如下形式:

    int *p = new int[42];
    
    分配一个数组得到一个元素类型的指针

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

    ==要记住我们所说的动态数组并不是数组类型,这是很重要的。==

    初始化动态分配对象的数组

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

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

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

    //10个int分别用列表对应的初始化器初始化
    int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
    //10个string,前4个用给定的初始化器初始化,剩余的进行值初始化
    string *psa3 = new string[10]{ "a","an","the",string(3,'x')};
    

    与内置数组对象的列表初始化一样,初始化器会用来初始化动态数组中开始部分的元素。如果初始化器数目小于元素数目,剩余元素将进行值初始化。如果初始化器数目大于元素数目,则new表达式失败,不会分配任何内存。

    动态分配一个空数组是合法的

    可以用任意表达式来确定要分配的对象的数目:

    size_t n = get_size(); //get_size返回需要的元素的数目
    int *p = new int[n]; //分配数组保存元素
    for(int *q = p;q != p + n;++q) {
        /*处理数组*/
    }
    

    这样产生了一个有意思的问题:如果get_size返回0,会发生什么?答案是代码仍能正常工作。虽然我们不能创建一个大小为0的静态数组对象,但当n等于0时,调用new[n]是合法的:

    char arr[0]; //错误:不能定义长度为0的数组
    char *cp = new char[0]; //正确:但cp不能解引用
    

    当我们用new分配一个大小为0的数组时,new返回一个合法的非空指针。此指针保证与new返回的其他任何指针都不相同。对于零长度的数组来说,此指针就像尾后指针一样。

    释放动态数组

    为了释放动态数组,我们使用一种特殊形式的delete——在指针前加一个空方括号对:

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

    第二条语句销毁pa指向的数组中元素,并释放对应的内存。数组中的元素按逆序销毁,即,最后一个元素首先被销毁,然后是倒数第二个,依此类推。

    当我们释放一个指向数组的指针时,空方括号对是必需的:它指示编译器此指针指向一个对象数组的第一个元素。如果我们在delete一个指向数组的指针时忽略了方括号,其行为是未定义的。

    回忆一下,当我们使用一个类型别名来定义一个数组类型时,在new表达式中不使用[]。即使是这样,在释放一个数组指针时也必须使用方括号:

    typedef int arrT[42]; //arrT是42个int数组的类型别名
    int *p = new arrT; //分配一个42个int的数组;p指向第一个元素
    delete[] p; //方括号是必需的,因为我们当初分配的是一个数组
    

    不管外表如何,p指向一个对象数组的首元素,而不是一个类型为arrT的单一对象。因此,在释放p时我们必须使用[]。

    智能指针和动态数组

    标准库提供了一个可以管理new分配的数组的unique_ptr版本。为了用一个unique_ptr管理动态数组,我们必须在对象类型后面跟一对空方括号:

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

    类型说明符中的方括号(<int[]>)指出up指向一个int数组而不是一个int。由于up指向一个数组,当up销毁它管理的指针时,会自动使用delete[]。

    2.2 allocator类

    new有一些灵活性上的局限,其中一方面表现在它将内存分配和对象构造组合在一起。类似的,delete将对象析构和内存释放组合在一起。

    当分配一大块内存时,我们通常计划在这块内存上按需构造对象。在此情况上,我们希望将内存分配和对象构造分离。这意味着我们可以分配大块内存,但只在真正需要时才真正执行对象创建操作。

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

    string *const p = new string[n]; //构造n个空string
    string s;
    string *q = p; //p指向第一个string
    while(cin >> s && q != p + n) {
        *q++ = s; //赋予*q一个新值
    }
    const size_t size = q - p; //记住我们读取了多少个string
    //使用数组
    delete[] p; //p指向一个数组;记得delete[]来释放
    

    new表达式分配并初始化了n个string。但是,我们可能不需要n个string,少量string可能就足够了。这样,我们就可能创建了一些永远也用不到的对象。

    allocator类

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

    类似vector,allocator是一个模板。为了定义一个allocator对象,我们必须指明这个allocator可以分配的对象类型。当一个allocator对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置:

    allocator<string> alloc; //可以分配string的allocator对象
    auto const p = alloc.allocate(n); //分配n个未初始化的string
    

    这个allocate调用为n个string分配了内存。

    allocator分配未构造的内存

    allocator分配的内存是未构造的。我们按需要在此内存中构造对象。在新标准库中,construct成员函数接受一个指针和零个或多个额外参数,在给定位置构造一个元素。额外参数用来初始化构造的对象。类似make_shared的参数,这些额外参数必须是与构造的对象的类型相匹配的合法的初始化器:

    auto q = p; //q指向最后构造的元素之后的位置
    alloc.construct(q++); //*q为空字符串
    alloc.construct(q++, 10, 'c');
    alloc.construct(q++,"hi");
    

    在早起版本的标准库中,construct只接受两个参数:指向创建对象位置的指针和一个元素类型的值。因此,我们只能将一个元素拷贝到未构造空间中,而不能用元素类型的任何其他构造函数来构造一个元素。

    还未构造对象的情况下就使用原始内存是错误的:

    cout << *p << endl; //正确:使用string的输出运算符
    cout << *q << endl; //灾难:q指向未构造的内存
    

    ==为了使用allocate返回的内存,我们必须用construct构造对象。使用未构造的内存,其行为是未定义的。==

    当我们使用完对象后,必须对每个构造的元素调用destroy来销毁它们。函数destory接受一个指针,对指向的对象执行析构函数:

    while (q != p)
    {
        alloc.destroy(--q);//释放我们真正构造的string
    }
    

    在循环开始处,q指向最后构造的元素之后的位置。我们在调用destroy之前对q进行了递减操作。因此,第一次调用destroy时,q指向最后一个构造的元素。最后一步循环中我们destroy了第一个构造的元素,随后q将与p相等,循环结束。

    ==我们只能对真正构造了的元素进行destory操作==

    一旦元素销毁后,就可以重新使用这部分内存来保存其他string,也可也将其归还给系统。释放内存通过调用deallocate来完成:

    alloc.deallocate(p,n);
    

    我们传递给deallocate的指针不能为空,它必须指向由allocate分配的内存。而且,传递给deallocate的大小参数必须与调用allocated分配内存时提供的大小参数具有一样的值。

    相关文章

      网友评论

        本文标题:第12章:动态内存

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