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

本文深入探讨了C++中的对象拷贝控制,包括右值引用、左值引用的区别,以及如何在自定义类中实现拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符。通过示例代码,展示了在不同场景下对象拷贝与移动的执行流程,强调了移动操作在提高效率方面的重要性。
摘要由CSDN通过智能技术生成

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

13.6节 对象移动 习题答案

练习13.45:解释右值引用和左值引用的区别。

【出题思路】

理解左值引用和右值引用。

【解答】

所谓右值引用就是必须绑定到右值的引用,通过&&获得。右值引用只能绑定到一个将要销毁的对象上,因此可以自由地移动其资源。左值引用,也就是“常规引用”,不能绑定到要转换的表达式、字面常量或返回右值的表达式。而右值引用恰好相反,可以绑定到这类表达式,但不能绑定到一个左值上。返回左值的表达式包括返回左值引用的函数及赋值、下标、解引用和前置递增/递减运算符,返回右值的包括返回非引用类型的函数及算术、关系、位和后置递增/递减运算符。可以看到,左值的特点是有持久的状态,而右值则是短暂的。

练习13.46:什么类型的引用可以绑定到下面的初始化器上?

int f();
vector<int> vi(100);
int? r1 = f();
int? r2 = vi[0];
int? r3 = r1;
int? r4 = vi[0] * f();

【出题思路】

深入理解左值引用和右值引用。

【解答】

1.r1必须是右值引用,因为f是返回非引用类型的函数,返回值是一个右值。

2.r2必须是左值引用,因为下标运算返回的是左值。

3.r3只能是左值引用,因为r1是一个变量,而变量是左值。

4.r4只能是右值引用,因为vi[0] * f()是一个算术运算表达式,返回右值。

练习13.47:对你在练习13.44(13.5节,第470页)中定义的String类,为它的拷贝构造函数和拷贝赋值运算符添加一条语句,在每次函数执行时打印一条信息。

【出题思路】

理解拷贝何时发生。

【解答】

在String.h和String.cpp中添加,编写一个简单的主程序使用String,编译运行它,观察其行为即可。主程序:

#ifndef STRING13_47_H
#define STRING13_47_H


#include <cstring>
#include <algorithm>
#include <cstddef>
#include <iostream>
#include <initializer_list>
#include <memory>

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

class String
{
    friend String operator+(const String &, const String &);
    friend String add(const String &, const String &);
    friend std::ostream &operator<<(std::ostream &, const String &);
    friend std::ostream &print(std::ostream &, const String &);

public:
    String() = default;

    // cp points to a null terminated array, allocate new memory & copy the array
    String(const char *cp)
    :sz(std::strlen(cp)), p(a.allocate(sz))
    {
        cout << "String(const char *cp) " << cp << "   this===" << this << endl;
        std::uninitialized_copy(cp, cp + sz, p);
    }

    // copy constructor: allocate a new copy of the characters in s 拷贝构造函数
    String(const String &s)
    :sz(s.sz), p(a.allocate(s.sz))
    {
        cout << "拷贝构造函数===s=" << &s << "    this===" << this << endl;
        std::uninitialized_copy(s.p, s.p + sz, p);
    }

    // move constructor: copy the pointer, not the characters, no memory allocation or deallocation
    String(String &&s) noexcept
    :sz(s.size()), p(s.p)
    {
        cout << "移动构造函数===s=" << &s << " s.p = " << s.p << " s.sz=" << s.sz << "    this===" << this << endl;
        s.p = 0;
        s.sz = 0;
    }

    String(size_t n, char c)
    :sz(n), p(a.allocate(n))
    {
        std::uninitialized_fill_n(p, sz, c);
    }

    // allocates a new copy of the data in the right-hand operand; deletes the memory used by the left-hand operand
    String &operator=(const String &);
    // moves pointers from right- to left-hand operand
    String &operator=(String &&) noexcept;

