目录

1 C++初识

1.1 第一个C++程序

1.1.1 创建项目

1.1.2 创建文件

1.1.3 编写代码

1.1.4 运行程序​编辑

1.2 注释

1.3 变量

1.4 常量

1.5 关键字

1.6 标识符命名规则

2 数据类型

2.1 整型

2.2 sizeof关键字

2.3 实型(浮点型)

2.4 字符型

2.5 转义字符

2.6 字符串型

2.7 布尔类型 bool

2.8 数据的输入

3 运算符

3.1 算术运算符

3.2 赋值运算符

3.3 比较运算符

3.4 逻辑运算符

4 程序流程结构

4.1 选择结构

4.1.1 if语句

4.1.2 三目运算符

4.1.3 switch语句

4.2 循环结构

4.2.1 while循环语句

4.2.2 do...while循环语句

4.2.3 for循环语句

4.2.4 嵌套循环

4.3 跳转语句

4.3.1 break语句

4.3.2 continue语句

4.3.3 goto语句

5 数组

5.1 概述

5.2.2 一维数组数组名

5.2.3 冒泡排序

5.3 二维数组

5.3.1 二维数组定义方式

5.3.2 二维数组数组名

5.3.3 二维数组应用案例

6 函数

6.1 概述

6.2 函数的定义

6.3 函数的调用

6.4 值传递

6.5 函数的常见样式

6.6 函数的声明

6.7 函数的分文件编写

7 指针

7.1 指针的基本概念

7.2 指针变量的定义和使用

7.3 指针所占内存空间

7.4 空指针和野指针

7.5 const修饰指针

7.6 指针和数组

7.7 指针和函数

7.8 指针、数组、函数

8 结构体

8.1 结构体基本概念

8.2 结构体定义和使用

8.3 结构体数组

8.4 结构体指针

8.5 结构体嵌套结构体

8.6 结构体做函数参数

8.7 结构体中const使用场景

8.8 结构体案例

8.8.1 案例1

8.8.2 案例2


课程链接:黑马c++课程https://www.bilibili.com/video/BV1et411b73Z/?spm_id_from=333.1245.recommend_more_video.0&trackid=web_related_0.router-related-2479604-gjmc5.1782207130274.589&vd_source=b0ff309389fa15ce9ed7b52de0ab9ccb

1 C++初识

1.1 第一个C++程序

编写一个C++程序总共分为4个步骤

* 创建项目
* 创建文件
* 编写代码
* 运行程序

1.1.1 创建项目

​ Visual Studio是我们用来编写C++程序的主要工具,我们先将它打开

1.1.2 创建文件

右键源文件,选择添加->新建项

1.1.3 编写代码

#include<iostream>      // 预处理指令:引入输入输出流头文件,提供cout、cin、endl等标准输入输出功能
using namespace std;     // 使用标准命名空间std,避免每次调用标准库函数时都要加std::前缀

int main() {             // 定义主函数,程序执行的入口点,返回类型为int整型

    cout << "Hello world" << endl;   // 使用cout输出字符串"Hello world"到控制台,<<为流插入运算符
    // endl表示换行并刷新输出缓冲区,相当于输出'\n'并强制刷新

    system("pause");     // 调用系统命令"pause",暂停程序执行并显示"按任意键继续..."提示
    // 仅在Windows操作系统下有效,用于调试时保持控制台窗口打开

    return 0;            // 返回0给操作系统,表示程序正常执行结束(0通常表示成功)
}                        // 主函数结束,程序终止

1.1.4 运行程序

1.2 注释

作用:在代码中加一些说明和解释,方便自己或其他程序员阅读代码

**两种格式**

1. **单行注释**:`// 描述信息`
  * 通常放在一行代码的上方,或者一条语句的末尾,对该行代码说明
2. **多行注释**: /* 描述信息 */
  * 通常放在一段代码的上方,对该段代码做整体说明

提示:编译器在编译代码时,会忽略注释的内容

visual studio快捷键:ctrl+shift+/键 可以一键注释

1.3 变量

**作用**:给一段指定的内存空间起名,方便操作这段内存

变量存在的意义:方便我们管理内存空间

**语法**:`数据类型 变量名 = 初始值;`

#include<iostream>
using namespace std;

int main() {

    //变量的定义
    //语法:数据类型  变量名 = 初始值

    int a = 10;

    cout << "a = " << a << endl;

    system("pause");

    return 0;
}

1.4 常量

**作用**:用于记录程序中不可更改的数据

C++定义常量两种方式

1. #define 宏常量: #define 常量名 常量值
  
  *通常在文件上方定义,表示一个常量
2. const修饰的变量 const 数据类型 常量名 = 常量值
  
  通常在变量定义前加关键字const,修饰该变量为常量,不可修改

        endl和"\n"都能换行

#include <iostream>
using namespace std;

//常量的定义方式
//1.#define 宏常量
//2.const修饰的变量

//1.#define宏常量
#define day 7

int main() {
	//day = 14;//day是常量,一旦修改就会报错
	cout << "一周总共有:" << day << "天" << endl;
	//2.const 修饰的变量
	const int month = 12;
	// month = 24;//错误,const修饰的变量也称为常量
	cout << "一年总共有:" << month << "个月份" << "\n";

	return 0;

}

1.5 关键字

**作用:**关键字是C++中预先保留的单词(标识符)

* **在定义变量或者常量时候,不要用关键字**

C++关键字如下:

asm do if return typedef
auto double inline short typeid
bool dynamic_cast int signed typename
break else long sizeof union
case enum mutable static unsigned
catch explicit 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

提示:在给变量或者常量起名称时候,不要用C++的关键字,否则会产生歧义。

1.6 标识符命名规则

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

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

> 建议:给标识符命名时,争取做到见名知意的效果,方便自己和他人的阅读

#include <iostream>
using namespace std;
/*
	* 标识符命名规则
	1.标识符不可以是关键字
	2.标识符由字母、数字、下划线构成
	3.第一个字符不能是数字,只能是字母或下划线
	4.标识符区分大小写
*/
int main() {
	
	//1.标识符不可以是关键字
	//int int = 10;

	//2.标识符由字母、数字、下划线构成
	int abc = 10;
	int _abc = 20;
	int _123abc = 30;

	//3.第一个字符不能是数字,只能是字母或下划线
	//int 123abc = 30;
	//4.标识符区分大小写
	int aaa = 100;
	//cout << AAA << endl;///AAA和aaa不是同一个名称

	//建议:给标识符命名时,争取做到见名知意的效果,方便自己和他人的阅读
	int num1 = 10;
	int num2 = 20;
	int sum = num1 + num2;
	cout << sum << endl;


	return 0;

}

2 数据类型

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

2.1 整型

**作用**:整型变量表示的是整数类型的数据

C++中能够表示整型的类型有以下几种方式,区别在于所占内存空间不同:

2.2 sizeof关键字

**作用:**利用sizeof关键字可以统计数据类型所占内存大小(单位为字节)

**语法:sizeof( 数据类型 / 变量)

**整型结论**:short < int <= long <= long long

int main() {

    cout << "short 类型所占内存空间为: " << sizeof(short) << endl;

    cout << "int 类型所占内存空间为: " << sizeof(int) << endl;

    cout << "long 类型所占内存空间为: " << sizeof(long) << endl;

    cout << "long long 类型所占内存空间为: " << sizeof(long long) << endl;

    system("pause");

    return 0;
}

2.3 实型(浮点型)

**作用**:用于表示小数

浮点型变量分为两种:

1. 单精度float 末尾后面加f
2. 双精度double

两者的区别在于表示的有效数字范围不同。

默认情况下 输出一个浮点数,会显示出6位有效数字

代码书写注意(字面量默认规则)

  • 直接写 3.14:编译器默认识别为 double 类型;
  • 3.14f:末尾加 f,代表这是 float 单精度小数。
float a = 3.14f;  // 正确,带f匹配float
double b = 3.14;  // 正确,默认double
int main() {

    float f1 = 3.14f;
    double d1 = 3.14;

    cout << f1 << endl;
    cout << d1<< endl;

    cout << "float  sizeof = " << sizeof(f1) << endl;
    cout << "double sizeof = " << sizeof(d1) << endl;

    //科学计数法
    float f2 = 3e2; // 3 * 10 ^ 2 
    cout << "f2 = " << f2 << endl;

    float f3 = 3e-2;  // 3 * 0.1 ^ 2
    cout << "f3 = " << f3 << endl;

    system("pause");

    return 0;
}

2.4 字符型

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

**语法:**`char ch = 'a';`

> 注意1:在显示字符型变量时,用单引号将字符括起来,不要用双引号

> 注意2:单引号内只能有一个字符,不可以是字符串

* C和C++中字符型变量只占用1个字节。
* 字符型变量并不是把字符本身放到内存中存储,而是将对应的ASCII编码放入到存储单元

int main() {

    char ch = 'a';
    cout << ch << endl;
    cout << sizeof(char) << endl;

    //ch = "abcde"; //错误,不可以用双引号
    //ch = 'abcde'; //错误,单引号内只能引用一个字符

    cout << (int)ch << endl;  //查看字符a对应的ASCII码
    ch = 97; //可以直接用ASCII给字符型变量赋值
    cout << ch << endl;

    system("pause");

    return 0;
}
ASCII码表格:
ASCII 值 控制字符 ASCII 值 字符 ASCII 值 字符 ASCII 值 字符
0 NUL 32 (空格) 64 @ 96 `
1 SOH 33 ! 65 A 97 a
2 STX 34 " 66 B 98 b
3 ETX 35 # 67 C 99 c
4 EOT 36 $ 68 D 100 d
5 ENQ 37 % 69 E 101 e
6 ACK 38 & 70 F 102 f
7 BEL 39 ' 71 G 103 g
8 BS 40 ( 72 H 104 h
9 HT 41 ) 73 I 105 i
10 LF (换行) 42 * 74 J 106 j
11 VT 43 + 75 K 107 k
12 FF 44 , 76 L 108 l
13 CR (回车) 45 - 77 M 109 m
14 SO 46 . 78 N 110 n
15 SI 47 / 79 O 111 o
16 DLE 48 0 80 P 112 p
17 DC1 49 1 81 Q 113 q
18 DC2 50 2 82 R 114 r
19 DC3 51 3 83 S 115 s
20 DC4 52 4 84 T 116 t
21 NAK 53 5 85 U 117 u
22 SYN 54 6 86 V 118 v
23 ETB 55 7 87 W 119 w
24 CAN 56 8 88 X 120 x
25 EM 57 9 89 Y 121 y
26 SUB 58 : 90 Z 122 z
27 ESC 59 ; 91 [ 123 {
28 FS 60 < 92 \ 124 |
29 GS 61 = 93 ] 125 }
30 RS 62 > 94 ^ 126 ~
31 US 63 ? 95 _ 127 DEL

ASCII 码大致由以下两部分组成:

* ASCII 非打印控制字符: ASCII 表上的数字 **0-31** 分配给了控制字符,用于控制向打印机等一些外围设备。
* ASCII 打印字符:数字 **32-126** 分配给了能在键盘上找到的字符,当查看或打印文档时就会出现。

C++ 必记核心规律

  1. 大写字母 A (65)~Z (90)

  2. 小写字母 a (97)~z (122),同一字母小写比大写大 32

  3. 数字 0 (48)~9 (57),数字字符 ASCII 值 = 数字 + 48

  4. 32 是空格,0~31、127 为不可打印控制字符

2.5 转义字符

**作用:**用于表示一些不能显示出来的ASCII字符

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

\n换行、\t制表对齐、\\打印反斜杠、\"和  \'输出引号、\0字符串结束符,写代码最常用。

转义字符 含义 ASCII 码值 (十进制)
\a 警报,触发蜂鸣提示 007
\b 退格 BS,光标前移一列 008
\f 换页 FF,光标移至下页开头 012
\n 换行 LF,光标移至下一行开头 010
\r 回车 CR,光标移至本行开头 013
\t 水平制表 HT,跳到下一个 Tab 对齐位 009
\v 垂直制表 VT 011
\\ 输出一个反斜杠字符 \ 092
\' 输出单引号(撇号)字符 039
\" 输出双引号字符 034
\? 输出问号字符 063
\0 空字符(字符串结束标记) 000
\ddd 八进制转义,ddd 为 3 位 0~7 的数字 对应 3 位八进制数值
\xhh 十六进制转义,hh 为 2 位 0~9、a~f、A~F 对应 2 位十六进制数值

易错提醒:打印文件路径时必须写双反斜杠 \\,只写单个\会被识别为转义字符,编译报错。

//水平制表符 \t 作用可以整齐输出数据

2.6 字符串型

**作用**:用于表示一串字符

注意:C++风格字符串,需要加入头文件#include<string>

**两种风格**

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

int main() {

    char str1[] = "hello world";
    cout << str1 << endl;

    system("pause");

    return 0;
}

> 注意:C风格的字符串要用双引号括起来

1. **C++风格字符串**: `string 变量名 = "字符串值"`

int main() {

    string str = "hello world";
    cout << str << endl;

    system("pause");

    return 0;
}
#include <iostream>
#include <string>//用c++风格字符串的时候要包含这个头文件
using namespace std;
int main() {

	//1.c风格字符串
	//注意事项 char 字符串名[]
	//注意事项2 等号后面要用双引号 包含起来字符串
	char str[] = "hello world";
	cout << str << endl;
	//2.c++风格字符串
	string str2 = "hello world";
	cout << str2 << endl;
	
	return 0;
}

2.7 布尔类型 bool

**作用:**布尔数据类型代表真或假的值

bool类型只有两个值:

* true --- 真(本质是1)
* false --- 假(本质是0)

**bool类型占1个字节大小

int main() {

    bool flag = true;
    cout << flag << endl; // 1

    flag = false;
    cout << flag << endl; // 0

    cout << "size of bool = " << sizeof(bool) << endl; //1

    system("pause");

    return 0;
}

2.8 数据的输入

**作用:用于从键盘获取数据**

**关键字:**cin

**语法:** `cin >> 变量 `

#include <iostream>     // ① 包含输入输出流头文件,提供 cout、cin、endl 等工具
#include <string>       // ② 包含字符串头文件,提供 string 类型支持
using namespace std;    // ③ 使用标准命名空间 std

int main() {            // ④ 程序入口函数

    // ============================================================
    // ★ 1. 整型输入
    // ============================================================
    int a = 0;          // ⑤ 定义一个整型变量 a,初始化为 0
    cout << "请输入整型变量:" << endl;   // ⑥ 输出提示信息
    cin >> a;           // ⑦ 从键盘读取一个整数,存入变量 a
    cout << a << endl;  // ⑧ 输出变量 a 的值

    // ============================================================
    // ★ 2. 浮点型输入
    // ============================================================
    double d = 0;       // ⑨ 定义一个双精度浮点变量 d,初始化为 0
    cout << "请输入浮点型变量:" << endl;
    cin >> d;           // ⑩ 从键盘读取一个浮点数,存入变量 d
    cout << d << endl;

    // ============================================================
    // ★ 3. 字符型输入
    // ============================================================
    char ch = 0;        // ⑪ 定义一个字符变量 ch,初始化为 0(空字符)
    cout << "请输入字符型变量:" << endl;
    cin >> ch;          // ⑫ 从键盘读取一个字符,存入变量 ch
    cout << ch << endl; // ⑬ 输出字符(注意:只能读取一个字符)

    // ============================================================
    // ★ 4. 字符串型输入
    // ============================================================
    string str;         // ⑭ 定义一个字符串变量 str(默认空字符串)
    cout << "请输入字符串型变量:" << endl;
    cin >> str;         // ⑮ ★ 从键盘读取一个字符串,存入 str
                        //    注意:cin >> str 只能读取到空格前
                        //    例如输入 "hello world",只会读取 "hello"
    cout << str << endl;

    // ============================================================
    // ★ 5. 布尔类型输入
    // ============================================================
    bool flag = true;   // ⑯ 定义一个布尔变量 flag,初始化为 true
    cout << "请输入布尔型变量:" << endl;
    cin >> flag;        // ⑰ ★ 从键盘读取一个整数(0 或 非 0)
                        //    输入 0 → false
                        //    输入 非 0(如 1、5、-3)→ true
    cout << flag << endl; // ⑱ 输出布尔值(true 输出 1,false 输出 0)

    system("pause");    // ⑲ 暂停控制台窗口
    return 0;           // ⑳ 程序正常退出(EXIT_SUCCESS 等同于 0)
}

3 运算符

**作用:**用于执行代码的运算

本章我们主要讲解以下几类运算符:

3.1 算术运算符

**作用**:用于处理四则运算

算术运算符包括以下符号:

总结:在除法运算中,除数不能为0

//加减乘除
int main() {

    int a1 = 10;
    int b1 = 3;

    cout << a1 + b1 << endl;
    cout << a1 - b1 << endl;
    cout << a1 * b1 << endl;
    cout << a1 / b1 << endl;  //两个整数相除结果依然是整数

    int a2 = 10;
    int b2 = 20;
    cout << a2 / b2 << endl; 

    int a3 = 10;
    int b3 = 0;
    //cout << a3 / b3 << endl; //报错,除数不可以为0


    //两个小数可以相除
    double d1 = 0.5;
    double d2 = 0.25;
    cout << d1 / d2 << endl;

    system("pause");

    return 0;
}
//取模
int main() {
    //取模运算的本质就是求余数

    int a1 = 10;
    int b1 = 3;

    cout << 10 % 3 << endl;

    int a2 = 10;
    int b2 = 20;

    cout << a2 % b2 << endl;

    int a3 = 10;
    int b3 = 0;

    //cout << a3 % b3 << endl; //取模运算时,除数也不能为0

    //两个小数不可以取模
    double d1 = 3.14;
    double d2 = 1.1;

    //cout << d1 % d2 << endl;

    system("pause");

    return 0;
}

总结:只有整型变量可以进行取模运算

//递增
int main() {

    //后置递增
    int a = 10;
    a++; //等价于a = a + 1
    cout << a << endl; // 11

    //前置递增
    int b = 10;
    ++b;
    cout << b << endl; // 11

    //区别
    //前置递增先对变量进行++,再计算表达式
    int a2 = 10;
    int b2 = ++a2 * 10;
    cout << b2 << endl;

    //后置递增先计算表达式,后对变量进行++
    int a3 = 10;
    int b3 = a3++ * 10;
    cout << b3 << endl;

    system("pause");

    return 0;
}
#include <iostream>     // ① 包含输入输出流头文件
using namespace std;    // ② 使用标准命名空间

// ============================================================
// ★ 递增运算符(++)的两种形式
// ============================================================
int main() {

    // ============================================================
    // ③ 后置递增:先使用,后自增
    // ============================================================
    int a = 10;         // ④ 定义变量 a,初始值为 10
    a++;                // ⑤ ★ 后置递增,等价于 a = a + 1
    cout << a << endl;  // ⑥ 输出 11

    // ============================================================
    // ⑦ 前置递增:先自增,后使用
    // ============================================================
    int b = 10;         // ⑧ 定义变量 b,初始值为 10
    ++b;                // ⑨ ★ 前置递增,等价于 b = b + 1
    cout << b << endl;  // ⑩ 输出 11

    // ============================================================
    // ★ 区别演示(核心)
    // ============================================================

    // ============================================================
    // ⑪ 前置递增:先自增,再计算表达式
    // ============================================================
    int a2 = 10;                // ⑫ 定义 a2 = 10
    int b2 = ++a2 * 10;         // ⑬ ★ 执行顺序:
                                //     ① a2 先自增 → a2 = 11
                                //     ② 计算 11 * 10 = 110
                                //     ③ 将 110 赋给 b2
    cout << b2 << endl;         // ⑭ 输出 110

    // ============================================================
    // ⑮ 后置递增:先计算表达式,再自增
    // ============================================================
    int a3 = 10;                // ⑯ 定义 a3 = 10
    int b3 = a3++ * 10;         // ⑰ ★ 执行顺序:
                                //     ① 计算 10 * 10 = 100
                                //     ② 将 100 赋给 b3
                                //     ③ a3 自增 → a3 = 11
    cout << b3 << endl;         // ⑱ 输出 100

    system("pause");    // ⑲ 暂停控制台窗口
    return 0;           // ⑳ 程序正常退出
}

总结:前置递增先对变量进行++,再计算表达式,后置递增相反

3.2 赋值运算符

**作用:**用于将表达式的值赋给变量

赋值运算符包括以下几个符号:

int main() {

    //赋值运算符

    // =
    int a = 10;
    a = 100;
    cout << "a = " << a << endl;

    // +=
    a = 10;
    a += 2; // a = a + 2;
    cout << "a = " << a << endl;

    // -=
    a = 10;
    a -= 2; // a = a - 2
    cout << "a = " << a << endl;

    // *=
    a = 10;
    a *= 2; // a = a * 2
    cout << "a = " << a << endl;

    // /=
    a = 10;
    a /= 2;  // a = a / 2;
    cout << "a = " << a << endl;

    // %=
    a = 10;
    a %= 2;  // a = a % 2;
    cout << "a = " << a << endl;

    system("pause");

    return 0;
}

3.3 比较运算符

**作用:**用于表达式的比较,并返回一个真值或假值

比较运算符有以下符号:

int main() {

    int a = 10;
    int b = 20;

    cout << (a == b) << endl; // 0 

    cout << (a != b) << endl; // 1

    cout << (a > b) << endl; // 0

    cout << (a < b) << endl; // 1

    cout << (a >= b) << endl; // 0

    cout << (a <= b) << endl; // 1

    system("pause");

    return 0;
}

注意:C和C++ 语言的比较运算中, “真”用数字“1”来表示, “假”用数字“0”来表示。

3.4 逻辑运算符

**作用:**用于根据表达式的值返回真值或假值

逻辑运算符有以下符号:

**示例1:**逻辑非

总结: 真变假,假变真

//逻辑运算符  --- 非
int main() {

    int a = 10;

    cout << !a << endl; // 0

    cout << !!a << endl; // 1

    system("pause");

    return 0;
}

**示例2:**逻辑与

总结:逻辑与运算符总结: 同真为真,其余为假

//逻辑运算符  --- 与
int main() {

    int a = 10;
    int b = 10;

    cout << (a && b) << endl;// 1

    a = 10;
    b = 0;

    cout << (a && b) << endl;// 0 

    a = 0;
    b = 0;

    cout << (a && b) << endl;// 0

    system("pause");

    return 0;
}
#include <iostream>
using namespace std;

int main() {
    int a = 10, b = 20;

    // ✅ 正确写法:加括号
    cout << (a && b) << endl;   // 输出 1(因为 10 && 20 为 true)

    // ❌ 错误写法:不加括号
    // cout << a && b << endl;  // 编译报错!

    return 0;
}

<< 的优先级高于 &&

示例3:**逻辑或

逻辑或运算符总结: 同假为假,其余为真

//逻辑运算符  --- 或
int main() {

    int a = 10;
    int b = 10;

    cout << (a || b) << endl;// 1

    a = 10;
    b = 0;

    cout << (a || b) << endl;// 1 

    a = 0;
    b = 0;

    cout << (a || b) << endl;// 0

    system("pause");

    return 0;
}

4 程序流程结构

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

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

4.1 选择结构

4.1.1 if语句

**作用:**执行满足条件的语句

if语句的三种形式

* 单行格式if语句
* 多行格式if语句
* 多条件的if语句​

1. 单行格式if语句:`if(条件){ 条件满足执行的语句 }`

注意:if条件表达式后不要加分号

int main() {

    //选择结构-单行if语句
    //输入一个分数,如果分数大于600分,视为考上一本大学,并在屏幕上打印

    int score = 0;
    cout << "请输入一个分数:" << endl;
    cin >> score;

    cout << "您输入的分数为: " << score << endl;

    //if语句
    //注意事项,在if判断语句后面,不要加分号
    if (score > 600)
    {
        cout << "我考上了一本大学!!!" << endl;
    }

    system("pause");

    return 0;
}

2.多行格式if语句:

`if(条件){ 条件满足执行的语句 }

