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

news/2024/11/30 12:35:17/

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

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

目录

  • 1. 内存分区模型
    • 1.1 程序运行前
    • 1.2 程序运行后
      • 1.2.1 栈区
      • 1.2.2 堆区
    • 1.3 new操作符
  • 2. 引用
    • 2.1引用的基本使用
    • 2.2 引用注意事项
    • 2.3 引用做函数参数
    • 2.4 引用做函数返回值
    • 2.5 引用的本质
    • 2.6 常量引用
  • 3. 函数提高
    • 3.1 函数默认参数
    • 3.2 函数占位参数
    • 3.3 函数重载
      • 3.3.1 函数重载概述
      • 3.3.2 函数重载的注意事项
  • 4. 类和对象
    • 4.1 封装
      • 4.1.1 封装的意义
        • 1. 封装意义一:
        • 2. 封装意义二:
      • 4.1.2 struct和class区别
      • 4.1.3 成员属性设置为私有
      • 练习案例1:设计立方体类
      • 练习案例2:点和圆的关系
        • main.cpp文件
        • point.h文件
        • point.cpp文件
        • circle.h文件
        • circle.cpp文件
    • 4.2 对象的初始化和清理
      • 4.2.1 构造函数和析构函数
      • 4.2.2 构造函数的分类及调用
      • 4.2.3 铐贝构造函数调用时机
      • 4.2.4 构造函数调用规则
      • 4.2.5 深拷贝与浅拷贝
      • 4.2.6 初始化列表
      • 4.2.7 类对象作为类成员
      • 4.2.8 静态成员
      • 1. 静态成员变量
      • 2. 静态成员函数
    • 4.3 C++对象模型和this指针
      • 4.3.1 成员变量和成员函数分开存储
      • 4.3.2 this指针概念
      • 4.3.3 空指针访问成员函数
      • 4.3.4 const修饰成员函数
    • 4.4 友元(friend)
      • 4.4.1 全局函数做友元
      • 4.4.2 类做友元
      • 4.4.3 成员函数做友元
    • 4.5 运算符重载
      • 4.5.1 加号运算符重载
      • 4.5.2 左移运算符 << 的重载
      • 4.5.3 递增运算符(++)重载
      • 4.5.4 赋值运算符=重载
      • 4.5.5 关系运算符重载
      • 4.5.6 函数调用运算符()重载 -> 小括号的重载
    • 4.6 继承
      • 4.6.1 继承的基本语法
      • 4.6.2 继承方式
      • 4.6.3 继承中的对象模型
      • 4.6.4 继承中构造Constructor和析构Destructor顺序
      • 4.6.5 继承同名成员处理方式
      • 4.6.6 继承同名静态成员处理方式
      • 4.6.7 多继承语法
      • 4.6.8 菱形继承
    • 4.7 多态
      • 4.7.1 多态的基本概念
      • 4.7.2 多态案例一:计算器类
      • 4.7.3 纯虚函数和抽象类
      • 4.7.4 多态案例二:制作饮品
      • 4.7.5 虚析构和纯虚析构
      • 4.7.6 多态案例三:电脑组装
  • 5. 文件操作
    • 5.1 文本文件
      • 5.1.1 写文件
      • 5.1.2 读文件
    • 5.2 二进制文件
      • 5.2.1 写文件
      • 5.2.2 读文件
  • 6. 职工管理系统
    • 6.1 管理系统需求
    • 6.2 创建管理类
      • 6.2.1 创建文件
      • 6.2.2 头文件实现
      • 6.2.3 源文件实现
    • 6.3 菜单功能
      • 6.3.1 添加成员函数
      • 6.3.2 菜单功能实现
      • 6.3.3 测试菜单功能
    • 6.4 退出功能
      • 6.4.1 提供功能接口
      • 6.4.2 实现退出功能
      • 6.4.3 测试功能
    • 6.5 创建职工类
      • 6.5.1 创建职工抽象类
      • 6.5.2 创建普通员工类
      • 6.5.3 创建经理类
      • 6.5.4 创建老板类
      • 6.5.5 测试多态
    • 6.6 添加职工
      • 6.6.1 功能分析
      • 6.6.2 功能实现
    • 6.7 文件交互:写文件
      • 6.7.1 设定文件路径
      • 6.7.2 成员函数声明
      • 6.7.3 保存文件功能实现
      • 6.7.4 保存文件功能测试
    • 6.8 文件交互:读文件
      • 6.8.1 文件未创建
      • 6.8.2 文件存在且数据为空
      • 6.8.3 文件存在且保存职工数据
        • 6.8.1.1 获取记录的职工人数
        • 6.8.1.2 初始化数组
    • 6.9 显示职工
      • 6.9.1 显示职工函数声明
      • 6.9.2 显示职工函数实现
    • 6.10 删除职工
      • 6.10.1 删除职工函数声明
      • 6.10.2 职工是否存在函数声明
      • 6.10.3 职工是否存在函数实现
      • 6.10.4 删除职工函数实现
    • 6.11 修改职工
      • 6.11.1 修改职工函数声明
      • 6.11.2 修改职工函数实现
    • 6.12 查找职工
      • 6.12.1 查找职工函数声明
      • 6.12.2 查找职工函数实现
    • 6.13 排序
      • 6.13.1 排序函数声明
      • 6.13.2 排序函数实现
    • 6.14 清空文件
      • 6.14.1 清空函数声明
      • 6.14.2 清空函数实现
    • 6.15 所有代码
      • 职工管理系统.cpp
      • work.h
      • workermanager.h
      • workermanager.cpp
      • employee.h
      • employee.cpp
      • manager.h
      • manager.cpp
      • boss.h
      • boss.cpp
      • postprocessing.h
      • postprocessing.cpp

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 = localint 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;  // 10std::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;  // 10std::cout << *p << std::endl;  // 10std::cout << *p << std::endl;  // 10std::cout << *p << std::endl;  // 10std::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;  // 10std::cout << *p << std::endl;  // 10std::cout << *p << std::endl;  // 10std::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;  // 10cout << "b: " << b << endl;  // 10b = 100;cout << "a: " << a << endl;  // 100cout << "b: " << b << endl;  // 100system("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;  // 10cout << "[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;  // 20cout << "[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;  // 20cout << "[swap_fn_03(地址传递)]b3:\t" << b3 << endl;  // 10return 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;  // 10cout << "ref1: " << ref1 << endl;  // 2041751800/** 因为test02中的a是全局变量,程序结束后才会被释放*/int& ref2 = test02();cout << "ref2: " << ref2 << endl;  // 10cout << "ref2: " << ref2 << endl;  // 10// 2. 函数的调用可以作为左值(如果函数的返回值是一个引用,那么这个函数的调用可以作为左值)test02() = 1000;  // 就是一个简单的赋值操作 <=> ref2 = 1000;cout << "ref2: " << ref2 << endl;  // 1000cout << "ref2: " << ref2 << endl;  // 1000return 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;  // 20cout << "ref: " << ref << endl;  // 20/** 在我们使用ref时,编译器发现ref是引用,会自动帮我们解引用*/func(a);cout << "ref: " << ref << endl;  // 100return 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: 1000cout << "b: " << b << endl;  // 1000int c = 100;show_value_02(c);  // val: 100cout << "c: " << c << endl;  // 100return 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// 注意事项2int res_3 = func(1, 2);cout << "res_3: " << res_3 << endl;  // 错误	C2572	“func” : 重定义默认参数: 参数 1system("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 func001func002(10, 20);  // This is a func002func003();  // This is a func003system("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.8319system("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();  // 姓名: 张三      学号: 1stu2.set_name("李四");stu2.set_id(2);stu2.show_info();  // 姓名: 李四      学号: 2system("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的默认权限是publicclass的默认权限是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;  // 100system("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;}// 设置couplevoid 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;  // 年龄: -1system("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×hl \times w \times hl×w×h
      • 其中lll为立方体的长,www为立方体的宽,hhh为立方体的高
  • 分别用全局函数和成员函数判断两个立方体是否相等。
