[学习笔记] 2. C++ / CPP核心编程

文章深入探讨了C++面向对象编程的核心概念,包括内存分区模型、类与对象的封装、继承与多态实现,以及运算符重载等高级特性。通过实例分析了内存管理、引用、构造函数与析构函数的工作原理,同时介绍了如何设计并实现一个完整的职工管理系统,涵盖文件读写操作与系统功能模块。
摘要由CSDN通过智能技术生成

本阶段主要针对C++面向对象编程技术做详细讲解,探讨C++中的核心和精髓。

面向对象是一种编程思想。

目录

1. 内存分区模型

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

  1. 代码区:存放函数体的二进制代码(所有写的代码),由操作系统进行管理的
  2. 全局区:存放全局变量和静态变量以及常量
  3. 栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
  4. 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

代码区和全局区都是程序在运行前就划分好的区域,而栈区和堆区是程序运行时生成的区域。

内存四区意义:

  • 不同区域存放的数据,赋予不同的生命周期,给我们更大的灵活编程。

1.1 程序运行前

在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域:

  1. 代码区:
    • 存放CPU执行的机器指令
    • 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
    • 代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令
  2. 全局区:
    • 全局变量和静态变量存放在此
    • 全局区还包含了常量区,字符串常量和其他常量(const修饰的变量)也存放在此
    • 该区域的数据在程序结束后由操作系统释放(这些变量的生命周期由操作系统决定)
#include <iostream>
using namespace std;


// 1. 全局变量
int g_a = 10;  // 不在函数中的变量即为全局变量
int g_b = 10;


// 3.2.1 const修饰的全局变量(称为全局常量)
const int c_g_a = 10;
const int c_g_b = 10;


// 全局区: 全局变量和静态变量存放在此 -> 全局变量、静态变量、常量
int main() {

	// 创建普通的局部变量
	int a = 10;  // a是main函数的局部变量
	int b = 10;

	cout << "局部变量a的地址为:\t" << (int) & a << endl;
	cout << "局部变量b的地址为:\t" << (int) &b << endl;


	cout << "全局变量g_a的地址为:\t" << (int) &g_a << endl;
	cout << "全局变量g_b的地址为:\t" << (int) &g_b << endl;


	// 2. 静态变量
	static int s_a = 10;
	static int s_b = 10;
	cout << "静态变量s_a的地址为:\t" << (int) &s_a << endl;
	cout << "静态变量s_b的地址为:\t" << (int) &s_b << endl;


	// 3. 常量: ①字符串常量;②const修饰的变量
	// 3.1 字符串常量
	cout << "字符串常量的地址为:\t" << (int) &"Hello World" << endl;


	/*
		3.2 const修饰的变量: 
			①const修饰的全局变量(称为全局常量);
			②const修饰的局部变量(称为局部常量)。
	*/
	cout << "全局常量c_g_a的地址为:\t" << (int) &c_g_a << endl;
	cout << "全局常量c_g_b的地址为:\t" << (int) &c_g_b << endl;

	// 3.2.2 const修饰的局部变量
	int c_l_a = 10;  // l = local
	int c_l_b = 10;
	cout << "局部常量c_l_a的地址为:\t" << (int) &c_l_a << endl;
	cout << "局部常量c_l_b的地址为:\t" << (int) &c_l_b << endl;
	
	/*
		局部变量a的地址为:      8388048
		局部变量b的地址为:      8388036
		全局变量g_a的地址为:    3850240
		全局变量g_b的地址为:    3850244
		静态变量s_a的地址为:    3850248
		静态变量s_b的地址为:    3850252
		字符串常量的地址为:     3841016
		全局常量c_g_a的地址为:  3840816
		全局常量c_g_b的地址为:  3840820
		局部常量c_l_a的地址为:  8388024
		局部常量c_l_b的地址为:  8388012
	*/

	system("pause");
	return 0;
}

在这里插入图片描述

在这里插入图片描述

总结

  • C++中在程序运行前分为全局区和代码区
  • 代码区特点是共享和只读
  • 全局区中存放全局变量、静态变量、常量
  • 常量区中存放const修饰的全局常量和字符串常量

1.2 程序运行后

1.2.1 栈区

  • 由编译器自动分配释放,存放函数的参数值,局部变量等
  • 注意事项:不要返回局部变量的地址,因为栈区开辟的数据由编译器自动释放

示例:

#include <iostream>
using namespace std;


/*
* 栈区的数据由编译器管理开辟和释放
* 注意:不要返回局部变量的地址
*/


int* func1() {
	int a = 10;  // 局部变量,存放在栈区。栈区的数据在函数执行完之后自己释放
	return &a;  // 返回局部变量的地址
}


int* func2(int b) {  // 形参数据也会放在栈区
	return &b;
}


int main() {

	int* p1 = func1();  // 接收func函数返回的地址
	std::cout << *p1 << std::endl;  // 10
	std::cout << *p1 << std::endl;  // 2059380984 -> 乱码

	/*
	* 第一次可以打印正确的数据,是因为编译器做了保留
	* 第二次这个数据就不再保留了(原本的内存已经被释放覆盖了,所以我们得到了乱码)
	*/

	int* p2 = func2(20);
	std::cout << *p2 << std::endl;  // 8982588 -> 乱码
	std::cout << *p2 << std::endl;  // 8982588 -> 乱码
	/*
	* 形参也是同理
	*/

	std::system("pause");
	return 0;
}

总结:不要返回局部变量的地址!

1.2.2 堆区

  • 由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
  • 在C++中主要利用new在堆区开辟内存

假如数据放在堆区,不释放可行吗? -> 不可以!因为这些数据一直不释放,那么当整个程序结束之后,系统也会帮我们回收这些数据!只是在程序运行区间由我们控制。

示例:

#include <iostream>


// 在堆区开辟数据


int* func() {
	/*
	* 利用new关键字,可以将数据开辟到堆区
	(因为我们直接创建数据的话,创建的是局部变量,是在栈区,由编译器释放)
	*/
	// int a = 10;  // 这个是局部变量,在栈区
	

	// 指针本质上也是局部变量,放在栈区,指针保存的数据是放在堆区的
	int* p = new int(10);  // new返回的是地址编号,所以要用指针来接收
	return p;  // 将地址进行返回(而非返回数据)

}


int main() {

	int* p = func();  // 用指针接收地址

	std::cout << *p << std::endl;  // 10
	std::cout << *p << std::endl;  // 10
	std::cout << *p << std::endl;  // 10
	std::cout << *p << std::endl;  // 10

	std::system("pause");
	return 0;
}

总结

  1. 堆区教据由程序员管理开辟和释放
  2. 堆区数据利用new关键字进行开辟内存

在这里插入图片描述

1.3 new操作符

  • C++中利用new操作符在堆区开辟数据
  • 堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符delete
  • 语法: new 数据类型
  • 利用new创建的教据,会返回该数据对应的类型的指针

释放单个数据: delete 地址
释放数组: delete[] 数组首地址

示例1:基本语法

#include <iostream>


// 1. new的基本语法
int* func_new() {
	// 在堆区创建一个整型的数据
	// new返回的是:该数据类型的指针
	int* p = new int(10);
	return p;  // 返回指针
}


void test01() {
	int* p = func_new();
	std::cout << *p << std::endl;  // 10
	std::cout << *p << std::endl;  // 10
	std::cout << *p << std::endl;  // 10
	std::cout << *p << std::endl;  // 10
	/* 
	* 堆区的数据由程序员管理开辟和释放
	* 如果想要释放堆区的数,利用关键字delete
	*/
	delete p;
	// std::cout << *p << std::endl;  // 引发了异常: 读取访问权限冲突。 -> 内存已经被释放,再次访问就是非法操作,会报错。
}


// 2. 在堆区利用new开辟数组
void test02() {
	// 在堆区创建10个int的数组
	int* arr = new int[10];  // 10代表数组有10个元素,返回的是数组的首地址

	// 操纵数组
	for (int i = 0; i < 10; i++)
	{
		arr[i] = i + 100;  // 给10个元素赋值
	}

	for (int i = 0; i < 10; i++)
	{
		std::cout << arr[i] << " ";
		// 100 101 102 103 104 105 106 107 108 109
	}
	std::cout << "\r\n";

	// 释放堆区的数组
	delete[] arr;  // 释放数组的时候需要加[]
}


int main() {

	test01();
	test02();

	std::system("pause");
	return 0;
}

2. 引用

2.1引用的基本使用

作用:给变量起别名

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

示例:

#include <iostream>
using namespace std;


int main() {

	// 引用基本语法: 数据类型& 别名 = 原名
	int a = 10;
	int& b = a;

	cout << "a: " << a << endl;  // 10
	cout << "b: " << b << endl;  // 10

	b = 100;
	cout << "a: " << a << endl;  // 100
	cout << "b: " << b << endl;  // 100

	system("pause");
	return 0;
}

2.2 引用注意事项

  • 引用必须初始化
  • 引用在初始化后,不可以改变(给一个变量取别名后,这个别名不能改为其他变量的别名了)

示例:

#include <iostream>
using namespace std;


int main() {

	int a = 10;

	// 1. 引用必须初始化
	// int& b;  // IDE: 引用变量"b"需要初始值设定项 未初始化本地变量
	int& b = a;
	cout << "b:\t" << b << endl;

	// 2. 引用在初始化后不可以改变
	int c = 20;
	// int& b = c;  // error C2374: “b”: 重定义;多次初始化

	system("pause");
	return 0;
}

2.3 引用做函数参数

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

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

示例:

#include <iostream>
using namespace std;


// 1. 值传递:形参不会改变实参
void swap_fn_01(int a, int b) {
	int tmp = a;
	a = b;
	b = tmp;
}


// 2. 地址传递:形参会改变实参
void swap_fn_02(int* a, int* b) {
	int tmp = *a;
	*a = *b;
	*b = tmp;
}


// 3. 引用传递
void swap_fn_03(int& a, int& b) {
	int tmp = a;
	a = b;
	b = tmp;
}


int main() {

	// 1. 值传递:形参不会改变实参
	int a1 = 10;
	int b1 = 20;
	swap_fn_01(a1, b1);
	cout << "[swap_fn_01(值传递)]a1:\t" << a1 << endl;  // 10
	cout << "[swap_fn_01(值传递)]b1:\t" << b1 << endl;  // 20


	// 2. 地址传递:形参会改变实参
	int a2 = 10;
	int b2 = 20;
	swap_fn_02(&a2, &b2);
	cout << "[swap_fn_02(地址传递)]a2:\t" << a2 << endl;  // 20
	cout << "[swap_fn_02(地址传递)]b2:\t" << b2 << endl;  // 10


	// 3. 引用传递:形参会改变实参
	int a3 = 10;
	int b3 = 20;
	swap_fn_02(&a3, &b3);
	cout << "[swap_fn_03(地址传递)]a3:\t" << a3 << endl;  // 20
	cout << "[swap_fn_03(地址传递)]b3:\t" << b3 << endl;  // 10


	return 0;
}

总结:通过引用参数产生的效果同按地址传递是一样的。引用的语法更清楚简单。

2.4 引用做函数返回值

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

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

用法:函数调用作为左值。

示例:

#include <iostream>
using namespace std;


// 1. 不要返回局部变量的引用
int& test01() {
	int a = 10;  // 局部变量(栈区)
	return a;
}


// 2. 函数的调用可以作为左值
int& test02() {
	static int a = 10;  // 静态变量(全局区)
	return a;
}


int main() {

	/*
	* 第一次结果正确,是因为编译器做了保留
	* 第二次结果错误,因为test01函数的局部变量a的内存已经释放
	*/
	int& ref1 = test01();
	cout << "ref1: " << ref1 << endl;  // 10
	cout << "ref1: " << ref1 << endl;  // 2041751800


	/*
	* 因为test02中的a是全局变量,程序结束后才会被释放
	*/
	int& ref2 = test02();
	cout << "ref2: " << ref2 << endl;  // 10
	cout << "ref2: " << ref2 << endl;  // 10


	// 2. 函数的调用可以作为左值(如果函数的返回值是一个引用,那么这个函数的调用可以作为左值)
	test02() = 1000;  // 就是一个简单的赋值操作 <=> ref2 = 1000;
	cout << "ref2: " << ref2 << endl;  // 1000
	cout << "ref2: " << ref2 << endl;  // 1000

	return 0;
}

2.5 引用的本质

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

讲解示例:

#include <iostream>
using namespace std;


// 发现是引用,转换为 int* const ref = &a;
void func(int& ref) {
	ref = 100;  // ref是引用,转换为*ref = 100;
}


int main() {
	int a = 10;

	/*
	* 自动转换为 int* const ref = &a; 
	* 指针常量是指针指向不可改变(指向地址的数据可以修改),
	* 这也说明了为什么引用不可以更改
	*/
	int& ref = a;
	ref = 20;  // 内部发现ref是引用,自动帮我们转换为: *ref = 20;

	cout << "a: " << a << endl;  // 20
	cout << "ref: " << ref << endl;  // 20
	/*
	* 在我们使用ref时,编译器发现ref是引用,会自动帮我们解引用
	*/

	func(a);
	cout << "ref: " << ref << endl;  // 100

	return 0;
}

结论:C++推荐用引用技术,因为语法方便,引用本质是指针常量,但是所有的指针操作编译器都帮我们做了。

2.6 常量引用

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

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

示例:

#include <iostream>
using namespace std;

/*
* 常量引用
*	使用场景:用来修饰形参,防止误操作
*/


// 打印函数(不用const修饰,会改变实参)
void show_value_01(int& val) {
	val *= 10;  // 因为传入的是引用,所以形参可以改变实参
	cout << "val: " << val << endl;
}


// 打印函数(用const修饰,修改实参会报错!)
void show_value_02(const int& val) {
	// val *= 10;  // E0137	表达式必须是可修改的左值

	cout << "val: " << val << endl;
}


int main() {

	int a = 10;
	// int& ref = 10;  // 引用本身需要一个合法的内存空间,因此这行错误

	/*
		加上const之后,编译器将代码修改为:
			int tmp = 10; 
			const int& ref = tmp;
	*/
	const int& ref = 10;
	// ref = 20;  // 加上const之后变为常量,不可以修改


	// 函数中可以利用常量防止误操作修改实参
	int b = 100;
	show_value_01(b);  // val: 1000
	cout << "b: " << b << endl;  // 1000

	int c = 100;
	show_value_02(c);  // val: 100
	cout << "c: " << c << endl;  // 100

	return 0;
}

3. 函数提高

3.1 函数默认参数

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

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

注意事项

  1. 如果某个位置已经有了默认参数,那么从这个位置往后,从左到右都必须有默认值(和Python是一样的)
  2. 如果函数的声明有了默认参数,那么函数的实现就不能有默认参数了 -> 函数的声明和实现只能有一个有默认参数
    1. 声明有默认值,实现没有默认值 -> √
    2. 声明没有默认值,实现有默认值 -> √
    3. 声明有默认值,实现也有默认值 -> ×
函数声明默认值函数实现默认值结论
没有可以
没有可以
不可以

示例:

#include <iostream>
using namespace std;


// 形参没有默认值
int func_without_default_value(int a, int b, int c) {
	return a + b + c;
}


// 形参有默认值
int func_with_default_value(int a, int b = 20, int c = 30) {
	return a + b + c;
}


/*
注意事项:
	1. 如果某个位置已经有了默认参数,那么从这个位置往后,从左到右都必须有默认值(和Python是一样的)
	2. 如果函数的声明有了默认参数,那么函数的实现就不能有默认参数了 -> 函数的声明和实现只能有一个有默认参数
		声明有默认值,实现没有默认值 -> √
		声明没有默认值,实现有默认值 -> √
		声明有默认值,实现也有默认值 -> ×
*/


// 注意事项2
int func(int a = 10, int b = 20);  // 函数声明


int func(int a = 12, int b = 30) {  // 函数实现
	return a + b;
}


int main() {

	int res_1 = func_without_default_value(10, 20, 30);
	cout << "res_1: " << res_1 << endl;

	int res_2 = func_with_default_value(14, 22);
	cout << "res_2: " << res_2 << endl;  // 66

	// 注意事项2
	int res_3 = func(1, 2);
	cout << "res_3: " << res_3 << endl;  // 错误	C2572	“func” : 重定义默认参数: 参数 1

	

	system("pause");
	return 0;
}

3.2 函数占位参数

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

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

注意:占位参数也可以有默认值。

void func(int a, int, double=3.14) {
	...
}

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

示例:

#include <iostream>
using namespace std;


// 函数的占位参数
void func001(int a) {
	cout << "This is a func001" << endl;
}


/*
	有一个问题:形参a我们是可以用的,但是第二个参数是一个占位符,它没有接收的变量,我们目前不知道怎么用
	在后面的课程中,占位参数会有用
*/
void func002(int a, int) {  // 第二个int起到占位的作用
	cout << "This is a func002" << endl;
}


// 占位参数也可以有默认值
void func003(int=10, double=3.14) {  // 第二个int起到占位的作用
	cout << "This is a func003" << endl;
}


int main() {

	func001(10);  // This is a func001
	func002(10, 20);  // This is a func002
	func003();  // This is a func003

	system("pause");
	return 0;
}

3.3 函数重载

3.3.1 函数重载概述

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

函数重载满足条件:

  1. 同一个作用域下
  2. 函数名称相同
  3. 以下三个至少满足一个:
    1. 参数类型不同
    2. 参数个数不同
    3. 参数顺序不同

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

示例:

#include <iostream>
using namespace std;


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

	函数重载的条件:
		1. 同一个作用域(全局作用域)
		2. 函数名称相同
		3. 函数的参数[类型不同]或[个数不同]或[顺序不同]
*/


void func_same() {
	cout << "func_same的调用" << endl;
}


//void func_same() {
//	cout << "------func_same的调用-------" << endl;
//}


// 1. 参数类型不同
void func0001() {
	cout << "函数func0001的调用" << endl;
}

void func0001(int a) {
	cout << "函数func0001(int a)的调用" << endl;
}

void func0001(double a) {
	cout << "函数func0001(double a)的调用" << endl;
}


// 2. 参数个数不同
void func0002(int a) {
	cout << "函数func0002(int a)的调用" << endl;
}

void func0002(int a, double b) {
	cout << "函数func0002(int a, double b)的调用" << endl;
}


// 3. 参数顺序不同
void func0003(int a, double b) {
	cout << "函数func0003(int a, double b)的调用" << endl;
}

void func0003(double a, int b) {
	cout << "函数func0003(double a, int b)的调用" << endl;
}


// 注意:函数的返回值不能作为函数重载的条件
void func_diff_return(int a) {
	cout << "函数void func_diff_return(int a)的调用" << endl;
}
//int func_diff_return(int a) {  // 错误(活动)	E0311	无法重载仅按返回类型区分的函数
//
//	cout << "函数int func_diff_return(int a)的调用" << endl;
//}


int main() {

	// func_same();  // 错误	C2084	函数“void func_same(void)”已有主体

	// 满足函数重载后,在调用函数时可以根据调用方式自动选择匹配的同名函数!

	// 1. 参数类型不同
	func0001();  // 函数func0001的调用
	func0001(10);  // 函数func0001(int a)的调用
	func0001(3.14);  // 函数func0001(double a)的调用


	// 2. 参数个数不同
	func0002(10);  // 函数func0002(int a)的调用
	func0002(10, 3.14);  // 函数func0002(int a, double b)的调用


	// 3. 参数顺序不同
	func0003(10, 3.14);  // 函数func0003(int a, double b)的调用
	func0003(3.14, 10);  // 函数func0003(double a, int b)的调用


	system("pause");
	return 0;
}

3.3.2 函数重载的注意事项

  1. 引用作为重载条件
  2. 函数重载碰到函数默认参数

示例:

#include <iostream>
using namespace std;


// 1. 引用作为重载的条件
void func00001(int& a) {
	cout << "func00001(int& a)的调用" << endl;
}

void func00001(const int& a) {
	cout << "func00001(const int& a)的调用" << endl;
}


// 2. 函数重载碰到默认参数
void func00002(int a) {
	cout << "func00002(int a)的调用" << endl;
}

void func00002(int a, int b = 10) {
	cout << "func00002(int a, int b)的调用" << endl;
}


int main() {
	// 1. 引用作为重载的条件
	/*
		这里按道理两种重载函数都可以调用,但因为变量a本身是可读可写的,而func00001(const int& a)会限制变量的可读可写,因此会优先调用限制少的,即func00001(int& a)
	*/
	int a = 10;
	func00001(a);  // func00001(int& a)的调用

	/*
		对于void func00001(int& a) {} 而言,直接传入10就等于 int& a = 10; 这句话本身就是不合法的,因此不能这么调用,所以不会走这个函数

		对于void func00001(const int& a) {} 而言,因为加了const,所以 const int& a = 10 就等价于 int tmp = 10; int& a = tmp; 这样就是合法的。
	*/
	func00001(10);  // func00001(const int& a)的调用
	

	// 2. 函数重载碰到默认参数
	/*
		当函数重载碰到了默认参数,会出现二义性(歧义),因此应该避免这种情况!

		对于func00002(10); 而言,既可以调用第一个函数,也可以调用第二个函数,所以会出现二义性!
	*/
	// func00002(10);  // 错误(活动)	E0308	有多个 重载函数 "func00002" 实例与参数列表匹配

	func00002(10, 20);

	system("pause");
	return 0;
}

4. 类和对象

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

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

和Python是一样的,万物皆可对象😂

例如:

  • 人可以作为对象,属性有姓名、年龄、身高、体重…,行为有走、跑、跳、吃饭、唱歌…
  • 车也可以作为对象,属性有轮胎、方向盘、车灯…行为有载人、放音乐、放空调…

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

4.1 封装

4.1.1 封装的意义

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

封装的意义:

  • 将属性和行为作为一个整体,表现生活中的事物
  • 将属性和行为加以权限控制
1. 封装意义一:
  • 在设计类的时候,属性和行为写在一起,表现事物。
  • 语法:class 类名 {访问权限: 属性 / 行为};

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

#include <iostream>
using namespace std;
const double PI = 3.1415926;  // 定义一个全局常量


/*
* 设计一个圆类,求圆的周长
*	求周长的公式:2 * PI * 半径
*/
class Circle {
	// 访问权限
public:  // 公共权限

	// 属性
	int radius;  // 半径

	// 行为
	double calc_girth() {  // 计算圆的周长
		return 2 * PI * radius;
	}
};


int main() {

	// 实例化Circle类的对象
	Circle c1;
	// 给圆的对象c1的属性进行赋值
	c1.radius = 10;

	cout << "圆的周长为: " << c1.calc_girth() << endl;  // 圆的周长为: 62.8319

	system("pause");
	return 0;
}

可以发现,和Python的class非常像。

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

#include <iostream>
using namespace std;


// 设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号。
class Student {
public:
	/*
	* 类中的属性和行为统一称为成员
	*	属性:成员变量 / 成员属性
	*	行为:成员函数 / 成员方法
	*/
	// 属性
	string name;  // 姓名
	int id;  // 学号

	// 行为(方法)
	void show_info() {
		cout << "姓名: " << name << "\t学号: " << id << endl;
	}

	// 给姓名赋值(方法)
	void set_name(string tmp_name) {
		name = tmp_name;
	}

	// 给学号赋值(方法)
	void set_id(int tmp_id) {
		id = tmp_id;
	}
};


int main() {
	// 实例化Student类
	Student stu1;
	Student stu2;

	// 给对象的属性赋值
	stu1.name = "张三";
	stu1.id = 1;
	stu1.show_info();  // 姓名: 张三      学号: 1

	stu2.set_name("李四");
	stu2.set_id(2);
	stu2.show_info();  // 姓名: 李四      学号: 2


	system("pause");
	return 0;
}

可以发现,和Python的class非常像。

2. 封装意义二:

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

访问权限有三种:

  1. public公共权限:成员在类内可以访问,类外也可以访问
  2. protected保护权限:成员在类内可以访问,在类外不可以访问,子类也可以方法
  3. private私有权限:成员在类内可以访问,类外不可以访问,子类不可以访问
权限类内类外子类是否可以访问
public
protected×
private××

protectedprivate现在看不出区别,具体是在继承的时候可以提现二者的区别。

#include <iostream>
using namespace std;


/*
	访问权限有三种:
		1. public公共权限:成员	类内可以访问	类外也可以访问 子类也可以方法
		2. protected保护权限:成员	类内可以访问	在类外不可以访问	子类也可以方法
		3. private私有权限:成员	类内可以访问	类外不可以访问 子类不可以访问
*/
class Person {
public:
	// 公共权限
	string name;  // 姓名

