《C++ Primer》第13章 13.1节习题答案

本章深入探讨了C++中的拷贝控制,包括拷贝构造函数、析构函数和拷贝赋值运算符。讲解了它们的作用、触发时机以及如何使用它们来管理动态内存。同时,引入了移动构造函数和移动赋值运算符,讨论了左值引用和右值引用的概念。通过一系列练习,读者可以掌握如何定义和使用拷贝控制成员,以实现类的正确资源管理。
摘要由CSDN通过智能技术生成

《C++ Primer》第13章 拷贝控制

本章介绍了拷贝控制成员和移动控制成员,包括:

●拷贝控制成员的概念、作用。

●如何用拷贝控制成员管理资源,来设计自己管理动态内存的类。

●移动控制成员的概念、作用。本章的练习着重帮助读者理解拷贝构造函数、析构函数、拷贝赋值运算符的基本概念,何时触发它们,它们起什么作用;理解合成拷贝控制成员的功能;练习定义拷贝控制成员来进行资源管理(特别是实现类值行为和类指针行为的类)或簿记工作;理解移动构造函数和移动赋值运算符及合成版本的概念与拷贝控制成员的区别;理解左值引用和右值引用;练习定义移动控制成员来实现资源的直接转移而非拷贝。

13.1节 拷贝、赋值与销毁 习题答案

练习13.1:拷贝构造函数是什么?什么时候使用它?

【出题思路】

理解拷贝构造函数的基本概念。

【解答】

如果构造函数的第一个参数是自身类类型的引用,且所有其他参数(如果有的话)都有默认值,则此构造函数是拷贝构造函数。拷贝构造函数在以下几种情况下会被使用:

●拷贝初始化(用=定义变量)。

●将一个对象作为实参传递给非引用类型的形参。

●一个返回类型为非引用类型的函数返回一个对象。

●用花括号列表初始化一个数组中的元素或一个聚合类中的成员。

●初始化标准库容器或调用其insert/push操作时,容器会对其元素进行拷贝初始化。

练习13.2:解释为什么下面的声明是非法的:

Sales_data::Sales_data(Sales_data &rhs);

【出题思路】

理解拷贝构造函数的参数为什么必须是引用类型。

【解答】

这一声明是非法的。因为对于上一题所述的情况,我们需要调用拷贝构造函数,但调用永远也不会成功。因为其自身的参数也是非引用类型,为了调用它,必须拷贝其实参,而为了拷贝实参,又需要调用拷贝构造函数,也就是其自身,从而造成死循环。

练习13.3:当我们拷贝一个StrBlob时,会发生什么?拷贝一个StrBlobPtr呢?

【出题思路】

理解合成的拷贝构造函数是如何工作的。

【解答】

这两个类都为定义拷贝构造函数,因此编译器为它们定义了合成的拷贝构造函数。合成的拷贝构造函数逐个拷贝非const成员,对内置类型的成员,直接进行内存拷贝,对类类型的成员,调用其拷贝构造函数进行拷贝。

因此,拷贝一个StrBlob时,拷贝其唯一的成员data,使用shared_ptr的拷贝构造函数来进行拷贝,因此其引用计数增加1。

拷贝一个StrBlobPtr时,拷贝成员wptr,用weak_ptr的拷贝构造函数进行拷贝,引用计数不变,然后拷贝curr,直接进行内存复制。

练习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;
}

【出题思路】

理解何时使用拷贝构造函数。

【解答】
如下几个地方使用了拷贝构造函数:
1、函数传参会调用拷贝构造函数
2、local = arg将arg拷贝给local。
3、*heap = new Point(global);传参会调用拷贝构造函数
4、*heap = local;将local拷贝到heap指定的地址中。
5、Point pa[4] = {local, *heap};将local和*heap拷贝给数组的前两个元素。
6、return *heap

#include <iostream>

using namespace std;

class Point{

public:
    Point():x(0){
        cout << "constructor==================start==" << this << endl;
    }

    Point(const Point &point){
        x = point.x;
        cout << "constructor==================copy==" << this << endl;
    }
    ~Point(){
        cout << "destructor===================end=" << endl;
    }

    //Point global;

    //传参会调用一次copy constructor
    Point foo_bar(Point arg)//如果是Point &arg只会调用一次copy constructor
    {
        cout << "foo_bar====================" << endl;
        Point local = arg;//这里调用copy constructor
        Point *heap = new Point(arg);//这里传参调用copy constructor
        //Point *heap = new Point();
        *heap = local; //这里调用copy constructor
        Point pa[2] = {local, *heap};//这里调用copy constructor
        cout << "foo_bar==================heap==" << heap << endl;
        return *heap;//这里调用copy constructor
    }

