在【守望先锋】学习C++的类与对象

C++是一门OOP(面向对象)的语言。
而C语言只是一门面向过程的语言。
里面有些思路需要重新改变。

前情提要:文章较长,请根据目录自行选择

一、守望先锋

在这里插入图片描述

天使姐姐不漂亮吗?
在这里插入图片描述

为一个场守望先锋街机先锋统计数据

如果是面向过程的程序员,可能会考虑:

记录每个玩家所选的英雄,击杀人数,对对面伤害量,治疗量,承受伤害量,命中率等等。

同时还要记录每个英雄的移动,释放技能,技能是否命中,技能造成的伤害的计算,甚至还要考虑技能的运动等等。
甚至还要考虑对方的技能对自己的影响,以及队友的技能对自己的影响。

用一个主函数 main 来获取所有数据。
调用另外一些函数来分别计算英雄的移动,技能伤害,技能影响,收到的伤害等等。

再调用另外一部分函数来显示结果。玩过守望先锋的玩家都知道,对于这些数据的 计算,统计,显示,都是瞬时性的,会随着玩家的操作不断变化。而且,一次组队并不是只打一场,可能会由两场左右。
在这里插入图片描述

守望先锋并不是传统的FPS游戏,而是FPS与Moba游戏的结合

而且,对于不同的英雄都要根据英雄的技能去计算,统计不同的数据。
如:安娜要分别统计开镜和不开镜的命中率,以及睡针的命中人数。
法拉要统计火箭弹直接命中人数等等。

在这里插入图片描述
在这里插入图片描述

而不同模式,又会有不同的变化,对英雄的移动,技能的影响也是不同的。
如:战斗狂欢,让所有英雄的血量翻倍,技能CD减半,这就会造成很多不一样的“化学反应”。
像我有一次战斗狂欢打了 1.5h 才打完(路霸一直卡车旁边,都在卡加时),对于普通快速10分钟左右可是相当长的。对于比赛的各个数据上限又该怎么设置?
在这里插入图片描述

而不一样的地图对角色的移动,技能的影响又是不一样的。比如:地形杀

但对于数据怎么办?又要重新统计。这相对于计算机而言都太复杂。(如果是这样,我相信网易的服务器早就崩了)。

总之,对于过程性编程,首先要考虑遵循的步骤,然后要考虑如何表示这些数据。

如果是一位OOP程序员,其不仅要考虑如何表示数据,还要考虑如何使用数据 。

我要跟踪什么呢?当然是每个玩家,因此要有一个对象来表示每个玩家的各个方面的数据。可以选择为各个不同的英雄定义不同的类,在通过类来为玩家创建对象,让计算机执行计算玩家之间的数据交互,可以自动计算。我们要做的仅仅是研究如何跟踪或表示,每个玩家之间的数据交互。
对于OOP程序员,只需为每个不同英雄定义属于他们的类,每次游戏开始,再为每个玩家根据其选择创建对象。再利用算法跟踪玩家之间的数据交互即

这不比面向过程编程简单?(不知道我理解的对不对)
在这里插入图片描述
ps:Dv.A爱你呦~

二、对象和类

