一个char*或char**可以冒充成FILE*吗?

6

在C语言中,我经常希望以相同的方式处理从文件读取的数据和从字符串数组读取的数据。通常,从文件中读取是为了生产用途,而从字符串中读取是为了测试用途。我最终编写了很多像这样的代码:

void handle_line(char *line, Things *things) {
    ...
}

Things *read_from_chars(char *lines[]) {
    Things *things = Things_new();

    for (int i = 0; lines[i] != NULL; i++) {
        handle_line(lines[i], things);
    }

    return things;
}

Things *read_from_input(FILE *input) {
    char *line = NULL;
    size_t linelen = 0;

    Things *things = Things_new();

    while (getline(&line, &linelen, input) > 0) {
        handle_line(line, things);
    }

    return things;
}

这是一种重复劳动。

有没有办法让字符串数组伪装成FILE *指针?反之亦然?或者有没有更好的模式来处理这个问题?

额外加分:解决方案应该使char *char **可用于标准文件函数,如fgetsgetline


你可以通过创建带有可变参数的函数来进行重载。耸肩 - Millie Smith
1
你当前的解决方案中并没有真正的工作重复。公共代码在一个公共函数handle_line中,而两个函数read_from_charsread_from_input执行两个不同的任务(实际上是你需要支持的两个任务)。这将是问题的正常解决方案。更复杂的方法是处理函数在需要一行时调用回调来读取一行。 - M.M
@user3125280 这不是一个好的解决方案,因为将整个文件读入内存是低效的。 - Schwern
@M.M 我必须编写 read_from_charsread_from_input 是重复劳动。它们在概念上都做相同的事情:初始化输出变量,为每行调用回调函数。 - Schwern
@Schwern 嗯?你接受的解决方案无论如何都会将整个数据存储在内存中,就像你发布的 read_from_chars 代码一样。而且正如你所写的,从文件读取通常是用于测试,效率并不重要。现在你正在依赖特定于平台的函数和 GitHub 库,却没有真正的理由? - user3125280
显示剩余3条评论
7个回答

6
你可以使用一个带有FILE*和指向数组的指针的联合体,然后编写一个get_next函数来处理它。
typedef struct {
    enum { is_file, is_array } type;
    union {
        FILE *file;
        struct {
            int index;
            int size;
            char **lines;
        } array;
    } data;
} file_or_array;

char *get_next(file_or_array foa) {
    if (foa.type == is_file) {
        char *line = NULL;
        size_t linelen = 0;
        getline(&line, &linelen, foa.data.file);
        return line;
    } else {
        if (foa.data.array.index < foa.data.array.size) {
            return strdup(foa.data.array.lines[foa.data.array.index++]);
        } else {
            return NULL;
        }
    }
}

调用strdup()是使其正常工作所必需的。由于getline()返回一个新分配的字符串,需要在调用方释放它,在从数组中返回字符串时也执行同样的操作。这样,调用方就可以在两种情况下安全地释放它。


为什么不通过让get_next()在数组情况下返回strdup(foa.array.lines[...])来修复泄漏呢?这将使设计保持一致 - 调用者总是需要释放。 - user4815162342
一个有趣的想法,但它仅适用于理解 union 的函数。 - Schwern
这就是在C语言中实现多态性的方法。有趣。 - Khaled.K
@Schwern 对,这是一个抽象层。 - Barmar
file_or_array 应该是一个结构体,其中包含 arrayfile 作为联合成员。 - a3f
@a3f 感谢您的纠正,我已经更新了代码。 - Barmar

5

有一个非标准函数fmemopen,它允许您打开一个char[]以进行读取或写入。我想它在大多数GNU libc版本和Linux的大多数版本中都可用。

(这使你可以从单个字符串中读取或写入,而不是你所询问的字符串数组。)


