目录
初始化列表
匿名
explicit、隐式类型转换、连续构造的优化
友元
类中的静态变量
类中类
话接上回
运算符重载
内置类型是祖师爷定义的,赋值=、或 加减乘除 祖师爷自己知道这些运算符应该进行怎么样的操作,平常给我们直接用就好;但是自定义类型是我们自己定义的,可能有动态空间地址的内置类型,这些我们才知道,所以自定义类型我们才知道怎么来加减乘除,赋值等操作,祖师爷很好,规定运算符(+、-、* 等)可以重载,让我们自己去实现对象之间的运算符操作
内置类型有它们的运算符规则(祖师爷自己设计);我们的自定义出的类型我们才知道怎么来加减等运算符操作,所以自定义类型我们重载自己想的运算符,去实现自己的 类的对象 的操作符运算
基本格式:
函数返回值 (类访问限定)operator运算符( 参数列表 )
等号操作符重载
介绍完再说等号赋值操作符是类中的一个默认成员函数。所以不能写在类外面,必须是成员函数;其他运算符重载函数可以写在类中(构成成员函数),也可以写在类外面(使用 “有缘”,但是不推荐,因为“有缘”会破坏类的封装的特性)
自动生成的默认赋值运算符重载函数也是一样,对内置类型进行浅拷贝(有动态内存空间就不行,不然两对象会拥有同一块动态空间),自定义类型调用它们自己的赋值运算符重载函数
学操作符顺便把复用这个长期技巧学习一下,操作符重载很多地方也可以用到复用,爽歪歪
比如:模拟日期类的比较日期类大小的判断性质比大小,只需要实现大于(或小于)和 == ,在 >= 、<=、!=、< 等都可以复用原先的两个基本运算符(用 >和== 来完成 >=、<、<=、!=)
讲两个概念
不一样在于对象:一个是新创建的被初始化;一个是两个已经存在的对象进行赋值
为什么将这个?因为编译器是通过这个标准来决定语句生成对应的汇编代码,编译器不管你的实现是怎么写的:
etc.
类名 对象2 = 对象1; //其中对象1已创建好,现创建一个对象2
看起来用了 = 赋值运算符重载,但是发现汇编调用的是拷贝构造。这里不管有没有定义=赋值运算符重载函数,都只会去调用拷贝构造函数
原因就是:编译器是按那两个标准去生成汇编代码。这里是创建了一个新对象 对象2 ,将 对象1 的内容给对象2 去初始化,所以在这里判断为是拷贝构造,虽然我们用了 =,而没有去调用 =赋值运算符重载函数
编译器看的也是对象
再讲一下前置和后置(前置++ 和 后置++ ,减减同理的,具体讲++)
前置后置都是用的++,怎么来具象化区别呢?
先来看到内置类型的 前置++ 和 后置++ 的具体,库里面(对应到内置类型)实现前置和后置是通过返回值来实现前置和后置的效果和区别的
前置 和 后置 都是有副作用的,所以在函数定义内部,对于本体都是要进行自增操作的,但是返回值不同有不同效果:
因为返回值不同,达到了前置和后置的效果,并且区分出了前置和后置
知道了它的原理,再来看看怎么区分++:因为前置后置用的都是++,运算符重载都是用的++,但是怎么来区分前置后置,不可能说用一模一样的函数名,必须去区分一下
别担心,祖师爷规定好啦:
首先肯定是用 函数重载 来实现前置和后置,那参数列表呢?不能一样,祖师爷就规定:后置的那个函数参数列表多一个 int 来占位置
前置:operator++()
后置:operator++(int)
这里的int只是来占位,为了形成不同的函数符号,用来区分是前置还是后置(右移保留了 int,才知道你用的是(九转)后置)
所以自己实现后置的重载函数的时候,直接在参数列表中塞一个int就好了,编译器自然懂你意思(int ==== 暗号)
塞好int后,不用担心自己的重载函数不给你区分前置后置,编译器看到int暗号自然知道这是后置的,你直接给对象用前置后置++就好了
前置后置的 - - (自减)也是同理的
这里回来了,来继续探讨爽歪歪的复用(偷懒。bushi,学习人的事怎么能叫偷懒呢)
现在训练:实现一个日期类(巧用复用,嘻嘻嘻)
Date.h
Date.cpp:
如果类中有const修饰的数据,引用型数据,不能在构造函数的函数体内赋值,因为程序在运行到函数体内部时,对象就已经分配了栈帧空间,创建好了,创建好说明编译器已经把一个随机值当做初始值赋给了我们的数据(有缺省除外),而现在想const修饰的数据和引用类型的数据:已经把随机值当做初始值赋给了它们,后面我们改不了,现在用已学的怎搞,没法搞 ——
祖师爷肯定会包你满意:
祖师爷说能不能在各种数据创建的时候,我去程序中取一个值给它初始化 ——
所以祖师爷设计了一个窗口:初始化列表; 把它设置在了函数头和函数体中间
格式:
函数头
初始化列表
函数体
即:
函数头
: 成员变量(初始值)
, 成员变量(初始值)
, 成员变量(初始值)
···
{ } //函数体
当然既然理解了初始化列表的基本格式和用法,再认识一下初始化列表除了能解决const和引用只能一次性初始化问题,还能解决在没有默认构造函数的其他类对象的传参问题
什么意思呢?没有默认构造,也就是要手动传参呗(构造的默认标准是看它传不传参),要手动传参的类的对象能不能在构造函数的函数体里面去构造传参啊?不能,仔细想想知道构造函数是在对象创建时去自动调用,而现在对象和上面的引用和const一样,到函数体前就已经创建好了,能不能到函数体中再去调用构造,不能也调不到
那能不能到函数体内再传,不能,已经晚咯
但是初始化列表可以~ 所以初始化列表,创建的时候可以拿初始化列表中对应的值,对象也就可以被成功创建
不过这里就是有一个细节必须注意:
各成员变量创建的顺序
—— 各成员变量是按声明先后的顺序去创建,而创建的时候去初始化列表中拿值,所以初始化列表中写的给值顺序是无效的
如下:
这样是先执行 _p 的取值, _capacity 还是随机值,就会造成问题
所以怎么更好地预防这个问题呢?
—— 初始化列表中的顺序 和 声明类中数据的顺序一样,能很好预防这个问题
一定要记住这个特点:临时的东西具有常性!!!!
匿名对象的创建格式就相当于没有名字的对象创建:
类名(参数列表);
Stack(10);
就是这样,没有花里胡哨
匿名的创建没什么说,关键在于它的作用和一些性质必须知道:
性质:正常匿名对象的生命周期在当前行,就是在当前行去创建一个临时对象(具有常性),出了该行就销毁该对象
但是,但是,一定要注意,这个时候有一个救心丸:
一般这种临时(还这么短)的变量不会去说引用它,但是一些地方希望它能活久一点,还想要继续去用它
所以祖师爷错了一个伸腿瞪眼丸:
const Stack& ss = Stack(10); //临时的具有常性,所以必须加 const
引用这个临时变量可以给这个临时变量续命!本来临时变量的声明周期就在当前行,为了不发生非法访问,也让我们可以继续用这个匿名对象,祖师爷就规定,引用了匿名对象可以让这个匿名对象的生命周期变长,从只能活一行变为可以活到所在作用域结束
匿名的作用:匿名后面我们一般是拿来偷偷懒的(准确来说是更便利),比如一些情况我们没有必要去创建一个对象然后通过这个对象去完成相关操作,我们可以直接用匿名对象去完成我们的一些操作(加const引用也可以续命继续完成我们要的操作)
赋值引用的权限问题
赋值不用考虑权限问题,操作数之间互不影响嘛
但是引用一定要注意权限问题,引用的操作数之间肯定是会相互影响的
所以看到:
const Stack S1;
Stack S2 = S1; //这里是拷贝构造哦,不记得去看类和对象中篇的运算符重载下面的构造赋值两概念
这里是赋值,两操作数之间不会互相影响,所以赋值根本不用考虑说权限的问题,尽管你S1是const常性的
但是引用:
const Stack S1;
const Stack& S2 = S1;
但是引用就必须要考虑权限的问题,引用取的别名不能权限更大,可以平移或缩小,这是引用的精髓。所以const修饰的S1不能直接赋值给 Stack& S2,
直接赋值会导致权限的放大(原本常性不可修改值 取个别名变为 可修改值当然不妥),所以要加const Stack& S2 = S1; 去拷贝构造初始化哦
那就在细说要注意引用权限的一些地方:
注意函数的参数加const才可以直接显示传内置类型过来,先识别到有隐式类型转换和拷贝构造再去优化,可以这样理解
隐式类型转换(这里讲 内置赋给自定义 时) 和 连续构造的优化(不能脱裤子放屁)
一般来说我们是对象与对象之间初始化或者赋值,如 对象来初始化一个新对象(拷贝构造),对象来赋值给对象(赋值调用赋值运算符重载)
但是如果直接写内置类型给对象呢(满足构造函数传参要求)
注意!!:这时候会发生隐式类型转换:先把内置类型提升为一个对象(临时的),再用这个临时对象(具有常性哦!)去初始化或者赋值给前面要初始化或赋值的对象
相当于会先调用构造函数(所以内置类型要满足构造函数的传参要求),再把构造的临时对象(常性!常性!常性!),拷贝构造或则赋值到目标对象
但是,这样是不是有点锉啊,直接把内置类型作为参数调用一次构造就可以了啊,还要升级成临时对象,再赋值(阿彪都会晕cai哦)
所以祖师爷当然会出手:祖师爷将所有连续的构造优化成一次构造,祖师爷全方位打击
而现在explicit是什么呢?[滑稽][滑稽]
explicit就是能阻止编译器优化连续构造
一看到有人会想这不是脱裤子放屁泰裤辣嘛,但是存在即合理啊xd们,我们现在只是眼界太小,后期有用到的地方
先记住它能阻止
在模拟实现自定义对象的cout、cin时,如果使用成员函数来实现,因为成员函数的第一个操作数默认作this指针,所以写出来只能这样调用:
d1 << cout; 或 d1.cout(); //太返祖了
但是把 << 的运算符重载做成全局函数,才能有 cout << d1; 这种顺序
所以祖师爷也没办法,破例咯,规定了一个东西叫友元 (friend)
友元 friend 在类内部使用,友元它更相当于一种前缀,一种声明,声明一个全局函数可以访问到类中的所有数据(所有成员)(私有保护都可以)
但是破坏了封装 [气愤][气愤] C++也用得少,也不推荐除了重载 <<、>>
格式:
(类中)
friend 全局函数;
格式简单,就只是在全局函数前加一个friend,声明一下,有必要加 const,不能让友元随便就能改类中的数据
类中的静态有静态变量和静态函数
静态成员变量:
在全局中的全局变量或者静态变量暴露子啊全局,在哪都可以随便修改到,但是这样不太好,不规范了;当然祖师爷也觉得
所以在C++中规定类中也可以定义静态的变量,目的是把全局的或者静态的变量也封装起来,规范,安全,巴适
所以在类中的 static 修饰的变量就有了封装的特点:
但是更多是保留了全局的一些性质:
私有的静态变量在定义的时候 破例 突破私有
改为公有后,定义给初值的方法没变,但是在类域外面可以指定类域去访问它
不过静态成员变量还有一个特别容易出错的地方:
一般不要在头文件中去定义(实例化)静态变量,如果不能马上反应,要回去再熟悉一下编译运行那些东西哦:
如果在头文件中实例化静态变量,我们知道如果有多个源文件,编译运行时,在预编译阶段会将各个源文件中包含的头文件进行展开,那么头文件中如果有一个静态变量,那么每一个源文件中都会有一个静态变量,那么各个源文件形成符号表后开始符号表链接,发现有一个重定义或者命名的符号,没错就是那个静态成员变量,所以一使用到静态变量,就不要去头文件中去定义,在源文件中要用的时候实例化就好了
静态成员函数:
静态成员函数是将 和类有关的 但是又不会去访问类中数据的 函数 封装到类里面来,设计的静态成员函数没有this指针,一定要注意,静态成员函数是没有this指针的,这是类和对象容易忘记的一个特点,一定要注意,不过静态成员函数还有一个很重要的特点还在后面
不过正因为有这个特点,也有它的独特之处:
静态成员变量 我们知道一般是放在私有里面,想访问可以通过实现一个成员函数,然后在对象里面去调用它,但是静态成员变量一旦创建就是为整个类,所有对象 共有的,不管有没有创建对象都是有的,属于这个类的;那现在我想在没对象的情况下还能访问到静态成员变量,那好像除了友元就没有其他什么办法了,但是在C++中我们不喜欢用友元破坏私有,有没有什么办法呢?
那就是静态成员函数:静态成员函数没有this指针,自然不能访问到我们类中的数据,但是,但是,它独宠类里面的静态变量,类中的静态成员函数它可以轻易访问到(静态成员变量劝静态成员函数雨露均沾,但是静态成员函数偏不听,就宠我,就宠我 ----- 静态成员变量)
正因为静态成员函数和静态成员变量天生般配,所以静态成员函数一般是和静态成员变量合起来一起用的,通过静态成员函数去访问静态成员变量
类定义在类中更相当于在类中定义了一个类的类型,(小)类可以访问到(大)类中的数据,但是奇葩的是,(大)类却不能访问到(小)类的数据
所以(小)类更相当于相当于友元函数的性质,(小)类相当于在(大)类中 被friend修饰,可以访问到(大)类中的数据,而(大)类却访问不到(小)类中的数据
但是不能完全按友元来理解,因为类中的类还能被访问限定符修饰,更相当于类中的一个类型
在public后在类外面可以被用类域访问限定符来访问,而修改为private就不能再通过公有的方法来搞,和普通的私有一样,可以通过函数来访问到私有的类
结构上也可以理解为是两个分开来类,一个友元friend修饰了就是
这个也是Java里面喜欢用,C++里也少
不过类中类的好处是什么,可以把一个类藏起来,甚至变为私有,这叫什么
—— 私藏对象(类)
const修饰函数参数与普通参数的重载和调用歧义