2016-01-19 20 views
6

In C, spesso desidero gestire i dati letti da un file e i dati letti da una serie di stringhe allo stesso modo. Di solito la lettura da un file è per la produzione e dalle stringhe è per testare. Finisco scrivendo un sacco di codice come questo:Un mascheramento char * o char ** come FILE *?

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

Si tratta di una duplicazione degli sforzi.

C'è un modo per rendere un array di stringhe mascherato come un puntatore FILE *? O vice versa? O c'è un modello migliore per affrontare questo problema?

per i punti bonus: la soluzione dovrebbe rendere char * o char ** utilizzabile con le funzioni di file standard come fgets e getline.

+0

Non c'è niente di integrato. Il tuo metodo mi sembra buono. Se stavi usando C++ potresti usare classi o funzioni sovraccariche, ma C non ha nulla di simile. – Barmar

+0

È possibile sovraccaricare creando funzioni con argomenti variadici. * Spallucce *. –

+0

puoi semplicemente scrivere una funzione che legge un file in un elenco di caratteri *? come read_from_chars (get_lines (file)); – user3125280

risposta

5

C'è una funzione non standard fmemopen che consente di aprire un carattere [] per la lettura o la scrittura. È disponibile nella maggior parte delle versioni di GNU libc, penso, e nella maggior parte delle versioni di Linux.

(Questo ti consente di leggere o scrivere ad una singola stringa, non è l'array di stringhe hai chiesto.)

+0

Sfortunatamente, l'argomento di input per la prima funzione dell'OP è 'char * lines []'. –

+0

'fmemopen' è supportato da' fopencookie' che consente di personalizzare (es. Fornirle) funzioni di lettura/scrittura/ricerca/chiusura. Non vedo molte ragioni per cui non puoi scriverle per leggere 'char * lines []' invece di 'char data []'. – inetknght

+1

@RSahu Le firme non sono scolpite nella pietra. – Schwern

6

Si potrebbe utilizzare un'unione discriminato che contiene un FILE* e un puntatore alla matrice, quindi scrivere una funzione get_next che fa la cosa giusta con esso.

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

La chiamata a strdup() è necessario per fare questo lavoro in modo coerente. Dal momento che getline() restituisce una stringa appena allocata, che il chiamante deve liberare, fa anche la stessa cosa quando restituisce una stringa dall'array. Quindi il chiamante può tranquillamente liberarlo in entrambi i casi.

+0

Perché non correggere la perdita avendo 'get_next()' return 'strdup (foa.array.lines [...])' nel caso dell'array? Ciò renderebbe il design coerente - il chiamante ha sempre bisogno di liberare. – user4815162342

+0

@ user4815162342 Buona idea, aggiornato il codice. – Barmar

+0

Un'idea interessante, ma funziona solo per funzioni che comprendono l'unione. – Schwern

2

Uno dei modi più potenti per gestire questo è tramite flussi. Li uso per nascondere file/stringa/porte seriali ecc

ho arrotolato la mia libreria flusso che uso principalmente su sistemi embedded

l'idea generale è: -

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

allora fate tutta una serie di funzioni

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); 

ecc

che utilizzano tali richiami delle funzioni di base.

il contesto nella struttura del flusso che si utilizza per puntare a una struttura per seriale, stringa, file o qualsiasi altra cosa si desideri. Quindi hai cose come file_create_stream(stream_t* stream, char* filename) che popoleranno i callback su stream con le funzioni relative ai file. Quindi per le stringhe hai qualcosa di simile ma gestisci le stringhe

+0

Grazie per l'idea, ma preferisco utilizzare le funzioni di elaborazione dello stream standard. In questo modo posso passare i dati di test a qualsiasi cosa che prende un puntatore di file. – Schwern

1

c'è un modello migliore per affrontare questo problema?

La mia soluzione proposta è di sovraccaricare le funzioni.

fornito tutti i possibili parametri:

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

Come utilizzare

FILE *f; 
char *l[100]; 

.. 

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

Si potrebbe incorporare nei dati:

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

Come usare

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); 
+0

C non ha sovraccarico del genere. –

+0

Funzionerebbe, ma funziona solo per una particolare funzione scritta in questo modo. – Schwern

+0

@MillieSmith Ho dimenticato, riparato ora. –

1

C'è più di un modo per skinare questo particolare gatto, ma in generale la soluzione a questo è nascondere l'implementazione dell'interfaccia pubblica dietro ad un riferimento indiretto che consente di iniettare "implementazioni" separate.

(Questa incarnazione del problema è anche strettamente legato al problema un po 'diverso di garantire la compatibilità ABI tra le versioni di codice.)

per risolvere questo in C è possibile farlo simile al Pimpl con-ereditarietà in C++ (protetto invece di d-pointer privato, con costruttori protetti sottoposti a override):

Si crea un oggetto opaco 'lettore'/'stream' (puntatore a forward struct dichiarato/typedef in C) e funzioni di costruzione opportunamente denominate da istanziare l'oggetto opaco che inietta l'implementazione desiderata.

Analizziamo i file di intestazione di esempio per darti un'idea di come le funzioni combacino. Cominciamo con il coraggio, la definizione degli oggetti/p-impl d-pointer (NB: Sto omettendo alcune boilerplate come guardie di intestazione):

lettore-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); 

stringa-reader.h

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

Questo è un modello generale per fare Pimpl/d-pointer con l'ereditarietà in C, in modo da poter astratto il coraggio di implementazione dietro un'interfaccia pubblica a cui si accede attraverso puntatori opachi. Questo meccanismo è generalmente utile per garantire la compatibilità API e ABI tra varie implementazioni dell'interfaccia pubblica e per implementare un modello di ereditarietà semplice.

+0

Grazie. Lo svantaggio è, come la maggior parte delle altre risposte, questo mi lascia con il mio sistema IO privato. Posso solo usarlo con funzioni che sono a conoscenza di questo sistema. – Schwern

+0

@Schwern corretto. Hai ragione: è per lo più interessante come una semplice dimostrazione della tecnica generale che potresti trovare utile per rendere le altre parti del tuo codice più estensibili e/o testabili, nonché avanti e/o retrocompatibili (in particolare ABI compatibile) . – user268396

1

Ecco un'implementazione utilizzando fcookieopen [IIRC, BSD ha qualcosa di simile]:

// 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; 
} 
+0

Grazie. [Qualcuno lo ha già fatto e lo mette su Github] (https://github.com/lemurs/fmemopen). – Schwern

+0

Dato che hai una _list_ di stringhe, non sono sicuro che 'fmemopen' funzionerà per quello. Ecco perché ho codificato 'fcookieopen'. Ho dovuto farlo per le mie cose e 'fmemopen' o' open_memstream' sembra sempre venire fuori a breve, a seconda di - YMMV –

+0

Il mio codice può essere facilmente adattato a 'char *'. – Schwern

1

Se avete bisogno di questa funzionalità solo per il debug, scrivere una funzione fopen_strings(char *list[]) a:

  • creare un file temporaneo
  • aprire quello con fopen con la modalità "r+"
  • scrivere tutto stringhe in esso
  • elimina il file (il FILE * può ancora operare su di esso, fino a quando non viene chiuso in modo esplicito o implicito alla fine del programma. Potrebbe essere necessario saltare questo passaggio su alcuni sistemi operativi che impediscono la cancellazione di file aperti.
  • rewind il flusso
  • restituisce lo stream e consente al programma di utilizzarlo come se fosse un file normale.