#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的面积为: 600c1的体积为: 1000c2的面积为: 600c2的体积为: 1000[全局函数]c1和c2是相等的![成员函数]c1和c2是相等的!*/system("pause");return 0;
}

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

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

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

在这里插入图片描述

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

两点之间距离=(x1−x2)2+(y1−y2)2两点之间距离=\sqrt{(x_1 - x_2)^2 + (y_1 - y_2)^2}两点之间距离=(x1x2)2+(y1y2)2
两点之间距离2=(x1−x2)2+(y1−y2)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:// 设置xvoid set_x(int xx);// 获取xint get_x();// 设置yvoid set_y(int yy);// 获取yint 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的年龄为: 10p3的年龄为: 10Person1的[析构]函数调用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的年龄为: 10p3的年龄为: 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的年龄为: 20Person03析构构造函数的调用Person03析构构造函数的调用------方法2. 值传递的方式给函数参数传值-------Person03默认构造函数的调用Person03拷贝构造函数的调用Person03析构构造函数的调用Person03析构构造函数的调用------方法3. 值方式返回局部变量-------Person03默认构造函数的调用00FAF9DC00FAF9DCPerson03析构构造函数的调用请按任意键继续. . .*/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() {// 自己定义了拷贝Constructorcout << "---------自己定义了拷贝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: 18p2._age: 18Person04的Destructor函数调用Person04的Destructor函数调用*/// 可以看到,虽然我们没有写拷贝Constructor,但是编译器给我们提供了,所以p22._age == 18test0002();/*Person042的默认Constructor调用p12._age: 18p22._age: 18Person042的Destructor函数调用Person042的Destructor函数调用*/test0003();/*Person043的有参Constructor调用p1._age: 20p2._age: 20Person043的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: 165Person05的Copy Constructor调用p2._age: 18     height: 165Person05的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) {  // 有参Constructorcout << "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调用张三拿着ApplePerson07的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;  // 100Person08 p2;p2._a = 200;cout << "p1._a: " << p1._a << endl;  // 200cout << "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// 下面是不用引用的结果,因为返回的是副本,所以只+10Person02_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){// 如果是空指针,直接结束该方法,不往下走了 -> 提高代码的Robustreturn;}// 剩余要调用成员变量的代码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;  // 创建一个类的空指针pp->show_class_name();  // This is Person03_1 class// 报错原因是空指针->age// p->show_person_age();  // **this** 是 nullptr
}void test03_2() {Person03_2* p = NULL;  // 创建一个类的空指针pp->show_class_name();  // This is Person03_1 classp->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 & DestructorBuilding() {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() {  // Constructorsittingroom = "客厅";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 << pcout << "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;  // 10Person 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 << pcout << "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;  // 1cout << ++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;  // 10cout << 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;  // 2cout << my_int << endl;  // 3cout << (my_int++)++ << endl;  // 3cout << my_int << endl;  // 4(不是我们预想的5,因为返回的是值而不是引用,不是同一个对象)
}int main() {test03_1();int a = 10;cout << a++ << endl;  // 10cout << 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;  // 1cout << ++++my_int << endl;  // 3cout << my_int << endl;  // 3// 1.2 重载后置++运算符cout << my_int++ << endl;  // 3cout << my_int++++ << endl;  // 4cout << my_int << endl;  // 5
}void test_sub_sub() {MyInteger my_int;cout << "---------重载--运算符---------" << endl;// 重载<<运算符cout << my_int << endl;  // 0// 2.1 重载前置--运算符cout << --my_int << endl;  // -1cout << ----my_int << endl;  // -3cout << my_int << endl;  // -3// 2.2 重载后置--运算符cout << my_int-- << endl;  // -3cout << my_int---- << endl;  // -4cout << 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:// ConstructorPerson04(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: 18cout << "p2.age: " << *p2.getter_age() << endl;  // p2.age: 20/*赋值操作 -> 堆区内存重复释放,程序崩溃!解决方案:利用DeepCopy解决浅拷贝带来的问题(在使用=运算符时,各自开辟空间,Destructor时各自释放各自的)*/p2 = p1;  // 赋值操作cout << "p1.age: " << *p1.getter_age() << endl;  // p1.age: 18cout << "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: 18cout << "p2.age: " << *p2.getter_age() << endl;  // p2.age: 18cout << "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: 10cout << "b: " << b << endl;  // b: 10cout << "c: " << c << endl;  // c: 10cout << "-------------test04_2-------------" << endl;test04_2();/*-------------test04_1-------------p1.age: 18p2.age: 20p1.age: 18p2.age: 18Person04的Destructor调用Person04的Destructor调用-------------c = b = a-------------a: 10b: 10c: 10-------------test04_2-------------p1.age: 18p2.age: 18p3.age: 18Person04的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:  // getterstring 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 继承中的对象模型.cppclass Son       size(16):+---0      | +--- (base class Base)0      | | a4      | | b8      | | 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: 200cout << "父类的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: 200cout << "[Base] a: " << son.Base06::a << endl;  // [Base] a: 100// 访问方式2:通过类名访问静态数据cout << "--------访问方式2:通过类名访问静态数据--------" << endl;cout << "[Son] a: " << Son06::a << endl;  // [Son] a: 200cout << "[Base] a: " << Son06::Base06::a << endl;  // [Base] a: 100// 访问方式3:通过匿名对象访问静态数据cout << "--------访问方式3:通过匿名对象访问静态数据--------" << endl;cout << "[Son] a: " << Son06().a << endl;  // [Son] a: 200cout << "[Base] a: " << Son06().Base06::a << endl;  // [Base] a: 100cout << "[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      | c12      | d+---*/// cout << "a: " << son.a << endl;  // "SonO7::a"不明确// 当父类VS出现同名成员,需要加作用域区分(因此在实际开发中不太建议使用多继承!)cout << "a: " << son.Base07_1::a << endl;  // a: 100cout << "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: 18cout << "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: 22cout << "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      | 01      | 8 (Alpacad(Sheep+0)Animal)  // 这里的8是偏移量(Sheep的vbptr+8就可以找到age)Alpaca::$vbtable@Camel@:0      | 01      | 4 (Alpacad(Camel+0)Animal)  // 这里的4是偏移量(Camel的vbptr+4就可以找到age)vbi:       class  offset o.vbptr  o.vbte fVtorDispAnimal       8       0       4 0vbptr: 虚基类指针(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成员函数前加关键字virtualdo_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|  00      | &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|  00      | &Animal::speak

在这里插入图片描述

3、Cat类重写speak虚函数:

class Cat       size(4):+---0      | +--- (base class Animal)0      | | {vfptr}| +---+---Cat::$vftable@:| &Cat_meta|  00      | &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;  // 操作数1int 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;  // 最小值或最大值的indexfor (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,不存在则返回-1int 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_emptythis->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;  // 最小值或最大值的indexfor (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");
}

