☰
【C++ 学习日记 · 9月25日】二维数组、函数、指针三件套,外加两道小鱼算法题
2026/9/26 2:08:23 网站建设 项目流程

摘要: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 65

2.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]4010 ✅
函数参数int *arr82 ❌

要点二:函数参数是"值传递"。

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

三个关键结论:

  1. arr在表达式中会隐式转换为&arr[0],所以int *p = arr;合法;
  2. p++不是加 1 字节,而是加sizeof(int) = 4字节——指针的算术运算自带类型步长;
  3. 循环里的*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=10

swap01是最典型的"看起来成功了"陷阱:函数内部打印的确是交换后的结果,很容易让人以为大功告成;但一出函数,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)。

三种解决办法:

  1. 临时:先执行chcp 65001切到 UTF-8 代码页;
  2. 一劳永逸:在 VS Code 的settings.json里设置"terminal.integrated.defaultProfile.windows"对应的 profile 参数,或直接在终端配置里加chcp 65001;
  3. 学习阶段最省事:调试输出先用英文,等逻辑跑通再换中文。

编译运行的命令也记一下:

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):

i0123456789
星期三四五六日一二三四五
是否游泳✅✅✅❌❌✅✅✅✅✅

共 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 思路:统一单位

时间差问题的通用套路是"先化成同一个最小单位,算完再拆回去":

  1. 把a 时 b 分化成a * 60 + b分钟;
  2. 两个分钟数相减,得到总分钟数ans;
  3. ans / 60取整数商 = 小时数(整数除法自动向下取整);
  4. 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. 明日计划

  1. 把bubbleSort的flag优化版重写一遍,并用随机数据对拍验证;
  2. 学const引用const int &,把printArray改成更安全的void printArray(const int *arr, int len);
  3. 用指针重写二维数组的遍历(*(*(s + i) + j)形式),加深"数组的数组"这一层理解;
  4. 再刷 2 道循环 / 模拟类入门题,重点练"周期拆分"这个套路。

本文为个人 C++ 学习记录。全部代码均在本地 MinGW-w64g++ 16.1.0(-std=c++17)下编译运行通过,文中的输入输出结果为实际运行结果,两道算法题的双解法已做 14000 组数据对拍验证。若有错误或更好的写法,欢迎在评论区指出,一起进步 🙌

标签建议:C++二维数组指针函数算法入门学习笔记

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询