这一篇来学习赋值运算符重载,先来了解什么是赋值运算符,也就是单个等号(=)就是赋值运算符。什么时候,我们需要对赋值运算符进行重载呢?这里面离不开值拷贝的知识。
1.C++编译器默认功能
C++编译器至少给一个类添加4个函数
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对属性进行值拷贝
4.赋值运算符operator=,对属性进行值拷贝
如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题
2.问题背景
这里先写一段代码,然后分析为什么会报错
#include <iostream>
using namespace std;
class Person
{
public:
// 把年龄这个属性写成指针
int *m_Age;
// 年龄开辟到堆区
Person(int age)
{
m_Age = new int(age); // new 来的对象都是一个内存地址
}
// 析构函数,程序员自己控制内存
~Person()
{
if (m_Age != NULL)
{
delete m_Age; // 删除所占内存,并置空
m_Age = NULL;
}
}
};
void test01()
{
Person p1(18);
Person p2(20);
//赋值操作
p2 = p1;
cout << "p1的age为: " << *p1.m_Age << endl;
cout << "p2的age为: " << *p2.m_Age << endl;
}
int main()
{
test01();
system("pause");
return 0;
}
上面代码解释:
1.写了有参构造和析构函数
2.有参构造中属性使用了在堆区开辟内存空间
3.类的成员变量定义为指针,所以test01()中cout输出,需要解引用,否则输出看到的是内存地址
4.在test01方法中p2 = p1 这行代码是赋值操作,所以p2的m_Age输出值应该和p1一样,也就是18岁。
运行结果:
虽然结果打印出来,但是程序没有运行到main函数中暂停这行代码就崩溃,报了一个为加载xxx.pdb的错误。
错误分析如下
1)p2 = p1是赋值操作,发送了浅拷贝也就是p2中m_Age的指针和p1中m_Age的指针一模一样
2)回顾下C++类中构造函数和析构函数执行顺序,p1的构造执行,然后执行p1的析构函数
3)在p1执行析构函数,进行了内存回收,m_Age这个指针设置为NULL
4)执行了p2 = p1操作,会执行p2的析构函数,也会释放内存
5)p1和p2都对同一个指针进行内存释放,第二个释放肯定会报错。
以上就是崩溃的错误分析过程。
为了避免这种方法,我们想在执行p2 = p1这行代码的时候,不想使用编译器默认的拷贝,而是在堆区重新开辟内存来指向m_Age,也就是执行深拷贝,所以我们需要对等号(赋值运算)进行重载。
3.赋值运算符重载
因为暂时不知道重载函数返回类型是什么,我们先按照void写,来一个成员函数重载的实现方式
#include <iostream>
using namespace std;
class Person
{
public:
// 把年龄这个属性写成指针
int *m_Age;
// 年龄开辟到堆区
Person(int age)
{
m_Age = new int(age); // new 来的对象都是一个内存地址
}
// 析构函数,程序员自己控制内存
~Person()
{
if (m_Age != NULL)
{
delete m_Age; // 删除所占内存,并置空
m_Age = NULL;
}
}
// 成员函数方法实现运算符重载
void operator=(Person &p)
{
// 编译器默认是执行 m_Age = p.m_Age; 操作(浅拷贝)
// 我们这里需判断,对象的属性是否在堆区存在,如果存在先释放内存,再进行深拷贝
if (m_Age != NULL)
{
delete m_Age; // 删除所占内存,并置空
m_Age = NULL;
}
// 深拷贝
m_Age = new int(*p.m_Age); // 解引用拿出年龄,例如18岁
}
};
void test01()
{
Person p1(18);
Person p2(20);
//赋值操作
p2 = p1;
cout << "p1的age为: " << *p1.m_Age << endl;
cout << "p2的age为: " << *p2.m_Age << endl;
}
int main()
{
test01();
system("pause");
return 0;
}
运行结果
看起来好像没有问题,但是我们test01()代码改成这样
void test01()
{
Person p1(18);
Person p2(20);
Person p3(30);
//赋值操作
p3 = p2 = p1;
cout << "p1的age为: " << *p1.m_Age << endl;
cout << "p2的age为: " << *p2.m_Age << endl;
cout << "p3的age为: " << *p3.m_Age << endl;
}
发现这行代码 p3 = p2 = p1会报错,但是我们肯定知道 如果是int 类型三个变量a,b,c 肯定支持 c = b = a这样的赋值操作,所以这里我们还需要思考赋值运算符重载函数的返回值是什么
#include <iostream>
using namespace std;
class Person
{
public:
// 把年龄这个属性写成指针
int *m_Age;
// 年龄开辟到堆区
Person(int age)
{
m_Age = new int(age); // new 来的对象都是一个内存地址
}
// 析构函数,程序员自己控制内存
~Person()
{
if (m_Age != NULL)
{
delete m_Age; // 删除所占内存,并置空
m_Age = NULL;
}
}
// 成员函数方法实现运算符重载
Person& operator=(Person &p)
{
// 编译器默认是执行 m_Age = p.m_Age; 操作(浅拷贝)
// 我们这里需判断,对象的属性是否在堆区存在,如果存在先释放内存,再进行深拷贝
if (m_Age != NULL)
{
delete m_Age; // 删除所占内存,并置空
m_Age = NULL;
}
// 深拷贝
m_Age = new int(*p.m_Age); // 解引用拿出年龄,例如18岁
// 返回当前对象,支持链式编程
return *this;
}
};
void test01()
{
Person p1(18);
Person p2(20);
Person p3(30);
//赋值操作
p3 = p2 = p1;
cout << "p1的age为: " << *p1.m_Age << endl;
cout << "p2的age为: " << *p2.m_Age << endl;
cout << "p3的age为: " << *p3.m_Age << endl;
}
int main()
{
test01();
system("pause");
return 0;
}
解释一下,为什么这个函数重载需要返回一个引用,主要原因就是要支持这种链式编程风格。这里使用返回引用才是真正的本身,如果改成返回值,会报错,因为返回值的话,就是调用拷贝,是对象本身的一个副本。