在C语言中从字符串中移除空格

58

在C语言中,最简单、最有效的去除字符串空格的方法是什么?


18
最简单和最有效并不一定相同。 - Alan H
@JimFell 那个问题的标题(曾经)非常误导人:它只是关于删除开头空格的。 - Wolf
16个回答

111

最容易和最有效往往不会同时发生...

这里有一个可能的原地删除解决方案:

void remove_spaces(char* s) {
    char* d = s;
    do {
        while (*d == ' ') {
            ++d;
        }
    } while (*s++ = *d++);
}

5
如果输入源是从字符串字面值初始化的,会发生什么? - Suppressingfire
11
假设您的意思是 RemoveSpaces("blah"); 而不是 char a[] = "blah"; RemoveSpaces(a);,那么这将产生未定义的行为。但这并不是代码的问题。不建议将只读字符串传递给文档记录为修改传递给它的字符串(例如删除空格)的函数。;-) - Steve Jessop
4
我认为你应该在最后加上 *i = '\0';。 - Nick Louloudakis
8
i = 0i = '\0' 是相同的 :) 它们都表示变量 "i" 的值为零,只是用不同的方式来表示。第一个是整数零,而第二个是空字符,但在C语言中,它们被视为相等的。 - Uxío
3
这个怎么运作的?我对C语言和指针很陌生,能否请您解释一下正在发生什么,将过程讲解清楚将不胜感激。 - starscream_disco_party
显示剩余9条评论

22

从发布的答案中可以看出,这并不是一个微不足道的任务。面对这样的任务,许多程序员似乎选择抛弃常识,以产生他们可能想得出的最隐晦的代码片段。

需要考虑的事情:

  • 您将希望复制字符串并删除其中的空格。修改传递的字符串是不好的实践,因为它可能是一个字符串字面量。此外,有时将字符串视为不可变对象是有益的。
  • 不能假定源字符串不为空。它可能仅包含单个空终止字符。
  • 在调用函数时,目标缓冲区可能包含任何未初始化的垃圾。检查其是否为空终止没有任何意义。
  • 源代码文档应说明目标缓冲区需要足够大,以容纳修剪后的字符串。最简单的方法是使其与未修剪的字符串一样大。
  • 在函数完成时,目标缓冲区需要保存一个没有空格的空终止字符串。
  • 考虑是要删除所有的空白字符还是只删除空格符' '
  • C编程并不是一个比谁可以在一行上挤入更多运算符的竞赛。相反,一个好的C程序包含可读性代码(始终是最重要的质量),而不会牺牲程序效率(有些重要)。
  • 出于这个原因,您不会因为让空终止插入成为复制代码的一部分而获得额外的积分。相反,使空终止插入明确化,以显示您不仅仅是意外地做对了它。

我会做什么:

void remove_spaces (char* restrict str_trimmed, const char* restrict str_untrimmed)
{
  while (*str_untrimmed != '\0')
  {
    if(!isspace(*str_untrimmed))
    {
      *str_trimmed = *str_untrimmed;
      str_trimmed++;
    }
    str_untrimmed++;
  }
  *str_trimmed = '\0';
}

在这段代码中,“str_untrimmed”源字符串没有被修改,这是通过适当的常量性保证的。如果源字符串仅包含空终止符,则不会崩溃。它总是将目标字符串以空终止。

内存分配留给调用方。算法应该只关注它的预期工作。它会删除所有的空格。

代码中没有微妙的技巧。它不会尝试在单行上挤入尽可能多的操作符。它将成为IOCCC的一个非常糟糕的候选者。然而它将产生几乎与更晦涩的单行版本相同的机器代码。

当复制某些东西时,你可以通过将两个指针声明为“restrict”,从而进行一些优化。这是程序员和编译器之间的契约,程序员保证目标和源不是同一地址。这样更有效率的优化,因为编译器可以直接从源复制到目标,而不需要中间的临时内存。


为什么要使用restrict关键字?你完全可以将同一个指针作为源和目的地进行传递,而且你的代码也支持这样做。 - chqrlie
@chqrlie 当然可以删除它,但这会以通用用例中的较慢代码为代价。我不认为我已经对这段代码进行了基准测试,但我怀疑它不应该有太大的影响。 - Lundin
2
这是我见过的最明智的答案。它清晰、简明,初学者也能很好地理解!谢谢。 - PageMaker
我会将 str_untrimmed 替换为 scattered,将 str_trimmed 替换为 condensed - Wolf
1
@Wolf,很好。现在请停止对他人的帖子进行微小的无关紧要的编辑或更改编码风格以符合您的个人喜好。显然,您的声望太高,无需审核即可获得编辑权限,否则您将面临编辑禁令。 - Lundin
@Lundin 感谢您让我知道这种编辑被认为是有害的。尽管如此,在大多数语言/库中,“trim”是指从字符串中删除前导和尾随空格的单词。 - Wolf

