ファイルシステムの基礎

ファイルシステムというか、ファイル入出力の基礎。

fopen()

ファイルを開くとき、つまりファイルをストリームと結びつけるときには fopen() を使う。fopen() の一般的な形式は次のとおり。

FILE *fopen(char *ファイル名, char *モード);

本(「独習 C」)に習ってプロトタイプ風に書いてみた。FILE * は戻り値の型を示していて、FILE という構造体へのポインタだ。
fopen() をはじめ、ファイルシステム関連の関数は stdio.h を使う。FILE も stdio.h で定義されている。
モードは、書き込みか読み込みか、あるいはテキストかバイナリかなどを示す文字列(へのポインタ)だ。次のようなモードがある。

r読み込み用にテキストファイルを開く
w書き込み用にテキストファイルを開く
aテキストファイルに追加する
rb読み込み用にバイナリファイルを開く
wb書き込み用にバイナリファイルを開く
abバイナリファイルに追加する
r+読み込み/書き込み用にテキストファイルを開く
w+読み込み/書き込み用にテキストファイルを作成する
a+読み込み/書き込み用にテキストファイルに追加する。またはテキストファイルを作成する。
r+b読み込み/書き込み用にバイナリファイルを開く
w+b読み込み/書き込み用にバイナリファイルを作成する
a+b読み込み/書き込み用にバイナリファイルに追加する。またはバイナリファイルを作成する。

ファイルを開く操作が失敗すると、fopen() はヌルポインタを返す。stdio.h では NULL というマクロがヌルポインタとして定義されている。ファイルを開いた時には、それが成功したかどうかをチェックしなければいけない。

FILE *fp;

if ((fp = fopen("myfile", "r")) == NULL) {
    printf("Channot open the file.");
    exit(1);
}

fclose()

ストリームが不要になった時にはこれを閉じる必要がある。ストリーム(ファイル)を閉じるには fclose() を使う。

int fclose(FILE *ストリーム);

文字の読み込みと書き込み

文字の読み込みには fgetc()、書き込みには fputc() をつかう。

int fgetc(FILE *ストリーム);
int fputc(int 文字, FILE *ストリーム);

fgetc() はストリームの次のバイトを unsigned char として読み込み、int として返す。エラーが起こったり、ファイルの終わりに達すると EOF を返す。fgetc() は int を返すけど、読み込んだ文字は下位バイトに含まれるので char に代入することもできる。
fputc() はストリームに「文字」の下位バイトを unsigned char の値として書き込む。「文字」は int と定義されているけど、char を渡しても問題ない。書き込みが成功すると書き込んだ文字を返し、失敗すると EOF を返す。

次のプログラムは、文字列をテキストファイル myfile に書き込み、いったん閉じたあと読み込み用に開きなおして、内容を表示する。

#include
#include

int main(void)
{
    char str[80] = "This is a test for file system.\n";
    FILE *fp;
    char ch, *p;

    /* open myfile to write */
    if ((fp = fopen("myfile", "w")) == NULL) {
        printf("Channot open the file.\n");
        exit(1);
    }

    /* write str. */
    p = str;
    while (*p) {
        if (fputc(*p, fp) == EOF) {
            printf("Error.\n");
            exit(1);
        }
        p++;
    }
    fclose(fp);

    /* open myfile to read */
    if ((fp = fopen("myfile", "r")) == NULL) {
        printf("Channot open the file.\n");
        exit(1);
    }

    /* read the file */
    for ( ; ; ) {
        ch = fgetc(fp);
        if (ch == EOF) {
            break;
        }
        putchar(ch);
    }
    fclose(fp);

    return 0;
}

実行結果:

takatoh@nightschool $ ./sample_9_2
This is a test for file system.

練習問題9.2

1. コマンドライン引数で指定したテキストファイルの内容を表示するプログラムを作成してください。

#include
#include

