☰
C语言字符串逆序:从双指针到边界条件的经典练习题
2026/10/13 0:21:27 网站建设 项目流程

C语言练习题做到第五个,这个节点很微妙。前面几题基本把变量、循环、条件分支、数组这些基础过了一遍,再往下走就是字符串和指针这两座大山。这期练习我选了一个特别经典的题目:字符串逆序输出。别小看这道题,它在PTA、学校OJ、期末考里反复出现,几乎每个学C的人都写过。更难的是,它能把指针、数组、字符串处理、边界条件这些知识点全串起来,一道题吃透,后面很多题都会顺手很多。这篇就围绕这个练习展开,把实现思路、完整代码、遇到的坑和排查方法全部分享出来,也顺便聊聊怎么把它和冒泡排序、九九乘法表这些邻居题串成一个完整的练习节奏。

1. 练习拆解:字符串逆序到底在考什么

1.1 需求表述

先看题目本身。一个典型的描述是:输入一行字符串,将其逆序输出。比如输入 "hello",输出 "olleh";输入 "C language",输出 "egaugnal C"。

看起来很简单,对吧?但仔细想就有几个问题冒出来了:字符串怎么读进来,读多长,存在哪里?逆序是就地反转还是重新生成一份?输出的时候怎么保证没有多余的换行或者乱码?

PTA上那道“字符串逆序”题是这么要求的:给定一个字符串,按逆序输出。有些变体是要求原地逆序,有些只要求输出结果。写之前先看清题目要求,这很重要,因为“只输出逆序”和“把数组真正反转过来再输出”是两种实现思路,虽然结果一样,但考察点完全不同。

这道题表面上是“逆序”,实际上是在同时考察字符串的存储本质、数组下标访问、指针运算、边界条件处理这几件事。尤其是它逼着你搞清楚一个核心概念:字符串在C语言里到底长什么样。

1.2 为什么它是经典题

字符串逆序能成为经典练习,不是因为它难,而是因为它“小且全”。

字符串在C语言里不是一种原生类型,它是一个以'\0'结尾的字符数组。这意味着你操作字符串时,本质上还是在操作数组,但多了一个“结束标志”的概念。处理这个结束标志,恰恰是新手最容易出错的地方。你可能会问:我直接定义一个字符数组,用循环把字符倒着打印出来不行吗?行,但那样你只完成了“输出”这一半,没有真正理解“字符串是数组”这个底层事实。

我见过很多同学学C学到指针就卡住,回头看其实就是字符串这关没过去。字符串、指针、数组三者在C语言里是三位一体的:数组名是首地址,指针可以遍历数组,字符串就是数组的特殊形态。逆序这道题,三种视角都用得上,确实是性价比极高的一题。

还有一层:这道题是很多后续算法题的“母题”。判断回文、反转单词顺序、大整数加减,底层都离不开逆序操作。把逆序写得干净利落,后面学那些题会轻松很多。

2. 动手之前,先把输入这关做好

2.1 scanf 与 fgets:读字符串的两种常见姿势

写逆序之前,第一步是读入字符串。这里就有第一个大坑:到底用scanf还是fgets?

很多教材例题喜欢写:

char str[100]; scanf("%s", str);

这行代码有个隐藏问题:scanf配合%s遇到空格、Tab、换行就停止读取了。如果你输入的是 "hello world",scanf只会读进 "hello","world" 被留在缓冲区里。而逆序题最常见的输入恰恰是“一行字符串”,中文语境下很可能是“I love C”这种带空格的句子。用scanf会导致结果完全不对,而且你还不容易排查到原因。

推荐的做法是用fgets:

char str[100]; fgets(str, sizeof(str), stdin);

fgets会读取一整行,包括空格,直到遇到换行符或者达到指定的长度上限。这个函数的第三个参数是文件流,stdin表示标准输入,也就是键盘输入。

关于fgets有一个细节必须注意:如果读入的行长度小于缓冲区容量,换行符'\n'也会被存进字符串里。比如输入 "abc" 然后回车,str里的实际内容是"abc\n\0"。这时候直接做逆序,结果会变成"\nba",输出看起来就是有个换行位置不对,特别难看。所以在逆序之前,要么手动把末尾的'\n'去掉,要么在计算长度时排除它。

我常用的处理方式比较笨但很稳:

size_t len = strlen(str); if (len > 0 && str[len - 1] == '\n') { str[len - 1] = '\0'; }

这个操作就一句话:如果最后一个字符是换行,就把它替换成字符串结束符。这样字符串才真正干净了。

