如何以标准方式修剪前导/后缀空白?


177

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

Answers:


164

如果可以修改字符串:

// 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;
}

6
抱歉,除非您不关心内存泄漏,否则第一个答案根本不好。现在,您有两个重叠的字符串(原来的字符串,其中的尾部空格被裁剪,而新的字符串)。只能释放原始字符串,但如果这样做,则第二个指向已释放的内存。
David Nehme

7
@nvl:没有分配内存,因此没有可用的内存。
亚当·罗森菲尔德

15
@nvl:编号 str是局部变量,对其进行更改不会更改传入的原始指针。C语言中的函数调用始终是按值传递,而不是按引用传递。
亚当·罗森菲尔德

11
@Raj:返回与传入的地址不同的地址没有内在的错误。这里没有要求返回的值是free()函数的有效参数。恰恰相反-我设计该方法是为了避免为了效率而分配内存。如果传入的地址是动态分配的,则调用方仍然负责释放该内存,并且调用方需要确保不要用此处返回的值覆盖该值。
亚当·罗森菲尔德2013年

3
你要投的论据isspaceunsigned char,否则你调用未定义的行为。
罗兰·伊利格

37

这是将字符串移到缓冲区的第一个位置的字符串。您可能想要这种行为,以便如果您动态分配了字符串,仍然可以在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”编译。


1
你要投的论据isspaceunsigned char,否则你调用未定义的行为。
罗兰·伊利格

@RolandIllig:谢谢,我从未意识到这是必要的。解决它。
indiv 2013年

@Simas:为什么这么说?函数调用后isspace(),为什么" "和之间会有区别"\n"?我添加了换行符的单元测试,对我来说看起来不错
indiv

1
@indiv手动分配时将访问无效的内存块。即此行:*(endp + 1) = '\0';。答案的示例测试使用64的缓冲区,可以避免此问题。
Simas

1
@nolandda:感谢您提供详细信息。我修复了它,并更新了测试以检测缓冲区溢出,因为目前我无法访问valgrind。
indiv

23

我的解决方案。字符串必须是可变的。与其他解决方案相比,它的优点是它将非空格部分移到开头,因此您可以继续使用旧指针,以防日后需要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);
}

4
trim()如果s"",则调用UB,就像第一次isspace()调用那样isspace(p[-1])p[-1]并且不一定引用合法位置。
chux-恢复莫妮卡

1
你要投的论据isspaceunsigned char,否则你调用未定义的行为。
罗兰·伊利格

1
应该添加if(l==0)return;以避免零长度的str
ch271828n

11

这是我的C mini库,用于在适当位置并单独修剪左右,全部修剪,并修剪一组指定的字符(默认情况下为空白)。

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)。它没有太多内容,并且仅在字符串上迭代一次。有些人可能会抱怨修剪右部从左边开始,但是,不需要从左边开始的任何努力。(您必须以一种或另一种方式到达字符串的末尾以进行正确的修整,因此您不妨随心所欲地完成工作。)关于流水线化和高速缓存大小,可能会有争论,例如-谁知道。由于该解决方案从左到右工作并且仅迭代一次,因此它也可以扩展为在流上工作。局限性:它并没有在工作的Unicode字符串。


2
我对此表示赞同,但我知道它已经过时了,但我认为这是一个错误。在将其用作数组索引之前dtab[*d]不会强制*d转换unsigned int为。在带有签名字符的系统上,这将读到最多,dtab[-127]这将导致错误并可能导致崩溃。
Zan Lynx

2
潜在的不确定行为,dtab[*delim++]因为char必须将索引值强制转换为unsigned char。该代码假定8位chardelim应该声明为const char *dtab[0xFF & (unsigned int)*d]会更清晰地显示为dtab[(unsigned char)*d]。该代码适用于UTF-8编码的字符串,但不会去除非ASCII空格序列。
chqrlie

@ michael-plainer,这看起来很有趣。您为什么不对其进行测试并将其放在GitHub上?
荒木大辅

8

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

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.
}

