本篇文章的讲解将按照上述的方式,逐层递进,分分钟帮你拿捏C++类和对象这部分。
初步了解面向过程和面向对象的区别
举个比较感人的例子用来帮助铁子们理解(友情提示:内容可能引发不适,不能接受请及时略过)
狗改不了吃屎,这其中的狗吃屎(狗选择了什么食物来填饱肚子)就可以理解为面向对象
而吃狗屎,(狗吃屎的过程)狗享受食物的这个过程就可以理解为面向过程
狗的食物需求种类丰富多样,可以是大米饭,也可以是猪排骨,还可以是屎…在这些用来填饱肚子的食物当中,狗对自己的便便算得上是情有独钟了,毕竟吃起来回味无穷,这样看起来如此秀色可餐的美食也让我们每个人都垂涎三尺了,但是很可惜的是我们无福消受。
我以定义菜单上的食物类为例,向大家讲解他们之间的区别:
(1)C语言的方式
(2)C++的方式
从上述的例子,我们就能轻而易举地看出其区别,C语言只允许有成员变量在结构体中,而C++的结构体中不仅仅只有成员变量,而且还允许有成员方法(即所谓的函数),也就是C++将结构体升级成为了类,所以更喜欢用class来表示类,而不是struct。(注:class在英文翻译中不单单是只有班级的意思,更还有类这方面的意思)
(1)模板样式
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面是有分号的,千万不要忘记,有的编译器会帮你自动补充,但是也有不会帮你自动补充的!
类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。
(2)类的两种定义方式:
以我前面写的食物类为例:
【1】 声明和定义全部放在类体中,需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
扩展迁移(何为内联函数?懂得小伙伴可以略过)
很多小伙伴可能不太清楚内联函数是什么意思,那这里我再简单讲解下内联函数:
① 概念:以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
② 特性:
a. inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。
b. inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等
等,编译器优化时会忽略掉内联。
c. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会
找不到。
【2】声明放在.h文件中,类的定义放在.cpp文件中
一般情况下,更期望采用第二种方式。
(1)访问限定符
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
【访问限定符说明】
① public修饰的成员在类外可以直接被访问
② protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
③ 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
④ class的默认访问权限为private,struct为public(因为struct要兼容C)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
拓展引申:
基于第④小点,我们就可以明确地清楚C++中struct和class的主要区别是什么?
即C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类。
和class是定义类是一样的,区别是struct的成员默认访问方式是public,class是的成员默认访问方式是
private。
(2)封装
在类和对象阶段,我们只研究类的封装特性,那什么是封装呢?
概念:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理:
就比如说当下全球都受到新冠疫情的影响,在疫情初期,我国作为世界上少有的大国,又是如何做到疫情防控楷模,有效地抑制疫情在中国的传播呢?我们使用健康码通行制度(红码、黄码、绿码),只有当你是绿码的时候,你才能自由通行去想去的地方,否则就要进行隔离,并且当有地方出现疫情以后,也会根据疫情的严重程度变为中高风险区、高风险区等等,只有当连续14天都没有出现疫情才降为低风险区,正是因为国家这么合理有效地管控疫情,我国疫情才能如此的平稳。很难想象如果我国不采用这些制度来管控疫情,那么我国疫情的现状又该如何?说到这里,我们就得大大的为国家点赞了,非常庆幸生在中国,感谢祖国的保护。
类的封装(使用protected/private把成员封装起来)也是一样的道理,好比你这边是疫情高风险区,就必须得封控起来不让你出去,也不能让外面进来,进而才能有效地抑制疫情的发展。如果你类的封装不够完善,安全性不高,就如同这疫情防控一样,岂不是乱套了吗?
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域。
以我前面写的食物类为例:
(1)简介:
概念:用类类型创建对象的过程,称为类的实例化。
① 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。
② 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量。
③ 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什
么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占
用物理空间。
以我前面写的食物类为例:
(1)如何计算类对象的大小?
问题:类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小呢?
实践出真知,我们对类对象在编译器中的存储方式进行分情况讨论:
情况1:类中既有成员变量,又有成员函数:。
情况2:类中只有成员函数。
情况3:类中什么都没有,即空类。
以下列测试代码为例:
通过计算我们可以得知,3种情况下的类的大小分别为4、1、1
发现一个很奇怪的现象就是在情况2和情况3下,类的大小算出来的结果都是1,why?这是因为编译器在类对象的存储过程中,将类中的成员函数放到了公共的代码段上面,这也就意味着一个类的大小计算方式本质上和C语言结构体大小的计算方式一样,就是该类中“成员变量”之和,成员函数并不参与计算。并且编译器规定空类(即既没有成员变量又没有成员函数的类)默认占1一个字节,代表空类的存在。
为什么编译器要把类对象的成员函数放到公共的代码段上面呢?
主要原因在于:比如我们类实例化出来的每个对象中的成员变量是不同的,但是需要调用同一份函数,假设不将类对象中的成员函数放到公共的代码段上面,那么当一个类创建多个对象时,每个对象中都会保存一份这样的代码,相同代码被保存多次,相当地浪费存储空间,这是非常愚蠢的做法。
因此大佬们才将类对象的成员函数规定放在公共的代码段上面,极大地节省存储空间。
关于C语言结构体内存对齐规则,若有遗忘的小伙伴可以参考我下面链接的这篇博客:
附:结构体内存对齐规则
(1)this指针的引出:以定义一个学生成绩类为例
针对该类,存在着这样的一个问题,就是StudentScore类中存在有Show和SetScore两个成员函数,函数体中并没有关于不同对象的区分,假设当s1调用SetScore函数时,该函数是如何知道应该设置s1对象,而不是设置s2对象呢?
为此,C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
(2)this指针的特性:
① this指针的类型:类类型* const。
② 只能在“成员函数”的内部使用。
③ this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this
形参。所以对象中不存储this指针。
④ this指针是成员函数第一个隐含的指针形参,一般情况下由编译器通过ecx寄存器自动传递,不需要用户
传递。
前面定义的一个学生类实际上就被编译器处理成了下面的这个样子,以此类推。
简而言之,切记就是:
① 调用成员函数时,不能显示传参给this。
② 定义成员函数时,也不能显示声明形参this。
③ 在成员函数内部时,我们可以显示使用this。(通常是在特殊情况下才会显示使用this,一般情况下我们都不会显示使用this)
假设一个类中什么成员都没有,我们就简称为空类。但是空类中实际上就真的什么都没有吗?然而并不是的,任何一个类在我们不写的情况下,都会自动生成下面六个默认成员函数。
(1)概念:构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次。
(2)特性:
需要注意的是,构造函数是特殊的成员函数,虽然名称叫做构造函数,但是它并不如其名,它的主要作用是初始化对象,而不是开空间创建对象。
① 函数名与类名相同。
② 无返回值。
③ 对象实例化时编译器自动调用对应的构造函数。
④ 构造函数可以重载。
以日期类为例(下同),向大家演示讲解:
⑤ 如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义编译器将不再生成。
⑥ 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的默认构造函数,都可以认为是默认成员函数。
编译时,我们会发现两个错误和一个警告。实际上在这段代码中无参的构造函数和全缺省的构造函数在语法上是完全正确,重载是成立的。但是编译器在调用过程中,会存在二义性,编译器无法识别这个无参的对象初始化到底应该调用哪一个构造函数。
但是如果我们不是无参对象初始化,编译器是能完全正确编译出来的。因此,在我们的代码实践中一定要注意避免这样类似的坑,防止代码出现二义性这种现象。
⑦ C++里面把类型分为两类:内置类型(基本类型)和自定义类型。内置类型为int/char/double/指针/内置类型数组等等,自定义类型为struct/class定义的类型等等。我们不写编译器默认生成构造函数,对于内置类型的成员变量不做初始化处理,对于自定义类型的成员变量会去调用它的默认构造函数(不用参数就可以调的)初始化,如果没有默认构造函数编译器就会报错
(1)概念:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象的资源清理工作。
(2)特性:
① 析构函数名是在类名前加上字符~。
② 无参数无返回值。
③ 一个类有且只有一个析构函数。若未显示定义,系统会自动生成默认的析构函数。
④ 对象生命周期结束时,编译器会自动调用析构函数。
日期类这样的通常是不需要我们自己写析构函数,但是类似于栈这样需要从堆申请开辟空间的就需要我们自己写析构函数,否则我们编译器自己是不能完成对象的资源清理工作。
⑤ 如果我们不写默认生成的析构函数和构造函数类似,都是对内置类型的成员变量不做处理,对于自定义类型的成员变量会去调用它的析构函数。
默认生成的构造函数和析构函数会对自定义类型成员变量调用它的构造和析构,在某种程度上也是起着一定的作用,并不是一无是处,切勿以偏概全。
(1)概念:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
(2)特性:
① 拷贝构造函数是构造函数的一个重载形式。
② 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。(调用拷贝构造,需要先传参数,传值传参又是一个拷贝构造,然后呢调用拷贝构造,又需要先传参数,传值传参又是一个拷贝构造,反复死循环)
③ 若未显示定义,系统生成默认的拷贝构造函数。(其作用为对于内置类型成员,会完成按字节序的浅拷贝;对于自定义类型成员,会调用它的拷贝构造函数)
所谓的浅拷贝就是默认的拷贝构造函数构造对象时按内存存储并按字节序完成的拷贝,与之相对应的为深拷贝(通常需要我们自己写的拷贝构造函数),类似日期类这样的拷贝构造函数不需要自己写也行。
但是类似栈这样的类就需要我们自己写拷贝构造函数,若是依靠系统生成的默认拷贝构造函数就会出现下面这样子的报错,程序直接就崩溃了,原因在于他们指向的同一块空间被析构了两次,因此为了避免这样类似的情况发生,我们自己需要谨记写好拷贝构造函数。
总结:关于拷贝构造函数,我们不写生成的默认拷贝构造函数对于内置类型和自定义类型都会拷贝处理。但是处理细节是不同的,这个跟析构和构造函数有所差异。
(1)简介:
① C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
② 函数名字为:关键字operator后面接需要重载的运算符符号。
③ 函数原型:返回值类型operator操作符(参数列表)。
④ 注意切记:
【1】不能通过连接其他符号来创建新的操作符:比如operator@。
【2】重载操作符必须有一个类类型或者枚举类型的操作数。
【3】用于内置类型的操作符,其含义不能改变,例如:内置的整形+,不能改变其含义。
【4】作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参。
【5】.*、::、sizeof、?:、.这5个操作符是不能用来重载的。
对比分析下拷贝构造和赋值运算符重载比较显著的区别,比如说拷贝构造是为了创建新对象能在原有的对象下进行复制,而赋值运算符重载是原有的对象之间的操作。
同时,当我们在写赋值运算符重载这些函数时,若懂得在合适的情况下使用引用返回,可以有效地减少拷贝构造函数的调用。
a、使用引用返回
b、没有使用引用返回
① 参数类型
② 返回值
③ 检测是否自己给自己赋值
④ 返回*this
⑤ 一个类如果没有显示定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。(即浅拷贝)
编译器默认生成赋值重载函数,跟上述的拷贝构造函数做的事情基本类似,对内置类型成员,会完成字节序的值拷贝(即浅拷贝),对于自定义类型成员变量,会调用它的赋值重载函数,这里我就不过多重复叙述。
一般我们写具体的项目的时候,都是类似将头文件、源文件、测试文件各部分功能分开写,这样子条理更加清晰,也便于找到错误。头文件将类所需要的库文件、类的方法声明这类的写好,而源文件就是将这些类方法定义实现好,测试文件就是用于验真这些方法的正确性。
日期类项目实现的功能大致划分为显示日期,两个日期的大小比较,推算日期(类似日期+天数,推算出新日期这样的),两个日期相差多少天,以及最后推算出当前日期是星期几。
Date.cpp 源文件的实现
① 类的构造函数实现:默认用户不初始化的日期都初始化为公元元年(1年1月1日),同时若用户初始化了日期,要将其判断是否为合法日期,防止出现非法日期,这又引申出了如何判断用户的日期是否合法的问题?很简单,我们只需要再创建一个方法(根据年月份推理出天数),如果天数不合理就是非法日期,反之就是合法日期。日期类的打印函数就相对简单,没什么可说的。
② 日期的大小比较函数,两个日期之间的大小关系有大于、小于、等于、大于等于、小于等于以及不等于,我们只需要具体实现大于或者小于配合等于的实现就能代码复用出其他的关系比较。
③ 推算日期(类似日期 + 天数,推算出新日期这样的)函数的实现。通过实现日期 += 天数的函数可以代码复用出 + 的函数实现, - = 也是同理复用出 - 的函数实现,通过前置 ++ 的实现,可以代码复用出后置 ++ 的实现,- - 也是同理。
日期 += 函数的实现思想就是天数就够就往月数进位,月数够了就往年数进位,以此类推,日期 + 的函数直接代码复用 += 的函数实现。
日期- = 函数的实现同理,天数不够减了就往月数去借,月也不够了就往年去借,日期 - 的实现同样是代码复用- = 函数。
需要注意出现 += 或者 - = 负数的情况,实际上就是 += 变为 - = ,然后 - = 变为 += 而已。稍加判断切换调用函数即可。eg1:2023年1月5日 += -34 天,也就是 2023年1月5日 - = 34 天;eg2:2023年2月13日 - = -66 天,也就是2023年2月13日 += 66 天。
在C++的标准规定中,为了区分前置 ++ 和后置 ++ 运算操作符重载函数的区别,引入了占位参数的概念,即后置 ++ 中会默认有一个规定为int类型的占位参数,便于编译器调用时进行区分前置还是后置,我们在使用时是不需要管这个默认占位参数的,编译器会自动帮助填充调用,但是在写方法的时候需要注明默认int类型的占位参数。
日期前置 ++ 的实现非常简单,在当前日期往后+一天用引用返回去即可;后置 ++ 的实现直接可以代码复用前置 ++ ,通过拷贝构造的方式初始化一个跟当前日期一样的日期,然后再将当前日期 += 1,返回之前拷贝构造的变量即可。前置 - - 和后置 - - 也是同理。
④ 两个日期相差多少天的函数实现。
我实现的思想是:默认两个日期相减时,第一个日期是大于第二个日期的,利用一个循环以小的日期为基准,看还差多少天才等于大的日期。这样子就可以算出来两个日期相差多少天了。
前面有提到我默认第一个日期是大于第二个日期的,那么当出现第一个日期小于第二个日期的情况怎么办呢?很简单,采用一个标记变量初始值为1,若第一个日期小于第二个日期就变为 - 1,然后对换两个日期的位置,这样子最大的日期就能始终默认是第一个日期了。最后在返回值做手脚(根据标记变量返回正负天数),若起初第一个日期就大于第二个日期,标记变量就是1,返回正数;否则标记变量为 - 1 返回负数即可。
⑤ 推算出当前日期是星期几的函数实现。
思想同样是以指定的一个标准日期来计算星期,我默认的是1900年的1月1日,这个时间对应的星期刚好是星期一。
之所以以1900年1月1日为标准是因为在网上查了很多关于日期星期的查询器,发现划到最早的只能看到1900年的1月1日,所以以此作为标准。
那么后续代码实现的操作就非常容易了。定义一个日期数组,元素是星期一到星期天。然后用当前日期减去标准日期的结果取余7(这样结果就只能是0到6),根据这个结果依次对应是星期几。
楼主实现的星期查询是存在缺陷的,就是只能查询出1900年1月1日以后的日期,在这之前的日期是无法准确查询出来的。
Test.cpp 源文件的实现
这个源文件主要是用来验证前面类中的各方法是否成功实现,在实践写代码的过程中,最好就是边写边测试,不要一次性写完再慢慢测试找错误,这样你会压根很难发现问题。
② 验证日期的大小比较的方法是否正确。
③ 验证推理日期的方法是否正确。
④ 验证两个日期相差多少天的方法是否正确。
⑤ 验证当前日期是星期几的方法是否正确。
附1:C++日期类模拟实现的源码提取
Date.h
#pragma once // 防止多个头文件被重复包含
#include
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1); // 构造函数 —— 日期的初始化
// 日期类的拷贝构造函数,可以不写
/*Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
cout << "Date(const Date& d)" << endl;
}*/
int AcquireMonthDay(int year, int month); // 获取月份天数函数 —— 根据年份月份,推算出合理月份的天数,免得出现非法日期
void Print(); // 打印函数 —— 打印日期
// 实现 > 或 < 和 == 其他的就可以完成代码的复用
// 不仅仅是Date类可以这样子,其它类要实现这样类似的比较,也可以以此类推
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);
Date& operator+=(int day); // 推算日期函数 —— 日期+天数,推算出新的日期,下同理
Date operator+(int day);
Date& operator-=(int day);
Date operator-(int day);
// ++d1; 前置++
Date& operator++();
// d1++; 后置++
// 后置++为了跟前置++进行区分,增加了一个参数占位(规定为int类型),跟前置++构成函数重载
Date operator++(int);
Date& operator--();
Date operator--(int);
// 日期 - 日期
int operator-(const Date& d);
// 显示当前日期所在的星期
void ShowWeekDay();
private:
int _year;
int _month;
int _day;
};
Date.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include "Date.h"
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
if (!(_year >= 0
&& (month > 0 && month < 13)
&& (day > 0 && day <= AcquireMonthDay(year, month))))
{
cout << "非法日期:";
Print();
}
}
int Date::AcquireMonthDay(int year, int month)
{
// 这里需要注意的是数组的下标是从0开始的,所以不需要考虑首元素的天数,其后分别代表1月、2月、3月以此类推
static int MonthDayArray[13] = { 6, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
int day = MonthDayArray[month];
// 需要清楚的是润年的二月是29天,平年是28天
if (month == 2 && ((year % 400 == 0) || (year % 4 == 0 && year % 100 != 0)))
{
day += 1;
}
return day;
}
void Date::Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
bool Date::operator>(const Date& d)
{
// 先比较年
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)
{
// 依旧是先比较年、再比较月、最后比较天
// 成功就会返回1(true),失败就返回0(false)
// && 的判断是其中有一个为假就会全部为假,即false,要全部满足条件才是true
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
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 !(*this == d);
}
// 天满了往月进位,月满了往年进位
Date& Date::operator+=(int day)
{
if (day < 0)
{
return *this -= -day;
}
_day += day;
while (_day > AcquireMonthDay(_year, _month))
{
_day -= AcquireMonthDay(_year, _month);
++_month;
if (_month == 13)
{
_month = 1;
_year++;
}
}
return *this;
}
Date Date::operator+(int day)
{
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)
{
--_year;
_month = 12;
}
_day += AcquireMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day)
{
Date ret(*this);
ret -= day;
return ret;
}
Date& Date::operator++()
{
*this += 1;
return *this;
}
Date Date::operator++(int)
{
Date ret(*this);
*this += 1;
return ret;
}
Date& Date::operator--()
{
*this -= 1;
return *this;
}
Date Date::operator--(int)
{
Date ret(*this);
*this -= 1;
return ret;
}
int Date::operator-(const Date& d)
{
// 默认情况下,第一个日期大于第二个日期
Date max = *this;
Date min = d;
int flag = 1; // 标记变量,第一个日期大于第二个日期时为1
// 当出现第二个日期大于第一个日期时,标记变量变为 - 1,同时对换两个日期的位置
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
// 计算小的日期还差多少天才能等于大的日期,由此得出两个日期相差多少天
int count = 0;
while (min != max)
{
++min;
++count;
}
// 若原先第一个日期大于第二个日期就返回正数,否则负数
return count * flag;
}
void Date::ShowWeekDay()
{
const char* arr[] = { "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期天" };
// 标准日期,此时为星期一
Date start(1900, 1, 1);
int count = *this - start;
cout << arr[count % 7] << endl;
}
Test.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include "Date.h"
void TestDate1()
{
Date d1;
d1.Print();
Date d2(2023, 2, 13);
d2.Print();
Date d3(2023, 2, 29);
d3.Print();
}
void TestDate2()
{
Date d1(2023, 2, 5);
Date d2(2001, 2, 9);
Date d3(2000, 11, 11);
Date d4(1993, 12, 12);
cout << (d1 > d2) << endl;
cout << (d3 > d4) << endl;
cout << (d1 < d2) << endl;
cout << (d4 >= d2) << endl;
cout << (d2 <= d4) << endl;
cout << (d4 != d3) << endl;
}
void TestDate3()
{
Date d1(2022, 1, 4);
Date d2(2025, 4, 7);
Date d3(2006, 3, 9);
Date d4 = d1 + 100;
d4.Print();
d2 += 99;
d2.Print();
d3 -= 88;
d3.Print();
}
void TestDate4()
{
Date d1(2023, 2, 13);
Date d2(1999, 9, 1);
cout << (d1 - d2) << endl;
Date d3(1889, 8, 9);
Date d4(2020, 9, 1);
cout << (d3 - d4) << endl;
}
void TestDate5()
{
Date d1(2033, 6, 6);
d1.ShowWeekDay();
Date d2(2023, 3, 14);
d2.ShowWeekDay();
Date d3(1900, 1, 4);
d3.ShowWeekDay();
}
int main()
{
TestDate5();
return 0;
}
附:日期类拓展出的相关习题
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this
指针,表明在该成员函数中不能对类的任何成员进行修改。
我们会发现,被const修饰定义出来的类变量无法使用打印函数,这是为何?原因在于传进去的是const Date了,不是单单的Date;而类中的Print函数隐藏this指针是接收Date的,却传进来const Date这就导致了权限的放大,不被允许。
然后如果我们在类方法的实现过程中也加上const,那么d2就也能使用Print方法,也不会影响d1的使用(权限缩小并不影响)。
并且在实际的代码操作过程中,成员函数加上const修饰是好的,推荐能加的就加上,这样普通对象和加了const修饰的对象都可以正常调用函数。
但是如果是需要修改成员变量的成员函数是不能加的,好似日期类中的+= 、++这样的实现,是一定不可以加const修饰的。
附2:那么前面实现的日期类加const修饰的升级版源码如下:
Date.h
#pragma once // 防止多个头文件被重复包含
#include
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1); // 构造函数 —— 日期的初始化
// 日期类的拷贝构造函数,可以不写
/*Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
cout << "Date(const Date& d)" << endl;
}*/
int AcquireMonthDay(int year, int month) const; // 获取月份天数函数 —— 根据年份月份,推算出合理月份的天数,免得出现非法日期
void Print() const; // 打印函数 —— 打印日期
// 实现 > 或 < 和 == 其他的就可以完成代码的复用
// 不仅仅是Date类可以这样子,其它类要实现这样类似的比较,也可以以此类推
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;
Date& operator+=(int day); // 推算日期函数 —— 日期+天数,推算出新的日期,下同理
Date operator+(int day) const;
Date& operator-=(int day);
Date operator-(int day) const;
// ++d1; 前置++
Date& operator++();
// d1++; 后置++
// 后置++为了跟前置++进行区分,增加了一个参数占位(规定为int类型),跟前置++构成函数重载
Date operator++(int);
Date& operator--();
Date operator--(int);
// 日期 - 日期
int operator-(const Date& d) const;
// 显示当前日期所在的星期
void ShowWeekDay() const;
private:
int _year;
int _month;
int _day;
};
Date.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include "Date.h"
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
if (!(_year >= 0
&& (month > 0 && month < 13)
&& (day > 0 && day <= AcquireMonthDay(year, month))))
{
cout << "非法日期:";
Print();
}
}
int Date::AcquireMonthDay(int year, int month) const
{
// 这里需要注意的是数组的下标是从0开始的,所以不需要考虑首元素的天数,其后分别代表1月、2月、3月以此类推
static int MonthDayArray[13] = { 6, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
int day = MonthDayArray[month];
// 需要清楚的是润年的二月是29天,平年是28天
if (month == 2 && ((year % 400 == 0) || (year % 4 == 0 && year % 100 != 0)))
{
day += 1;
}
return day;
}
void Date::Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
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
{
// 依旧是先比较年、再比较月、最后比较天
// 成功就会返回1(true),失败就返回0(false)
// && 的判断是其中有一个为假就会全部为假,即false,要全部满足条件才是true
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);
}
// 天满了往月进位,月满了往年进位
Date& Date::operator+=(int day)
{
if (day < 0)
{
return *this -= -day;
}
_day += day;
while (_day > AcquireMonthDay(_year, _month))
{
_day -= AcquireMonthDay(_year, _month);
++_month;
if (_month == 13)
{
_month = 1;
_year++;
}
}
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)
{
--_year;
_month = 12;
}
_day += AcquireMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day) const
{
Date ret(*this);
ret -= day;
return ret;
}
Date& Date::operator++()
{
*this += 1;
return *this;
}
Date Date::operator++(int)
{
Date ret(*this);
*this += 1;
return ret;
}
Date& Date::operator--()
{
*this -= 1;
return *this;
}
Date Date::operator--(int)
{
Date ret(*this);
*this -= 1;
return ret;
}
int Date::operator-(const Date& d) const
{
// 默认情况下,第一个日期大于第二个日期
Date max = *this;
Date min = d;
int flag = 1; // 标记变量,第一个日期大于第二个日期时为1
// 当出现第二个日期大于第一个日期时,标记变量变为 - 1,同时对换两个日期的位置
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
// 计算小的日期还差多少天才能等于大的日期,由此得出两个日期相差多少天
int count = 0;
while (min != max)
{
++min;
++count;
}
// 若原先第一个日期大于第二个日期就返回正数,否则负数
return count * flag;
}
void Date::ShowWeekDay() const
{
const char* arr[] = { "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期天" };
// 标准日期,此时为星期一
//Date start(1900, 1, 1);
//int count = *this - start;
int count = *this - Date(1900, 1, 1); //匿名对象,只作用在当前行
cout << arr[count % 7] << endl;
}
Test.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include "Date.h"
void TestDate1()
{
Date d1;
d1.Print();
Date d2(2023, 2, 13);
d2.Print();
Date d3(2023, 2, 29);
d3.Print();
}
void TestDate2()
{
Date d1(2023, 2, 5);
Date d2(2001, 2, 9);
Date d3(2000, 11, 11);
Date d4(1993, 12, 12);
cout << (d1 > d2) << endl;
cout << (d3 > d4) << endl;
cout << (d1 < d2) << endl;
cout << (d4 >= d2) << endl;
cout << (d2 <= d4) << endl;
cout << (d4 != d3) << endl;
}
void TestDate3()
{
Date d1(2022, 1, 4);
Date d2(2025, 4, 7);
Date d3(2006, 3, 9);
Date d4 = d1 + 100;
d4.Print();
d2 += 99;
d2.Print();
d3 -= 88;
d3.Print();
}
void TestDate4()
{
Date d1(2023, 2, 13);
Date d2(1999, 9, 1);
cout << (d1 - d2) << endl;
Date d3(1889, 8, 9);
Date d4(2020, 9, 1);
cout << (d3 - d4) << endl;
}
void TestDate5()
{
Date d1(2033, 6, 6);
d1.ShowWeekDay();
Date d2(2023, 3, 14);
d2.ShowWeekDay();
Date d3(1900, 1, 4);
d3.ShowWeekDay();
}
void TestDate6()
{
Date d1;
d1.Print();
const Date d2;
d2.Print();
}
int main()
{
TestDate6();
return 0;
}
这两个运算符一般是不需要重载的,因为这也是属于编译器会默认生成的。实际上编译器生成的就已经够用了,除非某些特殊情况下才需要你自己实现,比如你想让别人获取到指定的内容这样的。
又或者你就不想给别人知道地址。
(1)构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中的各个成员变量一个合适的初始值。
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称之为类对象成员的初始化,构造函数体中的语句只能将其称之为赋初值,而不能称为初始化。
因为初始化只能初始化一次,而构造函数体内可以多次赋值。
(2)初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分割的数据成员列表,每个“成员变量”的后面跟一个放在括号中的初始值或表达式。
注意:
① 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
② 类中包含以下成员,必须放在初始化列表位置进行初始化:
a、引用成员变量
b、const成员变量
c、自定义类型成员(该类没有默认构造函数)
很好理解,比如说你定义常量的时候也必须初始化赋值,否则编译器就会报错。类中成员变量亦是如此,一般的构造函数无法完成常量这类的初始化,但是初始化列表刚好可以。其余同理。
可以灵活控制构造函数,结合初始化列表,但是推荐上面这种初始化列表的方式。
牢记这3种情况必须在初始化列表里初始化!
③ 尽量使用初始化列表进行初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
对于内置类型的成员,在函数体和在初始化列表初始化都是可以的;而自定义类型的成员,建议在初始化列表初始化,这样会相对高效,因为无论如何都是要先使用初始化列表初始化。
④ 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。(因此建议在一个类中,尽量声明的顺序和初始化列表出现的顺序保持一致,就不容易出问题)
(3)explicit 关键字
构造函数不仅可以构造和初始化对象,对于单个参数的构造函数,还具有类型转换的作用。
用explicit关键字修饰构造函数,将会禁止单参构造函数的隐式转换。
(1)声明为static的类成员称为类的静态成员;用static修饰的静态成员变量,称为静态成员变量;用static修饰的成员函数,称为静态成员函数。静态的成员变量一定要在类外进行初始化。
(2)特性:
① 静态成员为所有类共享,不属于某个具体的实例。
② 静态成员变量必须在类外定义,定义时不添加static关键字。
③ 类静态成员即可用类名 :: 静态成员或者对象 . 静态成员来访问
④ 静态成员函数没有隐藏的this指针,不能访问任何非静态成员。
⑤ 静态成员和类的普通成员一样,也有public、protected、private 3种访问级别,也可以具有返回值。
代码引入演示讲解:
假设定义一个类叫H,需要计算它累计调用了多少次构造函数。
按照以前通用的方法就是需要定义一个全局变量,每调用一次构造函数就加一次,这样就能算出它累计调用了多少次构造函数。
But,这是以前的方法,在C++中这就非常out了(若是C语言当我没说)。
我们C++类中可是有静态成员变量的存在哦!千万不要忽视它的作用。
进入正题:
很多兄弟可能这里会不理解为什么静态的成员变量一定要在类外进行初始化呢?其实如果说成是需要定义而不是初始化可能你就会秒懂了。两个有个显著的区别:初始化是赋一个初始值,而定义是分配内存。在C++中,静态成员变量在类中仅仅是声明,没有定义,这就意味着静态成员变量并没有分配到实际的内存!因此,我们需要在类的外面定义,即所谓的初始化,本质就是给静态成员变量分配内存。
若我们忘记静态成员变量初始化,则编译器就会报错!
类静态成员可用类名 :: 静态成员或者对象 . 静态成员来访问就没什么好说的了,已经演示出来了。
介绍完静态成员变量那么接下来就是静态成员函数了!
静态成员函数跟静态成员变量用起来没什么区别,唯一需要切记的就是没有隐藏的this指针,不能访问任何非静态成员!
附:静态成员相关练习题
C++11 支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量缺省值。
xdm若文章看到这里的话就应该非常清楚地知道:由于在C++中对于内置类型处理,对于自定义类型不处理。这就非常有点令人捉摸不透,所以C++11的这个成员初始化新玩法语法实际上就是对这个问题打补丁的。优化了对于自定义类型的处理方式,同时也兼容了前面的C++语法,未发生冲突。
代码演示讲解:
我们初始化成员变量一般都是使用构造函数配合初始化列表初始化。但是从C++ 11开始,多出了成员变量缺省值的方法用于优化构造函数初始化方式。(牢记这里不是赋值,而是给缺省值)
并且呢,根据编译器的不同,可能优化缺省值的方式也存在略微的差异。比如说VS2013不支持数组缺省值的方法,但是VS2019下是支持的。
切记静态成员变量是不能这样给缺省值的,必须在类外面全局位置定义初始化,因为静态成员变量并不会去调用构造函数初始化!
简介:
友元分为友元函数和友元类。
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,因此友元不宜多用,建议非必要情况下避免使用。
说了通俗易懂的例子便于xdm理解,友元就好比黄牛这个老赖一般,破坏管理规则。尤其是比较火的明星演唱会,门票基本都是被秒抢的,比如说周XX演唱会。此时你又想去看演唱会的话,通常就是只能花几倍原价的价格跟黄牛买票了。
(1)友元函数
问题:
现在我们尝试去重载operator<<,然后我们发现没办法将operator重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际在使用中cout需要是第一个形参对象,才能正常使用。所以我们要将operator<<重载成全局函数。但是这样的话,又会导致类外没有办法访问成员,那么这里就需要我们所说的友元方法来解决了。
operator>>同理。
代码演示讲解如下:
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但是需要在类的内部声明,声明时需要加上friend关键字!
小贴士:
① 友元函数可以访问类的私有和保护成员,但不是类的成员函数。
② 友元函数不能用const修饰。
③ 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
④ 一个函数可以是多个类的友元函数。
⑤ 友元函数的调用与普通函数的调用和原理相同。
(2)友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另外一个类中的非公有成员。
友元关系是单向的,不具有交换性。
比如Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
友元关系不能传递,若B是A的友元,C是B的友元,则不能说明C是A的友元。
代码演示讲解:
(1)概念及特性
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
① 内部可以定义在外部类的public、protected、private都是可以的。
② 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
③ sizeof(外部类)=外部类,和内部类没有任何关系。
C++是基于面向对象的程序,面向对象有三大特性即:封装、继承、多态。
C++通过类,将一个对象的属性与行为结合在一起,使其更符合人们对于一件事物的认知,将属于该对象的所有东西打包在一起;通过访问限定符选择性的将其部分功能开放处理与其对象进行交互,而对于对象内部的一些实现细节,外部用户不需要知道,知道了有些情况下也没用,反而增加了使用或者维护的难度,让整个事情复杂化。
接下来举个比较真实的例子方便家人们理解类封装性带来的好处。
最简单明了的就是中国的地铁和印度的火车出行!
从上图显而易见地可以看出我们中国的地铁,有完美的秩序;而印度的火车是杂乱无章的。
先说下地铁站的工作流程:售票系统:负责售票——用户凭票进入,对号入座;而工作人员主要负责的是售票、咨询、安检、安保、卫生等行为;而动车只需要运载我们消费者带到目的地就好。
在这之中,我们地铁工作人员并不需要知道动车的构造,票务系统是如何具体实现的,只需要懂得如何操作这些功能,就能让消费者们坐车有条不紊的进行。
而我们的邻国印度,一个比较神奇的国度。经常追剧的家人们肯定也懂电视剧的夸张程度分为轻度、中度、重度以及印度这4个级别。所以印度这样的火车你敢做吗?火车没有外壳保护,站内的火车管理调度也是比较随意的。万一突然来一个急刹车的话…
但是转念一想,毕竟这是印度,已经是司空见惯的常事了。
通过图解,我们可以看出其实面向对象就是在模拟抽象映射现实世界。
写到这里,C++的类和对象初阶讲解完整版就已经完结了,进阶版的后续会接着更新。楼主不才,不喜勿喷,若有错误或需要改进的地方,非常感谢你的指出,我会积极学习采纳。谢谢家人们一直以来的支持和鼓励,我会继续努力再接再励创作出更多优质的文章来回报家人们的。编程爱好的xdm,若有编程学习方面的问题可以私信我一同探讨(我尽力帮),毕竟“众人拾柴火焰高”,大家一起交流学习,共同进步!
2022年12月1日