第4章 · 数组¶
本页最后更新于 ,内容可能过时
本章题目对应教材第 4 章,按 4.1 数组基本概念、4.2 一维数组、4.3 多维数组、4.4 字符串处理技术基础的顺序排列。
4.1 数组的基本概念¶
1 · 数组名不可修改(赋值与自增)¶
对于定义 int a[3]={0}, b[3], i = 3, *j; 下列是合法的表达式的是( )。
- A.
a = b; - B.
*j = *(a+i); - C.
a++; - D.
j=a+1;
答案
D
解析
逐项判断:
- A 错:数组名不是左值,不能整体赋值(想拷贝数组必须用循环或
memcpy)。 - B 错:
j声明后未初始化,*j = ...是往野指针写数据(未定义行为);而且a+i中i=3已越界(a只有 3 个元素,合法下标 0~2)。 - C 错:数组名不能自增(它不是变量)。
- D 对:
a+1是指向a[1]的指针,赋给int *j完全合法。✅
易错:B 有解引用,看着像合法表达式。这里野指针与越界两个问题同时存在。
2 · 数组名不可修改(比较与取地址)¶
如果定义了以下变量和数组,那么以下可以通过编译的合法表达式或者语句是( )。
- A.
j = a == b; - B.
b = a; - C.
&(i + j); - D.
a++;
答案
A
解析
- A 对:
a == b比较两个地址(结果为0),把int结果赋给j合法。虽然逻辑上永远为假(两个不同数组的地址不可能相等),但语法完全合法,本题问的就是"合法"。 - B 错:数组名不可赋值。
- C 错:
i + j是一个右值(临时结果),&只能作用于左值(有确定地址的对象)。 - D 错:数组名不能自增。
易错:A 看起来"没意义"就排除了。题目问的是"能否通过编译",不是"是否有意义"。
a = b、a++、&(i+j)这三种写法都不合法。
3 · 数组初始化与自动补 0¶
以下代码的输出是什么?
- A. 未定义行为
- B. 0
- C. 3
- D. 随机值
答案
B
解析
{0} 只显式初始化了 arr[0],但其余元素全部自动补 0(这是 C 的规则,不是"随机值")。所以 arr[3] = 0。
易错:数组初始化的补 0 规则和局部变量未初始化不同。局部变量(如
int x;)不初始化才是垃圾值;数组只要给了初始化列表,未列出的元素一律补 0。
4 · 数组大小、sizeof 与 VLA¶
以下关于数组的描述错误的是?
- A. 数组大小必须是一个常量表达式
- B. 数组的索引从 0 开始
- C. 可以用
sizeof运算符直接获得数组元素个数 - D. 数组名对应数组首元素地址,该地址不可被修改
答案
A
解析
本题答案是 A:
- A 错:C99 标准之后允许变长数组(VLA),数组大小可以是运行时才确定的变量(如
int n; scanf("%d",&n); int a[n];)。所以说"必须是常量表达式"是过时/错误的表述。 - C 对,但它的表述不够严谨:严格说
sizeof(a)得到的是字节数而不是"元素个数",要得到元素个数得写sizeof(a)/sizeof(a[0])。很多教材把sizeof(a)/sizeof(a[0])这个惯用法简称为"用 sizeof 获得元素个数",所以本题把 C 算作正确表述、把 A 定为答案。
材料上的一条批注:
- 选项 A:"C99 标准后,数组大小允许变长表达式"
- 选项 B、C、D 都对 √
4.2 一维数组¶
5 · 冒泡排序的边界与升降序(改错)¶
下面的程序用冒泡排序把数组 a 排成降序,请找出其中的错误并改正。
/* 1 */#include <stdio.h>
/* 2 */int main() {
/* 3 */ int a[6] = {8, 2, 13, 4, 9, 15 };
/* 4 */ int i, j, temp;
/* 5 */ for (i = 0; i < 6 - 1; i++) {
/* 6 */ for (j = 0; j < 6 - i; j++) {
/* 7 */ if (a[j] >a[j + 1]) {
/* 8 */ temp = a[j];
/* 9 */ a[j] = a[j + 1];
/* 10 */ a[j + 1] = temp;
/* 11 */ }
/* 12 */ }
/* 13 */ }
/* 14 */ for (i = 0; i < 6; i++)
/* 15 */ printf("%d ", a);
/* 16 */}
答案
3 处错误:
| 行 | 错误 | 改正 | 错因 |
|---|---|---|---|
| 6 | for (j = 0; j < 6 - i; j++) |
for (j = 0; j < 6 - 1 - i; j++) |
循环体里要访问 a[j+1],所以 j 最大只能到 5-1-i。写成 j < 6-i 时 j 会取到 5-i,a[j+1] 越界 |
| 7 | if (a[j] > a[j + 1]) |
if (a[j] < a[j + 1]) |
题目要求降序,应该是"前面的比后面的小就交换" |
| 15 | printf("%d ", a); |
printf("%d ", a[i]); |
要输出的是元素值 a[i],a 是数组名(地址) |
解析
- 第 6 行的越界不容易发现:
j < 6-i在i=0时j取到 5,然后a[6]就越界了。冒泡排序的内层边界标准写法是n-1-i。真机实测:保留
j < 6-i的版本在 gcc 15.2.0 下会触发*** stack smashing detected ***直接 abort(另一次运行虽未触发保护,打印的却是 6 个地址值414002160)——越界写a[6]属于未定义行为,不是"结果碰巧还对"。 - 第 7 行的"升降序":
>是升序,<是降序(因为"前者大于后者就交换"会把大的往后送,得到升序)。 - 第 15 行是"数组名 vs 元素"的混淆。
改正后的输出(降序,每个数后面跟两个空格、末尾无换行):
6 · 字符(ASCII 码)作数组下标¶
若有以下语句,各选项中数值为 4 的表达式是__。
- A.
a[g-c] - B.
a[4] - C.
a['d'-'c'] - D.
a['d'-c]
答案
D
解析
字符参与算术运算时按其 ASCII 码当整数用:'a'=97、'd'=100,所以 'd'-c = 100-97 = 3,a[3] 是第 4 个元素 = 4 ✅。
C 的 'd'-'c' = 1 → a[1] = 2;B 的 a[4] = 5;A 里的 g 声明后从未初始化,值不确定(用未初始化变量是未定义行为)。
考点:下标可以是任意整型表达式,字符常量/变量都算整型。
7 · sizeof 与字符数组的“长度”¶
那么( )。
- A.
x数组的长度等于y数组的长度 - B.
x数组的长度大于y数组的长度 - C.
x数组的长度少于y数组的长度 - D.
x数组与y数组的存储区域相同
答案
A
解析
这里的"长度"指数组占用的内存字节数,也就是 sizeof:
两者都是 6 → A ✅。D 错:x、y 是两个互相独立的数组对象,各占各的一块存储空间,地址不可能相同(题目问的是"长度",D 偷换成了"存储区域")。
这道题涉及 sizeof 与 strlen 的区别:
sizeof(x) |
strlen(x) |
sizeof(y) |
strlen(y) |
|
|---|---|---|---|---|
| 结果 | 6 | 5 | 6 | 5 |
| 数的是什么 | 数组占的字节数('\0' 算一格) |
开头到第一个 '\0' 的字符个数('\0' 不算) |
同左 | 同左 |
| 何时算 | 编译期运算符,看的是类型/对象有多大 | 运行时函数,看的是内容 | 同左 | 同左 |
要点:
char x[] = "12345"编译器会自动补一个'\0',所以x是 6 字节而不是 5 字节;而
y初值表里的那个'\0'是人自己写上去的——两种写法完全等价。若把
y写成{'1','2','3','4','5'}(不写'\0'),y就只有 5 字节,答案就变成 C 了。填空时注意两点:字符串字面量自带
'\0',初值表不会自动补'\0'。补充:本题按
strlen理解仍是 A(strlen(x)与strlen(y)都是 5),两种理解下 A 都成立。真机验证(gcc 15.2.0):
sizeof(x)=6 sizeof(y)=6 strlen(x)=5 strlen(y)=5。
4.3 多维数组¶
8 · sizeof 的用法¶
若定义 int a[][2]={{1},{2},{3},4}; 则 sizeof(a) 的返回值为( )。
- A. 48
- B. 32
- C. 16
- D. 24
答案
B
解析
- 二维数组第一维可以省略,由初始化列表的行数推导。
{{1},{2},{3},4}有 4 个初始化元素(前 3 个是{...},第 4 个是裸的4,它会开启新的一行),所以a是int[4][2]。 - 元素总个数 = 4 × 2 = 8 个
int。 sizeof(int)= 4 字节。sizeof(a)= 8 × 4 = 32。✅
行展开后是:
sizeof的规则:sizeof(类型)= 类型占的字节数;sizeof(数组名)= 整个数组的字节数(不是元素个数!);元素个数 =
sizeof(a) / sizeof(a[0]);sizeof是运算符不是函数,编译期求值,sizeof(x++)不会真的自增。易错:把
4也算进a[0]那一行(以为只有 3 行)。裸值会开启新的一行。
9 · sizeof 的用法¶
若定义 char a[][3]={'1','2','3','4'}; 则 sizeof(a) 的返回值为( )。
- A. 4
- B. 6
- C. 8
- D. 12
答案
B
解析
4 个初始化元素,每行 3 个 → 需要 2 行(3 + 1),所以 a 是 char[2][3]。
sizeof(a)= 2 × 3 × 1 = 6 字节。✅- 注意第 2 行只填了
'4',剩下a[1][1]、a[1][2]被自动补 0。
易错:以为 4 个元素就占 4 字节。二维数组按整行分配,不足的补 0。
补充:
char的sizeof是 1,所以这里的 6 既是字节数也是元素个数——换成int就得再乘 4。
10 · 矩阵操作¶
请写出下列程序的输出。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int b[3][3];
int c[3][3] = {0};
int i, j, k;
int a[3][3] = { {1,2,3},{4,5,6},{7,8,9} };
for (i = 0; i < 3; i++)
for (j = 0; j < 3; j++)
{
b[j][i] = a[i][j];
}
for (i = 0; i < 3; i++)
for (j = 0; j < 3; j++)
{
for ( k = 0; k < 3; k++)
{
c[i][j] += a[i][k] * b[k][j];
}
}
for (i = 0; i < 3; i++)
for (j = 0; j < 3; j++)
{
printf("%d ", c[i][j]);
}
}
解析
b[j][i] = a[i][j] 让 b = aᵀ,于是 c = a × b = a × aᵀ。
此时 c[i][j] = Σₖ a[i][k]·a[j][k],也就是第 i 行与第 j 行的内积:
| j=0 | j=1 | j=2 | |
|---|---|---|---|
| i=0 | 1²+2²+3² = 14 | 1·4+2·5+3·6 = 32 | 1·7+2·8+3·9 = 50 |
| i=1 | 32 | 4²+5²+6² = 77 | 4·7+5·8+6·9 = 122 |
| i=2 | 50 | 122 | 7²+8²+9² = 194 |
三个要点:
- 转置:
b[j][i] = a[i][j]是行列互换,即b = aᵀ,问题转化为"矩阵乘自己的转置"。 - 结果是对称矩阵(
c[i][j] == c[j][i]),下三角可以由上三角得到,只需算 6 个数(对角线 3 个 + 上三角 3 个)。算出的结果若不对称,说明计算有误。 printf("%d ", ...)每个数后面都有空格且没有换行,所以最后输出是... 194(带尾随空格)。
易错:把
c[i][j] += a[i][k] * b[k][j]里的b[k][j]误当b[i][k]。矩阵乘法
c[i][j]由a的第 i 行与b的第 j 列相乘求和得到,两边共用的中间指标是k。
11 · 二维数组定义只能省略第一维¶
下面不能正确定义二维数组的选项是__。
- A.
int a[2][]={{1,2},{3,4}}; - B.
int a[][2]={1,2,3,4,5}; - C.
int a[2][2]={{1},{2}}; - D.
int a[2][2]={1,2,3};
答案
A
解析
C 语言只允许省略第一维,第二维必须给出——编译器要靠第二维算出一行的长度和总行数,所以 a[2][] 直接非法。
B 由 {1,2,3,4,5} 推出 int a[3][2](最后一行只给一个数,另一个补 0);
C 每行只给一个初值,a[0]={1,0}、a[1]={2,0};D 只给 3 个数,第 4 个补 0,B、C、D 都合法。
声明和初始化二维数组时,省略第一维可以,省略第二维不行。
12 · 二维数组的反对角线访问¶
下面语句的输出结果是( )。
- A. 147
- B. 159
- C. 357
- D. 369
答案
C(即 357)
解析
初始化列表按行优先填满 3×3 数组:
x[i][j] |
j=0 | j=1 | j=2 |
|---|---|---|---|
| i=0 | 1 | 2 | 3 |
| i=1 | 4 | 5 | 6 |
| i=2 | 7 | 8 | 9 |
循环取的是 x[i][2-i],两个下标一个递增、一个递减,走过的是:
| i | 2-i |
元素 | 值 |
|---|---|---|---|
| 0 | 2 | x[0][2] |
3 |
| 1 | 1 | x[1][1] |
5 |
| 2 | 0 | x[2][0] |
7 |
即从右上到左下的反对角线(副对角线),结果 357 ✅。
printf 里没有空格也没有换行,三个数字连成一片,输出是 357,不是 3 5 7 或 3,5,7。
两条对角线一起记(
n阶方阵):
- 主对角线:
x[i][i]→ 本题会得到1 5 9= 159,正是干扰项 B;- 副对角线:
x[i][n-1-i]→ 本题n=3,就是x[i][2-i]= 357。易错:
2-i里的2是"最大下标"(n-1),不是长度n——写成x[i][3-i]第一轮就x[0][3]越界。真机验证(gcc 15.2.0):输出
357。
13 · 矩阵原地转置 · 逗号表达式¶
以下程序对一个 3×3 的矩阵进行行列互换(原地转置),请填空。
#include <stdio.h>
int main(int argc, char* argv[])
{
int i, j, temp, a[3][3] = { 1,2,3,4,5,6,7,8,9 };
for (i = 0; i < 3; i++)
for (j = 0; j < i; j++)
temp = a[i][j], (17) , (18) ;
printf("\n the result array is:\n");
for (i = 0; i < 3; i++)
{ printf("\n");
for (j = 0; j < 3; j++)
printf("%5d", a[i][j]);
}
return 0;
}
答案
| 序号 | 答案 |
|---|---|
| (17) | a[i][j] = a[j][i] |
| (18) | a[j][i] = temp |
解析
先把逗号表达式还原成三条动作。
temp = a[i][j], (17), (18); 是一条语句,逗号运算符优先级最低、结合方向从左到右,所以三个子表达式依次求值——这里要的不是整个表达式的值,而是这三步的副作用:
temp = a[i][j]; /* ① 把下三角的元素暂存起来 */
a[i][j] = a[j][i]; /* ② (17) 上三角的元素搬到下三角 */
a[j][i] = temp; /* ③ (18) 暂存的原值回填到上三角 */
这就是一次交换,temp 用作临时变量。
为什么 (18) 必须是 temp? 走到第 ③ 步时,a[i][j] 已经被 ② 覆盖掉了,原值只留在 temp 里。
第 (18) 空若填成 a[i][j],这条语句没有任何副作用——下三角被换成了上三角的值,上三角不变,得到的矩阵不是转置结果:
真机实测:填成
a[i][j]时结果是123 / 256 / 369(不是转置);填a[j][i] = temp才是147 / 258 / 369。
交换由三步组成:先暂存、再写入、后回填。
为什么内层必须是 j < i?
j < i即"列下标小于行下标",j只取0 ~ i-1——扫的是主对角线以下的那一半(下三角)。- 转置要交换的永远是
(i,j)与(j,i)这一对。外层i从小到大扫,内层只取j<i,于是每一对恰好被处理一次 ✅。 - 若把
j < i改成j < 3(整个矩阵都扫),(i,j)和(j,i)这一对会被交换两次——等于没换。第一次把上三角换到下面,第二次又把它换回来,最终矩阵原封不动。这是填空题必须写j < i的原因。真机实测:把内层改成
j < 3、填入同样的 (17)(18),输出仍是123456789(没转置);而j < i的版本得到正确的转置矩阵。
改完之后的运行结果:
printf("%5d", ...) 是每个数占 5 格、右对齐;printf("\n") 写在每行数字之前,所以第一行数字上方还有一个空行。
本题要点:上三角与下三角成对交换,只扫一半(
j<i),中间用temp暂存。 交换类填空(逆序、排序、转置)中连续两空通常对应一次交换的两个动作。真机验证(gcc 15.2.0):填
a[i][j] = a[j][i]与a[j][i] = temp后输出1 4 7 / 2 5 8 / 3 6 9。
4.4 字符串处理技术基础¶
14 · \0 与 strlen¶
调用 strlen("abcd\x1f\0g"); 的返回值为( )。
- A. 4
- B. 5
- C. 6
- D. 报错
答案
B
解析
把转义序列一个个拆开看:
abcd→ 4 个普通字符;\x1f→ 十六进制转义,1F₁₆ = 31,是一个字符(不可打印);\0→ 空字符,字符串到此结束;g→ 存在但不计入strlen。
所以 strlen 返回 4 + 1 = 5。✅
易错:
- 把
\x1f当成 3 个字符(\、x、1、f)- 忘了
\0会截断八进制转义只接受
0–7,十六进制转义接受0–9a-f;'\08'(错)与"hello\078world!"(对)的差别就是这么来的。
strlen数的是"从开头到第一个'\0'之间的字符个数",不含'\0'本身。
15 · strlen 与八进制转义¶
程序语句 printf("%d", strlen("hello\078world!")); 的输出结果是( )。
- A. 13
- B. 12
- C. 6
- D. 5
答案
A
解析
关键在于 \078 如何划分。
\07是八进制转义(7₈ = 7,一个不可打印字符),到此为止;- 后面剩下的
8是普通字符'8'。
所以字符串实际内容是:h e l l o + \07 + 8 + w o r l d ! = 5 + 1 + 1 + 6 = 13 个字符(strlen 不数结尾的 '\0')。✅
易错:以为
\078整体是一个转义字符(那样算出来是 12)。八进制转义遇到8/9就截断。区分:
'\08'错、"hello\078world!"对 —— 差别在于单引号里\0后面直接跟了8,单引号里有两个字符,就成了多字符常量;而字符串里
\07+'8'是两个独立的字符,完全合法。
16 · 转义序列与字符串字节数¶
C 语言中,"\\xfds" 在内存中占用的字节数是( )。
- A. 3
- B. 4
- C. 5
- D. 6
答案
D(6 字节)
解析
先数清字符个数——
\\是一个字符(转义后的反斜杠\);- 后面的
x、f、d、s是 4 个普通字符(\x这个引子已经被前面的\\用掉了,不再构成转义)。
所以字符串里共 5 个字符,再加上自动追加的结束符 '\0' → 6 字节。✅
反例对照:若写成单个反斜杠
"\xfds",则\xfd会构成一个十六进制转义(f、d都是十六进制数字,读到s才停),串里只剩\xfd和s两个字符→ 那时
sizeof是 3 字节(strlen是 2)。区分三种写法(下表数值均由 gcc 15.2.0 实测):
| 写法 | 类型 | 占用 |
|---|---|---|
'A' |
int(字符常量) |
4 字节 |
'\x41' |
int(十六进制转义,值 65) |
4 字节 |
"A" |
char[2](字符串字面量) |
2 字节 |
易错:
- 把
\\当成两个字符(那会数成 7 字节)- 以为
\xfd里fd是两个字符- 忘了字符串末尾那个
'\0'
17 · 字符串里的转义与截断¶
执行语句 char str[] = "ab%d\\tefgh/n\0"; printf("%s", str); 后输出( )。
- A.
ab%d\tefgh/n - B.
ab%d\\tefgh/n - C.
ab%d\tefgh - D.
ab\tefgh/n
答案
A
解析
把 "ab%d\\tefgh/n\0" 逐个字符拆开:\\ 是转义序列,只代表一个反斜杠字符(不是两个,这正是 B 错的地方);紧随其后的 t 只是普通字母 t,不存在 \t 制表符;/n 里的斜杠本来就是普通字符,它是两个普通字符 / 和 n,不是换行符(写的是 /n 不是 \n);%d 在字符串里也只是 %、d 两个字面字符,因为它是 printf 的实参而不是格式串。
最后 \0 是空字符,%s 遇到它就停止输出,所以只输出它前面的部分,结果正是 ab%d\tefgh/n。
C 漏掉了 /n、D 漏掉了 %d、B 把 \\ 当成两个字符,全是错选。
18 · 字符串原地逆序(填空)¶
以下代码是把一个字符串倒序,如 "abcd" 倒序后变为 "dcba"。请补全程序。
#include <stdio.h>
#include <string.h>
int main()
{
char str[]="abcd";
int len= (1) ;
char t;
for(int i=0; (2) ; i++)
{
t=str[i];
str[i]= (3) ;
(4) =t;
}
printf("%s",str);
return 0;
}
答案
| 序号 | 答案 |
|---|---|
| (1) | strlen(str) |
| (2) | i < len/2 |
| (3) | str[len-i-1] |
| (4) | str[len-i-1] |
解析
双指针交换法:i 从左端走、len-1-i 从右端走(代码里用 len-i-1 表示),交换到中点 len/2 为止。
注意一个反直觉之处:(3)、(4) 两空填的是完全相同的文本
str[len-i-1]。因为这就是一次交换——第二空是"把
t写回右端",所以左端也得是str[len-i-1]:
两个空答案相同时,先判断是不是一次交换。
循环次数为什么是 len/2? 交换只需进行到中点。若写成 i < len,会把已经换好的又换回去(偶数长度回到原样,奇数长度则出错)。
为什么这里
char str[]="abcd"可以改? 因为它是数组——编译器把字面量拷贝一份到str里,str是本地可写内存。这正好和字符指针指向只读字面量的情形形成对照。
19 · 字符串原地逆序¶
函数 fun 的功能是:使一个字符串按逆序存放,请填空。
void fun(char str[]) /* 题干未印 #include,为便于编译补上 <stdio.h>、<string.h> */
{
char m;
int i, j;
for (i = 0, j = strlen(str); i < ⑴ ; i++, j--)
{
m = str[i];
str[i] = ⑵ ;
str[j - 1] = m;
}
printf("%s\n", str);
}
答案
| 序号 | 答案 |
|---|---|
| ⑴ | j - 1 |
| ⑵ | str[j - 1] |
解析
先看懂 j 的取值方式:j = strlen(str) 是长度,不是"最后一个字符的下标"。
所以 j - 1 才是右端字符的位置——题干里 str[j - 1] = m; 这一行已经写好了,它就是 ⑵ 的提示:既然要往 str[j-1] 里写 m,那么被 m 顶替掉的那个右端字符,自然就是 str[j-1]。
这三行就是一次交换:
m = str[i]; /* ① 暂存左端 */
str[i] = str[j - 1]; /* ② ⑵ 把右端搬到左端 */
str[j - 1] = m; /* ③ 把暂存的原左端值回填到右端 */
⑵ 必须与第 ③ 行的 str[j-1] 配套。若 ⑵ 写成别的东西(比如写成 str[i]),左端就白写了——str[j-1] = m 只是把左端的值又抄到右端,字符串会被写坏。
填空题里"紧挨着的两空/一行"往往就是交换的两个动作,看到一个就能推出另一个。
逐轮演算("abcdef",长度 6,j 初值 6):
| 轮 | i |
j |
条件 i < j-1 |
交换 | 串变成 |
|---|---|---|---|---|---|
| 1 | 0 | 6 | 0 < 5 ✓ |
str[0] ↔ str[5] |
fbcdea |
| 2 | 1 | 5 | 1 < 4 ✓ |
str[1] ↔ str[4] |
fecdba |
| 3 | 2 | 4 | 2 < 3 ✓ |
str[2] ↔ str[3] |
fedcba |
| 4 | 3 | 3 | 3 < 2 ✗ |
停 | fedcba |
输出 fedcba ✅
为什么 ⑴ 是 j - 1 而不是别的:i 是左游标、j-1 是右游标,i++ 与 j-- 让两者从两端向中间夹。
循环条件是"左游标还在右游标左边时才需要换"——写成 i < j - 1,一旦两者相遇(i == j-1,此时是奇长度串正中间那个字符)或交叉就停手。
奇长度因此自动跳过正中间那个字符,"abcde" → edcba;空串、单字符也不会出问题(长度 0 时 j-1 = -1,0 < -1 为假,一轮都不跑)。
顺带一句:这里
i、j都是int,所以空串时j-1等于-1也不会出事。若把它们声明成
size_t等无符号类型,j-1在j == 0时会翻转成一个巨大的正数,条件反而为真 → 直接越界。这是"C 语言里做倒着数的下标时,用int比用无符号类型安全"的实例。
本空的一个常见错答:j / 2
它看着像"到中点为止",但在这段代码里是错的——因为 j 在循环里每轮都在减小,j/2 不是常数,条件会提前变假:第 i 轮时 j = len - i,i < j/2 即 3i < len,所以只跑约 len/3 轮,而正确做法需要 len/2 轮——轮数不够,串只反转了一半就收工。
真机实测(gcc 15.2.0):填 i < j / 2 跑 "abcdef" 得到 fecdba(只换了 2 对,本该 3 对,错);填 i < j - 1 才得到 fedcba(对);8 字符的 "abcdefgh" 前者给 hgfdecba、后者给 hgfedcba。
想写成"走一半"当然也可以,但必须用不随循环变化的量:i < (int)strlen(str) / 2(每轮重新算 strlen 得到的都是同一个长度)。循环的上界需要是一个不随循环体变化的量。
真机验证(gcc 15.2.0):
"abcdef"→fedcba;"abcde"→edcba(奇数长度中间字符不动);"a"→a;""→ 空行。
20 · 原地删除指定字符¶
编写程序,输入两个字符串 str1 和 str2,删除 str1 中所有在 str2 出现的字符,输出处理后的 str1。
样例
要求:补充完善函数 is_char_in_str 和 remove_chars_from_str1。
【程序框架】
#include <stdio.h>
#include <string.h>
/*函数功能:判断字符c 是否在str2 中存在。如果在,返回1,否则返回0*/
int is_char_in_str(char c, const char str2[]) {
//请补充代码实现
}
/*函数功能:从str1 中移除str2 包含的字符*/
void remove_chars_from_str1(char str1[], const char str2[]) {
//请补充代码实现
}
int main() {
char str1[100], str2[100];
printf("请输入字符串str1:");
fgets(str1, sizeof(str1), stdin);
str1[strcspn(str1, "\n")] = '\0';
printf("请输入字符串str2:");
fgets(str2, sizeof(str2), stdin);
str2[strcspn(str2, "\n")] = '\0';
remove_chars_from_str1(str1, str2);
printf("处理后的str1: %s\n", str1);
return 0;
}
答案(参考代码)
int is_char_in_str(char c, const char str2[]) {
int i = 0;
while (str2[i]) {
if (str2[i] == c) {
return 1; // 如果字符在 str2 中找到,返回 1
}
i++;
}
return 0; // 如果字符没有在 str2 中找到,返回 0
}
void remove_chars_from_str1(char str1[], const char str2[]) {
int i = 0, j = 0;
while (str1[i]) { // 遍历 str1 数组
if (!is_char_in_str(str1[i], str2)) { // 如果 str1[i] 不在 str2 中
str1[j] = str1[i]; // 将 str1[i] 保留到新的位置
j++;
}
i++;
}
str1[j] = '\0'; // 以 '\0' 结束字符串
}
解析(通用模板)
"原地删除 / 筛选"的双下标法
int i = 0, j = 0; // i = 读指针,j = 写指针 while (str[i]) { if (保留条件) str[j++] = str[i]; i++; } str[j] = '\0'; // 不要漏掉
这个模板同样适用于:数组去重、删除指定元素、快排的 partition。
边界与易错:
- 必须写
str1[j] = '\0';—— 材料上写明:"没有以\0结束字符串会带来逻辑错误,若学生没有写明,可酌情扣分。" 因为后面的printf("%s", str1)会一直往后找\0,把残留的旧字符也打印出来。 j <= i恒成立,所以"写指针"永远不会跑到"读指针"前面,原地覆盖是安全的——这就是这个模板不需要额外辅助数组的原因。- 注意样例:
"Test is important"去掉t/e/s后是"T i imporan"(保留空格),不要想当然地把空格也删了。 is_char_in_str用while (str2[i])而不是for (i=0; i<strlen(str2); i++)—— 前者每次判断只读一个字符,后者每轮都要重新算一遍strlen(O(n²))。这是写字符串题的常识性优化。
21 · 字符计数与统计¶
输入由小写字母构成的非空字符串 s(长度小于 1000),输出 s 中出现次数最少的小写字母的出现次数。
【样例 1】:输入 s = "aassddffd" → 输出 res = 2
【样例 2】:输入 s = "abbbac" → 输出 res = 1
【需要改正的程序】
/* 1 */ #include <stdio.h>
/* 2 */ int main() {
/* 3 */ char s[1000];
/* 4 */ int cnt[26] = { 0 };
/* 5 */ int res = 0;
/* 6 */ scanf("%s", &s);
/* 7 */ for (int i = 0; s[i]; i++) {
/* 8 */ cnt[s[i]]++;
/* 9 */ }
/* 10 */ for (int i = 0; s[i]; i++) {
/* 11 */ if (cnt[s[i]] < res) {
/* 12 */ res = cnt[i];
/* 13 */ }
/* 14 */ }
/* 15 */ printf("%d", res);
/* 16 */ return 0;
/* 17 */ }
答案(材料上给出的改法)
| 行 | 错误 | 改正 |
|---|---|---|
| 5 | int res = 0; |
int res = 1000;(求最小值,初值要给个足够大的数) |
| 6 | scanf("%s", &s); |
scanf("%s", s);(数组名本身就是地址,不能取地址) |
| 8 | cnt[s[i]]++; |
cnt[s[i]-'a']++;(要把字符 'a'~'z' 映射到下标 0~25) |
| 10 | for (int i = 0; s[i]; i++) |
for (int i = 0; i < 26; i++)(第二趟要遍历 26 个计数桶,不是在原字符串上再走一遍) |
| 11 | if (cnt[s[i]] < res) |
if (cnt[i] < res && cnt[i] > 0)(桶下标是 i;且只统计出现过的字母,次数为 0 的不能算) |
材料上只有第 5 行、第 6 行、第 8 行、第 10 行、第 11 行这 5 处标了改法,正好对应题干"有 5 行存在错误"。
第 12 行
res = cnt[i];本身没错,不需要改动。
解析(找错改错题)
总结出四条通用经验:
- 求最小值,初值要给最大。
int res = 0;求最小永远是 0,第一条就该改。对偶记忆:求最大,初值给最小(通常给
-1或INT_MIN)。 scanf("%s", s)不要写&s。数组名s的类型是char[1000],退化后就是char *;写成&s得到的是char (*)[1000](类型不对,gcc 会警告)。- 字符计数必须减
'a',写成cnt[s[i]-'a']++。否则下标是 ASCII 码 97~122,越界(cnt只有 26 个元素)。 - 统计类的第二趟循环,遍历的是"桶"(0~25),不是原串。第一趟是"扫描输入、往桶里投",第二趟是"扫描桶、找答案"——两趟的目标不同,循环变量自然也不同。
样例验证
s = "aassddffd"→ 字母a:2, s:2, d:3, f:2→ 最少出现次数 = 2 ✅s = "abbbac"→a:2, b:3, c:1→ 最少 = 1 ✅
cnt[i] > 0 这个条件的必要性
s 只由小写字母构成,但不是所有 26 个字母都出现过。没出现过的字母桶里是 0,如果不加 cnt[i] > 0,res 会被压到 0,答案就错成 0 了。"只统计出现过的"是这类"求最值"题的固定动作。
计数器数组的写法:
22 · 二维字符数组与 %6s 对齐¶
以下程序运行所产生的输出是__。
#include<stdio.h>
int main()
{
char a[2][6]={"Sun","Moon"};
int i,j,len[2];
for (i=0;i<2;i++){
for(j=0;j<6;j++)
if(a[i][j]=='\0'){
len[i]=j;break;
}
printf("%6s: %d\n",a[i],len[i]);
}
}
解析
内层循环从前往后找 '\0',找到时的下标 j 就是字符串长度(下标 = 字符个数,因为 '\0' 本身不计入),所以 "Sun"→3、"Moon"→4,break 保证一找到就跳出、不做多余扫描。
关键在于 %6s:它是右对齐、最小宽度 6、左边补空格——"Sun" 补 3 个空格、"Moon" 补 2 个空格,而不是左对齐补在右边(那是 %-6s)。
另外 %d 前面那个空格来自格式串里 : 后面的空格。
23 · 字符串比较 · 首个不同字符之差¶
以下程序比较两个字符串,输出它们第一处不同字符的 ASCII 码差值,请填空。
#include <stdio.h>
int main(void)
{
char a[100] = "Hello World!";
char b[100] = "Hello Chengdu!";
(13) ;
while ( (14) ) /* 完成对应字符的比较,并判定一个字符串是否结束了 */
{
i++;
}
printf("%d\n", a[i] - b[i]);
return 0;
}
答案
| 序号 | 答案 |
|---|---|
| (13) | int i = 0; |
| (14) | a[i] == b[i] && a[i] != '\0' |
解析
(13) 为什么是 int i = 0;:这个位置在"函数开头、循环之前",是 C89 要求的变量声明区,而后面循环体和 printf 都要用 i——所以这里要声明 i,并且从 0 开始(要从两个串的第一个字符比起)。
写成 int i = 0, n; 这类同时声明其他变量也不算错,核心是把 i 定义并置 0。
(14) 是两个条件,缺一不可:
a[i] == b[i]:当前这一对字符还相同 → 继续往后找。一旦不等就退出,此刻的i正好停在"第一处不同"上——这正是printf要的下标 ✅。a[i] != '\0':a走到字符串末尾就收手。这一条用于防止越界:万一两个串完全相同,光靠第一条会一路比下去、冲出数组边界。有了它,循环最远只到'\0'就停。- 两个条件用
&&连接,意思是"还相等,且还没到末尾,就接着比"。
优先级提示:
==、!=的优先级都高于&&,所以这样写不用加括号;写成(a[i] == b[i]) && (a[i] != '\0')同样正确。两个条件的先后顺序可以互换(
a[i] != '\0' && a[i] == b[i]也对)。就本题这两个数组而言两者等价;但换成指针版本时,把"判末尾"写在前面更保险——先确认还有字符可读,再去读它。
求值:"Hello World!" 与 "Hello Chengdu!" 的前 6 个字符 Hello(含空格)完全一样,第 7 个(下标 6)开始不同:
| 下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
a |
H | e | l | l | o | ␣ | W |
b |
H | e | l | l | o | ␣ | C |
i 停在 6,'W' = 87,'C' = 67,所以
输出:20 ✅
顺带值得知道的边界情况:若两串完全相同(比如
a = b = "Hello"),i会停在末尾的'\0'(下标 5),a[5] - b[5] = 0 - 0 = 0→ 输出0。"相同得 0、不同得差值",正是
strcmp的语义(标准库的实现返回的就是这个差值)。所以本题这段自己实现的代码,其实就是strcmp的原理版。易错:把条件写成
a[i] != b[i] && a[i] != '\0'——这是把逻辑整个反过来了。它表示"字符不同就继续",可本题要的是"字符相同就继续往前找"(找到第一处不同就停)。真机实测:这样写在本题样例上循环体一次都不执行,
i停在 0,输出a[0]-b[0] = 'H'-'H' = 0,完全错(正确答案 20)。判断方法:找相同 → 条件是"不等";找不同 → 条件是"相等"。本题是后者,所以条件里写
==。真机验证(gcc 15.2.0):填
int i = 0;与a[i] == b[i] && a[i] != '\0'后,i停在 6,a[6]='W'(87)、b[6]='C'(67),程序输出20。