C++中如何去掉额外的空格

19

我试图编写一个脚本,用于删除额外的空格,但我没能完成它。

基本上我想将abc sssd g g sdg gg gf转换为abc sssd g g sdg gg gf

在像PHP或C#这样的语言中,这将非常容易,但在C ++中似乎不是这样。这是我的代码:

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <cstring>
#include <unistd.h>
#include <string.h>

char* trim3(char* s) {
    int l = strlen(s);

    while(isspace(s[l - 1])) --l;
    while(* s && isspace(* s)) ++s, --l;

    return strndup(s, l);
}

char *str_replace(char * t1, char * t2, char * t6)
{
    char*t4;
    char*t5=(char *)malloc(10);
    memset(t5, 0, 10);
    while(strstr(t6,t1))
    {
        t4=strstr(t6,t1);
        strncpy(t5+strlen(t5),t6,t4-t6);
        strcat(t5,t2);
        t4+=strlen(t1);
        t6=t4;
    }

    return strcat(t5,t4);
}

void remove_extra_whitespaces(char* input,char* output)
{
    char* inputPtr = input; // init inputPtr always at the last moment.
    int spacecount = 0;
    while(*inputPtr != '\0')
    {
        char* substr;
        strncpy(substr, inputPtr+0, 1);

        if(substr == " ")
        {
            spacecount++;
        }
        else
        {
            spacecount = 0;
        }

        printf("[%p] -> %d\n",*substr,spacecount);

        // Assume the string last with \0
        // some code
        inputPtr++; // After "some code" (instead of what you wrote).
    }   
}

int main(int argc, char **argv)
{
    printf("testing 2 ..\n");

    char input[0x255] = "asfa sas    f f dgdgd  dg   ggg";
    char output[0x255] = "NO_OUTPUT_YET";
    remove_extra_whitespaces(input,output);

    return 1;
}

它不起作用。我尝试了几种方法。我想做的是逐字迭代字符串,并将其转储到另一个字符串中,只要一次有一个空格;如果有两个空格,请不要将第二个字符写入新字符串。

我该怎么解决?


2
这是C++代码,以下是我的编译和运行命令:clear; rm -f test2.exe; g++ -o test2.exe test2.cpp; ./test2.exe; - Damian
4
不要在C++中编写C风格的代码!使用语言特性。 - too honest for this site
5
在C++中以C风格编码有其合理的原因。大多数情况下,使用更符合C++惯用法和特点的替代方式可能更好,但这要根据具体情况而定。 - Deduplicator
4
语义仍然可能不同,即使语法相同。 - too honest for this site
9
嗯... 0x255...一个非常奇特的常量。 - Deduplicator
显示剩余6条评论
11个回答

30

已经有很多好的解决方案了。我向您提出一种基于专用<algorithm>的替代方案,旨在避免连续重复:unique_copy()

void remove_extra_whitespaces(const string &input, string &output)
{
    output.clear();  // unless you want to add at the end of existing sring...
    unique_copy (input.begin(), input.end(), back_insert_iterator<string>(output),
                                     [](char a,char b){ return isspace(a) && isspace(b);});  
    cout << output<<endl; 
}

这里是一个实时演示。请注意,我从C风格字符串更改为更安全、更强大的C++字符串。

编辑:如果你的代码需要保留C风格字符串,你可以使用几乎相同的代码,但要用指针代替迭代器。这就是C++的魔力。这里是另一个实时演示


是的,我也同意,字符串是最好的选择,但所有“脚本”都是用char *编写的(2000行)......并且此脚本必须在Centos 4,5.1,Debian 4,Unix基础系统上运行......因此最好使用尽可能简单的函数,以避免出现“分段错误”。 - Damian
你的函数没有正常工作。如果开头或结尾有空格,它会保留它们。这不是操作者想要的。 - Jts
1
@José我的函数按照OP的要求移除了多余的空格。我在问题中找不到任何证据表明起始空格或结束空格应该被移除。如果这是一个要求,你只需要用find_if()替换input.begin()并在返回之前添加一个条件删除。 - Christophe
1
@Damian,算法库的好处是许多算法也可以使用指针而不是迭代器。这里有一个在线演示,使用相同的算法,但保持C风格字符串,就像你喜欢的那样;-) - Christophe
2
顺便说一句:您可能想将cstring解决方案添加到您的答案中。 - Deduplicator
显示剩余3条评论

11

这里有一个简单的非C++11解决方案,使用与问题中相同的remove_extra_whitespace()签名:

#include <cstdio>

