文章目录
- 类的六个默认成员函数
- **构造函数**
- **构造函数的目的**
- **构造函数的特性**
- 析构函数
- 析构函数概念
- 析构函数处理的顺序
- 析构函数清理细节
- 拷贝构造函数
- 拷贝构造函数典型调用场景
- 赋值运算符重载
- 运算符重载
- 赋值运算重载
- 前置++和后置++ 重载
- const成员函数
- 再提权限的问题:
- 取地址及const取地址操作符重载
- 日期类的实现(日期计算):
- 获得某年某月的天数
- 比较两个日期
- 重点实现日期的计算
- 日期加天数
- 日期减日期(重点)
- 流插入和流提取运算符重载
类的六个默认成员函数
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。(对于空类,并不是什么都没有,编译器会自动默认生成以下六个默认成员函数)
接下来,这六个默认成员函数,将通过日期Data类来介绍
构造函数
概念:构造函数是特殊的成员函数,其中函数名与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。(构造函数完成的任务是初始化)
构造函数的目的
默认构造构造是为了解决创建对象,忘记对其对象进行初始化操作,同时麻烦调用Init函数进行初始化
构造函数的特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是不是开辟空间创建对象,而是对对象初始化
特征如下:
- 函数名与类名相同
- 无返回值
- 对象实例化时编译器自动调用对应的构造函数
- 构造函数支持函数重载
class Date
{
public:
//1.无参构造函数
Date()
{
_year = 2024;
_day = 6;
}
//2.带参构造函数
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//调用无参构造函数
Date d2(2024, 3, 6);//调用有参构造函数
d1.Print();
d2.Print();
Date d3();//未调用原型函数(是否有意用变量定义?)
return 0;
}
注意:
对于显式方式生成,同样需要遵守上述特征或者规则,关于Date d3(void)
报错,因为编译器很难区分.这里是对象实例化调用无参构造函数还是函数声明,为了避免混淆这两种情况,要求对象实例化调用无参构造函数,不允许添加括号。
同时对于无参构造和有参构造,无参构成采用函数内部设置好的数值,而有参构造则采用外部实参数值。对于这两个性质,可以使用缺省参数将这两个归并在一起,形成一个全缺省的构造函数。不建议全缺省构造函数和无参构造函数同时出现,语法上允许这种行为,但是调用使用过程,会存在歧义,编译器无法区分(有多种初始化方式,条件允许,实现一个全缺省最好用,比较灵活控制参数)
特征:如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义构造函数,编译器将不再生成。
如果Date类中构造函数注释后,代码可以通过编译,编译器自动生成了一个无参的默认构造函数。但是定义有参构造函数(没有无参),对象实例化调用需求调用无参的。因为有显式构造,编译器不会默认自动生成无参构造,只存在有参构造,对此没有合适的默认构造函数可用,会报错error C2512: “Date”: 没有合适的默认构造函数可用
class Date
{
public:
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
Date(int year, int mont3, int day)//有参构造
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//调用无参的,存在有参构造,编译器不会默认生成,但是这个有参构造,d1调不动。
d1.Print();
return 0;
}
特性:无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。无参构造函数、全缺省构造函数、我们没有写编译器默认生成的构成函数,都可以认为是默认构造函数(对于这三个默认构造函数,使用时只调用其中一个)
问题:从编译后结果来看,无论是显式的默认构造函数还是编译器默认生成无参构造,对于内置类型的初始化都是随机值,从结果上来说,确实完成了每个对象的初始化,但是没有多大意义,对此编译器生成的默认构造函数并没有用的?
class Time
{
public:
Time()
{
cout <<"Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
//内置类型
int _year;
int _month;
int _day;
//自定义类型
Time _t;
};
int main()
{
Date d1;
}
C/C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型(int/char/double ),自定义类型就是自己通过关键字定义的类型(struct /class/union)。
结论:
- 对内置类型不做处理
- 对自定义类型的成员,会去调用他们的默认构造(无参构造函数、全缺省构造函数、我们没有写编译器默认生成的构成函数)
对此,为什么不对内置类型做处理了呢?
*这里语言设计过程中遗留下来问题,在C++11中对于内置类型是否处理有了争执,当然内置类型不处理也可能有它的原因,对此C++11还是保持对内置类型不处理的态度,但是打了补丁,即是:内置类型成员变量在类中声明事可以给缺省值
问题:有了构造函数,我们就什么事都不用做了吗?
答:无论是自己写的还是编译器提供的,一般建议是保证每个类都有提供一个默认构造函数,如果类中还含有自定义类型成员,可以使用特性,自定义类型会调用他们的默认构造,从而完成初始化。
但是无论是内置类型或者是自定义类型,数据都是需要我们自己处理,只不过是间接和直接而已(套娃:所谓的自定义类型不过是包含内置类型,其中可能还有自定义类型,但是自定义类型最后一定是内置类型,是内置类型都需要人去设置处理)。但是还有很有价值的,比如在MyQueue
里面定义 stack s1
和stack s2
,这里会调用默认构造,完成对象s1
、s2
的初始化。
析构函数
析构函数概念
与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象的销毁时由编译器完成的,析构函数的主要任务是在对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数是特殊的成员函数,其特征如下:
- 析构函数名是在类名前加上字符~
- 无参数无返回值类型,导致了析构函数不支持重载函数
- 一个类只能有一个析构函数。若未显式定义,系统会在自动生成默认的析构函数。注意:析构函数不能重载
- 对象生命周期结束时,C++编译系统自动调用析构函数
首先,需要搞清楚,构造函数完成的是初始化,而析构函数完成的是清除资源(free),防止出现内存泄漏。这里的资源一般是指动态开辟的资源,同时如果没有析构函数处理,单纯只对对象进行开辟和销毁,没有考虑对象内部申请的动态空间,导致内存泄漏。(对象是存储在栈帧上,是由系统进行处理的,也称为自动变量)
通过个例子方便理解,对此我们需要先在创建函数中定义个类对象与显式析构函数,当函数结束后,更准确地说应该是当类对象出了作用域,生命周期销毁后,打印析构函数中数据,检查是否会自动调用析构函数
对此可以推断出,上述是正确的。这里涉及到了隐含this指针,对此调用析构函数中this指针存储对象的地址。
先谈下析构函数对内置类型和自定义类型的处理方式(同样以代码的方式展示)
结论:
- 内置类型不处理
- 自定义类型成员,调用对应的析构函数
析构函数处理的顺序
在上面打印地址中,不知道你们有没有注意到,打印顺序的问题,这里跟栈的特性是有关系的。栈的特点是先进后出,对此牢记两点。【先定义的,先构造】【后定义的,先析构】,这里两点都符合栈的特殊结构
class Date
{
public:
Date(int year=1)
{
_year = year;
}
~Date()
{
cout << "~Date()->" <<_year<< endl;
}
private:
int _year;
int _month;
int _day;
};
Date d5(5);//全局对象
static Date d6(6);//全局对象
void func()
{
Date d3(3);//局部变量
static Date d4(4);//局部的静态
}
int main()
{
Date d1(1);//局部变量
Date d2(2);//局部变量
func();
return 0;
}
结论:局部对象(后定义先析构)->局部的静态->全局对象(后定义先析构)
析构函数清理细节
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
对于默认析构函数内置类型不处理的方式,我们可以采用显式析构函数,里面的逻辑是自己设计的,可以对内置类型进行操作。但是这样子没有价值,内置类型不需要进行资源清除,同时将内置类型全部设置为0,同样没有完成清除的任务,对此在程序结束后,系统会自动回收内置类型的空间,不需要我们多此一举
d对象的销毁时,要将其内部包含的Time类的_t对象销毁,但是这里不是直接调用Time类的析构函数,因为实际要释放的是Date类对象,对此调用Date类对象对应的析构函数(编译器默认生成的析构函数),目的是在其内部调用Time。(没有直接调用Time类析构函数,通过Date类中析构函数间接调用)
注意:
- 内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收
- 创建哪个类的对象,则调用该类的析构函数,销毁那个类的对象,则调用该类的析构函数
- 如果类中没有申请资源(一般指堆上资源),析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date;有资源申请时,一定要写,否则会造成内存泄漏,比如Stack类(不是每一个类都需要析构)
拷贝构造函数
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时,由编译器自动调用
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// Date(const Date& d) // 正确写法
Date(const Date d) // 错误写法:编译报错,会引发无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
拷贝构造函数的特性:
- 拷贝构造函数是构造函数的一个重载形式,同类型对象进行初始化
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用(编译器可能会强制检查)
首先,先解决几个问题!
- 拷贝构造函数为什么只有一个参数?
- 为什么传值会引发无穷递归调用?可以写一个返回条件控制吗?不可以是指针接收吗?
- 为什么拷贝构造参数部分前面需要加const修饰?
第一个问题:
答:这里只需要传个需要拷贝对象参数就行,因为存在隐含this指针,将调用对象的地址传进来,编译器会自动处理
第二个问题:
答:关于这点,通过函数栈帧章节,得知如果是传值去调用拷贝构造函数,需要先形参开辟一块空间去拷贝一份实参数据。对此传值需要类对象的拷贝,就需要调用拷贝构造函数,要调用拷贝构造函数就需要先传值,这里就是套娃一样。
如写一个返回条件,这里压根就进不了拷贝构造函数,返回条件都用不上
指针可以,但是指针不适合这里。使用引用,就是给实参取别名,跟指向对象占用一块内存空间,对此就不需要拷贝一份数据,去调用拷贝函数
第三个问题:
保证被拷贝对象不会被修改,可以及时地报错检查是否位置放反了。同时如果拷贝构造传的是const修饰的变量,如果拷贝构造函数参数部分没用const修饰,就会造成权限放大(常引用章节有所涉及)
若未显示定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按照内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝(值拷贝)
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time(const Time& t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
cout << "Time::Time(const Time&)" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d1;
// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
Date d2(d1);
return 0;
}
这里也是需要注意的一点,拷贝构造是构造函数的一种特殊形式,如果存在拷贝构造,编译器不会自动生成默认构造函数,对此可以(函数名=default;)强制编译器生成。(大多出现在类中类)
在编译器生成的默认拷贝构造函数的结论:
- 内置类型按照字节方式直接拷贝
- 自定义类型是调用其他拷贝构造函数完成拷贝
对此,编译器默认生成的默认拷贝构造,本身可以实现内置类型按照字节方式直接拷贝,那么自己是否还需要实现显式拷贝构造吗?当然是在不同场景下,有不同场景的处理办法。
接下来如果继续使用浅拷贝程序就会崩溃掉,就需要使用深拷贝解决。因为这里两个对象调用析构函数,对同一块空间进行free,重复free会报错
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType *_array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
对于以上问题,这里可以对array使用深拷贝处理,开辟一块等大的空间,更换指向。对此解决两个对象指向同一块空间的问题,在生命周期结束时,会自动调用对应析构函数释放资源(数据拷贝到新空间,将指向转为指向新空间)
Stack(const Stack& st)
{
_array = (DataType*)malloc(st._capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
memcpy(_array, st._array, st._size * sizeof(DataType));//要记得把原来的数据拷贝过去
_size = st._size;
_capacity = st._capacity;
}
重点:
类中没有涉及资源申请,拷贝构造是否写都是可以
类中一旦涉及资源申请,拷贝构造一定要写,否则就是浅拷贝
拷贝构造函数典型调用场景
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
class Date
{
public:
Date(int year, int minute, int day)
{
cout << "Date(int,int,int):" << this << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d):" << this << endl;
}
~Date()
{
cout << "~Date():" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
Date Test(Date d)
{
Date temp(d);
return temp;
}
int main()
{
Date d1(2022,1,13);
Test(d1);
return 0;
}
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
赋值运算符重载
运算符重载
如果我们需要比较两个日期,那么就需要实现一个函数,但是不好的地方就是对函数名取名字(很依赖写代码人的素养)
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数(不用我们取名字),也是具其返回值类型,函数名字以及参数列表,其返回值类型与参数列表于普通的函数类似
- operator运算符重载(针对自定义类型)对于运算符的行为重新定义控制,跟函数重载不是同一个东西
- 函数名名字:关键字operator + 需要重载的运算符符号
- 函数原型:返回值 operator操作符(参数列表)
使用该函数注意点:
- 不能通过连接其他符号来创建新的操作符:比如operator@(需要是C/C++语法中存在)
- 重载操作符必须有一个类类型参数(不能去重载运算符改变内置类型的行为)
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少一个,因为成员函数的第一个参数为隐藏的this指针
.* :: sizeof ?: .
注意以上运算符不能重载。这个经常在笔试选择题中出现(注意第一个不是, 是可以重载的)- 并不是运算符都是需要重载的,需要看是否有存在的意义,参数部分需要对应顺序
还是通过样例来理解吧!
class Date
{
public:
Date(int year = 2024, int month = 3, int day = 9)
{
_year = year;
_month = month;
_day = day;
}
//private:
int _year;
int _month;
int _day;
};
bool operator==(const Date& x, const Date& y)
{
return x._year == y._year
&& x._month == y._month
&& x._day == y._day;
}
bool operator<(const Date& x, const Date& y)
{
if (x._year < y._year)
{
return true;
}
else if (x._year == y._year)
{
if (x._month < y._month)
{
return true;
}
else if (x._month == y._month)
{
return x._day < y._day;
}
}
return false;
};
祖师爷设置运算符重载的长期目标是自定义类型也可也使用运算符,同时这里编译器可以调用这两个对象,是原因存在this指针。但是可以更简单就是下面的写法(效果是等价的,同时注意优先级的问题)
问题:这里为什么可以访问类内成员?因为注释掉了private才能访问到,如果类外面不能随便访问成员,有什么办法可以解决呢?
答:有两种方式
- 在类中提供Get函数 int Getname(){return _name};
- 在类里面定义该函数,就可以使用类内成员
这里采用第二种方式
在类里面定义该函数,这样子该函数有隐含this指针,只需要传一个参数就行。就可以这样子写d1.operator<(d2)==operator(const this*d1,d2)
,同时在类内部定义就可以使用private去保护成员变量,完成了封装
注意:关于这两种写法都是可以的,编译器知道会调用这个函数。并且第一种写法不会转为第二种写法再调用,而是直接调用对应的函数,中间步骤省略。
赋值运算重载
赋值运算符重载格式:
- 参数类型:const typename &,传递引用可以提高传参效率
- 返回值类型:typename&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this:要复合连续赋值的含义
问题:类中实现显式的赋值运算符重载是这样的,那么如果注释该函数是否可以赋值成功呢?
void operator=(const Date& d)
{
_year=d._year;
_month=d._month;
_day=d._day;
}
从结果上,编译器默认生成的赋值运算符重载可以完成对内置类型的处理,这种处理方式跟拷贝构造很是类似,那么现在是考验大家的时候到了。
int main()
{
Date d1(2024, 1, 23);
Date d2=(2024, 2, 28);
Date d3 = d1;
return 0;
}
问题:这里Date d3 = d1;
是拷贝构造还是赋值运算符重载?
答案:属于拷贝构造
重要结论(d3不是已经存在的对象):
拷贝构造:同类型一个存在的对象,进行初始化要创建对象
赋值运算符重载:已经存在的对象,一个拷贝赋值给另一个
同时关于连续赋值的问题
int main()
{
Date d1(2024, 1, 23);
Date d2(2024, 2, 28);
Date d3;
int i; int j = 10;
i = j = 20;
d3 = d2 = d1;
return 0;
}
这里内置类型连续赋值是从右到左,这里是先处理j=20
这个表达式,之后返回一个临时变量存储返回值,再跟跟i赋值。
那么自定义类型连续赋值,先处理d2=d1;这里就会调用赋值运算符重载。如果是连续赋值的话,这里返回值需要改为Date,并且返回对象*this
Date operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
但是这个写法不好,因为返回值值先存储到寄存器中,传值不会返回对象本身,回返回他的拷贝,如果是同类的话,就需要调用拷贝构造,太浪费
那么我们可以使用引用(Date&)做返回值,直接返回d2本身,不需要拷贝。对此这样说明了有些地方,引用可以,但是指针不行的地方
当然可能会存在一种情况,自己给自己赋值(一般人都不这么干,主要是白干),对此一般会加给判断语句
Date& operator=(const Date& d)
{
if(this!=&d)
{
_year=d._year;
_month=d._month;
_day=d._day;
}
return *this;
}
特性:赋值运算符只能重载成类的成员函数不能重载成全局函数
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
Date& operator=(Date& left, const Date& right)
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}
//编译失败:
//error C2801: “operator =”必须是非静态成员
赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数。同时赋值运算符重载跟拷贝构造类似,如果不显式实现,编译器会生成一个默认的赋值运算符重载。此时用户再类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突,故而赋值运算符只能是类的成员函数
特性:用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝
在编译器生成默认赋值运算符重载的结论(跟拷贝构造类似):
- 内置类型成员变量是直接赋值的
- 自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值
既然编译器生成的默认赋值运算符重载已经可以完成字节序的值拷贝,还需要自己实现吗?这个问题跟拷贝构造函数那里的问题是一样的(对于像日期类这样的类没有必要)
接下来如果继续使用浅拷贝程序就会崩溃掉,就需要使用深拷贝解决。因为这里两个对象调用同一个函数,对同一块空间进行free,重复free会报错
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType *_array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2;
s2 = s1;
return 0;
}
总结:
如果类中未涉及到资源管理,赋值运算符是否实现都是可以的
如果类中涉及到资源管理,赋值运算符则必须实现
前置++和后置++ 重载
//++d1
Date operator++();
//d1++
Date operator++();
前置++和后置++ 都这样子写,编译器是无法区分的,对此需要特殊处理(解决语法逻辑不自洽,自相矛盾)–>++operator可以,但是C++给了其他的解决方法
前置++和后置++都是属于一元运算符,为了让前置++与后置++形成能正确重载。C++规定:后置++重载时,多增加一个int类型的参数(用来完成重载,没有实际意义),但是调用函数时该参数不用传参,编译器会自动传递。
//前置++
Date& operator++()
{
_day += 1;
return *this;
}
//后置++
Date operator++(int)
{
Date temp(*this);
_day += 1;
return temp;
}
const成员函数
将const修饰的"成员函数"称之为const成员函数,const修饰类成员函数,实际修饰改成员隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改
对此先提出几个问题:
- cosnt对象可以调用非const成员函数吗?
- 非const对象可以调用const成员函数吗?
- const成员函数内可以调用其他的非const成员函数吗?
- 非const成员函数内可以调用其他的const成员函数吗?
接下来,带着我们的问题进行。
int main()
{
Date d1(2024, 1, 31);
d1.Print();
const Date d2(2024, 3, 31);
d2.Print();
return 0;
}
这里原因很显然是d2的权限被放大了(权限可以缩小,但是不能放大)
对于我们需要修饰this指向的内容,但是规则是this中形参和实参的位置不允许写,那么怎么修改呢?祖师爷给了一个办法,在函数定义地方加个const。void fname() const
,至于为什么不是 const void fname()
还是那一句,我们是语法的学习者。这样处理完了之后,对于const对象和非const对象都可以调用该函数。
问题:全部函数加上const不就行了吗?干嘛这么麻烦。
总结:
- 成员变量,如果是一个对成员变量只进行读访问的函数->建议加const,这样const对象和非const对象都可以使用(比如print函数,只需要读权限)
- 成员变量,如果是一个对成员变量只进行读写访问的函数->不能加const,否则不能修改成员变量(需要读写权限)
问题:那么流插入和流提取能不能加上const修饰呢?
这里就需要搞清楚const修饰谁,这里流插入和流提取运算符重载函数,是在全局实现的,不是在类中实现,没有隐含的this指针,而const成员函数是修饰this指针的
再提权限的问题:
const int i=0;
int j=i;
int& r=i;//报错
const int*p1=&i1;
int *p2=p1//报错
总结;
是否可以完成拷贝,需要对象的改变是否影响被拷贝对象。关于报错的地址,是权限放大(指针和引用赋值才存在权限放大)
对于上面的几个问题的答案:
- cosnt对象可以调用非const成员函数吗?不可以,权限放大
- 非const对象可以调用const成员函数吗?可以,权限缩小
- const成员函数内可以调用其他的非const成员函数吗?不可以,权限放大
- 非const成员函数内可以调用其他的const成员函数吗?可以,权限缩小
取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义,编译器会默认生成的
这两个运算符一般不需要重载,使用编译器生成的默认取地址重载即可,只有特殊情况,才需要重载,比如想让别人获得指定的内容
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
int a = 0;
const int b = 10;
cout << &a << endl;
cout << &b << endl;
return 0;
}
特殊情况:
class Date
{
public:
Date* operator&()
{
return null;
}
const Date* operator&() const
{
return (const Date*)0xeeffee;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
对于返回地址具有选择性,可以指定返回空或者返回一个像模像样的地址(传了假地址都很难发现,写bug小妙招)
对此我们来实现天数的加法
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date& operator++()
{
_day += 1;
return *this;
}
Date operator++(int)
{
Date temp(*this);
_day += 1;
return temp;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
Date d1(2022, 1, 13);
d = d1++; // d: 2022,1,13 d1:2022,1,14
d = ++d1; // d: 2022,1,15 d1:2022,1,15
return 0;
}
结论:
前置++:返回+1之后的结果
注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
后置++:
注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1,而temp是临时对象,出了作用域生命周期结束,因此只能以值的方式返回,不能返回引用
(这样说明了,虽然this指针不能在形参和实现显式写,但是在函数里面可以使用,因为是有需求的)
日期类的实现(日期计算):
这个真的是一个大问题,对此我们需要将他不断分解为小问题解决!!!这里同样涉及到很多知识点。
如果你想让这个函数是内敛,可以在类里面定义的函数,默认是内敛,内敛不支持声明和定义分离。
获得某年某月的天数
关于计算日期,最频繁使用的就是某年某月的天数,对此可以单独实现一个函数获得该天数,但是关于表示年月日的成员对象都在日期类中封装起来,类外部不能随便访问类成员,只能在类中实现GetMonthDay函数,在通过return将获得的天数返回。
实现逻辑(涉及到很多历史发展背景,这里局限于现代的日期计算)
int Date::GetMonthDay(int year, int month) const
{
assert(month > 0 && month < 13);//保证月数合法
static int montharr[13] = { 0,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 montharr[month];
}
实现该接口:
- 这里由于自转和公转问题,当是闰年时,二月的天数加一
- 还有一些细节上的问题(但是CPU跑太快,没啥影响),
static int montharr
属于静态变量,只能定义一次,对此频繁调用时,不用多次定义。在判断语句中,可以将位置进行调正,这里跟&&短路知识点有关,如果前面是假,不同接下去判断,整个表达式都为假
比较两个日期
这里需要涉及到运算符重载,但是这里有个小技巧,只需要实现大于等于或者小于等于的函数就可以了。
下列是需要实现的函数声明:
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;
按照上面的小技巧,这边主要具体实现小于等于,之后大于等于取反就行了
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);//这里会调用的
}
bool Date::operator<(const Date& d) const
{
if (_year < d._year)
return true;
else if (_year == d._year && _month < d._month)
return true;
else if (_year == d._year && _month == d._month && _day < d._day)
return true;
else
return false;
}
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);
}
重点实现日期的计算
日期加天数
需要使用持续进位原则
Date& Date::operator+=(int 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 temp(*this);//拷贝构造
temp += day;
return temp;
}
实现该接口时:
- 当日期加完天数后,通过日期的规则,需要按照进位原则,对年月日数据进行调正
- 在实现operator+=(+),都可以间接实现operator+(+=)
- 这里operator+=使用引用返回,提高了效率和避免传值返回中的拷贝过程
- operator+这里不能使用引用返回,这里是创建了一个临时变量,调用完会销毁
- 那么是先实现+=,再间接实现+呢?先实现+,再间接实现operator+=
这里是推荐先实现operator+=,再间接实现operator+
理由如下:
这里不能交叉着比,需要横向对比。这里实现operator+是等价的,但是实现operator+=,左边需要复用+operator,对比之下就多了一次拷贝构造Date tmp=*this
;(这里为什么是拷贝构造,而不是赋值运算符重载,在赋值运算符重载有介绍)
这里就不详细介绍关于operator-和operator-=,跟operator+和operator+有异曲同工之处,具体在Date.cpp看看。
日期减日期(重点)
需要使用持续借位原则,如果天数为0,需要得到上月的天数
这里有两个办法(operator-,即使运算符重载也是函数重载)
第一个方法:不断++直到等于大的那个年份(方便,但效率有点低)
int Date::operator-(const Date& d)
{
int flag = 1;
Date max = *this;
Date min = d;
if (*this < d)
{
int flag = -1;//判断两个天数相差
max = d;
min = *this;
}
int n = 0;
while (min != max)
{
++min;//这里会调用operato++()
++n;//operator++()
}
return n * flag;
}
*第二个方法:先将两个年份修饰到1月1日,再计算两个年之间有多少个年,如果是平年+365,闰年+366
int Date::operator-(const Date& d) const
{
//不知道哪个操作数大,先假设
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)//假设错了就认错
{
Date max = d;
Date min = *this;
int flag = -1;//用来标记
}
int count =0;
//大的减到1月1日 count++
while (!(max._day == 1 && max._month == 1))
{
--max;
++count;
}
//小的减到1月1日 count--
while (!(min._day == 1 && min._month == 1))
{
--min;
--count;
}
//都减到1月1日了 算差多少年
while (min._year != max._year)
{
if (is_leapyear(min._year))
count += 366;
else
count += 365;
++min._year;
}
return flag * count;
}
流插入和流提取运算符重载
cout和cin的本质是输入和输出流对象,对于<<和>>用于重载的运算符,从图可以得,cout属于ostream类,cin属于istream类,可以自动识别类型。
对于我们可以在日期类中,实现<<和>>重载打印日期和提取日期!
int main()
{
Date d1(2024, 3, 10);
//void operator<<(ostream& out);
//cout<<d1;
d1 << cout;//->d1.operator<<(cout)->operator<<(const Date *d1,cout);
return 0;
}
如果使用运算符重载,隐含的this指针占用第一个参数的位置,Date必须是左操作数,d1<<cout
是不符合我们的习惯的
对此我们可以在类外实现该运算符重载函数,就可以自己设计参数部分的位置。但是又引出了另一个问题:类外不能访问类中的私有成员,如果将私有权限放开,就缺乏安全性,对此C++中有友元,接下来我们会涉及到,这里就使用下,虽然这个全局函数不在类中,但是可以访问私有成员函数
//友元,告诉该类这两个全局函数是我们的朋友,允许使用私有成员(在类中)
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "" << d._month << "" << d._day << "" << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
如果我们需要连续打印cout<<d1<<d2
的话,这里就不合适的,因为这里的结合性是从左往右,cout<<d1
会返回一个临时变量,那么这里运算符重载函数需要通过引用返回了。(C++存在私有的,printf不支持自定义打印,cout本质实现所用类型的打印)
注意:
- 用引用做返回值,应对连续流插入和流提取
- 流提取不是不能对Date进行const修饰,需要通过键盘读取数据存储在成员变量
最后需要注意一点,需要判断输入进去的数据是否有误!
源代码展示:
Date.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1);
bool operator<(const Date& d);
bool operator<=(const Date& d);
bool operator>(const Date& d);
bool operator>=(const Date& d);
bool operator==(const Date& d);
bool operator!=(const Date& d);
// d1 + 100
Date& operator+=(int day);
Date operator+(int day);
// d1 - 100
Date operator-(int day);
Date& operator-=(int day);
// ++d1
Date& operator++();
// 特殊处理:解决语法逻辑不自洽,自相矛盾的问题
// d1++
// 为了跟前置++区分,强行增加一个int形参,够成重载区分
Date operator++(int);
Date operator--(int);
Date& operator--();
// d1 - d2
int operator-(const Date& d);
// 本质就是inline
int GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
static int monthDays[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
// 365 自转 公转 365 5+h
// 366
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
return 29;
}
return monthDays[month];
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
Date.cpp
#include"Date.h"
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
bool Date::operator<(const Date& d)
{
if (_year < d._year)
{
return true;
}
else if (_year == d._year)
{
if (_month < d._month)
{
return true;
}
else if (_month == d._month)
{
if (_day < d._day)
{
return true;
}
}
}
return false;
}
// d1 <= d2
bool Date::operator<=(const Date& d)
{
return *this < d || *this == d;
}
bool Date::operator>(const Date& d)
{
return !(*this <= d);
}
bool Date::operator>=(const Date& d)
{
return !(*this < d);
}
bool Date::operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
bool Date::operator!=(const Date& d)
{
return !(*this == d);
}
// d1 += 10
Date& Date::operator+=(int 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)
{
//Date tmp(*this);
Date tmp = *this; //
tmp += day;
return tmp;
}
// d1 + 10
//Date Date::operator+(int day)
//{
// //Date tmp(*this);
// Date tmp = *this; //
//
// 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;
//}
//
d1 += 100
//Date& Date::operator+=(int day)
//{
// *this = *this + day;
//
// return *this;
//}
Date Date::operator-(int day)
{
Date tmp = *this;
tmp -= day;
return tmp;
}
Date& Date::operator-=(int day)
{
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
// ++d ->d.operator++()
Date& Date::operator++()
{
*this += 1;
return *this;
}
// d++ ->d.operator++(0)
Date Date::operator++(int)
{
Date tmp = *this;
*this += 1;
return tmp;
}
// d1 - d2
int Date::operator-(const Date& d)
{
int flag = 1;
Date max = *this;
Date min = d;
if (*this < d)
{
int flag = -1;
max = d;
min = *this;
}
int n = 0;
while (min != max)
{
++min;
++n;
}
return n * flag;
}
test.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include "Date.h"
#include"Date.h"
int main()
{
Date d1(2024, 1, 29);
Date d2 = d1 + 20;
d2.Print();
d1.Print();
d2 -= 20;
d2.Print();
d1 += 30000;
d1.Print();
++d1;
d1.operator++();
d1.Print();
d1++;
d1.operator++(10);
d1.Print();
/*bool ret = false;
if (ret)
{
d1.Print();
}*/
Date d4(2024, 1, 29);
Date d5(2024, 8, 1);
cout << d5 - d4 << endl;
return 0;
}
谢谢大家的观看,这里是个人笔记,希望对你学习C++有帮助。