    // unconditionally delete the memory because each String has its own memory
    ~String() noexcept
    {
        if(p)
            a.deallocate(p, sz);
    }

    // additional assignment operators
    String &operator=(const char *);                    // car = "Studebaker"
    String &operator=(char c);                            // model = 'T'
    String &operator=(std::initializer_list<char>);     //car = {'a', '4'}

    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 &s)
    {
        auto tmp = p;
        p = s.p;
        s.p = tmp;

        auto cnt = sz;
        sz = s.sz;
        s.sz = cnt;
    }

private:
    std::size_t sz = 0;
    char *p = nullptr;
    static std::allocator<char> a;
};


String make_plural(size_t ctr, const String &, const String &);
inline void swap(String &s1, String &s2)
{
    s1.swap(s2);
}


#endif // STRING13_47_H
#include "String13_47.h"

#include <cstring>
using std::strlen;

#include <algorithm>
using std::copy;

#include <cstddef>
using std::size_t;

#include <iostream>
using std::ostream;
using std::cout;
using std::endl;


#include <utility>
using std::swap;


#include <initializer_list>
using std::initializer_list;


#include <memory>
using std::uninitialized_copy;

#include <vector>
using std::vector;

// define the static allocator member
std::allocator<char> String::a;

// copy-assignment operator拷贝赋值运算符
String &String::operator=(const String &rhs)
{
    // copying the right-hand operand before deleting the left handles self-assignment
    auto newp = a.allocate(rhs.sz);    // copy the underlying string from rhs
    uninitialized_copy(rhs.p, rhs.p + rhs.sz, newp);

    if(p)
        a.deallocate(p, sz);        // 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
    cout << "拷贝赋值运算符=====rhs==" << &rhs << "     this=" << this << endl;
    return *this;
}

// move assignment operator移动赋值运算符
String &String::operator=(String &&rhs) noexcept
{
    // explicit check for self-assignment
    if(this != &rhs)
    {
        if(p)
        {
            a.deallocate(p, sz);    // do the work of the destructor
        }
        p = rhs.p;                  // take over the old memory
        sz = rhs.sz;
        rhs.p = nullptr;            // deleting rhs.p is safe
        rhs.sz = 0;
    }
    cout << "移动赋值运算符=====rhs==" << &rhs << "     this=" << this << endl;
    return *this;
}

String &String::operator=(const char *cp)
{
    if(p)
    {
        a.deallocate(p, sz);
    }
    p = a.allocate(sz = strlen(cp));
    uninitialized_copy(cp, cp + sz, p);
    return *this;
}

String &String::operator=(char c)
{
    if(p)
    {
        a.deallocate(p, sz);
    }
    p = a.allocate(sz = 1);
    *p = c;
    return *this;
}

String &String::operator=(initializer_list<char> il)
{
    // no need to check for self-assignment
    if(p)
    {
        a.deallocate(p, sz);            // do the work of the destructor
    }
    p = a.allocate(sz = il.size());     // do the work of the destructor
    uninitialized_copy(il.begin(), il.end(), p);
    return *this;
}

// named functions for operators
ostream &print(ostream &os, const String &s)
{
    auto 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 a copy of the newly created String
}

String make_plural(size_t ctr, const String &word, const String &ending)
{
    return (ctr != 1) ? add(word, ending) : word;
}

ostream &operator<<(ostream &os, const String &s)
{
    return print(os, s);
}

String operator+(const String &lhs, const String &rhs)
{
    return add(lhs, rhs);
}
//返回值为String类对象
String getStr()
{
    //定义一个局部对象,然后将局部对象作为结果返回
    String obj;
    //返回值是String类型
    return obj;
}