int main(int argc, char *argv[])
{
    FILE *fp;
    char ch;

    if (argc != 2) {
        printf("Usage: %s \n", argv[0]);
        exit(1);
    }

    if ((fp = fopen(argv[1], "r")) == NULL) {
        printf("Cannot open file: %s\n", argv[1]);
        exit(1);
    }

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

    return 0;
}
takatoh@nightschool $ ./practice_9_2_1
Usage: ./practice_9_2_1 <textfile>
takatoh@nightschool $ ./practice_9_2_1 myfile
This is a test for file system.

ストリームとファイル

C の入出力システムでは、プログラムと実際のデバイスとのあいだに抽象的なレベルを設けている。これをストリームという。ストリームのおかげで、プログラムからは実際のデバイスにかかわらずほとんど同じように扱える。
これに対して、実際のデバイスをファイルと呼ぶ。この文脈ではいわゆる普通のファイルだけでなく、画面、キーボード、ポートなどを含んでいる。

ストリームには、テキストストリームとバイナリストリームがある。
テキストストリームは ASCII 文字を扱う。テキストストリームでは、何らかの文字変換が行われることがある。例えば、改行文字を出力する場合には大抵復帰改行に変換される。なので、ストリームに送られるデータとファイルに書き込まれるデータが一致するとは限らない。
一方、バイナリストリームはどんな形式のデータでも扱うことが出来、文字変換も行わない。ストリームに送られれデータとファイルに書き込まれるデータは一致する。

もうひとつ、現在位置(current possition)という概念がある。現在位置は次のファイルアクセスが起こる位置を示している。例えば、100バイトのファイルがあり、すでに半分読み込んでいるとすると、次のアクセスでは 50 バイトめ(51バイトめ?)であり、これが現在位置ということになる。

ふむ、まあ、何とかわかった。次はいよいよファイルアクセスだ。

scanf()

scanf() の一般的な形式は次のとおり。

scanf(制御文字列, ...);

制御文字列は printf() とよく似たフォーマット指定子とその他の文字からなる。「…」の部分は可変長の引数で、制御文字列内のフォーマット指定子と対応する。これらの引数はフォーマット指定子の指定する型のアドレスでなければならない。scanf() 自体は入力されたフィールドの数を返す。

フォーマット指定子

%c文字
%d10進数
%i10進数
%e浮動小数点数
%f浮動小数点数
%g浮動小数点数
%o8進数
%s文字列
%x16進数
%pポインタ
%nこれまで読み込まれた文字数に等しい整数値を受け取る
%u符号なし整数
%[]文字集合

%c と %n 以外については最大フィールド幅を指定できる。
注意しなくちゃいけないのは %s は文字列を読み込むけど、空白文字が現れるとそこで読み込みをやめてしまうこと。例えば、

This is a test.

という文字列を %s で読み込もうとしても、「This」しか読み込まれない。

#include

int main(void)
{
    char str[80];

    printf("Input string: ");
    scanf("%s", str);
    printf("%s\n", str);

    return 0;
}
takatoh@nightschool $ ./sample_8_6
Input string: This is a test.
This

こういう場合は代わりに gets() を使う。

文字集合(スキャン集合)

%[ ] は大カッコで囲まれた文字を入力する。これをスキャン集合と呼ぶ。例えば

%[ABC]

という指定は A、B、C の続く限り読み込まれる。ほかの文字が現れた時点で次のフォーマット指定子に移る。[ ] 内の最初に ^ をつけることで否定を意味する。

#include

int main(void)
{
    char str[80];

    printf("Input string: ");
    scanf("%[a-z]", str);
    printf("%s\n", str);

    return 0;
}
takatoh@nightschool $ gcc sample_8_6b.c -o sample_8_6
takatoh@nightschool $ ./sample_8_6
Input string: ABCdefGHI
�l,

ありゃ、文字化けする。なんかおかしいんだろうか。

フォーマット指定子内の*

フォーマット指定子に * をつけると、そのフィールドは読み込まれずに捨てられる。入力に不要な文字が含まれている場合に便利。

int first, second;
scanf("%d%*c%d", &first, &second);

