c++ primer 第五版第十三章

不知道这份答案是否有人看,看某几章的答案有一点阅读量,如果有任何错误或意见可以评论哦~
13章内存管理这部分看了两周多,好慢好慢,不是很好理解~ 有没有也正在看这本书的小伙伴呀,欢迎一起讨论呢

13.01 拷贝构造函数是什么?什么时候使用它?

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

拷贝函数通常再发生拷贝初始化的时候使用,而拷贝初始化发生的条件如下:

  1. 使用“=”定义变量时。
  2. 将一个对象作为实参传递给一个非引用类型的形参。
  3. 从一个返回类型为非引用类型的函数返回一个对象。
  4. 用花括号列表初始化一个数组中的元素或一个聚合类中的成员。

13.02 解释为什么下面的声明是非法的:

Sales_data::Sales_data(Sales_data rhs);

拷贝构造函数的第一个参数必须是一个引用类型。

13.03 当我们拷贝一个StrBlob时,会发生什么?拷贝一个StrBlobPtr呢?

void test1303()
{
    StrBlob sb1;
    cout << "reference count of sb1 is " << sb1.count() << endl;	// 1
    StrBlob sb2(sb1);
    cout << "reference count of sb1 is " << sb1.count() << endl;	// 2

    StrBlobPtr sbp1(sb1);
    cout << "reference count of sbp1 is " << sbp1.count() << endl;		// 2
    StrBlobPtr sbp2 (sbp1);
    cout << "reference count of sbp1 is " << sbp1.count() << endl;		// 2
}

拷贝StrBlob时,其shared_ptr成员的引用计数会增加。拷贝StrBlobPtr,unique_ptr成员的引用计数不变,其引用了shared_ptr,但不影响shared_ptr的引用计数。

13.04 假定Point是一个类类型,它有一个public的拷贝构造函数,指出下面程序片段中哪些地方使用了拷贝构造函数。

Point global;
Point foo_bar(Point arg)							// 使用了拷贝构造1
{
    Point local = arg, *heap = new Point(global);	  // 使用了拷贝构造2,3
    *heap = local;
    Point pa[4] = {local, *heap};					// 使用了拷贝构造4,5
    return *heap;								   // 使用了拷贝构造
}

13.05 给定下面的类框架,编写一个拷贝构造函数,拷贝所有成员。你的构造函数应该动态分配一个新的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.06 拷贝赋值运算符是什么?什么时候使用它?合成拷贝赋值运算符完成什么工作?什么时候会生成合成拷贝赋值运算符?

拷贝赋值运算符是重载”=“运算符,即为一个名为operator=的函数,其参数与其所在类的的类型相同。

在发生赋值操作的时候使用。

合成拷贝赋值的工作:将后侧运算对象的每个非static成员赋予左侧运算对象的对应成员。

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

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

void test1307()
{
    StrBlob sb1;
    cout << "reference count of sb1 is " << sb1.count() << endl;	// 1
    StrBlob sb2 = sb1;
    cout << "reference count of sb1 is " << sb1.count() << endl;	// 2

    StrBlobPtr sbp1(sb1);
    cout << "reference count of sbp1 is " << sbp1.count() << endl;	// 2
    StrBlobPtr sbp2 = sbp1;
    cout << "reference count of sbp1 is " << sbp1.count() << endl;	// 2
}
// 会发生浅拷贝,所有的指针都指向同一块内存。与拷贝一样,赋值StrBlob时,shared_ptr的引用计数加1,赋值StrBlobPtr时,引用计数不变。

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

HasPtr& operator= (const HasPtr& hp)
	{
	    ps = new std::string (*hp.ps);
	    i = hp.i;
	    return *this;
	}

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

析构函数是类的一个成员函数,名字由波浪号接类名构成。它没有返回值,也不接受参数。

对于某些类,合成析构函数被用来阻止该类型的对象被销毁。如果不是这种情况,合成析构函数的函数体就为空。

当一个类未定义自己的析构函数时,编译器会为它定义一个合成析构函数。

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

销毁StrBlob时,分别会执行vector、shared_ptr、string的析构函数,vector析构函数会销毁我们添加到vector中的元素,shared_ptr析构函数会递减StrBlob对象的引用计数,

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