int main()
{
    String str1("One");
    String str2("Two");
    String str3(str2);
    cout << "str1======= " << str1 << "  str2===== " << str2 << "     str3==== " << str3 << endl;
    str3 = str1;
    cout << "str1======= " << str1 << "  str2===== " << str2 << "     str3==== " << str3 << endl;
    str3 = String("Three");
    cout << "str1======= " << str1 << "  str2===== " << str2 << "     str3==== " << str3 << endl;
    cout << endl;
    vector<String> vs;
    cout << "vs.size()=====" << vs.size() << " vs.capacity()=" << vs.capacity() << endl;
    vs.reserve(4);//加上这个会减少移动构造的次数,原因是如果vector空间不够重新分配内存时,会把之前的元素都重新拷贝到新的空间上
    vs.push_back(str1);
    cout << "vs.size()=====" << vs.size() << " vs.capacity()=" << vs.capacity() << endl;
    cout << endl;
    vs.push_back(std::move(str2));//触发两次移动构造函数
    cout << "vs.size()=====" << vs.size() << " vs.capacity()=" << vs.capacity() << endl;
    cout << endl;
    vs.push_back(String("Three"));//触发3次移动构造函数
    cout << "vs.size()=====" << vs.size() << " vs.capacity()=" << vs.capacity() << endl;
    cout << endl;
    vs.push_back("Four");//触发4次移动构造函数
    cout << "vs.size()=====" << vs.size() << " vs.capacity()=" << vs.capacity() << endl;
    cout << endl;
    for_each(vs.begin(), vs.end(), [](const String &s)
    {
        cout << s << "\t";
    });
    cout << endl;
    //getStr() 函数返回了一个String类型的对象(临时无名对象), 之后调用类的函数
    cout << "strStr().size()==============" << getStr().size() << endl;
    return 0;
}

 运行结果:

 

 练习13.48:定义一个vector<String>并在其上多次调用push_back。运行你的程序,并观察String被拷贝了多少次。

【出题思路】

理解拷贝何时发生。

【解答】

如上题程序。观察输出结果可以看出,由于String定义了接受C风格字符串的构造函数,因此只有前面1个push_back触发了拷贝构造函数。

练习13.49:为你的StrVec、String和Message类添加一个移动构造函数和一个移动赋值运算符。【出题思路】

本题练习定义移动控制成员。

【解答】

书中已有StrVec和Message类的移动构造函数和移动赋值运算符的详细设计,配套网站上也给出了这三个类的移动构造函数和移动赋值运算符的完整代码。读者可先尝试定义这些移动控制成员,然后与配套网站上的代码进行对比。String的移动构造函数设计如下:

String(String &&s) noexcept:sz(s.size()), p(s.p) {
    s.p = 0; s.sz = 0;
}

与StrVec和Message的设计思路类似,首先直接拷贝参数的指针成员p而非拷贝p指向的内容,然后将参数的指针和大小清空,使得它处于一个析构安全的状态。

String的移动赋值运算符设计如下:

String &String::operator=(String &&rhs) noexcept
{
	//显示检查赋值 
	if(this != &rhs) {
		if(p)
			a.deallocate(p, sz);//类似析构函数的工作
		p = rhs.p;//接管旧内存
		sz = rhs.sz;
		rhs.p = 0;//令rhs析构安全
		rhs.sz = 0;
	}
	return *this;
}

仍然是与书中例子类似的编写方式,首先显式检查是否自赋值。若不是,首先将赋值号左侧对象的资源(指针p)释放掉,然后将右侧对象的指针p拷贝给左侧对象,最后将右侧对象置于析构安全的状态。

练习13.50:在你的String类的移动操作中添加打印语句,并重新运行13.6.1节(第473页)的练习13.48中的程序,它使用了一个vector<String>,观察什么时候会避免拷贝。

【出题思路】

进一步理解何时使用拷贝控制成员,何时移动控制成员。

【解答】

在拷贝/移动构造函数和拷贝/移动赋值运算符中添加打印语句,运行练习13.47中的程序,观察输出结果即可。可以看到,vector操作部分输出了以下内容:

 容易看出,

