本阶段主要针对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.1 程序运行前
在程序编译后,生成了exe
可执行程序,未执行该程序前分为两个区域:
- 代码区:
- 存放CPU执行的机器指令
- 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
- 代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令
- 全局区:
- 全局变量和静态变量存放在此
- 全局区还包含了常量区,字符串常量和其他常量(
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;
}
总结:
- 堆区教据由程序员管理开辟和释放
- 堆区数据利用
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++中,函数的形参列表中的形参是可以有默认值的。
语法:返回值类型 函数名 (参数 = 默认值) {}
注意事项:
- 如果某个位置已经有了默认参数,那么从这个位置往后,从左到右都必须有默认值(和Python是一样的)
- 如果函数的声明有了默认参数,那么函数的实现就不能有默认参数了 -> 函数的声明和实现只能有一个有默认参数
- 声明有默认值,实现没有默认值 -> √
- 声明没有默认值,实现有默认值 -> √
- 声明有默认值,实现也有默认值 -> ×
函数声明默认值 | 函数实现默认值 | 结论 |
---|---|---|
有 | 没有 | 可以 |
没有 | 有 | 可以 |
有 | 有 | 不可以 |
示例:
#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 函数重载概述
作用:函数名可以相同,提高复用性。
函数重载满足条件:
- 同一个作用域下
- 函数名称相同
- 以下三个至少满足一个:
- 参数类型不同、
- 参数个数不同
- 参数顺序不同
注意:函数的返回值不可以作为函数重载的条件。
示例:
#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 函数重载的注意事项
- 引用作为重载条件
- 函数重载碰到函数默认参数
示例:
#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. 封装意义二:
类在设计时,可以把属性和行为放在不同的权限下,加以控制。
访问权限有三种:
public
公共权限:成员在类内可以访问,类外也可以访问protected
保护权限:成员在类内可以访问,在类外不可以访问,子类也可以方法private
私有权限:成员在类内可以访问,类外不可以访问,子类不可以访问
权限 | 类内 | 类外 | 子类是否可以访问 |
---|---|---|---|
public | √ | √ | √ |
protected | √ | × | √ |
private | √ | × | × |
protected
和private
现在看不出区别,具体是在继承的时候可以提现二者的区别。
#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++中struct
和class
唯一的区别就在于默认的访问权限不同区别:
struct
默认权限为公共class
默认权限为私有
在C++中
class
和struct
没有什么太大的区别,都可以定义一个类,知识默认的访问权限不同。
#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}两点之间距离=(x1−x2)2+(y1−y2)2
两点之间距离2=(x1−x2)2+(y1−y2)2两点之间距离^2=(x_1 - x_2)^2 + (y_1 - y_2)^2两点之间距离2=(x1−x2)2+(y1−y2)2
文件拆分注意事项:
- 在
.h
头文件中,只做声明,不做具体实现 - 在
.cpp
文件中做具体实现,且删除声明并引用.h
头文件 - 在
.cpp
文件中需要加上对应的作用域,例如Point::
- 拆分完毕后需要在
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):主要作用在于对象销毁前系统自动调用,执行一些清理工作。
构造函数负责创建
析构函数负责销毁构造和析构是反义词
构造函数语法:类名() {}
- 构造函数,没有返回值也不写
void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数语法:~类名() {}
- 析构函数,没有返回值也不写
void
- 函数名称与类名相同,在名称前加上符号
~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无须手动调用,且只会调用一次
示例:
#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 构造函数的分类及调用
两种分类方式:
- 按参数分为:有参构造和无参构(默认构造)
- 造按类型分为:普通构造和拷贝构造
三种调用方式:
- 括号法
- 显示法
- 隐式转换法
这三种方式都可以,推荐使用前两个
注意事项:
- [括号法]:调用默认构造函数时,不要加
()
。- 例如:
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++中拷贝构造函数调用时机通常有三种情况:
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值的方式返回局部对象
示例:
#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个函数:
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
- 如果用户定义有参构造函数,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
对象时,A
与B
的构造(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
,称为静态成员。
静态成员分为:
- 静态成员变量的特点:
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 在全局区
- 类内声明,类外初始化(必须初始化)
- 静态成员函数的特点:
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
静态成员变量和静态成员函数都是有访问权限的,
private
和protected
权限在类外访问不到。
1. 静态成员变量
静态成员变量的特点:
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 在全局区
- 类内声明,类外初始化(必须初始化)
静态成员变量的声明和初始化方式:
- 声明方式:[类内]
static int 静态成员变量名;
- 初始化方式:[类外]
int 类名::静态成员变量名 = xxx;
在使用静态成员变量时,必须在类外初始化!
访问方式:
- 通过对象进行访问:
obj.静态成员变量;
- 通过类名进行访问(和Python很像):
Object::静态成员变量;
静态成员变量和静态成员函数都是有访问权限的,
private
和protected
权限在类外访问不到。
#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. 静态成员函数
静态成员函数的特点:
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量(非静态成员变量是访问不了的)
- 原因:[非静态成员变量]必须创建对象才可以访问,而[静态成员函数]是可以通过类名直接调用的,直接调用时就不知道到底调用(修改)哪个对象的[非静态成员变量],所以不可以。
- 而[静态成员变量]因为是共享的(只有一份),所以[静态成员函数]就知道调用(修改)哪个了。
访问方式:
- 通过对象进行访问:
obj.静态成员函数;
- 通过类名进行访问(和Python很像):
Object::静态成员函数;
静态成员变量和静态成员函数都是有访问权限的,
private
和protected
权限在类外访问不到。
#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字节
#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修饰成员函数
const
和static
关键字要分清
-
成员函数后加
const
后我们称为这个函数为常函数 -
声明对象前加
const
称该对象为常对象 -
常函数:
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字
mutable
后,在常函数中依然可以修改
-
常对象:
- 常对象只能调用常函数
mutable
: 英[ˈmjuːtəbl] 美[ˈmjuːtəbl] adj. 可变的; 会变的;
语法:
- 常函数:
返回值类型 函数名() const {函数内容}
—— 例:void fn() const {}
- 常对象:
const 类型 实例对象名;
—— 例:const Person p;
- 可变变量:
mutable 数据类型 变量名;
—— 例:mutable int a;
this指针的本质是指针常量,即指针的指向是不可以修改的(指向地址的值是可以修改的)! 等价于 Person* const this;
那么this = NULL;
// 这是不可以的,指针常量的指向是不可以修改的!
Q:既然this
指针的指向不可以改,我们是否可以限制它指向地址的内容也不允许修改呢?
A:当然是可以的,用const
修饰成员方法即可,那么const
关键字放在哪里?
- 放在
返回值类型
前面? —— 修饰的是返回值了,不行 - 放在
(形参列表)
中? —— 修饰的是形参了,不行
因此没有办法,放在了成员函数()
的后面,即返回值类型 函数名() 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
友元的三种实现:
- 全局函数做友元
- 类做友元
- 成员函数做友元
语法:
- 全局函数做友元:在类中使用
friend
关键字声明全局函数,friend 返回值类型 全局函数名(形参列表);
(跟函数的声明是一样的,只不过前面加个friend
关键字)。 —— 例:friend void good_gay(Building& building);
- 类做友元:在类中使用
friend
关键字声明友元类,friend class 友元类名;
—— 例:friend class GoodGay;
- 成员函数做友元:在类中使用
friend
关键字声明友元成员函数,friend 类名::成员方法(形参列表);
—— 例:friend GoodGay::visit();
注意:
- 全局函数做友元必须写完整!
- 类做友元只写类名即可。
- 成员函数做友元也要写完整!
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 加号运算符重载
作用:实现两个自定义数据类型相加的运算。
语法有两种:
- 成员函数重载+号:
返回值类型 operator+(形参列表) {函数代码}
- 全局函数重载+号:
返回值类型 operator+(形参列表) {函数代码}
- 运算符重载的函数重载:
返回值类型 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+,所以我们可以直接使用 + 而不用再调用函数那么麻烦了。
重载运算符的本质:
- 成员函数重载+号的本质:
Person01 p3 = p1 + p2; 等价于 Person01 p3 = p1.operator+(p2);
- 全局函数重载+号的本质:
Person01 p3 = p1 + p2; 等价于 Person01 p3 = operator+(p1, p2);
注意:①成员函数重载+号和②全局函数重载+号不能都写了,因为都写了不满足函数重载的条件:[1]参数类型不同 || [2]参数个数不同 || [3]参数顺序不同。 —— 有多个运算符"+”与这些操作数匹配。
这里应该是成员函数会被编译器转换,所以二者会重复。
总结:
- 对于内置的数据类型(
int/double/float...
)的表达式的运算符是不可能改变的(我们上面说的都是自定义的数据类型) - 不要滥用运算符重载
代码示例:
#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;
}
注意:
- 通常情况下,我们不会利用成员函数重载
<<
运算符,因为无法实现cout
在左侧! —— 只能利用全局函数重载<<
运算符。 cout
的数据类型是ostream
(可以ctrl+左键看一下cout
的定义)。- 在使用
cout
时,一般会使用链式编程,所以重载<<
运算符时应该返回cout
的数据类型(即ostream
数据类型)。 - 重载
<<
时,我们可以会用到类的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 递增运算符(++)重载
作用:通过重载递增(++
)运算符,实现自己的整型数据。
递增运算符++
的位置不同,起到的效果也不同:
- 前置递增
- 后置递增
// 前置递增
int a = 10;
cout << ++a << endl; // 11// 后置递增
int b = 10;
cout << b++ << endl; // 10
cout << b << endl; // 11
注意:
- 前置递增:重载时,返回值类型需加上引用
&
- 后置递增:
- 重载时,返回值类型不用加上引用
&
- 形参必须写
int占位符
- 重载时,返回值类型不用加上引用
返回引用的目的是实现一直对一个对象进行操作。
而返回值并不是我们想象中的那样,返回一个数字,而是由函数返回值的数据类型决定的,可能是int/double/float/long...
,也可能是一个类(class
)。
原因:
-
前置递增:
- 如果返回值类型不是引用,会copy一份儿,那操作的对象就会变了。
- 那么就会发生一个问题:
cout << ++my_int << endl; // 1
cout << ++my_int << endl; // 1(还是1,因为不是原对象的)
-
后置递增:
- 如果不写
(int)
那么编译器会认为[前置递增重载函数]和[后置递增重载函数]发生了重定义(不满足函数重载,所以会引发二义性)int
是一个占位参数,目的:- 满足函数重载的条件;
- 可以用于区分前置和后置。
- 前置递增返回的是引用,但后置递增返回的是值(也是一个数据类型)。
- 因为如果我们返回的是引用,返回的是[临时变量]的引用,那么后续的操作就是非法的([临时变量]会被编译器自动销毁)
- 因为我们返回的值,这个值的数据类型是一个类,所以还是可以继续实现链式编程的
- 但这样也会带来一个问题:我们无法先前置递增那样,可以无限前置递增:
++(++my_int)
。当使用(my_int++)++
时,由于返回的是值,不是一直操作同一个对象,所以(my_int++)++
等价于my_int++
。 - 但这其实并不是一个问题,我们看一下下面的代码:
我们可以发现,在C++原生代码中,后置++就是不可以无限套娃的,直接会提示语法错误,所以我们写的代码没有问题!int a = 10; cout << a++ << endl; // 10 cout << a << endl; // 11 // cout << (a++)++ << endl; // 表达式必须是可修改的左值 // cout << a << endl;
- 如果不写
总结:前置递增返回引用,后置递增返回值。
代码示例:
#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个函数:
- 默认构造函数Constructor(无参,函数体为空)
- 默认析构函数Destructor(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝。需要注意的是,编译器提供的浅拷贝而非深拷贝,因此可能会引发一些问题,典型是重复释放相同的内存。
- 赋值运算符
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 子类 : 继承方式 父类 {};
继承方式一共有三种:
- 公共继承 ——
public
- 保护继承 ——
protected
- 私有继承 ——
private
通过继承,父类的属性在子类中权限的变化如下:
- 公共继承 ——
public
:访问不到private
,剩下的不变 - 保护继承 ——
protected
:访问不到private
,剩下的都变为protected
- 私有继承 ——
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)查看对象模型:
- 跳转盘符:
C:
- 跳转文件路径:
cd 具体路径
- 查看命令:
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 继承同名成员处理方式
问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?
- 访问子类同名成员直接访问即可:
子类.成员函数()/成员属性
- 访问父类同名成员需要加作用域:
子类.父类::成员函数()/成员属性
注意:如果子类中出现和父类同名的成员函数,那么子类的同名成员函数会隐藏掉所有父类的同名成员函数,即不能直接访问,想访问需要加作用域。
总结:
- 子类对象可以直接访问到子类中同名成员
- 子类对象加作用域可以访问到父类同名成员
- 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
直接访问的是自身的属性,访问父类就加上作用域。
示例:
#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 菱形继承
菱形继承概念:
- 两个派生类(子类)继承同一个基类(父类)
- 又有某个类同时继承者两个派生类
- 这种继承被称为菱形继承,或者钻石继承
典型的菱形继承案例:
举个例子:
菱形继承的问题:
- 羊继承了动物的数据,驼同样继承了动物的数据,当羊驼使用数据时,就会产生二义性。
- 羊驼继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
解决方案:利用虚继承可以解决菱形继承带来的数据重复问题!
语法:在继承方式前加上关键字virtual即可。
class Sheep : virtual public Animal{};
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++面向对象三大特性之一。多态分为两类:
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
C++的中的多态,大多都是动态多态。
静态多态和动态多态区别:
- 静态多态的函数地址早绑定(编译阶段确定函数地址)
- 动态多态的函数地址晚绑定(运行阶段确定函数地址)
动态多态的满足条件:
- 有继承关系
- 子类要重写父类的虚函数
重写和重载不一样,重载需要满足三个条件,但重写和原函数是一模一样的
子类写不写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
在堆区开辟内存
解决方式:将父类中的析构函数改为虚析构或者纯虚析构。
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
- 虚析构语法:
virtual ~类名() {}
- 纯虚析构语法:
virtual ~类名() = 0;
其中纯虚析构需要相应的代码实现,语法:类名::~类名() {}
因为子类在继承父类时,会调用父类的构造函数和析构函数。如果父类的析构函数是纯虚析构函数,因为没有具体实现,那么最后父类在调用时会报错,因此父类的纯虚析构函数需要额外的实现。
虚析构函数就是用来解决通过父类指针释放子类对象。
总结:
- 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
- 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
- 拥有纯虚析构函数的类也属于抽象类
示例:
#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,文件流
文件类型分为两种:
- 文本文件:文件以文本的ASCII码形式存储在计算机中
- 二进制文件:文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们
我们常见的
*.bin
就是二进制文件
操作文件的三大类:
ofstream
:写操作(不能读) —— o -> outputifstream
:读操作(不能写) —— i -> inputfstream
:读写操作(既可以读也可以写)
一般我们用
fstream
就行
这里为什么output是写,input是读呢?是不是反了?其实不然,我们之前理解的视角不同,这里的output和input是针对编译器而言的。对于IDE来说,将结果写到一个文件中,不就输出嘛(output);将文件中的信息写到IDE中,不就是输入嘛(input)。
这里说的IDE并不是说Visual Studio,而是
*.cpp
文件
5.1 文本文件
5.1.1 写文件
写文件步骤如下:
- 包含头文件:
#include <fstream>
- 创建流对象:
ofstream ofs;
- 打开文件:
ofs.open("文件路径", 打开方式);
- 写数据:
ofs << "写入的数据";
- 关闭文件:
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 读文件
读文件与写文件步骤相似,但是读取方式相对于比较多。
读文件步骤如下:
- 包含头文件:
#include<fstream>
- 创建流对象:
ifstream ifs;
- 打开文件并判断文件是否打开成功:
ifs.open("文件路径", 打开方式);
- 读数据:四种方式读取
- 关闭文件:
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++来实现一个基于多态的职工管理系统。
公司中职工分为三类:普通员工、经理、老板。显示信息时,需要显示职工编号、职工姓名、职工岗位、以及职责。
- 普通员工职责:完成经理交给的任务
- 经理职责:完成老板交给的任务,并下发任务给员工
- 老板职责:管理公司所有事务
管理系统中需要实现的功能如下:
- 退出管理程序:退出当前管理系统
- 增加职工信息:实现批是添加职工功能,将信息录入到文件中,职工信息为:职工编号、姓名、部门编号
- 显示职工信息:显示公司内部所有职工的信息
- 删除离职职工:按照编号删除指定的职工
- 修改职工信息:按照编号修改职工个人信息
- 查找职工信息:按照职工的编号或者职工的姓名进行查找相关的人员信息
- 按照编号排序:按照职工编号,进行排序,排序规则由用户指定
- 清空所有文档:清空文件中记录的所有职工信息(清空前需要再次确认,防止误删)
6.2 创建管理类
管理类负责的内容如下:
- 与用户的沟通菜单界面
- 对职工增删改查的操作
- 与文件的读写交互
6.2.1 创建文件
在头文件和源文件的文件夹下分别创建workerManager.h
和workerManager.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.h
和employee.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.h
和manager.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.h
和boss.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 文件交互:读文件
功能描述:将文件中的内容读取到程序中。
虽然我们实现了添加职工后保存到文件的操作,但是每次开始运行程序,并没有将文件中数据读取到程序中。而我们的程序功能中还有清空文件的需求,因此构造函数初始化数据的情况分为三种:
- 第一次使用,文件未创建
- 文件存在,但是数据被用户清空
- 文件存在,并且保存职工的所有数据
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");
}