~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();
} // 退出作用域时,对item1和item2调用析构函数。
// 参数accum在函数结束时会调用吗?
// 当一个对象的引用或指针离开作用域,并不会执行析构。

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

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

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

struct X {
    X() { std::cout << "X()" << std::endl; }
    X(const X&) { std::cout << "X(const X&)" << std::endl; }
    X& operator= (const X& x);
    ~ X() { std::cout << "~X()" << std::endl; }
};

X& X::operator=(const X& x)
{
    std::cout << "~X()" << std::endl;
}

void f (X& x1, X* x2)
{
    vector<X> xvec;
    xvec.push_back(x1);
    xvec.push_back(*x2);
    cout << "------- destructor in f -----" << endl;
}

void test1313()
{
    cout << "------- constructor -----" << endl;
    X x1;
    X *x2 = new X();
    cout << "------- copy constructor -----" << endl;
    f (x1, x2);
    cout << "------- destructor 1-----" << endl;
    delete x2;
    cout << "------- destructor 2-----" << endl;
}

// 结果如下
------- constructor -----
X()
X()
------- copy constructor -----
X(const X&)							// 调用f函数,为什么发生三次拷贝构造和一次析构???
X(const X&)
X(const X&)
~X()
------- destructor in f -----
~X()
~X()
------- destructor 1-----
~X()
------- destructor 2-----
~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(c);

会输出三个相同的序号。因为是合成拷贝,因此a, b, c实际使用同一个mysn结构。

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

会改变,因为调用f函数传参时以传值方式传递,需要调用拷贝构造函数,会生成一个新的序号,因此也会输出三个不同的序号。

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

不会改变,因为如果传的是引用,则传递时numbered对象不会调用拷贝构造,因此三个输出应该相同。

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

class numbered
{
public:
    numbered() { mysn = ++ initail_number; }
    numbered (const numbered& n) 
    { 
        mysn = ++ initail_number; 
        std::cout << "use copy constructor." << std::endl;
    }
    int mysn;
    static int initail_number;
};

int numbered::initail_number = 2002;

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

void test1317_3()
{
    numbered a, b = a, c = b;
    cout << "--------" << endl;
    f(a);
    f(b);
    f(c);
}
// 如果没有拷贝构造函数,则输出都是2003;
// 定义拷贝构造函数,输出分别是2003,2004,2005
// 第三种情况输出如下:
use copy constructor.
use copy constructor.
--------
2003
2004
2005
// 在执行numbered a, b = a, c = b; 也会调用拷贝构造函数,因为定义一个b,c是新的对象,调用拷贝构造

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

class Employee
{
public:
    Employee () = default;
    Employee (const string& n) : name (n)
    {
        employee_id = ++ increment_number;
    }

private:
    static int increment_number;
    int employee_id;
    string name;
};

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

不需要拷贝控制成员,因为每个雇员的ID都不同,不能有两个id和name都相同的雇员。

class Employee
{
public:
    Employee () = default;
    Employee (const string& n) : name (n)
    {
        employee_id = ++ increment_number;
    }

    Employee (const Employee& ) = delete;
    Employee& operator= (const Employee&) = delete;
private:
    static int increment_number;
    int employee_id;
    string name;
};

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

因为这两个类中使用的是智能指针,因此在拷贝时,类的所有成员都将被拷贝,在销毁时所有成员也将被销毁。

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

判断一个类是否需要自己版本的拷贝控制成员,一个基本原则是首先确定这个类是否需要一个析构函数。TextQuery和QueryResult类使用智能指针,可以自动控制释放内存,因为其不需要自己版本的析构函数,也就不需要自己版本的拷贝控制函数了。

13.22 假定我们希望HasPtr的行为像一个值。即,对于对象所指向的string成员,每个对象都有一份自己的拷贝。我们将在下一节介绍拷贝控制成员的定义。但是,你以及学习了定义这些成员所需要的所有知识。在继续学习下一节之前,为HasPtr编写拷贝构造函数和拷贝赋值运算符。

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 newp = new std::string(*hp.ps);
	    delete ps;
	    ps = newp;
	    i = hp.i;
        
        return *this;
	}

	string& getPs()
	{
	    cout << ps << endl;
	    return *ps;
    }

	~HasPtr () { delete ps; }
private:
    std::string *ps;
    int i;
};