21

以下是一个非常简洁但完全正确的版本:

do while(isspace(*s)) s++; while(*d++ = *s++);

以下是一些我自己为了好玩而缩短的代码,它们并不完全正确,会引起评论者的不满。

如果你可以冒一些未定义行为的风险,并且永远没有空字符串,那么你可以省略掉函数体:

while(*(d+=!isspace(*s++)) = *s);

天啊,如果您说的“space”只是指空格字符:

while(*(d+=*s++!=' ')=*s);

不要在生产环境中使用那个 :)


有趣的是,在我的机器上前两个函数都能运行。但我猜所有这些都未被定义,因为在一个语句中使用 s++ 和 *s 会导致未定义行为? - Andomar
确保在引用字符串时不超出其末尾。 - Casey
1
@Andomar:第一个完全安全可靠。后两个确实有问题(在GCC4.2中测试过)。 - Kornel
1
称其为“声音”或许有点太客气了。所有三个版本都完全无法阅读,而且没有任何性能提升。苹果公司也同意花括号是不必要的。我的意思是,相比于写括号所涉及的巨大痛苦,数百万美元的损失和全世界程序员嘲笑你算得了什么? - Lundin
为什么要冒着未定义行为的风险,而不使用逗号运算符和“for”循环来解决这种风险呢? - autistic
今晚在寻找最有效的方法作为练习时偶然发现了这个,我很喜欢它!(指第一个)。在赋值周围加上括号可以消除警告。 - Joe McDonagh

9
在C语言中,你可以直接替换一些字符串,例如通过strdup()函数返回的字符串:
char *str = strdup(" a b c ");

char *write = str, *read = str;
do {
   if (*read != ' ')
       *write++ = *read;
} while (*read++);

printf("%s\n", str);

其他字符串是只读的,例如在代码中声明的字符串。您需要将这些字符串复制到新分配的内存区域,并通过跳过空格来填充副本:

char *oldstr = " a b c ";

char *newstr = malloc(strlen(oldstr)+1);
char *np = newstr, *op = oldstr;
do {
   if (*op != ' ')
       *np++ = *op;
} while (*op++);

printf("%s\n", newstr);

你可以看出人们为什么发明了其他语言。;)

你的第二个例子忘记了正确终止目标字符串。 - caf
...而且你的第一个例子根本不做正确的事情(例如,如果字符串以两个非空格字符开头)。 - caf
@caf: 这个 while 循环会执行到 \0 终止符,因为它是 while (*(op++)) 而不是 while (*(++op)) - Andomar
这是真的,这意味着它仍然存在漏洞,因为它无论第一个字符是否为空格都会跳过它。 - caf
为什么要将“去除空格”算法与内存分配混合在一起?没有理由这样做。避免使用strdup(),因为它不是标准的。从malloc()返回的结果不要进行强制类型转换。 - Lundin
显示剩余2条评论

2
#include <ctype>

char * remove_spaces(char * source, char * target)
{
     while(*source++ && *target)
     {
        if (!isspace(*source)) 
             *target++ = *source;
     }
     return target;
}

注意事项;

  • 此程序不处理Unicode编码。

3
这样做不会跳过第一个字符吗? - Aaron
2
你应该将传递给 isspace 的值转换为 unsigned char,因为该函数被定义为接受在 unsigned char 范围内或 EOF 值。 - caf
2
它仍然会删除第一个字符,并且如果使用target的第一个元素包含'\0'调用它将失败(我不明白检查其内容的目的是什么)。将while(*source++ && *target) {...}更改为do {...} while(*source++);似乎可以正常工作。 - mMontu
1
你是不是想说 ctype.h - Spikatrix
3
1)未能去除“source”中的初始空格。 2)如果“source ==“”,则从未将终止空字符附加到“target”。 3)依赖于“target [0]”中的值。 - chux - Reinstate Monica
return target; 返回目标; - BLUEPIXY

2
如果您仍然感兴趣,这个函数将从字符串开头删除空格,我已经在我的代码中使其工作:
void removeSpaces(char *str1)  
{
    char *str2; 
    str2=str1;  
    while (*str2==' ') str2++;  
    if (str2!=str1) memmove(str1,str2,strlen(str2)+1);  
}

