C++ Primer(第五版)|练习题答案与解析(第十三章:拷贝控制)
本博客主要记录C++ Primer(第五版)中的练习题答案与解析。
参考:C++ Primer
C++ Primer
C++ Primer
练习题13.1
拷贝构造函数是什么?什么时候使用它?
P440。如果一个构造函数的第一个参数是自身类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
P441。
- 使用“=”定义变量时。
- 将一个对象作为实参传递给一个非引用类型的形参。
- 从一个返回类型为非引用类型的函数返回一个对象。
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员。
练习题13.2
解释为什么下面的声明是非法的:
Sales_data::Sales_data(Sales_data rhs);
P440,拷贝构造函数的第一个参数必须是一个引用类型。
练习题13.3
当我们拷贝一个StrBlob时,会发生什么?拷贝一个StrBlobPtr呢?
“StrBlob中元素复制,且智能指针计数加一。StrBlobStr中元素复制,弱指针复制不影响计数器”。
“拷贝StrBlob时,其shared_ptr成员的引用计数会增加。拷贝StrBlobPtr,unique_ptr成员的引用计数不变,其引用了shared_ptr,但不影响shared_ptr的引用计数。”
练习题13.4
假定Point是一个类类型,它有一个public的拷贝构造函数,指出下面程序片段中哪些地方使用了拷贝构造函数。
Point global;
Point foo_bar(Point arg) // 参数为非引用类型,需拷贝,使用了拷贝构造
{
Point local = arg, *heap = new Point(global); // 使用了拷贝构造
*heap = local;
Point pa[4] = {
local, *heap}; // 使用了拷贝构造
return *heap; // 函数的返回类型非引用,也需要进行拷贝,使用了拷贝构造
}
练习题13.5
给定下面的类框架,编写一个拷贝构造函数,拷贝所有成员。你的构造函数应该动态分配一个新的string,并将对象拷贝到ps指向的位置,而不是ps本身的位置。
class HasPtr {
public:
HasPtr (const std::string &s = std::string()) : ps (new std::string(s)), i(0){
}
// 拷贝构造函数
HasPtr(const HasPtr& hp) : ps (new std::string(*hp.ps)), i (hp.i) {
}
private:
std::string *ps;
int i;
}
练习题13.6
拷贝赋值运算符是什么?什么时候使用它?合成拷贝赋值运算符完成什么工作?什么时候会生成合成拷贝赋值运算符?
P443,拷贝赋值运算符是重载”=“运算符,即为一个名为operator=的函数,接受一个与其所在类相同类型的参数,在发生赋值操作的时候使用。
P444,合成拷贝赋值运算符将右侧运算对象的每个非static成员赋予左侧运算对象的对应成员,返回一个指向其左侧运算对象的引用。当类未定义自己的拷贝赋值运算符,编译器会生成一个合成拷贝运算符。
练习题13.7
当我们将一个StrBlob赋值给另一个StrBlob时,会发生什么?赋值StrBlobPtr呢?
会发生浅拷贝,所有的指针都指向同一块内存。赋值StrBlob时,智能指针所指对象内存相同,shared_ptr的引用计数加1,赋值StrBlobPtr时,弱指针所致对象内存相同,引用计数不变。
练习题13.8
为13.1.1节练习13.5中的HasPtr类编写赋值运算符。类似拷贝构造函数,你的赋值运算符应该将对象拷贝到ps指向的位置。
#include <string>
class HasPtr {
public:
HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0) {
}
HasPtr(const HasPtr &hp) : ps(new std::string(*hp.ps)), i(hp.i) {
}
HasPtr& operator=(const HasPtr &rhs_hp) {
//赋值运算符
if(this != &rhs_hp){
std::string *temp_ps = new std::string(*rhs_hp.ps);
delete ps;
ps = temp_ps;
i = rhs_hp.i;
}
return *this;
}
private:
std::string *ps;
int i;
};
练习题13.9
析构函数是什么?合成析构函数完成什么工作?什么时候会生成合成析构函数?
P444,析构函数执行与构造函数相反的操作,释放对象使用的资源,并销毁对象的非static数据成员。
P446,对于某些类,合成析构函数被用来阻止该类型的对象被销毁。如果不是这种情况,合成析构函数的函数体就为空。
当一个类未定义自己的析构函数时,编译器会为它定义一个合成析构函数。
练习题13.10
当一个StrBlob对象销毁时会发生什么?一个StrBlobPtr对象销毁时呢?
“销毁StrBlob时,分别会执行vector、shared_ptr、string的析构函数,vector析构函数会销毁我们添加到vector中的元素,shared_ptr析构函数会递减StrBlob对象的引用计数。”
练习题13.11
为前面练习中的HasPtr类添加一个析构函数。
#include <string>
class HasPtr {
public:
HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0) {
}
HasPtr(const HasPtr &hp) : ps(new std::string(*hp.ps)), i(hp.i) {
}
HasPtr& operator=(const HasPtr &hp) {
std::string *new_ps = new std::string(*hp.ps);
delete ps;
ps = new_ps;
i = hp.i;
return *this;
}
~HasPtr() {
//析构函数
delete ps;
}
private:
std::string *ps;
int i;
};
练习题13.12
在下面的代码片段中会发生几次析构函数调用?
bool fcn (const Sales_data *trans, Sales_data accum)
{
Sales_data item1 (*trans), item2 (accum);
return item1.isbn() != item2.isbn();
}
退出作用域时,item1和item2会调用析构函数。accum应该也会调用析构函数,trans没有。
P446,当一个对象的引用或指针离开作用域,并不会执行析构。
练习题13.13
理解拷贝控制成员和构造函数的一个好方法时定义一个简单的类,为该类定义这些成员,每个成员都打印出自己的名字:
struct X {
X() { std::cout << “X()” << std::endl; }
X(const X&) { std::cout << “X(const X&)” << std::endl; }
};
给X添加拷贝赋值运算符和析构函数,并编写一个程序以不同方式使用X的对象:将它们作为非引用和引用参数传递;动态分配它们;将它们存放于容器中;诸如此类。观察程序的输出,直到你确认理解了什么时候会使用拷贝控制成员,以及为什么会使用它们。当你观察程序输出时,记住编译器可以略过对拷贝构造函数的调用。
#include <iostream>
#include <vector>
#include <initializer_list>
struct X {
X() {
std::cout << "X()" << std::endl; }
X(const X&) {
std::cout << "X(const X&)" << std::endl; }
X& operator=(const X&) {
std::cout << "X& operator=(const X&)" << std::endl; return *this; }//拷贝赋值预算符
~X() {
std::cout << "~X()" << std::endl; }//析构函数
};
void f(const X &rx, X x)//X x这个也要使用构造函数,也会调用析构
{
std::vector<X> vec;
vec.push_back(rx);
vec.push_back(x);
std::cout << "-------离开f1作用域销毁-----" << std::endl;
}
void f2(const X &rx)
{
std::cout << "-------离开f2作用域销毁-----" << std::endl;
}
void f3(X x)//X x这个也要使用构造函数,也会调用析构
{
std::cout << "-------离开f3作用域销毁-----" << std::endl;
}
int main()
{
std::cout << "-------创建-----" << std::endl;
X *px = new X;
X x;
std::cout << "-------函数作用域1-----" << std::endl;
f(*px, *px);
std::cout << "-------函数作用域2-----" << std::endl;
f2(*px);
std::cout << "-------函数作用域3-----" << std::endl;
f3(x);
std::cout << "-------函数作用域1-----" << std::endl;
f(x, x);
std::cout << "-------销毁-----" << std::endl;
delete px;
std::cout << "-------程序结束销毁-----" << std::endl;
return 0;
}
测试:
-------创建-----
X()
X()
-------函数作用域1-----
X(const X&)
X(const X&)
X(const X&)
X(const X&)
~X()
-------离开f1作用域销毁-----
~X()
~X()
~X()
-------函数作用域2-----
-------离开f2作用域销毁-----
-------函数作用域3-----
X(const X&)
-------离开f3作用域销毁-----
~X()
-------函数作用域1-----
X(const X&)
X(const X&)
X(const X&)
X(const X&)
~X()
-------离开f1作用域销毁-----
~X()
~X()
~X()
-------销毁-----
~X()
-------程序结束销毁-----
~X()
练习题13.14
假定numbered是一个类,它有一个默认构造函数,能为每个对象生成一个唯一的序号,保存在名为mysn的数据成员中。假定numbered使用合成的拷贝控制成员,并给定如下函数:
void f (numbered s) { cout << s.mysn << endl; }
则下面代码输出什么内容?
numbered a, b = a, c = b;
f(a); f(b); f©;
会输出三个相同的序号。因为是合成拷贝,都指向同一块内存,所以a, b, c实际使用同一个mysn结构。
练习题13.15
假定numbered定义了一个拷贝构造函数,能生成一个新的序号。这会改变上一题中调用的输出结果吗?如果会改变,为什么?新的输出结果是什么?
会改变,因为调用f函数传参时以传值方式传递,需要调用拷贝构造函数,会生成一个新的序号,输出只有三个不同的序号。(一共生成6个不同的数字)
练习题13.16
如果f中的参数是const numbered&,将会怎样?这会改变输出结果吗?如果会改变,为什么?新的输出结果是什么?
参考13.13,如果传的是引用,则传递时numbered对象不会调用拷贝构造,也不会调用析构。但是使用拷贝,因此a,b,c均不同,因此三个输出不同。
练习题13.17
分别编写前三题中描述的numbered和f,验证你是否正确预测了输出结果。
#include <iostream>
//测试1
class numbered1 {
public:
numbered1() {
mysn = unique++;
}
int mysn;
static int unique;
};
int numbered1::unique = 10;
void f(numbered1 s) {
std::cout << s.mysn << std::endl;
}
//测试2
class numbered2 {
public:
numbered2() {
mysn = unique++;
}
numbered2(const numbered2& n) {
mysn = unique++;
std::cout << "使用拷贝构造" << std::endl;
}
int mysn;
static int unique;
};
int numbered2::unique = 10;
void f(numbered2 s) {
std::cout << s.mysn << std::endl;
}
//测试3
class numbered3 {
public:
numbered3() {
mysn = unique++;
}
numbered3(const numbered3& n) {
mysn = unique++;
std::cout << "使用拷贝构造" << std::endl;
}
int mysn;
static int unique;
};
int numbered3::unique = 10;
void f(const numbered3& s) {
std::cout << s.mysn << std::endl;
}
int main()
{
//测试1
std::cout << "测试1" << std::endl;
numbered1 a1, b1 = a1, c1 = b1;//10
f(a1);f(b1);f(c1);
//测试2
std::cout << "测试2" << std::endl;
numbered2 a2, b2 = a2, c2 = b2;//numbered2(const numbered2& n)
//a2.mysn = 10, b2=a2 -> b2.mysn = 11, c2 = b2 -> c2.mysn = 12;
//unique 是静态变量,拷贝完后不会被释放。
f(a2);f(b2);f(c2);//numbered2 s会使用构造函数,参考13.13
//测试3
std::cout << "测试3" << std::endl;
numbered3 a3, b3 = a3, c3 = b3;
f(a3);f(b3);f(c3);//const numbered3& s不会使用构造函数,参考13.13
}
测试1
10
10
10
测试2
使用拷贝构造
使用拷贝构造
使用拷贝构造
13
使用拷贝构造
14
使用拷贝构造
15
测试3
使用拷贝构造
使用拷贝构造
10
11
12
练习题13.18
定义一个Employee类,它包含雇员的姓名和唯一的雇员证号。为这个类定义默认构造函数,以及接受一个表示雇员姓名的string的构造函数。每个构造函数应该通过递增一个static数据成员来生成一个唯一的证号。
#include <string>
using std::string;
class Employee {
public:
Employee();
Employee(const string &name);
const int id() const {
return id_; }
private:
string name_;
int id_;
static int s_increment;
};
int Employee::s_increment = 0;
//默认构造函数
Employee::Employee() {
id_ = s_increment++;
}
//接受一个雇员姓名的构造函数
Employee::Employee(const string &name) {
id_ = s_increment++;
name_ = name;
}
int main()
{
return 0;
}
练习题13.19
你的Employee类需要定义它自己的拷贝控制成员吗?如果需要,为什么?如果不需要,为什么?实现你认为Employee需要的拷贝控制成员。
不需要拷贝控制成员,不存在两个id和name都相同的雇员,因为至少每个雇员的ID都不同。
#include <string>
using std::string;
class Employee {
public:
Employee();
Employee(const string &name);
Employee(const Employee&) = delete;
Employee& operator=(const Employee&) = delete;
const int id() const {
return id_; }
private:
string name_;
int id_;
static int s_increment;
};
练习题13.20
你的Employee类需要定义它自己的拷贝控制成员吗?如果需要,为什么?如果不需要,为什么?实现你认为Employee需要的拷贝控制成员。
因为这两个类中使用的是智能指针(shared_ptr
),因此在拷贝时,类的所有成员都将被拷贝,在销毁时所有成员也将被销毁。
练习题13.21
你认为TextQuery和QueryResult类需要定义它们自己版本的拷贝控制成员吗?如果需要,为什么?如果不需要,为什么?实现你认为这两个类需要的拷贝控制操作。
P447,当我们决定一个类是否需要自己版本的拷贝控制成员,一个基本原则是首先确定这个类是否需要一个析构函数。
TextQuery和QueryResult类使用智能指针,可以自动控制释放内存(shared_ptr
),因为其不需要自己版本的析构函数,所以不需要自己版本的拷贝控制函数了。
练习题13.22
假定我们希望HasPtr的行为像一个值。即,对于对象所指向的string成员,每个对象都有一份自己的拷贝。我们将在下一节介绍拷贝控制成员的定义。但是,你以及学习了定义这些成员所需要的所有知识。在继续学习下一节之前,为HasPtr编写拷贝构造函数和拷贝赋值运算符。
#include <string>
class HasPtr {
public:
HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0) {
}
HasPtr(const HasPtr &hp) : ps(new std::string(*hp.ps)), i(hp.i) {
}
HasPtr& operator=(const HasPtr &hp) {
auto new_p = new std::string(*hp.ps);
delete ps;
ps = new_p;
i = hp.i;
return *this;
}
~HasPtr() {
delete ps;
}
private:
std::string *ps;
int i;
};
练习题13.24
如果本节中的HasPtr版本未定义析构函数,将会发生什么?如果未定义拷贝构造函数,将会发生什么?
“若未定义析构函数,则每次类销毁时都不会释放ps指向内存,造成内存泄漏。”
“如果未定义拷贝构造函数,则如果使用另外一个HasPtr类对象构造新的HasPtr类对象,则两个对象的ps指向同一块内存。如果要销毁这两个对象,就会造成同一块内存被释放两次。”
练习题13.25
假定希望定义StrBlob的类值版本,而且希望继续使用shared_ptr,这样我们的StrBlobPtr类就仍能使用指向vector的weak_ptr了。你修改后的类将需要一个拷贝构造函数和一个拷贝赋值运算符,但不需要析构函数。解释拷贝构造函数的拷贝赋值运算符必须要做什么。解释为什么不需要析构函数。
“拷贝构造函数和拷贝赋值函数的作用是:保证类的对象在拷贝时可以自动分配内存,而不是指向右值的内存。
不需要析构函数的原因:StrBlob类中使用的是shared_ptr,可以自动管理内存,在离开作用域时自动销毁。”
练习题13.26
对上一题描述中的StrBlob类,编写你自己的版本。
在StrBlob类中添加智能指针:
StrBlob (const StrBlob& sb)
{
data = make_shared<vector<string>>(*sb.data);
}
StrBlob& operator= (const StrBlob& sb)
{
data = make_shared<std::vector<string>>(*sb.data);
return *this;
}
练习题13.27
定义你自己的使用引用计数版本的HasPtr。
#include <string>
class