本阶段主要针对C++面向对象编程技术做详细讲解,探讨C++中的核心和精髓。
面向对象是一种编程思想。
目录
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 = local
int c_l_b = 10;
cout << "局部常量c_l_a的地址为:\t" << (int) &c_l_a << endl;
cout << "局部常量c_l_b的地址为:\t" << (int) &c_l_b << endl;
/*
局部变量a的地址为: 8388048
局部变量b的地址为: 8388036
全局变量g_a的地址为: 3850240
全局变量g_b的地址为: 3850244
静态变量s_a的地址为: 3850248
静态变量s_b的地址为: 3850252
字符串常量的地址为: 3841016
全局常量c_g_a的地址为: 3840816
全局常量c_g_b的地址为: 3840820
局部常量c_l_a的地址为: 8388024
局部常量c_l_b的地址为: 8388012
*/
system("pause");
return 0;
}
总结:
- C++中在程序运行前分为全局区和代码区
- 代码区特点是共享和只读
- 全局区中存放全局变量、静态变量、常量
- 常量区中存放
const
修饰的全局常量和字符串常量
1.2 程序运行后
1.2.1 栈区
- 由编译器自动分配释放,存放函数的参数值,局部变量等
- 注意事项:不要返回局部变量的地址,因为栈区开辟的数据由编译器自动释放
示例:
#include <iostream>
using namespace std;
/*
* 栈区的数据由编译器管理开辟和释放
* 注意:不要返回局部变量的地址
*/
int* func1() {
int a = 10; // 局部变量,存放在栈区。栈区的数据在函数执行完之后自己释放
return &a; // 返回局部变量的地址
}
int* func2(int b) { // 形参数据也会放在栈区
return &b;
}
int main() {
int* p1 = func1(); // 接收func函数返回的地址
std::cout << *p1 << std::endl; // 10
std::cout << *p1 << std::endl; // 2059380984 -> 乱码
/*
* 第一次可以打印正确的数据,是因为编译器做了保留
* 第二次这个数据就不再保留了(原本的内存已经被释放覆盖了,所以我们得到了乱码)
*/
int* p2 = func2(20);
std::cout << *p2 << std::endl; // 8982588 -> 乱码
std::cout << *p2 << std::endl; // 8982588 -> 乱码
/*
* 形参也是同理
*/
std::system("pause");
return 0;
}
总结:不要返回局部变量的地址!
1.2.2 堆区
- 由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
- 在C++中主要利用
new
在堆区开辟内存
假如数据放在堆区,不释放可行吗? -> 不可以!因为这些数据一直不释放,那么当整个程序结束之后,系统也会帮我们回收这些数据!只是在程序运行区间由我们控制。
示例:
#include <iostream>
// 在堆区开辟数据
int* func() {
/*
* 利用new关键字,可以将数据开辟到堆区
(因为我们直接创建数据的话,创建的是局部变量,是在栈区,由编译器释放)
*/
// int a = 10; // 这个是局部变量,在栈区
// 指针本质上也是局部变量,放在栈区,指针保存的数据是放在堆区的
int* p = new int(10); // new返回的是地址编号,所以要用指针来接收
return p; // 将地址进行返回(而非返回数据)
}
int main() {
int* p = func(); // 用指针接收地址
std::cout << *p << std::endl; // 10
std::cout << *p << std::endl; // 10
std::cout << *p << std::endl; // 10
std::cout << *p << std::endl; // 10
std::system("pause");
return 0;
}
总结:
- 堆区教据由程序员管理开辟和释放
- 堆区数据利用
new
关键字进行开辟内存
1.3 new操作符
- C++中利用
new
操作符在堆区开辟数据 - 堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符
delete
- 语法:
new 数据类型
- 利用
new
创建的教据,会返回该数据对应的类型的指针
释放单个数据:
delete 地址
释放数组:delete[] 数组首地址
示例1:基本语法
#include <iostream>
// 1. new的基本语法
int* func_new() {
// 在堆区创建一个整型的数据
// new返回的是:该数据类型的指针
int* p = new int(10);
return p; // 返回指针
}
void test01() {
int* p = func_new();
std::cout << *p << std::endl; // 10
std::cout << *p << std::endl; // 10
std::cout << *p << std::endl; // 10
std::cout << *p << std::endl; // 10
/*
* 堆区的数据由程序员管理开辟和释放
* 如果想要释放堆区的数,利用关键字delete
*/
delete p;
// std::cout << *p << std::endl; // 引发了异常: 读取访问权限冲突。 -> 内存已经被释放,再次访问就是非法操作,会报错。
}
// 2. 在堆区利用new开辟数组
void test02() {
// 在堆区创建10个int的数组
int* arr = new int[10]; // 10代表数组有10个元素,返回的是数组的首地址
// 操纵数组
for (int i = 0; i < 10; i++)
{
arr[i] = i + 100; // 给10个元素赋值
}
for (int i = 0; i < 10; i++)
{
std::cout << arr[i] << " ";
// 100 101 102 103 104 105 106 107 108 109
}
std::cout << "\r\n";
// 释放堆区的数组
delete[] arr; // 释放数组的时候需要加[]
}
int main() {
test01();
test02();
std::system("pause");
return 0;
}
2. 引用
2.1引用的基本使用
作用:给变量起别名
语法:数据类型& 别名 = 原名
示例:
#include <iostream>
using namespace std;
int main() {
// 引用基本语法: 数据类型& 别名 = 原名
int a = 10;
int& b = a;
cout << "a: " << a << endl; // 10
cout << "b: " << b << endl; // 10
b = 100;
cout << "a: " << a << endl; // 100
cout << "b: " << b << endl; // 100
system("pause");
return 0;
}
2.2 引用注意事项
- 引用必须初始化
- 引用在初始化后,不可以改变(给一个变量取别名后,这个别名不能改为其他变量的别名了)
示例:
#include <iostream>
using namespace std;
int main() {
int a = 10;
// 1. 引用必须初始化
// int& b; // IDE: 引用变量"b"需要初始值设定项 未初始化本地变量
int& b = a;
cout << "b:\t" << b << endl;
// 2. 引用在初始化后不可以改变
int c = 20;
// int& b = c; // error C2374: “b”: 重定义;多次初始化
system("pause");
return 0;
}
2.3 引用做函数参数
作用:函数传参时,可以利用引用的技术让形参修饰实参。
优点:可以简化指针修改实参。
示例:
#include <iostream>
using namespace std;
// 1. 值传递:形参不会改变实参
void swap_fn_01(int a, int b) {
int tmp = a;
a = b;
b = tmp;
}
// 2. 地址传递:形参会改变实参
void swap_fn_02(int* a, int* b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
// 3. 引用传递
void swap_fn_03(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}
int main() {
// 1. 值传递:形参不会改变实参
int a1 = 10;
int b1 = 20;
swap_fn_01(a1, b1);
cout << "[swap_fn_01(值传递)]a1:\t" << a1 << endl; // 10
cout << "[swap_fn_01(值传递)]b1:\t" << b1 << endl; // 20
// 2. 地址传递:形参会改变实参
int a2 = 10;
int b2 = 20;
swap_fn_02(&a2, &b2);
cout << "[swap_fn_02(地址传递)]a2:\t" << a2 << endl; // 20
cout << "[swap_fn_02(地址传递)]b2:\t" << b2 << endl; // 10
// 3. 引用传递:形参会改变实参
int a3 = 10;
int b3 = 20;
swap_fn_02(&a3, &b3);
cout << "[swap_fn_03(地址传递)]a3:\t" << a3 << endl; // 20
cout << "[swap_fn_03(地址传递)]b3:\t" << b3 << endl; // 10
return 0;
}
总结:通过引用参数产生的效果同按地址传递是一样的。引用的语法更清楚简单。
2.4 引用做函数返回值
作用:引用是可以作为函数的返回值存在的。
注意:不要返回局部变量引用。
用法:函数调用作为左值。
示例:
#include <iostream>
using namespace std;
// 1. 不要返回局部变量的引用
int& test01() {
int a = 10; // 局部变量(栈区)
return a;
}
// 2. 函数的调用可以作为左值
int& test02() {
static int a = 10; // 静态变量(全局区)
return a;
}
int main() {
/*
* 第一次结果正确,是因为编译器做了保留
* 第二次结果错误,因为test01函数的局部变量a的内存已经释放
*/
int& ref1 = test01();
cout << "ref1: " << ref1 << endl; // 10
cout << "ref1: " << ref1 << endl; // 2041751800
/*
* 因为test02中的a是全局变量,程序结束后才会被释放
*/
int& ref2 = test02();
cout << "ref2: " << ref2 << endl; // 10
cout << "ref2: " << ref2 << endl; // 10
// 2. 函数的调用可以作为左值(如果函数的返回值是一个引用,那么这个函数的调用可以作为左值)
test02() = 1000; // 就是一个简单的赋值操作 <=> ref2 = 1000;
cout << "ref2: " << ref2 << endl; // 1000
cout << "ref2: " << ref2 << endl; // 1000
return 0;
}
2.5 引用的本质
本质:引用的本质在c++内部实现是一个指针常量。
讲解示例:
#include <iostream>
using namespace std;
// 发现是引用,转换为 int* const ref = &a;
void func(int& ref) {
ref = 100; // ref是引用,转换为*ref = 100;
}
int main() {
int a = 10;
/*
* 自动转换为 int* const ref = &a;
* 指针常量是指针指向不可改变(指向地址的数据可以修改),
* 这也说明了为什么引用不可以更改
*/
int& ref = a;
ref = 20; // 内部发现ref是引用,自动帮我们转换为: *ref = 20;
cout << "a: " << a << endl; // 20
cout << "ref: " << ref << endl; // 20
/*
* 在我们使用ref时,编译器发现ref是引用,会自动帮我们解引用
*/
func(a);
cout << "ref: " << ref << endl; // 100
return 0;
}
结论:C++推荐用引用技术,因为语法方便,引用本质是指针常量,但是所有的指针操作编译器都帮我们做了。
2.6 常量引用
作用:常量引用主要用来修饰形参,防止误操作。
在函数形参列表中,可以加const
修饰形参,防止形参改变实参。
示例:
#include <iostream>
using namespace std;
/*
* 常量引用
* 使用场景:用来修饰形参,防止误操作
*/
// 打印函数(不用const修饰,会改变实参)
void show_value_01(int& val) {
val *= 10; // 因为传入的是引用,所以形参可以改变实参
cout << "val: " << val << endl;
}
// 打印函数(用const修饰,修改实参会报错!)
void show_value_02(const int& val) {
// val *= 10; // E0137 表达式必须是可修改的左值
cout << "val: " << val << endl;
}
int main() {
int a = 10;
// int& ref = 10; // 引用本身需要一个合法的内存空间,因此这行错误
/*
加上const之后,编译器将代码修改为:
int tmp = 10;
const int& ref = tmp;
*/
const int& ref = 10;
// ref = 20; // 加上const之后变为常量,不可以修改
// 函数中可以利用常量防止误操作修改实参
int b = 100;
show_value_01(b); // val: 1000
cout << "b: " << b << endl; // 1000
int c = 100;
show_value_02(c); // val: 100
cout << "c: " << c << endl; // 100
return 0;
}
3. 函数提高
3.1 函数默认参数
在C++中,函数的形参列表中的形参是可以有默认值的。
语法:返回值类型 函数名 (参数 = 默认值) {}
注意事项:
- 如果某个位置已经有了默认参数,那么从这个位置往后,从左到右都必须有默认值(和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
// 注意事项2
int res_3 = func(1, 2);
cout << "res_3: " << res_3 << endl; // 错误 C2572 “func” : 重定义默认参数: 参数 1
system("pause");
return 0;
}
3.2 函数占位参数
C++中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置。
语法:返回值类型 函数名 (数据类型) {}
注意:占位参数也可以有默认值。
void func(int a, int, double=3.14) {
...
}
在现阶段函数的占位参数存在意义不大,但是后面的课程中会用到该技术。
示例:
#include <iostream>
using namespace std;
// 函数的占位参数
void func001(int a) {
cout << "This is a func001" << endl;
}
/*
有一个问题:形参a我们是可以用的,但是第二个参数是一个占位符,它没有接收的变量,我们目前不知道怎么用
在后面的课程中,占位参数会有用
*/
void func002(int a, int) { // 第二个int起到占位的作用
cout << "This is a func002" << endl;
}
// 占位参数也可以有默认值
void func003(int=10, double=3.14) { // 第二个int起到占位的作用
cout << "This is a func003" << endl;
}
int main() {
func001(10); // This is a func001
func002(10, 20); // This is a func002
func003(); // This is a func003
system("pause");
return 0;
}
3.3 函数重载
3.3.1 函数重载概述
作用:函数名可以相同,提高复用性。
函数重载满足条件:
- 同一个作用域下
- 函数名称相同
- 以下三个至少满足一个:
- 参数类型不同、
- 参数个数不同
- 参数顺序不同
注意:函数的返回值不可以作为函数重载的条件。
示例:
#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.8319
system("pause");
return 0;
}
可以发现,和Python的class非常像。
示例2:设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号。
#include <iostream>
using namespace std;
// 设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号。
class Student {
public:
/*
* 类中的属性和行为统一称为成员
* 属性:成员变量 / 成员属性
* 行为:成员函数 / 成员方法
*/
// 属性
string name; // 姓名
int id; // 学号
// 行为(方法)
void show_info() {
cout << "姓名: " << name << "\t学号: " << id << endl;
}
// 给姓名赋值(方法)
void set_name(string tmp_name) {
name = tmp_name;
}
// 给学号赋值(方法)
void set_id(int tmp_id) {
id = tmp_id;
}
};
int main() {
// 实例化Student类
Student stu1;
Student stu2;
// 给对象的属性赋值
stu1.name = "张三";
stu1.id = 1;
stu1.show_info(); // 姓名: 张三 学号: 1
stu2.set_name("李四");
stu2.set_id(2);
stu2.show_info(); // 姓名: 李四 学号: 2
system("pause");
return 0;
}
可以发现,和Python的class非常像。
2. 封装意义二:
类在设计时,可以把属性和行为放在不同的权限下,加以控制。
访问权限有三种:
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的默认权限是public
class的默认权限是private
*/
class C1 {
int a; // 默认权限是private
};
struct S1
{
int a; // 默认权限是public
};
int main() {
// 实例化
C1 c1;
// c1.a = 100; // private权限类外无法访问
S1 s1;
s1.a = 100; // public权限类外可以访问
cout << s1.a << endl; // 100
system("pause");
return 0;
}
4.1.3 成员属性设置为私有
优点1:将所有成员属性设置为私有,可以自己控制读写权限。
优点2:对于写权限,我们可以检测数据的有效性。
示例:
#include <iostream>
using namespace std;
#include <string>
/*
成员属性设置为私有。有如下优点:
1. 可以自己控制读写权限
2. 对于写可以检测数据的有效性
*/
class Person {
public:
// 设置姓名
void set_name(string n) {
name = n;
}
// 获取姓名
string get_name() {
return name;
}
// 获取性别
string get_gender() {
return gender;
}
// 设置couple
void set_couple(string cp) {
couple = cp;
}
// 设置年龄(小于零或大于150为非法)
void set_age(int ag) {
if (ag < 0 || ag > 150)
{
cout << "您的输入有误,年龄设置失败!" << endl;
age = -1; // 设置一个特定的年龄,表示年龄赋值失败
}
else
{
age = ag;
}
}
int get_age() {
return age;
}
private:
string name; // 设置可读可写的权限
string gender = "男"; // 设置只读的权限
string couple; // 设置可写的权限
int age; // 可读可写,但要验证数据
};
int main() {
Person p;
p.set_name("张三");
p.set_couple("李四");
p.set_age(1000);
cout << "姓名: " << p.get_name() << endl; // 姓名: 张三
cout << "性别: " << p.get_gender() << endl; // 性别: 男
cout << "年龄: " << p.get_age() << endl; // 年龄: -1
system("pause");
return 0;
}
练习案例1:设计立方体类
- 设计立方体类(Cube)
- 求出立方体的面积和体积
- 面积: 2 ( l × w + l × h + w × h ) 2(l\times w + l \times h + w \times h) 2(l×w+l×h+w×h)
- 体积:
l
×
w
×
h
l \times w \times h
l×w×h
- 其中 l l l为立方体的长, w w w为立方体的宽, h h h为立方体的高
- 分别用全局函数和成员函数判断两个立方体是否相等。
#include <iostream>
using namespace std;
#include <string>
/*
1. 创建立方体类
2. 设计属性
3. 设计行为
3.1 获取面积
3.2 获取体积
4. 分别利用全局函数和成员函数判断两个立方体是否相等
*/
class Cube {
public:
// 设置/获取长宽高
// 长
void set_length(int len) {
l = len;
}
int get_length() {
return l;
}
// 宽
void set_width(int width) {
w = width;
}
int get_width() {
return w;
}
// 高
void set_height(int height) {
h = height;
}
int get_height() {
return h;
}
// 获取立方体面积
int calc_area() {
return 2 * (l * w + l * h + w * h);
}
// 获取立方体体积
int calc_volume() {
return l * w * h;
}
// 利用成员函数判断两个立方体是否相等
bool is_same_cube(Cube& other_cube) {
if (l == other_cube.get_length() &&
w == other_cube.get_width() &&
h == other_cube.get_height())
{
return true;
}
else
{
return false;
}
}
private:
int l;
int w;
int h;
};
// 利用全局函数判断两个立方体是否相等
bool is_same_cube(Cube& c1, Cube& c2) { // 使用引用可以节省资源(而且我们也不打算修改值)
if (c1.get_length() == c2.get_length() &&
c1.get_width() == c2.get_width() &&
c1.get_height() == c2.get_height())
{
return true;
}
else
{
return false;
}
}
int main() {
// 创建第一个立方体
Cube c1;
c1.set_length(10);
c1.set_width(10);
c1.set_height(10);
cout << "c1的面积为: " << c1.calc_area() << endl;
cout << "c1的体积为: " << c1.calc_volume() << endl;
// 创建第二个立方体
Cube c2;
c2.set_length(10);
c2.set_width(10);
c2.set_height(10);
cout << "c2的面积为: " << c2.calc_area() << endl;
cout << "c2的体积为: " << c2.calc_volume() << endl;
// 利用全局函数判断两个立方体是否相等
bool res_1 = is_same_cube(c1, c2);
if (res_1)
{
cout << "[全局函数]c1和c2是相等的!" << endl;
}
else
{
cout << "[全局函数]c1和c2不相等的!" << endl;
}
// 利用成员函数判断两个立方体是否相等
bool res_2 = c1.is_same_cube(c2);
if (res_1)
{
cout << "[成员函数]c1和c2是相等的!" << endl;
}
else
{
cout << "[成员函数]c1和c2不相等的!" << endl;
}
/*
c1的面积为: 600
c1的体积为: 1000
c2的面积为: 600
c2的体积为: 1000
[全局函数]c1和c2是相等的!
[成员函数]c1和c2是相等的!
*/
system("pause");
return 0;
}
练习案例2:点和圆的关系
设计一个圆形类(Circle),和一个点类(Point) ,计算点和圆的关系。
- 点在圆的外侧
- 点在圆的内测
- 点在圆上
思路:知道圆心后,计算圆心和其他点的距离,根据距离再判断关系。
两点之间距离 = ( x 1 − x 2 ) 2 + ( y 1 − y 2 ) 2 两点之间距离=\sqrt{(x_1 - x_2)^2 + (y_1 - y_2)^2} 两点之间距离=(x1−x2)2+(y1−y2)2
两点之间距 离 2 = ( x 1 − x 2 ) 2 + ( y 1 − y 2 ) 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:
// 设置x
void set_x(int xx);
// 获取x
int get_x();
// 设置y
void set_y(int yy);
// 获取y
int get_y();
private:
int x;
int y;
};
point.cpp文件
#include "point.h"
// 设置x
void Point::set_x(int xx) { // 需要加上作用域Point::
x = xx;
}
// 获取x
int Point::get_x() {
return x;
}
// 设置y
void Point::set_y(int yy) {
y = yy;
}
// 获取y
int Point::get_y() {
return y;
}
circle.h文件
#pragma once
#include <iostream>
using namespace std;
#include "point.h"
class Circle {
public:
// 设置半径
void set_r(int rr);
// 获取半径
int get_r();
// 设置圆心
void set_center(Point& ct);
// 获取圆心
Point get_center();
private:
int r; // 半径
Point center; // 圆心
/*
* 在类中可以让另一个类作为本类的成员
*/
};
circle.cpp文件
#include "circle.h"
// 设置半径
void Circle::set_r(int rr) {
r = rr;
}
// 获取半径
int Circle::get_r() {
return r;
}
// 设置圆心
void Circle::set_center(Point& ct) {
center = ct;
}
// 获取圆心
Point Circle::get_center() {
return center;
}
4.2 对象的初始化和清理
生活中我们买的电子产品都基本会有出厂设置,在某一天我们不用时候也会删除一些自己信息数据保证安全。
C++中的面向对象来源于生活,每个对象也都会有初始设置以及对象销毁前的清理数据的设置。
4.2.1 构造函数和析构函数
对象的初始化和清理也是两个非常重要的安全问题。
- 一个对象或者变量没有初始状态,对其使用后果是未知的
- 同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
C++利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供,但是编译器提供的构造函数和析构函数是空实现(空实现:没有代码)。
- 构造函数(Constructor):主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
- 析构函数(Destructor):主要作用在于对象销毁前系统自动调用,执行一些清理工作。
构造函数负责创建
析构函数负责销毁构造和析构是反义词
构造函数语法:类名() {}
- 构造函数,没有返回值也不写
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的年龄为: 10
p3的年龄为: 10
Person1的[析构]函数调用
Person1的[析构]函数调用
Person1的[析构]函数调用
*/
// 2. 显示法
/*
注意事项2:不要利用拷贝构造函数来初始化匿名对象
因为编译器会认为 Person1(p33) <=> Person1 p33;
这样就会导致对象声明重复!
*/
cout << "---------2. 显示法---------" << endl;
Person1 p11; // 默认构造
Person1 p22 = Person1(10); // 有参构造
Person1 p33 = Person1(p22); // 拷贝构造
Person1(10); // 这个东西单独拿出来称之为匿名对象:当前行执行结束后系统会立即回收
cout << "Person1(10)的析构应该在这行上面显示!" << endl;
// 利用拷贝构造初始化匿名对象
// Person1(p33); // 警告 C26444 请勿尝试声明不带名称的局部变量
// 3. 隐式转换法
cout << "---------3. 隐式转换法---------" << endl;
Person1 p111 = 10; // [有参构造] 等价于 Person1 p111 = Person1(10)
Person1 p222 = p111; // [拷贝构造] 等价于 Person1 p222 = Person1(p111)
cout << "---------test02函数结束,下面是析构函数调用---------" << endl;
/*
---------1. 括号法---------
Person1的[无参]构造函数调用
Person1的[有参]构造函数调用
Person1的[拷贝]构造函数调用
p2的年龄为: 10
p3的年龄为: 10
---------2. 显示法---------
Person1的[无参]构造函数调用
Person1的[有参]构造函数调用
Person1的[拷贝]构造函数调用
Person1的[有参]构造函数调用
Person1的[析构]函数调用
Person1(10)的析构应该在这行上面显示!
---------3. 隐式转换法---------
Person1的[有参]构造函数调用
Person1的[拷贝]构造函数调用
---------test02函数结束,下面是析构函数调用---------
Person1的[析构]函数调用
Person1的[析构]函数调用
Person1的[析构]函数调用
Person1的[析构]函数调用
Person1的[析构]函数调用
Person1的[析构]函数调用
Person1的[析构]函数调用
Person1的[析构]函数调用
请按任意键继续. . .
*/
}
int main() {
test02();
system("pause");
return 0;
}
4.2.3 铐贝构造函数调用时机
C++中拷贝构造函数调用时机通常有三种情况:
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值的方式返回局部对象
示例:
#include <iostream>
using namespace std;
/*
* 拷贝构造函数的调用时机:
1. 使用一个已经创建完毕的对象来初始化一个新对象
2. 值传递的方式给函数参数传值
3. 以值传递的方式返回局部对象
*/
class Person03 {
public:
Person03() { // 默认构造
cout << "Person03默认构造函数的调用" << endl;
}
Person03(int age) { // 有参构造
cout << "Person03有参构造函数的调用" << endl;
_age = age;
}
Person03(const Person03& p) { // 拷贝构造
cout << "Person03拷贝构造函数的调用" << endl;
_age = p._age;
}
~Person03() {
cout << "Person03析构构造函数的调用" << endl;
}
int get_age() {
return _age;
}
private:
int _age;
};
// 方法1. 使用一个已经创建完毕的对象来初始化一个新对象
void test001() {
Person03 p1(20);
Person03 p2(p1);
cout << "p2的年龄为: " << p2.get_age() << endl; // p2的年龄为: 20
}
// 方法2. 值传递的方式给函数参数传值
void do_work(Person03 p) {
}
void test002() {
Person03 p; // 默认构造
do_work(p); // 值传递的时候会临时创建一个新的副本,因此这里会调用拷贝构造
}
// 方法3. 值方式返回局部变量
Person03 do_work_2() {
Person03 p1;
cout << (int*)&p1 << endl;
return p1; // 值方式返回
}
void test003() {
Person03 p = do_work_2();
cout << (int*)&p << endl;
}
int main() {
// 方法1. 使用一个已经创建完毕的对象来初始化一个新对象
cout << "------方法1. 使用一个已经创建完毕的对象来初始化一个新对象-------" << endl;
test001();
// 方法2. 值传递的方式给函数参数传值
cout << "------方法2. 值传递的方式给函数参数传值-------" << endl;
test002();
// 方法3. 值方式返回局部变量
cout << "------方法3. 值方式返回局部变量-------" << endl;
test003();
/*
------方法1. 使用一个已经创建完毕的对象来初始化一个新对象-------
Person03有参构造函数的调用
Person03拷贝构造函数的调用
p2的年龄为: 20
Person03析构构造函数的调用
Person03析构构造函数的调用
------方法2. 值传递的方式给函数参数传值-------
Person03默认构造函数的调用
Person03拷贝构造函数的调用
Person03析构构造函数的调用
Person03析构构造函数的调用
------方法3. 值方式返回局部变量-------
Person03默认构造函数的调用
00FAF9DC
00FAF9DC
Person03析构构造函数的调用
请按任意键继续. . .
*/
system("pause");
return 0;
}
4.2.4 构造函数调用规则
默认情况下,C++编译器至少给一个类添加3个函数:
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
- 如果用户定义有参构造函数,C++不在提供默认无参构造,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,C++不会再提供其他构造函数(默认和有参都不会提供了)
自定义 | 默认Constructor | 有参Constructor | 拷贝Constructor |
---|---|---|---|
默认Constructor | × | × | √ |
有参Constructor | × | √ | √ |
拷贝Constructor | × | × | √ |
示例:
#include <iostream>
using namespace std;
// 构造函数的调用规则
/*
1. 创建一个类,C++编译器会给每个类都添加至少3个构造函数
1. 默认构造(空实现)
2. 析构函数(空实现)
3. 拷贝构造
2. 如果我们写了有参Constructor,编译器就不再提供默认Constructor,但依然提供拷贝Construct
如果我们写了拷贝Constructor,那么编译器就不再提供普通的Constructor了(默认和有参都不提供了)
*/
class Person04 {
public:
Person04() {
cout << "Person04的默认Constructor调用" << endl;
}
Person04(int age) {
cout << "Person04的有参Constructor调用" << endl;
_age = age;
}
Person04(const Person04& p) {
cout << "Person04的拷贝Constructor调用" << endl;
_age = p._age;
}
~Person04() {
cout << "Person04的Destructor函数调用" << endl;
}
void set_age(int age) {
_age = age;
}
int get_age() {
return _age;
}
private:
int _age;
};
class Person042 {
public:
Person042() {
cout << "Person042的默认Constructor调用" << endl;
}
Person042(int age) {
cout << "Person042的有参Constructor调用" << endl;
_age = age;
}
//Person042(const Person042& p) {
// cout << "Person042的拷贝Constructor调用" << endl;
// _age = p._age;
//}
~Person042() {
cout << "Person042的Destructor函数调用" << endl;
}
void set_age(int age) {
_age = age;
}
int get_age() {
return _age;
}
private:
int _age;
};
class Person043 {
public:
//Person043() {
// cout << "Person043的默认Constructor调用" << endl;
//}
Person043(int age) {
cout << "Person043的有参Constructor调用" << endl;
_age = age;
}
//Person043(const Person043& p) {
// cout << "Person043的拷贝Constructor调用" << endl;
// _age = p._age;
//}
~Person043() {
cout << "Person043的Destructor函数调用" << endl;
}
void set_age(int age) {
_age = age;
}
int get_age() {
return _age;
}
private:
int _age;
};
class Person044 {
public:
//Person044() {
// cout << "Person044的默认Constructor调用" << endl;
//}
//Person044(int age) {
// cout << "Person044的有参Constructor调用" << endl;
// _age = age;
//}
Person044(const Person044& p) {
cout << "Person044的拷贝Constructor调用" << endl;
_age = p._age;
}
~Person044() {
cout << "Person044的Destructor函数调用" << endl;
}
void set_age(int age) {
_age = age;
}
int get_age() {
return _age;
}
private:
int _age;
};
void test0001() {
// 自己定义了拷贝Constructor
cout << "---------自己定义了拷贝Constructor---------" << endl;
Person04 p1;
p1.set_age(18);
Person04 p2(p1);
cout << "p1._age: " << p1.get_age() << endl;
cout << "p2._age: " << p2.get_age() << endl;
}
void test0002() {
// 没有定义拷贝Constructor,由编译器提供
cout << "---------没有定义拷贝Constructor,由编译器提供---------" << endl;
Person042 p12;
p12.set_age(18);
Person042 p22(p12);
cout << "p12._age: " << p12.get_age() << endl;
cout << "p22._age: " << p22.get_age() << endl;
}
void test0003() {
/*
因为没写了有参Constructor,编译器就不会提供给我们默认Constructor了,
如果我们还想用默认的Constructor,那么就会报错!
正确的做法是调用有参的Constructor
*/
// Person043 p; // 错误(活动) E0291 类 "Person043" 不存在默认构造函数
cout << "---------只定义了有参Constructor---------" << endl;
Person043 p1(20);
Person043 p2(p1);
cout << "p1._age: " << p1.get_age() << endl;
cout << "p2._age: " << p2.get_age() << endl;
}
void test0004() {
cout << "---------只定义了拷贝Constructor---------" << endl;
// Person044 p; // 错误(活动) E0291 类 "Person044" 不存在默认构造函数
// Person044 p(10); // 错误(活动) E0289 没有与参数列表匹配的构造函数 "Person044::Person044" 实例
// 如果class里面只有Copy Constructor,怎么调用我不会:joy:
}
int main() {
test0001();
/*
Person04的默认Constructor调用
Person04的拷贝Constructor调用
p1._age: 18
p2._age: 18
Person04的Destructor函数调用
Person04的Destructor函数调用
*/
// 可以看到,虽然我们没有写拷贝Constructor,但是编译器给我们提供了,所以p22._age == 18
test0002();
/*
Person042的默认Constructor调用
p12._age: 18
p22._age: 18
Person042的Destructor函数调用
Person042的Destructor函数调用
*/
test0003();
/*
Person043的有参Constructor调用
p1._age: 20
p2._age: 20
Person043的Destructor函数调用
Person043的Destructor函数调用
*/
system("pause");
return 0;
}
4.2.5 深拷贝与浅拷贝
深浅拷贝是面试经典问题,也是常见的一个坑。
- 浅拷贝:简单的赋值拷贝操作
- 深拷贝:在堆区重新申请空间,进行拷贝操作
浅拷贝带来的问题:堆区的内存重复释放。
如何解决:使用深拷贝解决浅拷贝带来的问题。
办法:在Copy Constructor中使用new
关键字重新开辟一块内存空间。
示例:
#include <iostream>
using namespace std;
class Person05 {
public:
Person05() {
cout << "Person05的默认Constructor调用" << endl;
}
Person05(int age, int height) {
_age = age;
_p_height = new int(height); // new返回的是一个地址,需要用指针接收(new出来的数据在堆区)
cout << "Person05的有参Constructor调用" << endl;
}
// 自己实现Copy Constructor,以解决浅拷贝带来的内存重复释放问题
Person05(const Person05& p) {
cout << "Person05的Copy Constructor调用" << endl;
_age = p._age;
// _p_height = p._p_height; // 编译器默认实现这行代码
_p_height = new int(*p._p_height); // 利用new关键字重新开辟一块内存,里面存储地址
}
~Person05() { // 在Heap Area开辟的数据做释放操作
if (_p_height != NULL) {
delete _p_height; // 释放
_p_height = NULL; // 防止野指针出现,将其置空
}
cout << "Person05的Destructor调用" << endl;
}
void set_age(int age) {
_age = age;
}
int get_age() {
return _age;
}
void set_height(int height) {
*_p_height = height;
}
int* get_height() {
return _p_height;
}
private:
int _age = -1;
int* _p_height; // 身高
};
void test00001() {
Person05 p1(18, 165);
cout << "p1._age: " << p1.get_age() <<
"\theight: " << *p1.get_height() << endl;
Person05 p2(p1);
cout << "p2._age: " << p2.get_age() <<
"\theight: " << *p2.get_height() << endl;
}
int main() {
test00001();
/*
Person05的有参Constructor调用
p1._age: 18 height: 165
Person05的Copy Constructor调用
p2._age: 18 height: 165
Person05的Destructor调用
Person05的Destructor调用
请按任意键继续. . .
*/
system("pause");
return 0;
}
总结:如果属性有在堆区开辟的,一定要自己提供Copy Constructor函数,防止浅拷贝带来的问题(具体为内存重复释放问题)。
4.2.6 初始化列表
作用:C++提供了初始化列表语法,用来初始化属性。
语法:构造函数(): 属性1(值1), 属性2(值2), ... {函数实现}
示例:
#include <iostream>
using namespace std;
// 初始化列表
class Person061 {
public:
// 传统初始化操作
Person061(int a, int b, int c) {
_a = a;
_b = b;
_c = c;
}
void show_info() {
cout << "------传统初始化属性的操作-------" << endl;
cout << "_a: " << _a << endl;
cout << "_b: " << _b << endl;
cout << "_c: " << _c << endl;
}
private:
int _a;
int _b;
int _c;
};
class Person062 {
public:
// 传统初始化操作
//Person062(int a, int b, int c) {
// _a = a;
// _b = b;
// _c = c;
//}
// 初始化列表初始化属性
Person062(int a, int b, int c) : _a(a), _b(b), _c(c) {}
void show_info() {
cout << "------初始化列表初始化属性的操作-------" << endl;
cout << "_a: " << _a << endl;
cout << "_b: " << _b << endl;
cout << "_c: " << _c << endl;
}
private:
int _a;
int _b;
int _c;
};
int main() {
Person061 p1(10, 20, 30);
p1.show_info();
Person062 p2(10, 20, 30);
p2.show_info();
system("pause");
return 0;
}
可以看得出来,有参Constructor和初始化列表的方式效果是一样的,只不过两者的语法不同。
4.2.7 类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员为对象成员。
例如:
class A {}
class B {
A a;
}
B
类中有对象A
作为成员,A
为对象成员。
那么当创建B
对象时,A
与B
的构造(Constructor)和析构(Destructor)的顺序是谁先谁后?
- [Constructor] 当其他类对象作为本类对象时,先构造其他类对象,再构造自身
- [Destructor] 析构的顺序与构造的顺序相反
示例:
#include <iostream>
using namespace std;
#include <string>
// 类对象作为类成员
class Phone {
public:
Phone(string brand) { // 有参Constructor
cout << "Phone的Constructor调用" << endl;
_brand = brand;
}
~Phone() {
cout << "Phone的Destructor调用" << endl;
}
string _brand; // 品牌名
};
class Person07 {
/*
1. [Constructor] 当其他类对象作为本类对象时,先构造其他类对象,再构造自身
2. [Destructor] 析构的顺序与构造的顺序相反
*/
public:
// Phone _phone = brand; 隐式转换法创建对象
Person07(string name, string brand) : _name(name), _phone(brand) {
cout << "Person的Constructor调用" << endl;
}
~Person07() {
cout << "Person07的Destructor调用" << endl;
}
string _name;
Phone _phone;
};
void test7_1() {
Person07 p1("张三", "Apple");
cout << p1._name << "拿着" << p1._phone._brand << endl;
}
int main() {
test7_1(); // 张三拿着Apple
/*
Phone的Constructor调用
Person的Constructor调用
张三拿着Apple
Person07的Destructor调用
Phone的Destructor调用
*/
system("pause");
return 0;
}
4.2.8 静态成员
静态成员就是在成员变量和成员函数前加上关键字static
,称为静态成员。
静态成员分为:
- 静态成员变量的特点:
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 在全局区
- 类内声明,类外初始化(必须初始化)
- 静态成员函数的特点:
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
静态成员变量和静态成员函数都是有访问权限的,
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; // 100
Person08 p2;
p2._a = 200;
cout << "p1._a: " << p1._a << endl; // 200
cout << "p2._a: " << p2._a << endl; // 200
}
void test8_2() {
/*
静态成员变量不属于某个对象,所有对象都共享同一份数据,
因此静态成员变量有两种访问方式:
1. 通过对象进行访问
2. 通过类名进行访问(和Python很像)
*/
// 1. 通过对象进行访问
Person08 p1;
cout << "p1._a: " << p1._a << endl; // 200
// 2. 通过类名进行访问(和Python很像)
cout << "Person::_a: " << Person08::_a << endl; // 200
// 静态成员变量也是有访问权限的:类外访问不到私有的静态成员变量
// cout << "Person::_b: " << Person08::_b << endl; // 错误(活动) E0265 成员 "Person08::_b" (已声明 所在行数 : 25) 不可访问 对象的初始化和清理
}
int main() {
test8_1();
test8_2();
system("pause");
return 0;
}
2. 静态成员函数
静态成员函数的特点:
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量(非静态成员变量是访问不了的)
- 原因:[非静态成员变量]必须创建对象才可以访问,而[静态成员函数]是可以通过类名直接调用的,直接调用时就不知道到底调用(修改)哪个对象的[非静态成员变量],所以不可以。
- 而[静态成员变量]因为是共享的(只有一份),所以[静态成员函数]就知道调用(修改)哪个了。
访问方式:
- 通过对象进行访问:
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
// 下面是不用引用的结果,因为返回的是副本,所以只+10
Person02_2 p4(10);
p4.add_age_return_value(p1).add_age_return_value(p1).add_age_return_value(p1);
cout << "p4.age: " << p4.age << endl; // p4.age: 20
}
int main() {
test02_1();
test02_2();
test02_3();
system("pause");
return 0;
}
4.3.3 空指针访问成员函数
C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this
指针。
- 如果用到
this
指针,需要加以判断保证代码的Robust。
因为空指针虽然可以调用成员函数,但是不能调用成员变量,所以如果指针是空的,调用成员变量时就会报错。因此在[成员函数]中调用[成员变量]时,最好加上空指针判断,以防止空指针出现,进而导致程序奔溃。
代码如下:
Person{
public:
void some_func() {
if (this == NULL)
{
// 如果是空指针,直接结束该方法,不往下走了 -> 提高代码的Robust
return;
}
// 剩余要调用成员变量的代码
cout << "age: " << this->age << endl;
}
private:
int age;
};
示例:
#include <iostream>
using namespace std;
// 空指针调用成员函数
class Person03_1 {
public:
void show_class_name() {
cout << "This is Person03_1 class" << endl;
}
void show_person_age() {
cout << "age: " << this->age << endl;
}
int age;
};
class Person03_2 {
public:
void show_class_name() {
cout << "This is Person03_2 class" << endl;
}
void show_person_age() {
if (this == NULL)
{
return; // 如果是空指针,直接结束该方法,不往下走了 -> 提高代码的Robust
}
cout << "age: " << this->age << endl;
}
int age;
};
void test03_1() {
Person03_1* p = NULL; // 创建一个类的空指针p
p->show_class_name(); // This is Person03_1 class
// 报错原因是空指针->age
// p->show_person_age(); // **this** 是 nullptr
}
void test03_2() {
Person03_2* p = NULL; // 创建一个类的空指针p
p->show_class_name(); // This is Person03_1 class
p->show_person_age(); // 不再报错,且没有输出
}
int main() {
test03_1();
test03_2();
system("pause");
return 0;
}
4.3.4 const修饰成员函数
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 & Destructor
Building() {
sittingroom = "客厅";
bedroom = "卧室";
}
~Building() {}
public:
string sittingroom; // 客厅
private:
string bedroom; // 卧室
};
// 全局函数
void guest(Building& building) {
cout << "[全局函数]客人正在访问: " << building.sittingroom << endl;
// cout << "[全局函数]客人正在访问: " << building.bedroom << endl; // 不能访问私有属性
}
void good_gay(Building& building) {
cout << "[全局函数]好朋友正在访问: " << building.sittingroom << endl;
cout << "[全局函数]好朋友正在访问: " << building.bedroom << endl;
}
void test01_1() {
Building building;
guest(building);
good_gay(building);
/*
[全局函数]客人正在访问: 客厅
[全局函数]好朋友正在访问: 客厅
[全局函数]好朋友正在访问: 卧室
*/
}
int main() {
test01_1();
system("pause");
return 0;
}
4.4.2 类做友元
语法:在类中使用friend
关键字声明友元类,friend class 友元类名;
—— 例:friend class GoodGay;
语法示例:
class Building02 {
// 声明友元
friend class GoodGay;
public: // 设计Constructor对类的属性进行初始化
Building02(); // 在类内声明一下,一会儿我们在类外进行具体的函数实现
public:
string sittingroom; // 客厅
private:
string bedroom; // 卧室
};
示例:
#include <iostream>
using namespace std;
#include <string>
// 类做友元
class Building02; // 类的声明,让编译器先不要报错。
class Guest {
public:
Guest(); // Constructor的声明
public:
void visit(); // 参观函数,让它访问Building02中的属性
Building02* building;
};
class Building02 {
// 声明友元
friend class GoodGay;
public: // 设计Constructor对类的属性进行初始化
Building02(); // 在类内声明一下,一会儿我们在类外进行具体的函数实现
public:
string sittingroom; // 客厅
private:
string bedroom; // 卧室
};
class GoodGay {
public:
GoodGay();
public:
void visit();
Building02* building;
};
// 在类外写成员函数的具体实现
Building02::Building02() {
sittingroom = "客厅";
bedroom = "卧室";
}
Guest::Guest() {
// 创建建筑物对象
building = new Building02;
}
GoodGay::GoodGay()
{
building = new Building02;
}
void GoodGay::visit()
{
cout << "[类]客人正在访问: " << building->sittingroom << endl;
cout << "[类]客人正在访问: " << building->bedroom << endl; // 友元可以访问私有属性
}
void Guest::visit() {
cout << "[类]客人正在访问: " << building->sittingroom << endl;
// cout << "[类]客人正在访问: " << building->bedroom << endl; // 不可以访问私有属性
}
void test02_1() {
Guest guest;
guest.visit();
GoodGay gg;
gg.visit();
/*
[类]客人正在访问: 客厅
[类]客人正在访问: 客厅
[类]客人正在访问: 卧室
*/
}
int main() {
test02_1();
system("pause");
return 0;
}
4.4.3 成员函数做友元
语法:在类中使用friend
关键字声明友元成员函数,friend 类名::成员方法();
—— 例:friend GoodGay::visit();
语法示例:
class Building03 {
// 声明友元
// 告诉编译器GoodGay类下的visit_friend成员函数作为本类的好朋友,
// 可以访问本类的私有属性
friend void GoodGay03::visit_friend();
public:
Building03(); // Constructor声明
string sittingroom;
private:
string bedroom;
};
#include <iostream>
using namespace std;
#include <string>
// 成员函数做友元
class Building03;
class GoodGay03 {
public:
GoodGay03();
void visit_friend(); // 让visit_friend函数可以访问Building中私有属性
void visit_norm(); // 让visit_norm函数可以访问Building中私有属性
Building03* building;
};
class Building03 {
// 声明友元
friend void GoodGay03::visit_friend(); // 告诉编译器GoodGay类下的visit_friend成员函数作为本类的好朋友,可以访问本类的私有属性
public:
Building03(); // Constructor声明
string sittingroom;
private:
string bedroom;
};
// 类外实现
Building03::Building03() { // Constructor
sittingroom = "客厅";
bedroom = "卧室";
}
GoodGay03::GoodGay03() {
building = new Building03;
}
void GoodGay03::visit_norm() {
cout << "GoodGay03的[成员函数]visit_norm正在访问: " << building->sittingroom << endl;
// cout << "GoodGay03的[成员函数]visit_norm正在访问: " << building->bedroom << endl; // 无法访问私有属性
}
void GoodGay03::visit_friend() {
cout << "GoodGay03的[成员函数]visit_friend正在访问: " << building->sittingroom << endl;
cout << "GoodGay03的[成员函数]visit_friend正在访问: " << building->bedroom << endl;
}
void test03_1() {
GoodGay03 gg;
gg.visit_norm();
gg.visit_friend();
/*
GoodGay03的[成员函数]visit_norm正在访问: 客厅
GoodGay03的[成员函数]visit_friend正在访问: 客厅
GoodGay03的[成员函数]visit_friend正在访问: 卧室
*/
}
int main() {
test03_1();
system("pause");
return 0;
}
4.5 运算符重载
运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
4.5.1 加号运算符重载
作用:实现两个自定义数据类型相加的运算。
语法有两种:
- 成员函数重载+号:
返回值类型 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 << p
cout << "a: " << p.a << "\tb: " << p.b;
return cout;
}
注意:
- 通常情况下,我们不会利用成员函数重载
<<
运算符,因为无法实现cout
在左侧! —— 只能利用全局函数重载<<
运算符。 cout
的数据类型是ostream
(可以ctrl+左键看一下cout
的定义)。- 在使用
cout
时,一般会使用链式编程,所以重载<<
运算符时应该返回cout
的数据类型(即ostream
数据类型)。 - 重载
<<
时,我们可以会用到类的private
属性,因此需要配合友元(friend
)关键字使用。
int a = 10;
cout << a << endl; // 10
Person p;
p.a = 10;
p.b = 20;
cout << p << endl; // // 没有与这些操作数匹配的"<<"运算符
// 我们想直接输出p.a和p.b,该怎么做? -> 重载左移运算符
#include <iostream>
using namespace std;
#include <string>
// 左移运算符重载
class Person02 {
// 添加友元
friend ostream& operator<<(ostream& cout, Person02 p);
public:
// 利用成员函数重载左移运算符 p.operator<<(cout) 简化版本 p << cout
// 通常情况下,我们不会利用成员函数重载<<运算符,因为无法实现cout在左侧! —— 只能利用全局函数重载<<运算符
// void operator<<(cout) {}
public:
Person02(int a, int b) {
this->a = a;
this->b = b;
}
private:
int a;
int b;
};
ostream& operator<<(ostream& cout, Person02 p) { // 本质 operator<< (cout, p) 简化为 cout << p
cout << "a: " << p.a << "\tb: " << p.b;
return cout;
}
void test02_1() {
Person02 p(10, 20);
// cout << p << endl; // 没有与这些操作数匹配的"<<"运算符
// 重载<<运算符之后
cout << p << endl; // 这种是链式编程,必须返回对象后才能无限连
// 本质
operator<<(cout, p) << endl; // a: 10 b: 20
}
int main() {
test02_1();
system("pause");
return 0;
}
4.5.3 递增运算符(++)重载
作用:通过重载递增(++
)运算符,实现自己的整型数据。
递增运算符++
的位置不同,起到的效果也不同:
- 前置递增
- 后置递增
// 前置递增
int a = 10;
cout << ++a << endl; // 11
// 后置递增
int b = 10;
cout << b++ << endl; // 10
cout << b << endl; // 11
注意:
- 前置递增:重载时,返回值类型需加上引用
&
- 后置递增:
- 重载时,返回值类型不用加上引用
&
- 形参必须写
int占位符
- 重载时,返回值类型不用加上引用
返回引用的目的是实现一直对一个对象进行操作。
而返回值并不是我们想象中的那样,返回一个数字,而是由函数返回值的数据类型决定的,可能是int/double/float/long...
,也可能是一个类(class
)。
原因:
-
前置递增:
- 如果返回值类型不是引用,会copy一份儿,那操作的对象就会变了。
- 那么就会发生一个问题:
cout << ++my_int << endl; // 1
cout << ++my_int << endl; // 1(还是1,因为不是原对象的)
-
后置递增:
- 如果不写
(int)
那么编译器会认为[前置递增重载函数]和[后置递增重载函数]发生了重定义(不满足函数重载,所以会引发二义性)int
是一个占位参数,目的:- 满足函数重载的条件;
- 可以用于区分前置和后置。
- 前置递增返回的是引用,但后置递增返回的是值(也是一个数据类型)。
- 因为如果我们返回的是引用,返回的是[临时变量]的引用,那么后续的操作就是非法的([临时变量]会被编译器自动销毁)
- 因为我们返回的值,这个值的数据类型是一个类,所以还是可以继续实现链式编程的
- 但这样也会带来一个问题:我们无法先前置递增那样,可以无限前置递增:
++(++my_int)
。当使用(my_int++)++
时,由于返回的是值,不是一直操作同一个对象,所以(my_int++)++
等价于my_int++
。 - 但这其实并不是一个问题,我们看一下下面的代码:
我们可以发现,在C++原生代码中,后置++就是不可以无限套娃的,直接会提示语法错误,所以我们写的代码没有问题!int a = 10; cout << a++ << endl; // 10 cout << a << endl; // 11 // cout << (a++)++ << endl; // 表达式必须是可修改的左值 // cout << a << endl;
- 如果不写
总结:前置递增返回引用,后置递增返回值。
代码示例:
#include <iostream>
using namespace std;
#include <string>
// 自定义整型
class MyInteger {
// 声明友元
friend ostream& operator<<(ostream& cout, MyInteger my_int);
public:
MyInteger() {
this->num = 0;
}
// 重载前置++运算符
MyInteger& operator++() {
/*
如果返回值类型不是引用,会copy一份儿,那操作的对象就会变了。
那么就会发生一个问题:
cout << ++my_int << endl; // 1
cout << ++my_int << endl; // 1(还是1,因为不是原对象的)
返回引用的目的是实现一直对一个对象进行操作
*/
// 先进行++运算
++this->num;
// 再将返回自身以满足链式编程
return *this;
}
// 重载后置++运算符
/*
如果不写(int)那么编译器会认为两个函数发生了重定义(不满足函数重载,所以会引发二义性)
int是一个占位参数,目的:①满足函数重载的条件;②可以用于区分前置和后置
前置递增返回的是引用,但后置递增返回的是值。
因为如果我们返回的是引用,返回的是tmp的引用,那么后续的操作就是非法的(tmp会被编译器自动销毁)
因为我们返回的值,这个值的数据类型是一个类,所以还是可以继续实现链式编程的
但这样也会带来一个问题:
我们无法先前置递增那样,可以无限前置递增:++(++my_int)
当使用(my_int++)++时,由于返回的是值,不是一直操作同一个对象,所以(my_int++)++ 等价于 my_int++
但这其实并不是一个问题,我们看一下下面的代码:
int a = 10;
cout << a++ << endl; // 10
cout << a << endl; // 11
// cout << (a++)++ << endl; // 表达式必须是可修改的左值
// cout << a << endl;
我们可以发现,在C++原生代码中,后者++就是不可以无限套娃的,直接会提示语法错误,所以我们写的代码没有问题!
*/
MyInteger operator++(int) {
// 先 记录当时的结果
MyInteger tmp = *this;
// 后 递增
num++;
// 最后 将记录的结果做返回操作
return tmp;
}
private:
int num;
};
// 重载左移运算符
ostream& operator<<(ostream& cout, MyInteger my_int) {
cout << my_int.num;
return cout;
}
void test03_1() {
MyInteger my_int;
// 重载<<运算符之前
// cout << my_int << endl; // 没有与这些操作数匹配的"<<"运算符
// 重载<<运算符之后
cout << my_int << endl; // 0
// 重载前置++运算符之前
// cout << ++my_int << endl; // 没有与这些操作数匹配的"++"运算符
// 重载前置++运算符之后
cout << ++(++my_int) << endl; // 2
// 重载后置++运算符之前
// cout << my_int++ << endl; // 没有与这些操作数匹配的"++"运算符
// 重载后置++运算符之后
cout << my_int++ << endl; // 2
cout << my_int << endl; // 3
cout << (my_int++)++ << endl; // 3
cout << my_int << endl; // 4(不是我们预想的5,因为返回的是值而不是引用,不是同一个对象)
}
int main() {
test03_1();
int a = 10;
cout << a++ << endl; // 10
cout << a << endl; // 11
// cout << (a++)++ << endl; // 表达式必须是可修改的左值
// cout << a << endl;
system("pause");
return 0;
}
递增运算符和递减运算符重载代码:
#include <iostream>
using namespace std;
#include <string>
class MyInteger {
// 声明友元
friend ostream& operator<<(ostream& cout, MyInteger my_int);
public:
// 1.1 重载前置++运算符
MyInteger& operator++() {
++this->num;
return *this;
}
// 1.2 重载后置++运算符
MyInteger operator++(int) {
MyInteger tmp = *this;
this->num++;
return tmp;
}
// 2.1 重载前置--运算符
MyInteger& operator--() {
--this->num;
return *this;
}
// 2.2 重载后置--运算符
MyInteger operator--(int) {
MyInteger tmp = *this;
this->num--;
return tmp;
}
public:
MyInteger() {
this->num = 0;
}
private:
int num;
};
// 重载<<运算符
ostream& operator<<(ostream& cout, MyInteger my_int) {
cout << my_int.num;
return cout;
}
void test_plus_plus() {
MyInteger my_int;
cout << "---------重载++运算符---------" << endl;
// 重载<<运算符
cout << my_int << endl; // 0
// 1.1 重载前置++运算符
cout << ++my_int << endl; // 1
cout << ++++my_int << endl; // 3
cout << my_int << endl; // 3
// 1.2 重载后置++运算符
cout << my_int++ << endl; // 3
cout << my_int++++ << endl; // 4
cout << my_int << endl; // 5
}
void test_sub_sub() {
MyInteger my_int;
cout << "---------重载--运算符---------" << endl;
// 重载<<运算符
cout << my_int << endl; // 0
// 2.1 重载前置--运算符
cout << --my_int << endl; // -1
cout << ----my_int << endl; // -3
cout << my_int << endl; // -3
// 2.2 重载后置--运算符
cout << my_int-- << endl; // -3
cout << my_int---- << endl; // -4
cout << my_int << endl; // -5
}
int main() {
test_plus_plus();
test_sub_sub();
system("pause");
return 0;
}
4.5.4 赋值运算符=重载
C++编译器至少给一个类添加4个函数:
- 默认构造函数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:
// Constructor
Person04(int age) {
this->age = new int(age); // new返回的是一个地址(在堆区)
}
// Destructor
~Person04() {
if (this->age != NULL) {
delete this->age;
this->age = NULL;
}
cout << "Person04的Destructor调用" << endl;
}
// 重载 赋值运算符
Person04& operator=(Person04& p) { // 一定要返回自身的引用,不要返回自身的值
// 编译器提供的是浅拷贝: this->age = p.age;
// 应该先判断是否有属性在堆区,如果有先释放干净,然后再深拷贝
if (this->age != NULL)
{
delete this->age;
this->age = NULL;
}
// 深拷贝
this->age = new int(*p.getter_age());
// 返回自身
return *this;
}
public:
int* getter_age() {
return this->age;
}
private:
int* age; // 创建一个指针
};
void test04_1() {
Person04 p1(18);
Person04 p2(20);
cout << "p1.age: " << *p1.getter_age() << endl; // p1.age: 18
cout << "p2.age: " << *p2.getter_age() << endl; // p2.age: 20
/*
赋值操作 -> 堆区内存重复释放,程序崩溃!
解决方案:利用DeepCopy解决浅拷贝带来的问题(在使用=运算符时,各自开辟空间,Destructor时各自释放各自的)
*/
p2 = p1; // 赋值操作
cout << "p1.age: " << *p1.getter_age() << endl; // p1.age: 18
cout << "p2.age: " << *p2.getter_age() << endl; // p2.age: 18
}
void test04_2() {
Person04 p1(18);
Person04 p2(20);
Person04 p3(30);
/*
从main函数中 c = b = a的操作中可以看到,是可以连等于的,
但我们现在写的会报错,这是因为我们重载赋值运算符时,
返回的是void,所以不能链式编程
解决方案:返回自身就好了
*/
// p3 = p2 = p1; // ERROR:没有与这些操作数匹配的"="运算符
// 重载赋值运算符时返回自身后
p3 = p2 = p1;
cout << "p1.age: " << *p1.getter_age() << endl; // p1.age: 18
cout << "p2.age: " << *p2.getter_age() << endl; // p2.age: 18
cout << "p3.age: " << *p3.getter_age() << endl; // p3.age: 18
}
int main() {
cout << "-------------test04_1-------------" << endl;
test04_1();
int a = 10;
int b = 20;
int c = 30;
cout << "-------------c = b = a-------------" << endl;
c = b = a;
cout << "a: " << a << endl; // a: 10
cout << "b: " << b << endl; // b: 10
cout << "c: " << c << endl; // c: 10
cout << "-------------test04_2-------------" << endl;
test04_2();
/*
-------------test04_1-------------
p1.age: 18
p2.age: 20
p1.age: 18
p2.age: 18
Person04的Destructor调用
Person04的Destructor调用
-------------c = b = a-------------
a: 10
b: 10
c: 10
-------------test04_2-------------
p1.age: 18
p2.age: 18
p3.age: 18
Person04的Destructor调用
Person04的Destructor调用
Person04的Destructor调用
*/
system("pause");
return 0;
}
4.5.5 关系运算符重载
系统默认的数据类型,比如int
,我们可以知道变量之间的大小关系,但是对于自定义数据类型,在对比的时候编译器不知道怎么对比。基于这个场景,我们需要重载关系运算符。
作用:重载关系运算符,可以让两个自定义类型对象进行对比操作。
代码示意:
// 重载关系运算符==
bool operator==(Person05& p) {
if (this->name == p.getter_name() && this->age == p.getter_age())
{
return true;
}
else
{
return false;
}
}
示例:
#include <iostream>
using namespace std;
#include <string>
// 重载关系运算符
class Person05 {
public:
Person05(string name, int age) {
this->name = name;
this->age = age;
}
// 重载关系运算符==
bool operator==(Person05& p) {
if (this->name == p.getter_name() && this->age == p.getter_age())
{
return true;
}
else
{
return false;
}
}
// 重载关系运算符!=
bool operator!=(Person05& p) {
if (this->name != p.getter_name() || this->age != p.getter_age())
{
return true;
}
else
{
return false;
}
}
// 重载关系运算符<
bool operator<(Person05& p) {
if (this->age < p.getter_age())
{
return true;
}
else
{
return false;
}
}
// 重载关系运算符<=
bool operator<=(Person05& p) {
if (this->age <= p.getter_age())
{
return true;
}
else
{
return false;
}
}
// 重载关系运算符>
bool operator>(Person05& p) {
if (this->age > p.getter_age())
{
return true;
}
else
{
return false;
}
}
// 重载关系运算符>=
bool operator>=(Person05& p) {
if (this->age >= p.getter_age())
{
return true;
}
else
{
return false;
}
}
public: // getter
string getter_name() {
return this->name;
}
int getter_age() {
return this->age;
}
private:
string name;
int age;
};
void test05_1() {
Person05 p1("Tom", 18);
Person05 p2("Tom", 18);
Person05 p3("Jerry", 16);
// 重载==之前
//if (p1 == p2) // Error: 没有与这些操作数匹配的"=="运算符
//{
// cout << "p1和p2是相等的" << endl;
//}
// 重载==之后
if (p1 == p2)
{
cout << "p1和p2是 相等 的" << endl; // p1和p2是相等的
}
else
{
cout << "p1和p2是 不相等 的" << endl;
}
// 重载!=之后
if (p1 != p3)
{
cout << "p1和p2是 不相等 的" << endl; // p1和p2是 不相等 的
}
else
{
cout << "p1和p2是 相等 的" << endl;
}
// 重载<之后
if (p1 < p3)
{
cout << "p1 < p2" << endl;
}
else
{
cout << "p1 >= p2" << endl; // p1 >= p2
}
// 重载<=之后
if (p1 <= p3)
{
cout << "p1 <= p2" << endl;
}
else
{
cout << "p1 > p2" << endl; // p1 > p2
}
// 重载>之后
if (p1 > p3)
{
cout << "p1 > p2" << endl; // p1 > p2
}
else
{
cout << "p1 <= p2" << endl;
}
// 重载>=之后
if (p1 >= p3)
{
cout << "p1 >= p2" << endl; // p1 >= p2
}
else
{
cout << "p1 < p2" << endl;
}
}
int main() {
test05_1();
system("pause");
return 0;
}
4.5.6 函数调用运算符()重载 -> 小括号的重载
- 函数调用运算符
()
也可以重载 - 由于重载后使用的方式非常像函数的调用,因此称为仿函数
- 仿函数没有固定写法,非常灵活
示例:
#include <iostream>
using namespace std;
#include <string>
// 函数调用运算符重载
// 打印输出类
class MyPrint {
public:
// 重载函数调用运算符
void operator()(string test) {
cout << test << endl;
}
};
void my_print(string test) { // 全局函数
cout << test << endl;
}
void test06_1() {
MyPrint mp;
// 这特么不就是Python的print函数吗?
/*
由于使用起来非常类似于函数调用,因此成为仿函数
*/
mp("Hello World!"); // Hello World!
my_print("Hello World!"); // Hello World!
}
// 仿函数非常灵活,没有固定的写法
// 加法类
class MyAdd {
public:
int operator()(int num1, int num2) {
return num1 + num2;
}
};
void test06_2() {
MyAdd madd;
int res = madd(100, 200);
cout << "res: " << res << endl; // res: 300
// 匿名函数对象:MyAdd()就是一个匿名对象,这行语句执行完毕后就被释放
cout << MyAdd()(100, 200) << endl; // 300
}
int main() {
test06_1();
test06_2();
system("pause");
return 0;
}
4.6 继承
继承是面向对象三大特性之一。
有些类与类之间存在特殊的关系,例如下图中:
我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。这个时候我们就可以考虑利用继承的技术,减少重复代码。
4.6.1 继承的基本语法
例如我们看到很多网站中,都有公共的头部,公共的底部,甚至公共的左侧列表,只有中心内容不同。
类继承的语法:class 子类 : 继承方式 父类 {};
继承的语法的实例:
class BaseClass{
public:
xxxxxx;
};
class ChildClass : public BaseClass {
子类的代码(不重写父类的代码默认会继承父类的代码);
};
- 子类也称为派生类
- 父类也称为基类
接下来我们分别利用普通写法和继承的写法来实现网页中的内容,看一下继承存在的意义以及好处。
普通实现:
#include <iostream>
#include <string>
using namespace std;
// 普通实现页面
class Java {
public:
void header() {
cout << "[公共头部]首页、公开课、登陆、注册..." << endl;
}
void footer() {
cout << "[公共底部]帮助中心、交流合作、站内地图..." << endl;
}
void left() {
cout << "[公共分类列表]Java、Python、C++..." << endl;
}
void content() {
cout << "Java学科视频" << endl;
}
// 重载()
void operator()() {
cout << "---------Java下网站页面如下---------:" << endl;
this->header();
this->footer();
this->left();
this->content();
cout << endl;
}
};
class Python {
public:
void header() {
cout << "[公共头部]首页、公开课、登陆、注册..." << endl;
}
void footer() {
cout << "[公共底部]帮助中心、交流合作、站内地图..." << endl;
}
void left() {
cout << "[公共分类列表]Java、Python、C++..." << endl;
}
void content() {
cout << "Python学科视频" << endl;
}
// 重载()
void operator()() {
cout << "---------Python下网站页面如下---------:" << endl;
this->header();
this->footer();
this->left();
this->content();
cout << endl;
}
};
class CPP {
public:
void header() {
cout << "[公共头部]首页、公开课、登陆、注册..." << endl;
}
void footer() {
cout << "[公共底部]帮助中心、交流合作、站内地图..." << endl;
}
void left() {
cout << "[公共分类列表]Java、Python、C++..." << endl;
}
void content() {
cout << "C++学科视频" << endl;
}
// 重载()
void operator()() {
cout << "---------C++下网站页面如下---------:" << endl;
this->header();
this->footer();
this->left();
this->content();
cout << endl;
}
};
void test01() {
Java java;
java();
Python python;
python();
CPP cpp;
cpp();
/*
---------Java下网站页面如下---------:
[公共头部]首页、公开课、登陆、注册...
[公共底部]帮助中心、交流合作、站内地图...
[公共分类列表]Java、Python、C++...
Java学科视频
---------Python下网站页面如下---------:
[公共头部]首页、公开课、登陆、注册...
[公共底部]帮助中心、交流合作、站内地图...
[公共分类列表]Java、Python、C++...
Python学科视频
---------C++下网站页面如下---------:
[公共头部]首页、公开课、登陆、注册...
[公共底部]帮助中心、交流合作、站内地图...
[公共分类列表]Java、Python、C++...
C++学科视频
*/
}
int main() {
test01();
system("pause");
return 0;
}
可以发现,当我们在写Java
类、Python
类、CPP
类时,有太多重复的代码了。因此这样的代码是不规范的,我们应该是继承来提高代码的复用率!
继承实现代码:
#include <iostream>
#include <string>
using namespace std;
// 继承实现页面
class BasePage { // 公共页面类
public: // 公共的信息
void header() {
cout << "[公共头部]首页、公开课、登陆、注册..." << endl;
}
void footer() {
cout << "[公共底部]帮助中心、交流合作、站内地图..." << endl;
}
void left() {
cout << "[公共分类列表]Java、Python、C++..." << endl;
}
};
// Java页面
class Java : public BasePage {
public:
void content() {
cout << "Java学科视频" << endl;
}
// 重载()
void operator()() {
cout << "---------Java下网站页面如下---------:" << endl;
this->header();
this->footer();
this->left();
this->content();
cout << endl;
}
};
// Python页面
class Python : public BasePage {
public:
void content() {
cout << "Python学科视频" << endl;
}
// 重载()
void operator()() {
cout << "---------Python下网站页面如下---------:" << endl;
this->header();
this->footer();
this->left();
this->content();
cout << endl;
}
};
// C++页面
class CPP : public BasePage {
public:
void content() {
cout << "C++学科视频" << endl;
}
// 重载()
void operator()() {
cout << "---------C++下网站页面如下---------:" << endl;
this->header();
this->footer();
this->left();
this->content();
cout << endl;
}
};
void test01() {
// 使用匿名对象
Java()();
Python()();
CPP()();
/*
---------Java下网站页面如下---------:
[公共头部]首页、公开课、登陆、注册...
[公共底部]帮助中心、交流合作、站内地图...
[公共分类列表]Java、Python、C++...
Java学科视频
---------Python下网站页面如下---------:
[公共头部]首页、公开课、登陆、注册...
[公共底部]帮助中心、交流合作、站内地图...
[公共分类列表]Java、Python、C++...
Python学科视频
---------C++下网站页面如下---------:
[公共头部]首页、公开课、登陆、注册...
[公共底部]帮助中心、交流合作、站内地图...
[公共分类列表]Java、Python、C++...
C++学科视频
*/
}
int main() {
test01();
system("pause");
return 0;
}
4.6.2 继承方式
继承的语法: class 子类 : 继承方式 父类 {};
继承方式一共有三种:
- 公共继承 ——
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 继承中的对象模型.cpp
class Son size(16):
+---
0 | +--- (base class Base)
0 | | a
4 | | b
8 | | c
| +---
12 | d
+---
示例:
#include <iostream>
#include <string>
using namespace std;
// 继承中的对象模型
class Base {
public:
int a;
protected:
int b;
private:
int c;
};
class Son : public Base {
public:
int d;
};
void test03_1() {
cout << "sizeof(Son): " << sizeof(Son) << "字节" << endl; // sizeof(Son): 16字节
}
int main() {
test03_1();
system("pause");
return 0;
}
结论:父类中私有成员也是被子类继承下去了,只是由编译器给隐藏后访问不到。
4.6.4 继承中构造Constructor和析构Destructor顺序
子类继承父类后,当创建子类对象,也会调用父类的构造函数。
问题:父类和子类的构造和析构顺序是谁先谁后?
回答:继承中的构造和析构顺序如下:
- 先构造父类,再构造子类
- 析构的顺序与构造的顺序相反 -> 先析构子类,再析构父类
示例:
#include <iostream>
#include <string>
using namespace std;
// 继承中的构造和析构顺序
class Base04 {
public:
Base04() {
cout << "Base04的构造Constructor函数" << endl;
}
~Base04() {
cout << "Base04的析构Destructor函数" << endl;
}
};
class Son04 : public Base04 {
public:
Son04() {
cout << "Son04的构造Constructor函数" << endl;
}
~Son04() {
cout << "Son04的析构Destructor函数" << endl;
}
};
void test04_1() {
cout << "---------test04_1-----------" << endl;
Base04 base;
/*
Base04的构造Constructor函数
Base04的析构Destructor函数
*/
}
void test04_2() {
cout << "---------test04_2-----------" << endl;
Son04 son;
/*
Base04的构造Constructor函数
Son04的构造Constructor函数
Son04的析构Destructor函数
Base04的析构Destructor函数
*/
}
int main() {
test04_1();
test04_2();
system("pause");
return 0;
}
4.6.5 继承同名成员处理方式
问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?
- 访问子类同名成员直接访问即可:
子类.成员函数()/成员属性
- 访问父类同名成员需要加作用域:
子类.父类::成员函数()/成员属性
注意:如果子类中出现和父类同名的成员函数,那么子类的同名成员函数会隐藏掉所有父类的同名成员函数,即不能直接访问,想访问需要加作用域。
总结:
- 子类对象可以直接访问到子类中同名成员
- 子类对象加作用域可以访问到父类同名成员
- 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
直接访问的是自身的属性,访问父类就加上作用域。
示例:
#include <iostream>
#include <string>
using namespace std;
// 继承中同名成员的处理
class Base05 {
public:
Base05() {
this->a = 100;
}
void func() {
cout << "父类的func函数调用" << endl;
}
void func(int a) { // 函数重载
cout << "父类的func(int a)函数调用" << endl;
}
int a;
};
class Son05 : public Base05 {
public:
Son05() {
this->a = 200;
}
void func() {
cout << "子类的func函数调用" << endl;
}
int a;
};
// 成员属性的处理
void test05_1() {
Son05 son;
cout << "子类的a: " << son.a << endl; // a: 子类的a: 200
cout << "父类的a: " << son.Base05::a << endl; // 父类的a: 100
}
// 成员函数的处理
void test05_2() {
Son05 son;
son.func(); // 子类的func函数调用
son.Base05::func(); // 父类的func函数调用
// 如果子类中出现和父类同名的成员函数,那么子类的同名成员函数会
// 隐藏掉所有父类的同名成员函数 -> 不能直接访问,想访问需要加作用域
// son.func(100); // 函数调用中的参数太多
son.Base05::func(100); // 父类的func(int a)函数调用
}
int main() {
test05_1();
test05_2();
system("pause");
return 0;
}
4.6.6 继承同名静态成员处理方式
问题:继承中同名的静态成员在子类对象上如何进行访问?
静态成员和非静态成员出现同名,处理方式一致:
- 访问子类同名成员直接访问即可
- 访问父类同名成员需要加作用域
总结:同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象和通过类名)
可以通过匿名对象来调用!
示例:
#include <iostream>
#include <string>
using namespace std;
/*
继承中同名静态成员的处理方式:
子类出现和父类同名的静态成员函数, 和普通成员函数一样,
编译器会为子类隐藏父类的同名静态成员函数。
如果想访问父类中被隐藏的同名成员函数,需要加作用域!
*/
class Base06 {
public:
static int a; // 需要在类外初始化
static void func() {
cout << "[Base06]static void func()的调用" << endl;
}
};
int Base06::a = 100;
class Son06 : public Base06 {
public:
static int a;
static void func() {
cout << "[Son06]static void func()的调用" << endl;
}
};
int Son06::a = 200;
// 同名静态成员属性
void test06_1() {
cout << "=========1. 同名静态成员属性===========" << endl;
// 访问方式1:通过对象访问静态数据
cout << "--------访问方式1:通过对象访问静态数据--------" << endl;
Son06 son;
cout << "[Son] a: " << son.a << endl; // [Son] a: 200
cout << "[Base] a: " << son.Base06::a << endl; // [Base] a: 100
// 访问方式2:通过类名访问静态数据
cout << "--------访问方式2:通过类名访问静态数据--------" << endl;
cout << "[Son] a: " << Son06::a << endl; // [Son] a: 200
cout << "[Base] a: " << Son06::Base06::a << endl; // [Base] a: 100
// 访问方式3:通过匿名对象访问静态数据
cout << "--------访问方式3:通过匿名对象访问静态数据--------" << endl;
cout << "[Son] a: " << Son06().a << endl; // [Son] a: 200
cout << "[Base] a: " << Son06().Base06::a << endl; // [Base] a: 100
cout << "[Base] a: " << Son06::Base06().a << endl; // [Base] a: 100
}
// 同名静态成员函数
void test06_2() {
cout << "\r\n=========2. 同名静态成员函数===========" << endl;
// 访问方式1:通过对象访问静态成员函数
cout << "--------访问方式1:通过对象访问静态数据--------" << endl;
Son06 son;
son.func(); // [Son06]static void func()的调用
son.Base06::func(); // [Base06]static void func()的调用
// 访问方式2:通过类名访问静态成员函数
cout << "--------访问方式2:通过类名访问静态成员函数--------" << endl;
Son06::func(); // [Son06]static void func()的调用
Son06::Base06::func(); // [Base06]static void func()的调用
// 访问方式3:通过匿名对象访问静态成员函数
cout << "--------访问方式3:通过匿名对象访问静态成员函数--------" << endl;
Son06().func(); // [Son06]static void func()的调用
Son06().Base06::func(); // [Base06]static void func()的调用
Son06::Base06::func(); // [Base06]static void func()的调用
}
int main() {
test06_1();
test06_2();
system("pause");
return 0;
}
4.6.7 多继承语法
C++允许一个类继承多个类。
语法:class 子类 : 继承方式 父类1, 继承方式 父类2, ...
多继承可能会引发父类中有同名成员出现,需要加作用域区分!
总结:多继承中如果父类中出现了同名情况,子类使用时候要加作用域,否则会出现歧义。
因此,C++实际开发中不建议用多继承
示例:
#include <iostream>
#include <string>
using namespace std;
// 多继承的语法
class Base07_1 {
public:
Base07_1() {
this->a = 100;
}
int a;
};
class Base07_2 {
public:
Base07_2() {
this->a = 200;
}
int a;
};
// 子类:需要继承Base1和Base2
// 语法:class 子类 : 继承方式 父类1, 继承方式 父类2, ... {};
class Son07 : public Base07_1, public Base07_2 {
public:
Son07() {
c = 300;
d = 400;
}
int c;
int d;
};
void test07_1() {
Son07 son;
cout << "sizeof(son): " << sizeof(son) << endl; // sizeof(son): 16
/*
用工具查看:
class Son07 size(16):
+---
0 | +--- (base class Base07_1)
0 | | a
| +---
4 | +--- (base class Base07_2)
4 | | a
| +---
8 | c
12 | d
+---
*/
// cout << "a: " << son.a << endl; // "SonO7::a"不明确
// 当父类VS出现同名成员,需要加作用域区分(因此在实际开发中不太建议使用多继承!)
cout << "a: " << son.Base07_1::a << endl; // a: 100
cout << "a: " << son.Base07_2::a << endl; // a: 200
}
int main() {
test07_1();
system("pause");
return 0;
}
4.6.8 菱形继承
菱形继承概念:
- 两个派生类(子类)继承同一个基类(父类)
- 又有某个类同时继承者两个派生类
- 这种继承被称为菱形继承,或者钻石继承
典型的菱形继承案例:
举个例子:
菱形继承的问题:
- 羊继承了动物的数据,驼同样继承了动物的数据,当羊驼使用数据时,就会产生二义性。
- 羊驼继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
解决方案:利用虚继承可以解决菱形继承带来的数据重复问题!
语法:在继承方式前加上关键字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: 18
cout << "alpaca.Camel::age: " << alpaca.Camel::age << endl; // alpaca.Camel::age: 22
/*
问题来了:羊驼的age是多少?是18该是22?
这份数据我们知道,只有一份就可以了,而菱形继承导致数据有两份,
这会导致资源浪费!
我们用工具看一下:
class Alpaca size(8):
+---
0 | +--- (base class Sheep)
0 | | +--- (base class Animal)
0 | | | age
| | +---
| +---
4 | +--- (base class Camel)
4 | | +--- (base class Animal)
4 | | | age
| | +---
| +---
+---
的确是有两份!
解决方案:利用虚继承可以解决菱形继承带来的数据重复问题!
语法:在继承方式前加上关键字virtual即可。
*/
// 使用虚继承之后,两个结果为:
alpaca.Sheep::age = 18;
alpaca.Camel::age = 22;
cout << "alpaca.Sheep::age: " << alpaca.Sheep::age << endl; // alpaca.Sheep::age: 22
cout << "alpaca.Camel::age: " << alpaca.Camel::age << endl; // alpaca.Camel::age: 22
// 而且我们也可以不加作用域直接使用age了,因为只有一份
cout << "alpaca.age: " << alpaca.age << endl; // alpaca.age: 22
/*
我们再用工具看一下:
class Alpaca size(12):
+---
0 | +--- (base class Sheep)
0 | | {vbptr}
| +---
4 | +--- (base class Camel)
4 | | {vbptr}
| +---
+---
+--- (virtual base Animal)
8 | age
+---
Alpaca::$vbtable@Sheep@:
0 | 0
1 | 8 (Alpacad(Sheep+0)Animal) // 这里的8是偏移量(Sheep的vbptr+8就可以找到age)
Alpaca::$vbtable@Camel@:
0 | 0
1 | 4 (Alpacad(Camel+0)Animal) // 这里的4是偏移量(Camel的vbptr+4就可以找到age)
vbi: class offset o.vbptr o.vbte fVtorDisp
Animal 8 0 4 0
vbptr: 虚基类指针(Virtual Base Pointer)。它会指向vbtable(虚基类表)
*/
}
int main() {
test08_1();
system("pause");
return 0;
}
4.7 多态
顾名思义,多态就是多种形态。
4.7.1 多态的基本概念
多态是C++面向对象三大特性之一。多态分为两类:
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
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成员函数前加关键字virtual
do_speak(cat); // 喵喵喵...
Dog dog;
do_speak(dog); // 汪汪汪...
}
int main() {
test01_1();
system("pause");
return 0;
}
使用工具进行分析:
1、不加虚函数的:
class Animal size(1):
+---
+---
2、加了虚函数的
class Animal size(4):
+---
0 | {vfptr}
+---
Animal::$vftable@:
| &Animal_meta
| 0
0 | &Animal::speak
- vfptr: virtual function pointer,虚函数(表)指针
- vftable: virtual function table: 虚函数表
3、当Cat
类没有发生speak
虚函数重写时:
class Cat size(4):
+---
0 | +--- (base class Animal)
0 | | {vfptr}
| +---
+---
Cat::$vftable@:
| &Cat_meta
| 0
0 | &Animal::speak
3、Cat
类重写speak
虚函数:
class Cat size(4):
+---
0 | +--- (base class Animal)
0 | | {vfptr}
| +---
+---
Cat::$vftable@:
| &Cat_meta
| 0
0 | &Cat::speak
因此当时利用父类的指针或引用 指向 子类对象,调用speak
虚函数时,就会从vftable
中找到&Cat::speak
的地址,即调用子类重写的虚函数,而不是父类的虚函数。
4.7.2 多态案例一:计算器类
案例描述:
分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类。
多态的优点:
- 代码组织结构清晰
- 可读性强
- 利于前期和后期的扩展以及维护
总结:C++开发提倡利用多态设计程序架构,因为多态优点很多。
示例:
#include <iostream>
#include <string>
using namespace std;
// 分别利用普通写法和多态技术实现计算器
// 普通写法
class Calculator {
public:
int get_res(string oper) {
if (oper == "+")
{
return this->num1 + this->num2;
}
else if (oper == "-")
{
return this->num1 - this->num2;
}
else if (oper == "*")
{
return this->num1 * this->num2;
}
else if (oper == "/")
{
return this->num1 / this->num2;
}
/*
如果想扩展新的功能(** % ...),需要修改源码,
在真实的开发中,我们提倡一种原则:开闭原则。
开闭原则:对扩展进行开放,对修改进行关闭。
*/
}
public:
int num1; // 操作数1
int num2; // 操作数2
};
void test02_1() {
// 创建计算器对象
Calculator calc;
calc.num1 = 10;
calc.num2 = 10;
cout << calc.num1 << " + " << calc.num2 << " = " << calc.get_res("+") << endl;
cout << calc.num1 << " - " << calc.num2 << " = " << calc.get_res("-") << endl;
cout << calc.num1 << " * " << calc.num2 << " = " << calc.get_res("*") << endl;
cout << calc.num1 << " / " << calc.num2 << " = " << calc.get_res("/") << endl;
}
// 利用多态实现计算器
// 实现计算器的抽象类(接口) —— 和之前学的设计模式的思想是一样的
class AbstractCalculator {
public:
virtual int get_res() {
return 0;
}
public:
int num1;
int num2;
};
// 加法计算器类
class AddCalculator : public AbstractCalculator {
public:
virtual int get_res() {
return this->num1 + this->num2;
}
};
// 减法计算器类
class SubCalculator : public AbstractCalculator {
virtual int get_res() {
return this->num1 - this->num2;
}
};
// 乘法计算器类
class MulCalculator : public AbstractCalculator {
virtual int get_res() {
return this->num1 * this->num2;
}
};
// 除法计算器类
class DivCalculator : public AbstractCalculator {
virtual int get_res() {
return this->num1 / this->num2;
}
};
void test02_2() {
/*
多态的使用条件:父类的指针/引用 指向 子类的对象
*/
// 加法运算
AbstractCalculator* abs_calc = new AddCalculator;
abs_calc->num1 = 100;
abs_calc->num2 = 100;
cout << abs_calc->num1 << " + " << abs_calc->num2 << " = " << abs_calc->get_res() << endl; // 100 + 100 = 200
// 用完后记得销毁(只是把数据销毁了,指针还在)
delete abs_calc;
// 减法运算
abs_calc = new SubCalculator;
abs_calc->num1 = 100;
abs_calc->num2 = 100;
cout << abs_calc->num1 << " - " << abs_calc->num2 << " = " << abs_calc->get_res() << endl; // 100 - 100 = 0
// 用完后记得销毁(只是把数据销毁了,指针还在)
delete abs_calc;
// 乘法运算
abs_calc = new MulCalculator;
abs_calc->num1 = 100;
abs_calc->num2 = 100;
cout << abs_calc->num1 << " * " << abs_calc->num2 << " = " << abs_calc->get_res() << endl; // 100 * 100 = 10000
// 用完后记得销毁(只是把数据销毁了,指针还在)
delete abs_calc;
// 除法运算
abs_calc = new DivCalculator;
abs_calc->num1 = 100;
abs_calc->num2 = 100;
cout << abs_calc->num1 << " / " << abs_calc->num2 << " = " << abs_calc->get_res() << endl; // 100 / 100 = 1
// 用完后记得销毁(只是把数据销毁了,指针还在)
delete abs_calc;
/*
多态带来的好处:
1. 组织结构清晰
2. 可读性强
3. 便于前期/后期的扩展和维护
*/
}
int main() {
test02_1();
test02_2();
system("pause");
return 0;
}
4.7.3 纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数。
纯虚函数语法:virtual 返回值类型 函数名(参数列表) = 0;
(不需要实现了)
当类中有了纯虚函数,这个类也称为抽象类(Abstract Class)(只要有一个就算抽象类)。
抽象类特点:
- 无法实例化对象(因为它本身就没有什么意义)
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
跟之前学习的设计模式中的思想是一样的。
重写纯虚函数,即便函数体内部是空的,也是重写,就可以实例化了!
示例:
#include <iostream>
#include <string>
using namespace std;
// 纯虚函数和抽象类
class Base {
public:
/*
只要有一个纯虚函数,这个类就称为抽象类
抽象类的特点:
1. 无法实例化对象(new也不行)
2. 抽象类的子类必须要重写父类中的纯虚函数,否则也属于抽象类
*/
virtual void func() = 0; // 纯虚函数
};
class Son1 : public Base {};
class Son2 : public Base {
public:
virtual void func() { // 重写纯虚函数(虽然函数体中没有内容,但这也算是重写)
}
};
class Son3 : public Base {
public:
virtual void func() {
cout << "func函数调用" << endl;
}
};
void test03_1() {
// 1. 抽象类是无法实例化对象的,new也不行
// Base base; // Error: 不允许使用抽象类类型"Base"的对象
// new Base; // Error: 不允许使用抽象类类型"Base"的对象
// 2. 抽象类的子类必须要重写父类中的纯虚函数,否则也属于抽象类
// Son1 son; // Error: 不允许使用抽象类类型"Son1"的对象
// new Son1; // Error: 不允许使用抽象类类型"Son1"的对象
// 3. 重写纯虚函数
Son2 son; // 不报错(可以正常实例化)
new Son2; // 不报错(可以正常实例化)
// 普通调用
Son3 son3;
son3.func(); // func函数调用
// 利用多态调用
Base* base = new Son3;
base->func(); // func函数调用
}
int main() {
test03_1();
system("pause");
return 0;
}
4.7.4 多态案例二:制作饮品
案例描述:
制作饮品的大致流程为:煮水->冲泡->倒入杯中->加入辅料
利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶。
#include <iostream>
#include <string>
using namespace std;
// 多态案例2:制作饮品
class AbstractMakeDrinking {
public:
// 1. 煮水
virtual void boil() = 0;
// 2. 冲泡
virtual void brew() = 0;
// 3. 倒入杯中
virtual void pour_in_cup() = 0;
// 4. 加入辅助酌料
virtual void put_somethings() = 0;
// 制作饮品(按顺序执行前面4步)
void make_drinking() {
this->boil();
this->brew();
this->pour_in_cup();
this->put_somethings();
}
};
// 制作咖啡
class Coffee : public AbstractMakeDrinking {
public: // 重写纯虚函数
// 1. 煮水
virtual void boil() {
cout << "Step 1 煮农夫山泉" << endl;
}
// 2. 冲泡
virtual void brew() {
cout << "Step 2 冲泡咖啡" << endl;
}
// 3. 倒入杯中
virtual void pour_in_cup() {
cout << "Step 3 倒入杯中" << endl;
}
// 4. 加入辅助酌料
virtual void put_somethings() {
cout << "Step 4 加入糖和牛奶" << endl;
}
};
// 制作茶水
class Tee : public AbstractMakeDrinking {
public: // 重写纯虚函数
// 1. 煮水
virtual void boil() {
cout << "Step 1 煮矿泉水" << endl;
}
// 2. 冲泡
virtual void brew() {
cout << "Step 2 冲泡茶叶" << endl;
}
// 3. 倒入杯中
virtual void pour_in_cup() {
cout << "Step 3 倒入杯中" << endl;
}
// 4. 加入辅助酌料
virtual void put_somethings() {
cout << "Step 4 加入枸杞和柠檬" << endl;
}
};
// 制作函数
void do_work(AbstractMakeDrinking* abs) {
abs->make_drinking();
// 释放资源
delete abs;
}
void test04_1() {
// 制作咖啡
do_work(new Coffee);
/*
Step 1 煮农夫山泉
Step 2 冲泡咖啡
Step 3 倒入杯中
Step 4 加入糖和牛奶
*/
cout << "--------------------------" << endl;
// 制作茶水
do_work(new Tee);
/*
Step 1 煮矿泉水
Step 2 冲泡茶叶
Step 3 倒入杯中
Step 4 加入枸杞和柠檬
*/
}
int main() {
test04_1();
system("pause");
return 0;
}
4.7.5 虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,这会造成内存的泄露。
在C++中主要利用
new
在堆区开辟内存
解决方式:将父类中的析构函数改为虚析构或者纯虚析构。
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
- 虚析构语法:
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; // 最小值或最大值的index
for (int j = i+1; j < this->employee_num; j++)
{
if (sort_mode == 1) // 升序
{
if (this->employee_arr[min_or_max]->id > this->employee_arr[j]->id)
{
min_or_max = j;
}
}
else if (sort_mode == 2) // 降序
{
if (this->employee_arr[min_or_max]->id < this->employee_arr[j]->id)
{
min_or_max = j;
}
}
else
{
cout << "选择有误!" << endl;
post_processing();
return;
}
}
// 判断一开始认定的最小值/最大值是不是计算的最小值或最大值,如果不是,则交互
if (i != min_or_max)
{
Worker* tmp = this->employee_arr[i]; // 记录第i个元素
this->employee_arr[i] = this->employee_arr[min_or_max];
this->employee_arr[min_or_max] = tmp;
}
}
cout << "排序成功,排序后的结果为: " << endl;
this->save(); // 排序后的结果保存到文件中
this->show_employee(); // 展示所有的职工
}
}
这里排序使用的是选择排序:先选定一个最小值,再遍历找真正的最小值,然后下一轮。
6.14 清空文件
功能描述:将文件中记录数据清空。
6.14.1 清空函数声明
在workerManager.h
中添加成员函数void clean_file();
//清空文件
void clean_file();
6.14.2 清空函数实现
在workerManager.cpp
中实现员函数void clean_file();
一个指针数组,直接
delete
是不好的,应该把数组里面每个元素都delete
后再delete
。
// 清空文件
void WorkerManager::clean_file() {
if (this->file_is_empty)
{
cout << "文件不存在或记录为空,无须清空!" << endl;
post_processing();
return;
}
cout << "确定清空文件(此操作无法撤销)? (Y/N)";
string user_chioce;
cin >> user_chioce;
if (user_chioce == "Y" || user_chioce == "y")
{
// 清空文件
ofstream ofs(FILENAME, ios::trunc); // 删除文件后重新创建文件
ofs.close();
if (this->employee_arr != NULL)
{
// 删除堆区的每个职工对象
for (int i = 0; i < this->employee_num; i++)
{
if (this->employee_arr[i] != NULL)
{
delete this->employee_arr[i];
this->employee_arr[i] = NULL;
}
}
// 删除堆区的数组指针
delete[] this->employee_arr;
this->employee_arr = NULL;
this->employee_num = 0;
this->file_is_empty = true;
}
cout << "清空成功!" << endl;
post_processing();
return;
}
else if (user_chioce == "N" || user_chioce == "n")
{
cout << "取消清空操作" << endl;
post_processing();
return;
}
else
{
cout << "您的输入有误" << endl;
post_processing();
return;
}
}
6.15 所有代码
职工管理系统.cpp
#include <iostream>
using namespace std;
#include "workerManager.h"
#include "woker.h"
#include "employee.h"
#include "manager.h"
#include "boss.h"
int main() {
// 测试代码
/* Worker* worker = new Employee(1, "张三", 1);
worker->show_info();
delete worker;
worker = new Manager(2, "李四", 2);
worker->show_info();
delete worker;
worker = new Boss(3, "王五", 3);
worker->show_info();
cout << endl*/;
// 实例化WorkerManager对象
WorkerManager wm;
int choice = -1; // 用来存储用户的选项
while (true)
{
// 调用WorkerManager的show_menu成员函数
wm.show_menu();
cout << "请输入您的选择: ";
cin >> choice; // 接收用户的键盘输入
switch (choice)
{
case 0: // 退出系统
wm.exit_system();
//break; // 这里的break就没有意义了
case 1: // 增加职工
wm.add_employee();
break;
case 2: // 显示职工
wm.show_employee();
break;
case 3: // 删除职工
wm.delete_employee();
break;
case 4: // 修改职工
wm.modify_employee();
break;
case 5: // 查找职工
wm.fine_employee();
break;
case 6: // 排序职工
wm.sort_employee();
break;
case 7: // 清空文档
wm.clean_file();
break;
default: // 继续选择
system("cls"); // 清屏操作
break;
}
}
system("pause");
return 0;
}
work.h
#pragma once
#include <iostream>
using namespace std;
#include <string>
/*
因为Worker类是纯虚类,因此不需要再写一个cpp文件来实现它了,
它就是一个Interface
*/
// 职工抽象基类
class Worker {
public:
// 显示个人信息
virtual void show_info() = 0;
// 获取岗位名称
virtual string get_dept_name() = 0;
public:
int id; // 职工编号
string name; // 职工姓名
int dept_id; // 部门编号
};
workermanager.h
#pragma once // 防止头文件重复包含
#include <iostream>
using namespace std; // 使用标准的命名空间
#include "woker.h"
#include "employee.h"
#include "manager.h"
#include "boss.h"
#include <fstream>
#define FILENAME "employee.txt"
class WorkerManager {
public:
// 构造函数
WorkerManager();
// 析构函数
~WorkerManager();
public:
// 展示菜单
void show_menu();
// 退出系统
void exit_system();
// 记录职工人数
int employee_num;
// 职工数组指针
Worker** employee_arr;
// 添加职工
void add_employee();
// 保存文件
void save();
// 判断文件是否为空
bool file_is_empty;
// 统计文件中的人数
int get_employee_num();
// 初始化员工
void init_employee();
// 显示职工
void show_employee();
// 删除职工
void delete_employee();
// 判断职工时候存在,存在返回职工在数组中为idx,不存在则返回-1
int is_exist(int id);
// 修改职工
void modify_employee();
// 查找职工
void fine_employee();
// 按照职工编号排序
void sort_employee();
// 清空文件
void clean_file();
};
workermanager.cpp
#include "workerManager.h"
#include "postprocessing.h" // 后处理
WorkerManager::WorkerManager() {
// 第一种情况:文件不存在
ifstream ifs;
ifs.open(FILENAME, ios::in); // 读文件
if (!ifs.is_open()) // 文件不存在
{
cout << "文件不存在" << endl;
// 初始化属性
this->employee_num = 0; // 初始化记录人数
this->employee_arr = NULL; // 初始化数组指针
this->file_is_empty = true; // 初始化文件是否为空
ifs.close();
return;
}
// 第二种情况:文件存在,但数据为空
char ch;
ifs >> ch; // 就读取一个字符
if (ifs.eof()) // eof: end of file, 文件末尾
{
// 就读取一个字符,如果该字符是EOF,那么说明文件中没有内容
cout << "文件为空" << endl;
// 初始化属性
this->employee_num = 0; // 初始化记录人数
this->employee_arr = NULL; // 初始化数组指针
this->file_is_empty = true; // 初始化文件是否为空
ifs.close();
return;
}
// 第三种情况:文件存在,并且记录数据
int num = this->get_employee_num();
cout << "职工的人数为: " << num << endl;
this->employee_num = num;
// 开辟空间
this->employee_arr = new Worker * [this->employee_num];
// 将文件中的数据初始化(存到维护的数组中)
this->init_employee();
//for (int i = 0; i < this->employee_num; i++)
//{
// cout << "职工编号: " << this->employee_arr[i]->id
// << "\t职工姓名: " << this->employee_arr[i]->name
// << "\t部门编号: " << this->employee_arr[i]->dept_id
// << endl;
//}
}
WorkerManager::~WorkerManager() {
if (this->employee_arr != NULL)
{
delete[] this->employee_arr;
this->employee_arr = NULL;
}
}
// 展示菜单
void WorkerManager::show_menu() {
cout << "*****************************************" << endl;
cout << "********* 欢迎使用职工管理系统! ********" << endl;
cout << "*********** 0. 退出管理程序 *************" << endl;
cout << "*********** 1. 增加职工信息 *************" << endl;
cout << "*********** 2. 显示职工信息 *************" << endl;
cout << "*********** 3. 删除离职职工 *************" << endl;
cout << "*********** 4. 修改职工信息 *************" << endl;
cout << "*********** 5. 查找职工信息 *************" << endl;
cout << "*********** 6. 按照编号排序 *************" << endl;
cout << "*********** 7. 清空所有文档 *************" << endl;
cout << "*****************************************" << endl;
cout << endl;
}
// 退出系统
void WorkerManager::exit_system() {
cout << "欢迎下次使用!" << endl;
system("pause");
exit(0); // 退出程序
/*
C++中exit()函数的参数是整数类型的参数,通常称为“退出状态码”(exit status code)或“返回值”(return value)。这个参数表示程序正常或异常退出的原因,如果参数为0,则表示程序正常退出,其他非零参数则表示程序异常退出,并且参数值通常用来表示错误代码或异常情况的类型。
*/
}
// 添加职工
void WorkerManager::add_employee() {
cout << "请输入添加职工的数量: ";
int add_num = 0; // 保存添加职工的数量
cin >> add_num;
if (add_num > 0)
{
// 添加
// 计算添加新空间大小(新空间人数 = 原来记录的人数 + 新增人数)
int new_size = this->employee_num + add_num;
// 开辟新空间
Worker** new_space = new Worker* [new_size];
// 将原来空间下的数据拷贝到新空间下
if (this->employee_arr != NULL)
{
for (int i = 0; i < this->employee_num; i++)
{
new_space[i] = this->employee_arr[i];
}
}
// 添加新数据
for (int i = 0; i < add_num; i++) {
int id; // 职工编号
string name; // 职工姓名
int dept_select; // 部门选择
cout << "请输入第" << i + 1 << "个新职工编号: ";
cin >> id;
cout << "请输入第" << i + 1 << "个新职工姓名: ";
cin >> name;
cout << "请选择该职工的岗位(1->职工; 2->经理; 3->老板): ";
cin >> dept_select;
Worker* worker = NULL;
switch (dept_select)
{
case 1:
worker = new Employee(id, name, 1);
break;
case 2:
worker = new Manager(id, name, 2);
break;
case 3:
worker = new Boss(id, name, 3);
break;
default:
break;
}
// 将创建的职工指针,保存到数组中
new_space[this->employee_num + i] = worker;
}
// 释放原有的空间
delete[] this->employee_arr;
// 更改新空间的指向
this->employee_arr = new_space;
// 更新新的职工人数
this->employee_num = new_size;
// 成功添加后保存到文件中
this->save();
// 更新file_is_empty
this->file_is_empty = false;
// 提示添加成功
cout << "成功添加" << add_num << "名新职工" << endl;
}
else
{
cout << "输入数据有误!" << endl;
}
// 按任意键后清屏回到上级目录
post_processing();
}
// 保存文件
void WorkerManager::save() {
ofstream ofs;
ofs.open(FILENAME, ios::out);
// 将每个人的数据写入到文件中
for (int i = 0; i < this->employee_num; i++)
{
ofs << this->employee_arr[i]->id << " "
<< this->employee_arr[i]->name << " "
<< this->employee_arr[i]->id << endl;
}
ofs.close();
}
int WorkerManager::get_employee_num() {
ifstream ifs;
ifs.open(FILENAME, ios::in);
int id;
string name;
int dept_id;
int count_num = 0;
while (ifs >> id && ifs >> name && ifs >> dept_id)
{
// 统计人数变量
count_num += 1;
}
/*
在这个 while 循环中,每次循环都会先调用 ifs 对象的运算符重载,
从文件中读取一个整数类型的数据,并将其存储到 id 变量中。
如果读取成功,则继续调用运算符重载,从文件中读取一个字符串
类型的数据,并将其存储到 name 变量中。
如果读取成功,则再次调用运算符重载,从文件中读取一个整数类
型的数据,并将其存储到 dept_id 变量中。
如果这三次读取都成功,那么 while 循环的条件判断为真,
循环体中的代码就会被执行;否则,循环终止。
*/
return count_num;
}
// 初始化员工
void WorkerManager::init_employee() {
ifstream ifs;
ifs.open(FILENAME, ios::in);
int id;
string name;
int dept_id;
int idx = 0;
while (ifs >> id && ifs >> name && ifs >> dept_id)
{
Worker* worker = NULL;
if (dept_id == 1) // 普通员工
{
worker = new Employee(id, name, dept_id);
}
else if (dept_id == 2) // 经理
{
worker = new Manager(id, name, dept_id);
}
else // 老板
{
worker = new Boss(id, name, dept_id);
}
this->employee_arr[idx] = worker;
idx += 1;
}
ifs.close();
}
void WorkerManager::show_employee() {
// 判断文件是否为空
if (this->file_is_empty)
{
cout << "文件不存在或记录为空" << endl;
}
else
{
for (int i = 0; i < this->employee_num; i++)
{
// 利用多态调用程序接口
this->employee_arr[i]->show_info();
}
}
// 按任意键后清屏
post_processing();
}
// 删除职工
void WorkerManager::delete_employee() {
if (this->file_is_empty)
{
cout << "文件不存在或记录为空" << endl;
}
else
{
// 按照职工编号删除职工
cout << "请输入想要删除职工的编号: ";
int id;
cin >> id;
int idx = this->is_exist(id);
if (idx != -1) // 职工存在
{
// 删除到idx位置上的职工
for (int i = idx; i < this->employee_num - 1; i++)
{
this->employee_arr[i] = this->employee_arr[i + 1];
}
// 更新数组中记录的人员个数
this->employee_num -= 1;
// 将数组中的数据同步更新到文件中
this->save();
cout << "删除成功!" << endl;
}
else
{
cout << "查无此人,删除失败!" << endl;
}
}
post_processing(); // 按任意键后清屏
}
// 判断职工时候存在,存在返回职工在数组中为idx,不存在则返回-1
int WorkerManager::is_exist(int id) {
int idx = -1;
for (int i = 0; i < this->employee_num; i++)
{
if (this->employee_arr[i]->id == id) // 找到职工
{
idx = i;
break;
}
}
return idx;
}
// 修改职工
void WorkerManager::modify_employee() {
if (this->file_is_empty)
{
cout << "文件不存在或记录为空" << endl;
}
else
{
cout << "请输入修改职工的编号: ";
int id;
cin >> id;
int idx = this->is_exist(id);
if (idx != -1) // 找到职工
{
// 先删除原来的职工
delete this->employee_arr[idx];
int new_id;
string new_name;
int new_dept_id;
cout << "查找到" << id << "号职工,请输入新的职工编号: ";
cin >> new_id;
cout << "请输入新的姓名: ";
cin >> new_name;
cout << "请输入新的岗位(1->职工; 2->经理; 3->老板): ";
cin >> new_dept_id;
// 创建新的对象
Worker* worker = NULL;
switch (new_dept_id)
{
case 1:
worker = new Employee(new_id, new_name, new_dept_id);
break;
case 2:
worker = new Manager(new_id, new_name, new_dept_id);
break;
case 3:
worker = new Boss(new_id, new_name, new_dept_id);
break;
default:
break;
}
// 更新数据到数组中
this->employee_arr[idx] = worker;
cout << "修改成功!" << endl;
// 保存到文件中
this->save();
}
else
{
cout << "未找到该职工,修改失败!" << endl;
}
}
post_processing();
}
// 查找职工
void WorkerManager::fine_employee() {
if (this->file_is_empty)
{
cout << "文件不存在或记录为空" << endl;
}
else
{
cout << "请输入查找的方式(1->按职工编号查找;2->按职工姓名查找): ";
int find_method;
cin >> find_method;
if (find_method == 1) // 按照编号查找
{
int id;
cout << "请输入查找的员工编号: ";
cin >> id;
int idx = this->is_exist(id);
if (idx != -1) // 找到职工
{
cout << "查找成功,该职工信息如下: " << endl;
this->employee_arr[idx]->show_info();
}
else
{
cout << "查无此人,查找失败" << endl;
}
}
else if (find_method == 2) // 按照姓名查找
{
string name;
cout << "请输入要查找职工的姓名: ";
cin >> name;
bool find_flag = false; // 是否查到
for (int i = 0; i < this->employee_num; i++)
{
if (this->employee_arr[i]->name == name)
{
if (!find_flag)
{
// 让这行话只显示一次!
cout << "查找成功,该职工信息如下: " << endl;
}
this->employee_arr[i]->show_info();
find_flag = true;
// break; // 不加break了,因为有可能有重名,都让他们显示出来
}
}
if (!find_flag)
{
cout << "查无此人,查找失败" << endl;
}
}
else
{
cout << "输入的选项有误" << endl;
}
}
post_processing();
}
// 按照职工编号排序
void WorkerManager::sort_employee() {
if (this->file_is_empty)
{
cout << "文件不存在或记录为空" << endl;
post_processing();
}
else
{
cout << "请选择排序的方式(1->升序;2->降序): ";
int sort_mode;
cin >> sort_mode;
for (int i = 0; i < this->employee_num; i++)
{
int min_or_max = i; // 最小值或最大值的index
for (int j = i+1; j < this->employee_num; j++)
{
if (sort_mode == 1) // 升序
{
if (this->employee_arr[min_or_max]->id > this->employee_arr[j]->id)
{
min_or_max = j;
}
}
else if (sort_mode == 2) // 降序
{
if (this->employee_arr[min_or_max]->id < this->employee_arr[j]->id)
{
min_or_max = j;
}
}
else
{
cout << "选择有误!" << endl;
post_processing();
return;
}
}
// 判断一开始认定的最小值/最大值是不是计算的最小值或最大值,如果不是,则交互
if (i != min_or_max)
{
Worker* tmp = this->employee_arr[i]; // 记录第i个元素
this->employee_arr[i] = this->employee_arr[min_or_max];
this->employee_arr[min_or_max] = tmp;
}
}
cout << "排序成功,排序后的结果为: " << endl;
this->save(); // 排序后的结果保存到文件中
this->show_employee(); // 展示所有的职工
}
}
// 清空文件
void WorkerManager::clean_file() {
if (this->file_is_empty)
{
cout << "文件不存在或记录为空,无须清空!" << endl;
post_processing();
return;
}
cout << "确定清空文件(此操作无法撤销)? (Y/N)";
string user_chioce;
cin >> user_chioce;
if (user_chioce == "Y" || user_chioce == "y")
{
// 清空文件
ofstream ofs(FILENAME, ios::trunc); // 删除文件后重新创建文件
ofs.close();
if (this->employee_arr != NULL)
{
// 删除堆区的每个职工对象
for (int i = 0; i < this->employee_num; i++)
{
if (this->employee_arr[i] != NULL)
{
delete this->employee_arr[i];
this->employee_arr[i] = NULL;
}
}
// 删除堆区的数组指针
delete[] this->employee_arr;
this->employee_arr = NULL;
this->employee_num = 0;
this->file_is_empty = true;
}
cout << "清空成功!" << endl;
post_processing();
return;
}
else if (user_chioce == "N" || user_chioce == "n")
{
cout << "取消清空操作" << endl;
post_processing();
return;
}
else
{
cout << "您的输入有误" << endl;
post_processing();
return;
}
}
employee.h
// 普通员工文件
#pragma once
#include <iostream>
using namespace std;
#include "woker.h"
class Employee : public Worker {
public:
Employee(int id, string name, int dept_id); // 构造函数
public: // 重写接口的纯虚函数,但是.h文件只做声明,不做实现
// 显示个人信息
virtual void show_info();
// 获取岗位名称
virtual string get_dept_name();
};
employee.cpp
#include "employee.h"
Employee::Employee(int id, string name, int dept_id) { // 构造函数
this->id = id;
this->name = name;
this->dept_id = dept_id;
}
// 重写接口的纯虚函数
void Employee::show_info() { // 显示个人信息
cout << "职工编号: " << this->id << "\t职工姓名: "
<< this->name << "\t岗位: " << this->get_dept_name()
<< "\t岗位职责: 完成经理交给的任务" << endl;
}
string Employee::get_dept_name() { // 获取岗位名称
return "员工";
}
manager.h
#pragma once
#include <iostream>
using namespace std;
#include "woker.h"
class Manager : public Worker {
public:
Manager(int id, string name, int dept_id);
public:
// 显示个人信息
virtual void show_info();
// 获取岗位名称
virtual string get_dept_name();
};
manager.cpp
#include "manager.h"
Manager::Manager(int id, string name, int dept_id) {
this->id = id;
this->name = name;
this->dept_id = dept_id;
}
// 显示个人信息
void Manager::show_info() {
cout << "职工编号: " << this->id << "\t职工姓名: "
<< this->name << "\t岗位: " << this->get_dept_name()
<< "\t岗位职责: 完成老板交给的任务,并给员工下发任务" << endl;
}
// 获取岗位名称
string Manager::get_dept_name() {
// 如果不转换则是C语言的字符串。
// (不转换也没事,编译器会帮我自动转换的)
return string("经理");
}
boss.h
#pragma once
#include <iostream>
using namespace std;
#include "woker.h"
class Boss : public Worker {
public:
Boss(int id, string name, int dept_id);
public:
virtual void show_info();
virtual string get_dept_name();
};
boss.cpp
#include "boss.h"
Boss::Boss(int id, string name, int dept_id) {
this->id = id;
this->name = name;
this->dept_id = dept_id;
}
void Boss::show_info() {
cout << "职工编号: " << this->id << "\t职工姓名: "
<< this->name << "\t岗位: " << this->get_dept_name()
<< "\t岗位职责: 管理公司所有的事务" << endl;
}
string Boss::get_dept_name() {
return string("老板");
}
postprocessing.h
#pragma once
#include <iostream>
using namespace std;
void post_processing();
postprocessing.cpp
#include "postprocessing.h"
void post_processing() {
system("pause");
system("cls");
}