2.2 数组长度、越界和 \0 的边界感

接下来要决定:字符数组开多大?

如果题目没给最大长度,一般开个 100 或 1000 就够用了;如果给了,比如“长度不超过 80”,那就开 81。为什么是 81 不是 80?因为还要留一个位置给'\0'。这个 80 vs 81 的问题,就是边界感的起点。

实际输入的长度往往和你声明的大小没直接关系。fgets的第二个参数会限制最多读入多少个字符,比如fgets(str, 100, stdin)最多读 99 个字符,第 100 个位置是'\0'。如果你声明的数组实际只有 50 个字节,却把 100 传给了fgets,那就会造成缓冲区溢出,程序直接崩掉。所以老实写sizeof(str)是比较稳妥的,只要str是数组,这个表达式会自动换算成数组的字节数,不会多读。

还有一点,写逆序循环时,最容易出错的是边界。假设字符串长度为len,下标范围是0到len-1,str[len]是'\0'。很多新手写逆序时会把结束符当普通字符处理,或者在判断到'\0'后还继续往下走,结果越界读到未知内存。这里的关键就是:字符串的长度不包括'\0',但你分配内存时必须给它留位置。

2.3 双指针交换的核心逻辑

字符串逆序最经典的做法是双指针原地交换:一个指针从前往后走,一个指针从后往前走,交换两个位置的字符,直到两个指针相遇或者交错。

str是数组名,也是首地址。定义两个下标或者两个指针:

int i = 0; int j = strlen(str) - 1;

这里j一定要减一,因为最后一个有效字符的下标是长度减一。循环条件是i < j,不是i <= j。为什么?因为当i == j时,两个指针指向同一个位置,交换同一个元素没有意义,徒增一次无用的操作。更重要的是,如果字符串长度是偶数,i和j会在中间某处“交错”,用i <= j可能会导致中间两个元素交换两次,结果又变回去了。这个细节在面试或者考试时经常被拿来扣分。

交换的过程就是经典的temp = a; a = b; b = temp;,只不过这里的a、b是数组元素。理解了这一步,后面学冒泡排序会发现,其实冒泡排序里面的交换也是同一套动作,知识就是这么串起来的。

3. 三种逆序实现,代码和运行分析一起给

3.1 方法一:双指针原地交换

这是最推荐掌握的写法,因为它空间复杂度是 O(1),只用了常数额外空间,而且逻辑非常直观。

#include <stdio.h> #include <string.h> int main() { char str[100]; printf("请输入一行字符串:"); fgets(str, sizeof(str), stdin); // 去掉末尾换行符 size_t len = strlen(str); if (len > 0 && str[len - 1] == '\n') { str[len - 1] = '\0'; len--; } // 双指针原地交换 int i = 0; int j = (int)len - 1; while (i < j) { char temp = str[i]; str[i] = str[j]; str[j] = temp; i++; j--; } printf("逆序结果:%s\n", str); return 0; }

运行一次看看效果:

请输入一行字符串:hello world 逆序结果:dlrow olleh

这段代码的关键点有几处。len用的是size_t类型,是strlen的返回类型,但在循环里int i和size_t混用容易产生编译警告,特别在严格编译选项下可能直接报错。所以我这里直接强转成int,学习阶段这样反而省心。另一个点是循环内部交换后i++、j--必须同时进行,只改一个会导致死循环或者只交换一半。

3.2 方法二:辅助数组拷贝

原题如果要求“不修改原串”,那就用辅助数组。思路是:把原字符串从后往前逐个字符拷贝到另一个数组里,然后输出。

#include <stdio.h> #include <string.h> int main() { char str[100]; char reversed[100]; int i, j; fgets(str, sizeof(str), stdin); size_t len = strlen(str); if (len > 0 && str[len - 1] == '\n') { str[len - 1] = '\0'; len--; } for (i = (int)len - 1, j = 0; i >= 0; i--, j++) { reversed[j] = str[i]; } reversed[j] = '\0'; printf("%s\n", reversed); return 0; }

注意最后一行reversed[j] = '\0';容易漏掉。reversed是局部数组,初始内容是未定义的“脏数据”,如果不在末尾手动加结束符,printf在输出完有效字符后会继续打印内存里的随机内容,直到碰巧遇到一个'\0'。这正是很多新手输出乱码的常见来源。运行结果和第一种方法一样,但代价是多用了 100 字节的空间。在嵌入式等内存受限场景下,第一种方法明显更优。

