【问题标题】:How do I trim leading/trailing whitespace in a standard way?如何以标准方式修剪前导/尾随空格?
【发布时间】:2010-09-12 11:09:09
【问题描述】:

是否有一种干净的、最好是标准的方法从 C 中的字符串中修剪前导和尾随空格?我会自己动手,但我认为这是一个常见问题,但也有同样常见的解决方案。

【问题讨论】:

    标签: c string whitespace trim


    【解决方案1】:

    如果可以修改字符串:

    // Note: This function returns a pointer to a substring of the original string.
    // If the given string was allocated dynamically, the caller must not overwrite
    // that pointer with the returned value, since the original pointer must be
    // deallocated using the same allocator with which it was allocated.  The return
    // value must NOT be deallocated using free() etc.
    char *trimwhitespace(char *str)
    {
      char *end;
    
      // Trim leading space
      while(isspace((unsigned char)*str)) str++;
    
      if(*str == 0)  // All spaces?
        return str;
    
      // Trim trailing space
      end = str + strlen(str) - 1;
      while(end > str && isspace((unsigned char)*end)) end--;
    
      // Write new null terminator character
      end[1] = '\0';
    
      return str;
    }
    

    如果你不能修改字符串,那么你可以使用基本相同的方法:

    // Stores the trimmed input string into the given output buffer, which must be
    // large enough to store the result.  If it is too small, the output is
    // truncated.
    size_t trimwhitespace(char *out, size_t len, const char *str)
    {
      if(len == 0)
        return 0;
    
      const char *end;
      size_t out_size;
    
      // Trim leading space
      while(isspace((unsigned char)*str)) str++;
    
      if(*str == 0)  // All spaces?
      {
        *out = 0;
        return 1;
      }
    
      // Trim trailing space
      end = str + strlen(str) - 1;
      while(end > str && isspace((unsigned char)*end)) end--;
      end++;
    
      // Set output size to minimum of trimmed string length and buffer size minus 1
      out_size = (end - str) < len-1 ? (end - str) : len-1;
    
      // Copy trimmed string and add null terminator
      memcpy(out, str, out_size);
      out[out_size] = 0;
    
      return out_size;
    }
    

    【讨论】:

    • 对不起,第一个答案一点都不好,除非你不关心内存泄漏。您现在有两个重叠的字符串(原始的,它的尾随空格被修剪,以及新的)。只有原始字符串可以被释放,但如果你这样做,第二个指向释放的内存。
    • @nvl: 没有分配内存,所以没有要释放的内存。
    • @nvl: 不。str 是一个局部变量,改变它不会改变传入的原始指针。C 中的函数调用总是按值传递,从不传递-参考。
    • @Raj:返回与传入的地址不同的地址本身并没有错。这里不要求返回的值是free() 函数的有效参数。恰恰相反——我设计它是为了避免为了效率而分配内存。如果传入的地址是动态分配的,那么调用者仍然负责释放该内存,并且调用者需要确保不要用此处返回的值覆盖该值。
    • 您必须将isspace 的参数转换为unsigned char,否则您会调用未定义的行为。
    【解决方案2】:

    这是一个将字符串移动到缓冲区的第一个位置的方法。你可能想要这种行为,这样如果你动态分配字符串,你仍然可以在 trim() 返回的同一个指针上释放它:

    char *trim(char *str)
    {
        size_t len = 0;
        char *frontp = str;
        char *endp = NULL;
    
        if( str == NULL ) { return NULL; }
        if( str[0] == '\0' ) { return str; }
    
        len = strlen(str);
        endp = str + len;
    
        /* Move the front and back pointers to address the first non-whitespace
         * characters from each end.
         */
        while( isspace((unsigned char) *frontp) ) { ++frontp; }
        if( endp != frontp )
        {
            while( isspace((unsigned char) *(--endp)) && endp != frontp ) {}
        }
    
        if( frontp != str && endp == frontp )
                *str = '\0';
        else if( str + len - 1 != endp )
                *(endp + 1) = '\0';
    
        /* Shift the string so that it starts at str so that if it's dynamically
         * allocated, we can still free it on the returned pointer.  Note the reuse
         * of endp to mean the front of the string buffer now.
         */
        endp = str;
        if( frontp != str )
        {
                while( *frontp ) { *endp++ = *frontp++; }
                *endp = '\0';
        }
    
        return str;
    }
    

    正确性测试:

    #include <stdio.h>
    #include <string.h>
    #include <ctype.h>
    
    /* Paste function from above here. */
    
    int main()
    {
        /* The test prints the following:
        [nothing to trim] -> [nothing to trim]
        [    trim the front] -> [trim the front]
        [trim the back     ] -> [trim the back]
        [    trim front and back     ] -> [trim front and back]
        [ trim one char front and back ] -> [trim one char front and back]
        [ trim one char front] -> [trim one char front]
        [trim one char back ] -> [trim one char back]
        [                   ] -> []
        [ ] -> []
        [a] -> [a]
        [] -> []
        */
    
        char *sample_strings[] =
        {
                "nothing to trim",
                "    trim the front",
                "trim the back     ",
                "    trim front and back     ",
                " trim one char front and back ",
                " trim one char front",
                "trim one char back ",
                "                   ",
                " ",
                "a",
                "",
                NULL
        };
        char test_buffer[64];
        char comparison_buffer[64];
        size_t index, compare_pos;
    
        for( index = 0; sample_strings[index] != NULL; ++index )
        {
            // Fill buffer with known value to verify we do not write past the end of the string.
            memset( test_buffer, 0xCC, sizeof(test_buffer) );
            strcpy( test_buffer, sample_strings[index] );
            memcpy( comparison_buffer, test_buffer, sizeof(comparison_buffer));
    
            printf("[%s] -> [%s]\n", sample_strings[index],
                                     trim(test_buffer));
    
            for( compare_pos = strlen(comparison_buffer);
                 compare_pos < sizeof(comparison_buffer);
                 ++compare_pos )
            {
                if( test_buffer[compare_pos] != comparison_buffer[compare_pos] )
                {
                    printf("Unexpected change to buffer @ index %u: %02x (expected %02x)\n",
                        compare_pos, (unsigned char) test_buffer[compare_pos], (unsigned char) comparison_buffer[compare_pos]);
                }
            }
        }
    
        return 0;
    }
    

    源文件是 trim.c。使用 'cc -Wall tr​​im.c -o trim' 编译。

    【讨论】:

    • 您必须将isspace 的参数转换为unsigned char,否则您会调用未定义的行为。
    • @RolandIllig:谢谢,我从来没有意识到这是必要的。修好了。
    • @Simas:你为什么这么说?该函数调用isspace() 那么为什么" ""\n" 之间会有区别?我为换行添加了单元测试,对我来说它看起来不错...ideone.com/bbVmqo
    • @indiv 手动分配时它将访问无效的内存块。即这一行:*(endp + 1) = '\0';。答案的示例测试使用了 64 的缓冲区,从而避免了这个问题。
    • @nolandda:感谢您提供的详细信息。我修复了它并更新了测试以检测缓冲区溢出,因为我目前无法访问 valgrind。
    【解决方案3】:

    我的解决方案。字符串必须是可变的。其他一些解决方案的优势在于它将非空格部分移动到开头,因此您可以继续使用旧指针,以防您以后必须 free() 它。

    void trim(char * s) {
        char * p = s;
        int l = strlen(p);
    
        while(isspace(p[l - 1])) p[--l] = 0;
        while(* p && isspace(* p)) ++p, --l;
    
        memmove(s, p, l + 1);
    }   
    

    此版本使用 strndup() 创建字符串的副本,而不是在原地编辑它。 strndup() 需要 _GNU_SOURCE,所以也许你需要用 malloc() 和 strncpy() 制作自己的 strndup()。

    char * trim(char * s) {
        int l = strlen(s);
    
        while(isspace(s[l - 1])) --l;
        while(* s && isspace(* s)) ++s, --l;
    
        return strndup(s, l);
    }
    

    【讨论】:

    • trim() 如果s"" 则调用UB,因为第一个isspace() 调用将是isspace(p[-1])p[-1] 不一定引用合法位置。
    • 您必须将isspace 的参数转换为unsigned char,否则您会调用未定义的行为。
    • 应该添加if(l==0)return;以避免零长度str
    【解决方案4】:

    这是我的 C 迷你库,用于修剪左、右、两者、全部、就地和单独,以及修剪一组指定的字符(或默认情况下的空白)。

    strlib.h 的内容:

    #ifndef STRLIB_H_
    #define STRLIB_H_ 1
    enum strtrim_mode_t {
        STRLIB_MODE_ALL       = 0, 
        STRLIB_MODE_RIGHT     = 0x01, 
        STRLIB_MODE_LEFT      = 0x02, 
        STRLIB_MODE_BOTH      = 0x03
    };
    
    char *strcpytrim(char *d, // destination
                     char *s, // source
                     int mode,
                     char *delim
                     );
    
    char *strtriml(char *d, char *s);
    char *strtrimr(char *d, char *s);
    char *strtrim(char *d, char *s); 
    char *strkill(char *d, char *s);
    
    char *triml(char *s);
    char *trimr(char *s);
    char *trim(char *s);
    char *kill(char *s);
    #endif
    

    strlib.c 的内容:

    #include <strlib.h>
    
    char *strcpytrim(char *d, // destination
                     char *s, // source
                     int mode,
                     char *delim
                     ) {
        char *o = d; // save orig
        char *e = 0; // end space ptr.
        char dtab[256] = {0};
        if (!s || !d) return 0;
    
        if (!delim) delim = " \t\n\f";
        while (*delim) 
            dtab[*delim++] = 1;
    
        while ( (*d = *s++) != 0 ) { 
            if (!dtab[0xFF & (unsigned int)*d]) { // Not a match char
                e = 0;       // Reset end pointer
            } else {
                if (!e) e = d;  // Found first match.
    
                if ( mode == STRLIB_MODE_ALL || ((mode != STRLIB_MODE_RIGHT) && (d == o)) ) 
                    continue;
            }
            d++;
        }
        if (mode != STRLIB_MODE_LEFT && e) { // for everything but trim_left, delete trailing matches.
            *e = 0;
        }
        return o;
    }
    
    // perhaps these could be inlined in strlib.h
    char *strtriml(char *d, char *s) { return strcpytrim(d, s, STRLIB_MODE_LEFT, 0); }
    char *strtrimr(char *d, char *s) { return strcpytrim(d, s, STRLIB_MODE_RIGHT, 0); }
    char *strtrim(char *d, char *s) { return strcpytrim(d, s, STRLIB_MODE_BOTH, 0); }
    char *strkill(char *d, char *s) { return strcpytrim(d, s, STRLIB_MODE_ALL, 0); }
    
    char *triml(char *s) { return strcpytrim(s, s, STRLIB_MODE_LEFT, 0); }
    char *trimr(char *s) { return strcpytrim(s, s, STRLIB_MODE_RIGHT, 0); }
    char *trim(char *s) { return strcpytrim(s, s, STRLIB_MODE_BOTH, 0); }
    char *kill(char *s) { return strcpytrim(s, s, STRLIB_MODE_ALL, 0); }
    

    一个主程序可以完成所有工作。 如果 src == dst 则修剪到位,否则, 它的工作方式类似于strcpy 例程。 它修剪字符串 delim 中指定的一组字符,如果为 null,则修剪空格。 它修剪左、右、两者和所有(如 tr)。 它没有太多内容,它只对字符串进行一次迭代。有些人可能会抱怨修剪右侧从左侧开始,但是,无论如何都不需要从左侧开始的 strlen。 (你必须以一种或另一种方式到达字符串的末尾以进行正确的修剪,所以你最好边做边做。)关于流水线和缓存大小等可能会有争论——谁知道呢.由于该解决方案从左到右工作并且只迭代一次,因此它也可以扩展到流上工作。限制:它适用于 unicode 字符串。

    【讨论】:

    • 我对此表示赞同,我知道它很旧,但我认为有一个错误。 dtab[*d] 在将其用作数组索引之前不会将 *d 转换为 unsigned int。在带有签名字符的系统上,这将读取到dtab[-127],这将导致错误并可能崩溃。
    • dtab[*delim++] 上的潜在未定义行为,因为必须将 char 索引值强制转换为 unsigned char。代码假定为 8 位 chardelim 应声明为 const char *dtab[0xFF &amp; (unsigned int)*d] 会比 dtab[(unsigned char)*d] 更清晰。该代码适用于 UTF-8 编码的字符串,但不会去除非 ASCII 间距序列。
    • @michael-plainer,这看起来很有趣。为什么不测试一下,放到 GitHub 上呢?
    【解决方案5】:

    这是我对一个简单但正确的就地修剪功能的尝试。

    void trim(char *str)
    {
        int i;
        int begin = 0;
        int end = strlen(str) - 1;
    
        while (isspace((unsigned char) str[begin]))
            begin++;
    
        while ((end >= begin) && isspace((unsigned char) str[end]))
            end--;
    
        // Shift all characters back to the start of the string array.
        for (i = begin; i <= end; i++)
            str[i - begin] = str[i];
    
        str[i - begin] = '\0'; // Null terminate string.
    }
    

    【讨论】:

    • 建议在str is "". Prevents str[-1]`时改成while ((end &gt;= begin) &amp;&amp; isspace(str[end]))防止UB。
    • 顺便说一句,我必须将其更改为 str[i - begin + 1] 才能工作
    • 您必须将isspace 的参数强制转换为unsigned char,否则您会调用未定义的行为。
    • @RolandIllig,为什么会是未定义的行为?该函数旨在与字符​​一起使用。
    • @wovano 不,不是。 &lt;ctype.h&gt; 中的函数旨在与 int 一起使用,它们表示 unsigned char 或特殊值 EOF。见stackoverflow.com/q/7131026/225757
    【解决方案6】:

    装饰派对迟到了

    特点:
    1. 与其他许多答案一样,快速修剪开头。
    2. 走到尽头后,修剪右边,每个循环只做1次测试。与 @jfm3 类似,但适用于全空白字符串)
    3. 当char 是有符号的char 时,为避免未定义的行为,请将*s 转换为unsigned char

    字符处理 "在所有情况下,参数都是int,其值应表示为unsigned char 或应等于宏EOF 的值。如果参数有任何其他值,行为未定义。” C11 §7.4 1

    #include <ctype.h>
    
    // Return a pointer to the trimmed string
    char *string_trim_inplace(char *s) {
      while (isspace((unsigned char) *s)) s++;
      if (*s) {
        char *p = s;
        while (*p) p++;
        while (isspace((unsigned char) *(--p)));
        p[1] = '\0';
      }
    
      // If desired, shift the trimmed string
    
      return s;
    }
    

    @chqrlie 评论上面不会移动修剪后的字符串。这样做....

    // Return a pointer to the (shifted) trimmed string
    char *string_trim_inplace(char *s) {
      char *original = s;
      size_t len = 0;
    
      while (isspace((unsigned char) *s)) {
        s++;
      } 
      if (*s) {
        char *p = s;
        while (*p) p++;
        while (isspace((unsigned char) *(--p)));
        p[1] = '\0';
        // len = (size_t) (p - s);   // older errant code
        len = (size_t) (p - s + 1);  // Thanks to @theriver
      }
    
      return (s == original) ? s : memmove(original, s, len + 1);
    }
    

    【讨论】:

    • 是的,终于有人知道 ctype 未定义行为了。
    • @chux 我认为应该是 len = (size_t) (p-s)+1;否则最后一个字母重叠。
    【解决方案7】:

    这是一个类似于@adam-rosenfields 就地修改例程的解决方案,但无需使用 strlen()。像@jkramer 一样,字符串在缓冲区内左调整,因此您可以释放相同的指针。对于大字符串来说不是最优的,因为它不使用 memmove。包括@jfm3 提到的 ++/-- 运算符。包括基于FCTX 的单元测试。

    #include <ctype.h>
    
    void trim(char * const a)
    {
        char *p = a, *q = a;
        while (isspace(*q))            ++q;
        while (*q)                     *p++ = *q++;
        *p = '\0';
        while (p > a && isspace(*--p)) *p = '\0';
    }
    
    /* See http://fctx.wildbearsoftware.com/ */
    #include "fct.h"
    
    FCT_BGN()
    {
        FCT_QTEST_BGN(trim)
        {
            { char s[] = "";      trim(s); fct_chk_eq_str("",    s); } // Trivial
            { char s[] = "   ";   trim(s); fct_chk_eq_str("",    s); } // Trivial
            { char s[] = "\t";    trim(s); fct_chk_eq_str("",    s); } // Trivial
            { char s[] = "a";     trim(s); fct_chk_eq_str("a",   s); } // NOP
            { char s[] = "abc";   trim(s); fct_chk_eq_str("abc", s); } // NOP
            { char s[] = "  a";   trim(s); fct_chk_eq_str("a",   s); } // Leading
            { char s[] = "  a c"; trim(s); fct_chk_eq_str("a c", s); } // Leading
            { char s[] = "a  ";   trim(s); fct_chk_eq_str("a",   s); } // Trailing
            { char s[] = "a c  "; trim(s); fct_chk_eq_str("a c", s); } // Trailing
            { char s[] = " a ";   trim(s); fct_chk_eq_str("a",   s); } // Both
            { char s[] = " a c "; trim(s); fct_chk_eq_str("a c", s); } // Both
    
            // Villemoes pointed out an edge case that corrupted memory.  Thank you.
            // http://stackoverflow.com/questions/122616/#comment23332594_4505533
            {
              char s[] = "a     ";       // Buffer with whitespace before s + 2
              trim(s + 2);               // Trim "    " containing only whitespace
              fct_chk_eq_str("", s + 2); // Ensure correct result from the trim
              fct_chk_eq_str("a ", s);   // Ensure preceding buffer not mutated
            }
    
            // doukremt suggested I investigate this test case but
            // did not indicate the specific behavior that was objectionable.
            // http://stackoverflow.com/posts/comments/33571430
            {
              char s[] = "         foobar";  // Shifted across whitespace
              trim(s);                       // Trim
              fct_chk_eq_str("foobar", s);   // Leading string is correct
    
              // Here is what the algorithm produces:
              char r[16] = { 'f', 'o', 'o', 'b', 'a', 'r', '\0', ' ',                     
                             ' ', 'f', 'o', 'o', 'b', 'a', 'r', '\0'};
              fct_chk_eq_int(0, memcmp(s, r, sizeof(s)));
            }
        }
        FCT_QTEST_END();
    }
    FCT_END();
    

    【讨论】:

    • 这个解决方案非常危险!如果原始字符串不包含任何非空白字符,则如果这些字节恰好包含“空白”字节,则修剪的最后一行会愉快地覆盖 a 之前的任何内容。在没有优化的情况下编译它,看看 y 会发生什么: unsigned x = 0x20202020;字符 [4] = " ";无符号 y = 0x20202020; printf("&x,&s,&y = %p,%p,%p\n", &x, &s, &y); printf("x, [s], y = %08x, [%s], %08x\n", x, s, y); trim_whitespace(s); printf("x, [s], y = %08x, [%s], %08x\n", x, s, y);
    • @Villemoes,感谢您的错误报告。当字符串仅包含空格时,我更新了逻辑以避免离开缓冲区的左侧。这个新版本是否解决了您的顾虑?
    • 语言律师可能会因为仅仅想推测创建一个指向 'a' 指向之前的字符的指针(这就是你的 '--p' 会做的事情)而对你大喊大叫。在现实世界中,你可能没问题。但您也可以将 '>=' 更改为 '>' 并将 p 的减量移动到 'isspace(*--p)'。
    • 我认为律师会没问题,因为它只是比较地址而不接触它,但我也喜欢你关于减量的建议。我已经相应地更新了它。谢谢。
    • doukremt,您是否担心 foobar 之后的整个缓冲区没有填充零?如果是这样的话,如果你这么明确地说而不是扔模糊的石头会更有帮助。
    【解决方案8】:

    另一种,一条线做真正的工作:

    #include <stdio.h>
    
    int main()
    {
       const char *target = "   haha   ";
       char buf[256];
       sscanf(target, "%s", buf); // Trimming on both sides occurs here
       printf("<%s>\n", buf);
    }
    

    【讨论】:

    • 使用scanf的好主意;但他只能使用一个可能不是 OP 想要的单词(即修剪“a b c”可能会导致“a b c”,而您的单个 scanf 只会导致“a”)。所以我们需要一个循环,以及一个带有%n 转换说明符的跳过字符的计数器,最后恐怕手动操作会更简单。
    • 当您希望字符串的第一个单词忽略任何初始空格时非常有用。
    【解决方案9】:

    我不喜欢这些答案中的大多数,因为他们做了以下一项或多项......

    1. 在原始指针的字符串中返回了一个不同的指针(将两个不同的指针同时指向同一事物有点痛苦)。
    2. 无偿使用诸如 strlen() 之类的东西来预迭代整个字符串。
    3. 使用了非便携式操作系统特定的库函数。
    4. 反向扫描。
    5. 使用比较 ' ' 而不是 isspace() 以便保留 TAB / CR / LF。
    6. 大型静态缓冲区浪费内存。
    7. sscanf/sprintf 等高成本函数浪费了周期。

    这是我的版本:

    void fnStrTrimInPlace(char *szWrite) {
    
        const char *szWriteOrig = szWrite;
        char       *szLastSpace = szWrite, *szRead = szWrite;
        int        bNotSpace;
    
        // SHIFT STRING, STARTING AT FIRST NON-SPACE CHAR, LEFTMOST
        while( *szRead != '\0' ) {
    
            bNotSpace = !isspace((unsigned char)(*szRead));
    
            if( (szWrite != szWriteOrig) || bNotSpace ) {
    
                *szWrite = *szRead;
                szWrite++;
    
                // TRACK POINTER TO LAST NON-SPACE
                if( bNotSpace )
                    szLastSpace = szWrite;
            }
    
            szRead++;
        }
    
        // TERMINATE AFTER LAST NON-SPACE (OR BEGINNING IF THERE WAS NO NON-SPACE)
        *szLastSpace = '\0';
    }
    

    【讨论】:

    • 您必须将isspace 的参数强制转换为unsigned char,否则您会调用未定义的行为。
    • 由于此答案与“浪费的周期”有关,请注意,当没有空间时,代码会不必要地复制整个字符串。领先的while (isspace((unsigned char) *szWrite)) szWrite++; 会阻止这种情况。代码还会复制所有尾随空格。
    • @chux 这个实现用单独的读写指针就地变异(而不是在不同的位置返回一个新指针),所以建议将 szWrite 跳转到第一个非空间线上-one 将在原始字符串中保留前导空格。
    • @chux,你说得对,它确实复制了尾随空格(在最后一个非空格字符后添加 null 之前),但这是我选择支付的价格以避免预先扫描细绳。对于适量的尾随 WS,只复制字节而不是预先扫描整个字符串以查找最后一个非 WS 字符会更便宜。对于大量的尾随 WS,预扫描可能值得减少写入。
    • @chux,对于“没有空间时复制”的情况,只有在指针不相等时执行*szWrite = *szRead才会跳过这种情况下的写入,但是我们添加了另一个比较/分支。使用现代 CPU/MMU/BP,我不知道该检查是损失还是收益。使用更简单的处理器和内存架构,只进行复制并跳过比较会更便宜。
    【解决方案10】:

    我不确定你认为什么是“无痛”。

    C 字符串非常痛苦。我们可以很容易地找到第一个非空白字符的位置:

    而 (isspace(* p)) p++;

    我们可以通过两个类似的琐碎动作找到最后一个非空白字符的位置:

    而 (* q) q++; 做{ q--; } 而 (isspace(* q));

    (我已经免除了您同时使用 *++ 运算符的痛苦。)

    现在的问题是你会用这个做什么?手头的数据类型实际上并不是一个很容易想到的大而健壮的抽象String,而实际上只是一个存储字节数组。由于缺乏健壮的数据类型,因此不可能编写出与 PHperytonby 的 chomp 函数相同的函数。 C 中这样的函数会返回什么?

    【讨论】:

    • 这很好用,除非字符串由所有空格组成。需要在do { q--; } ... 之前进行一次检查以了解*q != 0
    【解决方案11】:

    使用string library,例如:

    Ustr *s1 = USTR1(\7, " 12345 ");
    
    ustr_sc_trim_cstr(&s1, " ");
    assert(ustr_cmp_cstr_eq(s1, "12345"));

    ...正如您所说,这是一个“常见”问题,是的,您需要包含一个 #include 左右,它不包含在 libc 中,但不要发明自己的 hack 作业来存储随机指针和 size_t 那样只会导致缓冲区溢出。

    【讨论】:

      【解决方案12】:

      如果您使用的是glib,那么您可以使用g_strstrip

      【讨论】:

        【解决方案13】:

        聚会很晚了……

        没有回溯的单程前向扫描解决方案。源字符串中的每个字符都被精确地测试 一次 两次。 (所以它应该比这里的大多数其他解决方案更快,特别是如果源字符串有很多尾随空格。)

        这包括两种解决方案,一种是将源字符串复制并修剪为另一个目标字符串,另一种是将源字符串修剪到位。两个函数使用相同的代码。

        (可修改的)字符串被原地移动,因此指向它的原始指针保持不变。

        #include <stddef.h>
        #include <ctype.h>
        
        char * trim2(char *d, const char *s)
        {
            // Sanity checks
            if (s == NULL  ||  d == NULL)
                return NULL;
        
            // Skip leading spaces        
            const unsigned char * p = (const unsigned char *)s;
            while (isspace(*p))
                p++;
        
            // Copy the string
            unsigned char * dst = (unsigned char *)d;   // d and s can be the same
            unsigned char * end = dst;
            while (*p != '\0')
            {
                if (!isspace(*dst++ = *p++))
                    end = dst;
            }
        
            // Truncate trailing spaces
            *end = '\0';
            return d;
        }
        
        char * trim(char *s)
        {
            return trim2(s, s);
        }
        

        【讨论】:

        • 源字符串中的每个字符都只测试一次:不是真的,源字符串中的大多数字符都测试了两次:与'\0'比较,然后用@987654323测试@。用isspace() 测试所有字符似乎很浪费。对于非病理情况,从字符串末尾回溯应该更有效。
        • @chqrlie - 是的,每个角色都经过两次测试。与此处的其他算法相比,我希望看到此代码经过实际测试,尤其是给定带有大量尾随空格的字符串。
        • trim() 好的。角落案例:当d,ss &lt; d 重叠时,trim2(char *d, const char *s) 有问题。
        • @chux - 在那种极端情况下,trim() 应该如何表现?您要求修剪字符串并将其复制到字符串本身占用的内存中。与memmove() 不同,这需要在进行修剪之前确定源字符串的长度,这需要额外扫描整个字符串。最好编写一个不同的rtrim2() 函数,该函数知道将源向后复制到目标,并且可能需要一个额外的源字符串长度参数。
        【解决方案14】:

        为了保持这种增长,还有一个带有可修改字符串的选项:

        void trimString(char *string)
        {
            size_t i = 0, j = strlen(string);
            while (j > 0 && isspace((unsigned char)string[j - 1])) string[--j] = '\0';
            while (isspace((unsigned char)string[i])) i++;
            if (i > 0) memmove(string, string + i, j - i + 1);
        }
        

        【讨论】:

        • strlen() 返回一个可能超出int 范围的size_t。空白不限于空格字符。最后但最重要的是:strcpy(string, string + i * sizeof(char)); 上的未定义行为,因为源数组和目标数组重叠。使用memmove() 而不是strcpy()
        • @chqrlie 你是对的,只是包含了你的建议。我知道当源和目标重叠时复制可能会导致未定义的行为,但只是想指出在这种特殊情况下这不应该引起任何问题,因为我们总是要从内存的后面位置复制到开头,感谢您的反馈。
        • 源和目标数组如何重叠并不重要,这是未定义的行为。不要依赖这样的假设,即随着地址的增加,一次复制一个字节。另外我忘了提到while (isspace((int)string[i])) string[i--] = '\0'; 可能会超出字符串的开头。您应该将此循环与前面和后面的行结合起来并编写while (i &gt; 0 &amp;&amp; isspace((unsigned char)string[--i])) { string[i] = '\0'; } size_t end = i;
        • @chqrlie 好点,一个带有所有空格的字符串会导致循环超过开头,没想到。
        • 实际上,我的建议是不正确的,因为end 没有指向结尾的空字节,而您的end = ++i; 对于包含所有空白字符的字符串仍然存在问题。我刚刚修复了代码。
        【解决方案15】:

        我知道有很多答案,但我在这里发布我的答案,看看我的解决方案是否足够好。

        // Trims leading whitespace chars in left `str`, then copy at almost `n - 1` chars
        // into the `out` buffer in which copying might stop when the first '\0' occurs, 
        // and finally append '\0' to the position of the last non-trailing whitespace char.
        // Reture the length the trimed string which '\0' is not count in like strlen().
        size_t trim(char *out, size_t n, const char *str)
        {
            // do nothing
            if(n == 0) return 0;    
        
            // ptr stop at the first non-leading space char
            while(isspace(*str)) str++;    
        
            if(*str == '\0') {
                out[0] = '\0';
                return 0;
            }    
        
            size_t i = 0;    
        
            // copy char to out until '\0' or i == n - 1
            for(i = 0; i < n - 1 && *str != '\0'; i++){
                out[i] = *str++;
            }    
        
            // deal with the trailing space
            while(isspace(out[--i]));    
        
            out[++i] = '\0';
            return i;
        }
        

        【讨论】:

        • 注意:isspace(*str) UB 当*str &lt; 0.
        • 使用size_t n 很好,但是当n 太小而不能完整修剪字符串时,界面不会以任何方式通知调用者。考虑trim(out, 12, "delete data not")
        【解决方案16】:

        在字符串中跳过前导空格的最简单方法是,恕我直言,

        #include <stdio.h>
        
        int main()
        {
        char *foo="     teststring      ";
        char *bar;
        sscanf(foo,"%s",bar);
        printf("String is >%s<\n",bar);
            return 0;
        }
        

        【讨论】:

        • 这不适用于中间有空格的字符串,例如" foo bar "
        【解决方案17】:

        好的,这是我对这个问题的看法。我相信这是修改字符串的最简洁的解决方案(free 将起作用)并避免任何 UB。对于小字符串,它可能比涉及 memmove 的解决方案更快。

        void stripWS_LT(char *str)
        {
            char *a = str, *b = str;
            while (isspace((unsigned char)*a)) a++;
            while (*b = *a++)  b++;
            while (b > str && isspace((unsigned char)*--b)) *b = 0;
        }
        

        【讨论】:

        • b &gt; str 测试只需要一次。 *b = 0; 只需要一次。
        【解决方案18】:
        #include <ctype.h>
        #include <string.h>
        
        char *trim_space(char *in)
        {
            char *out = NULL;
            int len;
            if (in) {
                len = strlen(in);
                while(len && isspace(in[len - 1])) --len;
                while(len && *in && isspace(*in)) ++in, --len;
                if (len) {
                    out = strndup(in, len);
                }
            }
            return out;
        }
        

        isspace 有助于修剪所有空白。

        • 运行第一个循环,从最后一个字节开始检查空格字符并减少长度变量
        • 运行第二个循环以从第一个字节开始检查空格字符并减少长度变量并增加 char 指针。
        • 最后如果长度变量大于0,则使用strndup通过排除空格来创建新的字符串缓冲区。

        【讨论】:

        • 有点挑剔,strndup() 不是 C 标准的一部分,而只是 Posix。但由于它很容易实现,所以没什么大不了的。
        • trim_space("") 返回NULL。我希望有一个指向"" 的指针。 int len; 应该是 size_t len;isspace(in[len - 1])UB 当in[len - 1] &lt; 0.
        • len = strlen(in); 之前的初始while (isspace((unsigned char) *in) in++; 会比后面的while(len &amp;&amp; *in &amp;&amp; isspace(*in)) ++in, --len; 更有效
        【解决方案19】:

        这个简短而简单,使用 for 循环并且不会覆盖字符串边界。 如果需要,您可以将测试替换为 isspace()

        void trim (char *s)         // trim leading and trailing spaces+tabs
        {
         int i,j,k, len;
        
         j=k=0;
         len = strlen(s);
                            // find start of string
         for (i=0; i<len; i++) if ((s[i]!=32) && (s[i]!=9)) { j=i; break; }
                            // find end of string+1
         for (i=len-1; i>=j; i--) if ((s[i]!=32) && (s[i]!=9)) { k=i+1; break;} 
        
         if (k<=j) {s[0]=0; return;}        // all whitespace (j==k==0)
        
         len=k-j;
         for (i=0; i<len; i++) s[i] = s[j++];   // shift result to start of string
         s[i]=0;                // end the string
        
        }//_trim
        

        【讨论】:

          【解决方案20】:

          就个人而言,我会自己动手。您可以使用 strtok,但您需要注意这样做(尤其是在删除前导字符时),您知道什么是内存。

          删除尾随空格很容易,而且非常安全,因为您只需将 0 放在最后一个空格的顶部,从末尾倒数。摆脱领先空间意味着移动事物。如果您想在原地执行此操作(可能是明智的),您可以继续将所有内容移回一个字符,直到没有前导空格。或者,为了更有效,您可以找到第一个非空格字符的索引,然后将所有内容移回该数字。或者,您可以只使用指向第一个非空格字符的指针(但是您需要像使用 strtok 一样小心)。

          【讨论】:

          • strtok 通常不是一个很好用的工具——尤其是因为它不能重入。如果你留在单个函数中,它可以安全地使用,但如果有任何线程或调用其他本身可能使用 strtok 的函数的可能性,你就有麻烦了。
          【解决方案21】:
          #include "stdafx.h"
          #include "malloc.h"
          #include "string.h"
          
          int main(int argc, char* argv[])
          {
          
            char *ptr = (char*)malloc(sizeof(char)*30);
            strcpy(ptr,"            Hel  lo    wo           rl   d G    eo rocks!!!    by shahil    sucks b i          g       tim           e");
          
            int i = 0, j = 0;
          
            while(ptr[j]!='\0')
            {
          
                if(ptr[j] == ' ' )
                {
                    j++;
                    ptr[i] = ptr[j];
                }
                else
                {
                    i++;
                    j++;
                    ptr[i] = ptr[j];
                }
            }
          
          
            printf("\noutput-%s\n",ptr);
                  return 0;
          }

          【讨论】:

          • 这让我发笑,因为我认为 dreamlax 已经编辑了测试字符串以包含“sucks big time”。没有。原作者很诚实。
          • 不要使用此代码。它会产生缓冲区溢出。
          【解决方案22】:

          比赛有点晚了,但我会把我的日常工作投入到战斗中。它们可能不是最绝对有效的,但我相信它们是正确的并且很简单(rtrim() 推动了复杂性的极限):

          #include <ctype.h>
          #include <string.h>
          
          /*
              Public domain implementations of in-place string trim functions
          
              Michael Burr
              michael.burr@nth-element.com
              2010
          */
          
          char* ltrim(char* s) 
          {
              char* newstart = s;
          
              while (isspace( *newstart)) {
                  ++newstart;
              }
          
              // newstart points to first non-whitespace char (which might be '\0')
              memmove( s, newstart, strlen( newstart) + 1); // don't forget to move the '\0' terminator
          
              return s;
          }
          
          
          char* rtrim( char* s)
          {
              char* end = s + strlen( s);
          
              // find the last non-whitespace character
              while ((end != s) && isspace( *(end-1))) {
                      --end;
              }
          
              // at this point either (end == s) and s is either empty or all whitespace
              //      so it needs to be made empty, or
              //      end points just past the last non-whitespace character (it might point
              //      at the '\0' terminator, in which case there's no problem writing
              //      another there).    
              *end = '\0';
          
              return s;
          }
          
          char*  trim( char* s)
          {
              return rtrim( ltrim( s));
          }
          

          【讨论】:

          • 您应该将 char 参数转换为 isspace()(unsigned char) 以避免对潜在负值的未定义行为。如果没有必要,也要避免在ltrim() 中移动字符串。
          【解决方案23】:

          到目前为止,大多数答案都执行以下操作之一:

          1. 在字符串的末尾回溯(即找到字符串的末尾,然后向后查找,直到找到非空格字符)或
          2. 首先调用strlen(),第二次遍历整个字符串。

          这个版本只做一次,不回溯。因此它可能比其他的表现更好,尽管只有在通常有数百个尾随空格的情况下(这在处理 SQL 查询的输出时并不罕见。)

          static char const WHITESPACE[] = " \t\n\r";
          
          static void get_trim_bounds(char  const *s,
                                      char const **firstWord,
                                      char const **trailingSpace)
          {
              char const *lastWord;
              *firstWord = lastWord = s + strspn(s, WHITESPACE);
              do
              {
                  *trailingSpace = lastWord + strcspn(lastWord, WHITESPACE);
                  lastWord = *trailingSpace + strspn(*trailingSpace, WHITESPACE);
              }
              while (*lastWord != '\0');
          }
          
          char *copy_trim(char const *s)
          {
              char const *firstWord, *trailingSpace;
              char *result;
              size_t newLength;
          
              get_trim_bounds(s, &firstWord, &trailingSpace);
              newLength = trailingSpace - firstWord;
          
              result = malloc(newLength + 1);
              memcpy(result, firstWord, newLength);
              result[newLength] = '\0';
              return result;
          }
          
          void inplace_trim(char *s)
          {
              char const *firstWord, *trailingSpace;
              size_t newLength;
          
              get_trim_bounds(s, &firstWord, &trailingSpace);
              newLength = trailingSpace - firstWord;
          
              memmove(s, firstWord, newLength);
              s[newLength] = '\0';
          }
          

          【讨论】:

          • 如果您关心性能,请不要在紧密循环中使用strspn()strcspn()。这是非常低效的,而且开销将使单次前传的未经证实的优势相形见绌。 strlen() 通常用非常有效的代码内联扩展,这不是一个真正的问题。修剪字符串的开头和结尾将比测试字符串中的每个字符的白度要快得多,即使在具有非常少或没有非白色字符的字符串的特殊情况下也是如此。
          【解决方案24】:

          这是我能想到的最短的实现:

          static const char *WhiteSpace=" \n\r\t";
          char* trim(char *t)
          {
              char *e=t+(t!=NULL?strlen(t):0);               // *e initially points to end of string
              if (t==NULL) return;
              do --e; while (strchr(WhiteSpace, *e) && e>=t);  // Find last char that is not \r\n\t
              *(++e)=0;                                      // Null-terminate
              e=t+strspn (t,WhiteSpace);                           // Find first char that is not \t
              return e>t?memmove(t,e,strlen(e)+1):t;                  // memmove string contents and terminator
          }
          

          【讨论】:

          • 这个怎么样:char *trim(char *s) { char *p = s, *e = s + strlen(s); while (e &gt; s &amp;&amp; isspace((unsigned char)e[-1])) { *--e = '\0'; } while (isspace((unsigned char)*p)) { p++; } if (p &gt; s) { memmove(s, p, e + 1 - p); } return s; }
          【解决方案25】:

          这些函数会修改原来的缓冲区,所以如果动态分配,原来的 指针可以被释放。

          #include <string.h>
          
          void rstrip(char *string)
          {
            int l;
            if (!string)
              return;
            l = strlen(string) - 1;
            while (isspace(string[l]) && l >= 0)
              string[l--] = 0;
          }
          
          void lstrip(char *string)
          {
            int i, l;
            if (!string)
              return;
            l = strlen(string);
            while (isspace(string[(i = 0)]))
              while(i++ < l)
                string[i-1] = string[i];
          }
          
          void strip(char *string)
          {
            lstrip(string);
            rstrip(string);
          }
          

          【讨论】:

          • rstrip() 在空字符串上调用未定义的行为。 lstrip() 在具有较长的空白字符初始部分的字符串上不必要地慢。 isspace() 不应传递 char 参数,因为它在不同于 EOF 的负值上调用未定义的行为。
          【解决方案26】:

          您对使用头文件 Shlwapi.h 中定义的 StrTrim 函数有何看法?它是直截了当的,而不是您自己定义的。
          详情可见:
          http://msdn.microsoft.com/en-us/library/windows/desktop/bb773454(v=vs.85).aspx

          如果你有
          char ausCaptain[]="GeorgeBailey ";
          StrTrim(ausCaptain," ");
          这将使ausCaptain 成为"GeorgeBailey" 而不是"GeorgeBailey "

          【讨论】:

            【解决方案27】:

            为了从两边修剪我的琴弦,我使用了 oldie 但 gooody ;) 它可以修剪 ascii 小于空格的任何内容,这意味着控制字符也将被修剪!

            char *trimAll(char *strData)
            {
              unsigned int L = strlen(strData);
              if(L > 0){ L--; }else{ return strData; }
              size_t S = 0, E = L;
              while((!(strData[S] > ' ') || !(strData[E] > ' ')) && (S >= 0) && (S <= L) && (E >= 0) && (E <= L))
              {
                if(strData[S] <= ' '){ S++; }
                if(strData[E] <= ' '){ E--; }
              }
              if(S == 0 && E == L){ return strData; } // Nothing to be done
              if((S >= 0) && (S <= L) && (E >= 0) && (E <= L)){
                L = E - S + 1;
                memmove(strData,&strData[S],L); strData[L] = '\0';
              }else{ strData[0] = '\0'; }
              return strData;
            }
            

            【讨论】:

            • 您应该使用size_t 而不是unsigned int。该代码有很多冗余测试,并在strncpy(strData,&amp;strData[S],L) 上调用未定义的行为,因为源和目标数组重叠。使用memmove() 而不是strncpy()
            • 在这种情况下没关系,因为目标地址的索引总是比源地址小,但是 memmove 确实会更好。
            • 不,这不行。不管源数组和目标数组如何重叠,它都会调用未定义的行为,因为您无法安全地对库函数的实现做出超出其标准规范的假设。 现代编译器倾向于不公平地利用具有潜在未定义行为的情况,谨慎行事并远离 UB,并且不要让新手做出不安全的假设。
            【解决方案28】:

            我只包含代码,因为到目前为止发布的代码似乎不是最理想的(而且我还没有代表发表评论。)

            void inplace_trim(char* s)
            {
                int start, end = strlen(s);
                for (start = 0; isspace(s[start]); ++start) {}
                if (s[start]) {
                    while (end > 0 && isspace(s[end-1]))
                        --end;
                    memmove(s, &s[start], end - start);
                }
                s[end - start] = '\0';
            }
            
            char* copy_trim(const char* s)
            {
                int start, end;
                for (start = 0; isspace(s[start]); ++start) {}
                for (end = strlen(s); end > 0 && isspace(s[end-1]); --end) {}
                return strndup(s + start, end - start);
            }
            

            strndup() 是一个 GNU 扩展。如果您没有它或类似的东西,请自己动手。例如:

            r = strdup(s + start);
            r[end-start] = '\0';
            

            【讨论】:

            • isspace(0) 被定义为假,你可以简化这两个函数。还将memmove() 移动到if 块内。
            【解决方案29】:

            这里我使用动态内存分配将输入字符串修剪为函数trimStr。首先,我们找出输入字符串中有多少个非空字符。然后,我们分配一个具有该大小的字符数组并处理以空字符结尾的字符。当我们使用这个函数时,我们需要释放主函数内部的内存。

            #include<stdio.h>
            #include<stdlib.h>
            
            char *trimStr(char *str){
            char *tmp = str;
            printf("input string %s\n",str);
            int nc = 0;
            
            while(*tmp!='\0'){
              if (*tmp != ' '){
              nc++;
             }
             tmp++;
            }
            printf("total nonempty characters are %d\n",nc);
            char *trim = NULL;
            
            trim = malloc(sizeof(char)*(nc+1));
            if (trim == NULL) return NULL;
            tmp = str;
            int ne = 0;
            
            while(*tmp!='\0'){
              if (*tmp != ' '){
                 trim[ne] = *tmp;
               ne++;
             }
             tmp++;
            }
            trim[nc] = '\0';
            
            printf("trimmed string is %s\n",trim);
            
            return trim; 
             }
            
            
            int main(void){
            
            char str[] = " s ta ck ove r fl o w  ";
            
            char *trim = trimStr(str);
            
            if (trim != NULL )free(trim);
            
            return 0;
            }
            

            【讨论】:

              【解决方案30】:

              这是我的做法。它会在适当的位置修剪字符串,因此不必担心释放返回的字符串或丢失指向已分配字符串的指针。这可能不是最短的答案,但对大多数读者来说应该很清楚。

              #include <ctype.h>
              #include <string.h>
              void trim_str(char *s)
              {
                  const size_t s_len = strlen(s);
              
                  int i;
                  for (i = 0; i < s_len; i++)
                  {
                      if (!isspace( (unsigned char) s[i] )) break;
                  }
              
                  if (i == s_len)
                  {
                      // s is an empty string or contains only space characters
              
                      s[0] = '\0';
                  }
                  else
                  {
                      // s contains non-space characters
              
                      const char *non_space_beginning = s + i;
              
                      char *non_space_ending = s + s_len - 1;
                      while ( isspace( (unsigned char) *non_space_ending ) ) non_space_ending--;
              
                      size_t trimmed_s_len = non_space_ending - non_space_beginning + 1;
              
                      if (s != non_space_beginning)
                      {
                          // Non-space characters exist in the beginning of s
              
                          memmove(s, non_space_beginning, trimmed_s_len);
                      }
              
                      s[trimmed_s_len] = '\0';
                  }
              }
              

              【讨论】:

              • 对读者来说绝对清楚,但 strlen 执行另一个循环.. :)
              猜你喜欢
              • 2011-01-16
              • 2013-03-04
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-04-12
              • 1970-01-01
              • 1970-01-01
              • 2012-05-08
              相关资源
              最近更新 更多