C++ Primer 0x0D 练习题解

Posted 鱼竿钓鱼干

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ Primer 0x0D 练习题解相关的知识,希望对你有一定的参考价值。

📔 C++ Primer 0x0D 练习题解

更好的阅读体验(实时更新与修正)

推荐阅读 《C++ Primer 5th》知识点总结&练习题解

13.1 拷贝、赋值与销毁

13.1.1 拷贝构造函数

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

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

当拷贝初始化的时候会使用拷贝构造函数

拷贝初始化不仅在我们用=定义变量时发生,在下列情况也会发生

  • 将一个对象作为实参传递给一个非引用类型的形参
  • 从一个返回类型为非引用类型的函数返回一个对象
  • 用花括号列表初始化一个数组中的元素或聚合类中的成员
  • 某些类型还会对它们所分配的对象使用拷贝初始化(insertpush会进行拷贝初始化,emplace会直接初始化)

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

Sales_data::Sales_data(Sales_data rhs);

参数应该是自身类型的引用

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

StrBlob使用了shared_ptr引用计数加一

StrBlobPtr使用了weak_ptr引用计数不增加

其他数据成员直接拷贝

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

Point global;
Point foo_bar(Point arg) // 1

	Point local = arg, *heap = new Point(global); // 2,3
	*heap = local; //4
	Point pa[4] =  local, *heap ; // 5, 6
	return *heap;  // 7

  • 将一个对象作为实参传递给一个非引用类型的形参(1)
  • 使用=定义变量(2,3,4)
  • 用花括号列表初始化一个数组中的元素或聚合类中的成员(5,6 用了两次)
  • 从一个返回类型为非引用类型的函数返回一个对象(7)

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

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

#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)
private:
	std::string *ps;
	int i;
;

13.1.2 拷贝赋值运算符

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

  • 拷贝运算符是函数operator=的一个重载,拷贝赋值运算符接受一个与其所在类相同类型的参数

在使用赋值运算时会使用拷贝赋值运算符

合成拷贝赋值运算符

  • 如果一个类未定义自己的拷贝赋值运算符,编译器会为它生产一个合成拷贝赋值运算符
  • 对于某些类,合成拷贝赋值运算符用来禁止该类型对象的赋值
  • 一般的,拷贝赋值运算符会将右侧运算对象的每个非static成员赋予左侧运算对象的对应成员,这一工作是通过成员类型的拷贝赋值运算符来完成的。对于数组类型的成员,逐个赋值数组元素
  • 合成拷贝赋值运算符返回一个指向左侧运算对象的引用

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

StrBlob:会通过shared_ptr类的拷贝赋值运算符将shared_ptr拷贝赋值,且其计数器自增。

StrBlobPtr:会通过weak_ptr类的拷贝赋值运算符将weak_ptr拷贝赋值。curr调用内置类型的赋值运算符。

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)
		auto newp = new std::string(*rhs.ps);
		delete ps;
		ps = newp;
		i = rhs.i;
		return *this;
	
    ~HasPtr()  delete ps; 
private:
	std::string *ps;
	int i;
;

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

  • 析构函数:释放对象使用的资源,并销毁对象的非static数据成员
  • 析构函数是类的一个成员函数,名字波浪号接类名,没有返回值,也不接受参数,不能被重载,对一个给定类唯一

析构函数完成什么工作

  • 在析构函数中,首先执行函数体,然后销毁成员,成员按初始化顺序的逆序销毁
  • 不像构造函数有初始化列表,析构函数的析构部分是隐式的,成员销毁时发生什么完全依赖成员的类型
    • 销毁类类型的成员执行类类型的析构函数
    • 内置类型没有析构函数,销毁内置类型成员什么也不需要做
    • 隐式销毁一个内置指针类型的成员不会delete它所指向的对象
    • 与普通指针不同,智能指针是类类型,所以具有析构函数。智能指针成员在析构阶段会被自动销毁

合成析构函数

  • 当一个类未定义自己的析构函数时,编译器会为它定义一个合成析构函数
  • 对于某些类,合成析构函数被用来阻止该类型的对象被销毁
  • 一般,合成析构函数的函数体为空
  • **认识到析构函数体自身并不直接销毁成员很重要。成员是在析构函数体之后隐含的析构阶段被销毁的。**在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分进行的

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

StrBlobshared_ptr的引用计数减少

StrBlobPtr:不影响引用计数

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& rhs)
		auto newp = new std::string(*rhs.ps);
		delete ps;
		ps = newp;
		i = rhs.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();

3次

item1item2accum离开作用域被销毁

trans是指向对象的指针,析构函数不会执行

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 <string>
#include <vector>

struct X 
	X() std::cout << "X()" << std::endl;
	X(const X&) std::cout << "X(const X&)" << std::endl;
	X& operator=(const X&rhs)std::cout << "X& operator=(const X&rhs)" << std::endl;
	~X()std::cout << "~X()" << std::endl;
;
void f(X a,const X& b)
	std::vector<X>v;
	std::cout << "push_back a" << std::endl;
	v.push_back(a);
	std::cout << "push_back b" << std::endl;
	v.push_back(b);



int main()
	std::cout << "create pb" << std::endl;
	X a;

	std::cout << "create pb" << std::endl;
	X* pb = new X(a);

	std::cout << "f(a,*pb)" << std::endl;
	f(a,*pb);
	delete pb;


	
	return 0;

13.1.4 三/五法则

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 也没有关系

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

