【问题标题】:Remove extra white spaces in C++删除 C++ 中的多余空格
【发布时间】:2016-05-19 23:48:31
【问题描述】:

我尝试编写一个脚本来删除多余的空格,但我没能完成它。

基本上我想将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;
}

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

我该如何解决这个问题?

【问题讨论】:

  • 它是 c++,这就是我编译运行的方式clear; rm -f test2.exe; g++ -o test2.exe test2.cpp; ./test2.exe;
  • 不要在 C++ 中编写 C 风格的代码!使用语言功能。
  • @Olaf 在 C++ 中编写 C 风格的代码是有正当理由的。大多数情况下,一个更惯用和更明显的 C++ 替代方案可能会更好,但这取决于具体情况。
  • @Deduplicator:语义仍然可能不同。即使语法相同。
  • 嗯...0x255...一个非常好奇的常数。

标签: c++ string algorithm


【解决方案1】:

已经有很多不错的解决方案。我建议您使用基于专用&lt;algorithm&gt; 的替代方案,以避免连续重复: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; 
}

这里是 live demo请注意,我从 c 样式字符串更改为更安全、更强大的 C++ 字符串。

编辑:如果您的代码需要保留 c 风格的字符串,您可以使用几乎相同的代码,但使用指针而不是迭代器。这就是 C++ 的魔力。这是another live demo

【讨论】:

  • 这个不错。虽然它应该有原件的签名,可能。
  • @Deduplicator 是的,我编辑建议切换到std::string
  • @José 我的函数根据 OP 的要求删除了多余的空格。我在应该删除起始空格或结束空格的问题中找不到任何证据。如果这是一个要求,您只需将 input.begin() 替换为 find_if() 并在返回之前添加条件擦除。
  • @Damian 算法库的好处是许多算法也可以使用指针而不是迭代器。这里online demo 使用相同的算法,但保持你喜欢的 c 风格字符串 ;-)
  • 顺便说一句:您可能希望将 cstring-solution 添加到您的答案中。
【解决方案2】:

这是一个简单的非 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 分配的内存;如果是这样,您可能会遇到分段错误。
【解决方案3】:

由于您使用 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*...
  • 您可以通过构造函数来回转换 - 从char*string,然后通过c_str()strcpy 转换回来。为 CPU 做了大量不必要的工作,但对您来说却少了些麻烦。
  • @anatolyg:如果它在正确的时间在正确的地方完成,那么优化器最多可能会做一些额外的工作。
  • 是的,我也同意,字符串是最好的,但是所有 script 都是使用 char* 编写的(2000 行)...并且此脚本必须在 centos 4, 5.1 上运行,@ 987654339@, unix based systems ...等等,最好用最简单的函数,不要得到segmentation fault ...
  • @Damian:使用更简单的函数并不能保证避免错误。您必须自己编写的代码越多,而不是使用库工具,出现错误的机会就越大。显然你必须了解你使用的库函数,而 C++ 比 C 还要多。
【解决方案4】:

有很多方法可以做到这一点(例如,使用正则表达式),但你可以做到这一点的一种方法是使用 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* ...,可以吗?跨度>
  • 不确定您是否打算将评论发给我,但请参阅string::c_str
  • 如果字符串以空格结尾,则会在字符串末尾留下一个额外的空格。不确定是否需要考虑 OP 的不断变化的需求...
  • @jaggedSpire 好点。我必须说我想到了这一点,并决定(也许是一厢情愿)它符合问题要求。如果不行,可以在copy_if申请后一行解决。
  • 是的,我也同意,字符串是最好的,但是所有 script 都是使用 char* 编写的(2000 行)...并且此脚本必须在 centos 4, 5.1 上运行,@ 987654334@, unix based systems ...等等,最好用最简单的函数,不要得到segmentation fault ...
【解决方案5】:

您可以使用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;
}

