• C++ Primer 5th 第12章 动态内存


    到本章为止,我们还没有接触过动态内存,接下来我们会学习动态内存的知识。

    关于动态内存,第一个疑问是:为什么我们要使用动态内存?它有什么优点?

    要了解动态内存,得先了解一个进程在内存中的基本空间结构。进程是被执行起来的程序,程序是未执行的二进制文件。

    大概来说,程序包含了三大部分内容:一是程序代码,称为text段,二是程序中定义的初始化了的全局和静态变量(非0初始值),称为data段,三是未初始化的全局和静态变量(或者初始值为0),称为bss段。

    进程是执行起来的程序,自然包含上面的三部分内容,另外还会额外增加进两部分内容。一个是堆,另一个是栈。栈用来存储我们在程序中用到的局部变量,而堆则是用于本章所学习的动态内存存储。

    栈的效率高,速度快,局部访问,自动管理,空间大小有限。

    堆是一个内存池,它也被称为自由存储,堆的空间可以很大,也能全局访问共享,手动管理。

    有了这些基本知识,再回到开头的第一个问题:为什么使用动态内存及其优点。因为动态内存是存在堆中的,所以其优点是空间大小可以很大。除了优点之外,更有几个不使用动态内存则无法解决的问题,这也是为什么要使用动态内存的原因:

    1.我们不知道程序到底会需要多少空间或者说多少个对象。比如说,程序经常需要处理一批数据,有多少个数据不确定,为了存储这些数据,我们只能根据当时的情况来申请内存空间来进行存储和处理。

    2.为了实现面向对象的动态绑定功能。这点将在后面的章节进行学习。

    3.程序的多个对象之间进行数据的共享,或者说全局访问共享。典型的例子是接下来即将学习的智能指针。

    简而言之,动态内存的关键点在于“动”字上。

    加上动态内存,在C++总共就有了3种类型的内存:静态内存、动态内存、栈内存。静态内存存储全局变量、静态局部变量、类静态数据成员;栈内存用于存储局部变量、动态内存用于存储手动分配的对象。

    对于手动通过堆分配的内存来说,我们必须手动管理内存的释放,这是相当有难度的,尤其在C++中那些可能出现异常的地方。由于正确的管理内存很棘手,因此标准库提供了两个“智能指针”来帮助我们动态使用内存。一种是“共享指针”,它允许多个指针指向一个对象,而在没有指针指向对象时自动释放内存;另一种是“独占指针”,某一时刻,它只允许一个指针指向对象。标准库还有一个tiny工具weak_ptr,是一个辅助工具,用于指向“共享指针”指向的对象,但不会引起“共享指针”计数增加。

    头文件

    #include <memory>

    提供工具

    shared_ptr;
    unique_ptr;
    weak_ptr;
    make_shared;

    shared_ptr和vector一样,是一个模板,在提供了足够信息后,用于生成特定类型的智能指针。用法如下:

    shared_ptr<int>         *pi;    //int类型指针
    shared_ptr<string>      *ps;    //string类型指针
    shared_ptr<vector<int>> *pv;    //vector<int>容器指针

     以上3个指针都是默认初始化的,在智能指针中,默认初始化的指针将会被初始化为nullptr。

    智能指针的使用方式与普通指针相同,解引用智能指针返回所指对象,在条件判断中使用智能指针就是检测其是否为空。例如:

    if (pv && pv->empty())
    {
        pv.push_back(5);
    }

    最安全的分配和使用动态内存的方法是使用make_shared标准库函数,该函数能够代替我们去动态分配内存,并且将对象进行初始化,之后返回一个智能指针给我们使用。同样的,make_shared也是一个模板,虽然他是一个函数模板,但是却不能根据我们传入的参数进行类型推断。原因是该函数的模板参数存在于返回类型之中,导致无法直接推断,因此我们必须手动指定参数类型。其使用方式如下:

    shared_ptr<string> ps = make_shared<string>();
    auto ps2 = make_shared<string>(10, 'c');

    对于智能指针对象ps和ps2的初始化,我们传给make_shared的参数将会用于初始化ps和ps2所指内存中对象的初始化。如果我们没有传递参数给make_shared,那么ps和ps2依然会被初始化,但是是值初始化,也即ps和ps2指向了各自的内存(内存中的对象没有初始化),而不是默认初始化的nullptr;总而言之,make_shared一定会返回一个指向一块内存的智能指针,而不会返回nullptr;

    另外,由于make_shared不会返回nullptr,所以我们总是可以用auto关键词来让编译器进行推断,进而得出我们想要的特定类型的智能指针。

    我们可以认为每个shared_ptr对象都有一个关联的计数器,通常称为引用计数。当进行一些操作时,计数会根据实际的情况进行增加或减少。例如,拷贝一个智能指针,此时会增加指向对象的指针个数;一个智能指针超出作用域,此时,会减少指向对象的指针个数。当一个计数变为0时,该智能指针会自动释放所指的动态内存。减少计数和销毁动态内存是通过智能指针的析构函数来完成的。

    动态对象的生存周期同引用类型对象相同,当引用对象超出作用域时,引用对象本身会销毁,但引用对象所引用(或者说所指向)的对象不会销毁。同样的,当指向动态对象的对象(或者说指针)超出作用域之后,指向动态对象的对象销毁和动态对象之间并无关系。实例如下:

    int i = 5;
    { //新作用域
        int &ri = i; 
    } //引用对象ri超出作用域
    
    {    int *p = new int{};
    }

    上面代码中引用对象ri超出作用域之后,引用对象ri所引用的对象i并不会销毁。同样的,无名动态对象被指针p所指,当p超出作用域被销毁后,无名对象并不会被销毁。

    直接管理内存

     在C++中也支持直接手动分配和释放内存,使用new来分配内存,使用delete来释放内存,其中new和delete都是类似于"+"、"/"一样的运算符。

    使用new运算符得到是相应类型对象的指针,并且该对象是匿名的,只能使用该对象的指针进行间接访问。当然,我们也可以使用一个引用来绑定到这个动态对象上,从而通过引用来访问对象。

     使用new运算符分配的对象,默认情况下是默认初始化的。对于内置类型,这意味着对象的值是不确定的;对于自定义类型,意味着使用默认构造函数。如果我们想要对动态获得的对象进行初始化,可以使用直接初始化或者列表初始化的方式,当然,也可以使用值初始化的方式。

    对于自定义类类型,使用默认初始化,还是值初始化结果都是调用默认构造函数,通常情况下是没有区别的,除非是非常简单的没有定义任何构造函数的类类型会有差异。

    对于内置类型,使用默认初始化和值初始化则不相同,默认初始化的值是未定义的,值初始化的值则为0。

     我们还可以使用auto配合new来进行类型推断并初始化,语法如下:

    auto p1 = new auto(obj);
    auto p2 = new auto{obj};    //错误,不允许使用花括号

     该语句的作用是使用obj来推断动态对象的类型,并使用obj来初始化该动态对象,new返回的指针存储于p1中。

    动态对象也可以是const类型的,和其他const对象一样,const对象必须初始化。例如:

    const int *p1 = new const int(5);
    const int *p2 = new int(5);

    对于p1来说,p1是一个指向const对象的指针。

    对于p2来说,p2也是一个指向const对象的指针,然后实际对象并不是const对象。

    new分配内存也存在失败的情况。需要知道的是,new即使分配内存失败,也不会导致内存泄露。new内部自身会进行一些处理以防止内存泄露。默认情况下,new在妥善处理后,会抛出一个异常来告知内存分配出现问题。当然,如果我们的程序不接受异常或者没有异常处理,则可以在new后使用"(nothrow)"来告知new不要抛出异常,此时,new返回nullptr指针。如果要使用这些特性,需要包含头文件“new”。

    动态分配内存使得我们能够手动的申请内存,相应的,我们需要手动的释放申请的内存。方法是使用delete运算符,例如

    delete p;

    该运算符和一个运算对象组成一个表达式,此表达式的结果是:销毁p指向的对象,并且释放该对象所占用的内存。

    该运算符只能对动态申请的内存进行释放,并且只能释放一次。对非动态内存进行释放或者对某一内存多次释放,都是未定义的行为。

    另外,由于动态对象的生存周期是我们手动管理的,因此可以在动态对象的生存期间在整个程序中进行共享。

    如前所述,使用智能指针shared_ptr进行内存管理时,我们如果没有初始化shared_ptr对象,那么它指向一个空指针,除非我们使用make_shared来生成智能指针。

    除了使用make_shared来生成有效的智能指针,我们也可以使用new生成的指针来初始化shared_ptr对象从而得到一个有效的智能指针。方法是在定义智能指针对象时,使用new返回的指针来直接初始化。如下:

    shared_ptr<int> pi1(new int);
    shared_ptr<int> pi2 = new int;    //错误,shared_ptr的构造函数是explicit的

    由于shared_ptr的构造函数是explicit的,因此我们不能使用拷贝构造函数来进行初始化,必须使用直接初始化,即直接匹配构造函数的方式来进行初始化。

    还有一个问题是:当我们默认初始化了一个智能指针,该智能指针是指向nullptr的,随后我们自己使用new手动分配了动态内存,如何让默认初始化的智能指针指向我们分配的动态内存?我们可以使用智能指针的reset方法。如下:

    p.reset();
    p.reset(q);
    p.reset(q, d);

    如果p是唯一一个指向动态对象的智能指针,则reset会释放该动态对象,如果向reset传递了普通的非智能指针q,那么p会新指向q所指的内存,如果还向reset传递了d,那么会使用d来释放q,而不是默认的delete。

    unique_ptr

    一个unique_ptr拥有它所指向的对象,和shared_ptr不同,不同的unique_ptr之间不能“共享”指针指向的内存,当一个unique_ptr被销毁时,unique_ptr所指向的对象也会一并销毁。unique_ptr对象只能通过实例化unique_ptr类得到,没有类似make_shared的方法。

    unique_ptr可以默认初始化指向一个空指针,或者使用new返回的指针直接初始化,除此之外,没有其他的初始化方式了。unique_ptr没有拷贝构造和赋值运算的功能。

    虽然不能拷贝或者赋值unique_ptr,但是可以通过unique_ptr的release或者reset方法来转移unique_ptr中指针的所有权。例如:

    unique_ptr<string> p2(p1.release());    //p1所指对象所有权转移到p2,p1置空
    p4.reset(p3.release());             //p3所指对象所有权转移到p4,p4原对象释放

    weak_ptr

    weak_ptr是一种不控制所指对象生存周期的智能指针,也即当weak_ptr析构时,它不会销毁所指对象及释放所指对象的内存。

     动态数组

    在C++中,new和delete支持一次性分配或释放多个对象的动态内存。对比C语言,可以发现C语言是没有动态分配数组这一说的,C语言是典型的面向过程的编程语言。在动态分配时,C语言解决问题的思路过程是:我们需要申请多少个字节的内存;而C++则是我们需要申请构造一个动态对象还是一组动态对象,并没有C语言中直接意义上的多少字节内存。

    C++中动态数组的分配方式是:在欲申请的对象类型后"[ ]"中来指明需要对象的个数;在欲释放的动态数组的指针前面使用"[ ]"来告知delete释放整个数组。例如:

    int *p = new int[10];    //申请动态数组
    delete [] p;           //释放动态数组

    在动态分配数组时,和静态数组不同,动态数组的元素数量可以是个变量,但是该变量的类型必须是整形。 

    除了直接使用"[ ]"来分配数组,我们也可以类型别名来用于new表达式分配动态数组,例如:

    using int_Arr1 = int [10];
    typedef int int_Arr2[15];
    
    auto p1 = new int_Arr1;    //p1指向含有10个元素的动态数组
    auto p2 = new int_Arr2;    //p2指向含有15个元素的动态数组

    对于动态分配得到的数组,new返回的是一个单纯的指针类型,而不是数组类型,回忆第三章关于数组的内容,我们知道数组也是一种类型,是一种复合的复杂类型,其类型包括其中元素类型和元素个数。由于对于数组类型来说其类型由元素的个数和类型决定,因此我们可以使用sizeof对数组进行大小计算,也可以使用新的range-for形式for循环来遍历数组,当然也可以使用标准库的begin和end函数来取得其迭代器。但是new [ ]分配得到的数组却不是一个数组类型,而是一个指针类型,因此该动态数组不能像静态数组那样进行迭代器获取行为。

    动态数组中的元素默认也是默认初始化的,我们可以进行值初始化,如下:

    int *p1 = new int[10]();
    int *p2 = new int[10] {};    //C++11

    我们也可以使用C++11的列表初始化,如下:

    int *p = new int[10] {1, 2, 3, 4, 5};    //前5个元素使用给定值初始化,其余元素值初始化

    如果我们初始化动态数组时,提供的元素个数多于数组最大容纳个数,则new分配失败,并抛出一个异常。

    关于动态数组分配的最后一个问题是:如果动态分配的数组元素个数是0,那么new的行为是什么?答案是new正常返回一个非空指针,但不能对该指针解引用,该指针是一个尾后指针。

    释放动态数组的语法前面已经做出说明,现在考虑一下动态数组元素销毁的次序。对于动态数组释放,元素销毁的次序是逆序的,即先销毁最后一个元素,然后是倒数第二个,以此类推。

    我们在释放动态数组时,必须在指针名前使用"[ ]"以告知编译器销毁的是一个动态数组,而不是单一对象,如果忘记使用这个方括号,那么行为是未定义的,可能造成内存泄露,或者程序崩溃。

    令我们惊讶的是,标准库还提供了一个用于管理动态数组的智能指针,该智能指针是unique_ptr。为了使用unique_ptr,我们需要提供一个类型参数供模板实例化一个具体类。我们提供的用于管理动态数组的类型参数是int [ ],如下:

    unique_ptr<int []> up(new int[10]);
    up.release()    //自动使用delete []来释放数组

    和普通的unique_ptr不同,指向动态数组版本的unique_ptr不支持点和箭头运算符,因为这些解引用操作无意义。另一方面,我们的unique_ptr是指向动态数组的,所以我们可以使用下标索引("[ ]")来访问数组中的元素。

    因为unique_ptr同一时刻只能有一个对象拥有动态数组,如果我们需要在多个对象间共享需要怎么做?我们可以使用sharded_ptr,但是我们必须提供一个删除工具,来替代默认delete操作。如下:

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

    上面我们提供了一个lambda工具给shared_ptr,使得shared_ptr能够在没有对象使用动态数组时,使用该lambda正确释放动态数组。

    由于shared_ptr不支持动态数组管理,因此也就没有提供下标运算符,所以如果我们需要访问动态数组,只能使用shared_ptr中提供的get方法,通过get得到指针之后再继续我们的访问。

    allocator类

    对程序效率要求极高时,new的行为可能会造成一些局限。new运算符在分配动态内存时,无论如何都会对动态对象进行初始化,也即内存的申请和对象的初始化是组合在一起的。如果某些时候需要申请很大空间的动态数组,那么数组中元素的初始化可能就不那么有意义或者说有些开销浪费,因为我们需要在使用内存的时候自己去构造对象。而且对于没有默认构造函数的类来说,我们还必须列表初始化所有的元素。

    标准库allocator类提供了一些方法,能使得动态内存的分配和对象初始化分离开来,它存在于头文件中,如下:

    #include <memory>

    allocator类分配的内存是原始未构造的。

    类似vector,allocator也是一个模板,需要我们提供类型参数以便allocator类来分配相应类型的内存。allocator类会根据类型参数自动的确定内存的对齐方式。

    allocator类的使用示例如下:

    allocator<int> ai;        //实例化allocator类对象
    int *p = ai.allocate(5);  //使用类对象的方法来申请动态内存
    int *q = p;
    
    ai.construct(p, 6);       //对申请内存中第一个元素进行构造
    cout << *p << endl;       //输出被构造的对象的值
    ++q;                      //移动到第二个未构造的元素
    cout << *q++ << endl;     //未定义的行为,该内存处无对象

    练习12.1:在此代码的结尾,b1 和 b2 各包含多少个元素?

    StrBlob b1;
    {
        StrBlob b2 = {"a", "an", "the"};
        b1 = b2;
        b2.push_back("about");
    }

    b2被销毁,b1包含4个元素。

    练习12.2:编写你自己的StrBlob 类,包含const 版本的 front 和 back。

    const string& StrBlob::front() const
    {
        check(0, "front on empty StrBlob");
        return data->front();
    }
    
    const string& StrBlob::back() const
    
    {    check(0, "back on empty StrBlob");
        return data->front();
    }

    练习12.3:StrBlob 需要const 版本的push_back 和 pop_back吗?如果需要,添加进去。否则,解释为什么不需要。

    不需要,因为push_back和poo_back是写操作,不能使用const。

    练习12.4:在我们的 check 函数中,没有检查 i 是否大于0。为什么可以忽略这个检查?

    因为size_type是无符号类型,一定大于等于0。

    练习12.5:我们未编写接受一个 initializer_list explicit 参数的构造函数。讨论这个设计策略的优点和缺点。

    练习12.6:编写函数,返回一个动态分配的 int 的vector。将此vector 传递给另一个函数,这个函数读取标准输入,将读入的值保存在 vector 元素中。再将vector传递给另一个函数,打印读入的值。记得在恰当的时刻delete vector。

    #include <iostream>
    #include <vector>
    
    std::vector<int>* get_vector()
    {
        auto p = new std::vector<int>;
        return p;
    }
    
    void use_vector(std::istream &in, std::vector<int> *p)
    {
        int i;
        while (in >> i)
        {
            p->push_back(i);
        }
    }
    
    void print_vector(std::vector<int> *p)
    {
        for (auto b = p->begin(), e = p->end(); b != e; ++b)
        {
            std::cout << *b << '	';
        }
    }
    
    int main()
    {
        auto p = get_vector();
        use_vector(std::cin, p);
        print_vector(p);
        delete p;
        std::cout << std::endl;
        return 0;
    }

    练习12.7:重做上一题,这次使用 shared_ptr 而不是内置指针。

    #include <iostream>
    #include <memory>
    #include <vector>
    
    std::shared_ptr<std::vector<int>> get_vector()
    {
        return std::make_shared<std::vector<int>>();
    }
    
    void use_vector(std::istream &in, std::shared_ptr<std::vector<int>> p)
    {
        int i;
        while (in >> i)
        {
            p->push_back(i);
        }
    }
    
    void print_vector(std::shared_ptr<std::vector<int>> p)
    {
        for (auto b = p->begin(), e = p->end(); b != e; ++b)
        {
            std::cout << *b << '	';
        }
    }
    
    int main()
    {
        auto p = get_vector();
        use_vector(std::cin, p);
        print_vector(p);
        std::cout << std::endl;
        return 0;
    }

    练习12.8:下面的函数是否有错误?如果有,解释错误原因。

    bool b() 
    {
        int* p = new int;
        // ...
        return p;
    }

    有错误,函数返回值是bool,指针无法转换到bool

    练习12.9:解释下面代码执行的结果。

    int *q = new int(42), *r = new int(100);
    r = q;
    auto q2 = make_shared<int>(42), r2 = make_shared<int>(100);
    r2 = q2;

    q赋值给r,r所指的内存没有释放导致内存泄露。q2赋值给r2,r2的引用计数减1后为0,自动销毁。

    练习12.10:下面的代码调用了第413页中定义的process 函数,解释此调用是否正确。如果不正确,应如何修改?

    shared_ptr<int> p(new int(42));
    process(shared_ptr<int>(p));

    正确。用p初始化了一个临时的智能指针,该操作递增了引用计数,当临时对象被销毁时,智能指针的计数恢复但不为0。

    练习12.11:如果我们像下面这样调用 process,会发生什么?

    process(shared_ptr<int>(p.get()));
    将会导致智能指针所指的内存被释放2次。

    练习12.12:p 和 q 的定义如下,对于接下来的对 process 的每个调用,如果合法,解释它做了什么,如果不合法,解释错误原因:

    auto p = new int();
    auto sp = make_shared<int>();
    (a) process(sp);
    (b) process(new int());
    (c) process(p);
    (d) process(shared_ptr<int>(p));

    (a)正确
    (b)错误,不允许隐式转换
    (c)错误,类型不匹配
    (d)错误,可能会导致p所指内存被释放2次


    练习12.13:如果执行下面的代码,会发生什么?

    auto sp = make_shared<int>();
    auto p = sp.get();
    delete p;

    sp所指内存被释放2次

    练习12.14:编写你自己版本的用 shared_ptr 管理 connection 的函数。

    void end_connection(connection *p)
    {
        disconnect(*p);
    }
    
    void f(destination &d /* other parameters */)
    {
        connection c = connect(&d);
        shared_ptr<connection> p(&c, end_connection);
    }

    练习12.15:重写第一题的程序,用 lambda (参见10.3.2节,第346页)代替end_connection 函数。

    void f(destination &d /* other parameters */)
    {
        connection c = connect(&d);
        shared_ptr<connection> p(&c, [](connection * p)
        {
            disconnect (*p);
        });
    }

    练习12.16:如果你试图拷贝或赋值 unique_ptr,编译器并不总是能给出易于理解的错误信息。编写包含这种错误的程序,观察编译器如何诊断这种错误。

    note: 'unique_ptr' has been explicitly marked deleted here
    unique_ptr(const unique_ptr&) = delete;

    练习12.17:下面的 unique_ptr 声明中,哪些是合法的,哪些可能导致后续的程序错误?解释每个错误的问题在哪里。

    int ix = 1024, *pi = &ix, *pi2 = new int(2048);
    typedef unique_ptr<int> IntP;
    (a) IntP p0(ix);
    (b) IntP p1(pi);
    (c) IntP p2(pi2);
    (d) IntP p3(&ix);
    (e) IntP p4(new int(2048));
    (f) IntP p5(p2.get());

    (a)错误,不能用int来初始化unique_ptr
    (b)错误,unique_ptr必须使用动态分配的内存来初始化
    (c)正确。
    (d)错误,unique_ptr必须使用动态分配的内存来初始化
    (e)正确
    (f)错误,多个unique_ptr指向同一块内存。


    练习12.18:shared_ptr 为什么没有 release 成员?
    release 成员的作用是放弃对指针控制权,一个对象只能被一个unique_ptr所拥有,而shared_ptr则是多个指向同一个对象,因此不需要 release 成员。

    练习12.19:定义你自己版本的 StrBlobPtr,更新 StrBlob 类,加入恰当的 friend 声明以及 begin 和 end 成员。

    #include <iostream>
    #include <vector>
    #include <memory>
    
    using namespace std;
    
    class StrBlob;
    
    class StrBlobPtr
    {
    public:
        StrBlobPtr();
        StrBlobPtr(StrBlob &a, size_t sz);
        std::string& deref() const;
        StrBlobPtr& incr();
    
    private:
        shared_ptr<vector<string>> check(std::size_t, const std::string&) const;
        std::weak_ptr<std::vector<std::string>> wptr;
        std::size_t curr;
    };
    
    StrBlobPtr::StrBlobPtr(): curr(0)
    {
    
    }
    
    shared_ptr<vector<string>> StrBlobPtr::check(std::size_t i, const std::string &msg) const
    {
        auto ret = wptr.lock();
        if (!ret)
        {
            throw std::runtime_error("unbound StrBlobPtr");
        }
        if (i >= ret->size())
        {
            throw std::out_of_range(msg);
        }
        return ret;
    }
    
    std::string& StrBlobPtr::deref() const
    {
        auto p = check(curr, "dereference past end");
        return (*p)[curr];
    }
    
    StrBlobPtr& StrBlobPtr::incr()
    {
        check(curr, "increment past end of StrBlobPtr");
        ++curr;
        return *this;
    }
    
    class StrBlob
    {
        friend class StrBlobPtr;
    public:
        typedef std::vector<std::string>::size_type size_type;
        StrBlob();
        StrBlob(std::initializer_list<std::string> il);
        StrBlobPtr begin();
        StrBlobPtr end();
        size_type size() const
        {
            return data->size();
        }
        bool empty() const
        {
            return data->empty();
        }
        void push_back(const std::string &t)
        {
            data->push_back(t);
        }
        void pop_back();
        std::string& front();
        std::string& back();
    
    private:
        std::shared_ptr<std::vector<std::string>> data;
        void check(size_type i, const std::string &msg) const;
    };
    
    
    StrBlobPtr::StrBlobPtr(StrBlob &a, size_t sz = 0): wptr(a.data), curr(sz)
    {
    
    }
    
    StrBlobPtr StrBlob::begin()
    {
        return StrBlobPtr(*this);
    }
    
    StrBlobPtr StrBlob::end()
    {
        auto ret = StrBlobPtr(*this, data->size());
        return ret;
    }
    
    
    int main()
    {
    
        std::cout << std::endl;
        return 0;
    }


    练习12.20:编写程序,逐行读入一个输入文件,将内容存入一个 StrBlob 中,用一个 StrBlobPtr 打印出 StrBlob 中的每个元素。


    练习12.21:也可以这样编写 StrBlobPtr 的 deref 成员:

    std::string& deref() const {
    return (*check(curr, "dereference past end"))[curr];
    }
    你认为哪个版本更好?为什么?

    原版更易读。

    练习12.22:为了能让 StrBlobPtr 使用 const StrBlob,你觉得应该如何修改?定义一个名为ConstStrBlobPtr 的类,使其能够指向 const StrBlob。

    重载构造函数接受const Strblob &形参, 然后给 Strblob 类添加两个 const 成员函数 cbegin 和 cend,返回 ConstStrBlobPtr。

    练习12.23:编写一个程序,连接两个字符串字面常量,将结果保存在一个动态分配的char数组中。重写这个程序,连接两个标准库string对象。

    #include <iostream>
    #include <cstring>
    
    using namespace std;
    
    int main()
    {
        char *p = new char[strlen("hello world") + 1];
        strcat(p, "hello ");
        strcat(p, "world");
        cout << p << endl;
        delete [] p;
    
        string s1("hello "), s2("world");
        cout << s1 + s2 << endl;
        return 0;
    }


    练习12.24:编写一个程序,从标准输入读取一个字符串,存入一个动态分配的字符数组中。描述你的程序如何处理变长输入。测试你的程序,输入一个超出你分配的数组长度的字符串。

    #include <iostream>
    
    int main()
    {
        char *p = new char[64];
        std::cout << "input: ";
        std::string s;
        std::cin >> s;
        if (s.size() < 64)
        {
            for (int i = 0; i < 64; ++i)
            {
                p[i] = s[i];
            }
        }
    
        delete [] p;
    
        return 0;
    }


    练习12.25:给定下面的new表达式,你应该如何释放pa?

    int *pa = new int[10];
    delete [] pa;
  • 相关阅读:
    快速幂
    某年元宵节大礼包 矩阵快速幂
    HDU 3303 Harmony Forever 前缀和+树状数组||线段树
    HDU 4325 Flowers 树状数组+离散化
    11、【设计模式】构建器模式
    【基础】数据类型
    【Mybatis】Mybatis缓存
    【FTP】FTP(文件传输协议)工作原理(SFTP)
    Docker是什么
    【RabbitMQ】使用RabbitMQ实现延迟任务
  • 原文地址:https://www.cnblogs.com/pluse/p/5792144.html
Copyright © 2020-2023  润新知