C言語編 第48章 理解の定着・小休止⑤

先頭へ戻る

この章の概要

この章の概要です。

理解の定着・小休止⑤

この章では、これまでの章内容の理解を再確認しましょう。 また、1章丸ごとを割くほどでも無い細かい部分について、少し触れていきます。

今回は、以下の範囲が対象です。 ファイル操作と、文字の扱いがテーマとなります。

ストリーム

ストリームは、「流れ」という意味合いがあり、データが流れる経路のことを意味しています。

ストリームには、標準入力(stdin)、標準出力(stdout)、標準エラー(stderr) といった種類があります。これ以外にも存在することもありますが、それは環境依存です。

PC環境では多くの場合、標準入力はキーボード、標準出力と標準エラーは画面へ結び付けられていますが、リダイレクトという機能を使って、接続先を変更することも可能です。

データの入力や出力を、ストリームを介して行うことによって、実際にどんな機器やファイルとやり取りしているのかを気にする必要がなくなります。これによって、機器やファイルの性質の違いを考慮することなく、まったく同じ方法でプログラムを書くことができるようになっています。

テキストファイルの書き出し

テキストファイルへ、文字列を書き込む例を挙げます。

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

int main(void)
{
    FILE* fp;

    fp = fopen( "hello.txt", "w" );
    if( fp == NULL ){
        fputs( "ファイルオープンに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    fputs( "Hello, World\n", fp );

    if( fclose( fp ) == EOF ){
        fputs( "ファイルクローズに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    return 0;
}

実行結果



出力ファイル (hello.txt)

Hello, World

ファイルを使った入出力では、まずファイルを fopen関数でオープンし、fclose関数でクローズします。

fopen関数の第2引数によるオープンモードの指定には、様々なバリエーションがあります。

オープンモード 意味
r テキストファイルを読み込み用に開く
w テキストファイルを書き込み用に開く
a テキストファイルを追記用に開く
rb バイナリファイルを読み込み用に開く
wb バイナリファイルを書き込み用に開く
ab バイナリファイルを追記用に開く
r+ テキストファイルを読み書き両用に開く
w+ テキストファイルを読み書き両用に開く
a+ テキストファイルを読み書き両用で追加あるいは作成する
rb+ または r+b バイナリファイルを読み書き両用に開く
wb+ または w+b バイナリファイルを読み書き両用に開く
ab+ または a+b バイナリファイルを読み書き両用で追加あるいは作成する

テキストファイルに対する通常の書き込みには、"w" を指定し、追記書き込みの場合には、"a" を指定します。追記書き込みは、既にファイルに書き込まれているデータを壊すことなく、末尾にデータを追加で書き込んでいくというものです。"w" を指定した場合は、ファイルがオープンされた時点で、元々あったデータは失われてしまいます。

また、テキストファイルへの書き出しのためには、fputs関数)の他にも、fprintf関数fputc関数といった標準ライブラリ関数が使えます。

なお、これらの関数は実引数で、FILEオブジェクトを指すポインタ数を渡すことによって、そのファイルを対象に出力処理を行います。ここに stdout を渡すと、標準出力を対象とすることができます。

テキストファイルの読み込み

テキストファイルを読み込む際には、fopen関数の第2引数に "r" を指定します。

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

int main(void)
{
    FILE* fp;
    char buf[80];

    fp = fopen( "hello.txt", "r" );
    if( fp == NULL ){
        fputs( "ファイルオープンに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    for( ;; ){
        if( fgets( buf, sizeof(buf), fp ) == NULL ){
            if( feof( fp ) ){
                break;
            }
            else{
                fputs( "エラーが発生しました。\n", stderr );
                exit( EXIT_FAILURE );
            }
        }

        fputs( buf, stdout );
    }

    if( fclose( fp ) == EOF ){
        fputs( "ファイルクローズに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    return 0;
}

実行結果

Hello, World

入力ファイル (hello.txt)

Hello, World

テキストファイルからの読み込みには、fgets関数の他、fscanf関数fgetc関数といった標準ライブラリ関数が使えます。

ファイルの中身を全て読み込む場合、どうにかしてファイルの末尾を検出する必要があります。上記のサンプルプログラムでは、fgets関数の戻り値と、feof関数の助けを借りて、これを実現しています。つまり、次のように考えられます。

fgets関数の場合はこれで良いですが、他の関数を使う場合には、多少異なることもあります。例えば、fgetc関数は、ファイルの終端に達するか、エラーが発生すると EOF を返します。この場合も、ファイルの終端に達していることは、feof関数で調べられます。

一方で、エラーの有無に関しては、ferror関数で調べられるものもありますし、調べられないものもあります。標準ライブラリ関数のリファレンスを確認するなどして、正しい方法でエラーチェックを行うようにして下さい。

バイナリファイルの書き出し

バイナリファイルへの書き出しには、fwrite関数を使います。

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

int main(void)
{
    FILE* fp;
    int num = 900;
    double d = 7.85;
    char str[] = "xyzxyz";


    fp = fopen( "test.bin", "wb" );
    if( fp == NULL ){
        fputs( "ファイルオープンに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    fwrite( &num, sizeof(num), 1, fp );
    fwrite( &d, sizeof(d), 1, fp );
    fwrite( str, sizeof(str[0]), sizeof(str), fp );

    if( fclose( fp ) == EOF ){
        fputs( "ファイルクローズに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    return 0;
}

実行結果(標準出力)


実行結果(test.bin のテキスト表現)

????ffffff@xyzxyz

バイナリファイルを扱う際には、fopen関数の第2引数に "b" を含むオープンモードを指定します。

バイナリファイルの読み込み

バイナリファイルの読み込みには、fread関数を使います。

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

int main(void)
{
    FILE* fp;
    int num;
    double d;
    char str[7];


    fp = fopen( "test.bin", "rb" );
    if( fp == NULL ){
        fputs( "ファイルオープンに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    if( fread( &num, sizeof(num), 1, fp ) < 1 ){
        fputs( "読み込み中にエラーが発生しました。\n", stderr );
        exit( EXIT_FAILURE );
    }
    if( fread( &d, sizeof(d), 1, fp ) < 1 ){
        fputs( "読み込み中にエラーが発生しました。\n", stderr );
        exit( EXIT_FAILURE );
    }
    if( fread( str, sizeof(str[0]), sizeof(str), fp ) < sizeof(str)) {
        fputs( "読み込み中にエラーが発生しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    printf( "%d\n", num );
    printf( "%f\n", d );
    printf( "%s\n", str );

    if( fclose( fp ) == EOF ){
        fputs( "ファイルクローズに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    return 0;
}

実行結果(標準出力)

900
7.850000
xyzxyz

バイナリデータは、テキストデータとは異なり、改行文字に対して特別な扱いを行いません。改行文字を含んだファイルを読み込んでも、それを改行だとみなすことはありません。

改行文字の表現は、環境によって異なり、C言語ではこれを '\n' という文字で表現することで差異を吸収しています。例えば、Windows環境での改行文字は CR と LF という2つの文字の組み合わせで表現されます。つまり、2バイト必要としますが、他の環境では CR だけであったり、LF だけであったりします。

シーク

ファイルの読み書きを行うとき、ファイル内のどの辺りを対象にしているかは、ファイルポジションという値で管理されています。ファイルの読み書きを行うと、ファイルポジションが自動的に移動します。

ファイルポジションを強制的に移動させる操作を、シークと呼びます。シークは、fseek関数で行えます。また、現在のファイルポジションは、ftell関数で取得できます。

しかし、テキストファイルに対するシークは制限が厳しくなっており、以下の操作しか保証されません。

  1. 第3引数を SEEK_SET にし、第2引数に 0L を指定⇒ファイルの先頭へ移動
  2. 第3引数を SEEK_CUR にし、第2引数に 0L を指定⇒現在位置のまま
  3. 第3引数を SEEK_END にし、第2引数に 0L を指定⇒ファイルの末尾へ移動
  4. 第3引数を SEEK_SET にし、第2引数に ftell関数の戻り値を指定⇒ftell関数が返した位置へ移動

バイナリファイルの場合には、このような制限はありませんが、次の使い方については結果の保証がありません。

これは、ファイルサイズを調べるためによく使われている以下のような関数が、実は環境依存な処理であるということです。

long GetFileSize(FILE* fp)
{
    long fpos_save, size;

    /* 現在のファイルポジションを保存 */
    fpos_save = ftell( fp );

    /* ファイルの末尾まで移動して、その位置を調べる */
    fseek( fp, 0, SEEK_END );
    size = ftell(fp);

    /* ファイルポジションを元に戻す */
    fseek( fp, fpos_save, SEEK_SET );

    return size;
}


また、fseek関数や ftell関数の引数は、long int型ですが、巨大なファイルを扱うには大きさが不足する可能性があります。そのような場合には、fsetpos関数や、fgetpos関数を使った方が良いでしょう。

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

#define FILE_NAME  "hello.txt"

void putFileLine(FILE* fp);

int main(void)
{
    FILE* fp;
    fpos_t pos;


    fp = fopen( "test.txt", "r" );
    if( fp == NULL ){
        fputs( "ファイルオープンに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    putFileLine( fp );     /* 1行目 */
    fgetpos( fp, &pos );   /* ファイルポジションを保存 */
    putFileLine( fp );     /* 2行目 */
    putFileLine( fp );     /* 3行目 */
    fsetpos( fp, &pos );   /* 保存しておいた位置へ復帰 */
    putFileLine( fp );     /* 2行目 */

    if( fclose( fp ) == EOF ){
        fputs( "ファイルクローズに失敗しました。\n", stderr );
        exit( EXIT_FAILURE );
    }

    return 0;
}

/*
    ファイルから1行読み取って、標準出力へ出力。
    引数
        fp:		FILEオブジェクトへのポインタ。
*/
void putFileLine(FILE* fp)
{
    char buf[80];

    fgets( buf, sizeof(buf), fp );
    fputs( buf, stdout );
}

入力ファイル(test.txt)

1行目
2行目
3行目
4行目
5行目

実行結果(標準出力)

1行目
2行目
3行目
2行目

バッファリング

バッファリングとは、データを一旦どこかに蓄えておき、あるタイミングでまとめて処理する方法のことです。C言語での入出力処理においても、バッファリングが使われていることがあります。

setvbuf関数を使えば、バッファリングの方法を変更できますが、環境への依存性が高く、うまく動作しないこともあるかも知れません。

バッファリングされているために、標準出力への出力が思ったタイミングで行われないことがあるかも知れません。バッファリングされている場合に、任意のタイミングで出力を実行するためには、fflush関数を使用します。
fflush関数を stdin(標準入力)に対して行うプログラムを見かけますが、これが正常に動作するかどうかは環境に依存します。

また、標準エラーストリームに関しては、ほとんどの場合はバッファリングが無効になっています。これは、バッファリングしてしまうと、エラーが発生したタイミングですぐに有益なログを書き出すことができなくなってしまうためです。

ファイルに対する操作

ファイルに対する幾つかの操作は、標準ライブラリ関数として用意されています。直接的には用意されていなくても、ほかの目的で使う関数をうまく使うことで実現できる場合もあります。

操作 方法
ファイルの新規作成 fopen関数の "w"オープンモードで開き、何も書き込まずに閉じる
ファイルの削除 remove関数
ファイルのコピー バイナリモードでコピー元とコピー先のファイルを開き、1バイトずつ fread関数fwrite関数を使って複写する。
ファイルの移動 rename関数が事実上、移動と同じことをしている
ファイルの名前変更 rename関数
ファイルが存在しているか調べる fopen関数の "r"オープンモードでオープンできるか試行する。
ファイルサイズを調べる バイナリモードで開き、fseek関数ftell関数を駆使して調べる。。

エンディアン

2バイト以上の大きさのデータが、メモリ上にどのような順番で並ぶのかは、エンディアンバイトオーダー)というもので規定されています。

0x00000384 という 4バイトのデータを、逆の順番で「0x84 0x03 0x00 0x00」のように並べる方式は、リトルエンディアン方式と呼ばれます。Windows環境では一般的に、この方式が使われています。

一方、そのままの順番で「0x00 0x00 0x03 0x84」のように並べる方式は、ビッグエンディアン方式と呼ばれます。

コマンドライン

プログラムを実行する際に、コマンドライン引数を渡すことができます。Windows環境であれば、次のような方法が使えます。

  1. コマンドプロンプトから実行し、そのときに引数を渡す
  2. 実行ファイルのショートカットを作成し、ショートカット側のプロパティにある「リンク先」のところに引数を記述。ショートカット側を実行する
  3. VisualStudio のプロジェクト設定を利用する

3つ目の方法は、プログラムを作成している段階に限った話ですから、完成品を誰かが実行するときには使えません。通常は1つ目の方法を採ることになるでしょう。

コマンドライン引数を受け取るプログラムは、2つの引数を持った形式の main関数を使って作成する必要があります。次のプログラムは、コマンドライン引数の内容を標準出力へ出力しています。

#include <stdio.h>

int main(int argc, char *argv[])
{
    int i;

    for( i = 1; i < argc; ++i ){
        puts( argv[i] );
    }

    return 0;
}

コマンドライン引数

test message

実行結果

test
message

main関数の仮引数argc には、argv の要素数が入っています。argv の方は、argv[0] にプログラムの名前が格納され、argv[1] 以降に、渡されたコマンドライン引数が順番通りに格納されています。ただし、argv[0] は "" となる環境があるかも知れません。


また、コマンドライン引数を持つプログラムを実行する際、以下のように記述することができます。

test < test.txt

このように指定すると、標準入力の代わりに test.txt の内容を使うようになります。これを、標準入力をリダイレクトすると言います。 同様のことを標準出力に対して行う場合は、以下のようにします。

test > test.txt

マルチバイト文字

マルチバイト文字(多バイト文字)は、1文字を表現するために必要な大きさが可変になっています。

C言語で char型を使って表現する文字は、マルチバイト文字です。それが具体的に、ASCIIコードなのか、Shift_JIS なのか、UTF-8 なのか、それとも他の何かなのかは、コンパイラによって異なります。VisualStudio では、Shift_JIS が使われています。

Windows環境でのC言語プログラムでは、マルチバイト文字の表現に Shift_JIS を用いることが一般的になっています。しかし、C言語では ASCIIコードを使って文字を表現することが基本です。Shift_JIS でもある程度うまく動作するのは、Shift_JIS が ASCIIコードに対する互換性を持っているためです。つまり、ASCIIコードで表現できる文字は、Shift_JIS でもまったく同じ表現が通用するようになっています。

Shift_JIS で表現された日本語の文字列に対して、strlen関数を使っても、思ったような値は返ってきません。Shift_JIS による日本語の文字は、1文字を 2バイトで表現しているものが多いのに対し、strlen関数は ASCIIコードのように、1文字は必ず 1バイトであると想定しているためです。

ASCIIコードでないマルチバイトの文字列の文字数を数えるには、次のように複雑なプログラムが必要になります。

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

int main(void)
{
    const char str[] = "日本語を使うテスト";
    int char_count;
    int i;

    if( setlocale( LC_CTYPE, "" ) == NULL ){
        fputs( "ロケールの設定に失敗しました。\n", stderr );
        return EXIT_FAILURE;
    }
    
    i = 0;
    char_count = 0;
    while( str[i] != '\0' ){
        int res = mblen( &str[i], MB_CUR_MAX );
        if( res < 0 ){
            fputs( "不正な文字を含んでいます。\n", stderr );
            return EXIT_FAILURE;
        }

        i += res;
        char_count++;
    }

    printf( "length: %d\n", char_count );

    return 0;
}

実行結果

length: 9

setlocale関数を使って LC_CTYPE の設定値を変更しておかないと、マルチバイト文字の文字コードを、デフォルトのまま扱ってしまいます。デフォルトは、"C"ロケールと呼ばれるもので、この場合、恐らく ASCIIコードが使われています。

setlocale関数の第2引数を "" とすると、環境が定義する基本設定(ネイティブロケール)に変更されます。

ワイド文字

ワイド文字は、現在のロケールに存在するすべての文字が表現できる文字表現です。

C言語で ワイド文字を扱うには wchar_t型を使います。wchar_t型は、環境でサポートされているすべてのロケールの中で、最も大きい文字を表現可能な大きさを持つ整数型です。

ワイド文字が具体的にどのような文字コードで表現されているのかは、コンパイラによって異なります。VisualStudio では、UTF-16 が使われています。

ワイド文字を操作するために、幾つかの標準ライブラリ関数が用意されています。これらの関数は、ASCIIコードを対象とする str~系の関数に対して、ワイド文字版は wcs~ という名称になっています。例えば、strcpy関数に対応するワイド文字版は wcscpy関数です。

wchar_t型をワイド文字以外の表現のために使うのは間違っています。マルチバイト文字の方は、char型(あるいは、その配列)で扱うのが正しいです。反対に、ワイド文字は常に wchar_t型で表現しないと、ワイド文字を表現する値が納まりきらない可能性が高いです。

ワイド文字とマルチバイト文字の相互変換は、mbtowc関数)や wctomb関数で行えます。

また、文字列の相互変換であれば、mbstowcs関数)、wcstombs関数を使います。


練習問題

まとめとして、多めに練習問題を用意しました。★の数は難易度を表します。

問題① 絶対パスと相対パスの意味を説明して下さい。[★]

問題② プログラムの処理結果を、標準出力ではなく、ファイルへ出力することの利点は何か説明して下さい。 [★]

問題③ ファイルパスを文字列で与えると、拡張子部分を返す関数を作成して下さい。[★★]

問題④ マルチバイト文字とワイド文字の違いを説明して下さい。[★]

問題⑤ Shift_JIS を使用する環境において、0x5C問題が起こる理由と、その対策を説明して下さい。 [★]

問題⑥ L"abcde" の大きさを調べたところ 24バイトでした。L'\0' は何バイトですか? [★]

問題⑦ ファイルの名前が wchar_t型の文字列として与えられたとき、その名前のファイルをオープンし、クローズするだけのプログラムを作成して下さい。[★★]

問題⑧ 文字列の中から、文字列を探す strstr関数という標準ライブラリ関数があります。この関数は、第1引数に指定した文字列中から、第2引数で指定した文字列と一致する部分を探し、見つかれば、先頭のメモリアドレスを返し、見つからなければヌルポインタを返します。
また、この関数のワイド文字列版に wcsstr関数があります。単に、char*型から wchar_t*型に変わっただけですが、これと同じことをする関数を自作して下さい。[★★]

問題⑨ C言語のソースファイルやヘッダファイルを読み込んで、コメント部分を除去した結果を出力するプログラムを作成して下さい。読み込むファイルの名前は、コマンドライン引数から受け取るようにして下さい。[★★★]

問題⑩ コマンドライン引数から、ファイルパスを2つ指定し、1つ目のファイルの内容を、2つ目のファイルへ追記するプログラムを作成して下さい。 例えば、

test in.txt out.txt

としたとき、in.txt の内容を、out.txt の末尾へ追記します。 [★★★]

問題⑪ コマンドライン引数から、ファイルパスと、任意の文字列を入力し、ファイル内検索を行うプログラムを作成して下さい。

例えば、

test test.txt Hello

としたとき、test.txt の内容から、"Hello" という文字列を探します。 1行の文字数は 80文字以内であることが保証されているものとします。 発見できたら、その行数を標準出力へ出力させて下さい。 なお、同じ文字列が、ファイル内に複数存在する可能性も考慮して下さい。 [★★★]

問題⑫ バッファリングの意味と、その価値、問題点を説明して下さい。 [★]

問題⑬ 小さめで単色のビットマップファイルを用意し、その内容をバイナリエディタで確認して下さい。 色を変えて再度確認し、内容がどう変化するか調べて下さい。 その結果から、どんなことが分かりますか? [★]

問題⑭ 問題⑬の結果を踏まえ、赤色の単色画像を読み込んで、青色の単色画像に変換して出力するプログラムを作成して下さい。 [★★]

問題⑮ コマンドライン引数から指定したファイルを読み込み、バイナリエディタのように整形して出力するプログラムを作成して下さい。出力先は標準出力として、リダイレクトによってファイルへも書き出せることを確認して下さい。[★★★]


解答ページはこちら

参考リンク

更新履歴

'2018/4/20 「NULL」よりも「ヌルポインタ」が適切な箇所について、「ヌルポインタ」に修正。

'2018/4/2 「VisualC++」という表現を「VisualStudio」に統一。

'2018/3/23 全面的に文章を見直し、修正を行った。

'2018/2/22 「サイズ」という表記について表現を統一。 型のサイズ(バイト数)を表しているところは「大きさ」、要素数を表しているところは「要素数」。

'2018/2/21 文章中の表記を統一(bit、Byte -> ビット、バイト)

≪更に古い更新履歴を展開する≫





前の章へ(第47章 ワイド文字)

次の章へ(第49章 ビット演算)

C言語編のトップページへ

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS