13.1
【出题思路】
理解拷贝构造函数的基本概念。
【解答】
如果构造函数的第一个参数是自身类类型的引用,且所有其他参数(如果有的话)都有默认值,则此构造函数是拷贝构造函数。拷贝构造函数在以下几种情况下会被使用:
- 拷贝初始化(用
=
定义变量)。 - 将一个对象作为实参传递给非引用类型的形参。
- 一个返回类型为非引用类型的函数返回一个对象。
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员。
- 初始化标准库容器或调用其 insert/push 操作时,容器会对其元素进行拷贝初始化。
13.2
【出题思路】
理解拷贝构造函数的参数为什么必须是引用类型。
【解答】
这一声明是非法的。因为对于上一题所述的情况,我们需要调用拷贝构造函数,但调用永远也不会成功。因为其自身的参数也是非引用类型,为了调用它,必须拷贝其实参,而为了拷贝实参,又需要调用拷贝构造函数,也就是其自身,从而造成死循环。
13.3
【出题思路】
理解合成的拷贝构造函数是如何工作的。
【解答】
这两个类都未定义拷贝构造函数,因此编译器为它们定义了合成的拷贝构造函数。合成的拷贝构造函数逐个拷贝非 static 成员,对内置类型的成员,直接进行内存拷贝,对类类型的成员,调用其拷贝构造函数进行拷贝。
因此,拷贝一个 StrBlob 时,拷贝其唯一的成员 data,使用 shared_ptr 的拷贝构造函数来进行拷贝,因此其引用计数增加 1。
拷贝一个 StrBlobPtr 时,拷贝成员 wptr,用 weak_ptr 的拷贝构造函数进行拷贝,引用计数不变,然后拷贝 curr,直接进行内存复制。
13.4
【出题思路】
理解何时使用拷贝构造函数。
【解答】
如下几个地方使用了拷贝构造函数:
local = arg
将 arg 拷贝给 local。*heap = local;
将 local 拷贝到 heap 指定的地址中。Point pa[4] = { local, *heap };
将 local 和 *heap 拷贝给数组的前两个元素。return *heap;
。
13.5
【出题思路】
本题练习定义拷贝构造函数。
【解答】
class HasPtr {
public:
HasPtr(const std::string &s = std::string()) :
ps(new std::string(s)), i(0) { }
HasPtr(const HasPtr &);
private:
std::string *ps;
int i;
};
HasPtr::HasPtr(const HasPtr &orig) :
ps(new std::string(*orig.ps)), i(orig.i) { }
13.6
【出题思路】
理解拷贝赋值运算符的基本概念与合成的拷贝赋值运算符。
【解答】
拷贝赋值运算符本身是一个重载的赋值运算符,定义为类的成员函数,左侧运算对象绑定到隐含的 this 参数,而右侧的运算对象是所属类类型的,作为函数的参数。函数返回指向其左侧运算对象的引用。
当对类对象进行赋值时,会使用拷贝赋值运算符。
通常情况下,合成的拷贝赋值运算符会将右侧对象的非 static 成员逐个赋予左侧对象的对应成员,这些赋值操作是由类型的拷贝赋值运算符完成的。
若一个类未定义自己的拷贝赋值运算符,编译器就会为其合成拷贝赋值运算符,完成赋值操作,但对于某些类,还会起到禁止该类型对象赋值的效果。
13.7
【出题思路】
理解合成的拷贝赋值运算符。
【解答】
由于两个类都未定义拷贝赋值运算符,因此编译器为它们定义了合成的拷贝赋值运算符。
与拷贝构造函数的行为类似,赋值一个 StrBlob 时,赋值其唯一的成员 data,使用 shared_ptr 的拷贝赋值运算符来完成,因此其引用计数增加 1。
赋值一个 StrBlobPtr 时,赋值成员 wptr,用 weak_ptr 的拷贝赋值运算符进行赋值,引用计数不变,然后赋值 curr,直接进行内存复制。
13.8
【出题思路】
本题练习拷贝赋值运算符。
【解答】
class HasPtr {
public:
HasPtr(const std::string &s = std::string()) :
ps(new std::string(s)), i(0) { }
HasPtr(const HasPtr &); // 拷贝构造函数
HasPtr &operator=(const HasPtr &); // 拷贝赋值运算符
private:
std::string *ps;
int i;
};
HasPtr::HasPtr(const HasPtr &orig) :
ps(new std::string(*orig.ps)), i(orig.i) { }
HasPtr& HasPtr::operator=(const HasPtr &rhs) {
std::string *newps = new std::string(*rhs.ps); // 拷贝指针指向的对象
delete ps; // 销毁原 string
ps = newps; // 指向新 string
i = rhs.i; // 使用内置的 int 赋值
return *this; // 返回一个此对象的引用
}
13.9
【出题思路】
理解析构函数与合成析构函数的基本概念。
【解答】
析构函数完成与构造函数相反的工作:释放对象使用的资源,销毁非静态数据成员。从语法上看,它是类的一个成员函数,名字是波浪号,没有返回值,也不接受参数。
当一个类没有定义析构函数时,编译器会为它合成析构函数。
合成的析构函数体为空,但这并不意味着它什么也不做,当空函数体执行完后,非静态数据成员会被逐个销毁。也就是说,成员是在析构函数体之后隐含的析构阶段中进行销毁的。
13.10
【出题思路】
理解合成的析构函数。
【解答】
这两个类都没有定义析构函数,因此编译器会为它们合成析构函数。
对 StrBlob,合成析构函数的空函数体执行完毕后,会进行隐含的析构阶段,销毁非静态数据成员 data。这会调用 shared_ptr 的析构函数,将引用计数减 1,引用计数变为 0,会销毁共享的 vector 对象。
对 StrBlobPtr,合成析构函数在隐含的析构阶段会销毁数据成员 wptr 和 curr,销毁 wptr 会调用 weak_ptr 的析构函数,引用计数不变,而 curr 是内置类型,销毁它不会有特殊动作。
13.11
【出题思路】
练习设计析构函数。
【解答】
只需释放 string 对象占用的空间即可。
class HasPtr {
public:
HasPtr(const std::string &s = std::string()) :
ps(new std::string(s)), i(0) { }
HasPtr(const HasPtr &); // 拷贝构造函数
HasPtr &operator=(const HasPtr &); // 拷贝赋值运算符
~HasPtr(); // 析构函数
private:
std::string *ps;
int i;
};
HasPtr::HasPtr(const HasPtr &orig) :
ps(new std::string(*orig.ps)), i(orig.i) { }
HasPtr& HasPtr::operator=(const HasPtr &rhs) {
std::string *newps = new std::string(*rhs.ps); // 拷贝指针指向的对象
delete ps; // 销毁原 string
ps = newps; // 指向新 string
i = rhs.i; // 使用内置的 int 赋值
return *this; // 返回一个此对象的引用
}
HasPtr::~HasPtr() {
delete ps;
}
13.12
【出题思路】
理解析构函数何时执行。
【解答】
这段代码中会发生三次析构函数调用:
- 函数结束时,局部变量 item1 的生命期结束,被销毁,Sale_data 的析构函数被调用。
- 类似的,item2 在函数结束时被销毁,Sale_data 的析构函数被调用。
- 函数结束时,参数 accum 的生命期结束,被销毁,Sale_data 的析构函数被调用。
在函数结束时,trans 的生命期也结束了,但它是 Sale_data 的指针,并不是它指向的 Sale_data 对象的生命期结束(只有 delete 指针时,指向的动态对象的生命期才结束),所以不会引起析构函数的调用。
13.13
【出题思路】
理解拷贝构造函数、拷贝赋值运算符以及析构函数何时执行。
【解答】
程序如下所示。请编译运行程序,观察输出结果,仔细分析每次调用对应哪个对象。例如,程序结束时有 3 次析构函数的调用,分别对应 y、vx 的第一个元素和 x。
#include <iostream>
#include <vector>
using namespace std;
struct X {
X() { cout << "构造函数 X()" << endl; }
X(const X &) { cout << "拷贝构造函数 X(const X &)" << endl; }
X &operator=(const X &rhs) {
cout << "拷贝赋值运算符 =(const X &)" << endl;
return *this;
}
~X() { cout << "析构函数 ~X()" << endl; }
};
void f1(X x) { }
void f2(X &x) { }
int main() {
cout << "局部变量:" << endl;
X x;
cout << endl;
cout << "非引用参数传递:" << endl;
f1(x);
cout << endl;
cout << "引用参数传递:" << endl;
f2(x);
cout << endl;
cout << "动态分配:" << endl;
X *px = new X;
cout << endl;
cout << "添加到容器中:" << endl;
vector<X> vx;
vx.push_back(x);
cout << endl;
cout << "释放动态分配对象:" << endl;
delete px;
cout << endl;
cout << "拷贝初始化和赋值:" << endl;
X y = x;
y = x;
cout << endl;
cout << "程序结束!" << endl;
return 0;
}
// 运行结果
局部变量:
构造函数 X()
非引用参数传递:
拷贝构造函数 X(const X &)
析构函数 ~X()
引用参数传递:
动态分配:
构造函数 X()
添加到容器中:
拷贝构造函数 X(const X &)
释放动态分配对象:
析构函数 ~X()
拷贝初始化和赋值:
拷贝构造函数 X(const X &)
拷贝赋值运算符 =(const X &)
程序结束!
析构函数 ~X()
析构函数 ~X()
析构函数 ~X()
Process finished with exit code 0
13.14
【出题思路】
理解拷贝控制成员的应用场合。
【解答】
这是一个典型的应该定义拷贝控制成员的场合。如果不定义拷贝构造函数和拷贝赋值运算符,依赖合成的版本,则在拷贝构造和赋值时,会简单复制数据成员。对本问题来说,就是将序号简单复制给新对象。
因此,代码中对 a、b、c 三个对象调用函数 f,会输出三个相同的序号 —— 合成拷贝构造函数被调用时简单复制序号,使得三个对象具有相同的序号。
程序如下所示:
#include <iostream>
using namespace std;
class numbered {
private:
static int seq;
public:
numbered() { mysn = seq++; }
// 13.15
// numbered(numbered &n) { mysn = seq++; }
int mysn;
};
int numbered::seq = 0;
// 13.16
// void f(const numbered &s)
void f(const numbered s) {
cout << s.mysn << endl;
}
int main() {
numbered a, b = a, c = b;
f(a); f(b); f(c);
return 0;
}
// 运行结果
0
0
0
Process finished with exit code 0
13.15
【出题思路】
理解拷贝构造函数的使用。
【解答】
在此程序中,都是拷贝构造函数在起作用,因此定义能生成新的序号的拷贝构造函数会改变输出结果。
但注意,新的输出结果不是 0、1、2,而是 3、4、5。
因为在定义变量 a 时,默认构造函数起作用,将其序号设定为 0。当定义 b、c 时,拷贝构造函数起作用,将它们的序号分别设定为 1、2。
但是,在每次调用函数 f 时,由于参数是 numbered 类型,又会触发拷贝构造函数,使得每一次都将形参 s 的序号设定为新值,从而导致三次的输出结果是 3、4、5。
程序如下所示:
#include <iostream>
using namespace std;
class numbered {
private:
static int seq;
public:
numbered() { mysn = seq++; }
// 13.15
numbered(numbered &n) { mysn = seq++; }
int mysn;
};
int numbered::seq = 0;
// 13.16
// void f(const numbered &s)
void f(const numbered s) {
cout << s.mysn << endl;
}
int main() {
numbered a, b = a, c = b;
f(a); f(b); f(c);
return 0;
}
// 运行结果
3
4
5
Process finished with exit code 0
13.16
【出题思路】
理解参数类型与拷贝构造函数的关系。
【解答】
会改变输出结果,新结果是 0、1、2。
原因是,将参数改为 const numbered &
。由于形参类型由类类型变为引用类型,传递的不是类对象而是类对象的引用。因此,对每次调用 f,s 都是指向实参的引用,序号自然就是实参的序号。而不是创建一个新的对象,获得一个新序号。
程序如下所示:
#include <iostream>
using namespace std;
class numbered {
private:
static int seq;
public:
numbered() { mysn = seq++; }
// 13.15
numbered(numbered &n) { mysn = seq++; }
int mysn;
};
int numbered::seq = 0;
// 13.16
void f(const numbered &s) {
cout << s.mysn << endl;
}
int main() {
numbered a, b = a, c = b;
f(a); f(b); f(c);
return 0;
}
// 运行结果
0
1
2
Process finished with exit code 0
13.17
【出题思路】
验证你对拷贝构造函数的理解。
【解答】
前边每题已经列出代码,注意对比三题的代码不同部分和输出结果。
13.18
【出题思路】
练习定义拷贝构造成员。
【解答】
程序如下所示:
#include <iostream>
#include <string>
using namespace std;
class Employee {
private:
static int sn;
public:
Employee() { mysn = sn++; }
Employee(const string &s) : name(s), mysn(sn++) { }
const string &get_name() { return name; }
int get_mysn() { return mysn; }
private:
string name;
int mysn;
};
int Employee::sn = 0;
void f(Employee &s) {
cout << s.get_name() << ":" << s.get_mysn() << endl;
}
int main() {
Employee a("陈"), b = a, c;
c = b;
f(a); f(b); f(c);
return 0;
}
// 运行结果
陈:0
陈:0
陈:0
Process finished with exit code 0
13.19
【出题思路】
练习定义拷贝构造成员。
【解答】
#include <iostream>
#include <string>
using namespace std;
class Employee {
private:
static int sn;
public:
Employee() { mysn = sn++; }
Employee(const string &s) : name(s), mysn(sn++) { }
// 为 13.19 题定义的拷贝构造函数和拷贝赋值运算符
Employee(const Employee &e) : name(e.name), mysn(sn++) { }
Employee &operator=(Employee &rhs) {
name = rhs.name;
return *this;
}
const string &get_name() { return name; }
int get_mysn() { return mysn; }
private:
string name;
int mysn;
};
int Employee::sn = 0;
void f(Employee &s) {
cout << s.get_name() << ":" << s.get_mysn() << endl;
}
int main() {
Employee a("陈"), b = a, c;
c = b;
f(a); f(b); f(c);
return 0;
}
// 运行结果
陈:0
陈:1
陈:2
Process finished with exit code 0
当用 a 初始化 b 时,会调用拷贝构造函数。如果不定义拷贝构造函数,则合成的拷贝构造函数简单复制 mysn,会使两者的序号相同。
当用 b 为 c 赋值时,会调用拷贝赋值运算符。如果不定义自己的版本,则编译器定义的合成版本会简单复制 mysn,会使两者的序号相同。
定义的两个拷贝控制成员见上边代码中的注释处。
13.20
【出题思路】
理解拷贝构造成员。
【解答】
两个类都未定义拷贝控制成员,因此都是编译器为它们定义合成版本。
当 TextQuery 销毁时,合成版本会销毁其 file 和 wm 成员。对 file 成员,会将 shared_ptr 的引用计数减 1,若变为 0,则销毁所管理的动态 vector 对象(会调用 vector 和 string 的析构函数)。对 wm,调用 map 的析构函数(从而调用 string、shared_ptr 和 set 的析构函数),会正确释放资源。
当 QueryResult 销毁时,合成版本会销毁其 sought、lines 和 file 成员。类似 TextQery,string、shared_ptr、set、vector 的析构函数可能被调用,因为这些类都有良好的拷贝控制成员,会正确释放资源。
当拷贝一个 TextQery 时,合成版本会拷贝 file 和 wm 成员。对 file,shared_ptr 的引用计数会加 1。对 wm,会调用 map 的拷贝构造函数(继而调用 string、shared_ptr 和 set 的拷贝构造函数),因此会正确进行拷贝操作。赋值操作类似,只不过会将原来的资源释放掉,例如,原有的 file 的引用计数会减 1。
QueryResult 的拷贝和赋值类似。
13.21
【出题思路】
理解拷贝构造控制成员。
【解答】
两个类虽然都未定义拷贝控制成员,但它们用智能指针管理共享的动态对象(输入文件内容,查询结果的行号集合),用标准库容器保存大量容器。而这些标准库机制都有设计良好的拷贝控制成员,用合成的拷贝控制成员简单地拷贝、赋值、销毁它们,即可保证正确的资源管理。因此,这两个类并不需要定义自己的拷贝控制成员。实际上,这两个类的类对象之间就存在资源共享,目前的设计已能很好地实现这种共享,同类对象之间的共享也自然能够解决。
13.22
【出题思路】
本题练习如何让一个类 “行为像值”。
【解答】
在之前的练习中,我们已经为 HasPtr 定义了拷贝构造函数和拷贝赋值运算符,两者相结合,再加上析构函数(delete ps 即可),已可达到目的。
程序如下所示:
#include <iostream>
#include <string>
using namespace std;
class HasPtr {
public:
HasPtr(const std::string &s = std::string()) :
ps(new std::string(s)), i(0) { }
HasPtr(const HasPtr &); // 拷贝构造函数
HasPtr &operator=(const HasPtr &); // 拷贝赋值运算符
HasPtr &operator=(const string &); // 赋予新 string
string &operator*(); // 解引用
~HasPtr(); // 析构函数
private:
std::string *ps;
int i;
};
HasPtr::HasPtr(const HasPtr &orig) :
ps(new std::string(*orig.ps)), i(orig.i) { }
HasPtr& HasPtr::operator=(const HasPtr &rhs) {
std::string *newps = new std::string(*rhs.ps); // 拷贝指针指向的对象
delete ps; // 销毁原 string
ps = newps; // 指向新 string
i = rhs.i; // 使用内置的 int 赋值
return *this; // 返回一个此对象的引用
}
HasPtr& HasPtr::operator=(const string &rhs) {
*ps = rhs;
return *this;
}
string& HasPtr::operator*() {
return *ps;
}
HasPtr::~HasPtr() {
delete ps; // 释放 string 内存
}
int main() {
HasPtr h("hi mom!");
HasPtr h2(h); // 行为类值,h2、h3 和 h 指向不同 string
HasPtr h3 = h;
h2 = "hi dad!";
h3 = "hi son!";
cout << "h: " << *h << endl;
cout << "h2: " << *h2 << endl;
cout << "h3: " << *h3 << endl;
return 0;
}
// 运行结果
h: hi mom!
h2: hi dad!
h3: hi son!
Process finished with exit code 0
13.23
【出题思路】
理解拷贝控制成员的规范写法。
【解答】
请仔细体会拷贝赋值运算符的规范写法,它是如何保证自赋值安全的。
13.24
【出题思路】
理解拷贝控制成员的作用。
【解答】
如果未定义析构函数,在销毁 HasPtr 对象时,合成的析构函数不会释放指针 ps 指向的内存,造成内存泄漏。
如果未定义拷贝构造函数,在拷贝 HasPtr 对象时,合成的拷贝构造函数会简单复制 ps 成员,使得两个 HasPtr 指向相同的 string。当其中一个 HasPtr 修改 string 内容时,另一个 HasPtr 也被改变,这并不符合我们的设想。如果同时定义了析构函数,情况会更为糟糕,当销毁其中一个 HasPtr 时,ps 指向的 string 被销毁,另一个 HasPtr 的 ps 成为空悬指针。
13.25
【出题思路】
本题综合练习拷贝控制成员的使用。
【解答】
由于希望 StrBlob 的行为像值一样,因此在拷贝构造函数和拷贝赋值运算符中,我们应该将其数据 —— string 的 vector 拷贝一份,使得两个 StrBlob 对象指向各自的数据,而不是简单拷贝 shared_ptr 使得两个 StrBlob 指向同一个 vector。
StrBlob 不需要析构函数的原因是,它管理的全部资源就是 string 的 vector,而这是由 shared_ptr 负责管理的。当一个 StrBlob 对象销毁时,会调用 shared_ptr 的析构函数,它会正确调整引用计数,当需要时(引用计数变为 0)释放 vector。即,shared_ptr 保证了资源分配、释放的正确性,StrBlob 就不必进行相应的处理了。
13.26
【出题思路】
本题综合练习使用拷贝控制成员实现类值行为。
【解答】
基于练习 12.19 的程序如下所示。可以看到,由于我们定义了拷贝构造函数和拷贝赋值运算符,使得 StrBlob 的行为像值一样,因此 b2 和 b1、b3 和 b1 不再共享 vector,而是都指向自己的拷贝。当向其中之一添加元素时,另一个的内容不会发生改变。读者可以注视掉拷贝构造函数与(或)拷贝赋值运算符,观察有无拷贝控制成员时,程序输出结果的不同。
另外一个值得注意的是拷贝赋值运算符的写法,由于 StrBlob 是用 shared_ptr 而非内置指针类型来管理动态对象,因此直接将新创建的 shared_ptr 赋予了 data,这不会导致自赋值错误。data 指向新的动态对象,引用计数为 1;而 shared_ptr 的赋值运算符会将 data 原来指向的对象的引用计数减 1。当进行自赋值时,这显然不会导致非法指针问题,语义也是合理的 —— data 脱离原共享对象,指向与原对象内容相同的新对象。代码如下所示:
StrBlob.h
#ifndef TEST_STRBLOB_H
#define TEST_STRBLOB_H
#include <vector>
#include <string>
#include <initializer_list>
#include <memory>
#include <stdexcept>
using std::string;
using std::vector;
using std::initializer_list;
using std::shared_ptr;
using std::weak_ptr;
using std::make_shared;
using std::out_of_range;
using std::runtime_error;
// 对于 StrBlob 中的友元声明来说,此前置声明是必要的
class StrBlobPtr;
class StrBlob {
friend class StrBlobPtr;
public:
typedef vector<string>::size_type size_type;
StrBlob();
StrBlob(initializer_list<string> il);
// 拷贝构造函数
StrBlob(StrBlob &s);
// 拷贝赋值运算符
StrBlob &operator=(StrBlob &rhs);
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;
// 提供给 StrBlobPtr 的接口
// 返回指向首元素和尾后元素的 StrBlobPtr
StrBlobPtr begin(); // 定义 StrBlobPtr 后才能定义这两个函数
StrBlobPtr end();
private:
shared_ptr<vector<string>> data;
// 如果 data[i] 不合法,抛出一个异常
void check(size_type i, const string &msg) const;
};
StrBlob::StrBlob() : data(make_shared<vector<string>>()) { }
StrBlob::StrBlob(initializer_list <string> il) :
data(make_shared<vector<string>>(il)) { }
StrBlob::StrBlob(StrBlob &s) : data(make_shared<vector<string>>(*s.data)) { }
StrBlob& StrBlob::operator=(StrBlob &rhs) {
data = make_shared<vector<string>>(*rhs.data);
return *this;
}
void StrBlob::check(vector<string>::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();
}
class StrBlobPtr {
friend bool eq(const StrBlobPtr &, const StrBlobPtr &);
public:
StrBlobPtr() : curr(0) {}
StrBlobPtr(StrBlob &a, size_t sz = 0) : wptr(a.data), curr(sz) {}
string& deref() const;
StrBlobPtr& incr(); // 前缀递增
StrBlobPtr& decr(); // 后缀递减
private:
// 若检查成功,check 返回一个指向 vector 的 shared_ptr
shared_ptr<vector<string>> check(size_t, const string&) const;
// 保存一个 weak_ptr,意味着底层 vector 可能会被销毁
weak_ptr<vector<string>> wptr;
size_t curr; // 在数组中的当前位置
};
shared_ptr<vector<string>> StrBlobPtr::check(size_t i, const string &msg) const {
auto ret = wptr.lock(); // vector 还存在吗?
if (!ret)
throw runtime_error("unbound StrBlobPtr");
if (i >= ret->size())
throw out_of_range(msg);
return ret; // 否则,返回指向 vector 的 shared_ptr
}
string& StrBlobPtr::deref() const {
auto p = check(curr, "dereference past end");
return (*p)[curr]; // (*P) 是对象所指向的 vector
}
// 前缀递增:返回递增后的对象的引用
StrBlobPtr& StrBlobPtr::incr() {
// 如果 curr 已经指向容器的尾后位置,就不能递增它
check(curr, "increment past end of StrBlobPtr");
++curr; // 推进当前位置
return *this;
}
// 前缀递减:返回递减后的对象的引用
StrBlobPtr& StrBlobPtr::decr() {
// 如果 curr 已经为 0,递减它会产生一个非法下标
--curr; // 递减当前位置
check(-1, "decrement past begin of StrBlobPtr");
return *this;
}
// StrBlob 的 begin 和 end 成员的定义
StrBlobPtr StrBlob::begin() {
return StrBlobPtr(*this);
}
StrBlobPtr StrBlob::end() {
auto ret = StrBlobPtr(*this, data->size());
return ret;
}
// StrBlobPtr 的比较操作
bool eq(const StrBlobPtr &lhs, const StrBlobPtr &rhs) {
auto l = lhs.wptr.lock(), r = rhs.wptr.lock();
// 若底层的 vector 是同一个
if (l == r)
// 则两个指针都是空,或者指向相同元素时,它们相等
return (!r || lhs.curr == rhs.curr);
else
return false; // 若指向不同 vector,则不可能相等
}
bool neq(const StrBlobPtr &lhs, const StrBlobPtr &rhs) {
return !eq(lhs, rhs);
}
#endif //TEST_STRBLOB_H
main.cpp
#include <iostream>
#include "StrBlob.h"
using namespace std;
int main() {
StrBlob b1;
{
StrBlob b2 = {"a", "an", "the"};
b1 = b2;
b2.push_back("about");
cout << "b2 大小为" << b2.size() << endl;
cout << "b2 首尾元素为" << b2.front() << " " << b2.back() << endl;
}
// b2 在花括号外失效,作用域仅限于花括号内
cout << "b1 大小为" << b1.size() << endl;
cout << "b1 首尾元素为" << b1.front() << " " << b1.back() << endl;
StrBlob b3 = b1;
b3.push_back("next");
cout << "b3 大小为" << b3.size() << endl;
cout << "b3 首尾元素为" << b3.front() << " " << b3.back() << endl;
cout << "b1 全部元素:";
for (auto iter = b1.begin(); neq(iter, b1.end()); iter.incr())
cout << iter.deref() << " ";
cout << endl;
return 0;
}
// 运行结果
b2 大小为4
b2 首尾元素为a about
b1 大小为3
b1 首尾元素为a the
b3 大小为4
b3 首尾元素为a next
b1 全部元素:a an the
Process finished with exit code 0
13.27
【出题思路】
本题练习实现类指针行为。
可对比练习 13.22。体会 “行为像值” 和 “行为像指针” 的区别。
【解答】
参考本节内容,即可实现如下程序。请编译运行它,观察输出结果。
#include <iostream>
#include <string>
using namespace std;
class HasPtr {
public:
// 构造函数分配新的 string 和新的计数器,将计数器置为 1
HasPtr(const std::string &s = std::string()) :
ps(new std::string(s)), i(0), use(new size_t(1)) { }
// 拷贝构造函数拷贝所有三个数据成员,并递增计数器
HasPtr(const HasPtr &); // 拷贝构造函数
HasPtr &operator=(const HasPtr &); // 拷贝赋值运算符
HasPtr &operator=(const string &); // 赋予新 string
string &operator*(); // 解引用
~HasPtr(); // 析构函数
private:
std::string *ps;
int i;
size_t *use; // 用来记录有多少个对象共享 *ps 的成员
};
HasPtr::HasPtr(const HasPtr &orig) :
ps(orig.ps), i(orig.i), use(orig.use) { ++*use; }
HasPtr& HasPtr::operator=(const HasPtr &rhs) {
++*rhs.use; // 递增右侧运算对象的引用
if (--*use == 0) { // 然后递减本对象的引用计数
delete ps; // 如果没有其他用户
delete use; // 释放本对象分配的成员
}
ps = rhs.ps; // 将数据从 rhs 拷贝到本对象
i = rhs.i;
use = rhs.use;
return *this; // 返回本对象
}
HasPtr& HasPtr::operator=(const string &rhs) {
*ps = rhs;
return *this;
}
string& HasPtr::operator*() {
return *ps;
}
HasPtr::~HasPtr() {
if (--*use == 0) { // 如果引用计数变为 0
delete ps; // 释放 string 内存
delete use; // 释放计数器内存
}
}
int main() {
HasPtr h("hi mom!");
HasPtr h2 = h; // 未分配新 string,h2 和 h 指向相同的 string
h = "hi dad!";
cout << "h: " << *h << endl;
cout << "h2: " << *h2 << endl;
return 0;
}
// 运行结果
h: hi dad!
h2: hi dad!
Process finished with exit code 0
13.28
【出题思路】
本题练习根据问题的实际需求设计拷贝控制成员。
【解答】
这是一个二叉树数据结构,若实现类指针行为,且 count 用作引用计数,默认构造函数如下:
TreeNode::TreeNode()
: value(""), count(1), left(nullptr), right(nullptr) { }
BinStrTree::BinStrTree() : root(nullptr) { }
还可以定义其他构造函数,如(带参的构造函数):
TreeNode::TreeNode(const string &s = string(),
TreeNode *lchild = nullptr, TreeNode *rchild = nullptr)
: value(s), count(1), left(lchild), right(rchild) { }
BinStrTree::BinStrTree(TreeNode *t = nullptr) : root(t) { }
当然,为了创建出二叉树,还会有创建节点、设置左右孩子节点等函数,但不是本题的重点,因此不再讨论。
由于希望它的行为类指针,因此需要定义拷贝构造函数和拷贝赋值运算符来拷贝树节点指针而非树节点,并调整引用计数。还需定义析构函数,减少引用次数,当引用计数变为 0 时,释放内存。需要注意的是,二叉树结构并非单一的节点,而是多层结构,需要递归遍历左右子树中的所有节点,进行相应的操作。
拷贝构造函数:
BinStrTree::BinStrTree(const BinStrTree &bst)
: root(bst.root) { // 拷贝整棵树
root->CopyTree(); // 应拷贝整棵树,而非仅仅根节点
}
void TreeNode::CopyTree() { // 拷贝以此节点为根的子树 —— 增加引用计数
if (left) // 左子树不空,拷贝左子树
left->CopyTree();
if (right) // 右子树为空,拷贝右子树
right->CopyTree();
++count;
}
// 从某个节点开始拷贝子树
TreeNode::TreeNode(const TreeNode &tn)
: value(tn->value), count(1), left(tn->left), right(tn->right) {
if (left) // 左子树不空,拷贝左子树
left->CopyTree();
if (right) // 右子树为空,拷贝右子树
right->CopyTree();
}
析构函数:
BinStrTree::~BinStrTree() { // 释放整棵树
if (!root->ReleaseTree()) // 释放整棵树,而非仅仅根节点
delete root; // 引用计数为 0,释放节点空间
}
int TreeNode::ReleaseTree() { // 释放以此节点为根的子树
if (left) { // 左子树不空,释放左子树
if (!left->CopyTree())
delete left; // 左孩子引用计数为 0,释放其空间
}
if (right) { // 右子树不空,释放右子树
if (!right->CopyTree())
delete right; // 右孩子引用计数为 0,释放其空间
}
--count;
return count;
}
TreeNode::~TreeNode() {
// count 为 0 表示资源已被释放,是 delete 触发的析构函数,什么也不做即可
if (count)
ReleaseTree();
}
13.29
【出题思路】
理解 swap 调用的确定。
【解答】
在此 swap 函数中又调用了 swap 来交换 HasPtr 成员 ps 和 i。但这两个成员的类型分别是指针和整型,都是内置类型,因此函数中的 swap 调用被解析为 std::swap
,而不是 HasPtr 的特定版本 swap(也就是自身),所以不会导致递归循环。
13.30
【出题思路】
练习定义 swap。
将练习 13.22 稍加改动即可。
【解答】
参考书中本节内容即可完成 swap 函数的编写。
程序如下所示:
#include <iostream>
#include <string>
using namespace std;
class HasPtr {
friend void swap(HasPtr &, HasPtr &);
public:
HasPtr(const std::string &s = std::string()) :
ps(new std::string(s)), i(0) { }
HasPtr(const HasPtr &); // 拷贝构造函数
HasPtr &operator=(const HasPtr &); // 拷贝赋值运算符
HasPtr &operator=(const string &); // 赋予新 string
string &operator*(); // 解引用
~HasPtr(); // 析构函数
private:
std::string *ps;
int i;
};
inline
void swap(HasPtr &lhs, HasPtr &rhs) {
using std::swap;
swap(lhs.ps, rhs.ps); // 交换指针,而不是 string 数据
swap(lhs.i, rhs.i); // 交换 int 成员
}
HasPtr::HasPtr(const HasPtr &orig) :
ps(new std::string(*orig.ps)), i(orig.i) { }
HasPtr& HasPtr::operator=(const HasPtr &rhs) {
std::string *newps = new std::string(*rhs.ps); // 拷贝指针指向的对象
delete ps; // 销毁原 string
ps = newps; // 指向新 string
i = rhs.i; // 使用内置的 int 赋值
return *this; // 返回一个此对象的引用
}
HasPtr& HasPtr::operator=(const string &rhs) {
*ps = rhs;
return *this;
}
string& HasPtr::operator*() {
return *ps;
}
HasPtr::~HasPtr() {
delete ps; // 释放 string 内存
}
int main() {
HasPtr h("hi mom!");
HasPtr h2(h); // 行为类值,h2、h3 和 h 指向不同 string
HasPtr h3 = h;
h2 = "hi dad!";
h3 = "hi son!";
swap(h2, h3);
cout << "h: " << *h << endl;
cout << "h2: " << *h2 << endl;
cout << "h3: " << *h3 << endl;
return 0;
}
// 运行结果
h: hi mom!
h2: hi son!
h3: hi dad!
Process finished with exit code 0
13.31
【出题思路】
理解 swap 的应用。
对上一练习(13.30)稍加改动即可。
【解答】
<
运算符直接返回两个 HasPtr 的 ps 指向的 string 的比较结果即可,但需要注意的是,它应被声明为 const 的。
程序如下所示:
#include <iostream>
using std::cout; using std::endl;
#include <fstream>
using std::ifstream;
#include <sstream>
using std::istringstream;
#include <string>
using std::string;
#include <vector>
using std::vector;
#include <stdexcept>
using std::cerr;
class HasPtr {
friend void swap(HasPtr &, HasPtr &);
public:
HasPtr(const string &s = string()) :
ps(new string(s)), i(0) { }
HasPtr(const HasPtr &); // 拷贝构造函数
HasPtr &operator=(const HasPtr &); // 拷贝赋值运算符
HasPtr &operator=(const string &); // 赋予新 string
string &operator*(); // 解引用
bool operator<(const HasPtr &) const; // 比较运算
~HasPtr(); // 析构函数
private:
string *ps;
int i;
};
inline
void swap(HasPtr &lhs, HasPtr &rhs) {
using std::swap;
cout << "交换 " << *lhs.ps << " 和 " << *rhs.ps << endl;
swap(lhs.ps, rhs.ps); // 交换指针,而不是 string 数据
swap(lhs.i, rhs.i); // 交换 int 成员
}
HasPtr::HasPtr(const HasPtr &orig) :
ps(new string(*orig.ps)), i(orig.i) { }
HasPtr& HasPtr::operator=(const HasPtr &rhs) {
string *newps = new string(*rhs.ps); // 拷贝指针指向的对象
delete ps; // 销毁原 string
ps = newps; // 指向新 string
i = rhs.i; // 使用内置的 int 赋值
return *this; // 返回一个此对象的引用
}
HasPtr& HasPtr::operator=(const string &rhs) {
*ps = rhs;
return *this;
}
string& HasPtr::operator*() {
return *ps;
}
bool HasPtr::operator<(const HasPtr &rhs) const {
return *ps < *rhs.ps;
}
HasPtr::~HasPtr() {
delete ps; // 释放 string 内存
}
int main(int argc, char **argv) {
if (argc != 2) {
cerr << "请给出文件名" << endl;
return -1;
}
ifstream in(argv[1]);
if (!in) {
cerr << "无法打开输入文件" << endl;
return -1;
}
vector<HasPtr> hvec;
string line;
string word;
while (getline(in, line)) {
istringstream l_in(line); // 构造字符串流,读取单词
while (l_in >> word)
hvec.push_back(word);
}
for (auto p : hvec)
cout << *p << " ";
cout << endl;
sort(hvec.begin(), hvec.end());
for (auto p : hvec)
cout << *p << " ";
cout << endl;
return 0;
}
运行程序前,在 CLion -> Run -> Edit Configurations 下配置 Program arguments 为 ../data
。
注:../data
即为文件 data 的文件名及其相对路径(是相对于可执行程序所在目录的相对路径)。
并在文件 data 中写入如下内容:
hello world c++ primer fifth
clion jetbrain apple
iphone
运行程序,程序执行结果如下所示:
hello world c++ primer fifth clion jetbrain apple iphone
交换 hello 和 fifth
交换 world 和 apple
交换 primer 和 clion
交换 fifth 和 apple
交换 fifth 和 c++
交换 fifth 和 clion
交换 primer 和 jetbrain
交换 world 和 iphone
交换 primer 和 iphone
交换 jetbrain 和 iphone
apple c++ clion fifth hello iphone jetbrain primer world
Process finished with exit code 0
13.32
【出题思路】
深入理解 swap 和类指针的类。
类指针的例子如练习 13.27。
【解答】
默认 swap 版本(std::swap
)简单交换两个对象(内置类型的对象)的非静态成员,对 HasPtr 而言,就是交换 string 指针 ps、引用计数指针 use 和整型值 i。可以看出,这种语义是符合期望的 —— 两个 HasPtr 指向了原来对方的 string,而两者互换 string 后,各自的引用计数本应该是不变的(都是减 1 再加 1)。因此,默认 swap 版本已经能正确处理类指针 HasPtr 的交换,专用 swap 版本(用户自定义的 swap)不会带来更多收益。
13.33
【出题思路】
理解两个成员函数的语义,以及这种语义导致对参数类型的要求。
【解答】
首先,我们需要将给定 Folder 的指针添加到当前 Message 的 folders 集合中。这样,参数类型就不能是 Folder,必须是引用类型。否则,调用 save 时会将实参拷贝给形参,folders.insert(&f);
添加的就是形参(与局部变量一样在栈中,save 执行完毕就被销毁)的指针,而非原始 Folder 的指针。而参数为引用类型,就可以令形参与实参指向相同的对象,对形参 f 取地址,得到的就是原始 Folder(实参)的指针。
其次,我们需要调用 addMsg 将当前 Message 的指针添加到 Folder 的 message 集合中,这意味着我们修改了 Folder 的内容,因此参数不能是 const 的。
13.34
【出题思路】
练习使用拷贝控制成员实现薄记工作。
使用 CLion,编译链接程序时,可能会引发错误,解决方法可参考练习 12.32
【解答】
参考书中本节内容完成 Message 类的编写,并与配套网站代码进行对比。
我调试运行程序的结果如下所示:
Folder.h
#ifndef TEST_FOLDER_H
#define TEST_FOLDER_H
#include <string>
using std::string;
#include <set>
using std::set;
class Folder;
class Message {
friend void swap(Message&, Message&);
friend class Folder;
public:
// folders is implicitly initialized to the empty set
explicit Message(const string &str = "") : contents(str) { }
// copy control to manage pointers to this Message
Message(const Message&); // copy constructor
Message& operator=(const Message&); // copy assignment
~Message(); // destructor
// add/remove this Message from the specified Folder's set of messages
void save(Folder&);
void remove(Folder&);
void debug_print(); // print contents and it's list of Folders,
// printing each Folder as well
private:
string contents; // actual message text
set<Folder*> folders; // Folders that have this Message
// utility functions used by copy constructor, assignment, and destructor
// add this Message to the Folders that point to the parameter
void add_to_Folders(const Message&);
// remove this Message from every Folder in folders
void remove_from_Folders();
// used by Folder class to add self to this Message's set of Folder's
void addFldr(Folder *f) { folders.insert(f); }
void remFldr(Folder *f) { folders.erase(f); }
};
// declaration for swap should be in the same header as Message itself
void swap(Message&, Message&);
class Folder {
friend void swap(Message&, Message&);
friend class Message;
public:
~Folder(); // remove itself from Message in msgs
Folder(const Folder&); // add new folder to each Message in msgs
Folder& operator=(const Folder&); // delete Folder from lhs messages
// add Folder to rhs messages
Folder() { } // defaults ok
void save(Message&); // add this message to folder
void remove(Message&); // remove this message from this folder
void debug_print(); // print contents and it's list of Folders
private:
set<Message*> msgs; // messages in this folder
void add_to_Messages(const Folder&); // add this Folder to each Message
void remove_from_Msgs(); // remove this Folder from each Message
void addMsg(Message *m) { msgs.insert(m); }
void remMsg(Message *m) { msgs.erase(m); }
};
#endif //TEST_FOLDER_H
Folder.cpp
#include <iostream>
using std::cerr; using std::endl;
#include <set>
using std::set;
#include <string>
using std::string;
#include "Folder.h";
void swap(Message &lhs, Message &rhs) {
using std::swap; // not strictly needed in this case, but good habit
// remove pointers to each Message from their (original) respective Folders
for (set<Folder*>::iterator f = lhs.folders.begin();
f != lhs.folders.end(); ++f)
(*f)->remMsg(&lhs);
for (set<Folder*>::iterator f = rhs.folders.begin();
f != rhs.folders.end(); ++f)
(*f)->remMsg(&rhs);
// swap the contents and Folder pointer sets
swap(lhs.folders, rhs.folders); // uses swap(set&, set&)
swap(lhs.contents, rhs.contents); // swap(string&, string&)
// add pointers to each Message to their (new) respective Folders
for (set<Folder*>::iterator f = lhs.folders.begin();
f != lhs.folders.end(); ++f)
(*f)->addMsg(&lhs);
for (set<Folder*>::iterator f = rhs.folders.begin();
f != rhs.folders.end(); ++f)
(*f)->addMsg(&rhs);
}
Message::Message(const Message &m) : contents(m.contents), folders(m.folders) {
add_to_Folders(m); // add this Message to the Folders that point to m
}
Message& Message::operator=(const Message &rhs) {
// handle self-assignment by removing pointers before inserting them
remove_from_Folders(); // update existing Folders
contents = rhs.contents; // copy message contents from rhs
folders = rhs.folders; // copy Folder pointers from rhs
add_to_Folders(rhs); // add this Message to those Folders
return *this;
}
Message::~Message() {
remove_from_Folders();
}
// add this Message to Folders that point to m
void Message::add_to_Folders(const Message &m) {
for (set<Folder*>::iterator f = m.folders.begin();
f != m.folders.end(); ++f) // for each Folder that holds m
(*f)->addMsg(this); // add a pointer to this Message to that Folder
}
// remove this Message from the corresponding Folders
void Message::remove_from_Folders() {
for (set<Folder*>::iterator f = folders.begin();
f != folders.end(); ++f) // for each pointer in folders
(*f)->remMsg(this); // remove this Message from that Folder
}
void Folder::add_to_Messages(const Folder &f) {
for (set<Message*>::iterator msg = f.msgs.begin();
msg != f.msgs.end(); ++msg)
(*msg)->addFldr(this); // add this Folder to each Message
}
Folder::Folder(const Folder &f) : msgs(f.msgs) {
add_to_Messages(f); // add this Folder to each Message in f.msgs
}
Folder& Folder::operator=(const Folder &f) {
remove_from_Msgs(); // remove this folder from each Message in msgs
msgs = f.msgs; // copy the set of Message from f
add_to_Messages(f); // add this folder to each Message in msgs
return *this;
}
Folder::~Folder() {
remove_from_Msgs();
}
void Folder::remove_from_Msgs() {
while (!msgs.empty())
(*msgs.begin())->remove(*this);
}
void Message::save(Folder &f) {
folders.insert(&f); // add the given Folder to our list of Folders
f.addMsg(this); // add this Message to f's set of Messages
}
void Message::remove(Folder &f) {
folders.erase(&f); // take the given Folder out of our list of Folders
f.remMsg(this); // remove this Message to f's set of Messages
}
void Folder::save(Message &m) {
// add m and add this folder to m's set of Folders
msgs.insert(&m);
m.addFldr(this);
}
void Folder::remove(Message &m) {
// erase m from msgs and remove this folder from m
msgs.erase(&m);
m.remFldr(this);
}
void Folder::debug_print() {
cerr << "Folder contains " << msgs.size() << " messages" << endl;
int ctr = 1;
for (set<Message*>::iterator m = msgs.begin();
m != msgs.end(); ++m) {
cerr << "Message " << ctr++ << ":\n\t" << (*m)->contents << endl;
}
}
void Message::debug_print() {
cerr << "Message:\n\t" << contents << endl;
cerr << "Appears in " << folders.size() << " Folders" << endl;
}
FolderMain.cpp
#include "Folder.h"
int main() {
Message firstMail("hello");
Message signInMail("welcome to cppprimer");
Folder mailBox;
firstMail.debug_print(); // print: "hello"
firstMail.save(mailBox); // send to mailBox
mailBox.debug_print(); // print: "hello"
signInMail.debug_print(); // print "welcome to cppprimer"
signInMail.save(mailBox); // send to mailBox
mailBox.debug_print(); // print "welcome to cppprimer hello"
firstMail = firstMail; // test for assignment to self
firstMail.debug_print(); // print "hello"
mailBox.debug_print(); // print "welcome to cppprimer hello"
return 0;
}
// 运行结果
Message:
hello
Appears in 0 Folders
Folder contains 1 messages
Message 1:
hello
Message:
welcome to cppprimer
Appears in 0 Folders
Folder contains 2 messages
Message 1:
welcome to cppprimer
Message 2:
hello
Message:
hello
Appears in 1 Folders
Folder contains 2 messages
Message 1:
welcome to cppprimer
Message 2:
hello
Process finished with exit code 0
13.35
【出题思路】
理解类似本问题的薄记工作需求为何需要拷贝控制成员。
【解答】
Message 类包含两个数据成员:content 为 string 类型,folders 为 set。这两个标准库类都有完备的拷贝控制成员,因此 Message 使用合成的拷贝控制成员的话,简单拷贝这两个成员也能实现正确拷贝。
但是,本问题的需求不仅如此。
当拷贝 Message 时,不仅要拷贝这个 Message 在哪些 Folder 中,还要将 Message 加到每个 Folder 中 —— 调用 addMsg。
类似的,当销毁 Message 时,需要将它从所有 Folder 中删除 —— 调用 remMsg。
因此,不能依赖合成的拷贝控制成员,必须设计自己的版本来完成这些薄记工作。
13.36
【出题思路】
本题练习利用拷贝控制成员实现正确的薄记操作。
【解答】
首先,Folder 类有唯一的数据成员,保存文件夹中所有消息的指针:
set<Message*> msgs; // messages in this folder
其次,应该实现 Message 类所用到的两个函数 addMsg 和 remMsg,将消息添加到和删除出文件夹,直接调用 msgs 的操作即可:
void addMsg(Message *m) { msgs.insert(m); }
void remMsg(Message *m) { msgs.erase(m); }
类似 Message 类将自身添加到和删除出所有 Folder 的成员函数,Folder 也应有将自身添加到和删除出所有 Message 的 folders 集合的成员函数,方便拷贝控制成员调用:
void Folder::add_to_Messages(const Folder &f) {
for (set<Message*>::iterator msg = f.msgs.begin();
msg != f.msgs.end(); ++msg)
(*msg)->addFldr(this); // add this Folder to each Message
}
// remove this Folder from each Message
void Folder::remove_from_Msgs() {
while (!msgs.empty())
(*msgs.begin())->remove(*this);
}
此处用到的两个 Message 类的辅助成员函数:
// used by Folder class to add self to this Message's set of Folder's
void addFldr(Folder *f) { folders.insert(f); }
void Message::remove(Folder &f) {
folders.erase(&f); // take the given Folder out of our list of Folders
f.remMsg(this); // remove this Message to f's set of Messages
}
接下来就可以定义拷贝控制成员了。首先是拷贝构造函数,它先拷贝 msgs 集合,然后调用 add_to_Messages 添加到它所有 Message 的 folders 集合中:
Folder::Folder(const Folder &f) : msgs(f.msgs) {
add_to_Messages(f); // add this Folder to each Message in f.msgs
}
析构函数调用 remove_from_Msgs 从所有 Message 中删除本 Folder:
Folder::~Folder() {
remove_from_Msgs();
}
拷贝赋值运算符首先将 Folder 从每个旧 Message 中删除,然后从右侧 Folder 拷贝 Message 集合,最后将自身添加到每个新 Message 中:
Folder& Folder::operator=(const Folder &f) {
remove_from_Msgs(); // remove this folder from each Message in msgs
msgs = f.msgs; // copy the set of Message from f
add_to_Messages(f); // add this folder to each Message in msgs
return *this;
}
13.37
【出题思路】
本题练习复杂类结构中一些辅助函数的定义。
【解答】
练习 12.34 的 Folder.h 头文件中,Message 类中已经定义了。如下所示:
// used by Folder class to add self to this Message's set of Folder's
void addFldr(Folder *f) { folders.insert(f); }
void remFldr(Folder *f) { folders.erase(f); }
13.38
【出题思路】
深入理解这类薄记操作问题中拷贝控制成员的作用。
【解答】
如果采用拷贝并交换方式,执行过程是这样:
- 由于赋值运算符的参数是 Message 类型,因此会将实参拷贝给形参 rhs,这会触发拷贝构造函数,将实参的 contents 和 folders 拷贝给 rhs,并调用 add_to_Folders 将 rhs 添加到 folders 的所有文件夹中。
- 随后赋值运算符调用 swap 交换
*this
和 rhs,首先遍历两者的 folders,将它们从自己的文件夹中删除;然后调用 string 和 set 的 swap 交换它们的 contents 和 folders;最后,再遍历两者新的 folders,将它们分别添加到自己的新文件夹中。 - 最后,赋值运算符结束,rhs 被销毁,析构函数调用 remove_from_Folders 将 rhs 从自己的所有文件夹中删除。
显然,语义是正确的,达到了预期目的。但效率低下,rhs 创建、销毁并两次添加、删除是无意义的。而采用拷贝赋值运算符的标准编写方式,形参 rhs 为引用类型,就能避免这些冗余操作,具有更好的性能。
13.39
【出题思路】
本题练习设计自己管理内存的类。
【解答】
参考本节书中的代码。
StrVec.h
#ifndef TEST_STRVEC_H
#define TEST_STRVEC_H
#include <string>
using std::string;
#include <memory>
using std::allocator;
#include <utility>
using std::pair;
#include <initializer_list>
using std::initializer_list;
// 类 vector 类内存分配策略的简单实现
class StrVec {
public:
StrVec() : elements(nullptr), first_free(nullptr), cap(nullptr) { }
StrVec(const StrVec&); // 拷贝构造函数
StrVec &operator=(const StrVec&); // 拷贝赋值运算符
~StrVec(); // 析构函数
void push_back(const string&); // 拷贝元素
size_t size() const { return first_free - elements; }
size_t capacity() const { return cap - elements; }
string *begin() const { return elements; }
string *end() const { return first_free; }
// 新加的代码
// reserve 预留一部分空间,需要一个辅助函数 void reallocate(size_t)
void reserve(size_t n) { if (n > capacity()) reallocate(n); }
void resize(size_t);
void resize(size_t, const string &);
// 练习 13.40 —— 新添加的一个构造函数
StrVec(initializer_list<string>);
private:
// 由于静态成员需要类外初始化,所以我没有把 alloc 定义为 static
// 因为我不熟悉 allocator<string> 初始化方法
allocator<string> alloc; // 分配元素
// 被添加元素的函数所使用
void chk_n_alloc() {
if (size() == capacity())
reallocate();
}
// 工具函数,被拷贝构造函数、赋值运算符和析构函数所使用
pair<string*, string*> alloc_n_copy(const string*, const string*);
void free(); // 销毁元素并释放内存
void reallocate(); // 获得更多内存并拷贝已有元素
string *elements; // 指向数组首元素的指针
string *first_free; // 指向数组第一个空闲元素的指针
string *cap; // 指向数组尾后位置的指针
// 新加的代码
void reallocate(size_t);
};
#endif //TEST_STRVEC_H
StrVec.cpp
#include "StrVec.h"
void StrVec::push_back(const string &s) {
chk_n_alloc(); // 确保有空间容纳新元素
// 在 first_free 指向的元素中构造 s 的副本
alloc.construct(first_free++, s);
}
pair<string*, string*> StrVec::alloc_n_copy(const string *b, const string *e) {
// 分配空间保存给定范围中的元素
auto data = alloc.allocate(e - b);
// 初始化并返回一个 pair,该 pair 由 data 和 uninitialized_copy 的返回值构成
return {data, uninitialized_copy(b, e, data)};
}
void StrVec::free() {
// 不能传递给 deallocate 一个空指针,如果 elements 为 0,函数什么也不做
if (elements) {
// 逆序销毁旧元素
for (auto p = first_free; p != elements; /* 空 */)
alloc.destroy(--p);
alloc.deallocate(elements, cap - elements);
}
}
StrVec::StrVec(const StrVec &s) {
// 调用 alloc_n_copy 分配空间以容纳与 s 中一样多的元素
auto newdata = alloc_n_copy(s.begin(), s.end());
elements = newdata.first;
first_free = cap = newdata.second;
}
StrVec::~StrVec() {
free();
}
StrVec& StrVec::operator=(const StrVec &rhs) {
// 调用 alloc_n_copy 分配内存,大小与 rhs 中元素占用空间一样多
auto data = alloc_n_copy(rhs.begin(), rhs.end());
free();
elements = data.first;
first_free = cap = data.second;
return *this;
}
void StrVec::reallocate() {
// 我们将分配当前大小两倍的内存空间
auto newcapacity = size() ? 2 * size() : 1;
// 分配新内存
auto newdata = alloc.allocate(newcapacity);
// 将数据从旧内存移动到新内存
auto dest = newdata; // 指向新数组中下一个空闲位置
auto elem = elements; // 指向旧数组中下一个元素
for (size_t i = 0; i != size(); ++i)
alloc.construct(dest++, std::move(*elem++));
free(); // 一旦我们移动完元素就释放旧内存空间
// 更新我们的数据结构,执行新元素
elements = newdata;
first_free = dest;
cap = elements + newcapacity;
}
// 新加的代码,StrVec::reallocate() 函数的重载形式
void StrVec::reallocate(size_t newcapacity) {
// 分配新内存
auto newdata = alloc.allocate(newcapacity);
// 将数据从旧空间移动到新空间
auto dest = newdata; // dest 指向新空间中第一个空闲位置
auto elem = elements; // 指向旧空间中下一个元素
for (size_t i = 0; i != size(); ++i)
alloc.construct(dest++, std::move(*elem++));
free(); // 数据移动完毕,释放旧空间
// 更新指针,指向新空间开始、第一个空闲位置及末尾位置
elements = newdata;
first_free = dest;
cap = elements + newcapacity;
}
void StrVec::resize(size_t count) {
resize(count, string());
}
void StrVec::resize(size_t count, const string &s) {
if (count > size()) {
if (count > capacity())
reserve(count * 2);
for (size_t i = size(); i != count; ++i)
alloc.construct(first_free++, s);
}
else if (count < size()) {
while (first_free != elements + count)
alloc.destroy(--first_free);
}
}
StrVec::StrVec(initializer_list<string> il) {
// 调用 alloc_n_copy 分配与列表 il 中元素数目一样多的空间
auto newdata = alloc_n_copy(il.begin(), il.end());
elements = newdata.first;
first_free = cap = newdata.second;
}
13.40
【出题思路】
本题练习设计列表初始化。
【解答】
通过 begin 和 end 获得列表的整个范围,利用辅助函数 alloc_n_copy 分配足够多的空间,并将范围中的元素拷贝过去即可。
代码上一题已实现。
13.41
【出题思路】
理解几个指针的含义。
【解答】
因为 first_free 指向第一个空闲位置,也就是最后一个 string 的尾后位置。当添加新 string 时,应该保存在 first_free 指向的位置,然后将 first_free 推进一个位置,因此后置运算符恰好符合要求。
如果使用前置递增运算,则是先将 first_free 推进一个位置,然后将新 string 保存在新位置上。显然,这种方法意味着 first_free 指向最后一个 string,而非尾后位置,与 first_free 的设定不吻合。
13.42
【出题思路】
练习 StrVec 的使用。
【解答】
练习 13.39 的 StrVec.h 和 StrVec.cpp 不做任何修改。
练习 12.27,对 QueryResult.h、TextQuery.h 和 TextQuery.cpp 做如下修改(用 StrVec
替换 vector<string>
):
-
QueryResult.h 中,代码开始处添加:
#include "StrVec.h"
file 成员的定义改为:
shared_ptr<StrVec> file; // input file
构造函数的第三个参数改为:
shared_ptr<StrVec> f
成员函数 get_file 的定义改为:
shared_ptr<StrVec> get_file() { return file; }
-
TextQuery.h 中 file 成员的定义改为:
shared_ptr<StrVec> file; // input file
-
TextQuery.cpp 中构造函数初始化 file 成员的操作改为:
TextQuery::TextQuery(ifstream &is) : file(new StrVec)
与第 12 章的练习类似,由于类的封装特性,主程序不用进行任何修改。
完整的代码以及运行结果如下所示:
StrVec.h
#ifndef TEST_STRVEC_H
#define TEST_STRVEC_H
#include <string>
using std::string;
#include <memory>
using std::allocator;
#include <utility>
using std::pair;
#include <initializer_list>
using std::initializer_list;
// 类 vector 类内存分配策略的简单实现
class StrVec {
public:
StrVec() : elements(nullptr), first_free(nullptr), cap(nullptr) { }
StrVec(const StrVec&); // 拷贝构造函数
StrVec &operator=(const StrVec&); // 拷贝赋值运算符
~StrVec(); // 析构函数
void push_back(const string&); // 拷贝元素
size_t size() const { return first_free - elements; }
size_t capacity() const { return cap - elements; }
string *begin() const { return elements; }
string *end() const { return first_free; }
// 新加的代码
// reserve 预留一部分空间,需要一个辅助函数 void reallocate(size_t)
void reserve(size_t n) { if (n > capacity()) reallocate(n); }
void resize(size_t);
void resize(size_t, const string &);
// 练习 13.40 —— 新添加的一个构造函数
StrVec(initializer_list<string>);
private:
// 由于静态成员需要类外初始化,所以我没有把 alloc 定义为 static
// 因为我不熟悉 allocator<string> 初始化方法
allocator<string> alloc; // 分配元素
// 被添加元素的函数所使用
void chk_n_alloc() {
if (size() == capacity())
reallocate();
}
// 工具函数,被拷贝构造函数、赋值运算符和析构函数所使用
pair<string*, string*> alloc_n_copy(const string*, const string*);
void free(); // 销毁元素并释放内存
void reallocate(); // 获得更多内存并拷贝已有元素
string *elements; // 指向数组首元素的指针
string *first_free; // 指向数组第一个空闲元素的指针
string *cap; // 指向数组尾后位置的指针
// 新加的代码
void reallocate(size_t);
};
#endif //TEST_STRVEC_H
StrVec.cpp
#include "StrVec.h"
void StrVec::push_back(const string &s) {
chk_n_alloc(); // 确保有空间容纳新元素
// 在 first_free 指向的元素中构造 s 的副本
alloc.construct(first_free++, s);
}
pair<string*, string*> StrVec::alloc_n_copy(const string *b, const string *e) {
// 分配空间保存给定范围中的元素
auto data = alloc.allocate(e - b);
// 初始化并返回一个 pair,该 pair 由 data 和 uninitialized_copy 的返回值构成
return {data, uninitialized_copy(b, e, data)};
}
void StrVec::free() {
// 不能传递给 deallocate 一个空指针,如果 elements 为 0,函数什么也不做
if (elements) {
// 逆序销毁旧元素
for (auto p = first_free; p != elements; /* 空 */)
alloc.destroy(--p);
alloc.deallocate(elements, cap - elements);
}
}
StrVec::StrVec(const StrVec &s) {
// 调用 alloc_n_copy 分配空间以容纳与 s 中一样多的元素
auto newdata = alloc_n_copy(s.begin(), s.end());
elements = newdata.first;
first_free = cap = newdata.second;
}
StrVec::~StrVec() {
free();
}
StrVec& StrVec::operator=(const StrVec &rhs) {
// 调用 alloc_n_copy 分配内存,大小与 rhs 中元素占用空间一样多
auto data = alloc_n_copy(rhs.begin(), rhs.end());
free();
elements = data.first;
first_free = cap = data.second;
return *this;
}
void StrVec::reallocate() {
// 我们将分配当前大小两倍的内存空间
auto newcapacity = size() ? 2 * size() : 1;
// 分配新内存
auto newdata = alloc.allocate(newcapacity);
// 将数据从旧内存移动到新内存
auto dest = newdata; // 指向新数组中下一个空闲位置
auto elem = elements; // 指向旧数组中下一个元素
for (size_t i = 0; i != size(); ++i)
alloc.construct(dest++, std::move(*elem++));
free(); // 一旦我们移动完元素就释放旧内存空间
// 更新我们的数据结构,执行新元素
elements = newdata;
first_free = dest;
cap = elements + newcapacity;
}
// 新加的代码,StrVec::reallocate() 函数的重载形式
void StrVec::reallocate(size_t newcapacity) {
// 分配新内存
auto newdata = alloc.allocate(newcapacity);
// 将数据从旧空间移动到新空间
auto dest = newdata; // dest 指向新空间中第一个空闲位置
auto elem = elements; // 指向旧空间中下一个元素
for (size_t i = 0; i != size(); ++i)
alloc.construct(dest++, std::move(*elem++));
free(); // 数据移动完毕,释放旧空间
// 更新指针,指向新空间开始、第一个空闲位置及末尾位置
elements = newdata;
first_free = dest;
cap = elements + newcapacity;
}
void StrVec::resize(size_t count) {
resize(count, string());
}
void StrVec::resize(size_t count, const string &s) {
if (count > size()) {
if (count > capacity())
reserve(count * 2);
for (size_t i = size(); i != count; ++i)
alloc.construct(first_free++, s);
}
else if (count < size()) {
while (first_free != elements + count)
alloc.destroy(--first_free);
}
}
StrVec::StrVec(initializer_list<string> il) {
// 调用 alloc_n_copy 分配与列表 il 中元素数目一样多的空间
auto newdata = alloc_n_copy(il.begin(), il.end());
elements = newdata.first;
first_free = cap = newdata.second;
}
QueryResult.h
#ifndef TEST_QUERYRESULT_H
#define TEST_QUERYRESULT_H
#include <memory>
using std::shared_ptr;
#include <string>
using std::string;
#include <vector>
using std::vector;
#include <set>
using std::set;
#include <iostream>
using std::ostream;
#include "StrVec.h"
class QueryResult {
friend ostream &print(ostream &, const QueryResult &);
public:
typedef vector<string>::size_type line_no;
typedef set<line_no>::const_iterator line_it;
QueryResult(string s,
shared_ptr<set<line_no>> p,
shared_ptr<StrVec> f) :
sought(s), lines(p), file(f) { }
set<line_no>::size_type size( ) const { return lines->size(); }
line_it begin() const { return lines->begin(); }
line_it end() const { return lines->end(); }
shared_ptr<StrVec> get_file() { return file; }
private:
string sought; // word this query represents
shared_ptr<set<line_no>> lines; // lines it's on
shared_ptr<StrVec> file; // input file
};
ostream &print(ostream &, const QueryResult &);
#endif //TEST_QUERYRESULT_H
make_plural.h
#ifndef TEST_MAKE_PLURAL_H
#define TEST_MAKE_PLURAL_H
#include <cstddef>
using std::size_t;
#include <string>
using std::string;
#include <iostream>
using std::cout; using std::endl;
// return the plural version of word if ctr is greater than 1
inline string make_plural(size_t ctr, const string &word, const string &ending) {
return (ctr > 1) ? word + ending : word;
}
#endif //TEST_MAKE_PLURAL_H
TextQuery.h
#ifndef TEST_TEXTQUERY_H
#define TEST_TEXTQUERY_H
#include <memory>
using std::shared_ptr;
#include <string>
using std::string;
#include <vector>
using std::vector;
#include <map>
using std::map;
#include <set>
using std::set;
#include <fstream>
using std::ifstream;
#include "QueryResult.h"
/* this version of the query classes includes two
* members not covered in the book:
* cleanup_str: which removes punctuation and
* convert all text to lowercase
* display_map: a debugging routine that will print the contents
* of the lookup map
*/
class QueryResult; // declaration needed for return type in the query function
class TextQuery {
public:
typedef vector<string>::size_type line_no;
TextQuery(ifstream&);
QueryResult query(const string&) const;
void display_map(); // debugging aid: print the map
private:
shared_ptr<StrVec> file; // input file
// maps each word to the set of the lines in which that word appears
map<string, shared_ptr<set<line_no>>> wm;
// canonicalizes text: removes punctuation and makes everything lower case
static string cleanup_str(const string&);
};
#endif //TEST_TEXTQUERY_H
TextQuery.cpp
#include "TextQuery.h"
#include "make_plural.h"
#include <cstddef>
using std::size_t;
#include <memory>
using std::shared_ptr;
#include <sstream>
using std::istringstream;
#include <string>
using std::string;
using std::getline;
#include <vector>
using std::vector;
#include <map>
using std::map;
#include <set>
using std::set;
#include <iostream>
using std::cerr; using std::cout; using std::cin;
using std::endl; using std::ostream;
#include <fstream>
using std::ifstream;
#include <cctype>
using std::ispunct; using std::tolower;
#include <cstring>
using std::strlen;
#include <utility>
using std::pair;
// because we can't use auto, we'll define typedefs
// to simplify our code
// type of the lookup map in a TextQuery object
typedef map<string, shared_ptr<set<TextQuery::line_no>>> wmType;
typedef wmType::mapped_type lineType;
// the iterator type for the map
typedef wmType::const_iterator wmIter;
// type for the set that holds the line numbers
typedef set<TextQuery::line_no>::const_iterator lineIter;
// read the input file and build the map of lines to line numbers
TextQuery::TextQuery(ifstream &is) : file(new StrVec) {
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
word = cleanup_str(word);
// if word isn't already in wm, subscripting adds a new entry
lineType &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
}
}
}
// not covered in the book -- cleanup_str removes
// punctuation and converts all text to lowercase so that
// the queries operate in a case insensitive manner
string TextQuery::cleanup_str(const string &word) {
string ret;
for (string::const_iterator it = word.begin(); it != word.end(); ++it) {
if (!ispunct(*it))
ret += tolower(*it);
}
return ret;
}
QueryResult TextQuery::query(const string &sought) const {
// we'll return a pointer to this set if we don't find sought
static lineType nodata(new set<line_no>);
// use find and not a subscript to avoid adding words to wm!
// cleanup_str removes punctuation and convert sought to lowercase
wmIter loc = wm.find(cleanup_str(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 every element in the set
for (lineIter num = qr.lines->begin(); num != qr.lines->end(); ++num)
// don't confound the user with text lines starting at 0
os << "\t(line " << *num + 1 << ")"
<< *(qr.file->begin() + *num) << endl;
return os;
}
// debugging routine, not covered in the book
void TextQuery::display_map() {
wmIter iter = wm.begin(), iter_end = wm.end();
// for each word in the map
for ( ; iter != iter_end; ++iter) {
cout << "word: " << iter->first << "{";
// fetch location vector as a const reference to avoid copying it
lineType text_locs = iter->second;
lineIter loc_iter = text_locs->begin(), loc_iter_end = text_locs->end();
// print all line numbers for this word
while (loc_iter != loc_iter_end) {
cout << *loc_iter;
if (++loc_iter != loc_iter_end)
cout << ", ";
}
cout << "}\n"; // end list of output this word
}
cout << endl; // finished printing entire map
}
querymain.cpp
#include <string>
using std::string;
#include <fstream>
using std::ifstream;
#include <iostream>
using std::cin; using std::cout; using std::cerr;
using std::endl;
#include <cstdlib> // for EXIT_FAILURE
#include "TextQuery.h"
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;
}
}
// program takes single argument specifying the file to query
int main(int argc, char **argv) {
// open the file from which user will query words
ifstream infile;
// open returns void, so we use the comma operator XREF(commaOp)
// to check the state of infile after the open
if (argc < 2 || !(infile.open(argv[1]), infile)) {
cerr << "No input file!" << endl;
return EXIT_FAILURE;
}
runQueries(infile);
return 0;
}
运行程序前,在 CLion -> Run -> Edit Configurations 下配置 Program arguments 为 ../data
。
注:../data
即为文件 data 的文件名及其相对路径(是相对于可执行程序所在目录的相对路径)。
并在文件 data 中写入如下内容:
c++ primer 5th
C++ Primer 5th
example. Cplusplus
Primer example,
Example primer
运行程序,程序执行结果如下所示:
enter word to look for, or q to quit: example..
example.. occurs 3 times
(line 3)example. Cplusplus
(line 4) Primer example,
(line 5) Example primer
enter word to look for, or q to quit: primER
primER occurs 4 times
(line 1)c++ primer 5th
(line 2)C++ Primer 5th
(line 4) Primer example,
(line 5) Example primer
enter word to look for, or q to quit: q
Process finished with exit code 0
13.43
【出题思路】
复习 lambda 的定义和使用。
【解答】
将 for 循环改成如下形式即可。注意,elements 和 first_free 是 string*
类型,因此它们指出的范围中的元素是 string 类型。因此,lambda 的参数 s 应该是 string&
类型,在 lambda 的函数体中应该取 s 的地址,用来调用 destroy。
void StrVec::free() {
// 不能传递给 deallocate 一个空指针,如果 elements 为 0,函数什么也不做
if (elements) {
// // 逆序销毁旧元素
// for (auto p = first_free; p != elements; /* 空 */)
// alloc.destroy(--p);
for_each(elements, first_free, [this] (string &s) { alloc.destroy(&s); });
alloc.deallocate(elements, cap - elements);
}
}
我更倾向于 for_each 和 lambda 版本,因为这个版本只需指出范围及对范围中元素执行什么操作即可,而 for 版本则需要程序员小心控制指针的增减。
13.44
【出题思路】
本题练习定义自己管理内存的类。
【解答】
总体上需要注意的地方与 StrVec 的定义类似,只不过 StrVec 管理的元素为 string,而 String 管理的元素为 char。配套网站已有完整程序,读者可以尝试自己定义 String,然后与配套网站中的代码进行对照。
Debug 代码观察程序的执行流程及调用拷贝控制成员的情况。
程序及结果如下所示:
String.h
#ifndef TEST_STRING_H
#define TEST_STRING_H
#include <iostream>
using std::ostream;
#include <memory>
using std::allocator;
using std::uninitialized_copy;
using std::uninitialized_fill_n;
class String {
friend String operator+(const String&, const String&);
friend String add(const String&, const String&);
friend ostream &operator<<(ostream&, const String&);
friend ostream &print(ostream&, const String&);
public:
String() : sz(0), p(0) { }
// cp points to null terminated array, allocate new
// memory & copy the array
String(const char *cp) : sz(strlen(cp)), p(a.allocate(sz)) {
uninitialized_copy(cp, cp + sz, p);
}
String(size_t n, char c) : sz(n), p(a.allocate(n)) {
uninitialized_fill_n(p, sz, c);
}
// copy constructor: allocate a new copy of the characters in s
String(const String &s) : sz(s.sz), p(a.allocate(s.sz)) {
uninitialized_copy(s.p, s.p + sz, p);
}
// allocate a new copy of the data in the right-hand operand;
// deletes the memory used by the left-hand operand
String &operator=(const String&);
// unconditionally delete the memory because each String has
// its own memory
~String() { free(); }
public:
// additional assignment operators
String &operator=(const char*); // car = "Studebaker"
String &operator=(char); // model = 'T'
const char *begin() { return p; }
const char *begin() const { return p; }
const char *end() { return p + sz; }
const char *end() const { return p + sz; }
size_t size() const { return sz; }
void swap(String&);
private:
size_t sz;
char *p;
static allocator<char> a;
void free();
};
String make_plural(size_t ctr, const String&, const String&);
inline
void swap(String &s1, String &s2) {
s1.swap(s2);
}
#endif //TEST_STRING_H
String.cpp
#include "String.h"
#include <algorithm>
using std::for_each;
// define the static allocator member
allocator<char> String::a;
void String::free() {
if (p) {
for_each(p, p + sz, [this] (char &c) { a.destroy(&c); });
a.deallocate(p, sz);
}
}
void String::swap(String &s) {
char *tmp = p;
p = s.p;
s.p = tmp;
size_t cnt = sz;
sz = s.sz;
s.sz = cnt;
}
// copy-assignment operator
String& String::operator=(const String &rhs) {
// copying the right-hand operand before deleting
// the left handles self-assignment
char *newp = a.allocate(rhs.sz); // copy the underlying string from rhs
uninitialized_copy(rhs.p, rhs.p + rhs.sz, newp);
free(); // free the memory used by the left-hand operand
p = newp; // p now points to the newly allocated string
sz = rhs.sz; // update the size
return *this;
}
String& String::operator=(const char *cp) {
free(); // free the memory used by the left-hand operand
p = a.allocate(sz = strlen(cp));
uninitialized_copy(cp, cp + sz, p);
return *this;
}
String& String::operator=(char c) {
free(); // free the memory used by the left-hand operand
p = a.allocate(sz = 1);
*p = c;
return *this;
}
// named functions for operators
ostream &print(ostream &os, const String &s) {
const char *p = s.begin();
while (p != s.end())
os << *p++;
return os;
}
String add(const String &lhs, const String &rhs) {
String ret;
ret.sz = rhs.size() + lhs.size(); // size of the combined String
ret.p = String::a.allocate(ret.sz); // allocate new space
uninitialized_copy(lhs.begin(), lhs.end(), ret.p); // copy the operands
uninitialized_copy(rhs.begin(), rhs.end(), ret.p + lhs.sz);
return ret;
}
// return plural version of word if ctr isn't 1
String make_plural(size_t ctr, const String &word, const String &ending) {
return (ctr != 1) ? add(word, ending) : word;
}
// chapter 14 will explain overloaded operators
ostream &operator<<(ostream &os, const String &s) {
return print(os, s);
}
String operator+(const String &lhs, const String &rhs) {
return add(lhs, rhs);
}
main.cpp
#include "String.h"
#include <vector>
using std::vector;
using std::cout;
using std::endl;
int main() {
String s1("One"), s2("Two");
cout << s1 << " " << s2 << endl << endl;
String s3(s2);
cout << s1 << " " << s2 << " " << s3 << endl << endl;
s3 = s1;
cout << s1 << " " << s2 << " " << s3 << endl << endl;
s3 = String("Three");
cout << s1 << " " << s2 << " " << s3 << endl << endl;
vector<String> svec;
// svec.reserve(4);
svec.push_back(s1);
svec.push_back(std::move(s2));
svec.push_back(String("Three"));
svec.push_back("Four");
for_each(svec.begin(), svec.end(), [] (const String &s) { cout << s << " "; });
cout << endl;
return 0;
}
// 运行结果
One Two
One Two Two
One Two One
One Two Three
One Two Three Four
Process finished with exit code 0
13.45
【出题思路】
理解左值引用和右值引用。
【解答】
所谓右值引用就是必须绑定到右值的引用,通过 &&
获得。右值引用只能绑定到一个将要销毁的对象上,因此可以自由地移动其资源。
左值引用,也就是 “常规引用”,不能绑定到要转换的表达式、字面常量或返回右值的表达式。而右值引用恰好相反,可以绑定到这类表达式,但不能绑定到一个左值上。
返回左值的表达式包括:返回左值引用的函数及赋值、下标、解引用和前置递增/递减运算符;返回右值的包括:返回非引用类型的函数及算数、关系、位和后置递增/递减运算符。可以看到,左值的特点是有持久的状态,而右值则是短暂的。
13.46
【出题思路】
深入理解左值引用和右值引用。
【解答】
int f();
vector<int> vi(100);
int &&r1 = f(); // f 是返回非引用类型的函数,返回值是一个右值
int &r2 = vi[0]; // 下标运算返回的是左值
int &r3 = r1; // r1 是一个变量,而变量是左值
int &&r4 = vi[0] * f(); // vi[0] * f() 是一个算术运算表达式,返回右值
13.47
【出题思路】
理解拷贝何时发生。
【解答】
在 String.h 和 String.cpp 中添加,编写一个简单的主程序使用 String,编译运行它(debug观察程序执行过程),观察其行为即可。
程序如下所示:
String.h
#ifndef TEST_STRING_H
#define TEST_STRING_H
#include <iostream>
using std::ostream;
#include <memory>
using std::allocator;
using std::uninitialized_copy;
using std::uninitialized_fill_n;
class String {
friend String operator+(const String&, const String&);
friend String add(const String&, const String&);
friend ostream &operator<<(ostream&, const String&);
friend ostream &print(ostream&, const String&);
public:
String() : sz(0), p(0) { }
// cp points to null terminated array, allocate new
// memory & copy the array
String(const char *cp) : sz(strlen(cp)), p(a.allocate(sz)) {
uninitialized_copy(cp, cp + sz, p);
}
String(size_t n, char c) : sz(n), p(a.allocate(n)) {
uninitialized_fill_n(p, sz, c);
}
// copy constructor: allocate a new copy of the characters in s
String(const String &s) : sz(s.sz), p(a.allocate(s.sz)) {
std::cout << "copy constructor -- " << s.p << std::endl;
uninitialized_copy(s.p, s.p + sz, p);
}
// allocate a new copy of the data in the right-hand operand;
// deletes the memory used by the left-hand operand
String &operator=(const String&);
// unconditionally delete the memory because each String has
// its own memory
~String() { free(); }
public:
// additional assignment operators
String &operator=(const char*); // car = "Studebaker"
String &operator=(char); // model = 'T'
const char *begin() { return p; }
const char *begin() const { return p; }
const char *end() { return p + sz; }
const char *end() const { return p + sz; }
size_t size() const { return sz; }
void swap(String&);
private:
size_t sz;
char *p;
static allocator<char> a;
void free();
};
String make_plural(size_t ctr, const String&, const String&);
inline
void swap(String &s1, String &s2) {
s1.swap(s2);
}
#endif //TEST_STRING_H
String.cpp
#include "String.h"
#include <algorithm>
using std::for_each;
// define the static allocator member
allocator<char> String::a;
void String::free() {
if (p) {
for_each(p, p + sz, [this] (char &c) { a.destroy(&c); });
a.deallocate(p, sz);
}
}
void String::swap(String &s) {
char *tmp = p;
p = s.p;
s.p = tmp;
size_t cnt = sz;
sz = s.sz;
s.sz = cnt;
}
// copy-assignment operator
String& String::operator=(const String &rhs) {
std::cout << "copy-assignment operator -- " << rhs.p << std::endl;
// copying the right-hand operand before deleting
// the left handles self-assignment
char *newp = a.allocate(rhs.sz); // copy the underlying string from rhs
uninitialized_copy(rhs.p, rhs.p + rhs.sz, newp);
free(); // free the memory used by the left-hand operand
p = newp; // p now points to the newly allocated string
sz = rhs.sz; // update the size
return *this;
}
String& String::operator=(const char *cp) {
free(); // free the memory used by the left-hand operand
p = a.allocate(sz = strlen(cp));
uninitialized_copy(cp, cp + sz, p);
return *this;
}
String& String::operator=(char c) {
free(); // free the memory used by the left-hand operand
p = a.allocate(sz = 1);
*p = c;
return *this;
}
// named functions for operators
ostream &print(ostream &os, const String &s) {
const char *p = s.begin();
while (p != s.end())
os << *p++;
return os;
}
String add(const String &lhs, const String &rhs) {
String ret;
ret.sz = rhs.size() + lhs.size(); // size of the combined String
ret.p = String::a.allocate(ret.sz); // allocate new space
uninitialized_copy(lhs.begin(), lhs.end(), ret.p); // copy the operands
uninitialized_copy(rhs.begin(), rhs.end(), ret.p + lhs.sz);
return ret;
}
// return plural version of word if ctr isn't 1
String make_plural(size_t ctr, const String &word, const String &ending) {
return (ctr != 1) ? add(word, ending) : word;
}
// chapter 14 will explain overloaded operators
ostream &operator<<(ostream &os, const String &s) {
return print(os, s);
}
String operator+(const String &lhs, const String &rhs) {
return add(lhs, rhs);
}
main.cpp
#include "String.h"
#include <vector>
using std::vector;
using std::cout;
using std::endl;
int main() {
String s1("One"), s2("Two");
cout << s1 << " " << s2 << endl << endl;
String s3(s2);
cout << s1 << " " << s2 << " " << s3 << endl << endl;
s3 = s1;
cout << s1 << " " << s2 << " " << s3 << endl << endl;
s3 = String("Three");
cout << s1 << " " << s2 << " " << s3 << endl << endl;
vector<String> svec;
// svec.reserve(4);
svec.push_back(s1);
svec.push_back(std::move(s2));
svec.push_back(String("Three"));
svec.push_back("Four");
for_each(svec.begin(), svec.end(), [] (const String &s) { cout << s << " "; });
cout << endl;
return 0;
}
// 运行结果
One Two
copy constructor -- Two
One Two Two
copy-assignment operator -- One
One Two One
copy-assignment operator -- Three
One Two Three
copy constructor -- One
copy constructor -- Two
copy constructor -- One
copy constructor -- Three
copy constructor -- Two
copy constructor -- One
copy constructor -- Foure
One Two Three Four
Process finished with exit code 0
13.48
【出题思路】
理解拷贝何时发生。
【解答】
如上题程序。观察输出结果即可。
13.49
【出题思路】
本题练习定义移动控制成员。
【解答】
书中已有 StrVec 和 Message 类的移动构造函数和移动赋值运算符的详细设计。
StrVec
往练习 13.39 添加一个移动构造函数,一个移动赋值运算符以及改动 StrVec 的 reallocate() 成员。
代码如下所示:
StrVec.h
#ifndef TEST_STRVEC_H
#define TEST_STRVEC_H
#include <string>
using std::string;
#include <memory>
using std::allocator;
#include <utility>
using std::pair;
#include <initializer_list>
using std::initializer_list;
// 类 vector 类内存分配策略的简单实现
class StrVec {
public:
StrVec() : elements(nullptr), first_free(nullptr), cap(nullptr) { }
StrVec(const StrVec&); // 拷贝构造函数
StrVec &operator=(const StrVec&); // 拷贝赋值运算符
~StrVec(); // 析构函数
void push_back(const string&); // 拷贝元素
size_t size() const { return first_free - elements; }
size_t capacity() const { return cap - elements; }
string *begin() const { return elements; }
string *end() const { return first_free; }
// 新加的代码
// reserve 预留一部分空间,需要一个辅助函数 void reallocate(size_t)
void reserve(size_t n) { if (n > capacity()) reallocate(n); }
void resize(size_t);
void resize(size_t, const string &);
// 练习 13.40 —— 新添加的一个构造函数
StrVec(initializer_list<string>);
// 练习 13.49 —— 新添加的移动控制成员
StrVec(StrVec&&) noexcept; // 移动构造函数
StrVec &operator=(StrVec&&) noexcept; // 移动赋值运算符
private:
// 由于静态成员需要类外初始化,所以我没有把 alloc 定义为 static
// 因为我不熟悉 allocator<string> 初始化方法
allocator<string> alloc; // 分配元素
// 被添加元素的函数所使用
void chk_n_alloc() {
if (size() == capacity())
reallocate();
}
// 工具函数,被拷贝构造函数、赋值运算符和析构函数所使用
pair<string*, string*> alloc_n_copy(const string*, const string*);
void free(); // 销毁元素并释放内存
void reallocate(); // 获得更多内存并拷贝已有元素
string *elements; // 指向数组首元素的指针
string *first_free; // 指向数组第一个空闲元素的指针
string *cap; // 指向数组尾后位置的指针
// 新加的代码
void reallocate(size_t);
};
#endif //TEST_STRVEC_H
StrVec.cpp
#include "StrVec.h"
void StrVec::push_back(const string &s) {
chk_n_alloc(); // 确保有空间容纳新元素
// 在 first_free 指向的元素中构造 s 的副本
alloc.construct(first_free++, s);
}
pair<string*, string*> StrVec::alloc_n_copy(const string *b, const string *e) {
// 分配空间保存给定范围中的元素
auto data = alloc.allocate(e - b);
// 初始化并返回一个 pair,该 pair 由 data 和 uninitialized_copy 的返回值构成
return {data, uninitialized_copy(b, e, data)};
}
void StrVec::free() {
// 不能传递给 deallocate 一个空指针,如果 elements 为 0,函数什么也不做
if (elements) {
// 逆序销毁旧元素
for (auto p = first_free; p != elements; /* 空 */)
alloc.destroy(--p);
alloc.deallocate(elements, cap - elements);
}
}
StrVec::StrVec(const StrVec &s) {
// 调用 alloc_n_copy 分配空间以容纳与 s 中一样多的元素
auto newdata = alloc_n_copy(s.begin(), s.end());
elements = newdata.first;
first_free = cap = newdata.second;
}
StrVec::~StrVec() {
free();
}
StrVec& StrVec::operator=(const StrVec &rhs) {
// 调用 alloc_n_copy 分配内存,大小与 rhs 中元素占用空间一样多
auto data = alloc_n_copy(rhs.begin(), rhs.end());
free();
elements = data.first;
first_free = cap = data.second;
return *this;
}
//void StrVec::reallocate() {
// // 我们将分配当前大小两倍的内存空间
// auto newcapacity = size() ? 2 * size() : 1;
// // 分配新内存
// auto newdata = alloc.allocate(newcapacity);
// // 将数据从旧内存移动到新内存
// auto dest = newdata; // 指向新数组中下一个空闲位置
// auto elem = elements; // 指向旧数组中下一个元素
// for (size_t i = 0; i != size(); ++i)
// alloc.construct(dest++, std::move(*elem++));
// free(); // 一旦我们移动完元素就释放旧内存空间
// // 更新我们的数据结构,执行新元素
// elements = newdata;
// first_free = dest;
// cap = elements + newcapacity;
//}
void StrVec::reallocate() {
// 分配大小两倍于当前规模的内存空间
auto newcapacity = size() ? 2 * size() : 1;
auto first = alloc.allocate(newcapacity);
// 移动元素
auto last = uninitialized_copy(make_move_iterator(begin()),
make_move_iterator(end()),
first);
free(); // 释放旧空间
elements = first; // 更新指针
first_free = last;
cap = elements + newcapacity;
}
// 新加的代码,StrVec::reallocate() 函数的重载形式
void StrVec::reallocate(size_t newcapacity) {
// 分配新内存
auto newdata = alloc.allocate(newcapacity);
// 将数据从旧空间移动到新空间
auto dest = newdata; // dest 指向新空间中第一个空闲位置
auto elem = elements; // 指向旧空间中下一个元素
for (size_t i = 0; i != size(); ++i)
alloc.construct(dest++, std::move(*elem++));
free(); // 数据移动完毕,释放旧空间
// 更新指针,指向新空间开始、第一个空闲位置及末尾位置
elements = newdata;
first_free = dest;
cap = elements + newcapacity;
}
void StrVec::resize(size_t count) {
resize(count, string());
}
void StrVec::resize(size_t count, const string &s) {
if (count > size()) {
if (count > capacity())
reserve(count * 2);
for (size_t i = size(); i != count; ++i)
alloc.construct(first_free++, s);
}
else if (count < size()) {
while (first_free != elements + count)
alloc.destroy(--first_free);
}
}
StrVec::StrVec(initializer_list<string> il) {
// 调用 alloc_n_copy 分配与列表 il 中元素数目一样多的空间
auto newdata = alloc_n_copy(il.begin(), il.end());
elements = newdata.first;
first_free = cap = newdata.second;
}
StrVec::StrVec(StrVec &&s) noexcept // 移动操作不应抛出任何异常
// 成员初始化器接管 s 中的资源
: elements(s.elements), first_free(s.first_free), cap(s.cap) {
// 令 s 进入这样的状态 —— 对其运行析构函数是安全的
s.elements = s.first_free = s.cap = nullptr;
}
StrVec& StrVec::operator=(StrVec &&rhs) noexcept {
// 直接检测自赋值
if (this != &rhs) {
free(); // 释放已有元素
elements = rhs.elements; // 从 rhs 接管资源
first_free = rhs.first_free;
cap = rhs.cap;
// 将 rhs 置于可析构状态
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
Message
往练习 13.34 添加一个辅助函数,一个移动构造函数和一个移动赋值运算符。
代码如下所示:
Folder.h
#ifndef TEST_FOLDER_H
#define TEST_FOLDER_H
#include <string>
using std::string;
#include <set>
using std::set;
class Folder;
class Message {
friend void swap(Message&, Message&);
friend class Folder;
public:
// folders is implicitly initialized to the empty set
explicit Message(const string &str = "") : contents(str) { }
// copy control to manage pointers to this Message
Message(const Message&); // copy constructor
Message& operator=(const Message&); // copy assignment
~Message(); // destructor
// add/remove this Message from the specified Folder's set of messages
void save(Folder&);
void remove(Folder&);
void debug_print(); // print contents and it's list of Folders,
// printing each Folder as well
// move the Folder pointers from m to this Message
void move_Folders(Message*);
// move constructor
Message(Message&&);
// move-assignment operator
Message &operator=(Message&&);
private:
string contents; // actual message text
set<Folder*> folders; // Folders that have this Message
// utility functions used by copy constructor, assignment, and destructor
// add this Message to the Folders that point to the parameter
void add_to_Folders(const Message&);
// remove this Message from every Folder in folders
void remove_from_Folders();
// used by Folder class to add self to this Message's set of Folder's
void addFldr(Folder *f) { folders.insert(f); }
void remFldr(Folder *f) { folders.erase(f); }
};
// declaration for swap should be in the same header as Message itself
void swap(Message&, Message&);
class Folder {
friend void swap(Message&, Message&);
friend class Message;
public:
~Folder(); // remove itself from Message in msgs
Folder(const Folder&); // add new folder to each Message in msgs
Folder& operator=(const Folder&); // delete Folder from lhs messages
// add Folder to rhs messages
Folder() { } // defaults ok
void save(Message&); // add this message to folder
void remove(Message&); // remove this message from this folder
void debug_print(); // print contents and it's list of Folders
private:
set<Message*> msgs; // messages in this folder
void add_to_Messages(const Folder&); // add this Folder to each Message
void remove_from_Msgs(); // remove this Folder from each Message
void addMsg(Message *m) { msgs.insert(m); }
void remMsg(Message *m) { msgs.erase(m); }
};
#endif //TEST_FOLDER_H
Folder.cpp
#include <iostream>
using std::cerr; using std::endl;
#include <set>
using std::set;
#include <string>
using std::string;
#include "Folder.h";
void swap(Message &lhs, Message &rhs) {
using std::swap; // not strictly needed in this case, but good habit
// remove pointers to each Message from their (original) respective Folders
for (set<Folder*>::iterator f = lhs.folders.begin();
f != lhs.folders.end(); ++f)
(*f)->remMsg(&lhs);
for (set<Folder*>::iterator f = rhs.folders.begin();
f != rhs.folders.end(); ++f)
(*f)->remMsg(&rhs);
// swap the contents and Folder pointer sets
swap(lhs.folders, rhs.folders); // uses swap(set&, set&)
swap(lhs.contents, rhs.contents); // swap(string&, string&)
// add pointers to each Message to their (new) respective Folders
for (set<Folder*>::iterator f = lhs.folders.begin();
f != lhs.folders.end(); ++f)
(*f)->addMsg(&lhs);
for (set<Folder*>::iterator f = rhs.folders.begin();
f != rhs.folders.end(); ++f)
(*f)->addMsg(&rhs);
}
Message::Message(const Message &m) : contents(m.contents), folders(m.folders) {
add_to_Folders(m); // add this Message to the Folders that point to m
}
Message& Message::operator=(const Message &rhs) {
// handle self-assignment by removing pointers before inserting them
remove_from_Folders(); // update existing Folders
contents = rhs.contents; // copy message contents from rhs
folders = rhs.folders; // copy Folder pointers from rhs
add_to_Folders(rhs); // add this Message to those Folders
return *this;
}
Message::~Message() {
remove_from_Folders();
}
// add this Message to Folders that point to m
void Message::add_to_Folders(const Message &m) {
for (set<Folder*>::iterator f = m.folders.begin();
f != m.folders.end(); ++f) // for each Folder that holds m
(*f)->addMsg(this); // add a pointer to this Message to that Folder
}
// remove this Message from the corresponding Folders
void Message::remove_from_Folders() {
for (set<Folder*>::iterator f = folders.begin();
f != folders.end(); ++f) // for each pointer in folders
(*f)->remMsg(this); // remove this Message from that Folder
}
void Folder::add_to_Messages(const Folder &f) {
for (set<Message*>::iterator msg = f.msgs.begin();
msg != f.msgs.end(); ++msg)
(*msg)->addFldr(this); // add this Folder to each Message
}
Folder::Folder(const Folder &f) : msgs(f.msgs) {
add_to_Messages(f); // add this Folder to each Message in f.msgs
}
Folder& Folder::operator=(const Folder &f) {
remove_from_Msgs(); // remove this folder from each Message in msgs
msgs = f.msgs; // copy the set of Message from f
add_to_Messages(f); // add this folder to each Message in msgs
return *this;
}
Folder::~Folder() {
remove_from_Msgs();
}
void Folder::remove_from_Msgs() {
while (!msgs.empty())
(*msgs.begin())->remove(*this);
}
void Message::save(Folder &f) {
folders.insert(&f); // add the given Folder to our list of Folders
f.addMsg(this); // add this Message to f's set of Messages
}
void Message::remove(Folder &f) {
folders.erase(&f); // take the given Folder out of our list of Folders
f.remMsg(this); // remove this Message to f's set of Messages
}
void Folder::save(Message &m) {
// add m and add this folder to m's set of Folders
msgs.insert(&m);
m.addFldr(this);
}
void Folder::remove(Message &m) {
// erase m from msgs and remove this folder from m
msgs.erase(&m);
m.remFldr(this);
}
void Folder::debug_print() {
cerr << "Folder contains " << msgs.size() << " messages" << endl;
int ctr = 1;
for (set<Message*>::iterator m = msgs.begin();
m != msgs.end(); ++m) {
cerr << "Message " << ctr++ << ":\n\t" << (*m)->contents << endl;
}
}
void Message::debug_print() {
cerr << "Message:\n\t" << contents << endl;
cerr << "Appears in " << folders.size() << " Folders" << endl;
}
void Message::move_Folders(Message *m) {
folders = std::move(m->folders); // uses set move assignment
for (auto f : folders) { // for each Folder
f->remMsg(m); // remove the old Message from the Folder
f->addMsg(this); // add this Message to that Folder
}
m->folders.clear(); // ensure that destroying m is harmless
}
Message::Message(Message &&m) : contents(std::move(m.contents)) {
move_Folders(&m); // moves folders and updates the Folder pointers
}
Message& Message::operator=(Message &&rhs) {
if (this != &rhs) { // direct check for self-assignment
remove_from_Folders();
contents = std::move(rhs.contents); // move assignment
move_Folders(&rhs);
}
return *this;
}
FolderMain.cpp
#include "Folder.h"
int main() {
Message firstMail("hello");
Message signInMail("welcome to cppprimer");
Folder mailBox;
firstMail.save(mailBox);
signInMail.save(mailBox);
mailBox.debug_print();
firstMail = std::move(signInMail);
mailBox.debug_print();
return 0;
}
// 运行结果
Folder contains 2 messages
Message 1:
welcome to cppprimer
Message 2:
hello
Folder contains 1 messages
Message 1:
welcome to cppprimer
Process finished with exit code 0
String
往练习 13.44 添加一个移动构造函数,一个移动赋值运算符。原理与本节书中 StrVec 类的移动构造函数和移动赋值运算符类似。
代码如下所示:
String.h
#ifndef TEST_STRING_H
#define TEST_STRING_H
#include <iostream>
using std::ostream;
#include <memory>
using std::allocator;
using std::uninitialized_copy;
using std::uninitialized_fill_n;
class String {
friend String operator+(const String&, const String&);
friend String add(const String&, const String&);
friend ostream &operator<<(ostream&, const String&);
friend ostream &print(ostream&, const String&);
public:
String() : sz(0), p(0) { }
// cp points to null terminated array, allocate new
// memory & copy the array
String(const char *cp) : sz(strlen(cp)), p(a.allocate(sz)) {
uninitialized_copy(cp, cp + sz, p);
}
String(size_t n, char c) : sz(n), p(a.allocate(n)) {
uninitialized_fill_n(p, sz, c);
}
// copy constructor: allocate a new copy of the characters in s
String(const String &s) : sz(s.sz), p(a.allocate(s.sz)) {
uninitialized_copy(s.p, s.p + sz, p);
}
// allocate a new copy of the data in the right-hand operand;
// deletes the memory used by the left-hand operand
String &operator=(const String&);
// unconditionally delete the memory because each String has
// its own memory
~String() { free(); }
// exercise 13.49
// move constructor
String(String&&) noexcept;
// move-assignment operator
String &operator=(String&&) noexcept;
public:
// additional assignment operators
String &operator=(const char*); // car = "Studebaker"
String &operator=(char); // model = 'T'
const char *begin() { return p; }
const char *begin() const { return p; }
const char *end() { return p + sz; }
const char *end() const { return p + sz; }
size_t size() const { return sz; }
void swap(String&);
private:
size_t sz;
char *p;
static allocator<char> a;
void free();
};
String make_plural(size_t ctr, const String&, const String&);
inline
void swap(String &s1, String &s2) {
s1.swap(s2);
}
#endif //TEST_STRING_H
String.cpp
#include "String.h"
#include <algorithm>
using std::for_each;
// define the static allocator member
allocator<char> String::a;
void String::free() {
if (p) {
for_each(p, p + sz, [this] (char &c) { a.destroy(&c); });
a.deallocate(p, sz);
}
}
void String::swap(String &s) {
char *tmp = p;
p = s.p;
s.p = tmp;
size_t cnt = sz;
sz = s.sz;
s.sz = cnt;
}
// copy-assignment operator
String& String::operator=(const String &rhs) {
// copying the right-hand operand before deleting
// the left handles self-assignment
char *newp = a.allocate(rhs.sz); // copy the underlying string from rhs
uninitialized_copy(rhs.p, rhs.p + rhs.sz, newp);
free(); // free the memory used by the left-hand operand
p = newp; // p now points to the newly allocated string
sz = rhs.sz; // update the size
return *this;
}
String& String::operator=(const char *cp) {
free(); // free the memory used by the left-hand operand
p = a.allocate(sz = strlen(cp));
uninitialized_copy(cp, cp + sz, p);
return *this;
}
String& String::operator=(char c) {
free(); // free the memory used by the left-hand operand
p = a.allocate(sz = 1);
*p = c;
return *this;
}
// named functions for operators
ostream &print(ostream &os, const String &s) {
const char *p = s.begin();
while (p != s.end())
os << *p++;
return os;
}
String add(const String &lhs, const String &rhs) {
String ret;
ret.sz = rhs.size() + lhs.size(); // size of the combined String
ret.p = String::a.allocate(ret.sz); // allocate new space
uninitialized_copy(lhs.begin(), lhs.end(), ret.p); // copy the operands
uninitialized_copy(rhs.begin(), rhs.end(), ret.p + lhs.sz);
return ret;
}
// return plural version of word if ctr isn't 1
String make_plural(size_t ctr, const String &word, const String &ending) {
return (ctr != 1) ? add(word, ending) : word;
}
// chapter 14 will explain overloaded operators
ostream &operator<<(ostream &os, const String &s) {
return print(os, s);
}
String operator+(const String &lhs, const String &rhs) {
return add(lhs, rhs);
}
// exercise 13.49
String::String(String &&s) noexcept // 移动操作不应抛出任何异常
// 成员初始化器接管 s 中的资源
: sz(s.sz), p(s.p) {
// 令 s 进入这样的状态 —— 对其运行析构函数是安全的
s.sz = 0;
s.p = nullptr;
}
String& String::operator=(String &&rhs) noexcept {
// 直接检测自赋值
if (this != &rhs) {
free(); // 释放已有元素
p = rhs.p; // 从 rhs 接管资源
sz = rhs.sz;
// 将 rhs 置于可析构状态
rhs.p = nullptr;
rhs.sz = 0;
}
return *this;
}
13.50
【出题思路】
进一步理解何时使用拷贝控制成员,何时使用移动控制成员。
【解答】
String.h
#ifndef TEST_STRING_H
#define TEST_STRING_H
#include <iostream>
using std::ostream;
#include <memory>
using std::allocator;
using std::uninitialized_copy;
using std::uninitialized_fill_n;
class String {
friend String operator+(const String&, const String&);
friend String add(const String&, const String&);
friend ostream &operator<<(ostream&, const String&);
friend ostream &print(ostream&, const String&);
public:
String() : sz(0), p(0) { }
// cp points to null terminated array, allocate new
// memory & copy the array
String(const char *cp) : sz(strlen(cp)), p(a.allocate(sz)) {
uninitialized_copy(cp, cp + sz, p);
}
String(size_t n, char c) : sz(n), p(a.allocate(n)) {
uninitialized_fill_n(p, sz, c);
}
// copy constructor: allocate a new copy of the characters in s
String(const String &s) : sz(s.sz), p(a.allocate(s.sz)) {
std::cout << "copy constructor -- " << s.p << std::endl;
uninitialized_copy(s.p, s.p + sz, p);
}
// allocate a new copy of the data in the right-hand operand;
// deletes the memory used by the left-hand operand
String &operator=(const String&);
// unconditionally delete the memory because each String has
// its own memory
~String() { free(); }
// exercise 13.49
// move constructor
String(String&&) noexcept;
// move-assignment operator
String &operator=(String&&) noexcept;
public:
// additional assignment operators
String &operator=(const char*); // car = "Studebaker"
String &operator=(char); // model = 'T'
const char *begin() { return p; }
const char *begin() const { return p; }
const char *end() { return p + sz; }
const char *end() const { return p + sz; }
size_t size() const { return sz; }
void swap(String&);
private:
size_t sz;
char *p;
static allocator<char> a;
void free();
};
String make_plural(size_t ctr, const String&, const String&);
inline
void swap(String &s1, String &s2) {
s1.swap(s2);
}
#endif //TEST_STRING_H
String.cpp
#include "String.h"
#include <algorithm>
using std::for_each;
// define the static allocator member
allocator<char> String::a;
void String::free() {
if (p) {
for_each(p, p + sz, [this] (char &c) { a.destroy(&c); });
a.deallocate(p, sz);
}
}
void String::swap(String &s) {
char *tmp = p;
p = s.p;
s.p = tmp;
size_t cnt = sz;
sz = s.sz;
s.sz = cnt;
}
// copy-assignment operator
String& String::operator=(const String &rhs) {
std::cout << "copy-assignment operator -- " << rhs.p << std::endl;
// copying the right-hand operand before deleting
// the left handles self-assignment
char *newp = a.allocate(rhs.sz); // copy the underlying string from rhs
uninitialized_copy(rhs.p, rhs.p + rhs.sz, newp);
free(); // free the memory used by the left-hand operand
p = newp; // p now points to the newly allocated string
sz = rhs.sz; // update the size
return *this;
}
String& String::operator=(const char *cp) {
free(); // free the memory used by the left-hand operand
p = a.allocate(sz = strlen(cp));
uninitialized_copy(cp, cp + sz, p);
return *this;
}
String& String::operator=(char c) {
free(); // free the memory used by the left-hand operand
p = a.allocate(sz = 1);
*p = c;
return *this;
}
// named functions for operators
ostream &print(ostream &os, const String &s) {
const char *p = s.begin();
while (p != s.end())
os << *p++;
return os;
}
String add(const String &lhs, const String &rhs) {
String ret;
ret.sz = rhs.size() + lhs.size(); // size of the combined String
ret.p = String::a.allocate(ret.sz); // allocate new space
uninitialized_copy(lhs.begin(), lhs.end(), ret.p); // copy the operands
uninitialized_copy(rhs.begin(), rhs.end(), ret.p + lhs.sz);
return ret;
}
// return plural version of word if ctr isn't 1
String make_plural(size_t ctr, const String &word, const String &ending) {
return (ctr != 1) ? add(word, ending) : word;
}
// chapter 14 will explain overloaded operators
ostream &operator<<(ostream &os, const String &s) {
return print(os, s);
}
String operator+(const String &lhs, const String &rhs) {
return add(lhs, rhs);
}
// exercise 13.49
String::String(String &&s) noexcept // 移动操作不应抛出任何异常
// 成员初始化器接管 s 中的资源
: sz(s.sz), p(s.p) {
std::cout << "move constructor -- " << s.p << std::endl;
// 令 s 进入这样的状态 —— 对其运行析构函数是安全的
s.sz = 0;
s.p = nullptr;
}
String& String::operator=(String &&rhs) noexcept {
std::cout << "move-assignment operator -- " << rhs.p << std::endl;
// 直接检测自赋值
if (this != &rhs) {
free(); // 释放已有元素
p = rhs.p; // 从 rhs 接管资源
sz = rhs.sz;
// 将 rhs 置于可析构状态
rhs.p = nullptr;
rhs.sz = 0;
}
return *this;
}
main.cpp
#include "String.h"
#include <vector>
using std::vector;
using std::cout;
using std::endl;
int main() {
String s1("One"), s2("Two");
cout << s1 << " " << s2 << endl << endl;
String s3(s2);
cout << s1 << " " << s2 << " " << s3 << endl << endl;
s3 = s1;
cout << s1 << " " << s2 << " " << s3 << endl << endl;
s3 = String("Three");
cout << s1 << " " << s2 << " " << s3 << endl << endl;
vector<String> svec;
// svec.reserve(4);
svec.push_back(s1);
svec.push_back(std::move(s2));
svec.push_back(String("Three"));
svec.push_back("Four");
for_each(svec.begin(), svec.end(), [] (const String &s) { cout << s << " "; });
cout << endl;
return 0;
}
// 运行结果
One Two
copy constructor -- Two
One Two Two
copy-assignment operator -- One
One Two One
move-assignment operator -- Three
One Two Three
copy constructor -- One
move constructor -- Two
move constructor -- One
move constructor -- Three
move constructor -- Two
move constructor -- One
move constructor -- Four
One Two Three Four
Process finished with exit code 0
在拷贝/移动构造函数和拷贝/移动赋值运算符中添加打印语句,运行练习 13.47 中的程序,观察输出结果即可。可以看到,vector 操作部分输出了以下内容:
copy constructor -- One
move constructor -- Two
move constructor -- One
move constructor -- Three
move constructor -- Two
move constructor -- One
move constructor -- Four
One Two Three Four
容易看出,
svec.push_back(s1); // 触发一次拷贝构造,对应第一行输出
svec.push_back(std::move(s2)); // 触发一次移动构造,对应第二行输出
svec.push_back(String("Three")); // 触发一次移动构造,对应第四行输出
svec.push_back("Four"); // 触发一次移动构造,对应第七行输出
那么,其他几次(移动)构造函数是如何触发的呢?
回忆一下,默认初始化的 vector 不分配内存空间。当 push_back 发现 vector 空间不足以容纳新元素时,分配新的空间(通常是加倍),将数据移动到新的空间中(由于 String 定义了移动构造函数,这里确实是 “移动” 而非 “拷贝”),然后释放旧空间。
因此,当插入 s2 时,空间由 1 扩为 2,并将原有元素(One)移动到新空间,对应第三行输出。
当插入 Three 时,空间由 2 扩为 4,将 Two、One 移动到新空间,产生两次移动构造,对应第五、六两行输出。
尝试在创建 vector 后为它预留足够空间:vs.reserve(4)
,则输出为:
copy constructor -- One
move constructor -- Two
move constructor -- Three
move constructor -- Four
One Two Three Four
因空间扩展引起的移动构造就不存在了。
13.51
【出题思路】
理解不可拷贝类型的例外及移动控制成员的触发条件。
不可拷贝类型,比如 unique_ptr类 和 IO类。
【解答】
unique_ptr 不能拷贝,但有一个例外 —— 将要被销毁的 unique_ptr 是可以拷贝或销毁的。因此,在 418 页的 clone 函数中返回局部 unique_ptr 对象 ret 是可以的,因为 ret 马上就要被销毁了。而此时 “拷贝” 其实是触发移动构造函数进行了移动。
13.52
【出题思路】
理解移动控制成员的执行过程。
【解答】
对 hp = hp2
,因为 hp2 是一个变量,变量是一个左值,因此它传递给赋值运算符参数 rhs 的过程是拷贝构造过程,rhs 获得 h2 的一个副本,rhs.ps 与 h2.ps 指向不同的 string,但两个 string 包含相同的内容。在赋值运算符中,交换 hp 和 rhs,rhs 指向 hp 原来的 string,在赋值结束后 rhs 被销毁。最终结果,hp 和 hp2 指向两个独立的 string,但内容相同。
对 hp = std::move(hp2)
,hp2 传递给赋值运算符参数 rhs 的过程是移动构造过程,rhs.ps 指向 hp2.ps 原来的 string,hp2 的 ps 被设置为空指针。然后在赋值运算符中,交换 hp 和 rhs,rhs 指向 hp 原来的 string,在赋值结束后 rhs 被销毁。最终结果,hp 指向 hp2 原来的 string,而 hp2 则变为空。
13.53
【出题思路】
从性能角度考虑移动控制成员的定义。
Why is it not efficient to use a single assignment operator handling both copy and move assignment?
【解答】
在进行拷贝赋值时,先通过拷贝构造创建了 hp2 的拷贝 rhs,然后再交换 hp 和 rhs,rhs 作为一个中间媒介,只是起到将值从 hp2 传递给 hp 的作用,是一个冗余的操作。
类似的,在进行移动赋值时,先从 hp2 转移到 rhs,在交换到 hp,也是冗余的。
也就是说,这种实现方式唯一的用处是统一了拷贝赋值运算和移动赋值运算,但在性能角度,多了一次从 rhs 的间接传递,性能不好。
程序如下所示:
HasPtr.h
#ifndef TEST_HASPTR_H
#define TEST_HASPTR_H
#include <string>
class HasPtr {
friend void swap(HasPtr &, HasPtr &);
public:
HasPtr(const std::string &s = std::string());
HasPtr(const HasPtr &); // 拷贝构造函数
HasPtr &operator=(HasPtr); // 赋值运算符(拷贝并交换版本)
HasPtr(HasPtr&&) noexcept; // 移动构造函数
~HasPtr(); // 析构函数
// HasPtr &operator=(const HasPtr &); // 拷贝赋值运算符
// HasPtr &operator=(HasPtr&&) noexcept; // 移动赋值运算符
private:
std::string *ps;
int i;
};
#endif //TEST_HASPTR_H
HasPtr.cpp
#include "HasPtr.h"
#include <iostream>
inline
void swap(HasPtr &lhs, HasPtr &rhs) {
using std::swap;
swap(lhs.ps, rhs.ps); // 交换指针,而不是 string 数据
swap(lhs.i, rhs.i); // 交换 int 成员
}
HasPtr::HasPtr(const std::string &s)
: ps(new std::string(s)), i(0) {
std::cout << "constructor -- " << s << std::endl;
}
HasPtr::HasPtr(const HasPtr &orig) :
ps(new std::string(*orig.ps)), i(orig.i) {
std::cout << "copy constructor -- " << *orig.ps << std::endl;
}
// 注意 rhs 是按值传递的,意味着 HasPtr 的拷贝构造函数
// 将右侧运算对象中 string 拷贝到 rhs
HasPtr& HasPtr::operator=(HasPtr rhs) {
// 交换左侧运算对象和局部变量 rhs 的内容
swap(*this, rhs); // rhs 现在指向本对象曾经使用的内存
return *this; // rhs 被销毁,从而 delete 了 rhs 中的指针
}
HasPtr::HasPtr(HasPtr &&p) noexcept // 移动操作不应抛出任何异常
// 成员初始化器接管 p 中的资源
: ps(p.ps), i(p.i) {
std::cout << "move constructor -- " << *p.ps << std::endl;
// 令 p 进入这样的状态 —— 对其运行析构函数是安全的
p.ps = nullptr;
p.i = 0;
}
HasPtr::~HasPtr() {
delete ps; // 释放 string 内存
}
//HasPtr& HasPtr::operator=(const HasPtr &rhs) {
// std::cout << "copy-assignment operator -- " << *rhs.ps << std::endl;
//
// std::string *newps = new std::string(*rhs.ps); // 拷贝指针指向的对象
// delete ps; // 销毁原 string
// ps = newps; // 指向新 string
// i = rhs.i; // 使用内置的 int 赋值
// return *this; // 返回一个此对象的引用
//}
//
//HasPtr& HasPtr::operator=(HasPtr &&rhs) noexcept {
// std::cout << "move-assignment operator -- " << *rhs.ps << std::endl;
//
// // 直接检测自赋值
// if (this != &rhs) {
// delete ps; // 释放旧 string
// ps = rhs.ps; // 从 rhs 接管 string
// rhs.ps = nullptr; // 将 rhs 置于可析构状态
// rhs.i = 0;
// }
// return *this;
//}
main.cpp
#include "HasPtr.h"
int main() {
HasPtr h("hi mom!");
HasPtr h2(h);
HasPtr h3 = h;
h2 = h3;
h2 = std::move(h3);
return 0;
}
// 运行结果
constructor -- hi mom!
copy constructor -- hi mom!
copy constructor -- hi mom!
copy constructor -- hi mom!
move constructor -- hi mom!
Process finished with exit code 0
当注释掉赋值运算符的声明与实现,取消拷贝赋值运算符和移动赋值运算符的声明与实现的注释后,再次运行程序。结果如下:
constructor -- hi mom!
copy constructor -- hi mom!
copy constructor -- hi mom!
copy-assignment operator -- hi mom!
move-assignment operator -- hi mom!
Process finished with exit code 0
13.54
【出题思路】
理解两种赋值运算符的关系(赋值运算符(拷贝交换版本)和移动赋值运算符)
【解答】
会产生编译错误。
因为对于 h2 = std::move(h3);
这样的赋值语句来说,两个运算符匹配的一样好,从而产生了二义性。
程序如下所示:
HasPtr.h
#ifndef TEST_HASPTR_H
#define TEST_HASPTR_H
#include <string>
class HasPtr {
friend void swap(HasPtr &, HasPtr &);
public:
HasPtr(const std::string &s = std::string());
HasPtr(const HasPtr &); // 拷贝构造函数
HasPtr &operator=(HasPtr); // 赋值运算符(拷贝并交换版本)
HasPtr(HasPtr&&) noexcept; // 移动构造函数
~HasPtr(); // 析构函数
// HasPtr &operator=(const HasPtr &); // 拷贝赋值运算符
HasPtr &operator=(HasPtr&&) noexcept; // 移动赋值运算符
private:
std::string *ps;
int i;
};
#endif //TEST_HASPTR_H
HasPtr.cpp
#include "HasPtr.h"
#include <iostream>
inline
void swap(HasPtr &lhs, HasPtr &rhs) {
using std::swap;
swap(lhs.ps, rhs.ps); // 交换指针,而不是 string 数据
swap(lhs.i, rhs.i); // 交换 int 成员
}
HasPtr::HasPtr(const std::string &s)
: ps(new std::string(s)), i(0) {
std::cout << "constructor -- " << s << std::endl;
}
HasPtr::HasPtr(const HasPtr &orig) :
ps(new std::string(*orig.ps)), i(orig.i) {
std::cout << "copy constructor -- " << *orig.ps << std::endl;
}
// 注意 rhs 是按值传递的,意味着 HasPtr 的拷贝构造函数
// 将右侧运算对象中 string 拷贝到 rhs
HasPtr& HasPtr::operator=(HasPtr rhs) {
// 交换左侧运算对象和局部变量 rhs 的内容
swap(*this, rhs); // rhs 现在指向本对象曾经使用的内存
return *this; // rhs 被销毁,从而 delete 了 rhs 中的指针
}
HasPtr::HasPtr(HasPtr &&p) noexcept // 移动操作不应抛出任何异常
// 成员初始化器接管 p 中的资源
: ps(p.ps), i(p.i) {
std::cout << "move constructor -- " << *p.ps << std::endl;
// 令 p 进入这样的状态 —— 对其运行析构函数是安全的
p.ps = nullptr;
p.i = 0;
}
HasPtr::~HasPtr() {
delete ps; // 释放 string 内存
}
//HasPtr& HasPtr::operator=(const HasPtr &rhs) {
// std::cout << "copy-assignment operator -- " << *rhs.ps << std::endl;
//
// std::string *newps = new std::string(*rhs.ps); // 拷贝指针指向的对象
// delete ps; // 销毁原 string
// ps = newps; // 指向新 string
// i = rhs.i; // 使用内置的 int 赋值
// return *this; // 返回一个此对象的引用
//}
HasPtr& HasPtr::operator=(HasPtr &&rhs) noexcept {
std::cout << "move-assignment operator -- " << *rhs.ps << std::endl;
// 直接检测自赋值
if (this != &rhs) {
delete ps; // 释放旧 string
ps = rhs.ps; // 从 rhs 接管 string
rhs.ps = nullptr; // 将 rhs 置于可析构状态
rhs.i = 0;
}
return *this;
}
main.cpp
#include "HasPtr.h"
int main() {
HasPtr h("hi mom!");
HasPtr h2(h);
HasPtr h3 = h;
h2 = h3;
h2 = std::move(h3);
return 0;
}
// 编译运行结果
error: use of overloaded operator '=' is ambiguous (with operand types 'HasPtr' and 'typename remove_reference<HasPtr &>::type' (aka 'HasPtr'))
h2 = std::move(h3);
13.55
【出题思路】
练习定义右值引用版本成员函数。
【解答】
定义如下,与左值引用版本的差别除了参数类型外,就是将参数用 move 处理后使用。
void push_bach(string &&t) { data->push_back(std::move(t)); }
13.56
【出题思路】
理解左值引用和右值引用版本的成员函数。
【解答】
首先,局部变量 ret 拷贝了被调用对象的一个副本。然后,对 ret 调用 sorted,由于并非是函数返回语句或函数结束(虽然写成一条语句,但执行过程是先调用 sorted,然后将结果返回),因此编译器认为它是左值,仍然调用左值引用版本,产生递归循环。
利用右值引用版本来完成排序的期望不能实现。
代码如下所示:
#include <vector>
#include <algorithm>
#include <iostream>
class Foo {
public:
Foo sorted() &&; // 可用于可改变的右值
Foo sorted() const &; // 可用于任何类型的 Foo
// Foo 的其他成员的定义
private:
std::vector<int> data;
};
// 本对象为右值,因此可以原址排序
Foo Foo::sorted() && {
std::cout << "右值引用版本" << std::endl;
sort(data.begin(), data.end());
return *this;
}
// 本对象是 const 或是一个左值,哪种情况我们都不能对其进行原址排序
Foo Foo::sorted() const & {
std::cout << "左值引用版本" << std::endl;
Foo ret(*this); // 拷贝一个副本
return ret.sorted();
}
int main() {
Foo f;
f.sorted(); // 调用返回的是引用类型,故是左值(练习 13.45 区分左值右值)
return 0;
}
// 运行结果
左值引用版本
左值引用版本
左值引用版本
... ...
左值引用版本
左值引用版本
左值引用版本
Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)
最后导致栈溢出,程序异常结束。
13.57
【出题思路】
理解左值引用和右值引用版本的成员函数。
【解答】
与上一题不同,本题的写法可以正确利用右值引用版本来完成排序。原因在于,编译器认为 Foo(*this)
是一个 “无主” 的右值,对它调用 sorted 会匹配右值引用版本。
程序如下所示:
#include <vector>
#include <algorithm>
#include <iostream>
class Foo {
public:
Foo sorted() &&; // 可用于可改变的右值
Foo sorted() const &; // 可用于任何类型的 Foo
// Foo 的其他成员的定义
private:
std::vector<int> data;
};
// 本对象为右值,因此可以原址排序
Foo Foo::sorted() && {
std::cout << "右值引用版本" << std::endl;
sort(data.begin(), data.end());
return *this;
}
// 本对象是 const 或是一个左值,哪种情况我们都不能对其进行原址排序
Foo Foo::sorted() const & {
std::cout << "左值引用版本" << std::endl;
// Foo ret(*this); // 拷贝一个副本
// return ret.sorted();
return Foo(*this).sorted();
}
int main() {
Foo f;
f.sorted();
return 0;
}
// 运行结果
左值引用版本
右值引用版本
Process finished with exit code 0
13.58
【出题思路】
理解左值引用和右值引用版本的成员函数。
可参考练习 13.45 区分左值右值。
【解答】
程序如上练习 12.56、12.57,练习 12.56 的写法会一直输出 “左值引用版本”,直至栈溢出,程序异常退出。而练习 12.57 的写法会输出一个 “左值引用版本” 和一个 “右值引用版本” 后正常结束。
评论区