http://www.ppmy.cn/news/34561.html

相关文章

【SpringBoot入门】SpringBoot的配置

SpringBoot的配置文件一、SpringBoot配置文件分类二、yaml 概述三、多环境配置四、Value 和 ConfigurationProperties五、总结一、SpringBoot配置文件分类 SpringBoot 是基于约定的&#xff0c;很多配置都是默认的&#xff08;主方法上SpringBootApplication注解的子注解Enabl…

springboot校友社交系统

050-springboot校友社交系统演示录像开发语言&#xff1a;Java 框架&#xff1a;springboot JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09; 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;e…

Android APP检查设备是否为平板

正文 Android APP判断设备是否为平板的三种方法&#xff1a; 通过屏幕尺寸判断。一般来说&#xff0c;平板电脑的屏幕尺寸比手机大很多&#xff0c;可以根据屏幕的长宽比和尺寸等信息来区分设备类型。通过屏幕像素密度判断。一般来说&#xff0c;平板电脑的屏幕像素密度比手机…

从零开始搭建游戏服务器 第一节 创建一个简单的服务器架构

目录引言技术选型正文创建基础架构IDEA创建项目添加Netty监听端口编写客户端进行测试总结引言 由于现在java web太卷了&#xff0c;所以各位同行可以考虑换一个赛道&#xff0c;做游戏还是很开心的。 本篇教程给新人用于学习游戏服务器的基本知识&#xff0c;给新人们一些学习…

