目录
1、运算符重载要注意的事项
2、赋值运算符重载
3、关系运算符重载
4、算术运算符重载
5、其他运算符重载
6、总结
1、运算符重载要注意的事项
1.1、不应该滥用运算符重载,使其偏离对应运算符原本的意义。比如++运算符应该是实现
递增操作,不应写成实现的递减操作。
1.2、运算符重载的本质为函数重载,所以也满足函数重载。函数重载的三个条件 + 一个注意事项:
条件:(1)、函数参数类型不同(2)、函数参数个数不同(3)、函数参数顺序不同。
注意事项:函数返回值不作为函数重载的条件。
1.3、重载运算符时,应满足对应运算符的运算顺序和操作个数不变。
1.4、不能创造新的运算符,并且有六个运算符不能重载。分别是:
运算符 | 含义 |
. | 类属关系运算符 |
* | 指针运算符 |
:: | 作用域运算符 |
?: | 条件运算符 |
# | 预处理运算符 |
sizeof |
1.5、运算符重载本身是对该运算符的拓展,是对自定义数据类型的操作,所以不要偏离其原本意义。
运算符重载可分为四大类:1、赋值运算符重载 2、关系运算符重载 3、算术运算符重载
4、其他运算符重载
2、赋值运算符重载
赋值运算符重载的意义和深拷贝构造函数一样,就是为了防止内存泄漏!!!
在学到构造函数和析构函数时,我们知道:编译器为我们提供了四个函数:
函数名 |
默认无参构造函数 |
默认拷贝构造函数 // 进行浅拷贝 |
默认析构函数 |
默认赋值运算符重载 operator= // 进行值赋值 |
当我们自定义数据类型中存在堆区申请的内存时,编译器提供的默认拷贝构造函数和默认
赋值运算符重载都是进行简单的赋值,容易导致我们释放堆区内存时存在重复释放的问题或者内存泄漏问题!
(1)测试默认拷贝构造的内存重复释放问题
#include <iostream>
using namespace std;
// 学生类
class Student
{
public:
int *m_Age; // 年龄
Student() // 无参构造函数
{
m_Age = nullptr; // 初始化为空指针
}
// 我们不定义拷贝构造函数,让编译器为我们提供默认拷贝构造函数
~Student() // 析构函数
{
if(m_Age != nullptr)
{
delete m_Age;
m_Age = nullptr;
}
}
};
void test01()
{
Student std1;
std1.m_Age = new int(18);
Student std2(std1); // 进行拷贝构造
std::cout << "std1.m_Age = " << *std1.m_Age << std::endl;
std::cout << "std2.m_Age = " << *std2.m_Age << std::endl;
}
int main()
{
test01();
return 0;
}
(2)测试默认赋值赋值运算符重载
#include <iostream>
using namespace std;
// 学生类
class Student
{
public:
int *m_Age; // 年龄
Student()
{
m_Age = nullptr; // 初始化
}
~Student() // 析构函数
{
if(m_Age != nullptr)
{
delete m_Age;
m_Age = nullptr;
}
}
};
void test01()
{
Student std1;
std1.m_Age = new int(18);
Student std2;
std2 = std1; // 进行默认值赋值
std::cout << "std1.m_Age = " << *std1.m_Age << std::endl;
std::cout << "std2.m_Age = " << *std2.m_Age << std::endl;
}
int main()
{
test01();
return 0;
}
得出结论:通过测试结果我们可以看出函数返回值不为 0,说明了函数运行错误!而错误的原因就是内存重复释放问题!因为当我们释放了std1中指针所申请的内存后,std2中的指针并没有被赋值为nullptr,依然指向那段的内存,这时候当编译器调用std2的析构函数时,又重新释放了那段内存(即内存重复释放)。
所以,这里我们可以通过自定义拷贝构造函数(进行深拷贝)和赋值运算符重载(进行深赋值)解决这两个问题!下面我只举例一个:
// 成员函数实现
Student& operator=(const Student& std) // 返回本身引用是为了符合 c = b = a; 这种关系式
{
if(this != &std) // 防止赋值给自身,或者导致使用空指针问题
{
int temp = *std.m_Age;
if(nullptr != this->m_Age) // 不为空就先置空
{
delete m_Age;
m_Age = nullptr;
}
m_Age = new int(temp);
}
return *this;
}
这段自定义赋值运算符重载函数要注意的两个问题就是:
(1)赋值给自身时如果不注意就会导致使用了空指针的问题!所以要加个判断!!!
(2)只能用成员函数进行重载!
3、关系运算符重载
C++的关系运算符有 6 种:> 、< 、 == 、!= 、>=、<= 。
我们要明确其作用,含义,才能写出对应的重载代码。下面我只用 == 运算符重载作为例子,其他的仿照就可以了。
// 成员函数实现 == 运算符重载
bool operator==(const Student& std)
{
if(*this->m_Age == *std.m_Age)
{
return true;
}
return false;
}
// 全局函数实现 == 运算符重载
bool operator==(const Student& std1,const Student& std2)
{
if(*std1.m_Age == *std2.m_Age) // 如果该属性是私有属性,还要将这个函数设置为Student类的友元
{
return true;
}
return false;
}
一般返回值设置为 bool 型 即可。然后根据想要比较属性的优先性进行判断即可。
4、算术运算符重载
C++的算术运算符有 10 种:+、-、*、/、%、+=、-=、*=、/=、%=
下面我以 + 号运算符重载为例:
(1)成员函数实现
Student operator+(const Student& std)
{
Student temp;
int t = *this->m_Age + *std.m_Age;
if(temp.m_Age == nullptr)
{
temp.m_Age = new int(t);
}
else
{
*temp.m_Age = t;
}
return temp;
}
(2)全局函数实现
Student operator+(const Student& std1,const Student& std2)
{
Student temp;
int t = *std1.m_Age + *std2.m_Age; // // 如果该属性是私有属性,还要将这个函数设置为Student类的友元
if(temp.m_Age == nullptr)
{
temp.m_Age = new int(t);
}
else{
*temp.m_Age = t;
}
return temp;
}
需要注意的是函数的返回值类型,因为我们要模拟内置数据类型的加法操作:d = a + b + c;
所以应该返回的是一个副本。
5、其他运算符重载
C++ 还有很多运算符,这里就不一一列举,基本思想都是一样的。下面我就以左移运算符重载为例子。
// 全局函数
std::ostream& operator<<(std::ostream& out,const Student& std)
{
out << std.m_Age << ' ';// 如果该属性是私有属性,还要将这个函数设置为Student类的友元
return out;
}
这里需要解释一下为什么最好用全局函数重载左移运算符。因为我们用C++的标准输出流时习惯这种写法:
std::cout << "hello! world!" << std::endl;
就是将 std::cout 写在左边,而如果我们用成员函数实现左移运算符重载时,只能将 std::cout 写在右边,不符合我们的习惯,当然你想那么写也可以,就是不太符合规范。这里要提醒一句:越是风格不要求那么高的语言,我们应该更要写的符合大众的规范,让别人可以更好的阅读我们的代码,看起来更舒服。
6、总结
运算符重载就是为了对我们的自定义数据类型(类)实现符合我们需求的操作实现,是一种运算符操作的拓展,我们不能滥用。所以当我们要对我们的自定义数据类型进行一些运算符操作时,最好使用自定义的运算符重载函数,更好的保证不会出现内存泄漏问题。同时我们要探究那些运算符对内置数据类型进行操作的本质,才能更好的写出符合其原本含义的运算符重载函数!