嗨喽大家好呀,今天阿鑫给大家带来的是剩下的默认构造函数,好久不见啦,下面让我们进入本节博客的内容吧!
类和对象第四弹之默认成员函数最终章
1.赋值运算符重载
2.const成员函数
3.取地址及const取地址操作符重载
1.1 运算符重载
在c++中,
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
1.不能通过连接其他符号来创建新的操作符:比如operator@
2.重载操作符必须有一个类类型参数
3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义.
4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
5. . ::sizeof ?: .* 注意以上5个运算符不能重载。这个经常在笔试选择题中出
至此程序员自己定义的运算符就在自己定义的类里面自己进行控制,支持运算符重载对于c++有重大意义
1.2赋值拷贝
下面我们从赋值拷贝来引入运算符的重载,我们首先进行函数声明与定义的分离
Date& operator=(const Date& d);
Date& Date::operator=(const Date& d) //赋值拷贝显式调用类似d1.operator=(d2)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
如上代码,便是赋值拷贝函数的原型,注意:操作数和形参是按照顺序来的,这点在后面我们也需要用到
在我们进行函数调用时,分为显式调用和隐式调用
d1 = d2;//隐式调用
d1.operator=(d2);显式调用
我们在日常写代码的过程中只需要写第一种,编译器会自动转换成显式调用
有些同学会有疑惑,为什么这个函数的返回值我们给的是引用而不是Date,下面我们就为什么传引用而不是Date做一个分析,利用func函数
Date func()
{
Date d(2024, 4, 16);
return d;
}
int main()
{
const Date& ref = func();
return 0;
}
Date::Date(int year, int month, int day)//构造函数
{
//cout << "Date(const Date& d)" << endl;
_year = year;
_month = month;
_day = day;
}
Date::Date(const Date& d)//拷贝构造
{
cout << "Date():" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
Date::~Date()//析构函数
{
cout<< "~Date()" << endl;
_year = -1;
_month = -1;
_day = -1;
}
上面我们应用了传值调用,那么由上节课的内容我们可以知道,在自定义类型进行传值调用时,会进行拷贝构造
看到上面的调用过程,有些聪明的同学此时就会想,对啊我们可以直接将返回值设为d的引用,此时可以节省不需要拷贝构造,那么让我们看看是否能正确实现我们的代码呢?
咦?为啥我们没能正确的对ref进行初始化呢,那我又要提到了,聪明的同学肯定已经注意到了上面的d已经进行了析构,我们在将析构函数的时候讲过,当一个函数或者变量生命周期到了,那么便会调用析构函数,清理资源。我们可以画出一个图,来生动阐述这个过程。
我们可以知道ref相当于d的别名,而我们站在底层的角度可以知道,ref建立在main函数的栈帧,而d是在func函数的栈帧中创建的,在func函数调用完成后,栈帧会销毁,而d也会被析构。从而导致ref不能被正确的初始化
我们可以将这里两个的地址打印出来给同学们看一下
好的,那么同学们就疑问了,啥时候用传值啥时候传引用啊?哈哈,下面我们给大家解释
其实上面的代码中我们只需要在d前面加一个static就可以了。
其实从上面的图我们已经可以得出
出了作用域,返回对象还在没有析构,那就可以用引用返回,减少拷贝
a返回对象生命周期到了,会析构,传值返回
b返回对象生命周期没到,不会析构,传引用返回
我们如果将d放在静态区,那么在函数调用结束时,我们的d也不会被析构,从而能正确的初始化ref并且可以减少拷贝。
下面我们举出一个例子来说明传引用和传值的区别
int main()
{
Date d1(2024, 4, 16);
Date d2(d1);
Date d3 = d1;
Date d4(2024, 5, 1);
d1 = d4;
d1 = d2 = d4;
return 0;
}
我们可以很明显的看出传值和传引用的区别,但是如果在特定的情境下,我们必须进行传值,那不能自作聪明地进行传引用,毕竟bug比多拷贝几次的问题大多了。
1.3赋值拷贝与拷贝构造的区别
#include"Date.h";
int main()
{
Date d1(2024, 4, 15);
// 拷贝构造
// //一个已经存在的对象,拷贝给另一个要创建初始化的对象
Date d2 = d1;
Date d3(2005, 7, 16);
// 赋值拷贝
// 赋值重载一个已经存在的对象,拷贝赋值给另一个已经存在的对象
d1 = d3;
return 0;
}
1.4编译器默认生成的赋值构造函数
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。类似于析构和构造。
编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了 ,还需要自己实现吗?当然像日期类这样的类是没必要的。( 与析构函数原理相同 )
2.1const成员函数
在引入const的成员函数前,我们先实现一个较为完整的日期类
#pragma once
#include<iostream>
#include<cassert>
using namespace std;
class Date
{
public:
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
Date(int year = 1900, int month = 1, int day = 1);
Date(const Date& d);
~Date();
int GetMonthDay(int year, int month)
{
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;
}
else
{
return monthDayarray[month];
}
}
void Print();
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);
//日期加天数,返回日期
Date& operator+=(int day);
Date& operator-=(int day);
Date& operator=(const Date& d)const;//赋值构造,当两个对象都已经存在,将一个对象的值赋值给另外一个
Date operator+(int day)const;
Date operator-(int day)const;
int operator-(const Date& d)const;//d1-d2
//前置++,返回++之后的结果
Date& operator++();
//后置++,返回++之前的结果
Date operator++(int);
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
#include"Date.h";
Date::Date(int year, int month, int day)//构造函数
{
//cout << "Date(const Date& d)" << endl;
_year = year;
_month = month;
_day = day;
}
Date::Date(const Date& d)//拷贝构造
{
cout << "Date():" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
Date::~Date()//析构函数
{
cout<< "~Date()" << endl;
_year = -1;
_month = -1;
_day = -1;
}
Date& Date::operator=(const Date& d)const //赋值拷贝显式调用类似d1.operator=(d2)
{
//把前面一个赋值给后面一个
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
//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;
}
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 || *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 !(*this <= d);
}
void Date::Print()const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
Date& Date:: operator+=(int day)//这里我刚刚有一个疑问,GetMonthDay的参数谁来传,但其实不用疑问,因为他是成员函数,会有一个
//隐含的this指针,也就是我调用的时候没有传Date,但其实已经传了Date的地址过去
//d3.operator==(d4);类似这样已经传了d3的地址过去
{
_day += day;
//当天数大于当前月的天数时,进入循环
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_month = 1;
_year++;
}
}
return *this;
}
Date Date:: operator+(int day)const
{
Date tmp = *this;
tmp += day;
return tmp;
}
Date& Date:: operator-=(int day)
{
_day -= day;//将传的天数减掉,判断此时的天数是正还是负
while (_day > 0)
{
if (_day < GetMonthDay(_year, _month))
{
_month--;
if (_month == 0)
{
_month == 12;
_year--;
}
_day += GetMonthDay(_year, _month);
}
}
return *this;
}
Date Date:: operator-(int day)
{
Date tmp = *this;
tmp -= day;
return tmp;
}
//前置++,返回++后的值
Date& Date::operator++()
{
*this+=1;
return *this;
}
//后置++,返回++前的值
Date Date::operator++(int)
{
Date tmp = *this;
*this += 1;
return tmp;
}
//d1-d2
//flag用来返回负值或正值
int Date:: operator-(const Date& d)const
{
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;//说明最后返回要返回一个负值
flag = -1;
}
int n = 0;
while (max != min)
{
++min;
++n;
}
return n * flag;
}
ostream& operator<<(ostream& out, const Date& d)const
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
istream& operator>>(istream& in, Date& d)const
{
cout << "请输入年月日:" << endl;
in>> d._year >> d._month >>d._day ;
return in;
}
下面我们就前置++和后置++进行一下解释,c++在之前提出函数重载概念的时候就说,函数的重载与返回值类型无关,而与参数有关,但细心的同学可以看到,前置++和后置++的参数和函数名都完全相同
此时我们c++祖师爷就提出了在参数内加一个形参int,并且这个形参不需要函数名,只是便于编译器用来区分前置++和后置++
我们还是通过一个函数来引入我们的const成员函数的概念
在如图,我们创建了一个TestDate5函数,我们在函数中实例化了一个const对象d1,并调用了类中的成员函数,但会发现编译器不支持这样的行为。
因为这里存在权限的放大,我们的this指针是一个Dateconst类型的,而(&d1)是const Date类型的,因为this指针是编译器自动生成的,所以我们不好直接操作,c++便提出了const成员函数的概念,即在函数定义后加上const,从而使得权限进行平移,使之能够调用Print函数
下面提出const成员函数的注意事项
当成员函数不会对成员变量进行修改时我们便可以加上const,从而保证函数的正确调用
而且我们可以看出,采用const成员函数的好处,是无论对象是可改还不不可改,都可以调用const成员函数
而对于+=,-=这种改变成员变量的我们是不能够用const的成员函数的
3.取地址及const取地址操作符重载
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
};
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。只有遇到有特殊情况,才需
要重载。