news 2026/10/11 12:57:25

C语言字符串逆序实战:函数传参、指针运算与工程化实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言字符串逆序实战:函数传参、指针运算与工程化实现

C经典100例练到第43题,说实话已经过了最容易劝退的阶段。前面那些变量、循环、数组题目做完,基本语法都摸过一遍了,这一题开始转向“函数+指针+字符串处理”的综合运用,需要你从“写代码能跑”过渡到“写代码有章法”。

第43题的题目本身不复杂:编写一个函数,将一个字符串中的字符逆序存放,然后输出。比如输入字符串”abcdef”,处理后输出”fedcba”。但就是这道看似简单的题,背后几乎覆盖了C语言最核心的几个知识点:参数传递的方式、数组名作为函数参数时的本质、指针运算、字符串的表示与终止符、原地修改与额外空间的区别。

我把这道题从“会写”到“写对”,再到“写得像工程代码”,完整拆一遍。顺便把我在练习过程中踩的坑、查的错、总结的调试方法一并分享出来,对刚学到函数和指针这部分的朋友应该会有帮助。

1. 题目到底在考什么?先别急着动手

拿到习题集,很多人习惯直接看参考答案,抄一遍跑通就算过。我建议停下来先拆解一下:这道题表面上让你实现字符串逆序,实际上至少考了4个点。

第一,函数如何接收字符串。字符串可以用字符数组char str[]存储,也可以用它退化成指针后的char *str来传递。题目要求“编写一个函数”,那么参数应该写成什么形式?是写void reverse(char s[])还是void reverse(char *s)?这两个在语法上等价,但理解上有差别。数组形式说“我传进来一个数组”,指针形式说“我传进来一个地址”。后者更接近机器层面的真实情况,因为数组名作为实参传递时,压根不会复制整个数组,只传首地址。

第二,如何求字符串长度。字符串在C语言里没有“长度”这种内置属性,全靠结尾的\0字符来判定边界。所以需要自己遍历一遍找到\0的位置,才算得到长度。很多人直接用sizeof(str)想拿长度,跑出来发现是100(数组声明的大小)或者8(64位机器上指针的大小),就是没搞清楚sizeof在数组和指针上下文里的区别。

第三,如何交换字符。逆序的本质是“对称位置互换”,s[0]和s[len-1]换,s[1]和s[len-2]换,直到中间点。交换需要临时变量,这个写多了自然就会。但要考虑循环次数是len/2,不能是len,否则换过去又换回来等于没换。

第四,如何体现“原地”还是“复制”。如果新开一个数组,把原始字符串倒着拷进去,再拷回原数组,也是一种实现,但多用了O(n) 的额外空间。如果直接在原数组上交换,就是O(1) 的额外空间。前者代码更直观,后者更考察对内存的理解。面试里经常追问的就是这层区别。

2. 动手前必须搞懂的3个基础概念

与其直接上代码,不如先花点时间把三个基础概念理清楚。这节内容不鸡肋,直接决定你后面能否独立写出各种变体。

2.1 数组名、指针和sizeof的爱恨情仇

在绝大多数表达式中,数组名会“退化”为指向首元素的指针。char str[100]里的str,在传给函数、参与运算时,它的值就是&str[0]。所以函数形参写成char s[]也好,char *s也罢,编译器最终都当成指针处理。

这套机制带火了一个经典陷阱:在函数内部写sizeof(s),如果s是形参,它的大小就是指针的大小——64位系统上通常是8字节,哪怕你实参传进来的数组有100字节。这就是为什么求字符串长度必须用strlen或者自己遍历,不能指望sizeof。

与它对应的是函数外部的sizeof(str),这里的str还是完整数组,sizeof的结果是100。如果整个程序里都用sizeof试图求长度,一会儿得100一会儿得8,结果必然乱套。想知道当前字符串实际有多长,任何情况下都只认\0。

2.2 字符串字面量和字符数组的差异

