C++Primer习题第十三章

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

答:

如果构造函数的第一个参数类型是自身类类型的引用,而且其他额外的参数都有默认值,则此构造函数是拷贝构造函数。

拷贝构造函数在以下几种情况被使用:

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

(2)类作为实参传递给非引用类型的形参。

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

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

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


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

Sales_data::Sales_data( Sales_data rhs );

类的拷贝构造函数必须是引用类型。因为:作为实参传递给拷贝构造函数的非引用类类型的形参会又调用拷贝构造函数,无限循环,所以就解释了为什么拷贝构造函数的参数类型为什么必须是引用类型的。


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

----------------------------------------------------------------------------------------------------------------------------------------------------------

PS:一般情况,合成的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中。编译器从给定对象依次将每个非static成员拷贝到正在创建的对象中。

每个成员的类型决定了它如何拷贝:对类类型的成员,会使用其拷贝构造函数来拷贝,内置类型则直接拷贝。

----------------------------------------------------------------------------------------------------------------------------------------------------------

拷贝一个StrBlob:

只有一个数据成员data , 调用shared_ptr的拷贝构造函数来进行拷贝,因此其引用计数加1。


拷贝一个StrBlobPtr:

有两个数据成员,

curr直接拷贝。直接进行内存复制。

wptr调用weak_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 );

private:

std::string *ps;

int i;

};


HasPtr::HasPtr( const HasPtr &hp ){

ps = new string( *(hp.ps) );

i = hp.i;

}


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


拷贝赋值运算符本身是一个重载的赋值运算符,定义为类的成员左侧运算对象绑定到隐含的this参数。拷贝赋值运算符控制类对象如何赋值。拷贝赋值运算符接受一个与其所在类相同类型的参数,通常返回一个指向其左侧运算对象的引用。

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

对于某些类,合成拷贝赋值运算符用来禁止该类型对象的赋值。若非此目的,它会将右侧运算对象的每个非static成员赋予左侧运算对象的对应成员。

如果一个类为定义自己的拷贝赋值运算符,编译器会为其生成一个合成拷贝赋值运算符。


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

将一个StrBlob赋值给另一个StrBlob时:

StrBlob只有一个数据成员data, 会调用shared_ptr的拷贝赋值运算符。

赋值StrBlobPtr:

对数据成员 wptr,调用weak_ptr的拷贝赋值元素符。

对数据成员curr,直接使用内置的赋值运算符。


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

#ifndef HASPTR_H_INCLUDED
#define HASPTR_H_INCLUDED

using namespace std;

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

//拷贝构造函数
HasPtr::HasPtr( const HasPtr &hp ){
    ps = new string( *(hp.ps) );
    i = hp.i;
}

//拷贝赋值运算符
HasPtr::HasPtr& HasPtr::operator=( const HasPtr &hp ){
    auto newps = new string( *(hp.ps) );
    delete ps; //销毁原ps
    ps = newps; //指向新string
    i = hp.i;
}


#endif // HASPTR_H_INCLUDED


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

答:

析构函数释放对象使用的资源,并销毁对象的非static数据成员。从语法上看,它是类的一个成员,名字是波浪号接类名,没有返回值,没有参数。

对于某些类,合成析构函数被用来阻止该类型的对象的销毁。若非这种情况,则合成析构函数的函数体为空,但这并不意味这它什么也不做,当空函数体执行完后,非静态数据成员会被逐个销毁。也就是说,成员是在析构函数体之后隐含的析构阶段中进行销毁的。

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


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

当一个StrBlob对象被销毁时,合成析构函数的空函数体执行完后,隐含的析构阶段中,会调用shared_ptr的析构函数销毁data成员,将引用计数减1,若引用计数变为0,会销毁共享的vector对象。

当一个StrBlobPtr对象被销毁时,合成析构函数的空函数体执行完后,隐含的析构阶段中,会销毁数据成员wptr和curr,销毁wptr会调用weak_ptr的析构函数,引用计数不变;curr是内置类型,销毁它不会有特殊动作。


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

#ifndef HASPTR_H_INCLUDED
#define HASPTR_H_INCLUDED

using namespace std;

class HasPtr{
public:
    //构造函数
    HasPtr( const string &s = string() ):
        ps( new string(s) ), i( 0 ) { }
    //拷贝构造函数
    HasPtr( const HasPtr &hp );
    //拷贝赋值运算符
    HasPtr& operator=( const HasPtr &hp );
    //析构函数
    ~HasPtr() { delete ps; } //要释放ps指向对象的内存空间
        
private:
    string *ps;
    int i;
};

//拷贝构造函数
HasPtr::HasPtr( const HasPtr &hp ){
    ps = new string( *(hp.ps) );
    i = hp.i;
}

//拷贝赋值运算符
HasPtr::HasPtr& HasPtr::operator=( const HasPtr &hp ){
    auto newps = new string( *(hp.ps) );
    delete ps; //销毁原ps
    ps = newps; //指向新string
    i = hp.i;
}


