本系列文章为黑马程序员C++教程学习笔记,前面的系列文章链接如下
C++核心编程:P1->程序的内存模型
C++核心编程:P2->引用
C++核心编程:P3->函数提高
C++核心编程:P4->类和对象----封装
C++核心编程:P5->类和对象----对象的初始化和清理
C++核心编程:P6->类和对象----C++对象模型和this指针
C++核心编程:P7->类和对象----友元
运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
对于内置的数据类型,编译器知道如何运算,比如下面我们都知道c的结果是20。
int a = 10;
int b = 10;
int c = a + b;
如果现在我们有个Person类,类中有两个成员变量m_A和m_B。现在我们有两个Person对象p1和p2,如果我们直接通过加号运算符+
将两个对象的成员变量相加并创建出一个新的对象是不行的。
class Person
{
public:
int m_A;
int m_B;
};
Person p1;
p1.m_A = 10;
p1.m_B = 10;
Person p2;
p2.m_A = 10;
p2.m_B = 10;
Person p3 = p1 + p2;
此时我们可以想个办法:通过自己写个成员函数,实现两个对象的成员变量相加并返回新的对象。
class Person
{
public:
int m_A;
int m_B;
Person PersonAddPerson(Person& p)
{
Person tmp;
tmp.m_A = this->m_A + p.m_A;
tmp.m_B = this->m_B + p.m_B;
return tmp;
}
};
这种方法可行但也有缺点。如果多个人都实现了这样的函数,可能每个人的函数名都不一样,比如我写的叫PersonAddPerson,别人写的叫person_add_person…为了方便编写代码,编译器就直接给我们提供一个函数名:oprator+
Person operator+(Person& p)
{
Person tmp;
tmp.m_A = this->m_A + p.m_A;
tmp.m_B = this->m_B + p.m_B;
return tmp;
}
Person p3 = p1.operator+(p2);
当都使用编译器提供的这种名称时,就可以简化为两个数相加的形式,这就和我们预期的写法一致。以上就是通过成员函数重载+号
Person p3 = p1 + p2;
整体实现代码如下
#include <iostream>
#include <string>
using namespace std;
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;
};
void test01()
{
Person p1;
p1.m_A = 10;
p1.m_B = 10;
Person p2;
p2.m_A = 10;
p2.m_B = 10;
//本质上是Person p3 = p1.operator+(p2)
Person p3 = p1 + p2;
cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl;
}
int main(void)
{
test01();
return 0;
}
运行,可以看出可以通过+号直接完成两个对象的成员变量相加。
我们也可以通过全局函数重载+号运算符,此时就需要两个参数。
Person operator+(Person &p1, Person &p2)
{
Person tmp;
tmp.m_A = p1.m_A + p2.m_A;
tmp.m_B = p1.m_B + p2.m_B;
return tmp;
}
Person p3 = operator+(p1, p2);
此时调用的方式可以简化为
Person p3 = p1 + p2;
完整代码如下
#include <iostream>
#include <string>
using namespace std;
class Person {
public:
Person() {};
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
public:
int m_A;
int m_B;
};
Person operator+(Person &p1, Person &p2)
{
Person tmp;
tmp.m_A = p1.m_A + p2.m_A;
tmp.m_B = p1.m_B + p2.m_B;
return tmp;
}
void test01()
{
Person p1;
p1.m_A = 10;
p1.m_B = 10;
Person p2;
p2.m_A = 10;
p2.m_B = 10;
//本质上是Person p3 = operator+(p1, p2)
Person p3 = p1 + p2;
cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl;
}
int main(void)
{
test01();
return 0;
}
运行,可以看出通过+直接完成两个对象的成员变量相加
如果现在我们想让Person变量和int类型变量相加,即将int类型的变量值加在Person的两个成员变量上,我们可以对运算符重载使用函数重载。
#include <iostream>
#include <string>
using namespace std;
class Person {
public:
Person() {};
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
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 = p1 + 10; //相当于 operator+(p1,10)
cout << "mA:" << p2.m_A << " mB:" << p2.m_B << endl;
}
int main() {
test();
return 0;
}
运行,可以发现调用的是下面那个Person operator+(const Person& p2, int val)
注:
①对于内置的数据类型的表达式的的运算符是不可能改变的。
②不要滥用运算符重载。
我们可以通过cout + << 输出一些内置的数据类型。现在我们有个Person类,里面有m_A和m_B两个成员变量。如果想通过 << + Person对象就能直接输出m_A和m_B,则必须要重载才行。
int a = 10;
cout << a << endl; //可以输出内置数据类型
Person p;
p.m_A = 10;
p.m_B = 10;
cout << p << endl; //不行
假如利用成员函数重载左移运算符,效果如下。可以看出调用方式p.operator<<(p)
不是我们想要的效果,这里出现两个对象,而用 << 输出时只有1个对象
class Person {
public:
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
//调用方式就是p.operator<<(p)
void operator<<(Person& p){
}
private:
int m_A;
int m_B;
};
如果我们进行修改,由于成员函数重载的本质是对象去调用成员函数,这样又会使得cout在右侧
class Person {
public:
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
//调用方式就是p.operator<<(cout)
void operator<<(cout){
}
private:
int m_A;
int m_B;
};
因此我们通常不会使用成员函数重载<<运算符,因为无法实现cout在左侧,只能利用全局函数重载左移运算符。
因此我们通过全局函数来重载左移运算符,大概框架如下
class Person {
public:
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
private:
int m_A;
int m_B;
};
//全局函数实现左移重载
//这样就能实现 cout << p
void operator<<(cout, p) {
}
我们首先看看cout的定义,可以看出cout的数据类型是ostream,而ostream就是标准输出流类。
于是我们将ostream拿过来,而cout这个对象只能有1个,于是我们传引用
#include <iostream>
#include <string>
using namespace std;
class Person {
public:
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
public:
int m_A;
int m_B;
};
//全局函数实现左移重载
//ostream对象只能有一个
void operator<<(ostream& out, Person& p) {
out << "a:" << p.m_A << " b:" << p.m_B;
}
void test() {
Person p1(10, 20);
cout << p1;
}
int main() {
test();
system("pause");
return 0;
}
运行,可以看出正确重载了<<并输出了p的成员变量。
可以看出最后没有换行。如果我们再加个 << endl,则会报错
这是因为我们在一行使用多个<<这种链式编程思想时。而我们这里返回的是void,即没有返回,所以无法追加<<。因此我们需要以引用的方式将返回cout。
通常我们在定义一个类是里面的一些成员变量权限为私有,所以可以让这个全局的重载函数作为类的友元。
#include <iostream>
#include <string>
using namespace std;
class Person {
friend ostream& operator<<(ostream& out, Person& p);
public:
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
private:
int m_A;
int m_B;
};
//全局函数实现左移重载
//ostream对象只能有一个
ostream& operator<<(ostream& out, Person& p) {
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;
}
运行,可以看到重载函数也能直接访问私有成员变量。
我们先来看看前置递增和后置递增的使用
#include <iostream>
using namespace std;
int main()
{
int a = 10;
cout << ++a << endl; //11
cout << a << endl; //11
int b = 10;
cout << b++ << endl; //10
cout << b << endl; //11
return 0;
}
可以看到前置递增是先将变量+1然后做其它操作。后置递增则是先做操作然后再+1。
现在我们想自己定义一个数据类型然后实现递增运算。比如这里我们自己定义一个数据类型MyInter,含有一个成员变量m_Num。默认构造函数给其一个初始值1,然后我们希望可以重载递增运算符实现递增操作。
#include <iostream>
using namespace std;
class MyInter
{
public:
MyInter()
{
m_Num = 0;
}
private:
int m_Num;
};
MyInter myint;
cout << myint << endl; //0
cout << ++myint << endl; //1
cout << myint++ << endl; //1
cout << myint << endl; //2
如果要可以搭配使用cout,则需要先使用全局函数来重载左移运算符。
#include <iostream>
using namespace std;
class MyInter
{
friend ostream& operator<<(ostream &cout, MyInter myint);
public:
MyInter()
{
m_Num = 0;
}
private:
int m_Num;
};
ostream& operator<<(ostream &cout, MyInter myint)
{
cout << myint.m_Num;
return cout;
}
int main()
{
MyInter myint;
cout << myint << endl; //0
return 0;
}
此时可以输出自己定义的数据类型了
首先我们实现前置递增
#include <iostream>
using namespace std;
class MyInter
{
friend ostream& operator<<(ostream &cout, MyInter myint);
public:
MyInter()
{
m_Num = 0;
}
MyInter& operator++() //要返回引用
{
m_Num++; //先进行++运算
return *this; //再将自身返回
}
private:
int m_Num;
};
ostream& operator<<(ostream &cout, MyInter myint)
{
cout << myint.m_Num;
return cout;
}
int main()
{
MyInter myint;
cout << myint << endl; //0
cout << ++myint << endl;
return 0;
}
运行,可以看到结果正确。
这里的重载函数如果返回值会出现一些问题。对于普通数据类型,我做连续两次递增操作,可以看到是对同一个变量连续做两次操作,结果是正确的
int a = 0;
cout << ++(++a) << endl; //2
cout << a << endl; //2
如果是返回值,我们测试看一下
#include <iostream>
using namespace std;
class MyInter
{
friend ostream& operator<<(ostream &cout, MyInter myint);
public:
MyInter()
{
m_Num = 0;
}
MyInter operator++()
{
m_Num++; //先进性++运算
return *this; //再将自身返回
}
private:
int m_Num;
};
ostream& operator<<(ostream &cout, MyInter myint)
{
cout << myint.m_Num;
return cout;
}
int main()
{
MyInter myint;
cout << ++(++myint) << endl; //2
cout << myint << endl; //1
return 0;
}
可以看到返回值时连续做两次递增,只成功了一次,这是因为每一次返回的都是一个新的变量,下一次操作是对这个新的变量做操作。所以我们要返回引用,为了一直对一个数据做操作。
要实现后置递增,则需要对递增运算符重载实现函数重载。于是我们在参数里面加个int,代表一个站位参数,编译器就会认为这是后置递增。
后置递增对应的重载函数要返回值。如果返回引用就是返回一个局部对象的引用,局部对象会在函数结束后被释放。
#include <iostream>
using namespace std;
class MyInter
{
friend ostream& operator<<(ostream &cout, MyInter myint);
public:
MyInter()
{
m_Num = 0;
}
MyInter& operator++()
{
m_Num++; //先进性++运算
return *this; //再将自身返回
}
MyInter operator++(int)
{
//先记录当时结果
MyInter temp = *this;
//后递增
m_Num++;
//最后将记录结果做返回
return temp;
}
private:
int m_Num;
};
ostream& operator<<(ostream &cout, MyInter myint)
{
cout << myint.m_Num;
return cout;
}
int main()
{
MyInter myint;
cout << myint++ << endl; //0
cout << myint << endl; //1
return 0;
}
运行,结果正确
c++编译器会至少给一个类添加4个函数
默认构造函数(无参,函数体为空)
默认析构函数(无参,函数体为空)
默认拷贝构造函数,对属性进行值拷贝
赋值运算符 operator=, 对属性进行值拷贝
我们先来测试下编译器自动提供的赋值运算符。我们创建一个Person类,包含一个成员变量m_Age,是个指向开辟在堆区数据age的指针。
#include <iostream>
using namespace std;
class Person
{
public:
Person(int age)
{
//将年龄数据开辟到堆区
m_Age = new int(age);
}
//年龄的指针
int *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();
system("pause");
return 0;
}
我们直接使用=,可以发现能够实现成员变量值得复制,没有错误。
由于类中有属性指向堆区,所以我们需要写个析构函数来释放这块内存
#include <iostream>
using namespace std;
class Person
{
public:
Person(int age)
{
//将年龄数据开辟到堆区
m_Age = new int(age);
}
~Person()
{
if (m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
}
//年龄的指针
int *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();
system("pause");
return 0;
}
可以发现报错,这是由于出现了前面讲的深浅拷贝问题,即堆区数据重复释放。
所以我们重载=号时要进行深拷贝,而不是直接将值进行复制。
#include <iostream>
using namespace std;
class Person
{
public:
Person(int age)
{
//将年龄数据开辟到堆区
m_Age = new int(age);
}
~Person()
{
if (m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
}
void operator=(Person &p)
{
if (m_Age != NULL) //如果自己开辟的内存释放掉
{
delete m_Age;
m_Age = NULL;
}
m_Age = new int(*p.m_Age); //重新开辟一块内存
}
//年龄的指针
int *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();
system("pause");
return 0;
}
运行,可以看出不会出现浅拷贝了
但是还没完。对于普通类型,如果有这种连等,则是把最右边那个数赋值给左边的所有数。
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
所以需要对重载函数进行修改。注意,不要返回值,否则又会调用拷贝构造函数创建一个副本
#include <iostream>
using namespace std;
class Person
{
public:
Person(int age)
{
//将年龄数据开辟到堆区
m_Age = new int(age);
}
~Person()
{
if (m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
}
Person& operator=(Person &p)
{
if (m_Age != NULL) //如果自己开辟的内存释放掉
{
delete m_Age;
m_Age = NULL;
}
m_Age = new int(*p.m_Age); //重新开辟一块内存
return *this;
}
//年龄的指针
int *m_Age;
};
void test01()
{
Person p1(18);
Person p2(20);
Person 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();
system("pause");
return 0;
}
运行,可以看出成功实现了连等。
关系运算符包含==和!=。如果现在我们想对比两个自定义的数据类型,则需要重载关系运算符。假如现在我有个Person类,包含一个string类型的成员变量name和int类型的成员变量age。如果两个Person对象的name和age相等就打印相等,否则打印不相等。
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
bool operator==(Person &p)
{
if (this->age == p.age && this->name == p.name)
return true;
else
return false;
}
bool operator!=(Person &p)
{
if (this->age != p.age || this->name != p.name)
return true;
else
return false;
}
public:
Person(string m_Name, int m_Age)
{
name = m_Name;
age = m_Age;
}
public:
string name;
int age;
};
int main()
{
Person p1("Tom", 18);
Person p2("Tom", 18);
if (p1 == p2)
{
cout << "p1 和 p2 是相等的" << endl;
}
if (p1 != p2)
{
cout << "p1 和 p2 是不相等的" << endl;
}
return 0;
}
运行,可以看到结果正确。
例: 假设现在我们要创建一个MyPrint类,通过重载函数调用运算符完成字符串打印输出。
#include <iostream>
#include <string>
using namespace std;
class MyPrint
{
public:
void operator()(string str)
{
cout << str << endl;
}
};
int main()
{
MyPrint myfunc;
myfunc("hello world");
return 0;
}
运行,可以看出类似于函数一样完成了字符串的打印输出。
例: 假设现在要创建一个MyAdd类,通过重载函数调用运算符完成两个整数相加。
#include <iostream>
#include <string>
using namespace std;
class MyAdd
{
public:
int operator()(int a, int b)
{
return a + b;
}
};
int main()
{
MyAdd func2;
cout << func2(10, 20) << endl;
cout << MyAdd()(10, 20) << endl; //匿名对象调用
return 0;
}
运行,可以看到结果正确。其中,我们这里使用了匿名函数调用,即先通过MyAdd()创建一个匿名对象,这个匿名对象在当前行执行结束后会被释放,然后为这个匿名对象调用了重载的()运算符函数。