写char *p = "hello"时,"hello"是存于只读区的字符串字面量,p只是指向它的指针,如果你试图p[0] = 'H',很多环境下直接段错误。写char arr[] = "hello"时,编译器在栈上开了一个6字节数组(5个字符+1个\0),然后把字面量的内容拷贝进去,这时候改arr[0]没问题。

第43题如果要原地逆序,就要求传入的字符串必须是“可修改的”,也就是字符数组形式。如果实参是字符串字面量,逆序操作会涉嫌改动只读区域,这是程序设计上的错误。虽然有的编译器放行,但行为不可控。最好养成习惯:练习这一类函数时,统一用char s[] = ...来准备测试数据。

2.3strlen的工作原理

strlen是标准库函数,头文件string.h里声明。它的实现思路简单粗暴:从起始地址开始,一个字节一个字节往后比对,直到发现\0,返回走过的字符个数,不包含\0本身。时间复杂度O(n)。

这个细节之所以重要,是因为你在reverse函数里要先strlen(s)获取长度,再基于这个长度做首尾交换。如果字符串没有以\0结尾,比如你用了char s[3] = {'a','b','c'},strlen会一直往后越界找\0,直到碰巧遇到一个0字节为止,这种未定义行为非常危险,可能读穿栈帧,导致奇怪的结果甚至崩溃。

3. 三种实现方案,照着写一遍比只看不练强十倍

方案无所谓绝对对错,不同阶段选不同的写法。我建议三种都敲一遍,互相比较,体会差别。

3.1 方案一:数组下标法,最直白的思路

这个方案借助临时变量,首尾对调。核心代码块:

#include <stdio.h> #include <string.h> void reverse(char s[]) { int len = strlen(s); for (int i = 0; i < len / 2; i++) { char temp = s[i]; s[i] = s[len - 1 - i]; s[len - 1 - i] = temp; } } int main() { char str[] = "abcdef"; reverse(str); printf("逆序后: %s\n", str); return 0; }

为什么循环只到len/2而不是len?如果是len,前半段把后半段换过来,后半段又把它换回去,转一圈等于没动。中间那条“对称轴”在字符串长度为奇数时,正好是中间那个字符,它不需要交换。

这个版本是“解码器”级别的写法,逻辑最清晰,适合验证思路。数组下标s[len - 1 - i]是标准的首尾映射,一眼能看懂。

3.2 方案二:双指针法,锻炼指针思考方式

用两个指针p指向开头,q指向末尾,然后while (p < q)交换,交换完p++、q--。核心代码块:

#include <stdio.h> #include <string.h> void reverse(char *s) { char *p = s; char *q = s + strlen(s) - 1; while (p < q) { char temp = *p; *p = *q; *q = temp; p++; q--; } } int main() { char str[] = "hello world"; reverse(str); printf("逆序后: %s\n", str); return 0; }

指针版本的好处是:你不再纠结“下标到底从0还是从1开始”,而是盯着两个地址比较大小。p < q这个条件保证只处理到中间,天然避开了“换回来”的问题。指针运算在这里直观体现出来:s + strlen(s) - 1指向最后一个有效字符,而不是\0。

很多教材推荐先学指针再做这类题,但我个人认为,你先把方案一写熟,再切换到方案二,会比较顺。下标版本帮你把“位置”这个抽象概念建立起来,指针版本帮你在“内存地址”层面再理解一遍。

3.3 方案三:递归反转

递归版本的思路是:每次把第一个字符存起来,把剩余部分递归逆序,再将第一个字符拼接到末尾。实现上有两种常见写法。

一种是“边递归边输出”,不修改原字符串:

#include <stdio.h> void reverse_print(const char *s) { if (*s == '\0') { return; } reverse_print(s + 1); putchar(*s); } int main() { char str[] = "abcdef"; printf("递归逆序输出: "); reverse_print(str); printf("\n"); return 0; }