#endif // HASPTR_H_INCLUDED

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

bool fcn( const Sales_data *trans, Sales_data accum )

{

Sales_data item1( *trans ), item2( accum );

return item1.isbn() != item2.isbn();

}


三次析构函数的调用。

函数结束时:

局部变量item1,item2的生命期结束,被销毁,Sales_data的析构函数被调用。

参数accum的生命期结束,被销毁,调用Sales_data的析构函数。


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

struct X{

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

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

};

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

#include<iostream>

using namespace std;

class X{
public:
    X( int i = 0 ): No( i ) { cout << "X(): " << No << endl; }
    //拷贝构造函数
    X( const X &cpy ) { No = cpy.No; cout << "X( const X& ):" << No << endl; }
    //拷贝赋值运算符
    X& operator=( const X &x ) { No = x.No; cout << "operator=" << endl; return *this; }
    //析构函数
    ~X() { cout << "~X()" << No << endl; }
    unsigned No; //用来表示编号,是public的。
};


void f1( X x ) { }
void f2( X &x ) { }

int main()
{
    X a0;
    cout << endl;

    X a1( 1 );

    cout << endl;

    a0.No = 2;
    X a2( a0 );

    cout << endl;

    a2.No = 3;
    X a3 = a2;

    cout << endl;

    a3 = X(4);

    cout << endl;

    f1( a0 );
    f2( a0 );                                                                                             cout << endl;
    return 0;
}


结果符合预期。大致理解了什么时候使用拷贝控制成员。


练习13.14:假定Numbered是一个类,它有一个默认构造函数,能为每个对象生成一个唯一的序号,保存在名为mysn的数据成员中。假定numbered使用合成的拷贝控制成员,并给定如下函数:

void f( numbered s ) { cout << s.mysn << endl; }

则下面代码输出什么内容?

Numbered a, b = a, c = b;

f(a);f(b);f(c);

应该会输出三行相同的序号。


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

会改变。

新的输出结果是三行不同的序号。但是这三个不同的序号并不对应 a,b,c对象中的mysn成员。

因为输出的结果是经过拷贝初始化的函数参数对象的mysn成员,同样会生成一个新的序号。


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

改变输出结果。

因为这次传递的是对象的引用。

会输出三个不同的序号,而且分别对应a,b,c的mysn成员。


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

预测正确。程序略了。


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

#ifndef EMPLOYEE_H_INCLUDED
#define EMPLOYEE_H_INCLUDED

class Employee{
private:
    static int Seq;
public:
    //默认构造函数
    Employee(): EmployeeNo( Seq++ ) { }
    //接受一个string的构造函数
    Employee( const string &s ): Name(s), EmployeeNo(Seq++) { }
    //拷贝构造函数
    Employee( const Employee &ep ) { Name = ep.Name; EmployeeNo = Seq++; }
    //拷贝赋值运算符
    Employee& operator=( const Employee &ep ) { Name = ep.Name; return *this; }
private:
    string Name;
    int EmployeeNo;
};

int Employee::Seq = 0;

#endif // EMPLOYEE_H_INCLUDED


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

需要。当我们拷贝初始化一个Employee类对象或者对一个Employee类对象赋值时,我们希望得到不同的雇员证号,而不是直接复制相同的证号。

实现见上题代码。


练习13.20:解释当我们拷贝、赋值或销毁TextQuery和QueryResult类对象时会发生什么?

拷贝TextQuery对象时:

拷贝file和wm成员。对于file,会使得引用计数加1。对于wm成员,会调用map的拷贝构造函数(继而调用string 和set的拷贝构造函数),因此会正确地进行拷贝操作。赋值操作类似,但会将原来的资源释放掉。

拷贝QueryResult时:

拷贝sought和lines和file成员。其中,lines和file成员的引用计数加1。赋值操作类似。

销毁TextQuery对象时:

会销毁成员file和wm。对于file,引用计数减1,如果引用计数减为0,会销毁所管理的动态vector对象(调用vector和string的析构函数)。对于wm,调用map的析构函数(从而调用string、shared_ptr和set的析构函数),会正确释放资源。


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

不需要。目前的设计已经很好地实现两个对象的资源共享。


练习13.22:假定我们希望HasPtr的行为像一个值。即,对于对象所指向的string成员,每个对象都有一份自己的拷贝。为HasPtr编写拷贝构造函数和赋值运算符。

#ifndef HASPTR_H_INCLUDED
#define HASPTR_H_INCLUDED

using namespace std;

class HasPtr{
public:
    //构造函数
    HasPtr( const string &s = string() ):
        ps( new string(s) ), i( 0 ) { }
    //拷贝构造函数
    HasPtr( const HasPtr &hp );
    //拷贝赋值运算符
    HasPtr& operator=( const HasPtr &hp );
    //析构函数
    ~HasPtr() { delete ps; }

private:
    string *ps;
    int i;
};