	// 保护权限
protected:
	string car;  // 汽车

	// 私有权限
private:
	int pwd;  // 密码

public:  
	void func() {  // 类内怎么都是可以访问成员变量的
		name = "张三";
		car = "拖拉机";
		pwd = 123456;
	}
};


int main() {

	// 实例化具体对象
	Person p1;

	p1.name = "李四";
	// p1.car = "奔驰";  // 保护权限类外不可以访问
	// p1.pwd = 456789;  // 私有权限类外不可以访问


	system("pause");
	return 0;
}

4.1.2 struct和class区别

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

  • struct默认权限为公共
  • class默认权限为私有

在C++中classstruct没有什么太大的区别,都可以定义一个类,知识默认的访问权限不同。

#include <iostream>
using namespace std;


/*
	struct和class的区别:
		struct的默认权限是public
		class的默认权限是private
*/
class C1 {
	int a;  // 默认权限是private
};


struct S1
{
	int a;  // 默认权限是public
};


int main() {

	// 实例化
	C1 c1;
	// c1.a = 100;  // private权限类外无法访问

	S1 s1;
	s1.a = 100;  // public权限类外可以访问
	cout << s1.a << endl;  // 100


	system("pause");
	return 0;
}

4.1.3 成员属性设置为私有

优点1:将所有成员属性设置为私有,可以自己控制读写权限。
优点2:对于写权限,我们可以检测数据的有效性。

示例:

#include <iostream>
using namespace std;
#include <string>

/*
	成员属性设置为私有。有如下优点:
		1. 可以自己控制读写权限
		2. 对于写可以检测数据的有效性
*/
class Person {
public:
	// 设置姓名
	void set_name(string n) {
		name = n;
	}
	// 获取姓名
	string get_name() {
		return name;
	}

	// 获取性别
	string get_gender() {
		return gender;
	}

	// 设置couple
	void set_couple(string cp) {
		couple = cp;
	}

	// 设置年龄(小于零或大于150为非法)
	void set_age(int ag) {
		if (ag < 0 || ag > 150)
		{
			cout << "您的输入有误,年龄设置失败!" << endl;
			age = -1;  // 设置一个特定的年龄,表示年龄赋值失败
		}
		else
		{
			age = ag;
		}
	}
	int get_age() {
		return age;
	}

private:
	string name;  // 设置可读可写的权限
	string gender = "男";  // 设置只读的权限
	string couple;  // 设置可写的权限
	int age;  // 可读可写,但要验证数据
};


int main() {

	Person p;
	p.set_name("张三");
	p.set_couple("李四");
	p.set_age(1000);

	cout << "姓名: " << p.get_name() << endl;  // 姓名: 张三
	cout << "性别: " << p.get_gender() << endl;  // 性别: 男
	cout << "年龄: " << p.get_age() << endl;  // 年龄: -1

	system("pause");
	return 0;
}

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

  • 设计立方体类(Cube)
  • 求出立方体的面积和体积
    • 面积: 2 ( l × w + l × h + w × h ) 2(l\times w + l \times h + w \times h) 2(l×w+l×h+w×h)
    • 体积: l × w × h l \times w \times h l×w×h
      • 其中 l l l为立方体的长, w w w为立方体的宽, h h h为立方体的高
  • 分别用全局函数和成员函数判断两个立方体是否相等。
#include <iostream>
using namespace std;
#include <string>

/*
	1. 创建立方体类
	2. 设计属性
	3. 设计行为
		3.1 获取面积
		3.2 获取体积
	4. 分别利用全局函数和成员函数判断两个立方体是否相等
*/
class Cube {

public:
	// 设置/获取长宽高
	// 长
	void set_length(int len) {
		l = len;
	}
	int get_length() {
		return l;
	}

	// 宽
	void set_width(int width) {
		w = width;
	}
	int get_width() {
		return w;
	}

	// 高
	void set_height(int height) {
		h = height;
	}
	int get_height() {
		return h;
	}


	// 获取立方体面积
	int calc_area() {
		return 2 * (l * w + l * h + w * h);
	}

	// 获取立方体体积
	int calc_volume() {
		return l * w * h;
	}

	// 利用成员函数判断两个立方体是否相等
	bool is_same_cube(Cube& other_cube) {
		if (l == other_cube.get_length() &&
			w == other_cube.get_width() &&
			h == other_cube.get_height())
		{
			return true;
		}
		else
		{
			return false;
		}
	}


private:
	int l;
	int w;
	int h;
};


// 利用全局函数判断两个立方体是否相等
bool is_same_cube(Cube& c1, Cube& c2) {  // 使用引用可以节省资源(而且我们也不打算修改值)
	if (c1.get_length() == c2.get_length() && 
		c1.get_width() == c2.get_width() &&
		c1.get_height() == c2.get_height())
	{
		return true;
	}
	else
	{
		return false;
	}
}


int main() {

	// 创建第一个立方体
	Cube c1;
	c1.set_length(10);
	c1.set_width(10);
	c1.set_height(10);

	cout << "c1的面积为: " << c1.calc_area() << endl;
	cout << "c1的体积为: " << c1.calc_volume() << endl;

	// 创建第二个立方体
	Cube c2;
	c2.set_length(10);
	c2.set_width(10);
	c2.set_height(10);

	cout << "c2的面积为: " << c2.calc_area() << endl;
	cout << "c2的体积为: " << c2.calc_volume() << endl;

	// 利用全局函数判断两个立方体是否相等
	bool res_1 = is_same_cube(c1, c2);
	if (res_1)
	{
		cout << "[全局函数]c1和c2是相等的!" << endl;
	}
	else
	{
		cout << "[全局函数]c1和c2不相等的!" << endl;
	}

	// 利用成员函数判断两个立方体是否相等
	bool res_2 = c1.is_same_cube(c2);
	if (res_1)
	{
		cout << "[成员函数]c1和c2是相等的!" << endl;
	}
	else
	{
		cout << "[成员函数]c1和c2不相等的!" << endl;
	}

	/*
		c1的面积为: 600
		c1的体积为: 1000
		c2的面积为: 600
		c2的体积为: 1000
		[全局函数]c1和c2是相等的!
		[成员函数]c1和c2是相等的!
	*/

	system("pause");
	return 0;
}

练习案例2:点和圆的关系

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

  • 点在圆的外侧
  • 点在圆的内测
  • 点在圆上

在这里插入图片描述

思路:知道圆心后,计算圆心和其他点的距离,根据距离再判断关系。

两点之间距离 = ( x 1 − x 2 ) 2 + ( y 1 − y 2 ) 2 两点之间距离=\sqrt{(x_1 - x_2)^2 + (y_1 - y_2)^2} 两点之间距离=(x1x2)2+(y1y2)2
两点之间距 离 2 = ( x 1 − x 2 ) 2 + ( y 1 − y 2 ) 2 两点之间距离^2=(x_1 - x_2)^2 + (y_1 - y_2)^2 两点之间距2=(x1x2)2+(y1y2)2

文件拆分注意事项

  1. .h头文件中,只做声明,不做具体实现
  2. .cpp文件中做具体实现,且删除声明并引用.h头文件
  3. .cpp文件中需要加上对应的作用域,例如Point::
  4. 拆分完毕后需要在main.cpp文件中引用相应的.h头文件
main.cpp文件
#include <iostream>
using namespace std;
#include <string>
#include <math.h>
#include "point.h"  // 引用相应的头文件
#include "circle.h"  // 引用相应的头文件


// 判断点和圆的关系
void calc_relation(Circle& c, Point& p) {
	// 计算两点之间的距离的平方
	int distance = pow(c.get_center().get_x() - p.get_x(), 2) + pow(c.get_center().get_y() - p.get_y(), 2);

	// 计算半径的平方
	int r_pow = pow(c.get_r(), 2);

	// 判断关系
	if (distance == r_pow)
	{
		cout << "点在圆上" << endl;
	}
	else if (distance > r_pow)
	{
		cout << "点在圆外" << endl;
	}
	else
	{
		cout << "点在圆内" << endl;
	}
}


int main() {

	// 创建圆
	Circle c;
	c.set_r(10);
	Point center;  // 圆心
	center.set_x(10);
	center.set_y(0);
	c.set_center(center);

	// 创建点
	Point p;
	p.set_x(10);
	p.set_y(9);


	// 判断关系
	calc_relation(c, p);  // 点在圆内

	system("pause");
	return 0;
}
point.h文件
#pragma once  // 防止头文件重复包含
#include <iostream>
using namespace std;


class Point {  // 在.h头文件中,只做声明!
public:
	// 设置x
	void set_x(int xx);
	// 获取x
	int get_x();

	// 设置y
	void set_y(int yy);

	// 获取y
	int get_y();

private:
	int x;
	int y;
};
point.cpp文件
#include "point.h"


// 设置x
void Point::set_x(int xx) {  // 需要加上作用域Point::
	x = xx;
}
// 获取x
int Point::get_x() {
	return x;
}

// 设置y
void Point::set_y(int yy) {
	y = yy;
}

// 获取y
int Point::get_y() {
	return y;
}
circle.h文件
#pragma once
#include <iostream>
using namespace std;
#include "point.h"


class Circle {
public:
	// 设置半径
	void set_r(int rr);

	// 获取半径
	int get_r();

	// 设置圆心
	void set_center(Point& ct);

	// 获取圆心
	Point get_center();

private:
	int r;  // 半径
	Point center;  // 圆心
	/*
	* 在类中可以让另一个类作为本类的成员
	*/
};
circle.cpp文件
#include "circle.h"


// 设置半径
void Circle::set_r(int rr) {
	r = rr;
}

// 获取半径
int Circle::get_r() {
	return r;
}

// 设置圆心
void Circle::set_center(Point& ct) {
	center = ct;
}

// 获取圆心
Point Circle::get_center() {
	return center;
}

4.2 对象的初始化和清理

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

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

4.2.1 构造函数和析构函数

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

  • 一个对象或者变量没有初始状态,对其使用后果是未知的
  • 同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题

C++利用了构造函数析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供,但是编译器提供的构造函数和析构函数是空实现(空实现:没有代码)。

  • 构造函数(Constructor):主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
  • 析构函数(Destructor):主要作用在于对象销毁前系统自动调用,执行一些清理工作。

构造函数负责创建
析构函数负责销毁

构造和析构是反义词

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

  1. 构造函数,没有返回值也不写void
  2. 函数名称与类名相同
  3. 构造函数可以有参数,因此可以发生重载
  4. 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次

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

  1. 析构函数,没有返回值也不写void
  2. 函数名称与类名相同,在名称前加上符号~
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 程序在对象销毁前会自动调用析构,无须手动调用,且只会调用一次

示例:

#include <iostream>
using namespace std;


class Person {
public:
	/*1. 构造函数(①没有返回值也不用写void;②与类名相同;
	* ③可以有参数也可以重载;④自动调用且调用一次)
	*/
	Person() {  // 无参的构造函数
		cout << "Person 构造函数的调用" << endl;
	}

	/*
	* 2. 析构函数(①没有返回值也不用写void;②与类名相同,但前面需要加~;
	* ③没有参数,不能重载;④自动调用且一次)
	*/
	~Person() {
		cout << "Person 析构函数调用" << endl;
	}
};


// 构造和析构都是必须有的实现,如果我们自己不提供,编译器会提供一个空实现的构造和析构函数(里面什么都没有)
void test01() {
	Person p;  // 在栈区创,test01执行完毕后会自动调用析构函数
}


int main1() {

	test01();
	/*
		Person 构造函数的调用
		Person 析构函数调用
	*/


	Person p;
	/*
		Person 构造函数的调用
		请按任意键继续. . .
		Person 析构函数调用
	*/

	system("pause");
	return 0;
}

4.2.2 构造函数的分类及调用

两种分类方式:

  1. 按参数分为:有参构造和无参构(默认构造)
  2. 造按类型分为:普通构造和拷贝构造

三种调用方式:

  1. 括号法
  2. 显示法
  3. 隐式转换法

这三种方式都可以,推荐使用前两个

注意事项:

  • [括号法]:调用默认构造函数时,不要加()
    • 例如:Person p1();
    • 因为我们加了(),编译器会认为这是一个函数的声明,例:void func(); 所以不会认为在创建对象!
  • [显示法]:不要利用拷贝构造函数来初始化匿名对象。
    • 例如:Person(p1);
    • 因为编译器会认为 Person(p1) <=> Person p1; -> 又创建了一个p1对象
    • 这样就会导致对象声明重复!
  • [隐式转换法]:None

示例:

#include <iostream>
using namespace std;

/*
	构造函数的两种分类方式:
		1. 按参数分为:有参构造和无参构
		2. 造按类型分为:普通构造和拷贝构造
*/
class Person1 {
public:
	// 1. 按参数分为:有参构造和无参构
	Person1() {  // 无参构造函数(默认构造)
		cout << "Person1的[无参]构造函数调用" << endl;
	}

	Person1(int a) {  // 有参构造函数
		age = a;
		cout << "Person1的[有参]构造函数调用" << endl;
	}


	// 2. 造按类型分为:普通构造和拷贝构造
	// 2.1 普通构造函数(上面两个都属于普通构造函数)
	// 2.2 拷贝构造函数
	Person1(const Person1& p) {
		// 将传入的Person的所有属性拷贝到自己身上
		age = p.age;
		cout << "Person1的[拷贝]构造函数调用" << endl;
	}

	~Person1() {  // 析构函数
		cout << "Person1的[析构]函数调用" << endl;
	}


	// 获取年龄
	int get_age() {
		return age;
	}
private:
	int age;
};


/*
	三种调用方式:
		1. 括号法
		2. 显示法
		3. 隐式转换法
*/
void test02() {
	// 1. 括号法
	cout << "---------1. 括号法---------" << endl;
	// 注意事项1:调用默认构造函数时,不要加()
	Person1 p1;  // 默认构造函数调用(无参构造)
	Person1 p4();  // 不显示调用构造函数。
	// 这是因为我们加了(),编译器会认为这是一个函数的声明,例:void func(); 所以不会认为在创建对象!
	Person1 p2(10);  // 有参构造函数调用
	Person1 p3(p2);  // 拷贝构造函数调用
	cout << "p2的年龄为: " << p2.get_age() << endl;
	cout << "p3的年龄为: " << p3.get_age() << endl;
	/*
		Person1的[无参]构造函数调用
		Person1的[有参]构造函数调用
		Person1的[拷贝]构造函数调用
		p2的年龄为: 10
		p3的年龄为: 10
		Person1的[析构]函数调用
		Person1的[析构]函数调用
		Person1的[析构]函数调用
	*/


	// 2. 显示法
	/*
		注意事项2:不要利用拷贝构造函数来初始化匿名对象
		因为编译器会认为 Person1(p33) <=> Person1 p33;
		这样就会导致对象声明重复!
	*/
	cout << "---------2. 显示法---------" << endl;
	Person1 p11;  // 默认构造
	Person1 p22 = Person1(10);  // 有参构造
	Person1 p33 = Person1(p22);  // 拷贝构造

	Person1(10);  // 这个东西单独拿出来称之为匿名对象:当前行执行结束后系统会立即回收
	cout << "Person1(10)的析构应该在这行上面显示!" << endl;

	// 利用拷贝构造初始化匿名对象
	// Person1(p33);  // 警告	C26444	请勿尝试声明不带名称的局部变量


	// 3. 隐式转换法
	cout << "---------3. 隐式转换法---------" << endl;
	Person1 p111 = 10;  // [有参构造] 等价于 Person1 p111 = Person1(10)
	Person1 p222 = p111;  // [拷贝构造] 等价于 Person1 p222 = Person1(p111)


	cout << "---------test02函数结束,下面是析构函数调用---------" << endl;

	/*
		---------1. 括号法---------
		Person1的[无参]构造函数调用
		Person1的[有参]构造函数调用
		Person1的[拷贝]构造函数调用
		p2的年龄为: 10
		p3的年龄为: 10
		---------2. 显示法---------
		Person1的[无参]构造函数调用
		Person1的[有参]构造函数调用
		Person1的[拷贝]构造函数调用
		Person1的[有参]构造函数调用
		Person1的[析构]函数调用
		Person1(10)的析构应该在这行上面显示!
		---------3. 隐式转换法---------
		Person1的[有参]构造函数调用
		Person1的[拷贝]构造函数调用
		---------test02函数结束,下面是析构函数调用---------
		Person1的[析构]函数调用
		Person1的[析构]函数调用
		Person1的[析构]函数调用
		Person1的[析构]函数调用
		Person1的[析构]函数调用
		Person1的[析构]函数调用
		Person1的[析构]函数调用
		Person1的[析构]函数调用
		请按任意键继续. . .
	*/
}


int main() {

	test02();

	system("pause");
	return 0;
}

4.2.3 铐贝构造函数调用时机

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

  1. 使用一个已经创建完毕的对象来初始化一个新对象
  2. 值传递的方式给函数参数传值
  3. 以值的方式返回局部对象

示例:

#include <iostream>
using namespace std;


/*
*	拷贝构造函数的调用时机:
	1. 使用一个已经创建完毕的对象来初始化一个新对象
	2. 值传递的方式给函数参数传值
	3. 以值传递的方式返回局部对象
*/
class Person03 {
public:
	Person03() {  // 默认构造
		cout << "Person03默认构造函数的调用" << endl;
	}

	Person03(int age) {  // 有参构造
		cout << "Person03有参构造函数的调用" << endl;
		_age = age;
	}

	Person03(const Person03& p) {  // 拷贝构造
		cout << "Person03拷贝构造函数的调用" << endl;
		_age = p._age;
	}

	~Person03() {
		cout << "Person03析构构造函数的调用" << endl;
	}


	int get_age() {
		return _age;
	}

private:
	int _age;
};


// 方法1. 使用一个已经创建完毕的对象来初始化一个新对象
void test001() {
	Person03 p1(20);
	Person03 p2(p1);

	cout << "p2的年龄为: " << p2.get_age() << endl;  // p2的年龄为: 20
}


// 方法2. 值传递的方式给函数参数传值
void do_work(Person03 p) {
}

void test002() {
	Person03 p;  // 默认构造
	do_work(p);  // 值传递的时候会临时创建一个新的副本,因此这里会调用拷贝构造
}


// 方法3. 值方式返回局部变量
Person03 do_work_2() {
	Person03 p1;
	cout << (int*)&p1 << endl;
	return p1;  // 值方式返回
}

void test003() {
	Person03 p = do_work_2();
	cout << (int*)&p << endl;
}


int main() {

	// 方法1. 使用一个已经创建完毕的对象来初始化一个新对象
	cout << "------方法1. 使用一个已经创建完毕的对象来初始化一个新对象-------" << endl;
	test001();


	// 方法2. 值传递的方式给函数参数传值
	cout << "------方法2. 值传递的方式给函数参数传值-------" << endl;
	test002();


	// 方法3. 值方式返回局部变量
	cout << "------方法3. 值方式返回局部变量-------" << endl;
	test003();


	/*
		------方法1. 使用一个已经创建完毕的对象来初始化一个新对象-------
		Person03有参构造函数的调用
		Person03拷贝构造函数的调用
		p2的年龄为: 20
		Person03析构构造函数的调用
		Person03析构构造函数的调用
		------方法2. 值传递的方式给函数参数传值-------
		Person03默认构造函数的调用
		Person03拷贝构造函数的调用
		Person03析构构造函数的调用
		Person03析构构造函数的调用
		------方法3. 值方式返回局部变量-------
		Person03默认构造函数的调用
		00FAF9DC
		00FAF9DC
		Person03析构构造函数的调用
		请按任意键继续. . .
	*/

	system("pause");
	return 0;
}

4.2.4 构造函数调用规则

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

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝构造函数,对属性进行值拷贝

构造函数调用规则如下:

  • 如果用户定义有参构造函数,C++不在提供默认无参构造,但是会提供默认拷贝构造
  • 如果用户定义拷贝构造函数,C++不会再提供其他构造函数(默认和有参都不会提供了)
自定义默认Constructor有参Constructor拷贝Constructor
默认Constructor××
有参Constructor×
拷贝Constructor××

示例:

#include <iostream>
using namespace std;

// 构造函数的调用规则
/*
	1. 创建一个类,C++编译器会给每个类都添加至少3个构造函数
		1. 默认构造(空实现)
		2. 析构函数(空实现)
		3. 拷贝构造
	2. 如果我们写了有参Constructor,编译器就不再提供默认Constructor,但依然提供拷贝Construct
		如果我们写了拷贝Constructor,那么编译器就不再提供普通的Constructor了(默认和有参都不提供了)
*/
class Person04 {
public:
	Person04() {
		cout << "Person04的默认Constructor调用" << endl;
	}

	Person04(int age) {
		cout << "Person04的有参Constructor调用" << endl;
		_age = age;
	}

	Person04(const Person04& p) {
		cout << "Person04的拷贝Constructor调用" << endl;
		_age = p._age;
	}

	~Person04() {
		cout << "Person04的Destructor函数调用" << endl;
	}

	void set_age(int age) {
		_age = age;
	}

	int get_age() {
		return _age;
	}
private:
	int _age;
};


class Person042 {
public:
	Person042() {
		cout << "Person042的默认Constructor调用" << endl;
	}

	Person042(int age) {
		cout << "Person042的有参Constructor调用" << endl;
		_age = age;
	}

	//Person042(const Person042& p) {
	//	cout << "Person042的拷贝Constructor调用" << endl;
	//	_age = p._age;
	//}

	~Person042() {
		cout << "Person042的Destructor函数调用" << endl;
	}

	void set_age(int age) {
		_age = age;
	}

	int get_age() {
		return _age;
	}
private:
	int _age;
};


class Person043 {
public:
	//Person043() {
	//	cout << "Person043的默认Constructor调用" << endl;
	//}

	Person043(int age) {
		cout << "Person043的有参Constructor调用" << endl;
		_age = age;
	}

	//Person043(const Person043& p) {
	//	cout << "Person043的拷贝Constructor调用" << endl;
	//	_age = p._age;
	//}

	~Person043() {
		cout << "Person043的Destructor函数调用" << endl;
	}

	void set_age(int age) {
		_age = age;
	}

	int get_age() {
		return _age;
	}
private:
	int _age;
};


class Person044 {
public:
	//Person044() {
	//	cout << "Person044的默认Constructor调用" << endl;
	//}

	//Person044(int age) {
	//	cout << "Person044的有参Constructor调用" << endl;
	//	_age = age;
	//}

	Person044(const Person044& p) {
		cout << "Person044的拷贝Constructor调用" << endl;
		_age = p._age;
	}

	~Person044() {
		cout << "Person044的Destructor函数调用" << endl;
	}

	void set_age(int age) {
		_age = age;
	}

	int get_age() {
		return _age;
	}
private:
	int _age;
};


void test0001() {
	// 自己定义了拷贝Constructor
	cout << "---------自己定义了拷贝Constructor---------" << endl;
	Person04 p1;
	p1.set_age(18);

	Person04 p2(p1);

	cout << "p1._age: " << p1.get_age() << endl;
	cout << "p2._age: " << p2.get_age() << endl;
}

void test0002() {
	// 没有定义拷贝Constructor,由编译器提供
	cout << "---------没有定义拷贝Constructor,由编译器提供---------" << endl;
	Person042 p12;
	p12.set_age(18);
	Person042 p22(p12);
	cout << "p12._age: " << p12.get_age() << endl;
	cout << "p22._age: " << p22.get_age() << endl;
}


void test0003() {
	/*
		因为没写了有参Constructor,编译器就不会提供给我们默认Constructor了,
		如果我们还想用默认的Constructor,那么就会报错!

		正确的做法是调用有参的Constructor
	*/
	// Person043 p;  // 错误(活动)	E0291	类 "Person043" 不存在默认构造函数

	cout << "---------只定义了有参Constructor---------" << endl;
	Person043 p1(20);
	Person043 p2(p1);
	cout << "p1._age: " << p1.get_age() << endl;
	cout << "p2._age: " << p2.get_age() << endl;

}


void test0004() {
	cout << "---------只定义了拷贝Constructor---------" << endl;
	// Person044 p;  // 错误(活动)	E0291	类 "Person044" 不存在默认构造函数

	// Person044 p(10);  // 错误(活动)	E0289	没有与参数列表匹配的构造函数 "Person044::Person044" 实例
	
	// 如果class里面只有Copy Constructor,怎么调用我不会:joy:
}


