目录
1. 运算符重载概念
小拓展
2. 运算符重载示例
2.1 全局运算符重载函数
2.2 == 运算符重载
2.3 > 或 < 运算符重载
2.4 +运算符重载
3. 赋值运算符重载
对比
4. cin与cout的绑定关系
小拓展
5. const成员函数
6. &运算符重载
7. 总结
Date.h
Date.cpp
1. 运算符重载概念
运算符重载就是让系统内置的运算符运算自己定义的类型。运算符重载是具有特殊名字的函数,他的名字是由operator和后面要定义的运算符共同构成。和其他函数一样,它也具有其返回类型和参数列表以及函数体。例如,对于加法运算符+,其重载函数的名称为operator+。
特点:
- 当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。
- 重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
- 如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个。
- 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。
- 不能通过连接语法中没有的符号来创建新的操作符:比如operator@。
- .* :: sizeof ?: . 以上5个运算符不能重载。
- 重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义。如 int operator+ (int x , int y)
- 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。 C++规定,后置++重载时,增加一个int形参,跟前置++构成函数重载,方便区分。
- 重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第一个形参位置,第一个形参位置是左侧运算对象,调用时就变成了 对象<<cout,不符合使用习惯和可读性。重载为全局函数把ostream/istream放到第一个形参位置就可以了,第二个形参位置当类类型对象。
小拓展
了解一下 .* 运算符
用于函数指针和成员函数指针
可以将成员函数指针赋值给一个函数指针变量,然后通过.* 运算符来调用该成员函数。
示例:
#include<iostream> using namespace std;class A { public:int add(int x,int y){ return x + y;} };int main() {A obj;//成员函数指针类型int (A:: * pf)(int, int) = &A::add; //C++规定成员函数要加&才能取到函数指针int result = (obj.*pf)(2, 3); //对象调用成员函数指针时,使用.*运算符cout << "计算结果是: " << result << endl;return 0; }
上面代码定义一个类A,包含一个公有成员函数add,创建对象obj,接着定义一个指向A类成员函数的指针pf,通过函数指针和.* 调用成员函数完成相加计算。
2. 运算符重载示例
2.1 全局运算符重载函数
#include<iostream>
using namespace std;
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//private:int _year;int _month;int _day;
};//全局运算符重载函数,类外部进行定义,将上面成员变量属性改为 public
bool operator==(const Date& d1, const Date& d2)
{return d1._year == d2._year&& d1._month == d2._month&& d1._day == d2._day;
}int main()
{Date d1(2025, 3, 25);Date d2(2025, 3, 26);cout << (d1 == d2) << endl; //d1 == d2 编译器会转换成 operator==(d1, d2)cout << operator==(d1, d2) << endl; //显示调用return 0;
}
运行结果:
全局运算符重载函数的参数数量等于运算符操作数数量,因为两个操作数都需要显示传递。
2.2 == 运算符重载
#include<iostream>
using namespace std;
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//运算符重载作为成员函数bool operator==(const Date& d){return _year == d._year&& _month == d._month&& _day == d._day;}private:int _year;int _month;int _day;
};int main()
{Date d1(2025, 3, 25);Date d2(2025, 3, 26); cout << (d1 == d2) << endl; //d1 == d2 编译器会转换成 d1.operator == (d2)cout << d1.operator == (d2) << endl;//显示调用return 0;
}
运行结果:
运算符重载函数参数数量为:运算符操作数数量-1。因为成员函数的左操作数是当前对象(通过this指针隐式传递),比如上面 d1.operator == (d2) 中左操作数d1由this指向。
2.3 > 或 < 运算符重载
#include<iostream>
using namespace std;
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}bool operator<(const Date& obj){if (_year < obj._year){return true;}else if (_year == obj._year){if (_month < obj._month){return true;}else if (_month == obj._month){return _day < obj._day;}}return false;}private:int _year;int _month;int _day;
};int main()
{Date d1(2025, 3, 25);Date d2(2025, 3, 26); cout << d1.operator<(d2) << endl;cout << (d1 < d2) << endl;return 0;
}
运行结果:
2.4 +运算符重载
#include<iostream>
#include<string>
using namespace std;
class Student
{
public:string name = "LING";int GetAge(){return this->age;}//类对象与整数相加int operator+ (int m){return this->age + m;}//两个类对象相加Student operator+ (Student& obj){Student obj4;obj4.age = this->age + obj.age;return obj4;}private:int age = 18;};int main()
{Student obj1;Student obj2;int a = 5;int val = obj1 + 5; //val=obj1.operator + (5);cout << "类对象与整数相加: " << val << endl;Student obj3;obj3 = obj1 + obj2; //obj3 = obj1.operator + (obj2);cout <<"两个类对象相加: " << obj3.GetAge() << endl;return 0;
}
运行结果:
3. 赋值运算符重载
赋值运算符重载是一个默认成员函数,用于将一个已存在的对象赋值给另一个已存在的对象。这里与拷贝构造函数不同,拷贝构造函数用于创建新对象时,用已存在对象的内容初始化新对象。
特点:
- 赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成 const 当前类类型引用,否则会传值传参进行拷贝。
- 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
- 没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝,对自定义类型成员变量会调用它的赋值重载函数。
#include<iostream>
using namespace std;
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//赋值运算符重载函数void operator=(const Date& d){_year = d._year;_month = d._month;_day = d._day;}/*void operator=(Date d) //传值也行,会调用拷贝构造函数,不会引发无穷递归,建议写成引用,减少不必要的拷贝{_year = d._year;_month = d._month;_day = d._day;}*///拷贝构造函数Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}private:int _year;int _month;int _day;
};int main()
{Date d1(2025, 3, 27);Date d2(2025, 3, 28); //赋值重载d1 = d2;//拷贝构造Date d3(d2);Date d4 = d2; return 0;
}
#include<iostream>
using namespace std;
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//赋值运算符重载函数1 (d4 = d3 = d1;) d3=d1Date& operator=(const Date& d)//返回引用{_year = d._year;_month = d._month;_day = d._day;return *this;// d3=d1表达式返回对象应该为d3,也就是*this}/* 赋值运算符重载函数2 (d4 = d3 = d1;) d3=d1Date operator=(const Date& d){_year = d._year;_month = d._month;_day = d._day;return *this;//返回值类型是值类型,会调用拷贝构造函数生成一个*this(也就是d3)的临时对象,然后将临时对象返回。临时对象会被用来给d4赋值,影响效率}*///拷贝构造函数Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}private:int _year;int _month;int _day;
};int main()
{Date d1(2025, 3, 27);Date d2(2025, 3, 28); //拷贝构造Date d3(d2);Date d4 = d2; d4 = d3 = d1; //d4.operator=(d3.operator=(d1));//连续赋值 从右往左进行结合运算//步骤1:d3.operator=(d1)返回d3的引用(Date&)//步骤2:d4.operator=(返回的引用)直接操作d3的值,最终d4和d3的值都等于d1return 0;
}
赋值运算符重载函数的返回值类型类型设为值类型会产生额外的拷贝开销,而返回引用类型( Date&)可以避免这种问题,从而提高代码效率。
像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显示实现赋值运算符重载。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的赋值运算符重载会调用Stack的赋值运算符重载, 也不需要我们显示实现MyQueue的赋值运算符重载。如果一个类显示实现了析构并释放资源,那么他就需要显示写赋值运算符重载,否则就不需要。
注意:拷贝构造函数是构造函数的一种,所以在对象创建时调用,而赋值运算符是类的成员函数,在对象存在后调用。
对比
特性 | 拷贝构造函数 | 赋值运算符重载 |
功能 | 创建新对象时,用已存在对象的内容初始化新对象 | 将一个已存在对象的值赋值给另一个已存在对象 |
参数 | (类名)(const 类名&)(必须是引用,避免无限递归调用拷贝构造函数) | 类名& operator=(const 类名&)(返回引用以支持链式赋值) |
返回值 | 无返回值 | 类名&(支持链式赋值) |
4. cin与cout的绑定关系
翻译:cin被绑定到标准输出流cout(参见ios::tie),这表示在对cin执行每个i/o操作之前,会刷新cout的缓冲区。
在C++中,cin与cout存在绑定关系,这意味着在执行输入操作(如cin)时,会先自动刷新cout的缓冲区。cin与cout绑定的主要目的是为了确保在输入操作前,输出缓冲区中的内容已经被正确输出并显示给用户。
C++中可以通过 tie 函数来管理流的绑定关系,默认情况下,cin绑定到cout,输入时自动刷新cout,这可能影响性能,因为每次输入都要刷新输出缓冲区。如果程序中不需要输入前立即显示输出,解除绑定(设置为nullptr)可以提高性能,因为减少了不必要的刷新操作。对于大量输入输出程序(如算法竞赛中的大数据处理),解除绑定可显著提高速度。
#include<iostream>
using namespace std;
int main()
{//在IO需求⽐较⾼的地⽅,如部分⼤量输⼊的竞赛题中,加上以下几⾏代码,可以提⾼C++程序 IO操作的效率 ios_base::sync_with_stdio(false);//关闭C++的iostream流与C标准IO流(stdio)的同步,减少同步开销cin.tie(nullptr);//解除cin与cout的绑定cout.tie(nullptr);//解除cout与其他流的绑定,减少不必要的同步操作return 0;
}
小拓展
缓冲区机制:
C++的标准流(如cout)默认使用缓冲区来减少I/O次数。例如,cout会将数据先存入缓冲区,达到一定量或遇到换行符\n 时才实际输出。
绑定的优缺点:
当cin与cout绑定时,每次输入操作都会强制刷新cout,破坏缓冲区的优化效果。解除绑定后,cout仅在自然刷新条件(如缓冲区满、换行符、或手动flush)时输出,从而减少I/O次数。
- 同步机制启用时(默认状态)即:ios_base::sync_with_stdio(true);C++输入输出流会与C的标准流同步,缓冲区是共享的,无论使用cout还是printf,数据都会写入同一个缓冲区,程序结束时自动刷新缓冲区。当cout输出或cin输入时,会自动刷新共享缓冲区(通过C标准库的fflush),确保C和C++混合使用时输入输出一致。
- 同步机制禁用时(默认状态)即:ios_base::sync_with_stdio(false); cin、cout使用自己独立的缓冲区,与stdin、stdout的缓冲区完全分离。程序结束时,C++缓冲区不会自动刷新,若要让C++缓冲区数据输出,需手动刷新。
- ios_base::sync_with_stdio(false); 与 cin.tie(nullptr);二者共同作用才能完全避免因默认关联机制导致的cout缓冲区自动刷新,如果没有关闭同步机制,仅仅是解除绑定关系,因同步产生的缓冲区刷新依然会发生。
示例(在DEVC++观察运行结果):
5. const成员函数
将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后面。
#include<iostream>
using namespace std;class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print() const //void Print(const Date* const this) const {cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};
int main()
{const Date d1(2025, 4, 7);d1.Print(); //&d1的类型是const Date* 传给this指针Date d2(2025, 5, 8); //这⾥非const对象也可以调用const成员函数(权限缩⼩)d2.Print();return 0;
}
const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。this指针默认是 Date* const this &d1的类型是 const Date* 传给this指针 会触发权限放大 ,所以const 修饰Date类的Print成员函数,Print函数隐含的this指针就由 Date* const this 变为 const Date* const this 了。
6. &运算符重载
取地址运算符重载分为 普通取地址运算符重载 和 const取地址运算符重载 ,默认情况下,用户不需要定义operator&,因为编译器会隐式处理取地址操作,返回对象的内存地址,不需要我们去显示实现。除非一些很特殊的场景,比如我们不想让别人取到当前类对象的地址(返回虚假地址),就可以自己实现一份,胡乱返回一个地址。
class Date
{
public:Date* operator&(){return this;//return nullptr;//return (Date*)0x0012ff40;}const Date* operator&() const{return this;//return nullptr;//return (Date*)0x0012ff48;}
private:int _year;int _month;int _day;
};
7. 总结
Date.h
#pragma once#include<iostream>
using namespace std;
#include<assert.h>class Date
{
public://友元函数声明friend ostream& operator<<(ostream& out, const Date& d);friend istream& operator>>(istream& in, Date& d);bool CheckDate()const;Date(int year = 1900, int month = 1, int day = 1);void Print()const;int GetMonthDay(int year, int month) const //定义在类里面的成员函数默认为inline{assert(month > 0 && month < 13);static int monthDayArray[13] = { -1,31,28,31,30,31,30,31,31,30,31,30,31 };if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)){return 29;}return monthDayArray[month];}bool operator<(const Date& d) const;bool operator<=(const Date& d) const;bool operator>(const Date& d) const;bool operator>=(const Date& d) const;bool operator==(const Date& d) const;bool operator!=(const Date& d) const;Date operator+(int day) const;Date& operator+=(int day);Date operator-(int day) const;Date& operator-=(int day);//d1++Date operator++(int);//++d1;Date& operator++();int operator-(const Date& d) const;//void operator<<(ostream& out);成员函数Date* operator&(){return this;//return nullptr;//return (Date*)0x0012ff40;}const Date* operator&() const{return this;//return nullptr;//return (Date*)0x0012ff48;} private:int _year;int _month;int _day;
};ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
Date.cpp
#include"Date.h"bool Date::CheckDate() const
{if (_month < 1 || _month>12 || _day<1 || _day>GetMonthDay(_year, _month)){return false;}else{return true;}}Date::Date(int year, int month, int day)
{_year = year;_month = month;_day = day;if (!CheckDate()){cout << "日期非法" << endl;Print();}
}void Date::Print() const
{cout << _year << "/" << _month << "/" << _day << endl;
}//d1<d2
bool Date::operator<(const Date& d) const
{if (_year < d._year){return true;}else if (_year == d._year){if (_month < d._month){return true;}else if (_month == d._month){return _day < d._day;} }return false;
}//d1<=d2
bool Date::operator<=(const Date& d) const
{return *this < d || *this == d;
}bool Date::operator>(const Date& d) const
{return !(*this <= d);
}bool Date::operator>=(const Date& d) const
{return !(*this < d);
}bool Date::operator==(const Date& d) const
{return _year == d._year && _month == d._month && _day == d._day;
}bool Date::operator!=(const Date& d) const
{return !(*this == d);
}Date Date::operator++(int)
{Date tmp = *this;*this += 1;return tmp;
}Date& Date::operator++()
{*this += 1;return *this;
}int Date::operator-(const Date& d) const
{int flag = 1;Date max = *this;Date min = d;if (*this < d){max = d;min = *this;flag = -1;}int n = 0;while (min != max){++min;++n;}return n * flag;}Date& Date::operator+=(int day)
{if (day < 0){return *this -= (-day);}_day += day;while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);++_month;if (_month == 13){_year++;_month = 1;}}return *this;
}Date Date::operator+(int day) const
{Date tmp = *this;tmp += day;/*tmp._day += day;while (tmp._day > GetMonthDay(tmp._year,tmp._month)){tmp._day -= GetMonthDay(tmp._year, tmp._month);++tmp._month;if (tmp._month == 13){tmp._year++;tmp._month = 1;}}*/return tmp;
}//方式一:
Date& Date::operator-=(int day)
{if (day < 0){return *this += (-day);}_day -= day;while (_day <= 0){_month--;if (_month == 0){_month = 12;_year--;}_day+= GetMonthDay(_year, _month);}return *this;
}Date Date::operator-(int day) const
{Date tmp = *this;tmp -= day;return tmp;
}//方式二:相对于方式一多了3次拷贝
//Date Date::operator-(int day) const
//{
// Date tmp = *this;
// tmp._day -= day;
//
// while (tmp._day <= 0)
// {
// tmp._month--;
// if (tmp._month == 0)
// {
// tmp._month = 12;
// tmp._year--;
// }
// tmp._day+= GetMonthDay(tmp._year,tmp._month);
// }
//
// return tmp;
//}
//
//
//Date& Date::operator-=(int day)
//{
// *this = *this - day;
//
// return *this;
//}//写成成员函数有问题!“没有接受"Date"类型的右操作数的运算符 参数不匹配 cout<<d1 --> [d1 << cout; d1.operator<<(cout);]可匹配
//
//void Date::operator<<(ostream& out)
//{
// out << _year << "年" << _month << "月" << _day << "日" << endl;
//}//写成全局的 (定义成友元函数可以访问私有成员)
ostream& operator<<(ostream& out, const Date& d) //流插入运算符<<的结合性从左到右 返回流对象的引用,通过返回引用可以实现连续输出 cout<<d1<<d2;
{out << d._year << "年" << d._month << "月" << d._day << "日" << endl;//访问私有成员//每个步骤都会返回引用(ostream&)//用户自定义的operator<<是程序的"入口",而标准库的operator<<是该入口内部处理具体成员变量的工具!return out; //返回当前输出流对象,这样可以在同一个流上进行操作,为了支持链式调用,例如: cout << d1 << d2;
}istream& operator>>(istream& in, Date& d)//提取的值要放到对象里面,不能加const
{ while (1){cout << "请依次输入年月日:"; //在进行下面流提取之前,cout缓冲区已经被刷新出去了。in >> d._year >> d._month >> d._day;//调用标准库的operator<<函数 来读取int类型(d._year)成员变量//每个步骤都会返回流的引用(istream&)if (!d.CheckDate())//构造出来的日期和流提取输入的日期都要有检查{cout << "输入日期非法" << endl;d.Print();cout << "请重新输入!!!" << endl; }else{break;}}return in;//返回引用,以支持链式调用 cin >> d1 >> d2;
}