C++ 入门学习笔记

  1. 一、工具安装
  2. 二、基础入门
    1. 2.1 认识 C++
      1. 2.1.1 注释
      2. 2.1.2 变量与常量
      3. 2.1.3 关键字
      4. 2.1.4 标识符命名规则
      5. 2.1.5 main 函数
    2. 2.2 数据类型
      1. 2.2.1 整型
      2. 2.2.2 浮点型
      3. 2.2.3 字符型
      4. 2.2.4 布尔类型 bool
      5. 2.2.5 转义字符
      6. 2.2.6 字符串
      7. 2.2.7 进制
      8. 2.2.8 auto 关键字
      9. 2.2.9 数据类型转换
    3. 2.3 算术运算
      1. 2.3.1 输入与输出
      2. 2.3.2 算术运算符
      3. 2.3.3 关系运算符
      4. 2.3.4 位运算符
      5. 2.3.5 赋值运算符
      6. 2.3.6 杂项运算符
      7. 2.3.7 类型转换
    4. 2.4 程序流结构
      1. 2.4.1 选择结构
    5. 2.5 循环结构
      1. 2.5.1 while 循环
      2. 2.5.2 do while 循环
      3. 2.5.3 for 循环
      4. 2.5.4 嵌套循环
    6. 2.6 跳转语句
      1. 2.6.1 break 语句
      2. 2.6.2 continue 语句
      3. 2.6.3 goto 语句
    7. 2.7 数组
      1. 2.7.1 创建数组
      2. 2.7.2 一维数组
      3. 2.7.3 冒泡排序
      4. 2.7.4 二维数组
    8. 2.8 函数
      1. 2.8.1 函数概述
      2. 2.8.2 函数的声明
      3. 2.8.3 函数的分文件编写
      4. 2.8.4 静态变量
    9. 2.9 指针
      1. 2.9.1 指针变量的定义和使用
      2. 2.9.2 空指针与野指针
      3. 2.9.3 const 修饰指针
      4. 2.9.4 指针和数组
      5. 2.9.5 指针和函数
      6. 2.9.6 指针,数组和函数
    10. 2.10 结构体
      1. 2.10.1 基本概念
      2. 2.10.2 结构体数组
      3. 2.10.3 结构体指针
      4. 2.10.4 结构体嵌套
      5. 2.10.5 结构体做函数参数
      6. 2.10.6 结构体中 const 使用场景
      7. 2.10.6 案例
    11. 2.11 枚举类型
      1. 2.11.1 定义枚举类型
      2. 2.11.2 定义枚举变量
      3. 2.11.3 关系运算
      4. 2.11.4 枚举类
  3. 三、实战 1- 通讯录管理系统
  4. 四、C++ 核心编程
    1. 4.1 内存分区模型
    2. 4.2 new 操作符
    3. 4.3 引用
      1. 4.3.1 引用作函数实参
      2. 4.3.2 引用作函数的返回值
      3. 4.3.3 引用的本质
      4. 4.3.4 常量引用
    4. 4.4 函数的提高
      1. 4.4.1 函数默认值
      2. 4.4.2 函数占位参数
      3. 4.4.3 函数重载
    5. 4.5 类和对象
      1. 4.5.1 封装
      2. 4.5.2 对象的初始化和清理
        1. 4.5.2.1 构造函数和析构函数
        2. 4.5.2.2 构造函数的分类及调用
        3. 4.5.2.3 拷贝构造函数调用时机
        4. 4.5.2.4 构造函数调用规则
        5. 4.5.2.5 深浅拷贝
        6. 4.5.2.6 初始化列表
        7. 4.5.2.7 类对象作为类成员
        8. 4.5.2.8 静态成员
        9. 4.5.2.9 const 成员
      3. 4.5.3 C++ 对象模型和 this 指针
        1. 4.5.3.1 成员变量和成员函数分开储存
        2. 4.5.3.2 this 指针
        3. 4.5.3.3 空指针访问成员函数
        4. 4.5.3.4 const 修饰成员函数
      4. 4.5.4 友元
      5. 4.5.5 运算符重载
        1. 4.5.5.1 加号运算符重载
        2. 4.5.5.2 左移运算符重载
        3. 4.5.5.3 递增运算符重载
        4. 4.5.5.4 赋值运算符重载
        5. 4.5.5.5 关系运算符重载
        6. 4.5.5.6 函数调用运算符重载
      6. 4.5.6 继承
        1. 4.5.6.1 继承基本语法
        2. 4.5.6.2 继承方式
        3. 4.5.6.3 继承中的对象模型
        4. 4.5.6.4 继承中构造和析构顺序
        5. 4.5.6.5 继承中同名成员处理方式
        6. 4.5.6.6 继承同名静态成员处理方式
        7. 4.5.6.7 多继承语法
        8. 4.5.6.8 菱形继承
        9. 4.5.6.9 链式继承
      7. 4.5.7 多态
        1. 4.5.7.1 多态的基本概念
        2. 4.5.7.2 多态案例
        3. 4.5.7.3 纯虚函数和抽象类
        4. 4.5.7.4 多态案例
        5. 4.5.7.5 虚析构和纯虚析构
    6. 4.6 结构体与类
    7. 4.7 文件操作
      1. 4.7.1 文本文件
        1. 4.7.1.1 写文件
        2. 4.7.1.2 读文件
      2. 4.7.2 二进制文件
        1. 4.7.2.1 写文件
        2. 4.7.2.2 读文件
  5. 五、实战 2- 职工管理系统

一、工具安装

Microsoft Visual Studio(简称 VS)是美国 微软公司 开发工具 包系列产品。VS 是一个基本 完整 的开发工具集,它包括了整个 软件生命周期 中所需要的大部分工具,如 UML 工具、代码 管控 工具、集成开发环境 (IDE) 等等。

社区免费版:https://visualstudio.microsoft.com/zh-hans/downloads/

【编辑器】:纯用来写代码的,如 vim

【编译器】:将源程序转化为二进制形式的目标程序的工具,如 GCC

【IDE】:集成了编辑器、编译器以及链接器等众多功能的一个集成开发环境。

二、基础入门

2.1 认识 C++

2.1.1 注释

单行注释:

// 注释信息

多行注释:

/*
多行注释 1
多行注释 2
*/

2.1.2 变量与常量

变量:给一段内存起名,方便我们管理内存。

// 变量创建   数据类型 变量名 = 变量初始值;
int a = 10;

常量:用于记录程序中不可更改的数据。

C++ 中定义常量的两种方法:

  • 宏常量#define 常量名 常量值 。通常在文件开头定义。
  • const 修饰的常量const 数据类型 常量名 = 常量值
#include<iostream>
using namespace std;

#define Day 7  // 宏常量

int main(){
    const int a = 10;   //const 修饰的常量

    cout<<Day<<endl;
    cout<<a<<endl;

    system("pause")
    return 0;
}

2.1.3 关键字

作用:关键字是 C++ 中保留的单词(标识符),在 C++ 中拥有固定的,预设的含义。因此在定义变量 / 常量的时候,禁止使用关键字。

关键字 关键字 关键字 关键字 关键字
asm do if return typedef
auto double inline short typeid
bool dynamic int signed typename
break else long sizeof union
case enum mutable static unsigned
catch explict namespace static_cast using
char export new struct virtual
class extern operator switch void
const false private template volatile
const_cast float protected this wchar_t
continue for public throw while
default friend register true
delete goto reinterpret_cast try

2.1.4 标识符命名规则

作用:C++ 规定给标识符(变量,常量)命名时,有一套自己的规则

  • 标识符不能是关键字
  • 标识符只能由字母,数字,下划线组成
  • 第一个字符必须为字母或者下划线组成
  • 标识符中字母区分大小写

给标识符命名时,尽量做到见名知意,方便阅读。

2.1.5 main 函数

在 C++ 中,main 函数为必须的,C++ 程序自动从 main 函数开始执行。

2.2 数据类型

C++ 在创建一个变量或者常量时,必须要指定出相应的数据类型,否则无法给变量分配内存。

可以通过 typeid(变量名).name()查看数据类型。

C++ 为程序员提供了种类丰富的内置数据类型和用户自定义的数据类型。下表列出了七种基本的 内置C++ 数据类型:

类型 关键字 可加修饰符
整型 int signed、unsigned、short、long
单浮点型 float /
双浮点型 double long
字符型 char signed、unsigned
布尔型 bool /
无类型 void /
宽字符型 wchar_t /

其实 wchar_t 是这样来的:

typedef short int wchar_t;

C++ 允许使用速记符号来声明 无符号短整数 无符号长整数。可以不写 int,只写单词 unsigned、shortlongint 是隐含的。

  • cout 输出时,默认只输出六位有效数字。可使用 cout.precision(10); 更改为 10 位有效数字。

2.2.1 整型

作用:用于表示整数类型的数据。

整型类型:

数据类型 占用空间 取值范围
short(短整型) 2 byte(字节) -2^15 – 2^15-1 (-32786–32767)
int(整型) 4 byte -2^31 – 2^31-1
long(长整型) 4 byte (window/linux x32); 8 byte(linux x64) -2^31 – 2^31-1
long long (长长整型) 8 byte -2^63 – 2^63-1
signed int(有符号整型) 4 byte -2^31 – 2^31-1
unsigned int(无符号整型) 4 byte 0 – 2^32-1
unsigned short(无符号短整型) 2 byte 0~65535

数据类型存在的意义:给变量分配合适的内存空间,避免造成资源浪费。1byte 字节 =8bit 比特,二进制数系统中,每个 0 或 1 就是一个位(bit)

MB 与 byte 换算:

1MB = 1048576byte = 262144 int 类型数据

// 一个内存溢出造成数据错误的例子:

#include<iostream>
using namespace std;

#define Day 7

int main() {
     short a = 33000;

    cout << a << endl;

    system("pause");
    return 0;
}

// 输出 a 的结果:-32536

利用 sizeof 关键字可以统计数据类型所占的内存空间大小

语法:sizeof(数据类型) 或者 sizeof(变量名字)

注意:将一个负数强制转成无符号数,并不是取绝对值的关系:

int main()
{
    int a = -1;
    cout << (unsigned)a << endl; // 输出 4294967295,即 2^32-1

    int b = -10;
    cout << (unsigned)b << endl;  // 输出 4294967286,即 2^32-10

}

int 类型数据取值范围为什么是 -2^31 – 2^31-1?

举个例子:

2^(4*8-1)=2^31

因为 0 的源码是:

00000000 00000000 00000000 00000000 占了一个位置,所以正整数范围是0-2^31-1 一共是 2^31 个数。

题外话:

在计算机中,负数的二进制是用其源码的补码储存的。正数是用其源码直接存储。

补码:反码加 1 称为补码。

反码:将二进制源码按位取反。

111001   源码
000110   反码
000111   补码 

2.2.2 浮点型

作用:用于表示小数。

浮点型分为两种:

  1. 单精度 float
  2. 双精度 double
数据类型 占用空间 有效数字范围
float 4 byte 7 位有效数字
double 8 byte 15-16 位有效数字
long double 16 byte 18-19 位有效数字
//...
int main() {
    float a = 3.14f;
    double b = 3.141592654;

    cout << a << endl;
    cout << b << endl;

    // 科学计数法
    float c = 2e5; // 2*10^5

    system("pause");
    return 0;
}
/* 输出
 3.14 
 3.14159
 */

默认情况下,输出一个小数,最多只能输出 6 位有效数字。

2.2.3 字符型

作用:字符型变量用于显示单个字符

语法:char ch = 'a'

在显示字符型变量时,用单引号括起来,不要用双引号。

单引号内只能有一个字符,不可以是字符串。

C 和 C++ 中,字符型变量只占用 1 个字节

数据类型 占用空间 取值范围
char 1byte -2^7– 2^7-1
unsigned char 1byte 0–2^8-1

字符型变量并不是把字符本身放到内存中存储,而是将其对应的 ASCII 编码放到存储单元。

//...
int main() {
    char ch = 'a';
    char ch2 = 'A';
    cout << (int)ch << endl;    // 输出 97
    cout << (int)ch2 << endl;   // 输出 65
    system("pause");
    return 0;
}

// 输出 97  65

1MB = 1048576byte = 1048576 char 类型数据

2.2.4 布尔类型 bool

作用:布尔类型代表真或假

布尔类型只有两个值:

  • true — 真(本质是 1)
  • false — 假 (本质是 0)

布尔类似只占用 1 个字节。

2.2.5 转义字符

作用:用于表示一些不能显示出来的 ASCII 字符,如一些表示格式的字符。反斜杠 \

现阶段我们常用的转义字符\n \\ \t

2.2.6 字符串

作用:用于表示一串字符

两种风格:

  1. C 风格字符串:char 变量名[] = "abcd"

    char ch[] = { 'a','b','c','\0'};   // 必须加结尾符 \0
    cout << ch << endl;                // 输出 abc,为字符串数组,
    
    char ch2[] = "abc";
    cout << ch2 << endl;       // 输出 abc
  2. C++ 风格字符串:string 变量名 = “abcd”

如果要使用 C++ 风格字符串,需要加头文件: #include<string>

在 C++ 中,字符串类型本质是一个容器类。

字符串以 \0 字符结尾。

int main()
{

    char c[] = { 'a','b','c','d','\0'};
    cout << strlen(c) << endl;  // 输出 4
    cout << sizeof(c) << endl;  // 输出 5

    string a = "abcd";
    cout << sizeof(a) << endl;  // 输出 28,字符串 a 本质是一个类
    cout << a.length() << endl;  // 输出 4

}

