Gobble up pudding

プログラミングの記事がメインのブログです。

MENU

どんな列(幅)でも行数でも読込む関数作りました

スポンサードリンク



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

追記

この記事を書いた後、別のコード例を紹介してくださった記事がありました。

ソースコードを見ると綺麗で無駄がなく、ヘンテコな構造体を定義していなく
普通の用途で使うならこれがいいのでは?
という素晴らしいコードでした。解説もわかりやすいです!