很遗憾,OP的第一个函数的输入参数是char *lines[] - R Sahu
fmemopen是由fopencookie支持的,它允许自定义(例如,您提供它们)读取/写入/定位/关闭函数。我不太明白为什么不能编写它们来读取char *lines[]而不是char data[] - inetknght
1
@RSahu 签名并不是铁板钉钉的。 - Schwern
fmemopen在BSD操作系统中不存在(我正在使用OS X),但是我找到了一个实现(https://github.com/lemurs/fmemopen)。谢谢!这个答案给了我完全的灵活性,可以将字符串传递到任何接受`FILE *`的函数中。 - Schwern
@Schwern:很高兴你找到了那个软件包,并感谢你提到它,因为它对我在我的Mac上也可能有用! - Steve Summit

2

处理这个问题最强大的方式之一是使用流。我用它们来隐藏文件/字符串/串行端口等。

我已经开发了自己的流库,主要用于嵌入式系统。

总体思路是:

typedef struct stream_s stream_t;

struct stream_s
{
    BOOL (*write_n)(stream_t* stream,  char* s, WORD n);
    BOOL (*write_byte)(stream_t* stream,  BYTE b);
    BOOL (*can_write)(stream_t* stream);
    BOOL (*can_read)(stream_t* stream);
    BYTE (*read_byte)(stream_t* stream);
    void* context;
};

然后您可以创建一系列的函数。
BOOL stream_create(stream_t* stream);
BOOL stream_write_n(stream_t* stream, char* s, WORD n);
BOOL stream_can_read(stream_t* stream);
BYTE stream_read_byte(stream_t* stream);

使用这些基本函数回调的程序等等。

在流结构中,您可以使用上下文指向串行、字符串、文件或任何您想要的结构体。然后,您有像file_create_stream(stream_t* stream, char* filename)这样的东西,它会将与文件相关的函数填充到stream上的回调中。然后对于字符串,您也有类似的处理方式。


谢谢您提供的想法,但我宁愿使用标准流处理函数。这样,我可以将测试数据传递给任何接受文件指针的东西。 - Schwern

1

有没有更好的模式来处理这个问题?

我的建议解决方案是使用函数重载。

提供所有可能的参数:

Things* readThings(FILE *f, char *l[])
{
    char *line = NULL;
    size_t linelen = 0;
    Things *things = Things_new();

    if (f)
    {
        while(getline(&line, &linelen, input) > 0)
            handle_line(line, things);
    }
    else
    {
        for(int i = 0; lines[i] != NULL; i++)
            handle_line(lines[i], things);
    }

    return things;
}

Things* readThingsChar(char *l[]){ return readThings(0, l); }

Things* readThingsFile(FILE *f){ return readThings(f, 0); }

如何使用
FILE *f;
char *l[100];

..

Things *a = readThings(f,0); // or readThingsFile(f)
Things *b = readThings(0,l); // or readThingsChar(l)

你可以将它嵌入数据中:

Things* readThings(char *l[])
{
    char *line = NULL;
    size_t linelen = 0;
    Things *things = Things_new();
    FILE *f = NULL;

    if (l[0][0]==UNIQUE_IDENTIFIER)
    {
        f = fopen(l[0]+1);

        while(getline(&line, &linelen, input) > 0)
            handle_line(line, things);

        fclose(f);
    }
    else
    {
        for(int i = 0; lines[i] != NULL; i++)
            handle_line(lines[i], things);
    }

    return things;
}

如何使用
char *f[1] = { "_file.txt" };
char *l[100] = { "first line", .. "last line" };

f[0][0] = UNIQUE_IDENTIFIER;

Things *a = readThings(f);
Things *b = readThings(l);

C语言没有像那样的重载。 - Millie Smith
那样做是可行的,但它只适用于以那种方式编写的一个特定函数。 - Schwern
@MillieSmith 我有点忘了,现在已经修复了。 - Khaled.K
这与 OP 已经在做的非常相似。他正在寻求更通用的东西,以便不必复制或特殊处理他的所有代码。 - chqrlie

1

有多种方法可以解决这个问题,但通常的解决方案是将公共接口的实现隐藏在一个间接层之后,这样你就可以注入单独的“实现”。

(你的问题的这种化身也与确保代码版本之间的ABI兼容性有密切关系。)

要在C中解决这个问题,可以像在C++中使用继承的pimpl一样做(使用受保护的而不是私有的d指针,并重写受保护的构造函数):

您创建一个不透明的“读取器”/“流”对象(具有C中typedef的前向声明结构的指针),并适当命名的构造函数来实例化注入所需的实现的不透明对象。

让我们草拟一些示例头文件,以便让您了解如何将函数组合在一起。让我们从实质开始,即定义d指针/p-impl对象(N.B .:我省略了一些类似头文件守卫的样板):

reader-private.h:

/* probably should be in its proper C file, but here for clarification */
struct FileReaderPrivateData {
   FILE * fp;
};

/* probably should be in its proper C file, but here for clarification */
struct StringReaderPrivateData {
   size_t nlines;
   size_t cursor;
   char ** lines;
};

/* in C we don't have inheritance, but we can 'fix' it using callbacks */
struct ReaderPrivate {
   int (* close)(void* pData); /* impl callback */
   ssize_t (* readLine)(void* pData, char** into); /* impl callback */
   /* impl-specific data object, callbacks can type cast safely */
   void * data;
};

/* works like a plain p-impl/d-pointer, delegates to the callbacks */
struct Reader {
    struct ReaderPrivate * dPtr;
}

reader.h:

typedef struct Reader* Reader;
/* N.B.: buf would be a pointer to set to a newly allocated line buffer. */
ssize_t readLine(Reader r, char ** buf); 
int close(Reader r);

file-reader.h

#include "reader.h"
Reader createFileReader(FILE * fp);
Reader createFileReader(const char* path);

string-reader.h

#include "reader.h"
Reader createStringReader(const char**, size_t nlines);

这是在C语言中使用pimpl/d-pointer实现继承的一般模式,因此您可以通过不透明指针访问公共接口背后的实现细节。这种机制通常用于保证公共接口的API和ABI兼容性,并实现简单的继承模式。

谢谢。缺点是,与其他答案大多数类似,这使我拥有了自己的私人IO系统。我只能与知道这个系统的函数一起使用它。 - Schwern
@Schwern 正确。你是对的:它主要是一个简单易懂的演示通用技术的例子,你可能会发现它对于使你代码的其他部分更具可扩展性和/或可测试性,以及向前和/或向后兼容(特别是ABI兼容)非常有用。 - user268396

1
这里有一个使用fcookieopen实现的例子 [如果我没记错,BSD也有类似的东西]:
// control for string list
struct cookie {
    char **cook_list;                       // list of strings
    int cook_maxcount;                      // maximum number of strings

    int cook_curidx;                        // current index into cook_list
    int cook_curoff;                        // current offset within item
};

int cookie_close(void *vp);
ssize_t cookie_read(void *vp,char *buf,size_t size);

cookie_io_functions_t cook_funcs = {
    .read = cookie_open;
    .close = cookie_close;
};

// cookie_open -- open stream
FILE *
cookie_open(char **strlist,int count,const char *mode)
// strlist -- list of strings
// count -- number of elements in strlist
// mode -- file open mode
{
    cookie *cook;
    FILE *stream;

    cook = calloc(1,sizeof(cookie));
    cook->cook_list = strlist;
    cook->cook_maxcount = count;

    stream = fopencookie(cook,mode,&cook_funcs);

    return stream;
}

// cookie_close -- close stream
int
cookie_close(void *vp)
{

    free(vp);

    return 0;
}

// cookie_read -- read stream
ssize_t
cookie_read(void *vp,char *buf,size_t size)
{
    cookie *cook = vp;
    char *base;
    ssize_t totcnt;

    totcnt = 0;

    while (size > 0) {
        // bug out if all strings exhausted
        if (cook->cook_curidx >= cook->cook_maxcount)
            break;

        base = cook->cook_list[cook->cook_curidx];
        base += cook->cook_curoff;

        // if at end of current string, start on the next one
        if (*base == 0) {
            cook->cook_curidx += 1;
            cook->cook_curoff = 0;
            continue;
        }

        // store character and bump buffer and count
        *buf++ = *base;
        size -= 1;
        totcnt += 1;

        cook->cook_curoff += 1;
    }

    return totcnt;
}

由于您有一个字符串列表,我不确定fmemopen是否适用于它。这就是为什么我编写了fcookieopen的原因。我不得不为自己的东西做到这一点,而fmemopenopen_memstream似乎总是不够用,具体情况因人而异。 - Craig Estey
如果你可以将代码从char **list切换到char *bigbuf = "line1\nline2\n...\n",那么fmemopen应该没问题。另外,我发现有时候把字符串列表转储到临时文件中[不可见]更容易处理,然后用fopen(tmp,"r"); unlink(tmp)打开它并正常处理。总之,选择最适合你的方法即可。 - Craig Estey

1
如果您只需要此功能进行调试,请编写一个fopen_strings(char *list[])函数来实现以下操作:
  • 创建一个临时文件
  • 使用fopen"r+"模式打开该文件
  • 将所有字符串写入文件中
  • 删除该文件(FILE*仍然可以对其进行操作,直到显式或隐式地在程序结束时关闭。在某些阻止打开文件的操作系统上,您可能需要跳过此步骤)
  • rewind
  • 返回流并让程序像使用常规文件一样使用它。

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