C++Primer 拷贝、赋值与销毁

拷贝构造函数:如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数
拷贝构造函数的第一个参数必须是一个引用类型,此参数几乎总是一个const的引用,因为拷贝构造函数在几种情况下都会被隐式地使用,所以拷贝构造函数通常不应该是explicit的
合成拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中。编译器从给定对象中依次将每个非static成员拷贝到正在创建的对象中
每个成员的类型决定了它如何拷贝:对类类型的成员,会使用其拷贝构造函数来拷贝,内置类型的成员则直接拷贝,合成拷贝构造函数会逐元素地拷贝一个数组类型的成员

class Sales_data {
public:
	//与合成的拷贝构造函数等价的拷贝构造函数的声明
	Sales_data(const Sales_data&);
private:
	std::string bookNo;
	int units_sold = 0;
	double revenue = 0.0;
};
Sales_data::Sales_data(const Sales_data &orig):
	bookNo(orig.bookNo),       //使用string的拷贝构造函数
	units_sold(orig.units_sold),   //拷贝 orig.units_sold
	revenue(orig.revenue){ }    //拷贝orig.revenue 空函数体

直接初始化和拷贝初始化的区别:当使用直接初始化时,我们实际上是要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数。当我们使用拷贝初始化时,我们要求编译器
将右侧运算符对象拷贝到正在创建的对象中,如果需要还要进行类型转换

string dots(10, '.');     //直接初始化
stirng s(dots);           //直接初始化
string s2 = dots;         //拷贝初始化
string null_book = "9-999-999";    //拷贝初始化
string nines = string(100, '9');   //拷贝初始化

拷贝初始化不仅在我们用=定义变量时会发生,在下列条件下也会发生
1、将一个对象作为实参传递给非引用类型的形参
2、从一个返回类型为非引用类型的函数返回一个对象
3、用花括号列表初始化一个数组中的元素或者一个聚合类中的成员
我们初始化标准容器库或是调用其insert或push成员时,容器会对其元素进行拷贝初始化,与之相对,用emplace成员创建的成员进行直接初始化

参数和返回值:
在调用过程中,具有非引用类型的参数要进行拷贝初始化。类似的,当一个函数具有非引用的返回类型时,返回值会被用来初始化调用方的结果
拷贝构造函数被用来初始化非引用类类型参数,这一特性解释了为什么拷贝构造函数自己的参数必须是引用类型。如果参数不是引用类型,则调用永远不会成功——为了调用拷贝构造函数,我们必须拷贝它的实参,但为了拷贝它的实参,我们又需要调用拷贝构造函数,无限循环

拷贝初始化的限制:
当传递一个实参或从函数返回一个值时,我们不能隐式使用一个explicit构造函数。如果我们希望使用一个explicit构造函数就必须显式地使用

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

Sales_data::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, *heap = new Point(global);
	*heap = local;
	Point pa[4] = {local, *heap};
	return *heap;
}
//1、local = arg,将arg拷贝给了local
//2、将local拷贝到了heap指向的地址,*heap = loal
//3、point pa[4] = {local, *heap}将local和*heap拷贝给了数组前两个元素
//4、return *heap;

练习13.5
给定一个类,编写一个拷贝构造函数,拷贝所有成员

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

你的拷贝构造函数应动态分配一个新的string,并将对象拷贝到ps指向的位置,而不是拷贝ps本身

HasPtr::HasPtr(const HasPtr &ptr){
	ps = new std::string(*ptr.ps);
	i = ptr.i;
}

拷贝赋值运算符
如果一个运算符是一个成员函数,其左侧运算对象就绑定到隐式的this参数。对于一个二元运算符,例如赋值运算符,其右侧运算对象作为显式参数传递
为了与内置类型的赋值保持一致,赋值运算符通常返回一个指向其左侧运算对象的引用

扫描二维码关注公众号,回复: 8705749 查看本文章
class Foo{
public:
	Foo& operator = (const Foo&);   //赋值运算符
}

合成拷贝赋值运算符
对于数组类型的成员,逐个赋值数组元素,合成拷贝赋值运算符返回一个指向其左侧对象的引用

//等价于合成拷贝赋值运算符
Sales_data& Sales_data::operator=(const Sales_data &rhs){
	bookNo = rhs.bookNo;    //调用string::operator=
	units_sold = rhs.units_sold;    //使用内置的int赋值
	revenue = rhs.revenue;     //使用内置的double赋值
	return *this;             //返回一个此对象的引用
}

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

