摘要:9 月 25 日的 C++ 学习记录。今天集中攻克三个基础模块——二维数组、函数封装、指针操作,把 4 个练习程序全部手敲并跑通;最后刷了两道"小鱼"系列算法题(双休游泳里程、游泳用时计算)。本文包含完整可运行代码、样例手推过程、复杂度分析,以及我实际踩到的 5 个坑。
0. 今天干了什么
先上清单,方便复盘:
| 序号 | 模块 | 具体内容 | 代码文件 |
|---|---|---|---|
| 1 | 二维数组 | 3×3 矩阵的输入与求和、行主序内存布局 | 10.cpp |
| 2 | 函数 | 冒泡排序与数组打印的函数封装、数组参数退化 | maopaopaixu.cpp |
| 3 | 指针 | 指针遍历数组、const与指针、值传递 vs 指针传递 | zzhsz.cpp、zhizheng.cpp、zzhhs.cpp |
| 4 | 算法题 | 小鱼游泳(双休版)、小鱼游泳时间 | 8.cpp、9.cpp |
环境:VS Code + MinGW-w64 的g++,工作目录C:\Users\ASUS\Desktop\vs。
1. 二维数组:3×3 矩阵求和
1.1 题目
读入一个 3×3 的整数矩阵,输出它们的元素之和。
1.2 我的代码(10.cpp)
cpp
复制
#include <iostream> using namespace std; int main() { int s[3][3]; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { cin >> s[i][j]; } } int sum = 0; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { sum += s[i][j]; } cout << sum << endl; } return 0; }1.3 二维数组的本质:数组的数组
int s[3][3]不是"3 乘 3 的表格",而是"长度为 3 的数组,每个元素是int[3]"。所以:
s[i]的类型是int[3],s[i][j]的类型才是int;- 下标写法
s[i][j]完全等价于指针写法*(*(s + i) + j); - 在内存里它是连续的行主序排列:
s[0][0] s[0][1] s[0][2] s[1][0] ... s[2][2],一共 9 个int紧挨着放。这一点在后面讲指针遍历时会用到。
顺便,sizeof(s)是3 * 3 * 4 = 36字节,sizeof(s[0])是 12 字节,sizeof(s[0][0])是 4 字节。
1.4 复盘:一个很隐蔽的"没清零"逻辑问题
上面这段代码如果输入:
复制
1 2 3 4 5 6 7 8 9实际输出是:
复制
6 21 45而不是我原本以为的45。
原因在于sum定义在外层循环之外,而cout放在了外层循环之内——于是它打印的不是"每行之和",而是"前 i 行的累计和":第一轮1+2+3=6;第二轮6+(4+5+6)=6+15=21;第三轮21+(7+8+9)=21+24=45。也就是说,第一次循环结束时sum是 6,第二次结束是 21,所以中间的21看起来既不像行和(15)也不像总和(45),非常容易让人误以为是"算错了"。
这个 bug 很有意思:代码语法完全正确,编译零警告,但语义和我的意图不一致。它逼我想清楚了"变量该定义在哪个作用域"这个平时被忽略的问题。
版本 A:只求总和(推荐)
cpp
复制
int sum = 0; for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) sum += s[i][j]; cout << sum << endl; // 输出 45版本 B:每行求和(sum必须在外层循环内部清零)
cpp
复制
for (int i = 0; i < 3; i++) { int sum = 0; // ← 关键:每进入新的一行就归零 for (int j = 0; j < 3; j++) { sum += s[i][j]; } cout << "第 " << i + 1 << " 行之和:" << sum << endl; }📷配图位置 1:贴
10.cpp的编辑器截图。
1.5 小结
- 二维数组的遍历一定要"外层行、内层列",顺序写反不会报错,但缓存友好度差很多;
- 累加器变量的作用域,就是它应当被清零的边界;
cout放循环里还是循环外,决定了输出"过程值"还是"最终值"。
2. 函数:把重复逻辑装进盒子
2.1 冒泡排序封装(maopaopaixu.cpp)
cpp
复制
#include <bits/stdc++.h> using namespace std; void buccleSort(int *arr, int len) { for (int i = 0; i < len - 1; i++) { for (int j = 0; j < len - i - 1; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } } void printArray(int *arr, int len) { for (int i = 0; i < len; i++) { cout << arr[i] << " "; } cout << endl; } int main() { int arr[10] = {4, 5, 6, 4, 5, 3, 23, 45, 65, 34}; int len = sizeof(arr) / sizeof(arr[0]); buccleSort(arr, len); printArray(arr, len); return 0; }运行结果:
复制
3 4 4 5 5 6 23 34 45 652.2 三个必须记住的要点
要点一:数组作参数会"退化"成指针。
void buccleSort(int *arr, int len)里的arr已经不是数组了,而是一个int *指针。所以:
- 函数内部
sizeof(arr)得到的是指针的大小(64 位下 8 字节),不是 40 字节; sizeof(arr) / sizeof(arr[0])在函数里会算出8 / 4 = 2,这是一个经典到可以进教科书的错误。
所以长度必须由调用方显式传进来,这就是len参数存在的唯一理由。
| 位置 | sizeof(arr) | sizeof(arr)/sizeof(arr[0]) |
|---|---|---|
main中的int arr[10] | 40 | 10 ✅ |
函数参数int *arr | 8 | 2 ❌ |
要点二:函数参数是"值传递"。
arr这个指针是按值复制进来的,函数里改arr = arr + 1不影响外面。但arr[j] = ...改的是指针指向的内存,那块内存在main的栈上,所以外面的数组真的被改动了。这是"值传递却修改了实参"的唯一解释:改的不是指针,是指针指的东西。
要点三:取名要准。
函数名buccleSort我敲错了,正确拼写是bubbleSort(冒泡)。虽然不影响运行,但一周后回看会很别扭,当场改掉:
cpp
复制
void bubbleSort(int *arr, int len)2.3 一个小优化
冒泡排序最好的情况(已经有序)本可以只跑一趟。加一个flag就实现了提前退出:
cpp
复制
void bubbleSort(int *arr, int len) { for (int i = 0; i < len - 1; i++) { bool swapped = false; for (int j = 0; j < len - i - 1; j++) { if (arr[j] > arr[j + 1]) { swap(arr[j], arr[j + 1]); // 标准库自带 swap swapped = true; } } if (!swapped) break; // 本轮无交换 → 已经有序 } }- 最坏 / 平均时间复杂度:
O(n²) - 最好情况(已有序):
O(n) - 空间复杂度:
O(1),原地排序 - 稳定性:相等元素不交换,稳定排序
📷配图位置 2:贴
maopaopaixu.cpp的编辑器截图。
3. 指针:C++ 的分水岭
3.1 数组名就是首元素地址(zzhsz.cpp)
cpp
复制
#include <bits/stdc++.h> using namespace std; int main() { int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; cout << "第一个元素为:" << arr[0] << endl; int *p = arr; // 数组名退化为首元素地址 cout << "利用指针访问第一个元素为:" << *p << endl; p++; // 指针后移一个 int(4 字节) cout << "利用指针访问第二个元素为:" << *p << endl; cout << "利用指针遍历数组为:" << endl; int *p2 = arr; for (int i = 0; i < 10; i++) { // cout << arr[i] << endl; // 下标法,等价写法 cout << *p2 << endl; // 指针法 p2++; } return 0; }输出:
复制
第一个元素为:1 利用指针访问第一个元素为:1 利用指针访问第二个元素为:2 利用指针遍历数组为: 1 2 ... 10三个关键结论:
arr在表达式中会隐式转换为&arr[0],所以int *p = arr;合法;p++不是加 1 字节,而是加sizeof(int) = 4字节——指针的算术运算自带类型步长;- 循环里的
*p2与arr[i]完全等价(arr[i]就是*(arr + i)的语法糖)。
3.2const与指针的三种组合(zhizheng.cpp)
cpp
复制
#include <bits/stdc++.h> using namespace std; int main() { int a = 10; int b = 20; const int *p = &a; // p 指向"const int",即 *p 不能被改 p = &b; // 但 p 自己可以被改,指向别处 cout << *p << endl; // 输出 20 return 0; }输出20。这说明const int *p里,const修饰的是*p,不是p。
三种组合一定要分清,口诀是"const 在*左边管值,在*右边管指针":
| 写法 | 能否改*p | 能否改p | 记忆 |
|---|---|---|---|
const int *p(等价int const *p) | ❌ | ✅ | 指向常量的指针 |
int * const p | ✅ | ❌ | 指针常量,必须在定义时初始化 |
const int * const p | ❌ | ❌ | 都锁死 |
const int *p最常见的用途是函数参数:void print(const int *arr, int len)—— 向调用者承诺"我只读,不改你的数组",同时让函数能接收const数组。
📷配图位置 3:贴
zhizheng.cpp的编辑器截图,以及终端输出20的那一屏。
3.3 值传递 vs 指针传递(zzhhs.cpp)—— 今天最有收获的一段
cpp
复制
#include <bits/stdc++.h> using namespace std; void swap01(int a, int b) { // 值传递:操作的是副本 int temp = a; a = b; b = temp; cout << "a=" << a << endl; // 10 20 → 这里打印 20 cout << "b=" << b << endl; // 这里打印 10 } void swap02(int *a, int *b) { // 指针传递:操作的是原变量 int temp = *a; *a = *b; *b = temp; cout << "*a=" << *a << endl; cout << "*b=" << *b << endl; } int main() { int a = 10; int b = 20; swap01(a, b); cout << "a=" << a << endl; // 仍然是 10 ← 没换成功! cout << "b=" << b << endl; // 仍然是 20 swap02(&a, &b); cout << "a=" << a << endl; // 20 ← 成功 cout << "b=" << b << endl; // 10 }实际输出(共 8 行):
复制
a=20 ← swap01 内部:副本被换过来了 b=10 a=10 ← 回到 main:原变量纹丝不动 b=20 *a=20 ← swap02 内部:通过地址改的就是本体 *b=10 a=20 ← 回到 main:真的换成了 b=10swap01是最典型的"看起来成功了"陷阱:函数内部打印的确是交换后的结果,很容易让人以为大功告成;但一出函数,main里的a、b纹丝不动。因为swap01拿到的是两个副本,副本怎么折腾都与本体无关。
内存层面的解释:
swap01(a, b):形参a、b是main里a、b的拷贝,各自占独立内存。交换副本,本体不动。swap02(&a, &b):传进去的是地址。形参*a解引用后直接命中main的变量,*a = *b改的是本体内存,所以真的换成了。
延伸:C++ 更推荐的写法是引用。
cpp
复制
void swap03(int &a, int &b) { // 引用:实参的别名,无需解引用 int temp = a; a = b; b = temp; } // 调用:swap03(a, b); —— 和值传递写法一样干净,但真的能改引用写法兼具"指针的能力"和"值传递的简洁",这也是为什么 C++ 标准库的std::swap用的是引用。
📷配图位置 4:贴
zzhhs.cpp的编辑器截图(含swap01/swap02两段)。
3.4 顺手解决的一个环境问题:终端中文乱码
在 VS Code 的 PowerShell 终端里跑zzhsz.exe,中文输出会变成乱码(类似掰弯弯 消回匣 ...这种)。原因是源码是 UTF-8,而 Windows 终端默认是 GBK(代码页 936)。
三种解决办法:
- 临时:先执行
chcp 65001切到 UTF-8 代码页; - 一劳永逸:在 VS Code 的
settings.json里设置"terminal.integrated.defaultProfile.windows"对应的 profile 参数,或直接在终端配置里加chcp 65001; - 学习阶段最省事:调试输出先用英文,等逻辑跑通再换中文。
编译运行的命令也记一下:
powershell
复制
g++ zzhsz.cpp -o zzhsz # 编译 .\zzhsz # 运行(PowerShell 里必须带 .\)4. 算法题一:小鱼游泳(双休版)
4.1 题目
有一只小鱼,它平日每天游泳 250 公里,周末休息(实行双休日)。假设从周 x 开始算起,过了 n 天以后,小鱼一共累计游泳了多少公里?
- 输入:两个正整数
x, n(1 ≤ x ≤ 7,1 ≤ n ≤ 10⁶),表示从周 x 算起,经过 n 天。 - 输出:一个整数,表示累计游泳公里数。
- 样例:输入
3 10→ 输出2000。
4.2 解法一:模拟(8.cpp)
cpp
复制
#include <bits/stdc++.h> using namespace std; int main() { int x, n; cin >> x >> n; int ans = 0; for (int i = 0; i < n; i++) { int day = (x + i - 1) % 7 + 1; // 1~7 表示 周一~周日 if (day != 6 && day != 7) { // 6,7 为周六、周日 ans += 250; } } cout << ans << endl; return 0; }核心就一行:(x + i - 1) % 7 + 1。
i = 0时结果是(x - 1) % 7 + 1 = x,对得上起点;- 当
x = 7(周日)时,(7 - 1) % 7 + 1 = 7,不会溢出成 0,也不会变成 8。
这个"先减一、取模、再加一"的写法,是把1~7这种 1-based 循环编号映射到[0, 6]的通用套路,值得背下来。
样例手推(x = 3 表示周三,n = 10):
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|---|
| 星期 | 三 | 四 | 五 | 六 | 日 | 一 | 二 | 三 | 四 | 五 |
| 是否游泳 | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
共 8 天游泳,8 × 250 = 2000,与样例一致。
复杂度:时间O(n),n ≤ 10⁶完全够用;空间O(1)。
4.3 解法二:数学法(O(1))
把这 n 天拆成"完整的周 + 零头":每 7 天必定是 5 个工作日,零头最多 6 天,直接枚举零头即可。
cpp
复制
#include <bits/stdc++.h> using namespace std; int main() { int x, n; cin >> x >> n; int start = (x - 1) % 7; // 0=周一 ... 6=周日 long long weeks = n / 7; int rem = n % 7; long long ans = weeks * 5LL * 250; // 每个完整周 5 天 for (int i = 0; i < rem; i++) { // 零头最多 6 天 if ((start + i) % 7 < 5) ans += 250; } cout << ans << endl; return 0; }为什么不直接用闭式公式?因为"零头里有几个工作日"要分start落在周一~周五还是周末讨论,很容易写错边界。枚举至多 6 次既保证O(1),又不需要分类讨论——这是竞赛里非常实用的"常数级枚举替代分类讨论"技巧。
两种解法对比:
| 解法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 逐天模拟 | O(n) | O(1) | n 小时最直观,不易错 |
| 整周 + 零头 | O(1) | O(1) | n 很大(如 10¹⁸)时必须用 |
为了确认两种解法完全等价,我把它们写成两个函数做了对拍:枚举x = 1…7、n = 1…2000,共14000 组数据,不一致数为 0;另外x = 1, n = 10⁶时两者都得178571500。写题时养成"换一种思路验证一遍"的习惯,比对着样例改半天靠谱得多。
4.4 易错点
- 周末是"周六 + 周日"两天,别只减一天;
x的起点是"从周 x 算起,也就是第 1 天就是周 x",不是"从周 x 的第二天算起";ans最大约2.5 × 10⁸,int装得下(int上限约2.1 × 10⁹),但数据范围一旦放大到10¹⁸就必须用long long,养成习惯更好。
5. 算法题二:小鱼游泳时间
5.1 题目
小鱼从a时b分一直游泳到当天的c时d分(24 小时制),求它这天一共游了多少小时多少分钟。
- 输入:一行四个整数
a, b, c, d。 - 输出:两个整数
e和f,依次表示小时数和分钟数,其中f < 60。 - 样例:输入
12 50 19 10→ 输出6 20。 - 数据范围:
0 ≤ a, c ≤ 24,0 ≤ b, d ≤ 60,且结束时间一定晚于开始时间。
5.2 代码(9.cpp)
cpp
复制
#include <bits/stdc++.h> using namespace std; int main() { int a, b, c, d; cin >> a >> b >> c >> d; int num1, num2; num1 = a * 60 + b; // 开始时刻,转成"从 0 点起的分钟数" num2 = c * 60 + d; // 结束时刻,同样转成分钟数 int ans = num2 - num1; // 总时长(分钟) cout << ans / 60 << " " << ans % 60 << endl; return 0; }5.3 思路:统一单位
时间差问题的通用套路是"先化成同一个最小单位,算完再拆回去":
- 把
a 时 b 分化成a * 60 + b分钟; - 两个分钟数相减,得到总分钟数
ans; ans / 60取整数商 = 小时数(整数除法自动向下取整);ans % 60取余 = 剩余分钟数,天然满足f < 60。
样例验证:12:50 → 12 × 60 + 50 = 770,19:10 → 19 × 60 + 10 = 1150,1150 - 770 = 380分钟;380 / 60 = 6,380 % 60 = 20,输出6 20,与样例一致 ✅
5.4 易错点
- 别用
(c - a)和(d - b)分开算:样例里19 - 12 = 7小时、10 - 50 = -40分钟,还得处理借位,非常容易写错。统一成分钟就完全没有借位问题; ans / 60与ans % 60的顺序:必须都基于同一个原始ans计算,不能先算/再用结果算%;- 输出格式:两个数之间一个空格,题目特别强调
f < 60,这正好是取模运算免费带来的保证; - 本题保证"结束时间晚于开始时间",所以不用处理跨天。但如果遇到
c < a的跨天情况,只需补一句if (num2 < num1) num2 += 24 * 60;。
6. 今日踩坑清单(Top 5)
| # | 坑 | 现象 | 修复 |
|---|---|---|---|
| 1 | 累加器作用域放错 | 矩阵求和输出6 / 15 / 45而非45 | 明确"清零边界":整体求和放循环外,按行求和放循环内 |
| 2 | 函数里算数组长度 | sizeof(arr)/sizeof(arr[0])在函数内得到 2 | 数组作参数会退化为指针,长度必须显式传参 |
| 3 | 误以为swap01交换成功 | 函数内打印已交换,main里没变 | 值传递只操作副本,改用指针或引用 |
| 4 | 误读const int *p | 以为p = &b会编译报错 | const修饰*p;const在*左边管值,右边管指针 |
| 5 | 终端中文乱码 | 输出掰弯弯 消回匣 ... | chcp 65001,或调试期先用英文输出 |
7. 知识点速查
二维数组
s[i][j]≡*(*(s + i) + j),内存行主序连续;- 遍历顺序"先行后列",累加器清零边界 = 它的作用域。
函数
- 数组作参数退化为指针,长度必须显式传递;
- C++ 参数默认值传递;要改实参用指针或引用(推荐引用);
- 函数名/变量名拼写要准,
bubbleSort不是buccleSort。
指针
- 数组名退化为首元素地址;
p++按sizeof(T)步进; const int *p/int * const p/const int * const p三者含义不同;- 指针传参能改实参,是因为改的是指向的内存,而不是指针本身。
算法
- 1-based 循环编号映射:
(x + i - 1) % 7 + 1; - 时间差统一换算成最小单位(分钟)再拆回;
n很大时,用"整周期 + 零头枚举"把O(n)降到O(1)。
8. 明日计划
- 把
bubbleSort的flag优化版重写一遍,并用随机数据对拍验证; - 学
const引用const int &,把printArray改成更安全的void printArray(const int *arr, int len); - 用指针重写二维数组的遍历(
*(*(s + i) + j)形式),加深"数组的数组"这一层理解; - 再刷 2 道循环 / 模拟类入门题,重点练"周期拆分"这个套路。
本文为个人 C++ 学习记录。全部代码均在本地 MinGW-w64g++ 16.1.0(
-std=c++17)下编译运行通过,文中的输入输出结果为实际运行结果,两道算法题的双解法已做 14000 组数据对拍验证。若有错误或更好的写法,欢迎在评论区指出,一起进步 🙌
标签建议:C++二维数组指针函数算法入门学习笔记