int main() {
	
	test0001();
	/*
		Person04的默认Constructor调用
		Person04的拷贝Constructor调用
		p1._age: 18
		p2._age: 18
		Person04的Destructor函数调用
		Person04的Destructor函数调用
	*/


	// 可以看到,虽然我们没有写拷贝Constructor,但是编译器给我们提供了,所以p22._age == 18
	test0002();
	/*
	Person042的默认Constructor调用
	p12._age: 18
	p22._age: 18
	Person042的Destructor函数调用
	Person042的Destructor函数调用
	*/


	test0003();
	/*
		Person043的有参Constructor调用
		p1._age: 20
		p2._age: 20
		Person043的Destructor函数调用
		Person043的Destructor函数调用
	*/

	system("pause");
	return 0;
}

4.2.5 深拷贝与浅拷贝

深浅拷贝是面试经典问题,也是常见的一个坑。

  • 浅拷贝:简单的赋值拷贝操作
  • 深拷贝:在堆区重新申请空间,进行拷贝操作

浅拷贝带来的问题:堆区的内存重复释放。
如何解决:使用深拷贝解决浅拷贝带来的问题。
办法:在Copy Constructor中使用new关键字重新开辟一块内存空间。


示例:

#include <iostream>
using namespace std;


class Person05 {
public:
	Person05() {
		cout << "Person05的默认Constructor调用" << endl;
	}

	Person05(int age, int height) {
		_age = age;
		_p_height = new int(height);  // new返回的是一个地址,需要用指针接收(new出来的数据在堆区)
		cout << "Person05的有参Constructor调用" << endl;
	}

	// 自己实现Copy Constructor,以解决浅拷贝带来的内存重复释放问题
	Person05(const Person05& p) {
		cout << "Person05的Copy Constructor调用" << endl;
		_age = p._age;
		// _p_height = p._p_height;  // 编译器默认实现这行代码
		_p_height = new int(*p._p_height);  // 利用new关键字重新开辟一块内存,里面存储地址
	}

	~Person05() {  // 在Heap Area开辟的数据做释放操作
		if (_p_height != NULL) {
			delete _p_height;  // 释放
			_p_height = NULL;  // 防止野指针出现,将其置空
		}
		cout << "Person05的Destructor调用" << endl;
	}

	void set_age(int age) {
		_age = age;
	}

	int get_age() {
		return _age;
	}

	void set_height(int height) {
		*_p_height = height;
	}

	int* get_height() {
		return _p_height;
	}

private:
	int _age = -1;
	int* _p_height;  // 身高
};


void test00001() {
	Person05 p1(18, 165);
	cout << "p1._age: " << p1.get_age() <<
		"\theight: " << *p1.get_height() << endl;

	Person05 p2(p1);
	cout << "p2._age: " << p2.get_age() <<
		"\theight: " << *p2.get_height() << endl;
}


int main() {

	test00001();

	/*
		Person05的有参Constructor调用
		p1._age: 18     height: 165
		Person05的Copy Constructor调用
		p2._age: 18     height: 165
		Person05的Destructor调用
		Person05的Destructor调用
		请按任意键继续. . .
	*/

	system("pause");
	return 0;
}

在这里插入图片描述

总结:如果属性有在堆区开辟的,一定要自己提供Copy Constructor函数,防止浅拷贝带来的问题(具体为内存重复释放问题)。

4.2.6 初始化列表

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

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

示例:

#include <iostream>
using namespace std;


// 初始化列表
class Person061 {
public:
	// 传统初始化操作
	Person061(int a, int b, int c) {
		_a = a;
		_b = b;
		_c = c;
	}


	void show_info() {
		cout << "------传统初始化属性的操作-------" << endl;
		cout << "_a: " << _a << endl;
		cout << "_b: " << _b << endl;
		cout << "_c: " << _c << endl;
	}

private:
	int _a;
	int _b;
	int _c;
};


class Person062 {
public:
	// 传统初始化操作
	//Person062(int a, int b, int c) {
	//	_a = a;
	//	_b = b;
	//	_c = c;
	//}


	// 初始化列表初始化属性
	Person062(int a, int b, int c) : _a(a), _b(b), _c(c) {}


	void show_info() {
		cout << "------初始化列表初始化属性的操作-------" << endl;
		cout << "_a: " << _a << endl;
		cout << "_b: " << _b << endl;
		cout << "_c: " << _c << endl;
	}

private:
	int _a;
	int _b;
	int _c;
};


int main() {
	
	Person061 p1(10, 20, 30);
	p1.show_info();

	Person062 p2(10, 20, 30);
	p2.show_info();


	system("pause");
	return 0;
}

可以看得出来,有参Constructor和初始化列表的方式效果是一样的,只不过两者的语法不同。

4.2.7 类对象作为类成员

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

例如:

class A {}

class B {
	A a;
}

B类中有对象A作为成员,A为对象成员。

那么当创建B对象时,AB的构造(Constructor)和析构(Destructor)的顺序是谁先谁后?

  • [Constructor] 当其他类对象作为本类对象时,先构造其他类对象,再构造自身
  • [Destructor] 析构的顺序与构造的顺序相反

示例:

#include <iostream>
using namespace std;
#include <string>


// 类对象作为类成员
class Phone {
public:
	Phone(string brand) {  // 有参Constructor
		cout << "Phone的Constructor调用" << endl;
		_brand = brand;
	}

	~Phone() {
		cout << "Phone的Destructor调用" << endl;
	}

	string _brand;  // 品牌名
};


class Person07 {
	/*
		1. [Constructor] 当其他类对象作为本类对象时,先构造其他类对象,再构造自身
		2. [Destructor] 析构的顺序与构造的顺序相反
	*/
public:

	// Phone _phone = brand; 隐式转换法创建对象
	Person07(string name, string brand) : _name(name), _phone(brand) {
		cout << "Person的Constructor调用" << endl;
	}  

	~Person07() {
		cout << "Person07的Destructor调用" << endl;
	}

	string _name;
	Phone _phone;
};


void test7_1() {
	Person07 p1("张三", "Apple");
	cout << p1._name << "拿着" << p1._phone._brand << endl;
}


int main() {

	test7_1();  // 张三拿着Apple
	/*
		Phone的Constructor调用
		Person的Constructor调用
		张三拿着Apple
		Person07的Destructor调用
		Phone的Destructor调用
	*/

	system("pause");
	return 0;
}

4.2.8 静态成员

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

静态成员分为:

  1. 静态成员变量的特点:
    • 所有对象共享同一份数据
    • 在编译阶段分配内存
      • 在全局区
    • 类内声明,类外初始化(必须初始化)
  2. 静态成员函数的特点:
    • 所有对象共享同一个函数
    • 静态成员函数只能访问静态成员变量

静态成员变量和静态成员函数都是有访问权限的,privateprotected权限在类外访问不到。

1. 静态成员变量

静态成员变量的特点:

  • 所有对象共享同一份数据
  • 在编译阶段分配内存
    • 在全局区
  • 类内声明,类外初始化(必须初始化)

静态成员变量的声明和初始化方式:

  • 声明方式:[类内]static int 静态成员变量名;
  • 初始化方式:[类外]int 类名::静态成员变量名 = xxx;

在使用静态成员变量时,必须在类外初始化

访问方式:

  1. 通过对象进行访问:obj.静态成员变量;
  2. 通过类名进行访问(和Python很像):Object::静态成员变量;

静态成员变量和静态成员函数都是有访问权限的,privateprotected权限在类外访问不到。

#include <iostream>
using namespace std;


/*
	静态成员变量:
		1. 所有对象都共享同一份数据
		2. 编译阶段就分配了内存(全局区)
		3. 类内声明,类外初始化

		静态成员变量也是有访问权限的:类外访问不到私有的静态成员变量
*/
class Person08 {
public:
	static int _a;  

private:
	// 静态成员变量也是有访问权限的:类外访问不到私有的静态成员变量
	static int _b;
};


// 类外初始化
int Person08::_a = 100;
int Person08::_b = 200;


void test8_1() {
	Person08 p1;
	cout << "p1._a: " << p1._a << endl;  // 100

	Person08 p2;
	p2._a = 200;
	cout << "p1._a: " << p1._a << endl;  // 200
	cout << "p2._a: " << p2._a << endl;  // 200
}


void test8_2() {
	/*
		静态成员变量不属于某个对象,所有对象都共享同一份数据,
		因此静态成员变量有两种访问方式:
			1. 通过对象进行访问
			2. 通过类名进行访问(和Python很像)
	*/

	// 1. 通过对象进行访问
	Person08 p1;
	cout << "p1._a: " << p1._a << endl;  // 200

	// 2. 通过类名进行访问(和Python很像)
	cout << "Person::_a: " << Person08::_a << endl;  // 200

	// 静态成员变量也是有访问权限的:类外访问不到私有的静态成员变量
	// cout << "Person::_b: " << Person08::_b << endl;  // 错误(活动)	E0265	成员 "Person08::_b" (已声明 所在行数 : 25) 不可访问	对象的初始化和清理

}


int main() {

	test8_1();

	test8_2();

	system("pause");
	return 0;
}

2. 静态成员函数

静态成员函数的特点:

  • 所有对象共享同一个函数
  • 静态成员函数只能访问静态成员变量(非静态成员变量是访问不了的)
    • 原因:[非静态成员变量]必须创建对象才可以访问,而[静态成员函数]是可以通过类名直接调用的,直接调用时就不知道到底调用(修改)哪个对象的[非静态成员变量],所以不可以。
    • 而[静态成员变量]因为是共享的(只有一份),所以[静态成员函数]就知道调用(修改)哪个了。

访问方式:

  1. 通过对象进行访问:obj.静态成员函数;
  2. 通过类名进行访问(和Python很像):Object::静态成员函数;

静态成员变量和静态成员函数都是有访问权限的,privateprotected权限在类外访问不到。

#include <iostream>
using namespace std;


/*
	静态成员函数
		1. 所有对象共享同一个函数
		2. 静态成员函数只能访问静态成员变量,不能访问非静态成员变量
			原因:[非静态成员变量]必须创建对象才可以访问,而[静态成员函数]是可以通过类名直接调用的,
				直接调用时就不知道到底调用(修改)哪个对象的[非静态成员变量],所以不可以。
				而[静态成员变量]因为是共享的(只有一份),所以[静态成员函数]就知道调用(修改)哪个了。
		
		静态成员函数也是有权限的
*/
class Person09 {
public:
	// 静态成员函数
	static void func1() {
		cout << "静态成员函数func1调用" << endl;
	}

	// 2. 静态成员函数只能访问静态成员变量,不能访问非静态
	static void func2() {
		// 2.1 访问静态成员变量
		_a = 100;  // 静态成员函数可以访问静态成员变量

		// 2.2 访问非静态成员变量
		// _b = 200;  // 错误(活动)	E0245	非静态成员引用必须与特定对象相对

		cout << "静态成员函数func2调用,并修改静态成员变量" << endl;
	}

	static int _a;
	int _b;  // 非静态成员变量


private:
	// 静态成员函数也是有权限的
	static void func3() {
		cout << "private权限下的静态成员函数调用" << endl;
	}
};

// 初始化静态成员变量
int Person09::_a = 0;


void test09_1() {
	/*
		静态成员函数和静态成员变量一样,也有两种访问方式:
			1. 通过对象调用
			2. 通过类名调用
	*/
	// 1. 通过对象调用
	Person09 p;
	p.func1();

	// 2. 通过类名调用
	Person09::func1();


	// 静态成员函数也是有权限的
	// Person09::func3();  // 错误(活动)	E0265	函数 "Person09::func3" (已声明 所在行数 : 37) 不可访问	

}


int main() {

	test09_1();

	system("pause");
	return 0;
}

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

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

在C++中,类内的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上。其他数据,如静态成员变量、静态成员函数、非静态成员函数都不属于类的对象上。

注意事项:

  1. 只有非静态成员变量才属于类的对象上
  2. 空类占用内存空间大小为1字节
#include <iostream>
using namespace std;


// 成员变量和成员函数是分开存储的
class Person1_1 {

};


class Person1_2 {
	int _a;  // 非静态成员变量
};


class Person1_3 {
	static int _a;  // 静态成员变量
};
int Person1_3::_a = 0;  // 静态成员变量初始化


class Person1_4 {
	void func() {}  // 非静态成员函数
};


class Person1_5 {
	static void func() {}  // 静态成员函数
};


void test1_1() {
	Person1_1 p;
	
	/*
		空对象占用内存空间为:1
			这是因为C++编译器会给每个空对象也分配一个字节空间,这是为了区分空对象占内存的位置。
			每个空对象也应该有一个独一无二的内存地址
	*/
	cout << "sizeof(p): " << sizeof(p) << "字节" << endl;  // sizeof(p): 1字节
}


void test1_2() {
	/*
		非静态成员变量 属于 类的对象上的
	*/
	Person1_2 p;
	cout << "sizeof(p): " << sizeof(p) << "字节" << endl;  // sizeof(p): 4字节
}


void test1_3() {
	/*
		静态成员变量 不属于 类的对象上的
	*/
	Person1_3 p;
	cout << "sizeof(p): " << sizeof(p) << "字节" << endl;  // sizeof(p): 1字节
}


void test1_4() {
	/*
		非静态成员函数 不属于 类的对象上的
	*/
	Person1_4 p;
	cout << "sizeof(p): " << sizeof(p) << "字节" << endl;  // sizeof(p): 1字节
}


void test1_5() {
	/*
		静态成员函数 不属于 类的对象上的
	*/
	Person1_5 p;
	cout << "sizeof(p): " << sizeof(p) << "字节" << endl;  // sizeof(p): 1字节
}


int main() {

	test1_1();
	test1_2();
	test1_3();
	test1_4();
	test1_5();

	/*
		1. 空类占用1字节内存空间大小
		2. 只有非静态成员变量 属于 类的对象上,剩下的都不属于类的对象上
	*/

	system("pause");
	return 0;
}

4.3.2 this指针概念

通过 4.3.1 我们知道在C++中成员变量和成员函数是分开存储的。每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码。

那么问题是:这一块代码是如何区分哪个对象调用自己的呢?

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

this和Python中cls的self很像,都是指向实例对象。

  • this指针是隐含每一个非静态成员函数内的一种指针
  • this指针不需要定义,直接使用即可

this指针的语法:this->成员变量

this指针的用途:

  • 当形参和成员变量同名时,可用this指针来区分
  • 在类的非静态成员函数中返回对象本身,可使用return *this

因为this是一个指针,指向实例化对象,那么*this是对地址解引用,即对象自身。

#include <iostream>
using namespace std;


/*
	this指针的作用:
		1. 解决名称冲突
		2. 返回实例化对象本身 return *this;
*/
class Person02_1 {
public:
	Person02_1(int age) {
		age = age;  // 编译器会认为这三个age是同一个
	}

	int age;
};


class Person02_2 {
public:
	Person02_2(int age) {
		/* 1. 解决名称冲突
			this指针指向的是实例化对象
		*/
		this->age = age;  // 编译器会认为这三个age是同一个
	}

	void add_age(Person02_2& p) {  // 把其他对象的age加到自身age上
		this->age += p.age;
	}

	/*
		2. 返回实例化对象本身 return *this;
			
			这个方法的返回值应该是对象的引用,因为返回引用表明返回的就是自身,
			如果不加引用,那么就是返回值,编译器会Copy一份一样的返回,就不是返回自身了!

			Person02_2& p:为什么是引用呢?这里使用引用的主要目的是引用不会再Copy一个副本,从而节省资源。
	*/
	Person02_2& add_age_return_this(Person02_2& p) {
		this->age += p.age;
		return *this;
	}

	Person02_2 add_age_return_value(Person02_2& p) {
		this->age += p.age;
		return *this;
	}

	int age;
};


void test02_1() {
	Person02_1 p(18);
	cout << "p.age: " << p.age << endl;  // p.age: -858993460
}


void test02_2() {
	Person02_2 p(18);
	cout << "p.age: " << p.age << endl;  // p.age: 18
}

void test02_3() {
	Person02_2 p1(10);
	Person02_2 p2(10);
	p2.add_age(p1);
	cout << "p2.age: " << p2.age << endl;  // p2.age: 20

	// 2. 返回实例化对象本身 return *this;
	// 能不能多调用几次add_age()方法?-> 方法返回值不应该是void,而应该是自身
	// 这种一直追加的思想叫做链式编程思想
	Person02_2 p3(10);
	p3.add_age_return_this(p1).add_age_return_this(p1).add_age_return_this(p1);
	cout << "p3.age: " << p3.age << endl;  // p3.age: 40

	// 下面是不用引用的结果,因为返回的是副本,所以只+10
	Person02_2 p4(10);
	p4.add_age_return_value(p1).add_age_return_value(p1).add_age_return_value(p1);
	cout << "p4.age: " << p4.age << endl;  // p4.age: 20
}


int main() {

	test02_1();
	test02_2();
	test02_3();

	system("pause");
	return 0;
}

4.3.3 空指针访问成员函数

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

  • 如果用到this指针,需要加以判断保证代码的Robust。

因为空指针虽然可以调用成员函数,但是不能调用成员变量,所以如果指针是空的,调用成员变量时就会报错。因此在[成员函数]中调用[成员变量]时,最好加上空指针判断,以防止空指针出现,进而导致程序奔溃。

代码如下:

Person{
public:
	void some_func() {
	if (this == NULL)
		{
			// 如果是空指针,直接结束该方法,不往下走了 -> 提高代码的Robust
			return;
		}
	
	// 剩余要调用成员变量的代码
	cout << "age: " << this->age << endl;
	}

private:
	int age;
};

示例:

#include <iostream>
using namespace std;


// 空指针调用成员函数
class Person03_1 {
public:
	void show_class_name() {
		cout << "This is Person03_1 class" << endl;
	}

	void show_person_age() {
		cout << "age: " << this->age << endl;
	}

	int age;
};


class Person03_2 {
public:
	void show_class_name() {
		cout << "This is Person03_2 class" << endl;
	}

	void show_person_age() {
		if (this == NULL)
		{
			return;  // 如果是空指针,直接结束该方法,不往下走了 -> 提高代码的Robust
		}
		cout << "age: " << this->age << endl;
	}

	int age;
};


void test03_1() {
	Person03_1* p = NULL;  // 创建一个类的空指针p

	p->show_class_name();  // This is Person03_1 class

	// 报错原因是空指针->age
	// p->show_person_age();  // **this** 是 nullptr
}


void test03_2() {
	Person03_2* p = NULL;  // 创建一个类的空指针p

	p->show_class_name();  // This is Person03_1 class

	p->show_person_age();  // 不再报错,且没有输出
}


int main() {

	test03_1();
	test03_2();


	system("pause");
	return 0;
}

4.3.4 const修饰成员函数

conststatic关键字要分清

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

  2. 声明对象const称该对象为常对象

  3. 常函数

    • 常函数内不可以修改成员属性
    • 成员属性声明时加关键字mutable后,在常函数中依然可以修改
  4. 常对象

    • 常对象只能调用常函数

mutable: 英[ˈmjuːtəbl] 美[ˈmjuːtəbl] adj. 可变的; 会变的;

语法:

  1. 常函数:返回值类型 函数名() const {函数内容} —— 例:void fn() const {}
  2. 常对象:const 类型 实例对象名; —— 例:const Person p;
  3. 可变变量:mutable 数据类型 变量名; —— 例:mutable int a;

this指针的本质是指针常量,即指针的指向是不可以修改的(指向地址的值是可以修改的)! 等价于 Person* const this;
那么this = NULL; // 这是不可以的,指针常量的指向是不可以修改的!

Q:既然this指针的指向不可以改,我们是否可以限制它指向地址的内容也不允许修改呢?
A:当然是可以的,用const修饰成员方法即可,那么const关键字放在哪里?

  1. 放在返回值类型前面? —— 修饰的是返回值了,不行
  2. 放在(形参列表)中? —— 修饰的是形参了,不行

因此没有办法,放在了成员函数()的后面,即返回值类型 函数名() const {函数内容} —— 例:void func(int a) const {}

在成员函数后面加const,实际上修饰的是this指针,让其指向的值也不可以修改,即this指针的指向不可变,其指向地址的内容也不可以变。

示例:

#include <iostream>
using namespace std;


// 常函数
class Person04_1 {
public:
	/*
		this指针的本质是指针常量,指针的指向是不可以修改的(指向地址的值是可以修改的)!
		Person* const this;

		this = NULL;  // 这是不可以的,指针常量的指向是不可以修改的!

		那么this指针的指向不可以改,我们是否可以限制它指向地址的内容也不允许修改呢?
		当然是可以的,用const修饰成员方法即可,那么const关键字放在哪里?
			1. 放在void前面? —— 修饰的是返回值了,不行
			2. 放在()中? —— 修饰的是形参了,不行
		因此没有办法,放在了()的后面

		> 在成员函数后面加const,修饰的是this指针,让其指向的值也不可以修改
	*/
	void fn_1() {  // 普通的成员方法
		// this = NULL;  // this指针的本质是指针常量,指针的指向是不可以修改的
	}

	void fn_2() {  // 普通的成员方法
		this->a = 100;  // this指针虽然是指针常量,但指向地址的内容可以修改
	}

	void fn_3() const {  // 加了const修饰后,变为常函数,即this既是指针常量也是常量指针,指向和内容都不可以修改!
		// this->a = 100;  // Error: 表达式必须是可修改的左值
	}

	void fn_4() const{  // 常函数
		this->b = 100;
	}

	int a;
	mutable int b;  // 加了mutable关键字后,变为特殊变量,即使在常函数中也可以修改这个值
};


// 常对象
void test04_1() {
	// 在实例化对象前加const,该对象变为常对象
	const Person04_1 p;
	// p.a = 100;  // 表达式必须是可修改的左值
	p.b = 100;  // 可变变量(特殊变量),所以可以修改

	// 常对象只能调用常函数
	p.fn_3();
	p.fn_4();
	// p.fn1();  // 类"Person04_1"没有成员"fn1"
	// p.fn2();  // 类"Person04_1"没有成员"fn2"
	/*
		常对象只能调用常函数,这是因为对于普通成员函数而言,是可以修改普通成员变量的,
		但是常对象本身是不允许修改成员属性的,因此不能调用普通成员函数

		> 在IDE中,自动补全只会显示常函数,不会显示普通成员函数。
	*/
}


int main() {

	test04_1();
	system("pause");
	return 0;
}

4.4 友元(friend)

生活中你的家有客厅(public),有你的卧室(private)。客厅所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去。但是呢,你也可以允许你的好闺蜜好基友进去。

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

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

友元的关键字:friend

友元的三种实现:

  1. 全局函数做友元
  2. 类做友元
  3. 成员函数做友元

语法:

  1. 全局函数做友元:在类中使用friend关键字声明全局函数,friend 返回值类型 全局函数名(形参列表);(跟函数的声明是一样的,只不过前面加个friend关键字)。 —— 例:friend void good_gay(Building& building);
  2. 类做友元:在类中使用friend关键字声明友元类,friend class 友元类名; —— 例:friend class GoodGay;
  3. 成员函数做友元:在类中使用friend关键字声明友元成员函数,friend 类名::成员方法(形参列表); —— 例:friend GoodGay::visit();

注意:

  1. 全局函数做友元必须写完整!
  2. 类做友元只写类名即可。
  3. 成员函数做友元也要写完整!

4.4.1 全局函数做友元

语法:在类中使用friend关键字声明全局函数,friend 返回值类型 全局函数名(形参列表);(跟函数的声明是一样的,只不过前面加个friend关键字)。

语法示例:

class Building {
	// 声明一下:全局变量good_gay是Building类的好朋友,可以访问Building中的私有成员
	friend void good_gay(Building& building);

public:
	string sittingroom;  // 客厅

private:
	string bedroom;  // 卧室
};

示例:

#include <iostream>
using namespace std;
#include <string>


class Building {
	// 声明一下:全局变量good_gay是Building类的好朋友,可以访问Building中的私有成员
	friend void good_gay(Building& building);

public:  // Constructor & Destructor
	Building() {
		sittingroom = "客厅";
		bedroom = "卧室";
	}

	~Building() {}

public:
	string sittingroom;  // 客厅

private:
	string bedroom;  // 卧室
};


// 全局函数
void guest(Building& building) {
	cout << "[全局函数]客人正在访问: " << building.sittingroom << endl;
	// cout << "[全局函数]客人正在访问: " << building.bedroom << endl;  // 不能访问私有属性
}


