第9章 字符串去空格:手写 trim、ltrim、rtrim 函数

各位同学,咱们今天聊一个看似简单、实则暗藏玄机的话题——字符串去空格。

你可能会想:“不就是去掉首尾的空格吗?有什么好讲的?”

嗯,我当年也是这么想的。直到有一次,我在一个嵌入式项目里,因为一个trim函数没写好,导致整个协议解析模块出了bug,排查了整整一个下午。从那以后,我对这种“基础函数”再也不敢掉以轻心。

说白了,trimltrimrtrim这三个函数,是字符串处理中最常用的工具之一。但标准C库并没有提供它们,你得自己手写。怎么写?写得好不好?直接关系到代码的健壮性和性能。

9.1 什么是 trim、ltrim、rtrim?

先明确概念:

  • trim:去掉字符串首尾的空格(包括空格、制表符、换行符等空白字符)
  • ltrim:只去掉字符串左侧(开头)的空格
  • rtrim:只去掉字符串右侧(结尾)的空格

举个例子:

char str[] = "   hello world   ";
// trim 后变成 "hello world"
// ltrim 后变成 "hello world   "
// rtrim 后变成 "   hello world"

看起来很简单,对吧?但实现起来,有几个坑你得注意。

9.2 手写 ltrim 函数

先从最简单的 ltrim 开始。思路很直接:找到第一个非空格字符的位置,然后把后面的内容整体往前挪。

我个人习惯用指针操作,效率高,代码也简洁。

char* ltrim(char* str) {
    if (str == NULL) return NULL;  // 防御性编程,必须的

    char* p = str;
    // 跳过开头的空白字符
    while (*p && isspace((unsigned char)*p)) {
        p++;
    }
    // 如果开头就有非空格字符,p == str,不会触发memmove
    if (p != str) {
        memmove(str, p, strlen(p) + 1);  // +1 是为了拷贝结尾的 '\0'
    }
    return str;
}

这里有个细节:我用了 memmove 而不是 memcpy。为什么?因为源和目标内存区域可能重叠。memmove 能正确处理重叠情况,而 memcpy 不行。这是很多新手容易踩的坑。

注意: 当 p 指向字符串中间位置时,memmove(str, p, ...) 的源和目标有重叠,必须用 memmove

9.3 手写 rtrim 函数

rtrim 稍微绕一点。你得从字符串末尾往前找,找到第一个非空格字符,然后在其后一个位置放上 '\0'

char* rtrim(char* str) {
    if (str == NULL) return NULL;

    size_t len = strlen(str);
    if (len == 0) return str;  // 空字符串直接返回

    char* p = str + len - 1;   // 指向最后一个字符
    // 从后往前找,跳过空白字符
    while (p >= str && isspace((unsigned char)*p)) {
        p--;
    }
    // 在最后一个非空格字符后面放 '\0'
    *(p + 1) = '\0';
    return str;
}

这里有个边界情况:如果字符串全是空格,p 会一直减到 str - 1,然后 *(p + 1) 就是 *str,也就是把第一个字符设为 '\0',字符串变成空串。逻辑是对的。

我曾经在项目里见过有人用 while (p > str) 做判断,结果全空格字符串时漏掉了第一个字符,导致 rtrim 没完全去掉空格。嗯,这种边界情况,你写单元测试时一定要覆盖到。

9.4 手写 trim 函数

trim 最简单的方式就是组合 ltrimrtrim

char* trim(char* str) {
    if (str == NULL) return NULL;
    return rtrim(ltrim(str));
}

先去掉左边空格,再去掉右边空格。顺序无所谓,结果一样。

但如果你追求极致性能,也可以一次遍历完成。不过说实话,对于绝大多数场景,组合调用的性能完全够用。我一般不会为了这点微优化去增加代码复杂度。

9.5 完整代码示例

下面是一个完整的、带 main 函数测试的版本:

#include <stdio.h>
#include <string.h>
#include <ctype.h>

char* ltrim(char* str) {
    if (str == NULL) return NULL;
    char* p = str;
    while (*p && isspace((unsigned char)*p)) {
        p++;
    }
    if (p != str) {
        memmove(str, p, strlen(p) + 1);
    }
    return str;
}

char* rtrim(char* str) {
    if (str == NULL) return NULL;
    size_t len = strlen(str);
    if (len == 0) return str;
    char* p = str + len - 1;
    while (p >= str && isspace((unsigned char)*p)) {
        p--;
    }
    *(p + 1) = '\0';
    return str;
}

char* trim(char* str) {
    if (str == NULL) return NULL;
    return rtrim(ltrim(str));
}

int main() {
    char test1[] = "   hello world   ";
    char test2[] = "\t\n  foo bar  \r\n";
    char test3[] = "no_spaces";
    char test4[] = "   ";
    char test5[] = "";

    printf("trim('%s') = '%s'\n", test1, trim(test1));
    printf("trim('%s') = '%s'\n", test2, trim(test2));
    printf("trim('%s') = '%s'\n", test3, trim(test3));
    printf("trim('%s') = '%s'\n", test4, trim(test4));
    printf("trim('%s') = '%s'\n", test5, trim(test5));

    return 0;
}

输出结果:

trim('   hello world   ') = 'hello world'
trim('	  
  foo bar  
') = 'foo bar'
trim('no_spaces') = 'no_spaces'
trim('   ') = ''
trim('') = ''

9.6 避坑指南与性能考量

写这几个函数,有几个坑我帮你总结一下:

  • NULL 指针检查:函数入口一定要判断 str == NULL,否则传入 NULL 直接崩溃。我在生产代码里见过这种事故。
  • 空字符串处理strlen(str) 为 0 时,rtrimstr + len - 1 会变成 str - 1,这是未定义行为。所以要先判断。
  • isspace 的参数类型isspace 接收 int,但 char 在有些平台上是 signed。如果字符的 ASCII 值大于 127,直接传 char 会导致负数,isspace 的行为是未定义的。所以要用 (unsigned char) 强转。
  • 不要修改传入的字符串常量:如果你传一个字符串常量(如 " hello ")给 trim,函数内部会修改它,这是未定义行为。所以调用方要确保传入的是可修改的内存(如字符数组)。
小技巧: 如果你不想修改原字符串,可以写一个返回新字符串的版本,内部用 malloc 分配内存。但记得调用方要 free

9.7 知识体系图

下面这张图帮你理清本章的核心逻辑:

字符串去空格函数体系 ltrim 去掉左侧空格 rtrim 去掉右侧空格 trim 去掉首尾空格 指针前移 + memmove 找到第一个非空格字符 指针后移 + 置 '\0' 找到最后一个非空格字符 ltrim + rtrim 组合 先左后右,顺序无关 ⚠ 注意事项 NULL检查 | 空字符串 | isspace参数强转 | 不可修改字符串常量

9.8 扩展思考

如果你需要处理 Unicode 字符串(比如 UTF-8),事情就复杂了。空格字符不止 ' ' 一种,还有不间断空格(U+00A0)、全角空格(U+3000)等。这时候 isspace 就不够用了,你得用专门的 Unicode 库。

另外,有些场景下你只想去掉普通空格,保留制表符和换行符。那你就不能直接用 isspace,得自己写判断条件:

while (*p == ' ') { p++; }  // 只去掉空格,不去掉 \t \n

你看,一个简单的去空格函数,背后有这么多门道。写代码就是这样,细节决定成败。

好了,本章的内容就到这里。记住我强调的那几个坑,下次写 trim 函数时,你就能一次写对,不用像我当年那样排查一下午了。


公众号:蓝海资料掘金营,微信 deep3321