《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指向它。