13.23 比较上一节练习中你编写的拷贝控制成员和这一节中的代码。确定你理解了你的代码和我们的代码之间的差异(如果有的话)。

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。

class HasPtr {
public:
	HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0), use_count(new std::size_t(1)){}
	HasPtr (const HasPtr& hp) : ps(hp.ps), i(hp.i), use_count(hp.use_count)
	{
	    ++ *use_count;
	}

	HasPtr& operator= (const HasPtr& hp);
	string& getPs()
	{
	    cout << ps << endl;
	    return *ps;
    }

	~HasPtr ()
	{
	    if (0 == *--use_count) {
            delete ps;
            delete use_count;
	    }
    }
private:
    std::string *ps;
    int i;
    std::size_t *use_count;
};

HasPtr& HasPtr::operator=(const HasPtr& hp)
{
    ++ *hp.use_count;       // 递增右侧运算对象的引用计数
    if (0 == --*use_count) {
        delete ps;
        delete use_count;
    }
    ps = hp.ps;
    i = hp.i;
    use_count = hp.use_count;

    return *this;
}

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

// .h文件
class TreeNode
{
public:
    TreeNode () : value(std::string()), count (new int(1)), left(nullptr), right(nullptr) {}
    TreeNode (const TreeNode& tn) : value (tn.value), count (tn.count), left (tn.left), right(tn.right) { ++ *count; }
    TreeNode& operator= (const TreeNode& tn);
    ~ TreeNode()
    {
        if (0 == -- *count) {
            delete left;
            delete right;
            delete count;
        }
    }
private:
    std::string value;
    int         *count;
    TreeNode    *left;
    TreeNode    *right;
};

class BinStrTree
{
public:
    BinStrTree() : root (new TreeNode()) {}
    BinStrTree(const BinStrTree& bst) : root (bst.root) {}
    BinStrTree& operator= (const BinStrTree& bst);
    ~ BinStrTree () { delete root; }
private:
    TreeNode *root;
};

// .cpp 文件
TreeNode& TreeNode::operator= (const TreeNode& tn)
{
    ++ *tn.count;

    if (0 == *--count) {
        delete left;
        delete right;
        delete count;
    }
    value = tn.value;
    count = tn.count;
    left = tn.left;
    right = tn.right;
}

BinStrTree& BinStrTree::operator= (const BinStrTree& bst)
{
    TreeNode *new_root = new TreeNode(*bst.root);
    delete root;
    root = new_root;
    return *this;
}

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

因为swap(HasPtr&, HasPtr&)函数中,swap (lhs.ps, rhs.ps);调用了std::swap(string, string),swap (lhs.i, rhs.i);调用了std::swap(int, int)。调用了不同的swap函数,因此不会导致递归循环。

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

// .h文件
class HasPtr {
    friend void swap(HasPtr&, 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 newp = new std::string(*hp.ps);
	    delete ps;
	    ps = newp;
	    i = hp.i;
        return *this;
	}

	std::string& getPs()
	{
	    return *ps;
    }

	~HasPtr () { delete ps; }
private:
    std::string *ps;
    int i;
};

inline void swap(HasPtr& lhs, HasPtr& rhs)
{
    using std::swap;
    swap (lhs.ps, rhs.ps);
    swap (lhs.i, rhs.i);
    std::cout << "call the swap function." << std::endl;
}

// 测试 .cpp
int main()
{
    HasPtr hp1 ("hello");
    HasPtr hp2 ("world");

    swap(hp1, hp2);
    cout << hp1.getPs() << endl;
    cout << hp2.getPs() << endl;

    return 0;
}

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

// .h文件,注意要重写operator=、swap函数;新增operator<函数。
class HasPtr {
    friend void swap(HasPtr&, HasPtr&);
    friend bool operator< (HasPtr& lhs, HasPtr& rhs);
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= (HasPtr hp)
	{
	    this->swap(hp);
        std::cout << "call operator=(HasPtr &rhs)" << std::endl;
	    return *this;
	}

	void swap(HasPtr &rhs)
    {
        using std::swap;
        swap(ps, rhs.ps);
        swap(i, rhs.i);
        std::cout << "call swap(HasPtr &rhs)" << std::endl;
    }

	std::string& getPs()
	{
	    return *ps;
    }

