本系列文章为黑马程序员C++教程学习笔记,前面的系列文章链接如下
C++核心编程:P1->程序的内存模型
C++核心编程:P2->引用
C++核心编程:P3->函数提高
C++核心编程:P4->类和对象----封装
C++核心编程:P5->类和对象----对象的初始化和清理
C++核心编程:P6->类和对象----C++对象模型和this指针
C++核心编程:P7->类和对象----友元
- 前言
- 一、加号运算符重载
- 1.1 成员函数重载+号运算符
- 1.2 全局函数重载+号运算符
- 1.3 运算符重载的函数重载
- 二、左移运算符重载
- 2.1 成员函数重载的缺陷
- 2.2 全局函数重载
- 2.3 一些问题
- 三、递增运算符重载
- 3.1 重载前置
- 3.2 后置递增
- 四、赋值运算符重载
- 4.1 赋值运算符重载的一些问题
- 4.2 解决方案
- 五、关系运算符重载
- 六、函数调用运算符重载
前言
运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
一、加号运算符重载
对于内置的数据类型,编译器知道如何运算,比如下面我们都知道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;
1.1 成员函数重载+号运算符
此时我们可以想个办法:通过自己写个成员函数,实现两个对象的成员变量相加并返回新的对象。
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#include 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; }
运行,可以看出可以通过+号直接完成两个对象的成员变量相加。
1.2 全局函数重载+号运算符
我们也可以通过全局函数重载+号运算符,此时就需要两个参数。
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#include 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; }
运行,可以看出通过+直接完成两个对象的成员变量相加
1.3 运算符重载的函数重载
如果现在我们想让Person变量和int类型变量相加,即将int类型的变量值加在Person的两个成员变量上,我们可以对运算符重载使用函数重载。
#include#include 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)
注:
①对于内置的数据类型的表达式的的运算符是不可能改变的。
②不要滥用运算符重载。
二、左移运算符重载 2.1 成员函数重载的缺陷
我们可以通过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在左侧,只能利用全局函数重载左移运算符。
2.2 全局函数重载
因此我们通过全局函数来重载左移运算符,大概框架如下
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#include 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的成员变量。
2.3 一些问题
可以看出最后没有换行。如果我们再加个 << endl,则会报错
这是因为我们在一行使用多个<<这种链式编程思想时。而我们这里返回的是void,即没有返回,所以无法追加<<。因此我们需要以引用的方式将返回cout。
通常我们在定义一个类是里面的一些成员变量权限为私有,所以可以让这个全局的重载函数作为类的友元。
#include#include 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; }
运行,可以看到重载函数也能直接访问私有成员变量。
三、递增运算符重载
我们先来看看前置递增和后置递增的使用
#includeusing 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,然后我们希望可以重载递增运算符实现递增操作。
#includeusing 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,则需要先使用全局函数来重载左移运算符。
#includeusing 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; }
此时可以输出自己定义的数据类型了
3.1 重载前置
首先我们实现前置递增
#includeusing 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
如果是返回值,我们测试看一下
#includeusing 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; }
可以看到返回值时连续做两次递增,只成功了一次,这是因为每一次返回的都是一个新的变量,下一次操作是对这个新的变量做操作。所以我们要返回引用,为了一直对一个数据做操作。
3.2 后置递增
要实现后置递增,则需要对递增运算符重载实现函数重载。于是我们在参数里面加个int,代表一个站位参数,编译器就会认为这是后置递增。
后置递增对应的重载函数要返回值。如果返回引用就是返回一个局部对象的引用,局部对象会在函数结束后被释放。
#includeusing 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=, 对属性进行值拷贝
4.1 赋值运算符重载的一些问题
我们先来测试下编译器自动提供的赋值运算符。我们创建一个Person类,包含一个成员变量m_Age,是个指向开辟在堆区数据age的指针。
#includeusing 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; }
我们直接使用=,可以发现能够实现成员变量值得复制,没有错误。
由于类中有属性指向堆区,所以我们需要写个析构函数来释放这块内存
#includeusing 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; }
可以发现报错,这是由于出现了前面讲的深浅拷贝问题,即堆区数据重复释放。
4.2 解决方案
所以我们重载=号时要进行深拷贝,而不是直接将值进行复制。
#includeusing 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
所以需要对重载函数进行修改。注意,不要返回值,否则又会调用拷贝构造函数创建一个副本
#includeusing 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#include 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#include using namespace std; class MyPrint { public: void operator()(string str) { cout << str << endl; } }; int main() { MyPrint myfunc; myfunc("hello world"); return 0; }
运行,可以看出类似于函数一样完成了字符串的打印输出。
例: 假设现在要创建一个MyAdd类,通过重载函数调用运算符完成两个整数相加。
#include#include 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()创建一个匿名对象,这个匿名对象在当前行执行结束后会被释放,然后为这个匿名对象调用了重载的()运算符函数。



