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

追記

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

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

C言語のグローバル変数とexternについて

f:id:fa11enprince:20200628230841j:plain
C言語では言語仕様上、グローバル変数は良く使うと思います。
できるだけ避けるのは言うまでもありませんが。
そこでよく混乱するのがexternではないでしょうか?
ヘッダなんかをインクルードすると
あれ?そういえばexternって……どうなんだっけ…ってことになります。

私なんかはなんかの参考書でプログラミングを学び始めたときに、
externつけてもつけなくても一緒というような怪しげな解説を見た記憶があり、
それによって、余計に混乱してしまいました。
ただし、関数に関しては一緒です。
ここより正確な記載のあるサイトを見つけましたのでURLを記載します。
C言語のexternキーワードについて(関数編) – cloudtofu
いまだに検索流入が多い(2019年8月時点)のでちょっとびっくりします。それだけC言語が息の長い言語であり、
年々使用者が減少しているのでしょうね。私自身もCはもう5年以上触っていません。
いまなら限られた環境でない限りはC++(もしくはGoかRustかもしれない)を使うでしょうね。

externは
他の場所に定義があって、宣言ですよ
って明示するためのものです。

宣言と定義について

厳密な説明ではないのですが、
C言語における宣言とは値や中身がかかれていないものです。
例えば、

int g_value;
extern int g_value2;
int foo(void);

は宣言です。
一方、定義は

int g_value = 0;
int foo(void) {
    return g_value;
}

などです。

グローバル変数を使うときどうすればいいか、
基本的にヘッダ側(.h)はextern付の宣言をして、.cファイルのどこかに
externなしの定義を書きます。その際に初期値を代入します。
これでほぼOKです。
もちろん、.h側にexternなしの変数宣言をしてはいけません。
ヘッダファイルにはいろいろお作法があるのですが、
きちんと書かれているものが少ないように思われます。
そういうわけで巷には間違って書かれている
ヘッダファイルがあふれているのではないでしょうか?

ただし、これでは定義がどこにあるのか、
しかも一つでなくてはならないので、管理が複雑になり、混乱します。
しかも仮定義という厄介な概念があり、もっと事情は複雑です。

こうしておけばよい

最初に最終版を書きます。
GLOBAL_VALUE_DEFINEDをmainのあるファイルにdefineし
マクロでextern有無しを制御します。
初期値を0以外に指定したいときはやはりマクロで制御します。

test.h

#ifndef TEST_H_INCLUDED_
#define TEST_H_INCLUDED_

#ifdef GLOBAL_VALUE_DEFINE
  #define GLOBAL
  #define GLOBAL_VAL(v) = (v)
#else
  #define GLOBAL extern
  #define GLOBAL_VAL(v)
#endif

GLOBAL int g_value;   // この場合は最初の定義で0で初期化
/* GLOBAL int g_value GLOBAL_VAL(1); */

void foo(void);

#endif /* TEST_H_INCLIDED_ */

test.c

#include "test.h"

void foo(void) {
    g_value++;
}

myapp.c

#define GLOBAL_VALUE_DEFINE
#include <stdio.h>
#include "test.h"

int main(void) {
    printf("%d\n", g_value);
    foo();
    printf("%d\n", g_value);
    g_value++;
    printf("%d\n", g_value);
    return 0;
}

Makefile

あとコンパイルがやや面倒なのでMakefileを書きます。
Makefileの解説をすると長くなるので割愛します。

CC           = gcc
CFLAGS       = -Wall
DEBUGFLAGS   = -O0 -D _DEBUG -g
PROG         = myapp
SOURCES      = myapp.c test.c
OBJS         = $(SOURCES:.c=.o)
INCDIR       =
LIBDIR       =
LIB          =

.PHONY: all
all: $(SOURCES) $(PROG)

# Primary Target
$(PROG): $(OBJS)
        $(CC) $(CFLAGS) $(DEBUGFLAGS) -o $@ $^ $(INCDIR) $(LIBDIR) $(LIB)

# Suffix Rule
.c.o:
        $(CC) $(CFLAGS) $(DEBUGFLAGS) -c $< $(INCDIR) $(LIBDIR) $(LIB)