vs.push_back(str1);触发拷贝构造函数
cout << "vs.size()=====" << vs.size() << " vs.capacity()=" << vs.capacity() << endl;
cout << endl;
vs.push_back(std::move(str2));//触发两次移动构造函数
cout << "vs.size()=====" << vs.size() << " vs.capacity()=" << vs.capacity() << endl;
cout << endl;
vs.push_back(String("Three"));//触发3次移动构造函数
cout << "vs.size()=====" << vs.size() << " vs.capacity()=" << vs.capacity() << endl;
cout << endl;
vs.push_back("Four");//触发4次移动构造函数

那么,其他几次(移动)构造函数是如何触发的呢?回忆一下,默认初始化的vector不分配内存空间。当push_back发现vector空间不足以容纳新元素时,分配新的空间(通常是加倍),将数据移动到新的空间中(由于String定义了移动构造函数,这里确实是“移动”而非“拷贝”),然后释放旧空间。因此,当插入s2时,空间由1扩为2,并将原有元素(One)移动到新空间,

当插入Three时,空间由2扩为4,将One、Two移动到新空间,产生两次移动构造,

尝试在创建vector后为它预留足够空间:vs.reserve(4),则输出为:

 因空间扩展引起的移动构造就不存在了。

练习13.51:虽然unique_ptr不能拷贝,但我们在12.1.5节(第418页)中编写了一个clone函数,它以值方式返回一个unique_ptr。解释为什么函数是合法的,以及为什么它能正确工作。

【出题思路】

理解不可拷贝类型的例外及移动控制成员的触发条件。

【解答】

unique_ptr不能拷贝,但有一个例外——将要被销毁的unique_ptr是可以拷贝或销毁的。因此,在418页的clone函数中返回局部unique_ptr对象ret是可以的,因为ret马上就要被销毁了。而此时的“拷贝”其实是触发移动构造函数进行了移动。

练习13.52:详细解释第478页中的HasPtr对象的赋值发生了什么?特别是,一步一步描述hp、hp2以及HasPtr的赋值运算符中的参数rhs的值发生了什么变化。

【出题思路】

理解移动控制成员的执行过程。

【解答】

对hp = hp2,因为hp2是一个变量,是一个左值,因此它传递给赋值运算符参数rhs的过程是拷贝构造过程,rhs获得hp2的一个副本,rhs.ps与hp2.ps指向不同的string,但两个string包含相同的内容。在赋值运算符中,交换hp和rhs,rhs指向hp原来的string,在赋值结束后被销毁。最终结果,hp和hp2指向两个独立的string,但内容相同。

对hp = std::move(hp2),hp2传递给rhs的过程是移动构造过程,rhs.ps指向hp2.ps原来的string,hp2的ps被设置为空指针。然后赋值运算符交换hp和rhs,rhs指向hp原来的string,在赋值结束后被销毁。最终结果hp指向hp2原来的string,而hp2则变为空。

练习13.53:从底层效率的角度看,HasPtr的赋值运算符并不理想,解释为什么。为HasPtr实现一个拷贝赋值运算符和一个移动赋值运算符,并比较你的新的移动赋值运算符中执行的操作和拷贝并交换版本中执行的操作。

【出题思路】

从性能角度考虑移动控制成员的定义。

【解答】

在进行拷贝赋值时,先通过拷贝构造创建了hp2的拷贝rhs,然后再交换hp和rhs,rhs作为一个中间媒介,只是起到将值从hp2传递给hp的作用,是一个冗余的操作。

类似的,在进行移动赋值时,先从hp2转移到rhs,再交换到hp,也是冗余的。

