初阶C++——C++第二节——类和对象(大全篇)

简介: 万物皆对象。因为只要我们去研究它,它就成为我们的对象。在编程世界,其可以指所有的数据。并且该说法尤其是与类同时出现的频次较高。

目录


一、对象的概念


二、面向过程和面向对象初步认识


三、类的引入


四、类的定义


4-1 类的两种定义方式


4-1-1. 声明和定义全部放在类体中。


4-1-2. 声明放在.h文件中,类的定义放在.cpp文件中


五、类的访问限定符及封装


5-1 访问限定符


5-2 封装


5-3 类的作用域


六、类的实例化


七、类的大小计算


八、this指针


九、***(Thribble Star)类的4个默认成员函数(重要)


9-1 构造函数


9-1-1 引入


9-1-2 特性


9-1-3 初始化赋值


9-1-4 初始化列表


9-1-5 explicit关键字


9-2 析构函数


9-2-1 特性


9-3 拷贝构造函数


拷贝构造函数的特点


9-4 运算符重载


9-4-1 特性


9-4-2 赋值运算符重载


十、const成员


10-1 const修饰类的成员函数


十一、static成员


十二、友元( 关键字friend )


12-1 友元函数


12-2 友元类


十三、内部类


十四、Date类的模拟实现


函数1:Date::Date(int year, int month, int day)   //构造函数


函数2:void Date::print()//打印函数


 函数3:  int operator-(const Date& d); //两个日期相减


 函数4:  Date& operator+=(int day);//一个日期加上day天,返回Date


  函数5:  Date& operator-=(int day); //一个日期减去day天,返回date


 函数6:  Date operator+(int day);   //一个日期加上day天,返回值


 函数7:   Date operator-(int day);//一个日期减去day天,返回值


 函数8:   Date& operator++();    //自增;前置


 函数9:   Date operator++(int);  //自增;后置


剩余函数——用于判断日期前后


一、对象的概念

对象是一种开发的抽象,它包含了需要操作的数据及其可以对该数据进行操作的方法。


这是官方的说法。


笼统的来说,万物皆对象。因为只要我们去研究它,它就成为我们的对象。在编程世界,其可以指所有的数据。并且该说法尤其是与类同时出现的频次较高。


行了行了,这个概念我们了解一下就可以了。


反正记住一点,在C语言中我们称之为变量的,在C++里我们都可以称之为对象。


二、面向过程和面向对象初步认识

关于面向过程和面向对象这两者概念的区分,真正意义上来说,其不是一时半会就能够理解的。它可能需要你将所有的C/C++的知识学习完毕、工作若干年后,才能够真正意义上去理解。


所以,在这个地方,我们就是简单地、初步地理解一下。


我们以送快递举例:如果把送快递看成是一个项目工程,


那么面向对象 就是 我要把快递送给谁 ,经历怎么样的流程:


比如说,快递要先通过买家下单商家发货,然后要有快递公司来送快递,最后买家到快递点去取快递......


而面向过程大概的意思就是关心其中具体的过程是怎样实现的:


比如说,买家下单快递的渠道是怎样的,商家如何精准发货、快递公司怎样送达这样的问题...


可以说,一个是以对象作为主体,而一个是以具体实现的过程为主体。


好,这部分我们只是了解一下,这种东西通过日后的不断探索、实践,会理解地更加深刻,并且效果也更好。


从接下来开始,就是硬菜了。


三、类的引入

C语言中,结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数


不过,C++中我们更喜欢用class来定义,而不喜欢用struct。原因是什么,我们一会便会说。