//拷贝构造函数
HasPtr::HasPtr( const HasPtr &hp ){
    ps = new string( *(hp.ps) );
    i = hp.i;
}

//拷贝赋值运算符
HasPtr::HasPtr& HasPtr::operator=( const HasPtr &hp ){
    auto newps = new string( *(hp.ps) );
    delete ps; //销毁原ps
    ps = newps; //指向新string
    i = hp.i;
}


#endif // HASPTR_H_INCLUDED

练习13.23:比较上一节练习中你编写的拷贝控制成员和这一节中的代码。

略了。


练习13.24:如果本节中的HasPtr版本未定义析构函数,将会发生什么?如果未定义拷贝构造函数,将会发生什么?

如果未定义析构函数,销毁HasPtr类对象的时候,成员ps指向的对象的内存无法被释放。

如果未定义拷贝构造函数,拷贝初始化时,ps成员直接复制,导致两个对象的ps成员指向同一个string,这不符合我们的期望。而且这样的话,当其中一个对象的ps被销毁,其指向的内存将被释放,那么另一个对象的ps将成为空悬指针。


练习13.25:假定希望定义StrBlob的类值版本,而且希望继续使用shared_ptr,这样我们的StrBlobPtr类就仍能使用指向vector的weak_ptr了。你修改后的类将需要一个拷贝构造函数和一个拷贝赋值运算符,但不需要析构函数。解释拷贝构造函数和拷贝赋值运算符必须要做什么。解释为什么不需要析构函数。

拷贝构造函数和拷贝赋值运算符,使得两个对象的data指向独立的相同的vector而不是共享同一个vector。

因为数据成员data是shared_ptr类,当data的引用计数减为0时,合成的析构函数会自动调用shared_ptr类的析构函数释放data指向的内存。已经能够保证资源分配、释放的正确性。


练习13.26:对上一题描述的StrBlob类,编写你自己的版本。

已经标红。

#ifndef STRBLOB_H_INCLUDED
#define STRBLOB_H_INCLUDED
#include<vector>
#include<string>
#include<stdexcept>
#include<initializer_list>
#include<memory>

using namespace std;

class StrBlobPtr;//友元类外部声明。

class StrBlob{
public:
    //友元类声明
    friend class StrBlobPtr;
    //构造函数
    StrBlob();
    StrBlob( initializer_list<string> il );
    
    //类值版本的拷贝构造函数
    StrBlob( const StrBlob &sb ) { data = make_shared<vector<string>>( *sb.data ); }
    //类值版本的拷贝赋值运算符
    StrBlob& operator=( const StrBlob &sb ) { data = make_shared<vector<string>>( *sb.data ); return *this; }
    
    //容器大小访问
    vector<string>::size_type size() const { return data->size(); }
    bool empty() const { return data->empty(); }
    //添加和删除元素
    void push_back( const std::string &str ) { data->push_back( str ); }
    void pop_back();
    //元素访问
    string& front();
    const string& front() const;
    string& back();
    const string& back() const;
    //返回指向首元素和尾元素的StrBlobPtr
    StrBlobPtr begin();
    StrBlobPtr end();

private:
    std::shared_ptr<std::vector<std::string>> data;
    void check( vector<string>::size_type i, const std::string &msg ) const ;

};


inline StrBlob::StrBlob(): data( make_shared<vector<string>>() ) { }
inline StrBlob::StrBlob(initializer_list<string> il): data( make_shared<vector<string>> (il) ) { }


inline void StrBlob::check( vector<string>::size_type i, const std::string &msg ) const
{
    if( i >= data->size() )
        throw out_of_range( msg );
}

inline void StrBlob::pop_back()
{
    check( 0, "pop_back on empty StrBlob" );
    data->pop_back();
}

inline string& StrBlob::front()
{
    check( 0, "front on empty StrBlob" );
    return data->front();
}

inline const string& StrBlob::front() const
{
    check( 0, "front on empty StrBlob");
    return data->front();
}

inline string& StrBlob::back()
{
    check( 0, "back on empty StrBlob" );
    return data->back();
}

inline const string& StrBlob::back() const
{
    check( 0, "back on empty StrBlob" );
    return data->back();
}



//StrBlobPtr类

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();//前缀递增

private:
    shared_ptr<vector<string>> check( size_t, const string& ) const;
    weak_ptr<vector<string>> wptr;
    size_t curr;
};

inline shared_ptr<vector<string>> StrBlobPtr::check( size_t i, const string &msg ) const
{
    auto ret = wptr.lock();
    if( !ret )
        throw runtime_error( "unbound StrBlobPtr" );
    if( i >= ret->size() )
        throw out_of_range( msg );
    return ret;
}

inline string& StrBlobPtr::deref() const
{
    auto sp = check( curr, "dereference pass end" );

    return (*sp)[ curr ];
}