void remove_extra_whitespaces(char* input, char* output)
{
    int inputIndex = 0;
    int outputIndex = 0;
    while(input[inputIndex] != '\0')
    {
        output[outputIndex] = input[inputIndex];

        if(input[inputIndex] == ' ')
        {
            while(input[inputIndex + 1] == ' ')
            {
                // skip over any extra spaces
                inputIndex++;
            }
        }

        outputIndex++;
        inputIndex++;
    }

    // null-terminate output
    output[outputIndex] = '\0';
}

int main(int argc, char **argv)
{
    char input[0x255] = "asfa sas    f f dgdgd  dg   ggg";
    char output[0x255] = "NO_OUTPUT_YET";
    remove_extra_whitespaces(input,output);

    printf("input: %s\noutput: %s\n", input, output);

    return 1;
}

输出:

input: asfa sas    f f dgdgd  dg   ggg
output: asfa sas f f dgdgd dg ggg

没问题。请注意,remove_extra_whitespaces() 假设最终字符串不会超出为 output 分配的内存;如果超出了,可能会导致分段错误。 - villapx

7

由于您使用C ++,因此可以利用为此类工作设计的标准库功能。 您可以使用std :: string(而不是 char [0x255] )和 std :: istringstream ,这将替换大部分指针算术。

首先,创建一个字符串流:

std::istringstream stream(input);

接着,从中读取字符串。它将自动删除空白分隔符:

std::string word;
while (stream >> word)
{
    ...
}

在循环内部,构建您的输出字符串:
    if (!output.empty()) // special case: no space before first word
        output += ' ';
    output += word;

这种方法的一个缺点是它动态分配内存(包括多次重新分配,当输出字符串增长时)。

是的,string > char[0x255],我同意,但我想坚持使用char*,因为所有的代码都是用char*编写的... - Damian
1
你可以通过构造函数来回转换 char*string,并通过 c_str()strcpy 进行反向转换。这会给 CPU 带来很多不必要的工作,但对你来说会少些麻烦。 - anatolyg
@anatolyg:如果在正确的地方和正确的时间完成,优化器可能只需要少量的额外工作。 - Deduplicator
是的,我也同意,字符串是最好的选择,但所有的“脚本”都是使用“char *”编写的(2000行)...并且这个脚本必须在CentOS 4、5.1、Debian 4、Unix基础系统等上运行...因此最好使用尽可能简单的函数,以避免出现“分段错误”。 - Damian
1
@Damian:使用更简单的函数并不能保证避免错误。你需要编写更多的代码,而不是使用库工具,这样出现错误的可能性就越大。显然,你需要理解你所使用的库函数,而C++比C拥有更多的库函数。 - Peter Cordes

4
您可以使用std::unique,它根据您定义的两个元素相等的条件,将相邻的重复元素缩减为单个实例。

在这里,如果两个元素都是空格字符,则将它们定义为相等。

inline std::string& remove_extra_ws_mute(std::string& s)
{
    s.erase(std::unique(std::begin(s), std::end(s), [](unsigned char a, unsigned char b){
        return std::isspace(a) && std::isspace(b);
    }), std::end(s));

    return s;
}

inline std::string remove_extra_ws_copy(std::string s)
{
    return remove_extra_ws_mute(s);
}

std::unique 移动重复字符到字符串的结尾,并返回一个指向它们开头的迭代器,以便可以删除它们。

此外,如果您必须使用低级别字符串,则仍然可以在指针上使用std::unique

char* remove_extra_ws(char const* s)
{
    std::size_t len = std::strlen(s);

    char* buf = new char[len + 1];
    std::strcpy(buf, s);

    // Note that std::unique will also retain the null terminator
    // in its correct position at the end of the valid portion
    // of the string    
    std::unique(buf, buf + len + 1, [](unsigned char a, unsigned char b){
        return (a && std::isspace(a)) && (b && std::isspace(b));
    });

    return buf;
}

3

有很多方法可以做到这一点(例如使用正则表达式),但你可以使用std::copy_if与一个记住上一个字符是否为空格的有状态函数来实现。

#include <algorithm>
#include <string>
#include <iostream>

struct if_not_prev_space
{
    // Is last encountered character space.
    bool m_is = false;

    bool operator()(const char c)
    {                                      
        // Copy if last was not space, or current is not space.                                                                                                                                                              
        const bool ret = !m_is || c != ' ';
        m_is = c == ' ';
        return ret;
    }
};


int main()
{
    const std::string s("abc  sssd g g sdg    gg  gf into abc sssd g g sdg gg gf");
    std::string o;
    std::copy_if(std::begin(s), std::end(s), std::back_inserter(o), if_not_prev_space());
    std::cout << o << std::endl;
}

