【C/C++】拷贝构造函数 赋值运算符的重载

 1.拷贝构造函数

1.1拷贝构造函数基本形式

就类对象而言,相同类型的类对象是通过拷贝构造函数来在对象初始化期间完成整个复制过程的。

拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它的唯一的一个参数是本类型的一个引用变量,该参数是const类型,不可变的。例如:类T的拷贝构造函数的形式为T(const T& t)

当用一个已初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用。也就是说,当类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数

一个对象以值传递的方式传入函数体

一个对象以值传递的方式从函数返回

一个对象需要通过另外一个已存在的对象进行初始化。

如果没有自己实现拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数,默认的拷贝构造函数完成对象之间的位拷贝,又称浅拷贝。对于任何含有指针变量的类,默认的(浅)拷贝函数注定出错。

浅拷贝只是简单地将一个对象内存的数据复制给另一个对象。在类中含有指针变量状况下,指针变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。

深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。

下面实现了一个拷贝构造函数的例子:

class Cgoods
{
public:
	Cgoods()
	{
		cout<<"Cgoods()"<_num = num;
		_price = price;
	}

	//拷贝构造函数
	Cgoods(const Cgoods& good)
	{
		cout<<"Cgoods(Cgoods& good)"<

 

 

初始化good1时调用了构造函数,初始化good2时调用了拷贝构造函数。

1.2为什么拷贝构造函数必须为引用传递,而不能值传递? 

参数传递过程到底发生了什么:

将地址传递和值传递统一起来,归根结底还是传递的是"值"(地址也是值,只不过通过它可以找到另一个值)!

(1)值传递

对于内置数据类型的传递时,直接赋值拷贝给形参(注意形参是函数内局部变量);
对于类类型的传递时,需要首先调用该类的拷贝构造函数来初始化形参(局部对象);

(2)引用传递

无论对内置类型还是类类型,传递引用或指针最终都是传递的地址值!而地址总是指针类型(属于简单类型), 显然参数传递时,按简单类型的赋值拷贝,而不会有拷贝构造函数的调用(对于类类型)。

因此,如果拷贝构造函数为值传递的话,会造成调用拷贝构造函数的死循环。


2.赋值运算符的重载

2.1赋值运算符的重载函数基本形式

在对象进行赋值操作的时候调用赋值运算符的重载函数,一般来说,类T的赋值运算符重载函数的形式为:

T& operator=(const T &t)

赋值运算符重载函数的参数是函数所在类的const类型的引用。加引用可以避免在函数调用时对实参的一次拷贝,提高了效率。

但const和引用都不是必须的。

赋值运算符重载函数的返回值是被赋值者的引用,即*this。加引用可以在函数返回时避免一次拷贝。最重要的是,通过返回引用值可以实现连续赋值。即类似a=b=c这样。(如果不是返回引用而是返回值类型,那么,执行a=b时,调用赋值运算符重载函数,在函数返回时,由于返回的是值类型,所以要对return后边的“东西”进行一次拷贝,得到一个未命名的副本(有些资料上称之为“匿名对象”),然后将这个副本返回,而这个副本是右值,所以,执行a=b后,得到的是一个右值,再执行=c就会出错。)

这也不是必须的,可以将函数返回值声明为void,什么也不返回,但那样就不能连续赋值了。

下面实现了一个赋值运算符重载函数的例子:

class Cgoods
{
public:

	Cgoods()
	{
		cout<<"Cgoods()"<_num = num;
		_price = price;
	}

	Cgoods(const Cgoods& good)
	{
		cout<<"Cgoods(Cgoods& good)"<

【C/C++】拷贝构造函数 赋值运算符的重载_第1张图片

可以看到调用了两次构造函数,一次拷贝构造函数,和两次赋值运算符的重载函数。

2.2赋值运算符重载函数要避免自赋值

在赋值运算符重载函数的实现部分可以看到,我们首先判断是否是自赋值,避免自赋值情况的发生。一般地,我们通过比较赋值者与被赋值者的地址是否相同来判断两者是否是同一对象,正如if(this!=&good)这一句。

 为什么要避免自赋值?

①为了效率。显然,自己给自己赋值完全是毫无意义的无用功,如果我们一旦判定是自赋值,就立即return *this,会避免对其它函数的调用。

②如果类的数据成员中含有指针,自赋值有时会导致灾难性的后果。对于指针间的赋值(这里假设用_p给p赋值),先要将p所指向的空间delete掉(为什么要这么做呢?因为指针p所指的空间通常是new来的,如果在为p重新分配空间前没有将p原来的空间delete掉,会造成内存泄露),然后再为p重新分配空间,将_p所指的内容拷贝到p所指的空间。如果是自赋值,那么p和_p是同一指针,在赋值操作前对p的delete操作,将导致p所指的数据同时被销毁。那么重新赋值时,拿什么来赋?

所以,对于赋值运算符重载函数,一定要先检查是否是自赋值,如果是,直接return *this

2.3默认的赋值运算符重载函数

当程序没有显式地提供一个以本类或本类的引用为参数的赋值运算符重载函数时,编译器会自动生成这样一个浅拷贝的赋值运算符重载函数。也就是说只有程序显式提供了以本类或本类的引用为参数的赋值运算符重载函数时,编译器才不会提供默认的版本。

看一个例子:

#include
#include
using namespace std;

class Data
{
private:
    int data;
public:
    Data() {};
    Data(int _data)
        :data(_data)
    {
        cout << "constructor" << endl;
    }
    Data& operator=(const int _data)    //参数类型不是本类或本类的引用
    {
        cout << "operator=(int _data)" << endl;
        data = _data;
        return *this;
    }
};

int main()
{
    Data data1(1);
    Data data2,data3;
    cout << "=====================" << endl;
    data2 = 1;
    cout << "=====================" << endl;
    data3 = data2;
    return 0;
}

【C/C++】拷贝构造函数 赋值运算符的重载_第2张图片

我们提供了一个带int型参数的赋值运算符重载函数,data2 = 1;一句调用了该函数,如果编译器不再提供默认的赋值运算符重载函数,那么,data3 = data2;一句将不会编译通过,但我们看到事实并非如此。说明此种情况下,系统提供了一个默认的赋值运算符重载函数。 

2.4区分拷贝构造函数和赋值运算符重载的一些调用形式

class Cgoods
{
public:
	Cgoods()
	{
		cout<<"Cgoods()"<_num = num;
		_price = price;
	}

	Cgoods(const Cgoods& good)
	{
		cout<<"Cgoods(Cgoods& good)"<

【C/C++】拷贝构造函数 赋值运算符的重载_第3张图片

你可能感兴趣的:(C/C++)