在Linux中使用stdio读取和排序管道数据

3

正如标题所说,我需要编写一个小程序来读取标准输入的数据,对其进行排序并将其发送到标准输出。该程序应该接受1个参数,告诉它一条记录的长度(以字节为单位)。以下是我的测试方式:

printf 'D\x00C\x00\x00B\x00A' | ./binsort 2 | od -c

以上应该输出类似于:
0000000  \0   A  \0   B   C  \0   D  \0
0000010

这是我目前有的代码(binsort.c):

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

using namespace std;


void print_usage()
{
        printf("%s\n", "Usage: ");
}


int compare (const void * a, const void * b) // the compare function for qsort... might need some work
{
  return ( *(int*)a - *(int*)b );
}


int main(int argc, char *argv[])
{
        if (argc != 2 || stdin == NULL) // check for argument and piped input
        {
                print_usage();
                exit(EXIT_FAILURE);
        }

        int entry_size = atoi(argv[1]);

        if (entry_size <= 0 || entry_size >= INT_MAX) // check for valid range of entry size
        {
                print_usage();
                exit(EXIT_FAILURE);
        }

        char *input = new char[entry_size]; // to hold single record

        while (fgets(input, entry_size, stdin) != NULL)
        {
                printf("%s", input); // output single record we just read
        }

        exit(EXIT_SUCCESS);
}

然后使用 g++ binsort.c -o binsort 进行编译。

上面的代码可以编译,但它没有输出 printf 发送给它的数据。它应该以 2 字节为一组输出,就像 D\0 C\0 \0B \0A... 但是它没有。

我在考虑使用 qsort 在一个由 malloc/realloc 分配的数组上进行排序。然而,我从未有过这方面的经验,在这个小实用程序上已经苦苦挣扎了几天。有人可以帮忙吗?

P.S. 有人问这是否是作业任务... 不是 - 我公司的开发人员想使用它来调试其项目的输出。


不是作业 - 我公司的开发人员想使用它来调试他们项目的输出。 - DV.
6个回答

5
不要使用scanf()printf(),它们应该用于文本数据。由于你正在处理二进制数据,因此你需要使用更低级别的fread()fwrite()函数。由于你不知道总共有多少数据,所以你必须使用动态数据结构(如可调整大小的数组)来存储输入。你不能在线处理数据--你必须先读取所有数据,然后对其进行排序,最后再写回。你的排序函数也是错误的--你只比较每个记录的前4个字节,如果记录大小不是4个字节,则这是错误的。请改用memcmp()

这应该可以解决问题:

char *input = NULL;
size_t input_size = 0;
int num_items = 0;
int entry_size;

int compare_func(const void *e1, const void *e2)
{
  return memcmp(e1, e2, entry_size);
}

int main(int argc, char **argv)
{
   // ...
  char *datum = malloc(entry_size);  // check for NULL
  input_size = 4096;
  input = malloc(input_size);  // check for NULL

  while(1)
  {
    if(fread(datum, 1, entry_size, stdin) < entry_size)
      break;
    size_t new_size = (num_items + 1) * entry_size;
    if(new_size > input_size)
    {
      input = realloc(input, input_size * 2);  // check for NULL
      input_size *= 2;
    }
    memcpy(input + num_items * entry_size, datum, entry_size);
    num_items++;
  }

  qsort(input, num_items, entry_size, compare_func);

  fwrite(input, entry_size, num_items, stdout);

  return 0;
}

不在realloc上加倍缓冲区大小可能会节省冗长且不必要的解释,但这让我感到不安。 - dmckee --- ex-moderator kitten
当调整缓冲区(即输入)大小时,通过加倍缓冲区大小,可以保证摊销O(n)的复制成本。 只在需要时进行扩展的最坏情况为O(n ^ 2)。 这里可能不是问题(因为只有缓冲区使用堆),但在其他情况下很重要。 - dmckee --- ex-moderator kitten
在上面的代码中,你可以编写类似以下内容: int temp = input_size ? input_size*2 : entry_size; input = realloc(input, temp); input_size = temp; 第一次看到时可能不太清晰。 - dmckee --- ex-moderator kitten
修正了;在编写时我想过,但我忘了。 - Adam Rosenfield

3

P.S. 有人问这是否是一项家庭作业...不是 - 我公司的开发人员想要使用它来调试他们项目的输出。

如果这不是家庭作业,并且在Linux下,为什么不使用已包含的"sort"程序呢?

例如:

