跳转至

第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 · 数组名不可修改(比较与取地址)

如果定义了以下变量和数组,那么以下可以通过编译的合法表达式或者语句是( )。

int a[3], b[3], i = 3, j;
  • 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

以下代码的输出是什么?

int arr[5] = {0};
printf("%d\n", arr[3]);
  • 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 元素"的混淆。

改正后的输出(降序,每个数后面跟两个空格、末尾无换行):

15  13  9  8  4  2


6 · 字符(ASCII 码)作数组下标

若有以下语句,各选项中数值为 4 的表达式是__。

int a[12]={1,2,3,4,5,6,7,8,9,10,11,12};
char c = 'a', d, g;
  • 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 与字符数组的“长度”

static char x[] = "12345";
char y[] = {'1','2','3','4','5','\0'};

那么( )。

  • A. x 数组的长度等于 y 数组的长度
  • B. x 数组的长度大于 y 数组的长度
  • C. x 数组的长度少于 y 数组的长度
  • D. x 数组与 y 数组的存储区域相同
答案

A

解析

这里的"长度"指数组占用的内存字节数,也就是 sizeof:

sizeof(x) == 6     /* '1'~'5' 五个字符 + 字面量隐含的那个 '\0' */
sizeof(y) == 6     /* 初值表里明写了 6 个字符,含自己写上的 '\0' */

两者都是 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. 二维数组第一维可以省略,由初始化列表的行数推导。{{1},{2},{3},4} 有 4 个初始化元素(前 3 个是 {...},第 4 个是裸的 4,它会开启新的一行),所以 a 是 int[4][2]。
  2. 元素总个数 = 4 × 2 = 8 个 int。
  3. sizeof(int) = 4 字节。
  4. sizeof(a) = 8 × 4 = 32。✅

行展开后是:

a[0] = {1, 0}
a[1] = {2, 0}
a[2] = {3, 0}
a[3] = {4, 0}

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]);
        }
}
答案
14 32 50 32 77 122 50 122 194
解析

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

三个要点:

  1. 转置:b[j][i] = a[i][j] 是行列互换,即 b = aᵀ,问题转化为"矩阵乘自己的转置"。
  2. 结果是对称矩阵(c[i][j] == c[j][i]),下三角可以由上三角得到,只需算 6 个数(对角线 3 个 + 上三角 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 · 二维数组的反对角线访问

下面语句的输出结果是( )。

int i, x[3][3] = { 1,2,3,4,5,6,7,8,9 };
for (i = 0; i < 3; i++)
    printf("%d", x[i][2 - i]);
  • 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 的版本得到正确的转置矩阵。

改完之后的运行结果:

 the result array is:

    1    4    7
    2    5    8
    3    6    9

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。✅

易错:

  1. 把 \x1f 当成 3 个字符(\、x、1、f)
  2. 忘了 \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 字节

易错:

  1. 把 \\ 当成两个字符(那会数成 7 字节)
  2. 以为 \xfd 里 fd 是两个字符
  3. 忘了字符串末尾那个 '\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]:

t            = str[i];              // t 暂存左端
str[i]       = str[len-i-1];        // (3) 右端搬到左端
str[len-i-1] = t;                   // (4) 暂存值写回右端

两个空答案相同时,先判断是不是一次交换。

循环次数为什么是 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。

样例

输入:  Test is important
       test
输出: T i imporan

要求:补充完善函数 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]; 本身没错,不需要改动。

解析(找错改错题)

总结出四条通用经验:

  1. 求最小值,初值要给最大。int res = 0; 求最小永远是 0,第一条就该改。

    对偶记忆:求最大,初值给最小(通常给 -1 或 INT_MIN)。

  2. scanf("%s", s) 不要写 &s。数组名 s 的类型是 char[1000],退化后就是 char *;写成 &s 得到的是 char (*)[1000](类型不对,gcc 会警告)。
  3. 字符计数必须减 'a',写成 cnt[s[i]-'a']++。否则下标是 ASCII 码 97~122,越界(cnt 只有 26 个元素)。
  4. 统计类的第二趟循环,遍历的是"桶"(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 了。"只统计出现过的"是这类"求最值"题的固定动作。

计数器数组的写法:

int cnt[26] = {0};                    /* ① 清零(数组初始化列表补 0) */
for (i = 0; s[i]; i++) cnt[s[i]-'a']++;   /* ② 第一趟:投桶 */
for (i = 0; i < 26; i++) ...              /* ③ 第二趟:扫桶 */


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]);
    }
}
答案
   Sun: 3
  Moon: 4
解析

内层循环从前往后找 '\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]  &&  a[i] != '\0'
  • 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,所以

87 - 67 = 20

输出: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。