是的,string > char[0x255],我同意,但我想坚持使用char*,因为所有的代码都是用char*写的...,这可行吗? - Damian
不确定你是不是想把评论发给我,但是可以参考string::c_str - Ami Tavory
如果字符串以空格结尾,则此操作会在字符串末尾留下一个额外的空格。不确定原帖中的需求是否需要处理这种情况... - jaggedSpire
@jaggedSpire 很好的观点。我必须说我想到了这一点,并决定(也许是希望地认为)它符合问题要求。如果不是,那么在应用 copy_if 后可以用一行代码解决。 - Ami Tavory
C是一种非常古老的编程语言,它总是让我头痛...看看这个:http://stackoverflow.com/questions/35873677/segmentation-fault-on-malloc-function - Damian
显示剩余2条评论

3

对于就地修改,您可以应用删除-移除技术:

#include <string>
#include <iostream>
#include <algorithm>
#include <cctype>

int main()
{
    std::string input {"asfa sas    f f dgdgd  dg   ggg"};
    bool prev_is_space = true;
    input.erase(std::remove_if(input.begin(), input.end(), [&prev_is_space](unsigned char curr) {
        bool r = std::isspace(curr) && prev_is_space;
        prev_is_space = std::isspace(curr);
        return r;

    }), input.end());

    std::cout << input << "\n";
}

首先,您需要将所有额外的空格移动到字符串的末尾,然后截断它。


C++的巨大优势在于它足够通用,只需进行少量修改即可将代码移植到纯C静态字符串中:

void erase(char * p) {
    // note that this ony works good when initial array is allocated in the static array
    // so we do not need to rearrange memory
    *p = 0; 
}

int main()
{
    char input [] {"asfa sas    f f dgdgd  dg   ggg"};
    bool prev_is_space = true;
    erase(std::remove_if(std::begin(input), std::end(input), [&prev_is_space](unsigned char curr) {
        bool r = std::isspace(curr) && prev_is_space;
        prev_is_space = std::isspace(curr);
        return r;

    }));

    std::cout << input << "\n";
}

有趣的是,这里的remove步骤与字符串表示无关。它将在不做任何修改的情况下与std::string一起使用。


是的,string > char[0x255],我同意,但我想坚持使用char*,因为所有的代码都是用char*编写的... - Damian
1
不错,但是如果您多次执行此块(在循环、函数或多个线程中),静态变量 prev_is_space 将不会被重置。为了安全地使用它,您需要捕获一个本地布尔值,以便在需要时进行重置。 - Christophe
@Christophe,我明白了。谢谢。 - Lol4t0
是的,我也同意,字符串是最好的选择,但是所有的“脚本”都是使用char*编写的(2000行)...而且这个脚本必须在centos 4, 5.1debian 4基于Unix的系统上运行...因此最好使用尽可能简单的函数,以避免出现“段错误”。 - Damian

2

我有一种沉痛的感觉,好像老旧的 scanf 方法就能解决问题(实际上,这是 C 语言中与 Anatoly 的 C++ 解决方案相当的做法):

void remove_extra_whitespaces(char* input, char* output)
{
    int srcOffs = 0, destOffs = 0, numRead = 0;

    while(sscanf(input + srcOffs, "%s%n", output + destOffs, &numRead) > 0)
    {
        srcOffs += numRead;
        destOffs += strlen(output + destOffs);
        output[destOffs++] = ' '; // overwrite 0, advance past that
    }
    output[destOffs > 0 ? destOffs-1 : 0] = '\0';
}

我们利用了 `scanf` 具有神奇的内置空格跳过功能的事实。然后使用可能不太知名的 `%n` "转换" 说明符,它给出了由 `scanf` 消耗的字符数。当从字符串中读取时,这个特性经常会派上用场,就像这里一样。使这种解决方案不完美的是对输出进行 `strlen` 调用(不幸的是没有“我实际刚刚写了多少字节”的转换说明符)。
最后,使用 scanf 在这里很容易,因为保证在 output 中存在足够的内存;如果不是这种情况,由于缓冲区和溢出处理,代码将变得更加复杂。

sscanfжҳҜдёҖдёӘеҮҪж•°пјҢеҸҜд»ҘеңЁANSI CпјҲзәҜCпјүдёӯдҪҝз”Ёеҗ—пјҹ - Damian
@Damian 是的,它是C标准的一部分(并且随之成为类Unix系统POSIX标准的一部分)。 - Peter - Reinstate Monica
谢谢你,你知道,C语言是一种非常古老的编程语言,它总是让我头痛...看这个:http://stackoverflow.com/questions/35873677/segmentation-fault-on-malloc-function - Damian

1

既然你在写C风格的代码,这里有一种实现你想要的方法。请注意,你可以删除换行符'\r''\n'(当然,如果你认为它们是空格,那就看你自己了)。

这个函数应该比任何其他替代方案都快,即使传入std::string时也不会发生内存分配(我已经重载了它)。

char temp[] = " alsdasdl   gasdasd  ee";
remove_whitesaces(temp);
printf("%s\n", temp);