    int getPointX(){
        return x;
    }

private:
    int x;
};

int main()
{
    Point pa;
    pa.foo_bar(pa);

    cout << "Hello World!" << endl;
    return 0;
}

运行结果:

练习13.5:给定下面的类框架,编写一个拷贝构造函数,拷贝所有成员。你的构造函数应该动态分配一个新的string(参见12.1.2节,第407页),并将对象拷贝到ps指向的位置,而不是拷贝ps本身。

class HasPtr{
public:
    HasPtr(const std::string &s = std::string()):ps(new std::string(s), i(0))  {  }
privte:
    std::string *ps;
    int i;
};

【出题思路】

本题练习定义拷贝构造函数。

【解答】

HasPtr(const HasPtr &hasPtr){
   ps = new string(*hasPtr.ps); //重新申请内存,拷贝ps指向的对象,而不是拷贝指针本身
   i = hasPtr.i;
}

练习13.6:拷贝赋值运算符是什么?什么时候使用它?合成拷贝赋值运算符完成什么工作?什么时候会生成合成拷贝赋值运算符?

【出题思路】

理解拷贝赋值运算符的基本概念与合成的拷贝赋值运算符。

【解答】

拷贝赋值运算符本身是一个重载的赋值运算符,定义为类的成员函数,左侧运算对象绑定到隐含的this参数,而右侧运算对象是所属类类型的,作为函数的参数,函数返回指向其左侧运算对象的引用。

当对类对象进行赋值时,会使用拷贝赋值运算符。

通常情况下,合成的拷贝赋值运算符会将右侧对象的非static成员逐个赋予左侧对象的对应成员,这些赋值操作是由成员类型的拷贝赋值运算符来完成的。若一个类未定义自己的拷贝赋值运算符,编译器就会为其合成拷贝赋值运算符,完成赋值操作,但对于某些类,还会起到禁止该类型对象赋值的效果。

练习13.7:当我们将一个StrBlob赋值给另一个StrBlob时,会发生什么?赋值StrBlobPtr呢?

【出题思路】

理解合成的拷贝赋值运算符。

【解答】

由于两个类都未定义拷贝赋值运算符,因此编译器为它们定义了合成的拷贝赋值运算符。

与拷贝构造函数的行为类似,赋值一个StrBlob时,赋值其唯一的成员data,使用shared_ptr的拷贝赋值运算符来完成,因此其引用计数增加1。

赋值一个StrBlobPtr时,赋值成员wptr,用weak_ptr的拷贝赋值运算符进行赋值,引用计数不变,然后赋值curr,直接进行内存复制。

练习13.8:为13.1.1节(第443页)练习13.5中HasPtr类编写赋值运算符。类似拷贝构造函数,你的赋值运算符应该将对象拷贝到ps指向的位置。

【出题思路】

本题练习拷贝赋值运算符。

【解答】

hasPtr& HsPtr::operator=(const HasPtr &rhs)
{
    auto newps = new string(*rhs.ps);   //拷贝指针指向的对象
    delete ps;                          //销毁原string
    ps = newps;                         //指向新string
    i = rhs.i;                          //使用内置的int赋值
    return *this;                       //返回一个此对象的引用
}

练习13.9:析构函数是什么?合成析构函数完成什么工作?什么时候会生成合成析构函数?

【出题思路】

理解析构函数的基本概念与合成析构函数。

【解答】

析构函数完成与构造函数相反的工作:释放对象使用的资源,销毁非静态数据成员。从语法上看,它是类的一个成员函数,名字是波浪号接类名,没有返回值,也不接受参数。

当一个类没有定义析构函数时,编译器会为它合成析构函数。

合成的析构函数体为空,但这并不意味着它什么也不做,当空函数体执行完后,非静态数据成员会被逐个销毁。也就是说,成员是在析构函数体之后隐含的析构阶段中进行销毁的。

练习13.10:当一个StrBlob对象销毁时会发生什么?一个StrBlobPtr对象销毁时呢?

【出题思路】

理解合成的析构函数。

【解答】

这两个类都没有定义析构函数,因此编译器会为它们合成析构函数。对StrBlob,合成析构函数的空函数体执行完毕后,会进行隐含的析构阶段,销毁非静态数据成员data。这会调用shared_ptr的析构函数,将引用计数减1,引用计数变为0,会销毁共享的vector对象。

对StrBlobPtr,合成析构函数在隐含的析构阶段会销毁数据成员wptr和curr,销毁wptr会调用weak_ptr的析构函数,引用计数不变,而curr是内置类型,销毁它不会有特殊动作。

练习13.11:为前面练习中的HasPtr类添加一个析构函数。

【出题思路】

练习设计析构函数。

