运算符重载概念
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
-
不能通过连接其他符号来创建新的操作符
比如:operator@
-
重载操作符必须有一个类类型或者枚举类型的操作数
-
用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义 ,内置类型语言已经定义了其逻辑功能,不能对其做改变。运算符重载相当于在自定义类型中定义另一种逻辑。
-
作为类成员的重载函数时,其形参看起来比操作数数目少1个,成员函数的操作符有一个默认的形参this,限定为第一个形参。
-
注意以下的运算符不能重载
1. 作用域操作符:::
2. 条件操作符: ? :
3. 成员访问操作符:.
4. 指向成员操作的指针操作符:.*
5. 长度运算符:sizeof
. 和 .* 运算符不能重载是为了保证访问成员的功能不能被改变,
域运算符和sizeof运算符的运算对象是类型而不是变量或一般表达式,不具备重载的特征。
演示代码:
#include <iostream>
using namespace std;
// 全局的operator==
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//private:
int _year;
int _month;
int _day;
};
// 这里会发现运算符重载成全局的就需要成员变量是共有的,那么问题来了,封装性如何保证?
// 这里其实可以用我们后面学习的友元解决,或者干脆重载成成员函数。
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
void Test() {
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
cout << (d1 == d2) << endl;
}
int main() {
Test(); // 输出 0
return 0;
}
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// bool operator==(const Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this指向的调用函数的对象;在函数末尾加const相当于对this加const
bool operator==(const Date& d2)const
{
return _year == d2._year && _month == d2._month && _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
void Test() {
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
cout << (d1 == d2) << endl;
}
int main() {
Test(); // 输出 0
return 0;
}
两者的区别:
-
全局函数运算符需要几个参数就得输入几个参数。
-
成员函数运算符个数时所需参数再减1,其中第一个参数被this指针所代替。
赋值运算符重载
拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用
String a(“hello”);
String b(“world”);
String c = a; // 调用了拷贝构造函数,最好写成 c(a);
c = b; // 调用了赋值函数
本例中第三个语句的风格较差,宜改写成String c(a) 以区别于第四个语句。
重载赋值运算符要点
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date& operator=(const Date& d)
{
if (this != &d) // 仅仅减少运算量,没有也没太大关系
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
- 返回值的类型需为该类型的引用,并在函数结束前 返回实例自身的引用( *this )参数类型。(只有返回一个引用,才可以连续赋值)
- 传入的参数类型需为常量引用(提高代码效率,避免调用拷贝构造函数),参数还应加上 const 关键字(赋值运算符函数内不会改变传入参数的状态)
- 检测是否自己给自己赋值(会在释放自身内存的时候出现严重的问题:如果传入的参数与*this 是同一个实例,一旦释放了自身的内存,传入的参数的内存也同时被释放了)
- 是否释放实例自身已有的内存(如果忘记释放已有的空间,程序会出现内存泄漏)
- 分配新的内存资源,并复制字符串。注意函数strlen返回的是有效字符串长度,不包含结束符‘\0’。函数strcpy则连‘\0’一起复制。
- 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝(浅拷贝)。
#include <iostream>
using namespace std;
class Date
{
private:
int _year;
int _month;
int _day;
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void print() {
cout << _year << ' ' << _month << ' ' << _day << endl;
}
};
int main() {
Date d1;
Date d2(2018, 10, 1);
// 这里d1调用的编译器生成operator=完成拷贝,d2和d1的值也是一样的。
d1 = d2;
d1.print(); //输出 2018 10 1
return 0;
}
编译器生成的默认赋值重载函数可以完成字节序的值拷贝了,但是这样的拷贝只是 浅拷贝 !
默认赋值运算符针对String类引发浅拷贝问题: 点击进入——>深拷贝
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
class String {
public:
String(const char* str = "jack") {
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String() {
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
int main() {
String s1("hello");
String s2("world");
s1 = s2;
return 0;
}
有申请内存空间资源的操作,需要自己定义这两个成员函数。没有的话,若采用编译器默认生成的函数使用:
- 会产生对同一块内存重复析构的问题
- 对 s1 原有的内存未释放,造成内存泄漏。
要点总结
- 类会自动提供一个赋值运算符的重载,执行的是浅拷贝,跟拷贝构造相同
- 浅拷贝:直接复制内存
- 深拷贝:当成员中有指向堆的指针,就必须重新给指针分配空间,然后将目标对象指针所指空间的内容拷贝到新分配的空间。(如果不这样做,会导致两个指针指向同一片空间,从而在析构中多次释放,导致出现错误)
实现赋值运算符重载
- 将运算符看成函数,把他的几目当成参数,通过参数的类型识别出对应的操作方法,相当于函数重载。
- 运算符重载有制定的规则,规则根据运算符来制定。
// 面试题1:赋值运算符函数
// 题目:如下为类型CMyString的声明,请为该类型添加赋值运算符函数。
#include<cstring>
#include<cstdio>
class CMyString
{
public:
CMyString(char* pData = nullptr);
CMyString(const CMyString& str);
~CMyString(void);
CMyString& operator = (const CMyString& str);
void Print();
private:
char* m_pData;
};
CMyString::CMyString(char *pData)
{
if(pData == nullptr)
{
m_pData = new char[1];
m_pData[0] = '\0';
}
else
{
int length = strlen(pData);
m_pData = new char[length + 1];
strcpy(m_pData, pData);
}
}
CMyString::CMyString(const CMyString &str)
{
int length = strlen(str.m_pData);
m_pData = new char[length + 1];
strcpy(m_pData, str.m_pData);
}
CMyString::~CMyString()
{
delete[] m_pData;
}
CMyString& CMyString::operator = (const CMyString& str)
{
if(this == &str)
return *this;
delete []m_pData;
m_pData = nullptr;
m_pData = new char[strlen(str.m_pData) + 1];
strcpy(m_pData, str.m_pData);
return *this;
}
void CMyString::Print()
{
printf("%s", m_pData);
}
考虑异常安全性的解法
在前面的函数中,我们在分配内存之前先用 delete 释放了实例 m_pData 的内存。如果此时内存不足导致 new char 抛出异常,则 m_pData 将是一个空指针,这样很容易导致程序崩溃。 也就是说一旦在赋值运算符函数内不抛出一个异常, CMyString的实例不再保持有效的状态,这就违背了异常安全性原则。(不泄漏任何资源、不破坏数据)
要想在赋值运算符中实现异常安全性,我们有两种方法,一个简单的办法是我们先用new分配新内容再用delete释放已有的内容。这样只在分配内容成功之后再释放原来的内容,也就是当分配内存失败时我们能确保CMyString的实例不会被修改。我们还有一个更好的办法,先创建一个临时实例,在交换临时实例和原来的实例。见下面的代码:
CMyString& CMyString::operator =(const CMyString& str)
{
if(this != &str)
{
CMyString strTemp(str);
char *pTemp = strTemp.m_pData;
strTemp.m_pData = m_pData;
m_pData = pTemp;
}
return *this;
}
在上面的函数中,我们先创建一个临时变量strTemp, 接着把strTemp.m_pData和实例自身的m_pData做交换。由于strTemp是一个局部变量,但程序运行到if的外面时也就出了该变量的作用域, 就会自动调用strTemp的析构函数,把strTemp.m_pData所指向的之前的m_pData的内存,这就相当于自动调用析构函数释放实例的内存。
在新的代码中,我们在CMyString的构造函数里用new分配内存。如果由于内存不足抛出诸如bad_alloc等异常,我们还没有修改原来实例的状态,因此实例的状态还是有效的,这也就保证了异常安全性。
如有不同见解,欢迎留言讨论~~~