【讨论】:

    【解决方案6】:

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

    #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*...
    • 很好,但是如果您多次执行此 bloc(在循环或函数中或在多个线程中),则不会重置静态 prev_is_space。为了安全地工作,您需要捕获一个本地布尔值,您可以在需要时重置它。
    • @Christophe,我明白了。谢谢。
    • 是的,我也同意,字符串是最好的,但所有 script 都是使用 char* 编写的(2000 行)...并且此脚本必须在 centos 4, 5.1 上运行,@ 987654333@, unix based systems ...等等,最好用最简单的函数,不要得到segmentation fault ...
    【解决方案7】:

    我有种好的 ol'scanf 会做的沉沦感(其实这就是相当于 Anatoly 的 C++ 方案的 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 处存在足够的内存;如果不是这样,由于缓冲和溢出处理,代码会变得更加复杂。

    【讨论】:

    • sscanfANSI C (plain C)也可以使用的函数吗?
    • @Damian 哦,是的。它是 C 标准的一部分(同时也是类 Unix 系统的 POSIX 标准的一部分)。
    • 谢谢,你知道,C 是一门非常古老的编程语言,它一直让我头疼……看看这个:stackoverflow.com/questions/35873677/…
    【解决方案8】:

    由于您正在编写 c 风格,这里有一种方法可以做您想做的事。 请注意,您可以删除作为换行符的 '\r''\n'(当然,是否考虑这些空格取决于您)。

    这个函数应该和其他任何替代函数一样快或更快,即使使用 std::strings 调用它也不会发生内存分配(我已经重载了它)。

    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::objects 的 C++ 函数基本上是对编写良好的 extern "C" 函数的包装。所以不要害怕在 C++ 应用程序中使用 C 函数,如果它们写得很好并且你可以重载它们以支持 std::strings 等。

    例如,在 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));
        }
    

    【讨论】:

    • hmm,很有趣,所以基本上你的int remove_whitesaces(char *p) 函数,不必带两个参数,只需借助指针的力量“即时”修改它,对吧?
    • 是的,因为输出长度总是等于或小于输入长度,所以不需要创建另一个对象。我还重载了它以支持 std::strings (同样没有发生内存分配)。我以为你会接受我的回答,因为它实际上是可定制的(并且不接受几乎每个人都认为是空格的制表符 ('\t')。如果需要,它可以忽略换行符。
    【解决方案9】:

    这是一个不使用指针的冗长(但简单)的解决方案。 它可以进一步优化,但它确实有效。

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

    【讨论】:

      【解决方案10】:

      我不知道这是否有帮助,但这就是我在家庭作业中的做法。唯一可能会中断的情况是字符串EX“word ds”开头有空格时,它会将其更改为“word 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
            }
         }
      }
      

      【讨论】:

        【解决方案11】:

        我在这里遇到了一个稍微不同的问题。由于我不知道该放在哪里,而且我发现了问题所在,所以我在这里分享它。请不要生我的气。 我有一些字符串会在它们的末端打印额外的空格,同时在调试中显示没有空格。在 Windows 中形成的字符串调用像 VerQueryValue(),除了其他东西输出字符串长度,例如以下行中的 iProductNameLen 将结果转换为名为 strProductName 的字符串:

            strProductName = string((LPCSTR)pvProductName, iProductNameLen)
        

        然后生成一个末尾带有 \0 字节的字符串,它在调试器中不容易显示,但在屏幕上打印为空格。我将把这个问题的解决方案留作练习,因为一旦你意识到这一点,它一点也不难。

        【讨论】:

          【解决方案12】:

          无需使用任何内置函数即可删除多余空格的简单程序。

          #include<iostream>
          #include<string.h>
          #include<stdio.h>
          using namespace std;
          
          int main()
          {
            char str[1200];
            int i,n,j,k, pos = 0 ;
            cout<<"Enter string:\n";
            gets(str);
            n = strlen(str);
            for(i =0;i<=n;i++)
            {
                if(str[i] == ' ')
                {
                    for(j= i+1;j<=n;j++)
                    {
                            if(str[j] != ' ')
                            {
                                pos = j;
                                break;
                            }
                     }
                   if(pos != 0 && str[pos] != ' ')
                   {
                      for(k =i+1;k< pos;k++)
                       {   if(str[pos] == ' ')
                               break;
                           else{
                              str[k] = str[pos];
                              str[pos] = ' ';
                              pos++;
                           }
          
                       }
                   }
          
                }
            }
            puts(str); 
          }
          

          【讨论】:

          • 一般来说,如果答案包含对代码的用途的解释,以及为什么在不介绍其他人的情况下解决问题的原因,答案会更有帮助。