int remove_whitesaces(char *p)
{
    int len = strlen(p);
    int new_len = 0;
    bool space = false;

    for (int i = 0; i < len; i++)
    {
        switch (p[i])
        {
        case ' ': space = true;  break;
        case '\t': space = true;  break;
        case '\n': break; // you could set space true for \r and \n
        case '\r': break; // if you consider them spaces, I just ignore them.
        default:
            if (space && new_len > 0)
                p[new_len++] = ' ';
            p[new_len++] = p[i];
            space = false;
        }
    }

    p[new_len] = '\0';

    return new_len;
}

// and you can use it with strings too,

inline int remove_whitesaces(std::string &str)
{
    int len = remove_whitesaces(&str[0]);
    str.resize(len);
    return len; // returning len for consistency with the primary function
                // but u can return std::string instead.
}

// again no memory allocation is gonna take place,
// since resize does not not free memory because the length is either equal or lower

如果你仔细查看C++标准库,你会发现很多返回std::string或其他std::对象的C++函数基本上是对一个良好编写的extern "C"函数的包装。因此,如果这些函数编写良好并且可以重载以支持std::strings等内容,那么在C++应用程序中使用C函数是可行的。
例如,在Visual Studio 2015中,std::to_string就是这样编写的:
inline string to_string(int _Val)
    {   // convert int to string
    return (_Integral_to_string("%d", _Val));
    }

inline string to_string(unsigned int _Val)
    {   // convert unsigned int to string
    return (_Integral_to_string("%u", _Val));
    }

而_Integral_to_string是一个C函数sprintf_s的包装器

template<class _Ty> inline
    string _Integral_to_string(const char *_Fmt, _Ty _Val)
    {   // convert _Ty to string
    static_assert(is_integral<_Ty>::value,
        "_Ty must be integral");
    char _Buf[_TO_STRING_BUF_SIZE];
    int _Len = _CSTD sprintf_s(_Buf, _TO_STRING_BUF_SIZE, _Fmt, _Val);
    return (string(_Buf, _Len));
    }

嗯,非常有趣,所以基本上你的 int remove_whitesaces(char *p) 函数不需要带两个参数,只需利用指针的力量“即时”修改它,对吧? - Damian
是的,因为输出长度始终等于或小于输入长度,所以无需创建另一个对象。我还过载了它来支持std ::字符串(再次不会进行内存分配)。我认为你会接受我的答案,因为它实际上是可定制的(并且不接受制表符('\t'),因为几乎所有人都认为它们是空格)。如果需要,它也可以忽略换行符。 - Jts

0

这里有一个较长(但易懂)的解决方案,不使用指针。 它可以进一步优化,但是它能够工作。

#include <iostream>
#include <string>
using namespace std;
void removeExtraSpace(string str);
int main(){
    string s;
    cout << "Enter a string with extra spaces: ";
    getline(cin, s);
    removeExtraSpace(s);
    return 0;
}
void removeExtraSpace(string str){
    int len = str.size();
    if(len==0){
        cout << "Simplified String: " << endl;
        cout << "I would appreciate it if you could enter more than 0 characters. " << endl;
        return;
    }
    char ch1[len];
    char ch2[len];
    //Placing characters of str in ch1[]
    for(int i=0; i<len; i++){
        ch1[i]=str[i];
    }
    //Computing index of 1st non-space character
    int pos=0;
    for(int i=0; i<len; i++){
        if(ch1[i] != ' '){
            pos = i;
            break;
        }
    }
    int cons_arr = 1;
    ch2[0] = ch1[pos];
    for(int i=(pos+1); i<len; i++){
        char x = ch1[i];
        if(x==char(32)){
            //Checking whether character at ch2[i]==' '
            if(ch2[cons_arr-1] == ' '){
                continue;
            }
            else{
                ch2[cons_arr] = ' ';
                cons_arr++;
                continue;
            }
        }
        ch2[cons_arr] = x;
        cons_arr++;
    }
    //Printing the char array
    cout << "Simplified string: " << endl;
    for(int i=0; i<cons_arr; i++){
        cout << ch2[i];
    }
    cout << endl;
}

0

我不知道这是否有帮助,但这是我在作业中的做法。唯一可能出问题的情况是当字符串开头有空格时,例如 " wor ds "。在这种情况下,它会把它改为 " wor ds"。

void ShortenSpace(string &usrStr){
   char cha1;
   char cha2;
   for (int i = 0; i < usrStr.size() - 1; ++i) {
      cha1 = usrStr.at(i);
      cha2 = usrStr.at(i + 1);
      
      if ((cha1 == ' ') && (cha2 == ' ')) {
         usrStr.erase(usrStr.begin() + 1 + i);
         --i;//edit: was ++i instead of --i, made code not work properly
      }
   }
}

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