【C++】类和对象【中下】
目录
- 一、类与对象
- 1、运算符重载
- 1.2 赋值运算符重载
- 1.3 `<<`运算符和`>>`运算符
- 1.4 前置`++`与后置`++`
- 2、 `const`成员函数
- 3、取地址运算符重载
个人主页<—请点击
C++专栏<—请点击
一、类与对象
本期的主题是一步步完善日期类的编写,将要讲解的知识融入在代码中。
1、运算符重载
- 当运算符被用于类类型的对象时,
C++
语言允许我们通过运算符重载的形式指定运算符新的含义。C++
规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。 - 运算符重载是具有特殊名字的函数,它的名字是由
operator
和后面要定义的运算符
共同构成。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体。 - 重载运算符函数的参数个数和该运算符作用的运算对象数量⼀样多。⼀元运算符有⼀个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第二个参数。
- 如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的
this
指针,因此运算符重载作为成员函数时,参数比运算对象少⼀个。(this
指针在上期博客中有讲解) - 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。
- 不能通过连接语法中没有的符号来创建新的操作符:如
operator@
。 - 下面
5
个运算符不能重载:.*、::、sizeof、? : 、.
- 重载操作符至少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如:
int operator+(int x, int y)
。
我们依旧以实现过的日期类作为例子,这里我将源代码分为3
部分管理,分别是test.cpp、Date.h、Date.cpp
初始代码:
Date.h
:
#include <iostream>
using namespace std;class Date
{
public:void Print();Date(int year, int month, int day){_year = year;_month = month;_day = day;}Date(const Date& d){_year = d._year;_day = d._day;_month = d._month;}private:int _year;int _month;int _day;
};
Date.cpp
:
#include "Date.h"void Date::Print()
{cout << _year << "/" << _month << "/" << _day << endl;
}
test.cpp
:
#include "Date.h"int main()
{Date d1(2025, 1, 1);return 0;
}
⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义。我们知道日期加上天数是有意义的,能够让我们知道过了若干天后是几年几月几日。接下来我们来实现一下+=
的运算符重载operator+=
。
在实现重载运算符之前我们还要考虑如果天数加多了,要考虑进位的问题,所以我们先把每年的几月有多少天实现一下,便于我们实现+=
运算符的重载。
GetMonthDay
:
这个函数定义为日期类的成员函数。
int GetMonthDay(int year,int month)
{assert(month > 0 && month < 13);int a[15] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };if (month == 2 && ((year % 4 == 0 && year % 400 != 0) || (year % 400 == 0))){return 29;}else return a[month];
}
判断闰年,然后对2
月特殊处理,返回那一月的天数即可。
operator+=
:
Date& Date::operator+=(int day)
{_day += day;while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);_month++;if (_month == 13){_month = 1;_year++;}}return *this;
}
由于成员函数的声明和定义分离,所以访问时要加上Date::
,我们已经实现了日期加上天数,下面让我们验证一下正确性。
int main()
{Date d1(2025, 1, 1);d1.Print();d1 += 300;d1.Print();return 0;
}
这里+=
面对类对象,编译器会自动处理为调用运算符重载函数。d1
传给了第一个参数,也就是隐含的this
指针,300
传给了day
。
运行测试:
那我们如果不想让d1
中的天数发生改变该怎么办呢?这时候我们就可以实现operator+
了。
Date Date::operator+(int day)
{//调用拷贝构造创建另一个类Date tmp(*this);tmp += day;return tmp;
}
我们已经实现了+=
,所以我们可以利用+=
实现+
这里返回的时候就不能使用引用了,因为调用函数结束后tmp
会销毁,这里返回的时候程序会创建一个临时对象同时调用拷贝构造函数为临时对象初始化,然后临时对象传出去。
int main()
{Date d1(2025, 1, 1);//拷贝构造//Date d2(d1 + 10000);//等效Date d2 = d1 + 10000;Date d2 = d1 + 10000;d1.Print();d2.Print();return 0;
}
运行测试:
注意这里还有一处细节,就是临时对象是具有常性的,不能被更改,我们知道执行Date d2 = d1 + 10000;
的时候会首先调用operator+
函数将d1
传给this
指针,10000
传给day
,等到返回的时候,要建立临时对象,假设叫做zmp
,会隐含执行const Date zmp(tmp);
由于临时对象具有常性,所以d2的那段代码会处理为Date d2=zmp;
此时又会调用我们写的拷贝构造函数,如果我们的拷贝函数是Date(Date& d)
时,就会报错,因为涉及到了权限的放大,zmp
相当于const Date
肯定不能转换成Date
。
注意:这里的自己写的拷贝构造函数的参数必须是const Date& d
,当然这里编译器自己生成的拷贝构造函数对于日期类来讲也可以满足需求,你也可以不写。
1.2 赋值运算符重载
赋值运算符重载是⼀个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于⼀个对象拷贝初始化给另⼀个要创建的对象。
//拷贝构造
Date d1(2025,1,1);
Date d2 = d1;//赋值运算符重载的场景
Date d1(2025,2,2);
Date d2(2025,1,3);
d2 = d1;
赋值运算符重载的特点:
- 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成
const
当前类类型引用,否则会传值传参会有拷贝,浪费资源。 - 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
- 没有显式实现时,编译器会自动生成⼀个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用它的赋值重载函数。小技巧:如果⼀个类显示实现
了析构并释放资源,那么它就需要显示写赋值运算符重载,否则就不需要。
我们现在可以去尝试写一下operator=
:
void operator=(const Date& d)
{_year = d._year;_month = d._month;_day = d._day;
}
Date d1(2012, 3, 9);
Date d2(2025, 1, 1);
d1 = d2;
d1.Print();
d2.Print();
测试结果:
我们这里的确写出了operator=
,并且也起作用了,但我们没有遵从规则中的:有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景
,这里的场景是为了满足连续赋值;假设我们现在执行下面这段代码它会报错的:
因为我们的赋值重载函数没有返回值,此时将void
改为Date&
就可以解决问题。
Date& operator=(const Date& d)
{_year = d._year;_month = d._month;_day = d._day;return *this;
}
1.3 <<
运算符和>>
运算符
还记得我们写打印的时候用到的<<
吗? 它是流插入运算符,本质上也是运算符重载:
<<
重载了各种各样的内置类型,所以它用起来才会这么好用。
这时候对于自定义类型,我们可以自己重载<<
运算符,使其能够以合适的方式输出自定义类型的对象,方便进行输入输出操作。
operator<<
:
这里为了方便连续输出,<<
返回值要用ostream&
ostream& operator<<(ostream& out)
{out << _year << "/" << _month << "/" << _day << endl;return out;
}
注意:此时函数是类中的成员函数。
Date d3(2025, 1, 3);
cout << d3;
测试结果:
居然报错了?!我们再来仔细看看函数,发现第一个参数是this指针,而第二个参数是输出流对象,原来是我们传反参数了,我们翻一下:
Date d3(2025, 1, 3);
d3 << cout;
虽然这次我们的代码可以正常运行,但还是感觉哪里怪怪的,欸,这不倒反天罡了吗?这更不行了,我们知道C++
的类中的成员函数的第一个参数是this
指针,我们不能在函数的实参和形参中显示的写this
指针,所以只要写在C++
类域中,第一个参数就只能是this指针,唯一的方法就是定义在类外:
- 重载
<<
和>>
时,需要重载为全局函数,因为重载为成员函数,this
指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调用时就变成了对象<<cout
,不符合使用习惯和可读性。重载为全局函数把ostream/istream
放到第⼀个形参位置就可以了,第二个形参位置当类类型对象。
ostream& operator<<(ostream& out, const Date& d)
{cout << d._year << "/" << d._month << "/" << d._day << endl;return out;
}
这里我们将它定义成了全局变量,但是当面临全局变量的时候,又遇到了新的危机,我们定义成全局变量没办法访问类域中的成员呀,它们可是私有,此时我们可以把这个定义成友元函数(简单使用,后期博客还会讲解),就是在类域中声明这个函数是类的好朋友,可以允许这个函数访问私有成员。
此时程序运行起来就没有问题了:
>>
:
>>
是流提取运算符,本质上也是运算符重载。
>>
中也重载了各种各样的内置类型,所以它用起来才会这么好用。
这时候对于自定义类型,我们可以自己重载>>
运算符,使其能够以合适的方式输出自定义类型的对象,方便进行输入输出操作。
operator>>
:
这里为了方便连续输出,<<
返回值要用istream&
,借鉴operator<<
写成类成员函数的情况我们知道this
指针依旧会占第一个参数的位置,所以我们要定义成全局变量。然后为了访问类中的成员变量,我们依旧要借助friend
友元函数。
operator>>
:
istream& operator>>(istream& in, Date& d)
{cin >> d._year >> d._month >> d._day;return in;
}
注意:这里要向d
对象中输入年月日的值,所以形参不能写成const Date& d
,这样就不能向其中输入数据了,就会报错。
Date
类中的声明:
friend istream& operator>>(istream& in, Date& d);
运行的代码片段:
Date d3(2025, 1, 3);
cin >> d3;
cout << d3;
运行结果:
代码的运行是没有问题的,这样就完成了自定义类型输入的重载函数。
1.4 前置++
与后置++
前置++
与后置++
它们的函数重载名称都是operator++
,无法很好的区分它俩,于是C++
规定,后置++
重载时,增加⼀个int
形参,跟前置++
构成函数重载,方便区分。
//++d1;编译器处理-》d1.operator++();
Date& operator++();
//d1++;编译器处理-》d1.operator++(n);
//n是随便的一个整数值
Date operator++(int);
前置++
:
//++d1
Date& Date::operator++()
{*this += 1;return *this;
}
++d1
时d1
的本身会改变所以可以返回它本身,而d1++
时d1
不会改变,所以这时候就要再拷贝构造创建一个类对象,然后返回它。我们看到就是+=
上1
,因为这里+=
符号已经重载过了,所以这里会调用operator+=
函数来实现+=
。
//d1++
Date Date::operator++(int)
{Date tmp(*this);*this += 1;return tmp;
}
测试代码片段:
Date d1(2025, 5, 1);
Date d2 = d1++;
d1.Print();
d2.Print();
Date d3(2025, 5, 1);
Date d4 = ++d3;
d3.Print();
d4.Print();
这里大家可以看到我们的operator=
也派上用场了。
运行结果:
2、 const
成员函数
- 将
const
修饰的成员函数称之为const
成员函数,const
修饰成员函数放到成员函数参数列表的后面。 const
实际修饰该成员函数隐含的this
指针,表明在该成员函数中不能对类的任何成员进行修改。例如:const
修饰Date
类的Print
成员函数,即void Print() const;
,Print
隐含的this
指针由Date* const this
变为const Date* const this
。
如果我们没有添加const
,然后我们执行下面这段代码就会报错。
const Date d1(2025, 5, 1);
d1.Print();
原因:
但是我们又不能在成员函数的形参上显示定义this
指针,所以此时C++
才做出开头展示的规定。
void Date::Print() const
{cout << _year << "/" << _month << "/" << _day << endl;
}
这时候就是const
成员函数,只要不改变调用对象本身的成员函数都建议加const
。
代码运行:
3、取地址运算符重载
取地址运算符重载分为普通取地址运算符重载和const
取地址运算符重载,⼀般这两个函数,编译器自动生成的就用了,不需要去显示实现。
运行以下代码:
Date d1(2025, 5, 1);
const Date d2(2025, 5, 2);
cout << &d1 << endl;
cout << &d2 << endl;
当没有实现这两个函数时:
所以一般这两个函数我们没必要去实现,编译器自动生成的就满足需求了。除非是你不希望别人获取到这个类对象的地址,此时就可以自己实现。
Date* operator&()
{return nullptr;
}const Date* operator&() const
{return nullptr;
}
有两份函数,它们构成函数重载,此时编译器会针对不同的类型,普通类对象调用上面,const
修饰的调用下面。
运行结果:
这样就得不到类对象的地址了,当然这还不是最坏的,甚至可以给你返回一个假地址。
Date* operator&()
{return (Date*)0x00FF1120;
}const Date* operator&() const
{return (Date*)0x00FF4330;
}
运行结果:
地址看着也没有问题,这时候能把你坑死。
总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~