另一种是“递归修改字符串内容”,需要额外注意哨兵字符的位置。这种写法在很多参考书上只给代码不给解释,我建议你自己推演一遍:递归到底层时,\0要安放在哪个下标,返回上一层时才能把首字符放到正确位置。推不出来的话,用调试器单步看几轮就明白了。

递归版本在面试里常被用来考察“函数调用栈”的理解,但在第43题这种教学场景下,我建议把它作为“理解递归”的练习题,而不是日常逆序的首选方案。递归有栈深度限制,如果字符串有几万个字符,递归很容易爆栈。

4. 完整代码工程化:从“能跑”到“能复用”

题目练习到第43题,你已经具备把一段代码组织成小工程的能力了。别老是把所有函数都塞在一个main.c里,尝试一下拆文件、写头文件、配Makefile,对后续学习大有好处。

4.1 目录结构

我习惯这样组织一个练习项目:

reverse/ ├── include/ │ └── str_util.h ├── src/ │ ├── str_util.c │ └── main.c └── Makefile

include放头文件,src放源文件,Makefile负责构建。规模不大,但结构清晰,以后项目变大时直接套这个骨架。

4.2 头文件怎么写

str_util.h内容:

#ifndef STR_UTIL_H #define STR_UTIL_H void reverse(char *s); #endif

#ifndef外加#define和#endif是头文件保护,防止同一个头文件被多个源文件包含时重复声明。这个细节在小型练习里看不出问题,但项目一大就会遇到“重复定义”“重复声明”的报错。趁早养成习惯。

4.3 源文件怎么写

str_util.c内容:

#include <string.h> #include "str_util.h" void reverse(char *s) { int len = strlen(s); for (int i = 0; i < len / 2; i++) { char temp = s[i]; s[i] = s[len - 1 - i]; s[len - 1 - i] = temp; } }

main.c内容:

#include <stdio.h> #include "str_util.h" int main(void) { char str[] = "abcdef"; reverse(str); printf("逆序后: %s\n", str); return 0; }

注意源文件包含头文件时,如果是自己项目的头文件,用双引号;如果是标准库头文件,用尖括号。这是一个约定俗成的规则:双引号优先在当前文件所在目录找,找不到再去系统头文件路径找;尖括号直接去系统头文件路径找。虽然混着用也能编译过,但按照规范能避免很多“头文件找不到”的困惑。

4.4 Makefile 配置

以下是可以直接用的最小Makefile:

CC = gcc CFLAGS = -Wall -Wextra -std=c11 -Iinclude all: reverse reverse: src/main.c src/str_util.c include/str_util.h $(CC) $(CFLAGS) -o $@ src/main.c src/str_util.c clean: rm -f reverse

-Wall和-Wextra开起来,让编译器把可疑代码都提示出来。-std=c11指定语言标准,避免一些老旧写法被默认标准惯坏了。-Iinclude告诉编译器去include目录找头文件。

生成二进制后运行:

make ./reverse

4.5 一个更健壮的边界测试程序

题目只要求输入一个示例字符串,但实际写代码时,我强烈建议你额外测试三个边界场景:空字符串、只有一个字符的字符串、全部相同字符的字符串。可以写成这样的测试结构:

#include <stdio.h> #include <string.h> #include "str_util.h" static int test_reverse(const char *input, const char *expected) { char buf[128]; strcpy(buf, input); reverse(buf); if (strcmp(buf, expected) != 0) { printf("FAIL: reverse(\"%s\") = \"%s\", 期望 \"%s\"\n", input, buf, expected); return 0; } printf("PASS: \"%s\" -> \"%s\"\n", input, buf); return 1; } int main(void) { int pass = 0; pass += test_reverse("abcdef", "fedcba"); pass += test_reverse("hello world", "dlrow olleh"); pass += test_reverse("a", "a"); pass += test_reverse("", ""); pass += test_reverse("aaaa", "aaaa"); printf("通过 %d/5 项测试\n", pass); return pass == 5 ? 0 : 1; }