inline StrBlobPtr& StrBlobPtr::Incr()
{
    auto sp = check( curr, "increment past end of StrBlobPtr" );

    ++curr;

    return *this;
}


inline StrBlobPtr StrBlob::begin()
{
    return StrBlobPtr( *this );
}

inline StrBlobPtr StrBlob::end()
{
    auto ret = StrBlobPtr( *this, data->size() );
    return ret;
}


inline bool eq( const StrBlobPtr &lhs, const StrBlobPtr &rhs )
{
    auto l = lhs.wptr.lock(), r = rhs.wptr.lock();
    if( l == r )
        return ( !r || lhs.curr == rhs.curr );
    else
        return false;
}

inline bool uneq( const StrBlobPtr &lhs, const StrBlobPtr &rhs )
{
    return !eq( lhs, rhs );
}




#endif // STRBLOB_H_INCLUDED

练习13.27:定义你自己的使用引用计数版本的HasPtr。

//定义像指针行为的HasPtr类
using namespace std;

class HasPtr{
public:
    //构造函数
    HasPtr( const string &s = string() ):
        ps( new string(s) ), i( 0 ), use_count( new size_t( 1 ) ) { }
    //拷贝构造函数
    HasPtr( const HasPtr &hp );
    //拷贝赋值运算符
    HasPtr& operator=( const HasPtr &hp );
    //析构函数
    ~HasPtr();

private:
    string *ps;
    int i;
    size_t *use_count;
};

//拷贝构造函数
HasPtr::HasPtr( const HasPtr &hp ){
    ps = hp.ps;
    i = hp.i;
    use_count = hp.use_count;
    ++*use_count;
}
//拷贝赋值运算符
HasPtr& HasPtr::operator=( const HasPtr &hp ){
    ++*hp.use_count;
    if( --*use_count == 0 ){
        delete ps;
        delete use_count;
    }
    ps = hp.ps;
    i = hp.i;
    use_count = hp.use_count;
    
    return *this;

}

//析构函数
HasPtr::~HasPtr(){
    if( --*use_count == 0 ){
        delete ps;
        delete use_count;
    }
}


练习13.28:给定下面的类,为其实现一个默认构造函数和必要的拷贝控制成员。

(a)

class TreeNode{

private:

std::string value;

int count;

TreeNode *left;

TreeNode *right;

};


(b) class BinStrTree{

private:

TreeNode *root;

};


这题先搁置。回头再做。


练习13.29:解释swap( HasPtr&, HasPtr& )中对swap的调用不会导致递归循环。

swap的定义如下:

inline void swap( Hasptr &lhs, HasPtr &rhs)

{

using std::swap;

swap( lhs.ps, rhs.ps );

swap( lhs.i, rhs.i );

}

swap( HasPtr&, HasPtr& )内部函数体调用的swap不是本身,而是标准库定义的swap,所以并不会导致递归循环。



练习13.30:为你的类值版本的HasPtr编写swap函数,并测试它。为你的swap函数添加一个打印语句,指出函数什么时候执行。

略了。


练习13.31:为你的HasPtr类定义一个<运算符,并定义一个HasPtr的vector。为这个vector添加一些元素,并对它执行sort。注意何时调用swap。

当元素数目过少时sort使用的是插入排序算法,未使用swap。增加元素数目至一定程度,sort会使用快速排序算法,此时使用自定义版本的swap。

略了。

练习13.32:类指针的HasPtr版本会从swap函数受益吗?如果会,得到了什么益处?如果不是,为什么?

答:

默认版本简单交换两个对象的非静态成员,对HasPtr而言,就是交换string指针ps、引用计数指针use和整数i。可以看出,这种语义是符合期望的。默认swap版本已经能够正确处理指针HasPtr的交换,专用的版本并不会带来更多收益。


练习13.33:为什么Message的成员save和remove的参数是一个Folde&?为什么我们不将参数定义为Folder或者是const Folder&?

答:

参数如果不是引用类型,我们的save和remove操作都将仅仅操作在目标Folder的拷贝上,目标Folder并不会改变。

而且,我们不能将参数设置为const,因为我们要改变对象的状态。



练习13.34:编写本节所描述的Message。

#ifndef MESSAGE_H_INCLUDED
#define MESSAGE_H_INCLUDED

#include<string>
#include<set>
#include<iostream>
using namespace std;

class Message;
void swap( Message &lhs, Message &rhs );

class Folder{
    friend void swap( Message &lhs, Message &rhs );
    friend class Message;
public:
    Folder() = default;
    Folder( const Folder& );
    ~Folder();
    Folder& operator=( const Folder& );

    void print( ostream& );

private:
    set<Message*> msgs;
    //工具函数
    void addMsg( Message *m ) { msgs.insert( m ); }
    void remMsg( Message *m ) { msgs.erase( m ); }
    void add_to_Messages( const Folder& );
    void remove_from_Messages();
};