void good_gay(Building& building) {
	cout << "[全局函数]好朋友正在访问: " << building.sittingroom << endl;
	cout << "[全局函数]好朋友正在访问: " << building.bedroom << endl;
}


void test01_1() {
	Building building;
	guest(building);
	good_gay(building);

	/*
		[全局函数]客人正在访问: 客厅

		[全局函数]好朋友正在访问: 客厅
		[全局函数]好朋友正在访问: 卧室
	*/
}


int main() {

	test01_1();



	system("pause");
	return 0;
}

4.4.2 类做友元

语法:在类中使用friend关键字声明友元类,friend class 友元类名;—— 例:friend class GoodGay;

语法示例:

class Building02 {
	// 声明友元
	friend class GoodGay;

public:  // 设计Constructor对类的属性进行初始化
	Building02();  // 在类内声明一下,一会儿我们在类外进行具体的函数实现

public:
	string sittingroom;  // 客厅

private:
	string bedroom;  // 卧室
};

示例:

#include <iostream>
using namespace std;
#include <string>


// 类做友元
class Building02;  // 类的声明,让编译器先不要报错。
class Guest {
public:
	Guest();  // Constructor的声明

public:
	
	void visit();  // 参观函数,让它访问Building02中的属性

	Building02* building;
};

class Building02 {
	// 声明友元
	friend class GoodGay;

public:  // 设计Constructor对类的属性进行初始化
	Building02();  // 在类内声明一下,一会儿我们在类外进行具体的函数实现

public:
	string sittingroom;  // 客厅

private:
	string bedroom;  // 卧室
};


class GoodGay {
public:
	GoodGay();

public:
	void visit();

	Building02* building;
};


// 在类外写成员函数的具体实现
Building02::Building02() {  
	sittingroom = "客厅";
	bedroom = "卧室";
}

Guest::Guest() {
	// 创建建筑物对象
	building = new Building02;
}

GoodGay::GoodGay()
{
	building = new Building02;
}

void GoodGay::visit()
{
	cout << "[类]客人正在访问: " << building->sittingroom << endl;
	cout << "[类]客人正在访问: " << building->bedroom << endl;  // 友元可以访问私有属性
}

void Guest::visit() {
	cout << "[类]客人正在访问: " << building->sittingroom << endl;
	// cout << "[类]客人正在访问: " << building->bedroom << endl;  // 不可以访问私有属性
}


void test02_1() {
	Guest guest;
	guest.visit();

	GoodGay gg;
	gg.visit();

	/*
		[类]客人正在访问: 客厅
		[类]客人正在访问: 客厅
		[类]客人正在访问: 卧室
	*/
}


int main() {

	test02_1();

	system("pause");
	return 0;
}

4.4.3 成员函数做友元

语法:在类中使用friend关键字声明友元成员函数,friend 类名::成员方法(); —— 例:friend GoodGay::visit();

语法示例:

class Building03 {

	// 声明友元
	// 告诉编译器GoodGay类下的visit_friend成员函数作为本类的好朋友,
	// 可以访问本类的私有属性
	friend void GoodGay03::visit_friend();  

public:
	Building03();  // Constructor声明
	string sittingroom;

private:
	string bedroom;
};
#include <iostream>
using namespace std;
#include <string>


// 成员函数做友元
class Building03;
class GoodGay03 {
public:
	GoodGay03();

	void visit_friend();  // 让visit_friend函数可以访问Building中私有属性
	void visit_norm();  // 让visit_norm函数可以访问Building中私有属性

	Building03* building;
};


class Building03 {

	// 声明友元
	friend void GoodGay03::visit_friend();  // 告诉编译器GoodGay类下的visit_friend成员函数作为本类的好朋友,可以访问本类的私有属性

public:
	Building03();  // Constructor声明
	string sittingroom;

private:
	string bedroom;
};


// 类外实现
Building03::Building03() {  // Constructor
	sittingroom = "客厅";
	bedroom = "卧室";
}

GoodGay03::GoodGay03() {
	building = new Building03;
}

void GoodGay03::visit_norm() {
	cout << "GoodGay03的[成员函数]visit_norm正在访问: " << building->sittingroom << endl;
	// cout << "GoodGay03的[成员函数]visit_norm正在访问: " << building->bedroom << endl;  // 无法访问私有属性
}

void GoodGay03::visit_friend() {
	cout << "GoodGay03的[成员函数]visit_friend正在访问: " << building->sittingroom << endl;
	cout << "GoodGay03的[成员函数]visit_friend正在访问: " << building->bedroom << endl;
}


void test03_1() {
	GoodGay03 gg;
	gg.visit_norm();
	gg.visit_friend();

	/*
		GoodGay03的[成员函数]visit_norm正在访问: 客厅
		GoodGay03的[成员函数]visit_friend正在访问: 客厅
		GoodGay03的[成员函数]visit_friend正在访问: 卧室
	*/
}


int main() {

	test03_1();

	system("pause");
	return 0;
}

4.5 运算符重载

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

4.5.1 加号运算符重载

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

语法有两种:

  1. 成员函数重载+号:返回值类型 operator+(形参列表) {函数代码}
  2. 全局函数重载+号:返回值类型 operator+(形参列表) {函数代码}
  3. 运算符重载的函数重载:返回值类型 operator+(形参列表) {函数代码}

运算符重载也可以发生函数重载,以适应不同的形式。

语法示例:

// 1. 成员函数重载+号
Person01 operator+(Person01& p) {
	Person01 tmp;
	tmp.a = this->a + p.a;
	tmp.b = this->b + p.b;
	return tmp;
}

// 2. 全局函数重载+号
Person01 operator+(Person01& p1, Person01& p2) {
	Person01 tmp;
	tmp.a = p1.a + p2.a;
	tmp.b = p1.b + p2.b;
	return tmp;
}

// 3. 运算符重载的函数重载
Person01 operator+(Person01& p1, int num) {
	Person01 tmp;
	tmp.a = p1.a + num;
	tmp.b = p1.b + num;
	return tmp;
}

因为加号运算符的函数名就叫operator+,所以我们可以直接使用 + 而不用再调用函数那么麻烦了。

重载运算符的本质

  1. 成员函数重载+号的本质:Person01 p3 = p1 + p2; 等价于 Person01 p3 = p1.operator+(p2);
  2. 全局函数重载+号的本质:Person01 p3 = p1 + p2; 等价于 Person01 p3 = operator+(p1, p2);

注意:①成员函数重载+号和②全局函数重载+号不能都写了,因为都写了不满足函数重载的条件:[1]参数类型不同 || [2]参数个数不同 || [3]参数顺序不同。 —— 有多个运算符"+”与这些操作数匹配。

这里应该是成员函数会被编译器转换,所以二者会重复。

总结

  1. 对于内置的数据类型(int/double/float...)的表达式的运算符是不可能改变的(我们上面说的都是自定义的数据类型)
  2. 不要滥用运算符重载

代码示例:

#include <iostream>
using namespace std;
#include <string>


/*
	加号运算符重载:
		1. 成员函数重载+号
		2. 全局函数重载+号
*/
class Person01 {
public:

	// 1. 成员函数重载+号
	//Person01 operator+(Person01& p) {
	//	Person01 tmp;
	//	tmp.a = this->a + p.a;
	//	tmp.b = this->b + p.b;
	//	return tmp;
	//}

	int a;
	int b;
};


// 2. 全局函数重载+号
Person01 operator+(Person01& p1, Person01& p2) {
	Person01 tmp;
	tmp.a = p1.a + p2.a;
	tmp.b = p1.b + p2.b;
	return tmp;
}


// 运算符重载也可以发生函数重载
Person01 operator+(Person01& p1, int num) {
	Person01 tmp;
	tmp.a = p1.a + num;
	tmp.b = p1.b + num;
	return tmp;
}


void test01_1() {
	Person01 p1;
	p1.a = 10;
	p1.b = 20;

	Person01 p2;
	p2.a = 100;
	p2.b = 200;

	// 不对加号运算符进行重载时,会报错
	// Person01 p3 = p1 + p2;  // 没有与这些操作数匹配的"+"运算符
	
	// 1. 成员函数重载 + 号 & 2. 全局函数重载+号
	Person01 p3 = p1 + p2;
	cout << "p3.a: " << p3.a << "\tp3.b: " << p3.b << endl;  // p3.a: 110       p3.b: 220

	/*
		1. 成员函数重载+号的本质:
			Person01 p3 = p1 + p2; 等价于 Person01 p3 = p1.operator+(p2);

		2. 全局函数重载+号的本质:
			Person01 p3 = p1 + p2; 等价于 Person01 p3 = operator+(p1, p2);

		因为加号运算符的函数名就叫operator+,所以我们可以直接使用 + 而不用再调用函数那么麻烦了

		3. 运算符重载也可以发生函数重载

		> 注意:①成员函数重载+号和②全局函数重载+号不能都写了,因为都写了不满足函数重载的条件:1. 参数**类型不同** || 2. 参数**个数不同** || 3. 参数**顺序不同**。 —— 有多个运算符"+”与这些操作数匹配。
		> 这里应该是成员函数会被编译器转换,所以二者会重复
	*/

	// 运算符重载也可以发生函数重载
	Person01 p4 = p1 + 10;  //没有与这些操作数匹配的"+"运算符
	// 加了运算符重载函数后,就不会报错了!
	cout << "p4.a: " << p4.a << "\tp4.b: " << p4.b << endl;  // p4.a: 20        p4.b: 30
}


int main() {

	test01_1();

	system("pause");
	return 0;
}

4.5.2 左移运算符 << 的重载

左移运算符就是<<

重载<<的作用:可以输出自定义数据类型。

语法:ostream& operator<<(ostream& cout, 其他数据类型 变量名) {}

语法示例:

ostream& operator<<(ostream& cout, Person02 p) {  // 本质 operator<< (cout, p) 简化为 cout << p
	cout << "a: " << p.a << "\tb: " << p.b;
	return cout;
}

注意:

  1. 通常情况下,我们不会利用成员函数重载<<运算符,因为无法实现cout在左侧! —— 只能利用全局函数重载<<运算符。
  2. cout的数据类型是ostream(可以ctrl+左键看一下cout的定义)。
  3. 在使用cout时,一般会使用链式编程,所以重载<<运算符时应该返回cout的数据类型(即ostream数据类型)。
  4. 重载<<时,我们可以会用到类的private属性,因此需要配合友元(friend)关键字使用。
int a = 10;
cout << a << endl;  // 10

Person p;
p.a = 10;
p.b = 20;
cout << p << endl;  // // 没有与这些操作数匹配的"<<"运算符

// 我们想直接输出p.a和p.b,该怎么做? -> 重载左移运算符
#include <iostream>
using namespace std;
#include <string>


// 左移运算符重载
class Person02 {
	// 添加友元
	friend ostream& operator<<(ostream& cout, Person02 p);

public:
	// 利用成员函数重载左移运算符 p.operator<<(cout) 简化版本 p << cout
	// 通常情况下,我们不会利用成员函数重载<<运算符,因为无法实现cout在左侧! —— 只能利用全局函数重载<<运算符
	// void operator<<(cout) {}

public:
	Person02(int a, int b) {
		this->a = a;
		this->b = b;
	}

private:
	int a;
	int b;
};


ostream& operator<<(ostream& cout, Person02 p) {  // 本质 operator<< (cout, p) 简化为 cout << p
	cout << "a: " << p.a << "\tb: " << p.b;
	return cout;
}


void test02_1() {
	Person02 p(10, 20);

	// cout << p << endl;  // 没有与这些操作数匹配的"<<"运算符

	// 重载<<运算符之后
	cout << p << endl;  // 这种是链式编程,必须返回对象后才能无限连

	// 本质
	operator<<(cout, p) << endl;  // a: 10   b: 20
}


int main() {

	test02_1();

	system("pause");
	return 0;
}

4.5.3 递增运算符(++)重载

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

递增运算符++的位置不同,起到的效果也不同:

  1. 前置递增
  2. 后置递增
// 前置递增
int a = 10;
cout << ++a << endl;  // 11

// 后置递增
int b = 10;
cout << b++ << endl;  // 10
cout << b << endl;  // 11

注意

  1. 前置递增:重载时,返回值类型需加上引用&
  2. 后置递增:
    1. 重载时,返回值类型不用加上引用&
    2. 形参必须写int占位符

返回引用的目的是实现一直对一个对象进行操作。
而返回值并不是我们想象中的那样,返回一个数字,而是由函数返回值的数据类型决定的,可能是int/double/float/long...,也可能是一个类(class)。

原因

  1. 前置递增:

    • 如果返回值类型不是引用,会copy一份儿,那操作的对象就会变了。
    • 那么就会发生一个问题:
      • cout << ++my_int << endl; // 1
      • cout << ++my_int << endl; // 1(还是1,因为不是原对象的)
  2. 后置递增:

    1. 如果不写(int)那么编译器会认为[前置递增重载函数]和[后置递增重载函数]发生了重定义(不满足函数重载,所以会引发二义性)
      1. int是一个占位参数,目的:
        1. 满足函数重载的条件;
        2. 可以用于区分前置和后置。
    2. 前置递增返回的是引用,但后置递增返回的是(也是一个数据类型)。
      1. 因为如果我们返回的是引用,返回的是[临时变量]的引用,那么后续的操作就是非法的([临时变量]会被编译器自动销毁)
      2. 因为我们返回的值,这个值的数据类型是一个类,所以还是可以继续实现链式编程的
      3. 但这样也会带来一个问题:我们无法先前置递增那样,可以无限前置递增:++(++my_int)。当使用(my_int++)++时,由于返回的是值,不是一直操作同一个对象,所以(my_int++)++ 等价于 my_int++
      4. 但这其实并不是一个问题,我们看一下下面的代码:
      int a = 10;
      cout << a++ << endl;  // 10
      cout << a << endl;  // 11
      // cout << (a++)++ << endl;  // 表达式必须是可修改的左值
      // cout << a << endl;
      
      我们可以发现,在C++原生代码中,后置++就是不可以无限套娃的,直接会提示语法错误,所以我们写的代码没有问题!

总结:前置递增返回引用,后置递增返回值。

代码示例:

#include <iostream>
using namespace std;
#include <string>


// 自定义整型
class MyInteger {
	// 声明友元
	friend ostream& operator<<(ostream& cout, MyInteger my_int);

public:
	MyInteger() {
		this->num = 0;
	}

	// 重载前置++运算符
	MyInteger& operator++() {
		/*
			如果返回值类型不是引用,会copy一份儿,那操作的对象就会变了。
			那么就会发生一个问题:
				cout << ++my_int << endl;  // 1
				cout << ++my_int << endl;  // 1(还是1,因为不是原对象的)

			返回引用的目的是实现一直对一个对象进行操作
		*/
		// 先进行++运算
		++this->num;

		// 再将返回自身以满足链式编程
		return *this;
	}


	// 重载后置++运算符
	/*
		如果不写(int)那么编译器会认为两个函数发生了重定义(不满足函数重载,所以会引发二义性)
		int是一个占位参数,目的:①满足函数重载的条件;②可以用于区分前置和后置

		前置递增返回的是引用,但后置递增返回的是值。
			因为如果我们返回的是引用,返回的是tmp的引用,那么后续的操作就是非法的(tmp会被编译器自动销毁)

			因为我们返回的值,这个值的数据类型是一个类,所以还是可以继续实现链式编程的
			但这样也会带来一个问题:
				我们无法先前置递增那样,可以无限前置递增:++(++my_int)
				当使用(my_int++)++时,由于返回的是值,不是一直操作同一个对象,所以(my_int++)++ 等价于 my_int++

			但这其实并不是一个问题,我们看一下下面的代码:
				int a = 10;
				cout << a++ << endl;  // 10
				cout << a << endl;  // 11
				// cout << (a++)++ << endl;  // 表达式必须是可修改的左值
				// cout << a << endl;
			我们可以发现,在C++原生代码中,后者++就是不可以无限套娃的,直接会提示语法错误,所以我们写的代码没有问题!
				
	*/
	MyInteger operator++(int) {
		// 先 记录当时的结果
		MyInteger tmp = *this;

		// 后 递增
		num++;

		// 最后 将记录的结果做返回操作
		return tmp;
	}

private:
	int num;
};


// 重载左移运算符
ostream& operator<<(ostream& cout, MyInteger my_int) {
	cout << my_int.num;
	return cout;
}


void test03_1() {
	MyInteger my_int;

	// 重载<<运算符之前
	// cout << my_int << endl;  // 没有与这些操作数匹配的"<<"运算符

	// 重载<<运算符之后
	cout << my_int << endl;  // 0

	// 重载前置++运算符之前
	// cout << ++my_int << endl;  // 没有与这些操作数匹配的"++"运算符

	// 重载前置++运算符之后
	cout << ++(++my_int) << endl;  // 2

	// 重载后置++运算符之前
	// cout << my_int++ << endl;  // 没有与这些操作数匹配的"++"运算符

	// 重载后置++运算符之后
	cout << my_int++ << endl;  // 2
	cout << my_int << endl;  // 3

	cout << (my_int++)++ << endl;  // 3
	cout << my_int << endl;  // 4(不是我们预想的5,因为返回的是值而不是引用,不是同一个对象)
}


int main() {

	test03_1();

	int a = 10;
	cout << a++ << endl;  // 10
	cout << a << endl;  // 11
	// cout << (a++)++ << endl;  // 表达式必须是可修改的左值
	// cout << a << endl;

	system("pause");
	return 0;
}

递增运算符和递减运算符重载代码:

#include <iostream>
using namespace std;
#include <string>


class MyInteger {
	// 声明友元
	friend ostream& operator<<(ostream& cout, MyInteger my_int);

public:
	// 1.1 重载前置++运算符
	MyInteger& operator++() {
		++this->num;
		return *this;
	}

	// 1.2 重载后置++运算符
	MyInteger operator++(int) {
		MyInteger tmp = *this;
		this->num++;
		return tmp;
	}

	// 2.1 重载前置--运算符
	MyInteger& operator--() {
		--this->num;
		return *this;
	}

	// 2.2 重载后置--运算符
	MyInteger operator--(int) {
		MyInteger tmp = *this;
		this->num--;
		return tmp;
	}

public:
	MyInteger() {
		this->num = 0;
	}

private:
	int num;
};


// 重载<<运算符
ostream& operator<<(ostream& cout, MyInteger my_int) {
	cout << my_int.num;
	return cout;
}


void test_plus_plus() {
	MyInteger my_int;

	cout << "---------重载++运算符---------" << endl;

	// 重载<<运算符
	cout << my_int << endl;  // 0

	// 1.1 重载前置++运算符
	cout << ++my_int << endl;  // 1
	cout << ++++my_int << endl;  // 3
	cout << my_int << endl;  // 3

	// 1.2 重载后置++运算符
	cout << my_int++ << endl;  // 3
	cout << my_int++++ << endl;  // 4
	cout << my_int << endl;  // 5
}


void test_sub_sub() {
	MyInteger my_int;

	cout << "---------重载--运算符---------" << endl;

	// 重载<<运算符
	cout << my_int << endl;  // 0

	// 2.1 重载前置--运算符
	cout << --my_int << endl;  // -1
	cout << ----my_int << endl;  // -3
	cout << my_int << endl;  // -3

	// 2.2 重载后置--运算符
	cout << my_int-- << endl;  // -3
	cout << my_int---- << endl;  // -4
	cout << my_int << endl;  // -5
}


int main() {

	test_plus_plus();
	test_sub_sub();

	system("pause");
	return 0;
}

4.5.4 赋值运算符=重载

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

  1. 默认构造函数Constructor(无参,函数体为空)
  2. 默认析构函数Destructor(无参,函数体为空)
  3. 默认拷贝构造函数,对属性进行值拷贝。需要注意的是,编译器提供的浅拷贝而非深拷贝,因此可能会引发一些问题,典型是重复释放相同的内存
  4. 赋值运算符operator=,对属性进行值拷贝

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

赋值运算符是=,千万不要写成==

语法示意:


// 一定要返回自身的引用,不要返回自身的值
Person04& operator=(Person04& p) { 
	// 编译器提供的是浅拷贝: this->age = p.age;

	// 应该先判断是否有属性在堆区,如果有先释放干净,然后再深拷贝
	if (this->age != NULL)
	{
		delete this->age;
		this->age = NULL;
	}

	// 深拷贝
	this->age = new int(*p.getter_age());

	// 返回自身
	return *this;
}

示例:

#include <iostream>
using namespace std;
#include <string>


class Person04 {
public:
	// Constructor
	Person04(int age) {
		this->age = new int(age);  // new返回的是一个地址(在堆区)
	}

	// Destructor
	~Person04() {
		if (this->age != NULL) {
			delete this->age;
			this->age = NULL;
		}
		cout << "Person04的Destructor调用" << endl;
	}

	// 重载 赋值运算符
	Person04& operator=(Person04& p) {  // 一定要返回自身的引用,不要返回自身的值
		// 编译器提供的是浅拷贝: this->age = p.age;

		// 应该先判断是否有属性在堆区,如果有先释放干净,然后再深拷贝
		if (this->age != NULL)
		{
			delete this->age;
			this->age = NULL;
		}

		// 深拷贝
		this->age = new int(*p.getter_age());

		// 返回自身
		return *this;
	}

public:
	int* getter_age() {
		return this->age;
	}

private:
	int* age;  // 创建一个指针
};


void test04_1() {
	Person04 p1(18);
	Person04 p2(20);

	cout << "p1.age: " << *p1.getter_age() << endl;  // p1.age: 18
	cout << "p2.age: " << *p2.getter_age() << endl;  // p2.age: 20

	/*
		 赋值操作 -> 堆区内存重复释放,程序崩溃!

		 解决方案:利用DeepCopy解决浅拷贝带来的问题(在使用=运算符时,各自开辟空间,Destructor时各自释放各自的)
	*/
	p2 = p1;  // 赋值操作
	cout << "p1.age: " << *p1.getter_age() << endl;  // p1.age: 18
	cout << "p2.age: " << *p2.getter_age() << endl;  // p2.age: 18
}


void test04_2() {
	Person04 p1(18);
	Person04 p2(20);
	Person04 p3(30);

	/*
		从main函数中 c = b = a的操作中可以看到,是可以连等于的,
		但我们现在写的会报错,这是因为我们重载赋值运算符时,
		返回的是void,所以不能链式编程

		解决方案:返回自身就好了
	*/
	// p3 = p2 = p1;  // ERROR:没有与这些操作数匹配的"="运算符

	// 重载赋值运算符时返回自身后
	p3 = p2 = p1;
	cout << "p1.age: " << *p1.getter_age() << endl;  // p1.age: 18
	cout << "p2.age: " << *p2.getter_age() << endl;  // p2.age: 18
	cout << "p3.age: " << *p3.getter_age() << endl;  // p3.age: 18
}


int main() {
	cout << "-------------test04_1-------------" << endl;
	test04_1();

	int a = 10;
	int b = 20;
	int c = 30;

	cout << "-------------c = b = a-------------" << endl;
	c = b = a;
	cout << "a: " << a << endl;  // a: 10
	cout << "b: " << b << endl;  // b: 10
	cout << "c: " << c << endl;  // c: 10

	cout << "-------------test04_2-------------" << endl;
	test04_2();

	/*
		-------------test04_1-------------
		p1.age: 18
		p2.age: 20
		p1.age: 18
		p2.age: 18
		Person04的Destructor调用
		Person04的Destructor调用
		-------------c = b = a-------------
		a: 10
		b: 10
		c: 10
		-------------test04_2-------------
		p1.age: 18
		p2.age: 18
		p3.age: 18
		Person04的Destructor调用
		Person04的Destructor调用
		Person04的Destructor调用
	*/

	system("pause");
	return 0;
}

4.5.5 关系运算符重载

系统默认的数据类型,比如int,我们可以知道变量之间的大小关系,但是对于自定义数据类型,在对比的时候编译器不知道怎么对比。基于这个场景,我们需要重载关系运算符。

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

代码示意:

// 重载关系运算符==
bool operator==(Person05& p) {
	if (this->name == p.getter_name() && this->age == p.getter_age())
	{
		return true;
	}
	else
	{
		return false;
	}
}

示例:

#include <iostream>
using namespace std;
#include <string>


// 重载关系运算符
class Person05 {
public:
	Person05(string name, int age) {
		this->name = name;
		this->age = age;
	}