把测试用例和主逻辑分离,这在工作后是家常便饭,但很多人从学习阶段就没有这个意识,导致代码越写越难维护。哪怕只是做练习,这个习惯也值得提前培养。

5. 我练这题时踩过的一堆坑,按真实代价排序

下面这些坑不是杜撰出来的理论风险,都是实际敲代码时遇到过的问题。按我记忆中翻车频率从高到低排列。

5.1 用sizeof代替strlen求长度

新手最容易犯的第一个错。示例:

char str[] = "abcdef"; int len = sizeof(str); // 结果是7,不是6

因为str数组长度是7,包含结尾的\0。逆序时如果按7来处理,str[6]是\0,交换之后\0跑到开头,后面打印直接空字符串,或者产生乱码。这是最典型的“编译通过但结果全错”的情况。

用strlen能得到6,正确处理。或者自己写个my_strlen循环计数,这题练习时可以故意不用标准库,手动实现一遍,加深印象。

5.2 实参传了字符串字面量

下面这种写法放在很多编译器上能跑,但实际上是危险操作:

char *str = "abcdef"; reverse(str); // 试图修改只读区

在我的环境上直接段错误。因为字符串字面量通常存放在只读段。真要用指针方式,也必须指向可写内存,比如:

char str[] = "abcdef"; char *p = str; reverse(p);

这样就安全了。记住这句话:字符数组可改,字符串字面量不可改。

5.3 循环次数多算一个

写for (i = 0; i < len; i++)的后果是逆了个寂寞,字符串完全没变。我见过有人查了很久,最后发现是循环边界写错。调试方法很简单,输出中间状态的数组看一眼就明白了。

5.4 输入带空格时用了scanf("%s", ...)

如果你想让用户输入一个类似hello world的字符串再用程序逆序,scanf("%s", str)只能读到空格前的内容,world丢了。正确做法是用fgets:

char str[128]; fgets(str, sizeof(str), stdin); str[strcspn(str, "\n")] = '\0'; // 去掉末尾换行符

然后传给reverse。注意fgets会把换行符也读进去,不加处理的话,逆序后换行符会跑到最前面,打印时产生奇怪的空行,类似“摆错位”的效果。

5.5 忽略中文编码问题

如果你测试的字符串里有中文,比如"你好世界",在UTF-8编码下一个汉字占3字节,strlen返回的是字节数而不是字符数。简单按字节逆序会把汉字内部字节顺序打乱,输出乱码。这不是逆序算法不对,而是“字节”和“字符”不是一回事。

想正确处理中文,就得引入宽字符、多字节字符处理函数,或者直接限定题目场景为纯英文字符串。练习第43题时,我建议先用纯ASCII字符串,把指针和内存机制吃透。等学了宽字符处理再回来扩展。

5.6 忽视头文件

忘了#include <string.h>直接用strlen,编译器会报警告,有些环境强行编过但运行行为诡异。因为老标准下没有声明的函数会隐式声明为返回int,而strlen返回size_t,位数可能不一致,导致长度计算出错,逆序结果不可预期。现在的编译器普遍把隐式声明当错误处理,但为了兼容老代码还是建议把该包含的头文件写全。

6. 用GDB和Valgrind给程序做体检

如果你只会“运行完看结果对不对”,那排查能力会受限。这道题虽然小,但验证工具链提前上手能受益很久。

6.1 用GDB单步看交换过程

编译时加-g参数:

gcc -g -o reverse src/main.c src/str_util.c gdb ./reverse

在GDB里打断点:

(gdb) break reverse (gdb) run (gdb) print s (gdb) print strlen(s) (gdb) next (gdb) print s[0] (gdb) print s[len-1]

你会发现print s显示的是字符串,print s[0]显示的是字符,这个区别很直观。单步走几轮,就能看清指针在函数内怎么指向内存。

