# C++ 程序设计课讲义:流程控制、数组与字符串
**课时:3 小时**
**适用对象:C++ 入门到基础算法阶段学生**
## 一、课程安排
| 时间 | 内容 | 目标 |
| --- | --- | --- |
| 0:00 - 0:30 | 复习:顺序、分支、循环结构 | 能正确读写基本程序 |
| 0:30 - 1:00 | 嵌套循环 | 能用双层循环处理表格、枚举问题 |
| 1:00 - 1:20 | 调试程序 | 能定位常见编译错误、运行错误和逻辑错误 |
| 1:20 - 2:00 | 一维数组 | 会定义、读入、遍历、统计数组 |
| 2:00 - 2:30 | 桶技巧 | 能用“值作下标”统计频次、判重 |
| 2:30 - 3:00 | 一维字符数组 | 理解 C 风格字符串及常用函数 |
---
# 第一部分:顺序、分支、循环结构复习
## 问题引入
输入一个整数 `n`,输出:
- 如果 `n` 是正数,输出 `positive`
- 如果 `n` 是负数,输出 `negative`
- 如果 `n` 是 `0`,输出 `zero`
- 最后计算 `1 + 2 + ... + n`,当 `n > 0` 时输出和
这道题需要依次完成输入、判断、计算和输出,分别对应顺序、分支和循环结构。
## 知识点讲解
### 1. 顺序结构
程序从上到下依次执行。
```cpp
#include <iostream>
using namespace std;
int main() {
int a, b;
cin >> a >> b;
int sum = a + b;
cout << sum << endl;
return 0;
}
2. 分支结构
if
if (条件) {
条件成立时执行的语句;
}
if...else
if (条件) {
条件成立时执行;
} else {
条件不成立时执行;
}
多分支
if (n > 0) {
cout << "positive" << endl;
} else if (n < 0) {
cout << "negative" << endl;
} else {
cout << "zero" << endl;
}
3. 循环结构
for 循环
适合循环次数明确的情况。
for (int i = 1; i <= n; i++) {
cout << i << " ";
}
while 循环
适合“不知道具体循环多少次,但知道结束条件”的情况。
int x;
while (cin >> x) {
cout << x << endl;
}
如何应用
计算 1 到 n 的和:
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
if (n <= 0) {
cout << "n must be positive" << endl;
} else {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
cout << sum << endl;
}
return 0;
}
学习难点
- 理解条件表达式结果只有“成立”或“不成立”。
- 判断区间时要注意边界,例如
n >= 0与n > 0不同。 - 理解循环变量
i的变化过程。 - 明确循环体
{}中的语句会重复执行。
易错点
if (x = 5)
这里是赋值,不是比较。应写为:
if (x == 5)
循环条件写错:
for (int i = 1; i < n; i++)
这只能遍历 1 到 n - 1。若要遍历到 n,应写:
for (int i = 1; i <= n; i++)
存在的问题
- 输入
n = 0时,求和循环是否执行? - 输入负数时,是否应该计算
1到n的和? if条件中能否写多个条件?
问题提问
if (a > b)中,a == b时会进入哪个分支?for (int i = 0; i < 5; i++)一共执行几次?for循环中的i++是在循环体之前还是之后执行?- 如何输出
1到100中所有的偶数?
第二部分:嵌套循环
问题引入
输出一个 n × n 的乘法表。例如 n = 3:
1 2 3
2 4 6
3 6 9
一行中有多个数,且有多行,因此需要“循环里再套循环”。
知识点讲解
嵌套循环是一个循环体中包含另一个循环。
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cout << i * j << " ";
}
cout << endl;
}
其中:
- 外层循环变量
i控制行数。 - 内层循环变量
j控制每一行输出的列数。 - 每次外层循环执行一次,内层循环会完整执行一遍。
如何应用
1. 输出矩形
输入 n 和 m,输出 n 行 m 列的星号。
#include <iostream>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cout << "*";
}
cout << endl;
}
return 0;
}
2. 输出直角三角形
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i; j++) {
cout << "*";
}
cout << endl;
}
return 0;
}
3. 枚举两个数
求 1 到 n 中所有满足 i < j 的数对。
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
cout << i << " " << j << endl;
}
}
return 0;
}
学习难点
- 外层循环每执行一次,内层循环都要从头开始。
- 分清行、列和循环变量之间的对应关系。
- 根据图形规律确定内层循环上界,例如
j <= i。
易错点
忘记换行:
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cout << "*";
}
}
应在外层循环中补上:
cout << endl;
内外层变量重复定义:
for (int i = 1; i <= n; i++) {
for (int i = 1; i <= n; i++) {
}
}
应使用不同变量,例如 i 和 j。
存在的问题
- 双层循环的时间复杂度通常是多少?
- 当
n = 1000时,n × n次操作是否仍然可接受? - 如何打印倒三角形?
问题提问
- 外层循环执行 5 次,内层循环每次执行 3 次,总共执行多少次?
- 如何输出
n行,每行恰好n - i + 1个星号? - 怎样用嵌套循环找出
1到n中所有两两不同的数对?
第三部分:调试程序
问题引入
下面程序希望计算 1 到 n 的和,但结果不正确:
#include <iostream>
using namespace std;
int main() {
int n, sum;
cin >> n;
for (int i = 1; i <= n; i++) {
sum += i;
}
cout << sum << endl;
return 0;
}
输入 5,理论答案是 15,但程序可能输出随机数。
知识点讲解
程序错误通常分为三类。
1. 编译错误
程序无法编译,通常由语法错误造成。
cout << "Hello" << endl
缺少分号,应改为:
cout << "Hello" << endl;
2. 运行错误
程序可以编译,但运行时崩溃或异常退出。
int a[5];
cout << a[10] << endl;
访问超出数组范围的位置,行为不确定。
3. 逻辑错误
程序能运行,但结果不符合题意。
int sum = 0;
for (int i = 1; i < n; i++) {
sum += i;
}
这里漏加了 n,应改为 i <= n。
如何应用
修复前面的求和程序:
#include <iostream>
using namespace std;
int main() {
int n;
int sum = 0;
cin >> n;
for (int i = 1; i <= n; i++) {
sum += i;
}
cout << sum << endl;
return 0;
}
调试步骤:
- 阅读题意,明确输入、输出和边界条件。
- 用小数据手动模拟,例如
n = 1、n = 2、n = 5。 - 在关键位置输出变量值。
- 检查变量是否初始化。
- 检查循环边界和数组下标。
输出中间变量:
for (int i = 1; i <= n; i++) {
sum += i;
cout << "i = " << i << ", sum = " << sum << endl;
}
比赛提交前应删除调试输出。
学习难点
- 区分“程序不能运行”和“程序运行但答案错误”。
- 能够手动模拟循环中的变量变化。
- 理解未初始化变量的值不可预测。
易错点
- 忘记初始化累加变量,例如
sum、ans、cnt。 - 数组访问越界。
- 循环少执行一次或多执行一次。
- 调试输出没有删除,导致格式错误。
- 输入多个测试数据时,变量没有重新初始化。
存在的问题
- 为什么
int sum;不一定自动等于0? - 为什么数组下标越界有时没有立即报错?
- 为什么程序本地正确,提交后却错误?
问题提问
for (int i = 0; i <= n; i++)在数组长度为n时可能有什么风险?- 如何设计最小测试数据来检查循环边界?
- 当答案比正确值小一项时,应优先检查程序的什么位置?
第四部分:一维数组
问题引入
输入 n 个整数,求它们的最大值、最小值和总和。
如果没有数组,只能定义很多变量:
int a1, a2, a3, a4, a5;
当数据有 100 个、1000 个时,这种写法不可行。数组可以用一个名字保存一组同类型数据。
知识点讲解
1. 数组定义
int a[100];
表示定义一个能存放 100 个整数的数组。
数组下标从 0 开始:
a[0] = 10;
a[1] = 20;
若数组长度为 n,合法下标为:
0, 1, 2, ..., n - 1
2. 数组读入
for (int i = 0; i < n; i++) {
cin >> a[i];
}
3. 数组遍历
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
4. 数组性质
- 同一个数组中的元素类型相同。
- 数组元素连续存储。
- 下标必须是合法范围。
- 数组长度在定义时确定。
a[i]表示第i + 1个元素。
如何应用
求最大值、最小值和总和
#include <iostream>
using namespace std;
int main() {
int n;
int a[1005];
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
int sum = 0;
int maximum = a[0];
int minimum = a[0];
for (int i = 0; i < n; i++) {
sum += a[i];
if (a[i] > maximum) {
maximum = a[i];
}
if (a[i] < minimum) {
minimum = a[i];
}
}
cout << "sum = " << sum << endl;
cout << "max = " << maximum << endl;
cout << "min = " << minimum << endl;
return 0;
}
逆序输出数组
for (int i = n - 1; i >= 0; i--) {
cout << a[i] << " ";
}
统计偶数个数
int count = 0;
for (int i = 0; i < n; i++) {
if (a[i] % 2 == 0) {
count++;
}
}
学习难点
- 数组下标从
0开始,而不是1。 a[n]不是第n个元素,而是第n + 1个位置,通常会越界。- 明确数组容量和实际使用元素个数不同。
- 初始化最大值、最小值时,通常使用第一个元素。
易错点
错误写法:
int a[100];
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
若 n = 100,会访问 a[100],数组越界。
推荐写法:
for (int i = 0; i < n; i++) {
cin >> a[i];
}
访问空数组第一个元素:
int maximum = a[0];
当 n = 0 时,这种写法不合法。应先确认题目是否保证 n >= 1。
存在的问题
- 数组
int a[10]的最大合法下标是什么? - 如何交换数组中两个元素?
- 如何原地翻转一个数组?
问题提问
- 输入
n个数后,如何输出其中最后一个数? - 如何统计数组中负数的个数?
- 如何求数组中第二大的不同元素?
- 如何判断一个数组是否已经升序排列?
第五部分:一维数组进阶:桶技巧
问题引入
输入 n 个范围在 0 到 100 的整数,统计每个数字出现多少次。
例如:
输入:2 1 2 3 2 1
输出:
1 出现 2 次
2 出现 3 次
3 出现 1 次
如果每个数字都和其他数字比较,可能需要使用双层循环。若数据范围较小,可以把“数值”直接当作数组下标。
知识点讲解
桶技巧也称计数数组、频次数组。
int bucket[101] = {};
数组下标代表某个数值:
bucket[0]表示数字0出现次数。bucket[1]表示数字1出现次数。bucket[100]表示数字100出现次数。
读到一个数字 x 时:
bucket[x]++;
这表示数字 x 的出现次数加一。
如何应用
1. 统计每个数出现次数
#include <iostream>
using namespace std;
int main() {
int n;
int bucket[101] = {};
cin >> n;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
bucket[x]++;
}
for (int x = 0; x <= 100; x++) {
if (bucket[x] > 0) {
cout << x << " appears " << bucket[x] << " times" << endl;
}
}
return 0;
}
2. 判断是否有重复数字
#include <iostream>
using namespace std;
int main() {
int n;
int bucket[1001] = {};
bool hasDuplicate = false;
cin >> n;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
bucket[x]++;
if (bucket[x] >= 2) {
hasDuplicate = true;
}
}
if (hasDuplicate) {
cout << "Yes" << endl;
} else {
cout << "No" << endl;
}
return 0;
}
3. 找出只出现一次的数
for (int x = 0; x <= 100; x++) {
if (bucket[x] == 1) {
cout << x << " ";
}
}
4. 利用桶排序
当数值范围较小时,按下标从小到大扫描桶,就能得到有序序列。
for (int x = 0; x <= 100; x++) {
for (int k = 1; k <= bucket[x]; k++) {
cout << x << " ";
}
}
学习难点
- 理解“数组下标表示值,数组元素表示次数”。
- 正确确定桶数组范围。
- 判断输入值是否可能为负数。
- 区分原数组
a[i]和计数数组bucket[x]的含义。
易错点
桶数组开得过小:
int bucket[100];
如果输入可能等于 100,访问 bucket[100] 会越界。应定义:
int bucket[101] = {};
没有初始化:
int bucket[101];
应初始化为 0:
int bucket[101] = {};
输入存在负数时直接使用:
bucket[x]++;
若 x < 0,数组下标非法。可通过偏移处理:
int bucket[2001] = {};
bucket[x + 1000]++;
适用于 -1000 到 1000 的数据。
存在的问题
- 若数据范围达到
10^9,还能直接开桶吗? - 若输入包含负数,如何设计桶下标?
- 桶技巧和双层循环统计频次相比,哪个更快?
问题提问
- 输入 20 个
0到9的数字,如何输出出现次数最多的数字? - 如果有多个数字出现次数最多,如何输出最小的那个?
- 如何用桶技巧判断两个数组是否包含相同的元素及次数?
- 为什么桶排序只适合数值范围相对较小的数据?
第六部分:一维字符数组与 C 风格字符串
问题引入
输入一个英文单词,统计其中有多少个元音字母。
例如:
输入:algorithm
输出:3
单词本质上是一串字符,可以用一维字符数组保存。
知识点讲解
1. 字符数组定义
char s[105];
char 用于保存单个字符:
char ch = 'A';
字符使用单引号:
char ch = 'a';
字符串使用双引号:
char s[] = "hello";
2. 字符串结束标志 '\0'
C 风格字符串以 '\0' 结尾。
char s[] = "hello";
实际存储为:
h e l l o \0
因此 "hello" 有 5 个可见字符,但至少需要长度为 6 的字符数组。
char s[6] = "hello";
3. 输入字符数组
读取一个不含空格的单词:
cin >> s;
读取一整行文字:
cin.getline(s, 105);
4. 遍历字符数组
for (int i = 0; s[i] != '\0'; i++) {
cout << s[i] << endl;
}
5. 常见字符串函数
使用字符串函数时需要:
#include <cstring>
strlen
获取字符串长度,不包括结尾的 '\0'。
int len = strlen(s);
strcmp
比较两个字符串。
if (strcmp(a, b) == 0) {
cout << "same" << endl;
}
strcpy
复制字符串。
strcpy(a, b);
strcat
拼接字符串。
strcat(a, b);
如何应用
1. 统计元音字母
#include <iostream>
#include <cstring>
using namespace std;
int main() {
char s[105];
cin >> s;
int count = 0;
for (int i = 0; s[i] != '\0'; i++) {
if (s[i] == 'a' || s[i] == 'e' || s[i] == 'i' ||
s[i] == 'o' || s[i] == 'u') {
count++;
}
}
cout << count << endl;
return 0;
}
2. 使用 strlen 遍历字符串
#include <iostream>
#include <cstring>
using namespace std;
int main() {
char s[105];
cin >> s;
int len = strlen(s);
for (int i = 0; i < len; i++) {
cout << s[i] << " ";
}
return 0;
}
3. 判断回文字符串
回文字符串正读和倒读相同,例如 level、abcba。
#include <iostream>
#include <cstring>
using namespace std;
int main() {
char s[105];
cin >> s;
int len = strlen(s);
bool isPalindrome = true;
for (int i = 0; i < len / 2; i++) {
if (s[i] != s[len - 1 - i]) {
isPalindrome = false;
break;
}
}
if (isPalindrome) {
cout << "Yes" << endl;
} else {
cout << "No" << endl;
}
return 0;
}
字符数组与 string 的区别
| 对比项 | 字符数组 char s[105] |
string s |
|---|---|---|
| 本质 | 固定大小的字符数组 | C++ 标准库字符串类型 |
| 结尾 | 需要 '\0' 标记结束 |
自动管理结尾 |
| 长度 | strlen(s) |
s.length() 或 s.size() |
| 比较 | strcmp(a, b) |
a == b |
| 拼接 | strcat(a, b) |
a + b 或 a += b |
| 输入空格 | cin.getline() |
getline(cin, s) |
| 数组容量 | 必须预先确定 | 可自动扩展 |
基础算法阶段应理解字符数组的底层结构;实际 C++ 编程中,很多题目使用 string 会更安全方便。
学习难点
- 区分字符
'a'和字符串"a"。 - 理解
'\0'不是普通可见字符,而是字符串结束标志。 - 理解字符数组容量必须为“字符个数加一”。
- 掌握
cin >> s和cin.getline()的区别。 - 理解
strcmp返回0才代表两个字符串相等。
易错点
字符与字符串混用:
char ch = "a";
应改为:
char ch = 'a';
字符数组空间不足:
char s[5] = "hello";
应改为:
char s[6] = "hello";
不能直接用 == 比较两个字符数组:
if (a == b) {
}
应使用:
if (strcmp(a, b) == 0) {
}
cin >> s 无法读取空格后的内容:
char s[105];
cin >> s;
若输入为 hello world,只能读到 hello。读取整行应使用:
cin.getline(s, 105);
存在的问题
- 为什么字符数组必须预留一个位置给
'\0'? strlen(s)和sizeof(s)有什么区别?- 为什么
strcmp(a, b) == 0表示两个字符串相等? - 使用
cin >> s后立刻使用cin.getline(),为什么可能读到空行?
问题提问
- 如何统计字符串中数字字符的个数?
- 如何将字符串中的小写字母转换为大写字母?
- 如何判断两个单词是否由相同字母组成?
- 如何统计一个句子中单词的数量?
- 如何删除字符串中所有的空格?
课后练习
基础题
- 输入
n,输出1到n中所有 3 的倍数。 - 输入
n,输出一个边长为n的实心正方形。 - 输入
n个整数,输出最大值和最小值。 - 输入
n个0到100的整数,输出出现次数最多的数。 - 输入一个单词,统计其中大写字母、小写字母和数字的数量。
提高题
- 输入
n个整数,判断是否存在重复元素。 - 输入
n个0到1000的整数,按从小到大顺序输出。 - 输入一个字符串,判断它是否为回文串。
- 输入两个字符串,判断它们是否互为字母重排词。
- 输入一行文本,统计每个英文字母出现的次数,忽略大小写。 ```
—— 本文来自火龙信奥(义乌睿码科技):义乌青少年信息学奥赛与编程教育平台,专注 CSP-J/S、NOIP、GESP 竞赛培训,线上线下融合教学,助力编程升学。网址:hlcoding.com