	~HasPtr () { delete ps; }
private:
    std::string *ps;
    int i;
};

void swap(HasPtr& lhs, HasPtr& rhs)
{
    lhs.swap(rhs);
}

bool operator< (HasPtr& lhs, HasPtr& rhs)
{
    return *lhs.ps < *rhs.ps;
}

// .cpp文件
void test1331()
{
    HasPtr hp1 ("hello");
    HasPtr hp2 ("world");
    HasPtr hp3 ("Anna");
    vector<HasPtr> hvec;
    hvec.push_back(hp1);
    hvec.push_back(hp2);
    hvec.push_back(hp3);

    std::sort (hvec.begin(), hvec.end());

    for (auto i : hvec) {
        cout << i.getPs() << endl;
    }
}

// 输出结果:
------------
call swap(HasPtr &rhs)
call operator=(HasPtr &rhs)
call swap(HasPtr &rhs)
call operator=(HasPtr &rhs)
call swap(HasPtr &rhs)
call operator=(HasPtr &rhs)
call swap(HasPtr &rhs)
call operator=(HasPtr &rhs)
------------
Anna
hello
world

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

类指针的版本,使用std::swap函数就可以实现指针交换,节省内存空间,不需要重新自定义swap函数。因此类指针的版本不会从swap函数收益。

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

不定义为Folder的原因是:如果是值传递,则会在传参的时候传递参数的副本,执行修改参数的操作,修改的是参数的副本。而且要为副本重新分配内存,浪费空间。

不定义为const Folder&的原因是:在save和remove中都要改变参数,因此不能定义为const类型。

13.34 编写本节所描述的Message。

// .h 文件
class Folder;
class Message
{
    friend class Folder;
    friend void swap (Message& lhs, Message& rhs);
public:
    explicit Message (const std::string& str = "") : contents(str) {}
    Message (const Message&);
    Message& operator= (const Message&);
    ~ Message ();

    void save (Folder &f);
    void remove (Folder &f);
private:
    std::string contents;       // 实际消息文本
    std::set<Folder*> folders;      // 包含本Message的Folder
    void add_to_Folders (const Message&);
    void remove_from_Folders ();
};

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

    for (auto f : lhs.folders) {
        f->remMsg(&lhs);
    }

    for (auto f : rhs.folders) {
        f->remMsg(&rhs);
    }

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

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

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

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

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

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

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

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

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

Message& Message::operator= (const Message& rhs)
{
    this->remove_from_Folders();         // 先从当前文件中移除
    this->contents = rhs.contents;
    this->folders = rhs.folders;
    add_to_Folders(rhs);
    return *this;
}

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

如果使用合成的拷贝控制成员,则当拷贝一个Message对象时,不会向folder中添加新的Message,folder将不会同步更新。

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

class Folder
{
public:
    Folder ();
    Folder (const Folder&);
    Folder& operator= (const Folder&);
    ~ Folder();
    void addMsg (Message* m)
    {
        messages.insert(m);
    }
    void remMsg (Message* m)
    {
        messages.erase(m);
    }
private:
    std::set<Message*> messages;
};

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

// 在Message类中加:
	void addFolder (Folder* f)
    {
        folders.insert(f);
    }

    void deleteFolder (Folder* f)
    {
        folders.erase(f);
    }	

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

当涉及到动态分配时,使用拷贝和交换方式来实现赋值运算符是一个很好的方式(因为又共同的delete操作)。但Message类并未涉及到动态分配,此时如果使用拷贝和交换的方式就没有意义。

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

// .h文件
#include <string>

class StrVec
{
public:
    StrVec() : elements(nullptr), first_free(nullptr), cap(nullptr) {}
    StrVec(const StrVec&);
    StrVec& operator= (const StrVec&);
    ~ StrVec();

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