% printf 'D\x00C\x00\x00B\x00A' | sort -z | od -c
0000000  \0   A  \0   B  \0   C  \0   D  \0
0000011

FSF/GNU sort 提供 -z 选项:

-z, --zero-terminated
        end lines with 0 byte, not newline

添加说明:

好的,既然你还想要自己的代码...

并且看到还没有其他人发布基于STL的方法。

请注意使用结构体FUNCTOR进行比较(通过stl::sort())。如果你愿意,你也可以使用ostream_iterator<string>(cout, "\n")来使事情更加易读。


#include <iostream>
#include <fstream>
#include <iomanip>
#include <string>
#include <vector>
#include <algorithm>
#include <iterator>

using namespace std;

   /* ifstream won't set EOF-state until we try to read past the end-of-file.*/
   /* (Makes for lots of grief trying to read in files...)                   */
inline bool
IsStreamStillGood( istream & theStream )
{
  return theStream && (theStream . peek() != EOF);
}

template<class TYPE> inline void DELETE( TYPE * x)
{
  delete x;
  x = (TYPE *) NULL;
}

struct FUNCTOR
{
  bool operator()(const string & x, const string & y) { return x < y; }
};


int
main(int argc, char **argv)
{
  istream *       stream;
  vector<string>  v;

  UASSERT( argc, >, 1 );

  const int recordSize = atoi( argv[1] );
  char      buffer [ recordSize + 1 ];

  UASSERT( recordSize, >, 0 );


  if ( argc > 2 )
    stream = new ifstream( argv[2] );
  else
    stream = & cin;


  while ( IsStreamStillGood( * stream ) )
  {
    stream-> read( buffer, recordSize );
    v.push_back( string( buffer, stream->gcount() ) );
  }

  UASSERT( v.back().size(), ==, size_t(recordSize) );


  FUNCTOR functor;
  sort( v.begin(), v.end(), functor );

  copy( v.begin(), v.end(), ostream_iterator<string>(cout) );


  if ( argc > 2 )
    DELETE(stream);
}

再次修改以添加:

关于在STL方法中使用字符串的用法问题:由于输入中可能有空字符('\0'),那么字符串会不会因此混淆?有人建议使用char [],但我怀疑同样的问题会出现。

如果我有 char c [10];,我可以自由地说c [0] ='\ 0';。 对于c [1]c [9]也是如此。 我可以有尽可能多的空字符在该数组中。(当然要满足数组的大小)

现在,在将c用作c字符串时,问题是它有多长。 它是1个字符长还是9个字符长? 通常,在C中,这是通过第一个NULL字符出现的位置来决定的。

所以像printf(%s)scanf(%s)strncat()strncpy()strncmp()等都不会真正喜欢在我们的二进制数据数组中嵌入NULL('\0')字符。

但是C ++ std :: string 单独跟踪长度。 至少看起来是这样,它允许使用:myString.append(10,'\0');

因此,通过使用stream->read(buffer,recordSize),我们读入一组字符(字节)。 我们真的不在乎它们是否是空的('\0')。 这很好。 只要给我recordSize个字节。

通过创建 v.push_back(string(buffer,stream->gcount())),我们推回一个包含stream->gcount()个字符(字节)的新字符串。 同样,我们不在乎它们是否是空字符('\0')。 我们只需要stream->gcount()个字节。

Sort使用函数对象,该对象使用operator<(const string&,const string&),该对象使用string :: compare(),该对象再次将使用字符串的长度,并且不关心实际包含在字符串中的数据。 空字符('\0')非常好用。

现在,如果我们尝试使用v.back()。c_str(),那么我们就没有长度了,所以NULL会让我们困惑。 但只要我们使用包含数据和长度的字符串对象(例如v.back()),我们就没问题了。

这就把我们带到输出。 再次,我们正在输出字符串,而不是myString.c_str(),因此会打印出字符串中的所有字符。 包括空字符('\0')。


答案:因为他们想要在排序方面拥有很大的灵活性。例如,他们会为qsort()编写自己的compare_func()函数。 - DV.
谢谢您的建议 :) 我可以使用这些命令来测试我的程序是否正常工作。 - DV.
关于您在STL方法中使用字符串的问题:由于输入中可能存在空字符('\0'),那么字符串会因此而混淆吗?有人建议使用char[],但我怀疑同样的问题也会出现。 - DV.
通过修改原始帖子进行了回答。(需要超过300个字符。) - Mr.Ree

2

