复制控制

我们知道一个类的构造函数指明了当我们定义一个类的对象时会发生什么,这一小节主要讨论另外几个与类的创建及删除有关的概念:复制构造函数(当复制一个类的对象时会发生什么),赋值构造操作符(当对类的对象进行赋值操作时会发生什么),以及析构函数(撤销这个类的对象时会发生什么)。这三个函数统称为复制控制。从它们的作用来看,其实它们非常重要,但是为什么我们平时不太注意他们,甚至有些书籍也对他们不太提及呢?因为如果我们没有显式的定义它们,编译器会自动为了定义最基本的这些操作。

先看复制构造函数
顾名思义,当发生类的对象的复制时,是通过调用复制构造函数完成的。具体的说,下面几种情况会调用复制构造函数:
用一个同类型的对象初始化一个对象。
当一个对象作为函数的实参时,实参到形参的转化会发生复制操作。
当以一个类作为函数的返回值时,也会返回return语句中的值的副本。
初始化顺序容器的元素时。
根据元素的初始化列表初始化元素时。

其实,不论我们是否自己定义了复制构造函数,编译器都会为我们合成一个所谓的“合成构造函数”,这个函数的为类的每一个成员初始化(不包括static成员,因为它属于这个类而不是这个类的某一个对象)。初始化的方式是由这个成员的类型决定的:内置类型的成员直接复制,类类型的成员使用它的复制构造函数复制,而数组,需要特别注意:虽然我们说不能直接复制数组,但是如果数组是一个类成员,复制构造函数会复制整个数组。
既然编译器会为你合成一个复制构造函数,为什么我们还要自己写一个呢?如果一个类的数据成员是指针,或者有成员在构造函数中分配其他资源,或者想在创建新对象时干一些其他的事情,(其实是前两种情况)就需要自己编写复制构造函数了。

那么如何定义复制构造函数呢?其实跟构造函数差不多,不过形参是类类型的引用,这个引用通常是const修饰。

也可用构造函

struct Noname
{
	//构造函数
	Noname():pstring(new string),i(0),d(0){}
	//复制构造函数声明
	Noname(const Noname& other);
	//析构函数
	~Noname()
	{
		//删除构造函数中new的资源
		delete pstring;
	}
private:
	string *pstring;
	int i;
	double d;

};

//定义复制构造函数
Noname::Noname(const Noname& other)
{
	//new了一个新的指向string的指针
	pstring = new string;
	//将这个指针指向
	*pstring = *(other.pstring);
	i = other.i;
	d = other.d;
}


也可以同初始化列表的方法定义复制构造函数:

Noname::Noname(const Noname& other):
pstring(new string(*(other.pstring))),i(other.i),d(other.d){}

有一点需要特别注意:复制构造函数的形参一定是一个引用!原因很简单,如果形参是普通的非引用类,那么形参到实参的传递过程是通过“值传递”发生的:即实参将值复制给形参,可是复制的规则却还没有定义呢!

再看赋值构造函数。假设我们有一个类Employee,并定义了两个它的对象a,b,那么这个函数定义了当我使用a=b时,具体都干了什么事情。
同样的,当我们没有定义它时,编译器会合成一个,它会对又操作数对象的每个成员赋值给做操作数对象的对应成员:内置类型常规赋值,类类型使用这个类的赋值构造函数赋值,数组则是对数组的每个元素赋值。
它的定义方法依赖于另一个概念:重载操作符。简单的说,重载操作符就是定义了平时我们使用的+、-、*、/、=等操作符在操作数是类而并非普通的内置类型时,这些操作表达的含义。所以可以看出,所谓的赋值构造函数,就是重载=这个操作,是得它的操作数为类时,也能得到我们想要的结果。重载方法说起来很绕口,先看一个例子:

class Employee
{
public:
	//默认构造函数
	Employee():name("noname"),ID(counter){counter++;}
	//接受一个string类参数的构造函数
	Employee(const string nm):name(nm),ID(counter){counter++;}
	//复制构造函数
	Employee(Employee &other):name(other.name),ID(other.ID){}
	//赋值构造函数
	Employee& operator=(const Employee &rhs)
	{
		name = rhs.name;
		//返回this指针意味着可以连续赋值
		return *this;
	}
private:
	string name;
	int ID;
	static int counter;

};