嵌入式学习笔记——SysTick(系统滴答)

系统滴答前言SysTick概述SysTick是个啥SysTick结构框图1. 时钟选择2.计数器部分3.中断部分工作一个计数周期&#xff08;从重装载值减到0&#xff09;的最大延时时间工作流程SysTick寄存器1.控制和状态寄存器SysTick->CTRL2.重装载值寄存器SysTick->LOAD3.当前值寄存器Sy…

【CodeForces】Codeforces Round 859 (Div. 4) D

嘿嘿嘿&#xff0c;CF虐我千百遍&#xff0c;我待CF如初见&#xff01; &#xff08;doge&#xff09; 目录 题目含义&#xff1a; 前缀和&#xff1a; 代码 &#x1f386;音乐分享&#xff08;点击链接可以听哦&#xff09; A Hundred Miles&#xff08;一百英里&#xff09;…

HAL库 STM32 串口通信

一、实验条件将STM32的PA9复用为串口1的TX&#xff0c;PA10复用为串口1的RX。STM32芯片的输出TX和接收RX与CH340的接收RX和发送TX相连&#xff08;收发交叉且PCB上默认没有相连&#xff0c;所以需要用P3跳线帽进行手动连接&#xff09;&#xff0c;CH340的另一端通过USB口引出与…

用嘴写代码?继ChatGPT和NewBing之后,微软又开始整活了,Github Copilot X!

用嘴写代码&#xff1f;继ChatGPT和NewBing之后&#xff0c;微软又开始整活了&#xff0c;Github Copilot X&#xff01; AI盛行的时代来临了&#xff0c;在这段时间&#xff0c;除了爆火的GPT3.5后&#xff0c;OpenAI发布了GPT4版本&#xff0c;同时微软也在Bing上开始加入了A…