为了节省内存和方便,定义字符串的时候,可以采用这种方法:

char ch2[] = "abcd";
cout << sizeof(ch2) << endl;  // 输出 5

相比 string a = "abcd"; 节省了空间,不过 string 提供很多方法可供使用,后面容器部分会有详细介绍。这里先介绍 C 风格字符串的方法:

C 风格字符串的方法:

函数 目的
strcpy(s1, s2); 复制字符串 s2 到字符串 s1
strcat(s1, s2); 连接字符串 s2 到字符串 s1 的末尾。连接字符串也可以用 +
strlen(s1); 返回字符串 s1 的长度。
strcmp(s1, s2); 如果 s1 和 s2 是相同的,则返回 0;如果 s1<s2 则返回值小于 0;如果 s1>s2 则返回值大于 0。
strchr(s1, ch); 返回一个指针,指向字符串 s1 中字符 ch 的第一次出现的位置。
strstr(s1, s2); 返回一个指针,指向字符串 s1 中字符串 s2 的第一次出现的位置。

2.2.7 进制

进制 标识符 示例
二进制(0 和 1 组成) 0b 或者 0B 0b101=5(十进制)
八进制(0-7 组成) 0 0555=365(十进制)
十六进制(0-9,A-F 组成) 0x 或者 0X 0x10F=271(十进制)

转换成十进制的换算方法:

0555 = 5*8^2 + 5*8^1 + 5*8^0 = 365

0x10F = 1*16^2 + 0*16^1 + 15*16^0 = 271 注意:F 代表 15, A 代表 10

总结 :第 i 位数乘以 进制的 (i-1) 次方 + 第i-1 位数乘以 进制的 (i-1-1) 次方 + … + 第 1 位数乘以进制的(1-1) 次方。


利用计算机程序进行进制转换:

#include<iostream>
#include<bitset>

int main()
{
    int a = 60;

    cout << (bitset<10>)a << endl;  // 二进制需要用到 bitset 库, 这里的 10 是输出二进制的位数,输出 10 位二进制数

    cout << oct << a << endl;       // 八进制输出

    cout << hex << a << endl;       // 十六进制输出

}
/*
0000111100
74
3c
*/

2.2.8 auto 关键字

C++11 引入了 auto 和 decltype 关键字实现类型推导,通过这两个关键字不仅能方便地获取复杂的类型,而且还能简化书写,提高编码效率。

auto x = 5;                      // OK: x 是 int 类型
auto pi = new auto(1);           // OK: pi 被推导为 int*
const auto *v = &x, u = 6;       // OK: v 是 const int* 类型,u 是 const int 类型
static auto y = 0.0;             // OK: y 是 double 类型
auto int r;                      // error: auto 不再表示存储类型指示符
auto s;                          // error: auto 无法推导出 s 的类型

auto 并不能代表一个实际的类型声明,只是一个类型声明的“占位符”。使用 auto 声明的变量必须马上初始化,以让编译器推断出它的实际类型,并在编译时将 auto 占位符替换为真正的类型。

但是 auto 使用是有限制的:

  • auto 变量必须在定义时初始化,这类似于 const 关键字。

  • 定义在一个 auto 序列的变量必须始终推导成同一类型。

    auto a4 = 10, a5 = 20, a6 = 30;   //a4 a5 a6 必须为同一类型
  • 如果初始化表达式是引用,则去除引用语义。

    int a = 10;
    int &b = a;
    
    auto c = b;//c 的类型为 int 而非 int&(去除引用)
    auto &d = b;// 此时 c 的类型才为 int&
  • 如果初始化表达式为 const 或 volatile(或者两者兼有),则除去 const/volatile 语义。

    const int a1 = 10;
    auto  b1= a1; //b1 的类型为 int 而非 const int(去除 const)
    const auto c1 = a1;// 此时 c1 的类型为 const int
    b1 = 100;// 合法
    c1 = 100;// 非法
  • 如果 auto 关键字带上 & 号,则不去除 const 语意。

    const int a2 = 10;
    auto &b2 = a2;// 因为 auto 带上 &,故不去除 const,b2 类型为 const int
    b2 = 10; // 非法
  • 初始化表达式为数组时,auto 关键字推导类型为指针。

    int a3[3] = { 1, 2, 3 };
    auto b3 = a3;
  • 若表达式为数组且 auto 带上 &,则推导类型为数组类型。

    int a7[3] = { 1, 2, 3 };
    auto & b7 = a7;
  • 函数或者模板参数不能被声明为 auto

    void func(auto a){}  // 错误
  • 时刻要注意 auto 并不是一个真正的类型。
    auto 仅仅是一个占位符,它并不是一个真正的类型,不能使用一些以类型为操作数的操作符,如 sizeof 或者 typeid。

2.2.9 数据类型转换

字符串转整型:比如将“123” 转成 123 –> std::stoi(str) = intStr

字符串转单精度:比如将“123.22” 转成 123.22 –> std::stof(str) = floatStr

字符串转双精度:比如将“123.22121” 转成 123.22121 –> std::stod(str) = doubleStr

数值转字符串:比如将 123.11 转成字符串“123.11” –> std::to_string(int) = str

2.3 算术运算

2.3.1 输入与输出

关键字:cin输入 cout输出

//...

int main() {
    int a, b;
    cin >> a >> b;

    cout << b << endl;
    cout << a << endl;

    system("pause");
    return 0;
}

计算机怎么知道你终端输入是否完成呢?

cin 以空格为结束符

cin.getline() 以换行符(回车键)为结束,但它不保存换行符,在存储字符串时,它用空字符代替换行符(存储换行符为空字符)

cin.get() 以换行符(回车键)为结束,它读取到换行符的前一个字符,此时换行符仍在输入队列中。

cin.get(name, mSize)  // 读取输入保存 mSize 个字符到 name 中
cin.get(diss, mSize)  // 第二次调用 cin.get 时首先就看到换行符,故它什么都读取不到就结束了

cin.get()有另一种变体来处理换行符:

cin.get(name, mSize)  
cin.get()     //  读取下一个字符,包括换行符,可以有返回值
cin.get(diss, mSize) 
  • cout 默认输出六位有效数字,可使用 cout.precision(10); 更改为 10 位。

2.3.2 算术运算符

运算符 描述 实例
+ 把两个操作数相加 10 + 20 将得到 30
- 从第一个操作数中减去第二个操作数 10 - 20 将得到 -10
* 把两个操作数相乘 10 * 20 将得到 200
/ 除数 10 / 20.0 将得到 0.5
% 取模运算符,整除后的余数 10 % 20 将得到 10
++ 自增运算符,整数值增加 1 10++ 将得到 11
自减运算符,整数值减少 1 10– 将得到 9

2.3.3 关系运算符

运算符 描述
&& 称为逻辑与运算符。如果两个操作数都 true,则条件为 true。
|| 称为逻辑或运算符。如果两个操作数中有任意一个 true,则条件为 true。
! 称为逻辑非运算符。用来逆转操作数的逻辑状态,如果条件为 true 则逻辑非运算符将使其为 false。

2.3.4 位运算符

位运算符作用于位,并逐位执行操作。对两个数的二进制数的每一位进行位运算。