3.3 方法三:递归实现

递归实现逆序是进阶玩法,适合已经掌握前两种方法之后再来研究。思路是:每次取出字符串的第一个字符,递归处理剩下的部分,处理完再把这个字符放到最后。

#include <stdio.h> #include <string.h> void reverse_recursive(char str[], int start, int end) { if (start >= end) { return; } char temp = str[start]; str[start] = str[end]; str[end] = temp; reverse_recursive(str, start + 1, end - 1); } int main() { char str[100]; fgets(str, sizeof(str), stdin); size_t len = strlen(str); if (len > 0 && str[len - 1] == '\n') { str[len - 1] = '\0'; len--; } reverse_recursive(str, 0, (int)len - 1); printf("%s\n", str); return 0; }

递归的边界是start >= end,每次递归交换两端字符,然后向中间收缩。这个写法的好处是代码结构清晰,但它有一个致命的限制:递归深度等于字符串长度的一半。字符串是 998 个字符时,要递归约 499 层,C 语言的函数调用栈默认一般是 1MB 到 8MB 不等,太深会栈溢出。考试题目的测试数据一般不算太大,但你要知道这个边界在哪里,免得在实际开发里写出一个递归版本然后被线上数据打爆。

实体上,我建议初学者先掌握双指针法,辅助数组法作为对照,递归法作为思维扩展,三种都会写才算真的吃透。

3.4 主函数组织和实际运行

不管哪种方法,主函数的组织都有一些小技巧。比如多组测试数据的场景,fgets要放在循环里,而且要注意每次读取后缓冲区是否残留了字符。如果上一组数据有剩余内容,下一组fgets可能读到的是残留值,导致结果错得莫名其妙。

一个稳妥的多组输入框架长这样:

