C言語を使っているとC++のstring/vectorが使えないせいで
可変長の文字列を含んだファイルを読込むときは
非常に泥臭いことをしないといけない。。。か、もしくは
決めうちで列幅を固定してしまったりすることが多いと思います。
そんなわけでどんなに列幅があってもどんなに行数があっても
簡単に読込める関数を作りました。勉強を兼ねて。
作るところで、簡単にできるだろうと目論んでいましたが、
やってみると盛大にバグりました。
細かいミスがすごい出ました。しかも不具合の原因がすぐにわからない。
久々にC言語のキツさを思い知りました。
低レイヤーの言語って泥臭いけど楽しいですよね。
使い方
こんな感じです。
2次元配列的な構造を列幅、行数を意識せずに取得できるようにしたものです。
もちろん、ヘッダに分離させてソースコードを置いたらincludeとかしないとダメですが。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <assert.h> #define SAFE_RELEASE(p) if(p) { free(p); p=NULL; } // 極端に小さくしていますが、実用上は128とかを設定したいところ。 #define DEFAULT_SIZE 3 // ==== DECLARATION ==== typedef struct Tag_LineData LineData; typedef struct Tag_StringList StringList; StringList *getStringList(const char *fileName); void deallocateStringList(StringList *stringList); // ==== DEFINITION ==== typedef struct Tag_LineData { char *line; size_t length; /* size */ } LineData; typedef struct Tag_StringList { LineData *lineData; size_t length; /* rows */ } StringList; /** * 1行を読込み、読み切れなかったら領域拡張して続きを読む関数 * この関数もっと簡潔に書けないかな… */ static char *getLineDataRecursive(LineData *lineData, FILE *fp, int depth) { // エラー処理のためいったんtmpで拡張 char *tmp = (char *)realloc(lineData->line, DEFAULT_SIZE * depth * sizeof(char)); if (tmp == NULL) { perror("realloc"); SAFE_RELEASE(lineData->line); exit(1); } // 初回だけメモリクリア 再帰呼び出し時はクリアするとダメ。 if (depth == 1) { memset(tmp, 0, DEFAULT_SIZE); } lineData->line = tmp; // tmpを本来拡張したいアドレスに格納 char buf[DEFAULT_SIZE] = { 0 }; char *ret = fgets(buf, DEFAULT_SIZE - 1, fp); // 一時用領域に1行読込み(途中なら途中から読込み) if (ferror(fp)) { perror("read failed"); exit(1); // おそらく何もできることはないので終了 } strcat(lineData->line, buf); // 拡張領域に一時用領域の文字列を連結 // '\n'文字がある場合、 if (strchr(lineData->line, '\n') != NULL) { lineData->length = strlen(lineData->line); // '\n'文字を取り除く lineData->line[lineData->length - 1] = '\0'; return ret; // 改行があるので終わり } // 改行がない。つまり、DEFAULT SIZEでは必要な分の領域がなく、読み切れなかったということ。 #if _DEBUG printf("recursive call===\n"); #endif // 読切れなかった(=retに何かしら値がある)場合、再帰呼び出しで領域拡張する return ret != NULL ? getLineDataRecursive(lineData, fp, ++depth) : ret; } /** * 1行読込関数 */ static char *getLineData(LineData *lineData, FILE *fp) { return getLineDataRecursive(lineData, fp, 1); } /** * 最小限の基本領域確保関数 */ static StringList* allocateStringList(int lineCnt, FILE* fp) { StringList *stringList = (StringList *)malloc(lineCnt * sizeof(StringList)); if (stringList == NULL) { perror("malloc"); exit(1); } stringList->length = lineCnt; for (int i = 0; i < lineCnt; i++) { stringList[i].lineData = (LineData *)malloc(sizeof(LineData)); if (stringList[i].lineData == NULL) { perror("malloc"); exit(1); } } return stringList; } /** * 行数取得関数 */ static int getLineCnt(FILE* fp) { int lineCnt = 0; LineData lineData = { 0 }; while (getLineData(&lineData, fp) != NULL) { SAFE_RELEASE(lineData.line); lineCnt++; } rewind(fp); return lineCnt; } /** * ファイル読込み&データ取得関数 */ StringList *getStringList(const char *fileName) { assert(DEFAULT_SIZE > 2); FILE* fp = fopen(fileName, "r"); if (fp == NULL) { perror("fopen"); exit(1); } int lineCnt = getLineCnt(fp); printf("lineCnt: %d\n", lineCnt); StringList* stringList = allocateStringList(lineCnt, fp); for (int i = 0; i < lineCnt; i++) { getLineData(stringList[i].lineData, fp); } fclose(fp); return stringList; } /** * 領域開放 */ void deallocateStringList(StringList *stringList) { for (int i = 0; i < stringList->length; i++) { SAFE_RELEASE(stringList[i].lineData->line); SAFE_RELEASE(stringList[i].lineData); } SAFE_RELEASE(stringList); } int main(void) { StringList *stringList = getStringList("test.txt"); for (int i = 0; i < stringList->length; i++) { printf("%s\n", stringList[i].lineData->line); } deallocateStringList(stringList); return 0; }
追記
この記事を書いた後、別のコード例を紹介してくださった記事がありました。
ソースコードを見ると綺麗で無駄がなく、ヘンテコな構造体を定義していなく
普通の用途で使うならこれがいいのでは?
という素晴らしいコードでした。解説もわかりやすいです!