阶段三:C++核心编程
Chapter7:类和对象-C++运算符重载
运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
7.1 加号运算符重载
- 作用:实现两个自定义数据类型相加的运算
总结1:对于内置的数据类型的表达式的的运算符是不可能改变的,比如int double等等 只有对自定义的可以通过重载改变
总结2:不要滥用运算符重载;比如看到的是两个数相加的重载 但是函数实现的内部是两个数相减,可以这样做也不会报错,但是这样不规范,也不便于维护代码
#include "iostream"
using namespace std;
/*
上课笔记如下:
对于内置数据类型,编译器知道如何进行计算。
如:
int a = 10;
int b = 10;
int c = a + b;
但是如果是两个自定义类型相加,如:Person,该怎么办呢,编译器是不知道的,因此我们要告诉编译器应该怎么计算
如:
class Person
{
public:
int m_A;
int m_B;
}
Person p1;//实例化第一个对象p1
p1.m_A = 10;
p1.m_B = 10;
Person p2;//实例化第二个对象p2
p2.m_A = 10;
p2.m_B = 10;
Person p3 = p1 + p2;//实例化第三个对象p3,等于p1+p2,那么编译器这时该怎么办呢?
按照道理应该是:
p1的第一个属性 + p2的第一个属性 = p3的第一个属性 即:p3.m_A = p1.m_A + p2.m_A;
p1的第二个属性 + p2的第二个属性 = p3的第二个属性 即:p3.m_B = p1.m_B + p2.m_B;
但是编译器并不会实现这样的计算方式,编译器不会按照我的这种想法去实现相加
那么我们应该怎么做呢?
首先抛开这里要学的新技术 运算符重载
我们可以通过一个成员函数来计算
如:
通过自己写成员函数,实现两个对象相加属性后返回新的对象
Person PersonAddPerson(Person &p)
{
Person temp;
temp.m_A = this->m_A + p.m_A;//让对象自身的属性加上当前传进来的对象的属性
temp.m_B = this->m_B + p.m_B;
return temp;
}
这时候,这里的PersonAddPerson是程序员自己起的名字,每个程序员起的都不一样,因此编译器希望自己起一个名字来统一规范
编译器给起了一个通用名称:
operator+
那么就写成了这样
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;
}
那么当我们调用的时候就是这样的:
Person p3 = p1.operator+(p2);
当我们使用了编译器统一的名字之后,这种写法可以简化为:
Person p3 = p1 + p2;
而简化后的写法正好符合我们的平常的认知。
这就是通过成员函数重载+号
*/
/*
这里分析
通过全局函数重载+号
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 p3 = operator+(p1,p2);
这种方式也可以简化为:
Person p3 = p1 + p2;
*/
class Person
{
public:
Person() {};
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
//成员函数实现 + 号运算符重载
Person operator+(const Person& p) {
Person temp;
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;
};
//全局函数实现 + 号运算符重载
//Person operator+(const Person& p1, const Person& p2) {
// Person temp(0, 0);
// temp.m_A = p1.m_A + p2.m_A;
// temp.m_B = p1.m_B + p2.m_B;
// return temp;
//}
//运算符重载 可以发生函数重载
Person operator+(const Person& p2, int val)
{
Person temp;
temp.m_A = p2.m_A + val;
temp.m_B = p2.m_B + val;
return temp;
}
void test()
{
Person p1(10, 10);
Person p2(20, 20);
//成员函数方式
Person p3 = p2 + p1; //相当于 p2.operaor+(p1)
cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl;
//函数重载的版本;这里相当于是一个Person + int的类型
Person p4 = p3 + 10; //相当于 operator+(p3,10)
cout << "mA:" << p4.m_A << " mB:" << p4.m_B << endl;
}
int main()
{
test();
system("pause");
return 0;
}
7.2 左移运算符重载
#include "iostream"
using namespace std;
/*
* 4.5.2 左移运算符重载
*
* 左移运算符:<<
* 作用:可以输出自定义数据类型
*
* 总结:重载左移运算符配合友元可以实现输出自定义数据类型
*/
/*
如:int a = 10;
cout << a << endl;
class Person
{
public:
int m_A;
int m_B;
};
Person p;
p.m_A = 10;
p.m_B = 10;
cout << p.m_A << endl;//这样是ok的 因为这里的m_A和m_B是整型的,是内置运算符
cout << p.m_B << endl;
但是这样可以吗:cout << p << endl;
我这里想实现的就是输出p的所有属性数据,这样行不行呢?
这样肯定是不可以的,因为编译器不知道p里面是什么,这里的p是一个对象,不是内置运算符
一旦这样写就会报错: 没有与这些操作数匹配的“<<”运算符
但是我们怎么操作呢?我们需要重载这个符号
*/
class Person
{
friend ostream& operator<<(ostream& out, Person& p);//友元,可以访问私有属性
public:
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
/*
成员函数 实现不了 p << cout 不是我们想要的效果
如果这样可以的话,那么最终实现的时候应该这样写:p.operator<<(p);这样是什么?不是我们想要的效果,我们只有一个对象,
或者:void operator<<(cout){},那么简化的时候:p << cout 也不是我们想要的效果 因为我们想要的是 cout << p
因此我们不会利用成员函数去重载左移运算符,因此我们只能用全局运算符去重载左移运算符
*/
//void operator<<(Person& p)
//{
//
//}
private:
int m_A;
int m_B;
};
/*
全局函数实现左移重载,成员函数不行
ostream对象只能有一个
当返回值未知的时候我们先写一个void,当知道了是什么返回值的类型的时候,再替换掉void
void operator<<(cout, p) //operator<<(cout,p) 这样简化成 cout << p
这里需要知道cout是什么数据类型,转到定义 __PURE_APPDOMAIN_GLOBAL extern _CRTDATA2_IMPORT ostream cout;
ostream翻译过来是输出流对象 cout是标准的输出流对象,o是输出 stream是数据流对象
ostream是一个类 因此就写成了这样
ostream& operator<<(ostream& out, Person& p)
endl是换行的意思
*/
ostream& operator<<(ostream& out, Person& p) //这里用out来替换cout,是因为这里写了out相当于给cout起了别名,因为这里是引用
{
out << "a:" << p.m_A << " b:" << p.m_B;
return out;
}
void test()
{
Person p1(10, 20);
cout << p1 << "hello world" << endl; //链式编程,本质就是输出一段之后输出的类型和下一段的类型是一样的
}
int main()
{
test();
system("pause");
return 0;
}
7.3 递增运算符重载
#include "iostream"
using namespace std;
/*
* 4.5.3 递增运算符重载
*
* 作用: 通过重载递增运算符,实现自己的整型数据
*
* 总结,注意重点: 前置递增返回引用,后置递增返回值
*
*
* 思考,学会了递增,也要学会递减
*/
/*
内置数据类型的 ++ 是如何进行的呢?
int a = 10;
cout << ++a <<endl; //11 前置递增是要 先 递增 后 进行表达式运算
cout << a << endl; //11
int b = 10;
cout << b++ <<endl; //10 后置递增是要 先 进行表达式运算 后 递增
cout << b <<endl; //11
然后我们要自定义,要递增运算符重载 ++
我们要怎么去写呢?
先试着这样写
class MyInteger
{
public:
MyInteger() //构造函数里面对这个数据进行初始化
{
m_Num = 0;
}
private://有一个私有成员属性
int m_Num;
};
当我们去创建对象的时候
MyInteger myint;
cout << myint << endl; // 输出就是0
cout << ++myint << endl;// 输出就是1
cout << myint++ << endl;// 输出就是1,这里是因为上面那行代码已经先执行了
cout << myint << endl; // 输出就是2,这里是因为上面那行代码已经先执行了
因此我们就要分开写前置递增和后置递增
//前置递增
//后置递增
*/
//自定义整型
class MyInteger
{
friend ostream& operator<<(ostream& out, MyInteger myint);//不写友元,外部无法访问私有属性
public:
MyInteger() //构造函数对属性进行初始化
{
m_Num = 0;
}
//重载++运算符
//前置++
/*
这里一定是要返回引用,不要返回值,因此要写成 MyInteger& 而不是 MyInteger
因为如果对于内置数据类型而言,如:
int a = 0;
cout << ++(++a) << endl;//这里输出为2
cout << a << endl; //这里输出为2
说明内置数据类型 ++(++a)是对a进行递增
所以我们自定义也要达到这样的效果,
而如果我们返回值类型是值而不是引用,是 MyInteger 而不是 MyInteger&
就会出现这样的结果,如:
MyInteger myInt;
cout << ++(++myInt) << endl;
//此时输出是2,因为执行完(++myInt)这个之后,就是一个新的对象了,
//因此再执行++(++myInt)时候,就是对(++myInt)这个新对象进行递增,故而读取myInt的时候是1而不是2
cout << myInt << endl; //此时输出是1
因此,要返回值是引用,这样就保证了一直是对一个量进行操作,
我们要做的是一个高仿 不能只仿制 内置数据类型的一部分功能 要全部
*/
MyInteger& operator++() //
{
//先进行++的运算,这样让自己的属性先进行++的操作
m_Num++;
//再将自身做一个返回
return *this;
}
//后置++
/*
MyInteger operator++()这里如果这样写,就会报错函数重定义,因此要来一个区别,就来一个参数不同,
这里的int就是一个占位参数,这里就是让编译器区分前置和后置递增
返回值不能作为区分函数重定义与否的标志,参数可以
后置要返回值而不是引用,因为局部对象temp再返回之后,就被释放掉了,如果在对其进行操作就是违法会报错
*/
MyInteger operator++(int)
{
//先返回
MyInteger temp = *this; //记录当前本身的值,然后让本身的值加1,但是返回的是以前的值,达到先返回后++;
m_Num++;
return temp;//最后把记录的结果返回
}
/*
* 注意:
前置递增返回值类型必须是引用
后置递增返回值类型必须是值
*/
private:
int m_Num;
};
//只能用全局函数来写
ostream& operator<<(ostream& out, MyInteger myint)
{
out << myint.m_Num;
return out;
}
//前置++ 先++ 再返回
void test01()
{
MyInteger myInt;
cout << ++myInt << endl; //重载前置递增运算符之后,此代码才OK
cout << myInt << endl; //重载左移运算符之后,此代码才OK
}
//后置++ 先返回 再++
void test02()
{
MyInteger myInt;
cout << myInt++ << endl; //重载后置递增运算符之后,此代码才OK
cout << myInt << endl;
}
int main()
{
test01();
//test02();
system("pause");
return 0;
}
7.4 赋值运算符重载
#include "iostream"
using namespace std;
/*
* 4.5.4 赋值运算符重载
*
*
* 前面学的是c++编译器至少给一个类添加3个函数
* 这里会增添一个,C++也就学这4个编译器默认的添加的函数
*
*
* c++编译器至少给一个类添加4个函数
* 1.默认构造函数(无参,函数体为空)
* 2.默认析构函数(无参,函数体为空)
* 3.默认拷贝构造函数,对属性进行值拷贝
* 4.赋值运算符 operator=, 对属性进行值拷贝
*
* 如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题
*/
class Person
{
public:
Person(int age)
{
//将年龄数据开辟到堆区
m_Age = new int(age);
}
//重载赋值运算符
/*
为什么要自己写 重载赋值运算符 函数呢?编译器不是会自动创建吗?
这是因为编译器创建的是浅拷贝,如果我们的数据是存在于堆区,
那么就会误入浅拷贝的大坑,因此要用深拷贝来解决浅拷贝带来的问题
这个问题存在于析构的时候,会造成二次释放,报错
所以这是要自己去写这个函数的原因
而这个函数有一个需要注意的地方就是 这个函数的返回值一定要有 且是自身
这样才能达到和编译器默认的内置数据类型一样的赋值功能 就是连续赋值:a = b = c;
*/
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);
//返回自身
/*
对于编译器自身的内置数据类型含有以下的功能
int a = 10;
int b = 20;
int c = 30;
c = b = a;
cout << "a = " << a << endl;//输出10
cout << "b = " << b << endl;//输出10
cout << "c = " << c << endl;//输出10
因此我们这里也要写出来有这样的功能,
所以我们需要有返回值,因此返回值类型不能是void 也就是必须有返回值
且返回值得是自身;那就是this指针,还要找到指针本身 就要解引用 因此就是*this
*/
return *this;
}
~Person()
{
if (m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
}
//年龄的指针
int* m_Age;
};
/*
该函数就是为了验证 a = b = c;就是返回值类型的问题
*/
void test01()
{
Person p1(18);
Person p2(20);
Person p3(30);
p3 = p2 = p1; //赋值操作
cout << "p1的年龄为:" << *p1.m_Age << endl;//18
cout << "p2的年龄为:" << *p2.m_Age << endl;//18
cout << "p3的年龄为:" << *p3.m_Age << endl;//18
}
int main()
{
test01();
//int a = 10;
//int b = 20;
//int c = 30;
//c = b = a;
//cout << "a = " << a << endl;
//cout << "b = " << b << endl;
//cout << "c = " << c << endl;
system("pause");
return 0;
}
7.5 关系运算符重载
#include "iostream"
using namespace std;
/*
* 4.5.5 关系运算符重载
*
* 作用:重载关系运算符,可以让两个自定义类型对象进行对比操作
*
*
*/
/*
首先我们先看一下 内置数据类型 在关系运算符方面实现的功能是什么
int a = 10;
int b = 10;
if(a == b)
{
cout << "a和b相等" << endl;
}
那么我们要在自定义类型中实现同样的功能
如:
Person p1;
Person p2;
if(p1 == p2)
{
cout << "p1和p2相等" << endl;
}
if(p1 != p2)
{
cout << "p1和p2不相等" << endl;
}
这就要用到重载关系运算符
*/
class Person
{
public:
Person(string name, int age)//利用构造函数给属性进行赋初值的操作
{
this->m_Name = name;
this->m_Age = age;
};
bool operator==(Person& p)
{
if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
{
return true;
}
else
{
return false;
}
}
bool operator!=(Person& p)
{
if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
{
return false;
}
else
{
return true;
}
}
string m_Name;
int m_Age;
};
void test01()
{
//int a = 0;
//int b = 0;
Person a("孙悟空", 18);
Person b("孙悟空", 18);
if (a == b)
{
cout << "a和b相等" << endl;
}
else
{
cout << "a和b不相等" << endl;
}
if (a != b)
{
cout << "a和b不相等" << endl;
}
else
{
cout << "a和b相等" << endl;
}
}
int main()
{
test01();
system("pause");
return 0;
}
7.6 函数调用运算符重载
#include "iostream"
using namespace std;
/*
* 4.5.6 函数调用运算符重载
*
* 函数调用运算符 () 也可以重载
* 由于重载后使用的方式非常像函数的调用,因此称为仿函数
* 仿函数没有固定写法,非常灵活 在后续的STL里面用的非常多
*
*/
/*
*/
//打印输出的类
class MyPrint
{
public:
//重载函数调用运算符
void operator()(string text)
{
cout << text << endl;
}
};
void test01()
{
//重载的()操作符 也称为仿函数
MyPrint myFunc;
myFunc("hello world");
}
//两个数加法类
class MyAdd
{
public:
int operator()(int v1, int v2)
{
return v1 + v2;
}
};
void test02()
{
MyAdd add;
int ret = add(10, 10);
cout << "ret = " << ret << endl;
/*
匿名对象调用
上面的方式是创建了一个对象,然后用上述的写法。
如果我只是调用一次仿函数,而不是继续对该创建的对象进行其他的操作,
那么其实也可以不创建对象,利用匿名对象调用的方式
就是下面的写法
MyAdd()(100, 100)
直接是仿函数跟上参数列表的数据就可以了
*/
cout << "MyAdd()(100,100) = " << MyAdd()(100, 100) << endl;
}
/*
为什么说仿函数非常的灵活
看着两个例子
第一个函数 void operator()(string text)
第二个函数 int operator()(int v1, int v2)
他们的返回值可以不同 参数个数也可以不同 因此很灵活
*/
int main()
{
test01();
test02();
system("pause");
return 0;
}