.PHONY: clean
clean:
        $(RM) $(OBJS) $(PROG)

コンパイル&リンク

$ make
gcc -Wall  -O0 -D _DEBUG -g -c myapp.c
gcc -Wall  -O0 -D _DEBUG -g -c test.c
gcc -Wall  -O0 -D _DEBUG -g -o myapp myapp.o test.o

実行結果

$ ./myapp.exe
0
1
2

解説

結論はわかったとしてどうしてこうするのかというのを説明します。
話を単純化するために例えば次のような2つのファイルがあるとします。

test.c

extern int g_value;

void foo(void) {
    g_value++;
}

myapp.c

#include <stdio.h>

int g_value = 0;
/* extern */ void foo(void);

int main(void) {
    printf("%d\n", g_value);
    foo();
    printf("%d\n", g_value);
    g_value++;
    printf("%d\n", g_value);
    return 0;
}

もちろんこれは次のようにコンパイルすればちゃんと問題なく動きます。

コンパイル

gcc -Wall  -O0 -D _DEBUG -g -c myapp.c
gcc -Wall  -O0 -D _DEBUG -g -c test.c
gcc -Wall  -O0 -D _DEBUG -g -o myapp myapp.o test.o

補足ですが関数定義はexternがあってもなくても外部結合(=ファイルの外から見える)ので
あってもなくてもよいです。

ここで、test.c側のextern int g_value;
int g_value = 0;
に書き換えると、当然

test.o:test.c:(.bss+0x0): `g_value' が重複して定義されています

というようなエラーがでます。
つまり、.hファイルに単純にくくりだしてint g_value = 0;
として両者の.cでincludeすると同じことが起こります。
そのようなことを防ぐために最初のようにマクロで制御しています。

しかし、厄介なのがANSI Cの仮定義という概念で、
externをかかず、いろんなファイルでint g_value;とした場合は……

test.c

int g_value; /* 他に定義がないので int g_value = 0;の定義として扱う */
...

myapp.c

#include <stdio.h>

int g_value; /* 既にg_valueの定義があるので、extern int g_value;(宣言)として扱う */
...

これはなんとコンパイルが通ります。理由は上記コードのコメント部分です。

その他constのグローバル変数について

constにも同じことが言えます。
ただし、constでexternかどうか気にしたことがないぞ?っていう人もいるかもしれません。
……そもそも伝統的なCを使っている人は#defineを使っているかもしれませんが。
実はconstの場合、CとC++で微妙に振る舞いが違います。
externを使わない場合、
Cの場合は外部リンケージ
C++の場合は内部リンケージとなります。

参考リンク

Linuxを使いたいけど、vi(Vim)って難しいよね。できれば使いたくない!……ってひと向けnanoエディタ


Linuxって使うのにいろいろハードルがあるのですが、
その一つの大きな障壁がコマンドライン上でのテキスト編集作業ではないでしょうか?
デフォルトで用意されていてかつメジャーなのが
viというこれまた変な操作体系のエディタで
最初使ったらなんじゃこりゃ。入力できないし、編集できないし……。
終了の仕方がわからず、そっとターミナルを閉じた……。
なんて人は多いんじゃないでしょうか?僕です。
第一、viなんてよっぽどのモノ好きでないと使いこなせるようになりません。
というか必要に迫られないと覚えられないようなものです。
Linuxが使いたいだけなのに変な操作体系を覚えさせられる…。
おれはLinuxをやりたいんだ。
でもなぜかviの学習をしている……。ってなんだかおかしいですよね。
GUIが整っている環境なら、geditというエディタを使って編集という手もありますが、
なかなかそうもいかず、ターミナル(黒い画面)経由でアクセスしてなんてことも多いですよね。
また、VPSなんか使って最小構成にしていると、GUIすらないし……。
ああ、もう、FTPかSCPでファイル取ってきてWindows上で編集してまたあげるか……
それは過去の僕です。
やっぱりviか……となるんですが、ターミナル上でもメモ帳感覚で使えるエディタがあるのです!
それがnanoエディタ。CentOSならだいたい最初から入っていることが多いです。
もしない場合は下記コマンドでインストールできます。(ルートユーザでやってね!)

使ってみよう

# yum install nano

起動は次のようにします。ファイルネームを引数にしてあげます。

$ nano [filename]

今回はtestというファイル名でファイルを作ってみるので

$ nano test

でエンターダンッ!
CentOSのターミナル上でつかうとこうなります(Ubuntu/Macでも使えるよ!)。

日本語環境をちゃんと整え切っていないので英語になっていますが、
日本語にも対応できます。
基本的にメモ帳感覚で使えます。
保存がCtrl+Oなのがちょっと奇妙ですが……。
もちろんvi同様カスタマイズができて、homeに.nanorcを置いて
何か書いてやればカスタマイズも可能です。
ちなみにviな人が使って油断するとjjjjjkkkkkとか思わず入力してしまいます。
※私はVimmerです

関連記事

Javaで優先順位をつけて複数のキーでソートする方法



久々にPureなJavaを書きました(*´Д`)