class Date
{
public:
  void Init(int year, int month, int day)
  {
  _year = year;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1;
  d1.Init(2021, 11, 13);
  Date d2;
  d2.Init(2021, 5, 25);
  return 0;
}


像这样的,就是最最简单的一种类了。


至于public、private之类的,我们接下来会详细介绍。


四、类的定义

简单来说,就是这样:

class className
{
    //...
};


class为定义类的关键字,


ClassName为类的名字,


{}中为类的主体,


注意类定义结束时后面分号。


类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。


4-1 类的两种定义方式

4-1-1. 声明和定义全部放在类体中。

需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。


像刚刚的那种定义Date类就是采用这样一种方法。


4-1-2. 声明放在.h文件中,类的定义放在.cpp文件中

这种定义方式,我们接下来在模拟实现STL的时候,可能会用到。


我们马上也是需要用这样的方式来模拟实现日期类的。


如图,这里就是我在Date.h文件当中声明的日期类。

而下面是我在.cpp文件当中所定义实现的日期类的具体方式:


(当然,还有很多,这里就不一一展示了,下面笔者都会讲解)


我们一般情况下,更期望第二种方式。


五、类的访问限定符及封装

5-1 访问限定符

C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。


【访问限定符说明】

1. public修饰的成员在类外可以直接被访问


2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)


3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止


4. class的默认访问权限为private,struct为public(因为struct要兼容C)


注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别


写的应该算是很清楚详细了。不需要过多解释。


5-2 封装

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。


这一点,我们 在数据结构模拟实现的时候,就是通过三个文件的封装,将.h文件开放给用户,.c的实现文件就是用二进制的方式封装起来。


对于这一点,理解到封装就是我把所有的数据封在一个黑箱子里,对外提供各种各样的接口,让用户能用什么,不能用(知道或者看见)什么的一种手段和方式。


那么类的封装也是一样。我通过public和private(protected),实现什么数据能让你访问、什么数据不能让你去访问。所以说,封装本质上是一种管理。


5-3 类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域。


举个例子:



这个就必须 要写成A::print()


六、类的实例化

用类类型创建对象的过程,称为类的实例化。


这一点和结构体基本是一模一样。


1. 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它


2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量


3. 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间


说了这么多,总结一下,就是说,类前面写了那么多,实际上只算是声明。就像空中楼阁一般,编译器并不会为其分配内存空间。只有为类实际创建对象的时候,其才会为其开辟空间。


七、类的大小计算

对于一个类来说,其既有成员变量,又有成员函数。如果理解的话,成员变量很好理解,其所占大小我们也很熟悉。


但是对于成员函数呢?我们怎么界定其大小呢?这就和类数据的存储方式有关系了。


一种情况是,其成员变量和成员函数都可能在我们调用的时候,在栈区开辟空间。


还记得我们这个图吗?

微信图片_20221209135823.png



如果类的所有的成员在每次调用的时候,都会进行一次开辟,那么这样,也就会会造成大量的内存浪费。


所以,我们在调用类的时候,类并不是将所有的成员都存储在栈区。


实际上,成员函数是存储公共代码段的。        


也就是说,在调用类的时候,类的成员变量是存储在栈区,但其成员函数是存储在公共代码段中。


如图:

image.png



所以说,类的大小实际上就是它的成员变量的所占的大小。那它的成员变量怎么计算呢?


一句话,和结构体基本一样。


同样地,遵循内存对齐原则。


我们来举几个例子:


(如图:就不再做过多赘述了)

我们会发现,这里的private里的成员变量遵循的是内存对齐的原则。

需要注意,成员变量为空的类其内存大小被标识成 1



结论:一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类。


八、this指针

this 是一个关键字。


C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。


只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。


我们还是通过举例子的方式来说明:

class Date
{
public://哪个成员函数去调用函数,成员函数中访问的就是哪个对象的成员变量,通过this指针来实现
  void Init(int year, int month, int day)
  {//实际上编译器会增加一个隐含的指针--Date* this
  //通过函数去给,检查函数的合法性
  _year = year;
  //编译器处理成这个样子:this->_year = year
  }//栈中存储,但是不同编译器中不一样,就比如vs是存储在寄存器中的
  //并且this是一个关键字
private://凡是成员变量,建议命名风格区分一下
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1;
  d1.Init(2021, 11, 13);
  //(&d1,2021,11,13)
  Date d2;
  d2.Init(2021, 5, 25);
  return 0;
}



我们上面已经说的很清楚了。我们在调用成员函数的时候,会隐藏地传过去一个类的地址。即d1.Init(...),实际在传的过程中,会是d1.Init(&d1,...).