while (fgets(str, sizeof(str), stdin) != NULL) { // 去换行、逆序、输出 }

fgets返回NULL表示读到文件末尾或者出错,配合循环就能一直读下去。在本地环境里想结束输入,按 Ctrl+Z(Windows)或 Ctrl+D(Linux)就行。

运行验证时,除了正常的hello,我建议专门测试三种边界情况:空字符串、单个字符、带空格的长字符串。空字符串时len是 0,j是 -1,循环根本不会执行,程序直接输出空行,这是正确的。单个字符时i和j相等,循环不执行,原样输出,也是正确的。这两个边界能通过,说明代码基本健壮了。

4. 调试现场:我踩过的坑和排查方法

4.1 坑一:输出多了一个换行

这是我最早写这道题时遇到的情况。明明逻辑看着没问题,但逆序后输出的第一个字符(原字符串的最后一个字符)前面莫名其妙多个空行,或者输出结果中间夹了一个换行。

原因前面说过:fgets把回车也读进去了。比如输入abc回车,str的内容是abc\n\0,逆序后变成\ncba\0,用printf输出自然先换了一行再打cba。

排查方法很简单:在逆序之前打印一下每个字符的 ASCII 码,你会看到末尾多了一个10,那就是换行符的 ASCII 码。写代码时顺手在fgets后面加三行去换行的逻辑,这是几乎所有字符串练习题都要做的标准动作,建议形成肌肉记忆。

4.2 坑二:中文字符串乱码

练习里可能遇到中文输入,比如输入你好世界,逆序输出变成乱码。这其实不是逆序逻辑的错,而是编码问题。

在 GBK 或 UTF-8 编码下,中文字符通常占用多个字节,按字节逆序会把一个汉字的字节拆散,比如你在 UTF-8 中是三个字节\xE4\xBD\xA0,逆序后这三个字节被打乱重组,输出的东西自然没法看。

这道题的常规解法是按字节处理的,所以设计上就只适合纯英文、数字、标点这类单字节字符。遇到中文,需要按字符边界处理,那牵扯到编码库了。考试和练习一般用英文用例,这个问题了解即可。但我见过不少同学在自己电脑上输入中文,然后怀疑代码写错了,先确认输入内容是否包含多字节字符,能少走很多弯路。

4.3 坑三:用 %d 读入字符,后面全乱了

热搜词里有一条“c语言变量用%d输入一个字符后的值”,这确实是很多初学者的迷惑点。比如你写了:

char c; scanf("%d", &c);

然后输入一个数字,比如65,scanf会按整数的规则解析,把 65 这个值存进c的一个字节里。由于char只有 1 个字节,存储结果其实是 65 对应的二进制截断值。再往后用%c或%s输出,结果完全不是你想的那样。

%d对应的是int*,%c对应的是char*,这两种格式符的解析规则完全不同,混用轻则数据错乱,重则产生编译警告,在严格模式下直接报错。我有一回调试别人的代码,发现整个程序输出全是问号,最后定位到就是这里写错了。规范做法是格式符和变量类型严格匹配,用%d读整数就声明int,用%c读字符就声明char,不要为了省一个变量类型自作聪明。

4.4 常见问题速查表

现象可能原因排查方式
逆序结果前多一个换行fgets读入了'\n'打印字符串长度和最后一个字符的 ASCII 码
输出乱码字符串未加'\0'或中文多字节检查辅助数组末尾的结束符,确认输入编码
程序崩溃数组越界或缓冲区溢出检查fgets长度参数是否与数组大小匹配
死循环循环内i、j更新遗漏单步调试,看变量变化
编译警告size_t与int混用统一类型或显式强转

5. 题库里的邻居题:冒泡排序与九九乘法表

5.1 冒泡排序:另一种“交换”思想

字符串逆序里用到的交换操作,在冒泡排序里会再次遇见。冒泡排序的核心也是交换,只不过它交换的条件不再是“位置对称”,而是“顺序不对”。

一个典型的冒泡排序实现:

#include <stdio.h> void bubble_sort(int arr[], int n) { for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - 1 - i; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } } int main() { int arr[] = {5, 2, 9, 1, 6}; int n = sizeof(arr) / sizeof(arr[0]); bubble_sort(arr, n); for (int i = 0; i < n; i++) { printf("%d ", arr[i]); } printf("\n"); return 0; }

内层循环条件j < n - 1 - i里的- i是冒泡排序的核心优化:每一轮都会把一个最大值“冒”到最后,下一轮就没必要再比较已经排好的位置。这个- i很多新手会漏掉,漏掉后排序结果依然正确,但比较次数变多,性能差些。如果你想在练习里做一个对比实验:把- i去掉,打印内外层循环的总执行次数,你会直观看到优化前后的差距。

逆序和排序的共同本质是“交换”,区别在于交换的驱动力不同。逆序是位置对称强制交换,排序是大小关系条件交换。把两题连着写,你会发现交换的临时变量temp用法已经熟练到不用思考了。

5.2 九九乘法表:双重循环的格式控制

九九乘法表是另一个必写的基础题,它和逆序题在结构上没有直接关系,但它能训练你对循环边界的敏感度。

#include <stdio.h> int main() { for (int i = 1; i <= 9; i++) { for (int j = 1; j <= i; j++) { printf("%d*%d=%-2d ", j, i, i * j); } printf("\n"); } return 0; }

%-2d表示左对齐、最小宽度 2,这样输出表格才能对齐,不然不同位数乘积会错落难看。内层循环条件是j <= i,只打印下三角部分。如果你想输出完整的 9×9 方阵,把条件改成j <= 9就行。这一题同样在考循环边界,但它的陷阱比逆序少,适合在写逆序题前后作为暖身练习。

5.3 练习节奏建议

如果你按照“C 练习”这个系列往下走,我建议的节奏是这样的:先写九九乘法表,熟悉双重循环和格式控制,再做字符串逆序,吃透数组、指针、字符串这三者的关系,最后写冒泡排序,把交换、比较、嵌套循环融会贯通。三题练完,C语言的核心循环和数组的基础就算是打牢了。

做题的时候不要只盯着“能跑出正确结果”。我见过太多同学,题目过了就丢,过了两周再写同样的题又卡住了。更好的做法是每道题都问自己三个问题:能不能换一种实现方式?边界条件是什么?这个思路还能用在什么场景?比如逆序的双指针思路,在判断回文时其实就是两个指针从两端向中间移动,逐一比较字符。你把逆序吃透了,回文题基本就是顺手的事。

我自己的习惯是每道练习题至少写三遍:第一遍看着书写,理解每一行在做什么;第二遍合上书自己写,卡住了就回去看,但要做记号;第三遍隔几天再写,要求一次性通过,并且能说出为什么这样写。这个过程比较费时间,但效果比大量刷题要好得多。

最后再分享一个小技巧。学C语言时手里一定要有一个趁手的调试环境,VSCode 配置 C/C++ 环境其实是几分钟的事,装好编译器、配好 launch.json 就能单步调试。遇到逆序这种边界问题,单步执行比反复读代码快得多,你能直观看到i、j怎么移动、每次交换后数组变成什么样。这个习惯从练习题阶段就养成,后面做项目会感谢自己的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询