    void reserve(size_t n);
    void resize (size_t n);
    void resize (size_t, const std::string& s);

private:
    void chk_n_alloc()
    {
        if (size() == capacity()) {
            reallocate();
        }
    }
    std::pair<std::string*, std::string*> alloc_n_copy
            (const std::string* element, const std::string* cap);       // begin pointer and after tail pointer.
    void alloc_n_move (size_t capacity);
    void free();            // destroy the elements and free memory
    void reallocate();      //

private:
    std::allocator<std::string> alloc;
    std::string* elements;          // point to the first element of array
    std::string* first_free;        // point to the first free element of array
    std::string* cap;               // point to after the last element of array
};
// .cpp文件
void StrVec::reallocate()
{
    auto newcapacity = size() ? 2 * size() : 1;
    auto newdata = alloc.allocate (newcapacity);

    auto dest = newdata;        // point to the first free position of new array
    auto elem = elements;       // elements point to origin elements

    for (size_t i = 0; i != size(); ++ i) {
        alloc.construct (dest++, std::move(*elem++));
    }
    free();
    elements = newdata;
    first_free = dest;
    cap = elements + newcapacity;
}

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

StrVec& StrVec::operator=(const StrVec& s)
{
    auto new_data = alloc_n_copy (s.begin(), s.end());
    this->free ();
    this->elements = new_data.first;
    this->first_free = this->cap = new_data.second;

    return *this;
}


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

void StrVec::free()
{
    if (elements) {
        for (auto p = first_free; p != elements; ) {
            alloc.destroy (--p);
        }
        alloc.deallocate (elements, cap-elements);
    }
}
void StrVec::push_back(const std::string& s)
{
    chk_n_alloc();
    alloc.construct (first_free++, s);
}

pair<string*, string*> StrVec::alloc_n_copy(const std::string* element, const std::string* cap)
{
    auto data = alloc.allocate (cap-element);
    return {data, uninitialized_copy(element, cap, data)};
}

void StrVec::alloc_n_move(size_t capacity)
{
    auto newdata = alloc.allocate (capacity);

    auto dest = newdata;
    auto elem = elements;

    for (size_t i = 0; i != size(); ++i) {
        alloc.construct (dest++, std::move(*elem++));
    }
    elements = newdata;
    first_free = dest;
    cap = elements + capacity;
}
void StrVec::reserve(size_t n)
{
    if (n <= capacity()) return;
    alloc_n_move (n);
}

void StrVec::resize(size_t n)
{
    resize (n, "");
}

void StrVec::resize (size_t n, const std::string& s)
{
    if (n > size()) {
        if (n > capacity()) {
            reserve (n);
        }

        for (size_t i = size(); i < n; ++i) {
            alloc.construct (first_free++, s);
        }
    }
    else if (n < size()) {
        while ((elements+n) != first_free) {
            alloc.destroy (--first_free);
        }
    }
}

13.40 为你的StrVec类添加一个构造函数,它接受一个initializer_list参数。

// .h文件中添加:
public:
	StrVec(std::initializer_list<std::string>& il);
private:
	void range_initial(const std::string* first, const std::string* last);
// .cpp文件中添加:
void StrVec::range_initial(const string* first, const string* last)
{
    auto newdata = alloc_n_copy (first, last);
    elements = newdata.first;
    first_free = cap = newdata.second;
}

StrVec::StrVec(std::initializer_list<string>& il)
{
    range_initial (il.begin(), il.end());
}

13.41 在push_back中,我们为什么在construct调用中使用前置递增运算?如果使用后置递增运算的话,会发生什么?(题目有误,construct中使用的是后置递增运算)

first_free指向第一个空位置,使用后置递增,在插入元素时插入当前指向的空位置,然后递增移向后移。

如果使用前置递增,则first_free先向后移动,再插入,此时first_free指向第二个空位置。

13.42 在你的TextQuery和QueryResult类中用你的StrVec类代替vector,以此来测试你的StrVec类。

//在StrVec.h中增加at方法的实现:
std::string& at (size_t pos) { return *(elements+pos); }
const std::string& at(size_t pos) const { return *(elements + pos); }

#include "StrVec.h"

class QueryResult;
class TextQuery
{
public:
//    using line_no = StrVec::size_type();
    TextQuery(std::ifstream& ifs);
    QueryResult query (const std::string& word) const;

private:
    std::shared_ptr<StrVec> sp_text;
    // 每个单词到它所出现的行号的映射
    std::map<std::string, std::shared_ptr<std::set<size_t>>> sp_dictionary;
};

