第十二章 动态内存
练习12.1
在此代码的结尾,
b1和b2各包含多少个元素?
StrBlob b1;{StrBlob b2 = {"a", "an", "the"};b1 = b2;b2.push_back("about");}
解:
它们实际操作的是同一个vector,都包含4个元素。在代码的结尾,b2 被析构了,不影响 b1 的元素。
练习12.2
编写你自己的
StrBlob类,包含const版本的front和back。
解:
头文件:
#include <vector>#include <string>#include <initializer_list>#include <memory>#include <exception>using std::vector; using std::string;class StrBlob {public:using size_type = vector<string>::size_type;StrBlob():data(std::make_shared<vector<string>>()) { }StrBlob(std::initializer_list<string> il):data(std::make_shared<vector<string>>(il)) { }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() {check(0, "pop_back on empty StrBlob");data->pop_back();}std::string& front() {check(0, "front on empty StrBlob");return data->front();}std::string& back() {check(0, "back on empty StrBlob");return data->back();}const std::string& front() const {check(0, "front on empty StrBlob");return data->front();}const std::string& back() const {check(0, "back on empty StrBlob");return data->back();}private:void check(size_type i, const string &msg) const {if (i >= data->size()) throw std::out_of_range(msg);}private:std::shared_ptr<vector<string>> data;};
主函数:
#include "ex12_02.h"#include <iostream>int main(){const StrBlob csb{ "hello", "world", "pezy" };StrBlob sb{ "hello", "world", "Mooophy" };std::cout << csb.front() << " " << csb.back() << std::endl;sb.back() = "pezy";std::cout << sb.front() << " " << sb.back() << std::endl;}
练习12.3
StrBlob需要const版本的push_back和pop_back吗?如需要,添加进去。否则,解释为什么不需要。
解:
不需要。push_back 和 pop_back 会改变对象的内容。而 const 对象是只读的,因此不需要。
练习12.4
在我们的
check函数中,没有检查i是否大于0。为什么可以忽略这个检查?
解:
因为 size_type 是一个无符号整型,当传递给 check 的参数小于 0 的时候,参数值会转换成一个正整数。
练习12.5
我们未编写接受一个
initializer_list explicit参数的构造函数。讨论这个设计策略的优点和缺点。
解:
构造函数不是 explicit 的,意味着可以从 initializer_list 隐式转换为 StrBlob。在 StrBlob 对象中,只有一个数据成员 data,而 StrBlob 对象本身的含义,也是一个管理字符串的序列。因此,从 initializer_list 到 StrBlob 的转换,在逻辑上是可行的。而这个设计策略的缺点,可能在某些地方我们确实需要 initializer_list,而编译器仍会将之转换为 StrBlob。
练习12.6
编写函数,返回一个动态分配的
int的vector。将此vector传递给另一个函数,这个函数读取标准输入,将读入的值保存在vector元素中。再将vector传递给另一个函数,打印读入的值。记得在恰当的时刻delete vector。
解:
#include <iostream>#include <vector>using std::vector;vector<int>* alloc_vector(){return new vector<int>();}void assign_vector(vector<int>* p){int i;while (std::cin >> i){p->push_back(i);}}void print_vector(vector<int>* p){for (auto i : *p){std::cout << i << std::endl;}}int main(){auto p = alloc_vector();assign_vector(p);print_vector(p);delete p;return 0;}
练习12.7
重做上一题,这次使用
shared_ptr而不是内置指针。
解:
#include <iostream>#include <vector>#include <memory>using std::vector;std::shared_ptr<vector<int>> alloc_vector(){return std::make_shared<vector<int>>();}void assign_vector(std::shared_ptr<vector<int>> p){int i;while (std::cin >> i){p->push_back(i);}}void print_vector(std::shared_ptr<vector<int>> p){for (auto i : *p){std::cout << i << std::endl;}}int main(){auto p = alloc_vector();assign_vector(p);print_vector(p);return 0;}
练习12.8
下面的函数是否有错误?如果有,解释错误原因。
bool b() {int* p = new int;// ...return p;}
解:
有错误。p会被强制转换成bool,继而没有释放指针 p 指向的对象。
练习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;
解:
r 和 q 指向 42,而之前 r 指向的 100 的内存空间并没有被释放,因此会发生内存泄漏。r2 和 q2 都是智能指针,当对象空间不被引用的时候会自动释放。
练习12.10
下面的代码调用了第413页中定义的
process函数,解释此调用是否正确。如果不正确,应如何修改?
shared_ptr<int> p(new int(42));process(shared_ptr<int>(p));
解:
正确。shared_ptr<int>(p) 会创建一个临时的智能指针,这个智能指针与 p 引用同一个对象,此时引用计数为 2。当表达式结束时,临时的智能指针被销毁,此时引用计数为 1。
练习12.11
如果我们像下面这样调用
process,会发生什么?
process(shared_ptr<int>(p.get()));
解:
这样会创建一个新的智能指针,它的引用计数为 1,这个智能指针所指向的空间与 p 相同。在表达式结束后,这个临时智能指针会被销毁,引用计数为 0,所指向的内存空间也会被释放。而导致 p 所指向的空间被释放,使得 p` 成为一个空悬指针。
练习12.12
p和sp的定义如下,对于接下来的对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) 合法。将
sp拷贝给process函数的形参,在函数里面引用计数为 2,函数结束后引用计数为 1。 - (b) 不合法。不能从内置指针隐式转换为智能指针。
- (c) 不合法。不能从内置指针隐式转换为智能指针。
- (d) 合法。但是智能指针和内置指针一起使用可能会出现问题,在表达式结束后智能指针会被销毁,它所指向的对象也被释放。而此时内置指针
p依旧指向该内存空间。之后对内置指针p的操作可能会引发错误。
练习12.13
如果执行下面的代码,会发生什么?
auto sp = make_shared<int>();auto p = sp.get();delete p;
解:
智能指针 sp 所指向空间已经被释放,再对 sp 进行操作会出现错误。
练习12.14
编写你自己版本的用
shared_ptr管理connection的函数。
解:
#include <iostream>#include <memory>#include <string>struct connection{std::string ip;int port;connection(std::string i, int p) : ip(i), port(p) {}};struct destination{std::string ip;int port;destination(std::string i, int p) : ip(i), port(p) {}};connection connect(destination* pDest){std::shared_ptr<connection> pConn(new connection(pDest->ip, pDest->port));std::cout << "creating connection(" << pConn.use_count() << ")" << std::endl;return *pConn;}void disconnect(connection pConn){std::cout << "connection close(" << pConn.ip << ":" << pConn.port << ")" << std::endl;}void end_connection(connection* pConn){disconnect(*pConn);}void f(destination &d){connection conn = connect(&d);std::shared_ptr<connection> p(&conn, end_connection);std::cout << "connecting now(" << p.use_count() << ")" << std::endl;}int main(){destination dest("220.181.111.111", 10086);f(dest);return 0;}
练习12.15
重写上一题的程序,用
lambda代替end_connection函数。
解:
#include <iostream>#include <memory>#include <string>struct connection{std::string ip;int port;connection(std::string i, int p) : ip(i), port(p) {}};struct destination{std::string ip;int port;destination(std::string i, int p) : ip(i), port(p) {}};connection connect(destination* pDest){std::shared_ptr<connection> pConn(new connection(pDest->ip, pDest->port));std::cout << "creating connection(" << pConn.use_count() << ")" << std::endl;return *pConn;}void disconnect(connection pConn){std::cout << "connection close(" << pConn.ip << ":" << pConn.port << ")" << std::endl;}void f(destination &d){connection conn = connect(&d);std::shared_ptr<connection> p(&conn, [] (connection* p){ disconnect(*p); });std::cout << "connecting now(" << p.use_count() << ")" << std::endl;}int main(){destination dest("220.181.111.111", 10086);f(dest);return 0;}
练习12.16
如果你试图拷贝或赋值
unique_ptr,编译器并不总是能给出易于理解的错误信息。编写包含这种错误的程序,观察编译器如何诊断这种错误。
解:
#include <iostream>#include <string>#include <memory>using std::string; using std::unique_ptr;int main(){unique_ptr<string> p1(new string("pezy"));// unique_ptr<string> p2(p1); // copy// ^// Error: Call to implicitly-deleted copy constructor of 'unique_ptr<string>'//// unique_ptr<string> p3 = p1; // assign// ^// Error: Call to implicitly-deleted copy constructor of 'unique_ptr<string>'std::cout << *p1 << std::endl;p1.reset(nullptr);}
练习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) 不合法。在定义一个
unique_ptr时,需要将其绑定到一个new返回的指针上。 - (b) 不合法。理由同上。
- (c) 合法。但是也可能会使得
pi2成为空悬指针。 - (d) 不合法。当
p3被销毁时,它试图释放一个栈空间的对象。 - (e) 合法。
- (f) 不合法。
p5和p2指向同一个对象,当p5和p2被销毁时,会使得同一个指针被释放两次。
练习12.18
shared_ptr为什么没有release成员?
release 成员的作用是放弃控制权并返回指针,因为在某一时刻只能有一个 unique_ptr 指向某个对象,unique_ptr 不能被赋值,所以要使用 release 成员将一个 unique_ptr 的指针的所有权传递给另一个 unique_ptr。而 shared_ptr 允许有多个 shared_ptr 指向同一个对象,因此不需要 release 成员。
练习12.19
定义你自己版本的
StrBlobPtr,更新StrBlob类,加入恰当的friend声明以及begin和end成员。
解:
#include <string>#include <vector>#include <initializer_list>#include <memory>#include <stdexcept>using std::vector; using std::string;class StrBlobPtr;class StrBlob{public:using size_type = vector<string>::size_type;friend class StrBlobPtr;StrBlobPtr begin();StrBlobPtr end();StrBlob() : data(std::make_shared<vector<string>>()) {}StrBlob(std::initializer_list<string> il) : data(std::make_shared<vector<string>>(il)) {}size_type size() const { return data->size(); }bool empty() const { return data->empty(); }void push_back(const string& s) { data->push_back(s); }void pop_back(){check(0, "pop_back on empty StrBlob");data->pop_back();}std::string& front(){check(0, "front on empty StrBlob");return data->front();}std::string& back(){check(0, "back on empty StrBlob");return data->back();}const std::string& front() const{check(0, "front on empty StrBlob");return data->front();}const std::string& back() const{check(0, "back on empty StrBlob");return data->back();}private:void check(size_type i, const string& msg) const{if (i >= data->size())throw std::out_of_range(msg);}private:std::shared_ptr<vector<string>> data;};class StrBlobPtr{public:StrBlobPtr() :curr(0) {}StrBlobPtr(StrBlob &a, size_t sz = 0) :wptr(a.data), curr(sz) {}bool operator!=(const StrBlobPtr& p) { return p.curr != curr; }string& deref() const{auto p = check(curr, "dereference past end");return (*p)[curr];}StrBlobPtr& incr(){check(curr, "increment past end of StrBlobPtr");++curr;return *this;}private:std::shared_ptr<vector<string>> check(size_t i, const 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::weak_ptr<vector<string>> wptr;size_t curr;};StrBlobPtr StrBlob::begin(){return StrBlobPtr(*this);}StrBlobPtr StrBlob::end(){return StrBlobPtr(*this, data->size());}
练习12.20
编写程序,逐行读入一个输入文件,将内容存入一个
StrBlob中,用一个StrBlobPtr打印出StrBlob中的每个元素。
解:
#include <iostream>#include <fstream>#include "exercise12_19.h"using namespace std;int main(){ifstream ifs("books.txt");StrBlob sb;string s;while (getline(ifs, s)){sb.push_back(s);}for (StrBlobPtr sbp = sb.begin(); sbp != sb.end(); sbp.incr()){cout << sbp.deref() << endl;}return 0;}
练习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 <string>#include <cstring>#include <memory>int main() {const char *c1 = "Hello ";const char *c2 = "World";unsigned len = strlen(c1) + strlen(c2) + 1;char *r = new char[len]();strcat_s(r, len, c1);strcat_s(r, len, c2);std::cout << r << std::endl;std::string s1 = "Hello ";std::string s2 = "World";strcpy_s(r, len, (s1 + s2).c_str());std::cout << r << std::endl;delete[] r;return 0;}
练习12.24
编写一个程序,从标准输入读取一个字符串,存入一个动态分配的字符数组中。描述你的程序如何处理变长输入。测试你的程序,输入一个超出你分配的数组长度的字符串。
解:
#include <iostream>int main(){std::cout << "How long do you want the string? ";int size{ 0 };std::cin >> size;char *input = new char[size + 1]();std::cin.ignore();std::cout << "input the string: ";std::cin.get(input, size + 1);std::cout << input;delete[] input;return 0;}
练习12.25
给定下面的
new表达式,你应该如何释放pa?
int *pa = new int[10];
解:
delete [] pa;
练习12.26
用
allocator重写第427页中的程序。
#include <iostream>#include <string>#include <memory>using namespace std;int main(){int n = 5;allocator<string> alloc;auto p = alloc.allocate(n);string s;auto q = p;while (cin >> s && q != p + n){alloc.construct(q++, s);}while (q != p){std::cout << *--q << " ";alloc.destroy(q);}alloc.deallocate(p, n);return 0;}
练习12.27
TextQuery和QueryResult类只使用了我们已经介绍过的语言和标准库特性。不要提前看后续章节内容,只用已经学到的知识对这两个类编写你自己的版本。
解:
头文件:
#ifndef EX12_27_H#define EX12_27_H#include <fstream>#include <memory>#include <vector>#include <string>#include <map>#include <set>class QueryResult;class TextQuery{public:using line_no = std::vector<std::string>::size_type;TextQuery(std::ifstream&);QueryResult query(const std::string& s) const;private:std::shared_ptr<std::vector<std::string>> file;std::map<std::string, std::shared_ptr<std::set<line_no>>> wm;};class QueryResult{public:friend std::ostream& print(std::ostream&, const QueryResult&);QueryResult(std::string s,std::shared_ptr<std::set<TextQuery::line_no>> p,std::shared_ptr<std::vector<std::string>> f) :sought(s), lines(p), file(f){}private:std::string sought;std::shared_ptr<std::set<TextQuery::line_no>> lines;std::shared_ptr<std::vector<std::string>> file;};std::ostream& print(std::ostream&, const QueryResult&);#endif
实现:
#include "ex_12_27.h"#include <sstream>#include <fstream>#include <vector>#include <string>using namespace std;TextQuery::TextQuery(ifstream& ifs) : file(new vector<string>){string text;while (getline(ifs, text)){file->push_back(text);int n = file->size() - 1;istringstream line(text);string word;while (line >> word){auto &lines = wm[word];if (!lines)lines.reset(new set<line_no>);lines->insert(n);}}}QueryResult TextQuery::query(const string& s) const{static shared_ptr<set<line_no>> nodata(new set<line_no>);auto loc = wm.find(s);if (loc == wm.end())return QueryResult(s, nodata, file);elsereturn QueryResult(s, loc->second, file);}std::ostream& print(std::ostream& os, const QueryResult& qr){os << qr.sought << " occurs " << qr.lines->size() << " "<< "time" << (qr.lines->size() > 1 ? "s" : "") << endl;for (auto num : *qr.lines)os << "\t(line " << num + 1 << ") " << *(qr.file->begin() + num) << endl;return os;}
主函数:
#include <iostream>#include <string>#include <fstream>#include "ex_12_27.h"using namespace std;void runQueries(ifstream& infile){TextQuery tq(infile);while (true){cout << "enter word to look for, or q to quit: ";string s;if (!(cin >> s) || s == "q") break;print(cout, tq.query(s)) << endl;}}int main(){ifstream ifs("storyDataFile.txt");runQueries(ifs);return 0;}
练习12.28
编写程序实现文本查询,不要定义类来管理数据。你的程序应该接受一个文件,并与用户交互来查询单词。使用
vector、map和set容器来保存来自文件的数据并生成查询结果。
解:
#include <string>using std::string;#include <vector>using std::vector;#include <memory>using std::shared_ptr;#include <iostream>#include <fstream>#include <sstream>#include <map>#include <set>#include <algorithm>int main(){std::ifstream file("H:/code/C++/Cpp_Primer_Answers/data/storyDataFile.txt");vector<string> input;std::map<string, std::set<decltype(input.size())>> dictionary;decltype(input.size()) lineNo{ 0 };for (string line; std::getline(file, line); ++lineNo){input.push_back(line);std::istringstream line_stream(line);for (string text, word; line_stream >> text; word.clear()){std::remove_copy_if(text.begin(), text.end(), std::back_inserter(word), ispunct);dictionary[word].insert(lineNo);}}while (true){std::cout << "enter word to look for, or q to quit: ";string s;if (!(std::cin >> s) || s == "q") break;auto found = dictionary.find(s);if (found != dictionary.end()){std::cout << s << " occurs " << found->second.size() << (found->second.size() > 1 ? " times" : " time") << std::endl;for (auto i : found->second)std::cout << "\t(line " << i + 1 << ") " << input.at(i) << std::endl;}else std::cout << s << " occurs 0 time" << std::endl;}}
练习12.29
我们曾经用
do while循环来编写管理用户交互的循环。用do while重写本节程序,解释你倾向于哪个版本,为什么?
解:
do {std::cout << "enter word to look for, or q to quit: ";string s;if (!(std::cin >> s) || s == "q") break;print(std::cout, tq.query(s)) << std::endl;} while ( true );
我更喜欢 while,这可能是习惯的问题。
练习12.30
定义你自己版本的
TextQuery和QueryResult类,并执行12.3.1节中的runQueries函数。
解:
同12.27。
练习12.31
如果用
vector代替set保存行号,会有什么差别?哪个方法更好?为什么?
如果用 vector 则会有单词重复的情况出现。而这里保存的是行号,不需要重复元素,所以 set 更好。
练习12.32
重写
TextQuery和QueryResult类,用StrBlob代替vector<string>保存输入文件。
解:
TextQuery 和 QueryResult 类中的 file 成员,改为 指向 StrBlob 的智能指针。在访问 StrBlob 时,要使用 StrBlobPtr。
练习12.33
在第15章中我们将扩展查询系统,在
QueryResult类中将会需要一些额外的成员。添加名为begin和end的成员,返回一个迭代器,指向一个给定查询返回的行号的set中的位置。再添加一个名为get_file的成员,返回一个shared_ptr,指向QueryResult对象中的文件。
解:
class QueryResult{public:using Iter = std::set<line_no>::iterator;// ...Iter begin() const { return lines->begin(); }Iter end() const { return lines->end(); }shared_ptr<std::vector<std::string>> get_file() const{return std::make_shared<std::vector<std::string>>(file);}private:// ...};
