《C++ Primer》第12章 12.1节习题答案

《C++ Primer》第12章 动态内存

导读本章介绍了智能指针与动态内存管理,包括:

●智能指针的基本概念,特别是用它来管理动态内存的好处。

●用allocator管理动态数组。

●用文本查询这样一个较大的例子来展示动态内存管理。

本章练习的重点是让读者熟悉智能指针的使用,包括使用shared_ptr和unique_ptr管理动态内存时要注意的一些问题;用allocator管理动态数组;以及基于文本查询例子的一些较大的练习。

12.1节动态内存与智能指针 习题答案

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

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

【出题思路】

理解智能指针的基本特点。

【解答】

由于StrBlob的data成员是一个指向string的vector的shared_ptr,因此StrBlob的赋值不会拷贝vector的内容,而是多个StrBlob对象共享同一个(创建于动态内存空间上)vector对象。代码第3行创建b2时提供了3个string的列表,因此会创建一个包含3个string的vector对象,并创建一个shared_ptr指向此对象(引用计数为1)。第4行将b2赋予b1时,创建一个shared_ptr也指向刚才创建的vector对象,引用计数变为2。因此,第4行向b2添加一个string时,会向两个StrBlob共享的vector中添加此string。最终,在代码结尾,b1和b2均包含4个string。

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

【出题思路】

本题练习智能指针的简单使用。

【解答】

参考书中代码,并补充front和back对const的重载,即可完成自己的StrBlob类:

#ifndef SYSTRBLOB_H
#define SYSTRBLOB_H

#include <vector>
#include <string>
#include <initializer_list>
#include <memory>
#include <stdexcept>

using namespace std;

class StrBlob
{
public:
    typedef vector<string>::size_type size_type;
    StrBlob();
    StrBlob(initializer_list<string> i1);
    size_type size() const { return data->size(); }
    bool empty() const { return data->empty(); }
    //添加和删除元素
    void push_back(const string &t) { data->push_back(t); }
    void pop_back();
    //元素访问
    string& front();
    const string& front() const;
    string& back();
    const string& back() const;

private:
    shared_ptr<std::vector<std::string>> data;
    //如果data[i]不合法,抛出一个异常
    void check(size_type i, const std::string &msg) const;
};

#endif // SYSTRBLOB_H
#include "SYStrBlob.h"

StrBlob::StrBlob():data(make_shared<vector<string>>())
{

}

StrBlob::StrBlob(initializer_list<string> i1):data(make_shared<vector<string>>(i1))
{

}

void StrBlob::check(size_type i, const string &msg) const
{
    if(i >= data->size())
        throw out_of_range(msg);
}

string& StrBlob::front()
{
    //如果vector为空,check会抛出一个异常
    check(0, "front on empty StrBlob");
    return data->front();
}

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

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

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

void StrBlob::pop_back()
{
    check(0, "pop_back on empty StrBlob");
    data->pop_back();
}
#include <iostream>
#include "SYStrBlob.h"

using std::cout;
using std::endl;

int main(int argc, const char * argv[])
{
    StrBlob b1;
    {
        StrBlob b2 = {"a", "an", "the"};
        b1 = b2;
        b2.push_back("about");
        cout << "b2.size==================" << b2.size() << endl;
    }
    cout << "b1.size==================" << b1.size() << endl;
    cout << b1.front() << "  " << b1.back() << endl;

    const StrBlob b3 = b1;
    cout << b3.front() << "  " << b3.back() << endl;
    return 0;
}

运行结果:

 

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

【出题思路】

理解const版本和非const版本的差别。

【解答】

push_back和pop_back的语义分别是向StrBlob对象共享的vector对象添加元素和从其删除元素。因此,我们不应为其重载const版本,因为常量StrBlob对象是不应被允许修改共享vector对象内容的。

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

【出题思路】

理解私有成员函数和公有成员函数的差别。

【解答】

我们将check定义为私有成员函数,亦即,它只会被StrBlob的成员函数调用,而不会被用户程序所调用。因此,我们可以很容易地保证传递给它的i的值符合要求,而不必进行检查。

练习12.5:我们未编写接受一个initializer_list explicit(参见7.5.4节,第264页)参数的构造函数。讨论这个设计策略的优点和缺点。

【出题思路】

复习隐式类类型转换和显式转换的区别。

【解答】

未编写接受一个初始化列表参数的显式构造函数,意味着可以进行列表向StrBlob的隐式类型转换,亦即在需要StrBlob的地方(如函数的参数),可以使用列表进行替代。而且,可以进行拷贝形式的初始化(如赋值)。这令程序编写更为简单方便。

但这种隐式转换并不总是好的。例如,列表中可能并非都是合法的值。再如,对于接受StrBlob的函数,传递给它一个列表,会创建一个临时的StrBlob对象,用列表对其初始化,然后将其传递给函数,当函数完成后,此对象将被丢弃,再也无法访问了。对于这些情况,我们可以定义显式的构造函数,禁止隐式类类型转换。

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