我在初学阶段对指针一直半懂不懂,后来靠GDB把指针的地址和值打出来,看到p和q的地址动态变化,一下子通了。记忆犹新的场景是:

(gdb) print p $1 = 0x7fffffffe580 "abcdef" (gdb) print q $2 = 0x7fffffffe585 "f"

地址相差5,因为我打印的是q而不是*q。q作为一个指针,它的值是一个地址;*q才是地址上的字符。这种情况下容易混淆,打印时刻意加个*区分一下就行。

6.2 用Valgrind检查越界和内存泄漏

项目在Linux/macOS下可以装Valgrind。用法极简:

valgrind --leak-check=full ./reverse

如果代码没问题,它会输出类似All heap blocks were freed -- no leaks are possible的话。如果检测到非法读写,会直接提示非法的地址和调用堆栈。虽然这题没有动态内存分配,但非法读\0之外的内存照样可以查出来,前提是你测试时构造了越界场景。

Windows 环境下可以用Dr. Memory或者Application Verifier,效果类似。

6.3 自己写日志式调试

在没有调试器的环境里,最笨也最有效的办法就是打印中间态。比如逆序函数的每次交换都打印出来:

printf("交换 %d 和 %d: %c <-> %c\n", i, len-1-i, s[i], s[len-1-i]);

这个输出能直观告诉你程序执行的路径。测试完再把这些临时打印删掉或注释掉。很多人觉得打印调试低级,但在简单练习场景下,它比GDB省时间,而且打印信息好看懂。

7. 这一题之后的延伸思考,比题目本身更值钱

第43题做完不代表完事了。我列几个练完这题可以思考的方向,每个方向都是一道新题。

7.1 从逆序到反转单词

给定一个句子字符串,不反转字符顺序,而是反转每个单词的顺序。比如"hello world c language"变成"language c world hello"。常见做法是:整体逆序一次,再对每个单词内部逆序一次。这正是第43题变体的典型代表。想清楚两次逆序为什么能凑出单词倒序的效果,你对“逆序”这个操作的认识就不一样了。

7.2 从原地到考虑内存重叠

标准库里有memcpy和memmove两个函数。memmove专门处理内存重叠的情况。第43题的逆序如果改成“把字符串的前半段移到后半段”,就会遇到内存重叠问题,你得想想为什么不能用memcpy,以及怎么判断两个区间是否重叠。掌握这个点,在实现内存缓冲区的数据移动时会少很多潜在bug。

7.3 从C字符串到动态字符串

char s[100]是定长数组,如果用户输入超过99个字符,fgets读不完整,缓冲区就可能溢出。练习完这道题,可以写一个my_reverse_dynamic(const char *s),用malloc分配所需空间,返回一个逆序的新字符串,并记得释放内存。这自然就锻炼了指针、动态内存分配、返回指针的规范(谁分配谁释放)等知识。

7.4 从单文件到多文件的Makefile进阶之路

推荐给想进阶的人一份自发练习清单:

  • 把reverse做成一个静态库,让主程序链接静态库
  • 写一个.gitignore,把编译产物和备份文件忽略掉
  • 给reverse函数添加注释规范,写清参数、返回值、注意事项
  • 扩展测试代码,自动生成随机字符串测试,对比reverse(reverse(s))是否等于原字符串

这些练习完全不依赖新知识,只是把工程化的惯用做法过一遍,但带来的收获比单纯刷题大得多。

7.5 算法复杂度意识

第43题的时间复杂度是O(n),因为strlen要O(n),交换要O(n/2),总共O(n)。空间复杂度是O(1),因为只用了临时变量。

很多人在初学阶段对复杂度不敏感,但如果你将来想进大厂或者做底层开发,面试必问复杂度。这道题可以当复杂度分析的第一课:为什么O(n)?能不能省掉strlen这趟遍历?有没有方法同时逆序和求长度?如果允许额外空间,能否做出O(n) 空间但更简单的实现?