而成员函数中,通过this指针来接收这个地址。


在成员函数中,_year ,实际上编译器会转换为 this -> _year,也就是说,私有成员实际上是通过this指针来去访问的。

class A
{
public:
  void print()
  {
  cout << _a << endl;
  }
  void show()
  {
  cout << "show()" << endl;
  }
private:
  int _a;
};
int main()
{
  A* p = nullptr;
  p->print();//空指针去访问   成员函数中接收到的this是空指针,然后this->_a,就会引发崩溃
  p->show();//正常运行     因为该函数没有对p的解引用,也就是说,this指针没有用到
  //这也证实了public里面的函数是不会存储到对象里面而是存在公共的代码区域中
  return 0;
}



如上面代码所示。当给出p的地址为nullptr,然后去调用,当没有用到this指针的时候,其不会报错,而当其用到了this指针的时候,其会崩溃。这就从侧面说明this指针确实是存在的。


其作为成员函数的第一个参数传到过去(并且用户看不见)


this 指针的特性:


1. this指针的类型:类类型* const(比如int* const)

2. 只能在“成员函数”的内部使用

3. this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。

4. this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递


如果说,到此的内容都比较简单,那么难点和重点接下来就来了。


硬菜的重点就此开始。


九、***(Thribble Star)类的4个默认成员函数(重要)

9-1 构造函数

什么叫做构造函数?


9-1-1 引入

我们来看下面一段代码:


我们如果每一次创建类,都要调用这么一个Init函数,是不是有些过于复杂了呢?


能不能就是我们在类的实例化的时候就能初始化呢?


答案是可以的。


实际上,编译器在类实例化的时候就已经为其初始化了,只是我们不知道。其调用的就是构造函数。我们并没有写,编译器自己实现的。


构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。


9-1-2 特性

构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。


其特征如下:

1. 函数名与类名相同。

2. 无返回值。

3. 对象实例化时编译器自动调用对应的构造函数。

4. 构造函数可以重载。


再举个例子:



注意我们创建变量a的时候,调用的并不是无参的,而是全缺省的。


我们不写编译器默认生成的、我们写了什么都不干的、还有我们写的全缺省的,都叫做默认构造函数。


默认构造函数只能有一个。


原因很简单。就比如刚刚笔者写的那两个,如果我把屏蔽的内容去掉,那么我如果不给参数,编译器会根本不知道调用哪个作为自己的构造函数。


我们在后面的模拟实现中会详细介绍和练习。


而显式构造函数如果存在,那么编译器就不会再自动生成一个默认构造函数了。


比如下面这段代码:


这个时候,编译器就会调用我们所写的显式的构造函数。并且默认构造函数不再生成。


关于编译器生成的默认成员函数,可能会有这样的困惑:在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象year/month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么卵用??


实际上...emmm...在我看来,确实没有什么卵用。。。


不过要是硬要说它有那么点用,也是可以的。就比如,在私有类中出现了自定义类型,这个时候,它会调用其默认构造函数,这样,就实际上相当于提前构造好了一个自定义类型出来。


还有后面我们在说到动态内存分配的时候,我们也会提到,用new定义自定义类型的时候其好处就有可以调用自己的构造和析构函数。


9-1-3 初始化赋值

我们知道,在创建类的时候,编译器会为我们调用构造函数。那么下面这样调用的构造函数算不算是初始化呢?

#include<iostream>
using namespace std;
class A
{
public:
  A(int a,int b)
  {
  _a = a;
  _b = b;
  }
private:
  int _a;
  int _b;
};
int main()
{
  A a(2, 3);
  return 0;
}


注意,虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称作为类对象成员的初始化,构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。


那怎么样才能算叫做初始化呢?


9-1-4 初始化列表

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。

我们还是以日期类来举例

#include<iostream>
using namespace std;
class Date
{
public:
  Date(int year = 0,int month = 1,int day =1)
  :_year(year)
  ,_month(month)
  ,_day(day)
  {}
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  return 0;
}


如上所示,即为用初始化列表初始化。