// textQuery.h文件实现,将vector<string>都改为StrVec,原来的line_no用size_t替换。
class QueryResult
{
public:
     friend std::ostream& print (std::ostream&, const QueryResult&);
public:
    QueryResult(const std::string& s,
                std::shared_ptr<std::set<size_t>> sp_set,
                std::shared_ptr<StrVec> sp_vec):
            sought (s), lines (sp_set), file (sp_vec) {}


private:
    std::string sought;     // 要查找的单词
    std::shared_ptr<std::set<size_t>> lines;       // 出现的行号
    std::shared_ptr<StrVec> file;     // 输入文件
//    vector<string> occur_line;
};

std::ostream& print (std::ostream&, const QueryResult&);

// textQuery.cpp的实现
#include "textQuery.h"
#include "StrVec.cpp"
// 我在windows下实现,这里要加include "StrVec.cpp",否则在链接时找不到StrVec类中方法的实现,就会报链接错误。在linux下应该可以把StrVec编译为目标文件,然后与textQuery一起编译。
// undefined reference to `StrVec::~StrVec()'
using namespace std;

TextQuery::TextQuery(std::ifstream& ifs) : sp_text (new StrVec())
{
    string text;

    while (getline(ifs, text)) {
        sp_text->push_back(text);
        int n = sp_text->size() - 1;    // 当前行号
        istringstream line (text);
        string word;
        while (line >> word) {
            auto &lines = sp_dictionary[word];

            if (!lines) {
                lines.reset (new set<size_t>);
            }
            lines->insert(n);
        }
    }
}

QueryResult TextQuery::query(const std::string& word) const
{
    // 如果未找到sought, 返回一个指向此set的指针。
    static shared_ptr<set<size_t>> nodata (new set<size_t>);
    auto loc = sp_dictionary.find(word);

    if (loc == sp_dictionary.end()) {
        return QueryResult(word, nodata, sp_text);
    }
    else {
        return QueryResult(word, loc->second, sp_text);
    }
}

ostream& print(ostream& os, const QueryResult& qr)
{
    os << qr.sought << " occurs " << qr.lines->size() << " times." << endl;

    for (auto i : *qr.lines) {
        os << "\t(line " << i+1 << qr.file->at(i) << endl;
    }
    return os;
}

void runQueries (ifstream &infile)
{
    TextQuery tq (infile);

    while (true) {
        cout << "Enter word to look for, or q to quit: ";
        string s;

        if (!(cin >> s) || s == "q") break;
            print (cout, tq.query(s)) << endl;
    }
}

int main()
{
    ifstream ifs ("C:/Users/tutu/Documents/code/cpp_primer/ch12/textQuery.cpp");
    runQueries(ifs);
    return 0;
}

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

 for_each(elements, first_free, [this](string& p){ alloc.destroy(&p); });
// for_each和lambda一起使用更简洁,但是for循环更易读。

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

// .h文件
#include <memory>

class String
{
public:
    String() : String("") {}
    String (const char* s);
    String (const String& s);
    String& operator= (String& s);
    ~ String();

    void free();
    const char* c_str() { return begin; }
    size_t size() { return end - begin; }
    size_t length() { return end - begin - 1; }
private:
    std::pair<char*, char*> alloc_n_copy (const char* beg, const char* end);
    void range_initial(const char* first, const char* last);

private:
    char *begin;
    char *end;
    std::allocator<char> alloc;
};

// .cpp 文件
#include "String.h"
#include <string.h>

using namespace std;

pair<char*, char*> String::alloc_n_copy (const char* beg, const char* end)
{
    auto str = alloc.allocate (end - beg);
    return {str, uninitialized_copy(beg, end, str)};
}

void String::range_initial (const char* first, const char* last)
{
    auto newString = alloc_n_copy (first, last);
    begin = newString.first;
    end = newString.second;
}

String::String(const char* s)
{
    range_initial (s, s+sizeof(s));
}

String::String (const String& s)
{
    range_initial (s.begin, s.end);
    cout << "copy constructor." << endl;
}

void String::free()
{
    if (begin) {
        for (auto i = begin; i != end; ++ i) {
            alloc.destroy (i);
        }
        alloc.deallocate (begin, end - begin);
    }

}

String& String::operator= (String& s)
{
    auto new_s = alloc_n_copy (s.begin, s.end);
    free();
    begin = new_s.first;
    end = new_s.second;
    
    cout << "copy assign constructor." << endl;
    return *this;
}

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