1、拷贝赋值运算符本身是一个重载的赋值运算符,定义为类的成员函数,左侧运算对象绑定到隐含的this参数,而右侧运算对象是所属类类型的,作为函数的参数,函数返回指向其左侧运算对象的引用
2、对类对象进行拷贝时,会使用拷贝赋值运算符
3、合成的拷贝赋值运算符将右侧对象的非static成员逐个赋予左侧对象的对应成员,这些赋值操作是由成员类型自身的拷贝赋值运算符来完成的
4、若一个类未定义自己的拷贝赋值运算符,编译器就会为其合成拷贝赋值运算符,完成赋值操作,对于某些类,还会起到禁止该类型对象赋值的效果

练习13.8
为13.5中HasPtr类编写赋值运算符

HasPtr::operator=(const Hasptr &ptr){
	auto newps = new std::string(*ptr.ps);   //拷贝指针指向的对象
	delete ps;     //销毁原来的string
	ps = newps;    //ps指向新地址
	i = rhs.i;
	return *this
}

析构函数是类的一个成员函数,名字由波浪号接类名构成,它没有返回值,也不接受参数
析构函数特点:
1、由于析构函数不接受参数,因此它不能被重载,对于一个
给定类,只会有唯一一个析构函数
2、该函数的作用是释放对象所用的资源,并销毁对象的非static数据成员
3、在一个析构函数中,首先执行函数体,然后销毁成员,成员按初始化的逆序销毁。成员销毁时发生什么完全依赖于成员的类型。销毁类类型的成员需要执行成员自己的析构函数。内置类型没有析构函数,因此销毁内置类型成员什么也不需要做
4、隐式销毁一个内置指针类型的成员不会delete它所指的对象
5、智能指针是类类型,该成员在析构阶段会被自动销毁
什么时候会调用析构函数:
1、变量在离开作用域时被销毁
2、当一个对象被销毁时,其成员被销毁
3、容器被销毁时,其元素被销毁
4、对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁
5、对于临时对象,当创建完它的完整表达式结束时被销毁
6、当指向一个对象的引用或者指针离开作用域时,析构函数不会执行

{
	Sales_data *p = new Sales_data;    //p为一个内置指针
	auto p2 = make_shared<Sales_data>();   //p2是一个shared_ptr
	Sales_data item(*p)    //拷贝构造函数将*p拷贝到item中
	vector<Sales_data> vec;   //局部对象
	vec.push_back(*p2);         //拷贝p2指向的对象
	delete p;         //对p指向的对象执行析构函数
}
//退出局部作用域;对item、p2、vec调用析构函数
//销毁p2会递减它的引用计数,如果引用计数为0,对象被释放
//销毁vec会销毁它的元素

合成析构函数:
当一个类未定义自己的析构函数时,编译器会对它定义一个合成析构函数。类似拷贝构造函数和拷贝赋值运算符,对于某些类,合成析构函数被用来阻止该类型的对象被销毁。如果不是这种情况,合成析构函数就为空
析构函数体自身并不直接销毁成员,成员是在析构函数体之后隐含的析构阶段中被销毁的。在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的
练习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();
}

该段代码会发生三次析构函数的调用
1、函数结束时,局部变量item1的生命周期结束,被销毁,Sales_data的析构函数被调用
2、item局部变量也被销毁,Sales_data的析构函数被调用
3、函数结束时,参数accum的生命周期结束,Sales_data的析构函数被调用
在函数结束时,trans的生命周期也结束了,但是它是Sales_data的指针,并不是它指向的Sales_data对象的生命期结束(只有delete指针时,指向的动态对象的生命期才结束),所以不会引起析构函数的调用

练习13.3:为该类成员定义这些函数,每个成员都打印自己的名字

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

给x添加拷贝赋值运算符和析构函数,并编写一个程序以不同的方式使用x的对象:
1、将它作为非引用和引用参数传递
2、动态分配他们
3、将他们存放于容器中

#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 &rhs) {cout << "拷贝赋值运算符=(const X&)" << endl; return *this;}
	~X() {cout << "析构函数~X()" << endl;}
};
void f1(X x){}
void f2(X &x){}
int main(){
	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;
	return 0;
}

在这里插入图片描述
三五法则:
有三个基本操作可以控制类的拷贝操作:拷贝构造函数、拷贝赋值运算符、析构函数
需要析构函数的类也需要拷贝和赋值操作:如果这个类需要一个析构函数,我们几乎可以肯定它也需要一个拷贝构造函数和一个拷贝赋值运算符
如果我们为HasPtr定义一个析构函数,但使用合成版本的拷贝构造函数和拷贝赋值运算符,考虑会发生什么:

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