fgets和fprintf处理字符串(在C中是以特殊字符\0结尾的char数组)。

对于二进制数据,请使用fread和fwrite。qsort很好用,但您的比较函数需要逐字节比较entry_size字节,而不仅仅是从void转换为int,这是不安全且可能不正确的。


2

以下是一些可能有用的提示(我假设这是一项作业,因此具有教育性结构):

  • 在累加数据时,您需要将数据存储在某个地方。由于您不知道要处理多少数据,因此需要进行动态分配。有很多选择:动态数组、链表、哈希表等等。选择一个适合您的(见下文)。

  • 一旦您获得了数据,您需要对其进行排序。或者,您可以在第一次存储时就进行排序。您是否知道一种可以管理它的数据结构?

  • 一旦它被排序,您只需将其输出即可。

很容易,不是吗?


关于评论:
  • 你有K&R吗?或者其他关于cc++的基础教材吗?你需要一本。并且你需要选择其中一种语言并坚持使用它。如果你已经习惯了在其他上下文中使用面向对象编程,请使用c++。如果你两种都不知道,就用c++

  • 如果你正在使用c++,STL容器将为你处理动态存储。最简单的方法是使用一个std::vector<char[2]>(使用命令行参数中的大小),并在其上调用<algorithm>排序。

  • 其他人指出你需要进行二进制IO。这很重要,因为你的输入和输出都不像文本IO那样结构化。


感谢建议。我尝试使用排序集合(set.h),但不知道如何让它接受来自fgets()的输入。此外,我猜测qsort可以更快地处理大量输入。 我最大的问题是弄清楚如何正确使用所有这些函数想要的char*,char**,&char等。 - DV.

2

printf将你的流中的\0解释为终止字符。

请改用read()write()

    int res;
    int count;
    while (1) {
            count = 0;
            while(count < entry_size) {
                    res = read(STDIN_FILENO, input + count, entry_size - count);
                    if (res <= 0)
                            exit(errno);
                    count += res;
            }
            count = 0;
            while(res) {
                    res = write(STDOUT_FILENO, input + count, entry_size - count);
                    if (res < 0)
                            exit(errno);
                    count += res;
            }
    }

1

感谢所有人提供的好建议!仅供记录,这是完成的binsort.c,它可以实现预期功能。

#include <stdio.h>
#include <stdlib.h>
#include <cstring>
#include <limits.h>

using namespace std;


int num_items = 0;
size_t input_size = 0;
int entry_size = 0;


void print_usage()
{
        printf("%s\n", "Usage: <binary data> | ./binsort [RECORD_LENGTH]");
        printf("%s\n", "For example: printf 'D\\x00C\\x00\\x00B\\x00A' | ./binsort 2 | od -c");
}


int compare (const void * a, const void * b)
{
        return memcmp(a, b, entry_size);
}


int main(int argc, char *argv[])
{
        if (argc != 2 || stdin == NULL)
        {
                print_usage();
                exit(EXIT_FAILURE);
        }

        entry_size = atoi(argv[1]);

        if (entry_size <= 0 || entry_size >= INT_MAX)
        {
                print_usage();
                exit(EXIT_FAILURE);
        }

        char *input = NULL;
        char *datum = (char*) malloc(entry_size);
        if (datum == NULL)
                exit(EXIT_FAILURE);

        while(1)
        {
                int read_size = fread(datum, 1, entry_size, stdin);

                if (read_size == 0)
                        break;

                if(read_size < entry_size)
                {
                        while(read_size < entry_size)
                        {
                                memcpy(datum, '\0', 1);
                                read_size++;
                        }
                        break;
                }

                size_t new_size = (num_items + 1) * entry_size;
                if(new_size > input_size)
                {
                        input = (char*) realloc(input, new_size);
                        if (input == NULL)
                                exit(EXIT_FAILURE);
                        input_size = new_size;
                }
                memcpy(input + num_items * entry_size, datum, entry_size);
                num_items++;
        }

        qsort(input, num_items, entry_size, compare);

        fwrite(input, entry_size, num_items, stdout);

        exit(EXIT_SUCCESS);
}

我相信这会起作用。但是你可能要看一下我上面的STL方法。虽然在内存使用方面不太高效,但也许更容易理解... - Mr.Ree
谢谢!是的,下次我会使用STL方法:D 在学习所有C语言内存管理技巧之前,它可以节省大量的工作。 - DV.
即使你学会了C语言的所有内存管理技巧,也能节省很多时间...;-) - Mr.Ree

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