C++ Primer(第五版)|练习题答案与解析(第十三章:拷贝控制)

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 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值