什么是拷贝赋值运算符
拷贝赋值运算符就是上一章拷贝初始化中提到的“=”,即类与类之间的赋值操作依托赋值运算符进行,此处要注意区别下初始化与赋值的概念。
//person是用户自定义类类型
person p1=p2;//此处为拷贝初始化,即创建一个p1对象,使用创建好的p2对象对它进行拷贝初始化
person p3;//调用默认构造函数初始化p1对象
p3=p1;//将p1对象赋值给已经初始化好的p3对象
person p4(30,"老王");//调用有参构造初始化p4对象
p4=p1;//将p1的成员数据赋值给p4,即改变p4的年龄与姓名
现在我们应该理解了赋值与初始化的区别:初始化是指创建这个对象的时刻对它进行一些成员变量的定义、赋值等,例如常见的初始化有拷贝构造初始化,默认构造初始化,有参构造初始化;赋值是指对已经初始化好了的对象进行值的修改变动,将已经初始化了的一个对象的参数赋给另一个已经初始化了的对象。
什么时候需要自己定义拷贝赋值运算符
当我们没有进行自定义拷贝构造函数、拷贝赋值运算符以及析构函数时,编译器会提供一个合成拷贝构造函数,其中包含编译器提供的拷贝构造函数、拷贝赋值运算符以及析构函数,那么什么时候我们需要自己定义拷贝赋值运算符呢?
当成员变量存在指针时!
成员变量存在指针时涉及浅拷贝和深拷贝,此时我们必须自己定义拷贝构造函数将成员变量在堆区重新开辟一个内存,定义析构函数手动进行释放以及定义拷贝构造赋值运算符使编译器可以顺利进行对象与对象之间的赋值操作。(关于深拷贝与浅拷贝的问题本人在上一个博客“C++_详解拷贝构造函数_深入浅出”中有详细介绍)
如何定义拷贝赋值运算符
在自定义拷贝赋值运算符之前我们应该了解赋值操作如何进行。
p1=p2;//将p2赋值给p1
首先在赋成员属性之前应该先把p1“清理干净”,此处清理指的是将p1对象中的指针成员曾经指向的那块内存释放,再开辟一个新的内存,用来接收p2对象中指针成员存储的数据,最后必须需要在拷贝赋值运算符函数中返回一个自身的引用。
拷贝赋值运算符函数的标志是“operator=”,具体实现见代码:
class person
{
public:
...//此处省略默认构造函数和有参构造函数
person& operator=(person& p)
{
cout << "拷贝赋值运算符调用" << endl;
auto new_age = new int(*p.m_age);//创建一个临时变量接收实参传来的对象数据,并将它开辟到堆区内存中
delete m_age;//释放指针原来指向的内存
auto new_name = new string(*p.m_name);
delete m_name;
m_age = new_age;//将临时变量拷贝到自身类的指针对象中
m_name = new_name;
return *this;
}
int* m_age;//年龄指针
int* m_name;//姓名指针
}
当编写一个拷贝赋值运算符时,一个好的模式是先将传入的形参对象拷贝到一个局部临时对象中。当拷贝完成后,销毁左侧运算对象的现有数据就是安全的。左侧运算对象数据被销毁后,就可以将数据从临时对象中拷贝到左侧运算对象中。此刻便完成了赋值操作。
测试
int main()
{
person p(23,"老王");//有参构造初始化一个对象p,年龄23,姓名老王
person p2 = p;//调用拷贝构造函数将p拷贝给p2
person p3;//调用默认构造函数初始化一个p3对象
p2 = p2;//将p2赋值给自身
p3 = p2;//将p2赋值给p3
cout << *(p2.m_age) << " " << *(p2.m_name) <<endl
<<*(p3.m_age)<<" "<<*(p3.m_name)<< endl;//打印输出
return 0;
}
运行结果如下:
根据运行结果证实了该自定义拷贝赋值运算符可以实现将一个对象赋值给自身、将一个对象赋值给另一个对象。
总结
1、拷贝赋值运算符、拷贝构造函数、析构函数,他们三个可以看作是一个整体,当类中需要自定义三者之一时,则其他两个函数也必须自定义。(通常发生在类中存在指针成员时)
2、如果将一个对象赋值给它自身,赋值运算符必须能正确工作
3、大多数赋值运算符组合了拷贝构造函数和析构函数的工作