跳转至

知识点 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 个字符(\x1f);② 忘了 \0 会截断。

八进制转义只吃 07,十六进制转义吃 09a-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

解析

先分清两个对象的可写性:

char b[] = "abcd";   // b 是数组,内容可改 ✅
char *p  = "abcd";   // p 指向字符串字面量,内容只读 ❌

字面量存放在只读区,试图通过 pp[i] 修改它 = 未定义行为(通常直接崩溃)。

  • A 错:数组名不是可修改左值。gcc 报 error: assignment to expression with array type,编译就过不去。
  • B 错:p 指向字符串字面量,strcpy 会往只读区写入。实测这段代码编译通过、连警告都没有,一到运行就以退出码 139(段错误)结束 —— 编译能过、运行必挂,最阴险。
  • C 对:bp 都是地址,比较两个地址是合法表达式。
  • 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] 指向可写数组 ap[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]

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 是本地可写内存。这正好和第 2、3 题的"只读视图"形成对照。


5 · 字符串原地修改

编写程序,输入两个字符串 str1str2,删除 str1 中所有在 str2 出现的字符,输出处理后的 str1

样例

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

要求:补充完善函数 is_char_in_strremove_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_strwhile (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 个。这是原地去重思路在"压缩"场景下的应用。

这题有三个值得学的点:

  1. malloc(len + 1)+1 是给结束符 '\0' 留的位置(strlen 不含 '\0')。
  2. 函数返回堆内存时,调用方负责 free —— main 里的 free(trimmed) 正体现了这一点。
  3. 健壮性检查齐全: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"。函数接口定义为:

char** str_split(const char* src, char c, int * out_length);
  • 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. 先数——遍历一遍数出分隔符个数 +1 就是子串数("a,b,c" 有 2 个逗号 → 3 个子串)。

    int n = 1 这个初值很关键:n 个分隔符切出 n+1 段,所以初值是 1 不是 0。

  2. 申请——char **res = malloc(n * sizeof(char *)),注意这里是指针数组,每个元素还是一个 char *
  3. 切分——用一个 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,考场推荐):

res[idx] = strdup(cur);   /* strdup 自带 malloc,并自动补 '\0' */
需要先把子串临时拷到一个缓冲区里——原因是 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]; nameschar *[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 行带对的——很多人会误改它。

解析('找错改错'答题套路的完美范例)

总结出四条通用经验:

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

    对偶记忆:求最大,初值给最小(通常给 -1INT_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] > 0res 会被压到 0,答案就错成 0 了。"只统计出现过的"是这类"求最值"题的固定动作。

计数器数组的三步模板:

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


10 · 指针遍历字符串

关于以下代码段,哪个说法是正确的?

char str[] = "world";
char *p = str;
while (*p) {
    putchar(*p++);
}
  • 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 · 指针遍历字符串

关于以下代码段,哪个说法是正确的?

char str[] = "Hello";
char *p = str;
while (*p) {
    printf("%c", *p++);
}
  • 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]);
    }
}
答案
   Sun: 3
  Moon: 4
解析

内层循环从前往后找 '\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 am a qjh.
解析

第一步:认清有两个游标,起点不同、速度相同。

i0 起,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→qi→jg→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]  &&  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


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 = -10 < -1 为假,一轮都不跑)。

顺带一句:这里 ij 都是 int,所以空串时 j-1 等于 -1 也不会出事。

若把它们声明成 size_t 等无符号类型,j-1j == 0 时会翻转成一个巨大的正数,条件反而为真 → 直接越界。这是"C 语言里做倒着数的下标时,用 int 比用无符号类型安全"的实例。

本空的一个常见错答:j / 2

它看着像"到中点为止",但在这段代码里是错的——因为 j 在循环里每轮都在减小,j/2 不是常数,条件会提前变假:第 i 轮时 j = len - ii < j/23i < 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 处。