初始化列表是以一个冒号开头,逗号分开,每个成员变量后跟着一个初始值或者表达式。


注意:每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)


类中包含以下成员,必须放在初始化列表位置进行初始化:


引用成员变量;


const 成员变量;          


自定义类型成员;(该类没有默认的构造函数时)


原因其实很简单,第一第二个是因为引用成员和const修饰的成员变量只能够被初始化一次。第三个就是因为其类没有构造函数。


尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。

成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

9-1-5 explicit关键字

构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用


当加上了explicit关键字之后,其会禁止构造函数进行隐式类型转换。


C++ explicit关键字详解 - 矮油~ - 博客园 (cnblogs.com)(这篇文章详细地说了这个关系,写得不错,可以参考参考)


这样如果类型不一致,都是不允许通过的。


9-2 析构函数

实际上,构造函数和析构函数是一对。


为什么呢?


构造函数是初始化对象,把对象中的值弄出来;


那这些对象的数据是怎么没的呢?调用的就是析构函数。


注意:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作


就是说,该置空的置空,该变成0的变成0 之类的


9-2-1 特性

1. 析构函数名是在类名前加上字符 ~。

2. 无参数无返回值。

3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。

4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。


关于编译器自动生成的析构函数,是否会完成一些事情呢?


和拷贝构造一样。对于自定义类型,编译器在结束的时候就会调用其析构函数。这一点,我们在动态内存的章节也会进行讲解


9-3 拷贝构造函数

只有单个形参,该形参是对 本类类型对象 的引用(一般常用const修饰),


并且 在用已存在的类类型对象 创建新对象时 由编译器自动调用。


拷贝构造函数的特点

1. 拷贝构造函数是构造函数的一个重载形式。

2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。

3. 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝


有的时候,当我们需要完成深拷贝的时候,我们是需要自己去写拷贝构造的。


解释一下上述的第二点:

image.png



假如说,这里不加&,那么,当下面调用这个拷贝构造函数的时候,首先会把一个类拷贝给这个临时对象d,这个时候,便又调用了一次拷贝构造。那么就需要再次调用拷贝构造。而调用这个拷贝构造函数,其又需要传值,又会进行一次拷贝,便又会调用一次拷贝构造函数.....无穷无尽,无限循环。


9-4 运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型和参数列表与普通的函数类似。


函数名为:关键字operator后面接需要重载的运算符符号。


函数原型:返回值类型 operator操作符(参数列表)


9-4-1 特性

注意:


不能通过连接其他符号来创建新的操作符:比如operator@

重载操作符必须有一个类类型或者枚举类型的操作数

用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义

作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的

操作符有一个默认的形参this,限定为第一个形参

.* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。

就是说,你只能把其用法创新,不可以创造新的运算符。并且创新要合理。


我们来举一个简单的例子:



如图,这里我们就调用了运算符重载函数 operator== 来完成本不可以完成的两个类的数据元素的判断。


9-4-2 赋值运算符重载

需要注意,赋值运算符有几个特点:


1. 参数类型

2. 返回值

3. 检测是否自己给自己赋值

4. 返回*this

5. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。


我们再来举一个例子:


十、const成员

10-1 const修饰类的成员函数

将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改


就比如


image.png


左边是我们写的函数,而右边是编译器实际转换成的函数


注意 以下几点:


1. const对象可以调用非const成员函数。

2. 非const对象不可以调用const成员函数。

3. const成员函数内可以调用其它的非const成员函数。

4. 非const成员函数内不可以调用其它的const成员函数。


注意,const和&这两个运算符一般不需要重载,编译器会默认生成。


十一、static成员

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。


静态的成员变量一定要在类外进行初始化。因为在类里面并不能够直接访问到。其静态成员函数没有this指针。


因此说,


1. 静态成员为所有 类对象 所共享,不属于某个具体 类对象 的实例

2. 静态成员变量 必须在类外定义 ,定义时不添加static关键字

3. 类静态成员即可用类名::静态成员 或者 对象.静态成员 来访问(直接访问)  

4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员

5. 静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值