複数のキーでソートする書き方の説明

いろいろ書き方はありますが、
対象のクラスにComparableをimplementsして
compareTo()をオーバーライドすると
ソート順を定義できます。
そのうえで、

-1(左が先)
 0(同じ)
 1(右が先)

の値を返してやればいいのですが、
複数ある時は、
第1条件の判定結果を変数に格納してやり、
それで0の時は第2条件を判定、
さらに0の時は第3条件を判定……
さらに0の時は第4条件を判定……
とすると、第1条件、第2条件、第3条件………の順でソートされます。

ちなみに並び順を変えたいときは定数なり、enumなりでcompareTo()の中を分岐させます。

サンプルコード

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

@Data
@AllArgsConstructor
class Student implements Comparable<Student> {
    // 並び順にあまり意味はない…並び替えの名前もテキトー
    public static enum SortMethod {
        GRADE_CLAZZ_ASC,   /* 学年(ASC) > 学級(ASC) > 名前(ASC) > 班長(T>F) */
        GRADE_CLAZZ_DESC,  /* 学年(DSC) > 学級(DESC) > 名前(DESC) > 班長(F>T) */
    }
    @Getter
    @Setter
    private static SortMethod method = SortMethod.GRADE_CLAZZ_ASC;

    private int grade; /* 学年 */
    private String clazz; /* 学級 */
    private String name; /* 名前 */
    private boolean isLeader; /* 班長 */

    @Override
    public int compareTo(Student o) {
        int result = 0;
        if (method == SortMethod.GRADE_CLAZZ_ASC) {
            result = Integer.valueOf(this.grade).compareTo(Integer.valueOf(o.grade));
            if (result == 0) {
                result = this.clazz.compareTo(o.clazz);
            }
            if (result == 0) {
                result = this.name.compareTo(o.clazz);
            }
            if (result == 0) {
                result = Boolean.valueOf(this.isLeader).compareTo(Boolean.valueOf(o.isLeader));
            }
        } else if (method == SortMethod.GRADE_CLAZZ_DESC) {
            result = -Integer.valueOf(this.grade).compareTo(Integer.valueOf(o.grade));
            if (result == 0) {
                result = -this.clazz.compareTo(o.clazz);
            }
            if (result == 0) {
                result = -this.name.compareTo(o.clazz);
            }
            if (result == 0) {
                result = -Boolean.valueOf(this.isLeader).compareTo(Boolean.valueOf(o.isLeader));
            }
        }
        return result;
    }

    @Override
    public String toString() {
        return "学年 [" + this.grade + "] クラス [" + this.clazz + "] 名前 [ "
                + String.format("%1$-20s", this.name)  // 20文字右スペース埋め (こんな便利なメソッドあったんだ)
                + "] " + (isLeader ? "班長" : "");
    }
}