2
建议while ((end >= begin) && isspace(str[end]))str is ““ . Prevents str [-1]` 时进行更改以防止UB 。
chux-恢复莫妮卡

顺便说一句,我必须将其更改为str [i-begin + 1]才能正常工作
truongnm '16

1
你要投的论据isspaceunsigned char,否则你调用未定义的行为。
罗兰·伊利格

@RolandIllig,为什么会有未定义的行为?该功能旨在与字符​​一起使用。
wovano

@wovano不,不是。from的功能<ctype.h>旨在与int配合使用,int表示unsigned char或特殊值EOF。请参阅stackoverflow.com/q/7131026/225757
罗兰·伊利格

8

装修晚会

特点:
1.与其他许多答案一样,快速修剪开始。
2.结束后,每个循环仅对1个测试进行修整。类似于@ jfm3,但适用于所有空格字符串)
3.为避免char带符号的未定义行为char,将*s其强制转换为unsigned char

字符处理 “在所有情况下,参数均为an int,其值应表示为an unsigned char或等于宏的值EOF。如果参数具有任何其他值,则行为未定义。” C11§7.41

#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);
}

2
是的,终于有人知道了ctype未定义的行为。
罗兰·伊利格

1
@chux我认为应该是len =(size_t)(ps)+1; 否则最后一个字母重叠。
–river

4

这是一种类似于@ adam-rosenfields就地修改例程的解决方案,但无需使用strlen()。像@jkramer一样,字符串在缓冲区内向左调整,因此您可以释放相同的指针。不适用于大型字符串,因为它不使用记忆。包括@ 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; char s [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

@Villemoes,感谢您的错误报告。我更新了逻辑,以避免在字符串仅包含空格时走出缓冲区的左侧。这个新版本可以解决您的问题吗?
Rhys Ulerich

语言律师可能会对您大喊大叫,因为仅仅想到推测要在指向'a'的字符之前创建一个指向char的指针(这就是您的'--p'会做的事情)。在现实世界中,您可能还可以。但是您也可以将'> ='更改为'>',然后将p的减量移至'isspace(*-p)'。
Villemoes

我认为律师会没事的,因为它只是比较一个地址而不碰它,但我也很喜欢您关于减量的建议。我已经相应地更新了。谢谢。
Rhys Ulerich

1
doukremt,您是否担心foobar之后的整个缓冲区未填充零?如果是这样,那么如果您明确地说而不是扔出模糊的石头,那将大有帮助。
Rhys Ulerich'3

3

另一行,其中一行完成实际工作:

#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);
}

1
使用scanf的好主意;但他只能使用可能不是OP想要的单词的单词(即,修剪“ abc”可能会导致“ ab c”,而您的单个scanf只会导致“ a”)。因此,我们需要一个循环和一个带有%n转换说明符的跳过字符的计数器,恐怕最终,手工完成会更简单。
彼得-恢复莫妮卡

当您想要字符串的第一个单词而不考虑任何初始空格时,此功能非常有用。
J ... S

3

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

  1. 在原始指针的字符串内返回了一个不同的指针(很难将两个不同的指针杂凑到同一事物上)。
  2. 免费使用了诸如strlen()之类的东西,它们会重复整个字符串。
  3. 使用了非便携式OS特定的lib函数。
  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';
}

2
你要投的论据isspaceunsigned char,否则你调用未定义的行为。
罗兰·伊利格

由于此答案与“浪费的循环”有关,因此请注意,如果没有空间,代码会不必要地复制整个字符串。领导while (isspace((unsigned char) *szWrite)) szWrite++;将阻止这种情况。代码还会复制所有尾随空白。
chux-恢复莫妮卡

@chux此实现使用单独的读写指针在原地进行更改(与在不同位置返回新指针相反),因此建议将szWrite跳转到第一行的第一个非空格将在行首留下空格。原始字符串。
詹森·斯图尔特

@chux,您是正确的,它确实复制了结​​尾的空格(在最后一个非空格字符之后添加null之前),但这是我选择为避免预扫描字符串而付出的代价。对于少量的尾随WS,仅复制字节而不是为最后一个非WS字符预扫描整个字符串会更便宜。对于大量的尾随WS,减少扫描次数可能值得进行预扫描。
詹森·斯图尔特

@chux,对于“在没有空间的情况下复制”的情况,只有*szWrite = *szRead在指针不相等的情况下才执行,在这种情况下将跳过写操作,但是随后我们添加了另一个比较/分支。使用现代的CPU / MMU / BP,我不知道该检查是损失还是收益。使用更简单的处理器和内存体系结构,仅执行复制并跳过比较会更便宜。
詹森·斯图尔特

2

晚会很晚...

单遍向前扫描解决方案,无回溯。在源字符串中每个字符都是测试一次两次。(因此,它应该比这里的大多数其他解决方案更快,尤其是在源字符串包含大量尾随空格的情况下。)

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

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

#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);
}

1
源字符串中的每个字符都只能测试一次:并非如此,源字符串中的大多数字符都会被测试两次:与进行比较'\0',然后使用进行测试isspace()。用来测试所有字符似乎是浪费的isspace()。对于非病理情况,从字符串末尾回溯应该更有效。
chqrlie

@chqrlie-是的,每个字符都经过两次测试。我希望看到此代码经过实际测试,尤其是与此处其他算法相比,给定带大量尾随空格的字符串。
David R Tribble,

trim()好。极端情况:与和重叠trim2(char *d, const char *s)时遇到麻烦。d,ss < d
chux-恢复莫妮卡

@chux-在那种极端情况下,应该如何trim()表现?您要修剪字符串并将其复制到字符串本身占用的内存中。与不同memmove(),这需要在修剪之前确定源字符串的长度,这需要额外扫描整个字符串。最好编写一个不同的rtrim2()函数,该函数知道将源反向复制到目标,并且可能需要附加的源字符串长度参数。
David R Tribble

1

我不确定您认为“无痛”。

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

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

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

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

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

现在的问题是您如何处理?眼前的数据类型实际上并不是一个String容易想到的强大的抽象,而实际上仅仅是一个存储字节数组。缺少健壮的数据类型,不可能编写与PHperytonby chomp函数相同的函数。C语言中的此类函数将返回什么?


除非字符串由所有空格组成,否则此方法效果很好。需要一次检查才能do { q--; } ...知道*q != 0
chux-恢复莫妮卡2014年

1

使用字符串库,例如:

Ustr *s1 = USTR1(\7, " 12345 ");

ustr_sc_trim_cstr(&s1, " ");
assert(ustr_cmp_cstr_eq(s1, "12345"));

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



1

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

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);
}

1
strlen()传回的值size_t可能会超出的范围int。空格不限于空格字符。最后但最重要的一点是:strcpy(string, string + i * sizeof(char));由于源数组和目标数组重叠,导致未定义的行为。使用memmove()代替strcpy()
chqrlie

@chqrlie,您是对的,只是提供了您的建议。我了解到,当源和目标重叠时进行复制会导致未定义的行为,但我想指出的是,在这种特殊情况下,这不会造成任何问题,因为我们总是要从后面的内存位置复制到开头,感谢您的反馈。
wallek876

1
源数组和目标数组如何重叠无关紧要,这是未定义的行为。不要依赖这样的假设,即随着地址的增加,复制一次可能发生一个字节。我也忘记提及while (isspace((int)string[i])) string[i--] = '\0';可能会超出字符串开头的地方。您应该将此循环与上一行和下一行结合起来并编写while (i > 0 && isspace((unsigned char)string[--i])) { string[i] = '\0'; } size_t end = i;
chqrlie

@chqrlie好点,一个带有所有空格的字符串会导致循环通过开头,没想到。
wallek876

实际上,我的建议是不正确的,因为它end没有指向结尾的空字节,并且您end = ++i;对于包含所有空白字符的字符串仍然存在问题。我刚刚修复了代码。
chqrlie

1

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

// 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;
}

1
注意:isspace(*str)UB时*str < 0
chux-恢复莫妮卡

1
使用size_t n很好,但是当界面n对于完整的修剪过的字符串而言太小时,该界面不会以任何方式通知调用者。考虑trim(out, 12, "delete data not")
chux-恢复莫妮卡

1

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

#include <stdio.h>

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

1
这不适用于中间带有空格的字符串,例如" foo bar "
David R Tribble

1

好的,这是我对这个问题的看法。我认为这是最简洁的解决方案,可以修改字符串(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 > str测试仅需要一次。 *b = 0;只需要一次。
chux-恢复莫妮卡

1
#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 有助于修剪所有空白。

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

只是一点点挑剔,strndup()不是C标准的一部分,而只是Posix。但是由于实施起来非常容易,所以没什么大不了的。
PatrickSchlüter19年

trim_space("")返回NULL。我希望有一个指向的指针""int len;应该是size_t len;isspace(in[len - 1])UB什么时候in[len - 1] < 0
chux-恢复莫妮卡

最初while (isspace((unsigned char) *in) in++;之前len = strlen(in);会比后来更有效while(len && *in && isspace(*in)) ++in, --len;
chux -恢复莫妮卡

0

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

摆脱尾随的空格很容易,而且非常安全,因为您可以在最后一个空格的顶部放一个0,从末尾倒数。摆脱领先空间意味着要四处走动。如果您想就位(可能明智的话),可以一直将所有内容移回一个字符,直到没有前导空格为止。或者,为了提高效率,您可以找到第一个非空格字符的索引,然后将所有内容都移回该数字。或者,您可以仅使用指向第一个非空格字符的指针(但是随后您需要像对待strtok一样小心谨慎)。


4
strtok通常不是很好使用的工具-尤其是因为它不是可重入的。如果您留在单个函数中,则可以安全地使用它,但是如果有线程或调用其他本身可能使用strtok的函数的可能性,那么您会遇到麻烦。
乔纳森·莱夫勒

0
#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;
}

3
这让我发笑,因为我以为dreamlax已编辑测试字符串以包括“大吸顶”。不。原始作者是诚实的。
詹姆斯·莫里斯

1
不要使用此代码。它产生缓冲区溢出。
罗兰·伊利格

0

游戏有点晚了,但是我会把日常工作弄得一团糟。它们可能不是最绝对有效的,但是我相信它们是正确的并且很简单(通过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));
}

1
您应该将char参数转换isspace()为,(unsigned char)以避免对潜在的负值产生不确定的行为。ltrim()如有必要,还应避免移动弦。
chqrlie

0

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

  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';
}

1
如果你关心性能,请不要使用strspn(),并strcspn()在一个紧凑的循环。这是非常低效的,开销将使单向通过的未经验证的优势相形见.。 strlen()通常使用非常有效的代码内联扩展,而不是真正的问题。修剪字符串的开头和结尾将比测试字符串中的每个字符的白度要快得多,即使在很少或没有非白色字符的字符串的特殊情况下。
chqrlie

0

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

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
}

1
怎么样:char *trim(char *s) { char *p = s, *e = s + strlen(s); while (e > s && isspace((unsigned char)e[-1])) { *--e = '\0'; } while (isspace((unsigned char)*p)) { p++; } if (p > s) { memmove(s, p, e + 1 - p); } return s; }
chqrlie

0

这些函数将修改原始缓冲区,因此,如果动态分配原始指针,则可以将其释放。

#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
chqrlie


0

要从两侧修剪我的琴弦,我会使用老歌,但会发粘;)它可以以小于空格的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,&strData[S],L)由于源数组和目标数组重叠而调用未定义的行为。使用memmove()代替strncpy()
chqrlie

在这种情况下,可以这样,因为目标地址的索引总是比源地址的索引小,但是内存确实会更好。
ДеянДобромиров

不,这不行。不管源数组和目标数组如何重叠,它都会调用未定义的行为,因为您无法安全地对超出其标准规范的库函数的实现进行假设。 现代编译器倾向于不合理地利用具有潜在未定义行为的情况,谨慎行事并远离UB,并且不要让新手做出不安全的假设。
chqrlie

0

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

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';

1
isspace(0)如果定义为false,则可以简化两个函数。同时移动模块memmove()内部if
chqrlie

0

在这里,我使用动态内存分配将输入字符串修剪为函数trimStr。首先,我们发现输入字符串中存在多少个非空字符。然后,我们分配一个具有该大小的字符数组,并注意以null结尾的字符。使用此函数时,需要释放main函数内部的内存。

#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;
}

0

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

#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会执行另一个循环.. :)
ingconti

0
char* strtrim(char* const str)
{
    if (str != nullptr)
    {
        char const* begin{ str };
        while (std::isspace(*begin))
        {
            ++begin;
        }

        auto end{ begin };
        auto scout{ begin };
        while (*scout != '\0')
        {
            if (!std::isspace(*scout++))
            {
                end = scout;
            }
        }

        auto /* std::ptrdiff_t */ const length{ end - begin };
        if (begin != str)
        {
            std::memmove(str, begin, length);
        }

        str[length] = '\0';
    }

    return str;
}

2
尽管此代码可以回答问题,但提供有关如何和/或为什么解决问题的其他上下文将提高​​答案的长期价值。
Nic3500 '18
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.