	// 重载关系运算符==
	bool operator==(Person05& p) {
		if (this->name == p.getter_name() && this->age == p.getter_age())
		{
			return true;
		}
		else
		{
			return false;
		}
	}

	// 重载关系运算符!=
	bool operator!=(Person05& p) {
		if (this->name != p.getter_name() || this->age != p.getter_age())
		{
			return true;
		}
		else
		{
			return false;
		}
	}

	// 重载关系运算符<
	bool operator<(Person05& p) {
		if (this->age < p.getter_age())
		{
			return true;
		}
		else
		{
			return false;
		}
	}

	// 重载关系运算符<=
	bool operator<=(Person05& p) {
		if (this->age <= p.getter_age())
		{
			return true;
		}
		else
		{
			return false;
		}
	}

	// 重载关系运算符>
	bool operator>(Person05& p) {
		if (this->age > p.getter_age())
		{
			return true;
		}
		else
		{
			return false;
		}
	}

	// 重载关系运算符>=
	bool operator>=(Person05& p) {
		if (this->age >= p.getter_age())
		{
			return true;
		}
		else
		{
			return false;
		}
	}

public:  // getter
	string getter_name() {
		return this->name;
	}

	int getter_age() {
		return this->age;
	}
	
private:
	string name;
	int age;
};


void test05_1() {
	Person05 p1("Tom", 18);
	Person05 p2("Tom", 18);
	Person05 p3("Jerry", 16);

	// 重载==之前
	//if (p1 == p2)  // Error: 没有与这些操作数匹配的"=="运算符
	//{
	//	cout << "p1和p2是相等的" << endl;
	//}

	// 重载==之后
	if (p1 == p2)
	{
		cout << "p1和p2是 相等 的" << endl;  // p1和p2是相等的
	}
	else
	{
		cout << "p1和p2是 不相等 的" << endl;
	}

	// 重载!=之后
	if (p1 != p3)
	{
		cout << "p1和p2是 不相等 的" << endl;  // p1和p2是 不相等 的
	}
	else
	{
		cout << "p1和p2是 相等 的" << endl;
	}

	// 重载<之后
	if (p1 < p3)
	{
		cout << "p1 < p2" << endl;
	}
	else
	{
		cout << "p1 >= p2" << endl;  // p1 >= p2
	}

	// 重载<=之后
	if (p1 <= p3)
	{
		cout << "p1 <= p2" << endl;
	}
	else
	{
		cout << "p1 > p2" << endl;  // p1 > p2
	}

	// 重载>之后
	if (p1 > p3)
	{
		cout << "p1 > p2" << endl;  // p1 > p2
	}
	else
	{
		cout << "p1 <= p2" << endl;
	}

	// 重载>=之后
	if (p1 >= p3)
	{
		cout << "p1 >= p2" << endl;  // p1 >= p2
	}
	else
	{
		cout << "p1 < p2" << endl;
	}
}


int main() {

	test05_1();

	system("pause");
	return 0;
}

4.5.6 函数调用运算符()重载 -> 小括号的重载

  • 函数调用运算符()也可以重载
  • 由于重载后使用的方式非常像函数的调用,因此称为仿函数
  • 仿函数没有固定写法,非常灵活

示例:

#include <iostream>
using namespace std;
#include <string>


// 函数调用运算符重载

// 打印输出类
class MyPrint {
public:
	// 重载函数调用运算符
	void operator()(string test) {
		cout << test << endl;
	}
};


void my_print(string test) {  // 全局函数
	cout << test << endl;
}


void test06_1() {
	MyPrint mp;

	// 这特么不就是Python的print函数吗?
	/*
		由于使用起来非常类似于函数调用,因此成为仿函数
	*/
	mp("Hello World!");  // Hello World!

	my_print("Hello World!");  // Hello World!
}


// 仿函数非常灵活,没有固定的写法
// 加法类
class MyAdd {
public:
	int operator()(int num1, int num2) {
		return num1 + num2;
	}
};


void test06_2() {
	MyAdd madd;
	int res = madd(100, 200);
	cout << "res: " << res << endl;  // res: 300


	// 匿名函数对象:MyAdd()就是一个匿名对象,这行语句执行完毕后就被释放
	cout << MyAdd()(100, 200) << endl;  // 300
}


int main() {

	test06_1();

	test06_2();

	system("pause");
	return 0;
}

4.6 继承

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

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

动物
其他动物
加菲猫
布偶猫
波斯猫
...
哈士奇
京巴
德国牧羊犬
....

我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。这个时候我们就可以考虑利用继承的技术,减少重复代码

4.6.1 继承的基本语法

例如我们看到很多网站中,都有公共的头部,公共的底部,甚至公共的左侧列表,只有中心内容不同。

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

继承的语法的实例:

class BaseClass{
public:
	xxxxxx;
};

class ChildClass : public BaseClass {
	子类的代码(不重写父类的代码默认会继承父类的代码);
};
  • 子类也称为派生类
  • 父类也称为基类

接下来我们分别利用普通写法继承的写法来实现网页中的内容,看一下继承存在的意义以及好处。

普通实现:

#include <iostream>
#include <string>
using namespace std;


// 普通实现页面
class Java {
public:
	void header() {
		cout << "[公共头部]首页、公开课、登陆、注册..." << endl;
	}

	void footer() {
		cout << "[公共底部]帮助中心、交流合作、站内地图..." << endl;
	}

	void left() {
		cout << "[公共分类列表]Java、Python、C++..." << endl;
	}

	void content() {
		cout << "Java学科视频" << endl;
	}

	// 重载()
	void operator()() {
		cout << "---------Java下网站页面如下---------:" << endl;
		this->header();
		this->footer();
		this->left();
		this->content();
		cout << endl;
	}
};


class Python {
public:
	void header() {
		cout << "[公共头部]首页、公开课、登陆、注册..." << endl;
	}

	void footer() {
		cout << "[公共底部]帮助中心、交流合作、站内地图..." << endl;
	}

	void left() {
		cout << "[公共分类列表]Java、Python、C++..." << endl;
	}

	void content() {
		cout << "Python学科视频" << endl;
	}

	// 重载()
	void operator()() {
		cout << "---------Python下网站页面如下---------:" << endl;
		this->header();
		this->footer();
		this->left();
		this->content();
		cout << endl;
	}
};


class CPP {
public:
	void header() {
		cout << "[公共头部]首页、公开课、登陆、注册..." << endl;
	}

	void footer() {
		cout << "[公共底部]帮助中心、交流合作、站内地图..." << endl;
	}

	void left() {
		cout << "[公共分类列表]Java、Python、C++..." << endl;
	}

	void content() {
		cout << "C++学科视频" << endl;
	}

	// 重载()
	void operator()() {
		cout << "---------C++下网站页面如下---------:" << endl;
		this->header();
		this->footer();
		this->left();
		this->content();
		cout << endl;
	}
};


void test01() {
	Java java;
	java();

	Python python;
	python();

	CPP cpp;
	cpp();

	/*
		---------Java下网站页面如下---------:
		[公共头部]首页、公开课、登陆、注册...
		[公共底部]帮助中心、交流合作、站内地图...
		[公共分类列表]Java、Python、C++...
		Java学科视频

		---------Python下网站页面如下---------:
		[公共头部]首页、公开课、登陆、注册...
		[公共底部]帮助中心、交流合作、站内地图...
		[公共分类列表]Java、Python、C++...
		Python学科视频

		---------C++下网站页面如下---------:
		[公共头部]首页、公开课、登陆、注册...
		[公共底部]帮助中心、交流合作、站内地图...
		[公共分类列表]Java、Python、C++...
		C++学科视频
	*/
}


int main() {

	test01();

	system("pause");
	return 0;
}

可以发现,当我们在写Java类、Python类、CPP类时,有太多重复的代码了。因此这样的代码是不规范的,我们应该是继承来提高代码的复用率!

继承实现代码:

#include <iostream>
#include <string>
using namespace std;


// 继承实现页面
class BasePage {  // 公共页面类
public:  // 公共的信息
	void header() {
		cout << "[公共头部]首页、公开课、登陆、注册..." << endl;
	}

	void footer() {
		cout << "[公共底部]帮助中心、交流合作、站内地图..." << endl;
	}

	void left() {
		cout << "[公共分类列表]Java、Python、C++..." << endl;
	}
};

// Java页面
class Java : public BasePage {
public:
	void content() {
		cout << "Java学科视频" << endl;
	}
	// 重载()
	void operator()() {
		cout << "---------Java下网站页面如下---------:" << endl;
		this->header();
		this->footer();
		this->left();
		this->content();
		cout << endl;
	}
};

// Python页面
class Python : public BasePage {
public:
	void content() {
		cout << "Python学科视频" << endl;
	}
	// 重载()
	void operator()() {
		cout << "---------Python下网站页面如下---------:" << endl;
		this->header();
		this->footer();
		this->left();
		this->content();
		cout << endl;
	}
};

// C++页面
class CPP : public BasePage {
public:
	void content() {
		cout << "C++学科视频" << endl;
	}
	// 重载()
	void operator()() {
		cout << "---------C++下网站页面如下---------:" << endl;
		this->header();
		this->footer();
		this->left();
		this->content();
		cout << endl;
	}
};


void test01() {
	// 使用匿名对象
	Java()();
	Python()();
	CPP()();

	/*
		---------Java下网站页面如下---------:
		[公共头部]首页、公开课、登陆、注册...
		[公共底部]帮助中心、交流合作、站内地图...
		[公共分类列表]Java、Python、C++...
		Java学科视频

		---------Python下网站页面如下---------:
		[公共头部]首页、公开课、登陆、注册...
		[公共底部]帮助中心、交流合作、站内地图...
		[公共分类列表]Java、Python、C++...
		Python学科视频

		---------C++下网站页面如下---------:
		[公共头部]首页、公开课、登陆、注册...
		[公共底部]帮助中心、交流合作、站内地图...
		[公共分类列表]Java、Python、C++...
		C++学科视频
	*/
}


int main() {

	test01();

	system("pause");
	return 0;
}

4.6.2 继承方式

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

继承方式一共有三种:

  1. 公共继承 —— public
  2. 保护继承 —— protected
  3. 私有继承 —— private

通过继承,父类的属性在子类中权限的变化如下:

在这里插入图片描述

  1. 公共继承 —— public:访问不到private,剩下的不变
  2. 保护继承 —— protected:访问不到private,剩下的都变为protected
  3. 私有继承 —— private:访问不到private,剩下的都变为private

代码示例:

#include <iostream>
#include <string>
using namespace std;


/*
	1. 公共继承 —— `public`:访问不到`private`,剩下的不变
	2. 保护继承 —— `protected`:访问不到`private`,剩下的都变为`protected`
	3. 私有继承 —— `private`:访问不到`private`,剩下的都变为`private`
*/


// 创建父类
class BaseClass {
public:
	int a;
protected:
	int b;
private:
	int c;
};


// 1. 公共继承
class Son1 : public BaseClass {
public:
	void test() {
		this->a = 10;  // 父类中的public权限拿到手了
		this->b = 20;  // 父类中的protected权限拿到手了
		// this->c = 30;  // 父类中的private权限拿不到!
	}
};


// 2. 保护继承
class Son2 : protected BaseClass {
	void test() {
		this->a = 10;  // 父类中的public权限拿到手了
		this->b = 20;  // 父类中的protected权限拿到手了
		// this->c = 30;  // 父类中的private权限拿不到!
	}
};


// 3. 私有继承
class Son3 : private BaseClass {
	void test() {
		this->a = 10;  // 父类中的public权限拿到手了
		this->b = 20;  // 父类中的protected权限拿到手了
		// this->c = 30;  // 父类中的private权限拿不到!
	}
};

// 3.1 再创建一个子类,继承Son3
class GrandSon : public Son3 {
	void test() {
		// this->a = 10;  // 拿不到,说明是private权限
		// this->b = 20;  // 拿不到,说明是private权限
		// this->c = 30;  // 拿不到,说明是private权限

		/*
			通过GrandSon继承Son3,说明Son3在私有继承父类时,将父类的属性的权限
			改为了private!
		*/
	}
};


void test02_1() {
	// 1. 公共继承
	Son1 s1;
	s1.a = 100;
	// s1.b = 200;  // 拿不到,既不是public权限,也不是private权限,那么就一定是protected权限!


	// 2. 保护继承
	Son2 s2;
	// s2.a = 100;  // 拿不到,既不是public权限,也不是private权限,那么就一定是protected权限!
	// s2.b = 200;  // 拿拿不到,既不是public权限,也不是private权限,那么就一定是protected权限!

	// 3. 私有继承
	Son3 s3;
	// s3.a = 100;  // 拿不到,说明可能是protected也可能是private
	// s3.b = 200;  // 拿不到,说明可能是protected也可能是private
	// 看3.1,看看在子类内是否可以访问,如果可以说明是protected权限,如果不可以,说明是private权限
}


int main() {

	system("pause");
	return 0;
}

4.6.3 继承中的对象模型

问题:从父类继承过来的成员,哪些属于子类对象中?

结论:父类中所有非静态成员属性都会被子类继承下去。父类中私有的成员属性是被编译器隐藏了,因此子类无法访问,但确实被子类继承下去了。

利用开发人员命令提示工具(Developer Command Prompt for VS 2022)查看对象模型:

  1. 跳转盘符:C:
  2. 跳转文件路径: cd 具体路径
  3. 查看命令: cl /d1 reportSingleClassLayout类名 文件名.cpp

结果如下:

03 继承中的对象模型.cpp

class Son       size(16):
        +---
 0      | +--- (base class Base)
 0      | | a
 4      | | b
 8      | | c
        | +---
12      | d
        +---

示例:

#include <iostream>
#include <string>
using namespace std;


// 继承中的对象模型
class Base {
public:
	int a;
protected:
	int b;
private:
	int c;
};


class Son : public Base {
public:
	int d;
};


void test03_1() {
	cout << "sizeof(Son): " << sizeof(Son) << "字节" << endl;  // sizeof(Son): 16字节
}


int main() {

	test03_1();

	system("pause");
	return 0;
}

结论:父类中私有成员也是被子类继承下去了,只是由编译器给隐藏后访问不到。

4.6.4 继承中构造Constructor和析构Destructor顺序

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

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

回答:继承中的构造和析构顺序如下:

  • 先构造父类,再构造子类
  • 析构的顺序与构造的顺序相反 -> 先析构子类,再析构父类

示例:

#include <iostream>
#include <string>
using namespace std;


// 继承中的构造和析构顺序
class Base04 {
public:
	Base04() {
		cout << "Base04的构造Constructor函数" << endl;
	}

	~Base04() {
		cout << "Base04的析构Destructor函数" << endl;
	}
};


class Son04 : public Base04 {
public:
	Son04() {
		cout << "Son04的构造Constructor函数" << endl;
	}

	~Son04() {
		cout << "Son04的析构Destructor函数" << endl;
	}
};


void test04_1() {
	cout << "---------test04_1-----------" << endl;
	Base04 base;

	/*
		Base04的构造Constructor函数
		Base04的析构Destructor函数
	*/
}


void test04_2() {
	cout << "---------test04_2-----------" << endl;
	Son04 son;

	/*
		Base04的构造Constructor函数
		Son04的构造Constructor函数
		Son04的析构Destructor函数
		Base04的析构Destructor函数
	*/
}


int main() {

	test04_1();

	test04_2();

	system("pause");
	return 0;
}

4.6.5 继承同名成员处理方式

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

  • 访问子类同名成员直接访问即可: 子类.成员函数()/成员属性
  • 访问父类同名成员需要加作用域: 子类.父类::成员函数()/成员属性

注意:如果子类中出现和父类同名的成员函数,那么子类的同名成员函数会隐藏掉所有父类的同名成员函数,即不能直接访问,想访问需要加作用域。

总结:

  1. 子类对象可以直接访问到子类中同名成员
  2. 子类对象加作用域可以访问到父类同名成员
  3. 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数

直接访问的是自身的属性,访问父类就加上作用域。

示例:

#include <iostream>
#include <string>
using namespace std;


// 继承中同名成员的处理
class Base05 {
public:
	Base05() {
		this->a = 100;
	}

	void func() {
		cout << "父类的func函数调用" << endl;
	}

	void func(int a) {  // 函数重载
		cout << "父类的func(int a)函数调用" << endl;
	}

	int a;
};


class Son05 : public Base05 {
public:

	Son05() {
		this->a = 200;
	}

	void func() {
		cout << "子类的func函数调用" << endl;
	}

	int a;
};


// 成员属性的处理
void test05_1() {
	Son05 son;
	cout << "子类的a: " << son.a << endl;  // a: 子类的a: 200
	cout << "父类的a: " << son.Base05::a << endl;  // 父类的a: 100
}


// 成员函数的处理
void test05_2() {
	Son05 son;
	son.func();  // 子类的func函数调用
	son.Base05::func();  // 父类的func函数调用

	// 如果子类中出现和父类同名的成员函数,那么子类的同名成员函数会
	// 隐藏掉所有父类的同名成员函数 -> 不能直接访问,想访问需要加作用域
	// son.func(100);  // 函数调用中的参数太多
	son.Base05::func(100);  // 父类的func(int a)函数调用
}


int main() {

	test05_1();
	test05_2();

	system("pause");
	return 0;
}

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

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

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

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

总结:同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象和通过类名)

可以通过匿名对象来调用!

示例:

#include <iostream>
#include <string>
using namespace std;

/*
	继承中同名静态成员的处理方式:
		子类出现和父类同名的静态成员函数,	和普通成员函数一样,
		编译器会为子类隐藏父类的同名静态成员函数。
		如果想访问父类中被隐藏的同名成员函数,需要加作用域!
*/
class Base06 {
public:
	static int a;  // 需要在类外初始化

	static void func() {
		cout << "[Base06]static void func()的调用" << endl;
	}
};
int Base06::a = 100;


class Son06 : public Base06 {
public:
	static int a;

	static void func() {
		cout << "[Son06]static void func()的调用" << endl;
	}
};
int Son06::a = 200;


// 同名静态成员属性
void test06_1() {
	cout << "=========1. 同名静态成员属性===========" << endl;
	// 访问方式1:通过对象访问静态数据
	cout << "--------访问方式1:通过对象访问静态数据--------" << endl;
	Son06 son;
	cout << "[Son] a: " << son.a << endl;  // [Son] a: 200
	cout << "[Base] a: " << son.Base06::a << endl;  // [Base] a: 100


	// 访问方式2:通过类名访问静态数据
	cout << "--------访问方式2:通过类名访问静态数据--------" << endl;
	cout << "[Son] a: " << Son06::a << endl;  // [Son] a: 200
	cout << "[Base] a: " << Son06::Base06::a << endl;  // [Base] a: 100


	// 访问方式3:通过匿名对象访问静态数据
	cout << "--------访问方式3:通过匿名对象访问静态数据--------" << endl;
	cout << "[Son] a: " << Son06().a << endl;  // [Son] a: 200
	cout << "[Base] a: " << Son06().Base06::a << endl;  // [Base] a: 100
	cout << "[Base] a: " << Son06::Base06().a << endl;  // [Base] a: 100
}

// 同名静态成员函数
void test06_2() {
	cout << "\r\n=========2. 同名静态成员函数===========" << endl;
	// 访问方式1:通过对象访问静态成员函数
	cout << "--------访问方式1:通过对象访问静态数据--------" << endl;
	Son06 son;
	son.func();  // [Son06]static void func()的调用
	son.Base06::func();  // [Base06]static void func()的调用


	// 访问方式2:通过类名访问静态成员函数
	cout << "--------访问方式2:通过类名访问静态成员函数--------" << endl;
	Son06::func();  // [Son06]static void func()的调用
	Son06::Base06::func();  // [Base06]static void func()的调用


	// 访问方式3:通过匿名对象访问静态成员函数
	cout << "--------访问方式3:通过匿名对象访问静态成员函数--------" << endl;
	Son06().func();  // [Son06]static void func()的调用
	Son06().Base06::func();  // [Base06]static void func()的调用
	Son06::Base06::func();  // [Base06]static void func()的调用
}

int main() {

	test06_1();
	test06_2();

	system("pause");
	return 0;
}

4.6.7 多继承语法

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

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

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

总结:多继承中如果父类中出现了同名情况,子类使用时候要加作用域,否则会出现歧义。

因此,C++实际开发中不建议用多继承

示例:

#include <iostream>
#include <string>
using namespace std;

// 多继承的语法
class Base07_1 {
public:

	Base07_1() {
		this->a = 100;
	}

	int a;
};


class Base07_2 {
public:

	Base07_2() {
		this->a = 200;
	}

	int a;
};


// 子类:需要继承Base1和Base2
// 语法:class 子类 : 继承方式 父类1, 继承方式 父类2, ... {};
class Son07 : public Base07_1, public Base07_2 {
public:

	Son07() {
		c = 300;
		d = 400;
	}
	
	int c;
	int d;
};


void test07_1() {
	Son07 son;
	cout << "sizeof(son): " << sizeof(son) << endl;  // sizeof(son): 16

	/*
		用工具查看:
			class Son07     size(16):
					+---
			 0      | +--- (base class Base07_1)
			 0      | | a
					| +---
			 4      | +--- (base class Base07_2)
			 4      | | a
					| +---
			 8      | c
			12      | d
					+---
	*/


	// cout << "a: " << son.a << endl;  // "SonO7::a"不明确
	// 当父类VS出现同名成员,需要加作用域区分(因此在实际开发中不太建议使用多继承!)
	cout << "a: " << son.Base07_1::a << endl;  // a: 100
	cout << "a: " << son.Base07_2::a << endl;  // a: 200
}


int main() {

	test07_1();

	system("pause");
	return 0;
}

4.6.8 菱形继承

菱形继承概念:

  • 两个派生类(子类)继承同一个基类(父类)
  • 又有某个类同时继承者两个派生类
  • 这种继承被称为菱形继承,或者钻石继承

典型的菱形继承案例:

A
B
C
E

举个例子:

动物
羊驼

菱形继承的问题:

  1. 羊继承了动物的数据,驼同样继承了动物的数据,当羊驼使用数据时,就会产生二义性。
  2. 羊驼继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。

解决方案:利用虚继承可以解决菱形继承带来的数据重复问题!

语法:在继承方式前加上关键字virtual即可。

  1. class Sheep : virtual public Animal{};
  2. class Camel : virtual public Animal{};

示例:

#include <iostream>
#include <string>
using namespace std;

// 菱形继承


// 动物类
class Animal{
public:
	int age;
};


// 羊类
class Sheep : virtual public Animal{};


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


// 羊驼类
class Alpaca : public Sheep, public Camel{};


void test08_1() {
	Alpaca alpaca;
	// alpaca.age = 18;  // "Alpaca:age"不明确
	
	alpaca.Sheep::age = 18;
	alpaca.Camel::age = 22;

	// 当菱形继承且两个父类拥有相同数据,需要加以作用域区分
	cout << "alpaca.Sheep::age: " << alpaca.Sheep::age << endl;  // alpaca.Sheep::age: 18
	cout << "alpaca.Camel::age: " << alpaca.Camel::age << endl;  // alpaca.Camel::age: 22

	/*
		问题来了:羊驼的age是多少?是18该是22?

		这份数据我们知道,只有一份就可以了,而菱形继承导致数据有两份,
		这会导致资源浪费!

		我们用工具看一下:
			class Alpaca    size(8):
					+---
			 0      | +--- (base class Sheep)
			 0      | | +--- (base class Animal)
			 0      | | | age
					| | +---
					| +---
			 4      | +--- (base class Camel)
			 4      | | +--- (base class Animal)
			 4      | | | age
					| | +---
					| +---
					+---
		的确是有两份!

		解决方案:利用虚继承可以解决菱形继承带来的数据重复问题!
		语法:在继承方式前加上关键字virtual即可。
	*/
	// 使用虚继承之后,两个结果为:
	alpaca.Sheep::age = 18;
	alpaca.Camel::age = 22; 
	cout << "alpaca.Sheep::age: " << alpaca.Sheep::age << endl;  // alpaca.Sheep::age: 22
	cout << "alpaca.Camel::age: " << alpaca.Camel::age << endl;  // alpaca.Camel::age: 22

	// 而且我们也可以不加作用域直接使用age了,因为只有一份
	cout << "alpaca.age: " << alpaca.age << endl;  // alpaca.age: 22

	/*
		我们再用工具看一下:
			class Alpaca    size(12):
					+---
			 0      | +--- (base class Sheep)
			 0      | | {vbptr}
					| +---
			 4      | +--- (base class Camel)
			 4      | | {vbptr}
					| +---
					+---
					+--- (virtual base Animal)
			 8      | age
					+---

			Alpaca::$vbtable@Sheep@:
			 0      | 0
			 1      | 8 (Alpacad(Sheep+0)Animal)  // 这里的8是偏移量(Sheep的vbptr+8就可以找到age)

			Alpaca::$vbtable@Camel@:
			 0      | 0
			 1      | 4 (Alpacad(Camel+0)Animal)  // 这里的4是偏移量(Camel的vbptr+4就可以找到age)
			vbi:       class  offset o.vbptr  o.vbte fVtorDisp
					  Animal       8       0       4 0
		
		vbptr: 虚基类指针(Virtual Base Pointer)。它会指向vbtable(虚基类表)
	*/
}