【解答】

只需释放string对象占用的空间即可。

~HasPtr() {delete ps;}

练习13.12:在下面的代码片段中会发生几次析构函数调用?

bool fcn(const Sales_data *trans, Sales_data accum)
{
    Sales_data item1(*trans), item2(accum);
    return item1.isbn() != item2.isbn();
}

【出题思路】

理解析构函数何时执行。

【解答】

这段代码中会发生三次析构函数调用:1.函数结束时,局部变量item1的生命期结束,被销毁,Sales_data的析构函数被调用。2.类似的,item2在函数结束时被销毁,Sales_data的析构函数被调用。3.函数结束时,参数accum的生命期结束,被销毁,Sales_data的析构函数被调用。

在函数结束时,trans的生命期也结束了,但它是Sales_data的指针,并不是它指向的Sales_data对象的生命期结束(只有delete指针时,指向的动态对象的生命期才结束),所以不会引起析构函数的调用。

练习13.13:理解拷贝控制成员和构造函数的一个好方法是定义一个简单的类,为该类定义这些成员,每个成员都打印出自己的名字:

struct X {
    X() { std::cout << "X()" << std::endl; }
    X{const X&} { std::cout << "X(const X&)" << std::endl; }
}

给X添加拷贝赋值运算符和析构函数,并编写一个程序以不同方式使用X的对象:将它们作为非引用和引用参数传递;动态分配它们;将它们存放于容器中;诸如此类。观察程序的输出,直到你确认理解了什么时候会使用拷贝控制成员,以及为什么会使用它们。当你观察程序输出时,记住编译器可以略过对拷贝构造函数的调用。

【出题思路】

理解拷贝构造函数、拷贝赋值运算符以及析构函数何时执行。

【解答】

程序如下所示。请编译运行程序,观察输出结果,仔细分析每次调用对应哪个对象。例如,程序结束时有三次析构函数的调用,分别对应x、y和vx的第一个元素。

#include <iostream>
#include <vector>

using std::cout;
using std::endl;
using std::vector;

struct X {
    X()
    {
        cout << "构造函数X()" << endl;
    }

    X(const X&)
    {
        cout << "拷贝构造函数X(const X&)" << endl;
    }

    X& operator=(const X &rhs)
    {
        cout << "拷贝赋值运算符=(const X &rhs)" << endl;
        return *this;
    }

    ~X()
    {
        cout << "析构函数~X()" << endl;
    }
};

void f1(X x)
{

}

void f2(X &x)
{

}

int main(int argc, const char * argv[])
{
    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;
    std::cout << "Hello, World!\n";
    return 0;
}

运行结果:

 练习13.14:假定mumbered是一个类,它有一个默认构造函数,能为每个对象生成一个唯一的序号,保存在名为mysn的数据成中。假定numbered使用合成的拷贝控制成员,并给定如下函数:
void f(numbered s) { cout << s.mysn << endl; }
则下面代码输出什么内容?
numbered a, b = a, c = b;
f(a); f(b); f(c);

【出题思路】

理解拷贝控制成员的应用场合。

【解答】

这是一个典型的应该定义拷贝控制成员的场合。如果不定义拷贝构造函数和拷贝赋值运算符,依赖合成的版本,则在拷贝构造和赋值时,会简单复制数据成员。对本问题来说,就是将序号简单复制给新对象。因此,代码中对a、b、c三个对象调用函数f,会输出三个相同的序号——合成拷贝构造函数被调用时简单复制序号,使得三个对象具有相同的序号。

练习13.15:假定numbered定义了一个拷贝构造函数,能生成一个新的序号。这会改变上一题中调用的输出结果吗?如果会改变,为什么?新的输出结果是什么?

【出题思路】

理解拷贝构造函数的使用。

【解答】

在此程序中,都是拷贝构造函数在起作用,因此定义能生成新的序号的拷贝构造函数会改变输出结果。

但注意,新的输出结果不是0、1、2,而是3、4、5。

因为在定义变量a时,默认构造函数起作用,将其序号设定为0。当定义b、c时,拷贝构造函数起作用,将它们的序号分别设定为1、2。但是,在每次调用函数f时,由于参数是numbered类型,又会触发拷贝构造函数,使得每一次都将形参s的序号设定为新值,从而导致三次的输出结果是3、4、5。

练习13.16:如果f中的参数是const numbered&,将会怎样?这会改变输出结果吗?如果会改变,为什么?新的输出结果是什么?

【出题思路】

理解参数类型与拷贝构造函数的关系。

【解答】