class Message{
    friend class Folder;
    friend void swap( Message&, Message& );
public:
    //默认构造函数
    Message( const string &s = "" ): contents(s) { }
    //拷贝构造函数
    Message( const Message& );
    //析构函数
    ~Message();
    //拷贝赋值运算符
    Message& operator=( const Message& );

    //成员函数
    void save( Folder& );
    void remove( Folder& );

private:
    //数据成员
    string contents;
    set<Folder*> folders;
    //工具函数
    void add_to_folders( const Message& );
    void remove_from_folders();
    void addFldr( Folder *f ) { folders.insert( f ); }
    void remFldr( Folder *f ) { folders.erase( f ); }
};

void Folder::add_to_Messages( const Folder &f ){
    for( auto m : f.msgs )
        m->addFldr( this );
}

void Folder::remove_from_Messages(){
    for( auto m : msgs )
        m->remFldr( this );
}

Folder::Folder( const Folder &f ): msgs( f.msgs ){
    add_to_Messages( f );
}

Folder::~Folder(){
    remove_from_Messages();
}

Folder& Folder::operator=( const Folder &f ){
    remove_from_Messages();
    msgs = f.msgs;
    add_to_Messages( f );

    return *this;
}

void Folder::print( ostream &os ){
    for( auto m : msgs )
        os << m->contents << endl;
    os << endl;
}


void Message::add_to_folders( const Message &m ){
    for( auto f : m.folders )
        f->addMsg( this );
}

void Message::remove_from_folders(){
    for( auto f : folders )
        f->remMsg( this );
}


Message::Message( const Message &m ):
    contents( m.contents ), folders( m.folders ) {
        add_to_folders( m );
    }


Message::~Message(){
    remove_from_folders();
}


Message& Message::operator=( const Message &m ){
    remove_from_folders();
    contents = m.contents;
    folders = m.folders;
    add_to_folders( m );

    return *this;
}

void Message::save( Folder &f ){
    folders.insert( &f );
    f.addMsg( this );
}

void Message::remove( Folder &f ){
    folders.erase( &f );
    f.remMsg( this );
}

void swap( Message &lhs, Message &rhs ){
    using std::swap;

    lhs.remove_from_folders();
    rhs.remove_from_folders();

    swap( lhs.contents, rhs.contents );
    swap( lhs.folders, rhs.folders );

    for( auto f : lhs.folders )
        f->addMsg( &lhs );
    for( auto f : rhs.folders )
        f->addMsg( &rhs );
}

#endif // MESSAGE_H_INCLUDED



练习13.35:如果Message使用合成的拷贝控制成员,将会发生什么?

对于合成的拷贝构造函数;

当我们用类对象m拷贝初始化一个 Message对象时,对应的Folder对象中的set并不包含这个副本的指针。相当于这个副本的信息并未添加到对应的Folder对象中。

对于合成的析构函数:

当我们的类对象m销毁时,原来保存有m指针的Folder对象中的set依然存有m的指针。相当于信息并未删除。

对于合成的拷贝赋值运算符:

相当于原信息不删除,新信息不添加。


练习13.36:设计并实现对应的Folder类。此类应该保存一个指向Folder中包含的Message的set。

见练习13.34。



练习13.37:为Message类添加成员,实现向 folders添加或删除一个给定的Folder*。这两个成员类似Folder类中的addMsg和remMsg操作。

见练习13.34。


练习13.38:我们并未使用拷贝和交换的方式来设计Message的赋值运算符。你认为其原因是什么?

因为非引用方式的拷贝,会导致实参的副本也添加到相应的Folder类对象之中。(之后销毁还会删除)。但这是一种浪费开销的方式。我们需要创建、销毁形参对象并两次添加、删除,导致效率低下。


练习13.39:编写你自己版本的StrVec,包括你自己版本的reserve、capacity 和 resize。

#ifndef STRVEC_H_INCLUDED
#define STRVEC_H_INCLUDED

//类 vector类分配策略的简化实现
#include<memory>
#include<utility>
#include<initializer_list>
using namespace std;

class StrVec{
public:
    //默认构造函数
    StrVec():
        elements( nullptr ), first_free( nullptr ), cap( nullptr ) { }

    //其他构造函数
    StrVec( initializer_list<string>& );
    //拷贝构造函数
    StrVec( const StrVec& );
    //析构函数
    ~StrVec();
    //拷贝赋值运算符
    StrVec& operator=( const StrVec& );

    void push_back( const string& );
    size_t size() const { return first_free - elements; }
    size_t capacity() const { return cap - elements; }
    void resize( size_t n );
    void reserve( size_t n );
    string* begin() const { return elements; }
    string* end() const { return first_free; }

private:

    //数据成员
    static allocator<string> alloc;

    string *elements;
    string *first_free;
    string *cap;

    //工具函数
    void chk_n_alloc();
    pair<string*, string*>
        alloc_n_copy( const string*, const string* );
    void free();
    void reallocate();
    void reallocate( size_t n );

};
//注意:静态成员的类外定义。
allocator<string> StrVec::alloc;