其中的Employee& operator=(const Employee &rhs)便是对“=”的重载。其中operator=表明重载的是“=”操作符。形参必须与操作符对应的操作数一致:按理来说,应该是“=”左右两边的类型都有,但这里隐式的包含了*this指针,指向了“=”的做操作数,所以只有一个形参。函数体的具体内容很明显,就是用一个对象的数据成员给另一个对象的数据成员复制。返回this指针意味着我们可以进行a=b=c这样连续赋值的操作。

最后我们看看析构函数。
可以把它认为是构造函数的“反函数”,当撤销一个对象时,会调用析构函数。举几个例子,比如你在函数体内定义了一个局部对象,当这个函数使用完(遇到})时,这个对象的生命周期就结束了,需要调用析构函数对其撤销。同理,撤销一个容器(不论是标准库还是数组)时,对于容器的每一个元素,都会使用析构函数对其撤销。
同样的,当你不定义析构函数时,系统会为你合成一个默认的析构函数。但这个析构函数却并不一定能够符合你对它的期望:释放全部应该释放的内存,原因很简单,因为假如对象的成员中,有动态分配的对象,那么只有当该对象的指针被删除时,才能释放内存。这个工作编译器是不会帮你完成的,你只能自己写析构函数来完成,正如第一个程序中的:

	~Noname()
	{
		//删除构造函数中new的资源
		delete pstring;
	}

通常,复制构造函数,赋值构造函数以及析构函数会在一起使用。这被称为“三法则”。
最后通过一个例子来说明这三个函数在什么时候会被调用。程序很变态,它定义了一个类,这个类没有数据成员,但还是为它定义了复制控制函数,而每个函数并没有做它们本来该做的事情,而是打印自己,这使得我们能够清楚的看出来调用的是谁。然后定义了3个函数,函数本身也没有做什么事情,但是通过参数的传递(引用,非引用)和返回值,使得复制控制被调用了,最后还验证了容器和动态创建时的调用情况:

#ifndef EXMPL1
#define EXMPL1

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

using std::string;
using std::cout;
using std::endl;
using std::vector;

class Exmpl1
{
public:
	//默认构造函数
	Exmpl1(){cout<<"Exmpl1()"<<endl;}
	//赋值构造函数
	Exmpl1(const Exmpl1&){cout<<"Exmpl1(const Exmpl1)"<<endl;}
	//赋值操作符
	Exmpl1& operator = (const Exmpl1 &rhs)
	{
		cout<<"operator = (const Exmpl1)"<<endl;
		return *this;
	}

	//析构函数
	~Exmpl1(){cout<<"~Exmpl1()"<<endl;}
};

//定义一些调用这个类的函数

//形参为Exmpl1对象
void func1(Exmpl1 obj){}
//形参为Exmpl1对象的引用
void func2(Exmpl1 &obj){}
//返回Exmpl1对象
Exmpl1 func3()
{
	Exmpl1 obj;
	return obj;
}


#endif


主函数如下,注释分析了每一行代码要调用什么函数。

#include "exmpl.h"


int main()
{
	Exmpl1 eobj;//调用默认构造函数
	func1(eobj);//调用复制构造函数将形参Exmpl1对象为实参Exmpl1的副本
				//函数执行完毕后调用析构函数撤销形参Exmpl1对象

	func2(eobj);//形参为Exmpl1引用,无需调用复制构造函数传递实参

	eobj = func3();//调用默认构造函数创建局部Exmpl1对象
					//函数返回时调用复制构造函数创建返回值的副本
					//调用析构函数撤销局部Exmpl1对象
					//调用赋值操作符
					//执行完复制操作以后,调用析构函数撤销返回值副本的Exmpl1对象

	Exmpl1 *p = new Exmpl1;//调用默认构造函数动态创建Exmpl1对象

	vector<Exmpl1> evec(3);	//调用默认构造函数
							//创建临时的Exmpl1对象
							//3次调用复制构造函数
							//将临时值Exmpl1复制到vector容器的每个元素
							//调用西沟函数消除临时值Exmpl1
	delete p;				//调用析构函数撤销动态创建的Exmpl1对象
			
	return 0;				//evec以及eobj生命周期结束,自动调用析构函数撤销
							//撤销evec需要调用析构函数3次
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值