【C++ Primer】第12章 动态内存 (2)


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[]> uu 可以指向动态分配的数组,其元素类型为 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;
}

【C++ primer】目录

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值