C++[面向对象的程序设计]_基础入门(中)(万字总结)(建议收藏!!!)

目录

2. C++ 核心编程

2.1 程序的内存模型

2.1.1 内存四区 - 代码区

2.1.2 内存四区 - 全局区

2.1.3 内存四区 - 栈区

2.1.4 内存四区 - 堆区

2.1.5 new 运算符

2.2 C++ 中的引用

2.2.1 引用的基本语法

2.2.2 引用的注意事项

2.2.3 引用做函数参数

2.2.4 引用做函数返回值

2.2.5 引用的本质

2.2.6 常量引用

2.3 函数高级

2.3.1 函数的默认参数

2.3.2 函数的占位参数

2.3.3 函数重载 - 基本语法

2.3.4 函数重载 - 注意事项

2.4 类和对象

2.4.1 封装

2.4.1.1 封装的意义

2.4.1.2 struct 和 class的区别

2.4.1.3 成员属性设置为私有

2.4.2 对象的初始化和清理

2.4.2.1 构造函数和析构函数

2.4.2.2 构造函数的分类及调用

2.4.2.3 拷贝构造函数调用时机

2.4.2.4 构造函数调用规则

2.4.2.5 深拷贝与浅拷贝

2.4.2.6 初始化列表

2.4.2.7 类对象作为类成员

2.4.2.8 静态成员

2.4.3 C++ 对象模型和 this 指针

2.4.3.1 成员变量和成员函数分开存储

2.4.3.2 this 指针概念

2.4.3.3 空指针访问成员函数

2.4.3.4 const 修饰成员函数

2.4.4 友元

2.4.5 运算符重载

2.4.5.1 加号运算符重载

2.4.5.2 左移运算符重载

2.4.5.3 递增运算符重载

2.4.5.4 赋值运算符重载

2.4.5.5 关系运算符重载

2.4.5.6 函数调用运算符重载

2.4.6 继承

2.4.6.1 继承的基本语法

2.4.6.2 继承方式

2.4.6.3 继承中的对象模型

2.4.6.4 继承中构造和析构顺序

2.4.6.5 继承同名成员处理方式

2.4.6.6 继承同名静态成员处理方式

2.4.6.7 多继承语法

2.4.6.8 菱形继承

2.4.7 多态

2.4.7.1 多态的基本概念

2.4.7.2 多态的原理剖析

2.4.7.3 案例一:计算器类

2.4.7.4 纯虚函数和抽象类

2.4.7.5 案例二:制作饮品

2.4.7.6 虚析构和纯虚析构

2.5 文件操作

2.5.1 文本文件 

2.5.1.1 写文件

2.5.1.2 读文件

2.5.2 二进制文件

2.5.2.1 写文件

2.5.2.2 读文件

2.6 职工管理系统

2.6.1 管理系统需求

2.6.2 创建项目

2.6.3 创建管理类

2.6.4 菜单功能

2.6.5 退出功能

2.6.6 创建职工类

2.6.7 添加职工

2.6.8 文件交互 - 写文件

2.6.9 文件交互 - 读文件

2.6.10 显示职工

2.6.11 删除职工

2.6.12 修改职工

2.6.13 查找职工

2.6.14 排序

2.6.15 清空文件

2.6.16 完整程序代码

2.6.16.1 ManagerWorker.cpp

2.6.16.2 ManagerWorker.h


2. C++ 核心编程

2.1 程序的内存模型

C++ 程序在执行时,将内存大方向划分为 4 个区域

  •         代码区:存放函数体的二进制代码,由操作系统进行管理
  •         全局区:存放全局变量静态变量以及常量
  •         栈区:由编译器自动分配释放,存放函数的参数值局部变量
  •         堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收存放动态开辟的内存空间

内存四区的意义不同的区域存放不同的数据,赋予不同的生命周期,给我们更大的灵活编程空间!

2.1.1 内存四区 - 代码区

        在程序编译后,会生成一个 .exe 的可执行文件,还没有执行该程序前会分成两部分

代码区:

        

        这就是一个 .exe 的可执行文件,也就是说点击这个文件就会执行我们的程序!(注意:这个文件是编译代码后生成的,而不是运行代码后生成的;)

1. 该区域存放 CPU 执行的机器指令

