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

 1.拷贝构造函数

1.1拷贝构造函数基本形式

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

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

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

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

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

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

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

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

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

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

class Cgoods
{
public:
	Cgoods()
	{
		cout<<"Cgoods()"<<endl;
	}
	Cgoods(char *name,int num,double price)
	{
		cout<<"Cgoods(char *name,int num,double price)"<<endl;
		if(NULL == name)
		{
			return;
		}

		_name = new char[strlen(name)+1];
		strncpy(_name,name,strlen(name)+1);

		this->_num = num;
		_price = price;
	}

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

		_name = new char[strlen(good._name)+1];		//深拷贝
		strncpy(_name,good._name,strlen(good._name)+1);

		_num = good._num;
		_price = good._price;
	}

	void show()
	{
		cout<<"name:"<<_name<<endl;
		cout<<"num:"<<_num<<endl;
		cout<<"price:"<<_price<<endl;
	}

	~Cgoods()
	{
		cout<<"~Cgoods()"<<endl;
		if(NULL != _name)
		{
			delete []_name;
			_name = NULL;
		}
	}
private:
	char *_name;
	int _num;
	double _price;
};


int main()
{
	Cgoods good1("shangpin1",10,23.5);

	Cgoods good2=good1;	
	//Cgoods good2(good1);

	return  0;
}

 

 

初始化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()"<<endl;
		_name = NULL;
	}

	Cgoods(char *name,int num,double price)
	{
		cout<<"Cgoods(char *name,int num,double price)"<<endl;
		if(NULL == name)
		{
			return;
		}
		_name = new char[strlen(name)+1];
		strncpy(_name,name,strlen(name)+1);
		this->_num = num;
		_price = price;
	}

	Cgoods(const Cgoods& good)
	{
		cout<<"Cgoods(Cgoods& good)"<<endl;
		_name = new char[strlen(good._name)+1];
		strncpy(_name,good._name,strlen(good._name)+1);
		_num = good._num;
		_price = good._price;
	}

	void show()
	{
		cout<<"name:"<<_name<<endl;
		cout<<"num:"<<_num<<endl;
		cout<<"price:"<<_price<<endl;
	}

	~Cgoods()
	{
		cout<<"~Cgoods()"<<endl;
		if(NULL != _name)
		{
			delete []_name;
			_name = NULL;
		}
	}

	//赋值运算符重载函数
	Cgoods& operator=(const Cgoods& good)
	{
		cout<<"Cgoods operator=(const Cgoods& good)"<<endl;
		if(this != &good)	//避免自赋值
		{
			if(NULL != _name)	//避免内存泄漏
			{
				delete []_name;
			}
			_name = new char[strlen(good._name)+1];
			strncpy(_name,good._name,strlen(good._name)+1);
			_num = good._num;
			_price = good._price;
		}
		return *this;
	}


private:
	char *_name;
	int _num;
	double _price;
};

int main()
{
	Cgoods good1("shangpin1",10,23.5);

	Cgoods good2("shangpin2",20,25.3);

	Cgoods good3 = good1;

	good3 = good2 = good1;

	return 0;
}

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

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<iostream>
#include<string>
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;
}

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

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

class Cgoods
{
public:
	Cgoods()
	{
		cout<<"Cgoods()"<<endl;
		_name = NULL;
	}
	Cgoods(char *name,int num = 10,double price = 2.5)
	{
		cout<<"Cgoods(char *name,int num,double price)"<<endl;
		if(NULL == name)
		{
			return;
		}
		_name = new char[strlen(name)+1];
		strncpy(_name,name,strlen(name)+1);
		this->_num = num;
		_price = price;
	}

	Cgoods(const Cgoods& good)
	{
		cout<<"Cgoods(Cgoods& good)"<<endl;
		_name = new char[strlen(good._name)+1];
		strncpy(_name,good._name,strlen(good._name)+1);
		_num = good._num;
		_price = good._price;
	}

	void show()
	{
		cout<<"name:"<<_name<<endl;
		cout<<"num:"<<_num<<endl;
		cout<<"price:"<<_price<<endl;
	}

	~Cgoods()
	{
		cout<<"~Cgoods()"<<endl;
		if(NULL != _name)
		{
			delete []_name;
			_name = NULL;
		}
	}

	Cgoods& operator=(const Cgoods& good)
	{
		cout<<"Cgoods operator=(const Cgoods& good)"<<endl;
		if(this != &good)
		{
			if(NULL != _name)
			{
				delete []_name;
			}
			_name = new char[strlen(good._name)+1];
			strncpy(_name,good._name,strlen(good._name)+1);
			_num = good._num;
			_price = good._price;
		}
		return *this;
	}

private:
	char *_name;
	int _num;
	double _price;
};

int main()
{
	Cgoods good1;	//Cgoods()
	Cgoods good2("shangpin2",2,12.5);	//Cgoods(char *name,int num,double price)
	Cgoods good3("shangpin3");	//Cgoods(char *name,int num,double price)
	Cgoods good4 = good3;	//Cgoods(Cgoods& good)

	Cgoods good5 = Cgoods("shangpin5");		//Cgoods(char *name,int num,double price)
	//构造临时对象    -》     用临时拷贝构造   -》析构临时对象 
	//编译器优化流程为:直接构造good5

	Cgoods Cgood6 = "shangpin6";	//Cgoods(char *name,int num,double price)
	//构造good6

	Cgoods good7;
	good7 = Cgoods("shangpin7");
	//构造临时对象  -》  运算符重载  -》析构临时对象
	//Cgoods()
	//Cgoods(char *name,int num,double price)
	//Cgoods operator=(const Cgoods& good)

	Cgoods good8  = (Cgoods)"shangpin";
	//~Cgoods()
	//Cgoods(char *name,int num,double price)

	return 0;
}

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
这个问题涉及到 C 和 C++ 中的指针类型和字符串常量,常常是由于类型不匹配或者没有正确地分配内存空间而导致的错误。 在 C 和 C++ 中,指针类型是非常重要的数据类型,它们存储了变量的内存地址。而字符串常量是指在程序中直接使用的字符串,比如 "hello world"。在 C 和 C++ 中,字符串常量是不可修改的,因此它们通常被定义为 const char* 类型。 当我们将一个 const char* 类型的字符串常量传递给一个 char* 类型的形参时,由于类型不匹配,会出现编译器警告或错误。这是由于 const char* 类型的指针指向的是只读内存区域,而 char* 类型的指针可以修改其指向的内存位置。因此,如果我们试图在 char* 类型的函数中修改一个 const char* 类型的字符串常量,就会导致运行时错误或者崩溃。 另外,C++ 中的深拷贝和浅拷贝也经常会导致程序崩溃。深拷贝是指在创建一个新对象时,将原对象的所有成员变量都复制一份到新对象中;而浅拷贝则只是将原对象的指针成员变量复制到新对象中。如果我们在使用深拷贝或者浅拷贝时没有正确地分配内存空间或者没有正确地释放内存空间,就会导致程序崩溃。 赋值运算符重载也可能导致程序崩溃,特别是在涉及到指针类型或者动态内存分配时。如果我们没有正确地释放旧对象占用的内存空间,就会导致内存泄漏和程序崩溃。 因此,在编写程序时,我们需要特别注意指针类型、字符串常量和内存管理等问题,以避免程序崩溃和内存泄漏。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值