会改变输出结果,新结果是0、1、2。原因是,将参数改为const numbered &。由于形参类型由类类型变为引用类型,传递的不是类对象而是类对象的引用。这意味着调用f时不再触发拷贝构造函数将实参拷贝给形参,而是传递实参的引用。因此,对每次调用,s都是指向实参的引用,序号自然就是实参的序号。而不是创建一个新的对象,获得一个新序号。

练习13.17:分别编写前三题中所描述的numbered和f,验证你是否正确预测了输出结果。

【出题思路】

验证你对拷贝构造函数的理解。

【解答】

程序如下所示,注释部分是三题不同的部分。

#include <iostream>
#include <vector>

using std::cout;
using std::endl;
using std::vector;

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(numbered s)
{
    cout << s.mysn << endl;
}

int main(int argc, const char * argv[])
{
    numbered a, b = a, c = b;
    f(a);
    f(b);
    f(c);

    std::cout << "Hello, World!\n";
    return 0;
}

运行结果:

练习13.18:定义一个Employee类,它包含雇员的姓名和唯一的雇员证号。为这个类定义默认构造函数,以及接受一个表示雇员姓名的string的构造函数。每个构造函数应该通过递增一个static数据成员来生成一个唯一的证号。

【出题思路】

练习定义拷贝构造成员。

【解答】

程序如下所示。

#include <iostream>
#include <vector>
#include <string>

using std::cout;
using std::endl;
using std::vector;
using std::string;

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(int argc, const char * argv[])
{
    Employee a("陈"), b = a, c;
    c = b;
    f(a);
    f(b);
    f(c);
    
    std::cout << "Hello, World!\n";
    return 0;
}

运行结果:

 

练习13.19:你的Employee类需要定义它自己的拷贝控制成员吗?如果需要,为什么?如果不需要,为什么?实现你认为Employee需要的拷贝控制成员。

【出题思路】

练习定义拷贝构造成员。

【解答】

如上题程序,当用a初始化b时,会调用拷贝构造函数。如果不定义拷贝构造函数,则合成的拷贝构造函数简单复制mysn,会使两者的序号相同。当用b为c赋值时,会调用拷贝赋值运算符。如果不定义自己的版本,则编译器定义的合成版本会简单复制mysn,会使两者的序号相同。

#include <iostream>
#include <vector>
#include <string>

using std::cout;
using std::endl;
using std::vector;
using std::string;

class Employee
{
private:
    static int sn;

public:
    Employee()
    {
        mysn = sn++;
    }

    Employee(const string &s)
    {
        name = s;
        mysn = sn++;
    }

    Employee(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(int argc, const char * argv[])
{
    Employee a("陈"), b = a, c;
    c = b;
    f(a);
    f(b);
    f(c);

    std::cout << "Hello, World!\n";
    return 0;
}

运行结果:

 练习13.20:解释当我们拷贝、赋值或销毁TextQuery和QueryResult类(参见12.3节,第430页)对象时会发生什么。

【出题思路】

理解拷贝构造成员。

【解答】

两个类都未定义拷贝控制成员,因此都是编译器为它们定义合成版本。

当TextQuery销毁时,合成版本会销毁其file和wm成员。对file成员,会将shared_ptr的引用计数减1,若变为0,则销毁所管理的动态vector对象(会调用vector和string的析构函数)。对wm,调用map的析构函数(从而调用string、shared_ptr和set的析构函数),会正确释放资源。

当QueryResult销毁时,合成版本会销毁其sought、lines和file成员。类似TextQuery,string、shared_ptr、set、vector的析构函数可能被调用,因为这些类都有设计良好的拷贝控制成员,会正确释放资源。

当拷贝一个TextQuery时,合成版本会拷贝file和wm成员。对file,shared_ptr的引用计数会加1。对wm,会调用map的拷贝构造函数(继而调用string、shared_ptr和set的拷贝构造函数),因此会正确进行拷贝操作。赋值操作类似,只不过会将原来的资源释放掉,例如,原有的file的引用计数会减1。QueryResult的拷贝和赋值类似。

练习13.21:你认为TextQuery和QueryResult类需要定义它们自己版本的拷贝控制成员吗?如果需要,为什么?如果不需要,为什么?实现你认为这两个类需要的拷贝控制操作。

【出题思路】

理解拷贝构造成员。

【解答】

两个类虽然都未定义拷贝控制成员,但它们用智能指针管理共享的动态对象(输入文件内容,查询结果的行号集合),用标准库容器保存大量容器。而这些标准库机制都有设计良好的拷贝控制成员,用合成的拷贝控制成员简单地拷贝、赋值、销毁它们,即可保证正确的资源管理。因此,这两个类并不需要定义自己的拷贝控制成员。实际上,这两个类的类对象之间就存在资源共享,目前的设计已能很好地实现这种共享,同类对象之间的共享也自然能够解决。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值