【问题标题】:Can a char * or char ** masquerade as a FILE *?char * 或 char ** 可以伪装成 FILE * 吗?
【发布时间】:2016-04-25 14:06:27
【问题描述】:

在 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 等标准文件函数一起使用。

【问题讨论】:

  • 没有内置任何东西。你的方法对我来说看起来不错。如果您使用 C++,您可以使用类或重载函数,但 C 没有类似的东西。
  • 您可以通过创建带有可变参数的函数来重载。 耸耸肩
  • 你能写一个把文件读入char*列表的函数吗?比如 read_from_chars(get_lines(file));
  • 您当前的解决方案实际上没有任何重复工作。通用代码在一个通用函数handle_line中,read_from_charsread_from_input这两个函数做了两个不同的任务(其实就是你需要支持的两个任务)。这将是该问题的正常解决方案。更复杂的方法是处理函数在需要一行时调用回调来读取一行
  • @user3125280 这不是一个好的解决方案,因为将整个文件读入内存效率很低。

标签: c file file-io io


【解决方案1】:

您可以使用包含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[...]) 来修复泄漏?这将使设计保持一致——调用者总是需要释放。
  • 一个有趣的想法,但它只适用于理解并集的函数。
  • 这就是你在 C 中做多态性的方式。很有趣。
  • @Schwern 对,这是一个抽象层。
  • file_or_array 应该是一个以 arrayfile 作为联合成员的结构。
【解决方案2】:

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

我建议的解决方案是进行函数重载。

提供所有可能的参数:

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 没有那样的重载。
  • 这可行,但它只适用于以这种方式编写的一个特定函数。
  • @MillieSmith 我有点忘了,现在修好了。
  • 这与 OP 已经在做的事情非常相似。他要求更通用的东西,所以他不必复制或特殊情况下他的所有代码。
【解决方案3】:

如果您仅需要此功能进行调试,请将fopen_strings(char *list[]) 函数写入:

  • 创建一个临时文件
  • 使用fopen 模式"r+" 打开它
  • 将所有字符串写入其中
  • 删除文件(FILE* 仍然可以对其进行操作,直到它在程序结束时显式或隐式关闭。在某些阻止删除打开文件的操作系统上,您可能需要跳过此步骤。
  • rewind
  • 返回流并让您的程序像使用常规文件一样使用它。

【讨论】:

    【解决方案4】:

    这是一个使用fcookieopen [IIRC,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;
    }
    

    【讨论】:

    • 由于您有一个 list 字符串,我不确定fmemopen 是否适用。这就是我编码fcookieopen 的原因。我不得不为自己的东西做这件事,fmemopenopen_memstream 似乎总是很短,这取决于--YMMV
    • 好吧,如果您可以将代码从char **list 切换到char *bigbuf = "line1\nline2\n...\n"fmemopen 应该没问题。另外,我发现,有时将字符串列表转储到临时文件 [不可见] 更容易,用fopen(tmp,"r"); unlink(tmp) 打开它并正常处理。再一次,任何最适合你的。
    【解决方案5】:

    给这只特定的猫换皮的方法不止一种,但一般来说,解决这个问题的方法是将公共接口的实现隐藏在允许您注入单独“实现”的间接后面。

    (您的问题的这种化身也与确保代码版本之间的 ABI 兼容性的一些不同问题密切相关。)

    要在 C 中解决此问题,您可以使用类似于 C++ 中的带有继承的 pimpl(受保护而不是私有 d 指针,具有覆盖的受保护构造函数):

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

    让我们勾勒出示例头文件,让您了解这些函数是如何组合在一起的。让我们从胆量开始,d-pointer/p-impl 对象的定义(注意:我省略了一些样板文件,如标头保护):

    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 正确。你是对的:它作为一个简单的通用技术演示最有趣,你可能会发现它有助于使代码的其他部分更具可扩展性和/或可测试性,以及向前和/或向后兼容(特别是 ABI 兼容) .
    【解决方案6】:

    处理此问题的最强大方法之一是通过流。我用它们来隐藏文件/字符串/串行端口等

    我推出了自己的流库,主要用于嵌入式系统

    总体思路是:-

    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 上的回调。然后对于字符串,你有类似但处理字符串的东西

    【讨论】:

    • 感谢您的想法,但我宁愿使用标准的流处理功能。这样我就可以将测试数据传递给任何需要文件指针的东西。
    【解决方案7】:

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

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

    【讨论】:

    • 不幸的是,OP的第一个函数的输入参数是char *lines[]
    • fmemopenfopencookie 支持,它允许自定义(例如,您提供它们)读/写/搜索/关闭功能。我看不出你为什么不能写它们来阅读char *lines[]而不是char data[]
    • @RSahu 签名不是一成不变的。
    • fmemopen 在 BSD 操作系统上不存在(我使用的是 OS X)but I found an implementation。谢谢!这个答案让我可以完全灵活地将字符串传递给任何接受 FILE * 的东西。
    • @Schwern:很高兴你找到了那个包,感谢你提到它,因为它也可能在我的 Mac 上派上用场!
    猜你喜欢
    • 1970-01-01
    • 2013-02-11
    • 2018-05-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-25
    • 2020-01-31
    • 2020-12-31
    相关资源
    最近更新 更多