左值引用,也就是常规引用,返回左值的函数有赋值、下标、解引用和前置递增/递减运算符。

右值引用就是必须绑定到右值上的引用,返回右值的函数包括算术、关系、位及后置递增/递减运算符。

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

int f();
vector<int> vi(100);
int? r1 = f();				// int &&r = f(); f()的返回值相当于一个常量,只能做右值引用或const引用。
int? r2 = vi[0];			// 下标运算返回左值,所以应该用int &r = vi[0] 
int? r3 = r1;				// r1此时相当与变量,int &r3 = r1;
int? r4 = vi[0] * f();		 // 算术运算产生右值,int &&r4 = vi[0] * f();

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

同13.44题。

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

void foo(String x)
{
}

void bar(const String& x)
{
}

String baz()
{
  String ret("world");
  return ret;
}

int main()
{
  String s0;
  String s1("hello");
  String s2(s0);
  String s3 = s1;
  s2 = s1;

  foo(s1);
  bar(s1);
  foo("temporary");
  bar("temporary");
  String s4 = baz();

  std::vector<String> svec;
    cout << " ---------- " << endl;

  svec.push_back(s0);
  svec.push_back(s1);
  cout << " ---------- " << endl;
  svec.push_back(baz());
  svec.push_back("good job");
}
// print result:
copy constructor.
copy constructor.
copy assign constructor.
copy constructor.
 ----------
copy constructor.	
copy constructor.
copy constructor.		// push_back s0和s1,却调用3次拷贝构造,因为vector最初capacity为1,push_back(s1)时,capacity需要增长至2,原来的元素都要拷贝到新的内存中。
 ----------
copy constructor.		// capacity增长至4,拷贝之前的两个元素到新内存。
copy constructor.
copy constructor.
copy constructor.		// push_back("good job");调用一次拷贝构造。
// 每次push_back都会调用一次拷贝构造。

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

// Str.h文件中加:
StrVec (StrVec&& rhs) noexcept;
StrVec& operator= (StrVec&& rhs) noexcept;
// StrVec.cpp中加:
StrVec::StrVec(StrVec&& rhs) noexcept : elements (rhs.elements), first_free (rhs.first_free), cap (rhs.cap)
{
    rhs.elements = rhs.first_free = rhs.cap = nullptr;
}

StrVec& StrVec::operator=(StrVec&& rhs) noexcept
{
    if (this != &rhs) {
        free ();
        elements = rhs.elements;
        first_free = rhs.first_free;
        cap = rhs.cap;
        rhs.elements = rhs.first_free = rhs.cap = nullptr;
    }
    return *this;
}
//String.h
String (String&& s) noexcept;
String& operator= (String&& s) noexcept;
// String.cpp中加:
String::String(String&& s) noexcept : begin (s.begin), end (s.end)
{
    s.begin = s.end = nullptr;
}

String& String::operator= (String&& s) noexcept
{
    if (this != &s) {
        free();
        begin = s.begin;
        end = s.end;
        s.begin = s.end = nullptr;
    }
    return *this;
}
// Message.h
Message (Message&& m);
Message& operator= (Message&& m);
void moveFolders (Message* m);
// Message.cpp
void Message::moveFolders (Message* m)
{
    folders = std::move(m->folders);

    for (auto f : folders) {
        f->remMsg(m);
        f->addMsg(this);
    }
    m->folders.clear();
}

Message::Message (Message&& m) : contents (std::move(m.contents))
{
    moveFolders(&m);
}

Message& Message::operator=(Message&& m)
{
    if (this != &m) {
        remove_from_Folders();
        contents (std::move(m.contents));
        moveFolders(&m);
    }
    return *this;
}

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

int main()
{
  String s0;
  String s1("hello");
  String s2(s0);
  String s3 = s1;
  s2 = s1;

  cout << "1 ---------- " << endl;
  foo(s1);
  bar(s1);
  foo("temporary");
  bar("temporary");
  String s4 = baz();

  std::vector<String> svec;
    cout << "2 ---------- " << endl;

  svec.push_back(s0);
  svec.push_back(s1);
  cout << "3 ---------- " << endl;
  svec.push_back(baz());
  svec.push_back("good job");
}