运算符 描述 实例
& 按位与操作,按二进制位进行”与”运算。运算规则:
0&0=0; 0&1=0; 1&0=0; 1&1=1;
二进制(100& 001) 将得到 000
| 按位或运算符,按二进制位进行”或”运算。运算规则:
`0
0=0; 0
^ 异或运算符,按二进制位进行”异或”运算,相同得 0。运算规则:
0^0=0; 0^1=1; 1^0=1; 1^1=0;
二进制(100 ^ 001) 将得到 101
~ 取反运算符,按二进制位进行”取反”运算。运算规则:
~1=0; ~0=1;
二进制(~000100) 将得到 111011
<< 二进制左移运算符。将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补 0)。 二进制 000101<< 2 将得到 010100
>> 二进制右移运算符。将一个数的各二进制位全部右移若干位,正数左补 0,负数左补 1,右边丢弃。 二进制 000101 >> 2 将得到 000001

位运算的作用:

  • & 按位与

    • 快速清零

      int a=0x0011;
      a&=0;        //a = 0x0011 & 0x0000 = 0x0000
    • 判断奇偶

      奇数的二进制末尾一定是 1;偶数的二进制末尾一定是 0。

      int a = 2;
      int b = 3;
      
      a = a&1;   //a 结果为 0,偶数为 0
      b = b&1;   //b 结果为 1,奇数为 1
  • | 按位或

    • 设定指定位的数据

      int a=0x0001;
      int b=0x0010;
      
      int c=a|b;  //c: 0x0011
  • ^ 按位异或

    • 交换数值

      int main()
      {
          int a = 32;
          int b = 34;
      
          a = a ^ b;
          b = b ^ a;
          a = a ^ b;
      
          cout << a << endl;  // a:34
          cout << b << endl;  // b:32
      
      }
  • << 和 >> 左移和右移

    • m << n 等于m*2^n

      int main()
      {
          int m = 32;
          int n = 3;
      
          if (m << n == m * pow(2, n)) {
              cout << "True" << endl;
          }
      
      }
      // 输出 True
    • m >> n 等于floor(m/(2^n))

      当 m 为负数,情况就不一样了

      int main()
      {
          int m = -31;
          int n = 3;
      
          int c = m >> n;
          cout << c << endl;    // 输出 -4
      
          double d = m / pow(2, n);   
          cout << d << endl;    // 输出 -3.875  floor(-3.875) = -4
      }

2.3.5 赋值运算符

运算符 描述
= 简单的赋值运算符,把右边操作数的值赋给左边操作数
+= 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数
-= 减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数
*= 乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数
/= 除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数
%= 求模且赋值运算符,求两个操作数的模赋值给左边操作数
<<= 左移且赋值运算符
>>= 右移且赋值运算符
&= 按位与且赋值运算符
^= 按位异或且赋值运算符
|= 按位或且赋值运算符

2.3.6 杂项运算符

运算符 描述
sizeof sizeof 运算符 返回变量所占空间的大小。例如,sizeof(a) 将返回 4,其中 a 是整数 int。
Condition ? X : Y 条件运算符。如果 Condition 为真 ? 则值为 X : 否则值为 Y。
, 逗号运算符 会顺序执行一系列运算。整个逗号表达式的值是以逗号分隔的列表中的最后一个表达式的值。
.(点)和 ->(箭头) 成员运算符 用于引用类、结构和共用体的成员。
Cast 强制转换运算符 把一种数据类型转换为另一种数据类型。例如,int(2.2000) 将返回 2。
& 指针运算符 & 返回变量的地址。例如 &a; 将给出变量的实际地址。
* 指针运算符 * 指向一个变量。例如,*var; 将指向变量 var。

2.3.7 类型转换

在 C++ 中,表达式中包含不同的数据类型,C++ 将对其值进行转换。

int main()
{
    int a = 10;
    double b = 10.0;

    if (a == b) {
        cout << "true" << endl;
    }
    else {
        cout << "false" << endl;
    }
}

上述结果会输出:true

这里的等于判断,只会判断数值是否相等。不判断数据类型。

2.4 程序流结构

C/C++ 支持最基本的三种程序运行结构,顺序结构;选择结构;循环结构。

  • 顺序结构:程序按照顺序执行,不发生跳转
  • 选择结构:依据条件是否满足,有选择的执行相应功能
  • 循环结构:依据条件是否满足,循环多次执行某段代码

2.4.1 选择结构

  • if 语句

    • 单行格式 if 语句

      if (条件){
         // 满足条件代码块
      };
    • 多行格式 if 语句

      if (条件){
         // 满足条件代码块
      }
      else{
         // 不满足条件代码块
      }
    • 多条件的 if 语句

      if (条件){
         // 满足条件代码块
      }
      else if (条件){
         // 不满足条件代码块
      }
      else {
         // 不满足条件代码块
      }
  • 三目运算符

    语法:表达式 1?表达式 2:表达式 3 。解释:如果表达式 1 为真,则执行表达式 2,否则执行表达式 3

  • switch 语句

    switch(表达式){
      case 结果 1:执行语句 1;break;
      case 结果 2:执行语句 2;break;
      case 结果 3:执行语句 3;break;
      default: 执行语句 3;break;
    }

2.5 循环结构

作用:满足条件,反复执行某段代码。

2.5.1 while 循环

while(条件){
   // 满足条件执行
}

一个猜数字的例子:

int main() {
    srand((unsigned int)time(NULL));  // 随机种子,避免每次都是一样的随机数

    int a = rand() % 100; // 随机生成 0-99 的随机数;
    int var;

    while (1) {
        cin >> var;
        if (var > a) {
            cout << " 猜测过大 " << endl;
        }
        else if (var < a) {
            cout << " 猜测过小 " << endl;
        }
        else {
            cout << " 恭喜猜对了 " << endl;
            break;  // 可以利用 break 关键字退出循环
        }
    }

    system("pause");
}

2.5.2 do while 循环

do {
   // 执行一次
}
while (条件);

案例—水仙花数:

水仙花数是指一个 3 位数,它的每个位的数字的 3 次之幂之和等于它本身

例如:1^3 + 5^3 + 3^3 = 153

利用 do while 循环,求出所有 3 位数中的水仙花数。

int main() {
    int A = 100;
    int a, b, c;

    do{
        a = A / 100;
        b = (A - a * 100) / 10;
        c = (A - a * 100 - b * 10);

        if (a*a*a + b*b*b + c*c*c == A) {
            cout << A << endl;
        }

        A++;
    } while (A < 1000);

    system("pause");
}
// 输出 153 370 371 407

注意:C++ 中不能用 ^ 表示幂指数,^ 是位异或的意思。

2.5.3 for 循环

作用:满足循环条件,执行循环语句。

语法:for(起始表达式;条件表达式;末尾循环体){ 循环语句 }

for(int i=0; i<10; i++){
   cout << i <<endl;
}

在 C++ 中,for 循环有了一个新用法for(接受数据的变量名:容器){}

int arr[] = {0,1,2,3,4,5};

for (int n : arr) {
    cout << n << " ";         // 输出 1 2 3 4 5 6
}
cout << endl;

2.5.4 嵌套循环

int main() {

    for (int i = 0; i < 10; i++) {
        for (int j = 0; j < 10; j++) {
            cout << "* ";
        }
        cout << endl;
    }

    system("pause");
}

2.6 跳转语句

2.6.1 break 语句

作用:用于跳出选择结构或者循环结构。一碰到 break 关键词就 跳出当前循环

break 使用时机:

  • 出现 switch 条件语句中,作用是终止 case 并跳出 switch
  • 出现在循环语句中,作用是跳出当前的循环语句
  • 出现在嵌套循环中,跳出最近的内层循环语句

2.6.2 continue 语句

作用:在循环语句中,跳出本次循环中余下未执行的语句,继续执行下一次循环

int main() {

    for (int i = 0; i < 10; i++) {
        if (i % 2 == 0) {

            continue;  // 可以筛选条件,执行到此后面的 cout i 就不再执行

        }
        cout << i << endl;
    }

    system("pause");
}
// 输出 1 3 5 7 9

2.6.3 goto 语句

作用:可以无条件跳转语句。如果标记名词存在,执行到 goto 语句时,会跳转到标记位置。

语法:goto 标记

int main() {

    cout << "1. XXXXX" << endl;
    cout << "2. XXXXX" << endl;

    goto FLAG;

    cout << "3. XXXXX" << endl;
    cout << "4. XXXXX" << endl;

    FLAG:
    cout << "5. XXXXX" << endl;

    system("pause");
}
/* 输出
1. XXXXX
2. XXXXX
5. XXXXX
*/

2.7 数组

所谓数组,就是几个集合,里面存放了相同类型的数据类型。

  • 数组中每一个数据元素都是相同的数据类型
  • 数组是由连续的内存位置组成

2.7.1 创建数组

// 第一种定义方式
int arr[3];  // 声明数组
arr[0] = 1;  // 赋值
arr[1] = 2;
arr[2] = 3;

// 第二种定义方式
int arr2[5] = { 1,2,3 };
for (int i = 0; i < 5; i++) {
     cout << arr2[i] << endl;
}
/* 输出 1 2 3 0 0 */ 

// 第三种定义方式
int arr3[] = { 1,2,3,4 };

2.7.2 一维数组

一维数组名称的用途:

  • 获取数组的长度

  • 获取数组在内存中的首地址

int arr[] = { 1,2,3,4 };

cout << sizeof(arr)/sizeof(arr[0]) << endl;   // 输出数组长度
cout << arr << endl;  // 输出数组的首地址
cout << &arr[0] << endl;  // 输出数组第一个元素的地址

& 是取址符

练习案例 - 小猪称体重: 在一个数组中记录了五只小猪的体重,找出并打印最重的小猪的体重。

int main() {

    int pigs[] = {250,380,129,566,412};

    int max = pigs[0];  // 假设第一个值最大
    for (int i = 1; i < (sizeof(pigs) / sizeof(pigs[0])); i++) {
        if (pigs[i]>max) {
            max = pigs[i];        // 逐一比较,更新最大值
        }
    }
    cout <<" 最大的数值为:" << max << endl;


    system("pause");
}

练习案例 - 元素逆置:将一个数组里的元素逆置,并输出逆置后的结果。

int main() {

    int a[] = {1,2,3,4,5,6};
    int start = 0;
    int end = sizeof(a) / sizeof(a[0]) -1;
    int temp;

    while(start < end){
            temp = a[start];
            a[start] = a[end];
            a[end] = temp;
            start++;
            end--;
        }
    for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++) {
        cout << a[i]<<endl;

    }

    system("pause");
}

2.7.3 冒泡排序

冒泡排序算法是最常用的算法,对数组内元素进行排序

  1. 比较相邻元素,如果第一个比第二个大,就交换它们两个
  2. 对每一对相邻的元素做同样的工作,执行完毕后,找到第一个最大值。
  3. 重复以上的步骤,每次比较次数减一,直到不需要比较。
int main() {

    int a[] = {4,2,8,0,5,7,1,3,9};
    int len = sizeof(a) / sizeof(a[0]);
    int temp;
    // 排序轮数
    for (int i = 0; i < len-1; i++) {
        // 每轮对比次数
        for (int j = 0; j < len-i-1; j++) {
            if (a[j] > a[j+1]) {
                temp = a[j+1];
                a[j+1] = a[j];
                a[j] = temp;
            }
        }
    }
    for (int i = 0; i < len; i++) {
        cout << a[i] << endl;
    }

    system("pause");
}

2.7.4 二维数组

二维数组定义的四种方式:

  • 数据类型 数组名[行数][列数];
  • 数据类型 数组名[行数][列数] = {{ 数据 1, 数据 2},{ 数据 3, 数据 4}};
  • 数据类型 数组名[行数][列数] = { 数据 1, 数据 2, 数据 3, 数据 4};
  • 数据类型 数组名[][列数] = { 数据 1, 数据 2, 数据 3, 数据 4};

建议采用第二种,更加直观。

int arr[2][3] = { {1,2,3}, {4,5,6} };

cout << sizeof(arr) << endl;   // 输出数组所占内存空间大小
cout << sizeof(arr[0]) << endl;   // 输出数组第一行所占内存空间大小

int row = sizeof(arr)/sizeof(arr[0]);             // 行数
int col = sizeof(arr[0])/sizeof(arr[0][0]);       // 列数

cout << arr << endl;  // 输出数组的首地址
cout << &arr[0][0] << endl;  // 输出数组第一个元素的地址
cout << arr[0]<< endl;  // 输出数组第一行的首地址
cout << arr[1]<< endl;  // 输出数组第二行的首地址

2.8 函数

2.8.1 函数概述

作用:将一段经常使用的代码封装起来,减少代码重复

函数的定义一般主要由 5 个步骤:

  1. 返回值类型
  2. 函数名
  3. 参数列表
  4. 函数体语句
  5. return 表达式
/*
返回值类型 函数名(参数列表){
    // 函数体语句;
    return 表达式
}
*/

void swap(int a, int b) {      // 这里 a,b 均为形参
    int temp;
    temp = b;
    b = a;
    a = temp;
}

int main() {

    int a = 10;
    int b = 20;               
    swap(a, b);   // 这里 a,b 均为实参
    cout << a << endl;
    cout << b << endl;

    return 0;  // 可省略

    system("pause");
}
// 输出 a,b 为 10,20

如果函数不需要返回值,声明函数的时候可以写void

当我们在做值传递的时候,函数的形参发生改变,不会影响实参。

2.8.2 函数的声明

由于 C++ 编译器的特点,main 函数一定要位于其调用其他函数的后面。在实际使用过程中,会常将定义的函数在开头做一个声明,从而不用将函数体定义在 main 函数的前面。

int get_max(int a, int b);  // 函数的声明,提前告诉编译器有这个函数

int main() {

    int a = 10;
    int b = 25;
    cout << get_max(a, b) << endl;

    return 0;  // 可省略

    system("pause");
}

// 函数的定义
int get_max(int a, int b) {     
    return a > b ? a : b;
}

函数可以声明写多次,定义只能写一次。

2.8.3 函数的分文件编写

作用:让代码结构更加清晰。

函数分文件编写一般有四个步骤:

  1. 创建后缀名为.h 的头文件
  2. 创建后缀名为.cpp 的源文件
  3. 在头文件中写函数的声明
  4. 在源文件中写函数的定义,并链接头文件 #include " 头文件名 "

2.8.4 静态变量

静态变量:加关键词 static 的变量。三个特点:

  • 作用范围:只本文件中可访问,对其他文件是隐藏的
  • 创建与释放:程序开始时分配空间,结束时释放空间,数据存放于内存的全局区中
  • 初始化默认为 0,使用时也可对其重新赋值

2.9 指针

指针的作用:可以通过指针间接访问内存。

  • 内存编号是从 0 开始记录的,一般用十六进制数字表示
  • 可以利用指针变量保存地址

2.9.1 指针变量的定义和使用

指针变量定义的语法:数据类型 * 变量名

int main() {

    int a = 10;

    int* p;   // 定义一个指针 p

    p = &a;   // 将变量 a 的地址赋值给 p,& 是取址符

    int b = *p;  // 指针前加 * 代表解引用,找到指针指向内存的数据

    return 0;  // 可省略

    system("pause");
}

指针所占的内存空间:32 位系统—4byte,64 位系统—8byte,无论什么数据类型的指针。

2.9.2 空指针与野指针

空指针:指针变量指向内存中编号为 0 的空间

  • 用途:初始化指针变量

  • 注意:空指针指向的内存是不可以访问的

野指针:指针变量指向非法的内存空间

int* p = NULL; // 初始化指针为空

int* p1 = (int*)0x11001  // 指针指向了无访问权限的地址,野指针

总结:空指针和野指针都不是我们申请的空间,因此不要访问。

2.9.3 const 修饰指针

const 修饰指针有三种情况:

  1. const 修饰指针 — 常量指针:指针的指向可以改,但是指针指向的值不可以改

    int a=10;
    int b=20;
    
    const int* p = &a;   // 常量指针
    
    *p = 20;  // 错误操作,指针指向的值不可更改
    p = &b;   // 允许操作,指针指向可以改

    可以记忆为“常量的指针”,常量即值不可以修改。

  2. const 修饰常量 — 指针常量:指针的指向不可以改,但是指针指向的值可以改

    int a=10;
    int b=20;
    
    int* const p = &a;   // 指针常量
    
    *p = 20;  // 允许操作,指针指向的值可改
    p = &b;   // 错误操作,指针指向不可以改

    可以记忆为“指针是常量”,指针为常量即指针指向不可以改。

  3. const 既修饰指针,又修饰常量:指针的指向不可以改,指针指向的值也不可改

    int a=10;
    int b=20;
    
    const int* const p = &a;  //
    
    *p = 20;  // 错误操作,指针指向的值不可改
    p = &b;   // 错误操作,指针指向不可改

2.9.4 指针和数组

作用:利用指针访问数组中的元素

int main() {

    int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
    int* p = arr;            // 赋值数组首地址给指针 p
    cout << *p << endl;      // 获取首地址(第一个元素)的值
    p++;                     // 指针右偏移四个字节
    cout << *p << endl;      // 获取第二个元素的值

    system("pause");
}

2.9.5 指针和函数

作用:利用指针作函数参数,可以修改实参的值

void swap(int* p1, int* p2) {
    int temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}

int main() {
    int a = 1;
    int b = 2;
    swap(&a, &b);  // 地址传递
    cout << a << endl;  // 输出 a 为 2
    cout << b << endl;  // 输出 b 为 1

    system("pause");
}

地址传递可以改变实参的值。

如果想改变实参的值,就用地址传递;如果不想改变实参,就用值传递。

2.9.6 指针,数组和函数

封装一个函数,利用冒泡排序,实现对整型数组的升序排列。

void bubbleSort(int* arr, int len) {

    for (int i = 0; i < len - 1; i++) {
        for (int j = 0; j < len - i - 1; j++) {
            if (arr[j + 1] < arr[j]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }           
        }
    }
}

int main() {
    int arr[] = { 4,3,6,9,1,2,10,8,7,5 };
    int len = sizeof(arr) / sizeof(arr[0]);
    bubbleSort(arr, len);
    for (int i = 0; i < len; i++) {
        cout << arr[i] << " ";
    }

    system("pause");
}

2.10 结构体

2.10.1 基本概念

结构体属于用户自定义的数据类型,允许用户储存不同的数据类型。

语法:struct 结构体名 { 结构体成员列表 };

通过结构体创建变量的方式有三种:

  • struct 结构体名,变量名
  • struct 结构体名 变量名 = {成员 1 值,成员 2 值…}
  • 定义结构体时顺便创建变量
struct student {
    string name;
    int age;
    int scores;
}s3;                   // 第三种方式:定义结构体时顺便创建变量

int main() {

    // 第一种方式
    struct student s1;      //struct 关键词可以省略
    s1.name = "zs";
    s1.age = 25;
    s1.scores = 80;

    // 第二种方式
    struct student s2 = {"ls", 27, 90};          

    s3.name = "we";
    s3.age = 20;
    s3.scores = 52;

    cout << s1.name << endl;   // 通过. 点来访问成员
    cout << s2.name << endl;
    cout << s3.name << endl;

    system("pause");
    return 0;
}

2.10.2 结构体数组

作用:将自定义的结构体放入到数组中方便维护

语法:struct 结构体名 数组名[] = {{},{},{}}

struct student {
    string name;
    int age;
    int scores;
};

int main() {

    struct student s1[] = {
        {"zs", 19, 68},
        {"li", 25, 70},
        {"dw", 26, 85},
    };

    system("pause");
    return 0;
}

2.10.3 结构体指针

作用:通过指针访问结构体中的成员

  • 利用操作符 -> 可以通过结构体指针访问结构体属性。
struct student {
    string name;
    int age;
    int scores;
};

int main() {

    struct student s1 = { "zs", 19, 68 };

    student * p = &s1;  // 创建结构体指针并赋值

    cout << p->name << endl; // 访问属性

    system("pause");
    return 0;
}

2.10.4 结构体嵌套

在结构体中可以定义另一个结构体作为成员,用来解决实际问题。

struct student {
    string name;
    int age;
    int scores;
};

struct teacher{
    string name;
    int age;
    string course;
    struct student stu;  // 嵌套一个 student 结构体
};

int main() {

    teacher t1;
    t1.name = "wteacher";
    t1.age = 45;
    t1.course = "mathematics";
    t1.stu.name = "zs";
    t1.stu.age = 25;
    t1.stu.scores = 80;

    system("pause");
    return 0;
}

2.10.5 结构体做函数参数

作用:将结构体作为参数向函数中传递

传递方式有两种:

  • 值传递
  • 地址传递
struct student {
    string name;
    int age;
    int scores;
};

// 值传递
void printstudent1(struct student s) {
    s.age = 80;
    cout << s.name <<" ";
    cout << s.age << " ";
    cout << s.scores <<endl;

};

// 地址传递
void printstudent2(struct student* s) {
    s->age = 100;
    cout << s->name << " ";
    cout << s->age << " ";
    cout << s->scores << endl;

};

int main() {

    student s1 = { "zs", 19, 68 };

    printstudent1(s1);          // 输出 "zs", 80, 68 
    cout << s1.name << " ";
    cout << s1.age << " ";
    cout << s1.scores << endl;   // 输出 "zs", 19, 68   不改变原来的值

    printstudent2(&s1);          // 输出 "zs", 100, 68 
    cout << s1.name << " ";
    cout << s1.age << " ";
    cout << s1.scores << endl;   // 输出 "zs", 100, 68 改变原来的值

    system("pause");
    return 0;
}

2.10.6 结构体中 const 使用场景

在实际使用场景中,若使用值传递,相当于又要拷贝一份数据给函数,会显著增加内存资源损耗。使用地址传递就不会存在这个问题,一个指针只占四个字节,会极大的节省空间,但是地址传递在新的函数中处理数据时,会改变原来数据,这时候就可以使用 const 修饰,避免数据篡改。

void printstudent1(const struct student* s) {   // 传地址时加 const 限制
    s->age = 100;   // 此修改行为不允许
    cout << s->name << " ";
    cout << s->age << " ";
    cout << s->scores << endl;

};

int main() {

    student s1 = { "zs", 19, 68 };

    printstudent1(&s1);

    system("pause");
    return 0;
}

2.10.6 案例

案例描述:设计一个英雄的结构体,包括成员姓名,性别;创建结构体数组,数组中存放 5 名英雄。

通过冒泡排序算法,将数组中的英雄按照年龄进行升序排列,最终打印排序后的结果。

struct Hero {
    string name;
    int age;
    string sex;
};

void bubblesort(struct Hero sanguo[],int len) {
    for (int i = 0; i < len-1; i++) {
        for (int j = 0; j < len-1-i; j++) {
            if (sanguo[j].age > sanguo[j + 1].age) {
                struct Hero temp = sanguo[j];
                sanguo[j] = sanguo[j + 1];
                sanguo[j + 1] = temp;
            }
        }
    }
}

int main() {

    Hero sanguo[] = {
        {" 刘备 ",23," 男 "},
        {" 关羽 ",22," 男 "},
        {" 张飞 ",20," 男 "},
        {" 赵云 ",21," 男 "},
        {" 貂蝉 ",19," 女 "},
    };
    int len = sizeof(sanguo) / sizeof(sanguo[0]);

    bubblesort(sanguo, len);

    for (int i = 0; i < len - 1; i++) {
        cout << sanguo[i].name << " ";
        cout << sanguo[i].age << " ";
        cout << sanguo[i].sex << endl;
    }   
    system("pause");
    return 0;
}
  • 结构体变量作函数参数时,函数内的操作不会改变结构体的值,结构体的各成员作为实参传递给了函数的形参,实际操作的是形参,不会影响实参;
  • 结构体数组作为函数参数时,实际上是将结构体数组的第一个数组成员的地址传递给了形参,用对应的指针或者直接用结构体数组的名称作为实参效果是一样的,操作都直接对结构体数组进行,可以改变其值。

2.11 枚举类型

枚举类型 (enumeration) 是 C++ 中的一种派生 数据类型,它是由用户定义的若干枚举常量的集合。

2.11.1 定义枚举类型

enum 数据类型名 { 枚举常量表 };

# 举例
enum Week {Mon, Tus, Wed, Thus, Fri, Sat, Sun}

语句将创建一个名为 Week 的数据类型—枚举类型(与整型,浮点型等类似)。

枚举常量表——由枚举常量构成,以标识符形式表示的整型量,而不能是整型、字符型等文字常量。

枚举常量代表该枚举类型的变量可能取的值,默认情况下,编译系统为每个枚举常量指定一个整数值,从 0 开始,依次加 1;也可自行指定。

若自行指定,而指定值之后的枚举常量按依次加 1 的原则取值。 各枚举常量的值可以重复,枚举标识符不能重复。

enum letter_set {'a','d','F','s','T'};                // 非法
enum year_set{2000,2001,2002,2003,2004,2005};         // 非法

enum fruit_set {apple, orange, banana=1, peach, grape}   // 合法
# apple=1, orange=2, banana=1, peach=2, grape=3

2.11.2 定义枚举变量

定义枚举数据类型后,可以接着使用枚举类型指定枚举变量。

// 定义枚举类型
enum Week {Mon, Tus, Wed, Thus, Fri, Sat, Sun};

// 定义枚举变量 w1 和 w2, 并赋值 w2
Week w1, w2 = Tus;

也可以:类型与变量同时定义(甚至类型名可省)

enum {Mon, Tus, Wed, Thus, Fri, Sat, Sun} w1, w2;

枚举变量的值只能取:枚举常量表中所列的标识符。虽然枚举常量表中的标识符中的背后代表的是枚举常量,但是枚举变量的值不能取整型常量值,如 1,2 等。

枚举变量占用内存的大小与整型数相同。不管枚举类型有多少枚举量,枚举数都占 4 bytes.

枚举变量只能参与 赋值 关系运算 以及 输出操作,其中参与运算时用其本身的整数值。

允许的赋值操作如下:

enum {Mon, Tus, Wed, Thus, Fri, Sat, Sun} w1, w2;

w1 = Fri;
w2 = w1;
int i = w2;
int j = Sun;

非法操作:

enum {Mon, Tus, Wed, Thus, Fri, Sat, Sun} w1, w2;

w1 = 1;              // 非法
w2 = FFF;            // 非法

2.11.3 关系运算

可以使用整数值而不是符号名称来测试枚举变量。还可以使用关系运算符来比较两个枚举变量

enum { Mon, Tus, Wed, Thus, Fri, Sat, Sun } w1 = Mon, w2 = Fri;

if (w1 == Mon) {
   cout << "w1 的枚举变量名是 Mon" << endl;
}

if (w1 == 0) {
    cout << "w1 的枚举常量是 0" << endl;
}

if (w2 > w1) {
    cout << "w2 的枚举常量大于 w2" << endl;
}

2.11.4 枚举类

C++11 中新增了枚举类,也称作【限定作用域的枚举类】。关键字为:enum class

enum 现在被称为【不限范围】的枚举型别。

enum class 是【限定作用域】枚举型别,他们仅在枚举型别内可见,且只能通过强制转换转换为其他型别。

两种枚举都支持底层型别指定,enum class 默认是 int,enum 没有默认底层型别。enum 可以前置声明,但仅在指定默认底层型别的情况下才能前置声明。

枚举类的基本用法和枚举数一致。

枚举类优势:

  • 降低命名空间污染

    // 枚举数
    enum Week{ Mon, Tus, Wed, Thus, Fri, Sat, Sun } w1 = Mon, w2 = Fri;
    
    int Mon = 100;   // 错误
    
    // 枚举类
    enum class Month { Jan, Feb, Mar, Apr, May } m1 = Month::Jan, m2;
    
    m2 = Month::May;
    
    int Jan = 100;   // 允许
  • 避免发生隐式转换

    // 枚举数
    enum Week{ Mon, Tus, Wed, Thus, Fri, Sat, Sun } w1 = Mon;
    
    if (w1 < 7) {}
    
    // 枚举类
    enum class Month { Jan, Feb, Mar, Apr, May } m1 = Month::Jan;
    
    if (m1 < 7) {}   // 不允许

    限定作用域的枚举型别不允许发生任何隐式转换。如果非要转换,按就只能使用 static_cast 进行强制转换。

  • 可以前置声明

    enum Color;          // 非法
    
    enum class Color;    // 合法

三、实战 1- 通讯录管理系统

通讯录是一个可以记录亲人,好友信息的工具。系统中需要实现的功能如下:

  • 添加联系人:向通讯录中添加新人,信息包括(姓名,性别,年龄,联系电话,家庭住址),最多记录 100 人
  • 显示联系人:显示通讯录总所有联系人的信息
  • 删除联系人:按照姓名进行删除指定联系人
  • 查找联系人:按照姓名进行查找指定联系人
  • 修改联系人:按照姓名重新修改指定联系人
  • 清空联系人:清空通讯录中所有信息
  • 退出通讯录:退出当前使用的通讯录
#include<iostream>
#include<string>
using namespace std;
#define MAX 1000

void showMenu() {
    cout << "**************************" << endl;
    cout << "*****  1. 添加联系人  *****" << endl;
    cout << "*****  2. 显示联系人  *****  " << endl;
    cout << "*****  3. 删除联系人  *****" << endl;
    cout << "*****  4. 查找联系人  *****" << endl;
    cout << "*****  5. 修改联系人  *****" << endl;
    cout << "*****  6. 清空联系人  *****" << endl;
    cout << "*****  0. 退出通讯录  *****" << endl;
    cout << "**************************" << endl;
}

struct member {
    string name;
    int sex;  //1 为男 2 为女
    int age;
    string tel;
    string addr;
};

struct addressbooks {
    struct member memberarr[MAX];  // 通讯录名单
    int size;                      // 人数
};

void addperson(addressbooks * abs) {
    if (abs->size == MAX) {
        cout << " 联系人已满,请删除不必要的人员再添加 " << endl;
        return;
    }
    else {
        string name;
        int sex=0;  //1 为男 2 为女
        int age=0;
        string tel;
        string addr;

        cout << " 请输入姓名:" << endl;
        cin >> name;
        abs->memberarr[abs->size].name = name;
        cout << " 请输入性别:" << endl;
        cout << "1  ---  男 " << endl;
        cout << "2  ---  女 " << endl;
        while (true) {
            cin >> sex;
            if (sex == 1 || sex == 2) {
                abs->memberarr[abs->size].sex = sex;
                break;
            }
            cout << " 输入有误,请重新输入!" << endl;

        }

        cout << " 请输入年龄:" << endl;
        cin >> age;
        abs->memberarr[abs->size].age = age;
        cout << " 请输入电话:" << endl;
        cin >> tel;
        abs->memberarr[abs->size].tel = tel;
        cout << " 请输入地址:" << endl;
        cin >> addr;
        abs->memberarr[abs->size].addr = addr;

        abs->size++;
        cout << " 添加成功 " << endl;
        system("pause");
        system("cls");

    }
}

void showperson(addressbooks * abs) {
    if (abs->size == 0) {
        cout << " 通讯录为空 " << endl;
    }
    else {
        for (int i = 0; i < abs->size; i++) {
            cout << " 姓名: " << abs->memberarr[i].name;
            cout << "\t 性别: " << (abs->memberarr[i].sex==1?" 男 ":" 女 ");
            cout << "\t 年龄: " << abs->memberarr[i].age;
            cout << "\t 电话: " << abs->memberarr[i].tel;
            cout << "\t 地址: " << abs->memberarr[i].addr << endl;
        }
    }
    system("pause");
    system("cls");
}

int isExist(addressbooks* abs, string name) {
    for (int i = 0; i < abs->size; i++) {
        if (abs->memberarr[i].name == name) {
            return i;
        }
    }
    return -1;
}

void delperson(addressbooks* abs) {
    cout << " 请输入删除的联系人:" << endl;
    string name;
    cin >> name;
    int ret = isExist(abs,name);
    if (ret != -1) {
        for (int i = ret; i < abs->size; i++) {
            abs->memberarr[i] = abs->memberarr[i + 1];
        }
        abs->size--;
        cout << " 删除成功 " << endl;
    }
    else {
        cout << " 查无此人 " << endl;
    }
    system("pause");
    system("cls");
    }

void findperson(addressbooks* abs) {
    cout << " 请输入查找的联系人:" << endl;
    string name;
    cin >> name;
    int ret= isExist(abs, name);
    if (ret != -1) {
        cout << " 姓名: " << abs->memberarr[ret].name << "\t";
        cout << " 性别: " << (abs->memberarr[ret].sex == 1 ? " 男 " : " 女 ") << "\t";
        cout << " 年龄: " << abs->memberarr[ret].age << "\t";
        cout << " 电话: " << abs->memberarr[ret].tel << "\t";
        cout << " 地址: " << abs->memberarr[ret].addr << endl;
    }
    else {
        cout << " 查无此人 " << endl;
    }
    system("pause");
    system("cls");
}

void modifyperson(addressbooks* abs) {
    cout << " 请输入修改的联系人:" << endl;
    string name;
    cin >> name;
    int ret = isExist(abs, name);
    if (ret != -1) {
        cout << " 请输入姓名:" << endl;
        cin >> name;
        abs->memberarr[ret].name = name;
        cout << " 请输入性别:" << endl;
        cout << "1  ---  男 " << endl;
        cout << "2  ---  女 " << endl;
        while (true) {
            int sex = 0;
            cin >> sex;
            if (sex == 1 || sex == 2) {
                abs->memberarr[abs->size].sex = sex;
                break;
            }
            cout << " 输入有误,请重新输入!" << endl;
        }
        int age = 0;
        cout << " 请输入年龄:" << endl;
        cin >> age;
        abs->memberarr[ret].age = age;
        cout << " 请输入电话:" << endl;
        string tel;
        cin >> tel;
        abs->memberarr[ret].tel = tel;
        cout << " 请输入地址:" << endl;
        string addr;
        cin >> addr;
        abs->memberarr[ret].addr = addr;
        cout << " 修改成功 " << endl;

    }
    else {
        cout << " 查无此人 " << endl;
    }
    system("pause");
    system("cls");
}

void cleanperson(addressbooks* abs) {
    abs->size = 0;
    cout<<" 联系人已全部清空 " << endl;
    system("pause");
    system("cls");
}

int main() {

    int select = 0;
    addressbooks abs;
    abs.size = 0;

    while (true) {
        showMenu();
        cin >> select;
        switch (select)
        {
        case 1:
            addperson(&abs);
            break;
        case 2:
            showperson(&abs);
            break;
        case 3:
            delperson(&abs);
            break;
        case 4:
            findperson(&abs);
            break;
        case 5:
            modifyperson(&abs);
            break;
        case 6:
            cleanperson(&abs);
            break;
        case 0:
            cout << " 欢迎下次使用 " << endl;
            system("pause");
            return 0 ;
            break;
        default:
            break;

        }        
    }    
}

四、C++ 核心编程

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

4.1 内存分区模型

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

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

内存四区的意义:

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

—————–程序运行前————–

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

  1. 代码区:存放 CPU 执行的机器指令;代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份即可;代码区是只读的,防止程序意外的修改了它的指令。
  2. 全局区:全局变量 静态变量 存放于此;全局区还包含了 常量 区,字符串常量和其他常量也存放于此;该区域的数据在程序结束后由操作系统释放。
int g_a = 10;   // 全局变量

const int c_g_a = 10; //const 修饰的全局变量

int main(){

    int a = 10;   // 局部变量

    static int s_a = 10; // 静态变量,在普通变量前加 static,属于静态变量

    string str_a = "hello word";    // 字符串常量

    const int c_a = 10;    //const 修饰的局部变量

    system("pause");

}

—————–程序运行中————–

栈区:由编译器自动分配释放,存放函数的参数值,局部变量等。注意:不要返回局部变量的地址。

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

int* func() {
    int* p = new int(10);   //new 创建的数据,返回指针
    return p;
}

int main(){

    int* p = func();

    cout << *p << endl;
    cout << *p << endl;

    system("pause");

}

4.2 new 操作符

C++ 中利用 new 操作符在堆区开辟数据.

堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符 delete

语法:new 数据类型

利用 new 创建的数据,会返回该数据对应的类型的指针。

int* func() {
    int* arr = new int[10];   // 创建一个数组,返回首地址

    arr[0] = 0;

    return arr;
}

int main(){

    int* p = func();

    cout << *p << endl;
    cout << *p << endl;

    delete[] p;  // 释放数组时,要加[]

    system("pause");

}

4.3 引用

作用:给变量起别名

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

int main(){

    int a = 10;

    int& b = a;
    cout << a << endl;  // 输出 10
    cout << b << endl;  // 输出 10

    b = 100;

    cout << a << endl;  // 输出 100
    cout << b << endl;  // 输出 100


    system("pause");

}

注意事项:引用必须初始化,且在初始化后不可更改。

4.3.1 引用作函数实参

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

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

void swap(int &a,int &b){
    int temp = a;
    a = b;
    b = temp;
}

int main(){

    int a = 10;
    int b = 20;

    swap(a, b);     // 引用传递,形参也会修饰实参的

    cout << a << endl;    // 输出 20
    cout << b << endl;    // 输出 10

    system("pause");
}

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

4.3.2 引用作函数的返回值

作用:引用是可以作为函数的返回值存在的。注意:不要返回局部变量引用

用法:函数调用作为左值

int& test() {
    static int a = 10;
    return a;   // 返回静态变量的引用
}

int main(){

    int& ref = test();   

    cout << ref << endl;
    cout << ref << endl;

    // 如果函数作左值,必须返回函数的引用
    test() = 1000;  

    cout << ref << endl;
    cout << ref << endl;

    system("pause");

}

4.3.3 引用的本质

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

void func(int& ref) {
    ref = 50;   //ref 是引用,转换为 *ref = 50
}

int main() {

    int a = 10;

    int& ref = a;  // 自动转换为 int* const ref=&a; 指针常量时指针指向不可改,也说明为什么引用不可更改

    ref = 100; // 内部发现 ref 是引用,自动帮我们转换为 *ref=100;

    func(a);

 }

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

4.3.4 常量引用

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

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

void func(const int& v) {
    v = 50;   // 常量引用不可赋值,报错
    cout << v << endl;
}

int main() {

    //int& ref = 10;  // 报错。常量引用,引用必须引一块合法的内存空间

    const int& ref = 10; 
    // int temp=10; const int& ref = temp;  // 实际是这样的
    //ref = 20; 加入 const 之后变为只读,不可以修改

    // 函数中利用常量引用防止误操作修改实参
    int a = 20;

    func(a);

    system("pause");

 }

4.4 函数的提高

4.4.1 函数默认值

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

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

// 定义函数时,可以设置初始默认值
int func(int a, int b = 20,int c = 30) {
    return (a + b + c);
}

int main() {

    cout << func(10) << endl;  
    cout << func(10, 40) << endl;      // 如果有传参,就用传的参数,若无,就用默认的值
    cout << func(10, 40, 50)<< endl;
    //cout << func(, , 50)<< endl;        // 非法调用,不能只默认前两个,改第三个

    system("pause");
 }

定义函数时,如果某个位置已经有了默认值,那么从这个位置往后,从左到右都必须有默认值。

函数声明和函数定义,二者只能有一种默认参数,不能函数声明和函数定义都写默认参数。

int func(int a, int b = 30,int c = 0)        // 函数声明

int func(int a, int b = 30,int c = 0) {     // 函数定义
    return (a + b + c);
}

// 以上的默认值写法是错的,不能同时存在

4.4.2 函数占位参数

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

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

void func(int a, int,int) {    
    cout << "this is a test" << endl;
}

int main() {

    func(10, 10, 10);

    system("pause");
 }

4.4.3 函数重载

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

函数重载满足条件:

  1. 同一个作用域下
  2. 函数名相同
  3. 函数参数 类型不同 或者 个数不同 或者 顺序不同
void func() {    
    cout << "func()的调用 " << endl;
}

void func(int a) {
    cout << "func(int a)的调用 " << endl;
}

int main() {

    func();        // 输出:func()的调用
    func(10);      // 输出:func(int a)的调用

    system("pause");
 }

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

函数参数引用可以作为函数重载的条件

使用函数重载时,尽量不要写默认参数

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

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

int main() {
    int a = 10;
    func(a);                // 输出:func1 的调用

    const int b = 10;
    func(b);               // 输出:func2 的调用

    system("pause");

 }

4.5 类和对象

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

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

4.5.1 封装

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

封装的意义一:

  • 将属性和行为作为一个整体,表现生活中的事物
  • 将属性和行为加以权限控制

语法:class 类名 { 访问权限:属性 / 行为 };

一个圆类:

const double PI = 3.14;

class Circle {

public:              // 访问权限:公共权限
    int m_r;

    double calcul_perimeter() {
        return 2 * PI * m_r;
    }
};

int main() {

    Circle C1;   // 通过圆类,创建具体的圆(对象),实例化

    C1.m_r = 10;   // 给属性赋值

    cout << C1.calcul_perimeter() << endl;  // 输出:62.8

    system("pause");
 }

类中的属性和行为,我们统一称为成员。

属性:成员属性、成员变量

行为:成员函数、成员方法

封装的意义二:

  • 类在设计时,可以把属性和行为放在不同的权限下,加以控制
  • 访问权限有三种
    1. public 公共权限:类内可以访问,类外可以访问
    2. protected 保护权限:类内可以访问,类外不可以访问
    3. private 私有权限:类内可以访问,类外不可以访问
class Person {

public:             
    string m_name;
protected:
    string m_car;
private:
    int m_password = 1001;

public:
    void get_info() {
        //m_name = "liu wen";
        m_car = "BWM";
        m_password = 123456;
        cout << m_name << endl;
        cout << m_car << endl;
        cout << m_password << endl;
    }
};

int main() {

    Person p1;  
    p1.m_name = "li qiang";
    p1.get_info();             // 输出:li qiang   BWM   123456

    system("pause");
 }

struct 和 class 区别:

在 C++ 中,struct 和 class 唯一的区别就在于默认的访问权限不同。

区别:

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

成员属性设为私有

  • 将所有成员属性设为私有,可以自己控制读写权限
  • 对于写权限,我们可以检测数据的有效性

举一个例子:

class Person {

public:             
    void setname(string name) {
        m_name = name;
    }
    string getname() {
        return m_name;
    }
    string getcar() {
        string m_car = "BWM";  // 初始化
        return m_car;
    }
    void setpassword(int password) {
        m_password = password;
    }

private:
    string m_name;   
    string m_car;   
    int m_password;  

};

int main() {

    Person p1;  

    p1.setname("li qiang");           // 可写
    cout << p1.getname() << endl;     // 可读

    cout << p1.getcar() << endl;     // 只读

    p1.setpassword(456789);            // 只写

    system("pause");

 }

一个正方体的例子:

class Cube {

public:             
    void setL(int L) {
        m_L = L;
    }
    double getL() {
        return m_L;
    }
    void setW(int W) {
        m_W = W;
    }
    double getW() {
        return m_W;
    }
    void setH(int H) {
        m_H = H;
    }
    double getH() {
        return m_H;
    }
    bool isSame(Cube& c) {
        if (m_L == c.getL() && m_W == c.getW() && m_H == c.getH()) {
            return true;
        }
        return false;
    }

private:
    double m_L;   
    double m_W;
    double m_H;
};

bool isSame(Cube& c1, Cube& c2) {
    if (c1.getL() == c2.getL() && c1.getW() == c2.getW() && c1.getH() == c2.getH()) {
        return true;
    }
    return false;
}
int main() {

    Cube C1;
    C1.setL(100);
    C1.setH(100);
    C1.setW(100);

    Cube C2;
    C2.setL(100);
    C2.setH(100);
    C2.setW(100);

    // 成员函数判断
    bool tag = C1.isSame(C2);

    if (tag) {
        cout << "C1 和 C2 是相同的 " << endl;
    }
    else {
        cout << "C1 和 C2 是不同的 " << endl;
    }

    // 全局函数判断
    isSame(C1, C2);

    system("pause");

 }

4.5.2 对象的初始化和清理

4.5.2.1 构造函数和析构函数

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

一个对象或者变量没有初始状态,对其使用后果是未知的

同样使用完一个对象或者变量,没有及时清理,也会造成一定的安全问题

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

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

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

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

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

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

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

void test() {
    Person p;   // 在栈上的数据,函数执行完会被自动释放
}

int main() {

    test();   // 构造,析构函数均会被调用

    Person p1;   // 这里不会析构,p1 执行完没被释放,按 enter 后才被释放,调用析构

    system("pause");

 }

4.5.2.2 构造函数的分类及调用

两种分类方式:

  • 按参数分:有参构造和无参构造
  • 按类型分:普通构造和拷贝构造

三种调用方式:括号法 显示法 隐式转换法

class Person {
public:

    //Person 类的构造函数
    Person() {
        cout << " 默认构造函数的调用 " << endl;
    }
    //Person 类的构造函数的重载 --- 有参构造
    Person(int a) {
        age = a;
        cout << " 有参构造函数的调用 " << endl;
    }

    //Person 类的构造函数的重载 --- 拷贝构造
    Person(const Person &p) {
        age = p.age;
        cout << " 拷贝构造函数的调用 " << endl;

    }

    //Person 类的析构函数
    ~Person() {
        cout << "Person 析构函数的调用 " << endl;
    }

    int age;
    int name;
};

int main() {

    // 括号调用法
    Person p1;          // 无参构造调用,注意:无参构造调用不能写括号 p1()
    //Person p4(); 这种写法会让编译器认为这是函数声明

    Person p2(10);      // 有参构造调用
    Person p3(p2);      // 拷贝参构造调用

    // 显示法
    Person p1;
    Person p2 = Person(10); //Person(10); 匿名对象,执行完系统会立即回收
    Person p3 = Person(p2);

    // 隐式转换法
    Person p2 = 10;      // 相当于 Person p2(10)
    Person p3 = p2;      // 拷贝构造

    system("pause");

 }

4.5.2.3 拷贝构造函数调用时机

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

  • 使用一个已经创建完毕的对象来初始化一个新对象
  • 值传递的方式给函数参数传值
  • 以值方式返回给局部对象
class Person {
public:

    Person() {
        cout << " 默认构造函数的调用 " << endl;
    }
    //Person 类的构造函数的重载 --- 拷贝构造

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

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

    //Person 类的析构函数
    ~Person() {
        cout << "Person 析构函数的调用 " << endl;
    }

    int age;
    int name;
};

//1、使用一个已经创建完毕的对象来初始化一个新对象
void test1() {
    Person p1(20);
    Person p2(p1);
    cout << p2.age << endl;
}

//2、值传递的方式给函数参数传值
void doWork2(Person p) {

}
void test2() {
    Person p1;
    doWork2(p1);
}

//3、以值方式返回给局部对象
Person doWork3() {
    Person p1;
    cout << (int*)&p1 << endl;
    return p1;
}
void test3() {
    Person p = doWork3();
    cout << (int*)&p << endl;
}

int main() {

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

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

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

    system("pause");

 }

4.5.2.4 构造函数调用规则

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

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

构造函数的调用规则如下:

  • 如果用户定义有参构造函数,C++ 不再提供默认无参构造,但是会提供默认拷贝构造
  • 如果用户定义拷贝构造函数,C++ 不会再提供其他构造函数。
  • 总的来说,必须写拷贝构造函数,有它就行。

4.5.2.5 深浅拷贝

浅拷贝:简单的赋值拷贝操作

深拷贝:在堆区重新申请空间,进行拷贝操作

默认采用类的拷贝构造函数—浅拷贝:

class Person {
public:

    Person() {
        cout << " 默认构造函数的调用 " << endl;
    }

    Person(int a,int b) {
        age = a;
        height = new int(b);
        cout<< " 有参构造函数的调用 " << endl;

    }

    // 析构函数,将堆区开辟的数据做释放操作
    ~Person() {
        if (height != NULL) {
            delete height;
            height = NULL;
        }
        cout << "Person 析构函数的调用 " << endl;
    }

    int age;
    int *height;
};

void test1() {
    Person p1(18,160);
    cout << p1.age << endl;
    cout << *p1.height << endl;

    Person p2(p1);         // 默认浅拷贝

    cout << p2.age << endl;
    cout << *p2.height << endl;
}

int main() {

    test1();   // 浅拷贝带来的问题是内存的重复释放,造成程序异常终止

    system("pause");
 }

自己写类的拷贝构造函数—深拷贝:

class Person {
public:

    Person() {
        cout << " 默认构造函数的调用 " << endl;
    }

    Person(int a,int b) {
        age = a;
        height = new int(b); 
        cout<< " 有参构造函数的调用 " << endl;

    }
    // 自己写拷贝构造函数
    Person(const Person& p) {
        age = p.age;
        //height = p.height; 编译器默认写法
        height = new int(*p.height);  // 深拷贝
    }

    // 析构函数,将堆区开辟的数据做释放操作
    ~Person() {
        if (height != NULL) {
            delete height;
            height = NULL;
        }
        cout << "Person 析构函数的调用 " << endl;
    }

    int age;
    int *height;
};

void test1() {...}

int main() {...}

总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题

4.5.2.6 初始化列表

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

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

class Person {
public:

    Person() : A(0), B(0), C(1){   // 初始化默认属性
        cout << " 构造函数的调用 " << endl;
        cout << A << endl;
        cout << B << endl;
        cout << C << endl;
    }

    Person(int a, int b, int c) : A(a), B(b), C(c){   // 通过传参初始化属性
        cout << " 构造函数的调用 " << endl;
        cout << A << endl;
        cout << B << endl;
        cout << C << endl;
    }

    ~Person() {
        cout << " 析构函数的调用 " << endl;
    }
private:
    int A;
    int B;
    int C;
};

int main() {

    Person p(10,20,30);

    system("pause");
 }

4.5.2.7 类对象作为类成员

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

class Phone {
public:
    string phone_name;

    Phone(string pname) {
        phone_name = pname;
    }    
    ~Phone() {
        cout << "Phone 析构函数的调用 " << endl;
    }
};

class Person {
public:

    Person(int a,string b) : m_id(a), m_phone(b)
    {
        cout << m_id << endl;
        cout << m_phone.phone_name << endl;
    }

    ~Person() {
        cout << "Person 析构函数的调用 " << endl;
    }
private:
    int m_id;
    Phone m_phone;   // 对象成员

};

int main() {

    Person p(111, "iphone13");

    system("pause");

 }

4.5.2.8 静态成员

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

静态成员分为:

  • 静态成员变量
    • 所有对象共享同一份数据
    • 在编译阶段分配内存
    • 类内声明,类外初始化
  • 静态成员函数
    • 所有对象共享同一个函数
    • 只能访问静态成员变量
class Person {
public:
    static void func(){

        cout << " 静态成员函数的调用 " << endl;
        cout << m_A << endl;
        //cout << m_B << endl;           不能访问 m_B, 静态成员函数只能访问静态成员变量

    }

    static int m_A;  // 类内声明
    int m_B;
};

int Person::m_A = 0;  // 类外初始化

int main() {

    // 通过对象访问
    Person p;
    p.func();

    // 通过类名访问
    Person::func();

    system("pause");

 }

static 在 C++ 中的作用:

  • 在修饰变量的时候,static 修饰的静态局部变量只执行初始化一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。
  • static 修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是 extern 外部声明也不可以。
  • static 修饰一个函数,则这个函数的只能在本文件中调用,不能被其他文件调用。static 修饰的变量存放在全局数据区的静态变量区,包括全局静态变量和局部静态变量,都在全局数据区分配内存。初始化的时候自动初始化为 0。
  • 不想被释放的时候,可以使用 static 修饰。比如修饰函数中存放在栈空间的数组。如果不想让这个数组在函数调用结束释放可以使用 static 修饰。
  • 考虑到数据安全性(当程序想要使用全局变量的时候应该先考虑使用 static)。

引用:C/C++ 中 static 的用法全局变量与局部变量 | 菜鸟教程 (runoob.com)

4.5.2.9 const 成员

const 对象只能调用 const 成员函数;非 const 对象是可以调用 const 成员函数的。

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

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

在 C++ 中,类内的成员变量和成员函数分开储存

只有非静态成员变量才属于类的对象

首先举一个简单例子:一个空类占的内存空间

class Person {

};

void test1() {
    Person p1;
    cout << sizeof(p1) << endl;  // 输出结果 1,C++ 编译器会给每个空对象分配 1 字节空间,是为了区分空对象占内存的位置
}

int main() {

    test1();

    system("pause");

 }

非空类占的内存空间:非静态成员变量才属于类的对象,按 成员变量计算内存空间;静态变量不属于,不计算内存。

class Person {
public:
    void func() {                   // 成员函数分开储存
        cout << m_A << endl;
    }

    int m_A;               // 计算成员变量 内存空间 4
    static int m_B;        // 不计算
};

void test1() {
    Person p1;
    cout << sizeof(p1) << endl;    // 输出结果 4
}

int main() {

    test1();

    system("pause");

 }

4.5.3.2 this 指针

每一个非静态成员函数只会诞生一份函数实例,也就是多个同类的对象会共用一块代码,那么这一块代码是如何区分每个对象调用自己的呢?

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

this 指针是隐含每一个非静态成员函数内的一种指针

this 指针不需要定义,直接使用即可

this 指针的用途:

  • 当形参和成员变量同名时,可以用 this 指针来区分
  • 在类的非静态成员函数中返回对象本身,可使用 return *this
class Person {
public:
    Person(int age) {
        this->age = age;   // 形参 age 和成员变量 age 同名
    }
    Person& addAge(Person &p) {
        this->age += p.age;
        return *this;            // 可以链式调用
    }

    int age;
};

void test1() {
    Person p1(18);
    cout << p1.age << endl;
}

void test2() {
    Person p1(10);
    Person p2(10);
    p2.addAge(p1).addAge(p1).addAge(p1);
    cout << p2.age << endl;
}

int main() {

    test1();

    test2();

    system("pause");
 }

4.5.3.3 空指针访问成员函数

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

如果用到 this 指针,需要加以判断保证代码的健壮性

class Person {
public:
    void func() {
        cout << " 调用成员函数 " << endl;
    }
    void getAge() {
        cout << age << endl;  // 等同于 this->age,传入的指针为 NULL
    }
    void getName() {
        if (this == NULL) {         // 加个判断,避免传入空指针
            return;
        }
        cout << name << endl;
    }

    int age;
    string name;
};

void test1() {
    Person* p = NULL;

    p->func();
    //p->getAge(); 报错

    p->getName();
}

int main() {

    test1();

    system("pause");
 }

4.5.3.4 const 修饰成员函数

常函数:

  • 成员函数后加 const 后我们称之为这个函数为常函数
  • 常函数内不可以修改成员属性
  • 成员属性声明时加关键字 mutable 后,在常函数中依然可以修改

常对象:

  • 声明对象前加 const 称该对象为常对象
  • 常对象只能调用常函数
class Person {
public:

    // 在成员函数后面加 const,称为常函数,修饰的是 this 指向,让指针指向的值不可以修改
    void getAge() const{
        //this->age = 100; // 报错
        this->name = "ssss";   //name 是 mutable 属性,可以修改

    }

    void func1() {
        this = NULL;     // 报错,this 本质是指针常量,不可以修改指针的指向
    }
    void func2() {
        age = 100;
    }

    int age;
    mutable string name;  // 特殊变量,即使在常函数中,也可以修改这个值
};

void test1() {
    const Person p;   // 常对象
    p.age = 100;     // 报错,也不可修改
    p.name = "sd";   // 可以修改

    // 常对象只能调用常函数
    p.getAge();
    p.func2();   // 报错

}

int main() {

    test1();

    system("pause");
 }

4.5.4 友元

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

友元的目的就是让一个函数或者类,访问另一个类中私有属性。一个类中可以有多个友元。

友元的关键字为friend

友元的三种实现:

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元

全局函数做友元:

class Building {

    friend void Goodgay(Building& b); // 声明该函数为 Building 类的友元, 可以访问 Building 的私有属性

public:
    Building() {
        m_sittingroom = " 客厅 ";
        m_bedgroom = " 卧室 ";
    }

    string m_sittingroom;

private:
    string m_bedgroom;

};

void Goodgay(Building &b) {

    cout << b.m_sittingroom << endl;
    cout << b.m_bedgroom << endl;     // 访问 Building 类的私有属性

}
void test1() {
    Building b1;
    Goodgay(b1);
}

int main() {

    test1();

    system("pause");
 }

类做友元

class Building {
    friend class Goodgay;  // 友元可以访问 Building 的私有成员

public:
    Building();

    string m_sittingroom;

private:
    string m_bedroom;
};

class Goodgay {

public:
    Goodgay();

    void visit();

    Building *b;
};

// 类外写成员函数
Building::Building() {
    m_sittingroom = " 客厅 ";
    m_bedroom = " 卧室 ";
}
Goodgay::Goodgay(){
    b = new Building;
}
void Goodgay::visit() {
    cout << b->m_sittingroom << endl;
    cout << b->m_bedroom << endl;
}

void test1() {
    Goodgay gg;
    gg.visit();
}

int main() {

    test1();

    system("pause");
 }

成员函数做友元

class Building;

class Goodgay {

public:
    Goodgay();

    void visit();

    Building *b;
};

class Building {
    friend void Goodgay::visit();  // 成员函数做友元

public:
    Building();

    string m_sittingroom;

private:
    string m_bedroom;
};

// 类外写成员函数
Building::Building() {
    m_sittingroom = " 客厅 ";
    m_bedroom = " 卧室 ";
}
Goodgay::Goodgay(){
    b = new Building;
}
void Goodgay::visit() {
    cout << b->m_sittingroom << endl;
    cout << b->m_bedroom << endl;          // 可以访问私有属性
}
void test1() {
    Goodgay gg;
    gg.visit();
}

int main() {

    test1();

    system("pause");
 }

注意 Building 类必须写在 Goodgay 类后面

关于友元,有两点需要说明:

  • 友元的关系是单向 的而不是双向的。如果声明了类 B 是类 A 的友元类,不等于类 A 是类 B 的友元类,类 A 中的成员函数不能访问类 B 中的 private 成员。
  • 友元的关系不能传递。如果类 B 是类 A 的友元类,类 C 是类 B 的友元类,不等于类 C 是类 A 的友元类。

以上的例子中,都是使用 A 类中的成员函数作为 B 类的友元,因此 A 可以访问 B 类的私有属性。其实加友元关键字后,该函数可以变成全局函数,这个全局函数写在类内(声明和实现都写在类内)。可以在其他地方直接调用(无需写作用域)。

class Person {

    // 全局函数在类内实现, 加 friend 关键字
    friend void showPerson(Person &p) {
        cout << p.m_name << endl;
        cout << p.m_age << endl;
    };

public:
    Person(string name, int age) {
        this->m_name = name;
        this->m_age = age;
    };

private:
    string m_name;
    int m_age;
};

void test() {
    Person p1("Tom", 25);
    showPerson(p1);     // 这是一个全局函数

}

int main() {

    test();

    system("pause");
}

4.5.5 运算符重载

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

4.5.5.1 加号运算符重载

作用:实现两个自定义数据类型相加的运算。对于编译器内置的数据类型(如整型,浮点型),编译器知道如何进行加减乘除,但是对于自定义的数据类型,就不管用了。

成员函数实现 + 号重载

class Person {
public:
    // 成员函数实现 + 号重载
    Person operator+(Person& p) {
        Person temp;
        temp.m_A = this->m_A + p.m_A;
        temp.m_B = this->m_B + p.m_B;
        return temp;
    }
    int m_A ;
    int m_B ;
};

void test1() {
    Person p1;
    p1.m_A = 10;
    p1.m_B = 20;

    Person p2;
    p2.m_A = 100;
    p2.m_B = 200;

    Person p3 = p1 + p2;    //Person 类型的数据进行加号运算
    cout << p3.m_A << endl;
    cout << p3.m_B << endl;

}

int main() {

    test1();

    system("pause");
 }

全局函数实现 + 号重载

class Person {...};

// 全局函数重载 + 号运算符
Person operator+(Person& p1, Person& p2) {
    Person temp;
    temp.m_A =p1.m_A + p2.m_A;
    temp.m_B = p1.m_B + p2.m_B;
    return temp;
}

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

class Person {...};

Person operator+(Person& p1, Person& p2) {
    Person temp;
    temp.m_A =p1.m_A + p2.m_A;
    temp.m_B = p1.m_B + p2.m_B;
    return temp;
}

Person operator+(Person& p1, int a) {
    Person temp;
    temp.m_A = p1.m_A + a;
    temp.m_B = p1.m_B + a;
    return temp;
}

void test1() {
    Person p1;
    Person p2;
    Person p3 = p1 + p2;    //Person 类型的数据进行加号运算
    Person p4 = p1 + 10;    //Person 类型的数据与整型数据进行加号运算
}

4.5.5.2 左移运算符重载

class Person {
public:

    int m_A = 10 ;
    int m_B = 20 ;
};

// 全局函数重载 << 号运算符
ostream & operator<<(ostream &out,Person &p) {
    out << p.m_A << endl;
    out << p.m_B << endl;
    return out;
}

void test1() {
    Person p1;
    cout << p1 << endl;   // 重载过的 << 运算符,可以直接输出 Person 类型的数据
    cout << p1.m_A;  // 原有的 int 类型数据也可以照样输出
}

int main() {

    test1();

    system("pause");
 }

左移运算符只能在全局函数中重载。成员函数中达不到这个效果。

4.5.5.3 递增运算符重载

class Myinteger {

    friend ostream& operator<<(ostream& out, Myinteger a);

public:

    Myinteger() {
        m_num = 0;
    }
    // 重载 ++ 运算符,前置 ++
    Myinteger& operator++() {
        m_num++;
        return *this;
    }
    // 重载 ++ 运算符,后置 ++
    Myinteger operator++(int) {     //int 是占位参数,区分前置和后置递增
        Myinteger temp = *this;
        m_num++;
        return temp;   // 返回值,不能返回引用,temp 是一个局部(临时)变量
    }
private:

    int m_num ;

};

ostream& operator<<(ostream& out, Myinteger a) {
    out << a.m_num;
    return out;
}
void test1() {
    Myinteger m;
    cout << ++m << endl;  // 结果 1
    cout << m << endl;    // 结果 1

    Myinteger n;
    cout << n++ <<endl;  // 结果 0
    cout << n << endl;   // 结果 1
}

int main() {

    test1();

    system("pause");
 }

重载后置 ++ 时,也可以将 temp 开辟在堆区,就可以返回引用了。

4.5.5.4 赋值运算符重载

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

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝构造函数,对属性进行值拷贝
  4. 赋值运算符 operator=,对属性进行值拷贝

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

class Person {
public:
    Person(int age) {
        m_A = new int(age);  // 指针 m_A 指向堆区开辟的内存,内存存放 age 的值
    }
    ~Person() {
        if (m_A != NULL) {
            delete m_A;
            m_A = NULL;
        }
    }
    // 重载赋值运算符
    Person &operator=(Person &p) {
        //m_A = p.m_A; 编译器提供的浅拷贝

        if (m_A != NULL) {   // 先判断是否有属性在堆区,先释放干净,再深拷贝
            delete m_A;
            m_A = NULL;
        }
        m_A = new int(*p.m_A);   // 深拷贝

        return *this;
    }

    int* m_A;
};

void test1() {
    Person p1(20);
    Person p2(18);
    Person p3(30);

    p2 = p1;     // 采用自写的赋值深拷贝

    p3 = p2 = p1;  // 链式赋值,这就需要重载赋值时 return 本身

    cout << *p1.m_A << endl;
    cout << *p2.m_A << endl;
    cout << *p3.m_A << endl;
}

int main() {

    test1();

    system("pause");
 }

4.5.5.5 关系运算符重载

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

class Person {
public:
    Person(string a, int b) {
        m_Name = a;
        m_Age = b;
    }

    bool operator==(Person& p) {
        if (this->m_Name == p.m_Name && this->m_Age == p.m_Age) {
            return true;
        }
        else {
            return false;
        }
    }

    string m_Name;
    int m_Age;
};

void test1() {
    Person p1("Tom", 25);
    Person p2("Tom", 25);

    if (p1 == p2) {
        cout << "the same" << endl;
    }
    else {
        cout << "the different" << endl;
    }
}

int main() {

    test1();

    system("pause");
 }

4.5.5.6 函数调用运算符重载

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

}
func()  //func 是函数名,()是函数调用的意思

下面就是重载函数调用符括号():

class Myprint {
public:
    void operator()(string test) {
        cout << test << endl;
    }
};

class Myadd {
public:
    int operator()(int a, int b) {
        return a + b;
    }
};

void test1() {
    Myprint myprint;
    myprint("hello world");

    Myadd add;
    cout << add(10, 20) << endl;
    cout << Myadd()(20, 30) << endl;  // 匿名函数对象
}

int main() {

    test1();

    system("pause");
 }

4.5.6 继承

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

有些类与类之间存在特殊的关系,下级别的成员除了拥有上一级的共性,还有自己的特性。这个时候我们就可以考虑利用继承的技术,减少重复代码。

4.5.6.1 继承基本语法

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

子类也叫派生类;父类也叫基类

// 定义父类
class BasePage {
public:
    void header() {
        cout << " 首页、公开课、登陆、注册……(公共头部)" << endl;
    }
    void footer() {
        cout << " 帮助中心、交流合作、站内地图……(公共底部)" << endl;
    }
private:
    int date;
};

// 子类:Java 页面,继承 BasePage 类的属性
class Java : public BasePage{
public:
    void content() {
        cout << "JAVA 学科视频 " << endl;
    }
};

// 子类:CPP 页面,继承 BasePage 类的属性
class CPP : public BasePage {
public:
    void content() {
        cout << "CPP 学科视频 " << endl;
    }
};

void test1() {
    Java java;
    java.header();
    java.footer();
    java.content();

    CPP cpp;
    cpp.header();
    cpp.footer();
    cpp.content();
}

int main() {

    test1();

    system("pause");
 }

4.5.6.2 继承方式

继承方式一共有三种:默认为,私有继承(private)。

  • 公共继承(public)
  • 保护继承(protected)
  • 私有继承(private)

继承方式

基类的私有属性永远都无法通过继承访问。

只能采用 friend 友元技术访问。

4.5.6.3 继承中的对象模型

从父类继承过来的成员,哪些成员继承到子类中了?

其实,父类中所有的非静态成员属性都会被子类继承下去,只是父类中的私有属性被编译器隐藏了,不可访问,但是确实继承下去了。

class BasePage {
public:
    int a;
protected:
    int b;
private:
    int c;
};

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

void test1() {
    Son s1;
    cout << sizeof(s1) << endl;  // 输出 16 (4*4)
}

int main() {

    test1();

    system("pause");
 }

4.5.6.4 继承中构造和析构顺序

子类继承父类后,当创建子类对象,也会调用父类的构造函数。那么父类和子类的构造和析构顺序是谁先谁后?

class BasePage {
public:
    BasePage() {
        cout << " 父类构造函数 " << endl;
    }
    ~BasePage() {
        cout << " 父类析构函数 " << endl;
    }
};

class Son :public BasePage {
public:
    Son() {
        cout << " 子类构造函数 " << endl;
    }
    ~Son() {
        cout << " 子类析造函数 " << endl;
    }
};

void test1() {
    Son s1;            
}

int main() {

    test1();

    system("pause");
 }

/* 输出:
父类构造函数
子类构造函数
子类析造函数
父类析构函数
*/

4.5.6.5 继承中同名成员处理方式

继承中允许子类和父类有同名成员,不会覆盖。当子类与父类出现同名的成员,如何通过子类对象,访问到子类或者父类中同名的数据呢?

  • 访问子类同名成员,直接访问即可
  • 访问父类同名成员,需要加作用域
class BasePage {
public:
    void func() {
        cout << " 父类函数的调用 " << endl;
    }

    int m_A = 100;
    int m_B = 200;

};

class Son :public BasePage {
public:
    void func() {
        cout << " 子类函数的调用 " << endl;
    }

    int m_A = 10;
};

void test1() {
    Son s1;
    cout << s1.m_A << endl;              // 输出子类中自有的数据
    cout << s1.BasePage::m_A << endl;    // 输出父类中同名的数据
    cout << s1.m_B << endl;

    s1.func();                     // 输出子类中自有的函数
    s1.BasePage::func();           // 输出父类中同名的函数    
}

int main() {

    test1();

    system("pause");
 }

总结:

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

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

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

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

  • 访问子类同名成员,直接访问即可
  • 访问父类同名成员,需要加作用域
class BasePage {
public:
    static void func() {
        cout << " 父类函数的调用 " << endl;
    }

    static int m_A;
    int m_B = 200;

};
int BasePage::m_A = 100;


class Son :public BasePage {
public:
    static void func() {
        cout << " 子类函数的调用 " << endl;
    }
    static int m_A;
};

int Son::m_A = 10;

void test1() {
    Son s1;

    // 通过对象访问
    cout << s1.m_A << endl;              // 输出子类中自有的数据
    cout << s1.BasePage::m_A << endl;    // 输出父类中同名的数据
    cout << s1.m_B << endl;

    // 通过类名访问
    cout << Son::m_A << endl;            // 访问子类自己的属性
    cout << Son::BasePage::m_A << endl;  // 访问子类的父类属性

}

int main() {

    test1();

    system("pause");
 }

4.5.6.7 多继承语法

C++ 允许一个类继承多个类。一个类继承了多个父类,称为多继承。

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

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

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

class Base1 {
public:
    void func() {
        cout << " 父类 1 函数的调用 " << endl;
    }

    int m_A = 100;
    int m_B = 200;

};
class Base2 {
public:
     void func() {
        cout << " 父类 2 函数的调用 " << endl;
    }

    int m_A = 10;
    int m_B = 20;
    int m_C = 30;

};

class Son :public Base1,public Base2 {
public:
    void func() {
        cout << " 子类函数的调用 " << endl;
    }

    int m_A = 1;
    int m_D = 2;
};



void test1() {
    Son s1;
    cout << s1.m_A << endl;
    cout << s1.Base1::m_A << endl;
    cout << s1.Base2::m_A << endl;
    cout << s1.Base1::m_B << endl;
    cout << s1.m_C << endl;

}

int main() {

    test1();

    system("pause");
 }

4.5.6.8 菱形继承

菱形继承(钻石)概念:

  1. 两个派生类(B,C)继承同一个基类(A)
  2. 又有某个类(D)同时继承两个派生类

菱形继承的问题:D 类同时通过从 B/C 类继承了 A 的数据,也就是 D 类有两份 A 的数据,其实我们只需要一份就可以。

这就需要用到虚继承了,关键字 virtual,解决内存浪费的问题。

class Animal {
public:
    int m_A;
};
class Yang: virtual public Animal {};
class Tuo: virtual public Animal {};
class Son :public Yang,public Tuo {};

void test1() {
    Son s1;
    s1.Yang::m_A = 100;
    s1.Tuo::m_A = 10;
    cout << s1.Yang::m_A << endl;  // 输出 10
    cout << s1.Tuo::m_A << endl;   // 输出 10
    cout << s1.m_A << endl;       // 输出 10

}

int main() {

    test1();

    system("pause");
 }

若上述两个类 YangTuo在继承 Animal 时不加 virtual 关键字,Son类就会继承两份 m_A,且不可用s1.m_A 访问到 m_A 数据。

函数在基类中被声明为 virtual 后,它在派生类中将自动成为虚方法。此时我们在派生类中将此方法声明不声明为 virtual 都没关系了,但是最好是声明出来好标记哪些方法是虚的。

4.5.6.9 链式继承

链式继承:C 继承自 B,B 继承自 A…

class Father {

public:
    void printA() {
        cout << " 父类中的方法 " << endl;
    }
};

class Son:public Father{

public:
    void printB() {
        cout << " 子类中的方法 " << endl;
    }
};

class GrandSon:public Son {

public:
    void printC() {
        cout << " 孙类中的方法 " << endl;
    }
};

int main() {

    GrandSon gs;

    gs.printA();    // 输出“父类中的方法”

    system("pause");
}

4.5.7 多态

4.5.7.1 多态的基本概念

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

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

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

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

静态多态:

class Animal {
public:
    void speak() {
        cout << " 动物在说话 " << endl;
    }
};

class Cat:public Animal {
public:
    void speak() {
        cout << " 小猫在说话 " << endl;
    }
};

// 地址早绑定,在编译阶段就确定函数的地址,传的是 animal 对象
void doSpeak(Animal &animal) {
    animal.speak();    
}

int main() {

    Cat cat;

    doSpeak(cat);  // 输出动物说话

    system("pause");
 }

动态多态:

动态多态满足条件:

  • 得有继承关系

  • 子类要重写父类中的虚函数

    重写:函数返回值,函数名称,参数列表完全相同

    子类重写时,也可以是虚函数

class Animal {
public:    
    virtual void speak() {                  // 虚函数
        cout << " 动物在说话 " << endl;
    }
};

class Cat:public Animal {
public:
    void speak() {                         // 普通成员函数
        cout << " 小猫在说话 " << endl;
    }
};

void doSpeak(Animal &animal) {
    animal.speak();       //animal 的 speak 函数定义为虚函数,地址晚绑定
}

int main() {

    Cat cat;
    doSpeak(cat);  // 输出小猫说话

    Animal animal;
    animal.speak("woo");   // 父类虚函数也可以直接调用

    system("pause");
 }

动态多态的使用:

  • 父类的指针或者引用,执行子类对象 。如上例中,doSpeak() 传入的父类的引用,然后调用的时候,传的是子类对象。

再举一个链式继承中多态的例子:

class Father {
public:
    virtual void print() {cout << " 父类中的方法 " << endl;}
};

class Son:public Father{
public:
    virtual void print() {cout << " 子类中的方法 " << endl;}
};

class GrandSon:public Son {
public:
    virtual void print() {cout << " 孙类中的方法 " << endl;}
};

int main() {

    Son son;

    Father *fa=&son;

    fa->print();

    GrandSon gs;

    Father* faa = &gs;

    faa->print();

    system("pause");
}

4.5.7.2 多态案例

分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类

多态的优点:

  1. 代码组织结构清晰
  2. 可读性强
  3. 利于前期和后期的扩展以及维护
//--------- 普通类实现 -----------------
class Calculator {
public:

    int getResult(string oper) {
        if (oper == "+") {
            return m_num1 + m_num2;
        }
        else if (oper == "-") {
            return m_num1 - m_num2;
        }
        else if (oper == "*") {
            return m_num1 * m_num2;
        }
        // 这个函数里,如果想扩展新功能(如加入开方运算),需要修改源码
        // 在真实开发环境中,提倡开闭原则
        // 开闭原则:对扩展进行开发,对修改进行关闭
    }

    int m_num1;
    int m_num2;
};

void test() {
    Calculator cal;
    cal.m_num1 = 10;
    cal.m_num2 = 5;
    cout << cal.getResult("*") << endl;
}

//---------- 利用多态实现 ----------------
class AbstractCalculator {
public:
    virtual int getResult() {
        return 0;
    }
    void doOther() {
        cout << "some free" << endl;
    }
    int m_A;    // 这两个必须是 public, 否则子类对象不可访问
    int m_B;  
};

class AddCalculator:public AbstractCalculator {
public:
    int getResult() {
        return m_A + m_B;
    }
};

class SubCalculator :public AbstractCalculator {
public:
    int getResult() {
        return m_A - m_B;
    }
};

class MultiCalculator:public AbstractCalculator {
public:
    int getResult() {
        return m_A * m_B;
    }
};

void test2() {
    // 多态的使用条件:父类指针或者引用指向子类对象
    AbstractCalculator* abs = new AddCalculator;   // 多态的调用
    abs->m_A = 10;
    abs->m_B = 5;
    cout << abs->getResult() << endl;
    delete abs;

    abs = new SubCalculator;
    abs->m_A = 20;
    abs->m_B = 10;
    cout << abs->getResult() << endl;
}
// 多态好处:组织结构清晰,可读性强,对于前期和后期扩展以及维护性高

int main() {

    //test();

    test2();

    system("pause");
 }

4.5.7.3 纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容。抽象类是类族的公共接口。

因此可以将虚函数改为纯虚函数

纯虚函数语法:virtual 返回类型 函数名(参数列表)= 0;

这样不是纯虚函数:virtual 返回类型 函数名(参数列表){return 0;}

当类中有了纯虚函数,这个类就称为抽象类。只要有一个纯虚函数就行,就是抽象类,就满足抽象类的特点。

抽象类特点:

  • 无法实例化对象

  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类

//Base 为抽象类
class Base {
public:
    virtual void func() = 0;  // 纯虚函数

    void doOther() {
        cout << "free" << endl;
    }
};

class Son :public Base {
public:
    void func() {                                // 重写父类中的抽象类
        cout << " 子类函数的调用 " << endl;
    }
};

void test() {
    //Base b1; 无法实例化,因为 Base 是抽象类
    Base* base = new Son;  // 父类指针或者引用指向子类对象
    base->func();
}

int main() {

    test();

    system("pause");
 }

在父类中写纯虚函数,就是为了在子类中重写这个函数。

4.5.7.4 多态案例

案例描述:制作饮品的大致流程为:煮水—冲泡—倒入杯中—加入辅料

利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶

咖啡制作:煮水—冲泡咖啡—倒入杯中—加糖和牛奶

茶水制作:煮水—冲泡茶叶—倒入杯中—加柠檬

class AbstractDrinking {
public:
    virtual void Boil() = 0;  // 煮水
    virtual void Brew() = 0;  // 冲泡
    virtual void PourInCup() = 0;  // 倒入杯中
    virtual void PutSomething() = 0;  // 加入辅料

    void makeDrinking() {
        Boil(); 
        Brew(); 
        PourInCup(); 
        PutSomething() ;
    }
};

class MakeCoffee :public AbstractDrinking {
public:
    void Boil() {
        cout << " 煮纯净水 " << endl;
    }
    void Brew() {
        cout << " 冲泡咖啡 " << endl;
    }
    void PourInCup() {
        cout << " 将咖啡水倒入杯中 " << endl;
    }
    void PutSomething() {
        cout << " 加糖和牛奶 " << endl;
    }
};

class MakeTea :public AbstractDrinking {
public:
    void Boil() {
        cout << " 煮山泉水 " << endl;
    }
    void Brew() {
        cout << " 冲泡茶叶 " << endl;
    }
    void PourInCup() {
        cout << " 将茶水水倒入杯中 " << endl;
    }
    void PutSomething() {
        cout << " 加柠檬 " << endl;
    }
};

void test() {
    AbstractDrinking* abs = new MakeCoffee;
    abs->makeDrinking();
    delete abs;
    cout << "----------" << endl;

    abs = new MakeTea;
    abs->makeDrinking();
}

int main() {

    test();

    system("pause");
 }

4.5.7.5 虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

解决方式:将父类中的析构函数改为虚析构或者纯虚析构

虚析构和纯虚析构共性:

  • 可以解决父类指针释放子类对象
  • 都需要有具体的函数实现

虚析构和纯虚析构区别:

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象

虚析构语法:virtual ~ 类名(){}

纯虚析构语法:

  • virtual ~ 类名()=0 在类中定义纯虚析构
  • 类名::~ 类名(){} 在全局中定义具体的实现
class Animal {
public:
    Animal() {
        cout << " 父类的构造函数的调用 " << endl;
    }
    // 利用虚析构可以解决 父类指针释放子类对象不干净的问题
    virtual ~Animal() {
        cout << " 父类的析构函数的调用 " << endl;
    }
    virtual void Speak() = 0;
};

class Cat: public Animal {
public:

    Cat(string name) {
        cout << " 子类构造函数的调用 " << endl;
        m_Name = new string(name);             // 堆区开辟内存
    }
    ~Cat() {
        if (m_Name != NULL) {
            delete m_Name;
            m_Name = NULL;
            cout << " 子类析构函数的调用 " << endl;
        }
    }
    void Speak() {
        cout << *m_Name << " 小猫在说话 " << endl;
    }
    string* m_Name;
};

void test() {
    Animal* animal = new Cat("Tom");   // 子类 Cat 开辟在堆区
    animal->Speak();  
    delete animal;  // 释放父类指针,父类含有虚析构,就可以走子类的析构
}

int main() {

    test();

    system("pause");
 }

总结:

  1. 虚析构或纯虚构就是用来解决通过父类指针释放子类对象
  2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构。
  3. 拥有纯虚析构函数的类也属于抽象类

4.6 结构体与类

C++中,结构体 它们都是有构造函数、析构函数和成员函数的,他们两者的根本区别就是:

  • 结构体中访问控制默认是 public

  • 而类中默认的访问控制是 private 的。

示例一:

struct Mt {
    int A ;
    int B ;

    int get_a() {
        return A;
    }
};

struct Mm :public Mt {int C = 5;};

int main() {

    Mm mm;
    mm.A = 10;

    cout << mm.A << endl;   
    cout << mm.C << endl; 
}

结构体成员函数也用 点. 获取。

示例二:

class Mt {
    int A ;
    int B ;

    int get_a() {
        return A;
    }
};

struct Mm :public Mt {int C = 5;};

int main() {

    Mm mm;
    mm.A = 10;   // 报错,无法访问,Mm 无法继承 Mt 的私有属性(默认私有)
}

类和结构体无明显区别,大多数情况下可以看成是一种东西。除了上述提到的默认访问控制问题。

4.7 文件操作

通过文件操作将数据持久化

C++ 中对文件操作需要包含头文件<fstream>

文件类型分为两种:

  1. 文本文件 — 文件以文本 ASCII 码形式存储在计算机中
  2. 二进制文件 — 文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们

操作文件的三大类:

  1. ofstream:写操作,将 程序 数据输出到文件中
  2. ifstream:读操作,将文件中数据输入到 程序
  3. fstream:读写操作

4.7.1 文本文件

4.7.1.1 写文件

  1. 包含头文件 #<fstream>
  2. 创建流对象 ofstream ofs;
  3. 打开文件 ofs.open('file_path', open_mode);
  4. 写数据 ofs<<'write txt';
  5. 关闭文件 ofs.close();

文件打开方式 open_mode:

打开方式 解释
ios::in 为读文件而打开文件
ios::out 为写而打开文件
ios::ate 初始位置:文件尾
ios::app 追加方式写文件
ios::trunc 如果文件存在先删除,再创建
ios::binary 二进制方式

注意:文件打开方式可以配合使用,利用 | 操作符

如:ios::binary | ios::out

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


void main(){
    ofstream ofs;
    ofs.open("test.txt", ios::out);
    ofs << "xxxxxxx" << endl;
    ofs << "yyyyyyy" << endl;
    ofs.close();

}

4.7.1.2 读文件

读文件步骤如下:

  1. 包含头文件 #<fstream>
  2. 创建流对象 ifstream ifs;
  3. 打开文件并判断文件是否打开成功 ifs.open('file_path', open_mode);
  4. 读数据
  5. 关闭文件 ofs.close();
# include<iostream>
# include<string>
using namespace std;
# include<fstream>

void main() {
    ifstream ifs;
    ifs.open("test.txt", ios::in);

    if (ifs.is_open()) {
        // 读数据
        /* 方法 1
        char buf[1024] = { 0 };    // 初始化一个数组
        while (ifs >> buf) {
            cout << buf << endl;
        }
        */

        /* 方法 2
        char buf2[1024] = { 0 };    // 初始化一个数组
        while (ifs.getline(buf2, sizeof(buf2))) {
            cout << buf2 << endl;
        };
        */

        // 方法 3
        string buf3;
        while (getline(ifs, buf3)) {
            cout << buf3 << endl;
        }

        /* 方法 4
        char buf4;
        while ((buf4 = ifs.get()) != EOF){
            cout << buf4 << endl;
}
        */
        ifs.close();
    }
    else {
        cout << " 文件打开失败 " << endl;
    }
}

4.7.2 二进制文件

以二进制的方式对文件进行读写操作

打开方式要指定为 ios::binary

4.7.2.1 写文件

二进制方式写文件主要利用流对象调用成员函数 write

函数原型:ostream& write(const char* buffer, int len);

参数解释:字符型指针 buffer 指向内存中一段存储空间,len 是读写的字节数

4.7.2.2 读文件

二进制方式读文件主要利用流对象调用成员函数 read

函数原型:istream& read(char* buffer, int len);

参数解释:字符型指针 buffer 指向内存中一段存储空间,len 是读写的字节数

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

class Person {
public:
    char m_name[64];
    int age;
};

// 二进制文件的写入
void write() {
    ofstream ofs("person.txt", ios::binary | ios::out);;
    Person p = { " 张三 ", 18 };
    ofs.write((const char*)&p, sizeof(Person));        //(const char*)强制转换成字符常量指针
    ofs.close();
}

// 二进制文件的读取
void read() {
    ifstream ifs("person.txt", ios::binary | ios::in);
    if (ifs.is_open()) {
        Person p;
        ifs.read((char*)&p, sizeof(Person));
        cout << p.m_name << endl;
        cout << p.age << endl;
        ifs.close();
    }
}

int main() {

    write();

    read();

}

五、实战 2- 职工管理系统

需求:职工管理系统可以用来管理公司内鄋员工的信息

本案例主要利用 C++ 来实现一个基于多态的职工管理系统

公司中职工分为三种,普通员工、经理、老板。显示信息时,需要显示职工编号,职工姓名、职工岗位、以及职责

普通员工职责:完成经理交给的任务

经理职责:完成老板交给的任务,并下发任务给员工

老板职责:管理公司所有事务

管理系统中需要实现的功能如下:

  • 退出管理程序:退出当前管理系统
  • 增加职工信息:实现批量添加职工功能,将信息录入到文件中,职工信息为:职工编号,姓名、部门编号
  • 显示职工信息:显示公司内部所有职工的信息
  • 删除离职员工:按照编号删除指定的职工
  • 修改职工信息:按照编号修改职工个人信息
  • 查找职工信息:按照编号或者姓名进行查找相关的人员信息
  • 按照编号排序:按照职工编号,进行排序,排序规则由用户指定
  • 清空所有文档:清空文件中所有职工信息(清空前需要再次确认,防止误删)

欢迎各位看官及技术大佬前来交流指导呀,可以邮件至 jqiange@yeah.net