に対して

123-456

という入力があったとすると、first には 123、second には 456 が入力される。捨てられるフィールドに対応する引数は不要。

#include

int main(void)
{
    int first, second;

    printf("Input: ");
    scanf("%d%*c%d", &first, &second);
    printf("%d %d\n", first, second);

    return 0;
}
takatoh@nightschool $ ./sample_8_6c
Input: 123-456
123 456

制御文字列内のその他の文字の働き

制御文字列内に空白文字がある場合、scanf() は空白文字でない文字が現れるまで空白文字を捨てていく。その他の文字がある場合、一致しない文字が現れるまでその文字すべてを捨てていく。

include

int main(void)
{
    int first, second;

    printf("Input: ");
    scanf("A%d% %d", &first, &second);
    printf("%d %d\n", first, second);

    return 0;
}
takatoh@nightschool $ gcc sample_8_6d.c -o sample_8_6d
sample_8_6d.c: In function ‘main’:
sample_8_6d.c:9:5: warning: unknown conversion type character 0x20 in format [-Wformat=]
     scanf("A%d% %d", &first, &second);
     ^
takatoh@nightschool $ ./sample_8_6d
Input: AAA123    456
0 0

あれ、これもうまくいかない。

printf()

printf() はこれまでにも使ってきたけど改めて。一般的な形式は次のとおり。

printf(制御文字列, ...);

「…」の部分は可変長の引数で、制御文字列内に現れるフォーマット指定子に(数、順番ともに)対応する。printf() 自体は出力した文字数を返す。
フォーマット指定子は次のものがある。

%c文字
%d符号付き10進整数
%i符号付き10進整数
%e指数部付き表記(小文字)
%E指数部付き表記(大文字)
%f10進の浮動小数点数
%g%eと%fのいずれか短いほう
%G%Eと%fのいずれか短いほう
%o符号なし8進数
%s文字列
%u符号なし10進数
%x符号なし16進数(小文字)
%X符号なし16進数(大文字)
%pポインタを表示
%n対応する引数は整数へのポインタでなければならず、その領域にこれまで出力された文字数を書き込む
%%%記号

%%、%c、%p、%n以外は最小フィールド幅指定子と精度指定子を指定できる。また、デフォルトでは数値は右詰めで表示されるけど、-記号をつけることで左詰めにできる。ま、だいたいわかるな。

ちょっとわかりづらいのは %n だ。これは出力するためのものではなくて、出力した文字数を保存するためのものだ。次のプログラムはその例。

#include

int main(void)
{
    int i;

    printf("%d %f\n%n", 100, 123.45, &i);
    printf("Output %d characters\n", i);

    return 0;
}
takatoh@nightschool $ ./sample_8_5
100 123.450000
Output 15 characters

%n が現れるまでに 15 文字出力したと(15 文字目は改行文字)。

gets()とputs()

gets() はキーボードから文字列を読み込む。puts() はコンソールに文字列を出力する。どちらも stdio.h が必要。

gets()

キーボードからの入力というのは、ユーザーが文字列に続いて Enter キーを押すまで。Enter キーが押されると、文字列を読み込んで最後にある改行をヌル文字に置き換えたうえで引数の配列に格納する。関数自体の戻り値は、入力が成功すると配列の先頭へのポインタを、失敗するとヌルポインタを返す。
次のプログラムでは、gets() の戻り値を p に格納して、p がヌルでないことを確認してから出力している。p も str も配列(文字列)の先頭のアドレスを保持しているので、同じ文字列が2回出力されるはず。

#include

int main(void)
{
    char *p, str[80];

    printf("Input string: ");
    p = gets(str);
    if (p) { /* if p is not null */
        printf("%s %s\n", p, str);
    }

    return 0;
}

前にも書いたと思うけど、コンパイルすると gets は危険だから使うべきじゃないと警告される。

実行例:

takatoh@nightschool $ ./sample_8_4a
Input string: hello
hello hello

puts()