这个版本的类使用了合成的拷贝构造函数和拷贝赋值运算符,这些函数简单拷贝指针成员,意味着多个HasPtr对象可能指向相同的内存

HasPtr f(HasPtr hp){
	HasPtr ret = hp;   //拷贝给定的HasPtr
	//处理ret
	return ret;    //ret和hp被销毁
}

当f返回时,hp和ret都被销毁,在两个对象上都会调用HasPtr的析构函数。此析构函数会delete ret和hp中的指针成员。但这两个对象包含相同的指针值。此代码会导致指针被delete两次
如果一个类需要自定义析构函数,几乎可以肯定它也需要自定义拷贝赋值运算符和拷贝构造函数

需要拷贝操作的类也需要赋值操作,反之亦然
作为一个例子,考虑一个类为每个对象分配一个独有的序列号,这个类需要一个拷贝构造函数为每个新创建的对象生成一个独有的序列号。除此以外,这个拷贝构造函数从给定对象拷贝所有其他数据成员。这个类还需要自定义拷贝赋值运算符来避免将独有的序号赋予目的对象。

练习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三个对象调用f()函数会输出三个相同的序列号

练习13.15 假定numbered定义了一个拷贝构造函数,能生成一个新的序号。会改变上一题的结果吗
解答:在此程序中,都是拷贝构造函数在起作用,因此定义能生成新的序号的拷贝构造函数会改变输出结果。但是新的输出结果不是0、1、2,而是3、4、5。因为在定义变量a时,默认构造函数其作用,将序号设定为0,当定义变量b、c时,拷贝构造函数起作用,将序号设定为1、2。但是,每次调用函数f()时,由于参数是numbered类型,又会触发拷贝构造函数,使得每一次都将形参s的序号增1,设定为新值。

#include <iostream>
using namespace std;
class numbered {
private:
	static int seq;
public:
	numbered() { mysn = seq++; }
	numbered(numbered& n) { mysn = seq++; }
	int mysn;
};
int numbered::seq = 0;
void f(numbered s) { cout << s.mysn << endl; }
int main() {
	numbered a, b = a, c = b;
	f(a); 
	f(b);
	f(c);
	return 0;
}

在这里插入图片描述练习13.16 如果f中的参数是const numbered&, 又会怎样
解答:会该变输出结果,结果变为了0、1、2
因为由于形参类型由类类型变为引用类型,传递的不是类对象而是类对象的引用。这意味着调用f()函数时,不再触发拷贝构造函数将实参拷贝给形参,而是传递实参的引用。因此,对于每次调用,s都是指向实参的引用,序列号也是实参的序列号,而不是新建立一个对象,获得新的序列号

#include <iostream>
using namespace std;
class numbered {
private:
	static int seq;
public:
	numbered() { mysn = seq++; }
	numbered(numbered& n) { mysn = seq++; }
	int mysn;
};
int numbered::seq = 0;
void f(const numbered &s) { cout << s.mysn << endl; }
int main() {
	numbered a, b = a, c = b;
	f(a); 
	f(b);
	f(c);
	return 0;
}

在这里插入图片描述
使用=default
我们可以通过将拷贝控制成员定义为=default来显式地要求编译器生成合成的版本

class Sales_data{
public:
	//拷贝控制成员;使用default
	Sales_data() = default;
	Sales_data(const Sales_data&) = default;
	Sales_data& operator=(const Sales_data&);
	~Sales_data() = default;
}
Sales_data& Sales_data::operator=(const Sales_data&) = default;

当我们在类内使用=default修饰成员的声明时,合成的函数将隐式地声明为内联的。如果我们不希望合成的成员是内联函数,应该只对成员的类外定义使用=default,就像对拷贝运算符所做的那样

阻止拷贝
定义删除的函数:
在新标准下,我们可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数(deleted function)来阻止拷贝。删除的函数是这样一种函数:我们虽然声明了他们,但不能以任何方式使用它们。在函数的参数列表后面加上=delete来指出我们希望将它定义为删除的

struct NoCopy{
	NoCopy() = default;    //使用合成的默认构造函数
	NoCopy(const NoCopy&) = delete;    //阻止拷贝
	NoCopy& operator=(const NoCopy&) = delete;   //阻止赋值
	~NoCopy() = default;     //使用合成的析构函数
}