inline
StrVec::StrVec( initializer_list<string> &ls ){
    auto newdata = alloc_n_copy( ls.begin(), ls.end() );

    elements = newdata.first;
    first_free = cap = newdata.second;
}


//如果没有空间,则调用reallocate分配更多内存。
inline void StrVec::chk_n_alloc( void ){
    if( size() == capacity() )
        reallocate();
}


//分配内存,并拷贝一个给定范围中的元素
pair<string*, string*>
        StrVec::alloc_n_copy( const string *b, const string *e )
{
    auto data = alloc.allocate( e - b );

    return { data, uninitialized_copy( b, e, data ) };
}

//会销毁构造的元素并释放内存
void StrVec::free(){
    //不能传递给deallocate一个空指针
    if( elements ){//逆序销毁旧元素
        for( auto p = first_free; p != elements; /* 空 */ )
            alloc.destroy( --p );
        alloc.deallocate( elements, cap - elements );
    }
}

StrVec::StrVec( const StrVec &sv ){
    auto newdata = alloc_n_copy( sv.begin(), sv.end() );
    elements = newdata.first;
    first_free = cap = newdata.second;
}


StrVec::~StrVec(){
    free();
}

StrVec& StrVec::operator=( const StrVec &sv ){
    auto newdata = alloc_n_copy( sv.begin(), sv.end() );
    free();
    elements = newdata.first;
    first_free = cap = newdata.second;

    return *this;
}

void StrVec::push_back( const string &s ){
    chk_n_alloc();

    alloc.construct( first_free++, s );
}


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( size_t n ){
    auto newdata = alloc.allocate( n );
    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 = newdata + n;
}

void StrVec::resize( size_t n ){
    if( n > size() ){
        while( size() < n )
            push_back( "" );
    }
    else if( n < size() ){
        while( size() > n )
            alloc.destroy( --first_free );
    }
}

void StrVec::reserve( size_t n ){
    if( n > capacity() )
        reallocate( n );
}

#endif // STRVEC_H_INCLUDED


练习13.40:为你的StrVec类添加一个构造函数,它接受一个initializer_list<string>的参数。

见练习13.39。


练习13.41:在push_back中,我们为什么在construct调用使用前置递增运算?如果使用后置递增运算的话,会发生什么?

答:

因为first_free本来就指向实际元素的下一个位置。所以构造新元素的话,应该就在first_free指向的位置,构造完再指向下一个位置。

如果使用后置递增运算的话,先指向下一个位置再构造新元素,那么first_free就指向了最后构造的元素的位置,而不是它的下一个位置。与我们的期望不符。


练习13.42:在你的TextRequery和QueryResult类中用你的StrVec类代替vector<string>,以此来测试你的StrVec类。

#ifndef TEXTQUERY_H_INCLUDED
#define TEXTQUERY_H_INCLUDED

#include<vector>
#include<fstream>
#include<memory>
#include<sstream>
#include<string>
#include<map>
#include<set>
#include"StrVec.h"

using namespace std;

class QueryResult; //前向声明

class TextQuery{
public:
    //类型别名
    typedef /*vector<string>::size_type*/size_t line_no;
    //构造函数。
    TextQuery( ifstream& );
    QueryResult query( const string& ) const;
private:
    shared_ptr<StrVec> file;
    map<string, shared_ptr<set<line_no>>> wordMap;

};

//构造函数定义。读取文件按行存入vector,并且建立单词与行号映射。
TextQuery::TextQuery( ifstream &fin ): file( new StrVec )
{
    string line;
    //为了使得行号从1开始对应。我把vector的位置0先设为空串。
    file->push_back( "" );
    while( getline( fin, line ) ){
        file->push_back( line );
        unsigned lineN = file->size() - 1; //lineN用来保存行号。
        istringstream sin( line );
        string word;
        while( sin >> word ){
            auto &lines = wordMap[ word ]; //lines是一个shared_ptr
            if( !lines )
                lines.reset(  new set<line_no> ); //如果lines为空指针(set没有元素),分配一个新的set
            lines->insert( lineN );
        }
    }
}


class QueryResult{
    friend ostream& print( ostream&, const QueryResult& );
public:
    QueryResult( const string& str, shared_ptr<set<TextQuery::line_no>> l,
                shared_ptr<StrVec> f ): sought( str ), lines( l ), file( f ) { }
private:
    string sought;
    shared_ptr<set<TextQuery::line_no>> lines;
    shared_ptr<StrVec> file;

};

QueryResult TextQuery::query( const string &s ) const
{
    static shared_ptr<set<line_no>> nodata( new set<line_no> );

    auto loc = wordMap.find( s );
    if( loc == wordMap.end() )
        return QueryResult( s, nodata, file );
    else
        return QueryResult( s, loc->second, file );
}