// print result
String (const char* s)
construct
String (const char* s)
copy constructor.
copy constructor.
copy assign constructor.
1 ----------
copy constructor.
String (const char* s)
String (const char* s)
String (const char* s)
2 ----------
copy constructor.
copy constructor.
move construct.			// 内存增长到2时,旧元素移动到新内存中。
3 ----------
String (const char* s)
move construct.			// 有移动构造函数,push_back时会调用移动构造
move construct.			// 内存增长至4,旧有的2个元素移到新内存中
move construct.
String (const char* s)
move construct.

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

不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr,编译器知道要返回的对象将要被销毁,因此会执行一种特殊的"拷贝" -- 移动。最常见的就是从函数返回unique_ptr。

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

1、hp = hp2,hp2是一个左值,rhs将使用拷贝构造函数来初始化,拷贝构造函数将分配一个新的string,并拷贝hp2指向的string。

2、hp = std::move(hp2),我们调用std::move将一个右值引用绑定到hp2上。在此情况下,拷贝构造函数和移动构造函数都是可行的。但是,由于实参是一个右值引用,移动构造函数是精确匹配的。移动构造函数从hp2拷贝指针,而不会分配内存。

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

赋值运算效率低是因为调用的swap操作每次交换时,都会创建临时空间,用来存放临时变量的值。

// HasPtr.h
#include <iostream>

class HasPtr {
    friend void swap(HasPtr&, HasPtr&);
    friend bool operator< (HasPtr& lhs, HasPtr& rhs);
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= (HasPtr hp)
	{
	    this->swap(hp);
	    std::cout << "call operator=(HasPtr &rhs)" << std::endl;
	    return *this;
	}
    HasPtr (HasPtr&& hp) noexcept;
//    HasPtr& operator= (HasPtr&& hp) noexcept;

	void swap(HasPtr &rhs)
    {
        using std::swap;
        swap(ps, rhs.ps);
        swap(i, rhs.i);
        std::cout << "call swap(HasPtr &rhs)" << std::endl;
    }

	std::string& getPs()
	{
	    return *ps;
    }

	~HasPtr () { delete ps; }
private:
    std::string *ps;
    int i;
};

void swap(HasPtr& lhs, HasPtr& rhs)
{
    lhs.swap(rhs);
}

// HasPtr.cpp
using namespace std;
HasPtr::HasPtr(HasPtr&& hp) noexcept : ps (hp.ps), i (hp.i)
{
    hp.ps = nullptr;
    cout << "call move constructor." << endl;
}

int main()
{
    HasPtr hp1("hello"), hp2("world");
    //hp1 = hp2;
    hp1 = std::move (hp2);
    cout << hp1.getPs() << endl;
    return 0;
}

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

// 如果定义移动赋值运算符,未改变拷贝并交换运算符,则会出现如下报错:
error: ambiguous overload for 'operator=' (operand types are 'HasPtr' and 'std::remove_reference<HasPtr&>::type {aka HasPtr}')|

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

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()时不会匹配到Foo sorted() &&右值版本,而是继续调用自身Foo sorted() const&,因此程序会陷入无限递归中,并且不停复制this,造成栈内存泄漏。

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

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

因为Foo(*this)返回的是一个右值,因此可以调用sorted的右值版本,返回排序后的Foo。

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

// .h文件
class Foo
{
public:
    Foo (std::vector<int>& ivec) : data(ivec) {}
    Foo sorted() &&;
    Foo sorted() const &;
    std::vector<int>& getData() { return data; }
private:
    std::vector<int> data;
};

.cpp文件
Foo Foo::sorted() &&
{
    sort(data.begin(), data.end());
    cout << "right value sorted." << endl;
    return *this;
}

Foo Foo::sorted() const &
{
    cout << "left value sorted." << endl;
    Foo ret (*this);
//    return ret.sorted();
    sort (ret.data.begin(), ret.data.end());
    return ret;
 //   return Foo(*this).sorted();
}

int main()
{
    vector<int> ivec = {1, 2, 5, 4, 7};
    Foo f1(ivec);
    Foo f2 = f1.sorted();

    for (auto i : f2.getData()) {
        cout << i << " ";
    }
    cout << endl;

    return 0;
}
posted @ 2018-09-19 17:17  安月月  阅读(3776)  评论(2编辑  收藏  举报