1
最简单和最有效的从字符串中去除空格的方法是直接从字符串字面量中删除空格。例如,使用您的编辑器对“hello world”进行“查找和替换”,将其替换为“helloworld”,完成!
好的,我知道这不是您的意思。并不是所有的字符串都来自字符串字面量,对吧?假设您希望从中删除空格的字符串并不来自字符串字面量,我们需要考虑您的字符串的来源和目的地...我们需要考虑您的整个算法,您要解决的实际问题,以便建议最简单和最优化的方法。
也许您的字符串来自文件(例如stdin),并且必须写入另一个文件(例如stdout)。如果是这种情况,我会质疑为什么它需要首先成为一个字符串。只需将其视为一系列字符,并在遇到空格时将其丢弃...
#include <stdio.h>

int main(void) {
    for (;;) {
        int c = getchar();
        if (c == EOF) { break;    }
        if (c == ' ') { continue; }
        putchar(c);
    }
}

通过消除字符串存储的需求,不仅整个程序变得更加简短,而且从理论上讲也更加高效。

2
这个问题并没有提到字符串字面量。但是你必须假设一个字符串字面量可以被传递给函数。如果输入来自其他地方,例如你正在编写某种文本解析器,那该怎么办呢? - Lundin
当我们质疑一个程序的效率时,我们必须考虑整个程序,而不仅仅是其中的一小部分。这就是我在这里试图传达的内容,我认为你错过了这一点,@Lundin。 - autistic

1
#include<stdio.h>
#include<string.h>
main()
{
  int i=0,n;
  int j=0;
  char str[]="        Nar ayan singh              ";
  char *ptr,*ptr1;
  printf("sizeof str:%ld\n",strlen(str));
  while(str[i]==' ')
   {
     memcpy (str,str+1,strlen(str)+1);
   }
  printf("sizeof str:%ld\n",strlen(str));
  n=strlen(str);
  while(str[n]==' ' || str[n]=='\0')
    n--;
  str[n+1]='\0';
  printf("str:%s ",str);
  printf("sizeof str:%ld\n",strlen(str));
}

4
strlen函数返回size_t类型的值。因此请使用%zu而不是%ld。同时,请使用int main()作为程序入口,并在结尾处加上return 0;语句。 - Spikatrix
1
另外,memcpy 不适用于复制重叠的内存区域。请使用 memmove - autistic

1
/* Function to remove all spaces from a given string.
   https://www.geeksforgeeks.org/remove-spaces-from-a-given-string/
*/
void remove_spaces(char *str)
{
    int count = 0;
    for (int i = 0; str[i]; i++)
        if (str[i] != ' ')
            str[count++] = str[i];
    str[count] = '\0';
}

counti 的类型更改为 size_t,您将拥有一个干净且稳健的解决方案。 - chqrlie

0

代码取自zString库

/* search for character 's' */
int zstring_search_chr(char *token,char s){
        if (!token || s=='\0')
        return 0;

    for (;*token; token++)
        if (*token == s)
            return 1;

    return 0;
}

char *zstring_remove_chr(char *str,const char *bad) {
    char *src = str , *dst = str;

    /* validate input */
    if (!(str && bad))
        return NULL;

    while(*src)
        if(zstring_search_chr(bad,*src))
            src++;
        else
            *dst++ = *src++;  /* assign first, then incement */

    *dst='\0';
    return str;
}

代码示例

  Exmaple Usage
      char s[]="this is a trial string to test the function.";
      char *d=" .";
      printf("%s\n",zstring_remove_chr(s,d));

  Example Output
      thisisatrialstringtotestthefunction

看一下 zString 代码,你可能会发现它很有用 https://github.com/fnoyanisi/zString


为什么你要一遍又一遍地检查传递的参数是否为空呢?多余的空指针检查使得这个版本成为所有发布版本中效率最低的。为什么不使用标准的 strpbrk 而是使用自己编写的版本呢?而且,常量正确性在哪里呢? - Lundin
好的,第一个 if 语句可以被移除,检查可以在 for 循环的逻辑测试部分中完成,谢谢你的建议,我会研究一下......>> 为什么不使用标准的 strpbrk 而要使用自己编写的版本?只是为了好玩而编写了这个代码(整个 zString 的东西),并尽量不使用标准函数。所以,可以说这是一个 _有趣的项目_,但这当然不应该阻止任何人贡献代码。 - fnisi
与注释所说的不同,zstring_search_chr 不会返回 chr 的索引,它的 char* 参数应该是 const 限定的。函数 zstring_remove_chr 效率相当低下。 - chqrlie
@chqrlie,更新了zstring_remove_chr()函数的注释和代码。我很想看到你更高效的版本或者一些建议。谢谢。 - fnisi
1
如果您愿意,您可以在http://codereview.stackexchange.com上发布代码。如果您这样做,我会写一篇评论。确实有一些改进的想法。 - chqrlie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接