目录
1.类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数![]()
建议:![]()
2. 构造函数
2.1.概念
对于以下的日期类:
对于Date类,可以通过SetDate公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?构造函数是一个 特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且 在对象的生命周期内只调用一次。
2.2.特性
构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务 并不是开空间创建对象,而是初始化对象。其特征如下:1. 函数名与类名相同。2. 无返回值。3. 对象实例化时编译器自动调用对应的构造函数。4. 构造函数可以重载。![]()
注:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
5.无参的构造函数和全缺省的构造函数都称为默认构造函数(默认构造函数还有一个是编译器自动生成的默认构造函数,下面会讲)(默认函数简单说是不用传参就可以调用的),并且默认构造函数只能有一个
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
注:
1.无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
2.上面data无参的构造函数和data全缺省的构造函数在语法上构成重载可以同时存在,编译阶段没有问题,但是在使用时即代码运行时,存在调用歧义。
3.我们一般使用全缺省构造函数,因为全缺省构造函数不传参就是默认初始化,完成无参构造函数功能,传参的话就完成带参构造函数功能,同时也可以传一部分参数,其余参数就使用默认值,用法十分灵活
6.如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
问题:根据打印的结果来看,自动生成的默认构造函数好像什么事也没做,那它有什么用呢?
解答:
内置类型/基本类型:int/char/double/指针...
自定义类型:class/struct去定义类型对象
默认生成构造函数对于内置类型成员变量不做处理,对于自定义类型成员变量才会处理,如下图所示
注:上面Data类自动生成的默认构造函数要对自定义类型成员变量进行初始化,初始化的过程是再调用A类的默认构造函数对其进行初始化
那么什么时候会用自动生成的默认构造函数呢?如下图所示,利用自动生成的默认构造函数对两个栈进行初始化
注:如果Stack里面也不手动写默认构造函数而使用自动生成的默认构造函数,那么MyQueue类里面自动生成的默认构造函数会调用Stack类里面的默认构造函数,Stack里面也是自动生成的默认构造函数,对于其里面的内置类型不会初始化,内置类型仍是随机值,因此做了无用功。
总结:如果一个类中的成员全是自定义类型,我们就可以用自动生成的默认构造函数(如上图MyQueue类)。如果有内置类型的成员,或者需要显示传参初始化,那么都要自己实现构造函数
一旦类里面有用户显式定义的构造函数(显式定义的构造函数也就是自己写的构造函数),编译器就不会再自动生成默认构造函数。而如果此时里面显式定义的构造函数不是默认构造函数,那么编译就会出错。如下图所示,MyQueue类尝试调用Stack里面的默认构造函数,而Stack里面没有默认构造函数,会报错。这种情况需要我们使用初始化列表进行显式处理,初始化列表后面会介绍
7.针对编译器自动生成的默认构造函数无法对内置类型成员变量初始化的问题(c++早期设计的一个败笔),C++11打了一个补丁,可以给内置成员类型缺省值,仅供默认构造函数使用,如下图所示。
注意:int _size = 0不是初始化,此处是声明,是不开辟空间的
结论:
一般情况一个c++类,都要自己写默认构造函数。一般只有少数情况可以让编译器自动生成默认构造函数:
(1)类里面成员都是自定义类型成员,并且这些成员都提供了默认构造函数
(2)如果类里面还有内置类型成员,这些内置类型成员声明时给了缺省值(如上图所示)
类似下面场景就不需要自己写,默认生成就够用了
3.析构函数
3.1.概念
前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的?析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而 对象在销毁时会自动调用析构函数,完成类的一些资源清理工作
3.2.特性
析构函数是特殊的成员函数。其特征如下:1. 析构函数名是在类名前加上字符 ~。2. 无参数无返回值(不能构成函数重载)。3. 对象生命周期结束时,C++编译系统系统自动调用析构函数。有些类我们需要写析构函数,有些类我们不需要写析构函数,如下图所示。Data类不需要写析构函数,里面没有资源需要清理,stack类需要析构函数,里面malloc开辟的空间需要清理![]()
4.析构顺序和构造顺序是反的,构造是代码从上往下依次构造,析构是代码从下往上依次析构,如下图所示
5. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:系统自动生成的默认析构函数与系统自动生成的默认重构函数相似,也是内置类型不做处理,自定义类型会去调用它的析构函数![]()
4. 拷贝构造函数
4.1.概念
在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。
那在创建对象时,可否创建一个与一个对象一模一样的新对象呢?拷贝构造函数: 只有单个形参,该形参是对本 类型对象的引用(一般常用const修饰),在用 已存在的类型对象创建新对象时由编译器自动调用
4.2.特征
拷贝构造函数也是特殊的成员函数,其特征如下:1. 拷贝构造函数是构造函数的一个重载形式2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用为什么传值方式会引发无穷递归调用:我们先理解一个知识,c++中自定义类型传值传参是要调用拷贝构造的,如下图所示。注:下面的代码中,因为Func(const Date d)中d是拷贝过来的,因此前面的const可以不加![]()
如果我们使用引用传参,因为此时d是d1的别名,那么就不会进行拷贝构造了,如下图所示。
注:下面的代码中,因为Date(const Date& d)中d是d1的别名,而Data在类里面,private不会进行保护,因此如果修改了d的成员变量会影响d1,前面最好加上const(如果不加const,一些编译器会报错)
![]()
这样传值传参是一个拷贝构造,而拷贝构造又要先传参,这样就无限循环下去了,如下图所示。因此拷贝构造函数传参必须使用引用传参
3.类似栈这种类,他的拷贝构造不能进行浅拷贝(也就是对值进行拷贝),因为如果简单的对值进行拷贝,那么st1和st2就指向了同一块内存空间了,此时操作st1后对st2进行操作,有可能就会把对st1操作的结果覆盖掉,并且系统到后面会崩(因为最后析构的时候会对这同一块地址空间进行前后两次清理,第二次清理的时候系统崩掉),这种情况只能进行深拷贝(深拷贝的实现后面会讲)
这就是为什么c++规定,自定义类型对象,拷贝构造函数形参传值传参或对象用另一个对象赋值初始化要使用拷贝构造函数,因为如果遇见需要深拷贝的地方,例如栈,我们不调用拷贝构造函数而只是像c语言一样简单的赋值,那么最后像析构等地方会出问题
4.若未显示定义,系统生成默认的拷贝构造函数。内置类型的成员会完成值拷贝(浅拷贝),自定义类型的成员,去调用这个成员的拷贝构造
对于内置类型的成员,系统生成默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。 如下图一所示,默认生成的拷贝构造函数进行了浅拷贝。下图二所示的情况,应该自己写函数进行深拷贝,如果不写,默认生成函数进行了浅拷贝,此时两个指针都指向了同一块内存空间的地址,此时操作st1后对st2进行操作,有可能就会把对st1操作的结果覆盖掉,并且析构的时候会对该空间析构两次,第二次进行析构的时候就会报错,解决方案为自己实现拷贝构造函数来完成深拷贝(深拷贝后面会单独讲解)
注:数组属于内置类型,浅拷贝的时候,数组里面的值也会被拷贝过去,如下图所示
对于自定义类型的成员,系统生成默认的拷贝构造函数会去调用这个自定义类型成员的拷贝构造函数,如下图所示。其中系统生成的默认拷贝构造函数对_size内置成员变量直接进行浅拷贝,对_st1和_st2自定义成员变量调用其对应的拷贝构造函数。
注:上面代码其实是有问题的,stack类中的拷贝构造函数不能使用自动生成的拷贝构造函数来进行浅拷贝,应该自己写拷贝构造函数实现深拷贝功能
5.总结
1.只有内置成员的类,并且是data这种一般的类,系统自动生成拷贝构造进行浅拷贝就够用了。
2.只有内置成员的类,并且是stack这种需要进行深拷贝的类,就需要自己写拷贝构造函数实现深拷贝功能。
3.有自定义类型成员的类(像queue这种类),使用系统自动生成的拷贝构造函数即可。
5.赋值重载函数
5.1.运算符重载函数
内置类型的变量是可以直接使用各种运算符(比较,自加减等)的,自定义类型的变量不能直接使用各种运算符。为了自定义类型可以使用各种运算符,c++给出了一套运算符重载的规则,如下图所示
注:
1.运算符重载函数的函数名:operate是一个关键字,operate后面跟运算符就可以重载这个运算符的行为,得到一个运算符重载函数的函数名
2.运算符重载函数的形参:运算符重载函数的形参个数是由运算符的操作数决定的,运算符有几个操作数那么形参个数就为几
3.运算符重载函数的返回值:运算符运算后的结果是什么,返回值就是什么
4.下面这两种调用operate==函数的方式都是正确的,使用第二种方法if(d1==d2)进行调用,编译器遇到会处理成对应重载运算符调用 if (operator==(d1, d2))
![]()
![]()
5.运算符重载函数如果在类的外面定义,如果成员变量设置成私有,在类的外面是不能直接访问类中私有的成员变量的,如下图所示。
所以,我们应该将运算符重载函数放在类里面,变成类的成员函数,但是如果直接将operator==函数拷贝到类里面,如下图所示,编译器会报运算符函数参数过多的错误。报这个错误是因为类的成员参数其实还有一个隐藏的this参数,所以会报参数过多错误
因此如果将运算符重载函数放在类里面,我们应该少写一个参数,并且显式调用该成员函数的时候也需要改变成if (d1.operator==(d2)),如果隐式调用if (d1 == d2),编译器会自动处理成对应重载运算符调用 if (d1.operator==(d2)),如下图所示
因为编译器还要处理this指针,所以编译器最终会分别将运算符重载函数的调用和定义处理成下图所示代码
6.因为传值传参会调用拷贝构造,这样效率很低,改进方法是进行引用传参,并使用const修饰(不用const修饰一些编译器会报错),如下图所示
7.如果类里面有运算符重载函数,类外面也有运算符重载函数,这种情况是符合语法结构的,因为不在同一个作用域里面,如下图所示。下面这种情况,其实不管是if (d1.operator==(d2))调用还是if (d1 == d2),都优先调用的是类里面的operator==函数,如果类里面没有才会调用类外面的
8.运算符重载函数实现日期比较下的<运算符代码如下图所示
C++为了增强代码的可读性引入了运算符重载, 运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。函数名字为:关键字 operator后面接需要重载的运算符符号。函数原型: 返回值类型 operator操作符(参数列表)注意:1.不能通过连接其他符号来创建新的操作符:比如operator@2.重载操作符必须有一个类类型或者枚举类型的操作数3.用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义4.作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参5. .* 、 :: 、 sizeof 、 ?: 、 . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
5.2.赋值运算符重载
拷贝构造:一个存在的对象去初始化另一个要创建的对象
赋值重载(复制拷贝):两个已经存在对象之间赋值
实现代码如下图所示:
注:
1.下面图一赋值重载函数的实现是有问题的,因为赋值表达式是有返回值的如下图二所示,返回值应该为赋值符号的左操作数,因此改进代码如下图三所示
2.上面这个代码是进行传值返回,会调用拷贝构造函数,代价会比较大。这里出了赋值重载函数的作用域,this指针指向的对象还在,所以可以使用引用返回,如下图所示
注:上面的引用返回前面不能用const修饰,如下图所示,因为如果用const修饰了,形如(k=j)=i这种代码就会报错,因为先执行k=j返回const修饰的k的引用,再将j赋值给const修饰的k的引用,程序报错。
3.上面代码还有一个问题就是,如果将自己赋值给自己,那么赋值重载函数操作了一整其实做的是无用功,我们可以进行一个判断来进行优化,如下图所示
一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,内置类型的成员会完成字节序的值拷贝,自定义类型的成员,去调用这个成员的赋值运算符重载函数因此,是否写赋值运算符重载函数其实与是否写拷贝构造函数的问题一致,如下所示:1.只有内置成员的类,并且是data这种一般的类,系统自动生成的赋值运算符重载函数就够用了。
2.只有内置成员的类,并且是stack这种需要进行深拷贝的类,就需要自己写赋值运算符重载函数实现深拷贝功能。
3.有自定义类型成员的类(像queue这种类),使用系统自动生成的赋值运算符重载函数即可。
6.日期类的实现
test.cpp文件:
#define _CRT_SECURE_NO_WARNINGS 1
#include "Date.h"
void TestData1()
{
Date d1(2022, 5, 18);
Date d2(2023, 3, 20);
Date d3(2023, 3, 20);
cout << (d1 < d2) << endl;
cout << (d1 > d2) << endl;
cout << (d1 == d3) << endl;
cout << (d2 <= d3) << endl;
cout << (d2 == d3) << endl;
}
void TestData2()
{
Date d1(2022, 5, 18);
Date d2 = d1 + 15;
Date d3;
d3 = d1 + 15;
d2.Print();
d1.Print();
d1 += 15;
d1.Print();
}
void TestData3()
{
Date d1(2022, 5, 18);
Date d2 = d1 - 30;
d2.Print();
d1 -= 30;
d1.Print();
Date d3(2022, 5, 18);
d3 += 1000;
d3.Print();
d3 -= 1000;
d3.Print();
}
void TestData4()
{
Date d1(2022, 5, 18);
d1 -= -100;
d1.Print();
}
void TestData5()
{
Date d1(2022, 5, 18);
Date ret1 = ++d1;
ret1.Print();
d1.Print();
Date ret2 = d1++;
ret2.Print();
d1.Print();
}
void TestData6()
{
Date d1(2022, 5, 18);
Date d2(2020, 2, 4);
cout << (d1 - d2) << endl;
cout << (d2 - d1) << endl;
}
void Func(const Date& d)
{
d.Print(); //编译器转换成: d.Print(&d);
}
void TestData7()
{
Date d1(2022, 5, 18);
d1.Print(); //编译器转换成: d1.Print(&d1);
Func(d1);
(d1 + 100).Print();
}
int main()
{
//TestData1();
//TestData2();
//TestData3();
//TestData4();
//TestData5();
TestData7();
return 0;
}
Date.h文件:
#pragma once
#include <iostream>
#include <assert.h>
using std::cout;
using std::cin;
using std::endl;
class Date
{
public:
bool isLeapYear(int year)
{
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
int GetMonthDay(int year, int month);
Date(int year = 1, int month = 1, int day = 1);
//Date(const Date& d)
//{
// _year = d._year;
// _month = d._month;
// _day = d._day;
//}
//Date& operator=(const Date& d)
// {
// if (this != &d)
// {
// _year = d._year;
// _month = d._month;
// _day = d._day;
// }
// return *this;
//}
void Print() const//形参默认: const Data* const this
{
cout << _year << "-" << _month << "-" << _day << endl;
}
bool operator<(const Date& d) const;
bool operator==(const Date& d) const;
bool operator>(const Date& d) const
{
return !(*this <= d);
}
bool operator>=(const Date& d) const
{
return !(*this < d);
}
bool operator!=(const Date& d) const
{
return !(*this == d);
}
bool operator<=(const Date& d) const
{
return *this < d || *this == d;
}
Date operator+(int day) const;
Date& operator+=(int day);
Date operator-(int day) const;
Date& operator-=(int day);
// ++d1
Date& operator++() // 前置
{
*this += 1;
return *this;
}
// d1++
Date operator++(int) // 后置
{
Date tmp(*this);
*this += 1;
return tmp;
}
//--d1
Date& operator--() // 前置
{
*this -= 1;
return *this;
}
//d1--
Date operator--(int) // 后置
{
Date tmp(*this);
*this -= 1;
return tmp;
}
// d1 - d2
int operator-(const Date& d) const;
private:
int _year;
int _month;
int _day;
};
Date.cpp文件:
#define _CRT_SECURE_NO_WARNINGS 1
#include "Date.h"
int Date::GetMonthDay(int year, int month)
{
assert(year >= 0 && month > 0 && month < 13);
const static int monthDayArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if (month == 2 && isLeapYear(year))
{
return 29;
}
else
{
return monthDayArray[month];
}
}
Date::Date(int year, int month, int day)
{
if (year >= 1 &&
month <= 12 && month >= 1 &&
day >= 1 && day <= GetMonthDay(year, month))
{
_year = year;
_month = month;
_day = day;
}
else
{
cout << "日期非法" << endl;
}
}
bool Date::operator<(const Date& d) const
{
if ((_year < d._year)
|| (_year == d._year && _month < d._month)
|| (_year == d._year && _month == d._month && d._day < d._day))
{
return true;
}
else
{
return false;
}
}
bool Date::operator==(const Date& d) const
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
//+=的复用实现
//Date& Date::operator+=(int day)
//{
// *this = *this + day;
// return *this;
//}
// +的实现
//Date Date::operator+(int day)
//{
// Date ret(*this);
//
// ret._day += day;
// while (ret._day > GetMonthDay(ret._year, ret._month))
// {
// ret._day -= GetMonthDay(ret._year, ret._month);
// ret._month++;
// if (ret._month == 13)
// {
// ++ret._year;
// ret._month = 1;
// }
// }
//
// return ret;
//}
//+的复用实现
Date Date::operator+(int day) const
{
Date ret(*this);
ret += day;
return ret;
}
// +=的实现
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 ret = *this;
ret -= day;
return ret;
}
// -=的实现
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;
}
// d1 - d2
int Date::operator-(const Date& d) const
{
int flag = 1;
Date max = *this;
Date min = d;
if (*this < d)
{
min = *this;
max = d;
flag = -1;
}
int n = 0;
while (min != max)
{
++n;
++min;
}
return n * flag;
}
注:
1.有意义的运算符才会重载
2.写函数时应该多进行相互间的复用
3.代码 cout << d1 < d2 << endl; 是有问题的,<<运算符优先级是很高的,所以我们应该加上括号声明运算顺序:cout << (d1 < d2) << endl
4.运算符重载函数实现>的时候,也可以直接复用<运算符,如下图所示。但注意代码d<*this编译器翻译成d.operator>(*this),会把d传给operator<函数的默认this指针,d是const Date&类型的,不能对d的内容进行修改,而默认的this指针是可以对指针指向的内容进行修改的,需要在operator<函数后面加上const才能正常使用。
5.复用的函数代码段很短,因此使用复用实现的函数定义可以直接写到.h文件中实现内联,因为类里面定义的函数默认就是内联的
inline不支持声明和定义分别放到.h和.cpp,所以成员函数中要成为inline最好直接在类里面定义,类里面定义的默认就是inline
6.因为GetMonthDay函数会被频繁调用,每次调用都会建立一个monthDayArray数组,这样效率不高,我们可以在前面加一个static,第一次在静态区建立数组之后不会销毁,后面再调用该函数会直接去静态区访问该数组
7.运算符重载函数实现+的时候,下面的代码是错误的,因为这里直接对原对象进行了修改,是错误的。应该重新创建一个新对象,将原对象拷贝构造给新对象,然后在新对象的基础上进行运算并返回新对象,这样原对象不会被改变。
这里的返回不能用引用返回,因为出了该函数作用域,创建的新对象会被销毁。
8.拷贝构造用于一个对象去初始化同类型的另一个对象,所以下图代码Date d2 = d1 + 15;和Date d3 = d1;都是调用的拷贝构造(Date d3 = d1和Date d3(d1)作用是完全相同的)。两个已经存在的对象,如下图代码的d1=d2;调用的才是赋值运算符重载函数。
9.前面讲拷贝构造函数和赋值运算符重载函数的时候说过两个函数的形参都要用const进行修饰,原因如下面代码所示,Date d2 = d1 + 15;调用的是拷贝构造函数,d1+15调用加运算符重载返回一个拷贝的临时对象,临时对象具有常性,如果不用const修饰,那么存在权限放大的问题。赋值运算符重载函数也是同样的原因。并且加上const可以防止函数内误写的问题
10.运算符重载函数实现+=的时候,因为返回的*this中this指针指向的类出了函数还在,所以可以使用引用返回。
实现+=的时候可以复用+来实现,如下图一所示 。实现+的时候也可以复用+=来实现,如下图二所示。 如下图三所示,总体来说实现+用+=来复用的方式是更好的,因为这样使用+会拷贝两次,使用+=会拷贝一次, 如果是实现+=用+来复用的方式,实现+=要拷贝三次,实现+要拷贝两次,效率更低
运算符重载函数实现 - 和 -= 同理
重载函数实现 + 要考虑加的天数如果是一个负的天数,对应应该复用 - 来减正的天数。重载函数实现 += 要考虑加等于的天数如果是一个负的天数,对应应该复用-=来减等于正的天数。如果实现+时复用了+=,那么只需要优化+=即可,实现+=时复用+同理
重载函数实现 - 要考虑减的天数如果是一个负的天数,对应应该复用+来加正的天数。重载函数实现 -= 要考虑减等于的天数如果是一个负的天数,对应应该复用+=来加等于正的天数。如果实现-时复用了-=,那么只需要优化-=即可,实现-=时复用-同理
11.到底使用传值返回还是传引用返回,其实可以都先写成传值返回,因为传值返回一定是对的,只不过可能不是高效的。最后我们再判断返回的变量出了函数作用域是否会被销毁,如果没有被销毁,则可以优化为传引用返回
12.前置++和后置++如果正常要用运算符重载函数实现,那么两个函数名和参数是相同的二者无法构成重载,所以c++语法规定了运算符重载函数实现前后置++时,不带参数的是前置++,带整型参数的是后置++(这里编译器约定好的是int类型,使用其他类型编译器会报错)
当编译器遇到前置++如++d2时,会自动转换成d2.operator++()
当编译器遇到后置++如d2++时,会自动转换成d2.operator++(0) 这里括号里为任意整型,这里用0代替
形参类型要写,形参名是可以不写的,如果函数体里面不用传过来的值,那么就可以不写形参名,如下图所示
这里运算符重载函数实现后置++时,形参一定不能使用缺省参数,如果使用缺省参数,当使用前置++时,系统就分不清到底该调用哪个(与构造函数中无参构造函数和全缺省构造函数不能同时存在原因是相同的)
13.日期-日期和日期-天数是构成函数重载的
14.如下图所示,这样调用会报错,是因为这里涉及了一个权限的问题。
函数Print里面默认的this指针是Data* const类型,这样const修饰的是this本身,this本身不能被修改,this指向的内容可以被修改。
void TestData7()函数中d1.Print(); 编译器转换成: d1.Print(&d1); d1是Data类型,因此传参的类型是Data*,Data*传给Data* const因为这里面const修饰的是this本身,所以不涉及权限的放大或缩小,权限相同,没有问题
void Func(const Date& d)函数中d.Print(); 编译器转换成: d.Print(&d); d是const Date&类型,Data类型传参的类型是const Data*,const Data*传给Data* const,因为const Data*指向的内容不能被修改而Data* const指向的内容可以被修改,涉及权限的放大
所以void TestData7()函数中d1.Print();没有问题而void Func(const Date& d)函数中d.Print();会报错,因此这里面void Print()里面默认的形参this指针前面需要加const保护this指针指向的内容(const Data* const this),void Func(const Date& d)函数中d.Print();调用才不会出错。但是this指针是隐含的,我们没有地方去显式的在前面加const
c++给了我们可以在this指针前面加const用来保护this指针指向内容的机会,加上const后变成了const Data* const this。方式是在这个函数后面写一个const,如下图所示。
在函数后面加上一个const编译器会认为加在了this指针前面,用来保护this指针指向内容,结果如const Data* const this所示。
因此写成void Print() const,上面的代码就没有问题了,如下图所示。这样void TestData7()中d1.Print();调用Print函数涉及到了权限的缩小,是可以的,void Func(const Date& d)中d.Print();调用Print函数不涉及权限的变化,也是可以的
建议成员函数中不修改成员变量的成员函数(不使用this指针对类的成员进行修改),最好就在函数后面加上const,这样普通对象和const对象都可以调用
15.如下图所示,代码(d1 + 100).Print();中d1 + 100调用了加运算符重载函数,返回的是一个拷贝的临时对象,临时对象具有常性,如果void Print()后面没有const,是无法接收的,涉及到权限变大的问题,一些编译器会报错。这个样例也说明最好是在void Print()函数后面加上const
16.思考下面问题
7.取地址及const取地址操作符重载
取地址及const取地址操作符重载代码如下图所示:
注:
1.这两个默认成员函数一般不用重新定义 ,编译器默认会生成,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如:想让别人获取到指定的内容或者不想让别人拿到你的真实地址(返回空指针)
2.编译器会根据对象的权限去调用相对应的取地址及const取地址操作符重载函数,如下图所示,取const Data&类型的d的地址会去调用const取地址操作符重载函数,取Data类型的d1的地址会去调用取地址操作符重载函数。
8.流插入运算符和流提取运算符重载
<<: 流插入运算符
>>: 流提取运算符
流插入运算符和流提取运算符的简单介绍:
从图中可以看出,cout和cin其实是全局的对象,包含在iostream头文件中。cin是istream类型的对象,cout是ostream类型的对象
如上图所示,ostream里面已经把所有内置参数类型的流插入运算符进行了重载。istream里面同样对所有内置参数类型的流提取运算符进行了重载。因此前面讲的流插入和流提取运算符可以自动识别类型本质上源自于函数重载
也就是遇见代码 cout<<i 编译器会修改成 cout.operator<<(i)
对于自定义类型的类进行流提取和流插入,我们同样可以自己写流插入运算符和流提取运算符重载
如果我们按如下的方式重载,如下图所示。
代码cout << d1; 这样写本质上是cout.operator<<(d1);ostream类里面不可能有流插入重载d1这种data类的类型,因此这样是错误的。
代码d1 << cout; 这样写本质上是d1.operator<<(cout);Date类里面有我们自己写的流插入重载函数,将cout对象作为形参输入,这样是可行的。
但是这样写是十分别扭的,造成这个结果的原因是我们写成了成员函数,成员函数第一个参数是隐含的this是我们改不了的,这就限制了流插入运算符<<的左值必须是Date类型的对象。所以我们应该在类的外面写,写成全局的,如下图所示
这种写法有两个问题,一个问题是全局的函数定义不能写在.h文件里,因为两个.cpp文件都包含了.h文件,那么该全局的函数被定义了两次,两次定义完全一样,不会构成重载,编译器报错,应该把全局的函数定义放在Date.Cpp文件里,并在.h文件中写上声明。另一个问题是全局的函数无法访问类里面私有的成员变量,这里有两种方式可以解决,
第一种方式如下图所示,利用定义类函数GetYear、GetMonth、GetDay将私有的类成员变量值拿出来。但是c++里面我们一般不使用这种方式。
这里注意cout << d1; 这样写编译器转换成cout.operator<<(d1)编译器会在ostream类里面找,如果类里面找不到会去全局函数里面找,此时编译器当成是operator<<(cout,d1)
第二种方式如下图所示,如果一个类外面的函数想访问类的私有成员变量,那么就将该类外面的函数设置成该类的朋友就可以访问私有成员变量了,设置方法是将类的声明前加一个friend并放在类里面即可,这样该函数叫做该类的友元函数。(类里面的friend+函数声明的部分就相当于该函数的声明了,不用再在.h文件中类的外面再写一遍函数声明了)
上面的代码还有一个问题,如下图一所示,代码cout<<d1<<d2,如果是连续流插入,先进行cout<<d1返回void,然后void<<d2自然会报错,所以我们应该将cout类作为返回值,如下图二所示,这样就可以连续的流插入了
流提取同理,如下图所示,要注意的是流提取函数重载第二个形参Date& d前面不能用const修饰了,因为提取得到的值是要放到d里面的
如下图所示,这里还有一个小问题:如果输入一个非法的日期,这里面没有检查日期是有问题的代码,所以我们应该在流提取函数里面提取到对象d之后也检查一下日期,有问题日期报非法错误即可,这里就不再赘述