析构函数不能是删除的成员

struct NoDtor{
	NoDtor() = default;   //使用合成默认构造函数
	~NoDtor() = delete;    //我们不能销毁NoDtor类型的对象
};
NoDtor nd;    //错误:NoDtor的析构函数是删除的
NoDtor *p = new NoDtor();    //正确:但不能够delete p
delete p;   //错误:NoDtor的析构函数是删除的

对于析构函数已删除的类型,不能定义该类型的变量或释放指向该类型动态分配对象的指针

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

class Employee{
public:
	static int seq;
	Employee(){mysn = seq++;}    //默认构造函数
	Employee(const string &str){name = str; mysn = seq++;}   //默认构造函数
	Employee(Employee &n){name = n.name; mysn = seq++;}  //拷贝构造函数
	Employee& operator=(Employee &rhs){name = rhs.name; mysn = seq++; return *this;} //拷贝赋值运算符
	const string& get_name(){return name;}
	int get_mysn(){return mysn;}
private:
	string name;
	int mysn;
};
int Employee::seq = 0;
void f(Employee &s){
	cout << s.get_name() << ":" << s.get_mysn() << endl;
}
int main() {
	Employee a("zhao"), b = a, c, d;
	c = b;
	f(a); f(b); f(c); f(d);
	return 0;
}

在这里插入图片描述
解释:当Employee a(“zhao”)时,调用了默认构造函数Employee(const string &str),使得name为zhao,而seq为0;b = a,调用了拷贝构造函数,使得seq++,seq为1,然后调用默认构造函数Employee()构造了c,name初始化为空,seq为2,同样d的name为空,seq为3,在c = b语句时,调用了拷贝赋值运算符,使得seq再次递增,然后赋值给c,c的结果为4

练习13.19 你的Employee类需要定义它的拷贝控制成员吗?

#include <iostream>
using namespace std;
class Employee {
public:
	static int seq;
	Employee() { mysn = seq++; }    //默认构造函数
	Employee(const string& str) { name = str; mysn = seq++; }   //默认构造函数
	const string& get_name() { return name; }
	int get_mysn() { return mysn; }
private:
	string name;
	int mysn;
};
int Employee::seq = 0;
void f(Employee& s) {
	cout << s.get_name() << ":" << s.get_mysn() << endl;
}
int main() {
	Employee a("zhao"), b = a, c;
	c = b;
	f(a); f(b); f(c); 
	return 0;
}

在这里插入图片描述
去掉拷贝控制函数和拷贝赋值运算符的结果如上图所示,可以看出结果全部为0,是因为在用a初始化b时,会调用拷贝构造函数。而没有定义拷贝构造函数的情况下,编译器自身合成函数,合成的拷贝构造函数会简单的复制mysn,使两者的序列号相同。当用b初始化c时,会调用拷贝复制运算符,同样合成的版本会简单的复制mysn,会使得两者的序列号相同。

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

解答:两个类都未定义拷贝控制成员,因此都是编译器为它们定义合成版本

当TextQuery销毁时,合成版本会销毁其file和wm成员。对file成员,会将shared_ptr的引用计数减1,若变为0,则销毁所管理的动态vector对象(会调用vector和string的析构函数)。对于wm,调用map的析构函数(从而调用string、shared_ptr和set的析构函数),会正确释放资源
当QueryResult销毁时,合成版本会销毁其sought、lines和file成员。类似TextQuery,string、shared_ptr、和set、vector的析构函数可能被调用,正确释放资源。

当拷贝一个TextQuery时,合成版本会拷贝file和wm成员。对于file,shared_ptr的引用计数会加1。对于wm,会调用map的拷贝构造函数(继而调用string、shared_ptr和set的拷贝构造函数),因此会进行正确的拷贝操作。赋值操作类似,只不过会将原来的资源释放掉,例如,原有的file的引用计数会减去1。

练习13.21:你认为TextQuery和QureyResult类需要定义它们自己版本的拷贝控制成员?

解答:两个类虽然都未定义拷贝控制成员,但它们用智能指针管理共享的动态对象,用标准容器库保存大量的容器。而这些标准库机制都有设计良好的拷贝控制成员,用合成的拷贝控制成员简单地拷贝、赋值、销毁它们,即可以保证正确的资源共享。

发布了65 篇原创文章 · 获赞 4 · 访问量 979

猜你喜欢

转载自blog.csdn.net/CLZHIT/article/details/103992491