else{ 条件不满足执行的语句 };`

int main() {

    int score = 0;

    cout << "请输入考试分数:" << endl;

    cin >> score;

    if (score > 600)
    {
        cout << "我考上了一本大学" << endl;
    }
    else
    {
        cout << "我未考上一本大学" << endl;
    }

    system("pause");

    return 0;
}

3.多条件的if语句:`if(条件1){ 条件1满足执行的语句 }else if(条件2){条件2满足执行的语句}... else{ 都不满足执行的语句}`

int main() {

    int score = 0;

    cout << "请输入考试分数:" << endl;

    cin >> score;

    if (score > 600)
    {
        cout << "我考上了一本大学" << endl;
    }
    else if (score > 500)
    {
        cout << "我考上了二本大学" << endl;
    }
    else if (score > 400)
    {
        cout << "我考上了三本大学" << endl;
    }
    else
    {
        cout << "我未考上本科" << endl;
    }

    system("pause");

    return 0;
}

**嵌套if语句**:在if语句中,可以嵌套使用if语句,达到更精确的条件判断

案例需求:

* 提示用户输入一个高考考试分数,根据分数做如下判断
* 分数如果大于600分视为考上一本,大于500分考上二本,大于400考上三本,其余视为未考上本科;
* 在一本分数中,如果大于700分,考入北大,大于650分,考入清华,大于600考入人大。

int main() {

    int score = 0;

    cout << "请输入考试分数:" << endl;

    cin >> score;

    if (score > 600)
    {
        cout << "我考上了一本大学" << endl;
        if (score > 700)
        {
            cout << "我考上了北大" << endl;
        }
        else if (score > 650)
        {
            cout << "我考上了清华" << endl;
        }
        else
        {
            cout << "我考上了人大" << endl;
        }

    }
    else if (score > 500)
    {
        cout << "我考上了二本大学" << endl;
    }
    else if (score > 400)
    {
        cout << "我考上了三本大学" << endl;
    }
    else
    {
        cout << "我未考上本科" << endl;
    }

    system("pause");

    return 0;
}

**练习案例:** 三只小猪称体重

有三只小猪ABC,请分别输入三只小猪的体重,并且判断哪只小猪最重?

#include <iostream>
#include <string>//用c++风格字符串的时候要包含这个头文件
using namespace std;
int main() {

	//三只小猪称体重,判断哪只最重
	//1.创建三只小猪的体重变量
	int num1 = 0;
	int num2 = 0;
	int num3 = 0;
	//让用户输入三只小猪的重量
	cout << "请输入小猪A的体重" << endl;
	cin >> num1;
	cout << "请输入小猪B的体重" << endl;
	cin >> num2;
	cout << "请输入小猪C的体重" << endl;
	cin >> num3;
	cout << "小猪A的体重为:" << num1 << endl;
	cout << "小猪B的体重为:" << num2 << endl;
	cout << "小猪C的体重为:" << num3 << endl;

	//判断哪只最重
	//先判断A和B重量
	if (num1 > num2)//A比B重
	{
		if (num1 > num3)//A比C重
		{
			cout << "小猪A最重" << endl;
		}
		else//C比A重
		{
			cout << "小猪C最重" << endl;
		}
	}
	else//B比A重
	{
		if (num2 > num3)//B比C重
		{
			cout << "小猪B最重" << endl;

		}
		else {//C比B重
			cout << "小猪C最重" << endl;
		}
	}

	system("pause");
	return 0;
}

4.1.2 三目运算符

**作用:** 通过三目运算符实现简单的判断

**语法:**`表达式1 ? 表达式2 :表达式3`

**解释:**

如果表达式1的值为真,执行表达式2,并返回表达式2的结果;

如果表达式1的值为假,执行表达式3,并返回表达式3的结果。

**示例:

int main() {

    int a = 10;
    int b = 20;
    int c = 0;

    c = a > b ? a : b;
    cout << "c = " << c << endl;

    //C++中三目运算符返回的是变量,可以继续赋值

    (a > b ? a : b) = 100;

    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl;

    system("pause");

    return 0;
}

总结:和if语句比较,三目运算符优点是短小整洁,缺点是如果用嵌套,结构不清晰

4.1.3 switch语句

**作用:**执行多条件分支语句

**语法:

switch(表达式)

{

    case 结果1:执行语句;break;

    case 结果2:执行语句;break;

    ...

    default:执行语句;break;

}

示例:

int main() {

    //请给电影评分 
    //10 ~ 9   经典   
    // 8 ~ 7   非常好
    // 6 ~ 5   一般
    // 5分以下 烂片

    int score = 0;
    cout << "请给电影打分" << endl;
    cin >> score;

    switch (score)
    {
    case 10:
    case 9:
        cout << "经典" << endl;
        break;
    case 8:
        cout << "非常好" << endl;
        break;
    case 7:
    case 6:
        cout << "一般" << endl;
        break;
    default:
        cout << "烂片" << endl;
        break;
    }

    system("pause");

    return 0;
}

> 注意1:switch语句中表达式类型只能是整型或者字符型

> 注意2:case里如果没有break,那么程序会一直向下执行

> 总结:与if语句比,对于多条件判断时,switch的结构清晰,执行效率高,缺点是switch不可以判断区间

4.2 循环结构

4.2.1 while循环语句

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

**语法:while(循环条件){ 循环语句 }

**解释:只要循环条件的结果为真,就执行循环语句

int main() {

    int num = 0;
    while (num < 10)
    {
        cout << "num = " << num << endl;
        num++;
    }

    system("pause");

    return 0;
}

注意:在执行循环语句时候,程序必须提供跳出循环的出口,否则出现死循环

while循环练习案例:猜数字

**案例描述:**系统随机生成一个1到100之间的数字,玩家进行猜测,如果猜错,提示玩家数字过大或过小,如果猜对恭喜玩家胜利,并且退出游戏。

#include <iostream>
#include <string>//用c++风格字符串的时候要包含这个头文件
//time系统时间头文件包含
#include <ctime>
using namespace std;
int main() {

	//添加随机数种子,利用当前系统时间生成随机数,防止每次随机数都一样
	srand((unsigned int)time(0));

	//1.系统生成随机数
	int num = rand() % 100 + 1;//rand()%100+1生成1-100的随机数
	//cout << num << endl;
	
	//2.玩家进行猜测
	int val = 0;

	while (1) {
		cin >> val;

		//3.判断玩家的猜测
		
		//猜错 提示猜测的结果过大还是过小,返回第二步
		if (val > num)
		{
			cout << "猜测过大,请重新猜测" << endl;

		}
		else if(val<num){
			cout << "猜测过小,请重新猜测" << endl;
		}
		else {
			cout << "恭喜您猜对了" << endl;
			//猜对 退出游戏
			break;//break,可以利用该关键字来退出当前循环
		}
	}
	system("pause");
	return 0;
}

源代码: main.cpp
    │
    ▼ ① 预处理(#include 展开)
将 <ctime> 的内容复制到 main.cpp
    │
    ▼ ② 编译
编译器看到 time_t、time() 的声明,知道这是合法的
生成目标文件 main.obj(包含调用 time() 的指令,但不知道具体地址)
    │
    ▼ ③ 链接
链接器在 C++ 运行时库中找到 time() 的实现代码
将调用指令指向库中 time() 函数的实际地址
    │
    ▼
生成可执行文件 main.exe

<ctime> 头文件

// ctime standard header (core)
// 文件说明:这是 C++ 标准库中的 <ctime> 头文件(核心部分)
// 功能:提供 C++ 对 C 语言时间库的封装

// Copyright (c) Microsoft Corporation.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
// 版权信息:微软公司版权所有,使用 Apache-2.0 许可证

#ifndef _CTIME_
// ① 头文件守卫:如果 _CTIME_ 未被定义,则继续编译
#define _CTIME_
// ② 定义 _CTIME_,防止重复包含

#include <yvals_core.h>
// ③ 包含 MSVC 编译器的核心配置头文件
//    定义了 _STL_COMPILER_PREPROCESSOR、_STD_BEGIN 等宏

#if _STL_COMPILER_PREPROCESSOR
// ④ 如果编译器支持标准预处理,则编译以下内容

#include <time.h>
// ⑤ ★ 关键!包含 C 语言的 <time.h> 头文件
//    这是所有时间函数(time()、clock() 等)真正的声明来源

#pragma pack(push, _CRT_PACKING)
// ⑥ 保存当前内存对齐方式,设置为 CRT 指定的对齐

#pragma warning(push, _STL_WARNING_LEVEL)
// ⑦ 保存当前警告级别,设置 STL 使用的警告级别

#pragma warning(disable : _STL_DISABLED_WARNINGS)
// ⑧ 禁用 STL 中不需要的警告(如某些类型转换警告)

_STL_DISABLE_CLANG_WARNINGS
// ⑨ 禁用 Clang 编译器的某些特定警告

#pragma push_macro("new")
// ⑩ 保存当前 "new" 宏的定义(如果有)

#undef new
// ⑪ 取消 new 的宏定义,防止用户定义的 new 宏干扰标准库

_STD_BEGIN
// ⑫ 宏展开为:namespace std {
//    进入 C++ 标准命名空间 std

// ============================================================
// ★ 将 C 语言的类型导入 std 命名空间
// ============================================================

_EXPORT_STD using _CSTD clock_t;
// ⑬ 将 clock_t 类型(C 语言的时钟类型)导入 std 命名空间
//    之后可以使用 std::clock_t

_EXPORT_STD using _CSTD size_t;
// ⑭ 将 size_t 类型(C 语言的无符号整数)导入 std 命名空间

_EXPORT_STD using _CSTD time_t;
// ⑮ 将 time_t 类型(C 语言的时间类型)导入 std 命名空间

_EXPORT_STD using _CSTD tm;
// ⑯ 将 tm 结构体(C 语言的日期时间结构体)导入 std 命名空间

_EXPORT_STD using _CSTD asctime;
// ⑰ 将 asctime 函数声明导入 std 命名空间
//    将 tm 结构体转换为字符串

_EXPORT_STD using _CSTD clock;
// ⑱ 将 clock 函数声明导入 std 命名空间
//    获取程序运行的 CPU 时钟数

_EXPORT_STD using _CSTD strftime;
// ⑲ 将 strftime 函数声明导入 std 命名空间
//    将 tm 结构体格式化为自定义字符串

#if _HAS_CXX17
// ⑳ 如果支持 C++17 标准

_EXPORT_STD using _CSTD timespec;
// ㉑ 将 timespec 结构体(C11 的时间类型)导入 std 命名空间

#endif // _HAS_CXX17

#ifdef _BUILD_STD_MODULE
// ㉒ 如果正在构建 C++20 模块(特定于 MSVC 的构建配置)

_STL_DISABLE_DEPRECATED_WARNING
// ㉓ 禁用废弃 API 的警告

// ============================================================
// ★ 以下是对 C 语言时间函数的封装(Wrapper)
//    原因是 MSVC 内部使用了 64 位时间版本
// ============================================================

_EXPORT_STD
_Check_return_ _CRT_INSECURE_DEPRECATE(ctime_s) inline char* __CRTDECL ctime(_In_ const time_t* const _Time) noexcept
{
    return _CSTD _ctime64(_Time);
}
// ㉔ ★ ctime 函数封装:
//     参数:指向 time_t 的指针(只读)
//     返回值:字符串
//     内部调用 _ctime64(MSVC 的 64 位时间版本)
//     _CRT_INSECURE_DEPRECATE 提示使用更安全的 ctime_s

_EXPORT_STD
_Check_return_ inline double __CRTDECL difftime(_In_ const time_t _Time1, _In_ const time_t _Time2) noexcept
{
    return _CSTD _difftime64(_Time1, _Time2);
}
// ㉕ ★ difftime 函数封装:
//     计算两个时间的差值(秒)
//     内部调用 _difftime64

_EXPORT_STD
_Check_return_ _CRT_INSECURE_DEPRECATE(gmtime_s) inline tm* __CRTDECL gmtime(_In_ const time_t* const _Time) noexcept
{
    return _CSTD _gmtime64(_Time);
}
// ㉖ ★ gmtime 函数封装:
//     将时间转换为 UTC 时间(格林威治标准时间)
//     内部调用 _gmtime64

_EXPORT_STD
_CRT_INSECURE_DEPRECATE(localtime_s)
inline tm* __CRTDECL localtime(_In_ const time_t* const _Time) noexcept
{
    return _CSTD _localtime64(_Time);
}
// ㉗ ★ localtime 函数封装:
//     将时间转换为本地时间
//     内部调用 _localtime64

_EXPORT_STD
_Check_return_opt_ inline time_t __CRTDECL mktime(_Inout_ tm* const _Tm) noexcept
{
    return _CSTD _mktime64(_Tm);
}
// ㉘ ★ mktime 函数封装:
//     将本地 tm 结构体转换为 time_t 时间戳
//     内部调用 _mktime64

_EXPORT_STD
inline time_t __CRTDECL time(_Out_opt_ time_t* const _Time) noexcept
{
    return _CSTD _time64(_Time);
}
// ㉙ ★ time 函数封装:
//     获取当前时间(自 1970-01-01 起的秒数)
//     内部调用 _time64

_EXPORT_STD
_Check_return_ inline int __CRTDECL timespec_get(_Out_ timespec* const _Ts, _In_ const int _Base) noexcept
{
    return _CSTD _timespec64_get(reinterpret_cast<_timespec64*>(_Ts), _Base);
}
// ㉚ ★ timespec_get 函数封装(C11/C++17):
//     获取高精度时间
//     内部调用 _timespec64_get

_STL_RESTORE_DEPRECATED_WARNING
// ㉛ 恢复废弃 API 的警告

#else // ^^^ workaround / no workaround vvv

// ㉜ ★ 对于普通构建(非模块构建)
//     直接使用 C 语言版本,不需要封装

_EXPORT_STD using _CSTD ctime;
_EXPORT_STD using _CSTD difftime;
_EXPORT_STD using _CSTD gmtime;
_EXPORT_STD using _CSTD localtime;
_EXPORT_STD using _CSTD mktime;
_EXPORT_STD using _CSTD time;
// ㉝ 将 C 语言的这些函数直接导入 std 命名空间

#if _HAS_CXX17
_EXPORT_STD using _CSTD timespec_get;
// ㉞ 如果支持 C++17,也导入 timespec_get 函数
#endif // _HAS_CXX17

#endif // ^^^ no workaround ^^^

_STD_END
// ㉟ 宏展开为:} 结束 std 命名空间

#pragma pop_macro("new")
// ㊱ 恢复之前保存的 new 宏定义

_STL_RESTORE_CLANG_WARNINGS
// ㊲ 恢复 Clang 编译器的警告设置

#pragma warning(pop)
// ㊳ 恢复之前的警告级别

#pragma pack(pop)
// ㊴ 恢复之前的对齐方式

#endif // _STL_COMPILER_PREPROCESSOR

#endif // _CTIME_
// ㊵ 结束头文件守卫

<ctime> 头文件只包含时间函数的声明(函数原型),真正的实现代码在 C++ 运行时库中。包含头文件后,编译器知道函数的“样子”,链接器会找到“实现”,所以你就可以调用这些函数了。

4.2.2 do...while循环语句

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

**语法:** `do{ 循环语句 } while(循环条件);`

**注意:**与while的区别在于do...while会先执行一次循环语句,再判断循环条件

int main() {

    int num = 0;

    do
    {
        cout << num << endl;
        num++;

    } while (num < 10);


    system("pause");

    return 0;
}

**练习案例:水仙花数**

**案例描述:**水仙花数是指一个 3 位数,它的每个位上的数字的3次幂之和等于它本身

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

请利用do...while语句,求出所有3位数中的水仙花数

#include <iostream>
#include <cstring>
#include <bits/stdc++.h>
using namespace std;
int main() {
	//1.先打印所有三位数

	int num = 100;

	do {
		//2.从所有三位数字中找到水仙花数
		int a = 0;//个位
		int b = 0;//十位
		int c = 0;//百位
		a = num % 10;
		b = num /10 % 10;
		c = num / 100 % 10;
		if (a * a * a + b * b * b + c * c * c == num) {
			cout << num << endl;
		}
		num++;

	} while (num <= 999);
	
	system("pause");
	return 0;
}

4.2.3 for循环语句

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

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

int main() {

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

    system("pause");

    return 0;
}

> 注意:for循环中的表达式,要用分号进行分隔

> 总结:while , do...while, for都是开发中常用的循环语句,for循环结构比较清晰,比较常用

**练习案例:敲桌子**

案例描述:从1开始数到数字100, 如果数字个位含有7,或者数字十位含有7,或者该数字是7的倍数,我们打印敲桌子,其余数字直接打印输出。

#include <iostream>
#include <cstring>
#include <bits/stdc++.h>
using namespace std;
int main() {
	int num = 0;
	for (int i = 1; i <= 100; i++)
	{
		num = i;
		if (num % 10 == 7 || num / 10 % 10 == 7 || num % 7 == 0) {
			cout << "敲桌子" << endl;
		}
		else {
			cout << num << endl;
		}
	}

	
	system("pause");
	return 0;
}
#include <iostream>          // ① 包含输入输出流头文件,提供 cout、endl 等
#include <cstring>           // ② 包含 C 风格字符串函数(本代码未使用,可省略)
#include <bits/stdc++.h>     // ③ ⚠️ 这是一个非标准头文件,包含几乎所有 C++ 标准库
                              //    但会增加编译时间,不推荐实际项目使用
using namespace std;         // ④ 使用标准命名空间 std

int main() {                 // ⑤ 程序入口函数

    int num = 0;             // ⑥ 定义变量 num,用于存储当前数字

    // ============================================================
    // ★ 遍历 1 到 100 的所有数字
    // ============================================================
    for (int i = 1; i <= 100; i++)   // ⑦ i 从 1 到 100
    {
        num = i;             // ⑧ 把当前数字赋值给 num(可以直接用 i,这行可省略)

        // ============================================================
        // ★ 判断是否满足“敲桌子”的条件(逢7过/敲7游戏)
        //    条件:数字中包含 7 或者能被 7 整除
        // ============================================================
        if (num % 10 == 7 ||                    // ⑨ 个位数是 7(如 7, 17, 27...)
            num / 10 % 10 == 7 ||               // ⑩ ★ 十位数是 7(如 70~79)
            num % 7 == 0)                       // ⑪ 能被 7 整除(如 7, 14, 21...)
        {
            cout << "敲桌子" << endl;           // ⑫ 满足任一条件,输出"敲桌子"
        }
        else {
            cout << num << endl;                 // ⑬ 否则直接输出数字
        }
    }

    system("pause");         // ⑭ 暂停控制台窗口
    return 0;                // ⑮ 程序正常退出
}

4.2.4 嵌套循环

**作用:** 在循环体中再嵌套一层循环,解决一些实际问题

例如我们想在屏幕中打印如下图片,就需要利用嵌套循环

int main() {                                // 主函数入口,程序从这里开始执行

    //外层循环执行1次,内层循环执行1轮    
    // 注释说明:外层循环每迭代一次,内层循环完整执行一轮(10次)
    // 外层for循环:控制行数,i从0开始,每次递增1
    //当i<10时继续循环,共循环10次(i=0到9)
    // 外层循环体开始
    for (int i = 0; i < 10; i++)            
    {                                       
    // 内层for循环:控制每行打印的星号数量,j从0开始,每次递增1
    //当j<10时继续循环,共循环10次(j=0到9)
    // 内层循环体开始
        for (int j = 0; j < 10; j++)        
        {                                
            // 输出一个星号和一个空格,不换行(因为没有使用endl),连续打印10个星号   
              cout << "*" << " ";             

        }                                   

        cout << endl;                       

    }                                       

    system("pause");                        

    return 0;                               

}                                           

**练习案例:**乘法口诀表

案例描述:利用嵌套循环,实现九九乘法表

#include <iostream>
#include <cstring>
#include <bits/stdc++.h>
using namespace std;
int main() {
	for (int i = 1; i <= 9; i++)
	{
		for (int j = 1; j <= i; j++) {
			cout << j << "*" << i << "=" << j * i << " " ;
		}
		cout << endl;
	}

	
	system("pause");
	return 0;
}

对比项 for 循环 while 循环
语法结构 for(初始化; 条件; 更新) while(条件) { ... }
适用场景 知道循环次数 只知道条件,不知道次数
初始化变量 循环内部定义 循环外部定义
变量作用域 循环内(局部) 循环外(可能更广)
更新操作 在循环头中 在循环体内
可读性 适合“计数型”循环 适合“条件驱动型”循环
无限循环 for(;;) while(true)

4.3 跳转语句

4.3.1 break语句

**作用:** 用于跳出选择结构或者循环结构

break使用的时机:

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

int main() {
    //1、在switch 语句中使用break
    cout << "请选择您挑战副本的难度:" << endl;
    cout << "1、普通" << endl;
    cout << "2、中等" << endl;
    cout << "3、困难" << endl;

    int num = 0;

    cin >> num;

    switch (num)
    {
    case 1:
        cout << "您选择的是普通难度" << endl;
        break;
    case 2:
        cout << "您选择的是中等难度" << endl;
        break;
    case 3:
        cout << "您选择的是困难难度" << endl;
        break;
    }

    system("pause");

    return 0;
}
int main() {
    //2、在循环语句中用break
    for (int i = 0; i < 10; i++)
    {
        if (i == 5)
        {
            break; //跳出循环语句
        }
        cout << i << endl;
    }

    system("pause");

    return 0;
}
int main() {
    //在嵌套循环语句中使用break,退出内层循环
    for (int i = 0; i < 10; i++)
    {
        for (int j = 0; j < 10; j++)
        {
            if (j == 5)
            {
                break;
            }
            cout << "*" << " ";
        }
        cout << endl;
    }

    system("pause");

    return 0;
}

4.3.2 continue语句

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

int main() {

    for (int i = 0; i < 100; i++)
    {
        if (i % 2 == 0)
        {
            continue;
        }
        cout << i << endl;
    }

    system("pause");

    return 0;
}
#include <iostream>      // ① 包含输入输出流头文件,提供 cout、endl 等工具
using namespace std;     // ② 使用标准命名空间 std

int main() {             // ③ 程序入口函数

    // ============================================================
    // ★ for 循环:遍历 0 到 99
    // ============================================================
    for (int i = 0; i < 100; i++)   // ④ i 从 0 开始,每次 +1,直到 99
    {
        // ============================================================
        // ⑤ ★ 判断 i 是否为偶数
        //    i % 2 == 0 表示 i 能被 2 整除(偶数的余数为 0)
        // ============================================================
        if (i % 2 == 0)   // ⑥ 如果 i 是偶数
        {
            continue;     // ⑦ ★ 跳过本次循环剩余代码,直接进入下一次循环
                          //    即不执行下面的 cout,直接 i++ 进入下一轮
        }

        // ⑧ ★ 只有当 i 是奇数时,才会执行到这里
        cout << i << endl;   // ⑨ 输出奇数
    }

    system("pause");     // ⑩ 暂停控制台窗口
    return 0;            // ⑪ 程序正常退出
}

注意:continue并没有使整个循环终止,而break会跳出循环

4.3.3 goto语句

**作用:**可以无条件跳转语句

**语法:** `goto 标记;`

**解释:**如果标记的名称存在,执行到goto语句时,会跳转到标记的位置

int main() {

    cout << "1" << endl;

    goto FLAG;

    cout << "2" << endl;
    cout << "3" << endl;
    cout << "4" << endl;

    FLAG:

    cout << "5" << endl;

    system("pause");

    return 0;
}
#include <iostream>      // ① 包含输入输出流头文件,提供 cout、endl 等工具
using namespace std;     // ② 使用标准命名空间 std

int main() {             // ③ 程序入口函数

    // ④ 输出 "1"
    cout << "1" << endl;

    // ⑤ ★ goto 语句:跳转到标签 FLAG 所在的位置
    //    执行到这里时,程序会直接跳到 FLAG: 处继续执行
    //    中间的 cout << "2"、cout << "3"、cout << "4" 全部被跳过
    goto FLAG;

    // ⑥ ★ 以下代码被跳过,永远不会执行
    cout << "2" << endl;
    cout << "3" << endl;
    cout << "4" << endl;

    // ⑦ ★ 标签 FLAG:goto 语句跳转的目标位置
    //    标签后面跟一个冒号,表示这是一个跳转目标
FLAG:

    // ⑧ 程序从 FLAG 标签处继续执行
    cout << "5" << endl;

    system("pause");     // ⑨ 暂停控制台窗口
    return 0;            // ⑩ 程序正常退出
}

注意:在程序中不建议使用goto语句,以免造成程序流程混乱

5 数组

5.1 概述

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

**特点1:**数组中的每个数据元素都是相同的数据类型

**特点2:**数组是由连续的内存位置组成的

> 总结1:数组名的命名规范与变量名命名规范一致,不要和变量重名

> 总结2:数组中下标是从0开始索引

int main() {

    //定义方式1
    //数据类型 数组名[元素个数];
    int score[10];

    //利用下标赋值
    score[0] = 100;
    score[1] = 99;
    score[2] = 85;

    //利用下标输出
    cout << score[0] << endl;
    cout << score[1] << endl;
    cout << score[2] << endl;


    //第二种定义方式
    //数据类型 数组名[元素个数] =  {值1,值2 ,值3 ...};
    //如果{}内不足10个数据,剩余数据用0补全
    int score2[10] = { 100, 90,80,70,60,50,40,30,20,10 };

    //逐个输出
    //cout << score2[0] << endl;
    //cout << score2[1] << endl;

    //一个一个输出太麻烦,因此可以利用循环进行输出
    for (int i = 0; i < 10; i++)
    {
        cout << score2[i] << endl;
    }

    //定义方式3
    //数据类型 数组名[] =  {值1,值2 ,值3 ...};
    int score3[] = { 100,90,80,70,60,50,40,30,20,10 };

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

    system("pause");

    return 0;
}
#include <iostream>      // ① 包含输入输出流头文件,提供 cout、endl 等工具
using namespace std;     // ② 使用标准命名空间 std

int main() {             // ③ 程序入口函数

    // ============================================================
    // ★ 方式1:先声明,后赋值
    //    语法:数据类型 数组名[元素个数];
    //    注意:元素个数必须是常量,不能是变量
    // ============================================================
    
    // ④ 声明一个包含 10 个 int 类型元素的数组
    //    此时数组中的值都是随机垃圾值(未初始化)
    int score[10];

    // ⑤ ★ 利用下标给数组元素赋值(下标从 0 开始)
    score[0] = 100;    // ⑥ 第 1 个元素赋值为 100
    score[1] = 99;     // ⑦ 第 2 个元素赋值为 99
    score[2] = 85;     // ⑧ 第 3 个元素赋值为 85
    // 未赋值的元素(score[3] ~ score[9])保留垃圾值

    // ⑨ 利用下标输出数组元素
    cout << score[0] << endl;   // 输出 100
    cout << score[1] << endl;   // 输出 99
    cout << score[2] << endl;   // 输出 85

    // ============================================================
    // ★ 方式2:声明时直接初始化(指定长度)
    //    语法:数据类型 数组名[元素个数] = {值1, 值2, ...};
    //    特点:如果 {} 内的值少于元素个数,剩余元素自动补 0
    // ============================================================

    // ⑩ 声明并初始化数组,有 10 个元素
    int score2[10] = { 100, 90, 80, 70, 60, 50, 40, 30, 20, 10 };

    // ⑪ ★ 利用 for 循环遍历数组
    //    下标从 0 开始,到 9 结束(i < 10)
    for (int i = 0; i < 10; i++)
    {
        cout << score2[i] << endl;   // ⑫ 输出第 i 个元素
    }

    // ============================================================
    // ★ 方式3:声明时直接初始化(省略长度)
    //    语法:数据类型 数组名[] = {值1, 值2, ...};
    //    特点:编译器自动根据 {} 中的元素个数确定数组长度
    // ============================================================

    // ⑬ 声明并初始化数组,长度由 {} 中的元素个数自动确定(10 个)
    int score3[] = { 100, 90, 80, 70, 60, 50, 40, 30, 20, 10 };

    // ⑭ 遍历输出 score3 数组
    for (int i = 0; i < 10; i++)
    {
        cout << score3[i] << endl;   // ⑮ 输出第 i 个元素
    }

    system("pause");     // ⑯ 暂停控制台窗口
    return 0;            // ⑰ 程序正常退出
}

5.2.2 一维数组数组名

一维数组名称的用途:

1. 可以统计整个数组在内存中的长度
2. 可以获取数组在内存中的首地址

int main() {

    //数组名用途
    //1、可以获取整个数组占用内存空间大小
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };

    cout << "整个数组所占内存空间为: " << sizeof(arr) << endl;
    cout << "每个元素所占内存空间为: " << sizeof(arr[0]) << endl;
    cout << "数组的元素个数为: " << sizeof(arr) / sizeof(arr[0]) << endl;

    //2、可以通过数组名获取到数组首地址
    cout << "数组首地址为: " << (int)arr << endl;
    cout << "数组中第一个元素地址为: " << (int)&arr[0] << endl;
    cout << "数组中第二个元素地址为: " << (int)&arr[1] << endl;

    //arr = 100; 错误,数组名是常量,因此不可以赋值


    system("pause");

    return 0;
}
#include <iostream>      // ① 包含输入输出流头文件,提供 cout、endl 等工具
using namespace std;     // ② 使用标准命名空间 std

int main() {             // ③ 程序入口函数

    // ④ 定义一个包含 10 个整数的数组并初始化
    int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

    // ============================================================
    // ★ 用途1:获取整个数组占用的内存大小
    // ============================================================

    // ⑤ sizeof(arr):计算整个数组占用的字节数
    //    10 个 int × 4 字节 = 40 字节
    cout << "整个数组所占内存空间为: " << sizeof(arr) << endl;

    // ⑥ sizeof(arr[0]):计算数组中第一个元素占用的字节数
    //    int 类型占 4 字节
    cout << "每个元素所占内存空间为: " << sizeof(arr[0]) << endl;

    // ⑦ ★ 计算数组元素个数:总字节数 ÷ 单个元素字节数
    //    40 ÷ 4 = 10
    cout << "数组的元素个数为: " << sizeof(arr) / sizeof(arr[0]) << endl;

    // ============================================================
    // ★ 用途2:获取数组首地址
    // ============================================================

    // ⑧ ★ 数组名 arr 代表数组的首地址
    //     (int)arr 将地址值强制转换为整数,方便查看
    cout << "数组首地址为: " << (int)arr << endl;

    // ⑨ ★ 数组第一个元素的地址:&arr[0]
    //     和数组首地址相同
    cout << "数组中第一个元素地址为: " << (int)&arr[0] << endl;

    // ⑩ 数组中第二个元素的地址:&arr[1]
    //     比第一个元素地址大 4 字节(一个 int 的大小)
    cout << "数组中第二个元素地址为: " << (int)&arr[1] << endl;

    // ⑪ ★ 数组名是常量,不能作为左值被赋值
    // arr = 100;  // ❌ 错误!数组名是地址常量,不能修改

    system("pause");     // ⑫ 暂停控制台窗口
    return 0;            // ⑬ 程序正常退出
}

> 注意:数组名是常量,不可以赋值

> 总结1:直接打印数组名,可以查看数组所占内存的首地址

> 总结2:对数组名进行sizeof,可以获取整个数组占内存空间的大小

指针类型 示例 指向内容 +1 跳过的字节数
指向首元素 int* p = arr; 数组第 0 个元素 4 字节(1 个 int)
指向整个数组 int (*p)[10] = &arr; 整个数组(10 个元素) 40 字节(整个数组)

两者存储的地址值相同,但类型不同,所以指针运算的步长不同。

#include <iostream>
using namespace std;

int main() {
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // ★ 指向首元素(int*)
    int* p1 = arr;          // arr 退化为指向首元素的指针
    cout << "p1 的值: " << p1 << endl;       // 0x1000
    cout << "p1 + 1: " << p1 + 1 << endl;    // 0x1004(跳 4 字节)
    cout << "*p1: " << *p1 << endl;          // 1

    // ★ 指向整个数组(int (*)[10])
    int (*p2)[10] = &arr;   // &arr 取整个数组的地址
    cout << "p2 的值: " << p2 << endl;       // 0x1000(值相同!)
    cout << "p2 + 1: " << p2 + 1 << endl;    // 0x1028(跳 40 字节)
    cout << "**p2: " << **p2 << endl;        // 1

    // ★ 地址值相同,但类型不同
    cout << "p1 == p2: " << (p1 == (int*)p2) << endl; // 1(值相同)

    return 0;
}
内存布局(假设首地址 0x1000):

地址:  0x1000  0x1004  0x1008  0x100C  0x1010  0x1014  0x1018  0x101C  0x1020  0x1024
        ┌───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┐
        │   1   │   2   │   3   │   4   │   5   │   6   │   7   │   8   │   9   │  10   │
        └───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘
        ↑
        │
        └── p1(int*)指向这里 → +1 后跳到 0x1004
        └── p2(int (*)[10])指向这里 → +1 后跳到 0x1028(跳过 40 字节)

 为什么值相同但类型不同?

表达式 类型 说明
arr int* 数组名退化为指向首元素的指针
&arr int (*)[10] 取整个数组的地址(类型包含长度信息)
&arr[0] int* 取首元素的地址(与 arr 类型相同)

三种指针对比

指针类型 写法 指向内容 +1 步长 解引用得到
指向首元素 int* p = arr; 第 0 个元素 4 字节 1(int)
指向整行数组 int (*p)[10] = &arr; 整个数组 40 字节 arr 本身(int*)
指向首元素的地址 int* p = &arr[0]; 第 0 个元素 4 字节 1(int)

数组名在内存中是怎么存储的?

数组名本身是编译时确定的“标签”,它不会被分配独立的内存空间,而是直接代表数组首地址。

int arr[10];

// arr 不是变量,它没有自己的内存空间
// 编译器直接把它替换为地址常量 0x1000

总结

指向数组和指向首元素的指针,值相同(都是首地址),但类型不同。int* 的步长是 4 字节,int (*)[10] 的步长是 40 字节。类型决定了指针能“看到”多大的内存块。

arr 不是指针,它是一个“数组名标签”,不是指针变量。&arr 不是“取指针的地址”,而是“取整个数组对象的地址”。

所以 &arr 的类型是 int (*)[10](指向整个数组的指针),而不是 int**(指向指针的指针)。

arr 是一个数组标签(不是变量) → &arr 取整个数组的地址 → 新指针指向整个数组 → 类型是 int (*)[10]

#include <iostream>
using namespace std;

int main() {
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // ★ 验证 arr 不是指针变量
    cout << sizeof(arr) << endl;   // 40(整个数组大小)
    // 如果 arr 是指针,sizeof(arr) 应该是 8

    // ★ &arr 的类型是 int (*)[10],不是 int**
    int (*p)[10] = &arr;  // ✅ 正确
    // int** pp = &arr;    // ❌ 错误!类型不匹配

    // ★ &arr[0] 才是 int*
    int* q = &arr[0];     // ✅ 正确

    cout << "arr 的地址: " << (int)arr << endl;     // 0x1000
    cout << "&arr 的地址: " << (int)&arr << endl;   // 0x1000(值相同)
    cout << "&arr[0] 的地址: " << (int)&arr[0] << endl; // 0x1000(值相同)

    return 0;
}
┌─────────────────────────────────────────────────────────────────┐
│  arr 是一个数组标签(不是指针变量)                            │
│                                                                 │
│  arr 只是一个"门牌号"(0x1000),不占用独立内存               │
│  &arr 是"取这栋楼的地址",结果是 0x1000                       │
│  类型是 int (*)[10](指向整栋楼的指针)                       │
│                                                                 │
│  对比:                                                         │
│  &arr[0] 是"取第一个房间的门牌号",结果也是 0x1000            │
│  类型是 int*(指向第一个房间的指针)                          │
└─────────────────────────────────────────────────────────────────┘

核心概念对比

表达式 含义 类型 说明
arr 数组名 int* 退化为指向首元素的指针
&arr 取整个数组的地址 int (*)[10] 指向包含 10 个 int 的数组
arr[0] 首元素的值 int 值 1
&arr[0] 取首元素的地址 int* 指向首元素的指针

练习案例1:五只小猪称体重

案例描述:

在一个数组中记录了五只小猪的体重,如:int arr[5] = {300,350,200,400,250};

找出并打印最重的小猪体重。

#include <iostream>
#include <cstring>
#include <bits/stdc++.h>
using namespace std;
int main() {
    
	//1.创建5只小猪体重的数组
	int arr[5] = { 300,350,200,400,250 };
	//2.找到最大值
	int max = 0;
	for (int i = 0; i < 5; i++) {
		//如果访问的数字中的元素比我认定的最大值还要大,更新最大值
		if (arr[i] > max) {
			max = arr[i];
		}
	}
	//3.打印最大值
	cout << max << endl;

	system("pause");
	return 0;
}

**练习案例2:数组元素逆置

**案例描述:**请声明一个5个元素的数组,并且将元素逆置.

(如原数组元素为:1,3,2,5,4;逆置后输出结果为:4,5,2,3,1);

#include <iostream>
#include <cstring>
#include <bits/stdc++.h>
using namespace std;
int main() {
	//实现数组元素逆置
	//1、创建数组
	int arr[5] = { 1, 3, 2, 5, 4 };
	cout << "数组逆置前:" << endl;
	for (int i = 0; i < 5; i++)
	{
		cout << arr[i] << endl;
	}
	//2、实现逆置
	//2.1记录起始下标位置
	//2.2记录结束下标位置
	//2.3起始下标与结束下标的元素互换
	//2.4起始位置++  结束位置--
	//2.5循环执行2.1操作,直到起始位置 >= 结束位置
	int start = 0; //起始下标
	int end = sizeof(arr) / sizeof(arr[0]) - 1; //结束下标

	//实现元素互换
	while (start < end)
	{
		//实现元素互换
		int temp = arr[start];
		arr[start] = arr[end];
		arr[end] = temp;

		//下标更新
		start++;
		end--;
	}
	//3、打印逆置后的数组
	cout << "数组元素逆置后:" << endl;
	for (int i = 0; i < 5; i++)
	{
		cout << arr[i] << endl;
	}
	system("pause");
	return 0;
}

1. 思路(是什么)

        数组逆置的核心思想是“双指针交换法”:用两个指针分别指向数组的头部和尾部,交换它们指向的元素,然后头指针向后移动,尾指针向前移动,直到两个指针相遇或交错。

2. 过程(怎么做)

① 定义两个下标:start = 0 指向第一个元素,end = 长度-1 指向最后一个元素。
② 进入循环,条件是 start < end
③ 每次循环交换 arr[start] 和 arr[end] 的值。
④ 交换后 start++ 向右移动,end-- 向左移动。
⑤ 重复直到 start >= end,此时整个数组已经逆置完成。

3. 代码(核心实现)

int start = 0;
int end = 4;  // 数组长度-1

while (start < end) {
    // 交换
    int temp = arr[start];
    arr[start] = arr[end];
    arr[end] = temp;
    
    // 移动指针
    start++;
    end--;
}

4. 注意点

① 循环条件是 start < end,而不是 <=,因为中间元素不需要和自己交换。
② 数组长度计算用 sizeof(arr) / sizeof(arr[0]),避免硬编码。
③ 交换需要用临时变量保存,防止数据丢失。


 过程图解

原始数组:[1, 3, 2, 5, 4]

第1步:start=0, end=4
交换 arr[0] 和 arr[4] → [4, 3, 2, 5, 1]
start=1, end=3

第2步:start=1, end=3
交换 arr[1] 和 arr[3] → [4, 5, 2, 3, 1]
start=2, end=2

第3步:start=2, end=2
start < end 不成立,循环结束

最终结果:[4, 5, 2, 3, 1]

面试话术模板

        “数组逆置我通常用双指针法实现。定义 start 指向数组头部,end 指向数组尾部,进入 while 循环,只要 start < end,就交换它们指向的元素,然后 start++end--,直到两个指针相遇。这样做的时间复杂度是 O(n),空间复杂度是 O(1),不需要额外数组。”

 扩展对比

方法 时间复杂度 空间复杂度 说明
双指针法 O(n) O(1) 原地交换,效率最高 ✅
新建数组法 O(n) O(n) 需要额外空间,不推荐
递归法 O(n) O(n) 递归调用有栈开销

Q:如果数组很大,这个算法有什么问题?

“没有额外内存开销,是原地逆置,适合处理大规模数据。但如果数组非常大且需要频繁逆置,可以考虑用双向链表或 std::reverse 算法,它们底层实现更高效。”

Q:如果元素是自定义类型(如 string),交换会有问题吗?

“不会有问题,交换逻辑同样适用。但如果元素很大,频繁交换会有拷贝开销,可以考虑使用移动语义(std::move)来优化。”

Q:能否用 STL 实现?

“可以用 std::reverse(arr, arr + 5),它内部就是用双指针实现的,比自己手写更安全。”

总结

数组逆置用“双指针交换法”:头尾指针逐步向中间靠拢,交换元素,时间复杂度 O(n),空间复杂度 O(1)。面试时重点说清楚指针移动和交换逻辑即可。 

5.2.3 冒泡排序

**作用:** 最常用的排序算法,对数组内元素进行排序

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

示例:将数组 { 4,2,8,0,5,7,1,3,9 } 进行升序排序

#include <iostream>        // 1. 包含输入输出流头文件,提供 cout、endl 等标准输入输出工具
using namespace std;       // 2. 使用标准命名空间 std,可以直接写 cout、endl,而不需要写成 std::cout、std::endl

// 3. 程序入口函数
int main() {
 
    // 4. 定义一个包含9个整数的数组,并初始化为无序序列
    int arr[9] = { 4,2,8,0,5,7,1,3,9 };
 
    // 5. ★ 外层循环:控制排序的轮数
    //    共需要排序 9-1 = 8 轮
    //    每轮会将当前未排序部分的最大值"冒泡"到正确位置
    for (int i = 0; i < 9 - 1; i++)
    {
        // 6. ★ 内层循环:控制每轮比较和交换的次数
        //    随着轮数 i 增加,已排好的元素在右侧增多,比较次数减少
        //    j < 9 - 1 - i 表示每轮比较到未排序部分的倒数第二个元素
        for (int j = 0; j < 9 - 1 - i; j++)
        {
            // 7. 如果当前元素比后一个元素大
            if (arr[j] > arr[j + 1])
            {
                // 8. 交换 arr[j] 和 arr[j+1] 的位置
                int temp = arr[j];      // 9. 将 arr[j] 保存到临时变量
                arr[j] = arr[j + 1];    // 10. 将 arr[j+1] 赋给 arr[j]
                arr[j + 1] = temp;      // 11. 将临时变量(原 arr[j])赋给 arr[j+1]
            }
        }
    }
 
    // 12. 遍历数组,输出排序后的所有元素
    for (int i = 0; i < 9; i++)
    {
        cout << arr[i] << endl;   // 13. 输出第 i 个元素并换行
    }
 
    system("pause");   // 14. Windows 命令:暂停控制台窗口,等待用户按键
    return 0;          // 15. 程序正常退出,返回 0 表示执行成功
}

#include <iostream>
#include <cstring>
#include <bits/stdc++.h>
using namespace std;
int main() {
	
	//利用冒泡排序实现升序序列
	int arr[9] = { 4,2,8,0,5,7,1,3,9 };
	cout << "排序前: " << endl;

	for (int i = 0; i < 9; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	//总共排序轮数为元素个数-1
	for (int i = 0; i < 9-1; i++) {

		//内层循环对比 次数=  元素个数-当前轮数-1
		for (int j = 0; j < 9 - i - 1; j++) {
			//如果第一个数字比第二个数字大,交换两个数字
			if (arr[j] > arr[j + 1]) {
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
			}
		}
	}
	//排序后结果:
	cout << "排序后: " << endl;
	for (int i = 0; i < 9; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;


	system("pause");
	return 0;
}

用 n 改写后的通用代码

#include <iostream>        // ① 包含输入输出流头文件,提供 cout、endl 等工具
using namespace std;       // ② 使用标准命名空间 std

int main() {               // ③ 程序入口函数
    // ④ 定义并初始化一个整型数组
    int arr[] = {4, 2, 8, 0, 5, 7, 1, 3, 9};

    // ⑤ ★ 计算数组长度:总字节数 ÷ 单个元素字节数
    int n = sizeof(arr) / sizeof(arr[0]);

    // ============================================================
    // ★ 冒泡排序(从小到大)
    // 外层循环:控制排序轮数,共需 n-1 轮
    // ============================================================
    for (int i = 0; i < n - 1; i++) {
        // ★ 内层循环:每轮比较次数递减(n-1-i)
        // 因为每轮结束后,最大的元素已经"冒泡"到最右侧
        for (int j = 0; j < n - 1 - i; j++) {
            // ⑥ 如果前一个元素比后一个大,交换位置
            if (arr[j] > arr[j + 1]) {
                // ⑦ ★ 交换 arr[j] 和 arr[j+1]
                int temp = arr[j];      // 保存 arr[j] 到临时变量
                arr[j] = arr[j + 1];    // 将 arr[j+1] 赋值给 arr[j]
                arr[j + 1] = temp;      // 将临时变量(原 arr[j])赋值给 arr[j+1]
            }
        }
    }

    // ============================================================
    // ★ 输出排序后的数组
    // ============================================================
    for (int i = 0; i < n; i++) {
        cout << arr[i] << " ";   // ⑧ 输出每个元素,末尾加空格
    }
    cout << endl;                // ⑨ 换行

    system("pause");             // ⑩ 暂停控制台窗口
    return 0;                    // ⑪ 程序正常退出
}

先有整体逻辑,再讲过程,最后说细节

第1步:一句话解释思想

“冒泡排序的思想是:每一轮通过相邻元素的比较和交换,将当前未排序部分的最大值‘冒泡’到最右侧。”

第2步:实现过程

“外层循环控制轮数,一共需要 n-1 轮;内层循环控制每轮的比较次数,第 i 轮只需要比较 n-1-i 次,因为后面已经排好了。每次比较相邻两个元素,如果前一个比后一个大,就交换。最终数组从小到大排好。”

第3步:补充

“冒泡排序适合数据量小的场景,时间复杂度 O(n²),空间复杂度 O(1),是稳定的排序算法。”


图解冒泡排序过程

原始数组[4, 2, 8, 0, 5, 7, 1, 3, 9]

第1轮 第2轮 ... 最终
[2, 4, 0, 5, 7, 1, 3, 8, 9] [2, 0, 4, 5, 1, 3, 7, 8, 9] ... [0, 1, 2, 3, 4, 5, 7, 8, 9]

每轮把最大的数放到最后,就像气泡上浮。


 面试官可能追问的问题

1. 为什么外层循环是 n-1 轮?

因为 n 个数,只需要确定前 n-1 个的位置,最后一个自然就确定了。

2. 为什么内层循环条件是 n-1-i

因为每轮结束后,末尾已经排好了 i 个元素,它们不用再参与比较了。

3. 能不能优化?

可以加一个标志位 flag,如果某一轮没有发生任何交换,说明数组已经有序,可以提前结束,减少不必要的循环。


优化版本

for (int i = 0; i < n - 1; i++) {
    bool swapped = false;
    for (int j = 0; j < n - 1 - i; j++) {
        if (arr[j] > arr[j + 1]) {
            swap(arr[j], arr[j + 1]);
            swapped = true;
        }
    }
    if (!swapped) break;   // ★ 如果没交换,提前结束
}

// ============================================================
// ★ 冒泡排序优化版(带提前结束标志)
// ============================================================

// ① 外层循环:控制排序轮数,最多 n-1 轮
for (int i = 0; i < n - 1; i++) {
    
    // ② ★ 标志位:记录本轮是否发生过交换
    //    如果某一轮没有任何交换,说明数组已经有序
    bool swapped = false;
    
    // ③ 内层循环:比较相邻元素,每轮比较次数递减
    //    因为每轮结束后,最大的元素已经"冒泡"到末尾
    for (int j = 0; j < n - 1 - i; j++) {
        
        // ④ 如果前一个元素比后一个大
        if (arr[j] > arr[j + 1]) {
            // ⑤ 交换两个元素的位置
            swap(arr[j], arr[j + 1]);   // std::swap 交换两个值
            // ⑥ ★ 标记本轮发生过交换
            swapped = true;
        }
    }
    
    // ⑦ ★ 如果本轮没有任何交换,说明数组已经有序
    //    直接跳出外层循环,提前结束排序
    if (!swapped) {
        break;   // 提前结束,节省不必要的循环
    }
}

优化原理

为什么可以提前结束?

  • 冒泡排序的每一轮,都会把未排序部分的最大值“冒泡”到末尾。

  • 如果某一轮没有任何交换发生,说明所有相邻元素都已经满足 arr[j] <= arr[j+1],即数组已经完全有序

  • 此时后续的轮次都是多余的,可以直接退出。

效率提升

情况 原始冒泡 优化冒泡 说明
数组已有序(1,2,3,4,5) 4 轮 1 轮 第1轮无交换,提前退出 ✅
数组逆序(5,4,3,2,1) 4 轮 4 轮 每轮都有交换,无法提前退出
数组基本有序 完整执行 少部分轮次 部分场景可提前结束

        “这是一个优化版的冒泡排序。我加入了一个 swapped 标志位,每轮开始时设为 false,如果本轮发生了交换就置为 true。如果某一轮结束后 swapped 仍然是 false,说明数组已经有序,直接 break 提前退出。这个优化在处理已排序或接近有序的数组时能显著提升效率。”

        swapped 标志位用于检测本轮是否发生过交换,如果没发生说明数组已经有序,可以直接退出循环,避免不必要的比较。这是冒泡排序的经典优化,能让最佳时间复杂度降到 O(n)。

总结

“冒泡排序通过相邻元素两两比较和交换,每轮把最大值‘冒泡’到末尾。外层循环控制轮数 n-1,内层循环控制比较次数 n-1-i。最好情况 O(n),最坏 O(n²),空间 O(1),是稳定排序。可以加 flag 优化,提前结束。”

5.3 二维数组

二维数组就是在一维数组上,多加一个维度。

5.3.1 二维数组定义方式

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

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

> 建议:以上4种定义方式,利用第二种更加直观,提高代码的可读性

示例:

int main() {

    //方式1  
    //数组类型 数组名 [行数][列数]
    int arr[2][3];
    arr[0][0] = 1;
    arr[0][1] = 2;
    arr[0][2] = 3;
    arr[1][0] = 4;
    arr[1][1] = 5;
    arr[1][2] = 6;

    for (int i = 0; i < 2; i++)
    {
        for (int j = 0; j < 3; j++)
        {
            cout << arr[i][j] << " ";
        }
        cout << endl;
    }

    //方式2 
    //数据类型 数组名[行数][列数] = { {数据1,数据2 } ,{数据3,数据4 } };
    int arr2[2][3] =
    {
        {1,2,3},
        {4,5,6}
    };

    //方式3
    //数据类型 数组名[行数][列数] = { 数据1,数据2 ,数据3,数据4  };
    int arr3[2][3] = { 1,2,3,4,5,6 }; 

    //方式4 
    //数据类型 数组名[][列数] = { 数据1,数据2 ,数据3,数据4  };
    int arr4[][3] = { 1,2,3,4,5,6 };

    system("pause");

    return 0;
}
#include <iostream>      // ① 包含输入输出流头文件
using namespace std;     // ② 使用标准命名空间

int main() {             // ③ 程序入口函数

    // ============================================================
    // ★ 方式1:先声明,后赋值
    //    语法:数据类型 数组名[行数][列数];
    //    特点:先声明数组,再通过下标逐个赋值
    // ============================================================

    // ④ 声明一个 2 行 3 列的二维数组(未初始化,元素含垃圾值)
    int arr[2][3];

    // ⑤ ★ 通过下标逐个赋值
    arr[0][0] = 1;    // 第0行第0列
    arr[0][1] = 2;    // 第0行第1列
    arr[0][2] = 3;    // 第0行第2列
    arr[1][0] = 4;    // 第1行第0列
    arr[1][1] = 5;    // 第1行第1列
    arr[1][2] = 6;    // 第1行第2列

    // ⑥ ★ 双层 for 循环遍历二维数组
    //    外层循环控制行(i 从 0 到 1)
    //    内层循环控制列(j 从 0 到 2)
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 3; j++) {
            cout << arr[i][j] << " ";   // ⑦ 输出当前元素
        }
        cout << endl;   // ⑧ 每行结束换行
    }

    // ============================================================
    // ★ 方式2:声明时用嵌套大括号初始化(最清晰)
    //    语法:数据类型 数组名[行数][列数] = { {数据1, 数据2}, {数据3, 数据4} };
    //    特点:每行用独立的大括号包裹,结构清晰
    // ============================================================

    // ⑨ ★ 用嵌套大括号初始化二维数组
    int arr2[2][3] = {
        {1, 2, 3},   // 第0行
        {4, 5, 6}    // 第1行
    };
    // 注意:arr2 的内容与 arr 完全相同

    // ============================================================
    // ★ 方式3:声明时用一维列表初始化(按行填充)
    //    语法:数据类型 数组名[行数][列数] = { 数据1, 数据2, 数据3, 数据4 };
    //    特点:数据按行优先顺序填充,不足补 0,多余报错
    // ============================================================

    // ⑩ ★ 用一维列表初始化二维数组
    //     数据按行优先填充:先填满第0行,再填第1行
    int arr3[2][3] = { 1, 2, 3, 4, 5, 6 };

    // ============================================================
    // ★ 方式4:省略行数,让编译器自动计算
    //    语法:数据类型 数组名[][列数] = { 数据1, 数据2, ... };
    //    特点:行数可以省略,但列数必须指定
    //          编译器根据数据总量 ÷ 列数 自动计算行数
    // ============================================================

    // ⑪ ★ 省略行数,编译器自动计算
    //     总数据 6 个,列数 3,行数 = 6 ÷ 3 = 2
    int arr4[][3] = { 1, 2, 3, 4, 5, 6 };
    // arr4 也是 2 行 3 列,内容与 arr3 相同

    system("pause");     // ⑫ 暂停控制台窗口
    return 0;            // ⑬ 程序正常退出
}

四种定义方式对比

方式 语法 特点 适用场景
方式1 int arr[2][3];
arr[0][0] = 1;
先声明后赋值,需要逐个元素赋值 动态填值,或数据需要计算
方式2 int arr[2][3] = {{1,2,3},{4,5,6}}; 用嵌套大括号,最清晰直观 推荐,结构一目了然
方式3 int arr[2][3] = {1,2,3,4,5,6}; 一维列表,按行优先填充 数据连续,写法简洁
方式4 int arr[][3] = {1,2,3,4,5,6}; 省略行数,编译器自动计算 最简洁,适合不确定行数
规则 说明
列数必须指定 编译器需要知道每行有多少列,才能计算行数
行数可以省略 编译器根据数据总量 ÷ 列数 自动计算
数据不足补 0 如果数据少于 行数×列数,剩余元素自动补 0

总结:在定义二维数组时,如果初始化了数据,可以省略行数

5.3.2 二维数组数组名

* 查看二维数组所占内存空间
* 获取二维数组首地址

**示例:

int main() {

    //二维数组数组名
    int arr[2][3] =
    {
        {1,2,3},
        {4,5,6}
    };

    cout << "二维数组大小: " << sizeof(arr) << endl;
    cout << "二维数组一行大小: " << sizeof(arr[0]) << endl;
    cout << "二维数组元素大小: " << sizeof(arr[0][0]) << endl;

    cout << "二维数组行数: " << sizeof(arr) / sizeof(arr[0]) << endl;
    cout << "二维数组列数: " << sizeof(arr[0]) / sizeof(arr[0][0]) << endl;

    //地址
    cout << "二维数组首地址:" << (int)arr << endl;
    cout << "二维数组第一行首地址:" << (int)arr[0] << endl;
    cout << "二维数组第二行首地址:" << (int)arr[1] << endl;

    cout << "二维数组第一个元素地址:" << &arr[0][0] << endl;
    cout << "二维数组第二个元素地址:" << &arr[0][1] << endl;

    system("pause");

    return 0;
}
#include <iostream>      // ① 包含输入输出流头文件,提供 cout、endl 等工具
using namespace std;     // ② 使用标准命名空间 std

int main() {             // ③ 程序入口函数

    // ============================================================
    // ★ 定义并初始化一个 2 行 3 列的二维数组
    // ============================================================
    // ④ 二维数组可以看作“数组的数组”:arr[0] 是第一行,arr[1] 是第二行
    int arr[2][3] =
    {
        {1, 2, 3},       // ⑤ arr[0]:第 0 行,包含 3 个元素
        {4, 5, 6}        // ⑥ arr[1]:第 1 行,包含 3 个元素
    };

    // ============================================================
    // ★ 1. 使用 sizeof 获取内存大小
    // ============================================================

    // ⑦ sizeof(arr):整个二维数组占用的字节数
    //    2 行 × 3 列 × 4 字节(int) = 24 字节
    cout << "二维数组大小: " << sizeof(arr) << endl;

    // ⑧ sizeof(arr[0]):第一行占用的字节数
    //    3 列 × 4 字节 = 12 字节
    cout << "二维数组一行大小: " << sizeof(arr[0]) << endl;

    // ⑨ sizeof(arr[0][0]):单个元素占用的字节数
    //    int 类型占 4 字节
    cout << "二维数组元素大小: " << sizeof(arr[0][0]) << endl;

    // ⑩ ★ 计算行数:总大小 ÷ 一行的大小
    //    24 ÷ 12 = 2
    cout << "二维数组行数: " << sizeof(arr) / sizeof(arr[0]) << endl;

    // ⑪ ★ 计算列数:一行的大小 ÷ 一个元素的大小
    //    12 ÷ 4 = 3
    cout << "二维数组列数: " << sizeof(arr[0]) / sizeof(arr[0][0]) << endl;

    // ============================================================
    // ★ 2. 获取地址
    // ============================================================

    // ⑫ ★ arr 是数组名,代表整个数组的首地址
    //     类型是 int (*)[3](指向包含 3 个 int 的数组的指针)
    //     (int) 强制转换为整数,方便查看
    cout << "二维数组首地址:" << (int)arr << endl;

    // ⑬ ★ arr[0] 是第 0 行的首地址,与 arr 的值相同
    //     类型是 int*(指向 int 的指针)
    cout << "二维数组第一行首地址:" << (int)arr[0] << endl;

    // ⑭ ★ arr[1] 是第 1 行的首地址
    //     地址 = arr[0] 的地址 + 一行的大小(12 字节)
    cout << "二维数组第二行首地址:" << (int)arr[1] << endl;

    // ⑮ ★ &arr[0][0]:第 0 行第 0 列元素的地址
    //     与 arr 的值相同
    cout << "二维数组第一个元素地址:" << &arr[0][0] << endl;

    // ⑯ ★ &arr[0][1]:第 0 行第 1 列元素的地址
    //     地址 = &arr[0][0] 的地址 + 4 字节(一个 int 的大小)
    cout << "二维数组第二个元素地址:" << &arr[0][1] << endl;

    system("pause");     // ⑰ 暂停控制台窗口
    return 0;            // ⑱ 程序正常退出
}

> 总结1:二维数组名就是这个数组的首地址

> 总结2:对二维数组名进行sizeof时,可以获取整个二维数组占用的内存空间大小

*(解引用)和 &(取地址)是互逆操作:

&:取地址(从数据 → 地址)
*:解引用(从地址 → 数据)
操作 作用 结果类型
&数据 取地址 指针
*地址 解引用 数据

对变量操作

int a = 10;
int* p = &a;   // &a 取地址 → int*

cout << *p << endl;   // *p 解引用 → 10
表达式 类型 说明
a int 10 原始数据
&a int* 0x1000 取地址(得到指针)
*&a int 10 取地址再解引用 = 回到原位
p int* 0x1000 指针值(地址)
*p int 10 解引用(得到数据)
&p int** 0x2000 取指针的地址(二级指针)

*&a 等价于 a(取地址再解引用,回到本身)
&*p 等价于 p(解引用再取地址,回到本身)

对数组操作

一维数组

int arr[5] = {1, 2, 3, 4, 5};
// arr 本身是地址(指针常量)
表达式 类型 说明
arr int* 0x1000 数组首地址
&arr int (*)[5] 0x1000 取整个数组的地址(类型不同!)
*arr int 1 首元素值(arr[0]
arr[0] int 1 首元素值
&arr[0] int* 0x1000 首元素地址(与 arr 值相同,类型相同)
arr + 1 int* 0x1004 指向第二个元素
*(arr + 1) int 2 第二个元素值(arr[1]

二维数组

int arr[2][3] = {{1,2,3}, {4,5,6}};
表达式 类型 说明
arr int (*)[3] 0x1000 整个数组首地址(指向行)
&arr int (*)[2][3] 0x1000 取整个二维数组地址(类型不同!)
*arr int* 0x1000 第一行的首地址(等价于 arr[0]
arr[0] int* 0x1000 第一行首地址
**arr int 1 arr[0][0] 的值
arr[0][0] int 1 首元素值
&arr[0] int (*)[3] 0x1000 第一行地址(与 arr 值相同,类型相同)
&arr[0][0] int* 0x1000 首元素地址
*(arr + 1) int* 0x100C 第二行首地址(等价于 arr[1]
*(*(arr + 1) + 0) int 4 arr[1][0] 的值

关键理解

1. arr vs &arr

int arr[5];
cout << (int)arr << endl;   // 0x1000
cout << (int)&arr << endl;  // 0x1000

值相同,但类型不同

表达式 类型 +1 跳过的字节数
arr int* 4 字节
&arr int (*)[5] 20 字节(整个数组)

2. *arr 和 arr[0] 等价

int arr[5] = {1,2,3,4,5};
*arr == arr[0];   // true(都是 1)
*(arr+1) == arr[1];   // true(都是 2)

核心规律:arr[i] 等价于 *(arr + i)

3. &*p 和 *&a

int a = 10;
int* p = &a;

&*p   // 等价于 p(解引用再取地址,回到指针本身)
*&a   // 等价于 a(取地址再解引用,回到数据本身)

速查表

操作 普通变量 int a 指针 int* p 数组 int arr[5]
& 取地址 → int* 取指针地址 → int** 取数组地址 → int (*)[5]
* ❌ 不能解引用 取数据 → int 取首元素 → int

总结

& 取地址,把数据变成指针;* 解引用,把指针变回数据。对数组用 * 得到首元素,对指针用 * 得到指向的值。它们互为逆运算:*&a == a&*p == p

5.3.3 二维数组应用案例

**考试成绩统计:**

案例描述:有三名同学(张三,李四,王五),在一次考试中的成绩分别如下表,**请分别输出三名同学的总成绩

#include <iostream>        // 1. 包含输入输出流头文件,提供 cout、endl 等工具
using namespace std;       // 2. 使用标准命名空间 std,可以直接写 cout、endl

// 3. 程序入口函数
int main() {

    // 4. 定义一个 3 行 3 列的二维数组,存储三位同学的三科成绩
    //    行:代表不同的同学(张三、李四、王五)
    //    列:代表不同的科目(语文、数学、英语,这里统一抽象为三科成绩)
    int scores[3][3] =
    {
        {100,100,100},    // 5. 张三的成绩:100, 100, 100
        {90,50,100},      // 6. 李四的成绩:90, 50, 100
        {60,70,80},       // 7. 王五的成绩:60, 70, 80
    };

    // 8. 定义一个一维字符串数组,存储三位同学的名字
    //    下标 0 对应张三,下标 1 对应李四,下标 2 对应王五
    string names[3] = { "张三","李四","王五" };

    // 9. ★ 外层循环:遍历每一位同学(行)
    //    i 从 0 到 2,分别代表张三、李四、王五
    for (int i = 0; i < 3; i++)
    {
        int sum = 0;      // 10. ★ 定义并初始化总分变量,用于累计当前同学的三科成绩
                          //     注意:这个变量定义在外层循环内部,每换一个同学就会重新初始化为 0

        // 11. ★ 内层循环:遍历当前同学的三科成绩(列)
        //     j 从 0 到 2,分别代表三科成绩
        for (int j = 0; j < 3; j++)
        {
            sum += scores[i][j];   // 12. 将第 i 行第 j 列的成绩累加到 sum 中
        }

        // 13. 内层循环结束后,sum 中存放的是当前同学的总成绩
        //     输出该同学的名字和他/她的总成绩
        cout << names[i] << "同学总成绩为: " << sum << endl;
    }

    system("pause");   // 14. 暂停控制台窗口,等待用户按键
    return 0;          // 15. 程序正常退出,返回 0 表示执行成功
}

6 函数

6.1 概述

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

一个较大的程序,一般分为若干个程序块,每个模块实现特定的功能。

6.2 函数的定义

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

1、返回值类型

2、函数名

3、参数列表

4、函数体语句

5、return 表达式

**语法:**

返回值类型 函数名 (参数列表)
{

       函数体语句

       return表达式

}

在函数定义中

* 返回值类型 :一个函数可以返回一个值。
* 函数名:给函数起个名称
* 参数列表:使用该函数时,传入的数据
* 函数体语句:花括号内的代码,函数内需要执行的语句
* return表达式: 和返回值类型挂钩,函数执行完后,返回相应的数据

**示例:**定义一个加法函数,实现两个数相加

//函数定义
int add(int num1, int num2)
{
    int sum = num1 + num2;
    return sum;
}

6.3 函数的调用

**功能:**使用定义好的函数

**语法:**` 函数名(参数)`

//函数定义
int add(int num1, int num2) //定义中的num1,num2称为形式参数,简称形参
{
    int sum = num1 + num2;
    return sum;
}

int main() {

    int a = 10;
    int b = 10;
    //调用add函数
    int sum = add(a, b);//调用时的a,b称为实际参数,简称实参
    cout << "sum = " << sum << endl;

    a = 100;
    b = 100;

    sum = add(a, b);
    cout << "sum = " << sum << endl;

    system("pause");

    return 0;
}

总结:函数定义里小括号内称为形参,函数调用时传入的参数称为实参

6.4 值传递

* 所谓值传递,就是函数调用时实参将数值传入给形参
* 值传递时,如果形参发生改变,并不会影响实参

#include <iostream>
#include <string>
#include <bits/stdc++.h>
using namespace std;

void swap(int num1, int num2)
{
    cout << "交换前:" << endl;
    cout << "num1 = " << num1 << endl;
    cout << "num2 = " << num2 << endl;

    int temp = num1;
    num1 = num2;
    num2 = temp;

    cout << "交换后:" << endl;
    cout << "num1 = " << num1 << endl;
    cout << "num2 = " << num2 << endl;

    //return ; 当函数声明时候,不需要返回值,可以不写return
}
int main() {
      
    
       
    
    int a = 10;
    int b = 20;
    
    cout << "mian中的 a = " << a << endl;
    cout << "mian中的 b = " << b << endl;
    swap(a, b);
    
    cout << "mian中的 a = " << a << endl;
    cout << "mian中的 b = " << b << endl;

        
    
	system("pause");
	return 0;
}

> 总结: 值传递时,形参是修饰不了实参的

6.5 函数的常见样式

常见的函数样式有4种

1. 无参无返
2. 有参无返
3. 无参有返
4. 有参有返

//函数常见样式
//1、 无参无返
void test01()
{
    //void a = 10; //无类型不可以创建变量,原因无法分配内存
    cout << "this is test01" << endl;
    //test01(); 函数调用
}

//2、 有参无返
void test02(int a)
{
    cout << "this is test02" << endl;
    cout << "a = " << a << endl;
}

//3、无参有返
int test03()
{
    cout << "this is test03 " << endl;
    return 10;
}

//4、有参有返
int test04(int a, int b)
{
    cout << "this is test04 " << endl;
    int sum = a + b;
    return sum;
}
#include <iostream>      // ① 包含输入输出流头文件,提供 cout、endl 等工具
using namespace std;     // ② 使用标准命名空间 std

// ============================================================
// ★ 函数常见样式(4种组合)
// ============================================================

// ============================================================
// ③ ★ 方式1:无参无返
//    特点:不需要参数,不返回任何值
//    用途:只执行某个操作,不需要输入也不需要输出
// ============================================================
void test01()
{
    // ④ ❌ void 类型不能创建变量
    //    void a = 10; // 错误!void 是没有大小的类型,无法分配内存
    
    cout << "this is test01" << endl;
    // ⑤ 调用方式:test01();
}

// ============================================================
// ⑥ ★ 方式2:有参无返
//    特点:需要参数,不返回任何值
//    用途:执行操作需要外部数据,但不需要反馈结果
// ============================================================
void test02(int a)
{
    cout << "this is test02" << endl;
    cout << "a = " << a << endl;
    // ⑦ 调用方式:test02(10);
}

// ============================================================
// ⑧ ★ 方式3:无参有返
//    特点:不需要参数,但返回一个值
//    用途:计算或生成数据,不需要外部输入
// ============================================================
int test03()
{
    cout << "this is test03 " << endl;
    return 10;    // ⑨ ★ 返回 int 类型的值 10
    // ⑩ 调用方式:int result = test03();
}

// ============================================================
// ⑪ ★ 方式4:有参有返
//    特点:需要参数,也返回一个值
//    用途:根据输入参数计算并返回结果
// ============================================================
int test04(int a, int b)
{
    cout << "this is test04 " << endl;
    int sum = a + b;    // ⑫ 计算两个参数的和
    return sum;         // ⑬ ★ 返回计算结果
    // ⑭ 调用方式:int result = test04(10, 20);
}
int main() {
    // ① 无参无返
    test01();              // 直接调用

    // ② 有参无返
    test02(10);            // 传入参数

    // ③ 无参有返
    int result1 = test03(); // 接收返回值
    cout << result1 << endl;

    // ④ 有参有返
    int result2 = test04(10, 20); // 传入参数,接收返回值
    cout << result2 << endl;

    return 0;
}

四种函数样式对比

样式 函数声明 是否有参数 是否有返回值 适用场景
无参无返 void test01() 打印、日志、纯操作
有参无返 void test02(int a) 根据输入执行操作,不反馈
无参有返 int test03() 生成数据、获取当前状态
有参有返 int test04(int a, int b) 计算、数据处理

6.6 函数的声明

作用:告诉编译器函数名称及如何调用函数。函数的实际主体可以单独定义。

        函数的声明可以多次,但是函数的定义只能有一次

//声明可以多次,定义只能一次
//声明
int max(int a, int b);
int max(int a, int b);
//定义
int max(int a, int b)
{
    return a > b ? a : b;
}

int main() {

    int a = 100;
    int b = 200;

    cout << max(a, b) << endl;

    system("pause");

    return 0;
}
#include <iostream>      // ① 包含输入输出流头文件,提供 cout、endl 等工具
using namespace std;     // ② 使用标准命名空间 std

// ============================================================
// ★ 声明可以多次,定义只能一次
// ============================================================

// ③ ★ 函数声明(函数原型)
//    作用:告诉编译器“有这个函数存在”,但不提供具体实现
//    特点:没有函数体,以分号结尾
//    可以出现多次(只要内容一致)
int max(int a, int b);    // ④ 第一次声明
int max(int a, int b);    // ⑤ 第二次声明(重复声明,合法)
// 多次声明相同函数不会报错,只要声明完全一致

// ============================================================
// ⑥ ★ 函数定义(函数实现)
//    作用:提供函数的具体实现
//    特点:有函数体,以花括号 {} 包裹
//    只能出现一次(否则重复定义报错)
// ============================================================
int max(int a, int b)
{
    // ⑦ 使用三元运算符:如果 a > b 则返回 a,否则返回 b
    return a > b ? a : b;   // 等同于 if (a > b) return a; else return b;
}

// ============================================================
// ⑧ 主函数(程序入口)
// ============================================================
int main() {

    int a = 100;       // ⑨ 定义变量 a,初始值 100
    int b = 200;       // ⑩ 定义变量 b,初始值 200

    // ⑪ ★ 调用 max 函数,传入 a 和 b,接收返回值并输出
    //    因为 200 > 100,所以返回 200
    cout << max(a, b) << endl;   // 输出 200

    system("pause");   // ⑫ 暂停控制台窗口
    return 0;          // ⑬ 程序正常退出
}

6.7 函数的分文件编写

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

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

1. 创建后缀名为.h的头文件
2. 创建后缀名为.cpp的源文件
3. 在头文件中写函数的声明
4. 在源文件中写函数的定义

**示例:

#include <iostream>      // ① 包含输入输出流头文件,提供 cout、endl 等工具
using namespace std;     // ② 使用标准命名空间 std

// ============================================================
// ★ 函数声明(函数原型)
//    作用:告诉编译器“有 swap 这个函数存在”
//    特点:只有声明,没有实现
//    位置:头文件(.h),供多个源文件共享
// ============================================================

// ③ ★ 交换两个整数的函数声明
//    注意:这里是值传递,交换的是副本,不影响实参
//    只是为了演示多文件编译的结构,实际交换要用引用或指针
void swap(int a, int b);
// ============================================================
// ★ 包含头文件
//    将 swap.h 中的声明包含进来
//    如果头文件中没有包含 iostream,这里需要单独包含
// ============================================================

#include "swap.h"      // ④ ★ 包含自定义头文件(用双引号)

// ============================================================
// ★ 函数定义(函数实现)
//    位置:源文件(.cpp)
//    提供具体的实现代码
// ============================================================

// ⑤ ★ 交换两个整数的函数实现
//    参数:值传递(a 和 b 是实参的副本)
//    效果:交换的是副本,主函数中的实参不受影响
//    注意:这里只演示了输出交换后的值,没有真正修改实参
void swap(int a, int b)
{
    int temp = a;      // ⑥ 将 a 的值保存到临时变量 temp
    a = b;             // ⑦ 将 b 的值赋给 a
    b = temp;          // ⑧ 将 temp(原 a 的值)赋给 b

    // ⑨ 输出交换后的值(副本的值)
    cout << "a = " << a << endl;   // 输出 200
    cout << "b = " << b << endl;   // 输出 100
}
// ============================================================
// ★ 包含头文件
//    让 main 函数知道 swap 函数的存在
// ============================================================

#include "swap.h"      // ⑩ ★ 包含自定义头文件

// ============================================================
// ★ 主函数(程序入口)
// ============================================================

int main() {

    int a = 100;       // ⑪ 定义变量 a,初始值 100
    int b = 200;       // ⑫ 定义变量 b,初始值 200

    // ⑬ ★ 调用 swap 函数
    //     传入 a 和 b 的副本(值传递)
    //     函数内部交换的是副本,主函数中的 a 和 b 不会改变
    swap(a, b);

    // ⑭ ★ 证明实参没有被修改
    //     因为值传递,主函数中的 a 和 b 仍然是 100 和 200
    //     如果这里输出,会发现 a = 100, b = 200(未改变)
    //     所以这个 swap 实际上没有“真正”交换主函数中的值

    system("pause");   // ⑮ 暂停控制台窗口
    return 0;          // ⑯ 程序正常退出
}

1. 编译过程

C++ 程序的编译分为两个阶段:

源文件(.cpp)→ 预处理 → 编译 → 目标文件(.obj)→ 链接 → 可执行文件(.exe)
  • 预处理:处理 #include,把被包含的文件内容原地复制到当前文件

  • 编译:每个 .cpp 文件独立编译成 .obj 文件

  • 链接:把多个 .obj 合并成 .exe

2. main.cpp中如果包含 .cpp 会发生什么?

// main.cpp
#include "swap.cpp"   // 把 swap.cpp 的内容复制到 main.cpp 中
int main() {
    swap(a, b);
    return 0;
}
问题
┌─────────────────────────────────────────────────────────────────┐
│  预处理后:main.cpp 包含了 swap.cpp 的完整代码                 │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  void swap(int a, int b) { ... }   ← 函数定义          │    │
│  │  int main() { swap(a, b); }                            │    │
│  └─────────────────────────────────────────────────────────┘    │
│                              │                                  │
│                              ▼                                  │
│  编译:main.cpp → main.obj(包含了 swap 的代码)              │
│  编译:swap.cpp → swap.obj(也包含了 swap 的代码)            │
│                              │                                  │
│                              ▼                                  │
│  链接:main.obj + swap.obj → ❌ 重复定义错误!                 │
│  error: 'swap' already defined in main.obj                    │
└─────────────────────────────────────────────────────────────────┘

因为函数被定义了两次,链接器不知道用哪一个。

正确做法 vs 错误做法

做法 预处理结果 编译结果 链接结果
包含 .h(声明) 只复制声明 main.obj 只有调用,没有实现 链接时找 swap.cpp 的实现 ✅
包含 .cpp(定义) 复制完整实现 main.obj 有 swap 的实现 swap.obj 也有 swap 的实现 → 冲突 ❌

声明 vs 定义

概念 含义 可以多次出现? 在 .h 中 在 .cpp 中
声明 告诉编译器“有这个函数存在” ✅ 可以 ✅ 放这里 ❌ 不放
定义 提供函数的具体实现 ❌ 只能一次 ❌ 不放 ✅ 放这里

如何避免重复定义?

除了用 .h 存放声明外,还需用头文件守卫防止同一头文件被多次包含:

// ===== swap.h =====
#ifndef SWAP_H           // ① 如果没有定义 SWAP_H
#define SWAP_H           // ② 定义 SWAP_H

void swap(int a, int b); // ③ 声明

#endif                   // ④ 结束守卫
┌─────────────────────────────────────────────────────────────────┐
│  1. 预处理阶段(#include 展开)                                │
│                                                                 │
│  main.cpp            +    swap.h    =    main.i(含声明)      │
│  swap.cpp            +    swap.h    =    swap.i(含声明)      │
│                                                                 │
│  两个文件都通过 #include "swap.h" 获得了函数的声明             │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  2. 编译阶段(独立编译成目标文件)                              │
│                                                                 │
│  main.i  →  编译  →  main.obj(含:调用 swap 的指令)          │
│  swap.i  →  编译  →  swap.obj(含:swap 函数的机器码)         │
│                                                                 │
│  每个 .cpp 文件独立编译,互不影响                              │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  3. 链接阶段(合并目标文件)                                    │
│                                                                 │
│  main.obj 中调用 swap 的指令 → 找到 swap.obj 中的 swap 实现    │
│                                                                 │
│  main.obj + swap.obj → 链接 → 可执行文件(.exe)              │
│                                                                 │
│  链接器把两个 .obj 合并,并“补齐”调用的地址                    │
└─────────────────────────────────────────────────────────────────┘

声明和定义的关系

阶段 作用 谁需要它
声明(.h) 告诉编译器“有这个函数” 编译器(编译每个 .cpp 时)
定义(.cpp) 提供真正的代码实现 链接器(合并 .obj 时)

这样,即使多个 .cpp 都 #include "swap.h",头文件内容也只会被处理一次。

“头文件放声明,源文件放定义。#include 的作用是在预处理阶段将被包含文件的内容复制到当前文件中。如果包含 .cpp,会把函数定义复制到多个源文件,链接时会导致重复定义错误。所以用 .h 放声明,.cpp 放定义,多个文件通过声明共享接口。”

总结

包含 .h 是因为它只放声明,不会造成重复定义;不能包含 .cpp 是因为它放定义,会与自己的 .obj 冲突。声明可以多次,定义只能一次。

7 指针

7.1 指针的基本概念

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

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

7.2 指针变量的定义和使用

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

示例:

int main() {

    //1、指针的定义
    int a = 10; //定义整型变量a

    //指针定义语法: 数据类型 * 指针变量名 ;
    int * p;

    //指针变量赋值
    p = &a; //指针指向变量a的地址
    cout << &a << endl; //打印数据a的地址
    cout << p << endl;  //打印指针变量p

    //2、指针的使用
    //通过*操作指针变量指向的内存
    cout << "*p = " << *p << endl;

    system("pause");

    return 0;
}
#include <iostream>      // ① 包含输入输出流头文件,提供 cout、endl 等工具
using namespace std;     // ② 使用标准命名空间 std

int main() {             // ③ 程序入口函数

    // ============================================================
    // ★ 1. 指针的定义
    // ============================================================

    // ④ 定义一个整型变量 a,在栈上分配 4 字节,存储值 10
    int a = 10;

    // ⑤ ★ 指针定义语法:数据类型 * 指针变量名;
    //     int* p; 表示 p 是一个指向 int 类型的指针变量
    //     此时 p 还没有被赋值,里面存的是随机值(野指针)
    int* p;

    // ⑥ 指针变量赋值:把 a 的地址存到 p 中
    //     &a 是取地址运算符,获取 a 在内存中的地址
    p = &a;    // 现在 p 指向 a

    // ⑦ 打印 a 的地址(&a)和 p 的值
    //     两者应该相同(都是 a 的地址)
    cout << "a 的地址: " << &a << endl;   // 输出 a 的地址,如 0x1000
    cout << "p 的值: " << p << endl;      // 输出 p 存储的地址,也是 0x1000

    // ============================================================
    // ★ 2. 指针的使用(解引用)
    // ============================================================

    // ⑧ ★ 通过 * 操作指针变量指向的内存
    //     *p 是解引用运算符,去 p 指向的地址取出值
    cout << "*p = " << *p << endl;        // 输出 10(a 的值)

    // ⑨ 通过指针修改 a 的值
    *p = 20;    // 把 20 写入 p 指向的内存(也就是 a)
    cout << "a = " << a << endl;          // 输出 20
    cout << "*p = " << *p << endl;        // 输出 20

    system("pause");     // ⑩ 暂停控制台窗口
    return 0;            // ⑪ 程序正常退出
}

指针变量和普通变量的区别

* 普通变量存放的是数据,指针变量存放的是地址
* 指针变量可以通过" * "操作符,操作指针变量指向地址的内存空间,这个过程称为解引用

> 总结1: 我们可以通过 & 符号 获取变量的地址

引用是“别名”(变量),取地址是“计算”(获取地址值)。 两者在 C++ 中用法完全不同,但容易混淆,因为符号相同(&)。

核心区别

对比项 引用 取地址
是什么 变量的别名(另一个名字) 获取变量的地址值(指针)
符号 &(在声明中) &(在表达式中)
结果类型 int&(引用类型) int*(指针类型)
是否分配内存 底层有(指针常量),语法上不显式 ❌ 不分配,只是计算
能否单独存在 ❌ 必须初始化绑定到变量 ✅ 可以单独作为表达式
能否修改 修改引用 = 修改原变量 取地址只是一个值,不能修改
使用场景 函数参数、函数返回值、别名 获取变量地址给指针

 关键区分:看 & 的位置

int a = 10;

// ============================================================
// ★ 情况1:& 在类型后面 → 引用
// ============================================================
int& ref = a;   // ref 是 a 的引用(别名)
//  ↑
//  & 在类型 int 后面 → 声明引用

// ============================================================
// ★ 情况2:& 在变量前面 → 取地址
// ============================================================
int* p = &a;    // &a 取 a 的地址
//        ↑
//        & 在变量 a 前面 → 取地址运算符
#include <iostream>
using namespace std;

int main() {
    int a = 10;

    // ============================================================
    // ★ 引用:是别名
    // ============================================================
    int& ref = a;           // ref 是 a 的别名
    ref = 20;               // 修改 ref = 修改 a
    cout << a << endl;      // 20

    // 引用必须初始化,不能重新绑定
    int b = 30;
    // ref = b;             // 这是赋值(把 b 的值赋给 a),不是重新绑定

    // ============================================================
    // ★ 取地址:获取地址值
    // ============================================================
    int* p = &a;            // p 存 a 的地址
    cout << p << endl;      // 输出地址(如 0x1000)
    cout << *p << endl;     // 解引用得到 20

    // ============================================================
    // ★ 类型不同
    // ============================================================
    cout << typeid(ref).name() << endl;  // int(引用本身类型)
    cout << typeid(&a).name() << endl;   // int*(指针类型)
    cout << typeid(p).name() << endl;    // int*(指针类型)

    return 0;
}

图解对比

引用 ref(是 a 的别名):
┌─────────────────────────────────────────────────────────────────┐
│  int a = 10;                                                   │
│  int& ref = a;                                                 │
│                                                                 │
│  ref 和 a 是同一块内存的两个名字                               │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  地址 0x1000: 存储 10                                   │    │
│  │  a 和 ref 都指向这块内存                               │    │
│  └─────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘

取地址 &a(计算地址值):
┌─────────────────────────────────────────────────────────────────┐
│  int* p = &a;                                                  │
│                                                                 │
│  &a 计算出 a 的地址(0x1000),赋值给 p                       │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  p 在 0x2000,存储 0x1000                               │    │
│  │  a 在 0x1000,存储 10                                   │    │
│  └─────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘

函数参数中的区别

// ★ 引用传递:形参是实参的别名
void func1(int& x) {
    x = 100;   // 直接修改实参
}

// ★ 取地址传递:形参是指针,存实参的地址
void func2(int* p) {
    *p = 100;  // 通过地址修改实参
}

int main() {
    int a = 10;
    func1(a);   // 传引用(编译器自动)
    func2(&a);  // 传地址(手动取地址)
    return 0;
}

引用和取地址的底层关系

引用的底层实现就是指针常量,但编译器帮你隐藏了细节:

// 你写的代码
int& ref = a;
ref = 20;

// 编译器实际生成的代码
int* const ref = &a;
*ref = 20;

所以引用和指针在底层是相通的,但语法上引用更安全、更简洁。

& 在类型后面(int& ref)是声明“引用”(别名);& 在变量前面(&a)是“取地址”运算符,得到一个指针值。引用是变量,取地址是计算。

> 总结2:利用指针可以记录地址

> 总结3:对指针变量解引用,可以操作指针指向的内存

        取地址(&p)本身只是一个“计算过程”,它不会开辟新内存。只有当你用变量去“接收”这个地址值时,才会为新变量分配空间。

取地址(&p)的本质

&p 是一个“求地址”操作,它返回指针变量 p 自己在栈上的地址值。这个值是一个临时数据,不占内存,除非你把它存到一个变量里。

int a = 10;
int* p = &a;

&p;          // ① 计算 p 的地址,得到临时值(如 0x2000)
             // 但没存下来,这个值用完就丢了

int** pp = &p;  // ② &p 计算出地址(0x2000),赋值给 pp
                 // ★ 创建新内存的是 int** pp,不是 &p
取地址本身(不创建内存):
┌─────────────────────────────────────────────────────────────────┐
│  栈内存:                                                      │
│  地址 0x2000: p(存储 0x1000)                                 │
│  地址 0x1000: a(存储 10)                                    │
│                                                                 │
│  &p 的操作:读取 p 自己的地址(0x2000),返回这个值           │
│  这个值是一个临时数据,不会在内存中留下痕迹                    │
│  除非你把它存到一个新变量里                                    │
└─────────────────────────────────────────────────────────────────┘

创建新变量来接收(创建内存):
┌─────────────────────────────────────────────────────────────────┐
│  int** pp = &p;                                                │
│       ↑                                                       │
│       └── 这是创建新内存的地方(pp 在栈上占 8 字节)          │
│                                                                 │
│  &p 只是读取一个地址值(0x2000),然后赋值给新变量 pp        │
└─────────────────────────────────────────────────────────────────┘
#include <iostream>
using namespace std;

int main() {
    int a = 10;
    int* p = &a;

    // 1. &p 本身不创建内存
    cout << &p << endl;   // 输出 p 的地址(如 0x2000)
                          // 这个地址值只是临时输出,没有存储

    // 2. 创建新内存的是变量声明,不是取地址
    int** pp = &p;        // ✅ 创建了 pp(8字节),&p 只是提供地址值

    // 3. 验证:&p 和 pp 的值相同(都是 p 的地址)
    cout << &p << endl;   // 0x2000
    cout << pp << endl;   // 0x2000(相同)

    return 0;
}

完整对照表

代码 是否创建新内存 创建了什么 说明
&p(取地址) ❌ 不创建 只是读取 p 自己的地址值
int** pp = &p; ✅ 创建 pp(8字节) 创建新变量的是 int** pp
void* vp = &p; ✅ 创建 vp(8字节) 创建新变量的是 void* vp
&a(取变量地址) ❌ 不创建 只是读取 a 的地址值
int* p = &a; ✅ 创建 p(8字节) 创建新变量的是 int* p
概念 是否分配内存 说明
取地址 &p ❌ 不分配 只是返回 p 的地址值(临时)
int** pp = &p; ✅ 分配 创建指针变量 pp(8字节)
解引用 *p ❌ 不分配 只是访问 p 指向的内存
int b = *p; ✅ 分配 创建变量 b(4字节)

取地址和解引用一样,都是“地址运算”:一个取地址,一个取值。它们都是计算过程,不分配内存。只有变量声明才会真正在栈上开辟空间。

        指针本身是一种类型(int*char* 等),它存的是地址。指针变量也有自己的地址,这个地址的类型是“指向指针的指针”(二级指针,int**)。

        不存在“最后一个指针”,因为你可以无限嵌套——int*****... 可以一直写下去,但实际编程中一般只用到二级指针,极少超过三级。

1. 指针是一种类型

int a = 10;      // a 的类型是 int,存整数值
int* p = &a;     // p 的类型是 int*,存地址值
变量 类型 存储内容 占内存
a int 10 4 字节
p int* 0x1000(a 的地址) 8 字节

指针的本质就是:一种存地址的变量类型。

2. 指针的地址是什么类型?—— 二级指针

int a = 10;
int* p = &a;     // p 指向 a
int** pp = &p;   // pp 指向 p(二级指针)
int*** ppp = &pp; // ppp 指向 pp(三级指针)
变量 类型 存储内容 说明
a int 10 普通整型变量
p int* 0x1000(a 的地址) 一级指针,存 a 的地址
&p int** 0x2000(p 的地址) 取 p 的地址 → 二级指针
pp int** 0x2000(p 的地址) 二级指针,存 p 的地址
&pp int*** 0x3000(pp 的地址) 取 pp 的地址 → 三级指针
ppp int*** 0x3000(pp 的地址) 三级指针,存 pp 的地址
#include <iostream>
using namespace std;

int main() {
    int a = 10;        // ① 整型变量
    int* p = &a;       // ② p 指向 a

    // ★ 打印三个值
    cout << "a 的值: " << a << endl;              // 10
    cout << "a 的地址: " << &a << endl;           // 0x1000(假设)
    cout << "p 存储的值: " << p << endl;          // 0x1000(和 &a 相同)
    cout << "p 自己的地址: " << &p << endl;       // ★ 0x2000(p 自己的地址!)

    return 0;
}

7.3 指针所占内存空间

提问:指针也是种数据类型,那么这种数据类型占用多少内存空间?

**示例:

#include <iostream>        // 1. 包含输入输出流头文件,提供 cout、endl 等工具
using namespace std;       // 2. 使用标准命名空间 std,可以直接写 cout、endl

int main() {               // 3. 程序入口函数
 
    int a = 10;            // 4. 定义一个普通的整型变量 a,在内存中分配一块空间,存储值为 10
 
    int * p;               // 5. 定义一个整型指针变量 p
                           //    ★ p 是一个特殊的变量,它只能存储"内存地址"
                           //    此时 p 还没有被赋值,里面存放的是随机值(野指针),非常危险
    p = &a;                // 6. ★ 将变量 a 的地址赋值给 p
                           //    & 是"取地址运算符",作用是获取变量 a 在内存中的地址
                           //    执行后,p 就"指向"了 a
 
    cout << *p << endl;    // 7. ★ 解引用:通过 p 中存储的地址,访问该地址上的数据
                           //    * 是"解引用运算符",作用是"去 p 指向的那个地址看看,把里面的值取出来"
                           //    这里 p 指向 a,所以 *p 的值就是 a 的值,即 10
 
    cout << sizeof(p) << endl;       // 8. 输出指针变量 p 本身占用的内存大小
                                     //    在 64 位系统上,指针统一占 8 字节
    cout << sizeof(char *) << endl;  // 9. 输出 char* 类型指针的大小 → 同样是 8 字节
    cout << sizeof(float *) << endl; // 10. 输出 float* 类型指针的大小 → 同样是 8 字节
    cout << sizeof(double *) << endl;// 11. 输出 double* 类型指针的大小 → 同样是 8 字节
 
    system("pause");       // 12. 暂停控制台窗口,等待用户按键
    return 0;              // 13. 程序正常退出,返回 0 表示执行成功
}

总结:所有指针类型在32位操作系统下是4个字节

指针占用的字节数取决于你的系统架构,不取决于指针指向的数据类型。

系统架构 指针大小
32 位系统 4 字节
64 位系统 8 字节

指针存储的是内存地址,所以指针的大小由地址总线的位数决定。

  • 32 位系统:地址总线 32 位 → 最大寻址 2³² = 4GB → 一个地址用 4 字节表示

  • 64 位系统:地址总线 64 位 → 最大寻址 2⁶⁴ → 一个地址用 8 字节表示

架构 指针大小
常见的 64 位 Windows/Linux/macOS 8 字节
常见的 32 位 Windows/Linux 4 字节
嵌入式系统(如 8 位单片机) 2 字节 或 3 字节
16 位系统(DOS 时代) 2 字节

现代大部分开发环境都是 64 位,所以指针通常是 8 字节

7.4 空指针和野指针

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

**用途:**初始化指针变量

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

**示例1:空指针

#include <iostream>
using namespace std;

int main() {

    // 1. 定义一个整型指针变量 p,并将其初始化为 NULL
    // ★ NULL 在 C++ 中通常被定义为 0 或 (void*)0
    //    意思是将指针 p 指向“内存地址编号为 0”的位置
    int * p = NULL;

    // 2. ★ 尝试访问空指针指向的内存区域
    //    这行代码会试图读取地址 0 上存储的数据(即 *p)
    //    但在大多数操作系统中,内存地址 0 及其附近(通常 0 ~ 255)是被系统内核保留的,不允许用户程序读写
    //    所以程序运行到这里会发生“段错误(Segmentation Fault)”或“访问冲突”,直接崩溃
    cout << *p << endl;

    system("pause");
    return 0;
}

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

**示例2:野指针

int main() {

    //指针变量p指向内存地址编号为0x1100的空间
    int * p = (int *)0x1100;

    //访问野指针报错 
    cout << *p << endl;

    system("pause");

    return 0;
}

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

7.5 const修饰指针

const修饰指针有三种情况

1. const修饰指针 --- 常量指针,别名:指向常量的指针,常量指针可以理解为常量指针所指向的值,指针存放地址,所指向的值就是地址中存放的数据,常量指针表示指针所指的值为常量,不可修改
2. const修饰常量 --- 指针常量,别名:指针本身是常量,指针本身就是指针的指向,指针本身是常量就是指指针的指向不能变。

特点: 指针一经初始化,终身只能指向这块内存; 但是可以通过指针修改内存里存放的数据。

const修饰常量的意思是:const后面跟着的是变量也就是指针名,但是这个变量在const修饰下这个变量就变为常量,就成为const修饰常量
3. const既修饰指针,又修饰常量

指针常量 = 指针本身不能改,指针的指向不能改(地址锁死)

常量指针 = 不能通过指针修改指向的数据(内容锁死)

指针常量指针本身不能变,初始化必须指定内存空间否则报错,常量指针指针的指向可以变,因此可以不初始化。

有*是值,没*是地址

看const后面紧跟着什么,紧跟着*就是锁住值,值不能变,紧跟着地址,就是锁住地址,地址不能变

const 修饰它左边最近的那个东西。如果左边没有东西,就修饰它右边最近的那个东西。

常用场景

场景 推荐写法 说明
只读访问数据 const int* p; 可以指向不同变量,但不能修改数据
固定指向某地址 int* const p; 指向固定变量,但可以修改其值
完全只读 const int* const p; 既不能改指向,也不能改数据
函数参数(只读) void func(const int* p); 承诺不修改传入的数据
函数参数(固定地址) void func(int* const p); 承诺不修改指针本身
特性 指针常量 int* const p 常量指针 const int* p
写法 int* const const int*
const 位置 * 右边 * 左边(或 int 左边)
指针本身可变? ❌ 不可变 ✅ 可变
指向的数据可变? ✅ 可变(通过 *p ❌ 不可变(通过 p
初始化必须 ✅ 必须立即初始化 可以稍后赋值
典型场景 固定指向某个地址 只读访问数据

const int* p;    // const 修饰"指向数据的类型"
int* const p;    // const 修饰"指针本身的类型"
const int *p(等价 int const *p)

        p指针指向的数据类型就看*号前面的数据类型,p指针本身的类型就看“*和p中间有没有const,有const就表示这个指针被锁住了,它的指向无法改变”,p指针的类型表示其地址中存放数据的类型,看*前面的int,int就表示p指针本身存放的是整型数据的地址。

**示例:

int main() {

    int a = 10;
    int b = 10;

    //const修饰的是指针,指针指向可以改,指针指向的值不可以更改
    const int * p1 = &a; 
    p1 = &b; //正确
    //*p1 = 100;  报错


    //const修饰的是常量,指针指向不可以改,指针指向的值可以更改
    int * const p2 = &a;
    //p2 = &b; //错误
    *p2 = 100; //正确

    //const既修饰指针又修饰常量
    const int * const p3 = &a;
    //p3 = &b; //错误
    //*p3 = 100; //错误

    system("pause");

    return 0;
}

技巧:看const在指针的左侧还是右侧,在左侧即const *就是常量指针,在右侧即* const就是指针常量

多级指针的 const

三级指针的类型系统

int* const* p;        //p 是指向【指向 int 的指针常量】的指针
const int** p;        //p 是指向【指向 const int 的常量指针】的指针
const int* const* p;  //p 是指向【指向 const int 的指针常量】的指针

int* const* p;

读取规则:从变量 p 向左一层层看类型

  1. p 是指针
  2. *p 的类型 = int* const
  3. int* const指针常量
    • ptr 是指针,const 修饰指针本身;
    • 含义:这个指针不能改变指向,但可以修改指向的 int 值。
int a = 10;
int* const ptr = &a;
int* const* p = &ptr;
  • p = 别的地址; 合法(最外层 p 没有 const,可以修改 p 存的地址)
  • *p = nullptr; 非法(*p 的类型是int* const【指针常量】,不能修改指向)
  • **p = 100; 合法(底层 int,能修改数值)

表格

写法 标准名称 核心
const int *p / int const *p 常量指针(指向常量的指针) const*左侧,不能*p=xxx
int *const p 指针常量 const*右侧,不能p=&xxx


多级指针完整对照表

写法 p 可变 *p 可变 **p 可变 含义
int** p 普通二级指针
int* const* p 中间指针是常量
const int** p 最终数据是常量
const int* const* p 中间指针和最终数据都是常量
int* const* const p p 本身和中间指针都是常量
const int* const* const p 全部都是常量


声明 p 可修改 *p 可修改 **p 可修改
int* const* p;
const int** p;
const int* const* p;
代码 含义 能否改 p 能否改 * p 能否改 **p
int** const p; p 是指针常量
int const** p; 指向「const int*」的指针
const int** p; 和上面等价
int* const* p; p 指向「常量指针 int* const」
int* const* const p; 双重限制

拆解技巧:从右向左读

C++ 中 const 遵循“从右向左”的修饰规则:

  • const 在 * 左边:修饰指针指向的数据(数据不可变)

  • const 在 * 右边:修饰指针本身(指针不可变)

总结:const 是编译器层面的"类型约束",它在汇编层面不产生任何额外指令;const int* p 限制了"通过 p 写数据",int* const p 限制了"修改 p 本身"。这两者都是编译器检查,强制转换可以绕过,但绕过可能导致未定义行为。


核心规则(适用于任何组合)

int* p;       // 一级指针(1个*)
int** p;      // 二级指针(2个*)
int*** p;     // 三级指针(3个*)

指针的级数 = * 的个数。

const 的规则(永远不变):

  • const 在 * 的右边 → 修饰指针本身(指针不能改)

  • const 在 * 的左边 → 修饰指向的数据(数据不能改)


📊 按层级逐条拆解

🔹 1. 一级指针(1 个 *

写法 const 位置 锁住谁 指针可变 数据可变
int* p
int* const p * 右边 指针本身
const int* p * 左边 指向的数据
int const* p * 左边 指向的数据
const int* const p 两边都有 指针 + 数据
int const* const p 两边都有 指针 + 数据

🔹 2. 二级指针(2 个 *

写法 第1层 p 第2层 *p 数据 **p
int** p ✅ 可变 ✅ 可变 ✅ 可变
int* const* p ✅ 可变 ❌ 不可变 ✅ 可变
const int** p ✅ 可变 ✅ 可变 ❌ 不可变
int** const p ❌ 不可变 ✅ 可变 ✅ 可变
int* const* const p ❌ 不可变 ❌ 不可变 ✅ 可变
const int** const p ❌ 不可变 ✅ 可变 ❌ 不可变
const int* const* p ✅ 可变 ❌ 不可变 ❌ 不可变
const int* const* const p ❌ 不可变 ❌ 不可变 ❌ 不可变

拆解示例:

const int* const* p;
         ↑    ↑
         1    2    ← 2 个 `*` → 二级指针

- p:可变(没有被 const 修饰)
- *p:不可变(const 在第二个 * 右边 → 修饰指针本身)
- **p:不可变(const 在第一个 * 左边 → 修饰数据)

🔹 3. 三级指针(3 个 *

写法 p *p **p ***p
int*** p
int** const* p
int* const** p
const int*** p
int*** const p
int** const* const p
const int** const* p
const int* const** p
const int** const* const p
const int* const* const* p
const int* const* const* const p

拆解示例:

const int* const* const* p;
         ↑    ↑    ↑
         1    2    3   ← 3 个 `*` → 三级指针

- p:可变(没有 const 修饰 p 本身)
- *p:不可变(const 在第3层 * 右边)
- **p:不可变(const 在第2层 * 右边)
- ***p:不可变(const 在数据层左边)

理解int*** p;

  1. p 是一个指针

  2. 它指向 int**(二级指针)

  3. int** 是一个指针,指向 int*

  4. int* 是一个指针,指向 int


📝 判断技巧

逐层看 const 在 * 的哪一边:

const int* const* const* p;

从右往左:

层级 表达式 看什么 结论
第1层 p 紧挨着 p 的是 * 还是 const * → p 可变
第2层 *p const 在 * 右还是左? * 右边有 const → *p 不可变
第3层 **p const 在 * 右还是左? * 右边有 const → **p 不可变
数据 ***p const 在哪? const int → 数据不可变


💡 速查表

你看到的写法 快速判断
int* const p const 在 * 右 → 指针不能改
const int* p const 在 * 左 → 数据不能改
int* const* p 第2层指针不能改
const int** p 最终数据不能改
int** const p 顶层指针不能改
const int* const* p 第2层指针不能改 + 最终数据不能改
const int* const* const p 全都不能改


✅ 总结

指针的级数由 * 的个数决定,const 只决定“哪一层不能被修改”。逐层看 const 和 * 的相对位置:const 在 * 左 → 数据不能改;const 在 * 右 → 指针本身不能改。 


7.6 指针和数组

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

**示例:

int main() {

    int arr[] = { 1,2,3,4,5,6,7,8,9,10 };

    int * p = arr;  //指向数组的指针,arr就是数组的首地址


    cout << "第一个元素: " << arr[0] << endl;
    cout << "指针访问第一个元素: " << *p << endl;

    for (int i = 0; i < 10; i++)
    {
        //利用指针遍历数组
        cout << *p << endl;
        p++;
    }

    system("pause");

    return 0;
}

7.7 指针和函数

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

//值传递
void swap1(int a ,int b)
{
    int temp = a;
    a = b; 
    b = temp;
}
//地址传递
void swap2(int * p1, int *p2)
{
    int temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}

int main() {

    int a = 10;
    int b = 20;
    swap1(a, b); // 值传递不会改变实参

    swap2(&a, &b); //地址传递会改变实参

    cout << "a = " << a << endl;

    cout << "b = " << b << endl;

    system("pause");

    return 0;
}

四、关键对比表格

对比项 值传递 (swap1) 地址传递 (swap2)
形参类型 int a, int b int *p1, int *p2
实参传递 传递变量的(10和20) 传递变量的地址(&a和&b)
内存分配 形参在栈上开辟新内存存储值 形参在栈上开辟新内存存储地址
是否影响实参 ❌ 不影响(副本独立) ✅ 影响(通过地址修改)
修改方式 直接修改形参的值 通过解引用*修改指向的内存
函数结束后 形参销毁,实参不变 形参销毁,实参已被修改
内存占用 形参占用额外内存(8字节) 指针占用额外内存(8字节)
安全性 安全(不会意外修改外部) 需要小心(可能造成副作用)

总结:如果不想修改实参,就用值传递,如果想修改实参,就用地址传递

7.8 指针、数组、函数

**案例描述:**封装一个函数,利用冒泡排序,实现对整型数组的升序排序

例如数组:int arr[10] = { 4,3,6,9,1,2,10,8,7,5 };

**示例:

#include <iostream>        // 1. 包含输入输出流头文件,提供 cout、endl 等标准输入输出工具
using namespace std;       // 2. 使用标准命名空间 std,可以直接写 cout、endl,而不需要写成 std::cout、std::endl

// ============================================================
// 冒泡排序函数
// ============================================================

// 3. ★ 冒泡排序函数:对整型数组进行从小到大的排序
//    参数1:int * arr → 接收数组的首地址(指针)
//           等价写法:int arr[],两者在函数参数中完全等价,都表示接收一个整型数组的地址
//    参数2:int len  → 数组的长度(元素个数)
void bubbleSort(int * arr, int len)
{
    // 4. ★ 外层循环:控制排序的轮数
    //    共需要 len-1 轮,每轮将当前未排序部分的最大值"冒泡"到正确位置
    for (int i = 0; i < len - 1; i++)
    {
        // 5. ★ 内层循环:控制每轮比较和交换的次数
        //    len - 1 - i 表示每轮比较到未排序部分的倒数第二个元素
        //    随着轮数 i 增加,右侧已排好的元素增多,比较次数减少
        for (int j = 0; j < len - 1 - i; j++)
        {
            // 6. 如果当前元素比后一个元素大(需要交换)
            if (arr[j] > arr[j + 1])
            {
                // 7. ★ 交换 arr[j] 和 arr[j+1] 的位置
                int temp = arr[j];      // 8. 将 arr[j] 保存到临时变量
                arr[j] = arr[j + 1];    // 9. 将 arr[j+1] 的值赋给 arr[j]
                arr[j + 1] = temp;      // 10. 将临时变量(原 arr[j])赋给 arr[j+1]
            }
        }
    }
}

// ============================================================
// 打印数组函数
// ============================================================

// 11. 打印数组函数:逐行输出数组的所有元素
//     参数1:int arr[] → 接收数组首地址(等价于 int * arr)
//     参数2:int len   → 数组的长度
void printArray(int arr[], int len)
{
    // 12. 遍历数组,从 0 到 len-1
    for (int i = 0; i < len; i++)
    {
        cout << arr[i] << endl;   // 13. 输出第 i 个元素并换行
    }
}

// ============================================================
// 主函数
// ============================================================

int main() {                      // 14. 程序入口函数

    // 15. 定义一个包含 10 个整数的数组,并初始化为无序序列
    int arr[10] = { 4,3,6,9,1,2,10,8,7,5 };

    // 16. ★ 计算数组长度
    //     sizeof(arr):整个数组占用的总字节数(10 * 4 = 40 字节)
    //     sizeof(int):单个 int 占 4 字节
    //     相除得到元素个数:10
    int len = sizeof(arr) / sizeof(int);

    // 17. ★ 调用冒泡排序函数
    //     直接传入数组名 arr,它代表数组的首地址
    //     排序函数会直接修改原数组,无需返回值
    bubbleSort(arr, len);

    // 18. ★ 调用打印数组函数
    //     传入排序后的数组和长度,输出排序结果
    printArray(arr, len);

    system("pause");   // 19. 暂停控制台窗口,等待用户按键
    return 0;          // 20. 程序正常退出,返回 0 表示执行成功
}

总结:当数组名传入到函数作为参数时,被退化为指向首元素的指针

8 结构体

8.1 结构体基本概念

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

8.2 结构体定义和使用

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

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

1.struct 结构体名 变量名
2.struct 结构体名 变量名 = { 成员1值 , 成员2值...}
3.定义结构体时顺便创建变量

示例:

//结构体定义
struct student
{
    //成员列表
    string name;  //姓名
    int age;      //年龄
    int score;    //分数
}stu3; //结构体变量创建方式3 


int main() {

    //结构体变量创建方式1
    struct student stu1; //struct 关键字可以省略

    stu1.name = "张三";
    stu1.age = 18;
    stu1.score = 100;

    cout << "姓名:" << stu1.name << " 年龄:" << stu1.age  << " 分数:" << stu1.score << endl;

    //结构体变量创建方式2
    struct student stu2 = { "李四",19,60 };

    cout << "姓名:" << stu2.name << " 年龄:" << stu2.age  << " 分数:" << stu2.score << endl;


    stu3.name = "王五";
    stu3.age = 18;
    stu3.score = 80;


    cout << "姓名:" << stu3.name << " 年龄:" << stu3.age  << " 分数:" << stu3.score << endl;

    system("pause");

    return 0;
}
#include <iostream>
#include <cstring>
#include <bits/stdc++.h>
#include "swap.h"
using namespace std;

//创建学生数据类型:学生包括(姓名,年龄,分数)
//自定义数据类型,一些类型集合组成的一个类型
//语法 struct 类型名称{成员列表}
struct Student {
	//成员列表

	//姓名
	string name;
	//年龄
	int age;
	//分数
	int score;
}s3;


int main() {
	//2.通过学生类型创建具体学生
	//2.1   struct Student s1
	struct Student s1;//struct创建时可以省略
	
	//给s1属性赋值,通过.访问结构体变量中的属性
	s1.name = "张三";
	s1.age = 18;
	s1.score = 100;
	cout << "姓名: " << s1.name << " 年龄: " << s1.age << " 分数: " << s1.score << endl;
	
	//2.2  struct Student s2 = {...}
	struct Student s2 = { "李四",19,80 };
	cout << "姓名: " << s2.name << " 年龄: " << s2.age << " 分数: " << s2.score << endl;

	//2.3 在定义结构体时顺便创建结构体变量
	s3.name = "王五";
	s3.score = 90;
	s3.age = 18;
	cout << "姓名: " << s3.name << " 年龄: " << s3.age << " 分数: " << s3.score << endl;

    
	system("pause");
	return 0;

}

> 总结1:定义结构体时的关键字是struct,不可省略

> 总结2:创建结构体变量时,关键字struct可以省略

> 总结3:结构体变量利用操作符 ''.'' 访问成员

8.3 结构体数组

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

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

示例:

//结构体定义
struct student
{
    //成员列表
    string name;  //姓名
    int age;      //年龄
    int score;    //分数
}

int main() {

    //结构体数组
    struct student arr[3]=
    {
        {"张三",18,80 },
        {"李四",19,60 },
        {"王五",20,70 }
    };

    for (int i = 0; i < 3; i++)
    {
        cout << "姓名:" << arr[i].name << " 年龄:" << arr[i].age << " 分数:" << arr[i].score << endl;
    }

    system("pause");

    return 0;
}

8.4 结构体指针

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

* 利用操作符 `-> `可以通过结构体指针访问结构体属性

**示例:

//结构体定义
struct student
{
    //成员列表
    string name;  //姓名
    int age;      //年龄
    int score;    //分数
};


int main() {

    struct student stu = { "张三",18,100, };

    struct student * p = &stu;

    p->score = 80; //指针通过 -> 操作符可以访问成员

    cout << "姓名:" << p->name << " 年龄:" << p->age << " 分数:" << p->score << endl;

    system("pause");

    return 0;
}

总结:结构体指针可以通过 -> 操作符 来访问结构体中的成员

8.5 结构体嵌套结构体

**作用:** 结构体中的成员可以是另一个结构体

**例如:**每个老师辅导一个学员,一个老师的结构体中,记录一个学生的结构体

**示例:

//学生结构体定义
struct student
{
    //成员列表
    string name;  //姓名
    int age;      //年龄
    int score;    //分数
};

//教师结构体定义
struct teacher
{
    //成员列表
    int id; //职工编号
    string name;  //教师姓名
    int age;   //教师年龄
    struct student stu; //子结构体 学生
};


int main() {

    struct teacher t1;
    t1.id = 10000;
    t1.name = "老王";
    t1.age = 40;

    t1.stu.name = "张三";
    t1.stu.age = 18;
    t1.stu.score = 100;

    cout << "教师 职工编号: " << t1.id << " 姓名: " << t1.name << " 年龄: " << t1.age << endl;

    cout << "辅导学员 姓名: " << t1.stu.name << " 年龄:" << t1.stu.age << " 考试分数: " << t1.stu.score << endl;

    system("pause");

    return 0;
}

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

8.6 结构体做函数参数

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

传递方式有两种:

* 值传递
* 地址传递

传递方式 写法 传递内容 是否复制数据 能否修改原数据
值传递 void func(student s) 整个结构体的副本 ✅ 复制全部数据 ❌ 不能
指针传递 void func(student* s) 结构体的地址(8字节) ❌ 只复制指针 ✅ 能
引用传递 void func(student& s) 结构体的别名 ❌ 不复制 ✅ 能
对比项 值传递 指针传递 引用传递
语法 func(student s) func(student* s) func(student& s)
调用方式 func(stu) func(&stu) func(stu)
复制开销 大(复制全部数据) 小(只复制指针) 小(不复制)
能否修改原数据
是否可能为空 不可能 可能(传 NULL) 不可能
安全性 高(不会误改) 中(需判空) 高(不会为空)
适用场景 小结构体、只读 需要修改、可能为空 需要修改、不能为空

**示例:

#include <iostream>      // ① 包含输入输出流头文件,提供 cout、endl 等标准输入输出工具
#include <string>        // ② 包含 string 头文件,提供 string 类型支持
using namespace std;     // ③ 使用标准命名空间 std,可以直接写 cout、endl、string

// ============================================================
// ④ 定义学生结构体
//    结构体是一种自定义数据类型,可以组合多个不同类型的成员
// ============================================================
struct student
{
    // ⑤ 成员列表:结构体中包含的变量
    string name;   // ⑥ 姓名(string 类型)
    int age;       // ⑦ 年龄(int 类型)
    int score;     // ⑧ 分数(int 类型)
};

// ============================================================
// ⑨ 值传递函数
//    参数:student stu — 传入了整个结构体的副本
//    特点:函数内修改的是副本,不影响 main 中的原始数据
// ============================================================
void printStudent(student stu)   // ⑩ stu 是 main 中 stu 的一份完整拷贝
{
    stu.age = 28;                // ⑪ 修改的是副本中的 age,不影响 main 中的原始数据
    cout << "子函数中 姓名:" << stu.name          // ⑫ 输出副本中的姓名
         << " 年龄: " << stu.age                // ⑬ 输出副本中的年龄(已被改为 28)
         << " 分数:" << stu.score << endl;      // ⑭ 输出副本中的分数
}   // ⑮ 函数结束,副本 stu 被销毁(析构)

// ============================================================
// ⑯ 指针传递函数(地址传递)
//    参数:student* stu — 传入的是 main 中 stu 的地址
//    特点:通过指针直接修改原始数据
// ============================================================
void printStudent2(student* stu)   // ⑰ stu 是一个指针,存储的是 main 中 stu 的地址
{
    stu->age = 28;                 // ⑱ ★ 通过 -> 访问结构体成员,直接修改 main 中 stu 的 age
    cout << "子函数中 姓名:" << stu->name          // ⑲ 输出原始数据中的姓名
         << " 年龄: " << stu->age                // ⑳ 输出原始数据中的年龄(已被改为 28)
         << " 分数:" << stu->score << endl;      // ㉑ 输出原始数据中的分数
}   // ㉒ 函数结束,指针 stu 被销毁,但 main 中的 stu 已经被修改了

// ============================================================
// ㉓ 主函数(程序入口)
// ============================================================
int main() {

    // ㉔ 定义一个 student 类型的变量 stu,并初始化
    //     姓名:"张三",年龄:18,分数:100
    student stu = { "张三", 18, 100 };

    // ============================================================
    // ㉕ ★ 值传递调用
    //    调用 printStudent,传入 stu(值传递,复制一份)
    //    main 中的 stu 不会被修改
    // ============================================================
    printStudent(stu);   // ㉖ 传入 stu 的副本(复制全部数据)
    // ㉗ 子函数输出:姓名:张三 年龄:28 分数:100
    cout << "主函数中 姓名:" << stu.name          // ㉘ 输出 main 中的 stu 的姓名
         << " 年龄: " << stu.age                 // ㉙ 输出 main 中的 stu 的年龄(仍然是 18!)
         << " 分数:" << stu.score << endl;       // ㉚ 输出 main 中的 stu 的分数

    cout << endl;    // ㉛ 输出一个空行,便于阅读

    // ============================================================
    // ㉜ ★ 指针传递调用(地址传递)
    //    调用 printStudent2,传入 &stu(传入地址,只传 8 字节)
    //    main 中的 stu 会被直接修改
    // ============================================================
    printStudent2(&stu);   // ㉝ ★ 传入 stu 的地址(& 取地址符)
    // ㉞ 子函数输出:姓名:张三 年龄:28 分数:100
    cout << "主函数中 姓名:" << stu.name          // ㉟ 输出 main 中的 stu 的姓名
         << " 年龄: " << stu.age                 // ㊱ 输出 main 中的 stu 的年龄(已经被改为 28!)
         << " 分数:" << stu.score << endl;       // ㊲ 输出 main 中的 stu 的分数

    system("pause");   // ㊳ 暂停控制台窗口,等待用户按键
    return 0;          // ㊴ 程序正常退出,返回 0 表示执行成功
}

总结:如果不想修改主函数中的数据,用值传递,反之用地址传递

8.7 结构体中const使用场景

**作用:**用const来防止误操作

**示例:

#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;

//学生结构体定义
struct student
{
    //成员列表
    string name;  //姓名
    int age;      //年龄
    int score;    //分数
};

//const使用场景
void printStudent(const student* stu) //加const防止函数体中的误操作
{
    //stu->age = 100; //操作失败,因为加了const修饰
    cout << "姓名:" << stu->name << " 年龄:" << stu->age << " 分数:" << stu->score << endl;

}

int main() {

    student stu = { "张三",18,100 };

    printStudent(&stu);

    system("pause");

    return 0;
}

"表达式必须是可修改的左值" 的含义

什么是左值(Lvalue)?

  • 左值:有明确内存地址的表达式,可以出现在赋值号的左边

  • 右值:临时值,没有固定地址,只能出现在赋值号的右边

int a = 10;     // a 是左值(有地址),10 是右值(字面量)
a = 20;         // ✅ a 可以放在左边(可修改的左值)

10 = a;         // ❌ 错误!10 不是左值(不能修改常量)

int b = a;      // ✅ a 可以放在右边(读取值)

8.8 结构体案例

8.8.1 案例1

案例描述:

学校正在做毕设项目,每名老师带领5个学生,总共有3名老师,需求如下

设计学生和老师的结构体,其中在老师的结构体中,有老师姓名和一个存放5名学生的数组作为成员,学生的成员有姓名、考试分数,创建数组存放3名老师,通过函数给每个老师及所带的学生赋值,最终打印出老师数据以及老师所带的学生数据。

示例:

#include <iostream>      // 引入输入输出流头文件,提供cout、endl等标准输入输出功能
#include <string>        // 引入字符串头文件,提供string类型的支持
#include <ctime>         // 引入时间头文件,提供time()函数用于获取当前时间,作为随机数种子
using namespace std;     // 使用标准命名空间std,简化代码书写(如std::cout可简写为cout)

// 定义学生结构体,用于存储学生的姓名和分数
struct Student
{
    string name;         // 学生姓名,使用string类型存储
    int score;           // 学生分数,使用整型存储
};

// 定义老师结构体,用于存储老师姓名和他/她所带的学生数组
struct Teacher
{
    string name;         // 老师姓名
    struct Student sArray[5];  // 学生数组,每个老师带5名学生(struct关键字可省略)
};

// 给老师和学生赋值的函数
// 参数:tArray - 老师数组(传递指针/地址),len - 数组长度
void allocateSpace(Teacher tArray[], int len)
{
    string tName = "教师";      // 老师姓名的前缀
    string sName = "学生";      // 学生姓名的前缀
    string nameSeed = "ABCDE";  // 用于生成不同名字的字母种子
    
    // 外层循环:遍历每一位老师(len = 3,共3位老师)
    for (int i = 0; i < len; i++)
    {
        // 为当前老师设置姓名:教师 + A/B/C
        // 例如:教师A、教师B、教师C
        tArray[i].name = tName + nameSeed[i];

        // 内层循环:为当前老师所带的5名学生赋值
        for (int j = 0; j < 5; j++)
        {
            // 为学生设置姓名:学生 + A/B/C/D/E
            // 例如:学生A、学生B、学生C、学生D、学生E
            tArray[i].sArray[j].name = sName + nameSeed[j];
            
            // 为学生随机生成分数:范围 40~100
            // rand() % 61 生成 0~60,+40 后变为 40~100
            tArray[i].sArray[j].score = rand() % 61 + 40;
        }
    }
}

// 打印所有老师及所带学生信息的函数
// 参数:tArray - 老师数组,len - 数组长度
void printTeachers(Teacher tArray[], int len)
{
    // 外层循环:遍历每一位老师
    for (int i = 0; i < len; i++)
    {
        // 输出老师的姓名
        cout << tArray[i].name << endl;
        
        // 内层循环:遍历当前老师所带的5名学生
        for (int j = 0; j < 5; j++)
        {
            // 输出学生信息,\t是制表符用于对齐,使输出更美观
            // 输出格式:姓名:XXX 分数:XX
            cout << "\t姓名:" << tArray[i].sArray[j].name 
                 << " 分数:" << tArray[i].sArray[j].score << endl;
        }
    }
}

int main() {  // 主函数,程序入口

    // 设置随机数种子为当前时间戳,确保每次运行程序生成的随机数不同
    // time(NULL)返回从1970年1月1日到现在的秒数
    // (unsigned int)强制类型转换,匹配srand的参数类型
    // 需要包含头文件 <ctime>
    srand((unsigned int)time(NULL));

    // 创建包含3个老师的数组(未初始化,后续通过函数赋值)
    Teacher tArray[3];

    // 计算数组长度:总字节数 / 单个元素字节数 = 3
    // sizeof(tArray)返回整个数组占用的字节数
    // sizeof(Teacher)返回单个Teacher对象占用的字节数
    int len = sizeof(tArray) / sizeof(Teacher);

    // 调用函数为所有老师和学生赋值(创建数据)
    // 传递数组名(即数组首地址)和长度,函数内部修改会影响原始数组
    allocateSpace(tArray, len);

    // 调用函数打印所有老师和学生的信息
    printTeachers(tArray, len);

    // 调用系统命令暂停程序,显示"按任意键继续..."(仅Windows有效)
    // 用于调试时保持控制台窗口打开,方便查看输出结果
    system("pause");

    // 主函数返回0,表示程序正常结束
    return 0;
}

8.8.2 案例2

案例描述:

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

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

五名英雄信息如下:

        {"刘备",23,"男"},
        {"关羽",22,"男"},
        {"张飞",20,"男"},
        {"赵云",21,"男"},
        {"貂蝉",19,"女"},

**示例:

#include <iostream>      // 引入输入输出流头文件,提供cout、endl等标准输入输出功能
#include <string>        // 引入字符串头文件,提供string类型的支持
using namespace std;     // 使用标准命名空间std,简化代码书写

// 定义英雄结构体,用于存储英雄的姓名、年龄和性别
struct Hero
{
    string name;         // 英雄姓名,使用string类型存储
    int age;             // 英雄年龄,使用整型存储
    string sex;          // 英雄性别,使用string类型存储("男"或"女")
};

// 冒泡排序函数:按照英雄年龄从小到大排序(升序)
// 参数:heroArray - 英雄数组(传递指针/地址),len - 数组长度
void bubbleSort(struct Hero heroArray[], int len)
{
    // 外层循环:控制排序的轮数,每轮将最大的元素"冒泡"到最后
    // 共需要 len-1 轮(因为最后一轮只剩1个元素,无需比较)
    for (int i = 0; i < len - 1; i++)
    {
        // 内层循环:在每轮中进行相邻元素的比较和交换
        // j < len - i - 1:每轮结束后,最后i个元素已经排好序,无需再比较
        for (int j = 0; j < len - i - 1; j++)
        {
            // 如果当前英雄的年龄大于下一个英雄的年龄
            // 则交换两个元素的位置(实现升序排序)
            if (heroArray[j].age > heroArray[j + 1].age)
            {
                // 创建临时变量存储当前英雄(备份)
                struct Hero temp = heroArray[j];
                // 将下一个英雄赋值给当前位置
                heroArray[j] = heroArray[j + 1];
                // 将备份的当前英雄赋值给下一个位置
                heroArray[j + 1] = temp;
                // 完成交换,年龄大的元素往后移
            }
        }
    }
}

// 打印英雄数组信息的函数
// 参数:heroArray - 英雄数组,len - 数组长度
void printHero(struct Hero heroArray[], int len)
{
    // 遍历数组中的每一个英雄
    for (int i = 0; i < len; i++)
    {
        // 输出英雄的姓名、年龄和性别
        cout << "姓名: " << heroArray[i].name 
             << " 年龄: " << heroArray[i].age 
             << " 性别:" << heroArray[i].sex << endl;
    }
}

int main() {  // 主函数,程序入口

    // 1. 创建并初始化包含5名英雄的结构体数组
    // 使用列表初始化方式,按照姓名、年龄、性别的顺序赋值
    struct Hero heroArray[5] =
    {
        {"刘备", 23, "男"},   // 索引0:刘备,23岁,男
        {"关羽", 22, "男"},   // 索引1:关羽,22岁,男
        {"张飞", 20, "男"},   // 索引2:张飞,20岁,男
        {"赵云", 21, "男"},   // 索引3:赵云,21岁,男
        {"貂蝉", 19, "女"}    // 索引4:貂蝉,19岁,女
    };

    // 2. 计算数组长度:总字节数 / 单个元素字节数 = 5
    int len = sizeof(heroArray) / sizeof(heroArray[0]);

    // 打印排序前的英雄信息
    cout << "排序前打印:" << endl;
    // 遍历数组,逐个输出英雄信息
    for (int i = 0; i < len; i++)
    {
        cout << "姓名: " << heroArray[i].name 
             << " 年龄: " << heroArray[i].age 
             << " 性别:" << heroArray[i].sex << endl;
    }

    // 3. 调用冒泡排序函数,按照年龄从小到大排序(升序)
    bubbleSort(heroArray, len);

    // 4. 打印排序后的英雄信息
    cout << "排序后打印:" << endl;
    printHero(heroArray, len);  // 调用打印函数输出结果

    system("pause");  // 调用系统命令暂停程序(仅Windows有效),方便查看输出结果
    return 0;         // 主函数返回0,表示程序正常结束
}

更多推荐