【出题思路】

本题练习用new和delete直接管理内存。

【解答】

直接内存管理的关键是谁分配了内存谁就要记得释放。在此程序中,主函数调用分配函数在动态内存空间中创建int的vector,因此在读入数据、打印数据之后,主函数应负责释放vector对象。

#include <iostream>
#include <vector>
#include <new>

using std::cout;
using std::endl;
using std::vector;
using std::nothrow;
using std::cin;

vector<int> *new_vector(void)
{
    return new (nothrow) vector<int>;
}

void read_ints(vector<int> *pv)
{
    int v;
    while(cin >> v)
    {
        pv->push_back(v);
    }
}

void print_ints(vector<int> *pv)
{
    for(const auto &v: *pv)
        cout << v << " ";
    cout << endl;
}

int main(int argc, const char * argv[])
{

    vector<int> *pv = new_vector();
    if(!pv)
    {
        cout << "内存不足!" << endl;
        return -1;
    }

    read_ints(pv);
    print_ints(pv);
    delete pv;
    pv = nullptr;
    std::cout << "Hello, World!\n";
    return 0;
}

运行结果:

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

【出题思路】

本题练习用智能指针管理内存。

【解答】

与上一题相比,程序差别不大,主要是将vector<int> *类型变为shared_ptr<vector<int>>类型,空间分配不再用new而改用make_shared,在主函数末尾不再需要主动释放内存。最后一点的意义对这个小程序还不明显,但对于大程序非常重要,它省去了程序员释放内存的工作,可以有效避免内存泄漏问题。

#include <iostream>
#include <vector>
#include <memory>

using namespace std;

shared_ptr<vector<int>> new_vector(void)
{
    return make_shared<vector<int>>();
}

void read_ints(shared_ptr<vector<int>> spv)
{
    int v;
    while(cin >> v)
        spv->push_back(v);
}

void print_ints(shared_ptr<vector<int>> spv)
{
    for(const auto &v: *spv)
        cout << v << " ";
    cout << endl;
}

int main(int argc, const char * argv[])
{
    auto spv = new_vector();

    read_ints(spv);
    print_ints(spv);

    return 0;
}

运行结果:

 

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

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

【出题思路】

理解用new分配内存成功和失败的差别,以及复习类型转换。

【解答】

从程序片段看,可以猜测程序员的意图是通过new返回的指针值来区分内存分配成功或失败——成功返回一个合法指针,转换为整型是一个非零值,可转换为bool值true;分配失败,p得到nullptr,其整型值是0,可转换为bool值false。

但普通new调用在分配失败时抛出一个异常bad_alloc,而不是返回nullptr,因此程序不能达到预想的目的。

可将new int改为new (nothrow)int来令new在分配失败时不抛出异常,而是返回nullptr。但这仍然不是一个好方法,应该通过捕获异常或是判断返回的指针来返回true或false,而不是依赖类型转换。

练习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;

【出题思路】

理解直接管理内存和智能指针的差别。

【解答】

这段代码非常好地展示了智能指针在管理内存上的优点。

对于普通指针部分,首先分配了两个int型对象,指针分别保存在p和r中。接下来,将指针q的值赋予了r,这带来了两个非常严重的内存管理问题:

1.首先是一个直接的内存泄漏问题,r和q一样都指向42的内存地址,而r中原来保存的地址——100的内存再无指针管理,变成“孤儿内存”,从而造成内存泄漏。

2.其次是一个“空悬指针”问题。由于r和q指向同一个动态对象,如果程序编写不当,很容易产生释放了其中一个指针,而继续使用另一个指针的问题。继续使用的指针指向的是一块已经释放的内存,是一个空悬指针,继续读写它指向的内存可能导致程序崩溃甚至系统崩溃的严重问题。

而shared_ptr则可很好地解决这些问题。首先,分配了两个共享的对象,分别由共享指针p2和q2指向,因此它们的引用计数均为1。接下来,将q2赋予r2。赋值操作会将q2指向的对象地址赋予r2,并将r2原来指向的对象的引用计数减1,将q2指向的对象的引用计数加1。这样,前者的引用计数变为0,其占用的内存空间会被释放,不会造成内存泄漏。而后者的引用计数变为2,也不会因为r2和q2之一的销毁而释放它的内存空间,因此也不会造成空悬指针的问题。

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

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

【出题思路】

理解智能指针的使用。

【解答】

此调用是正确的,利用p创建一个临时的shared_ptr赋予process的参数ptr,p和ptr都指向相同的int对象,引用计数被正确地置为2。process执行完毕后,ptr被销毁,引用计数减1,这是正确的——只有p指向它。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值