笔记:
1.拷贝、赋值和销毁
重载运算符本质是函数。
析构函数是类的一个成员函数,没有返回值,也不接受参数,因此它不能被重载,对于一个给定类,只有唯一一个析构函数。
内置类型没有析构函数。
析构函数是不能删除的成员。
对于析构函数已删除的类型,不能定义该类型的变量或释放指向该类型动态分配对象的指针。
拷贝构造函数和拷贝赋值运算符是分开的。
2.拷贝控制和资源管理
赋值运算符通常组合了析构函数和构造函数的操作。
拷贝赋值运算符要保证即使是将一个对象赋予它自身,也要能正常工作。一个好方法是在销毁左侧运算对象资源之前拷贝右侧运算对象。
3.交换操作
与拷贝控制成员不同,swap并不是必要的。但是,对于分配了资源的类,定义swap可能是一种很重要的优化手段。
定义swap的类通常用swap来定义它们的赋值运算符。
使用拷贝和交换的赋值运算符自动就是异常安全的,且能正确处理自赋值。
4.拷贝控制示例
5.动态内存管理类
6.对象移动
IO类或unique_ptr这些类都包含不能被共享的资源(如指针或IO缓冲)。因此,这些类型的对象不能拷贝但可以移动。
在新标准中,我们可以用容器保存不可拷贝的类型,只要它们能被移动即可。
标准库容器、string和shared_ptr类既支持移动也支持拷贝。IO类和unique_ptr类可以移动但不可以拷贝。
一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。
左值持久,右值短暂。使用右值引用的代码可以自由地接管所引用的对象的资源。
我们在一个函数的参数列表后指定noexpect。在一个构造函数中,noexpect出现在参数列表和初始化列表开始的冒号之间。
不抛出异常的移动构造函数和移动赋值运算符必须标记为noexpect。
只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动移动赋值运算符。编译器可以移动内置类型的成员。
定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作。否则,这些成员默认地被定义为删除的。
引用限定符&,&&;&是左值限定符,&&是右值限定符。一个函数可以同时用const和引用限定。在此情况下,引用限定符必须跟随在const限定符之后,即const &。
如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须具有引用限定符。
课后习题:
练习13.1:拷贝构造函数是什么?什么时候使用它?
答:如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
拷贝构造函数在以下几种情况下会被使用:
1、拷贝初始化
2、将一个对象作为实参传递给非引用类型的形参
3、一个返回类型为非引用类型的函数返回一个对象
4、用花括号列表初始化一个数组中的元素或一个聚合类中的成员
5、初始化标准库容器或调用其insert/push操作时,容器会对其元素进行拷贝初始化。
练习13.2:解释为什么下面的声明是非法的:
Sales_data::Sales_data(Sales_data rhs);
答:这一声明是非法的, 因为对于上一题所述的情况,我们需要调用拷贝构造函数,但调用永远也不会成功。因为其自身的参数也是非引用类型,为了调用它,必须拷贝其实参,而为了拷贝实参,又需要调用拷贝构造函数,也就是其自身,从而造成死循环。
练习13.3:当我们拷贝一个StrBlob 时,会发生什么?拷贝一个StrBlobPtr 呢?
答:这两个类都没定义拷贝构造函数,因此编译器为它们定义了合成的拷贝构造函数。合成的拷贝构造函数逐个拷贝非const成员,对内置类型的成员,直接进行内存拷贝,对类类型的成员,调用其拷贝构造函数进行拷贝。
因此,拷贝一个StrBlob时,拷贝其唯一的成员data,使用shared_ptr的拷贝构造函数来进行拷贝,因此其引用计数增加1。
拷贝一个StrBlobPtr时,拷贝成员wptr,用weak_ptr的拷贝构造函数进行拷贝,引用计数不变,然后拷贝curr,直接进行内存复制。
练习13.4:假定Point 是一个类类型,它有一个public 的拷贝构造函数,指出下面程序片段中哪些地方使用了拷贝构造函数:
Point global;
Point foo_bar(Point arg)
{
Point local = arg, //将arg拷贝给local
Point *heap = new Point(global);
*heap = local; //将local拷贝到heap指定的地址中
Point pa[4] = { local, *heap }; //将local和*heap拷贝给数组的前两个元素
return *heap; //也用到了拷贝构造函数
}
练习13.5:给定下面的类框架,编写一个拷贝构造函数,拷贝所有成员。你的构造函数应该动态分配一个新的string(参见12.1. 2 节,第407 页),并将对象拷贝到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;
};
练习13.6:拷贝赋值运算符是什么?什么时候使用它?合成拷贝赋值运算符完成什么工作?什么时候会生成合成拷贝赋值运算符?
答:拷贝赋值运算符本身是一个重载的赋值运算符,定义为类的成员函数,左侧运算对象绑定到隐含的this参数,而右侧运算对象是所属类类型的,作为函数的参数,函数返回指向其左侧运算对象的引用。
当对类对象进行赋值时,会使用拷贝赋值运算符。
通常情况下,合成的拷贝赋值运算符会将右侧对象的非static成员逐个赋予左侧对象的对应成员,这些赋值操作是由成员类型的拷贝赋值运算符来完成的。
若一个类未定义自己的拷贝赋值运算符,编译器就会为其合成拷贝赋值运算符,完成赋值操作,但对于某些类,还会起到禁止该类型对象赋值的效果。
练习13.7:当我们将一个StrBlob 赋值给另一个StrBlob 时,会发生什么?赋值StrBlobPtr 呢?
答:由于这两个类都没定义拷贝赋值运算符,因此编译器为它们定义了合成的拷贝赋值运算符。。
与拷贝构造函数的行为类似,赋值一个StrBlob时,赋值其唯一的成员data,使用shared_ptr的拷贝赋值运算符来完成,因此其引用计数增加1。
赋值一个StrBlobPtr时,赋值成员wptr,用weak_ptr的拷贝赋值运算符进行赋值,引用计数不变,然后赋值curr,直接进行内存复制。
练习13.8:为13.1.1节(第443 页)练习13.5 中的HasPtr 类编写赋值运算符。类似拷贝构造函数,你的赋值运算符应该将对象拷贝到ps 指向的位置。
HasPtr& HasPtr::operator=(const HasPtr& hp)
{
auto newps = new string(*hp.ps); //拷贝指针指向的对象
delete ps; //销毁原string
ps = newps; //指向新的string
i = hp.i; //使用内置的int赋值
return *this; //返回一个此对象的引用
}
练习13.9:析构函数是什么?合成析构函数完成什么工作?什么时候会生成合成析构函数?
答:析构函数完成与构造函数相反的工作:释放对象使用的资源,销毁非静态数据成员。从语法上看,它是类的一个成员函数,名字是波浪号接类名,没有返回值,也不接收参数。
当一个类没有定义析构函数时,编译器会为它合成析构函数。
合成的析构函数体为空,但这并不意味着它什么也不做,当空函数体执行完后,非静态数据成员会被逐个销毁。也就是说,成员是在析构函数之后隐含的析构阶段中进行销毁的。
练习13.10:当一个StrBlob 对象销毁时会发生什么? 一个StrBlobPtr 对象销毁时呢?
答:这两个类都没有定义析构函数,因此编译器会为它们合成析构函数。
对StrBlob,合成析构函数的空函数体执行完毕后,会进行隐含的析构阶段,销毁非静态数据成员data。这会调用shared_ptr的析构函数,将引用计数减1,引用计数变为0,会销毁共享的vector对象。
对StrBlobPtr,合成析构函数在隐含的析构阶段会销毁数据成员wptr和curr,销毁wptr会调用weak_ptr的析构函数,引用计数不变,而curr是内置类型,销毁它不会有特殊动作。
练习13.11:为前面练习中的HasPtr 类添加一个析构函数。
~HasPtr()
{
delete ps; //只需释放string对象所占的空间即可
}
练习13.12:在下面的代码片段中会发生几次析构函数调用?
bool fcn(const Sales_data *trans, Sales_data accum)
{
Sales_data item1(*trans), item2(accum);
return item1.isbn() != item2.isbn();
}
答:这段代码中会发生三次析构函数调用:
1、函数结束时,局部变量item1的生命期结束,被销毁,Sales_data的析构函数被调用。
2、类似的,item2在函数结束时被销毁,Sales_data的析构函数被调用。
3、函数结束时,参数accum的生命期结束,被销毁,Sales_data的析构函数被调用。
在函数结束时,trans的生命期也结束了,但它是Sales_data的指针,并不是它指向的Sales_data对象的生命期结束(只有delete指针时,指向的动态对象的生命期才结束),所以不会引起析构函数的调用。
练习13.13:理解拷贝控制成员和构造函数的一个好方法是定义一个简单的类, 为该类定义这些成员, 每个成员都打印出自己的名字:
struct X
{
X() {std::cout << "X()" << std::endl; }
X(const X&) {std::cout << "X(const X&)" << std::endl; }
};
给x 添加拷贝赋值运算符和析构函数, 并编写一个程序以不同方式使用x 的对象:将它们作为非引用和引用参数传递:动态分配它们;将它们存放于容器中; 诸如此类。观察程序的输出, 直到你确认理解了什么时候会使用拷贝控制成员, 以及为什么会使用它们。当你观察程序输出时,记住编译器可以略过对拷贝构造函数的调用。
//练习 13.13
#include <iostream>
#include <vector>
using namespace std;
struct X
{
X() { cout << "构造函数 X()" << endl; }
X(const X&) { cout << "拷贝构造函数 X(const X&)" << endl; }
//拷贝赋值运算符,必须是成员函数
X& operator=(const X& conx) { cout << "拷贝赋值运算符 =(const X&)" << endl;
return *this; }
//析构函数
~X() { cout << "析构函数 ~X()" << endl; }
};
void f1(X x)
{
}
void f2(X &x)
{
}
int main(int argc, char *argv[])
{
cout << "局部变量:" << endl;
X x;
cout << endl;
cout << "非引用参数传递:" << endl;
f1(x);
cout << endl;
cout << "引用参数传递:" << endl;
f2(x);
cout << endl;
cout << "动态分配:" << endl;
X *px = new X;
cout << endl;
cout << "添加到容器中:" << endl;
vector<X> vx;
vx.push_back(x);
cout << endl;
cout << "释放动态分配对象:" << endl;
delete px;
cout << endl;
cout << "间接初始化和赋值:" << endl;
X y = x;
y = x;
cout << endl;
cout << "程序结束:" << endl;
system("pause");
return 0;
}
程序的输出结果如下:
可以看到,当作为引用参数传递的时候,是什么也不做的,间接初始化的时候是调用拷贝构造函数,这两点自己在分析的时候都错了,要注意一下。另外,在函数结束时,有三次析构函数的调用,分别对x、y和vx中的第一个元素。
练习13.14:假定numbered 是一个类,它有一个默认构造函数, 能为每个对象生成一个唯一的序号,保存在名为mysn 的数据成员中。假定numbered 使用合成的拷贝控制成员,并给定以下函数:
void d (numbered s) { cout << s.mysn << endl; }
则下面代码输出什么内容?
numbered a, b = a, c = b;
f(a); f(b); f(c);
答:该代码会输出三个相同的序号—合成拷贝构造函数被调用时简单复制序号,使得三个对象具有相同的序号。
练习13.15:假定numbered 定义了一个拷贝构造函数,能生成一个新的序号。这会改变上一题中调用的输出结果吗?如果会改变,为什么?新的输出结果是什么?
答:在此程序中,都是拷贝构造函数在起作用,因此定义能生成新的序号的拷贝构造函数会改变输出结果。
但注意,新的输出结果不是0、1、2,而是3、4、5。
因为在定义变量a时,默认构造函数起作用,将其序号设定为0。当定义b、c时,拷贝构造函数起作用,将他们的序号分别设定为1、2。
但是,在每次调用函数f时,由于参数是numbered类型,又会触发拷贝构造函数,使得每一次都将形参s的序号设定为新值,从而导致三次的输出结果是3、4、5。
练习13.16: