目录
日期类的完善:
日期类的前置--
日期类的后置--
日期类的相减
<<流插入运算符重载
编译和链接:
友元声明:
流提取重载:
const成员
取地址重载:
初始化列表:
Date& Date::operator--()
{
*this -= 1;
return *this;
}
Date Date::operator--(int)
{
Date ret(*this);
*this -= 1;
return ret;
}
我们进行分析:
答:前置--与后置--的区别就是前置--是先--后返回,后置--是先返回后--
后置--需要拷贝构造一个对象,让*this完成-=,然后返回拷贝构造的对象
注意:1:尽量使用前置--,因为前置--比后置--少使用了两次拷贝构造(后置--函数体内的拷贝构造和返回类对象时引起的拷贝构造)。
为什么前置--的返回值类型是引用,而后置--不是
答:因为前置--,我们返回的是*this,*this出函数作用域不会被销毁,所以我们可以用引用。
而后置--,我们返回的是在函数体内创建的局部对象ret,出了函数作用域就被释放,假设我们使用了引用,因为属于ret的空间已经被释放,所以我们得到的并不是后置--后的结果。
这两个函数有什么特点?
答:都是运算符重载,彼此又改成函数重载。
运算符重载:使用运算符就会转换成调用我们的运算符重载函数。
拷贝构造有哪两种形式:
Date ret(*this);
Date ret = *this;
答:这两种形式都构成拷贝构造。
我们检验我们写的前置--和后置--是否正确?
假如我们打印的结果是2022,8,10
2022,8,9
我们的函数写的就是正确的。
所以我们的代码写的是正确的。
int Date::operator-(const Date&d)
{
}
我们先写一个架构,这时候我们发现,日期类的相减与我们之前写的一个函数构成函数重载:
函数重载的定义:相同函数名,不同的参数类型参数类型构成函数重载,与函数的返回值类型无关。
我们如何去定义这个函数呢?
答:我们需要解决的问题有两个
1:日期类对象相减的主体思路
2:大的日期-小的日期和小的日期-大的日期的区分。
1:主体思路:我们可以让小的日期一直++,每执行一次++,我们对应的常数就++,直到小的日期和大的日期相等。
2:我们可以采取假设的方法
int Date::operator-(const Date&d)
{
Date max = *this;
Date min = d;
int flag = 1;
int n = 0;
if (*this < d)
{
min = *this;
max = d;
flag = -1;
}
while (max!=min)
{
++n;
++min;
}
return n*(flag);
}
这里就是假设的思想。
我们测试所写的代码是否正确。
假设我们写的代码是正确的话,打印的结果就应该是0.
我们所写的日期类的-是正确的。
还有一个问题:
答:这里不会
1:权限的放大和缩小一般在指针或者引用中出现。
2:这里的赋值本质上是一种拷贝,拷贝的话d的值的变化并不会影响max,所以这里不涉及权限的缩小和放大。
我们用这串代码举出一个权限的放大的例子:
这里也是赋值,为什么会造成权限的缩小。
答:这里虽然是赋值,但不是拷贝,因为我们是用引用来接收的,相当于我们的min是d的别名,我们的d的改变会导致min的改变。
又因为我们的d是只读的,而min的类型是可读可写的,所以权限就放大了。
我们写一些代码来警示自己:
秋招:
明年的九月份就是秋招的时间。
我们计算我们还有多长时间准备秋招:
我们只剩了320多天
我们再计算一下距离提前批的时间
提前批一般在七月份
我们只剩下260天去准备。
我的目标就是260天后能走提前批
我们可以先了解以下cin和cout
cin是istream类型的对象,而cout是ostream类型的对象。
我们来思考一个问题:为什么cout<<可以自动识别类型?
本质的原因是进行了函数重载:所以能够根据我们的类型来匹配调用的函数
同时,这里也是运算符重载,因为我们的cout是ostream类型的对象。
<<的名字叫做流插入运算符。
>>的名字叫做流提取运算符
这些都是运算符重载函数,这些函数又构成函数重载(根据类型调用函数)。
cin我们可以类比与scanf,cout我们可以类比为printf。
那内置类型库里面是如何支持的呢?
例如,为什么编译器能处理1+1=2;3.14+3.14=6.28
答:整型和浮点型是编译器自己定义的,运算符也是编译器提供的,我们只需要写出内置类型的运算,编译器就会直接转换成对应的指令。
日期的流插入和流提取可以吗?
我们进行实验:
日期类并不能直接流插入和流提取。
我们可以先尝试写日期类的流插入重载:
许多人下意识的就写成这种写法了,但这种写法是不对的。
<<运算符有两个操作数,一个操作数是ostream类的一个对象cout,一个操作数是日期类的一个对象d
所以这里的this指针指向的应该是对象d,我们在函数内部写的参数应该是ostream的对象cout
所以我们需要这么写,我们调用以下:
我们进行编译:
依旧报错,原因是什么呢?
原因在于我们写反了,假如按照我们写的<<重载函数来看,我们这样才可以调用该函数:
我们进行编译:
我们成功的调用了该流插入重载函数,但是我们有一点不足。
答:我们的可读性没有了,我们日常使用打印都是
cout<<+要打印的对象
现在反过来了就失去了其可读性,对于运算符重载,我们最着重的就是可读性。
我们的两个操作数的运算符重载是有要求的,分为左操作数和右操作数。
对于双操作数,第一个操作数就是左值,第二个操作数就是右值。
this指针默认就占了左操作数,也就是第一个位置。
所以我们写的函数实际上就等价于:
我们如何解决这个问题?
答:我们可以把这个函数不写成成员函数,可以写成公共函数,因为成员函数中就一定会有this指针,并且this指针永远指向日期类对象。
但是问题又来了,我们的成员函数可以访问日期类的成员变量,但是我们公共函数是无法访问日期类成员变量的,我们该如何处理呢?
答:首先,我们可以暴力一点,我们可以注释掉private访问限定符。
接下来,我们来证明思路是否正确。
我们流插入对应的日期类对象:
报错:提示我们重定义。
谁重定义了?
答:重定义的是流插入运算符
是因为流插入运算符重定义而产生的错误吗?
答:流插入运算符重定义了,但是这里并不会冲突,原因是我们和库里面的流插入的定义的参数是不同的,所以相当于我们和库里面的流插入函数构成了函数重载,并不会报错。
那错误的原因是什么?
答:错误的原因是他本身:
Date.h在预处理阶段,会分别在Date.cpp和test.cpp中进行包含,展开。
.cpp文件也就是源文件,在经过编译和链接之后,又会形成.o的目标文件。
test.o和Date.o的符号表中都有
这个函数。
然后我们进行链接,发现了同名函数,所以就造成了函数的重定义:
这里我们再复习以下编译和链接的过程:
三张图就能够解决编译和链接:
程序的运行需要包含两个大阶段:编译和链接
编译又分为预处理,编译,汇编。
第二张图:
源文件经过编译会形成对应的目标文件,再经过链接器的链接形成对应的可执行程序。
第三图:
段表对应的是.o文件的合并。
我们能够分析出什么结论?
答:全局函数和全局变量在多个文件中进行包含就会导致重定义的问题
我们换一个函数进行证明:
我们先注释掉原函数,然后在全局中写一个普通的两数相加函数:
然后我们进行调用:
我们进行编译:
依旧报错。
我们再证明一个全局变量:
我们来打印a
依旧报错。
证明了我们的结论: 全局变量和全局函数在多个文件中进行包含就会导致重定义。
我们如何去解决这个问题呢?
首先,声明和定义分离
为什么声明和定义就可以呢?
因为对一个函数的声明可以声明多次,而对于一个函数的定义只能有一次。
证明:
我们进行编译:
我们尝试声明和定义分离:
再调用函数:
所以证明声明和定义分离能够解决全局函数在多个文件中包含的重定义问题。
第二种方法:
还可以用static修饰全局函数:
static修饰全局函数能够改变函数的声明周期,使函数只能在当前文件中使用,只有当前文件的函数能够进入符号表,其他的文件中包含的这个函数无法进入符号表,所以就不会造成重定义的问题。
我们进行实验:
成功的打印出日期,证明static修饰的全局函数可以防止函数重定义的问题。
#pragma once的作用是防止头文件被多次包含,这里能够起到作用吗?
我们在每一个文件中都加上#pragma once,假如有用的话,应该能够打印出对应的日期:
依旧报错。
原因如下:
#pragma once的作用预防我们在同一个文件中写了多个头文件,
例如:
#pragma once能够预防这种情况。
我们最推荐的方法就是声明和定义分离。
全局函数的声明不会进入符号表,所以就不会引起重定义。
总结:1:在.h文件中最好不要定义全局变量和全局函数,容易导致重定义
2:用static把函数修饰成为静态的。
那我们现在的写法还有什么缺陷吗?
和赋值的思路很想,我们需要能够连续的日期类的流插入
我们尝试一下:
连续日期类的流插入就会报错。
不同点在于流插入是从左到右进行的,而赋值是从右到左进行的。
我们的流插入需要返回的应该是cout,cout的类型是ostream
注意:因为cout是全局的对象,我们不能对全局的对象进行赋值或者拷贝
所以我们的参数必须加引用,同理,返回值也必须加引用。
这个时候,我们进行连续的流插入。
能够打印出对应的结果,证明我们的写法是正确的。
注意:endl和cout不同,endl相当与一个内置类型,endl就等价于"\n",所以,我们不需要对end进行运算符重载。
除了声明和定义分离和static修饰全局变量还有一种方法也能够解决全局函数的重定义问题
我们可以使用内联函数:
内联函数:是一种以空间换取时间的做法,当我们调用内联函数时,内联函数会提前展开,不会进入符号表,并且每次调用不再需要开辟额外的栈帧,不再耗费时间。
内联在使用的时候就展开了,所以不能进入符号表,进入不了符号表就找不到对应的内联函数,所以就不会有重定义的问题。
我们进行检测:
没有报错,证明内联函数是可行的。
但是,当我们加上了访问限定符private时:
就会报出很多问题:
我们该如何解决这个问题呢?
我们可以沿用java的写法,在类的内部定义几个函数,这些函数可以根据我们输入的参数来取出私有的成员变量。
int GetYear()
{
return _year;
}
int GetMonth()
{
return _month;
}
int GetDay()
{
return _day;
}
然后我们可以在内联函数中调用这些类函数来取出对应的成员变量
然后我们进行调用:
假如我们写的函数是争取的,打印的结果应该是两个2022,10,8
证明:我们可以通过在类里面写函数来提取到成员变量。
但是,c++针对了这种无法访问到成员变量的函数开了绿灯
让我们类外的函数能够访问到类里面私有的成员变量
例如:就像我们刚才写的内联函数,我们可以这样操作:
我们可以在类的里面的任意位置进行对这个函数进行声明,声明之后我们类之外的这个函数就可以使用类里的成员变量了。
写完了流插入,我们写一个流提取运算符
inline istream& operator>>(istream&in, Date&d)
{
in >> d._year >> d._month >> d._day;
return in;
}
注意:cin是istream类型的对象 Date前面不要加引用,因为cin的作用是写入,我们要把成员变量的值写入到日期类对象中,所以我们要保证日期类对象是可读可写的。
我们记得写友元声明:
我们检验一下:
证明我们写的流插入是正确的。
这里出现了报错:
错误的原因是什么呢?
答:错误只要设计const修饰的转换为什么的就是权限的扩大。
这里是权限放大的原因:
答:我们的Printf函数并不是无参的,Printf函数的参数this指针,this指针的类型是Date*this,我们可以修改*this来改变该对象的成员变量的值,也就是说我们传递的参数是可读可写的,但是我们调用函数的对象却是只读的,所以就造成了权限的放大。
有什么办法能够解决这个权限放大的问题?
答:我们的思路是把this指针的类型由Date*转换为const Date*
但是这个this指针是掩藏的,我们无法显示修改this指针的参数类型。
但是c++已经给我们准备好了措施:
我们可以在这个函数定义的这个位置加上const,加上const表示我们的参数类型由原来的Date*转换为const Date*。
注意:这里的const只会修饰我们的this指针,对其他的参数没有任何影响。
我们调用函数检验是否有效:
所以这里的const是有效的。
所以,这里加const表示把this指针的类型从Date*转换为const Date*
为什么这里加上const,这两个函数都会调用。
第一个表示权限的缩小,因为我们的d1是可读可写的,所以我们可以从可读可写的参数转换为只读的参数。
第二个表示权限的平移,因为我们的d2是只读的,我们的传递的参数也是只读的,所以符合权限的平移。
什么情况下,我们需要在函数的定义后面加上const
传参的过程中,为了提高效率,我们可能会传引用,假如我们又不想让参数本身发生变化,这时候可以加上const
我们举一个例子:
对于日期类的-函数,假设我们把画出红线的那部分写反写成这样呢?
如图所示:
我们进行编译:
显示我们调用的函数不适配。
原因是什么呢?
答:d>*this本质上就是调用这样的函数:
我们这里写出来的*this其实是第二个参数,我们第一个参数是d的指针this,又因为指针this的参数为Date*,而我们传过来的&d的参数类型为const Date*,所以我们从const Date*到Date*发生权限的放大,所以会报错。
我们如何解决呢?
答:我们可以在>函数的声明和定义后面加上const
例如:
我们再进行运行:
编译成功,原因如下:
我们在函数的声明和定义的后面加上const,表示日期类的大于函数的this指针的类型是const Date*this,而我们调用operator传递的&d的参数类型也是const Date*this,是权限的平移,所以不会报错。
所以有哪些函数需要我们加上const?
这些最好加上const,原因是
答:1:因为这些都是比较函数,比较函数并不会改变*this,我们就不用担心加上const导致*this无法被修改的情况了。
2:加上const后,我们不仅可以比较普通的日期类对象,我们也可以比较加const修饰的日期类对象。
总结:凡是内部不改变成员变量,也就是*this数据的,这写成员函数都需要加上const
我们举一个不能加const的例子:
例如:构造函数:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
if (!(year >= 1
&& (month >= 1 && month <= 12)
&& (day >= 1 && day <= GetMonthDay(year, month))))
{
cout << "错误" << endl;
}
}
原因是:构造函数需要修改*this的数据,所以构造函数和析构函数后面不能加const。
+=可以使用const吗?
答:不能:
如图所示,所画红线的部分都表示对*this进行了修改,所以+=不可以加const。
那日期类的+可以加const吗
答:可以,如图所示:
我们可以发现,我们日期类的+并没有对*this进行修改,所以我们可以加上const。
我们甚至可以这样写:
进行编译:
相当于假如我们想要取日期类对象的地址出来,这时候,我们需要写一个取地址重载:
这个很简单:
然后我们就可以使用"&"符号来取出日期类的地址
我们进行实验:
我们再进行编译:
我们就可有通过"&"运算符来求出对应的地址。
需要注意的一点是:
答:
例如,我们日期类的构造函数,我们可以这样写:
这里就表示把year,month,day 分别赋给_year,_month,_day.
假如我们添加一个栈类:
我们的栈的拷贝构造是这样写的:
Stack(const Stack& st)
{
cout << "Stack(const Stack& st)" << endl;
_a = (int*)malloc(sizeof(int)*st._capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(_a, st._a, sizeof(int)*st._top);
_top = st._top;
_capacity = st._capacity;
}
我们使用初始化列表的话可以这样写:
Stack(int capacity = 4)
:_a((int*)malloc(sizeof(int)*capacity))
, _top(0)
, _capacity(4)
{
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
}
我们发现,这个_a可读性不太好,我们可以把_a写在我们构造函数的函数体内进行初始化。
假如,我们还需要把这部分空间初始化的话,我们需要这样写:
总结:我们可以发现,初始化列表并不能够解决所有的问题,大多数情况下都是初始化列表和函数体内初始化一起实现。
一个初始化列表的参数只能够出现一次:
例如:
为什么要写初始化列表呢?
答:有以下情况需要初始化列表的支持:
如图所示,假如我们的成员变量是一个const修饰的参数,我们在构造函数的内部是无法对_n进行修改的,但是我们可以使用初始化列表的。
我们知道,const修饰的成员变量只能初始化一次:也就是在其定义的时候进行初始化:
const修饰的成员变量只有一次初始化的机会。
所以,const修饰的成员变量必须在定义的时候进行初始化。
什么时候定义呢?
在我们创造对象的时候,就是我们初始化的时候。
对象实例化是整体定义。
const成员必须在定义的时候初始化,所以我们不能在构造函数的函数体内初始化,因为在函数体内,参数已经被定义出来了。
所以我们需要在初始化列表中对const修饰的成员变量进行初始化。
每个成员都要经过初始化列表,就算不显示在初始化列表中写,也会经过初始化列表。
例如:我们再额外创建一个普通参数:
我们在函数的初始化列表中并没有对_m进行初始化,我们进行调试查看:
我们发现,_m的确被初始化成了其他的值:
被初始化成了随机值。
为什么呢?
答:对于自定义类型,我们不进行初始化,会调用自定义类型的拷贝构造,对于内置类型,会使用随机值进行初始化。
这个随机值不太好,c++有一个修正:
我们可以对这些参数设置缺省值:
这个缺省值是在初始化列表中使用的。
证明:
我们设置一个缺省值,并在函数内部再进行初始化:
所以_m赋给的缺省值是在初始化列表中起作用的。
假如我们在初始化列表把_m进行初始化,那这里的缺省值就不起作用了。
如果没有在初始化列表中显示初始化的话
1:内置类型,有缺省值的话用缺省值,没有的话就使用随机值。
2:自定义类型,调用其默认构造函数,如果没有默认构造函数就报错。
我们对这个自定义类型的进行证明:
class A
{
public:
A(int a)
:_a(a)
{
}
private:
int _a;
};
class B
{
public:
B()
:_n(10)
, _m(5)
{
}
private:
const int _n;
int _m=10;
A _a;
};
我们写的A的构造函数并不是默认拷贝构造,默认拷贝构造不需要参数就可以调用的函数叫做默认构造函数。
我们进行编译:
三种默认构造函数:
全缺省,无参和我们没写,编译器自动调用的。
假如我们没写,编译器自动调用时,我们进行调试:
class A
{
public:
/*A(int a)*/
:_a(a)
//: _a(10)
//{
//
//}
private:
int _a;
};
class B
{
public:
B()
:_n(10)
, _m(5)
{
}
private:
const int _n;
int _m=10;
A _a;
};
总结:1:无论我们写不写,初始化列表都会进行,并且会处理每一个成员变量
2:假如我们没有写初始化列表时,初始化列表对于自定义类型,调用其默认构造,对于内置类型,赋随机值。
3:初始化列表发生在定义的时候,对于const类型的参数,我们需要在初始化列表中进行赋值。
4:默认构造:全缺省,无参,我们不写,编译器自动生成的。
但是这种情况呢?
class A
{
public:
A(int a)
:_a(a)
{
}
private:
int _a;
};
class B
{
public:
B()
:_n(10)
, _m(5)
{
}
private:
const int _n;
int _m=10;
A _a;
};
我们对自定义类型A的拷贝构造显示的写了,但是我们这里的拷贝构造并不是默认构造,所以我们不能调用。
我们该如何解决这种问题呢?
当我们对_a也进行了初始化,那我们就不会再调用其他的拷贝构造了。
我们写一个MyQueue
class MyQueue {
public:
void push(int x)
{
_pushST.Push(x);
}
private:
Stack _pushST;
Stack _popST;
size_t _size = 0;
};
假如我们的拷贝构造写成这个形式的话,并且我们没有显示初始化列表,会不会报错呢?
答:并不会报错:
原因如下:对于内置类型,我们没有写初始化列表,则被初始化成为随机值。
对于自定义类型,我们没有写初始化列表,则会调用其默认构造,栈的默认构造我们已经定义过了。
假设我们去掉这个4呢?
答:会报错:
因为假如我们显示的写了构造,并且不属于全缺省或者无参,那它就不是默认构造,那我们就无法调用默认构造,就会报错。
对于自定义类型的成员,假设我们又没有默认构造,这个时候,就会报错。
假设我们给一个参数呢?
我们该如何完成初始化呢?
还有什么成员必须在初始化列表中初始化?
答:引用,引用和const类似,在定义的时候只能够初始化一次:
注意:尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型的成员,一定会先使用初始化列表初始化。
题目:
这道题打印的结果是什么?
1和随机值,原因如下:
答:我们的初始化列表的次序是根据成员变量的先后顺序决定的,因为_a2的参数排在前面,所以我们要先初始化_a2,_a2是_a1的拷贝,但是_a1这个时候只是随机值,所以_a2就被初始化成了随机值,_a1再被初始化成1,所以结果为1和随机值。
成员变量在类中的声明次序就是其在初始化列表的初始化顺序,与其在初始化列表的先后顺序无关