高度な入出力とコマンドライン

コマンドライン引数はレストランでの注文のようなものです——プログラムに欲しいものを伝え(argv)、プログラムは注文の数を数え(argc)、そして注文を用意します。

コマンドライン引数

argcとargv

main関数は2つのパラメータを受け取ることができます:

C
int main(int argc, char *argv[])
C
#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("引数の数: %d\n", argc);
    for (int i = 0; i < argc; i++) {
        printf("argv[%d] = %s\n", i, argv[i]);
    }
    return 0;
}

実行:

BASH
./program hello world 123
TEXT
引数の数: 4
argv[0] = ./program
argv[1] = hello
argv[2] = world
argv[3] = 123
💡 ヒント: コマンドライン引数はすべて文字列です!数値が必要な場合は、atoiatolstrtolで変換する必要があります。

実践的な引数解析

実際のプロジェクトでは、コマンドライン引数に-プレフィックスを使ったオプションを指定するのが一般的です:

C
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[]) {
    int verbose = 0;
    int count = 1;
    const char *filename = NULL;

    for (int i = 1; i < argc; i++) {
        if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) {
            verbose = 1;
        } else if (strcmp(argv[i], "-n") == 0 && i + 1 < argc) {
            count = atoi(argv[++i]);
        } else if (argv[i][0] != '-') {
            filename = argv[i];
        } else {
            fprintf(stderr, "不明なオプション: %s\n", argv[i]);
            return 1;
        }
    }

    if (filename == NULL) {
        fprintf(stderr, "使い方: %s [-v] [-n count] filename\n", argv[0]);
        return 1;
    }

    printf("ファイル: %s, 回数: %d, 詳細: %s\n",
           filename, count, verbose ? "オン" : "オフ");
    return 0;
}
⚠️ 注意: 手動の引数解析は間違いを起こしやすいです。複雑なプロジェクトではgetopt関数を使うと、短いオプションと長いオプションの組み合わせを扱えます。

環境変数

getenv

C
char *getenv(const char *name);

環境変数の値を返します。存在しない場合はNULLを返します。

C
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    const char *path = getenv("PATH");
    if (path != NULL) {
        printf("PATH = %s\n", path);
    }

    const char *home = getenv("HOME");
    if (home != NULL) {
        printf("HOME = %s\n", home);
    }
    return 0;
}

extern変数environ

C言語には、すべての環境変数文字列の配列を指すグローバル変数environが定義されています:

C
#include <stdio.h>

extern char **environ;

int main(void) {
    char **env = environ;
    while (*env) {
        printf("%s\n", *env);
        env++;
    }
    return 0;
}
💡 ヒント: 環境変数はデータベース接続文字列やログレベルなど、プログラムの動作設定によく使われます。ソースコードに値をハードコードするより柔軟です。

errnoエラー処理

errnoの仕組み

C標準ライブラリ関数がエラーに遭遇した場合、通常は直接クラッシュせず、グローバル変数errnoを設定しエラー値を返します:

C
#include <errno.h>

errnoはライブラリ関数呼び出しが成功した後でも自動的にゼロにクリアされません!関数呼び出しが失敗した後にのみ確認する必要があります。

perror / strerror

C
void perror(const char *s);
char *strerror(int errnum);

perrorstderrs: エラーメッセージを出力します。strerrorはエラーコードに対応するテキスト説明を返します。

C
#include <stdio.h>
#include <errno.h>
#include <string.h>

int main(void) {
    FILE *fp = fopen("nonexistent_file.txt", "r");
    if (fp == NULL) {
        perror("fopen");
        printf("エラーコード: %d, エラーメッセージ: %s\n", errno, strerror(errno));
        return 1;
    }
    fclose(fp);
    return 0;
}
TEXT
fopen: No such file or directory
エラーコード: 2, エラーメッセージ: No such file or directory

完全なエラー処理パターン:

C
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "使い方: %s ファイル名\n", argv[0]);
        return 1;
    }

    FILE *fp = fopen(argv[1], "r");
    if (fp == NULL) {
        fprintf(stderr, "'%s'を開けません: %s\n", argv[1], strerror(errno));
        return 1;
    }

    int ch;
    while ((ch = fgetc(fp)) != EOF) {
        putchar(ch);
    }

    if (ferror(fp)) {
        fprintf(stderr, "読み取りエラーが発生しました: %s\n", strerror(errno));
        fclose(fp);
        return 1;
    }

    fclose(fp);
    return 0;
}
▶ 試してみよう
⚠️ 注意: ferrorはストリームで読み書きエラーが発生したかを確認するものであり、feof(ファイル終端の確認)とは異なります。

標準ストリーム

C言語はプログラム起動時に3つのストリームを自動的に開きます:

ストリーム 名称 デフォルトデバイス ファイル記述子
stdin 標準入力 キーボード 0
stdout 標準出力 画面 1
stderr 標準エラー 画面 2

stdoutstderrの違い:stdoutはバッファリングされ、stderrはバッファリングされません。リダイレクト時には互いに影響しません。

C
#include <stdio.h>

int main(void) {
    fprintf(stdout, "これは通常出力です\n");
    fprintf(stderr, "これはエラー出力です\n");
    return 0;
}
BASH
./program > output.txt