2. 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可(因为双击 .exe 文件就会运行我们的程序,如果双击第一次运行程序之后,再次双击执行该程序时,没必要再次把程序拿到 CPU 去执行,只需要执行上次的代码即可!

3. 代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令

2.1.2 内存四区 - 全局区

        上面说了没有执行程序之前,编译代码会生成一个 .exe 文件,没有执行程序之前会分成两部分,上面指出了一部分是代码区,那么另一部分就是这里提及的全局区

1. 全局变量和静态变量存放在此!

2. 全局区还包含常量区,字符串常量和其他常量也存放在此!(其中其他常量主要指 const 变量修饰的常量)

3. 该区域的数据在程序结束后由操作系统释放!

int g_a = 10;
int g_b = 20;

const int c_l_a = 10;
const int c_l_b = 20;

int main()
{
	int a = 10;
	int b = 20;

	static int s_a = 10;
	static int s_b = 20;
	// 常量分为  1. 字符串常量   2. const 修饰的变量
	// const 修饰的变量分为  1. const 修饰的全局变量	2. const 修饰的局部变量
	const int c_g_a = 10;
	const int c_g_b = 20;

	cout << "局部变量a的地址:" << (int)&a << endl;
	cout << "局部变量b的地址:" << (int)&b << endl;
	cout << "全局变量g_a的地址:" << (int)&g_a << endl;
	cout << "全局变量g_b的地址:" << (int)&g_b << endl;
	cout << "静态变量s_a的地址:" << (int)&s_a << endl;
	cout << "静态变量s_b的地址:" << (int)&s_b << endl;

	cout << "字符串常量的地址为:" << (int)"HelloWorld" << endl;

	cout << "const修饰的局部变量c_g_a的地址:" << (int)&c_g_a << endl;
	cout << "const修饰的局部变量c_g_b的地址:" << (int)&c_g_b << endl;
	cout << "const修饰的全局变量c_l_a的地址:" << (int)&c_l_a << endl;
	cout << "const修饰的全局变量c_l_b的地址:" << (int)&c_l_b << endl;

	system("pause"); // 按任意键继续!
	return 0;
}

这里可以看出:

        局部变量 const 修饰的局部变量属于栈区,因此它们的地址在同一个分区内!

        全局变量静态变量以及常量(这里说的常量指的是字符串常量和 const 修饰的全局变量)属于全局区,因此它们的地址在同一个分区内!

这里再次提一下:不管是代码区还是全局区,都是在程序执行之前生成的两个区;而在程序运行之后产生的两个区叫做:堆区和栈区

2.1.3 内存四区 - 栈区

栈区特点:

        1. 由编译器自动分配释放(也就是说栈区的数据的生命周期不是我们可以控制的),存放函数的参数值,局部变量等。

        注意:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放!

int* Function() // 这里因为需要返回局部变量a的地址,所以函数类型定义为 int*
{
	int a = 10; // 这里函数定义一个局部变量a,局部变量是存放在栈区
					 // 栈区是由编译器来管理的,变量的声明周期不由我们来设定!
	return &a; // 返回局部变量a的地址
}

int main()
{
	// 栈区数据的注意事项 ---- 不要返回局部变量的地址
	// 栈区的数据由编译器管理开辟和释放!!!
	int* p = Function(); // 定义指针p来接收函数的返回类型
	cout << "p=" << *p << endl; 
	// 这里解释一下为什么第一次是10,刚刚不是说了局部变量离开函数生命周期就会被销毁吗,怎么还会打印10?
	// 这是因为编译器做了一次保留,编译器认为我们是误操作,所以进行了一次保留
	cout << "p=" << *p << endl;
	// 第二次打印出来是乱码,是因为编译器只进行了一次的保留!

	system("pause"); // 按任意键继续!
	return 0;
}

2.1.4 内存四区 - 堆区

堆区特点:

        由程序员分配释放(这一点和栈区的数据正好相对应,其生命周期是由程序员来管理的!),若程序员不释放,程序结束时由操作系统回收!

        在 C++ 中主要利用 new 在堆区开辟内存!

可以看出在堆区开辟的内存空间,程序员是可以管理其生命周期的!!!

2.1.5 new 运算符

        C++ 中利用 new 操作符在堆区开辟数据。

        堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符 delete;

        语法:new 数据类型

        利用 new 创建的数据,会返回该数据对应的类型的指针!

在上述的 Test() 函数中,先打印两个解引用的 *p,然后 delete 释放开辟的这段内存;然后再去打印 *p,此时就会报错!!!因此开辟的堆区空间已经被释放了!

2.2 C++ 中的引用

2.2.1 引用的基本语法

        作用:给变量起别名

        语法:数据类型  &别名 = 原名

2.2.2 引用的注意事项

1. 引用必须初始化

2. 引用在初始化后,不可以改变

2.2.3 引用做函数参数

        作用函数传参时,可以利用引用的技术让形参修饰实参

        优点可以简化指针修改实参

在之前的学习中,我们知道函数传参存在值传递地址传递,在值传递的过程中,形参的改变是不会影响到实参的;但是 C++ 中,可以用引用技术来实现形参修饰实参!!!

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <string> // 用 C++ 风格的字符串需要包含这个头文件
#include <ctime> // time 系统时间头文件包含
using namespace std;

// 1. 值传递
void Swap01(int a,int b)
{
	int Temp = a;
	a = b;
	b = Temp;

	cout << "Swap01 a=" << a << endl;
	cout << "Swap01 b=" << b << endl;
}
// 2. 地址传递
void Swap02(int* a,int* b)
{
	int Temp = *a;
	*a = *b;
	*b = Temp;

	cout << "Swap02 a=" << *a << endl;
	cout << "Swap02 b=" << *b << endl;
}
// 3. 引用传递
void Swap03(int &a,int &b)
{
	int Temp = a;
	a = b;
	b = Temp;

	cout << "Swap03 a=" << a << endl;
	cout << "Swap03 b=" << b << endl;
}
int main()
{
	int a = 10;
	int b = 20;

	//Swap01(a, b); // 值传递,形参的改变是不会影响到实参的!
	//cout << "a=" << a << endl;
	//cout << "b=" << b << endl;
	//Swap02(&a,&b); // 地址传递,形参的改变是会影响到实参的!
	//cout << "a=" << a << endl;
	//cout << "b=" << b << endl;
	Swap03(a,b); // 引用传递,形参也会修饰到实参!
	cout << "a=" << a << endl;
	cout << "b=" << b << endl;

	system("pause"); // 按任意键继续!
	return 0;
}

这里来解释一下:为什么引用也会使得实参发生改变!!!

        首先引用函数 Swap03 的参数是 int &a,相当于变量 a 起了个别名,别名是可以和原变量一个名字的,别名是具有和原变量相同的功能的;所以这里更改别名相当于更改了原变量;

2.2.4 引用做函数返回值

        作用引用是可以作为函数的返回值存在的

        注意不要返回局部变量引用

2.2.5 引用的本质

        本质引用的本质在 C++ 内部实现是一个指针常量

// 1. 引用的本质:指针常量
void function(int& ref) // 发现是引用,转换为 int* const ref = &a;
{
	ref = 100; // ref 是引用,转换为 *ref = 100;
}
int main()
{
	int a = 10;

	int &ref = a;
	// 自动转换为 int* const ref = &a; 指针常量 const 修饰指针常量,指针的指向不可以更改,这也说明了为什么引用不可以更改
	ref = 20; // 当内部发现 ref 是引用之后,会自动的帮我们转换成:*ref = 20;

	system("pause"); // 按任意键继续!
	return 0;
}

引用的本质就是指针常量

当我们定义一个局部变量 int a = 10;以后,系统就会自动的在内存中开辟一块内存,并给这块内存起个名字 a,方便我们调用这块内存;然后 int &ref = a;相当于给这块内存起了个别名 ref;实际上编译器底层接收到的是:int* const ref = &a;指针常量;const 修饰 ref,指针的指向是不能发生改变的,这也意味着引用是不能被修改的;ref = 20;在内存中执行的实际上是对指针的解引用,拿到的是一个数据;

2.2.6 常量引用

        作用常量引用主要用来修饰形参,防止误操作

        在函数形参列表中,可以加 const 修饰形参,防止形参改变实参

// 1. const修饰形参
void ShowValue(int &Val)
{
	Val = 2000; // 这里通过引用传递形参是可以改变实参的!
	cout << "Val= " << Val << endl;
}
int main()
{
	int a = 10;
	ShowValue(a);

	system("pause"); // 按任意键继续!
	return 0;
}

// 1. const修饰形参
void ShowValue(const int &Val)
{
	//Val = 2000; // 当 const 修饰之后,值就不可以修改,形参的改变也就不会影响到实参
	cout << "Val= " << Val << endl;
}
int main()
{
	int a = 10;
	ShowValue(a);

	system("pause"); // 按任意键继续!
	return 0;
}

int main()
{
	int a = 10;
	int &ref = a; //这样定义别名是没有问题的
	int &ref = 10; //但是这样定义别名就会出错!
	const int &ref = 10; // 但是如果加上 const 修饰就不会报错了
	// 原因在于编译器:const int &ref = 10; 这行代码的意思是:编译器已经定义某个变量为10,
	// 但是不知道是哪个变量,直接给这个未知的变量起个变量 ref;

	system("pause"); // 按任意键继续!
	return 0;
}

2.3 函数高级

2.3.1 函数的默认参数

        在 C++ 中,函数的形参列表中的形参是可以有默认值的

        语法返回值类型  函数名  (参数 = 默认值) { }

// 函数的默认参数
int Function(int a,int b=20,int c=30)
{
	return a + b + c;
}
// 函数参数初始化的过程中需要注意:
// 如果某个位置已经有了默认参数,那么从这个位置往后,从左到右都必须有默认值
// 意思就是说:如果初始化a=10,那么b和c就必须初始化;
// 如果初始化b=20,那么c就必须初始化,但是a就不用初始化了;
// 如果初始化c=30,那么a和b就都不用初始化;
int Function01(int a, int b = 20, int c = 30)
{
	return a + b + c;
}
int main()
{
	// cout << Function(10, 20, 30) << endl; // 计算三个值的和,这个很清楚,应该是60

	// 但是当我们只给其中的一个参数的时候,就会报错!
	// cout << Function(10) << endl; // 错误类型:函数调用的参数太少

	// C++ 中规定是可以对函数的参数进行初始化的
	cout << Function(10) << endl; // 此时就不会报错,因为int Function(int a,int b=20,int c=30) b 和 c 是有初始值的,此时结果还是60
	cout << Function(10,30) << endl; // 此时结果就是70,意思就是说:如果没有默认参数,那么默认使用初始化的 int b = 20;
														   // 如果函数定义了参数,那么使用定义的 30 ;此时结果就是 10+30+30=70;

	system("pause"); // 按任意键继续!
	return 0;
}
// 在函数声明和函数实现中,只能有一个默认参数
int Function(int a,int b); // 声明的好处就在于:提前告诉编译器有这么一个函数,此时这个函数就可以放在main函数后面了

int Function(int a,int b)
{
	return a + b;
}
// 此时不管是初始化声明还是初始化定义的参数;只能初始化其中的一个;
// int Function(int a=10,int b=10);
/*int Function(int a=10, int b=10)
{
	return a + b;
}*/
// 只能初始化上述两个其中一个;

int main()
{
	

	system("pause"); // 按任意键继续!
	return 0;
}

2.3.2 函数的占位参数

        C++ 中函数的形参列表里可以有占位参数,用来作占位,调用函数时必须填补该位置

        语法返回值类型   函数名 (数据类型) { }

        在现阶段函数的占位参数存在意义不大,但是在后面的课程中会用到该技术

2.3.3 函数重载 - 基本语法

        作用函数名可以相同,提高复用性

函数重载满足条件:

        同一个作用域下

        函数名称相同

        函数参数类型不同 或者 个数不同 或者 顺序不同

        注意函数的返回值不可以作为函数重载的条件

// 函数重载
// 可以让函数名相同,提高复用性

// 函数重载必须满足条件:
// 1. 同一个作用域下(此时下面的两个函数都在全局域下,也就是都不在main函数里,所以都满足)
// 2. 函数名称相同(这个也满足,两个函数显然名称相同)
// 3. 函数参数类型不同,或者个数不同,或者顺序不同都可以

// void Function()		 void Function(int a)  此时函数参数个数不同
// void Function(int a)		 void Function(double a)  此时函数参数类型不同
// void Function(int a, double b)		 void Function(double a, int b)  此时函数参数顺序不同

void Function()
{
	cout << "函数的复用性 " << endl;
}
void Function() 
{
	cout << "函数的复用性!!! " << endl;
}

int main()
{
	Function(); // 此时会报错,程序不知道执行哪个函数

	system("pause"); // 按任意键继续!
	return 0;
}
// 注意事项
// 函数的返回值不可以作为函数重载的条件

// 意思就是说:
// int Function()  和  double Function() 是不能作为函数重载的条件的!!!

2.3.4 函数重载 - 注意事项

  •         引用作为重载条件
  •         函数重载碰到函数默认参数
// 函数重载的注意事项
// 1. 引用作为重载的条件
void Func(int &a)
{
	cout << "Func(int &a)调用 " << endl;
}
void Func(const int &a)
{
	cout << "Func(const int &a)调用 " << endl;
}

// 2. 函数重载碰到默认参数
void Function(int a,int b =10)
{
	cout << "Func(int &a)调用 " << endl;
}
void Function(int a)
{
	cout << "Func(const int &a)调用 " << endl;
}

int main()
{
	int a = 10;
	Func(a);  // 此时打印的结果是:Func(int &a)调用
	// 因为 int a = 10 是定义一个变量,而 const 修饰的 int &a 是只允许读的,不允许更改,也就是不允许写!!!
	// void Func(int &a) 接收到的是:int &a = a;这是成立的,别名是可以和原名相同的!
	// const int &a = a;这是不成立的!!!

	Func(10);  // 此时打印的结果是:Func(const int &a)调用
	// 因为 void Func(int &a) 接收到的是:int &a = 10;这是不成立的;
	// 但是 const int &a = 10;这个是成立的!!!

	Function(10); // 此时程序就会报错,函数重载碰到默认参数,出现二义性;也就是说Function(10);同时满足 void Function(int a,int b=10) 和 void Function(int a) 的条件,两个函数都可以执行!!!
	// void Function(int a,int b=10) 和 void Function(int a) 满足函数重载的条件
	// 但是在满足函数重载的条件下,同时也满足了默认参数

	Function(10,20); // 这样就没问题, 肯定是走 void Function(int a,int b =10) 这个函数了

	system("pause"); // 按任意键继续!
	return 0;
}

2.4 类和对象

        C++ 面向对象的三个特性为:封装、继承、多态

        C++ 认为万事万物都皆可为对象,对象上有其属性和行为

例如:

        人可以作为对象,属性有姓名、年龄、身高、体重……,行为有走、跑、跳、吃饭……

        车也可以作为对象,属性有轮胎、方向盘、车灯……行为有载人、放音乐、放空调……

        具有相同性质的对象,我们可以抽象称为类,人属于人类,车属于车类;

2.4.1 封装

2.4.1.1 封装的意义

        封装是 C++ 面向对象的三大特性之一

封装的意义:

        ag. 人可以作为对象,属性有姓名、年龄、身高、体重……,行为有走、跑、跳、吃饭……

        1. 将属性和行为作为一个整体,表现生活中的事物

        2. 将属性和行为加以权限限制

封装意义一:

        在设计类的时候,属性和行为写在一起,表现事物

        语法class 类名 {       访问权限: 属性  /  行为    };


        示例一设计一个圆类,求圆的周长

const double π = 3.14; // 圆周率
// 设计一个圆类,来求圆的周长
// 圆求周长的公式:2*PI*半径( 2πr )

// C++ 中当写出一个 class 的时候就代表我们要设计一个类,类后面紧跟着的就是类名称!!!
class Circle
{
	// 类中要包括类的属性和类的行为
	// 访问权限
	// 公共权限
public:

	// 属性
	int m_r; // 半径
	// 行为
	double CalculateZC()  // 计算圆的周长
	{
		return 2 * π*m_r;
	}
};

int main()
{
	// 通过圆类来创建一个具体的圆(对象)
	Circle cl; // Circle 代表类,相当于通过一个类创建一个具体的圆;
	// 给圆对象的属性进行具体的赋值:
	cl.m_r = 10;  // 定义圆的半径为10

	cout << "圆的周长为: " << cl.CalculateZC() << endl;

	system("pause"); // 按任意键继续!
	return 0;
}

         示例二设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号!

// 设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号!
class Student
{
public:

	// 属性
	string m_name; // 姓名
	int m_ID; // 学号
	// 行为
	// 1. 显示姓名和学号的函数
	void ShowStudent()
	{
		cout << "姓名:" << m_name <<"      "<< "学号:" << m_ID << endl;
	}
};

int main()
{
	// 创建一个具体的学生,实例化对象
	Student Stu;
	Stu.m_name = "张三";
	Stu.m_ID = 666;

	Stu.ShowStudent();

	Student STU;
	STU.m_name = "李四";
	STU.m_ID = 888;

	STU.ShowStudent();

	system("pause"); // 按任意键继续!
	return 0;
}

        当然也可以通过函数来进行属性的赋值!!!

// 设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号!
class Student
{
public:

	// 属性
	string m_name; // 姓名
	int m_ID; // 学号
	// 行为
	// 1. 显示姓名和学号的函数
	void ShowStudent()
	{
		cout << "姓名:" << m_name <<"      "<< "学号:" << m_ID << endl;
	}
	// 2. 给姓名赋值
	void SetName(string name)
	{
		m_name = name;
	}
	// 3. 给 ID 号赋值
	void SetID(int ID)
	{
		m_ID = ID;
	}
};

int main()
{
	// 创建一个具体的学生,实例化对象
	Student Stu;
	Stu.SetName("张三");
	Stu.SetID(666);
	Stu.ShowStudent();

	Student STU;
	STU.SetName("李四");
	STU.SetID(888);
	STU.ShowStudent();

	system("pause"); // 按任意键继续!
	return 0;
}

        类中的属性和行为,我们统一称为成员;

        类中的属性:成员属性、成员变量

        类中的行为:成员函数、成员方法

封装意义二:

         类在设计时,可以把属性和行为放在不同的权限下,加以控制

访问权限有三种:

        1. public         公共权限                类内可以访问,类外可以访问(类内指的就是 Class 里面)

        2. protected   保护权限                类内可以访问,类外不可以访问

        3. private       私有权限                 类内可以访问,类外不可以访问

// 1. public         公共权限                类内可以访问,类外可以访问(类内指的就是 Class 里面)
// 2. protected   保护权限                类内可以访问,类外不可以访问					儿子可以访问父亲中的保护内容
// 3. private        私有权限                类内可以访问,类外不可以访问					儿子不可以访问父亲的私有内容
class People
{
public:
	// 公共权限
	string m_name; // 姓名
protected:
	// 保护权限
	string m_car;	// 汽车
private:
	// 私有权限
	int m_Password; // 银行卡密码

public: // 类内可以访问,也就是在 class 内部可以访问
	void Function()
	{
		m_name = "张三";
		m_car = "拖拉机";
		m_Password = 123456;
	}
};

int main()
{
	People Stu; // 通过类实例化一个具体的对象
	Stu.m_name = "李四";
	Stu.m_car = "奔驰"; // 保护权限不可以在类外进行访问
	Stu.m_Password = 123; // 私有权限在类外也是不可以访问的

	Stu.Function();

	system("pause"); // 按任意键继续!
	return 0;
}

2.4.1.2 struct 和 class的区别

        在 C++ 中 struct 和 class 唯一的区别就在于:默认的访问权限不同

区别:

        struct 默认权限为公共

        class 默认权限为私有

2.4.1.3 成员属性设置为私有

        优点1将所有成员属性设置为私有,可以自己控制读写权限

        优点2对于写权限,我们可以检测数据的有效性

优点1将所有成员属性设置为私有,可以自己控制读写权限

// 成员属性设置为私有
// 1. 可以自己控制读写权限
// 2. 对于写可以检测数据有效性

// 人类
class People
{
	// 但是实际的应用中为了保证某些成员变量可读可写
	// 进行如下设置
public:
	void SetName(string name)
	{
		m_Name = name;
	}
	string GetName()
	{
		return m_Name;
	}
	int GetAge()
	{
		return m_Age;
	}
	void SetIdol(string Idol)
	{
		m_Idol = Idol;
	}

private:
	string m_Name; // 姓名 可读可写

	int m_Age=18; // 年龄 只读

	string m_Idol; // 偶像 只写
};

int main()
{
	// 如果类 class 中设置的是 private,所以类外不可以访问;
	People C1;
	C1.SetName("张三");
	cout << "姓名:" << C1.GetName() << endl;
	cout << "年龄:" << C1.GetAge() << endl;

	system("pause"); // 按任意键继续!
	return 0;
}

 优点2对于写权限,我们可以检测数据的有效性

// 所谓检测数据有效性就是,假设现在我们可以设置年龄了,但是年龄只能处于0~150岁!
// 加上所谓的限制条件!!!
void SetAge(int Age)
	{
		if (Age<0 || Age>150)
		{
			cout << "年龄输入有误,赋值失败!" << endl;
		}
		m_Age = Age;
	}

练习案例一:设计立方体类

        设计立方体类(Cube)

        求出立方体的面积和体积

        分别用全局函数和成员函数判断两个立方体是否相等

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <string> // 用 C++ 风格的字符串需要包含这个头文件
#include <ctime> // time 系统时间头文件包含
using namespace std;

// 设计立方体类(Cube)
// 求出立方体的面积和体积
// 分别用全局函数和成员函数判断两个立方体是否相等

class Cube
{
public:
	// 行为
	// 设置长宽高和获取长宽高
	void SetLength(int Length)
	{
		m_L = Length;
	}
	int GetLength()
	{
		return m_L;
	}
	void SetWidth(int Width)
	{
		m_W = Width;
	}
	int GetWidth()
	{
		return m_W;
	}
	void SetHigh(int High)
	{
		m_H = High;
	}
	int GetHigh()
	{
		return m_H;
	}
	int S()
	{
		return (m_L*m_W + m_L*m_H + m_H*m_W) * 2;
	}
	int V()
	{
		return m_L*m_H*m_W;
	}

private: // 设置为私有属性,只能类内进行访问
	// 属性
	int m_L; // 长
	int m_W; // 宽
	int m_H; // 高
public:
	// 利用成员函数来判断两个立方体是否相等
	bool IsSameFromClass(Cube &C2)
	{
		if (m_L == C2.GetLength() && m_W == C2.GetWidth() && m_H == C2.GetHigh())
		{
			return true;
		}
		return false;
	}
};

// 设置全局函数来判断两个立方体是否相等
bool IsSame(Cube &C1,Cube &C2)
{
	if (C1.GetLength() == C2.GetLength() && C1.GetWidth() == C2.GetWidth() && C1.GetHigh() == C2.GetHigh())
	{
		return true;
	}
	return false;
}

int main()
{
	Cube C1;
	C1.SetLength(10);
	C1.SetWidth(20);
	C1.SetHigh(30);
	cout << "立方体的长度为:" << C1.GetLength() << endl;
	cout << "立方体的宽度为:" << C1.GetWidth() << endl;
	cout << "立方体的高度为:" << C1.GetHigh() << endl;
	cout << "立方体的面积为:" << C1.S() << endl;
	cout << "立方体的体积为:" << C1.V() << endl;

	Cube C2;
	C2.SetLength(10);
	C2.SetWidth(20);
	C2.SetHigh(30);
	
	bool ret = IsSame(C1,C2);
	if (ret==true)
	{
		cout << "C1和C2是相等的!" << endl;
	}
	else
		cout << "C1和C2是不相等的!" << endl;

	// 利用成员函数来判断
	bool Temp = C2.IsSameFromClass(C2);
	if (Temp==true)
	{
		cout << "成员函数判断:C1和C2是相等的!" << endl;
	}
	else
		cout << "成员函数判断:C1和C2是不相等的!" << endl;

	system("pause"); // 按任意键继续!
	return 0;
}

练习案例二点和圆的关系

        设计一个圆类(Circle),和一个点类(Point),计算点和圆的关系。

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <string> // 用 C++ 风格的字符串需要包含这个头文件
#include <ctime> // time 系统时间头文件包含
using namespace std;

// 设计一个圆类(Circle),和一个点类(Point),计算点和圆的关系
class Point
{
public:
	// 设置圆心坐标、获取圆心坐标
	void SetX(int x)
	{
		m_X = x;
	}
	int GetX()
	{
		return m_X;
	}
	void SetY(int y)
	{
		m_Y = y;
	}
	int GetY()
	{
		return m_Y;
	}

private:
	int m_X; // 圆心的 X 轴坐标
	int m_Y; // 圆心的 Y 轴坐标
};

class Circle
{
public:
	// 设置半径、获取半径;
	// 设置圆心、获取圆心;
	void SetR(int R)
	{
		m_R = R;
	}
	int GetR()
	{
		return m_R;
	}
	void SetCenter(Point Center)
	{
		m_Center = Center;
	}
	Point GetCenter()
	{
		return m_Center;
	}
private: // 实际开发中通常把是属性设置为私有类型
	int m_R; // 圆的半径
	Point m_Center; // 圆心
};

// 进行比较
void IsCompareState(Circle &R,Point &Center)
{
	int Distant = // 两个坐标之间的距离
		(R.GetCenter().GetX() - Center.GetX()) * (R.GetCenter().GetX() - Center.GetX()) +
		(R.GetCenter().GetY() - Center.GetY()) * (R.GetCenter().GetY() - Center.GetY());
	// Circle 是圆的类,Point 是对应点的类
	// 我们需要从圆类里面获取圆的坐标,从点类里面获取点的坐标;
	int R_Distant = R.GetR() * R.GetR();  // 半径的平方
	if (Distant == R_Distant)
	{
		cout << "点在圆上!" << endl;
	}
	else if (Distant<R_Distant)
	{
		cout << "点在圆内!" << endl;
	}
	else
		cout << "点在圆外!" << endl;
}

int main()
{
	// 创建圆
	Circle C;
	C.SetR(10);
	Point Center;
	Center.SetX(10); // 这里设置圆心坐标为 (10,0);
	Center.SetY(0);
	C.SetCenter(Center); // 将圆心坐标放进来

	// 创建点
	Point P;
	P.SetX(10);
	P.SetY(10);

	// 判断它们两者之间的关系
	IsCompareState(C,P);

	system("pause"); // 按任意键继续!
	return 0;
}

2.4.2 对象的初始化和清理

        生活中我们买的电子产品都基本会有出厂设置,在某一天我们不用的时候也会删除一些自己信息数据保证安全

        C++ 面向对象来源于生活,每个对象也都会有初始设置以及 对象销毁前的清理数据的设置

2.4.2.1 构造函数和析构函数

        对象的初始化和清理也是两个非常重要的安全问题

                一个对象或者变量没有初始状态,对其使用后果是未知的

                同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题

        C++ 利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象的初始化和清理工作!

        对象的初始化和清理工作是编译器强制我们要做的事情,因此如果我们不提供构造和析构,编译器会提供编译器提供的构造函数和析构函数是空实现!

                构造函数主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用

                析构函数主要作用在于对象销毁前系统自动调用,执行一些清理工作

构造函数语法:类名(){}

        1. 构造函数,没有返回值也不写 void

        2. 函数名称与类名相同

        3. 构造函数可以有参数,因此可以发送重载

        4. 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次 (就像我们用手机一样,不用每天起床都要初始化一次)

析构函数语法:~类名(){}

        1. 析构函数,没有返回值也不写 void

        2. 函数名称与类名相同,在名称前面加上 ~

        3. 析构函数不可以有参数,因此不可以发送重载

        4. 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次

// 对象的初始化和清理
// 1. 构造函数  进行初始化操作
class People
{
public:
	// 1.1 构造函数
	// 没有返回值 不用写 void
	// 函数名 与类名相同
	// 构造函数可以有参数,可以发送重载
	// 创建对象的时候,构造函数会自动调用,而且只调用一次
	People()
	{
		cout << "构造函数的调用" << endl;
	}
};
void Test01()
{
	People P;
}
// 2. 析构函数  进行清理的操作

int main()
{
	Test01();

	system("pause"); // 按任意键继续!
	return 0;
}

        通过上述程序可以发现:在测试函数中,只是定义了一个对象 P,而没有去 P.People(),调用这个函数,但是主函数中运行测试函数,依然可以打印;这就再一次证实了构造函数是自动调用的,而且只会调用一次;如果我们不写这样的一个构造函数,那么编译器默认的构造函数是下面这样的:

People()
{
    // 无		
}
// 对象的初始化和清理
// 1. 构造函数  进行初始化操作
class People
{
public:
	// 1.1 构造函数
	// 没有返回值 不用写 void
	// 函数名 与类名相同
	// 构造函数可以有参数,可以发送重载
	// 创建对象的时候,构造函数会自动调用,而且只调用一次
	People()
	{
		cout << "构造函数的调用" << endl;
	}
	// 2. 析构函数  进行清理的操作
	// 没有返回值 不用写 void
	// 函数名和类名相同 在名称前加 ~
	// 析构函数不可以有参数,也就是不可以发生重载
	// 对象在销毁前会自动调用析构函数,而且只会调用一次
	~People()
	{
		cout << "析构函数的调用" << endl;
	}
};
void Test01()
{
	People P;
}


int main()
{
	Test01();

	system("pause"); // 按任意键继续!
	return 0;
}

这里可以发现:析构函数只是在类里定义,却没有运行,但是最终也打印出了析构函数的调用

                         原因在于:主函数运行测试函数 Test01,测试函数内部创建局部变量对象 P,局部变量是存放在栈区的,栈区的生命周期是运行完毕后就会自动的释放!

                                           析构函数的原则就是对象在销毁前就会自动调用析构函数,既然局部变量离开函数就会释放内存,也就等价于销毁,那么随之就会调用析构函数!

        如果我们不写析构函数的话,那么对应的析构函数是空函数!

2.4.2.2 构造函数的分类及调用

两种分类方式:

        按参数分为:有参构造和无参构造

        按类型分为:普通构造和拷贝构造

三种调用方法:

        括号法

        显示法

        隐式转换法

// 1. 构造函数的分类及调用
// 分类
class People
{
public:
	// 构造函数
	//		按参数分类	无参构造(默认构造)  和  有参构造
	//		按类型分类   普通构造  和  拷贝构造函数
	People()
	{
		cout << "People 的无参构造函数调用" << endl;
	}
	People(int a)
	{
		age = a;
		cout << "People 的有参构造函数调用" << endl;
	}
	// 拷贝构造函数
	People(const People &p) // 拷贝 拷贝 就是把原本的构造函数重新复制一份
		// 所以参数就是 People p,因为拷贝的过程中不能改变,所以加上 const 修饰,const People &p这是标准的拷贝函数的参数
	{
		// 拷贝构造函数的使用:就是在原本的构造函数基础上,通过调用实现相同的功能
		// 比如说,上述将输入的年龄a赋值给age
		// 那么拷贝函数中就可以:将传入的人身上的所有属性,拷贝到我身上!
		age = p.age;
		cout << "People 的拷贝构造函数调用" << endl;
	}
	// 析构函数
	~People()
	{
		cout << "People 的析构函数调用" << endl;
	}

	int age;
};
// 调用
void test01()
{
	// 默认构造函数调用
	People p;

	// 1. 括号法
	People P2(10); // 有参构造函数
	People P3(P2); // 拷贝构造函数

	cout << "P2的年龄是:" << P2.age << endl;
	cout << "P3的年龄是:" << P3.age << endl;

	// 括号法的注意事项:
	//			默认进行构造函数的时候,不要加小括号,也就是不要 People p();
	//			这样编译器会认为是一个函数的声明,不会认为在创建对象!

	// 2. 显示法
	People p1; // 默认函数构造
	People p2 = People(10); // 有参构造
	People p3 = People(p2); // 拷贝构造

	People(10); // 匿名对象  特点:当前执行结束后,系统会自动回收掉匿名对象

	// 注意事项
	// 不要利用拷贝构造函数  初始化匿名对象
	// People(p3); // 编译器会认为这是一个对象的声明

	// 3. 隐式转换法
	People p4 = 10; // 编译器会显示的转换为 People p4 = People(10);
	People p5 = p4; // 拷贝构造
}

int main()
{
	test01();

	system("pause"); // 按任意键继续!
	return 0;
}
2.4.2.3 拷贝构造函数调用时机

        C++ 中拷贝构造函数调用时机通常有三种情况:

  •                 使用一个已经创建完毕的对象来初始化一个新对象
  •                 值传递的方式给函数参数传值
  •                 以值方式返回局部对象
class People
{
public:
	People()
	{
		cout << "构造函数的调用!" << endl;
	}
	People(int Age) 
	{
		cout << "有参构造函数的调用!" << endl;
		m_Age = Age;
	}
	People(const People &p) // 拷贝构造函数
	{
		cout << "拷贝构造函数的调用!" << endl;
		m_Age = p.m_Age;
	}
	~People()
	{
		cout << "析构函数的调用!" << endl;
	}
	int m_Age;
};

// 拷贝构造函数的调用时机
// 1. 使用一个已经创建完毕的对象来初始化一个新对象
void test01()
{
	People p1(10); // 有参构造函数
	People p2(p1); // 拷贝构造函数

	cout << "p2的年龄为:" << p2.m_Age << endl;
}
// 2. 值传递的方式给函数参数传值
void doWork(People p) // 此时这种方式也会调用拷贝构造函数
{
	// 因为参数 People p 是值传递,也就是拷贝一个新的 p,这个 p 和函数传参的 p 不是一个 p
	// 此时函数体内 p.m_Age = 1000,实参的年龄仍然不会改变
}
void test02()
{
	People p;
	doWork(p); // 值传递传过去的只是拷贝的值,在拷贝的过程中会调用拷贝构造函数
}
// 3. 值方式返回局部对象
People doWork02()
{
	People p;
	return p; // 这里解释一下:return p,返回的是局部变量 p,局部变量的生命周期仅限于该函数!
	// 但是我的返回类型为 People,相当于离开这个函数之后重新构造了一个People类型的变量,相当于拷贝了一个 p
}
void test03()
{
	People p = doWork02(); // 存在拷贝的过程,因此会调用拷贝构造函数!
}

int main()
{
	// test01();
	//test02();
	test03();

	system("pause"); // 按任意键继续!
	return 0;
}
2.4.2.4 构造函数调用规则

默认情况下,C++ 编译器至少给一个类添加 3 个函数:

        1. 默认构造函数(无参,函数体为空)

        2. 默认析构函数(无参,函数体为空)

        3. 默认拷贝构造函数,对属性进行值拷贝

// 默认情况下,C++ 编译器至少给一个类添加 3 个函数:
// 1. 默认构造函数(无参,函数体为空)
// 2. 默认析构函数(无参,函数体为空)
// 3. 默认拷贝构造函数,对属性进行值拷贝

class People
{
public:
	People()
	{
		cout << "默认构造函数的调用!" << endl;
	}
	People(int Age)
	{
		m_Age = Age;
		cout << "有参构造函数的调用!" << endl;
	}
	People(const People &p)
	{
		cout << "拷贝构造函数的调用!" << endl;
		m_Age = p.m_Age;
	}
	~People()
	{
		cout << "析构函数的调用!" << endl;
	}
	int m_Age;
};
// 1. 如果用户定义有参构造函数,C++ 不再提供默认无参构造,但是会提供默认拷贝构造

// 2. 如果用户定义拷贝构造函数,C++ 不会再提供其他构造函数

int main()
{
	

	system("pause"); // 按任意键继续!
	return 0;
}

构造函数调用规则如下:

        如果用户定义有参构造函数,C++ 不再提供默认无参构造,但是会提供默认拷贝构造

// 默认情况下,C++ 编译器至少给一个类添加 3 个函数:
// 1. 默认构造函数(无参,函数体为空)
// 2. 默认析构函数(无参,函数体为空)
// 3. 默认拷贝构造函数,对属性进行值拷贝

class People
{
public:
	/*People()
	{
		cout << "默认构造函数的调用!" << endl;
	}*/
	People(int Age)
	{
		m_Age = Age;
		cout << "有参构造函数的调用!" << endl;
	}
	People(const People &p)
	{
		cout << "拷贝构造函数的调用!" << endl;
		m_Age = p.m_Age;
	}
	~People()
	{
		cout << "析构函数的调用!" << endl;
	}
	int m_Age;
};
// 1. 如果用户定义有参构造函数,C++ 不再提供默认无参构造,但是会提供默认拷贝构造
void Test()
{
	People p;
}
// 2. 如果用户定义拷贝构造函数,C++ 不会再提供其他构造函数

int main()
{
	Test();

	system("pause"); // 按任意键继续!
	return 0;
}

        这里可以发现,上述程序会提示出错:我们屏蔽默认构造函数的代码,同时定义有参构造,这时 C++ 编译器会默认不再提供无参构造,因此通过 Test() 调用默认构造函数会出错!!!

        如果用户定义拷贝构造函数,C++ 不会再提供其他构造函数

// 默认情况下,C++ 编译器至少给一个类添加 3 个函数:
// 1. 默认构造函数(无参,函数体为空)
// 2. 默认析构函数(无参,函数体为空)
// 3. 默认拷贝构造函数,对属性进行值拷贝

class People
{
public:
	/*People() 
	{
		cout << "默认构造函数的调用!" << endl;
	}*/
	/*People(int Age)
	{
		m_Age = Age;
		cout << "有参构造函数的调用!" << endl;
	}*/
	People(const People &p)
	{
		cout << "拷贝构造函数的调用!" << endl;
		m_Age = p.m_Age;
	}
	/*~People()
	{
		cout << "析构函数的调用!" << endl;
	}*/
	int m_Age;
};
// 1. 如果用户定义有参构造函数,C++ 不再提供默认无参构造,但是会提供默认拷贝构造
void Test()
{
	People p;
}
// 2. 如果用户定义拷贝构造函数,C++ 不会再提供其他构造函数

int main()
{
	Test();

	system("pause"); // 按任意键继续!
	return 0;
}

        此时程序依然会出错,因为我们屏蔽其他代码,只留下拷贝构造函数,C++ 编译器默认提供拷贝构造函数的情况下,不再提供其他构造函数!!!

2.4.2.5 深拷贝与浅拷贝

        深浅拷贝是面试的经典问题:

浅拷贝简单的赋值拷贝操作

深拷贝在堆区重新申请空间,进行拷贝操作

浅拷贝:

// 浅拷贝:简单的赋值拷贝操作(也就是编译器提供的等号赋值操作)
// 深拷贝:在堆区重新申请空间,进行拷贝操作

class People
{
public:
	People()
	{
		cout << "People 默认构造函数的调用!" << endl;
	}
	People(int Age)
	{
		m_Age = Age;
		cout << "People 有参构造函数的调用!" << endl;
	}
	~People()
	{
		cout << "People 析构函数的调用!" << endl;
	}
	int m_Age; // 年龄
};
void Test()
{
	People p1;
	p1.m_Age = 18; // 这里均为浅拷贝,只是编译器提供的简单的赋值拷贝操作

	cout << "p1 的年龄为:" << p1.m_Age << endl;

	People p2;
	p2.m_Age = 20; // 这里均为浅拷贝,只是编译器提供的简单的赋值拷贝操作

	cout << "p2 的年龄为:" << p2.m_Age << endl;
}

int main()
{
	Test();

	system("pause"); // 按任意键继续!
	return 0;
}

深拷贝:

// 浅拷贝:简单的赋值拷贝操作(也就是编译器提供的等号赋值操作)
// 深拷贝:在堆区重新申请空间,进行拷贝操作

class People
{
public:
	People()
	{
		cout << "People 默认构造函数的调用!" << endl;
	}
	People(int Age,int Higher)
	{
		m_Age = Age;
		m_Higher = new int(Higher); // new 相当于在堆区开辟相应的内存
		cout << "People 有参构造函数的调用!" << endl;
	}
	~People()
	{
		// 既然堆区开辟相应的内存,那么通过析构函数在函数消亡前将对应的内存释放
		if (m_Higher != NULL )
		{
			delete(m_Higher); // 通过 delete 动态释放
			m_Higher = NULL;
		}
		cout << "People 析构函数的调用!" << endl;
	}
	int m_Age; // 年龄
	int* m_Higher; // 现在用指针类型定义一个身高,指针指向地址,在堆区开辟相应的内存
};
void Test()
{
	People p1(18,160);// 这里均为浅拷贝,只是编译器提供的简单的赋值拷贝操作

	cout << "p1 的年龄为:" << p1.m_Age <<"p1 的身高为:"<<*p1.m_Higher<< endl;

	People p2(p1); // 虽然程序没有定义拷贝构造函数,但是编译器自动提供了拷贝构造函数

	cout << "p2 的年龄为:" << p2.m_Age << "p2 的身高为:" << *p2.m_Higher << endl;
}

int main()
{
	Test();

	system("pause"); // 按任意键继续!
	return 0;
}

        上述程序是通过是通过浅拷贝来实现的,但是程序在运行过程中却崩溃了,这里我们来分析一下上述出现这种情况的原因是什么?通过也引出了浅拷贝的致命缺点:对内存的重复释放导致程序出现异常!!!

// 浅拷贝:简单的赋值拷贝操作(也就是编译器提供的等号赋值操作)
// 深拷贝:在堆区重新申请空间,进行拷贝操作

class People
{
public:
	People()
	{
		cout << "People 默认构造函数的调用!" << endl;
	}
	People(int Age,int Higher)
	{
		m_Age = Age;
		m_Higher = new int(Higher); // new 相当于在堆区开辟相应的内存
		cout << "People 有参构造函数的调用!" << endl;
	}
	// 自己创建拷贝构造函数,解决浅拷贝带来的问题
	People(const People &p)
	{
		m_Age = p.m_Age;
		// m_Higher = p.m_Higher; // 现在我们不再采用该代码,因为这行代码是编译器提供的浅拷贝代码,简单的赋值拷贝
		m_Higher = new int(*p.m_Higher); // new 默认返回类型为 int 类型的指针
		cout << "拷贝构造函数的调用!" << endl;
	}
	~People()
	{
		// 既然堆区开辟相应的内存,那么通过析构函数在函数消亡前将对应的内存释放
		if (m_Higher != NULL )
		{
			delete(m_Higher); // 通过 delete 动态释放
			m_Higher = NULL;
		}
		cout << "People 析构函数的调用!" << endl;
	}
	int m_Age; // 年龄
	int* m_Higher; // 现在用指针类型定义一个身高,指针指向地址,在堆区开辟相应的内存
};
void Test()
{
	People p1(18,160);// 这里均为浅拷贝,只是编译器提供的简单的赋值拷贝操作

	cout << "p1 的年龄为:" << p1.m_Age <<"  身高为:"<<*p1.m_Higher<< endl;

	People p2(p1); // 虽然程序没有定义拷贝构造函数,但是编译器自动提供了拷贝构造函数

	cout << "p2 的年龄为:" << p2.m_Age << "  身高为:" << *p2.m_Higher << endl;
}

int main()
{
	Test();

	system("pause"); // 按任意键继续!
	return 0;
}

总结:

        如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题!

2.4.2.6 初始化列表

        作用C++ 提供了初始化列表语法,用来初始化属性

        语法构造函数():属性1(值1),属性2(值2)……{ }

传统意义上的属性初始化:

// 之前我们学习了构造函数,并且知道构造函数就是为属性进行初始化用的
// 同时 C++ 也提供了初始化列表的语法

// 构造函数():属性1(值1),属性2(值2)……{}
class People
{
public:
	// 传统意义上的属性初始化采用构造函数的形式进行初始化
	People(int a, int b, int c)
	{
		m_A = a;
		m_B = b;
		m_C = c;
	}
	int m_A;
	int m_B;
	int m_C;
};
void Test()
{
	People p(10, 20, 30);
	cout << "m_A=" << p.m_A << endl;
	cout << "m_B=" << p.m_B << endl;
	cout << "m_C=" << p.m_C << endl;
}

int main()
{
	Test();

	system("pause"); // 按任意键继续!
	return 0;
}

采用 C++ 提供的初始化列表进行初始化

// 之前我们学习了构造函数,并且知道构造函数就是为属性进行初始化用的
// 同时 C++ 也提供了初始化列表的语法

// 构造函数():属性1(值1),属性2(值2)……{}
class People
{
public:
	// 传统意义上的属性初始化采用构造函数的形式进行初始化
	/*People(int a, int b, int c)
	{
		m_A = a;
		m_B = b;
		m_C = c;
	}*/
	People() :m_A(10), m_B(20), m_C(30) // 这就相当于进行了初始化操作
	{

	}
	int m_A;
	int m_B;
	int m_C;
};
void Test()
{
	//People p(10, 20, 30);
	People p;
	cout << "m_A=" << p.m_A << endl;
	cout << "m_B=" << p.m_B << endl;
	cout << "m_C=" << p.m_C << endl;
}

int main()
{
	Test();

	system("pause"); // 按任意键继续!
	return 0;
}

更灵活的:

// 之前我们学习了构造函数,并且知道构造函数就是为属性进行初始化用的
// 同时 C++ 也提供了初始化列表的语法

// 构造函数():属性1(值1),属性2(值2)……{}
class People
{
public:
	// 传统意义上的属性初始化采用构造函数的形式进行初始化
	/*People(int a, int b, int c)
	{
		m_A = a;
		m_B = b;
		m_C = c;
	}*/
	People(int a,int b,int c) :m_A(a), m_B(b), m_C(c) // 这就相当于进行了初始化操作
	{

	}
	int m_A;
	int m_B;
	int m_C;
};
void Test()
{
	//People p(10, 20, 30);
	People p(30,20,10);
	cout << "m_A=" << p.m_A << endl;
	cout << "m_B=" << p.m_B << endl;
	cout << "m_C=" << p.m_C << endl;
}

int main()
{
	Test();

	system("pause"); // 按任意键继续!
	return 0;
}

2.4.2.7 类对象作为类成员

        C++ 类中的成员可以是另一个类的对象,我们称该成员为 对象成员

class A {}
class B
{
    A a;
}

// B 类中有对象 A 作为成员,A 为对象成员
// 类对象作为类成员
// 对象成员
class Phone
{
public:
	Phone(string PName)
	{
		cout << "Phone 构造函数的调用" << endl;
		m_PName = PName;
	}
	string m_PName; // 手机的型号
};
class People
{
public:
	People(string Name, string MName) :m_Name(Name), m_Phone(MName)
	{
		cout << "People 构造函数的调用" << endl;
	}
	string m_Name; // 姓名
	Phone m_Phone; // 手机
};
void Test()
{
	People p("张三","苹果MAX");
	cout << p.m_Name << "拿着:" << p.m_Phone.m_PName << endl;
}
int main()
{
	Test();

	system("pause"); // 按任意键继续!
	return 0;
}

结论:

        通过上述程序可以发现:当一个类作为另一个类的成员时,类内其他对象的构造函数先调用;相反的,类内其他对象的析构函数则后调用!

2.4.2.8 静态成员

        静态成员就是在成员变量和成员函数前加上关键字 static ,称其为静态成员

静态成员变量:

  •         所有对象共享同一份数据
  •         在编译阶段分配内存
  •         类内声明,类外初始化
// 静态成员变量
class People
{
public:

	// 1. 所有对象都共享同一份数据
	// 2. 编译阶段就分配内存
	// 3. 类内声明,类外初始化操作

	static int m_A; // 类内声明
};

int People::m_A = 100; // 类外初始化

int main()
{	
	People p;
	
	cout << p.m_A << endl;

	People p1;
	p1.m_A = 200; 

	cout << p.m_A << endl; // 所有对象共享同一份数据
	// 也就是说对象 p1 将 m_A 重新设置为 200
	// 此时用对象 p 去访问,m_A 的值还是 200;

	system("pause"); // 按任意键继续!
	return 0;
}

静态成员变量的访问方式:

静态成员变量同样存在访问权限:

静态成员函数:

  •         所有对象共享同一个函数
  •         静态成员函数只能访问静态成员变量
// 静态成员函数
// 1. 所有对象共享同一个函数
// 2. 静态成员函数只能访问静态成员变量 

class People
{
public:
	static void func() // 静态成员函数
	{ 
		m_A = 200; // static void func() 是静态成员函数,因此可以访问静态成员变量 m_A
		m_B = 200; // m_B 不是静态成员变量,因此静态成员函数不可以访问 m_B
		cout << "static void func() 的调用" << endl;
	}
	static int m_A; // 静态成员变量
	int m_B;
};

// 静态成员变量必须类内声明,类外访问
int People::m_A = 100;

// 访问静态成员函数
void Test()
{
	// 1. 通过对象进行访问
	People p;
	p.func();

	// 2. 通过类名进行访问
	People::func();
}

int main()
{	
	Test();

	system("pause"); // 按任意键继续!
	return 0;
}

// 之所以 m_B 会报错,是因为 m_B 不是静态成员变量,函数无法判断它属于哪个对象
// 之所以 m_A 就可以,是因为 m_A 是静态成员变量,静态成员变量本身不属于任何对象,它是共享的!

2.4.3 C++ 对象模型和 this 指针

2.4.3.1 成员变量和成员函数分开存储

        在 C++ 中,类内的成员变量和成员函数分开存储

        只有非静态成员变量才属于类的对象上

// 成员变量 和 成员函数分开存储
class People
{
public:
	int m_A; // 定义一个非静态的成员变量

	static int m_B; // 定义一个静态成员变量

	void Func() // 定义一个非静态的成员函数
	{

	}
	static void Func01(); // 静态成员函数 
};

int People::m_B = 100; // 静态成员变量类内声明,类外初始化

void Test()
{
	People p;
	// 因为上述定义的对象是空的,首先先来看一下空的对象占用多大的内存空间:
	cout << "Sizeof p =" << sizeof(p) << endl;
	// 打印可以显示:空的对象占用1个字节大小的内存
	// 原因在于:C++ 编译器会给每个空对象也分配一个字节空间,是为了区分空对象占内存的位置
	// 简单来说就是:现在假设我创建两个对象 p 和 p1,为了在内存上区别这两个对象,防止这两个对象占用同一块内存

	// Sizeof p =1
}
void Test01()
{
	People p;
	cout << "Sizeof p =" << sizeof(p) << endl; 
	// 此时类中有一个非静态的成员变量
	// 虽然空的对象本身占用1个字节大小,但是这一个字节大小是为了区分不同对象
	// 当类中不为空时,以实际存储的变量为主

	// Sizeof p =4

}
void Test02()
{
	People p;
	cout << "Sizeof p =" << sizeof(p) << endl;
	// 此时类中有一个非静态的成员变量 + 一个静态成员变量
	
	// 之所以 非静态的成员变量 + 一个静态成员变量 打印结果还是 4 
	// 是因为 静态成员变量 不属于任何一个类,它是被共享的
	// Sizeof p =4

}
void Test03()
{
	People p;
	cout << "Sizeof p =" << sizeof(p) << endl;
	// 此时类中有一个非静态的成员变量 + 一个静态成员变量 + 一个非静态的成员函数

	// 之所以 非静态的成员变量 + 一个静态成员变量 + 一个非静态的成员函数 打印结果还是 4 
	// 是因为 非静态成员变量 也是不属于任何一个类,成员变量 和 成员函数分开存储
	// Sizeof p =4

}

int main()
{	 
	// Test();
	//Test01();
	//Test02();
	Test03();

	system("pause"); // 按任意键继续!
	return 0;
}

注:

        只有非静态成员变量才属于类,其余的包括 静态成员变量、非静态成员函数 和 静态成员函数都不属于类;

        另外,一个空的对象的大小为 1 字节;

2.4.3.2 this 指针概念

        通过上一节的学习,我们知道 C++ 中成员变量和成员函数是分开存储的

        每一个非静态成员函数只诞生一个函数实例,也就是说多个同类型的对象对共用同一块代码,也就是说多个对象都可以调用这一个非静态成员函数

        那么如何区分到底是哪一个对象调用的成员函数呢?

C++ 通过特殊的对象指针,this 指针,解决上述问题,this 指针指向被调用的成员函数所属的对象

        this 指针是隐含在每一个非静态成员函数内的一种指针

        this 指针不需要定义,直接使用即可

this 指针的用途:

        当形参和成员变量同名时,可用 this 指针来区分

        在类的非静态成员函数中返回对象本身,可使用 return *this

class People
{
public:
	People(int age)
	{
		// this 指针指向 被调用的成员函数所属的对象
		// 解释一下上述这句话:如果不加 this 指针,那么无法区分形参和成员变量age
		// 如果加上this,那么此时this指向被调用的成员函数所属的对象
		// 主函数运行Test01    People p(18); ,this 指针指向被调用的成员函数People(int age)所属的对象
		// 成员函数 People 谁在调用呢?显然是 p,所以this指针指向成员函数所属的 p1;
		this->age = age;
	}
	People& PeopleAddAge(People p)
	{
		this->age = this->age + p.age; // 这段代码的意思就是:调用该函数传进来一个年龄,加到我们已有的age上,实现年龄的加法
		return *this;
	}
	int age;
};

// 1. 解决名称冲突
void Test01()
{
	People p(18); // 通过查看上述代码可以发现:函数的形参和成员变量相同都为age
	// 编译器认为三个 age 都是相同的,所以不存在赋值操作
	cout << "年龄为:" <<p.age<< endl;
	
	// 最终输出结果  年龄为:-858993460
	// 要想解决这一问题,1. 修改成员变量和形参变得不同;2. 需要引入 this 指针;
	// 加上 this 指针后输出的结果为 :年龄为:18
}
// 2. 返回对象本身用 *this
void Test02()
{
	People p1(10);
	People p2(10);

	// 下述程序实现把 p2 的年龄加到 p1 上;
	// p2.PeopleAddAge(p1);
	// 打印结果为 20 没有问题

	// 现在我加一次不爽,我想要多加几次
	p2.PeopleAddAge(p1).PeopleAddAge(p1).PeopleAddAge(p1); // 可以看到此时程序在报错
	// 如何更改我们看下述代码:

	// 首先明白出错的原因在于:p2.PeopleAddAge(p1)无返回值,也就是说加一次之后,返回值并不是p2,无法再调用下一个PeopleAddAge(p1)函数
	// 我们给它加上返回值
	// 因为this指向被调用成员函数所属的对象,本身是一个指针,解引用拿到对象本体
	/* People& PeopleAddAge(People p)
	 {
		 this->age = this->age + p.age; // 这段代码的意思就是:调用该函数传进来一个年龄,加到我们已有的age上,实现年龄的加法
		 return *this; 
	 }*/
	// 此时程序打印结果为 : 40

	// 接着看:
	/* People PeopleAddAge(People p)
	{
	this->age = this->age + p.age; // 这段代码的意思就是:调用该函数传进来一个年龄,加到我们已有的age上,实现年龄的加法
	return *this;
	}*/
	// 如果函数返回类型为 People,也就是按值返回,在之前的学习中,我们知道,按值返回只是重新拷贝了另一个变量,跟原本的p2是不同的
	// p2.PeopleAddAge(p1).PeopleAddAge(p1).PeopleAddAge(p1); 
	// p2.PeopleAddAge(p1)返回一个 p2',p2' 和 p2 是截然不同的
	// p2'.PeopleAddAge(p1)返回一个 p2'',p2'' 和 p2 也是截然不同的

	// 所以最终打印结果为 20 ,只有第一次相加是有效的

	cout << "p2 的年龄为:" << p2.age << endl;
}

int main()
{	 
	//Test01();
	Test02();

	system("pause"); // 按任意键继续!
	return 0;
}

切记:

        如果用值来返回,那么会一直产生一个新的变量,每一次产生的变量都是不同的;

        如果采用引用来返回,那么每一次返回的都是 p2 ,也就是都是相同的;

2.4.3.3 空指针访问成员函数

        C++ 中空指针也是可以调用成员函数的,但是也要注意有没有用到 this 指针

        如果用到 this 指针,那么需要加以判断保证代码的健壮性!

// 空指针调用成员函数
class People
{
public:
	void ShowClassName()
	{
		cout << "void ShowClassName()" << endl;
	}
	void ShowPeopleAge()
	{
		if (this==NULL)
		{
			return;
		} // 加上上述这个 if 判断语句,如果 this 指针指向 NULL,则直接 return 退出,保证代码的健壮性!
		cout << "age=" << m_Age << endl;
	}
	int m_Age;
};

void Test01()
{
	People* p = NULL;
	p->ShowClassName(); // 单独运行这个是不会报错的
	p->ShowPeopleAge();

	// 代码运行的结果是:void ShowClassName()
	// 之所以不运行 age= 
	// 是因为 cout << "age=" << m_Age << endl; 在编译器中
	// 默认 m_Age 之前是有 this 指向的
	// cout << "age=" << this->m_Age << endl;
	// 这个时候 p 指针初始化为NULL,导致 this 指针指向成员变量所对应的 p 也为NULL,所以系统报异常,导致访问不到指针指向的地址
}

int main()
{	 
	
	Test01();

	system("pause"); // 按任意键继续!
	return 0;
}
2.4.3.4 const 修饰成员函数

常函数:

        成员函数后加 const 我们称这个函数为常函数

        常函数内不可以修改成员属性

        成员属性声明时加关键字 mutable(英文译为可变的) 后,在常函数中依然可以修改

常对象:

        声明对象前加 const 称该对象为常对象

        常对象只能调用常函数

常函数:

// 常函数:
//		成员函数后加 const 我们称这个函数为常函数
//		常函数内不可以修改成员属性
//		成员属性声明时加关键字 mutable(英文译为可变的) 后,在常函数中依然可以修改
// 常对象:
//		声明对象前加 const 称该对象为常对象
//		常对象只能调用常函数
class People
{
public:
	// this 指针的本质		是指针常量,也就是说指针指向地址的值可以改变,但是指针的指向是不可以发生改变的
	// 如果在成员函数后面加上 const,修饰的是 this 的指向,让指针指向的值也不可以发生改变
	// 相当于 const People* const this
	void ShowPerson() const  // 常函数			此时都会报错!!!
	{
		/*this->m_A = 100;
		this = NULL;*/
		this->m_B = 100;
	}
	int m_A;
	mutable int m_B;  // 定义一个新的变量,在常函数中也可以修改值,用 mutable;
};

int main()
{	 
	
	

	system("pause"); // 按任意键继续!
	return 0;
}

2.4.4 友元

        生活中你的家有客厅(Public),有你的卧室(Private)

        客厅所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去

        但是呢,你也可以允许你的好闺蜜好基友进去。

        定义:在程序里,有些私有属性,也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术

        友元的目的就是让一个函数或者类  访问另一个类中的私有成员

        友元的关键字为:friend

友元的三种实现:

        全局函数做友元

        类做友元

        成员函数做友元

全局函数做友元:

// 全局函数做友元
class Building
{
	// 类似于函数的声明,在函数 void GoodGay(Building *Building); 之前加上关键字 friend
	// 表示 函数void GoodGay(Building *Building);是类的好朋友,此时可以访问私有权限下的属性
	friend void GoodGay(Building *Building);
public:
	Building()
	{
		m_SittingRoom = "客厅";
		m_BedRoom = "卧室"; 
	}
public: // 公共权限,类内可以访问,类外也可以访问
	string m_SittingRoom;  // 客厅
private: // 私人权限,类内可以访问,类外不可以访问
	string m_BedRoom;  // 卧室
};
// 现在通过全局函数做友元,访问私人权限
void GoodGay(Building *Building)
{
	cout << "好基友全局函数 正在访问:" << Building->m_SittingRoom << endl;

	// cout << "好基友全局函数 正在访问:" << Building->m_BedRoom << endl; // 如果访问 m_BedRoom 的话,很显然会报错,因为 m_BedRoom 是私人权限下
	// 但是如果加上关键字 friend,就不一样了:
	cout << "好基友全局函数 正在访问:" << Building->m_BedRoom << endl;
}
void Test()
{
	Building building;
	GoodGay(&building); // 因为 GoodGay 函数的参数是指针类型,所以需要传地址进去
}

int main()
{	 
	Test();

	system("pause"); // 按任意键继续!
	return 0;
}

类做友元:

// 类做友元
// 也就是一个类可以访问另一个类中的私有属性
class Building; // 必须声明这个对象,不然 GoodGay 中调用了Building这个类就会报错
class GoodGay
{
public:
	GoodGay();
	void visit(); // 定义visit函数用于去访问Building类的成员属性

	Building *building;
};
class Building
{
	// GoodGay() 是本类的好朋友,可以访问本类的私有属性
	friend class GoodGay;
public:
	Building(); // 构造函数初始化成员属性
public:
	string m_SittingRoom;  // 卧室
private:
	string m_BedRoom; // 客厅
};
// 类外初始化成员属性
Building::Building()
{
	m_SittingRoom = "客厅";
	m_BedRoom = "卧室";
}
GoodGay::GoodGay()
{
	building = new Building; // 相当于在堆区开辟一个对象,让 building 指向这个新开辟的对象
}
void GoodGay::visit()
{
	cout << "好基友正在访问:" << building->m_SittingRoom << endl;
	cout << "好基友正在访问:" << building->m_BedRoom << endl;
}
void Test()
{
	GoodGay gg; // 代码比较乱,下面解释一下这段代码什么意思?
	// 首先给类 GoodGay 创建一个对象gg,首先 GoodGay 就会调用自身的构造函数进行初始化,
	// 初始化中 building = new Building;  相当于在堆区创建一个对象 Building ,Building 就会调用自身的构造函数进行初始化
	// 此时会初始化 m_SittingRoom 和 m_BedRoom;
	gg.visit(); // 通过对象 gg 访问 visit() 
}
int main()
{	 
	Test();
	system("pause"); // 按任意键继续!
	return 0;
}

成员函数做友元:

// 类做友元
// 也就是一个类可以访问另一个类中的私有属性
class Building; // 必须声明这个对象,不然 GoodGay 中调用了Building这个类就会报错
class GoodGay
{
public:
	GoodGay();
	void visit(); // 定义visit函数用于去访问Building类的成员属性

	Building *building;
};
class Building
{
	// 这一行代码表示:成员函数做类的友元,也就表示该成员函数是Building的朋友,可以访问私有属性
	// GoodGay::visit(); 这是为了告诉编译器 visit 属于类 GoodGay;
	friend void GoodGay::visit();
public:
	Building(); // 构造函数初始化成员属性
public:
	string m_SittingRoom;  // 卧室
private:
	string m_BedRoom; // 客厅
};
// 类外初始化成员属性
Building::Building()
{
	m_SittingRoom = "客厅";
	m_BedRoom = "卧室";
}
GoodGay::GoodGay()
{
	building = new Building; // 相当于在堆区开辟一个对象,让 building 指向这个新开辟的对象
}
void GoodGay::visit()
{
	cout << "好基友正在访问:" << building->m_SittingRoom << endl;
	cout << "好基友正在访问:" << building->m_BedRoom << endl;
}
void Test()
{
	GoodGay gg; // 代码比较乱,下面解释一下这段代码什么意思?
	// 首先给类 GoodGay 创建一个对象gg,首先 GoodGay 就会调用自身的构造函数进行初始化,
	// 初始化中 building = new Building;  相当于在堆区创建一个对象 Building ,Building 就会调用自身的构造函数进行初始化
	// 此时会初始化 m_SittingRoom 和 m_BedRoom;
	gg.visit(); // 通过对象 gg 访问 visit() 
}
int main()
{
	Test();
	system("pause"); // 按任意键继续!
	return 0;
}

2.4.5 运算符重载

        运算符重载的概念对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型

// 对于内置的数据类型,编译器是知道如何去运算的
// 何为内置的数据类型,比如说加减乘除等……
int a = 10;
int b = 10;
int c = a + b; // 编译器是知道加法如何运算的

// 但是:
class People
{
public:
	int m_A;
	int m_B;
};
void Test()
{
	People p1;
	p1.m_A = 10;
	p1.m_B = 10;

	People p2;
	p2.m_A = 10;
	p2.m_B = 10;

	People p3 = p1 + p2; // 此时编译器是无法计算诸如这类的加法运算的;
}
// 通过自己写成员函数,实现两个对象相加属性后返回新的对象
People PeopleAddPerson(People &p) // 保证返回类型是 People 类型
{
	People Temp;
	Temp.m_A = this->m_A + p.m_A;
	Temp.m_B = this->m_B + p.m_B;
	return Temp;
}
2.4.5.1 加号运算符重载

        作用实现两个自定义数据类型相加的运算

成员函数重载加号:

// 加号运算符重载

class People
{
public:
	// 1. 成员函数重载+号
	People operator+(People &p)
	{
		People Temp;
		Temp.m_A = this->m_A + p.m_A;
		Temp.m_B = this->m_B + p.m_B;
		return Temp;
	}

public:
	int m_A;
	int m_B;
};
void Test()
{
	People p1;
	p1.m_A = 10;
	p1.m_B = 10;

	People p2;
	p2.m_A = 10;
	p2.m_B = 10;

	People p3 = p1 +  p2; // 这样会报错,显示没有与这个运算符相匹配的运算符
	// 但是如果上述定义了成员函数重载的话,那么这个加法就不再会报错了

	cout << "p3.m_A=" << p3.m_A << endl;
	cout << "p3.m_B=" << p3.m_B << endl;
}

// 2. 全局函数重载+号


int main()
{
	Test();
	system("pause"); // 按任意键继续!
	return 0;
}

全局函数重载加号:

// 加号运算符重载

class People
{
public:
	// 1. 成员函数重载+号
	/*People operator+(People &p)
	{
		People Temp;
		Temp.m_A = this->m_A + p.m_A;
		Temp.m_B = this->m_B + p.m_B;
		return Temp;
	}*/

public:
	int m_A;
	int m_B;
};
// 2. 全局函数重载+号
People operator+(People &p1,People &p2)
{
	People Temp;
	Temp.m_A = p1.m_A + p2.m_A;
	Temp.m_B = p1.m_B + p2.m_B;
	return Temp;
}
void Test()
{
	People p1;
	p1.m_A = 10;
	p1.m_B = 10;

	People p2;
	p2.m_A = 10;
	p2.m_B = 10;

	People p3 = p1 +  p2; // 这样会报错,显示没有与这个运算符相匹配的运算符
	// 但是如果上述定义了成员函数重载的话,那么这个加法就不再会报错了

	cout << "p3.m_A=" << p3.m_A << endl;
	cout << "p3.m_B=" << p3.m_B << endl;
}


int main()
{
	Test();
	system("pause"); // 按任意键继续!
	return 0;
}

// 学到这里,我们要明白:
// People p3 = p1 + p2;
// 成员函数实现运算符重载的本质:
// People p3 = p1.operator+(p2);
// 全局函数实现运算符的本质
// People p3 = operator+(p1,p2);
2.4.5.2 左移运算符重载

        作用可以输出自定义数据类型

所谓的可以输出自定义数据类型的意思就是说:

class People
{
public:

	int m_A;
	int m_B;
};
void Test()
{
	People p;
	p.m_A = 10;
	p.m_B = 10;
    // 上述程序很简单,就是创建一个类,定义全局函数 Test 去初始化这个类

	cout << p << endl;  // 这句代码显然会出错
	// 上述代码的意思就是打印自定义的数据类型p;
	// 实际上编译器是不知道 p 里面有 m_A 和 m_B 的;
	// 但是通过左移运算符重载可以实现输出自定义的数据类型;
}
// 左移运算符的重载
class People
{
	friend ostream & operator<<(ostream &cout, People &p);
public:
	People(int a, int b)
	{
		m_A = a;
		m_B = b;
	}
private:
	int m_A;
	int m_B;
};
// 采用全局函数进行运算符重载
ostream & operator<<(ostream &cout, People &p)
{
	cout << "m_A=" << p.m_A << " m_B=" << p.m_B;
	return cout; // 保证链式程序编写
}
void Test()
{
	People p(10,10);
	cout << p << endl;
}

int main()
{
	Test();
	system("pause"); // 按任意键继续!
	return 0;
}

这里解释一下:

        为什么加法运算符重载可以用成员函数实现,也可以使用全局函数实现;而左移运算符只能用全局函数来实现;

        因为重载的是cout,如果使用成员函数来实现的话,就是 People operator<<(People &p),想当于 p<<cout,正好和我们打印输出 cout<<p 是相反的;

2.4.5.3 递增运算符重载

        作用:通过重载递增运算符,实现自己的整型数据

// 重载递增运算符

// 自定义整型
class MyInteger
{
	friend ostream& operator<<(ostream& cout, MyInteger &p);
public:
	MyInteger()
	{
		m_Num = 0;
	}
	// 重载前置++运算符
	MyInteger& operator++() // 返回引用是为了一直对一个数据进行递增操作
		// 如果是返回值的话,那么每一次返回的值都不一样,相当于创建了一个新的数据
		// 引用只能初始化一次,是无法改变的;
	{
		m_Num++; // 先进行 ++ 运算
		return *this; // 然后再将自身做返回  (先加加再返回就是前置++的运算流程) 
	}
	// 重载后置++运算符
	MyInteger operator++(int) // 如果不加int,那么前置++和后置++会认为是函数重定义,返回类型是无法区分函数重定义的
		// 加上int,int代表占位参数,可以用于区分前置和后置递增;
	{
		MyInteger Temp = *this;
		m_Num++;
		return Temp; // 后置++就是先加完,然后返回加完之后的值;
		// 需要先定义一个变量记录当前的值,然后加完
		// 完后之前的值
	}// 之所以返回值而不是返回引用,是因为返回引用相当于返回Temp一个局部变量,局部变量的生命周期随函数的消亡而消亡!
	// 离开这个函数,局部变量Temp就无了,相当于非法操作!

private:
	int m_Num;
};
ostream& operator<<(ostream& cout, MyInteger &p) // 左移运算符重载
{
	cout << "m_Num=" << p.m_Num;
	return cout;
}
void Test()
{
	MyInteger myint;

	cout << ++myint << endl;
	cout << myint++ << endl;
}

int main()
{
	Test();
	system("pause"); // 按任意键继续!
	return 0;
}

2.4.5.4 赋值运算符重载

C++ 编译器至少给一个类添加 4 个函数:

  •         1. 默认构造函数(无参,函数体为空)
  •         2. 默认析构函数(无参,函数体为空)
  •         3. 默认拷贝构造函数,对属性进行值拷贝
  •         4. 赋值运算符 operator=,对属性进行值拷贝

如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题;

首先先来看这样的一个问题:

        首先,很明显内置运算符是允许我们进行 a=b=c 的操作的,显然输出的值是 30;那么赋值运算符重载是否允许呢?

// 赋值运算符重载
class People
{
public:
	People(int age)
	{
		m_Age = new int(age);
	}
	// 堆区开辟的内存需要我们主动去释放掉!
	~People()
	{
		if (m_Age!=NULL) // 开辟内存地址指向不为空
		{
			delete m_Age;
			m_Age = NULL;
		}
	}
	// 重载 赋值运算符
	People& operator=(People &p)
	{
		// 编译器提供的是浅拷贝,也就是最简单的赋值操作
		// m_Age = p.m_Age;

		// 先判断是否有属性在堆区,如果有先释放干净,然后再深拷贝
		if (m_Age!=NULL)
		{
			delete m_Age;
			m_Age = NULL;
		}
		// 深拷贝
		m_Age = new int(*p.m_Age); // 在堆区重新开辟一个和 m_Age 同样大小的空间来存储数据 m_Age;
		// 返回自身
		return *this;
	}
	int *m_Age;
};
void Test()
{
	People p1(18);
	People p2(20);
	People p3(30);

	p3 = p2 = p1; // 赋值操作
	// 赋值操作后,主动的释放掉内存,会报错
	// 此时会出现浅拷贝的问题,导致内存出现重复释放的问题;
	// 需要深拷贝来解决这一问题

	cout << "p1的年龄为:" << *p1.m_Age<<endl;
	cout << "p2的年龄为:" << *p2.m_Age << endl;
	cout << "p3的年龄为:" << *p3.m_Age << endl;
}

int main()
{
	Test();
	/*int a = 10;
	int b = 20;
	int c = 30;

	a = b = c;
	cout << "a=" << a << endl;
	cout << "b=" << b << endl;
	cout << "c=" << c << endl;*/

	system("pause"); // 按任意键继续!
	return 0;
}

2.4.5.5 关系运算符重载

        作用:重载关系运算符,可以让两个自定义类型对象进行对比操作

// 关系运算符重载
class People
{
public:
	People(string Name,int Age)
	{
		m_Name = Name;
		m_Age = Age;
	}
	// 重载等号
	bool operator==(People &p)
	{
		if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
			return true;
		else
			return false;
	}
	// 重载不等于!=
	bool operator!=(People &p)
	{
		if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
			return false;
		else
			return true;
	}

	string m_Name;
	int m_Age;
};

void Test()
{
	People p1("Tom",18);
	People p2("Tom", 18);

	if (p1!=p2)
	{
		cout << "p1 和 p2 是不相等的!" << endl;
	}
	else
		cout << "p1 和 p2 是相等的!" << endl;
}
int main()
{
	Test();
	
	system("pause"); // 按任意键继续!
	return 0;
}

2.4.5.6 函数调用运算符重载
  •         函数调用运算符()也可以重载
  •         由于重载后使用的方式非常像函数的调用,因此称为仿函数
  •         仿函数没有固定写法,非常灵活
// 函数调用运算符重载  小括号()
class MyPrint
{
public:
	// 重载函数调用运算符
	void operator()(string test) // 打印,所以把要打印的字符串作为参数传过来
	{
		cout << test << endl;
	}
};

void Test()
{
	MyPrint myprint;
	myprint("Hello World!");
}
// 仿函数非常灵活,接着看
class MyAdd
{
public:
	int operator()(int a,int b)
	{
		return a + b;
	}
};
void Test01()
{
	MyAdd myadd;
	int Temp=myadd(10, 10);
	cout << "Temp=" << Temp << endl;

	// 匿名函数对象
	cout << MyAdd()(10, 10) << endl;
	// MyAdd() 这称为匿名对象,MyAdd myadd;则代表给它起了个名字;
}

int main()
{
	Test();
	Test01();
	
	system("pause"); // 按任意键继续!
	return 0;
}

2.4.6 继承

        继承是面向对象三个特性之一;

有些类与类之间存在特殊的关系,例如下图中:

我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性

这个时候我们就可以考虑利用继承的技术,减少重复代码;

2.4.6.1 继承的基本语法

        假设我们要设计一个页面,那么不同页面必不可少的存在相同的部分,页头相同,又或者页尾相同,又或者左侧栏相同;

        如果借助继承的思想,可明显减少代码量;

// 普通页面实现
// Jave 页面
class Jave
{
public:
	// 页面的头部、尾部。左侧列表基本上都一样
	void Header()
	{
		cout << "首页、公开课、登录、注册……(公共头部)" << endl;
	}
	void footer()
	{
		cout << "帮助中心、交流合作、站内地图……(公共底部)" << endl;
	}
	void left()
	{
		cout << "Jave、Pathon、C++/……(公共分类列表)" << endl;
	}
	// 一个页面的中心内容,或者可以说是一个页面的核心是不同的
	void conter()
	{
		cout << "Jave学科视频" << endl;
	}
};
// Python 页面
class Python
{
public:
	// 页面的头部、尾部。左侧列表基本上都一样
	void Header()
	{
		cout << "首页、公开课、登录、注册……(公共头部)" << endl;
	}
	void footer()
	{
		cout << "帮助中心、交流合作、站内地图……(公共底部)" << endl;
	}
	void left()
	{
		cout << "Jave、Pathon、C++/……(公共分类列表)" << endl;
	}
	// 一个页面的中心内容,或者可以说是一个页面的核心是不同的
	void conter()
	{
		cout << "Python学科视频" << endl;
	}
};
// C++ 页面
class CPP
{
public:
	// 页面的头部、尾部。左侧列表基本上都一样
	void Header()
	{
		cout << "首页、公开课、登录、注册……(公共头部)" << endl;
	}
	void footer()
	{
		cout << "帮助中心、交流合作、站内地图……(公共底部)" << endl;
	}
	void left()
	{
		cout << "Jave、Pathon、C++/……(公共分类列表)" << endl;
	}
	// 一个页面的中心内容,或者可以说是一个页面的核心是不同的
	void conter()
	{
		cout << "C++学科视频" << endl;
	}
};
void Test()
{
	cout << "Jave下载视频页面如下:" << endl;
	Jave jave;
	jave.Header();
	jave.footer();
	jave.left();
	jave.conter();
	cout << "------------------------- " << endl;
	cout << "Python下载视频页面如下:" << endl;
	Python python;
	python.Header();
	python.footer();
	python.left();
	python.conter();
	cout << "------------------------- " << endl;
	cout << "C++下载视频页面如下:" << endl;
	CPP cpp;
	cpp.Header();
	cpp.footer();
	cpp.left();
	cpp.conter();
}

int main()
{
	Test();
	
	system("pause"); // 按任意键继续!
	return 0;
}

        仔细看上述代码:可以发现代码的重复部分很多,在实际的代码编写过程中,应尽可能的避免重复代码的出现;这就需要用到继承

继承的思想:

继承的好处:可以减少重复代码

语法:class 子类 : 继承方式  父类

子类:也称为  派生类

父类:也称为  基类

// 普通页面实现
// 继承思想实现页面
// 公共页面,大家都具有的元素
class BasePage
{
public:
	// 页面的头部、尾部。左侧列表基本上都一样
	void Header() 
	{
		cout << "首页、公开课、登录、注册……(公共头部)" << endl;
	}
	void footer()
	{
		cout << "帮助中心、交流合作、站内地图……(公共底部)" << endl;
	}
	void left()
	{
		cout << "Jave、Pathon、C++/……(公共分类列表)" << endl;
	}
};
// Jave 页面
class Jave :public BasePage // 继承思想下,class Jave 是不断的,加上冒号,public BasePage 表示公共的部分,
	// 也就表示 Jave 这个类默认调用公共的类:public BasePage
{
public:
	void center()
	{
		cout << "Jave 网课视频" << endl;
	}
};
// C++ 页面
class CPP:public BasePage
{
public:
	void center()
	{
		cout << "C++ 网课视频" << endl;
	}
};
// Python 页面
class Python:public BasePage
{
public:
	void center()
	{
		cout << "Python 网课视频" << endl;
	}
};

void Test()
{
	cout << "Jave下载视频页面如下:" << endl;
	Jave jave;
	jave.Header();
	jave.footer();
	jave.left();
	jave.center();
	cout << "------------------------- " << endl;
	cout << "Python下载视频页面如下:" << endl;
	Python python;
	python.Header();
	python.footer();
	python.left();
	python.center();
	cout << "------------------------- " << endl;
	cout << "C++下载视频页面如下:" << endl;
	CPP cpp;
	cpp.Header();
	cpp.footer();
	cpp.left();
	cpp.center();
}

int main()
{
	Test();
	
	system("pause"); // 按任意键继续!
	return 0;
}

2.4.6.2 继承方式

继承的语法:class 子类 : 继承方式  父类

继承方式一共有三种:

  •         公共继承
  •         保护继承
  •         私有继承

上述流程图的意思就是说:

        继承主要分为公共继承保护继承私有继承

        继承不管是哪种继承,父类中的私有属性三种继承都不可以访问;(也可以理解为父类也是有隐私的,子类不可以访问)

        如果是共用继承,Public,那么父类中成员属性属于哪个类,共有继承得到的就是哪个类

        如果是保护继承,Protected,那么父类中不管属性是属于哪个类,保护继承得到的都属于保护权限;

        如果是私有继承,Private,那么父类中不管属性是属于哪个类,私有继承得到的都属于私有权限;

 

2.4.6.3 继承中的对象模型

        本部分主要解决一个问题:从父类继承过来的成员,哪些属于子类对象中?

// 继承中的对象模型
// 父类
class Father
{
public:
	int m_A;
protected:
	int m_B;
private:
	int m_C;
};

class Son :public Father
{
public:
	int m_D;
};
void Test()
{
	Son son;
	// 父类中非静态的成员属性都会被子类所继承下去
	cout << "Size Of Son=" << sizeof(son) << endl; // 16
	// 这个16是子类的4个字节m_D+父类的 m_A、m_B、m_C 12个字节
	// 这里可能会有疑问,私有属性不是不可以访问吗?
	// 事实上,父类中私有属性是被编译器给隐藏了,因此是访问不到,但是确实被继承下去了
}

int main()
{
	Test();
	
	system("pause"); // 按任意键继续!
	return 0;
}
2.4.6.4 继承中构造和析构顺序

        子类继承父类后,当创建子类对象,也会调用父亲的构造函数。

        问题:父类和子类的构造和析构顺序是谁先谁后呢?

// 继承中的构造和析构的顺序
class Father
{
public:
	Father()
	{
		cout << "Father 的构造函数" << endl;
	}
	~Father()
	{
		cout << "Father 的析构函数" << endl;
	}
};
class Son :public Father
{
public:
	Son()
	{
		cout << "Son 的构造函数" << endl;
	}
	~Son()
	{
		cout << "Son 的析构函数" << endl;
	}
};
void Test()
{
	Son son; // 调用子类的对象,那么子类的对象对同时的调用父类的默认函数!
}

int main()
{
	Test();
	
	system("pause"); // 按任意键继续!
	return 0;
}

综上所述:

        继承中的构造和析构的顺序是:先有父类的构造,再有子类的构造;析构的顺序和构造顺序是相反的;(这也很好理解,先有父亲然后才能再有儿子)

2.4.6.5 继承同名成员处理方式

        问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?

  •         访问子类同名成员        直接访问即可
  •         访问父类同名成员        需要加作用域

同名成员变量:

// 继承中同名成员处理方式
class Father
{
public:
	Father()
	{
		m_A = 100;
	}
	int m_A;
};
class Son :public Father
{
public:
	Son()
	{
		m_A = 200;
	}
	int m_A;
};
void Test()
{
	Son son; 
	cout << "m_A=" << son.m_A << endl; // 当子类和父类成员变量出现同名的情况时,直接访问得到的是子类的成员变量,此时打印的是200
	// 要想要通过子类访问父类的同名成员变量,需要加作用域
	cout << "m_A=" << son.Father::m_A << endl; // 当加上作用域 Father 表示访问的是父类,此时打印的是 100;
}

int main()
{
	Test();
	
	system("pause"); // 按任意键继续!
	return 0;
}

同名成员函数:

// 继承中同名成员处理方式
class Father
{
public:
	Father()
	{
		m_A = 100;
	}
	void Func()
	{
		cout << "Father 下成员函数Func()的调用:" << endl;
	}
	int m_A;
};
class Son :public Father
{
public:
	Son()
	{
		m_A = 200;
	}
	void Func()
	{
		cout << "Son 下成员函数Func()的调用:" << endl;
	}
	int m_A;
};
void Test()
{
	Son son; 
	cout << "m_A=" << son.m_A << endl; // 当子类和父类成员变量出现同名的情况时,直接访问得到的是子类的成员变量,此时打印的是200
	// 要想要通过子类访问父类的同名成员变量,需要加作用域
	cout << "m_A=" << son.Father::m_A << endl; // 当加上作用域 Father 表示访问的是父类,此时打印的是 100;
}
// 成员函数同名的处理法则
void Test01()
{
	Son ss;
	ss.Func(); // 成员函数同名,直接访问的仍然是子类的成员函数,打印的结果是:Son 下成员函数Func()的调用:
	// 要想通过子类访问父类同名的成员函数,同样需要加作用域
	ss.Father::Func(); // 当成员函数同名时,需要加上作用域,表示Func成员函数属于Father类;此时打印的结果是:Father 下成员函数Func()的调用:
}

int main()
{
	// Test();
	Test01();
	
	system("pause"); // 按任意键继续!
	return 0;
}

注意:如果子类中出现和父类同名的成员函数,子类的同名成员函数会隐藏掉父类中所有同名成员函数!

           这个意思就是说:假设子类中有一个成员函数,但是父类中有100个成员函数,只要这100个成员函数中有一个成员函数和子类的成员函数同名,那么子类的这一个成员函数会隐藏掉父类中100个成员函数,这个需要特别注意,不是隐藏一个,而是隐藏掉100个成员函数,如果程序中调用父类的成员函数,就会报错,因为被隐藏掉了,需要加作用域;

2.4.6.6 继承同名静态成员处理方式

        问题:继承中同名的静态成员在子类对象上如何进行访问?

        静态成员和非静态成员出现同名,处理方式一致

  •         访问子类同名成员        直接访问即可
  •         访问父类同名成员        需要加作用域

同名静态变量访问:

// 继承中同名静态成员处理方式
class Father
{
public:
	static int m_A; 
	// 1. 静态成员变量类内声明,类外初始化!
	// 2. 所有变量共享一个函数域!
	// 3. 静态成员变量在编译时就分配内存!
};
int Father::m_A = 100;
class Son :public Father
{
public:
	static int m_A;
	// 1. 静态成员变量类内声明,类外初始化!
	// 2. 所有变量共享一个函数域!
	// 3. 静态成员变量在编译时就分配内存!
};
int Son::m_A = 200;
void Test()
{
	// 1. 静态成员变量可以通过类来访问
	cout << "通过对象来访问:" << endl;
	Son son;
	cout << "m_A=" << son.m_A << endl; // 直接访问的是子类的静态成员变量
	cout << "m_A=" << son.Father::m_A  << endl; // 加上作用域访问的就是父类的静态成员变量

	// 2. 静态成员变量还可以通过类名来访问
	cout << "通过类名来访问:" << endl;
	cout << "m_A=" << Son::m_A << endl; // 直接访问的是子类的静态成员变量
	cout << "m_A=" << Son::Father::m_A << endl; // 加上作用域访问的就是父类的静态成员变量
}

int main()
{
	Test();
	
	system("pause"); // 按任意键继续!
	return 0;
}

同名静态函数访问:

// 继承中同名静态成员处理方式
class Father
{
public:
	static int m_A; 
	// 1. 静态成员变量类内声明,类外初始化!
	// 2. 所有变量共享一个函数域!
	// 3. 静态成员变量在编译时就分配内存!
	static void Func()
	{
		cout << "父类下同名函数 Func() 的调用:" << endl;
	}
};
int Father::m_A = 100;
class Son :public Father
{
public:
	static int m_A;
	// 1. 静态成员变量类内声明,类外初始化!
	// 2. 所有变量共享一个函数域!
	// 3. 静态成员变量在编译时就分配内存!
	static void Func()
	{
		cout << "子类下同名函数 Func() 的调用:" << endl;
	}
};
int Son::m_A = 200;
void Test()
{
	// 1. 静态成员变量可以通过类来访问
	cout << "通过对象来访问:" << endl;
	Son son;
	cout << "m_A=" << son.m_A << endl; // 直接访问的是子类的静态成员变量
	cout << "m_A=" << son.Father::m_A  << endl; // 加上作用域访问的就是父类的静态成员变量

	// 2. 静态成员变量还可以通过类名来访问
	cout << "通过类名来访问:" << endl;
	cout << "m_A=" << Son::m_A << endl; // 直接访问的是子类的静态成员变量
	cout << "m_A=" << Son::Father::m_A << endl; // 加上作用域访问的就是父类的静态成员变量
}
void Test01()
{
	// 1. 静态成员函数可以通过类来访问
	cout << "通过对象来访问:" << endl;
	Son son;
	son.Func(); // 直接访问的是子类的静态成员函数
	son.Father::Func(); // 加上作用域访问的就是父类的静态成员函数

	// 2. 静态成员函数还可以通过类名来访问
	cout << "通过类名来访问:" << endl;
	Son::Func(); // 直接访问的是子类的静态成员函数
	Son::Father::Func(); // 加上作用域访问的就是父类的静态成员函数
}

int main()
{
	Test01();
	
	system("pause"); // 按任意键继续!
	return 0;
}

注意:如果子类中出现和父类同名的成员函数,子类的同名成员函数会隐藏掉父类中所有同名成员函数!

           这个意思就是说:假设子类中有一个成员函数,但是父类中有100个成员函数,只要这100个成员函数中有一个成员函数和子类的成员函数同名,那么子类的这一个成员函数会隐藏掉父类中100个成员函数,这个需要特别注意,不是隐藏一个,而是隐藏掉100个成员函数,如果程序中调用父类的成员函数,就会报错,因为被隐藏掉了,需要加作用域;

2.4.6.7 多继承语法

        C ++ 中允许一个类继承多个类

        语法:class 子类:继承方式  父类1,继承方式  父类2……

        多继承可能会引发父类中有同名成员出现,需要加作用域区分

        C++ 在实际的开发中不建议使用多继承

// 多继承语法
class Father1
{
public:
	Father1()
	{
		m_A = 100;
	}
	int m_A;
};
class Father2
{
public:
	Father2()
	{
		m_A = 200;
	}
	int m_A;
};
// 多继承,Son 同时继承Father1和Father2
class Son :public Father1, public Father2
{
public:
	Son()
	{
		m_C = 300;
		m_D = 400;
	}
	int m_C;
	int m_D;
};
void Test()
{
	Son son; // 因为此时子类son同时继承了两个父类
				  // 此时如果要访问m_A,那么究竟访问的是Father1中的m_A还是Father2中的m_A,我们不得而知!
	cout << "Father1 中的m_A=" << son.Father1::m_A << endl;
	cout << "Father2 中的m_A=" << son.Father2::m_A << endl;
}
int main()
{
	Test();
	
	system("pause"); // 按任意键继续!
	return 0;
}

2.4.6.8 菱形继承

        菱形继承是继承机制中一个非常有意思的现象:

菱形继承的概念:

        两个派生类(子类)继承同一个基类(父类)

        又有某个类同时继承着两个派生类

        这种继承被称为菱形继承,或者钻石继承

// 动物类
class Animal
{
public:
	int m_Age;
};
// 羊类
// 利用虚继承可以解决菱形继承带来的问题
// 继承之前  加上关键字 virtual 变为虚继承
// Animal 类称为:虚基类

class Sheep :virtual public Animal
{

};
//驼类
class Tuo :virtual public Animal
{

};
// 羊驼类
class SheepTuo :public Sheep, public Tuo
{

};
void Test()
{
	SheepTuo ss;
	ss.Sheep::m_Age = 18; // 羊类继承而来的年龄为18
	ss.Tuo::m_Age = 20; // 驼类继承来的年龄为20

	// 1. 当菱形继承时,两个父类拥有相同的数据,需要加以作用域区分
	cout << "ss.Sheep::m_Age=" << ss.Sheep::m_Age << endl;
	cout << "ss.Tuo::m_Age=" << ss.Tuo::m_Age << endl;
	// 2. 这份数据我们知道,菱形继承导致数据有两份,也就是说羊驼类继承羊类和驼类,会分别得到其中的年龄
	//     由于这两个年龄都是从动物类继承而来的,所以数据是相同的,两份会导致数据重叠,导致资源浪费!!!

}
int main()
{
	Test();
	
	system("pause"); // 按任意键继续!
	return 0;
}

2.4.7 多态

        多态是 C++ 面向对象三大特性之一

2.4.7.1 多态的基本概念

多态分为两类:

  •         静态多态:函数重载 和 运算符重载属于静态多态,复用函数名
  •         动态多态:派生类和虚函数实现运行时多态

静态多态和动态多态的区别:

  •         静态多态的函数地址早绑定 - 编译阶段确定函数地址
  •         动态多态的函数地址晚绑定 - 运行阶段确定函数地址
// 多态
// 动物类
class Animal
{
public:
	virtual void speak() // 加上virtual表示父类的成员函数变为了虚函数
	{
		cout << "动物在说话" << endl;
	}
};
// 猫类
class Cat:public Animal
{
public:
	void speak()
	{
		cout << "小猫在说话" << endl;
	}
};
// 执行说话的函数
void DoSpeak(Animal &animal)
{
	animal.speak(); 
}
void Test()
{
	Cat cat;
	DoSpeak(cat); // 此时打印出的结果是动物在说话
	// 这里就有疑问了,明明输入的参数是cat,最终打印出的却是动物在说话!!!
	// 原因在于void DoSpeak(Animal &animal)的参数是父类Animal类,DoSpeak(cat); 的参数却是子类的对象
	// 现在就是说父类函数在调用子类参数

	// 存在一个地址早绑定的问题,也就是说编译的时候地址就已经绑定好了:
	// 如果想打印小猫在说话的话,那么就不能地址早绑定,需要地址晚绑定!!!
	// 实现地址晚绑定,很简单,将父类的成员函数变为虚函数即可;
	// virtual void speak()
}

int main()
{
	Test();
	
	system("pause"); // 按任意键继续!
	return 0;
}
// 总结:
// 动态多态需要满足:
// 1. 必须存在父子继承关系
// 2. 子类重写父类虚函数(也就是子类重写了父类的virtual void speak()函数,至于virtual可加可不加)
//     重写的概念是:函数返回值类型、函数名、参数列表——完全相同

// 动态多态使用
// 父类的指针或者引用  指向子类对象
// void DoSpeak(Animal &animal),参数是父类 Animal 的引用 &animal
// 但是父类的引用却指向了子类的对象:Animal & animal = cat;
2.4.7.2 多态的原理剖析

        建立在上述代码的基础之上:

1. 假设我们先去掉 virtual,那么此时就不存在虚函数之说,那么就是父类引用指向子类对象,先来打印 Animal 的大小;

void Test01()
{
	cout << "sizeof Animal=" << sizeof(Animal) << endl;
	// 此时打印的结果是 1
	// Animal 中只有一个非静态的成员函数,分开存储,不属于类上的对象 
	// 编译器默认让类为1,在内存上区分不同之间的类

	// 但是当加上 virtual 之后,打印结果变为了 4 个字节
	// 试着猜想一下,类内增加了什么东西
	// 增加这4个字节,实际上是增加了一个指针
}

2. 

class Animal
{
public:
	virtual void speak() // 加上virtual表示父类的成员函数变为了虚函数
	{
		cout << "动物在说话" << endl;
	}
};

我们来看一下这个 Animal 动物类的内部结构

        首先上面第一条我们已经证明了虚函数本质还是一个指针,占用 4 个字节,该指针称为:vfptr(虚函数指针)

        v --- virtual

        f  --- function

        ptr  --- pointer

        虚函数指针 vfptr 指向虚函数表 vftable

        v --- virtual

        f  --- function

        table  --- table

        虚函数表中存储的是:虚函数 virtual void speak() 的地址;也就是 &Animal::speak()

我们来看一下这个 Cat 猫类的内部结构

        首先假设猫类还没有重写(函数返回值类型、函数名、参数列表完全相同)虚函数的时候:

class Cat:public Animal
{
public:
	
};

        这个时候猫类的内部结构依然是一个虚函数指向一个虚函数表

        不过这个时候是继承动物类 Animal 的,所以虚函数表中存储的依然是:&Animal::speak()

class Cat:public Animal
{
public:
	void speak()
	{
		cout << "小猫在说话" << endl;
	}
};

        但是当子类重写父类的虚函数的时候,子类的虚函数表中就会存储子类的虚函数地址&Cat::speak()

        这个时候就会出现动态多态的情况,也就是不再在编译的时候确定函数地址,而是在运行的时候确定函数地址;


        当父类的指针或者引用指向子类的对象时,就会发生多态

        Animal& animal = cat;

        animal.speak();

2.4.7.3 案例一:计算器类

        案例描述:分别利用普通写法多态技术,设计实现两个操作数进行运算的计算器类

普通写法:实现两个操作数进行运算的计算器类

// 普通写法
class Calculator
{
public:
	int GetResult(string str)
	{
		if (str=="+")
		{
			return m_A + m_B;
		}
		else if (str == "-")
		{
			return m_A - m_B;
		}
		else if (str == "*")
		{
			return m_A * m_B;
		}
		else if (str == "/")
		{
			return m_A / m_B;
		}
	}
	int m_A;
	int m_B;
};
void Test()
{
	Calculator c;
	c.m_A = 10;
	c.m_B = 10;

	cout << c.m_A << "+" << c.m_B << "=" << c.GetResult("+") << endl;
	cout << c.m_A << "-" << c.m_B << "=" << c.GetResult("-") << endl;
	cout << c.m_A << "*" << c.m_B << "=" << c.GetResult("*") << endl;
	cout << c.m_A << "/" << c.m_B << "=" << c.GetResult("/") << endl;

}

int main()
{
	Test();
	system("pause"); // 按任意键继续!
	return 0;
}

多态技术:实现两个操作数进行运算的计算器类

多态的优点:

        1. 代码组织结构清晰

        2. 可读性强
 

// 实现计算器抽象类
class AbstructCalculator
{
public:
	virtual int GetResult()
	{
		return 0;
	}
	int m_A;
	int m_B;
};
// 加法计算器类
class AddCalzulator :public AbstructCalculator
{
public:
	int GetResult()
	{
		return m_A + m_B;
	}
};
// 减法计算器类
class SubCalzulator :public AbstructCalculator
{
public:
	int GetResult()
	{
		return m_A - m_B;
	}
};
// 乘法计算器类
class MulCalzulator :public AbstructCalculator
{
public:
	int GetResult()
	{
		return m_A * m_B;
	}
};
void Test()
{
	// 多态的使用条件:
	// 父类的指针或者引用指向子类的对象
	// 加法运算
	AbstructCalculator *abc = new AddCalzulator; // 父类的指针指向子类的对象
	abc->m_A = 100;
	abc->m_B = 100;
	cout << abc->m_A << "+" << abc->m_B << "=" << abc->GetResult() << endl;
	delete abc;
	// 减法运算
	abc = new SubCalzulator; // 父类的指针指向子类的对象
	abc->m_A = 100;
	abc->m_B = 100;
	cout << abc->m_A << "-" << abc->m_B << "=" << abc->GetResult() << endl;
	delete abc;
	// 乘法运算
	abc = new MulCalzulator; // 父类的指针指向子类的对象
	abc->m_A = 100;
	abc->m_B = 100;
	cout << abc->m_A << "*" << abc->m_B << "=" << abc->GetResult() << endl;
	delete abc;
}

int main()
{
	Test();
	system("pause"); // 按任意键继续!
	return 0;
}

2.4.7.4 纯虚函数和抽象类

        在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容

        因此可以将虚函数改为纯虚函数

        纯虚函数语法virtual  返回值类型  函数名  (参数列表)= 0;

        当类中有了纯虚函数,这个类也称为抽象类

抽象类的特点:

        1. 无法实例化对象

        2. 子类必须重写抽象类中的纯虚函数,否则也属于抽象类

// 纯虚类和抽象类

// 实现计算器抽象类
class Father
{
public:
	virtual int GetResult() = 0;
	// 因为虚函数的内容本身是不起作用的,主要是子类重写虚函数
	// 所以父类的虚函数可以设置为纯虚函数,此时这个类称为抽象类;
	// virtual int GetResult() = 0; 这样的语法在 C++ 中是可以通过的
	// 但是如果不加virtual,int GetResult() = 0; 这样的语法是不被通过的;
	int m_A;
	int m_B;
};
// 抽象类的特点
// 1. 无法实例化对象
// 2. 抽象类的子类  必须要重写父类中的纯虚函数,否则也属于抽象类
// (子类必须重写父类的纯虚函数,否则就会报错!!!)

void Test()
{
	
}

int main()
{
	Test();
	system("pause"); // 按任意键继续!
	return 0;
}
2.4.7.5 案例二:制作饮品

        案例描述:制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料

        利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶

// 制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料
// 利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶
class AbstructDrinking
{
public:
	// 煮水:
	virtual void Boil() = 0;
	// 冲泡
	virtual void Brew() = 0;
	// 倒入杯中
	virtual void PourInCup() = 0;
	// 加入辅料
	virtual void PutSomeThing() = 0; 
	// 以上均为纯虚函数
	
	// 制作饮品
	void MakeDrink()
	{
		Boil();
		Brew();
		PourInCup();
		PutSomeThing();
	}
};
// 制作咖啡
class Coffee:public AbstructDrinking
{
public:
	// 煮水:
	virtual void Boil()
	{
		cout << "煮水" << endl;
	}
	// 冲泡
	virtual void Brew()
	{
		cout << "冲泡咖啡" << endl;
	}
	// 倒入杯中
	virtual void PourInCup()
	{
		cout << "倒入杯中" << endl;
	}
	// 加入辅料
	virtual void PutSomeThing()
	{
		cout << "加入糖和牛奶" << endl;
	}
};
// 制作茶叶
class Tea :public AbstructDrinking
{
public:
	// 煮水:
	virtual void Boil()
	{
		cout << "煮水" << endl;
	}
	// 冲泡
	virtual void Brew()
	{
		cout << "冲泡茶叶" << endl;
	}
	// 倒入杯中
	virtual void PourInCup()
	{
		cout << "倒入杯中" << endl;
	}
	// 加入辅料
	virtual void PutSomeThing()
	{
		cout << "加入柠檬" << endl;
	}
};
// 制作
void DoWork(AbstructDrinking* abc) // 提供一个父类的指针
{
	abc->MakeDrink();
	delete abc;
}
void Test()
{
	// 制作咖啡
	DoWork(new Coffee); // 这句程序相当于:AbstructDrinking* abc = new Coffee;
	// 相当于父类的指针指向了子类的对象,产生动态多态;
	cout << "---------------------" << endl;
	// 制作茶叶
	DoWork(new Tea);
}
int main()
{
	Test();
	system("pause"); // 按任意键继续!
	return 0;
}

2.4.7.6 虚析构和纯虚析构

        多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

        解决方法:将父类中的析构函数改为虚析构或者纯虚析构

虚析构和纯虚析构共性:

  •         1. 可以解决父类指针释放子类对象的问题
  •         2. 都需要有具体的函数实现

虚析构和纯虚析构的区别:

  •         如果是纯虚析构,那么该类属于抽象类,无法实例化对象

虚析构语法:virtual ~类名( ){ }

纯虚析构语法:virtual ~类名( ) = 0;

                         类名::~类名(){}

// 虚析构和纯虚析构
class Animal
{
public:
	Animal()
	{
		cout << "Animal 的构造函数调用" << endl;
	}
	// 纯虚函数
	virtual void speak() = 0;  
	~Animal()
	{
		cout << "Animal 的析构函数调用" << endl;
	}
};
class Cat :public Animal
{
public:
	Cat(string name)
	{
		cout << "Cat 的构造函数调用" << endl;
		m_Name = new string(name); // 这句代码的意思就是在堆区存储这个字符串string
		// 返回的类型就是 string 指针类型;用 m_Name 指针类型来接收
	}
	virtual void speak()
	{
		cout <<*m_Name<< "小猫在说话" << endl;
	}
	~Cat()
	{
		if (m_Name!=NULL)
		{
			cout << "Cat 的析构函数调用"<<endl;
			delete m_Name;
			m_Name = NULL;
		}
	}
	string* m_Name;
};
void Test()
{
	Animal* animal = new Cat("Tom");
	animal->speak();
	// 父类指针在析构时候,不用调用子类中析构函数,导致子类如果有堆区属性,出现内存泄露! 
	delete animal;
}

int main()
{
	Test();
	system("pause"); // 按任意键继续!
	return 0;
}

此时会发现:父类指针在析构的时候,不会调用子类析构函数,导致子类如果有堆区属性,出现内存泄露!!!

如何解决这个问题:

        只需要把父类的析构函数改为虚析构函数;

        此时就会执行子类的虚构函数!!!(可以发现,此时打印出:子类 Cat 析构函数调用

纯虚析构:

// 纯虚析构
virtual ~Animal()=0;
// 纯虚析构这样定义是会报错的!
// 因为析构函数内部一般需要定义!
// 这是因为父类也可能在堆区开辟内存,这样就需要析构函数是处理这块内存!

2.5 文件操作

        程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放

        通过文件可以将数据持久化

        C++ 中对文件操作需要包含头文件 <fstream>

文件类型分为两种:

  •         1. 文本文件   - 文件以文本的 ASCII 码形式存储在计算机中
  •         2. 二进制文件   - 文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们

操作文件的三大类:

  •         1. ofstream:写操作(o代表output)
  •         2. ifstream:读操作(i代表input)
  •         3. fstream:读写操作

2.5.1 文本文件 

2.5.1.1 写文件

写文件步骤如下:

        1. 包含头文件

                #include <fstream>

        2. 创建流对象

                ofstream ofs;

        3. 打开文件

                ofs.open("文件路径",打开方式);

打开方式                        解释

ios::in                        为读文件而打开文件

ios::out                      为写文件而打开文件

ios::ate                      初始位置:文件尾

ios::app                     追加方式写文件

ios::trunc                   如果文件存在先删除,再创建

ios::binary                 二进制方式

注意:文件打开方式可以配合使用,利用 | 操作符

例如:用二进制方式写文件 ios::binary | ios::out

        4. 写数据

                ofs<<"写入的数据";

        5. 关闭文件

                ofs.close();

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <string> // 用 C++ 风格的字符串需要包含这个头文件
#include <ctime> // time 系统时间头文件包含
#include <fstream> // 文件操作必须引用该头文件
using namespace std;

// 文本文件  写文件
void Test()
{
	// 1. 包含头文件 fstream
	// #include <fstream>
	// 2. 创建流对象
	ofstream ofs;
	// 3. 指定打开方式
	ofs.open("text.txt",ios::out);
	// 4. 写内容
	ofs << "姓名:张三" << endl;
	ofs << "姓别:男" << endl;
	ofs << "年龄:18" << endl;
	// 5. 关闭文件
	ofs.close();
}

int main()
{
	Test();
	system("pause"); // 按任意键继续!
	return 0;
}

总结:

        1. 文件操作必须包括头文件 fstream

        2. 读文件可以利用 ofstream,或者 fstream 类

        3. 打开文件时候需要指定操作文件的路径,以及打开方式

        4. 利用<<可以向文件中写数据

        5. 操作完毕,要关闭文件

2.5.1.2 读文件

读文件步骤如下:

        1. 包含头文件

                #include <fstream>

        2. 创建流对象

                ofstream ifs;

        3. 打开文件

                ifs.open("文件路径",打开方式);

打开方式                        解释

ios::in                        为读文件而打开文件

ios::out                      为写文件而打开文件

ios::ate                      初始位置:文件尾

ios::app                     追加方式写文件

ios::trunc                   如果文件存在先删除,再创建

ios::binary                 二进制方式

注意:文件打开方式可以配合使用,利用 | 操作符

例如:用二进制方式写文件 ios::binary | ios::out

        4. 读数据

                四种方式读取;

        5. 关闭文件

                ifs.close();

// 文本文件  读文件
void Test()
{
	// 1. 包含头文件 fstream
	// #include <fstream>
	// 2. 创建流对象
	ifstream ifs;
	// 3. 指定打开方式
	ifs.open("text.txt", ios::in);
	// 4. 读内容
	// 通过上节的写内容,此时 text.txt 里面存储着张三的姓名、年龄和性别
	if (!ifs.is_open()) // 判断是否打开文件成功
	{
		cout << "文件打开失败!" << endl;
		return;
	}
	// 第一种方式读数据
	/*char buf[1024] = { 0 };
	while (ifs >> buf) // 一行一行的读,当读到头;当右移到头,返回一个假的标志!
	{
		cout << buf << endl;
	}*/
	// 第二种方式读数据
	//char buf[1024] = { 0 };
	//while (ifs.getline(buf,sizeof(buf)))// getline 一行一行的读
	//	// 第一个参数 指针类型,正好数组名表示首元素地址
	//	// 第二个参数 要读的字节大小
	//{
	//	cout << buf << endl;
	//}
	// 第三种方式读数据
	//string buf;
	//while (getline(ifs,buf)) // 全局函数getline
	//{
	//	cout << buf << endl;
	//}
	// 第四种方式读数据
	char c;
	while ((c=ifs.get())!=EOF) // 一个字符一个字符的读,只要读到的不是字符尾部
		// 字符尾部标志为 EOF
	{
		cout << c;
	}

	// 5. 关闭文件
	ifs.close();
}

int main()
{
	Test();
	system("pause"); // 按任意键继续!
	return 0;
}

2.5.2 二进制文件

        以二进制的方式对文件进行读写操作

        打开方式要指定为 ios::binary

2.5.2.1 写文件

        二进制方式写文件主要利用流对象调用成员函数 write

        函数原型:ostream& write(const char* buffer,int len);

        参数解释:字符指针 buffer 指向内存中一段存储空间。len 是读写的字节数

// 二进制文件
// 写文件
class People
{
public:
	char m_Name[64]; // 姓名
	int m_Age; // 年龄
};
void Test()
{
	// 1. 包含头文件
	// 2. 创建流对象
	ofstream ofs;
	// 3. 打开文件
	ofs.open("People.txt",ios::out | ios::binary); // 二进制下写
	// 4. 写文件
	People p = { "张三", 18 };
	ofs.write((const char*)&p,sizeof(People));
	// 5. 关闭文件
	ofs.close();
}
int main()
{
	Test();
	system("pause"); // 按任意键继续!
	return 0;
}

2.5.2.2 读文件

        二进制方式读文件主要利用流对象调用成员函数 read

        函数原型:istream& read(char* buffer,int len);

        参数解释:字符指针 buffer 指向内存中一段存储空间。len 是读写的字节数

// 二进制文件
// 读文件
class People
{
public:
	char m_Name[64]; // 姓名
	int m_Age; // 年龄
};
void Test()
{
	// 1. 包含头文件
	// 2. 创建流对象
	ifstream ifs;
	// 3. 打开文件   判断文件是否打开成功
	ifs.open("People.txt", ios::in | ios::binary); // 二进制下读
	if (!ifs.is_open())
	{
		cout << "文件打开失败!" << endl;
		return;
	}
	// 4. 读文件 
	People p;
	ifs.read((char*)&p, sizeof(People));
	cout << "姓名:" << p.m_Name << endl;
	cout << "年龄:" << p.m_Age << endl;
	// 5. 关闭文件
	ifs.close();
}
int main()
{
	Test();
	system("pause"); // 按任意键继续!
	return 0;
}

2.6 职工管理系统

2.6.1 管理系统需求

        职工管理系统可以用来管理公司内所有员工的信息

        本次利用 C++ 来实现一个基于多态的职工管理系统

公司中职工分为三类:普通员工、经理、老板,显示信息时,需要显示职工编号、职工姓名、职工岗位、以及职责

普通员工职责:完成经理交给的任务

经理职责:完成老板交给的任务,并下发任务给员工

老板职责:管理公司所有事务

管理系统中需要实现的功能如下:

  •         退出管理程序:退出当前管理系统
  •         增加职工信息:实现批量添加职工功能,将信息录入到文件中,职工信息为:职工编号、姓名、部门编号
  •         显示职工信息:显示公司内部所有职工的信息
  •         删除离职职工:按照编号删除指定的职工
  •         修改职工信息:按照编号修改职工个人信息
  •         查找职工信息:按照职工的编号或者职工的姓名进行查找相关的人员信息
  •         按照编号排序:按照职工编号进行排序,排序规则由用户指定
  •         清空所有文档:清空文件中记录的所有职工信息(清空前需要再次确认,防止误删)

2.6.2 创建项目

创建项目:

        创建项目就是在 Visual Studio 中创建一个项目,这个具体可参考:C++[面向对象的程序设计]_基础入门(上)(万字总结)(建议收藏!!!)-CSDN博客

2.6.3 创建管理类

管理类负责的内容如下:

  •         与用户的沟通菜单界面
  •         对职工增删改查的操作
  •         与文件的读写交互

.cpp文件

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <string> // 用 C++ 风格的字符串需要包含这个头文件
#include <ctime> // time 系统时间头文件包含
#include <fstream> // 文件操作必须引用该头文件
#include "HelloWorld.h"
using namespace std;

// 构造和析构函数空实现
WorkerManager::WorkerManager() // 通过调用类初始化构造函数
{

}
WorkerManager::~WorkerManager() // 通过调用类初始化析构函数
{

}

.h 头文件

#pragma once   // 防止头文件重叠
#include <iostream>  // 包含输入输出流文件
using namespace std;  // 使用标准命名空间



class WorkerManager
{
public:
	// 构造函数
	WorkerManager();
	// 析构函数
	~WorkerManager();
};

2.6.4 菜单功能

.h文件

.h 文件中添加 展示菜单的函数!

class WorkerManager
{
public:
	// 构造函数
	WorkerManager();

	// 展示菜单的函数
	void Show_Menu();

	// 析构函数
	~WorkerManager();
};

.cpp 文件

// 展示菜单的函数
void WorkerManager::Show_Menu() // 调用类初始化菜单函数,表示该成员函数属于类 WorkerManager
{
	cout << "**********************************************" << endl;
	cout << "*********** 欢迎使用职工管理系统!************" << endl;
	cout << "*************  0. 退出管理程序  **************" << endl;
	cout << "*************  1. 增加职工信息  **************" << endl;
	cout << "*************  2. 显示职工信息  **************" << endl;
	cout << "*************  3. 删除离职职工  **************" << endl;
	cout << "*************  4. 修改职工信息  **************" << endl;
	cout << "*************  5. 查找职工信息  **************" << endl;
	cout << "*************  6. 按照编号排序  **************" << endl;
	cout << "*************  7. 清空所以文档  **************" << endl;
	cout << "**********************************************" << endl;
	cout << endl;
}

main 函数

int main()
{
	WorkerManager workerManager;
	workerManager.Show_Menu();  // 显示菜单

	system("pause"); // 按任意键继续!
	return 0;
}

2.6.5 退出功能

// 退出系统的函数
void WorkerManager::Exit_System()
{
	cout << "欢迎下次使用!" << endl;
	system("pause");
	exit(0);  // 退出程序
}

2.6.6 创建职工类

// 职工抽象类
class Worker
{
public:
	// 显示个人信息
	virtual void ShowInfo() = 0;
	// 获取岗位的名称
	virtual string GetDeptName() = 0;

	// 编号
	int m_ID;
	// 姓名
	string m_Name;
	// 部门编号
	int m_DeptID;
};

// 创建一个员工类去继承Worker职工类
class Employee :public Worker
{
public:
	// 构造函数
	Employee(int ID, string Name, int DID);
	// 重写
	// 显示个人信息
	void ShowInfo();
	// 获取岗位的名称
	string GetDeptName();
};

// 构造函数
Employee::Employee(int ID, string Name, int DID)
{
	this->m_ID = ID; // this 指向自身,表示初始化自身的ID、姓名以及编号
	this->m_Name = Name;
	this->m_DeptID = DID;
}
// 显示个人信息
void Employee::ShowInfo()
{
	cout << "职工的编号:" << this->m_ID
		<< "\t职工的姓名:" << this->m_Name
		<< "\t职工的岗位:" << this->GetDeptName()
		<< "\t岗位职责:完成经理交给的任务" << endl;
}
// 获取岗位的名称
string Employee::GetDeptName()
{
	return string("员工");
}

// 创建一个经理类去继承Worker职工类
class Manager :public Worker
{
public:
	// 构造函数
	Manager(int ID, string Name, int DID);
	// 重写
	// 显示个人信息
	void ShowInfo();
	// 获取岗位的名称
	string GetDeptName();
};
// 构造函数
Manager::Manager(int ID, string Name, int DID)
{
	this->m_ID = ID; // this 指向自身,表示初始化自身的ID、姓名以及编号
	this->m_Name = Name;
	this->m_DeptID = DID;
}
// 显示个人信息
void Manager::ShowInfo()
{
	cout << "职工的编号:" << this->m_ID
		<< "\t职工的姓名:" << this->m_Name
		<< "\t职工的岗位:" << this->GetDeptName()
		<< "\t岗位职责:完成老板交给的任务,并且下发任务给普通员工" << endl;
}
// 获取岗位的名称
string Manager::GetDeptName()
{
	return string("经理");
}

// 创建一个老板类去继承Worker职工类
class Boss :public Worker
{
public:
	// 构造函数
	Boss(int ID, string Name, int DID);
	// 重写
	// 显示个人信息
	void ShowInfo();
	// 获取岗位的名称
	string GetDeptName();
};
// 构造函数
Boss::Boss(int ID, string Name, int DID)
{
	this->m_ID = ID; // this 指向自身,表示初始化自身的ID、姓名以及编号
	this->m_Name = Name;
	this->m_DeptID = DID;
}
// 显示个人信息
void Boss::ShowInfo()
{
	cout << "职工的编号:" << this->m_ID
		<< "\t职工的姓名:" << this->m_Name
		<< "\t职工的岗位:" << this->GetDeptName()
		<< "\t岗位职责:管理公司所有的事务" << endl;
}
// 获取岗位的名称
string Boss::GetDeptName()
{
	return string("总裁");
}

2.6.7 添加职工

// 添加职工的过程
// 用户在批量创建时,可能会创建不同种类的职工
// 如果想将所有不同种类的员工都放入到一个数组中,可以将所有员工的指针维护到一个数组中
// 如果想在程序中维护这个不定长度的数组,可以将数组创建到堆区,并利用 Worker ** 的指针维护
// 添加职工
void WorkerManager::Add_Emp()
{
	cout << "请输入添加职工的数量:" << endl;

	int AddNum = 0; // 保存用户的输入数量
	cin >> AddNum;

	if (AddNum>0)
	{
		// 计算添加新数据的大小
		int NewSize = this->m_EmpNum + AddNum; // 新空间的人数 = 原来记录的人数 + 新添加的人数

		// 开辟新空间
		Worker** NewSpace = new Worker*[NewSize]; // 在上面计算出的新空间人数的基础上,在堆区开辟对应空间大小的新空间,new返回类型为指针

		// 假设原来就有人,那么需要把原来的人拷贝到新开辟的空间上
		if (this->m_EmpArray!=NULL)
		{
			for (int i = 0; i < this->m_EmpNum;i++) // 创建 for 循环,循环体大小为原来数组中已有的人数,因为要实现把已有的人转移到新数组中
			{
				NewSpace[i] = this->m_EmpArray[i]; // 拷贝到新空间
			}
		}

		// 添加新数据
		for (int i = 0; i < AddNum;i++)
		{
			int ID; // 职工编号
			string Name; // 职工姓名
			int DSelect; // 部门选择

			cout << "请输入第" << i + 1 << "个新职工编号:" << endl;
			cin >> ID;
			cout << "请输入第" << i + 1 << "个新职工姓名:" << endl;
			cin >> Name;

			cout << "请输入该职工岗位:" << endl;
			cout << "1、普通职工" << endl;
			cout << "2、经理" << endl;
			cout << "3、老板" << endl;
			cin >> DSelect;

			Worker *worker = NULL;
			switch (DSelect)
			{
			case 1:
				worker = new Employee(ID, Name, 1);
				break;
			case 2:
				worker = new Manager(ID, Name, 2);
				break;
			case 3:
				worker = new Boss(ID, Name, 3);
				break;
			default:
				break;
			}
			// 将创建的职工职责,保存到新数组中 
			NewSpace[this->m_EmpNum + i] = worker;
		}
		// 释放原有的空间
		delete [] this->m_EmpArray;

		// 更改新空间的指向
		this->m_EmpArray = NewSpace;

		// 更新新的职工人数
		this->m_EmpNum = NewSize;

		// 提示
		cout << "成功添加" << AddNum << "名新职员!" << endl;
	}
	else
	{
		cout << "输入有误!" << endl;
	}

	// 按任意键清屏,回到上级目录
	system("pause");
	system("cls");
}

2.6.8 文件交互 - 写文件

// 保存文件
void WorkerManager::Save()
{
	ofstream ofs;
	ofs.open(FILENAME,ios::out); // 用输出的方式打开文件,也就是写文件 

	// 将每个人的数据写到文件中
	for (int i = 0; i < this->m_EmpNum;i++)
	{ // m_EmpArray是一个指针数组,所以该数组内存储的都是指针,可以指向Worker
		ofs << this->m_EmpArray[i]->m_ID << " "
			<< this->m_EmpArray[i]->m_Name << " "
			<< this->m_EmpArray[i]->m_DeptID << endl;
	}
	ofs.close();
}

2.6.9 文件交互 - 读文件

#define FILENAME "EmpFile.txt"

// 构造函数空实现
WorkerManager::WorkerManager() // 通过调用类初始化构造函数
{
	// 1、文件不存在
	ifstream ifs;
	ifs.open(FILENAME,ios::in); // 读取这个文件

	if (!ifs.is_open())
	{
		cout << "文件不存在" << endl;
		// 初始化成员属性
		this->m_EmpNum = 0;
		this->m_EmpArray = NULL;
		this->m_FileIsEmpty = true;
		ifs.close();
		return;
	}

	// 2、文件存在  数据为空
	char ch;
	ifs >> ch; // 一个字符一个字符的读该文件
	if (ifs.eof())
	{
		// 文件为空
		cout << "文件为空!" << endl;
		// 初始化成员属性
		this->m_EmpNum = 0;
		this->m_EmpArray = NULL;
		this->m_FileIsEmpty = true;
		ifs.close();
		return;
	}
	
	// 3、文件存在且保存职工数据
	int Num = this->Get_EmpNum();
	cout << "职工人数为:" << Num << endl;
	this->m_EmpNum = Num; 

	this->m_EmpArray = new Worker*[this->m_EmpNum];
	// 将文件中的数据存放到数组中
	this->Init_Emp();

	for (int i = 0; i < this->m_EmpNum; i++)
	{
		cout << "职工编号:" << this->m_EmpArray[i]->m_ID
			<< "\t姓名:" << this->m_EmpArray[i]->m_Name
			<< "\t部门编号:" << this->m_EmpArray[i]->m_DeptID << endl;
	}
}

2.6.10 显示职工

// 显示职工
void WorkerManager::Show_Emp()
{
	// 判断文件是否为空
	if (this->m_FileIsEmpty)
	{
		cout << "文件不存在或记录为空!" << endl;
	}
	else
	{
		for (int i = 0; i < m_EmpNum;i++)
		{
			// 利用多态调用程序接口
			this->m_EmpArray[i]->ShowInfo();
		}
	}
	// 按任意键后清屏
	system("pause");
	system("cls");
}

2.6.11 删除职工

// 按照职工编号判断教职工是否存在,若存在返回职工在数组中的位置,不存在返回-1
int WorkerManager::IsExist(int ID)
{
	int Index = -1;

	for (int i = 0; i < this->m_EmpNum;i++)
	{
		if (this->m_EmpArray[i]->m_ID == ID) // 找到了,存在
		{ 
			Index = i;
			break;
		}
	}
	return Index;
}

// 删除职工
void WorkerManager::Del_Emp()
{
	if (this->m_FileIsEmpty)
	{
		cout << "文件不存在或记录为空!" << endl;
	}
	else
	{
		// 按照职工的编号进行删除职工
		int ID = 0;
		cout << "请输入要删除职工的编号:" << endl;
		cin >> ID;

		int Index = this->IsExist(ID);
		if (Index != -1) // 职工存在,并且这个职工在数组中的下标就是Index
		{
			for (int i = Index; i < this->m_EmpNum-1;i++) // 从当前找到的这个位置开始数据前移
			{
				this->m_EmpArray[i] = this->m_EmpArray[i + 1];
			}
			this->m_EmpNum--; // 更新数组中人员个数
			// 同步到文件中
			this->Save();

			cout << "删除成功!" << endl;
		}
		else
		{
			cout << "删除失败,未找到该职工" << endl;
		}
	}
	system("pause");
	system("cls");
}

2.6.12 修改职工

// 修改职工
void WorkerManager::Mod_Emp()
{
	if (this->m_FileIsEmpty)
	{
		cout << "文件不存在或记录为空!" << endl;
	}
	else
	{
		cout << "请输入修改职工的编号:" << endl;
		int ID;
		cin >> ID;

		int Ret = this->IsExist(ID);
		if (Ret!=-1) // 存在
		{
			// 释放原来的数据,创建新的
			delete this->m_EmpArray[Ret];

			int NewID = 0;
			string NewName = "";
			int NewDID = 0;

			cout << "查到:" << ID << "号职工,请输入新职工号" << endl;
			cin >> NewID;

			cout << "请输入新姓名" << endl;
			cin >> NewName;

			cout << "请输入新岗位" << endl;
			cout << "1、普通职工" << endl;
			cout << "2、经理" << endl;
			cout << "3、老板" << endl;
			cin >> NewDID;

			Worker* worker = NULL;
			switch (NewDID)
			{
			case 1:
				worker = new Employee(NewID, NewName, NewDID);
				break;
			case 2:
				worker = new Manager(NewID, NewName, NewDID);
				break;
			case 3:
				worker = new Boss(NewID, NewName, NewDID);
				break;
			default:
				break;
			}
		 // 更新数据到数组中
			this->m_EmpArray[Ret] = worker;

			// 修改成功
			cout << "修改成功!" << endl;

			// 保存到文件中
			this->Save();
		}
		else
		{
			cout << "修改失败,查无此人!" << endl;
		}
	}
	system("pause");
	system("cls");
}

2.6.13 查找职工

// 查找职工
void WorkerManager::Find_Emp()
{
	if (this->m_FileIsEmpty)
	{
		cout << "文件不存在或记录为空!" << endl;
	}
	else
	{
		cout << "请输入查找的方式:" << endl;
		cout << "1、按职工编号查找" << endl;
		cout << "2、按职工姓名查找" << endl;
		int Select = 0;
		cin >> Select;

		if (Select==1)
		{
			// 按照编号查
			int id;
			cout << "请输入查找职工的编号:" << endl;
			cin >> id;

			int ID = this->IsExist(id);
			if (ID!=-1) // 找到了
			{
				cout << "查找成功!该职工的信息如下:" << endl;
				this->m_EmpArray[ID]->ShowInfo();
			}
			else
			{
				cout << "查找失败!查无此人!" << endl;
			}
		}
		else if (Select == 2)
		{
			// 按照姓名查
			string Name;
			cout << "请输入查找的姓名:" << endl;
			cin >> Name;

			// 加入一个是否查到的标志 
			bool Flag = false;

			for (int i = 0; i < this->m_EmpNum; i++)
			{
				if (this->m_EmpArray[i]->m_Name==Name)
				{
					cout << "查找成功,职工编号为:" << this->m_EmpArray[i]->m_ID << "号职工信息如下:" << endl;
					this->m_EmpArray[i]->ShowInfo();
					Flag = true;
				}
			}
			if (Flag == false)
			{
				cout << "查找失败,查无此人!" << endl;
			}
		}
		else
		{
			cout << "输入选项有误!" << endl;
		}
	}
	system("pause");
	system("cls");
}

2.6.14 排序

// 排序职工
void WorkerManager::Sort_Emp()
{
	if (this->m_FileIsEmpty)
	{
		cout << "文件不存在或记录为空!" << endl;
		system("pause");
		system("cls");
	}
	else
	{ 
		cout << "请选择排序方式:" << endl;
		cout << "1、按职工号进行升序" << endl;
		cout << "2、按职工号进行降序" << endl;
		int Select = 0;
		cin >> Select;

		// 这里使用选择排序的思想进行排序:
		// 思想是:
		// 首先假设数组中第一个元素是最小值,从第二个元素开始遍历,遍历整个数组
		// 如果找到比第一个元素还要小的值,就和第一个元素进行互换;
		// 然后从第三个元素开始遍历找到比第二个元素还要小的值,和第二个元素进行互换;
		// 依次循环即可!

		for (int i = 0; i < this->m_EmpNum;i++)
		{
			int MinOrMax = i; // 定义一个最小值或者最大值的变量,用于进行升序或者降序的排列
			for (int j = i + 1; j<this->m_EmpNum;j++)
			{
				if (Select == 1) // 升序
				{
					if (this->m_EmpArray[MinOrMax]->m_ID > this->m_EmpArray[j]->m_ID)
					{
						MinOrMax = j; // 通过遍历整个数组的编号,一旦有比第一个元素小的,就将小的这个编号赋值给MinOrMax
						// 循环结束MinOrMax得到的就是最小的编号
					}
				}
				else
				{
					if (this->m_EmpArray[MinOrMax]->m_ID < this->m_EmpArray[j]->m_ID)
					{
						MinOrMax = j;
					}
				}
			}
			// 通过上述循环结束就可以得到最小或者最大的编号
			// 判断一开始认定的最小值是不是实际的最小值
			if (i != MinOrMax) // 也就是说我们一开始认定的编号最小值不是实际的编号最小值,此时把编号对应的信息进行交换
			{
				Worker* Temp = this->m_EmpArray[i];
				this->m_EmpArray[i] = this->m_EmpArray[MinOrMax];
				this->m_EmpArray[MinOrMax] = Temp;
			}
		}
		cout << "排序成功!排序以后的结果为:" << endl;
		this->Save();
		this->Show_Emp();
	}
}

2.6.15 清空文件

// 清空文件
void WorkerManager::Clean_File()
{
	cout << "确认清空?" << endl;
	cout << "1、确认" << endl;
	cout << "2、返回" << endl;

	int Select = 0;
	cin >> Select;

	if (Select==1)
	{
		// 打开模式  ios::trunc  如果存在删除文件并重新创建
		ofstream ofs(FILENAME,ios::trunc);
		ofs.close();

		if (this->m_EmpArray!=NULL) // 如果该数组不为空,则释放该数组
		{
			for (int i = 0; i < this->m_EmpNum;i++) // 通过 for 循环依次遍历整个数组
			{
				if (this->m_EmpArray != NULL)
				{
					delete this->m_EmpArray[i]; // 依次释放数组中的每一项
				}
			}
			this->m_EmpNum = 0; // 将数组中的成员数量设置为0
			delete[] this->m_EmpArray; // 在堆区将动态开辟的数组释放掉!
			this->m_EmpArray = NULL; // 将指针指向空
			this->m_FileIsEmpty = true;
		}
		cout << "清空成功!" << endl;
	}
	system("pause");
	system("cls");
}

2.6.16 完整程序代码

2.6.16.1 ManagerWorker.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <string> // 用 C++ 风格的字符串需要包含这个头文件
#include <ctime> // time 系统时间头文件包含
#include <fstream> // 文件操作必须引用该头文件
#include "HelloWorld.h"
using namespace std;

#define FILENAME "EmpFile.txt"

class Worker;

// 构造函数空实现
WorkerManager::WorkerManager() // 通过调用类初始化构造函数
{
	// 1、文件不存在
	ifstream ifs;
	ifs.open(FILENAME,ios::in); // 读取这个文件

	if (!ifs.is_open())
	{
		cout << "文件不存在" << endl;
		// 初始化成员属性
		this->m_EmpNum = 0;
		this->m_EmpArray = NULL;
		this->m_FileIsEmpty = true;
		ifs.close();
		return;
	}

	// 2、文件存在  数据为空
	char ch;
	ifs >> ch; // 一个字符一个字符的读该文件
	if (ifs.eof())
	{
		// 文件为空
		cout << "文件为空!" << endl;
		// 初始化成员属性
		this->m_EmpNum = 0;
		this->m_EmpArray = NULL;
		this->m_FileIsEmpty = true;
		ifs.close();
		return;
	}
	
	// 3、文件存在且保存职工数据
	int Num = this->Get_EmpNum();
	cout << "职工人数为:" << Num << endl;
	this->m_EmpNum = Num; 

	this->m_EmpArray = new Worker*[this->m_EmpNum];
	// 将文件中的数据存放到数组中
	this->Init_Emp();

	for (int i = 0; i < this->m_EmpNum; i++)
	{
		cout << "职工编号:" << this->m_EmpArray[i]->m_ID
			<< "\t姓名:" << this->m_EmpArray[i]->m_Name
			<< "\t部门编号:" << this->m_EmpArray[i]->m_DeptID << endl;
	}
}

// 展示菜单的函数
void WorkerManager::Show_Menu() // 调用类初始化菜单函数,表示该成员函数属于类 WorkerManager
{
	cout << "**********************************************" << endl;
	cout << "*********** 欢迎使用职工管理系统!************" << endl;
	cout << "*************  0. 退出管理程序  **************" << endl;
	cout << "*************  1. 增加职工信息  **************" << endl;
	cout << "*************  2. 显示职工信息  **************" << endl;
	cout << "*************  3. 删除离职职工  **************" << endl;
	cout << "*************  4. 修改职工信息  **************" << endl;
	cout << "*************  5. 查找职工信息  **************" << endl;
	cout << "*************  6. 按照编号排序  **************" << endl;
	cout << "*************  7. 清空所以文档  **************" << endl;
	cout << "**********************************************" << endl;
	cout << endl;
}

// 退出系统的函数
void WorkerManager::Exit_System()
{
	cout << "欢迎下次使用!" << endl;
	system("pause");
	exit(0);  // 退出程序
}

// 创建一个员工类去继承Worker职工类
class Employee :public Worker
{
public:
	// 构造函数
	Employee(int ID, string Name, int DID);
	// 重写
	// 显示个人信息
	void ShowInfo();
	// 获取岗位的名称
	string GetDeptName();
};

// 构造函数
Employee::Employee(int ID, string Name, int DID)
{
	this->m_ID = ID; // this 指向自身,表示初始化自身的ID、姓名以及编号
	this->m_Name = Name;
	this->m_DeptID = DID;
}
// 显示个人信息
void Employee::ShowInfo()
{
	cout << "职工编号:" << this->m_ID
		<< "\t职工姓名:" << this->m_Name
		<< "\t职工岗位:" << this->GetDeptName()
		<< "\t岗位职责:完成经理交给的任务" << endl;
}
// 获取岗位的名称
string Employee::GetDeptName()
{
	return string("员工");
}

// 创建一个经理类去继承Worker职工类
class Manager :public Worker
{
public:
	// 构造函数
	Manager(int ID, string Name, int DID);
	// 重写
	// 显示个人信息
	void ShowInfo();
	// 获取岗位的名称
	string GetDeptName();
};
// 构造函数
Manager::Manager(int ID, string Name, int DID)
{
	this->m_ID = ID; // this 指向自身,表示初始化自身的ID、姓名以及编号
	this->m_Name = Name;
	this->m_DeptID = DID;
}
// 显示个人信息
void Manager::ShowInfo()
{
	cout << "职工编号:" << this->m_ID
		<< "\t职工姓名:" << this->m_Name
		<< "\t职工岗位:" << this->GetDeptName()
		<< "\t岗位职责:完成老板交给的任务,并且下发任务给普通员工" << endl;
}
// 获取岗位的名称
string Manager::GetDeptName()
{
	return string("经理");
}

// 创建一个老板类去继承Worker职工类
class Boss :public Worker
{
public:
	// 构造函数
	Boss(int ID, string Name, int DID);
	// 重写
	// 显示个人信息
	void ShowInfo();
	// 获取岗位的名称
	string GetDeptName();
};
// 构造函数
Boss::Boss(int ID, string Name, int DID)
{
	this->m_ID = ID; // this 指向自身,表示初始化自身的ID、姓名以及编号
	this->m_Name = Name;
	this->m_DeptID = DID;
}
// 显示个人信息
void Boss::ShowInfo()
{
	cout << "职工编号:" << this->m_ID
		<< "\t职工姓名:" << this->m_Name
		<< "\t职工岗位:" << this->GetDeptName()
		<< "\t岗位职责:管理公司所有的事务" << endl;
}
// 获取岗位的名称
string Boss::GetDeptName()
{
	return string("总裁");
}

// 添加职工的过程
// 用户在批量创建时,可能会创建不同种类的职工
// 如果想将所有不同种类的员工都放入到一个数组中,可以将所有员工的指针维护到一个数组中
// 如果想在程序中维护这个不定长度的数组,可以将数组创建到堆区,并利用 Worker ** 的指针维护
// 添加职工
void WorkerManager::Add_Emp()
{
	cout << "请输入添加职工的数量:" << endl;

	int AddNum = 0; // 保存用户的输入数量
	cin >> AddNum;

	if (AddNum>0)
	{
		// 计算添加新数据的大小
		int NewSize = this->m_EmpNum + AddNum; // 新空间的人数 = 原来记录的人数 + 新添加的人数

		// 开辟新空间
		Worker** NewSpace = new Worker*[NewSize]; // 在上面计算出的新空间人数的基础上,在堆区开辟对应空间大小的新空间,new返回类型为指针

		// 假设原来就有人,那么需要把原来的人拷贝到新开辟的空间上
		if (this->m_EmpArray!=NULL)
		{
			for (int i = 0; i < this->m_EmpNum;i++) // 创建 for 循环,循环体大小为原来数组中已有的人数,因为要实现把已有的人转移到新数组中
			{
				NewSpace[i] = this->m_EmpArray[i]; // 拷贝到新空间
			}
		}

		// 添加新数据
		for (int i = 0; i < AddNum;i++)
		{
			int ID; // 职工编号
			string Name; // 职工姓名
			int DSelect; // 部门选择

			cout << "请输入第" << i + 1 << "个新职工编号:" << endl;
			cin >> ID;
			cout << "请输入第" << i + 1 << "个新职工姓名:" << endl;
			cin >> Name;

			cout << "请输入该职工岗位:" << endl;
			cout << "1、普通职工" << endl;
			cout << "2、经理" << endl;
			cout << "3、老板" << endl;
			cin >> DSelect;

			Worker *worker = NULL;
			switch (DSelect)
			{
			case 1:
				worker = new Employee(ID, Name, 1);
				break;
			case 2:
				worker = new Manager(ID, Name, 2);
				break;
			case 3:
				worker = new Boss(ID, Name, 3);
				break;
			default:
				break;
			}
			// 将创建的职工职责,保存到新数组中 
			NewSpace[this->m_EmpNum + i] = worker;
		}
		// 释放原有的空间
		delete [] this->m_EmpArray;

		// 更改新空间的指向
		this->m_EmpArray = NewSpace;

		// 更新新的职工人数
		this->m_EmpNum = NewSize;

		// 更新职工不为空的标志
		this->m_FileIsEmpty = false;

		// 提示
		cout << "成功添加" << AddNum << "名新职员!" << endl;

		this->Save(); // 添加完以后,将这个人保存到文件中
	}
	else
	{
		cout << "输入有误!" << endl;
	}

	// 按任意键清屏,回到上级目录
	system("pause");
	system("cls");
}

// 保存文件
void WorkerManager::Save()
{
	ofstream ofs;
	ofs.open(FILENAME,ios::out); // 用输出的方式打开文件,也就是写文件 

	// 将每个人的数据写到文件中
	for (int i = 0; i < this->m_EmpNum;i++)
	{ // m_EmpArray是一个指针数组,所以该数组内存储的都是指针,可以指向Worker
		ofs << this->m_EmpArray[i]->m_ID << " "
			<< this->m_EmpArray[i]->m_Name << " "
			<< this->m_EmpArray[i]->m_DeptID << endl;
	}
	ofs.close();
}

// 统计文件中的人数
int WorkerManager::Get_EmpNum()
{
	ifstream ifs;
	ifs.open(FILENAME, ios::in); // 打开文件 读

	int ID;
	string Name;
	int DID;

	int Num = 0;

	while (ifs>>ID && ifs>>Name && ifs>>DID)
	{
		// 统计人数
		Num++;
	}
	return Num;
}

// 初始化员工
void WorkerManager::Init_Emp()
{
	ifstream ifs;
	ifs.open(FILENAME,ios::in);

	int ID;
	string Name;
	int DID;

	int Index = 0;
	while (ifs>>ID && ifs>>Name && ifs>>DID)
	{
		Worker* worker = NULL;

		if (DID==1) // 普通员工
		{
			worker = new Employee(ID,Name,DID);
		}
		else if (DID == 2) // 经理员工
		{
			worker = new Manager(ID, Name, DID);
		}
		else if (DID == 3) // 老板员工
		{
			worker = new Boss(ID, Name, DID);
		}
		this->m_EmpArray[Index] = worker;
		Index++;
	}
	ifs.close();
}

// 显示职工
void WorkerManager::Show_Emp()
{
	// 判断文件是否为空
	if (this->m_FileIsEmpty)
	{
		cout << "文件不存在或记录为空!" << endl;
	}
	else
	{
		for (int i = 0; i < m_EmpNum;i++)
		{
			// 利用多态调用程序接口
			this->m_EmpArray[i]->ShowInfo();
		}
	}
	// 按任意键后清屏
	system("pause");
	system("cls");
}

// 按照职工编号判断教职工是否存在,若存在返回职工在数组中的位置,不存在返回-1
int WorkerManager::IsExist(int ID)
{
	int Index = -1;

	for (int i = 0; i < this->m_EmpNum;i++)
	{
		if (this->m_EmpArray[i]->m_ID == ID) // 找到了,存在
		{ 
			Index = i;
			break;
		}
	}
	return Index;
}

// 删除职工
void WorkerManager::Del_Emp()
{
	if (this->m_FileIsEmpty)
	{
		cout << "文件不存在或记录为空!" << endl;
	}
	else
	{
		// 按照职工的编号进行删除职工
		int ID = 0;
		cout << "请输入要删除职工的编号:" << endl;
		cin >> ID;

		int Index = this->IsExist(ID);
		if (Index != -1) // 职工存在,并且这个职工在数组中的下标就是Index
		{
			for (int i = Index; i < this->m_EmpNum-1;i++) // 从当前找到的这个位置开始数据前移
			{
				this->m_EmpArray[i] = this->m_EmpArray[i + 1];
			}
			this->m_EmpNum--; // 更新数组中人员个数
			// 同步到文件中
			this->Save();

			cout << "删除成功!" << endl;
		}
		else
		{
			cout << "删除失败,未找到该职工" << endl;
		}
	}
	system("pause");
	system("cls");
}

// 修改职工
void WorkerManager::Mod_Emp()
{
	if (this->m_FileIsEmpty)
	{
		cout << "文件不存在或记录为空!" << endl;
	}
	else
	{
		cout << "请输入修改职工的编号:" << endl;
		int ID;
		cin >> ID;

		int Ret = this->IsExist(ID);
		if (Ret!=-1) // 存在
		{
			// 释放原来的数据,创建新的
			delete this->m_EmpArray[Ret];

			int NewID = 0;
			string NewName = "";
			int NewDID = 0;

			cout << "查到:" << ID << "号职工,请输入新职工号" << endl;
			cin >> NewID;

			cout << "请输入新姓名" << endl;
			cin >> NewName;

			cout << "请输入新岗位" << endl;
			cout << "1、普通职工" << endl;
			cout << "2、经理" << endl;
			cout << "3、老板" << endl;
			cin >> NewDID;

			Worker* worker = NULL;
			switch (NewDID)
			{
			case 1:
				worker = new Employee(NewID, NewName, NewDID);
				break;
			case 2:
				worker = new Manager(NewID, NewName, NewDID);
				break;
			case 3:
				worker = new Boss(NewID, NewName, NewDID);
				break;
			default:
				break;
			}
		 // 更新数据到数组中
			this->m_EmpArray[Ret] = worker;

			// 修改成功
			cout << "修改成功!" << endl;

			// 保存到文件中
			this->Save();
		}
		else
		{
			cout << "修改失败,查无此人!" << endl;
		}
	}
	system("pause");
	system("cls");
}

// 查找职工
void WorkerManager::Find_Emp()
{
	if (this->m_FileIsEmpty)
	{
		cout << "文件不存在或记录为空!" << endl;
	}
	else
	{
		cout << "请输入查找的方式:" << endl;
		cout << "1、按职工编号查找" << endl;
		cout << "2、按职工姓名查找" << endl;
		int Select = 0;
		cin >> Select;

		if (Select==1)
		{
			// 按照编号查
			int id;
			cout << "请输入查找职工的编号:" << endl;
			cin >> id;

			int ID = this->IsExist(id);
			if (ID!=-1) // 找到了
			{
				cout << "查找成功!该职工的信息如下:" << endl;
				this->m_EmpArray[ID]->ShowInfo();
			}
			else
			{
				cout << "查找失败!查无此人!" << endl;
			}
		}
		else if (Select == 2)
		{
			// 按照姓名查
			string Name;
			cout << "请输入查找的姓名:" << endl;
			cin >> Name;

			// 加入一个是否查到的标志 
			bool Flag = false;

			for (int i = 0; i < this->m_EmpNum; i++)
			{
				if (this->m_EmpArray[i]->m_Name==Name)
				{
					cout << "查找成功,职工编号为:" << this->m_EmpArray[i]->m_ID << "号职工信息如下:" << endl;
					this->m_EmpArray[i]->ShowInfo();
					Flag = true;
				}
			}
			if (Flag == false)
			{
				cout << "查找失败,查无此人!" << endl;
			}
		}
		else
		{
			cout << "输入选项有误!" << endl;
		}
	}
	system("pause");
	system("cls");
}

// 排序职工
void WorkerManager::Sort_Emp()
{
	if (this->m_FileIsEmpty)
	{
		cout << "文件不存在或记录为空!" << endl;
		system("pause");
		system("cls");
	}
	else
	{ 
		cout << "请选择排序方式:" << endl;
		cout << "1、按职工号进行升序" << endl;
		cout << "2、按职工号进行降序" << endl;
		int Select = 0;
		cin >> Select;

		// 这里使用选择排序的思想进行排序:
		// 思想是:
		// 首先假设数组中第一个元素是最小值,从第二个元素开始遍历,遍历整个数组
		// 如果找到比第一个元素还要小的值,就和第一个元素进行互换;
		// 然后从第三个元素开始遍历找到比第二个元素还要小的值,和第二个元素进行互换;
		// 依次循环即可!

		for (int i = 0; i < this->m_EmpNum;i++)
		{
			int MinOrMax = i; // 定义一个最小值或者最大值的变量,用于进行升序或者降序的排列
			for (int j = i + 1; j<this->m_EmpNum;j++)
			{
				if (Select == 1) // 升序
				{
					if (this->m_EmpArray[MinOrMax]->m_ID > this->m_EmpArray[j]->m_ID)
					{
						MinOrMax = j; // 通过遍历整个数组的编号,一旦有比第一个元素小的,就将小的这个编号赋值给MinOrMax
						// 循环结束MinOrMax得到的就是最小的编号
					}
				}
				else
				{
					if (this->m_EmpArray[MinOrMax]->m_ID < this->m_EmpArray[j]->m_ID)
					{
						MinOrMax = j;
					}
				}
			}
			// 通过上述循环结束就可以得到最小或者最大的编号
			// 判断一开始认定的最小值是不是实际的最小值
			if (i != MinOrMax) // 也就是说我们一开始认定的编号最小值不是实际的编号最小值,此时把编号对应的信息进行交换
			{
				Worker* Temp = this->m_EmpArray[i];
				this->m_EmpArray[i] = this->m_EmpArray[MinOrMax];
				this->m_EmpArray[MinOrMax] = Temp;
			}
		}
		cout << "排序成功!排序以后的结果为:" << endl;
		this->Save();
		this->Show_Emp();
	}
}

// 清空文件
void WorkerManager::Clean_File()
{
	cout << "确认清空?" << endl;
	cout << "1、确认" << endl;
	cout << "2、返回" << endl;

	int Select = 0;
	cin >> Select;

	if (Select==1)
	{
		// 打开模式  ios::trunc  如果存在删除文件并重新创建
		ofstream ofs(FILENAME,ios::trunc);
		ofs.close();

		if (this->m_EmpArray!=NULL) // 如果该数组不为空,则释放该数组
		{
			for (int i = 0; i < this->m_EmpNum;i++) // 通过 for 循环依次遍历整个数组
			{
				if (this->m_EmpArray != NULL)
				{
					delete this->m_EmpArray[i]; // 依次释放数组中的每一项
				}
			}
			this->m_EmpNum = 0; // 将数组中的成员数量设置为0
			delete[] this->m_EmpArray; // 在堆区将动态开辟的数组释放掉!
			this->m_EmpArray = NULL; // 将指针指向空
			this->m_FileIsEmpty = true;
		}
		cout << "清空成功!" << endl;
	}
	system("pause");
	system("cls");
}

// 析构函数空实现
WorkerManager::~WorkerManager() // 通过调用类初始化析构函数
{
	if (this->m_EmpArray!=NULL)
	{
		for (int i = 0; i < this->m_EmpNum;i++)
		{
			if (this->m_EmpArray[i]!=NULL)
			{
				delete this->m_EmpArray[i];
			}
		}
		delete[] this->m_EmpArray;
		this->m_EmpArray = NULL;
	}
}

// 测试函数
void Test()
{
	Worker *worker; // 父类指针或者引用指向子类对象,引起多态
	worker = new Employee(1, "张三", 1);
	worker->ShowInfo();
	delete worker;

	worker = new Manager(2, "李四", 2);
	worker->ShowInfo();
	delete worker;

	worker = new Boss(3, "王五", 3);
	worker->ShowInfo();
	delete worker;
}

int main()
{
	// Test();
	WorkerManager workerManager;
	int Choice = 0; // 定义变量记录用户的选择

	while (true)
	{
		workerManager.Show_Menu();  // 显示菜单
		cout << "请输入您的选择:" << endl;
		cin >> Choice;

		switch (Choice)
		{
		case 0: // 退出系统
			workerManager.Exit_System();
			break;
		case 1: // 增加职工
			workerManager.Add_Emp();
			break;
		case 2: // 显示职工
			workerManager.Show_Emp();
			break;
		case 3: // 删除职工
			workerManager.Del_Emp();
			break;
		case 4: // 修改职工
			workerManager.Mod_Emp();
			break;
		case 5: // 查找职工
			workerManager.Find_Emp();
			break;
		case 6: // 排序职工
			workerManager.Sort_Emp();
			break;
		case 7: // 清空文档
			workerManager.Clean_File();
			break;
		default:
			system("cls"); // 清屏
			break;
		}
	}
	 
	system("pause"); // 按任意键继续!
	return 0;
}

2.6.16.2 ManagerWorker.h
#pragma once   // 防止头文件重叠
#include <iostream>  // 包含输入输出流文件
#include <fstream>
using namespace std;  // 使用标准命名空间


class Worker;

class WorkerManager
{
public:
	// 构造函数
	WorkerManager();

	// 展示菜单的函数
	void Show_Menu();

	// 退出系统
	void Exit_System();

	// 添加职工
	void Add_Emp();

	// 保存文件
	void Save();

	// 判断文件是否为空的标志
	bool m_FileIsEmpty;

	// 统计文件中的人数
	int Get_EmpNum();

	// 初始化员工
	void Init_Emp();

	// 记录职工人数
	int m_EmpNum;

	// 显示职工
	void Show_Emp();

	// 按照职工编号判断教职工是否存在,若存在返回职工在数组中的位置,不存在返回-1
	int IsExist(int ID);

	// 删除职工
	void Del_Emp();

	// 修改职工
	void Mod_Emp();

	// 查找职工
	void Find_Emp();

	// 职工数组指针
	Worker** m_EmpArray; // 用二维指针来维护,因为添加职工可能会一起添加很多人,每个人对应的职位也不同
										  // 所以借助数组来维护!

	// 排序职工
	void Sort_Emp();

	// 清空文件
	void Clean_File();

	// 析构函数
	~WorkerManager();
};

// 职工抽象类
// 职工包括员工、经理和老板,
// 这三类人统一继承职工
class Worker
{
public:
	// 显示个人信息
	virtual void ShowInfo() = 0;
	// 获取岗位的名称
	virtual string GetDeptName() = 0;

	// 编号
	int m_ID;
	// 姓名
	string m_Name;
	// 部门编号
	int m_DeptID;
};

  • 20
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值