int main() {

	test08_1();

	system("pause");
	return 0;
}

4.7 多态

顾名思义,多态就是多种形态。

4.7.1 多态的基本概念

多态是C++面向对象三大特性之一。多态分为两类:

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

C++的中的多态,大多都是动态多态。

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

  • 静态多态的函数地址早绑定(编译阶段确定函数地址)
  • 动态多态的函数地址晚绑定(运行阶段确定函数地址)

动态多态的满足条件

  1. 有继承关系
  2. 子类要重写父类的虚函数

重写和重载不一样,重载需要满足三个条件,但重写和原函数是一模一样的

子类写不写virtual都可以,但父类要写virtual —— 但是有一点需要明确:子类重写的虚函数也是一个虚函数!

重写:函数返回值类型 函数名 参数列表 完全一致称为重写。

动态多态的使用:父类的指针或者引用 指向 子类的对象

父类的成员函数前面加上virtual关键字,变成虚函数。对于虚函数,编译器在编译的时候就不能确定函数的调用了。

下面通过案例进行讲解多态:

#include <iostream>
#include <string>
using namespace std;

/*
	动态多态的满足条件:
		1. 有继承关系
		2. 子类要重写父类的虚函数
			重写和重载不一样,重载需要满足三个条件
			但重写和原函数是一模一样的
			(子类写不写virtual都可以,但父类要写virtual)

	动态多态的使用:父类的指针或者引用 指向 子类的对象
*/
class Animal {
public:

	/*
		函数前面加上virtual关键字,变成虚函数,speak函数就是虚函数。
		对于虚函数,编译器在编译的时候就不能确定函数的调用了。
	*/
	void virtual speak() {
		cout << "动物在说话..." << endl;
	}
};


class Cat : public Animal {
public:
	void speak() {
		cout << "喵喵喵..." << endl;
	}
};


class Dog : public Animal {
public:
	void speak() {
		cout << "汪汪汪..." << endl;
	}
};


// 执行说话的函数
/*
	全局函数:地址早绑定 —— 在编译阶段就确定了函数的地址

	如果想执行让猫说话,那么这个函数的地址就不能提前绑定,
	需要在运行阶段绑定,即地址晚绑定

	解决方案:在父类的speak函数前加上一个关键字virtual,即创造
			 一个父类的虚成员函数。
*/
void do_speak(Animal& animal) {  // Animal& animal = cat;
	animal.speak();
}


void test01_1() {
	Cat cat;
	do_speak(cat);  // 动物在说话...

	// 在speak成员函数前加关键字virtual
	do_speak(cat);  // 喵喵喵...

	Dog dog;
	do_speak(dog);  // 汪汪汪...
}


int main() {

	test01_1();

	system("pause");
	return 0;
}

在这里插入图片描述

使用工具进行分析:

1、不加虚函数的:

class Animal    size(1):
        +---
        +---

2、加了虚函数的

class Animal    size(4):
        +---
 0      | {vfptr}
        +---

Animal::$vftable@:
        | &Animal_meta
        |  0
 0      | &Animal::speak
  • vfptr: virtual function pointer,虚函数(表)指针
  • vftable: virtual function table: 虚函数表

在这里插入图片描述

3、当Cat类没有发生speak虚函数重写时:

class Cat       size(4):
        +---
 0      | +--- (base class Animal)
 0      | | {vfptr}
        | +---
        +---

Cat::$vftable@:
        | &Cat_meta
        |  0
 0      | &Animal::speak

在这里插入图片描述

3、Cat类重写speak虚函数:

class Cat       size(4):
        +---
 0      | +--- (base class Animal)
 0      | | {vfptr}
        | +---
        +---

Cat::$vftable@:
        | &Cat_meta
        |  0
 0      | &Cat::speak

在这里插入图片描述

因此当时利用父类的指针或引用 指向 子类对象,调用speak虚函数时,就会从vftable中找到&Cat::speak的地址,即调用子类重写的虚函数,而不是父类的虚函数。

4.7.2 多态案例一:计算器类

案例描述:

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

多态的优点:

  • 代码组织结构清晰
  • 可读性强
  • 利于前期和后期的扩展以及维护

总结:C++开发提倡利用多态设计程序架构,因为多态优点很多。

示例:

#include <iostream>
#include <string>
using namespace std;

// 分别利用普通写法和多态技术实现计算器

// 普通写法
class Calculator {
public:
	int get_res(string oper) {

		if (oper == "+")
		{
			return this->num1 + this->num2;
		}
		else if (oper == "-")
		{
			return this->num1 - this->num2;
		}
		else if (oper == "*")
		{
			return this->num1 * this->num2;
		}
		else if (oper == "/")
		{
			return this->num1 / this->num2;
		}
		/*
			如果想扩展新的功能(** % ...),需要修改源码,
			在真实的开发中,我们提倡一种原则:开闭原则。

			开闭原则:对扩展进行开放,对修改进行关闭。
		*/
	}

public:
	int num1;  // 操作数1
	int num2;  // 操作数2
};


void test02_1() {
	// 创建计算器对象
	Calculator calc;
	calc.num1 = 10;
	calc.num2 = 10;

	cout << calc.num1 << " + " << calc.num2 << " = " << calc.get_res("+") << endl;
	cout << calc.num1 << " - " << calc.num2 << " = " << calc.get_res("-") << endl;
	cout << calc.num1 << " * " << calc.num2 << " = " << calc.get_res("*") << endl;
	cout << calc.num1 << " / " << calc.num2 << " = " << calc.get_res("/") << endl;
}


// 利用多态实现计算器
// 实现计算器的抽象类(接口) —— 和之前学的设计模式的思想是一样的
class AbstractCalculator {
public:
	virtual int get_res() {
		return 0;
	}

public:
	int num1;
	int num2;
};


// 加法计算器类
class AddCalculator : public AbstractCalculator {
public:
	virtual int get_res() {
		return this->num1 + this->num2;
	}
};


// 减法计算器类
class SubCalculator : public AbstractCalculator {
	virtual int get_res() {
		return this->num1 - this->num2;
	}
};


// 乘法计算器类
class MulCalculator : public AbstractCalculator {
	virtual int get_res() {
		return this->num1 * this->num2;
	}
};


// 除法计算器类
class DivCalculator : public AbstractCalculator {
	virtual int get_res() {
		return this->num1 / this->num2;
	}
};


void test02_2() {
	/*
		多态的使用条件:父类的指针/引用 指向 子类的对象
	*/
	// 加法运算
	AbstractCalculator* abs_calc = new AddCalculator;
	abs_calc->num1 = 100;
	abs_calc->num2 = 100;

	cout << abs_calc->num1 << " + " << abs_calc->num2 << " = " << abs_calc->get_res() << endl;  // 100 + 100 = 200

	// 用完后记得销毁(只是把数据销毁了,指针还在)
	delete abs_calc;

	// 减法运算
	abs_calc = new SubCalculator;
	abs_calc->num1 = 100;
	abs_calc->num2 = 100;

	cout << abs_calc->num1 << " - " << abs_calc->num2 << " = " << abs_calc->get_res() << endl;  // 100 - 100 = 0

	// 用完后记得销毁(只是把数据销毁了,指针还在)
	delete abs_calc;

	// 乘法运算
	abs_calc = new MulCalculator;
	abs_calc->num1 = 100;
	abs_calc->num2 = 100;

	cout << abs_calc->num1 << " * " << abs_calc->num2 << " = " << abs_calc->get_res() << endl;  // 100 * 100 = 10000

	// 用完后记得销毁(只是把数据销毁了,指针还在)
	delete abs_calc;

	// 除法运算
	abs_calc = new DivCalculator;
	abs_calc->num1 = 100;
	abs_calc->num2 = 100;

	cout << abs_calc->num1 << " / " << abs_calc->num2 << " = " << abs_calc->get_res() << endl;  // 100 / 100 = 1

	// 用完后记得销毁(只是把数据销毁了,指针还在)
	delete abs_calc;

	/*
		多态带来的好处:
			1. 组织结构清晰
			2. 可读性强
			3. 便于前期/后期的扩展和维护
	*/
}


int main() {

	test02_1();
	test02_2();

	system("pause");
	return 0;
}

4.7.3 纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数。

纯虚函数语法:virtual 返回值类型 函数名(参数列表) = 0;(不需要实现了)

当类中有了纯虚函数,这个类也称为抽象类(Abstract Class)(只要有一个就算抽象类)。

抽象类特点:

  • 无法实例化对象(因为它本身就没有什么意义)
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类

跟之前学习的设计模式中的思想是一样的。

重写纯虚函数,即便函数体内部是空的,也是重写,就可以实例化了!

示例:

#include <iostream>
#include <string>
using namespace std;

// 纯虚函数和抽象类

class Base {
public:
	/*
		只要有一个纯虚函数,这个类就称为抽象类
		抽象类的特点:
			1. 无法实例化对象(new也不行)
			2. 抽象类的子类必须要重写父类中的纯虚函数,否则也属于抽象类
	*/
	virtual void func() = 0;  // 纯虚函数
};


class Son1 : public Base {};


class Son2 : public Base {
public:
	virtual void func() {  // 重写纯虚函数(虽然函数体中没有内容,但这也算是重写)
	}
};


class Son3 : public Base {
public:
	virtual void func() {
		cout << "func函数调用" << endl;
	}
};


void test03_1() {
	// 1. 抽象类是无法实例化对象的,new也不行
	// Base base;  // Error: 不允许使用抽象类类型"Base"的对象
	// new Base;  // Error: 不允许使用抽象类类型"Base"的对象

	// 2. 抽象类的子类必须要重写父类中的纯虚函数,否则也属于抽象类
	// Son1 son;  // Error: 不允许使用抽象类类型"Son1"的对象
	// new Son1;  // Error: 不允许使用抽象类类型"Son1"的对象

	// 3. 重写纯虚函数
	Son2 son;  // 不报错(可以正常实例化)
	new Son2;  // 不报错(可以正常实例化)

	// 普通调用
	Son3 son3;
	son3.func();  // func函数调用

	// 利用多态调用
	Base* base = new Son3;
	base->func();  // func函数调用
}


int main() {

	test03_1();

	system("pause");
	return 0;
}

4.7.4 多态案例二:制作饮品

案例描述:

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

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

在这里插入图片描述

#include <iostream>
#include <string>
using namespace std;


// 多态案例2:制作饮品
class AbstractMakeDrinking {
public:
	// 1. 煮水
	virtual void boil() = 0;

	// 2. 冲泡
	virtual void brew() = 0;

	// 3. 倒入杯中
	virtual void pour_in_cup() = 0;

	// 4. 加入辅助酌料
	virtual void put_somethings() = 0;

	// 制作饮品(按顺序执行前面4步)
	void make_drinking() {
		this->boil();
		this->brew();
		this->pour_in_cup();
		this->put_somethings();
	}
};


// 制作咖啡
class Coffee : public AbstractMakeDrinking {
public:  // 重写纯虚函数
	// 1. 煮水
	virtual void boil() {
		cout << "Step 1 煮农夫山泉" << endl;
	}

	// 2. 冲泡
	virtual void brew() {
		cout << "Step 2 冲泡咖啡" << endl;
	}

	// 3. 倒入杯中
	virtual void pour_in_cup() {
		cout << "Step 3 倒入杯中" << endl;
	}

	// 4. 加入辅助酌料
	virtual void put_somethings() {
		cout << "Step 4 加入糖和牛奶" << endl;
	}
};


// 制作茶水
class Tee : public AbstractMakeDrinking {
public:  // 重写纯虚函数
	// 1. 煮水
	virtual void boil() {
		cout << "Step 1 煮矿泉水" << endl;
	}

	// 2. 冲泡
	virtual void brew() {
		cout << "Step 2 冲泡茶叶" << endl;
	}

	// 3. 倒入杯中
	virtual void pour_in_cup() {
		cout << "Step 3 倒入杯中" << endl;
	}

	// 4. 加入辅助酌料
	virtual void put_somethings() {
		cout << "Step 4 加入枸杞和柠檬" << endl;
	}
};


// 制作函数
void do_work(AbstractMakeDrinking* abs) {
	abs->make_drinking();

	// 释放资源
	delete abs;
}


void test04_1() {
	// 制作咖啡
	do_work(new Coffee);
	/*
		Step 1 煮农夫山泉
		Step 2 冲泡咖啡
		Step 3 倒入杯中
		Step 4 加入糖和牛奶
	*/

	cout << "--------------------------" << endl;

	// 制作茶水
	do_work(new Tee);
	/*
		Step 1 煮矿泉水
		Step 2 冲泡茶叶
		Step 3 倒入杯中
		Step 4 加入枸杞和柠檬
	*/
}


int main() {

	test04_1();

	system("pause");
	return 0;
}

4.7.5 虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,这会造成内存的泄露。

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

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

虚析构和纯虚析构共性:

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

虚析构和纯虚析构区别:

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象
  1. 虚析构语法:virtual ~类名() {}
  2. 纯虚析构语法:virtual ~类名() = 0;

其中纯虚析构需要相应的代码实现,语法:类名::~类名() {}

因为子类在继承父类时,会调用父类的构造函数和析构函数。如果父类的析构函数是纯虚析构函数,因为没有具体实现,那么最后父类在调用时会报错,因此父类的纯虚析构函数需要额外的实现。

虚析构函数就是用来解决通过父类指针释放子类对象。

总结:

  1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
  2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
  3. 拥有纯虚析构函数的类也属于抽象类

示例:

#include <iostream>
#include <string>
using namespace std;


// 虚析构和纯虚析构


class Animal05 {
public:
	Animal05() {
		cout << "Animal05 Constructor调用" << endl;
	}

	//~Animal05() {
	//	cout << "Animal05 Destructor调用" << endl;
	//}
	/*
		普通的Destructor函数:
			父类指针在Destructor时,不会调用子类的Destructor
			导致子类如果有堆区属性,则出现内存泄露情况

			Animal05 Constructor调用
			Cat05 Constructor函数调用
			Tom小猫在说话
			Animal05 Destructor调用

		没有调用Cat05的Destructor函数

		解决方案:使用虚Destructor函数(普通析构和虚析构不能同时存在)
	*/


	//virtual ~Animal05() {  // 利用虚析构可以解决父类指针释放子类对象时不干净的问题
	//	cout << "Animal05的虚Destructor调用" << endl;
	//}
	/*
		Animal05 Constructor调用
		Cat05 Constructor函数调用
		Tom小猫在说话
		Cat05 Destructor函数调用
		Animal05的虚Destructor调用

	此时可以正常调用Cat05的析构函数了!
	*/


	// 纯虚析构(也不能和普通析构、虚析构存在)
	virtual ~Animal05() = 0;
	/*
		会报错,这是因为子类在继承父类时,先父类的构造,
		最后父类还需要析构,但纯虚析构函数中没有代码实现,
		因此会报错!

		解决方案:需要实现一下纯虚析构函数

		问题:以后都需要写虚析构或纯虚析构函数吗?
		回答:并不是。在本案例中,子类Cat05的name是通过指针开辟到堆区了,
			所以必须要走子类中的析构代码。如果用多态是走不到的,所以
			要在父类中加上虚析构函数或纯虚析构函数。
	*/

public:
	virtual void speak() = 0;  // 纯虚函数
};


// 纯虚析构的代码实现
Animal05::~Animal05() {
	cout << "Animal05的纯虚Destructor调用" << endl;
	/*
		Animal05 Constructor调用
		Cat05 Constructor函数调用
		Tom小猫在说话
		Cat05 Destructor函数调用
		Animal05的纯虚Destructor调用
	*/
}


class Cat05 : public Animal05 {
public:
	Cat05(string name) {
		cout << "Cat05 Constructor函数调用" << endl;
		this->name = new string(name);
	}
	
	~Cat05() {
		if (this->name != NULL) {
			cout << "Cat05 Destructor函数调用" << endl;
			delete this->name;
			this->name = NULL;
		}
	}

public:
	virtual void speak() {  // 重写父类的纯虚函数
		cout << *this->name << "小猫在说话" << endl;
	}

public:
	string *name;  // 指针 -> 让其在堆区创建
};


void test05_01() {
	Animal05* animal = new Cat05("Tom");
	animal->speak();
	delete animal;  // 释放资源
}


int main() {

	test05_01();

	system("pause");
	return 0;
}

4.7.6 多态案例三:电脑组装

案例描述:

电脑主要组成部件为CPU(用于计算),显卡(用于显示),内存条(用于存储)。将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件。例如Intel厂商和Lenovo厂商创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口;测试时组装三台不同的电脑进行工作。

示例:

#include <iostream>
#include <string>
using namespace std;


// 多态案例三:电脑组装


// 抽象不同零件类
class CPU {
public:
	// 抽象的计算函数
	virtual void calculator() = 0;
};


class GraphicCard {
public:
	// 抽象的显示函数
	virtual void display() = 0;
};


class Memory {
public:
	// 抽象的存储函数
	virtual void storage() = 0;
};


// 电脑类
class Computer {
public:
	Computer(CPU* cpu, GraphicCard* gpu, Memory* memory) {
		this->cpu = cpu;
		this->gpu = gpu;
		this->memory = memory;
	}

public:
	// 工作的函数
	void work() {
		this->cpu->calculator();
		this->gpu->display();
		this->memory->storage();
	}

	// 提供析构函数,释放三个电脑零件的指针
	~Computer() {
		if (this->cpu != NULL)
		{
			delete this->cpu;
			this->cpu = NULL;
		}

		if (this->gpu != NULL)
		{
			delete this->gpu;
			this->gpu = NULL;
		}

		if (this->memory != NULL)
		{
			delete this->memory;
			this->memory = NULL;
		}
	}

private:
	CPU* cpu;  // CPU零件的指针
	GraphicCard* gpu;  // 显卡零件的指针
	Memory* memory;  // 内存零件的指针
};


// 具体厂商 —— Intel
class IntelCPU : public CPU {
	// 重写纯虚函数
	virtual void calculator() {
		cout << "Intel的CPU开始计算了" << endl;
	}
};


class IntelGraphicCard : public GraphicCard {
	// 重写纯虚函数
	virtual void display() {
		cout << "Intel的显卡开始显示了" << endl;
	}
};


class IntelMemory : public Memory {
	// 重写纯虚函数
	virtual void storage() {
		cout << "Intel的内存开始存储了" << endl;
	}
};


// 具体厂商 —— Lenovo
class LenovoCPU : public CPU {
	// 重写纯虚函数
	virtual void calculator() {
		cout << "Lenovo的CPU开始计算了" << endl;
	}
};


class LenovoGraphicCard : public GraphicCard {
	// 重写纯虚函数
	virtual void display() {
		cout << "Lenovo的显卡开始显示了" << endl;
	}
};


class LenovoMemory : public Memory {
	// 重写纯虚函数
	virtual void storage() {
		cout << "Lenovo的内存开始存储了" << endl;
	}
};


void test06_01() {
	// 创建第一台电脑的零件
	CPU* intel_cpu = new IntelCPU;  // 父类的指针指向子类 -> 多态
	GraphicCard* intel_gpu = new IntelGraphicCard;
	Memory* intel_memory = new IntelMemory;

	// 创建第一台电脑
	Computer* computer1 = new Computer(intel_cpu, intel_gpu, intel_memory);
	computer1->work();
	delete computer1;
	/*
		Intel的CPU开始计算了
		Intel的显卡开始显示了
		Intel的内存开始存储了
	*/


	// 第二台电脑的组装
	Computer* computer2 = new Computer(new LenovoCPU, new LenovoGraphicCard, new LenovoMemory);
	computer2->work();
	delete computer2;

	/*
		Lenovo的CPU开始计算了
		Lenovo的显卡开始显示了
		Lenovo的内存开始存储了
	*/

	// 第三台电脑的组装
	Computer* computer3 = new Computer(
		new IntelCPU, new LenovoGraphicCard, new IntelMemory
	);
	computer3->work();
	delete computer3;
	/*
		Intel的CPU开始计算了
		Lenovo的显卡开始显示了
		Intel的内存开始存储了
	*/
}


int main() {

	test06_01();

	system("pause");
	return 0;
}

5. 文件操作

程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放。通过文件可以将数据持久化。

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

fstream: file stream,文件流

文件类型分为两种:

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

我们常见的*.bin就是二进制文件

操作文件的三大类:

  1. ofstream:写操作(不能读) —— o -> output
  2. ifstream:读操作(不能写) —— i -> input
  3. fstream :读写操作(既可以读也可以写)

一般我们用fstream就行

这里为什么output是写,input是读呢?是不是反了?其实不然,我们之前理解的视角不同,这里的output和input是针对编译器而言的。对于IDE来说,将结果写到一个文件中,不就输出嘛(output);将文件中的信息写到IDE中,不就是输入嘛(input)。

这里说的IDE并不是说Visual Studio,而是*.cpp文件

5.1 文本文件

5.1.1 写文件

写文件步骤如下:

  1. 包含头文件:#include <fstream>
  2. 创建流对象:ofstream ofs;
  3. 打开文件:ofs.open("文件路径", 打开方式);
  4. 写数据:ofs << "写入的数据";
  5. 关闭文件:ofs.close();

文件打开方式:

打开方式解释
ios::in为输入(读)打开文件
ios::out为输出(写)打开文件
ios::ate初始位置:文件尾
ios::app所有输出附加在文件末尾
ios::trunc若文件已存在先删除文件
ios::binary二进制方式
  • ate: at end表示打开文件后将文件指针定位到文件末尾。 —— 还得是ChatGPT
  • app: append —— 和Python中的append是一个意思
  • trunc: truncate:截断; 截短,缩短,删节(尤指掐头或去尾);
  • binary: 二进制,缩写为bin

ios = Input/Output Stream,即输入/输出流。它是C++标准库中提供的一个输入输出流类,用于进行文件读写、控制台输入输出、网络通信等操作。

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

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

示例:

#include <iostream>
#include <string>
using namespace std;
#include <fstream>  // 1. 包含头文件


// 文本文件——写文件
void test01_01() {
	// 1. 包含头文件 fstream

	// 2. 创建流对象
	ofstream ofs;

	// 3. 指定打开方式
	ofs.open("test.txt", ios::out);

	// 4. 写内容
	ofs << "姓名: 张三" << endl;  // 写内容时用endl也是换行
	ofs << "性别: 男" << endl;  // 写内容时用endl也是换行
	ofs << "年龄: 18" << endl;

	// 5. 关闭原件
	ofs.close();

	/*
		文件内容:
			姓名: 张三
			性别: 男
			年龄: 18
	*/
}


int main() {

	test01_01();

	system("pause");
	return 0;
}

总结:

  • 文件操作必须包含头文件 #include <fstream>
  • 读文件可以利用ofstream,或者fstream
  • 打开文件时候需要指定操作文件的路径,以及打开方式
  • 利用 << 可以向文件中写数据
  • 操作完毕,要关闭文件

5.1.2 读文件

读文件与写文件步骤相似,但是读取方式相对于比较多。

读文件步骤如下:

  1. 包含头文件:#include<fstream>
  2. 创建流对象:ifstream ifs;
  3. 打开文件并判断文件是否打开成功:ifs.open("文件路径", 打开方式);
  4. 读数据:四种方式读取
  5. 关闭文件:ifs.close();

四种方式读取的代码示意:

// 1. 第一种
char buffer[1024] = { 0 };
while (ifs >> buffer)
{
	cout << buffer << endl;
}

// 2. 第二种
char buffer[1024] = { 0 };
// .getline: 获取一行数据
while (ifs.getline(buffer, sizeof(buffer)))
{
	cout << buffer << endl;
}

// 3. 第三种
string buffer;
while (getline(ifs, buffer))
{
	cout << buffer << endl;
}