这个世界太复杂,处理复杂性的方法之一是简化抽象
守望先锋游戏中,通过为每个英雄定义一个类,为每个玩家创建一个对象来统筹数据。
在C++中,用户定义类型指点是实现抽象接口的类的设计。
(说人话就是按照需求,定义类。

1、什么是类

我们都知道建一栋房子首先需要什么?

需要图纸,房子的结构图纸。

在这里插入图片描述

有了图纸,我们就可以根据图纸见很多大同小异的房子(一张图纸建出来的么)。
在这里插入图片描述

在这里插入图片描述

房子可以建很多,而图纸始终是那一张。
这样可以帮助我们更好理解。
在这里插入图片描述

对像是一个实体,而类只是一个自定义的类型。
就像int a;
a 是一个实体变量,在内存中有空间,而 int 只是一个类型,表明 a 的身份。

这样应该理解了吧。
基本类型(内置类型)定义变量完成了三项操作

  1. 决定数据对象需要的内存数量
  2. 决定如何解释内存在的位(long和float在内存中占用的位数相同,但是他们转换成数值的方法不同);
  3. 决定可使用数据对象执行的操作或方法。

对内置类型而言,有关信息全部被内置到编译器中。但是C++在自定义类型是,必须要自己提供所有信息。

类的实例化:就是根据类创建一个对象

在这里插入图片描述

C语言创建一个栈的”类“

C语言也是存在”类“。我们常用的结构体。
比如我们用C语言去是实现一个栈

#define CAP 4
typedef int STData;
typedef struct Stack//结构体用于维护栈
{
	int top;//栈顶标记
	STData* arr;//栈的指针
	int capacity;//栈的容量
}STstack;

这是对于栈的数据的定义,栈就是我们用结构体定义的一个”自定义类型“–”栈类型“。(因为它并不具有自定义类型的全部信息

void InitStack(STstack* st);//栈的初始化
void StackPush(STstack* st, STData n);//元素入栈
STData StackPop(STstack* st);//元素退栈
void StackExpansion(STstack* st);//扩容
int StackEmpty(STstack* st);//判断栈是否为空
void StackDestory(STstack* st);//销毁栈,防止内存泄漏
void StackPrint(STstack* st);//打印栈的元素,但前提是要退栈才能得到元素

这些函数是我们能够对栈执行的操作

但是数据和执行方法都是分离的。

这就导致我们重点关注的是对栈操作的整个过程
就是,我们这一步选择入栈,下一步选择弹栈。

C++创建一个栈的类

我们会将栈看为一个类,也就是一个类型,一个自定义类型。
而一个类型,我们需要两个部分

1,类的成员变量(类的属性)
2,类的成员函数(类的行为/能够执行的操作)

这个也满足上面关于类型的三项操作。

对于C++而言,就不再用结构体这个概念了,该叫
C++将structC语言的结构体 - 升级到 - 类

目前可以认为C++中的类就是C语言的结构体除了定义结构体成员变量还有结构体成员函数
比如:

#define CAP 4
typedef int STData;
struct Stack//结构体用于维护栈
{
	//结构体成员变量
	int top;//栈顶标记
	STData* arr;//栈的指针
	int capacity;//栈的容量
	
	//结构体成员函数
	void InitStack(STData capacity=4);//栈的初始化//缺省
	void StackPush(STData n);//元素入栈
	STData StackPop();//元素退栈
	void StackExpansion();//扩容
	int StackEmpty();//判断栈是否为空
	void StackDestory();//销毁栈,防止内存泄漏
	void StackPrint();//打印栈的元素,但前提是要退栈才能得到元素
};

你目前可以理解成长这样。

而且,其类名就可以是其自定义类型的类名。
直接Stack a1;。这就定义了一个对象。
而且也不用传上面指针过去了,可以a1.Init(),就可以调用那些成员函数。
由于C++兼容C语言,这可以是一个类。但C++会使用class(其中文有类的意思)。

2、类的定义

class ClassName//class是定义类的关键字 后面接一个类的名字
{
//类体:由成员变量和成员函数组成
};

与定义结构体很像。
对于类而言,其中的成员函数。你可以直接在类中定义。也可以在.h文件类中声明,去.c文件中定义。但是,在类中的成员变量,那只是声明,并不是定义。

声明与定义的区别

声明:告诉编译器,我有一个这样的东西在这,但并未实现。
定义:根据声明,去实现这个对象的需求。

形象来说就是:

游戏公司建了一个新游戏的文件夹,并向外发布公告,说未来会发布一款新游戏,也可能文件夹都还没建好。
这就是声明,只是告诉你有,但你不知道游戏剧情、内容、玩法是什么。只知道有这个游戏。

当游戏发售后,你买了。你就可以知道游戏的所有内容。
这就是定义。

声明是对的是一个属性,而定义对的是一个实体。

访问限定符

C++对于类中的成员提供了访问限定符
private(私有),public(公有),protected(保护)
他们描述了对类成员的访问控制

根据类创建的对象,都可以访问到对象类的公有部分,并且只能通过公有函数来访问对象类的私有成员

访问控制,也是对对象类的数据的保护。

OOP编程的主要目标之一是隐藏数据,因此这类数据通常放在私有部分,
而组成类接口的成员函数放在公有部分,否则就无法调用这些函数。

由于C++兼容C语言,且C++对C语言的结构进行了拓展,如

struct A
{};
class A
{};

在C++中,都是类。

两者区别

对于struct创建的类,里面的成员默认公有
对于class 创建的类,里面成员默认私有

例如:创建一个OW英雄-安娜的类
在这里插入图片描述

class Ana
{
public:
	void ShowData();//展示数据
	void Teletherapy(bool input);//远程治疗
	void SleepyNeedle(bool input);//睡针
private:
	char Name[20];//名字
	int Weight;//体重
	int Height;//身高
	double Speed;//移动速度
	double Blood;//血量
	//睡针类 睡针
};

私有数据,我们是不能修改或访问的,
只能通过公有函数去操作,或通过公有函数来了解私有数据的状态。

如果我们能直接访问到私有数据,那么我们也能对私有数据进行修改,但那样不就成开挂了吗?(正常人谁打竞技游戏开挂)。

所以,一般情况下,成员变量都是私有的,想给你用的成员函数是公有的,不希望被调用的函数是私有的。

类大小的计算

根据规则:计算类的大小,是不考虑类中成员函数的,只计算类中成员变量,同时还要考虑结构体内存对齐规则,也就是计算结构体大小的规则。

结构体内存对齐规则

  1. 第一个成员在与结构体偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
    VS中默认的对齐数为8
  3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是
    所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

那就计算一下

#include <iostream> 
class Ana
{
public:
	void ShowData();//展示数据
	void Teletherapy(bool input);//远程治疗
	void SleepyNeedle(bool input);//睡针
private:
	char Name[20];//名字
	int Weight;//体重
	int Height;//身高
	double Speed;//移动速度
	double Blood;//血量
};
int main(void)
{
	Ana a1;
	Ana a2;
	std::cout << "a1 = "<<sizeof(a1)<<std::endl;
	std::cout << "a2 = "<<sizeof(a2)<< std::endl;
	return 0;
}

创建了两个对象。
在这里插入图片描述
编译器输出结果。

来看看其内存的概念图

在这里插入图片描述

对于类中的成员函数是不会进行计算大小的。

3、this指针

在这里插入图片描述

这也是类中一个比较重要的知识点

通过一个日期类来分析(因为日期类比较简单)

简单的日期类

#include <iostream>
class Date
{
public:
	void Init(int year = 1, int month = 1, int day = 1)//缺省参数
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
	void print(void)
	{
		std::cout << _year << "年" << _month << "月" << _day<<"日"<<std::endl;
	}
	
private:
	int _year;
	int _month;
	int _day;
};
int main(void)
{
	Date d1;//类的实例化
	Date d2;
	
	d1.Init(2002,2,5);//对象d1初始化
	d2.Init(2019,4,1);//对象d2初始化
	
	d1.print();//打印
	d2.print();//打印
}

输出结果
在这里插入图片描述

在调用对象类的成员函数初始化对象 d1,d2 。
在成员函数中可以访问到对象封装的成员变量。

再介绍一下
对于类的实例化,创建对象。

	Date d1;//类的实例化
	Date d2;

系统会为对象d1 d2中分配空间。
但是只是对象中的成员变量分配在该对象的空间内。

成员变量与成员函数的空间分布

但是对于成员函数,其是在一个代码公共区段,而不是在每个对象空间的内部。不会每调用一次就分配一块空间。
每个对象都可以访问那个区段
在这里插入图片描述

this指针的分析

我们就该想,当调用函数时,进入公共区段。如何确定调用的是那个对象的成员变量呢?
其实,传递过去的参数,不仅仅有显示声明定义的参数,还有一个隐藏的this指针。
即,C++编译器会为每一个非静态成员函数配备一个隐藏的指针参数。

该指针指向当前对象(函数运行时调用该函数的对象)。

在这里插入图片描述

在这里插入图片描述

当然,你也可以这样写

void Init(int year = 1, int month = 1, int day = 1)//缺省参数
{
	this->_year = year;
	this->_month = month;
	this->_day = day;
}
void print(void)
{
	std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;
}

但是我们不能自己在参数列表中加一个this指针,因为编译器自己会传递,会处理。要是自己加了,就会造成参数缺失。

这样也是允许的,不容易搞混。

成员函数调用的真实样子

d1.Init(2002, 2, 5);//->Init(&d1,2002, 2, 5);
d2.Init(2019, 4, 1);//->Init(&d2,2019, 4, 1);

d1.print();//->print(&d1)
d2.print();//->print(&d2)

图片更好看一些。
在这里插入图片描述

注意

而且,对于类的成员变量,最好命名独特一点,不然,如果和缺省参数名字一样,编译器会无法识别,this指针指向不明,从而报错

this指针的特性

  1. this指针的类型:类型const
  2. 只能在成员函数的内部使用
  3. this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
  4. this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递

this指针储存在哪?

就我目前微薄的知识,this指针是储存在栈中的。
因为,this指针毕竟是一个形参。而形参和局部变量都是储存在栈中的。
可以随着成员函数的调用而创建函数结束就销毁。但是不同编译器是按照不同的规则的,比如VS就是通过ecx寄存器自动传递。

看看这个,来理解

class A
{ 
public:
 	void PrintA() 
 	{
 		cout<<_a<<endl;
 	}
 
	 void Show()
	 {
 		cout<<"Show()"<<endl;
	 }
private:
 	int _a;
};
int main()
{
 	A* p = nullptr;
	p->PrintA(); 
	p->Show();
}

创建一个对象指针,并把它初始化为空。

p->PrintA();如果this->_a是行不通的,本身就是空指针,指向空,根本就不会有指向有权限的空间。算是非法访问

但是p->Show();,并未访问对象内,而是访问到类的公共区段。不会造成非法访问。

三、类的六个默认成员函数

类的默认成员函数即使我们

不自己声明定义,编译器也会自动创建定义

对于一个什么成员函数都没有的类,是一个空类。但是,当编译器处理时,会自动生成6个默认成员函数来防止出错。
在这里插入图片描述
事实上,真正用处大的是前4个,后面两个,基本上没多大用处。

1、 构造函数

该函数可以完成对对象的初始化。
C++提供这个默认成员函数。是为了解决,没有初始化就使用对象的问题。

对于日期类

class Date
{
public:
	//void Init(int year = 1, int month = 1, int day = 1)//缺省参数
	//{
	//	this->_year = year;
	//	this->_month = month;
	//	this->_day = day;
	//}

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

private:
	int _year;
	int _month;
	int _day;
};

如果没有Init()函数,如果是C语言的话,就会报错,因为没有初始化。
就比如这样

int main(void)
{
	
	Date d1;
	Date d2;
	d1.print();//->print(&d1)
	d2.print();//->print(&d2)
}

我们没有初始化,却直接打印。事实上,编译器输出
在这里插入图片描述
虽然没有报错,但是却输出了随机值。这样也是防止了程序直接崩溃的问题。

构造函数的概念

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

这是一个默认成员函数,意思就是如果我们自己不定义,系统就会自动生成。
加上构造函数

Date(int year = 1, int month = 1, int day = 1)
{
	_year = year;
	_month = month;
	_day = day;
}

输出结果
在这里插入图片描述

如果我们自己不定义,系统会自动生成,我们定义,系统就调用我们定义的函数。

什么?你不相信系统调用了构造函数

那我就加一个打印,来看看结果

Date(int year = 1, int month = 1, int day = 1)
{
	std::cout << "Date()" << std::endl;
	_year = year;
	_month = month;
	_day = day;
}

看看输出结果:
在这里插入图片描述
现在相信了吧。
在这里插入图片描述

构造函数的特性

构造函数虽然名字感觉像是负责为对象创建分配空间,
但实际上,起作用只是完成对对象的成员变量的初始化,防止未初始化就使用而造成的程序崩溃。

特性

  1. 名字与类名一样
  2. 函数无返回值(注意:是无返回值,void是返回一个空,实际上还是有返回值,而构造函数其根本就没有返回类型)
  3. 在对象实例化时,编译器会自动调用对应的构造函数(我们没定义就调用默认的)
  4. 构造函数可以重载

虽然可以重载
但是

Date()
{
	std::cout << "Date()" << std::endl;
	_year = 10;
	_month = 1;
	_day = 1;
}
Date(int year=1, int month = 1, int day = 1)
{
	std::cout << "Date()" << std::endl;
	_year = year;
	_month = month;
	_day = day;
}
	

却不受欢迎的,因为编译器也不知道如何处理。会报错。

但可以这样

Date(int year,int month = 1, int day = 1)
{
	std::cout << "Date()" << std::endl;
	_year = year;
	_month = month;
	_day = day;
}

在初始化的时候,自己无论如何都要提供一个值。

或者

Date()
{
	std::cout << "Date()" << std::endl;
	_year = 10;
	_month = 1;
	_day = 1;
}
Date(int year, int month, int day)
{
	std::cout << "Date()" << std::endl;
	_year = year;
	_month = month;
	_day = day;
}

可以运行,但还不如全缺省的构造函数。

自定义类型与内置类型

内置类型:内置类型就是语法已经定义好的类型:如int/char…,
自定义类型:是我们使用class/struct/union自己定义的类型

对于普通的成员变量而言,其默认的构造函数,感觉效果并不怎样。但是,如果有一个成员变量也是类?(自定义类型)。

#include <iostream>
class A
{
public:
	A()
	{
		std::cout << "A()" << std::endl;
	}
private:
	int a;
};
class Date
{
public:
	
	void print(void)
	{
		std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;
	}

private:
	int _year;
	int _month;
	int _day;
	A a1;
};
int main(void)
{
	
	Date d1;
	Date d2;
	d1.print();//->print(&d1)
	d2.print();//->print(&d2)
}

其输出结果为
在这里插入图片描述
像上面那段代码,对于类Date,我们自己没有定义构造函数,而编译器会生成默认构造函数,取对成员变量初始化。

而对于内置类型,直接初始化一个随机值,防止程序崩溃。

但是对于自定义类型,编译去会取调用它的构造函数对自定义类型的成员变量进行初始化

我定义了A的构造函数是为了可以更好的看到自定义类型的构造函数被调用。

对于内置类型,编译器基本不会处理,
对于自定义类型,编译器会去调用自定义类型的构造函数

大多数情况下,默认构造函数都不太顶用,最好还是自己写一个满足要求的。

对于构造函数的建议

一般而言,构造函数有三种(不需要传递参数就可以调用的函数

  1. 编译器默认生成的(直接赋随机值的那种)
  2. 我们自己写的无参数的
Date()
{
	_year = 10;
	_month = 1;
	_day = 1;
}

3.我们自己写的全缺省的

Date(int year=1, int month=1, int day=1)
{
	_year = year;
	_month = month;
	_day = day;
}

不过,就我而言,还是推荐使用全缺省的构造函数
因为,这个完全可以兼容前面两种,

既可以带参,
也可以不带参,
还可以带部分参

岂不美哉!!!!!
在这里插入图片描述
猩猩看了都说好

2、析构函数

这个和名字其实不太一样,析构函数并不是负责销毁对象的空间,只是做一些,资源清理的工作。

就跟内置类型的对象一样,类对象的销毁都是由编译器完成的,其本身不具备这种操作。

析构函数的定义

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

析构函数的特性

特征

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值。不能重载。
  3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

对于日期类,额,这个析构函数基本没什么作用。但是对于栈、队列等数据结构的类还是相当有意义的。

清理资源可以防止内存泄漏等等。

比如数组栈调用结束后,析构函数就会自动调用,去销毁其数组,就不会因为我们忘记调用free()而造成内存泄漏

对于内置类型,析构函数不会起什么作用
对于自定义类型,析构函数会清理对象的资源,进行析构。

多个对象,析构函数调用的顺序

对于日期类

class Date
{
public:
	
	Date(int year=1, int month=1, int day=1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void print(void)
	{
		std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
int main(void)
{
	
	Date d1(2002);
	Date d2;
	Date d3;
	Date d4;
	d1.print();//->print(&d1)
	d2.print();//->print(&d2)
}

对象d1 d2 d3 d4都是通过函数调用在为对象分配空间,当其生命周期结束后,编译器就会销毁对象在栈上的空间。

但是,对象销毁的顺序是什么呢?数据结构-栈

要知道,这几个对象都是存放在栈上的。

对于操作系统中的栈与数据结构中的栈,基本没啥关系。但是两者都符合后进先出的条件
在这里插入图片描述

我们知道数据结构对于数据元素的处理规则是后进先出。则,对于在栈上的对象而言,也会符合这个要求。

在生命周期结束后,
先创建的对象,后调用析构函数;
后创建的对象,先调用析构函数。


析构函数调用顺序

  1. d4
  2. d3
  3. d2
  4. d1

要注意,当生命周期到的时候。

3、拷贝构造函数

在这里插入图片描述
回声开大:人格复制
就是在一定时间内(好像最近时间又削短了),回声可以复制敌方任意一个英雄,并拥有其所有技能(充能时间也大大缩短)。

就相当于,在这一定时间内,你所操作的英雄换了一个类对象。

或者说,
对面一个被你选中的英雄的所有对象数据被你Ctrl C + Ctrl V。完全被你拷贝过来了。

换句话说,在这一定时间内,你根据他的类,创建了一个一模一样的对象。

拷贝构造函数的特性

特性如下:

  1. 拷贝构造函数是构造函数的一个重载形式。即函数名也是类名。不过其参数列表是与对象有关。
  2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用

为什么传值传参会引发无穷递归调用

正常的拷贝构造-传引用调用
对于引用的介绍请参考C++入门语法

Date(Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}
Date d1;
Date d2(d1);

如果是传值调用

Date(Date d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

因为,
对于形参要开辟一个对象空间,再将实参对象传递过来,将实参拷贝到这个形参空间,但,又需要调用拷贝构造函数,又需要开辟一个形参空间。。。。就这样不断拷贝,不断开辟空间,无限递归。

概念图
在这里插入图片描述

而对于传引用调用
传递的是要拷贝的对象的别名。只是拿到了目标对象的别名,可以对其进行访问,并且,这个过程并没有再开辟空间

其实,也可以传址调用

Date(Date* d)
{
	_year = d->_year;
	_month = d->_month;
	_day = d->_day;
}

Date d1;
Date d2(&d1);

虽然也可以达到拷贝的效果,但是,其语法上并不是拷贝构造函数,而且比较麻烦,毕竟指针这容易出错。

拷贝构造与传引用

如果是对于一个对象而言,

void test1(Date d)
{

}
void test2(Date& d)
{

}
test1(d1);
test2(d1);

对于test1()而言,是传值调用,要先开辟一个空间,将对象d1拷贝过去,就需要调用拷贝构造函数,
但是test2(),是传引用,直接就可以访问到源空间,根本不需要调用拷贝构造函数。这样对于效率也要快好多。

而且,拷贝构造毕竟是传引用,如果不小心会改变源空间的值,因为拷贝构造也是在类的公共区段,是在类内,不会被访问限定。
那么最好加一个const去防止源对象的成员变量被修改。

Date(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

这样,哪怕写反了复制顺序,编译器自己也会报错,而不会影响程序。

深拷贝与浅拷贝

拷贝构造也是一个默认成员函数
如果我们不自己定义,编译器也会自动生成。

如果是一个日期类,类的成员变量全部都是内置类型的那种。编译器会自动生成一个拷贝构造函数,一个个直接拷贝过去,简称:值拷贝浅拷贝

#include <iostream>

class A
{
public:
	A()
	{
		std::cout << "A()" << std::endl;
	}
private:
	int a;
};
class Date
{
public:
	
	Date(int year=1, int month=1, int day=1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void print(void)
	{
		std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;
	}

private:
	int _year;
	int _month;
	int _day;

};

int main(void)
{
	
	Date d1(2002);
	Date d2(d1);
	d1.print();//->print(&d1)
	d2.print();//->print(&d2)
}

在这里插入图片描述
这就是输出结果。依旧完成了拷贝构造。

但是对于一些自定义类型而言会出大问题。
比如:数组栈类

class Stack
{
private:
	int* arr;
	int size;
	int capacoty;
};

当其对于栈对象进行拷贝构造,对这三个内置类型进行浅拷贝
在这里插入图片描述
这样浅拷贝完全不符合我们的要求,我们是希望通过拷贝,可以得到另一个与原对象一模一样的对象,
而这样的浅拷贝,当对对象 s1 进行改变时,对象 s2 也会受到改变。

就像

int a=10;
int b=a;

这是我们希望达到的效果。

而对,像栈、队列这一类的类,浅拷贝都无法达到正确效果,只有依靠深拷贝去完成。(嘿嘿,这个下次再完成)
在这里插入图片描述
而如果日期类中有自定义类型的成员变量,其会去调用自定义类型的拷贝构造函数,来完成对自定义类型的拷贝。

4、赋值运算符重载

这也是类的默认成员函数

  • 函数重载:支持定义同名函数
  • 运算符重载:为了让自定义类型可以像内置类型一样去使用运算符

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

C++为了让自定义类型的对象也能像内置类型的对象一样进行例如+、-、*、/、=、==的一系列操作。提供了运算符重载这种特性。

运算符重载的特性

特性如下:

  1. 函数名字为:关键字operator后面接需要重载的运算符符号。
  2. 函数原型:返回值类型 operator操作符(参数列表)
  3. 不能通过连接其他符号来创建新的操作符:比如operator@
  4. 重载操作符必须有一个类类型或者枚举类型的操作数
  5. 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  6. 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的
    操作符有一个默认的形参this,限定为第一个形参

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

像这类函数,都是定义在类中,不然,访问不到成员变量。

继续拿日期类举例

#include <iostream>


class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	bool operator==(const Date& d)
	{
		return (_year == d._year) && (_month == d._month) && (_day = d._day);
	}
	void print(void)
	{
		std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
int main(void)
{
	
	Date d1(2020,1,3);
	Date d2(d1);
	std::cout << (d1 == d2) << std::endl;
	d1.print();//->print(&d1)
	d2.print();//->print(&d2)
}

在类中定义运算符重载函数,使用时,就像内置类型一样使用运算符即可。
同时,

要注意,在自定义类型使用运算符时,第一个参数会是this指向的对象。

第一个参数this指针是左操作数,第二个参数是右操作数

再来看一个数组类,有点神奇。

#include <iostream> 
class Array
{
public:
	Array()
	{
		for (int i = 0; i < 10; i++)
		{
			_arr[i] = i;
		}
		_size = 10;
	}
	int& operator[](int pos)
	{
		return _arr[pos];
	}
	int GetSize(void)
	{
		return _size;
	}
private:
	int _arr[10];
	int _size;
};
int main(void)
{
	Array arr;
	for (int i = 0; i < arr.GetSize(); i++)
	{
		std::cout << arr[i] << " ";
	}
	return 0;
}

在这里插入图片描述

这个[]运算符重载,让对象可以像数组一样访问,如果是返回引用的话
甚至可以对数组的值进行修改,就像普通数组那样操作

复制拷贝与拷贝构造

内置类型

int a=10;
int b=a;//拷贝构造
//
int a=10;
int b;
b=a;//复制拷贝

自定义类型

Date d1(2021,6,5);
Date d2(d1);//拷贝构造
//
Date d1(2021,6,5);
Date d2;
d2=d1;//复制拷贝

复制拷贝:
对象已经初始化好后,再把一个对象拷贝给另一个对象
拷贝构造:
在对象还在创建时,拿另一个同类对象去初始化这一个对象

对于自定义类型,
拷贝构造调用拷贝构造函数,
复制拷贝调用=运算符重载函数

Date& operator=(const Date& d)
{
	if (this != &d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	return *this;
}

为了让自定义类型能够像内置类型一样,连续赋值,与防止自己给自己赋值

a=b=c;

毕竟,赋值运算符重载函数是默认成员函数,即使我们不写,编译器也会自动生成。

#include <iostream>


class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	bool operator==(const Date& d)
	{
		return (_year == d._year) && (_month == d._month) && (_day = d._day);
	}
	void print(void)
	{
		std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
int main(void)
{
	
	Date d1(2020,1,3);
	Date d2;
	d2=d1;
	d1.print();//->print(&d1)
	d2.print();//->print(&d2)
}

输出结果
在这里插入图片描述

对于内置类型的复制拷贝,编译器会通过默认的赋值运算符重载完成浅拷贝。

对于自定义类型的复制拷贝,编译器会调用自定义类型的赋值运算符重载完成拷贝。

5、对前4个默认成员函数的总结

  1. 构造函数:完成对象的初始化,但大部分情况下,还是需要我们自己去定义构造函数
  2. 析构函数:在对像生命周期结束时,对对象的资源进行清理
  3. 拷贝构造函数:构造函数的重载,其中一个对象还未初始化,完成对对象之间的拷贝(深/浅拷贝)
  4. 赋值运算符重载:也是拷贝行为,但是是基于两个对象已经被初始化的情况下。

对于构造和析构的特性是类似的,
对于内置类型,我们不写,编译器基本不会处理,
而自定义类型,编译器会调用自定义类型的析构和构造

对于拷贝构造和赋值运算符重载的特性是类似的,
对于内置类型,编译器会进行浅拷贝(直接赋值),
对于自定义类型,编译器要调用自定义类型的拷贝构造和赋值运算符重载,进行深拷贝。
在这里插入图片描述

6、const成员函数

特性

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

注意:只有成员函数才能加const。如构造,析构等等都不能加

就是当我们确幸该成员函数中不会,也不会造成对象的任何成员变量被修改
就使用const去修饰成员函数,这样也是为了保险,防止错误操作。
比如:

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

几个问题理解const

  1. const对象可以调用非const成员函数吗?
  2. 非const对象可以调用const成员函数吗?
  3. const成员函数内可以调用其它的非const成员函数吗?
  4. 非const成员函数内可以调用其它的const成员函数吗?

const修饰后,对成员函数或者对象所能操作的权限就被缩小,而且,权限不能放大,且不能有放大权限的可能
对于题目

  1. 不可以,因为非const成员函数中其this指针没有const修饰,也就存在放大权限的可能,可能会对this指针指向的成员变量进行修改。
  2. 可以,这属于权限缩小,而缩小权限,是不会报错的。
  3. 不可以,本身this指针就是const,当调用非const成员函数后,其隐含的this指针未被const修饰,就存在权限放大的可能。
  4. 可以,this指针在const成员函数中被const修饰,属于权限缩小。

7、取地址操作符重载

class Date
{ 
public :
 	Date* operator&()
 	{
 		return this ;
 	}
 
 	const Date* operator&()const
 	{
 		return this ;
 	}
private :
 	int _year ; // 年
 	int _month ; // 月
 	int _day ; // 日
 }

这个操作符基本没什么那啥价值,根本不需要我们去实现它,默认的成员函数就完全够用了。

保护对象不被读取地址

但是,如果你不希望对象的地址被读出来,要对其进行保护

class Date
{ 
public :
 	Date* operator&()
 	{
 		return nullptr ;
 	}
 
 	const Date* operator&()const
 	{
 		return nullptr ;
 	}
private :
 	int _year ; // 年
 	int _month ; // 月
 	int _day ; // 日
 }

这样,当别人想读对象的地址时,就只能读出00000000。可以对你的对象进行保护。

四、一个完整的日期类(选读,可跳)

直接看代码,这里面是学会对运算符重载的应用,本身没什么算法知识。

日期类.cpp

#include "date.h"
void test(void)
{
	Date d1(-11,5,26);
	Date d2(2025,1,1);
	d1.print();
	//date d3= d1 + 4;//->operator(&d2,4) 
	//d3 = d1 + 3;//->operator(&d2,4)
	d3.print();
	//date d3 = d1 - 10;
	//d1.print();
	//d2.print();
	d3.print();
	//int days = d1 - d2;
	//std::cout << days;
	Date d3=d2++;
	d3.print();
	d2.print();
	if (d3 >= d2)
	{
		std::cout << "d3<=d2" << std::endl;
	}
	//d1 += 4;//->operator+=(&d1,4)
	//d1 += 1;
	/*d1.print();
	d2.print();
	d3.print();*/
}
int main(void)
{
	test();
	
}

Date.cpp

#include "Date.h"
inline int MonthDays(int year, int month)
{
	static int DayArrary[] = { 31,28,31,30,31,30,31,31,30,31,30,31 };
	int day = DayArrary[month - 1];
	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 <= 12 && month > 0 && day <= MonthDays(year, month) && day > 0)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		std::cout << "date illegal" << std::endl;
		assert(false);
	}
}
void Date::print(void)
{
	if (_year >= 1)
		std::cout << "公元 ";
	else
		std::cout << "公元前 ";
	std::cout << _year << "年" << _month << "月" << _day<<"日"<<std::endl;
}
Date::Date(const Date& d)//拷贝构造
{
	_year =d._year;
	_month = d._month;
	_day = d._day;
}
Date& Date::operator+=(int day)//+=运算赋重载
{
	_day += day;
	while (_day > MonthDays(_year, _month))
	{
		_day -= MonthDays(_year, _month);
		_month++;
		if (_month > 12)
		{
			_year++;
			if (_year == 0)
				_year = 1;
			_month = 1;
		}
	}
	return *this;
}
Date Date::operator+(int day)//+运算符重载
{
	//建立一个临时对象
	Date tmp(_year,_month,_day);//
	tmp += day;//->operator+=(&tmp,day);//对这个临时对象的操作
	return tmp;
}
Date& Date::operator-=(int day)//-运算符重载
{
	_day -= day;
	while (_day<=0)//终止条件->>_day > 0 && _day <= MonthDays(_year, _month)
	{
		_month--;
		if (_month < 1)
		{
			_year--;
			if (_year == 0)
				_year = -1;
			_month = 12;
		}
		_day += MonthDays(_year, _month);
	}
	return *this;
}
Date Date::operator-(int day)//-运算符重载
{
	Date tmp = (*this);
	tmp -= day;
	return tmp;
}



//日期之间的天数


int YearDays(int year)
{
	if (((year % 4 == 0) && (year % 100 != 0)) || year % 400 == 0)
		return 366;
	return 365;
}
int Date::operator-(Date& d)//日期减日期
{
	int Days = 0;
	Date bigDate;
	Date smallDate;
	if (_year > d._year)
	{
		bigDate = *this;
		smallDate = d;
	}
	else if (_year < d._year)
	{
		bigDate = d;
		smallDate = *this;
	}
	
	else//同一年
	{
		//小月的剩余天数加上中间月份的天数,再加上大月的天数
		if (_month > d._month)
		{
			bigDate = *this;
			smallDate = d;
		}
		else if (_month < d._month)
		{
			bigDate = d;
			smallDate = *this;
		}
		else//同一个月
		{
			int tmp = abs(_day - d._day);
			return tmp;
		}
	}
	int yearTmp = bigDate._year - smallDate._year - 1;
	int monthTmp = bigDate._month - smallDate._month - 1;
	
	for (int i = 1; i <= yearTmp; ++i)
	{
		Days += YearDays(smallDate._year + i);
	}

	Days += MonthDays(smallDate._year, smallDate._month) - smallDate._day;
	for (int i = smallDate._month + 1; i <= 12; i++)
		Days += MonthDays(smallDate._year, i);
	for(int i=1;i<bigDate._month;i++)
		Days+= MonthDays(smallDate._year, i);
	Days += bigDate._day;
	return Days;
}


Date& Date::operator++()//前置++
{
	*this += 1;
	return *this;
}
Date Date::operator++(int) 后置++
{
	Date tmp(*this);
	*this += 1;
	return tmp;
}
Date Date::operator--(int)// 后置--
{
	Date tmp(*this);
	--* this;
	return tmp;
}
Date& Date::operator--()// 前置--
{
	*this -= 1;
	return *this;
}
// >=运算符重载
bool Date::operator>=(const Date& d)
{
	if (_year > d._year)
		return 1;
	else if(_year<d._year)
		return 0;
	else
	{
		if (_month > d._month)
			return 1;
		else if (_month < d._month)
			return 0;
		else
		{
			if (_day > d._day)
				return 1;
			else if (_day < d._day)
				return 0;
			else
				return 1;
		}
	}
}

// <运算符重载
bool Date::operator<(const Date& d)
{
	if (_year < d._year)
		return 1;
	else if (_year > d._year)
		return 0;
	else
	{
		if (_month < d._month)
			return 1;
		else if (_month > d._month)
			return 0;
		else
		{
			if (_day < d._day)
				return 1;
			else if (_day > d._day)
				return 0;
			else
				return 0;
		}
	}
}
// <=运算符重载
bool Date::operator<=(const Date& d)
{
	if (_year < d._year)
		return 1;
	else if (_year > d._year)
		return 0;
	else
	{
		if (_month < d._month)
			return 1;
		else if (_month > d._month)
			return 0;
		else
		{
			if (_day < d._day)
				return 1;
			else if (_day > d._day)
				return 0;
			else
				return 1;
		}
	}
	
}
// !=运算符重载
bool Date::operator!=(const Date& d)
{
	if (_year == d._year && _month == d._month && _day == d._day)
		return 0;
	return 1;
}

这么长?源氏看来都要拔刀了
在这里插入图片描述

Date.h

#pragma once
#include <iostream>
#include <cassert>
class Date
{
public:
	//构造函数
	Date(int year = 1, int month = 1, int day = 1);
	Date(const Date& d);//拷贝构造
	Date& operator+=(int day);//+=运算赋重载
	Date operator+(int day);//+运算符重载
	Date& operator-=(int day);//-=运算符重载
	Date operator-(int day);//-运算符重载
	int operator-(Date& d);//日期减日期

	Date& operator++();//前置++
	Date operator++(int); 后置++
	Date operator--(int);// 后置--
	Date& operator--();// 前置--

	 // >运算符重载

	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);

	void print(void);
private:
	int _year;
	int _month;
	int _day;
};

实现>,<之类的运算符重载过于琐碎,大家小心观看,因为我当时的顺序不一样,写多了。

五、友元与>>、<<运算符重载

在这里插入图片描述

为什么cin>>、cout<<可以自动识别内置类型

因为,在库中早已经重载好了,并且可以自动识别类型,各个函数之间构成重载。

cout

在这里插入图片描述
我以我LZ的英文水平翻译看看。cout是一个ostream类的对象。可以通过操作符<<将格式化或未格式化的数据通过成员函数写入到。。。。我就不知道咋翻了。

cin

在这里插入图片描述
很明显,cin是一个istream的类的对象。通过<<与成员函数完成对输入流的写入。
这些都不重要。大概了解是啥就行。

cout是根据ostream创建的一个对象
cin是根据istream创建的一个对象

这样,我们可以完成对自定义类型简化为内置类型.

ostream& operator<<(ostream& _cout, const Date& d)
{
	_cout << d._year << d._month << d._day;
	return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
	_cin >> d._year >> d._month >> d._day;
	return _cin;
}

按照这样的形式,我们可以做到像内置类型一样直接输入。

如果,要定义在类中,我们的coutcin都是操作符的左边,导致,this指针就无法指向类中的成员变量。
所以定义在类中并不合适。只能定义在类外。

但是,在类外,我们就无法访问到类中的成员变量。
所以,引入一个友元

友元函数

提供友元函数。

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

class Date
{
public:
	friend ostream& operator<<(ostream& _cout, const Date& d);
	friend istream& operator>>(istream& _cin, Date& d);

	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	bool operator==(const Date& d)
	{
		return (_year == d._year) && (_month == d._month) && (_day = d._day);
	}
	void print(void)
	{
		std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
	_cout << d._year << d._month << d._day;
	return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
	_cin >> d._year >> d._month >> d._day;
	return _cin;
}
int main()
{
	 Date d;
	 cin>>d;
 	cout<<d<<endl;
 	return 0;
 }

这样,友元可以突破封装,相当于开了一个后门。

注意

  1. 友元函数可访问类的私有和保护成员,但不是类的成员函数
  2. 友元函数不能用const修饰
  3. 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  4. 一个函数可以是多个类的友元函数
  5. 友元函数的调用与普通函数的调用和原理相同

友元类

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

class Time;//时间类前置声明
class Date
{
public:
	friend class Time;//声明时间类是日期类的友元
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	bool operator==(const Date& d)
	{
		return (_year == d._year) && (_month == d._month) && (_day = d._day);
	}
	void print(void)
	{
		std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
class Time
{
public:
	Time(int second = 0, int minute = 0, int hour = 1)
	{
		_second = second;
		_minute = minute;
		_hour = hour;
	}
private:
	int _second;
	int _minute;
	int _hour;
};

时间类是日期类的友元,则,时间类中的所有成员函数都可以说日期类的友元函数。
则,在时间类中可以访问到日期类在的私有成员变量
但是,日期类依旧不能访问到时间类中的私有成员变量。

总结:

  1. 友元关系是单向的不具有交换性。(如上)
  2. 友元关系不能传递
    如果B是A的友元,C是B的友元,则不能说明C时A的友元。

在这里插入图片描述

六、深入了解构造函数

在创建对象的时候,对象通过调用构造函数来初始化对象。
C++在构造函数上提供了两种方案来初始化成员变量。

1、函数体内初始化

就是我们常见的初始化的模式

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

2、初始化列表

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};

此模式的初始化规则:

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

3、为什么要多此一举准备两种初始化模式呢?

事实上,虽然函数体内调用构造函数后,对象中已经有了一个初始值,但是不能将其称作为类对象成员的初始化,构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
例如:
在这里插入图片描述
初始化列表两次初始化,编译器直接报错,因为在列表中初始化只能初始化一次。但是,
在这里插入图片描述
编译器却可以允许,
来看看结果
在这里插入图片描述
这已经不叫初始化了,而是叫赋值,初始化毕竟只能初始化一次。
这也再一次证明了,函数体内“初始化”,准确来叫应该是赋值,而非初始化。
初始化列表才是真正的初始化成员变量
从而可以推出,创建一个对象,对于对象而言,其中的成员变量是在初始化列表的时候定义的

初始化列表才是单个成员变量定义的阶段

而且,对于有些成员变量,只有在初始化的时候才能赋初值,且之后就不能修改了。
例如

  1. const修饰的成员变量
  2. 引用成员变量
  3. 自定义类型的成员变量(没有默认构造函数)

对于没有要求一定要在定义的时候初始化的成员变量,可以在函数体内”初始化“。

例如这样

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
		,i(10)
		,ret(_year)
	{
		_year = 100;
		_year = 200;
	}
private:
	int _year;
	int _month;
	int _day;
	const int i;
	int& ret;
};

对于第三种,没有默认构造函数的自定义成员变量
也就是说,自定义类中没有不需要传参就能初始化的构造函数。也就是说,忘记传参就会导致报错。
这样将自定义成员变量,显示定义到初始化列表中,可以我i自定义成员变量初始化,还不会报错。

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)

		:_year(year)
		, _month(month)
		, _day(day)
		,i(10)
		,ret(_year)
		,t(0)
	{
		_year = 100;
		_year = 200;
	}
private:
	int _year;
	int _month;
	int _day;
	const int i;
	int& ret;
	Time t;
};
class Time
{
public:
	Time(int t)
	{
		_t = t;
	}
private:
	int _t;
};

如果不显示初始化列表,编译器就会报错没有默认的构造函数

还有就是注意,初始化列表是成员函数定义的阶段,无论是哪种“初始化”模式,最终都会经历初始化列表这个阶段
所以还是推荐使用初始化列表,保险一点

初始化列表初始化的顺序

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

这个知识点有点绕,但还蛮重要的。

学会了吗?不会?死神手把手教你,他很会绕的
在这里插入图片描述

文章关于类与对象的知识比较零碎,又比较多,还比较重要。其实更多是对我自己的总结。

看到这也挺不容易的。
在这里插入图片描述

  • 40
    点赞
  • 68
    收藏
    觉得还不错? 一键收藏
  • 32
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 32
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值