🎁个人主页:工藤新一¹
🔍系列专栏:C++面向对象(类和对象篇)
🌟心中的天空之城,终会照亮我前方的路
🎉欢迎大家点赞👍评论📝收藏⭐文章
文章目录
五、运算符重载
- 运算符重载的意义: 对已有的运算符进行重新定义,赋予其某一种功能,以适应不同数据类型之间的运算
5.0不同数据类型的运算
- 对于内置数据类型(如:int,float,double…),我们的编译器知道如何对其执行运算的功能
int a = 10, b = 20, c = a + b;
cout << c << endl;
//输出c = 30;
- 对于自定义数据类型,我们的编译器是否还存在对其执行运算的功能呢?
- 我们可以通过自己实现成员函数,将两个对象相加属性后返回新的对象,来实**
现自定义类型的加减法运算
**
#include<iostream>
using namespace std;
class Person {
public:
Person PersonAddP(Person& p)
{
Person temp;
//当前对象的m_a与参数p的m_a相加
temp.m_a = this->m_a + p.m_a;
temp.m_b = this->m_b + p.m_b;
return temp;//返回值的方式
}
public:
int m_a;
int m_b;
};
int main(){
Person p1, p2, p3;
p1.m_a = 10;
p1.m_b = 20;
p2.m_a = 20;
p2.m_b = 30;
//调用PersonAddP方法,并将结果赋值给p3
p3 = p1.PersonAddP(p2);
cout << "p3.m_a == " << p3.m_a << endl;
cout << "p3.m_b == " << p3.m_b << endl;
return 0;
}
- 因此,我们可以自己去实现内置数据类型的运算功能,但为了使用方便,**C++**为我们提供了简易清晰的内置类型的计算方式
- 于是就有了,内置数据类型的通用名称:
**opetator + 运算符号 + ()**
5.1加号运算符重载
- 作用:实现两个自定义数据类型的相加运算
5.1.1通过成员函数重载+ 号:
- 核心代码块:
Person operator+ (Person& p)
{
Person temp;
temp.m_a = this->m_a + p.m_a;
temp.m_b = this->m_b + p.m_b;
return temp;
}
//调用operator+ 成员函数
Person p3 = p1.operator+ (p2);
//可简化为:
Person p3 = p1 + p2;//--->这就是我们本预期的构架,使自定义数据类型与我们的内置数据类型的运算功能相同。
#include<iostream>
using namespace std;
class Person {
public:
//1.通过成员函数实现加法重载
Person operator+ (Person& p)
{
Person temp;
temp.m_a = this->m_a + p.m_a;
temp.m_b = this->m_b + p.m_b;
return temp;
}
int m_a;
int m_b;
};
void test()
{
Person p1, p2, p3;
p1.m_a = 10;
p1.m_b = 30;
p2.m_a = 40;
p2.m_b = 30;
p3 = p1.operator+(p2);//成员函数的本质调用
cout << "p3.m_a == " << p3.m_a << endl;
cout << "p3.m_b == " << p3.m_b << endl;
p3 = p1 + p2;
cout << "p3.m_a == " << p3.m_a << endl;
cout << "p3.m_b == " << p3.m_b << endl;
}
int main(){test();return 0;}
5.1.2通过全局函数重载+ :
- 核心代码块:
Person operator+ (Person& p1, Person& p2)
{
Person temp;
temp.m_a = p1.m_a + p2.m_a;
temp.m_b = p1.m_b + p2.m_b;
return temp;
}
//调用operator+ 全局函数
Person p3 = operator(p1, p2);
//可简化为:
Person p3 = p1 + p2;
#include<iostream>
using namespace std;
class Person {
public:
int m_a;
int m_b;
};
Person operator+ (Person& p1, Person& p2) {
Person temp;
temp.m_a = p1.m_a + p2.m_a;
temp.m_b = p1.m_b + p2.m_b;
return temp;
}
void test()
{
Person p1, p2, p3;
p1.m_a = 10;
p1.m_b = 30;
p2.m_a = 40;
p2.m_b = 30;
p3 = operator+ (p1, p2);//全局函数的本质调用
p3 = p1 + p3;
cout << "p3.m_a == " << p3.m_a << endl;
cout << "p3.m_b == " << p3.m_b << endl;
}
int main(){test();return 0;}
5.1.3函数重载
- 运算符重载也可以发生函数重载:
- 核心代码块:
Person operator+(Person& p, int num){
Person temp;
temp.m_a = p.m_a + num;
temp.m_b = p.m_b + num;
return temp;
}
p4 = p3 + 100;//Person + int
#include<iostream>
using namespace std;
class Person {
public:
int m_a;
int m_b;
};
Person operator+ (Person& p1, Person& p2) {
Person temp;
temp.m_a = p1.m_a + p2.m_a;
temp.m_b = p1.m_b + p2.m_b;
return temp;
}
Person operator+(Person& p, int num)
{
Person temp;
temp.m_a = p.m_a + num;
temp.m_b = p.m_b + num;
return temp;
}
void test()
{
Person p1, p2, p3, p4;
p1.m_a = 10;
p1.m_b = 30;
p2.m_a = 40;
p2.m_b = 30;
p3 = operator+ (p1, p2);
p4 = p3 + 100;//函数重载
cout << "p4.m_a == " << p4.m_a << endl;
cout << "p4.m_b == " << p4.m_b << endl;
}
int main(){test();return 0;}
- 总结1:对于内置数据类型表达式的运算符是不可以更改的
- 总结2:
不要滥用运算符重载
5.2左移运算符重载(输出运算符)
- 作用:输出自定义的数据类型
5.2.1成员函数重载左移运算符
*核心代码块:利用成员函数重载左移运算符
Person operator<< (Person& p)
{
...
}
//如何正确输出 p?
cout << p << endl;
进一步推断:
因此,我们通常不会利用成员函数重载左移运算符 << !
因为无法(很难)实现,cout在左侧,cout << p; 。
5.2.2全局函数重载左移运算符
- cout的数据类型:
- 点击 cout,右键转到定义
由此,我们得知**cout的数据类型:ostream(输出流对象)
class Person {public: int m_a; int m_b; };
void operator<< (ostream& cout, Person& p)
{
cout << "m_a == " << p.m_a;
cout << "m_b == " << p.m_b;
}
int main()
{
Person p;
p.m_a = 10, p.m_b = 20;
cout << p;
system("pause");
}
细节注意:我们通常会将“请按任意键继续”,进行回车换行。
方法一:
//直接在全局函数内 << endl;
cout << "m_a == " << p.m_a << endl;
cout << "m_b == " << p.m_b << endl;
但其实,这并不是我们最想要的结果,我们更顺手的是:cout << endl;
方法二:
5.2.2.1重温链式编程思想
- 我们先前有讲述过,之所以可以实现无限的追加输入一定离不开 —— **链式编程思想 **
- 核心代码实现:
class Person {public: int m_a; int m_b; };
//以引用的方式带动链式存储
ostream& operator<< (ostream& cout, Person& p)
{
cout << "m_a == " << p.m_a;
cout << " m_b == " << p.m_b;
return cout;//--->cout的数据类型属于标准输出流对象ostream
}
int main()
{
Person p;
p.m_a = 10, p.m_b = 20;
cout << p << endl;
system("pause");
}
- 流程图示:
5.2.3成员属性的权限设置
对于成员属性的权限设置,我们最好将其私有化来确保数据的安全性能。
- 数据保护:防止外部代码直接访问或修改类的内部数据,避免数据被非法篡改。
- 隐藏实现细节:用户只需要知道如何使用类,而不需要了解内部实现。
- 接口稳定性:即使内部实现发生变化,只要公共接口保持不变,外部代码无需修改。
- 数据验证:通过公共方法**(如构造函数、
set
方法,友元)**对数据进行验证,确保数据的合法性。
#include<iostream>
using namespace std;
class Person {
friend ostream& operator<<(ostream& cout, Person& p);
public:
Person(int m_a, int m_b)
{
this->m_a = m_a;
this->m_b = m_b;
}
private:
int m_a;
int m_b;
};
ostream& operator<< (ostream& cout, Person& p)
{
cout << "m_a == " << p.m_a;
cout << " m_b == " << p.m_b;
return cout;
}
int main()
{
Person p(10, 20);//构造函数的赋值语法
cout << p << " Hello World" << endl;
system("pause");return 0;}
- 总结:重载左移运算符配合友元可以实现输出自定义数据类型
5.3递增运算符重载
- 作用:通过重载递增运算符,实现自己的整形数据
5.3.1重载左移
#include<iostream>
using namespace std;
class MyInteger {
friend ostream& operator<< (ostream& cout, MyInteger& myint);
public:
MyInteger() {
m_Num = 0;
}
private:
int m_Num;
};
//全局函数重载左移运算符(<<)
ostream& operator<< (ostream& cout, MyInteger& myint)
{
cout << myint.m_Num << endl;
return cout;
}
int main()
{
MyInteger myint;
cout << myint << endl;
return 0;
}
5.3.2重载前置++运算符
前置递增返回引用
#include<iostream>
using namespace std;
class MyInteger {
friend ostream& operator<< (ostream& cout, MyInteger& myint);
public:
MyInteger() {
m_Num = 0;
}
//重载前置递增运算符
MyInteger& operator++ ()
{
//先进行++运算
m_Num++;
//再将自身作为返回
return *this;
}
private:
int m_Num;
};
//全局函数重载左移运算符(<<)
ostream& operator<< (ostream& cout, MyInteger& myint)
{
cout << myint.m_Num << endl;
return cout;
}
int main()
{
MyInteger myint;
cout << ++myint << endl;
return 0;
}
5.3.3重温引用的重要意义
//为什么这里我们要返回引用,而不是直接返回MyInteger值呢
MyInteger& operator++ ()--->引用做成员函数的返回值
{
//先进行++运算
m_Num++;
//再将自身作为返回
return *this;
}
- 我们先来对比内置数据类型的递增操作
- 说明在内置数据类型中进行递增操作,是会对“同一个对象”所进行的操作,什么意思呢?
- MyInteger& operator++ (),引用做函数的返回值
- MyInteger operator++ (),值返回
class MyInteger {
friend ostream& operator<< (ostream& cout, const MyInteger& myint);
public:
MyInteger() {
m_Num = 0;
}
//重载前置递增运算符 - 返回值
MyInteger operator++ ()
{
//先进行++运算
m_Num++;
//再将自身作为返回
return *this;
}
private:
int m_Num;
};
/*
ostream& operator<< (ostream& cout, MyInteger&myint)
这里,MyInteger& myint 是一个 引用参数,表示它只能绑定到一个 左值(lvalue),而不能绑定到 右值(rvalue)。然而,++(++myint) 返回的是一个 右值(临时对象),而不是左值。因此,当你尝试将 ++(++myint) 传递给 operator<< 时,编译器会报错,因为它无法将右值绑定到引用参数。
解决方法:
要解决这个问题,需要修改 operator<< 的定义,使其能够接受右值。最简单的方法是将参数改为 引用到常量(const reference):
ostream& operator<< (ostream& cout, const MyInteger& myint)
这样,operator<< 就可以接受左值和右值作为参数,而不会报错。
*/
ostream& operator<< (ostream& cout, const MyInteger& myint)
{
cout << myint.m_Num << endl;
return cout;
}
void test(){
MyInteger myint;
cout << ++(++myint) << endl;
cout << myint << endl;
}
int main(){test();return 0;}
由此,我们可以得出以返回值的方式对重载自增的运算是有缺漏的。
因此引用的主要作用是为了对* 同一份数据 *进行递增操作。
5.4.3不要返回局部变量的引用
-
局部变量,存放在栈区:
栈区特点:代码块执行完毕后,局部变量(栈区内存被释放)
-
乱码输出:
- 代码分析:
int& test()
{
int a = 10;//--->局部变量,存放在栈区
return 10;
}
//局部变量a 的内存空间已经被被释放-->非法操作
5.4.4重载后置++运算符
如何解决**函数重定义**现象?— 返回类型不可做重载条件
- 使用占位参数区分前置与后置递增操作
后置递增返回值!
MyInteger operator++(int)//int--->代表占位参数,用于区分前置与后置
{
MyInteger temp = *this;//先记录当前结果,this->myint
m_Num++;//再将m_Num进行++操作
return temp;//返回临时值
}
-
图示后置递增返回值的原理:
-
temp是一个局部对象,局部对象temp,在当前代码**(MyInteger operator++(int))**块执行结束后,就会被释放掉,如果此时继续返回temp的引用(temp已被释放掉),就会造成
非法操作
5.4赋值运算符重载
- 回顾:C++编译器至少给一个类添加4个编译器
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3**.默认拷贝构造函数**,对成员属性进行赋值
4.赋值运算符 operator=,对属性进行值拷贝
如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题
5.4.1开辟堆区数据
- 在构造函数中开辟堆区空间
#include<iostream>
using namespace std;
class Person {
public:
Person(int age)//age = 18
{
//将18 创建到堆区,
//并使用指针 m_Age 维护堆区的数据,[new int(age)的返回值就是一个int*]
this->m_Age = new int(age);
}
int* m_Age;//写成指针,并且将m_Age 真实的数据开辟到堆区
};
void test01(){
Person p1(18);
//p1.m_Age 也是一个指针,因此需要解引用
cout << "p1的年龄为:" << *p1.m_Age << endl;
}
int main(){test01();return 0;}
5.4.2堆区内存手动释放
- 析构函数释放堆区内存
~Person(){
if(this->m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
}
#include<iostream>
using namespace std;
class Person {
public:
Person(int age)//age = 18
{
//将18 创建到堆区,
//并使用指针 m_Age 维护堆区的数据,[new int(age)的返回值就是一个int*]
this->m_Age = new int(age);
}
~Person() {
if (this->m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
}
int* m_Age;//写成指针,并且将m_Age 真实的数据开辟到堆区
};
void test01()
{
Person p1(18);
//p1.m_Age 也是一个指针,因此需要解引用
cout << "p1的年龄为:" << *p1.m_Age << endl;
Person p2(20);
p2 = p1;
cout << "p1的年龄为:" << *p1.m_Age << endl;
cout << "p2的年龄为:" << *p2.m_Age << endl;
}
int main(){test01();return 0;}
为什么会产生这样的现象呢?
5.4.3重温浅拷贝与深拷贝问题
- 浅拷贝 — 堆区数据被重复释放
- 深拷贝 — 解决堆区数据重复释放
- 代码实现:
#include<iostream>
using namespace std;
class Person {
public:
Person(int age)//age = 18{
this->m_Age = new int(age);
}
~Person() {
if (this->m_Age != NULL){
delete m_Age;
m_Age = NULL;
}
}
//重载 赋值运算符
//p2 = p1;-->当p2 去调用p1的时候,需要将p1当作参数传入形参列表
void operator= (Person& p)
{
//编译器提供的是浅拷贝 - m_Age = p.m_Age;
//先判断是否有属性在堆区,如果有就先释放干净,再深拷贝
//eg:p2(20) - p2在堆区开辟了一个m_Age = 20的数据
if (this->m_Age != NULL)
{
delete this->m_Age;
this->m_Age = NULL;
}
//深拷贝
this->m_Age = new int(*p.m_Age);// - 传入的m_Age其实是一个指针
}
int* m_Age;//写成指针,并且将m_Age 真实的数据开辟到堆区
};
void test01(){
Person p1(18);
Person p2(20);
p2 = p1;
cout << "p1的年龄为:" << *p1.m_Age << endl;
cout << "p2的年龄为:" << *p2.m_Age << endl;
}
int main(){test01();return 0;}
*缺陷的存在:连等情况
Person& operator=(Person& p)
{
if(this->m_Age != NULL)
{
delete m_Age;
this->m_Age = NULL;
}
//深拷贝
this->m_Age = new int(*p.m_Age);
}
this->m_Age = new int(*p.m_Age);
return *this;
#include<iostream>
using namespace std;
class Person {
public:
Person(int age)
{
this->m_Age = new int(age);
}
~Person() {
if (this->m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
}
Person& operator= (Person& p)
{
if (this->m_Age != NULL)
{
delete this->m_Age;
this->m_Age = NULL;
}
this->m_Age = new int(*p.m_Age);// - 传入的m_Age其实是一个指针
return *this;
}
int* m_Age;//写成指针,并且将m_Age 真实的数据开辟到堆区
};
void test01(){
Person p1(10), p2(20), p3(30);
p3 = p2 = p1;
//输出时不要忘记进行解引用操作,因为我们的成员属性,本身是一个在堆区开辟的指针
cout << "p1 == " << *p1.m_Age << endl;
cout << "p2 == " << *p2.m_Age << endl;
cout << "p3 == " << *p3.m_Age << endl;
}
int main(){test01();return 0;}
5.5关系运算符重载
- 作用:重载关系运算符,可以让两个自定义类型对象进行比对操作
#include<iostream>
using namespace std;
class Person {
public:
Person(string name, int age){
m_Name = name;
m_Age = age;
}
//成员函数一般只需要一个参数,或者想一想,p1调用p2,那么参数只有p2
bool operator==(Person& p){
if (this->m_Name == p.m_Name && this->m_Age == p.m_Age) return true;
return false;
}
bool operator!=(Person& p)
{
if (this->m_Name == p.m_Name && this->m_Age == p.m_Age) return false;
return true;
}
string m_Name;
int m_Age;
};
int main(){
Person p1("啦啦啦", 17);
Person p2("呦呦呦", 3);
if (p1 == p2) cout << "p1 和 p2 是同一个人!" << endl;
else cout << "p1 和 p2 不是同一个人!" << endl;
if(p1 != p2) cout << "p1 和 p2 不是同一个人!" << endl;
else cout << "p1 和 p2 是同一个人!" << endl;
return 0;
}
5.6函数调用运算符重载 — 仿函数
- 函数调用运算符 “ () ” 也可以重载
- 由于重载后使用的方式特别像函数的调用,因此称为仿函数
- 仿函数没有固定写法,非常灵活
5.6.1仿函数 - 输出
#include<iostream>
using namespace std;
#include<string>
//仿函数
class My_Print {
public:
void operator()(string name)
{
cout << name << endl;
}
};
void test() {
My_Print my_print;
my_print("我是啦啦啦呀!");
}
void My_Print1(string name) {
cout << name << endl;
}
//普通函数
void test1() {
My_Print1("我是呦呦呦呀!");
}
int main() { test(); test1(); return 0; }
5.6.2仿函数的灵活性
- 两数相加
#include<iostream>
using namespace std;
class My_Add {
public:
int operator()(int num1, int num2)
{
return num1 + num2;
}
};
int main()
{
My_Add my_add;
cout << my_add(100, 200);
return 0;
}
5.6.3拓展内容 - 匿名函数对象
-
类名 + ( ): 匿名对象
-
匿名函数对象: 匿名对象 + 仿函数
-
临时使用:当只需要使用一个对象一次,并且不需要在其他地方引用它时,可以使用匿名对象来简化代码。例如,在调用一个方法时,直接创建一个匿名对象作为参数传递,而不需要单独定义一个变量。
-
减少变量声明:避免为临时对象声明过多的变量,使代码更加简洁。
#include<iostream>
using namespace std;
class My_Add{
public:
int operator()(int num1, int num2)
{
return num1 + num2;
}
};
int main()
{
//My_Add() - 匿名对象特点:当前行执行结束后,立即被释放
//且重载了()
cout << My_Add()(100, 200) << endl;
/*
对比:
My_Add my_add;
my_add(100, 200);
*/
return 0;
}
🌟 各位看官好,我是工藤新一¹呀~
🌈 愿各位心中所想,终有所致!