由于静态成员变量没有this指针,所以,静态成员变量是无法访问非静态成员变量的。


注意:对于非静态成员变量来说,其在C++11的标准下,在声明的时候可以给上缺省值。


十二、友元( 关键字friend )

总的来说,


友元分为:友元函数和友元类


12-1 友元函数

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用


友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。


那么其到底怎么使用呢?


现在我们尝试去重载operator<<,然后发现我们没办法将operator<<重载成成员函数。因为cout的输出流对象的隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。


所以我们要将operator<<重载成全局函数。


但是这样的话,又会导致类外没办法访问成员,那么这里就需要友元来解决。operator>>同理。


也就是说,我们 需要这个样子:

微信图片_20221209140928.png


#include<iostream>
using namespace std;
class Date
{
public:
friend ostream& operator<<(ostream& cout, Date& d);
  Date(int year = 0,int month = 1,int day =1)
  :_year(year)
  ,_month(month)
  ,_day(day)
  {}
  bool operator==(const Date& d)
  {
  return (_year == d._year) && (_day == d._day) && (_month == d._month);
  }
  Date& operator=(const Date& d)
  {
  if (this != &d)
  {
    _year = d._year;
    _month = d._month;
    _day = d._day;
  }
  return *this;
  }
private:
  int _year;
  int _month;
  int _day;
};
ostream& operator<<(ostream&cout ,const Date& d)  //不属于任何类,所以不需要加   Date::
{
  cout << d._year << " " << d._month << " " << d._day << endl;  //可以直接访问类的私有成员
}
int main()
{
  Date d1;
  Date d2(2022, 1, 14);
  cout << (d1 == d2) << endl;
  return 0;
}


再次说明:


友元函数可访问类的私有和保护成员,但不是类的成员函数

友元函数不能用const修饰

友元函数可以在类定义的任何地方声明,不受类访问限定符限制

一个函数可以是多个类的友元函数

友元函数的调用与普通函数的调用和原理相同



12-2 友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。


友元关系是单向的,不具有交换性。


比如有Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。



友元关系不能传递


如果B是A的友元,C是B的友元,则不能说明C时A的友元。


十三、内部类

如果一个类定义在另一个类的内部,这个内部类就叫做内部类。(相当于是嵌套的)


就相当于这样:



注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。


而内部类就是外部类的友元类。就是可以访问外部类。


特性:


1.内部类可以定义在外部类的public、protected、private都是可以的。

2. 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。

3. sizeof(外部类)=外部类,和内部类没有任何关系


好,到此为止,我们将类的所有内容暂时告一段落。即基本讲完。


接下来,我们就要用刚刚所讲的知识,模拟实现Date类。


十四、Date类的模拟实现

我们主要干以下几个事情:


写个构造;写几个操作符重载的函数;再写个打印的函数。赋值重载和拷贝构造可以不用写,因为这里的是浅拷贝,编译器默认生成的就够用。


具体来说,是以下这么多个函数:

Date(int year = 0, int month = 1, int day = 1);
  void print();
  int operator-(const Date& d);
  Date& operator+=(int day);
  Date& operator-=(int day);
  Date operator+(int day);
  Date operator-(int day);
  Date& operator++();
  Date operator++(int);
  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类。然后写出公有和私有的成员,该声明的声明。

微信图片_20221209141106.png



接着,是各个函数的具体实现。


我们来详细介绍一下:


首先是构造函数:


函数1:Date::Date(int year, int month, int day)   //构造函数

