Part II: The C++ Library
Chapter 12. Dynamic Memory
12.2 动态数组
new 和 delete 运算符一次分配一个对象。有些程序需要一次为多个对象分配空间的功能。
为支持上述用法,C++语言与库提供两种一次分配对象数组的方式。
C++语言定义了第二种 new
表达式,分配并初始化对象数组。
库包含一个名为 allocator
的模板类,允许将分配与初始化分离。使用 allocator 一般提供更好的性能与更灵活的内存管理。
实践:大多数应用程序应该使用库容器,而不是动态分配到数组。使用容器更简单,更不容易出现内存管理的错误,可能提供更好的性能。
使用容器的类可以使用默认版本的复制、赋值与析构操作。分配动态数组的类必须定义自己版本的操作,当对象被复制、赋值或销毁时管理相关联的内存。
new 与数组
要求 new 分配对象数组的方式:在类型名字后的方括号内指定要分配对象的数目。
这样,new 分配指定数目的对象,(假定分配成功) 返回指向第一个对象的指针。
// call get_size to determine how many ints to allocate
int *pia = new int[get_size()]; // pia points to the first of these ints
通过使用类型别名表示数组类型,也可以分配一个数组
typedef int arrT[42]; // arrT names the type array of 42 ints
int *p = new arrT; // allocates an array of 42 ints; p points to the first one
即使上面代码没有方括号,编译器仍是使用 new[]
执行这个表达式。
int *p = new int[42];
分配一个数组生成一个指向元素类型的指针
虽然通常将由 new T[]
分配的内存称为“动态数组”,但这个说法有一定误导性。
当使用 new 分配数组时,并没有获得一个数组类型的对象,而是获得一个指向数组元素类型的指针。
因为动态数组不具备数组类型的特征,所以不能在动态数组上调用 begin 或 end。也不能使用范围 for 处理动态数组中的元素。
初始化动态分配对象的数组
默认情况下,new 分配的对象被默认初始化。在数组大小后面加上一对空括号,可以值初始化数组中的元素。
int *pia = new int[10]; // block of ten uninitialized ints
int *pia2 = new int[10](); // block of ten ints value initialized to 0
string *psa = new string[10]; // block of ten empty strings
string *psa2 = new string[10](); // block of ten empty strings
在C++11标准下,也可以提供元素初始值的花括号列表。
// block of ten ints each initialized from the corresponding initializer
int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
// block of ten strings; the first four are initialized from the given initializers
// remaining elements are value initialized
string *psa3 = new string[10]{"a", "an", "the", string(3,'x')};
虽然可以使用空括号对数组的元素进行值初始化,但不能在圆括号内提供一个元素初始值。这一事实意味着不能使用 auto 来分配数组。
动态分配一个空数组是合法的
可以使用任意表达式来确定要分配对象的数目。
size_t n = get_size(); // get_size returns the number of elements needed
int* p = new int[n]; // allocate an array to hold the elements
for (int* q = p; q != p + n; ++q)
/* process the array */ ;
调用 new[n] 时 n 为 0 是合法的,虽然不能创建大小为 0 的数组变量。
char arr[0]; // error: cannot define a zero-length array
char *cp = new char[0]; // ok: but cp can't be dereferenced
当使用 new 分配一个大小为 0 的数组,new 返回一个有效的非零指针。这个指针的行为如同数组的尾后指针。可以像使用尾后迭代器那样使用该指针:可以进行比较,在指针上加/减 0,从该指针减去本身得到 0。这个指针不能解引用。
释放动态数组
为了释放动态数组,使用 delete 一种特殊形式:包含一对空的方括号。
delete p; // p must point to a dynamically allocated object or be null
delete [] pa; // pa must point to a dynamically allocated array or be null
第二条语句销毁 pa 指向的数组中的元素,并释放对应的内存。数组中元素以逆序销毁。即先销毁最后一个元素。
当使用类型别名定义数组类型时,分配数组时使用 new 不需要 []。即使如此,当删除指向该数组的指针时,必须使用方括号。
typedef int arrT[42]; // arrT names the type array of 42 ints
int *p = new arrT; // allocates an array of 42 ints; p points to the first one
delete [] p; // brackets are necessary because we allocated an array
警告:如果在 delete 指向数组的指针时忘记方括号,或者在 delete 指向对象的指针时使用了方括号,编译器很可能不会给出警告。然而,程序在执行过程中很容易出现行为异常而不发出警告。
智能指针与动态数组
库提供了一个 unique_ptr 版本,可以管理 new 分配的数组。
// up points to an array of ten uninitialized ints
unique_ptr<int[]> up(new int[10]);
up.release(); // automatically uses delete[] to destroy its pointer
因为 up 指向数组,当 up 销毁它管理的指针时,它自动使用 delete[]。
当 unique_ptr 指向数组时,不能使用点和箭头成员访问运算符,其他 unique_ptr 操作不变。
可以使用下标运算符访问数组中的元素。
for (size_t i = 0; i != 10; ++i)
up[i] = i; // assign a new value to each of the elements
表12.6 指向数组的 unique_ptr
操作 | 说明 |
---|---|
unique_ptr<T[]> u | u 可以指向动态分配的数组,其元素类型为 T。 |
unique_ptr<T[]> u(p) | u 指向动态分配的数组,内置指针 p 指向该数组。p 必须能够转换为 T*。 |
u[i] | 返回 u 拥有的数组中位置 i 上的对象。u 必须指向一个数组。 |
shared_ptr 没有为管理动态数组提供直接支持。如果想要使用 shared_ptr 管理动态数组,必须提供自己的删除器。
// to use a shared_ptr we must supply a deleter
shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; });
sp.reset(); // uses the lambda we supplied that uses delete[] to free the array
shared_ptr 不支持下标运算符。为了访问数组中的元素,必须使用 get 获取内置指针,使用它来访问元素。
// shared_ptrs don't have subscript operator and don't support pointer arithmetic
for (size_t i = 0; i != 10; ++i)
*(sp.get() + i) = i; // use get to get a built-in pointer
allocator 类
一般情况下,将分配与构造关联一起会造成浪费。
string *const p = new string[n]; // construct n empty strings
string s;
string *q = p; // q points to the first string
while (cin >> s && q != p + n)
*q++ = s; // assign a new value to *q
const size_t size = q - p; // remember how many strings we read
// use the array
delete[] p; // p points to an array; must remember to use delete[]
使用的元素被写入两次:第一次是元素默认初始化时,接着是赋值时。
更重要的是,没有默认构造函数的类不能动态分配成数组。
allocator 类
库 allocator
类定义在 memory 头文件中,允许将分配与构造分离开来。
它提供类型感知分配,分配的内存是原始的、未构造的。
表12.7 标准库 allocator 与定制算法
操作 | 说明 |
---|---|
allocator<T> a | 定于一个名为 a 的 allocator 对象,为类型 T 的对象分配内存。 |
a.allocator(n) | 分配原始的、未构造的内存,存储 n 个类型 T 的对象。 |
a.deallocator(p, n) | 释放存储 n 个类型 T 的对象的内存,该内存从 T* 指针 p 开始; p 必须是先前由 allocator 返回的指针,n 必须是创建 p 时要求的大小。 在调用 deallocator 之前,用户必须在该内存中已构造的每个对象上运行 destroy。 |
a.construct(p, args) | p 必须是指向类型 T 的指针,指向原始内存;args 传递给类型 T 的构造函数,用于在 p 指向的内存中构造一个对象。 |
a.destroy(p) | 在 T* 指针 p 指向的对象上运行析构函数。 |
allocator 是一种模板。为了定义一个 allocator,必须指定特定 allocator 可以分配的对象的类型。当 allocator 对象分配内存时,它将分配适当大小和对齐的内存,以容纳给定类型的对象:
allocator<string> alloc; // object that can allocate strings
auto const p = alloc.allocate(n); // allocate n unconstructed strings
allocator 分配未构造的内存
allocator 分配的内存是未构造的。使用内存时在内存中构造对象。
在C++11标准库中,construct
成员接受一个指针与 0 或多个额外实参;在给定位置上构造元素。
auto q = p; // q will point to one past the last constructed element
alloc.construct(q++); // *q is the empty string
alloc.construct(q++, 10, 'c'); // *q is cccccccccc
alloc.construct(q++, "hi"); // *q is hi!
使用未构造对象的原始内存是错误的。
cout << *p << endl; // ok: uses the string output operator
cout << *q << endl; // disaster: q points to unconstructed memory!
当使用对象完毕,必须销毁构造的元素,通过在每个构造的元素上调用 destroy
来实现。
destroy 函数接受一个指针,在指向的对象上运行析构函数。
while (q != p)
alloc.destroy(--q); // free the strings we actually allocated
警告:只能 destroy 真正已构造的元素。
一旦元素被销毁,可以重新使用内存存储其他元素,或将内存还给系统。通过调用 deallocate
释放内存。
alloc.deallocate(p, n);
传递给 deallocate 的指针不能为空;它必须指向 allocate 分配的内存。
复制与填充未初始化内存算法
作为 allocator 类的伴随,库还定义了两种算法,可以在未初始化的内存中构造对象。这些函数定义在 memory 头文件中。
表12.8 allocator 算法
操作 | 说明 |
---|---|
uninitialized_copy(b, e, b2) | 将迭代器 b 和 e 表示的输入范围内的元素,复制到迭代器 b2 表示的未构造的、初始内存。 b2 表示的内存必须足够大以容纳输入范围元素的副本。 |
uninitialized_copy_n(b, n, b2) | 从迭代器 b 表示的元素开始,复制 n 个元素到 b2 开始的内存中。 |
uninitialized_fill(b, e, t) | 在迭代器 b 和 e 表示的原始内存范围中构造对象,对象内容为 t 的副本。 |
uninitialized_fill_n(b, n, t) | 从 b 开始构造 n 个对象。b 必须指向未构造的、原始的内存,且足够容纳给定数目的对象。 |
这些函数是在目的位置上构造元素,而不是赋值给它们。
例如,给定一个 vector<int> 对象 vi,将其中的元素复制到动态数组的前一半中,在后一半中填充给定的值:
// allocate twice as many elements as vi holds
auto p = alloc.allocate(vi.size() * 2);
// construct elements starting at p as copies of elements in vi
auto q = uninitialized_copy(vi.begin(), vi.end(), p);
// initialize the remaining elements to 42
uninitialized_fill_n(q, vi.size(), 42);
与 copy 类似,uninitialized_copy 返回其(递增后的)目的位置迭代器。因此,uninitialized_copy 返回一个指针定位最后构造对象的后一位。
12.3 使用标准库:文本查询程序
程序允许用户在给定文件中搜索单词。查询的结果:单词出现的次数以及所在行的列表。如果一个单词在同一行中出现多次,此行仅显示一次。行将以升序显示。
例如,在给定文件中搜索单词 element,其结果显式类似下面这样:
element occurs 112 times
(line 36) A set element contains only a key;
(line 158) operator creates a new element
(line 160) Regardless of whether the element
(line 168) When we fetch an element from a map, we
(line 214) If the element is not found, find returns
...
查询程序设计
开始程序设计的一个好方式是:列出程序的操作。了解需要的操作可以帮助我们弄清需要的数据结构。从需求开始,此程序的任务必须包含下面这些:
- 当读取输入文件时,程序必须记住每个单词出现的行。因此,程序需要逐行读取输入文件,并将每行内容分解为独立的单词。
- 当生成输出时,
程序必须能够提取给定单词关联的行号;
行号必须以升序出现且无重复;
程序必须能够打印给定行的文本内容。
要满足这些要求,可以通过使用各种标准库设施:
- 使用 vector<string> 存储整个输入文件的副本。输入文件的每行是该 vector 的元素。当想要打印一行时,可以使用行号作为下标提取改行内容。
- 使用 istringstream 将每行内容分解成单词。
- 使用 set 存储每个单词出现的行号。使用 set 可以保证每个行号只出现一次,且以升序存储。
- 使用 map 将每个单词与行号 set 关联。使用 map 就可以提取给定单词的 set。
解决方案还使用了 shared_ptr。
数据结构
首先设计一个名为 TextQuery 的类,保存输入文件内容,令查询更方便。该类包含一个 vector 和一个 map,作用如上所述。类还具有一个读取给定输入文件内容的构造函数,以及执行查询的操作。
查询操作的工作:在 map 中查找给定单词是否出现。如果找到单词,需要获取单词出现的次数、单词出现的行号以及这些行号对应的文本。
定义第二个类 QueryResult,存储查询的结果。该类具有一个 print 函数,打印 QueryResult 的结果。
在类之间分享数据
QueryResult 数据需要存储在 TextQuery 对象中,确定如何访问它们。复制 set 与 vector 耗时耗空间。
返回指向 TextQuery 对象的迭代器(或指针)可以避免复制。但是若 TextQuery 对象被销毁,对应 QueryResult 就不能使用。
因此,QueryResult 的生存期应与 TextQuery 对象同步。这两个类在概念上“分享”数据,可以使用 shared_ptr 来反映这种关系。
使用 TextQuery 类
当设计一个类时,在实现该类的成员之前,编写使用该类的程序是一种有用的方式。这样,我们可以看到该类是否具有我们需要的操作。
例如,下面的程序使用 TextQuery 和 QueryResult 类。此函数接受一个 ifstream,指向将要处理的文件,与用户交互,打印给定单词的结果。
void runQueries(ifstream &infile) {
// infile is an ifstream that is the file we want to query
TextQuery tq(infile); // store the file and build the query map
// iterate with the user: prompt for a word to find and print results
while (true) {
cout << "enter word to look for, or q to quit: ";
string s;
// stop if we hit end-of-file on the input or if a 'q' is entered
if (!(cin >> s) || s == "q") break;
// run the query and print the results
print(cout, tq.query(s)) << endl;
}
}
定义查询程序类
QueryResult 类将共享代表输入文件内容的 vector 和存储行号的 set。因此,对这两种数据使用 shared_ptr。
class QueryResult; // declaration needed for return type in the query function
class TextQuery {
public:
using line_no = std::vector<std::string>::size_type;
TextQuery(std::ifstream&);
QueryResult query(const std::string&) const;
private:
std::shared_ptr<std::vector<std::string>> file; // input file
// map of each word to the set of the lines in which that word appears
std::map<std::string, std::shared_ptr<std::set<line_no>>> wm;
};
TextQuery 构造函数
// read the input file and build the map of lines to line numbers
TextQuery::TextQuery(ifstream &is): file(new vector<string>) {
string text;
while (getline(is, text)) { // for each line in the file
file->push_back(text); // remember this line of text
int n = file->size() - 1; // the current line number
istringstream line(text); // separate the line into words
string word;
while (line >> word) { // for each word in that line
// if word isn't already in wm, subscripting adds a new entry
auto &lines = wm[word]; // lines is a shared_ptr
if (!lines) // that pointer is null the first time we see word
lines.reset(new set<line_no>); // allocate a new set
lines->insert(n); // insert this line number
}
}
}
QueryResult 类
class QueryResult {
friend std::ostream& print(std::ostream&, const QueryResult&);
public:
QueryResult(std::string s, std::shared_ptr<std::set<line_no>> p, std::shared_ptr<std::vector<std::string>> f)
: sought(s), lines(p), file(f) { }
private:
std::string sought; // word this query represents
std::shared_ptr<std::set<line_no>> lines; // lines it's on
std::shared_ptr<std::vector<std::string>> file; // input file
};
query 函数
问题:如果给定的 string 没找到,应该怎么做?在这种情况下,没有 set 返回。
解决办法:定义一个局部 static 对象,它是一个指向空 set 的 shared_ptr。
QueryResult TextQuery::query(const string &sought) const {
// we'll return a pointer to this set if we don't find sought
static shared_ptr<set<line_no>> nodata(new set<line_no>);
// use find and not a subscript to avoid adding words to wm!
auto loc = wm.find(sought);
if (loc == wm.end())
return QueryResult(sought, nodata, file); // not found
else
return QueryResult(sought, loc->second, file);
}
打印结果
ostream &print(ostream & os, const QueryResult &qr) {
// if the word was found, print the count and all occurrences
os << qr.sought << " occurs " << qr.lines->size() << " " << make_plural(qr.lines->size(), "time", "s") << endl;
// print each line in which the word appeared
for (auto num : *qr.lines) // for every element in the set
// don't confound the user with text lines starting at 0
os << "\t(line " << num + 1 << ") " << *(qr.file->begin() + num) << endl;
return os;
}