C++ Primer 0x0D 练习题解
Posted 鱼竿钓鱼干
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ Primer 0x0D 练习题解相关的知识,希望对你有一定的参考价值。
📔 C++ Primer 0x0D 练习题解
推荐阅读 《C++ Primer 5th》知识点总结&练习题解
13.1 拷贝、赋值与销毁
13.1.1 拷贝构造函数
13.1 拷贝构造函数是什么?什么时候使用它?
如果一个构造函数的第一个参数是自身类类型的引用(且一般是一个const
的引用),且任何额外参数都有默认值,则此构造函数是拷贝构造函数
当拷贝初始化的时候会使用拷贝构造函数
拷贝初始化不仅在我们用=
定义变量时发生,在下列情况也会发生
- 将一个对象作为实参传递给一个非引用类型的形参
- 从一个返回类型为非引用类型的函数返回一个对象
- 用花括号列表初始化一个数组中的元素或聚合类中的成员
- 某些类型还会对它们所分配的对象使用拷贝初始化(
insert
、push
会进行拷贝初始化,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
对象销毁时呢?
StrBlob
:shared_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次
item1
、item2
、accum
离开作用域被销毁
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 分别编写前三题中所描述的
numbered
和f
,验证你是否正确预测了输出结果。
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 解释当我们拷贝、赋值或销毁
TextQuery
和QueryResult
类对象时会发生什么?
因为这两个类中使用的是智能指针,因此在拷贝时,类的所有成员都将被拷贝,在销毁时所有成员也将被销毁。
13.21你认为
TextQuery
和QueryResult
类需要定义它们自己版本的拷贝控制成员吗?如果需要,为什么?实现你认为这两个类需要的拷贝控制操作。
不需要,因为用的智能指针,合成版本可以自动控制内存释放
当我们决定一个类是否要定义他自己版本的拷贝控制成员时,一个基本原则就是确定这个类是否需要析构函数,需要析构函数的类也需要拷贝和赋值操作
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
类就仍能使用指向vector
的weak_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 练习题解的主要内容,如果未能解决你的问题,请参考以下文章