函数重载和运算符重载
- 一、函数重载
- 二、运算符重载
- (一)==运算符重载
- (二)=运算符重载
- (三)++运算符重载(前置和后置)
- (四)+、+=运算符重载
- (五)<<运算符重载
- 三、总结
重载分为两种情况,一种是函数重载,一种是运算符重载,它们常用来处理实现功能类似但是数据类型不同的问题。
一、函数重载
函数重载:在C++中,允许在同一作用域中声明多个同名函数,但是这些同名函数的形参列表(形参列表指的是:参数个数或类型或类型顺序)不同。满足这样的同名函数就构成重载。
注意:
1、返回值不作为函数重载的判断条件,所以返回值可同可不同。
2、当调用重载的函数时,编译器会根据形参和实参,优先调用最匹配的函数、其次次之,如果没有匹配的函数会报错(下面例子有讲解)。
下面是函数重载的例子:
#include <iostream>
using namespace std;
// 1、参数类型不同
int Add(int x, int y)
{cout << "Add(int x, int y)" << endl;return x + y;
}double Add(double x, double y)
{cout << "Add(double x, double y)" << endl;return x + y;
}double Add(int x, double y)
{cout << "Add(int x, double y)" << endl;
}// 2、参数个数不同
void f()
{cout << "f()" << endl;
}
void f(int a)
{cout << "f(int a)" << endl;
}// 3、参数类型顺序不同
void f(int a, char b)
{cout << "f(int a,char b)" << endl;
}void f(char b, int a)
{cout << "f(char b, int a)" << endl;
}int main()
{ //下面三次Add函数调用,编译器会优先查找参数最匹配的函数,显然这里都有最匹配的,则各自调用最匹配的。如果这里只是实现了三个Add中的任意一个,那么下面三次调用都会调用实现的那一个,虽然没有最匹配的,但是可以调用(可能会报警告)。Add(10, 20); //调用第一个Add函数Add(10.1, 20.2); //调用第二个Add函数Add(10, 20.5); //调用第三个Add函数f();f(10);f(10, 'a');f('a', 10);return 0;
}
二、运算符重载
运算符重载和函数重载类似,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号:比如:operator+
函数原型:返回值类型 operator操作符(参数列表)
注意:
1、不能重载没有的运算符,比如:operator#
2、用于内置类型的运算符,不能改变其含义,比如:+号,不能将它改变成减法
3、运算符函数的参数类型必须有一个是类类型
4、运算符函数的参数个数和运算符的操作数个数是相等的,比如:==号的操作数是两个,重载的时候参数个数也必须是两个
5、当有两个操作数的运算符时,运算符函数第一个参数是左操作数,第二个参数是右操作数
6、当运算符重载是类成员函数的时候,形参看起来比实际参数少一个,因为类成员函数的第一个参数为隐藏的this
7、有五个运算符不能重载,分别为:.*,::,sizeof, ?:, .
下面通过一个例子实现常见的几种运算符重载
(一)==运算符重载
假设我们有下面这样一个日期类,我现在的需求是用日期类实例化两个对象,然后比较两个对象是否相等。此时需要重载运算符==,如果不重载,无法比较,因为运算符==不能比较两个自定义类型。
#include <iostream>
using namespace std;class Date //日期类
{
public:Date(int year = 2000, int month = 1, int day = 1) //默认构造{_year = year;_month = month;_day = day;}//private:int _year;int _month;int _day;
};// 重载==运算符,判断日期类对象是否相等(这是一个全局函数)
bool operator==(const Date& d1, const Date& d2)
{//当运算符重载成全局函数的时候,在函数体内访问类成员变量的时候,类成员变量权限必须是公有的,否则函数体内无法访问//但是这也带来了一个问题,类成员的安全性,一旦公有,谁都可以访问篡改类成员变量//解决方法有两种:1、通常将运算符重载成类成员函数(通常采用的方法) 2、将运算符重载(全局函数)声明成类的友元函数return (d1._year == d2._year && d1._month == d2._month && d1._day == d2._day);
}int main()
{Date d1(2024, 2, 29);Date d2(2023, 2, 29);//当重载成全局函数的时候有两种调用方式//第一种:跟普通函数的调用一样if (operator==(d1, d2))cout << "相等" << endl;elsecout << "不相等" << endl;//第二种:跟内置类型判断是否相等写法一样,这样写的话看起来更符合运算符的写法//你可以理解为编译器会将d1==d2处理成第一种写法if (d1 == d2)cout << "相等" << endl;elsecout << "不相等" << endl;return 0;
}
为了解决上述类的成员变量安全性问题,我们将运算符重载成类的成员函数
#include <iostream>
using namespace std;class Date //日期类
{
public:Date(int year = 2000, int month = 1, int day = 1) //默认构造{_year = year;_month = month;_day = day;}//这里看起来少了一个参数,实际上是第一个参数(左操作数)隐藏起来了//函数原型可以看成:bool operator==(Date* this, const Date& d2)bool operator==(const Date& d2){return (this->_year == d2._year && this->_month == d2._month && this->_day == d2._day);}private:int _year;int _month;int _day;
};int main()
{Date d1(2024, 2, 29);Date d2(2023, 2, 29);//当重载成类的成员函数的时候有两种调用方式//第一种:跟类的成员函数调用一样cout << d1.operator==(d2) << endl;//第二种:跟内置类型使用一样,可以理解为编译器会将是d1==d2处理成第一种cout << (d1 == d2) << endl;return 0;
}
(二)=运算符重载
#include <iostream>
using namespace std;class Date //日期类
{
public:Date(int year = 2000, int month = 1, int day = 1) //默认构造{_year = year;_month = month;_day = day;}//赋值运算符返回值应是Date类型,因为要构成链式反应,支持连续赋值//比如int类型,a = b = c,可看成:a = (b == c)),其次返回引用是为了提高效率Date& operator=(const Date& d2){if (this != &d2) //防止自己给自己赋值{this->_year = d2._year;this->_month = d2._month;this->_day = d2._day;}return *this;}private:int _year;int _month;int _day;
};int main()
{Date d1(2024, 2, 29);Date d2(2023, 2, 29);Date d3;d3 = d1;return 0;
}
注意(下面都是类中知识的注意事项):
1、赋值运算符必须重载成类的成员函数,因为赋值运算符如果不显式在类中实现,编译器会生成一个默认的,此时用户再在类外自己重载一个全局的赋值运算符,在调用的时候会出现二义性,故赋值运算符重载只能是类的成员函数。
2、默认提供的赋值运算符函数,只是简单的值拷贝(将一个对象中成员变量按字节为单位拷贝给另一个成员的变量),如果成员变量中涉及动态开辟的资源,那么用默认的赋值运算符函数可能会出现浅拷贝问题。如果类中未涉及到资源管理,赋值运算符是否实现都可以,一旦涉及到资源管理则必须要实现。
(三)++运算符重载(前置和后置)
前置++和后置++都是一元运算符,重载时怎么区分它们呢?为了能正确重载,c++规定,后置++运算符函数多一个int类型的参数,调用时,由编译器自动传递。
#include <iostream>
using namespace std;class Date //日期类
{
public:Date(int year = 2000, int month = 1, int day = 1) //默认构造{_year = year;_month = month;_day = day;}//前置++//返回值必须返回引用,因为要构成链式反应,支持连续++//比如:++(++a),这里的结果是a增加了两次,括号外面的++也是对a自身的++,所有要返回引用,也就是返回它的自身//不然返回临时对象的话,临时对象增加之后销毁了,而它本身没有增加Date& operator++() {this->_day += 1; //这里只是简单的加1,没有做每个月天数处理return *this;}//后置++//返回值不是引用,因为返回的是临时变量,所有不能使用引用,并且后置++不支持连续++Date operator++(int) {Date tmp = *this;this->_day += 1;return tmp;}private:int _year;int _month;int _day;
};int main()
{Date d1(2024, 2, 29);Date d2(2023, 2, 29);Date d3 = d1++; //d3是(2024,2,29),d1变成了(2024,2,30)Date d4 = ++(++d2); //d4是(2023,2,31),d2变成了(2023,2,31)return 0;
}
前置–和后置–,跟前置++和后置++处理方法一样
(四)+、+=运算符重载
#include <iostream>
using namespace std;class Date //日期类
{
public:Date(int year = 2000, int month = 1, int day = 1) //默认构造{_year = year;_month = month;_day = day;}//返回值是Date,因为要返回它们计算的和//其次返回值不是引用,因为返回值是临时对象,加法的左操作数是不会改变的,所以返回临时对象Date operator+(const int day) {Date tmp = *this;tmp._day += day;return tmp;}//同上Date operator+(const Date& d2){Date tmp = *this;tmp._year += d2._year;tmp._month += d2._month;tmp._day += d2._day;return tmp;}//返回值是Date,因为要返回它们相加的和//返回引用,因为要和其他运算符构成链式反应,比如:++(a += 1),括号计算结束之后,前置++还会导致a增加一次,所以必须返回引用Date& operator+=(const int day){this->_day += day;return *this;}//同上Date& operator+=(const Date& d2){this->_year += d2._year;this->_month += d2._month;this->_day += d2._day;return *this;}private:int _year;int _month;int _day;
};int main()
{Date d1(2024, 2, 29);Date d2(2023, 2, 29);Date d3 = d2 + 1; //(2023, 2, 30)Date d4 = d2 + d3; //(4046, 4, 59)d3 += 1; //(2023, 2, 31)d4 += d3; //(6069, 6, 90)return 0;
}
-、-=运算符重载和+、+=运算符重载类似
(五)<<运算符重载
如果我们直接打印Date类型的数据(如:cout<<d1),是无法打印的,因为Date类型是自定义类型。根本原因还是ostream类中没有重载能处理Date类型的<<运算符函数,因为你是自定义类型,我不知道你要怎样打印,所以我无法重载这样的运算符函数。ostream中只重载了内置数据类型,平时使用的cout是ostream的一个对象,所以可以通过cout<<来打印数据。
下图是ostream类中<<运算符的重载和cout的声明
标准输出运算符必须重载成全局函数,并声明为类的友元函数,如果重载在类中,也能使用,但是用法和正常用法会不一样,所以为了统一,定义在类外。
首先看一下重载成类成员函数,会是怎样的情况:
#include <iostream>
using namespace std;class Date //日期类
{
public:Date(int year = 2000, int month = 1, int day = 1) //默认构造{_year = year;_month = month;_day = day;}//重载在类中//形参类型是ostream,因为我们打印Date类型数据终究还是要依靠标准输出流cout,只不过我们使用cout自定义如何打印,所以要传参cout//返回值是ostream,因为要构成链式反应(重载再类中的链式反应有缺陷),如cout<<a<<b,可以理解为(cout<<a)<<b,先是cout<<a,结果返回cout,再cout<<b。返回引用是因为效率ostream& operator<<(ostream& out){out << "(" << this->_year << ", " << this->_month << ", " << this->_day << ")";return out;}private:int _year;int _month;int _day;
};int main()
{Date d1(2024, 2, 29);Date d2(2023, 2, 29);//这种用法是错误,因为左操作数是运算符函数的第一个参数,而第一个参数是Date类型对象cout << d1 << endl;//正确用法,但是这种用法和我们平常用法相反d1 << cout << endl; //链式反应的缺陷d2 << d1 << cout; //错误,因为是从左到右运算,先是d2<<d1,无法被识别d2 << cout << 1 << "+" << "2" << endl; //正确(可以看出来每次都是第一次操作是反的:d2<<cout,后面都正常了)return 0;
}
重载成全局函数并声明为Date类的友元函数,友元函数意思就是我是Date的好朋友,我可以在我的函数体内访问你的私有成员变量了
#include <iostream>
using namespace std;class Date //日期类
{friend ostream& operator<<(ostream& out, const Date& d); //声明为友元函数
public:Date(int year = 2000, int month = 1, int day = 1) //默认构造{_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;
};ostream& operator<<(ostream& out, const Date& d)
{out << "(" << d._year << ", " << d._month << ", " << d._day << ")";return out;
}int main()
{Date d1(2024, 2, 29);Date d2(2023, 2, 29);cout << d1 << "," << d2 << endl;return 0;
}
三、总结
无论是函数重载还是运算符重载,它们都是满足三个条件:
1、在同一作用域中
2、函数名相同
3、参数列表不同