inline int GetMonthDay(int year, int month)
{
  static int a[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
  int day = a[month];
  if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
  {
  day = 29;
  }
  return day;
}//辅助函数
Date::Date(int year, int month, int day)   //构造函数
{
  if (year >= 0 && month > 0 && month < 13 && day > 0
  && day < GetMonthDay(year, month))
  {
  _month = month;
  _year = year;
  _day = day;
  }
  else
  {
  cout << "非法日期" << endl;
  cout << year << "年" << month << "月" << day << "日" << endl;
  }
}



注意这里的GetMonthDay,是我们为了辅助用的另一个函数。就是为了给出年月,能得出本月共有多少天。


函数2:void Date::print()//打印函数

void Date::print()
{
  cout << _year << "年" << _month << "月" << _day<<"日"<<endl;
}


 函数3:  int operator-(const Date& d); //两个日期相减

(这个函数实现的时候,用到了下面的操作符重载函数)

int Date::operator-(const Date& d)
{
  Date max = *this;
  Date min = d;
  int flag = 1;
  if (max < min)
  {
  max = d;
  min = *this;
  flag = -1;
  }                  
  int n = 0;
  while (min != max)  //循环遍历自增,直到相等为止
  {
  ++min;
  ++n;
  }
  return n;
}



 函数4:  Date& operator+=(int day);//一个日期加上day天,返回Date

Date& Date::operator+=(int day)
{
  if (day > 0)
  {
  _day += day;
  while (_day > GetMonthDay(_year, _month))
  {
    _day -= GetMonthDay(_year, _month);
    _month++;
    if (_month > 12)
    {
    ++_year;
    _month = 1;
    }
  }
  return *this;
  }
  else
  {
  *this += -day;
  return *this;
  }
}



  函数5:  Date& operator-=(int day); //一个日期减去day天,返回date

Date& Date::operator-=(int day)
{
  if (day < 0) 
  {
  *this += -day;//复用
  return *this;
  }
  else
  {
  _day -= -day;
  while (_day <= 0)
  {
    --_month;
    if (_month == 0)
    {
    --_year;
    _month = 12;
    }
    _day += GetMonthDay(_year, _month);
  }
  return *this;
  }
}


 函数6:  Date operator+(int day);   //一个日期加上day天,返回值

Date Date::operator+(int day)
{
  Date ret(*this);
  ret += day;
  /*while (_day > GetMonthDay(_year, _month))
  {
  _day -= GetMonthDay(_year, _month);
  _month++;
  if (_month > 12)
  {
    ++_year;
    _month = 1;
  }
  }*/       //也可以用屏蔽的内容。这个时候就不需要重载加等了
  return ret;//这里就是要用传指返回
}



 函数7:   Date operator-(int day);//一个日期减去day天,返回值

Date Date::operator-(int day)
{
  Date temp = *this;
  temp -= day;//temp.operator-=(&temp,day)
  return temp;
}



 函数8:   Date& operator++();    //自增;前置

Date& Date::operator++()
{
  *this += 1;
  return *this;  //返回的是一个类
}


 函数9:   Date operator++(int);  //自增;后置

Date Date::operator++(int)
{
  Date temp(*this);  //返回的值没有加等1,而this指向的类加等了1
  *this += 1;        //因为返回值是要用的,而这时后置加加,不会用到加一的值
  return temp;      //得到的是一个新的类 ;其会再次调用一次拷贝构造。
}


剩余函数——用于判断日期前后

会发现,当一个操作符写出来之后,其他的都可以去进行复用。


这也是一个非常巧妙的技巧。

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;
    }
  }
  }
  else
  {
  return false;
  }
}
bool Date::operator<(const Date& d)
{
  if (*this > d)
  {
  return true;
  }
  else
  {
  return false;
  }
}
bool Date::operator>=(const Date& d)
{
  return *this > d || *this == d;
}
bool Date::operator==(const Date& d)
{
  if (*this == d)
  {
  return true;
  }
  else
  {
  return false;
  }
}
bool Date::operator<=(const Date& d)
{
  if (*this > d)
  {
  return false;
  }
  else
  {
  return true;
  }
}
bool Date::operator!=(const Date& d)
{
  if (*this == d)
  {
  return false;
  }
  else
  {
  return true;
  }
  }//复用yyds!!!!!!


好啦,本章的内容先到此为止吧,我们下节再见。


目录
相关文章
|
25天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
41 2
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
83 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
80 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
88 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
31 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
32 4
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
26 1
|
2月前
|
存储 编译器 C语言
【C++打怪之路Lv3】-- 类和对象(上)
【C++打怪之路Lv3】-- 类和对象(上)
18 0
|
2月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
2月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)