第10章: 入出力

10.1 入出力の理論

ストリーム抽象化

C言語の入出力はストリーム(Stream)という抽象化に基づいている。

ストリームの概念:

プログラム ←→ ストリーム ←→ デバイス

ストリームは「バイトの流れ」を抽象化:
- 入力ストリーム: データがプログラムに流れ込む
- 出力ストリーム: データがプログラムから流れ出す

Dennis Ritchieの設計思想: > "Everything is a file"(全てはファイルである)

UNIXの哲学に基づき、キーボード、画面、ファイル、ネットワークなど、あらゆる入出力を統一的なインターフェースで扱える。

バッファリング

バッファリングの種類:

1. 完全バッファリング(Fully Buffered):
   バッファが満杯になるまで実際のI/Oを遅延
   [プログラム] → [バッファ: ████████] → [ファイル]

2. 行バッファリング(Line Buffered):
   改行文字が来るまで遅延
   [プログラム] → [バッファ: Hello\n] → [端末]

3. バッファなし(Unbuffered):
   即座に出力
   [プログラム] → [エラー出力]

標準ストリームのデフォルト:

  • stdin: 端末なら行バッファ、それ以外は完全バッファ
  • stdout: 端末なら行バッファ、それ以外は完全バッファ
  • stderr: バッファなし
  • 10.2 標準入出力

    標準ストリーム

    #include <stdio.h>
    
    /* 標準入出力ストリーム */
    FILE *stdin;   /* 標準入力(通常はキーボード) */
    FILE *stdout;  /* 標準出力(通常は画面) */
    FILE *stderr;  /* 標準エラー出力(通常は画面) */
    
    /* EOF: End Of File */
    #define EOF (-1)
    

    文字入出力

    /* 1文字入力 */
    int getchar(void);          /* stdin から1文字読む */
    int fgetc(FILE *stream);    /* 指定ストリームから1文字読む */
    int getc(FILE *stream);     /* fgetcと同等(マクロの場合あり) */
    
    /* 1文字出力 */
    int putchar(int c);         /* stdout に1文字書く */
    int fputc(int c, FILE *stream);  /* 指定ストリームに1文字書く */
    int putc(int c, FILE *stream);   /* fputcと同等 */
    
    /* 使用例 */
    int c;
    while ((c = getchar()) != EOF) {
        putchar(c);
    }
    

    行入出力

    /* 行入力 */
    char *fgets(char *s, int n, FILE *stream);
    
    /* 使用例(安全) */
    char buffer[100];
    if (fgets(buffer, sizeof(buffer), stdin) != NULL) {
        /* 改行文字が含まれる可能性がある */
        size_t len = strlen(buffer);
        if (len > 0 && buffer[len-1] == '\n')
            buffer[len-1] = '\0';  /* 改行を削除 */
    }
    
    /* gets() は危険!使用禁止(C11で削除) */
    /* char *gets(char *s);  ← バッファオーバーフローの危険 */
    
    /* 行出力 */
    int fputs(const char *s, FILE *stream);
    int puts(const char *s);  /* 自動的に改行を追加 */
    
    fputs("Hello", stdout);   /* "Hello" */
    puts("Hello");            /* "Hello\n" */
    

    フォーマット入出力

    /* printf ファミリー */
    int printf(const char *format, ...);           /* stdout へ */
    int fprintf(FILE *stream, const char *format, ...);  /* ストリームへ */
    int sprintf(char *str, const char *format, ...);     /* 文字列へ(危険) */
    int snprintf(char *str, size_t n, const char *format, ...);  /* 安全 */
    
    /* scanf ファミリー */
    int scanf(const char *format, ...);            /* stdin から */
    int fscanf(FILE *stream, const char *format, ...);   /* ストリームから */
    int sscanf(const char *str, const char *format, ...);/* 文字列から */
    

    フォーマット指定子

    /* 整数 */
    %d, %i    /* int(10進数) */
    %u        /* unsigned int(10進数) */
    %o        /* unsigned int(8進数) */
    %x, %X    /* unsigned int(16進数) */
    %ld       /* long */
    %lld      /* long long */
    %zu       /* size_t */
    
    /* 浮動小数点 */
    %f        /* double(小数点形式) */
    %e, %E    /* double(指数形式) */
    %g, %G    /* double(自動選択) */
    %Lf       /* long double */
    
    /* 文字と文字列 */
    %c        /* char */
    %s        /* char * */
    
    /* ポインタ */
    %p        /* void * */
    
    /* その他 */
    %%        /* %自体 */
    %n        /* 出力文字数を取得(危険!) */
    

    フィールド幅と精度

    /* フィールド幅 */
    printf("%10d", 42);     /* "        42" 右寄せ */
    printf("%-10d", 42);    /* "42        " 左寄せ */
    printf("%010d", 42);    /* "0000000042" ゼロ埋め */
    
    /* 精度 */
    printf("%.5d", 42);     /* "00042" 最小桁数 */
    printf("%.5f", 3.14);   /* "3.14000" 小数点以下桁数 */
    printf("%.5s", "Hello, World");  /* "Hello" 最大文字数 */
    
    /* フィールド幅と精度の組み合わせ */
    printf("%10.5f", 3.14); /* "   3.14000" */
    
    /* 動的なフィールド幅 */
    printf("%*d", 10, 42);  /* %10d と同等 */
    printf("%.*f", 5, 3.14);/* %.5f と同等 */
    

    長さ修飾子

    /* 長さ修飾子 */
    hh    /* char */
    h     /* short */
    l     /* long, wchar_t */
    ll    /* long long */
    j     /* intmax_t */
    z     /* size_t */
    t     /* ptrdiff_t */
    L     /* long double */
    
    /* 例 */
    printf("%hhd", (char)127);        /* char */
    printf("%hd", (short)32767);      /* short */
    printf("%ld", 2147483647L);       /* long */
    printf("%lld", 9223372036854775807LL);  /* long long */
    printf("%zu", sizeof(int));       /* size_t */
    

    10.3 ファイル操作

    ファイルのオープンとクローズ

    /* ファイルを開く */
    FILE *fopen(const char *filename, const char *mode);
    
    /* モード文字列 */
    "r"   /* 読み込み(ファイルが存在する必要あり) */
    "w"   /* 書き込み(存在すれば切り詰め、なければ作成) */
    "a"   /* 追記(なければ作成) */
    "r+"  /* 読み書き(存在する必要あり) */
    "w+"  /* 読み書き(切り詰めまたは作成) */
    "a+"  /* 読み書き(追記、なければ作成) */
    
    /* バイナリモード */
    "rb", "wb", "ab", "r+b", "w+b", "a+b"
    
    /* 使用例 */
    FILE *fp = fopen("data.txt", "r");
    if (fp == NULL) {
        perror("fopen");
        return -1;
    }
    
    /* 処理 */
    
    fclose(fp);
    

    ファイルの読み書き

    /* バイナリ読み書き */
    size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
    size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
    
    /* 使用例: 構造体の読み書き */
    struct Record {
        int id;
        char name[50];
        double value;
    };
    
    /* 書き込み */
    struct Record rec = {1, "Test", 3.14};
    FILE *fp = fopen("data.bin", "wb");
    fwrite(&rec, sizeof(struct Record), 1, fp);
    fclose(fp);
    
    /* 読み込み */
    struct Record rec2;
    fp = fopen("data.bin", "rb");
    fread(&rec2, sizeof(struct Record), 1, fp);
    fclose(fp);
    

    ファイル位置

    /* 位置の取得と設定 */
    long ftell(FILE *stream);           /* 現在位置を取得 */
    int fseek(FILE *stream, long offset, int whence);  /* 位置を設定 */
    void rewind(FILE *stream);          /* 先頭に戻る */
    
    /* whence の値 */
    SEEK_SET  /* ファイルの先頭から */
    SEEK_CUR  /* 現在位置から */
    SEEK_END  /* ファイルの末尾から */
    
    /* 使用例 */
    fseek(fp, 0, SEEK_END);     /* 末尾に移動 */
    long size = ftell(fp);       /* ファイルサイズを取得 */
    fseek(fp, 0, SEEK_SET);     /* 先頭に戻る */
    
    /* 大きなファイル用(C99) */
    int fgetpos(FILE *stream, fpos_t *pos);
    int fsetpos(FILE *stream, const fpos_t *pos);
    

    エラーとEOF

    /* エラーチェック */
    int ferror(FILE *stream);   /* エラーフラグをチェック */
    int feof(FILE *stream);     /* EOFフラグをチェック */
    void clearerr(FILE *stream); /* フラグをクリア */
    
    /* 使用例 */
    FILE *fp = fopen("data.txt", "r");
    int c;
    while ((c = fgetc(fp)) != EOF) {
        putchar(c);
    }
    
    if (ferror(fp)) {
        fprintf(stderr, "Read error occurred\n");
    }
    fclose(fp);
    

    バッファ制御

    /* バッファモードの設定 */
    int setvbuf(FILE *stream, char *buf, int mode, size_t size);
    void setbuf(FILE *stream, char *buf);
    
    /* mode の値 */
    _IOFBF  /* 完全バッファリング */
    _IOLBF  /* 行バッファリング */
    _IONBF  /* バッファなし */
    
    /* 使用例 */
    FILE *fp = fopen("data.txt", "w");
    setvbuf(fp, NULL, _IONBF, 0);  /* バッファなし */
    
    /* バッファをフラッシュ */
    int fflush(FILE *stream);
    
    /* 全ての出力ストリームをフラッシュ */
    fflush(NULL);
    

    10.4 低レベル入出力(POSIX)

    ファイルディスクリプタ

    #include <unistd.h>
    #include <fcntl.h>
    
    /* 標準ファイルディスクリプタ */
    #define STDIN_FILENO  0
    #define STDOUT_FILENO 1
    #define STDERR_FILENO 2
    
    /* ファイルを開く */
    int open(const char *pathname, int flags, ...);
    
    /* flags */
    O_RDONLY   /* 読み込み専用 */
    O_WRONLY   /* 書き込み専用 */
    O_RDWR     /* 読み書き */
    O_CREAT    /* 存在しなければ作成 */
    O_TRUNC    /* 既存の内容を切り詰め */
    O_APPEND   /* 追記 */
    O_EXCL     /* O_CREATと併用、存在すればエラー */
    
    /* 使用例 */
    int fd = open("data.txt", O_RDWR | O_CREAT, 0644);
    if (fd == -1) {
        perror("open");
        return -1;
    }
    
    close(fd);
    

    read と write

    /* 読み込み */
    ssize_t read(int fd, void *buf, size_t count);
    
    /* 書き込み */
    ssize_t write(int fd, const void *buf, size_t count);
    
    /* 使用例 */
    char buffer[1024];
    ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
    if (bytes_read == -1) {
        perror("read");
    } else if (bytes_read == 0) {
        /* EOF */
    }
    
    ssize_t bytes_written = write(fd, buffer, bytes_read);
    if (bytes_written == -1) {
        perror("write");
    }
    

    stdio と POSIX の対応

    /* FILE * からファイルディスクリプタを取得 */
    int fileno(FILE *stream);
    
    /* ファイルディスクリプタから FILE * を作成 */
    FILE *fdopen(int fd, const char *mode);
    
    /* 使用例 */
    int fd = fileno(stdout);  /* 通常は 1 */
    
    FILE *fp = fdopen(fd, "w");
    fprintf(fp, "Hello via fdopen\n");
    

    ファイル位置(POSIX)

    /* 位置の設定 */
    off_t lseek(int fd, off_t offset, int whence);
    
    /* 使用例: ファイルサイズの取得 */
    off_t size = lseek(fd, 0, SEEK_END);
    lseek(fd, 0, SEEK_SET);  /* 先頭に戻る */
    
    /* 大きなファイル用 */
    /* _FILE_OFFSET_BITS=64 でコンパイル */
    

    10.5 エラー処理

    errno

    #include <errno.h>
    #include <string.h>
    
    /* errno: グローバルエラー番号 */
    FILE *fp = fopen("nonexistent.txt", "r");
    if (fp == NULL) {
        /* errno には最後のエラーコードが設定される */
        printf("Error code: %d\n", errno);
        printf("Error message: %s\n", strerror(errno));
    }
    

    perror

    /* エラーメッセージを stderr に出力 */
    void perror(const char *s);
    
    /* 使用例 */
    FILE *fp = fopen("nonexistent.txt", "r");
    if (fp == NULL) {
        perror("fopen");
        /* 出力: fopen: No such file or directory */
    }
    

    一般的なエラーコード

    EACCES   /* Permission denied */
    EEXIST   /* File exists */
    EINTR    /* Interrupted system call */
    EINVAL   /* Invalid argument */
    EIO      /* I/O error */
    EISDIR   /* Is a directory */
    EMFILE   /* Too many open files */
    ENOENT   /* No such file or directory */
    ENOMEM   /* Out of memory */
    ENOSPC   /* No space left on device */
    ENOTDIR  /* Not a directory */
    EPERM    /* Operation not permitted */
    

    エラー処理パターン

    /* パターン1: 即座にリターン */
    FILE *fp = fopen(filename, "r");
    if (fp == NULL) {
        return -1;
    }
    
    /* パターン2: goto によるクリーンアップ */
    int process_file(const char *filename)
    {
        int ret = -1;
        FILE *fp = NULL;
        char *buffer = NULL;
    
        fp = fopen(filename, "r");
        if (fp == NULL) {
            perror("fopen");
            goto cleanup;
        }
    
        buffer = malloc(BUFFER_SIZE);
        if (buffer == NULL) {
            perror("malloc");
            goto cleanup;
        }
    
        /* 処理 */
        ret = 0;
    
    cleanup:
        free(buffer);
        if (fp)
            fclose(fp);
        return ret;
    }
    
    /* パターン3: EINTR の再試行 */
    ssize_t safe_read(int fd, void *buf, size_t count)
    {
        ssize_t ret;
        do {
            ret = read(fd, buf, count);
        } while (ret == -1 && errno == EINTR);
        return ret;
    }
    

    10.6 一時ファイル

    #include <stdio.h>
    
    /* 一時ファイルを作成 */
    FILE *tmpfile(void);
    /* 自動的に削除される、バイナリ読み書きモード */
    
    /* 一時ファイル名を生成 */
    char *tmpnam(char *s);    /* 非推奨: 競合状態の危険 */
    
    /* より安全な方法(POSIX) */
    #include <stdlib.h>
    int mkstemp(char *template);
    
    /* 使用例 */
    char template[] = "/tmp/myapp.XXXXXX";
    int fd = mkstemp(template);
    if (fd == -1) {
        perror("mkstemp");
        return -1;
    }
    /* template は実際のファイル名に変更される */
    printf("Created: %s\n", template);
    unlink(template);  /* ファイルを削除(fdは有効なまま) */
    /* ... fd を使用 ... */
    close(fd);
    

    10.7 入出力のパフォーマンス

    バッファサイズの選択

    /* 最適なバッファサイズ */
    #include <sys/stat.h>
    
    struct stat st;
    if (fstat(fd, &st) == 0) {
        size_t optimal_size = st.st_blksize;  /* ファイルシステムのブロックサイズ */
    }
    
    /* 一般的な選択肢 */
    #define BUFSIZ 8192  /* stdio のデフォルト */
    /* または 4096, 8192, 16384 など */
    

    効率的なファイルコピー

    /* 非効率: 1バイトずつ */
    int c;
    while ((c = fgetc(src)) != EOF)
        fputc(c, dst);
    
    /* 効率的: バッファを使用 */
    char buffer[8192];
    size_t bytes;
    while ((bytes = fread(buffer, 1, sizeof(buffer), src)) > 0)
        fwrite(buffer, 1, bytes, dst);
    
    /* さらに効率的: mmap(大きなファイル) */
    #include <sys/mman.h>
    void *mapped = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
    write(dst_fd, mapped, file_size);
    munmap(mapped, file_size);
    

    メモリマップドI/O

    #include <sys/mman.h>
    
    /* ファイルをメモリにマップ */
    void *mmap(void *addr, size_t length, int prot, int flags,
               int fd, off_t offset);
    
    /* マップを解除 */
    int munmap(void *addr, size_t length);
    
    /* prot: 保護 */
    PROT_READ   /* 読み取り可能 */
    PROT_WRITE  /* 書き込み可能 */
    PROT_EXEC   /* 実行可能 */
    PROT_NONE   /* アクセス不可 */
    
    /* flags */
    MAP_SHARED    /* 変更を他のプロセスと共有 */
    MAP_PRIVATE   /* コピーオンライト */
    
    /* 使用例 */
    int fd = open("data.bin", O_RDONLY);
    struct stat st;
    fstat(fd, &st);
    
    void *data = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (data == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return -1;
    }
    
    /* data をポインタとして直接アクセス */
    char *text = (char *)data;
    printf("First char: %c\n", text[0]);
    
    munmap(data, st.st_size);
    close(fd);
    

    10.8 まとめ

    本章では、C言語の入出力について学んだ:

  • ストリーム: 抽象化、バッファリング
  • 標準入出力: stdio.h、フォーマット入出力
  • ファイル操作: fopen, fread, fwrite, fseek
  • 低レベルI/O: ファイルディスクリプタ、read, write
  • エラー処理: errno, perror, strerror
  • 一時ファイル: tmpfile, mkstemp
  • パフォーマンス: バッファリング、mmap
  • 次章では、コンパイルとリンクについて学ぶ。

    ---

    参考文献

  • ISO/IEC 9899:2011, Programming languages — C
  • Stevens, W. R., & Rago, S. A. (2013). "Advanced Programming in the UNIX Environment", 3rd Edition, Addison-Wesley
  • Kernighan, B. W., & Ritchie, D. M. (1988). "The C Programming Language", 2nd Edition, Prentice Hall
  • Kerrisk, M. (2010). "The Linux Programming Interface", No Starch Press