知识点 06 · 字符串与字符数组¶
本页最后更新于 ,内容可能过时
1 · \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'本身。
2 · 字符数组 vs char *¶
设有 char b[5]="abcd", *p="abcd"; 则正确的语句为( )。
- A.
b=p; - B.
strcpy(p,b); - C.
b==p; - D.
strlwr(p);
答案
C
解析
先分清两个对象的可写性:
字面量存放在只读区,试图通过 p 或 p[i] 修改它 = 未定义行为(通常直接崩溃)。
- A 错:数组名不是可修改左值。gcc 报
error: assignment to expression with array type,编译就过不去。 - B 错:
p指向字符串字面量,strcpy会往只读区写入。实测这段代码编译通过、连警告都没有,一到运行就以退出码 139(段错误)结束 —— 编译能过、运行必挂,最阴险。 - C 对:
b和p都是地址,比较两个地址是合法表达式。 - D 错:
strlwr不是标准 C,只有 MSVC 才有。在 Linux 的 gcc / glibc 下报error: implicit declaration of function 'strlwr',编译就不通过;即使在 Windows 上编过了,它要就地改写p指向的字面量,照样是写只读区。
易错:题干问的"正确的语句"指的是"合法、能正确运行",不是"表达式的值为真"。
b==p的值其实是 0(两个地址不同),但作为一条语句它是合法的 —— 排除掉 A、B、D 之后只剩它。gcc 会对这一行给出
statement with no effect警告,也是同一个道理:算出来没用上。char *指向字面量时是"只读视图",只能读不能写。
3 · 字符数组 vs char *¶
已知 char a[]="123", *p[] = {a,"abc"};,错误的表达式是( )。
- A.
p[1] = "456" - B.
p[1][0] = 'B' - C.
*(p+1) = "B" - D.
*p = "B"
答案
B
解析
先看清类型——p 是指针数组 char *p[2],即两个 char *:p[0] 指向可写数组 a,p[1] 指向只读字面量 "abc"。
| 选项 | 含义 | 合法性 |
|---|---|---|
A p[1] = "456" |
修改指针本身,让它改指别处 | ✅ 合法 |
B p[1][0] = 'B' |
修改 p[1] 指向的字面量内容 |
❌ 写只读区 |
C *(p+1) = "B" |
等价 p[1] = "B" |
✅ 合法 |
D *p = "B" |
等价 p[0] = "B" |
✅ 合法 |
关键区分:p[1] = ...(改指针)✅ vs p[1][0] = ...(改内容)❌。
易错:C 里的
*(p+1)看着复杂,其实恒等于p[1]——记住p[i] ≡ *(p+i)就不会被吓住。注意这里
p[0]指向的a是可写的,所以如果选项写成p[0][0] = 'B'反而是合法的——判断可写性要看指针指向的是数组还是字面量。
易错:指针数组 vs 数组指针
别把 char *p[2](指针数组)和 char (*p)[2](数组指针)混了:前者是 2 个指针,后者是 1 个指向 2 元素数组的指针。
4 · 字符串原地修改¶
以下代码是把一个字符串倒序,如 "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是本地可写内存。这正好和第 2、3 题的"只读视图"形成对照。
5 · 字符串原地修改¶
编写程序,输入两个字符串 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' 结束字符串
}
解析(这是必须掌握的通用模板)
"原地删除 / 筛选"的双下标法
这个模板同样适用于:数组去重、删除指定元素、快排的 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²))。这是写字符串题的常识性优化。
6 · 字符串原地修改¶
给出以下程序的输出结果,并说明 ostr 函数功能。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
char* ostr(const char* src) {
if (!src) return NULL;
int len = strlen(src);
char* result = (char*)malloc(len + 1);
if (!result) return NULL;
int i = 0, j = 0;
while (src[i]) {
if (src[i] == ' ' && src[i + 1] == ' ') {
i++;
} else {
result[j++] = src[i++];
}
}
result[j] = '\0';
return result;
}
int main() {
const char* test_str = "This is a test string.";
char* trimmed = ostr(test_str);
printf("%s", trimmed);
free(trimmed); // 释放内存
return 0;
}
答案
- 输出:
This is a test string. - 功能:把字符串中连续出现的多个空格压缩成一个空格(同时原样保留其他所有字符)。
解析
核心在这一段:
if (src[i] == ' ' && src[i+1] == ' ') i++; // 发现"连续两个空格" → 只跳过一格,不输出
else result[j++] = src[i++]; // 否则拷过去
每遇到"当前位置和下一位置都是空格",就只把 i 前移一格而不写入 result。循环往复,长度为 N 的空格串最终只会被写入 1 个。这是原地去重思路在"压缩"场景下的应用。
这题有三个值得学的点:
malloc(len + 1)的+1是给结束符'\0'留的位置(strlen不含'\0')。- 函数返回堆内存时,调用方负责
free——main里的free(trimmed)正体现了这一点。 - 健壮性检查齐全:
if (!src) return NULL;处理空指针、if (!result) return NULL;处理分配失败。
边界情况(自己写的时候要想清楚): - 开头就是空格:
" abc"。第一轮src[0]、src[1]都是空格 →i++,跳过一个;第二轮src[1]是空格、src[2]是'a'→ 把src[1]这个空格写进去。结果是保留了 1 个前导空格——本函数只压缩连续空格,不负责去首尾空白。 - 结尾的空格:"abc "→ 两个空格压缩成 1 个,尾部仍留 1 个空格(src[i+1]此时是'\0',不满足"下一个也是空格")。 - 全是空格:" "→ 输出 1 个空格。一句话:它做的是"多空格 → 单空格",不是
trim。
7 · 子串与分割¶
编写函数,将指定字符串以指定的分割符分割为若干个子串。例如将 "hello,world,this,is,a,test" 以 ',' 分割,得到 6 个子串:"hello" "world" "this" "is" "a" "test"。函数接口定义为:
src:原始字符串输入;c:指定分割符;out_length:传出参数,表示分割得到子串的数量;- 返回值:
char**表示一个数组,每个元素为char*(表示一个字符数组)。需要使用malloc申请的内存!
提示:string.h 提供了一些有用的字符串操作函数。你可能会用到的
char *strncpy(char * dest, const char * src, int n):将 src 字符串拷贝的前 n 个拷贝到 dest。char *strdup(const char * s):拷贝字符串 s,函数使用 malloc 创建新的字符串内存空间。char *strchr(const char * s, int c):从 s 中找到第一个出现的与 c 相同的字符,返回其地址。如果没有找到返回 NULL。
PS:只要求写出 str_split 函数的定义,无需编写测试程序。
答案(参考代码)
char** str_split(const char* src, char c, int * out_length) {
int n = 1, i, idx = 0, start = 0, len;
char **res;
/* ① 先数一遍分隔符,算出子串个数 */
for (i = 0; src[i] != '\0'; i++) {
if (src[i] == c) n++;
}
/* ② 申请"指针数组" */
res = (char **)malloc(n * sizeof(char *));
if (res == NULL) { *out_length = 0; return NULL; }
/* ③ 逐段切分 */
for (i = 0; ; i++) {
if (src[i] == c || src[i] == '\0') {
len = i - start;
res[idx] = (char *)malloc(len + 1); /* +1 给 '\0' */
if (res[idx] == NULL) break;
strncpy(res[idx], src + start, len);
res[idx][len] = '\0'; /* 手动补结束符 */
idx++;
start = i + 1;
if (src[i] == '\0') break;
}
}
*out_length = idx;
return res;
}
解析(思路:三段式,记住就能套)
- 先数——遍历一遍数出分隔符个数
+1就是子串数("a,b,c"有 2 个逗号 → 3 个子串)。int n = 1这个初值很关键:n 个分隔符切出 n+1 段,所以初值是 1 不是 0。 - 申请——
char **res = malloc(n * sizeof(char *)),注意这里是指针数组,每个元素还是一个char *。 - 切分——用一个
start记住当前子串的起点;遇到分隔符或'\0'时,子串长度就是i - start,拷出来、补'\0'、idx++、start = i + 1。
三个易错点(也是本题的主要扣分点):
strncpy不会自动补'\0'!必须手动写res[idx][len] = '\0';malloc(len + 1)的+1别漏(len是字符数,'\0'要占一格)。*out_length是"传出参数",要通过指针写回调用方(写成out_length = idx;是改形参,调用方看不到)。
边界情况(题干没说,但必须想到):
- 连续分隔符:
"a,,b"按','切 → 得到 3 个子串"a"、""(空串)、"b"。本实现保留空串,因为len会是 0,malloc(1)后写入'\0',得到合法的空字符串。(若题目要求"跳过空段",需要在len > 0时才建结点。) - 首字符就是分隔符:
",a"→ 第 1 段是空串"",第 2 段是"a",共 2 段。 - 末尾就是分隔符:
"a,"→ 循环在i走到'\0'时还会再切一段,得到空串"",共 2 段。这正是if (src[i] == '\0') break;必须放在"切完这段之后"的原因——先切完最后一段再退出。 - 空串
"":n = 1,切出 1 个空串,*out_length = 1。 str中含'\0'之前的普通字符:for (i = 0; ; i++)是无界循环,靠内部的break退出——唯一的出口就是src[i] == '\0',不会越界。
更简洁的写法(用
需要先把子串临时拷到一个缓冲区里——原因是strdup,考场推荐):src里的这一段并不是以'\0'结尾的独立字符串,而strdup只接受'\0'结尾的串。(
const不是障碍:strdup的形参本来就是const char *。)
8 · 字符串比较¶
以下程序在 5 个字符串中求最小字符串并输出结果。请指明第几行有错误并写出正确的语句(不增删语句,每个错误 2 分)。
/* 1 */#include <stdio.h>
/* 2 */#include <string.h>
/* 3 */void main(){
/* 4 */ char names[5] = {"Windows", "Word", "Excel",
/* 5 */ "Powerpoint", "Outlook Express"};
/* 6 */ char *temp;
/* 7 */ int i;
/* 8 */ temp = names;
/* 9 */ for(i = 1; i < 5; i++)
/* 10 */ if(strcmp(temp, names[i]) < 0)
/* 11 */ temp = names[i];
/* 12 */ printf("%s\n", *temp);
/* 13 */}
答案
4 处错误:
| 行 | 错误 | 改正 | 错因 |
|---|---|---|---|
| 4 | char names[5] = {...} |
char *names[5] = {...} |
要装 5 个字符串,应该用指针数组,不能用一个字符数组装 5 个字符串 |
| 8 | temp = names; |
temp = names[0]; |
names 是 char *[5],退化为 char **,不能赋给 char *temp |
| 10 | strcmp(temp, names[i]) < 0 |
strcmp(temp, names[i]) > 0 |
求最小字符串:当前"擂主"比挑战者大时才换人 |
| 12 | printf("%s\n", *temp) |
printf("%s\n", temp) |
%s 需要的是字符串地址 char *;*temp 是单个字符 |
解析
- 第 4 行的关键:
char names[5]只能存 5 个字符,装不下 5 个字符串。正确写法是char *names[5](指针数组),或者char names[5][20](二维字符数组)。注意
"Powerpoint"有 10 个字符、"Outlook Express"有 15 个字符——用二维数组的话第二维至少得开 16(15 +'\0'),"长度 + 1" 这个老规矩别忘了。 - 第 8 行的关键:
names作为char *[5]退化成char **,赋给char *temp类型不匹配。要的是第一个字符串,所以写names[0]。 - 第 12 行是
%s的经典陷阱:printf("%s", p)传的是指针;传*p(单个字符,值是'E'= 69)会把这个小整数当成地址去解引用 → 段错误。 - 第 10 行的"打擂台"逻辑:
strcmp(a,b) > 0表示a大于b。要求最小值,就是"当前擂主比挑战者大就换人"。
答题技巧
第 3 行的 void main() 不是标准写法(应为 int main(void)),但材料上标出的 4 处错误里没有它——这类题要按题目给的错误数量作答,不要改这种"不规范但能编译"的地方,否则会超出错误数限制。
求最小/最大时,擂台上永远站着当前最优的那个:求最小在"当前擂主比挑战者大"时换人,求最大在"当前擂主比挑战者小"时换人。
9 · 字符计数与统计¶
输入由小写字母构成的非空字符串 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];本身没错,它是被第 10 行/第 11 行带对的——很多人会误改它。
解析('找错改错'答题套路的完美范例)
总结出四条通用经验:
- 求最小值,初值要给最大。
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 了。"只统计出现过的"是这类"求最值"题的固定动作。
计数器数组的三步模板:
10 · 指针遍历字符串¶
关于以下代码段,哪个说法是正确的?
- A. 输出
world - B. 输出
world\n - C. 输出
drow - D. 输出
\nworld
答案
A
解析
*p++ 的语义是"先取 *p 交给 putchar,再把 p 后移一格",所以依次输出 w o r l d;当 p 走到结尾的 '\0' 时 *p 为 0,while 条件变假退出。
这里 p 是独立的指针变量(不是数组名),p++ 改的是指针自己,str 数组的内容一个字节都没动。B 是干扰项——代码里根本没有输出 '\n' 的语句。
while (*p) putchar(*p++); 是"指针版遍历字符串"的标准写法,等价于 for (i = 0; str[i]; i++) putchar(str[i]);。
11 · 指针遍历字符串¶
关于以下代码段,哪个说法是正确的?
- A. 输出
Hello - B. 输出
Hello\n - C. 输出
hello - D. 输出
hello\n
答案
A
解析
*p++ 先取值、后移针,while (*p) 在 p 指到 '\0' 时结束,所以正好输出 5 个字符 Hello,'\0' 本身不会被打印。
C、D 的 hello 是"大小写混淆"干扰项——C 语言区分大小写,'H'(72) 和 'h'(104) 是两个不同的字符,程序原样输出大写 H。
这道题只要抓住"取值用的是自增前的地址"就不会错。
12 · 字符串里的转义与截断¶
执行语句 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 把 \\ 当成两个字符,全是错选。
13 · 二维字符数组与 %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 前面那个空格来自格式串里 : 后面的空格。
14 · 指针与下标两个游标¶
请写出以下程序的输出结果。
#include <stdio.h>
int main(void)
{
char str[100] = "I am a pig.";
char* p = &str[7];
int i;
for (i = 0; str[i] != '\0'; i++, p++)
{
if (*p >= 'a' && *p < 'z')
(*p)++;
else if (*p == 'z')
*p = 'a';
}
puts(str);
return 0;
}
解析
第一步:认清有两个游标,起点不同、速度相同。
i 从 0 起,p 从 &str[7] 起;循环的 i++, p++ 让它们每轮各走一格,所以 p 永远比 i 多走 7 格,p 指向的就是 str[i+7]。
第二步:循环跑几轮由 i 说了算。 条件是 str[i] != '\0':
| 下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
str[] |
I |
␣ | a |
m |
␣ | a |
␣ | p |
i |
g |
. |
\0 |
i 一直要走到 str[11] == '\0' 才停,所以循环共跑 11 轮(i = 0…10)——p 也就在这 11 轮里从 str[7] 一路走到了 str[17]。
第三步:这 11 个位置 p 到底读到了什么。
p 指向 |
str[7] |
str[8] |
str[9] |
str[10] |
str[11] |
str[12]~str[17] |
|---|---|---|---|---|---|---|
| 字符 | p |
i |
g |
. |
\0 |
0(数组零填充区) |
| 变换后 | q |
j |
h |
. |
不变 | 不变 |
变换规则是"小写字母向后挪一位":*p >= 'a' && *p < 'z' 时 (*p)++(p→q、i→j、g→h);'z' 单独用 else if 绕回 'a'(因为它不满足 < 'z'),合起来就是26 个字母的循环移位。
'.'(46) 既不是小写字母也不是 'z',原样不动;后面的 0 更不会被碰。
所以最终字符串是 I am a qjh. ✅
本题最关键、最容易被想当然的一点:
p并没有在字符串结尾的'\0'处停下——它继续走到了str[17],越过了字符串末尾 7 个字节。这在别的题里往往就是越界 UB,但这里为什么不会崩?因为
str声明的是char str[100],而 C 的规则是聚合初始化时没写到的成员一律补 0:= "I am a pig."只写了前 12 字节,余下 88 字节全是0。
p走进的正是这片零填充区,读到的永远是 0,既不越出数组边界,也不改变输出。可迁移的结论:判断"指针走过头会不会崩",要看的不是字符串有多长,而是数组开了多大。
char str[100]装 11 个字符,后面 89 个字节是安全的零区;如果写成char *str = "I am a pig."(指向字面量、没有多余空间),同样的循环就是实打实的越界了。真机验证:gcc 15.2.0 下逐轮打印
p-str与*p,确认p从 7 走到 17,str[11]起全为 0;开-fsanitize=address,undefined跑也是一片干净,无越界、无 UB。第二个坑:
(*p)++的括号绝对不能省。(*p)++是"把p指向的那个字符加 1";若写成*p++,那是"取*p然后让p自增"——根据优先级++作用在p上而不是*p上,字符串内容一个字符都不会改,整个程序就废了。要改的是"内容"就写
(*p)++,要移的是"指针"就写p++,本题两者各要一次,正好并排出现。第三个记忆点:
i在这段代码里只用来数循环次数(喂给str[i] != '\0'),真正读字符的是p,而p == &str[i+7]——两个游标之间差着一个固定的偏移量 7,这是最容易看漏的地方。想快速核对时,就在草稿纸上写一行
p → str[i+7],一切下标立刻对上。真机验证(gcc 15.2.0):输出
I am a qjh.。
15 · 字符串比较 · 首个不同字符之差¶
以下程序比较两个字符串,输出它们第一处不同字符的 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。
16 · 字符串原地逆序¶
函数 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;""→ 空行。
说明¶
- 第 8 题题干第 3 行的
void main():void main(){本身不是标准 C(应为int main(void))。但材料上标出的 4 处错误里没有包含它,下面按材料上的收录,只在解析里提示。若按严格标准把这行也算错误,则本题应为 5 处。