ostream& print( ostream &os, const QueryResult &qr )
{
    os << qr.sought << " occurs " << qr.lines->size()
        << ( ( qr.lines->size() > 1 ) ? " times" : " time" )
        << endl;
    for( auto num : *( qr.lines ) )
        os << "\t(line " << num << ") " <<  *( qr.file->begin() + num ) << endl;
    return os;
}

#endif // TEXTQUERY_H_INCLUDED

#include<iostream>
#include"TextQuery.h"
#include<fstream>

using namespace std;

int main()
{
    ifstream fin;
    fin.open( "T11_9_data.txt" );
    if( !fin ){
        cerr << "cant open file!";
        return -1;
    }
    TextQuery tq( fin );
    print( cout, tq.query( "every" ) );

    return 0;
}


练习13.43:重写free成员,用for_each和lambda来代替for循环destroy元素。你更倾向于哪种实现?为什么?

首先要包含头文件algorithm

//用for_each和lambda重写free
void StrVec::free(){
    for_each( elements, first_free, []( const string &s ) { alloc.destroy( &s ); } );

    alloc.deallocate( elements, cap - elements );
}


我更倾向于for_each方式的实现。只需给定范围和执行的操作即可。而for版本的还需要控制指针的增减,这里容易出错。


练习13.44:编写标准库string类的简化版本,命名为String。你的类至少有一个默认构造函数和一个接受C风格字符指针参数的构造函数。使用allocator为你的String类分配所需内存。

#ifndef STRING_H_INCLUDED
#define STRING_H_INCLUDED

#include<memory>
#include<cstring>
#include<utility>

using namespace std;

class String{
public:
    //默认构造函数
    String():
        sz(0), p( nullptr ){ }
    //其他构造函数
    String( const char *cp ):
        sz( strlen( cp ) ), p( alloc.allocate( sz ) ) { uninitialized_copy( cp, cp + sz, p ); }

    //拷贝构造函数
    String( const String &s ):
        sz( s.sz ), p( alloc.allocate( sz ) ) { cout << "调用了拷贝构造函数" << endl; uninitialized_copy( s.p, s.p + sz, p ); }
    //析构函数
    ~String();
    //移动构造函数
    String( String &&s ) noexcept :
        sz( s.sz ), p( s.p ) { cout << "调用了移动构造函数" << endl; s.sz = 0; s.p = nullptr; }

    //移动赋值运算符
    String& operator=( String&& ) noexcept;

    //拷贝赋值运算符
    String& operator=( const String & );
    String& operator=( const char *cp );
private:
    //数据成员
    static allocator<char> alloc;

    size_t sz;
    char *p;

};

//静态成员的类外定义
allocator<char> String::alloc;

inline
String::~String(){
    if( p )
        alloc.deallocate( p, sz );
}

String& String::operator=( const String &s ){
    cout << "调用了拷贝赋值运算符" << endl;
    char *newp = alloc.allocate( s.sz );
    uninitialized_copy( s.p, s.p + sz, newp );
    if( p )
        alloc.deallocate( p, sz );
    sz = s.sz;
    p = newp;

    return *this;
}

String& String::operator=( const char *cp ){
    cout << "调用了拷贝赋值运算符" << endl;
    auto len = strlen( cp );
    char *newp = alloc.allocate( len );
    uninitialized_copy( cp, cp + len, newp );
    if( p )
        alloc.deallocate( p, sz );
    sz = len;
    p = newp;

    return *this;
}

String& String::operator=( String &&s ) noexcept {
    cout << "调用了移动赋值运算符" << endl;
    if( this != &s ){
        if( p )
            alloc.deallocate( p, sz );
        p = s.p;
        sz = s.sz;

        s.p = nullptr;
        s.sz = 0;
    }
    return *this;
}

#endif // STRING_H_INCLUDED



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

答:

右值引用就是必须绑定到右值的引用。我们通过&&而不是&来获得右值引用。右值引用只能绑定到即将销毁的对象上,因此可以自由地移动其资源。

左值引用,也就是“常规引用”,不能绑定到要转换的表达式、字面常量或返回幼稚的表达式。而右值引用相反,可以绑定到这类表达式,但不能绑定到一个左值上。

返回左值的表达式包括返回左值引用及赋值、下标、解引用和前置递增/递减运算符,返回右值的包括返回非引用类型的函数及算术、关系、位和后置递增/递减运算符。可以看到,左值的特点是有持久的状态,而右值则是短暂的。


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

int &&r1 = f();

int &r2 = vi[0];

int &r3 = r1;

int &&r4 = vi[0] * f();


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

略了。


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

略了。


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


#ifndef STRVEC_H_INCLUDED
#define STRVEC_H_INCLUDED

//类 vector类分配策略的简化实现
#include<memory>
#include<utility>
#include<initializer_list>
#include<algorithm>

using namespace std;

class StrVec{
public:
    //默认构造函数
    StrVec():
        elements( nullptr ), first_free( nullptr ), cap( nullptr ) { }