在脑子里回答这些问题,比背答案有意义。

8. 关于这道题,我最后想说的几句话

第43题的价值不在于“把字符串倒过来”这个动作本身,而在于它强制你同时面对数组、指针、字符串终止符、函数传参、内存可写性、边界处理这几座大山。能独立写出双指针版本并且讲清楚为什么循环条件是p < q,说明C语言的核心基础已经立住了。

我在学这道题的时候,花了一个晚上搞明白sizeof和strlen的区别,又在第二天用GDB验证了指针地址的变化。回头想,那些“绕不过去的弯”恰恰是真正让我学会C语言的地方。如果你也在练习中卡住了,别急着找答案,先按我前面说的方式打印中间状态、用调试器看看内存,多折腾一会儿,收获绝对比5分钟看到答案多得多。

等这道题做明白了,下一步可以去试试反转句子里单词顺序的那道变体,或者把reverse接到你自己写的小项目里。祝学得顺利。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/11 12:56:46

STL list容器深度解析:双向链表、迭代器失效与性能误区

说到STL里的list容器&#xff0c;估计不少人都经历过这么个阶段&#xff1a;刚开始学C的时候&#xff0c;被各种资料安利“链表插入删除效率高”&#xff0c;于是遇到需要频繁增删的场景就条件反射地掏出list&#xff0c;结果跑起来发现性能还不如vector&#xff0c;心里一阵问…

作者头像 李华
网站建设 2026/10/11 12:53:54

Kubernetes 上跑 Java 服务:Quarkus 与 HBase 组合实战

别人问我为什么在Kubernetes上跑Java服务&#xff0c;最后绕不开Quarkus和HBase这个组合。这俩放一起&#xff0c;乍一看一个是云原生Java框架&#xff0c;一个是老牌分布式列式数据库&#xff0c;好像没什么交集。但真在K8s里把有状态的HBase集群和无状态的Java服务编排到一起…

作者头像 李华
网站建设 2026/10/11 12:53:53

SAR成像PFA算法实战:正视与斜视的Python实现及避坑指南

简介&#xff1a;这份资源面向雷达信号处理方向的学习者与研究人员&#xff0c;聚焦合成孔径雷达&#xff08;SAR&#xff09;成像中的极坐标格式算法&#xff08;PFA&#xff09;实现&#xff0c;解决从原始回波数据到聚焦成像的完整链路问题。内容基于走停模式生成SAR回波数据…

作者头像 李华
网站建设 2026/10/11 12:50:34

匿名管道原理与避坑指南:从文件描述符到内核缓冲区

说到匿名管道&#xff0c;我脑子里第一时间跳出来的就是那个经典实验&#xff1a;在Shell里执行 cat file | grep xxx &#xff0c;或者程序员面试时被反复问到的 pipe fork 。它名字里带个管道&#xff0c;用起来又是一对文件描述符&#xff0c; read()/write() 和读写…

作者头像 李华
网站建设 2026/10/11 12:50:28

游戏背包背后的数据库三层秘密

我们不背定义&#xff0c;直接看一个具体场景&#xff1a;你开发了一款游戏。玩家打开背包&#xff0c;看到自己有 3 瓶治疗药水。这个过程中&#xff0c;数据库的三层模型分别在哪里&#xff1f;先记住&#xff1a;不是有三份数据&#xff0c;而是从三个角度看同一套数据。1. …

作者头像 李华
网站建设 2026/10/11 12:48:54

上海 AI 双子星出海:基元律动 × 无问芯穹联手背后的产业信号

上海 AI 双子星出海&#xff1a;基元律动 无问芯穹联手背后的产业信号 【免费下载链接】NeoHorse-1-9B 项目地址: https://ai.gitcode.com/hf_mirrors/TokenRhythm/NeoHorse-1-9B 2026 年 9 月&#xff0c;一则不算高调但信息量极大的消息在 AI 圈发酵&#xff1a;由前…

作者头像 李华