输出三个不一样的数字,分别是a,b,c的数字

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

3.14

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

class numbered
public:
	friend void f(numbered s);
	numbered() mysn = num++;
private:
	static int num;
	int mysn;
;

int numbered::num = 0;

void f (numbered s)  std::cout << s.mysn << std::endl; 

int main()
	numbered a,b=a,c=b;
	f(a);f(b);f(c);
	return 0;

输出三个0

3.15

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

class numbered
public:
	friend void f(numbered s);
	numbered() mysn = num++;
	numbered(const numbered &s)
		mysn = num++;
	
private:
	static int num;
	int mysn;
;

int numbered::num = 0;

void f (numbered s)  std::cout << s.mysn << std::endl; 

int main()
	numbered a,b=a,c=b;
	f(a);f(b);f(c);
	return 0;

输出3,4,5

3.16

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

class numbered
public:
	friend void f(const numbered &s);
	numbered() mysn = num++;
	numbered(const numbered &s)
		mysn = num++;
	
private:
	static int num;
	int mysn;
;

int numbered::num = 0;

void f (const numbered &s)  std::cout << s.mysn << std::endl; 

int main()
	numbered a,b=a,c=b;
	f(a);f(b);f(c);
	return 0;

输出0,1,2

13.1.6 阻止拷贝

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

class Employee
public:
	Employee()id = id_num++;
	Employee(const std::string &s):name(s),id(id_num)
		id_num++;
	

private:
	std::string name;
	int id;
	static int id_num;
;
static int id_num = 1000;

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

不需要拷贝,雇员没有必要因为拷贝而递增编号。将拷贝构造和赋值构造运算符定义为删除来阻止拷贝和赋值

class Employee
public:
	Employee()id = id_num++;
	Employee(const std::string &s):name(s),id(id_num)
		id_num++;
	
	Employee(const Employee &)=delete;
	Employee& operator=(const Employee&)=delete;

private:
	std::string name;
	int id;
	static int id_num;
;
static int id_num = 1000;

13.20 解释当我们拷贝、赋值或销毁 TextQueryQueryResult 类对象时会发生什么?

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

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

不需要,因为用的智能指针,合成版本可以自动控制内存释放

当我们决定一个类是否要定义他自己版本的拷贝控制成员时,一个基本原则就是确定这个类是否需要析构函数,需要析构函数的类也需要拷贝和赋值操作

13.2 拷贝控制和资源管理

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

13.8

13.2.1 行为像值的类

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

因为先把整章看了一遍才写练习的,所以基本没区别。

朴素一些的写法是用if判一下是不是自赋值,高级一点的写法是用swap来实现,后面会有

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

三/五法则,拷贝成员控制的几个操作应该看成一个整体,一般要写就都写

未定义析构函数会内存泄漏

未定义拷贝构造函数会只拷贝指针的值,几个指针指向同一个地址

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

拷贝构造函数和拷贝赋值运算符要重新动态分配内存

因为用的智能指针,可以自动控制内存释放,引用计数为0自动销毁对象,所以不需要析构函数

13.26 对上一题中描述的 strBlob 类,编写你自己的版本。

StrBlob里添加

//拷贝构造
StrBlob(const StrBlob& sb):data(std::make_shared<std::vector<std::string>>(*sb.data))
//拷贝赋值
StrBlob& operator=(const StrBlob& sb)
    data = std::make_shared<std::vector<std::string>>(*sb.data);
    return *this;

13.2.2 定义行为像指针的类

13.27 定义你自己的使用引用计数版本的 HasPtr

class HasPtr 
public:
	HasPtr(const std::string &s = std::string())
		:ps(new std::string(s)),i(0),use(new std::size_t(1))
	HasPtr(const HasPtr &p)
		:ps(p.ps),i(p.i),use(p.use)++*use;
	HasPtr& operator=(const HasPtr& rhs)
		++*rhs.use;
		if(--*use == 0)
			delete ps;
			delete use;
		
		ps = rhs.ps;
		i = rhs.i;
		use = rhs.use;
		return *this;
	
	~HasPtr()
		if(--*use == 0)
			delete ps;
			delete use;
		
	
private:
	std::string *ps;
	int i;
	std::size_t *use;//引用计数
;

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

TreeNode 做了修改,count应该是引用计数,照道理这个应该放内存而不属于任何一个对象,所以改成了int* 类型

(a) 
class TreeNode 
private:
	std::string value;
	int *count;
	TreeNode *left;
	TreeNode *right;	
;
(b)
class BinStrTree
private:
	TreeNode *root;	
;
#include <cstddef>
#include <string>


class TreeNode 
public:
	TreeNode()
		:value(std::string()),count(new int(1)),left(nullptr),right(nullptr)
	TreeNode(const TreeNode& t)
		:value(t.value),count(t.count),left(t.left),right(t.right)
			++*count;
		
	TreeNode& operator=(const TreeNode& rhs)
		++*rhs.count;
		if(--*count == 0)
			delete left;
			delete right;
			delete count;
		
		value = rhs.value;
		left = rhs.left;
		right = rhs.right;
		return *以上是关于C++ Primer 0x0D 练习题解的主要内容,如果未能解决你的问题,请参考以下文章

C++ Primer 0x0B 练习题解

C++ Primer 0x03 练习题解

C++ Primer 0x08 练习题解

C++ Primer 0x09 练习题解

C++ Primer 0x04 练习题解

C++ Primer 0x07 练习题解