    //其他构造函数
    StrVec( initializer_list<string>& );
    //拷贝构造函数
    StrVec( const StrVec& );
    //析构函数
    ~StrVec();
    //拷贝赋值运算符
    StrVec& operator=( const StrVec& );
    //移动构造函数
    StrVec( StrVec &&sv ) noexcept :
        elements( sv.elements ), first_free( sv.first_free ), cap( sv.cap ) { }

    //移动赋值运算符
    StrVec& operator=( StrVec &&sv ) noexcept;

    void push_back( const string& );
    size_t size() const { return first_free - elements; }
    size_t capacity() const { return cap - elements; }
    void resize( size_t n );
    void reserve( size_t n );
    string* begin() const { return elements; }
    string* end() const { return first_free; }

private:

    //数据成员
    static allocator<string> alloc;

    string *elements;
    string *first_free;
    string *cap;

    //工具函数
    void chk_n_alloc();
    pair<string*, string*>
        alloc_n_copy( const string*, const string* );
    void free();
    void reallocate();
    void reallocate( size_t n );

};
//静态成员类外定义
allocator<string> StrVec::alloc;

inline
StrVec::StrVec( initializer_list<string> &ls ){
    auto newdata = alloc_n_copy( ls.begin(), ls.end() );

    elements = newdata.first;
    first_free = cap = newdata.second;
}


//如果没有空间,则调用reallocate分配更多内存。
inline void StrVec::chk_n_alloc( void ){
    if( size() == capacity() )
        reallocate();
}


//分配内存,并拷贝一个给定范围中的元素
pair<string*, string*>
        StrVec::alloc_n_copy( const string *b, const string *e )
{
    auto data = alloc.allocate( e - b );

    return { data, uninitialized_copy( b, e, data ) };
}

//会销毁构造的元素并释放内存

/*
void StrVec::free(){
    //不能传递给deallocate一个空指针
    if( elements ){//逆序销毁旧元素
        for( auto p = first_free; p != elements;  ) //for循环的第三个表达式为空。
            alloc.destroy( --p );
        alloc.deallocate( elements, cap - elements );
    }
}
*/
//用for_each和lambda重写free
void StrVec::free(){
    for_each( elements, first_free, []( const string &s ) { alloc.destroy( &s ); } );
    alloc.deallocate( elements, cap - elements );
}


StrVec::StrVec( const StrVec &sv ){
    auto newdata = alloc_n_copy( sv.begin(), sv.end() );
    elements = newdata.first;
    first_free = cap = newdata.second;
}


StrVec::~StrVec(){
    free();
}

StrVec& StrVec::operator=( const StrVec &sv ){
    auto newdata = alloc_n_copy( sv.begin(), sv.end() );
    free();
    elements = newdata.first;
    first_free = cap = newdata.second;

    return *this;
}

StrVec& StrVec::operator=( StrVec &&sv ) noexcept{
    if( this != &sv ){
        free();
        elements = sv.elements;
        first_free = sv.first_free;
        cap = sv.cap;
        //将rhs置于可析构的状态
        sv.elements = sv.first_free = sv.cap = nullptr;
    }
    return *this;
}


void StrVec::push_back( const string &s ){
    chk_n_alloc();

    alloc.construct( first_free++, s );
}


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( size_t n ){
    auto newdata = alloc.allocate( n );
    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 = newdata + n;
}

void StrVec::resize( size_t n ){
    if( n > size() ){
        while( size() < n )
            push_back( "" );
    }
    else if( n < size() ){
        while( size() > n )
            alloc.destroy( --first_free );
    }
}

void StrVec::reserve( size_t n ){
    if( n > capacity() )
        reallocate( n );
}

#endif // STRVEC_H_INCLUDED

String的代码见练习13.44。

Message略了。


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

略。


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

答:

因为函数返回类型是值方式,所以返回的是右值,所以初始化或者赋值给一个unique_ptr时会调用它的移动构造函数或者移动赋值运算符接管此函数中unique_ptr变量的所有权。所以是合法的。


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

对于hp = hp2:

hp2是一个左值,因此在参数传递时,构造形参rhs调用的是拷贝构造函数,rhs将获得hp2的一个副本,rhs和hp2是独立的,而且string的内容相同。赋值结束后,rhs被销毁。

对于hp = std::move(  hp2 );

构造rhs的时候调用的是移动构造函数,rhs接管hp2的所有权,然后swap交换this和rhs的指针,rhs指向this中数据成员原来指向的string,而this数据成员指向hp2指向的string。


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

进行赋值拷贝时,先拷贝构造了rhs,再交换到hp。

移动赋值类似。


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

编译错误。产生二义性。


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


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

Foo Foo::sorted() const & {

Foo ret( *this );

return ret.sorted();

}

ret是一个左值,会导致递归调用循环。


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

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

编译器会认为Foo(*this)是一个无主的右值,对他调用sorted()的右值版本。


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

略。







阅读更多
换一批

没有更多推荐了,返回首页