このように実行すると、「通常出力」はファイルに書き込まれ、「エラー出力」は画面に表示されたままになります。

リダイレクトの原理

コマンドラインでは:

BASH
./program 2>&1 all.log

このコマンドはstderrstdoutに統合し、両方をall.logに書き込みます。

一時ファイル

tmpfile

C
FILE *tmpfile(void);

"wb+"モードで開かれた一時ファイルを作成します。ファイルは閉じるときまたはプログラム終了時に自動的に削除されます。

C
#include <stdio.h>

int main(void) {
    FILE *tmp = tmpfile();
    if (tmp == NULL) {
        perror("tmpfile");
        return 1;
    }

    fprintf(tmp, "一時データ %d\n", 42);
    rewind(tmp);

    char buf[64];
    while (fgets(buf, sizeof(buf), tmp) != NULL) {
        printf("%s", buf);
    }

    fclose(tmp);
    return 0;
}
TEXT
一時データ 42

tmpnam

C
char *tmpnam(char *s);

既存のファイルと衝突しない一時ファイル名を生成します。ただし、名前の生成とファイルの作成の間に別のプログラムが同じ名前のファイルを作成する競合状態があります。

⚠️ 注意: tmpfileを優先してください——原子的にファイルを作成して開くため、より安全です。tmpnamはマルチスレッド環境では安全ではありません。

一時ファイルを使って中間データを処理します:

C
#include <stdio.h>
#include <string.h>

int main(void) {
    FILE *tmp = tmpfile();
    if (tmp == NULL) {
        perror("tmpfile");
        return 1;
    }

    char *lines[] = {"banana", "apple", "orange", "grape"};
    int n = 4;

    for (int i = 0; i < n; i++) {
        fprintf(tmp, "%s\n", lines[i]);
    }

    rewind(tmp);
    printf("ソート前:\n");
    char buf[64];
    while (fgets(buf, sizeof(buf), tmp) != NULL) {
        buf[strcspn(buf, "\n")] = '\0';
        printf("  %s\n", buf);
    }

    rewind(tmp);
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - 1 - i; j++) {
            char a[64], b[64];
            long pos = ftell(tmp);
            fgets(a, sizeof(a), tmp);
            fgets(b, sizeof(b), tmp);
            a[strcspn(a, "\n")] = '\0';
            b[strcspn(b, "\n")] = '\0';

            if (strcmp(a, b) > 0) {
                fseek(tmp, pos, SEEK_SET);
                fprintf(tmp, "%s\n%s\n", b, a);
            } else {
                fseek(tmp, pos, SEEK_SET);
                fprintf(tmp, "%s\n%s\n", a, b);
            }
            fseek(tmp, pos + strlen(a) + strlen(b) + 2, SEEK_SET);
        }
        rewind(tmp);
    }

    rewind(tmp);
    printf("ソート後:\n");
    while (fgets(buf, sizeof(buf), tmp) != NULL) {
        buf[strcspn(buf, "\n")] = '\0';
        printf("  %s\n", buf);
    }

    fclose(tmp);
    return 0;
}
▶ 試してみよう
TEXT
ソート前:
  banana
  apple
  orange
  grape
ソート後:
  apple
  grape
  orange
  banana

高度なフォーマット出力

snprintf

C
int snprintf(char *str, size_t size, const char *format, ...);

sprintfと比較して、snprintfsizeパラメータが追加され、書き込み長を制限してバッファオーバーフローを防ぎます。

C
#include <stdio.h>

int main(void) {
    char buf[10];
    int n = snprintf(buf, sizeof(buf), "Hello, %s!", "World");
    printf("buf = \"%s\", 必要な長さ = %d\n", buf, n);
    return 0;
}
TEXT
buf = "Hello, Wo", 必要な長さ = 13
💡 ヒント: snprintfの戻り値は、フォーマット出力が持つべき長さであり、実際に書き込まれたバイト数ではありません。戻り値がsize-1より大きい場合、出力は切り詰められています。

❓ よくある質問

Q argvの引数の順序はどうなっていますか?
A argv[0]はプログラム名自体で、argv[1]以降がユーザーが指定した引数であり、コマンドラインでの順序と同じです。
Q なぜperrorはstdoutではなくstderrに出力するのですか?
A エラーメッセージはstdoutがファイルにリダイレクトされても失われてはならないからです。stderrはバッファリングされず、stdoutから独立しているため、エラーメッセージが常に表示されます。
Q errnoはいつゼロにクリアされますか?
A 自動的にはクリアされません。失敗する可能性のある関数を呼び出す前に手動でerrno = 0を設定し、関数がエラー値を返した後に確認する必要があります。
Q tmpfileはどこにファイルを作成しますか?
A 場所はシステムに依存し、通常は一時ディレクトリ(/tmpなど)です。tmpfileはFILE*を返すためパスを気にする必要はなく、ファイルはプログラム終了時に自動的に消滅します。

📖 まとめ

📝 練習問題

  1. -cで文字数、-lで行数、-wで単語数をカウントするコマンドラインプログラムを作成してください
  2. tmpfileを使ってファイルの内容を逆順にするプログラムを作成してください(すべて一時ファイルに書き込み、逆順で読み戻す)
  3. >2>を使ってstdoutstderrのリダイレクトの違いを示すプログラムを作成してください
100%