也就是说,这种实现方式唯一的用处是统一了拷贝和移动赋值运算,但在性能角度,多了一次从rhs的间接传递,性能不好。练习13.8已经定义了拷贝赋值运算符,移动赋值运算符可定义如下:

    inline HasPtr& operator=(HasPtr &&rhs) noexcept
    {
        cout << "HasPtr====移动赋值运算符=========================" << endl;
        if(this != &rhs)
        {
            delete ps;          //释放旧string
            ps = rhs.ps;        //从rhs接管string
            rhs.ps = nullptr;   //将rhs置于析构安全状态
            rhs.i = 0;
        }
        return *this;           //返回一个此对象的引用
    }

练习13.54:如果我们为HasPtr定义了移动赋值运算符,但未改变拷贝并交换运算符,会发生什么?编写代码验证你的答案。

【出题思路】

理解两种赋值运算符的关系。

【解答】

会产生编译错误。因为对于hp = std::move(hp2)这样的赋值语句来说,两个运算符匹配得一样好,从而产生了二义性。

练习13.55:为你的StrBlob添加一个右值引用版本的push_back。

【出题思路】

练习定义右值引用版本成员函数。

【解答】

定义如下,与左值引用版本的差别除了参数类型外,就是将参数用move处理后使用。

void push_back(string &&t)
{
	data->push_back(std::move(t));
}

练习13.56:如果sorted定义如下,会发生什么:

Foo Foo::sorted() const &{
    Foo ret(*this);
    return ret.sorted();
}

【出题思路】

理解左值引用和右值引用版本的成员函数。

【解答】

首先,局部变量ret拷贝了被调用对象的一个副本。然后,对ret调用sorted,由于并非是函数返回语句或函数结束(虽然写成一条语句,但执行过程是先调用sorted,然后将结果返回),因此编译器认为它是左值,仍然调用左值引用版本,产生递归循环。

利用右值引用版本来完成排序的期望不能实现。

练习13.57:如果sorted定义如下,会发生什么:

Foo Foo::sorted() const & {return Foo(*this).sorted();}

【出题思路】

理解左值引用和右值引用版本的成员函数。

【解答】

与上一题不同,本题的写法可以正确利用右值引用版本来完成排序。原因在于,编译器认为Foo(*this)是一个“无主”的右值,对它调用sorted会匹配右值引用版本。

练习13.58:编写新版本的Foo类,其sorted函数中有打印语句,测试这个类,来验证你对前两题所给出的答案是否正确。

【出题思路】

理解左值引用和右值引用版本的成员函数。

【解答】

程序如下,练习13.56的写法会一直输出“左值引用版本”,直至栈溢出,程序退出。而练习13.57的写法会输出一个“左值引用版本”和一个“右值引用版本”后正确结束。

#include <iostream>
#include <vector>
#include <algorithm>
using std::cout;
using std::endl;
using std::vector;
using std::sort;

class Foo
{
public:
    Foo sorted() &&;//用于可改变的右值
    Foo sorted() const &;//可用于任何类型的Foo
    void push_back(const int &num);
    void print();

private:
    vector<int> data;
};

//本对象为右值,因此可以原址排序
Foo Foo::sorted() &&
{
    cout << "右值引用版本===================" << endl;
    sort(data.begin(), data.end());
    return *this;
}
//本对象是const或是一个左值,哪种情况我们都不对其进行原址排离序
Foo Foo::sorted() const &
{
    cout << "左值引用版本===================" << endl;
    Foo ret(*this);//拷贝一个副本
    //return ret.sorted();
    return Foo(*this).sorted();
}

void Foo::push_back(const int &num)
{
    data.push_back(num);
}

void Foo::print()
{
    for(auto it:data)
    {
        cout << "value==========" << it << endl;
    }
}

int main()
{
    Foo f;
    f.push_back(50);
    f.push_back(20);
    f.push_back(30);
    f.push_back(70);
    f.push_back(60);
    f.push_back(10);
    f.push_back(40);
    f.sorted();
    f.print();
    cout << "hello world===============" << endl;
    return 0;
}

运行结果:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值