// 4. 第四种(一个字符一个字符的读,效率最低)
char c;
// .get():每次读取一个字符
while ((c = ifs.get()) != EOF)  // EOF:End of File,文件尾
{
	cout << c;
}

四种读取方式,记住一种就好。

示例:

#include <iostream>
#include <string>
using namespace std;
#include <fstream>  // 1. 包含头文件


// 文本文件——读文件
void test02_01() {
	// 1. 包含头文件 fstream

	// 2. 创建流对象
	ifstream ifs;

	// 3. 打开文件,并且判断是否打开成功
	ifs.open("test.txt", ios::in);
	
	if (!ifs.is_open())
	{
		cout << "文件打开失败!" << endl;
		return;
	}

	// 4. 读数据 —— 第一种
	char buffer[1024] = { 0 };
	while (ifs >> buffer)
	{
		cout << buffer << endl;
	}

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


void test02_02() {
	// 1. 包含头文件 fstream

	// 2. 创建流对象
	ifstream ifs;

	// 3. 打开文件,并且判断是否打开成功
	ifs.open("test.txt", ios::in);

	if (!ifs.is_open())
	{
		cout << "文件打开失败!" << endl;
		return;
	}

	// 4. 读数据 —— 第二种
	char buffer[1024] = { 0 };
	// .getline: 获取一行数据
	while (ifs.getline(buffer, sizeof(buffer)))
	{
		cout << buffer << endl;
	}

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


void test02_03() {
	// 1. 包含头文件 fstream

	// 2. 创建流对象
	ifstream ifs;

	// 3. 打开文件,并且判断是否打开成功
	ifs.open("test.txt", ios::in);

	if (!ifs.is_open())
	{
		cout << "文件打开失败!" << endl;
		return;
	}

	// 4. 读数据 —— 第三种
	string buffer;
	while (getline(ifs, buffer))
	{
		cout << buffer << endl;
	}

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


void test02_04() {
	// 1. 包含头文件 fstream

	// 2. 创建流对象
	ifstream ifs;

	// 3. 打开文件,并且判断是否打开成功
	ifs.open("test.txt", ios::in);

	if (!ifs.is_open())
	{
		cout << "文件打开失败!" << endl;
		return;
	}

	// 4. 读数据 —— 第四种(一个字符一个字符的读,效率最低!)
	char c;
	// .get():每次读取一个字符
	while ((c = ifs.get()) != EOF)  // EOF:End of File,文件尾
	{
		cout << c;
	}

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


int main() {

	cout << "---------第一种读取方式---------" << endl;
	test02_01();
	cout << "---------第二种读取方式---------" << endl;
	test02_02();
	cout << "---------第三种读取方式---------" << endl;
	test02_03();
	cout << "---------第四种读取方式---------" << endl;
	test02_04();

	/*
		---------第一种读取方式---------
		姓名:
		张三
		性别:
		男
		年龄:
		18
		---------第二种读取方式---------
		姓名: 张三
		性别: 男
		年龄: 18
		---------第三种读取方式---------
		姓名: 张三
		性别: 男
		年龄: 18
		---------第四种读取方式---------
		姓名: 张三
		性别: 男
		年龄: 18
	*/

	system("pause");
	return 0;
}

总结:

  • 读文件可以利用ifstream,或者fstream
  • 利用is_open函数可以判断文件是否打开成功
  • close关闭文件

5.2 二进制文件

以二进制的方式对文件进行读写操作。打开方式要指定为ios::binary

5.2.1 写文件

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

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

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

因为write函数要的是const char*数据类型的参数,因此如果传入的参数不满足这样的条件,就用(const char*)强转一下。

示例:

#include <iostream>
#include <string>
using namespace std;
#include <fstream>  // 1. 包含头文件


// 二进制文件 —— 写文件
class Person {
public:
	// 在写字符串时,最好不要用C++的string,而是用C语言的字符数组代表字符串
	char name[64];
	int age;
};


void test03_01() {
	// 1. 包含头文件

	// 2. 创建流对象 并 3. 打开文件
	ofstream ofs("person.txt", ios::out | ios::binary);;

	// 4. 写文件
	Person p = { "张三", 18 };
	ofs.write((const char*)&p, sizeof(Person));

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


int main() {

	test03_01();
	/*
		张三                                                               

		写入的文件中看起来是乱码,但实际上并不是乱码,
		只要我们可以用二进制的方式读进来就是对的。
	*/

	system("pause");
	return 0;
}

5.2.2 读文件

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

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

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

示例:

#include <iostream>
#include <string>
using namespace std;
#include <fstream>  // 1. 包含头文件


// 二进制文件 —— 读文件
class Person04 {
public:
	char name[64];
	int age;
};


void test04_01() {
	// 1. 包含头文件

	// 2. 创建流对象
	ifstream ifs;

	// 3. 打开文件 并 判断文件是否打开成功
	ifs.open("person.txt", ios::in | ios::binary);

	if (!ifs.is_open())
	{
		cout << "文件打开失败" << endl;
	}

	// 4. 读文件
	Person04 p;

	ifs.read((char*)&p, sizeof(Person04));

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

	cout << "姓名: " << p.name << "\t 年龄: " << p.age << endl;
	// 姓名: 张三       年龄: 18
}


int main() {

	test04_01();

	system("pause");
	return 0;
}

6. 职工管理系统

6.1 管理系统需求

职工管理系统可以用来管理公司内所有员工的信息,本教程主要利用C++来实现一个基于多态的职工管理系统。

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

  • 普通员工职责:完成经理交给的任务
  • 经理职责:完成老板交给的任务,并下发任务给员工
  • 老板职责:管理公司所有事务

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

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

6.2 创建管理类

管理类负责的内容如下:

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

6.2.1 创建文件

在头文件和源文件的文件夹下分别创建workerManager.hworkerManager.cpp文件。

6.2.2 头文件实现

workerManager.h中设计管理类。

代码如下:

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


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

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

6.2.3 源文件实现

workerManager.cpp中将构造和析构函数空实现补全。

#include "workerManager.h"


WorkerManager::WorkerManager() {

}

WorkerManager::~WorkerManager() {

}

6.3 菜单功能

6.3.1 添加成员函数

在管理类workerManager.h中添加成员函数void show_menu();

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


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

	// 析构函数
	~WorkerManager();

public:
	// 展示菜单
	void show_menu();
};

6.3.2 菜单功能实现

在管理类workerManager.cpp中实现show_menu()函数。

#include "workerManager.h"


WorkerManager::WorkerManager() {

}

WorkerManager::~WorkerManager() {

}

// 展示菜单
void WorkerManager::show_menu() {
	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;
}

6.3.3 测试菜单功能

职工管理系统.cpp中测试菜单功能。

#include <iostream>
using namespace std;
#include "workerManager.h"

int main() {

	// 实例化WorkerManager对象
	WorkerManager wm;

	// 调用WorkerManager的show_menu成员函数
	wm.show_menu();

	system("pause");
	return 0;
}

效果如下:

在这里插入图片描述

6.4 退出功能

6.4.1 提供功能接口

在main函数中提供分支选择,提供每个功能接口。

代码:

#include <iostream>
using namespace std;
#include "workerManager.h"

int main() {

	// 实例化WorkerManager对象
	WorkerManager wm;

	int choice = -1;  // 用来存储用户的选项

	while (true)
	{
		// 调用WorkerManager的show_menu成员函数
		wm.show_menu();

		cout << "请输入您的选择: ";
		cin >> choice;  // 接收用户的键盘输入

		switch (choice)
		{
		case 0:  // 退出系统
			wm.exit_system();
			//break;  // 这里的break就没有意义了
		case 1:  // 增加职工
			break;
		case 2:  // 显示职工
			break;
		case 3:  // 删除职工
			break;
		case 4:  // 修改职工
			break;
		case 5:  // 查找职工
			break;
		case 6:  // 排序职工
			break;
		case 7:  // 清空文档
			break;
		default:  // 继续选择
			system("cls");  // 清屏操作
			break;
		}
	}


	system("pause");
	return 0;
}

6.4.2 实现退出功能

workerManager.h中提供退出系统的成员函数void exit_system();

workerManager.cpp中提供具体的功能实现。

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

C++中exit()函数的参数是整数类型的参数,通常称为“退出状态码”(exit status code)或“返回值”(return value)。这个参数表示程序正常或异常退出的原因,如果参数为0,则表示程序正常退出,其他非零参数则表示程序异常退出,并且参数值通常用来表示错误代码或异常情况的类型。

6.4.3 测试功能

在main函数分支0选项中,调用退出程序的接口。

效果如下:

在这里插入图片描述

6.5 创建职工类

6.5.1 创建职工抽象类

职工的分类为:普通员工、经理、老板。

将三种职工抽象到一个类(worker)中,利用多态管理不同职工种类。

职工的属性为:职工编号、职工姓名、职工所在部门编号。

职工的行为为:岗位职责信息描述,获取岗位名称。

头文件文件夹下创建文件worker.h文件并且添加如下代码:

#pragma once
#include <iostream>
using namespace std;
#include <string>


/*
	因为Worker类是纯虚类,因此不需要再写一个cpp文件来实现它了,
	它就是一个Interface
*/


// 职工抽象基类
class Worker {
public:
	// 显示个人信息
	virtual void show_info() = 0;

	// 获取岗位名称
	virtual string get_dept_name() = 0;

public:
	int id;  // 职工编号
	string name;  // 职工姓名
	int dept_id;  // 部门编号
};

6.5.2 创建普通员工类

普通员工类继承职工抽象类,并重写父类中纯虚函数。

在头文件和源文件的文件夹下分别创建employee.hemployee.cpp文件。

employee.h中代码如下:

// 普通员工文件
#pragma once
#include <iostream>
using namespace std;
#include "woker.h"


class Employee : public Worker {
public:
	Employee(int id, string name, int dept_id);  // 构造函数

public:  // 重写接口的纯虚函数,但是.h文件只做声明,不做实现
	// 显示个人信息
	virtual void show_info();

	// 获取岗位名称
	virtual string get_dept_name();
};

employee.cpp中代码如下:

#include "employee.h"


Employee::Employee(int id, string name, int dept_id) {	// 构造函数
	this->id = id;
	this->name = name;
	this->dept_id = dept_id;
}

// 重写接口的纯虚函数
void Employee::show_info() {  // 显示个人信息
	cout << "职工编号: " << this->id << "\t职工姓名: "
		<< this->name << "\t岗位: " << this->get_dept_name()
		<< "\t岗位职责: 完成经理交给的任务" << endl;
}

string Employee::get_dept_name() {  // 获取岗位名称
	return "员工";
}

6.5.3 创建经理类

经理类继承职工抽象类,并重写父类中纯虚函数,和普通员工类似。

在头文件和源文件的文件夹下分别创建manager.hmanager.cpp文件。

manager.h中代码如下:

#pragma once
#include <iostream>
using namespace std;
#include "woker.h"


class Manager : public Worker {
public:
	Manager(int id, string name, int dept_id);

public:
	// 显示个人信息
	virtual void show_info();

	// 获取岗位名称
	virtual string get_dept_name();
};

manager.cpp中代码如下:

#include "manager.h"


Manager::Manager(int id, string name, int dept_id) {
	this->id = id;
	this->name = name;
	this->dept_id = dept_id;
}

// 显示个人信息
 void Manager::show_info() {
	 cout << "职工编号: " << this->id << "\t职工姓名: "
		 << this->name << "\t岗位: " << this->get_dept_name()
		 << "\t岗位职责: 完成老板交给的任务,并给员工下发任务" << endl;
}

// 获取岗位名称
string Manager::get_dept_name() {
	// 如果不转换则是C语言的字符串。
	// (不转换也没事,编译器会帮我自动转换的)
	return string("经理");  
}

6.5.4 创建老板类

老板类继承职工抽象类,并重写父类中纯虚函数,和普通员工类似。

在头文件和源文件的文件夹下分别创建boss.hboss.cpp文件

boss.h中代码如下:

#pragma once
#include <iostream>
using namespace std;
#include "woker.h"


class Boss : public Worker {
public:
	Boss(int id, string name, int dept_id);

public:
	virtual void show_info();

	virtual string get_dept_name();
};

boss.cpp中代码如下:

#include "boss.h"


Boss::Boss(int id, string name, int dept_id) {
	this->id = id;
	this->name = name;
	this->dept_id = dept_id;
}

void Boss::show_info() {
	cout << "职工编号: " << this->id << "\t职工姓名: "
		<< this->name << "\t岗位: " << this->get_dept_name()
		<< "\t岗位职责: 管理公司所有的事务" << endl;
}

string Boss::get_dept_name() {
	return string("老板");
}

6.5.5 测试多态

职工管理系统.cpp中添加测试函数,并且运行能够产生多态。

测试代码如下:

#include "woker.h"
#include "employee.h"
#include "manager.h"
#include "boss.h"


// 测试代码
Worker* worker = new Employee(1, "张三", 1);
worker->show_info();

delete worker;

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

delete worker;

worker = new Boss(3, "王五", 3);
worker->show_info();

测试效果如下:

在这里插入图片描述

6.6 添加职工

功能描述:批量添加职工,并且保存到文件中。

6.6.1 功能分析

分析:

  • 用户在批量创建时,可能会创建不同种类的职工
  • 如果想将所有不同种类的员工都放入到一个数组中,可以将所有员工的指针维护到一个数组里
  • 如果想在程序中维护这个不定长度的数组,可以将数组创建到堆区,并利用Worker**的指针维护

在这里插入图片描述

如果Worker*[]指针数组放到栈区,那么会被系统自动回收,不方便,所以应该new出来,放在堆区。

new Worker*[],那么返回的应该是一个Worker**

6.6.2 功能实现

WokerManager.h头文件中添加成员属性代码:

// 记录职工人数
int employee_num;

// 职工数组指针
Worker** employee_arr;

WorkerManager构造函数中初始化属性

WorkerManager::WorkerManager() {
	// 初始化属性
	this->employee_num = 0;
	this->employee_arr = NULL;
}

WorkerManager析构函数中释放new出来的资源

WorkerManager::~WorkerManager() {
	if (this->employee_arr != NULL)
	{
		delete[] this->employee_arr;
		this->employee_arr = NULL;
	}
}

workerManager.h中添加成员函数

// 添加职工
void add_employee();

workerManager.cpp中实现该函数

// 添加职工
void WorkerManager::add_employee() {
	cout << "请输入添加职工的数量: ";
	int add_num = 0;  // 保存添加职工的数量
	cin >> add_num;

	if (add_num > 0)
	{
		// 添加
		// 计算添加新空间大小(新空间人数 = 原来记录的人数 + 新增人数)
		int new_size = this->employee_num + add_num;

		// 开辟新空间
		Worker** new_space = new Worker* [new_size];

		// 将原来空间下的数据拷贝到新空间下
		if (this->employee_arr != NULL)
		{
			for (int i = 0; i < this->employee_num; i++)
			{
				new_space[i] = this->employee_arr[i];
			}
		}

		// 添加新数据
		for (int i = 0; i < add_num; i++) {
			int id;  // 职工编号
			string name;  // 职工姓名
			int dept_select;  // 部门选择

			cout << "请输入第" << i + 1 << "个新职工编号: ";
			cin >> id;

			cout << "请输入第" << i + 1 << "个新职工姓名: ";
			cin >> name;

			cout << "请选择该职工的岗位(1->职工; 2->经理; 3->老板): ";
			cin >> dept_select;

			Worker* worker = NULL;
			switch (dept_select)
			{
			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;
			}

			// 将创建的职工指针,保存到数组中
			new_space[this->employee_num + i] = worker;
		}

		// 释放原有的空间
		delete[] this->employee_arr;

		// 更改新空间的指向
		this->employee_arr = new_space;

		// 更新新的职工人数
		this->employee_num = new_size;

		// TODO: 成功添加后应该保存到文件中

		// 提示添加成功
		cout << "成功添加" << add_num << "名新职工" << endl;
	}
	else
	{
		cout << "输入数据有误!" << endl;
	}
	// 按任意键后清屏回到上级目录
	system("pause");
	system("cls");
}

6.7 文件交互:写文件

功能描述:对文件进行读写。

在上一个添加功能中,我们只是将所有的数据添加到了内存中,一旦程序结束就无法保存了。因此文件管理类中需要一个与文件进行交互的功能,对于文件进行读写操作。

6.7.1 设定文件路径

首先我们将文件路径,在workerManager.h中添加宏常量,并且包含头文件fstream

#include <fstream>
#define FILENAME "employee_file.txt"

6.7.2 成员函数声明

workerManager.h中类里添加成员函数void save()

// 保存文件
void save();

6.7.3 保存文件功能实现

// 保存文件
void WorkerManager::save() {
	ofstream ofs;

	ofs.open(FILENAME, ios::out);

	// 将每个人的数据写入到文件中(覆盖重写)
	for (int i = 0; i < this->employee_num; i++)
	{
		ofs << this->employee_arr[i]->id << " "
			<< this->employee_arr[i]->name << " "
			<< this->employee_arr[i]->id << endl;
	}

	ofs.close();
}

6.7.4 保存文件功能测试

在添加职工功能中添加成功后添加保存文件函数。效果如下:

在这里插入图片描述

6.8 文件交互:读文件

功能描述:将文件中的内容读取到程序中。

虽然我们实现了添加职工后保存到文件的操作,但是每次开始运行程序,并没有将文件中数据读取到程序中。而我们的程序功能中还有清空文件的需求,因此构造函数初始化数据的情况分为三种:

  1. 第一次使用,文件未创建
  2. 文件存在,但是数据被用户清空
  3. 文件存在,并且保存职工的所有数据

6.8.1 文件未创建

workerManager.h中添加新的成员属性file_is_empty标志文件是否为空。

// 标志文件是否为空
bool file_is_empty;

修改WorkerManager.cpp中构造函数代码

WorkerManager::WorkerManager() {

	// 文件不存在
	ifstream ifs;
	ifs.open(FILENAME, ios::in);  // 读文件
	
	if (!ifs.is_open())  // 文件不存在
	{
		cout << "文件不存在" << endl;

		// 初始化属性
		this->employee_num = 0;  // 初始化记录人数
		this->employee_arr = NULL;  // 初始化数组指针
		this->file_is_empty = true;  // 初始化文件是否为空
		ifs.close();
		return;
	}
}

6.8.2 文件存在且数据为空

workerManager.cpp中的构造函数追加代码:

// 第二种情况:文件存在,但数据为空
char ch;
ifs >> ch;  // 就读取一个字符
if (ifs.eof())  // eof: end of file, 文件末尾
{
	// 就读取一个字符,如果该字符是EOF,那么说明文件中没有内容
	cout << "文件为空" << endl;

	// 初始化属性
	this->employee_num = 0;  // 初始化记录人数
	this->employee_arr = NULL;  // 初始化数组指针
	this->file_is_empty = true;  // 初始化文件是否为空
	ifs.close();
	return;
}

将文件创建后清空文件内容,并测试该情况下初始化功能。

我们发现文件不存在或者为空,file_is_empty都为真,那何时为假?

成功添加职工后,应该更改文件不为空的标志:

void workerManager::add_employee()成员函数中添加:

// 更新file_is_empty
this->file_is_empty = false;

6.8.3 文件存在且保存职工数据

6.8.1.1 获取记录的职工人数

workerManager.h中添加成员函数int get_employee_num();

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

workerManager.cpp中实现:

int WorkerManager::get_employee_num() {
	ifstream ifs;
	ifs.open(FILENAME, ios::in);

	int id;
	string name;
	int dept_id;

	int count_num = 0;
	while (ifs >> id && ifs >> name && ifs >> dept_id)
	{
		// 统计人数变量
		count_num += 1;
	}

	/*
		在这个 while 循环中,每次循环都会先调用 ifs 对象的运算符重载,
		从文件中读取一个整数类型的数据,并将其存储到 id 变量中。
		如果读取成功,则继续调用运算符重载,从文件中读取一个字符串
		类型的数据,并将其存储到 name 变量中。
		如果读取成功,则再次调用运算符重载,从文件中读取一个整数类
		型的数据,并将其存储到 dept_id 变量中。
		如果这三次读取都成功,那么 while 循环的条件判断为真,
		循环体中的代码就会被执行;否则,循环终止。
	*/

	return count_num;
}

workerManager.cpp构造函数中继续追加代码:

// 第三种情况:文件存在,并且记录数据
int num = this->get_employee_num();
cout << "职工的人数为: " << num << endl;
this->employee_num = num;
6.8.1.2 初始化数组

根据职工的数据以及职工数据,初始化workerManager中的Worker** employee_arr指针。

WorkerManager.h中添加成员函数void init_employee();

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

WorkerManager.cpp中实现

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

	int id;
	string name;
	int dept_id;

	int idx = 0;
	while (ifs >> id && ifs >> name && ifs >> dept_id)
	{
		Worker* worker = NULL;

		if (dept_id == 1)  // 普通员工
		{
			worker = new Employee(id, name, dept_id);
		}
		else if (dept_id == 2)  // 经理
		{
			worker = new Manager(id, name, dept_id);
		}
		else  // 老板
		{
			worker = new Boss(id, name, dept_id);
		}

		this->employee_arr[idx] = worker;
		idx += 1;
	}
	ifs.close();
}

workerManager.cpp构造函数中追加代码

// 开辟空间
this->employee_arr = new Worker * [this->employee_num];
// 将文件中的数据初始化(存到维护的数组中)
this->init_employee();

// for (int i = 0; i < this->employee_num; i++)
// {
// 	cout << "职工编号: " << this->employee_arr[i]->id
// 		<< "\t职工姓名: " << this->employee_arr[i]->name
// 		<< "\t部门编号: " << this->employee_arr[i]->dept_id
// 		<< endl;
// }

6.9 显示职工

功能描述:显示当前所有职工信息。

6.9.1 显示职工函数声明

workerManager.h中添加成员函数void show_employee();

// 显示职工
void show_employee();

6.9.2 显示职工函数实现

workerManager.cpp中实现成员函数void show_employee();

void WorkerManager::show_employee() {
	// 判断文件是否为空
	if (this->file_is_empty)
	{
		cout << "文件不存在或记录为空" << endl;
	}
	else
	{
		for (int i = 0; i < this->employee_num; i++)
		{
			// 利用多态调用程序接口
			this->employee_arr[i]->show_info();
		}
	}

	// 按任意键后清屏
	system("pause");
	system("cls");
}

6.10 删除职工

功能描述:按照职工的编号(id)进行删除职工操作。

6.10.1 删除职工函数声明

workerManager.h中添加成员函数void delete_employee();

// 删除职工
void delete_employee();

6.10.2 职工是否存在函数声明

很多功能都需要用到根据职工是否存在来进行操作如:删除职工、修改职工、查找职工。因此添加该公告函数,以便后续调用。

workerManager.h中添加成员函数int is_exist(int id);

// 按照职工编号判断职工是否存在,如存在则返回职工在数组中位置,不存在返回-1
int is_exist();

6.10.3 职工是否存在函数实现

workerManager.cpp中实现成员函数int is_exist(int id);

// 判断职工时候存在,存在返回职工在数组中为idx,不存在则返回-1
int WorkerManager::is_exist(int id) {
	int idx = -1;

	for (int i = 0; i < this->employee_num; i++)
	{
		if (this->employee_arr[i]->id == id)  // 找到职工
		{
			idx = i;
			break;
		}
	}

	return idx;
}

6.10.4 删除职工函数实现

workerManager.cpp中实现成员函数void delete_employee();

// 删除职工
void WorkerManager::delete_employee() {
	if (this->file_is_empty)
	{
		cout << "文件不存在或记录为空" << endl;
	}
	else
	{
		// 按照职工编号删除职工
		cout << "请输入想要删除职工的编号: ";
		int id;
		cin >> id;

		int idx = this->is_exist(id);

		if (idx != -1)  // 职工存在
		{
			// 删除到idx位置上的职工
			for (int i = idx; i < this->employee_num - 1; i++)
			{
				this->employee_arr[i] = this->employee_arr[i + 1];
			}

			// 更新数组中记录的人员个数
			this->employee_num -= 1;  
			// 将数组中的数据同步更新到文件中
			this->save();

			cout << "删除成功!" << endl;
		}
		else
		{
			cout << "查无此人,删除失败!" << endl;
		}
	}

	post_processing();  // 按任意键后清屏
}

post_processing函数里面就是system("pause"); system("cls");

6.11 修改职工

功能描述:能够按照职工的编号对职工信息进行修改并保存。

6.11.1 修改职工函数声明

workerManager.h中添加成员函数void modify_employee();

//修改职工
void modify_employee();

6.11.2 修改职工函数实现

workerManager.cpp中实现成员函数void modify_employee();

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

		int idx = this->is_exist(id);

		if (idx != -1)  // 找到职工
		{
			// 先删除原来的职工
			delete this->employee_arr[idx];

			int new_id;
			string new_name;
			int new_dept_id;

			cout << "查找到" << id << "号职工,请输入新的职工编号: ";
			cin >> new_id;
			
			cout << "请输入新的姓名: ";
			cin >> new_name;

			cout << "请输入新的岗位(1->职工; 2->经理; 3->老板): ";
			cin >> new_dept_id;

			// 创建新的对象
			Worker* worker = NULL;
			switch (new_dept_id)
			{
			case 1:
				worker = new Employee(new_id, new_name, new_dept_id);
				break;
			case 2:
				worker = new Manager(new_id, new_name, new_dept_id);
				break;
			case 3:
				worker = new Boss(new_id, new_name, new_dept_id);
				break;
			default:
				break;
			}

			// 更新数据到数组中
			this->employee_arr[idx] = worker;

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

			// 保存到文件中
			this->save();
		}
		else
		{
			cout << "未找到该职工,修改失败!" << endl;
		}
	}
	post_processing();
}

6.12 查找职工

功能描述:提供两种查找职工方式,一种按照职工编号,一种按照职工姓名。

6.12.1 查找职工函数声明

workerManager.h中添加成员函数void find_employee();

//查找职工
void find_employee();

6.12.2 查找职工函数实现

workerManager.cpp中实现成员函数void find_employee();

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

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

			int idx = this->is_exist(id);
			if (idx != -1)  // 找到职工
			{
				cout << "查找成功,该职工信息如下: " << endl;
				this->employee_arr[idx]->show_info();
			}
			else
			{
				cout << "查无此人,查找失败" << endl;
			}
		}
 		else if (find_method == 2)  // 按照姓名查找
		{
			string name;
			cout << "请输入要查找职工的姓名: ";
			cin >> name;

			bool find_flag = false;  // 是否查到

			for (int i = 0; i < this->employee_num; i++)
			{
				if (this->employee_arr[i]->name == name)
				{
					if (!find_flag)
					{
						// 让这行话只显示一次!
						cout << "查找成功,该职工信息如下: " << endl;
					}
					this->employee_arr[i]->show_info();
					find_flag = true;
					// break;  // 不加break了,因为有可能有重名,都让他们显示出来
				}
			}

			if (!find_flag)
			{
				cout << "查无此人,查找失败" << endl;
			}
		}
		else
		{
			cout << "输入的选项有误" << endl;
		}
	}
	post_processing();
}