public class SortSample {
    public static void main(String[] args) {
        // テキトーにデータをぶち込む
        List<Student> students = new ArrayList<>(
                Arrays.asList(
                        new Student(6, "A", "Robin Givens", true),
                        new Student(6, "A", "Richard Burgi", false),
                        new Student(2, "A", "Sarah Walker", false),
                        new Student(2, "A", "Jon Casey", false),
                        new Student(2, "B", "Morgan Grimes", false),
                        new Student(1, "A", "Elenor Batorwski", false),
                        new Student(6, "B", "Agatha Christie", true),
                        new Student(5, "A", "Tony Todd", true),
                        new Student(5, "A", "Matthew Bomer", false),
                        new Student(4, "A", "Linda Hamilton", false),
                        new Student(3, "A", "Lauren Cohan", true),
                        new Student(2, "A", "Chales Bartowski", true),
                        new Student(5, "B", "Rachel Bilson", true),
                        new Student(4, "A", "Nicole Richie", false),
                        new Student(1, "B", "Devon Woodcomb", false)
                )
        );
        Student.setMethod(Student.SortMethod.GRADE_CLAZZ_ASC);
        Collections.sort(students);
        for (Student s : students) {
            System.out.println(s);
        }
        System.out.println("------------------------------------------------");
        // Stream APIでソート
        Student.setMethod(Student.SortMethod.GRADE_CLAZZ_DESC);
        students.stream().sorted((e1, e2) -> e1.compareTo(e2)).forEach(System.out::println);
        System.out.println("------------------------------------------------");
    }
}

Lombokを使っていますので、とりあえず、Mavenを書きます。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>SortSample</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.source>1.8</maven.compiler.source>
    </properties>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

</project>

実行結果

学年 [1] クラス [A] 名前 [ Elenor Batorwski    ] 
学年 [1] クラス [B] 名前 [ Devon Woodcomb      ] 
学年 [2] クラス [A] 名前 [ Sarah Walker        ] 
学年 [2] クラス [A] 名前 [ Jon Casey           ] 
学年 [2] クラス [A] 名前 [ Chales Bartowski    ] 班長
学年 [2] クラス [B] 名前 [ Morgan Grimes       ] 
学年 [3] クラス [A] 名前 [ Lauren Cohan        ] 班長
学年 [4] クラス [A] 名前 [ Linda Hamilton      ] 
学年 [4] クラス [A] 名前 [ Nicole Richie       ] 
学年 [5] クラス [A] 名前 [ Tony Todd           ] 班長
学年 [5] クラス [A] 名前 [ Matthew Bomer       ] 
学年 [5] クラス [B] 名前 [ Rachel Bilson       ] 班長
学年 [6] クラス [A] 名前 [ Robin Givens        ] 班長
学年 [6] クラス [A] 名前 [ Richard Burgi       ] 
学年 [6] クラス [B] 名前 [ Agatha Christie     ] 班長
------------------------------------------------
学年 [6] クラス [B] 名前 [ Agatha Christie     ] 班長
学年 [6] クラス [A] 名前 [ Richard Burgi       ] 
学年 [6] クラス [A] 名前 [ Robin Givens        ] 班長
学年 [5] クラス [B] 名前 [ Rachel Bilson       ] 班長
学年 [5] クラス [A] 名前 [ Matthew Bomer       ] 
学年 [5] クラス [A] 名前 [ Tony Todd           ] 班長
学年 [4] クラス [A] 名前 [ Nicole Richie       ] 
学年 [4] クラス [A] 名前 [ Linda Hamilton      ] 
学年 [3] クラス [A] 名前 [ Lauren Cohan        ] 班長
学年 [2] クラス [B] 名前 [ Morgan Grimes       ] 
学年 [2] クラス [A] 名前 [ Chales Bartowski    ] 班長
学年 [2] クラス [A] 名前 [ Jon Casey           ] 
学年 [2] クラス [A] 名前 [ Sarah Walker        ] 
学年 [1] クラス [B] 名前 [ Devon Woodcomb      ] 
学年 [1] クラス [A] 名前 [ Elenor Batorwski    ] 
------------------------------------------------

あとがき

けっこうこういうコード書くはずなのだけど、ぐぐってもほとんど出てこなかったので書きました。
っていうかあれか、たいていDBから取ってくるときに既にソートして取ってるから
そのあと並び替えするなんてことあんまりしないですよね。
本当はStrategyパターンを書きたかったのですが、前書きで終了してしまいました。
次はStrategyパターンを書きます。

とおもったけど、そもそもStream APIでComparator.comparing()/Comparator.thenComparing()を使えばOKです。
blog1.mammb.com