本阶段主要针对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++
。 - 但这其实并不是一个问题,我们看一下下面的代码:
int a = 10; cout << a++ << endl; // 10 cout << a << endl; // 11 // cout << (a++)++ << endl; // 表达式必须是可修改的左值 // cout << a << endl;
我们可以发现,在C++原生代码中,后置++就是不可以无限套娃的,直接会提示语法错误,所以我们写的代码没有问题!
- 如果不写
总结:前置递增返回引用,后置递增返回值。
代码示例:
#include <iostream>
using namespace std;
#include <string>// 自定义整型
class MyInteger {// 声明友元friend ostream& operator<<(ostream& cout, MyInteger my_int);public:MyInteger() {this->num = 0;}// 重载前置++运算符MyInteger& operator++() {/*如果返回值类型不是引用,会copy一份儿,那操作的对象就会变了。那么就会发生一个问题:cout << ++my_int << endl; // 1cout << ++my_int << endl; // 1(还是1,因为不是原对象的)返回引用的目的是实现一直对一个对象进行操作*/// 先进行++运算++this->num;// 再将返回自身以满足链式编程return *this;}// 重载后置++运算符/*如果不写(int)那么编译器会认为两个函数发生了重定义(不满足函数重载,所以会引发二义性)int是一个占位参数,目的:①满足函数重载的条件;②可以用于区分前置和后置前置递增返回的是引用,但后置递增返回的是值。因为如果我们返回的是引用,返回的是tmp的引用,那么后续的操作就是非法的(tmp会被编译器自动销毁)因为我们返回的值,这个值的数据类型是一个类,所以还是可以继续实现链式编程的但这样也会带来一个问题:我们无法先前置递增那样,可以无限前置递增:++(++my_int)当使用(my_int++)++时,由于返回的是值,不是一直操作同一个对象,所以(my_int++)++ 等价于 my_int++但这其实并不是一个问题,我们看一下下面的代码:int a = 10;cout << a++ << endl; // 10cout << a << endl; // 11// cout << (a++)++ << endl; // 表达式必须是可修改的左值// cout << a << endl;我们可以发现,在C++原生代码中,后者++就是不可以无限套娃的,直接会提示语法错误,所以我们写的代码没有问题!*/MyInteger operator++(int) {// 先 记录当时的结果MyInteger tmp = *this;// 后 递增num++;// 最后 将记录的结果做返回操作return tmp;}private:int num;
};// 重载左移运算符
ostream& operator<<(ostream& cout, MyInteger my_int) {cout << my_int.num;return cout;
}void test03_1() {MyInteger my_int;// 重载<<运算符之前// cout << my_int << endl; // 没有与这些操作数匹配的"<<"运算符// 重载<<运算符之后cout << my_int << endl; // 0// 重载前置++运算符之前// cout << ++my_int << endl; // 没有与这些操作数匹配的"++"运算符// 重载前置++运算符之后cout << ++(++my_int) << endl; // 2// 重载后置++运算符之前// cout << my_int++ << endl; // 没有与这些操作数匹配的"++"运算符// 重载后置++运算符之后cout << my_int++ << endl; // 2cout << my_int << endl; // 3cout << (my_int++)++ << endl; // 3cout << my_int << endl; // 4(不是我们预想的5,因为返回的是值而不是引用,不是同一个对象)
}int main() {test03_1();int a = 10;cout << a++ << endl; // 10cout << a << endl; // 11// cout << (a++)++ << endl; // 表达式必须是可修改的左值// cout << a << endl;system("pause");return 0;
}
递增运算符和递减运算符重载代码:
#include <iostream>
using namespace std;
#include <string>class MyInteger {// 声明友元friend ostream& operator<<(ostream& cout, MyInteger my_int);public:// 1.1 重载前置++运算符MyInteger& operator++() {++this->num;return *this;}// 1.2 重载后置++运算符MyInteger operator++(int) {MyInteger tmp = *this;this->num++;return tmp;}// 2.1 重载前置--运算符MyInteger& operator--() {--this->num;return *this;}// 2.2 重载后置--运算符MyInteger operator--(int) {MyInteger tmp = *this;this->num--;return tmp;}public:MyInteger() {this->num = 0;}private:int num;
};// 重载<<运算符
ostream& operator<<(ostream& cout, MyInteger my_int) {cout << my_int.num;return cout;
}void test_plus_plus() {MyInteger my_int;cout << "---------重载++运算符---------" << endl;// 重载<<运算符cout << my_int << endl; // 0// 1.1 重载前置++运算符cout << ++my_int << endl; // 1cout << ++++my_int << endl; // 3cout << my_int << endl; // 3// 1.2 重载后置++运算符cout << my_int++ << endl; // 3cout << my_int++++ << endl; // 4cout << my_int << endl; // 5
}void test_sub_sub() {MyInteger my_int;cout << "---------重载--运算符---------" << endl;// 重载<<运算符cout << my_int << endl; // 0// 2.1 重载前置--运算符cout << --my_int << endl; // -1cout << ----my_int << endl; // -3cout << my_int << endl; // -3// 2.2 重载后置--运算符cout << my_int-- << endl; // -3cout << my_int---- << endl; // -4cout << my_int << endl; // -5
}int main() {test_plus_plus();test_sub_sub();system("pause");return 0;
}
4.5.4 赋值运算符=重载
C++编译器至少给一个类添加4个函数:
- 默认构造函数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");
}
查看全文
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.dgrt.cn/a/2122438.html
如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!
相关文章:
[学习笔记] 2. C++ / CPP核心编程
本阶段主要针对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 引用做函数返回……
37 UnitTest框架 – 生成HTML测试报告
目录
一、生成HTML测试报告
1、什么是HTML测试报告
2、生成测试报告的作用
3、HTMLTestReport
4、实例化HTMLTestReport对象
5、生成HTML测试报告的步骤
6、生成HTML格式测试报告示例
7、使用相对路径存放测试报告可能存在的问题
8、获取项目的路径
9、使用绝对路径生……
基于Springboot和Mybatis的外卖项目 瑞吉外卖Day6
瑞吉外卖Day6
移动端登录功能
一、移动端登录优点及流程
手机验证码登录的优点:方便快捷,无需注册,直接登录登录流程:输入手机号>获取验证码>输入验证码>点击登录>登录成功
注意:通过手机验证码登录&a……
List<Map<String,Object>>循环List,获取Map对应的Value进行操作(一)
基础练习:
package com.itheima;import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;public class Utils {public static void main(String[] args) {//创建集合对象List<Map<String,Object>> list new ArrayL……
sheetJS中合并单元格用首格数据填充
import { read, utils, writeFileXLSX } from xlsxconst fillMerge (sheet) >{// 遍历合并单元格sheet[!merges].forEach(item >{// 保存合并的起始格子的信息,后面的格子都填充得和他一样let v sheet[utils.encode_cell(item.s)]for(let c item.s.c; c &l……
L3-022 地铁一日游
团体天梯 L3-022 地铁一日游 L3-022 地铁一日游 (30 分)
森森喜欢坐地铁。这个假期,他终于来到了传说中的地铁之城——魔都,打算好好过一把坐地铁的瘾!
魔都地铁的计价规则是:起步价 2 元,出发站与到达站的最短距离&……
Python 把String–>int
# 把字符串映射为数字,例如{female:1, male:0} df_map {} # 保存映射关系 cols df.columns.values print(cols:,cols) for col in cols: if df[col].dtype ! np.int64 and df[col].dtype ! np.float64: temp {} x 0 for ele in se……
python 实现 ls -l | wc -l 生成日志 bash: ll: command not found
def get_janus_play_count(path,logname,log):#打开文件#path /var/log/20220106/ files os.listdir(path)filesos.chdir(path) #进入该目录#日志logfile log#janus_play_count_ca.log #读取条数comdls -l | wc -l #ll -l 就是不……
redis 安装报错
redis 安装报错 jemalloc/jemalloc.h: No such file or directory。
对于redis安装的这个错误,我在博客redis 安装 与错误解决办法最后有提及,但是网上大部分文章的对这个问题的解答都是有误的。所以在这里单列出来。
错误内容: jemalloc/j……
【WPF学习笔记】WPF 中使用附加属性解决 PasswordBox 的数据绑定问题
WPF 中使用附加属性解决 PasswordBox 的数据绑定问题1、前言2、实现步骤3、完整代码3.1、页面代码3.2、数据绑定辅助类 LoginPasswordBoxHelper3.3、其它代码4、附加功能:输入框添加水印5、效果展示1、前言
在 WPF 开发中 View 中的数据展示我们常通过 Binding 进行……
反序列化渗透与攻防(五)之shiro反序列化漏洞
Shiro反序列化漏洞
Shiro介绍
Apache Shiro是一款开源安全框架,提供身份验证、授权、密码学和会话管理。Shiro框架直观、易用,同时也能提供健壮的安全性
Apache Shiro 1.2.4及以前版本中,加密的用户信息序列化后存储在名为remember-me的Cookie中。攻击者可以使用Shiro的默……
vue2+vue3
vue2vue3尚硅谷vue2vue2 课程简介【02:24】vue2 Vue简介【17:59】vue2 Vue官网使用指南【14:07】vue2 搭建Vue开发环境【13:54】vue2 Hello小案例【22:25】了解: 不常用常用:id 更常用 简单class差值总结vue 实例vue 模板 : 先 取 ࿰……
【hello Linux】环境变量
目录 1. 环境变量的概念 2. 常见的环境变量 3. 查看环境变量 4. 和环境变量相关的命令 5. 环境变量的组织方式 6. 通过代码获取环境变量 7. 通过系统调用获取环境变量 Linux🌷 在开始今天的内容之前,先来看一幅图片吧! 不知道你们是否和我一……
【Linux基础】常用命令整理
ls命令
-a选项,可以展示隐藏的文件和文件夹-l选项,以列表形式展示内容-h,需要和-l搭配使用,可以展示文件的大小单位ls -lah等同于la -a -l -h
cd命令(change directory)
语法:cd [Linux路径]……
客快物流大数据项目(一百一十二):初识Spring Cloud
文章目录
初识Spring Cloud
一、Spring Cloud简介
二、SpringCloud 基础架构图…
C和C++中的struct有什么区别
区别一: C语言中: Struct是用户自定义数据类型(UDT)。 C语言中: Struct是抽象数据类型(ADT),支持成员函数的定义。
区别二:
C中的struct是没有权限设置的,……
docker的数据卷详解
数据卷 数据卷是宿主机中的一个目录或文件,当容器目录和数据卷目录绑定后,对方修改会立即同步
一个数据卷可以同时被多个容器同时挂载,一个容器也可以被挂载多个数据卷
数据卷作用:容器数据持久化 /外部机器和容器间接通信 /容器……
13、Qt生成dll-QLibrary方式使用
Qt创建dll,使用QLibrary类方式调用dll
一、创建项目
1、新建项目->其他项目->Empty qmake Project->Choose 2、输入项目名,选择项目位置,下一步 3、选择MinGW,下一步 4、完成 5、.pro中添加TEMPLATE subdirsÿ……
基于mapreduce 的 minHash 矩阵压缩
Minhash作用: 对大矩阵进行降维处理,在进行计算俩个用户之间的相似度。
比如: 俩个用户手机下载的APP的相似度,在一个矩阵中会有很多很多的用户要比较没俩个用户之间的相似度是一个很大的计算任务 如果首先对这个矩阵降维处理&am……
关于hashmap使用迭代器的问题
keySet获得的只是key值的集合,valueSet获得的是value集合,entryset获得的是键值对的集合。 package com.test2.test;import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;public class mapiterator……
编程日记2023/4/16 14:50:37