找了不执行break,就是防止员工重名,可以显示所有满足条件的员工姓名。

6.13 排序

功能描述:按照职工编号进行排序,排序的顺序由用户指定。

6.13.1 排序函数声明

workerManager.h中添加成员函数void sort_employee();

//排序职工
void sort_employee();

6.13.2 排序函数实现

workerManager.cpp中实现成员函数void sort_employee();

// 按照职工编号排序
void WorkerManager::sort_employee() {
	if (this->file_is_empty)
	{
		cout << "文件不存在或记录为空" << endl;
		post_processing();
	}
	else
	{
		cout << "请选择排序的方式(1->升序;2->降序): ";
		int sort_mode;
		cin >> sort_mode;

		for (int i = 0; i < this->employee_num; i++)
		{
			int min_or_max = i;  // 最小值或最大值的index
			for (int j = i+1; j < this->employee_num; j++)
			{
				if (sort_mode == 1)  // 升序
				{
					if (this->employee_arr[min_or_max]->id > this->employee_arr[j]->id)
					{
						min_or_max = j;
					}
				}
				else if (sort_mode == 2)  // 降序
				{
					if (this->employee_arr[min_or_max]->id < this->employee_arr[j]->id)
					{
						min_or_max = j;
					}
				}
				else
				{
					cout << "选择有误!" << endl;
					post_processing();
					return;
				}
			}

			// 判断一开始认定的最小值/最大值是不是计算的最小值或最大值,如果不是,则交互
			if (i != min_or_max)
			{
				Worker* tmp = this->employee_arr[i];  // 记录第i个元素
				this->employee_arr[i] = this->employee_arr[min_or_max];
				this->employee_arr[min_or_max] = tmp;
			}
		}
		cout << "排序成功,排序后的结果为: " << endl;
		this->save();  // 排序后的结果保存到文件中
		this->show_employee();  // 展示所有的职工
	}
}

这里排序使用的是选择排序:先选定一个最小值,再遍历找真正的最小值,然后下一轮。

6.14 清空文件

功能描述:将文件中记录数据清空。

6.14.1 清空函数声明

workerManager.h中添加成员函数void clean_file();

//清空文件
void clean_file();

6.14.2 清空函数实现

workerManager.cpp中实现员函数void clean_file();

一个指针数组,直接delete是不好的,应该把数组里面每个元素都delete后再delete

// 清空文件
void WorkerManager::clean_file() {
	if (this->file_is_empty)
	{
		cout << "文件不存在或记录为空,无须清空!" << endl;
		post_processing();
		return;
	}

	cout << "确定清空文件(此操作无法撤销)? (Y/N)";
	string user_chioce;
	cin >> user_chioce;

	if (user_chioce == "Y" || user_chioce == "y")
	{
		// 清空文件
		ofstream ofs(FILENAME, ios::trunc);  // 删除文件后重新创建文件
		ofs.close();

		if (this->employee_arr != NULL)
		{
			// 删除堆区的每个职工对象
			for (int i = 0; i < this->employee_num; i++)
			{
				if (this->employee_arr[i] != NULL)
				{
					delete this->employee_arr[i];
					this->employee_arr[i] = NULL;
				}
			}

			// 删除堆区的数组指针
			delete[] this->employee_arr;
			this->employee_arr = NULL;
			this->employee_num = 0;
			this->file_is_empty = true;
		}
		cout << "清空成功!" << endl;
		post_processing();
		return;
	}
	else if (user_chioce == "N" || user_chioce == "n")
	{
		cout << "取消清空操作" << endl;
		post_processing();
		return;
	}
	else
	{
		cout << "您的输入有误" << endl;
		post_processing();
		return;
	}
}

6.15 所有代码

职工管理系统.cpp

#include <iostream>
using namespace std;
#include "workerManager.h"
#include "woker.h"
#include "employee.h"
#include "manager.h"
#include "boss.h"


int main() {

	// 测试代码
/*	Worker* worker = new Employee(1, "张三", 1);
	worker->show_info();

	delete worker;

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

	delete worker;

	worker = new Boss(3, "王五", 3);
	worker->show_info();

	cout << endl*/;


	// 实例化WorkerManager对象
	WorkerManager wm;

	int choice = -1;  // 用来存储用户的选项

	while (true)
	{
		// 调用WorkerManager的show_menu成员函数
		wm.show_menu();

		cout << "请输入您的选择: ";
		cin >> choice;  // 接收用户的键盘输入

		switch (choice)
		{
		case 0:  // 退出系统
			wm.exit_system();
			//break;  // 这里的break就没有意义了
		case 1:  // 增加职工
			wm.add_employee();
			break;
		case 2:  // 显示职工
			wm.show_employee();
			break;
		case 3:  // 删除职工
			wm.delete_employee();
			break;
		case 4:  // 修改职工
			wm.modify_employee();
			break;
		case 5:  // 查找职工
			wm.fine_employee();
			break;
		case 6:  // 排序职工
			wm.sort_employee();
			break;
		case 7:  // 清空文档
			wm.clean_file();
			break;
		default:  // 继续选择
			system("cls");  // 清屏操作
			break;
		}
	}


	system("pause");
	return 0;
}

work.h

#pragma once
#include <iostream>
using namespace std;
#include <string>


/*
	因为Worker类是纯虚类,因此不需要再写一个cpp文件来实现它了,
	它就是一个Interface
*/


// 职工抽象基类
class Worker {
public:
	// 显示个人信息
	virtual void show_info() = 0;

	// 获取岗位名称
	virtual string get_dept_name() = 0;

public:
	int id;  // 职工编号
	string name;  // 职工姓名
	int dept_id;  // 部门编号
};

workermanager.h

#pragma once  // 防止头文件重复包含
#include <iostream>
using namespace std;  // 使用标准的命名空间
#include "woker.h"
#include "employee.h"
#include "manager.h"
#include "boss.h"
#include <fstream>
#define FILENAME "employee.txt"


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

	// 析构函数
	~WorkerManager();

public:
	// 展示菜单
	void show_menu();

	// 退出系统
	void exit_system();

	// 记录职工人数
	int employee_num;

	// 职工数组指针
	Worker** employee_arr;

	// 添加职工
	void add_employee();

	// 保存文件
	void save();

	// 判断文件是否为空
	bool file_is_empty;

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

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

	// 显示职工
	void show_employee();

	// 删除职工
	void delete_employee();

	// 判断职工时候存在,存在返回职工在数组中为idx,不存在则返回-1
	int is_exist(int id);

	// 修改职工
	void modify_employee();

	// 查找职工
	void fine_employee();

	// 按照职工编号排序
	void sort_employee();

	// 清空文件
	void clean_file();
};

workermanager.cpp

#include "workerManager.h"
#include "postprocessing.h"  // 后处理


WorkerManager::WorkerManager() {

	// 第一种情况:文件不存在
	ifstream ifs;
	ifs.open(FILENAME, ios::in);  // 读文件
	
	if (!ifs.is_open())  // 文件不存在
	{
		cout << "文件不存在" << endl;

		// 初始化属性
		this->employee_num = 0;  // 初始化记录人数
		this->employee_arr = NULL;  // 初始化数组指针
		this->file_is_empty = true;  // 初始化文件是否为空
		ifs.close();
		return;
	}

	// 第二种情况:文件存在,但数据为空
	char ch;
	ifs >> ch;  // 就读取一个字符
	if (ifs.eof())  // eof: end of file, 文件末尾
	{
		// 就读取一个字符,如果该字符是EOF,那么说明文件中没有内容
		cout << "文件为空" << endl;

		// 初始化属性
		this->employee_num = 0;  // 初始化记录人数
		this->employee_arr = NULL;  // 初始化数组指针
		this->file_is_empty = true;  // 初始化文件是否为空
		ifs.close();
		return;
	}

	// 第三种情况:文件存在,并且记录数据
	int num = this->get_employee_num();
	cout << "职工的人数为: " << num << endl;
	this->employee_num = num;

	// 开辟空间
	this->employee_arr = new Worker * [this->employee_num];
	// 将文件中的数据初始化(存到维护的数组中)
	this->init_employee();

	//for (int i = 0; i < this->employee_num; i++)
	//{
	//	cout << "职工编号: " << this->employee_arr[i]->id
	//		<< "\t职工姓名: " << this->employee_arr[i]->name
	//		<< "\t部门编号: " << this->employee_arr[i]->dept_id
	//		<< endl;
	//}
}

WorkerManager::~WorkerManager() {
	if (this->employee_arr != NULL)
	{
		delete[] this->employee_arr;
		this->employee_arr = NULL;
	}
}

// 展示菜单
void WorkerManager::show_menu() {
	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);  // 退出程序
	/*
		C++中exit()函数的参数是整数类型的参数,通常称为“退出状态码”(exit status code)或“返回值”(return value)。这个参数表示程序正常或异常退出的原因,如果参数为0,则表示程序正常退出,其他非零参数则表示程序异常退出,并且参数值通常用来表示错误代码或异常情况的类型。
	*/
}


// 添加职工
void WorkerManager::add_employee() {
	cout << "请输入添加职工的数量: ";
	int add_num = 0;  // 保存添加职工的数量
	cin >> add_num;

	if (add_num > 0)
	{
		// 添加
		// 计算添加新空间大小(新空间人数 = 原来记录的人数 + 新增人数)
		int new_size = this->employee_num + add_num;

		// 开辟新空间
		Worker** new_space = new Worker* [new_size];

		// 将原来空间下的数据拷贝到新空间下
		if (this->employee_arr != NULL)
		{
			for (int i = 0; i < this->employee_num; i++)
			{
				new_space[i] = this->employee_arr[i];
			}
		}

		// 添加新数据
		for (int i = 0; i < add_num; i++) {
			int id;  // 职工编号
			string name;  // 职工姓名
			int dept_select;  // 部门选择

			cout << "请输入第" << i + 1 << "个新职工编号: ";
			cin >> id;

			cout << "请输入第" << i + 1 << "个新职工姓名: ";
			cin >> name;

			cout << "请选择该职工的岗位(1->职工; 2->经理; 3->老板): ";
			cin >> dept_select;

			Worker* worker = NULL;
			switch (dept_select)
			{
			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;
			}

			// 将创建的职工指针,保存到数组中
			new_space[this->employee_num + i] = worker;
		}

		// 释放原有的空间
		delete[] this->employee_arr;

		// 更改新空间的指向
		this->employee_arr = new_space;

		// 更新新的职工人数
		this->employee_num = new_size;

		// 成功添加后保存到文件中
		this->save();

		// 更新file_is_empty
		this->file_is_empty = false;

		// 提示添加成功
		cout << "成功添加" << add_num << "名新职工" << endl;
	}
	else
	{
		cout << "输入数据有误!" << endl;
	}
	// 按任意键后清屏回到上级目录
	post_processing();
}


// 保存文件
void WorkerManager::save() {
	ofstream ofs;

	ofs.open(FILENAME, ios::out);

	// 将每个人的数据写入到文件中
	for (int i = 0; i < this->employee_num; i++)
	{
		ofs << this->employee_arr[i]->id << " "
			<< this->employee_arr[i]->name << " "
			<< this->employee_arr[i]->id << endl;
	}

	ofs.close();
}


int WorkerManager::get_employee_num() {
	ifstream ifs;
	ifs.open(FILENAME, ios::in);

	int id;
	string name;
	int dept_id;

	int count_num = 0;
	while (ifs >> id && ifs >> name && ifs >> dept_id)
	{
		// 统计人数变量
		count_num += 1;
	}

	/*
		在这个 while 循环中,每次循环都会先调用 ifs 对象的运算符重载,
		从文件中读取一个整数类型的数据,并将其存储到 id 变量中。
		如果读取成功,则继续调用运算符重载,从文件中读取一个字符串
		类型的数据,并将其存储到 name 变量中。
		如果读取成功,则再次调用运算符重载,从文件中读取一个整数类
		型的数据,并将其存储到 dept_id 变量中。
		如果这三次读取都成功,那么 while 循环的条件判断为真,
		循环体中的代码就会被执行;否则,循环终止。
	*/

	return count_num;
}


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

	int id;
	string name;
	int dept_id;

	int idx = 0;
	while (ifs >> id && ifs >> name && ifs >> dept_id)
	{
		Worker* worker = NULL;

		if (dept_id == 1)  // 普通员工
		{
			worker = new Employee(id, name, dept_id);
		}
		else if (dept_id == 2)  // 经理
		{
			worker = new Manager(id, name, dept_id);
		}
		else  // 老板
		{
			worker = new Boss(id, name, dept_id);
		}

		this->employee_arr[idx] = worker;
		idx += 1;
	}
	ifs.close();
}

void WorkerManager::show_employee() {
	// 判断文件是否为空
	if (this->file_is_empty)
	{
		cout << "文件不存在或记录为空" << endl;
	}
	else
	{
		for (int i = 0; i < this->employee_num; i++)
		{
			// 利用多态调用程序接口
			this->employee_arr[i]->show_info();
		}
	}

	// 按任意键后清屏
	post_processing();
}

// 删除职工
void WorkerManager::delete_employee() {
	if (this->file_is_empty)
	{
		cout << "文件不存在或记录为空" << endl;
	}
	else
	{
		// 按照职工编号删除职工
		cout << "请输入想要删除职工的编号: ";
		int id;
		cin >> id;

		int idx = this->is_exist(id);

		if (idx != -1)  // 职工存在
		{
			// 删除到idx位置上的职工
			for (int i = idx; i < this->employee_num - 1; i++)
			{
				this->employee_arr[i] = this->employee_arr[i + 1];
			}

			// 更新数组中记录的人员个数
			this->employee_num -= 1;  
			// 将数组中的数据同步更新到文件中
			this->save();

			cout << "删除成功!" << endl;
		}
		else
		{
			cout << "查无此人,删除失败!" << endl;
		}
	}

	post_processing();  // 按任意键后清屏
}

// 判断职工时候存在,存在返回职工在数组中为idx,不存在则返回-1
int WorkerManager::is_exist(int id) {
	int idx = -1;

	for (int i = 0; i < this->employee_num; i++)
	{
		if (this->employee_arr[i]->id == id)  // 找到职工
		{
			idx = i;
			break;
		}
	}

	return idx;
}


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

		int idx = this->is_exist(id);

		if (idx != -1)  // 找到职工
		{
			// 先删除原来的职工
			delete this->employee_arr[idx];

			int new_id;
			string new_name;
			int new_dept_id;

			cout << "查找到" << id << "号职工,请输入新的职工编号: ";
			cin >> new_id;
			
			cout << "请输入新的姓名: ";
			cin >> new_name;

			cout << "请输入新的岗位(1->职工; 2->经理; 3->老板): ";
			cin >> new_dept_id;

			// 创建新的对象
			Worker* worker = NULL;
			switch (new_dept_id)
			{
			case 1:
				worker = new Employee(new_id, new_name, new_dept_id);
				break;
			case 2:
				worker = new Manager(new_id, new_name, new_dept_id);
				break;
			case 3:
				worker = new Boss(new_id, new_name, new_dept_id);
				break;
			default:
				break;
			}

			// 更新数据到数组中
			this->employee_arr[idx] = worker;

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

			// 保存到文件中
			this->save();
		}
		else
		{
			cout << "未找到该职工,修改失败!" << endl;
		}
	}
	post_processing();
}


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

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

			int idx = this->is_exist(id);
			if (idx != -1)  // 找到职工
			{
				cout << "查找成功,该职工信息如下: " << endl;
				this->employee_arr[idx]->show_info();
			}
			else
			{
				cout << "查无此人,查找失败" << endl;
			}
		}
 		else if (find_method == 2)  // 按照姓名查找
		{
			string name;
			cout << "请输入要查找职工的姓名: ";
			cin >> name;

			bool find_flag = false;  // 是否查到

			for (int i = 0; i < this->employee_num; i++)
			{
				if (this->employee_arr[i]->name == name)
				{
					if (!find_flag)
					{
						// 让这行话只显示一次!
						cout << "查找成功,该职工信息如下: " << endl;
					}
					this->employee_arr[i]->show_info();
					find_flag = true;
					// break;  // 不加break了,因为有可能有重名,都让他们显示出来
				}
			}

			if (!find_flag)
			{
				cout << "查无此人,查找失败" << endl;
			}
		}
		else
		{
			cout << "输入的选项有误" << endl;
		}
	}
	post_processing();
}


// 按照职工编号排序
void WorkerManager::sort_employee() {
	if (this->file_is_empty)
	{
		cout << "文件不存在或记录为空" << endl;
		post_processing();
	}
	else
	{
		cout << "请选择排序的方式(1->升序;2->降序): ";
		int sort_mode;
		cin >> sort_mode;

		for (int i = 0; i < this->employee_num; i++)
		{
			int min_or_max = i;  // 最小值或最大值的index
			for (int j = i+1; j < this->employee_num; j++)
			{
				if (sort_mode == 1)  // 升序
				{
					if (this->employee_arr[min_or_max]->id > this->employee_arr[j]->id)
					{
						min_or_max = j;
					}
				}
				else if (sort_mode == 2)  // 降序
				{
					if (this->employee_arr[min_or_max]->id < this->employee_arr[j]->id)
					{
						min_or_max = j;
					}
				}
				else
				{
					cout << "选择有误!" << endl;
					post_processing();
					return;
				}
			}

			// 判断一开始认定的最小值/最大值是不是计算的最小值或最大值,如果不是,则交互
			if (i != min_or_max)
			{
				Worker* tmp = this->employee_arr[i];  // 记录第i个元素
				this->employee_arr[i] = this->employee_arr[min_or_max];
				this->employee_arr[min_or_max] = tmp;
			}
		}
		cout << "排序成功,排序后的结果为: " << endl;
		this->save();  // 排序后的结果保存到文件中
		this->show_employee();  // 展示所有的职工
	}
}


// 清空文件
void WorkerManager::clean_file() {
	if (this->file_is_empty)
	{
		cout << "文件不存在或记录为空,无须清空!" << endl;
		post_processing();
		return;
	}

	cout << "确定清空文件(此操作无法撤销)? (Y/N)";
	string user_chioce;
	cin >> user_chioce;

	if (user_chioce == "Y" || user_chioce == "y")
	{
		// 清空文件
		ofstream ofs(FILENAME, ios::trunc);  // 删除文件后重新创建文件
		ofs.close();

		if (this->employee_arr != NULL)
		{
			// 删除堆区的每个职工对象
			for (int i = 0; i < this->employee_num; i++)
			{
				if (this->employee_arr[i] != NULL)
				{
					delete this->employee_arr[i];
					this->employee_arr[i] = NULL;
				}
			}

			// 删除堆区的数组指针
			delete[] this->employee_arr;
			this->employee_arr = NULL;
			this->employee_num = 0;
			this->file_is_empty = true;
		}
		cout << "清空成功!" << endl;
		post_processing();
		return;
	}
	else if (user_chioce == "N" || user_chioce == "n")
	{
		cout << "取消清空操作" << endl;
		post_processing();
		return;
	}
	else
	{
		cout << "您的输入有误" << endl;
		post_processing();
		return;
	}
}

employee.h

// 普通员工文件
#pragma once
#include <iostream>
using namespace std;
#include "woker.h"


class Employee : public Worker {
public:
	Employee(int id, string name, int dept_id);  // 构造函数

public:  // 重写接口的纯虚函数,但是.h文件只做声明,不做实现
	// 显示个人信息
	virtual void show_info();

	// 获取岗位名称
	virtual string get_dept_name();
};

employee.cpp

#include "employee.h"


Employee::Employee(int id, string name, int dept_id) {	// 构造函数
	this->id = id;
	this->name = name;
	this->dept_id = dept_id;
}

// 重写接口的纯虚函数
void Employee::show_info() {  // 显示个人信息
	cout << "职工编号: " << this->id << "\t职工姓名: "
		<< this->name << "\t岗位: " << this->get_dept_name()
		<< "\t岗位职责: 完成经理交给的任务" << endl;
}

string Employee::get_dept_name() {  // 获取岗位名称
	return "员工";
}

manager.h

#pragma once
#include <iostream>
using namespace std;
#include "woker.h"


class Manager : public Worker {
public:
	Manager(int id, string name, int dept_id);

public:
	// 显示个人信息
	virtual void show_info();

	// 获取岗位名称
	virtual string get_dept_name();
};

manager.cpp

#include "manager.h"


Manager::Manager(int id, string name, int dept_id) {
	this->id = id;
	this->name = name;
	this->dept_id = dept_id;
}

// 显示个人信息
 void Manager::show_info() {
	 cout << "职工编号: " << this->id << "\t职工姓名: "
		 << this->name << "\t岗位: " << this->get_dept_name()
		 << "\t岗位职责: 完成老板交给的任务,并给员工下发任务" << endl;
}

// 获取岗位名称
string Manager::get_dept_name() {
	// 如果不转换则是C语言的字符串。
	// (不转换也没事,编译器会帮我自动转换的)
	return string("经理");  
}

boss.h

#pragma once
#include <iostream>
using namespace std;
#include "woker.h"


class Boss : public Worker {
public:
	Boss(int id, string name, int dept_id);

public:
	virtual void show_info();

	virtual string get_dept_name();
};

boss.cpp

#include "boss.h"


Boss::Boss(int id, string name, int dept_id) {
	this->id = id;
	this->name = name;
	this->dept_id = dept_id;
}

void Boss::show_info() {
	cout << "职工编号: " << this->id << "\t职工姓名: "
		<< this->name << "\t岗位: " << this->get_dept_name()
		<< "\t岗位职责: 管理公司所有的事务" << endl;
}

string Boss::get_dept_name() {
	return string("老板");
}

postprocessing.h

#pragma once
#include <iostream>
using namespace std;

void post_processing();

postprocessing.cpp

#include "postprocessing.h"


void post_processing() {
	system("pause");
	system("cls");
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值