puts() はポインタの指している文字列を出力する。その際、自動的に改行を追加する。

#include

int main(void)
{
    puts("A");
    puts("B");
    puts("C");

    return 0;
}
takatoh@nightschool $ ./sample_8_4b
A
B
C

ちゃんと1つずつ改行されてる。
puts() は printf() よりもサイズが小さくて速い。なので、複雑な文字列を出力するのでなければ puts() を使ったほうが有利。

#defineプリプロセッサディレクティブ

C のプリプロセッサはコンパイルの前にソースコードに対して様々な操作を行う。プリプロセッサへの命令がプリプロセッサディレクティブ。これまでにヘッダファイルを読み込む #include を使ってきた。
ここでもうひとつ、#define というディレクティブが出てきた。大まかに言うと、ソースコード中の特定の文字列を別の文字列に置き換えるものだ。これはマクロ置換(macro substitution) と呼ばれる。#define の一般的な形式は次のとおり。

#define マクロ名 文字列

マクロ名は C で有効な識別子なら何でもいい。大文字も小文字も使える。ただ、大文字で書くのが通常だそう。
マクロ名と文字列は1つ以上の空白文字で区切られていなければいけない。
文字列の方は途中に空白を含んでも構わない。ただし、ディレクティブ自体が1行で完結している必要があるので、行末までが有効な文字列ということになる。

次の例は MAX というマクロを定義している。プリプロセッサがこれを処理すると、for ループの中の MAX という文字列が 100 という文字列に置き換わる。

#incluce

#define MAX 100

int main(void)
{
    int i;

    for (i = 0; i < MAX; i++) {
        printf("%d\n", i);
    }

    return 0;
}

結果としてこのループは、コンパイラにはこう見える。

for (i = 0; i < 100; i++) {
    printf("%d\n", i);
} 

いったん定義したマクロはほかのマクロの中でも使える。次の例は正しい使い方だ。

#define SMALL 1
#define MEDIUM SMALL + 1
#define LARGE MEDIUM + 1 

マクロ名が二重引用符で囲まれた中に出てきた場合には、置き換えが行われない。例えば、

#define ERROR "error ocured."

とあっても、

printf("ERROR: Try again.\n")

のなかの ERROR は置き換えられない。これはちょっと注意だな。

main()の引数

C のプログラムでは、コマンドライン引数を main() の2つの引数、argc と argv で受け取る。argc と argv という名前は決まったものではないらしいけど、伝統的のこの名前が使われているらしい。
argc は int 型で、コマンドライン引数の数を受け取る。argv は文字列ポインタの配列で、コマンドライン引数自体を受け取る。注意が必要なのは、コマンドラインに打ち込んだプログラム自体も含まれているということだ。なので、argc は 1 以上の整数になり、argv[0] にはプログラム名が入っている。

これらは次のように使う。

int main(int argc, char *argv[])
{
    ....
}

*argv[] となっているのは、コマンドライン引数の数が決まっていないから。

ちょっと試してみよう。次のプログラムは、コマンドライン引数の数と、それぞれの引数を出力する。

#include

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

    printf("%d\n", argc);

    for (i = 0; i < argc; i++) {
        printf("%s\n", argv[i]);
    }

    return 0;
}

実行例:

takatoh@nightschool $ ./sample_7_4a
1
./sample_7_4a
takatoh@nightschool $ ./sample_7_4a foo bar baz
4
./sample_7_4a
foo
bar
baz

さて、コマンドライン引数は文字列として渡されるので、もし数値として使いたいなら、文字列から数値へ変換しなければいけない。よく使われる関数は次のとおり。これらを使うには stdlib.h を include する必要がある。

int atoi(char *str);
double atof(char *str);
long atol(char *str);

long は long int のことね。
試してみよう。

#include
#include

int main(int argc, char *argv[])
{
    int i;
    double d;
    long l;

    i = atoi(argv[1]);
    d = atof(argv[2]);
    l = atol(argv[3]);

    printf("%d %f %ld\n", i, d, l);

    return 0;
}
takatoh@nightschool $ ./sample_7_4 2 3.5 200000
2 3.500000 200000

