C++类和对象(中):默认成员函数、构造与析构、拷贝构造、运算符重载

目录

1.前言

[2. 类的默认成员函数](#2. 类的默认成员函数)

2.构造函数

[2.2 构造函数概念和举例](#2.2 构造函数概念和举例)

[2.3.1 无参构造函数](#2.3.1 无参构造函数)

[2.3.2 默认构造](#2.3.2 默认构造)

[2.3.3 全缺省构造](#2.3.3 全缺省构造)

[2.3.4 默认构造的函数类型](#2.3.4 默认构造的函数类型)

3.析构函数

[3.1 为什么要有析构函数](#3.1 为什么要有析构函数)

[3.2 析构函数的概念和举例](#3.2 析构函数的概念和举例)

[3.3 析构函数的特点](#3.3 析构函数的特点)

3.4自定义类型成员一定会自动析构

[3.5 判断是否要写析构函数](#3.5 判断是否要写析构函数)

4.拷贝构造(重点)

[4.1 拷贝构造的概念](#4.1 拷贝构造的概念)

[4.2 拷贝构造的特征](#4.2 拷贝构造的特征)

[4.3 是否必须进行拷贝构造](#4.3 是否必须进行拷贝构造)

[4.4 拷贝构造函数典型调用场景:](#4.4 拷贝构造函数典型调用场景:)

总结:

5.赋值运算符重载

[5.1 运算符重载的概念](#5.1 运算符重载的概念)

[5.2 运算符重载有些需要注意的地方](#5.2 运算符重载有些需要注意的地方)

[5.3 为什么运算符重载既能够全局又能在类里(有必要搞清楚)](#5.3 为什么运算符重载既能够全局又能在类里(有必要搞清楚))

[5.4 赋值运算符](#5.4 赋值运算符)

[5.4.1 赋值运算符的格式](#5.4.1 赋值运算符的格式)

[5.4.2 赋值运算符只能重载成类的成员函数不能重载成全局函数](#5.4.2 赋值运算符只能重载成类的成员函数不能重载成全局函数)

[5.4.3 何时用](#5.4.3 何时用)

[5.5 前置++和后置++](#5.5 前置++和后置++)

6.日期类实现需要注意的

[6.1 流提取和流插入](#6.1 流提取和流插入)

[6.2 获取某月天数](#6.2 获取某月天数)

[6.3 +=和+的区别](#6.3 +=和+的区别)

[6.4 两个日期相减(计算天数)](#6.4 两个日期相减(计算天数))

[6.5 为啥<<和>>要友元](#6.5 为啥<<和>>要友元)

7.const成员

[7.1 const 成员函数](#7.1 const 成员函数)

[7.2 取地址运算符重载](#7.2 取地址运算符重载)


1.前言

上文介绍了类和对象的一部分,算是C++的开胃小菜,不是特别难,在这一部分中有一些难以理解的知识点,需要我们两次甚至三次反复学习才能掌握的,一次是肯定不够的,我也是第一遍懵懵懂懂,第二次才拨云见雾,在上面的思维导图中,重点在于构造函数和析构函数,然后是拷贝构造函数和赋值运算符重载函数,然后我们会有个日期类的实现。

2. 类的默认成员函数

类和对象中有6个默认成员函数,但是比较重要的还是前面4个,尤其重要,需要反复学习观看,反复操练。

默认成员函数 作用
1.构造函数 完成对象的初始化工作
2.析构函数 完成对象的清理释放工作
3.拷贝构造函数 用已经有的对象去初始化一个新的对象
4.赋值运算重载 用一个对象去赋值另一个对象
5.const成员函数 不修改对象的数据
6.取地址操作符运算重载 获取对象地址和获取const对象地址

默认成员函数,就是用户没有显式自定义,让编译器自己去生成的构造函数叫默认成员函数。

复制代码
class func{};

2.构造函数

cpp 复制代码
class Date
{
public:
 void Init(int year, int month, int day)
 {
    _year = year;
    _month = month;
    _day = day;
 }
int main()
{
 Date d1;
 d1.Init(2026, 7, 5);
 return 0;
}
 

我们之前在创建栈的时候,或者创建日期类时,每次创建一个对象都要调用一个函数去初始化,设定相关信息,那么能不能在对象实例化的时候不去申请调用,而是编译器自发的去完成初始化呢,(我写的时候及其容易忘记调用初始化导致出错)于是前辈们就想到构造函数

2.2 构造函数概念和举例

是特殊成员函数,不是字面上的去开空间创造一个对象,而是自动去进行对象的初始化。构造函数的名字和类名相同,不带返回值,甚至可以不带参数。它在创建类对象时是自动调用,且只会调用一次。

那么有了构造函数后就会很方便了,我们之前在创建栈的时候,啥都写好了,可能就是会忘记去写一个,就会导致出现错误,程序运行不正常。现在有了构造函数,自动来,妈妈再也不用担心我粗心忘记了!

cpp 复制代码
st.Init();//忘记调用了,没有初始化
cpp 复制代码
 class Date
 {
  public:
      // 1.无参构造函数
     Date()
     {}
 };
 void TestDate()
 {
      Date d1; // 调用无参构造函数
 }

这是构造函数(无参情况),最简单的举例。

  1. 函数名与类名相同。
  2. 没有返回值
  3. 对象实例化时自动调用。
  4. 构造函数可以重载。
  5. 不写任何构造函数,C++编译器会自动生成一个无参构造函数
  6. 只要是自己定义的构造函数,无论是否带参,编译器就不会生成默认构造函数。

2.3.1 无参构造函数

这是构造函数的基本特点。那么问题来了,上面无参构造函数是不是我们显示定义的,还是说没有显示定义系统给的呢?肯定是显示定义的啊,就算它没有任何参数,那也是用户自己显示定义的,不是什么都没有写。无参构造函数,属于默认构造函数的一种:用户自己写的无参构造函数。

把代码注释掉

2.3.2 默认构造

cpp 复制代码
 class Date
 {
  public:
      // 1.无参构造函数
    /* Date()
     {}*/
 };
 void TestDate()
 {
      Date d1; // 调用无参构造函数
 }

等于我自己不写任何构造函数,C++编译器会自动生成一个无参构造函数,也是属于默认构造的一种,这个函数是个空函数体,不初始化,只是创建对象。

PS:但是要注意,无参构造函数后面不能加括号,不然会造成歧义,因为编译器不知道这是构造函数还是函数声明,比如以下代码就是会warning。

还有一种是全缺省构造函数,我们已经知道什么是全缺省了,那么之前铺的路就为这里打好了基础

2.3.3 全缺省构造

cpp 复制代码
	// 1.全缺
	Date(int year=2026,int month=8,int day=2)
	{
		_year = year;
		_month = month;
		_day = day;
	}

int main()
{
	Date d1; // 全缺省
}

至此,总结一下,C++中的默认构造函数有三个:共同点是都不传参。

默认构造函数 是否显示
无参构造 用户自己构造的(显示)
全缺省构造函数 用户显示构造
默认构造(编译器) 没有显示定义

但是这三个函数,尤其是第一个和第二个,因为都不传参,虽然是函数重载了,但是只能二选一,不然编译器不知道该怎么选。报错原因是包含多个默认构造

结论:默认构造函数有三个但是只能选择一个使用。

2.3.4 默认构造的函数类型

编译器默认生成的构造函数有内置类型的函数和自定义类型的,为什么会说这两个类型,就是我感觉在日期类的自定义中,不去自己编写构造函数的情况下,编译器生成默认构造函数看不到,但结果是随机值,这不是我想要的,所以编译器生成的默认构造函数没啥用感觉?然后我就带着问题去查了一下。

比较综合的答案是: C++把类型分为内置类型和自定义类型。

内置类型:语言类,int/char/double

自定义类型:struct,class等

看看编译器对自定义成员是怎么调用的:

cpp 复制代码
class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:

	int _hour;
	int _minute;
	int _second;
};
int main()
{
	Time d;
	return 0;
}

自定义类型中的成员

自定义类型成员会自动调用它自己的默认构造函数。我们可以通过监视窗口去看,但是调试是不会跳进去的。

但是如果我们不写,默认构造函数对内置类型不做处理,在某些编译器中会进行处理,但是我们还是默认他不处理吧,是否初始化是不确定的,这里就没有初始化,如果我强行进行打印,编译器就会报错,是能编译通过,但是无法运行

cpp 复制代码
struct Time
{

	int a;
	double b;
	int* p;
};
int main()
{
	Time d;
	
	return 0;
}

我在监视窗口看了一下d这个对象的内部数据,都是随机的垃圾值,野指针,这样证实了一个事情就是编译器默认构成对内置类型不做处理。

那么为了修补这个问题,当然不是说这个不初始化给修复了,就是做了一层修改,C++11中可以运行在内置类型的声明中加默认值,(全缺省可以在对象传参的时候带参构造函数,这样缺省值就会被替换掉)

cpp 复制代码
class Date
{
private:
 // 基本类型(内置类型)
 int _year = 1970;
 int _month = 1;
 int _day = 1;
 // 自定义类型
 Time _t;
};

3.析构函数

3.1 为什么要有析构函数

cpp 复制代码
void Destroy()
     {
         if (_array)
     {
     free(_array);
     _array = NULL;
     _capacity = 0;
     _size = 0;
     }
int main()
{ 
s.Destroy();
}

构造函数是初始化一个对象,设置好参数。那么有了开始是我们可能会忘记初始化,结束我就有可能会忘记destory,销毁栈空间。前辈既然考虑到前者,后者自然会有,要前后呼应,所以析构函数就诞生。

3.2 析构函数的概念和举例

析构函数:并非完成对对象本身的销毁,而是完成对象内部空间资源的清理释放工作。和构造函数是恰恰相反的。之前的栈空间申请空间后必须要自主申请调用destory

cpp 复制代码
destory(p);

我们难免会忘记,至此,我们就引用析构函数,在构造函数之后。这里会自动调用析构函数,为了显式,我在析构里写了一个输出,在主函数里并没有调用,只是进行了实例化。

cpp 复制代码
class Stack
{
public:
	Stack(size_t capacity=4)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			perror("defeat");
			return;
		}
		
		_capacity = capacity;_size = 0;
	}
	void Push(int data)
	{
		// CheckCapacity();
		_a[_size] = data;
		_size++;
	}
	~Stack()
	{
		cout << "~Stack";
		free(_a);
		_a = nullptr;
		_capacity = _size = 0;
	}
private:
	int* _a;
	size_t _size;
	size_t _capacity;
};
int main()
{
	Stack st;
	st.Push(1);
	st.Push(2);
	return 0;
}

3.3 析构函数的特点

  1. 函数名和类名相同,前面要加~
  2. 不含任何参数,无返回值无返回类型
  3. 析构函数只有一个,不能重载,生命周期结束时自动调用
  4. 要是不显式定义,那么编译器会自动生成析构函数,对内置类型依旧不做处理,自定义会调用它的默认构造函数

但是我会有一个和之前一样的疑问,对于自定义函数,如果我们不写,编译器自动生成默认析构函数,会对自定义成员调用它的析构函数吗?

3.4自定义类型成员一定会自动析构

cpp 复制代码
class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
	}
	~Time()
	{
		cout << "~Time()" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:

	Date()
	{
		cout << "Date()" << endl;
	}
	~Date()
	{
		cout << "~Date()" << endl;
	}
private:
	// 内置类型
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

程序运行结束后会输出~Time(),但是在main函数里没有创建Time类的,为什么会调用属于Time的析构函数呢?

解释如下:Date是自定义类型,main函数创了Date的实例化对象d,d中的private包含了四个对象,其中三个是内置类型,有一个是自定义类类型。内置类型是压根不需要进行资源清理,而_t是time类对象,所以在对象d进行销毁时,要将d里面的time类的_t对象销毁,所以调用time类的析构函数。

但是main中不能直接调用Time类的析构函数,只能销毁Date类对象,所以编译器会调用Date类的析构函数,在代码里我并没显示写Date类的析构,编译器就会自动生成Date类的默认析构函数,从而间接调用Time类的析构函数,那么在销毁清理Date类对象时,就能确保不但内置类型的内存被回收,自定义Time类的对象也会被清理。

结论:编译器并不能直接调用time类析构函数,所以会让编译器自动生成一个Date类的默认析构函数,从而再内部调用Time类析构完成目的。

它的完整执行流程是

  1. 实例化对象d,构造Date对象
  2. 构造内部成员_t,调用Time的默认构造;
  3. d出作用域,销毁Date对象
  4. 先调用Date类析构函数~Date();
  5. 销毁Date类内部成员_t,是Time类,调用~Time()

我在每个构造和析构都进行一个输出,方便证实:

这里也学习到一个额外的知识:构造顺序是先构造成员再构造本体;析构时反过来,先本体销毁在成员销毁

3.5 判断是否要写析构函数

先总结,就是如果要申请调配资源,那就要调用析构函数;如果没有要申请类里面的资源,那就没必要写。

比如stack类就要申请空间,构造和析构都要写,但是date类就不用写,因为并不涉及到释放资源,可以直接用编译器生成的默认构造。

要析构的:

cpp 复制代码
class Stack
{
public:
	Stack(size_t capacity=4)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			perror("defeat");
		return;
		}
    }
}

_a是指针,指向一块堆中申请的空间,需要析构函数来释放清理资源

不用析构的:

cpp 复制代码
class Date
{
private:
    int _year;
    int _month;
    int _day;
};

并没有申请空间资源,内置类型只有内存分配,静态资源在栈上,不需要我们去释放。
前面总结:C++中有构造和析构是为了更好的方便管理资源,以免造成资源的清理不及时。C中每个函数部分都进行和初始化销毁,难免会遗漏,那么析构的自动调用就会很方便,不用担心忘记释放的烦恼。

cpp 复制代码
//C
STInit(&st);
STDestroy(&st);

//C++
Stack st;

4.拷贝构造(重点)

4.1 拷贝构造的概念

拷贝构造是构造函数的重载形式,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用

cpp 复制代码
Date d1(2026,8,27);
Date d2(d1);//d2是d1的拷贝

其本质就是用一个已有的对象去初始化另一个对象

4.2 拷贝构造的特征

  1. 拷贝构造是构造函数的一个重载形式
  2. 拷贝构造的参数只有一个且必须引用,使用传值的话就会报错,会引发无穷递归
cpp 复制代码
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// Date(const Date& d)   // 正确写法
   // Date(const Date d)//错误写法
	Date(const Date& d) 
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2(d1);
	return 0;
}

为什么会引发无穷递归呢,因为传值传参首先要调用拷贝构造,那拷贝构造又是传值的话,它不会去拷贝而是再去调用拷贝构造,完成死循环。如果不理解的话也没关系,就记住要传引用就好了。或者用指针也可以,但是看着会比较别扭,因为不那么C++了。

cpp 复制代码
Date(const Date* d) 
{
	_year = d->_year;
	_month = d->_month;
	_day = d->_day;
}
Date d2(&d1);
cpp 复制代码
func(d);//自定义
func(10);//传值
cpp 复制代码
 Date(const Date& d)  
 {
cout<<"date"<<endl;
 _year = d._year;
 _month = d._month;
 _day = d._day;
 }
cpp 复制代码
void func(Date d)
{
    
}
void func(int i)
{
}

上面的代码当参数是自定义类型时,必定会先走拷贝构造,然后输出date,再去执行func(Date d)空函数体。

4.3 是否必须进行拷贝构造

如果不写拷贝构造,编译器会生成默认的拷贝。C++规定:

|--------|----------|
| 成员类型 | 是否拷贝构造 |
| 内置类型成员 | 直接拷贝/浅拷贝 |
| 自定义类型 | 必须进行拷贝构造 |

若未显式定义,编译器是会生成默认的拷贝构造函数:默认的拷贝构造函数对象按内存存储按字节序完成拷贝,不会申请新的空间资源,内置类型只是单纯按字节拷贝,而自定义函数则调用它的默认拷贝(没有源码,只有汇编能看到赋值,不会malloc)。

Date类就是不写也可以完成拷贝,因为它可以浅拷贝

但是栈就必须要自己写拷贝构造

cpp 复制代码
int main()
{
	Date d1(1,1,1);
	Date d2(d1);
	
	Stack st1;
	Stack st2(st1);
	return 0;
}

为什么会崩溃呢,因为实际上st1._a和st2._a指向的同一块空间,浅拷贝会造成两次析构,那第一次析构就释放空间了,第二次析构就会出错,析构的先后顺序是先析构st2,再析构st1,符合后进先出原则。并且浅拷贝还有另外一个问题是一次修改会修改另外变量的值

要解决这个问题就要进行深拷贝,深拷贝就是不会指向同一块堆空间,每个对象有自己的空间

cpp 复制代码
Stack(const Stack&st)//正确的深拷贝,并不是浅拷贝
{
	_a = (int*)malloc(sizeof(int) * st._capacity);
	if (_a == nullptr)
	{
		perror("defeat");
		return;
	}
	memcpy(_a, st._a, sizeof(int) * st._size);
	_capacity = st._capacity; _size = 0;
}

深拷贝比较复杂,再下一节文章会仔细说明。

4.4 拷贝构造函数典型调用场景:

  1. 使用同一个类型的对象进行拷贝到新对象。
  2. 函数参数类型为自定义类型。
  3. 函数的返回值为类对象。
cpp 复制代码
//使用同一个类型的对象进行拷贝到新对象。
Date(int year, int minute, int day)
 {
 cout << "Date(int,int,int):" << this << endl;
 }
//函数参数类型为自定义类型。
 Date(const Date& d)
 {
 cout << "Date(const Date& d):" << this << endl;
 }
//返回值为类对象
Date temp(Date d)
{
    Date d(d);
return d;
}
int main()
{
    Date d1(2026,8,20);
    Date d2(d1);
    temp(d1);
}

当然传引用要避免野引用,这上次说过,这里就不详细赘述了。

总结:

Date和myqueue不需要自己写拷贝构造,因为不存在管理申请资源,一个是内置类型,一个可以调用自己的拷贝构造。栈就不一样了,栈需要自己构造函数,自己析构函数释放资源,就要显示写拷贝构造。

5.赋值运算符重载

5.1 运算符重载的概念

在日期类中,如果想要加上几周或者几天,或着要比较两个日期哪个早,哪个晚,其实比较麻烦,因为在C++还是C中,语法并没有定义这些,只有比较内置类型可以,因为编译器内部可以直接转换成指令,而自定义是没规定。为了方便自定义的计算和可读性,运算符重载诞生。

C++为了代码的可读性引入了运算符重载,它也是具有特殊函数名的函数,有返回类型,运算符函数名,运算符和参数列表。

运算符重载的函数模板为:返回值类型 operator+操作符(形参)

cpp 复制代码
bool operator==(const Date& d1, const Date& d2);//  全局
cpp 复制代码
bool Date::operator==( const Date& d2);// 类成员函数

5.2 运算符重载有些需要注意的地方

  1. 重载操作符必须有一个自定义类型。
  2. 类成员函数重载,形参比操作数看起来是数量少一个,因为第一个参数是隐藏的this指针。
  3. 不能随意添加其他符号新建一个操作符,比如@,#,¥
  4. 不能重载内置类型,这不改变其含义,必须要有一个自定义类型
  5. 是否重载,看运算符对这个类有没有意义。
  6. 运算符可以全局,也可以放在类成员函数里,不过前者就是两个参数都要写,类成员的第一个参数是this,是左操作数。
    第一个:我们不能这样都是内置类型,必须要有一个是自定义类型,这并不会改变内置类型的含义
cpp 复制代码
int operator+(const int x1,const int x2)
{
    return x1+x2;
}

第二个:类成员函数中,形参要比操作数数目看起来少1

cpp 复制代码
class Date
{ 
public:
 Date(int year = 1900, int month = 1, int day = 1)
{
        _year = year;
        _month = month;
        _day = day;
   }
    
    // bool operator==(Date* this, const Date& d2)
    // 这里需要注意的是,左操作数是this,指向调用函数的对象
    bool operator==(const Date& d2)
 {
        return _year == d2._year;
            && _month == d2._month
            && _day == d2._day;
 }
private:
 int _year;
 int _month;
 int _day;
};

int main()
{
    Date d1(2018, 9, 26);
    Date d2(2018, 9, 27);
    cout<<(d1 == d2)<<endl;
}

第三个:是否重载看有无意义

运算符 是否重载
日期-日期 构成,看还相差多少天
日期-天数 构成,算具体日期,还剩多少天
日期+天数 构成,算具体日期,未来第几天
日期+日期 不构成,没get到

第四个:全局函数中,函数参数要齐全不能少

cpp 复制代码
class Date
{ 
public:
 Date(int year = 1900, int month = 1, int day = 1)
   {
        _year = year;
        _month = month;
        _day = day;
   }    
//private:
 int _year;
 int _month;
 int _day;
};

bool operator==(const Date& d1, const Date& d2)
{
    return d1._year == d2._year
   && d1._month == d2._month
        && d1._day == d2._day;
}

第五个:有些运算符不能重载

sizeof, ::, ., ?: ,.*(成员指针访问、作用域、大小、三目、点运算符不能重载。)

第六个:有几个参数个数就有几个操作符,全局是一一对应,类成员函数少一个。

5.3 为什么运算符重载既能够全局又能在类里(有必要搞清楚)

5.4 赋值运算符

赋值运算和拷贝构造需要搞清楚的它们之间的区别,前者是两个已经存在的对象进行拷贝;后者是用一个已经存在的对象去初始化另一个对象。

cpp 复制代码
Date d1=d2;//拷贝构造
d1=d3;//赋值运算

5.4.1 赋值运算符的格式

  1. 参数类型:const T&,传引用可以增加效率
  2. 返回值类型,T&,返回值为引用可以提高返回的效率,支持连续赋值
  3. 返回*this,不存在空间销毁就不存在,*this是类对象本身,作用域不在空间内
  4. 查看是否自己给自己赋值
cpp 复制代码
class Date
{ 
public :
 Date(int year = 1900, int month = 1, int day = 1)
   {
        _year = year;
        _month = month;
        _day = day;
   }
 //拷贝构造
 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;
 }

5.4.2 赋值运算符只能重载成类的成员函数不能重载成全局函数

cpp 复制代码
// 赋值运算符重载成全局函数,重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
 if (&left != &right)
 {
 left._year = right._year;
 left._month = right._month;
 left._day = right._day;
 }
 return left;
}

这里引申出一个问题:

为什么赋值运算符只能在类里面实现而不能在全局重载?
原因:如果赋值运算符不显式实现,那么编译器会生成一个默认赋值运算重载,也就是浅拷贝,那么你再在类外自己实现了一个全局的赋值运算符重载,那么两者就会冲突,所以赋值运算符重载必须在类里面实现。

在没有显式写的话,编译器也会自动生成默认赋值运算符重载,其实和默认拷贝构造类似,以值的方式按字节进行进行拷贝。内置类型是直接赋值,自定义类型调用它自己的默认赋值运算符重载。

5.4.3 何时用

这里也和拷贝构造一样,就是比如日期类这种用你没有写,但是编译器自己生成的默认赋值运算重载已经可以完成值拷贝了,就没必要。但是比如栈这种就必要了,浅拷贝是会崩溃的,必须要进行深拷贝。

关键识别:看是否涉及到资源管理,如果没有,那么赋值运算符是否实现都可以,如果涉及到了,一般都会写显式的构造和析构函数,那么自然也要写上赋值运算符重载。

5.5 前置++和后置++

前置++是返回对象++以后的值,(先+1,再返回引用),而后置++是返回对象++之前的数据(先搞一个临时变量保存之前的值,再去+1),但这个并不会构成函数重载

cpp 复制代码
Date& operator++();
Date  operator++();

后置 ++ : 前置++ 和后置 ++ 都是一元运算符,为了让前置 ++ 与后置 ++ 形成能正确重载
C++ 规定:后置 ++ 重载时多增加 一个 int 类型的参数,但调用函数时该参数不用传递

cpp 复制代码
Date& operator++();
Date operator++(int);
cpp 复制代码
Date& operator++()
{
    *this +=1;
    return *this;
}
//前置++:返回+1之后的结果
// this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
Date operator++(int)
{
    Date tmp =(*this);
    *this +=1;
    return tmp;
}
//后置++,是要先保存旧值,再去+1,故返回的是+1之前的值。所以要用临时变量去保存之前的*this
//而temp是临时变量,所以只能传值返回,不能传引用

6.日期类实现需要注意的

6.1 流提取和流插入

流提取的符号是<<,流插入的符号是>>,这是个双操作数,在全局上需要两个参数,这个在重载时一般为全局函数。如果按照之前习惯的写法

cpp 复制代码
cout<<d;

这个是错误的,它调用的是库里的operator,而不是你自定义的,得用全局函数。

cpp 复制代码
cout.operator<<(d)

为什么这么说,先说cout为啥能够自动识别类型,其实是运算符重载配合上函数重载的功劳。


cout是类ostream的对象<<被重载成了成员函数,编译器看到<<后面的东西会去查它的类型,然后去匹配函数重载中的相关类型。而运算符重载又会形成函数重载,因为只是参数类型不一样。通过函数重载,使得可以支持自定义类型,这个需要注意。

所以这个函数调用的是库里的类,而不是你自定义的Date类中,如果要调用自定义的就要这么写

cpp 复制代码
d.operator<<(cout)

也就是

cpp 复制代码
d<<cout

但是看起来很别捏,正常说是右边的流向左边,但是控制台流向类对象,好像不太对。<<的左操作符是cout,cout是库里的ostream类对象。那么你要去改的话,就要去修改库,但是这是不可能的,你写成员函数是肯定不行,因为Date类左操作数必须是this,右操作数是参数。
所以写成全局函数,让两个参数都显现出来,第一个是流,第二个参数是你的类。

cpp 复制代码
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);

流操作符的第一个参数是左操作数,cout是ostream类对象,cin是istream类对象;第二个参数是右操作数Date类对象d。返回值是ostream&和istream&,为了方便连续输入输出

核心区别就是在于:左操作数的身份是谁,如果是成员函数,第一个参数就必须是隐藏的this,也就是类对象;如果是全局函数,两个操作数就不是锁死的,没有this指针了

再补充一句,流既可以支持内置类型,也可以支持自定义类型,很方便。

6.2 获取某月天数

用哈希来表示,也就是要搞一个数组,来对应月份,记住第一个数据要写0,因为没有0越,从下标为1开始算的

cpp 复制代码
int GetMonthDay[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

6.3 +=和+的区别

+=会修改对象当前的值,要用引用返回

+是不修改对象当前的值,所以不用引用返回,它是返回临时对象

同样-=和-同理,不过多赘述

cpp 复制代码
Date& Date::operator+=(int day)
Date Date::operator+(int day)

6.4 两个日期相减(计算天数)

  1. 先假设默认一个日期大,一个日期小,设定一个标记是1,表示是大的比小的多几天。

2.比较之后,如果原先默认大的小于日期小的那一个,那就更改,同时把标记改为-1,表示小的-大的日期。

3.把小的那个一直加,直到小的和大的相等,那就是相差的日期,再和标记位相乘

6.5 为啥<<和>>要友元

全局函数无法访问私有成员,所以要友元函数,代表着临时访问。

7.const成员

7.1 const 成员函数

const成员的核心宗旨是只要成员函数内部不修改成员变量,都可以加上const(只读模式),这样const和普通对象都可以调用const函数,这里需要注意权限放大缩小平移的原则。

这里我个人可以引申为同级或者上级可以派遣同级或者下级,而下级不能越权指挥。

const修饰成员函数时,放在最后,含义是该成员函数内部不允许修改成员变量

cpp 复制代码
class Date
{
    public:
    void Print()const
    {
    cout<<_year<<"年"<<_month<<"月"<<_day<<
    "日"<<endl;
    }
}

Date* const this 修饰的是this本身不能被改变,const Date* const this修饰的*this不能被改变,也就是this指向的对象也不会被改变。

7.2 取地址运算符重载

并不是很重要,了解即可,分为const对象取地址和普通对象取地址

cpp 复制代码
//普通对象取地址
Date* operator&(Date *this)
{
    return this;
} 
//const对象取地址,两者构成函数重载
const Date* operator&(const Date* this)const

{
    return this
} 

结语:类和对象中就介绍到这里,这个部分还是很难的,感谢大家的观看阅读,如果这篇文章能够给你们带来哪怕一点点收获和解决困惑,那对我而言都是一种动力和成就感。如果本文对你来说有些帮助,可以点赞收藏关注博主,或者有什么不对的请批评指出,非常感谢!

下期预告:类和对象下篇,前面的一切都是在为后面打基础铺垫,所以我觉得还是不要放弃,虽然前面很难,但是后面应该会轻松一点。

相关推荐
weixin_307779131 小时前
一维无粘 Burgers 方程的激波形成问题:MacCormack 格式求解
c++·算法·matlab
Agudamu11611 小时前
AI 视频学习笔记工具拆解:4款工具谁能把视频变成复习资料
人工智能·学习·音视频
HugoStudio_SWAN1 小时前
洛谷 B4500 / B4449 / B3843 凯撒密码、密码强度与密码合规——加密与安全的三道门
c++·学习·程序人生·算法·安全
雾里看山2 小时前
源码目录、构建目录与 out-of-source build
c++·软件构建·cmake
一条破秋裤3 小时前
16_TIM编码器接口测速
笔记·stm32·学习
tju新生代魔迷3 小时前
Verilog HDL 学习笔记(十)| 第10章 时序和延迟
笔记·学习·fpga开发
幼儿园蛋小黄3 小时前
嵌入式学习总结:从入门到实战的完整路径
学习
-To be number.wan3 小时前
编译原理第三章:编译器是怎么“认字“的?——词法分析全解析
学习·编辑器
渡我白衣4 小时前
并查集:基础认识与模拟实现
android·java·javascript·数据结构·c++·算法·并查集