ところで、1つ疑問な点がる。それは「char *argv[]」という宣言だ。
最初に argv は文字列ポインタの配列だと書いた。だけどこの宣言はそうは見えない。「char ch」なら文字変数、「char str[]」なら文字の配列(文字列)。だとすると「char *argv[]」は文字列のポインタじゃないのか。それとも [] よりも * のほうが結合強度が強くて文字ポインタの配列か。どちらにせよよくわからないな。

引数の値渡しと参照渡し

int 型や float 型の引数を普通に渡すのが値渡し。このとき引数の値はコピーされて関数に渡されるので、関数の中で変更しても元の(つまり関数の外の)変数の値は変化しない。
これに対して、ポインタで渡すのが参照渡し。引数のポインタの持っているアドレスがコピーされて関数に渡されるけど、このアドレスは関数の呼び出し元のポインタと同じデータを指している。結果として、関数内で引数(ポインタ)の指すデータを変更すると、呼びさし側でもデータが変化することになる。

参照渡しの場合は、関数の仮引数をポインタとして定義する。次のプログラム中の関数 swap は2つの引数(ポインタ)をとって、相互に値を交換する。

#include

void swap(int *i, int *j);

int main(void)
{
    int num1, num2;

    num1 = 100;
    num2 = 800;

    printf("num1= %d, num2= %d\n", num1, num2);
    swap(&num1, &num2);
    printf("num1= %d, num2= %d\n", num1, num2);

    return 0;
}

void swap(int *i, int *j)
{
    int temp;

    temp = *i;
    *i = *j;
    *j = temp;
}

「void swap(int *i, int *j)」というのが、ポインタ引数の書き方だ。ここでは2つ使っている。
実行結果:

takatoh@nightschool $ ./sample_7_3
num1= 100, num2= 800
num1= 800, num2= 100

ちゃんと num1 と num2 の値が入れ替わった。

ところで、引数に配列(文字列を含む)を渡す場合、配列のアドレスが渡されるだけで配列そのものがコピーされるわけじゃない。これは参照渡しだ。だから、関数の仮引数はポインタ型として定義する必要がある。

#include

void f(int *num);

int main(void)
{
    int count[5] = {1, 2, 3, 4, 5};

    f(count);

    return 0;
}

void f(int *num)
{
    int i;

    for (i = 0; i < 5; i++) {
        printf("%d\n", num[i]);
    }
}
takatoh@nightschool $ ./sample_7_3a
1
2
3
4
5

再帰

C でも再帰関数が書ける。特別なことも必要なく、単に自分自身を呼びだせばいい。
次のプログラムは練習問題から。階乗を計算する。

#include

int fact(int n);

int main(void)
{
    int n;

    n = fact(5);

    printf("%d\n", n);

    return 0;
}

int fact(int n)
{
    if (n) {
        return n * fact(n - 1);
    } else {
        return 1;
    }
}
takatoh@nightschool $ ./sample_7_2
120

関数プロトタイプ

関数を使う前には、関数プロトタイプの宣言が必要。前にも書いたけどこんなふうに書く。

型 関数名(型 仮引数1, 型 仮引数2, ..., 仮引数N);

コンパイラはこの宣言から次の情報を得る。

  • 関数の戻り値の型
  • 仮引数の数
  • 仮引数の型

ANSI C では、歴史的経緯によりプロトタイプがなくてもいいことになっているそうだけど、関数を使う前に宣言しておくのが普通。というか常識。常に書くべし。

標準ライブラリの関数も、ヘッダファイルにプロトタイプが書かれている。使う関数によって対応するヘッダファイルを読み込む必要があるのはこのため。

関数の戻り値がない、仮引数がないときには、キーワード vold を使う。

void myfunc(int x);
int myfunc(void);

main 関数だけは特別で、プロトタイプが必要ない。