構造体のネスト

構造体を、別の構造体のメンバにすることもできる。これを「構造体のネスト」あるいは「ネストした構造体(nested structure)」と呼ぶ。
次の例は、10 人の作業員を配置する製造ラインの情報を格納するために、構造体をネストしている。

#define NUM_ON_LINE 10

struct worker {
    char name[80];
    int avg_units_per_hour;
    int avg_errs_per_hour;
};

struct asm_line {
    int product_code;
    double material_cost;
    struct worker wkers[NUM_ON_LINE];
} line1, line2;

製造ライン line1 の構造体配列 wkers の 2 番目の様相のメンバ avg_units_per_hour に値 12 を代入するには次のようにする。

line1.wkers[1].avg_units_per_hour = 12;

要は外から順にたどっていけばいい。

構造体へのポインタ

構造体へのポインタも、ほかの型の場合と同じように作ることができる。

struct s_type {
    int i;
    char str[80];
} s, *p;

上の例では、s_type という名前の構造体を定義し、その変数 s とポインタ p を宣言している。このとき p に s のアドレスを代入するには、

p = &s;

とする。これもほかの型の場合と一緒だな。
ただし、構造体へのポインタから構造体のメンバにアクセスするときは、ドット演算子ではなく、アロー演算子(->)を使う。

p->i = 100;

次のプログラムは、構造体へのポインタの使い方の簡単な例だ。

#include
#include

struct s_type {
    int i;
    char str[80];
} s, *p;

int main(void)
{
    p = &s;

    s.i = 10;
    p->i = 20;
    strcpy(p->str, "I like struct.");

    printf("%d %d %s\n", s.i, p->i, p->str);

return 0;
}

s_type 構造体の変数 s とポインタ p を宣言し、s のアドレスを p に代入しているので、p は構造体 s を指していることになる。
15 行目でドット演算子を使ってメンバに代入しているけど、次の行で今度はポインタからアロー演算子を使って違う値を代入している。結果として上書きしているわけだ。

takatoh@nightschool $ ./sample_10_2a
20 20 I like struct.

期待通り、s.i と p->i は同じ値を出力している。

構造体

構造体(structure)は互いに関連する2つ以上の変数で構成される複合型。構造体の持つ変数をメンバと呼び、それぞれのメンバは異なる型でも構わない。
構造体を定義するときの一般的な形式は次のとおり。

struct タグ名 {
    型 メンバ1;
    型 メンバ2;
    型 メンバ3;
    ...
    型 メンバN;
} 変数リスト;

タグ名は構造体につける名前だ。いってみれば型名のようなもの。
変数リストは、定義したい変数のリスト。
タグ名と変数リストは、どちらかは省略できる。タグ名を省略すると、名前のない構造体の変数を定義することになる。一方、変数リストを省略すると、タグ名のついて構造体だけの定義となって、実際に変数を使うときにはあとから改めて宣言する必要がある。
普通はタグ名だけつけていおいて、変数はあとで宣言するのがいいんではないかな。次の例では catalog というタグ名の構造体を定義して、あとからその変数を宣言している。

struct catalog {
    char name[40];
    char title[40];
    char pub[40];
    unsigned date;
    unsigned char ed;
};

struct catalog var1, var2, var3;

構造体を配列にすることもできる。

struct catalog cat[100];

配列の個々の構造体にアクセスするには、配列名に添字をつける。

cat[4]

構造体のメンバにアクセスするには、ドット演算子を使う。これは、メンバを参照するときもメンバに代入するときも同じ。構造体が配列になっている場合も同じ。

var1.date = 1776;
cat[50].ed = 2;

構造体を関数に渡すこともできるし、関数の戻り値とすることもできる。
また、両方の方が同じなら、構造体の変数(インスタンス)を別の変数にそのまま代入することもできる。
これはちょっと試してみよう。

#include

int main(void)
{
    struct s_type {
        int a;
        float f;
    } var1, var2;

    var1.a = 10;
    var1.f = 100.23;

    printf("var1: %d %f\n", var1.a, var1.f);

    var2 = var1;

    printf("var2: %d %f\n", var2.a, var2.f);

    var2.a = 5;
    var2.f = 10.56;

    printf("var1: %d %f\n", var1.a, var1.f);
    printf("var2: %d %f\n", var2.a, var2.f);

return 0;
}

構造体 var1 と var2 を宣言して var1 のメンバに値を代入したあと、var1 をそのまま var2 に代入する。それから var2 のメンバの値を変更してみる。

takatoh@nightschool $ ./sample_10_1a
var1: 10 100.230003
var2: 10 100.230003
var1: 10 100.230003
var2: 5 10.560000

最初の2行は var2 に var1 を代入した直後の出力。当然同じ値になっている。
次の2行は、var2 のメンバを変更したあとの出力。var2 のメンバは変更されているのに対して、var1 のメンバは変更されていない。てことは、構造体の代入では値がコピーされるってことでいいのかな。

構造体のサイズを調べるときには sizeof を使う。各メンバのサイズの合計を計算しようとしてはいけない。環境によっては構造体のサイズがメンバのサイズの合計と同じにならないからだ。

#include

struct s_type {
    int i;
    char ch;
    int *p;
    double d;
} s;

int main(void)
{
    printf("size of s_type is %ld bytes.\n", sizeof(struct s_type));

    return 0;
}
takatoh@nightschool $ ./sample_10_1b
size of s_type is 24 bytes.

標準ストリーム

C のプログラムが実行を開始すると、自動的に3つのストリーム、stdin(標準入力)、stdout(標準出力)、stderr(標準エラー出力)が開かれて使えるようになる。これらのストリームは FILE ポインタなので、例えばストリームに出力する fprintf() を使って画面に出力することもできる。

fprintf(stdout, "%d %c %s\n", 100, 'c', "This is a string.");

ただし、stdin、stdout、stderr は変数ではないことに注意。fopen() で割荒れることは出来ないし、fclose() で閉じることも出来ない。自由に利用できるけど、変更してはいけない。

rename()、remove()、rewind()

rename()

rename() はファイル名を変更する。

int rename(char *旧ファイル名, char *新ファイル名);

成功すると 0 を返し、エラーが発生すると 0 以外の値を返す。

#include
#include

int main(int argc, char *argv[])
{
    if (rename(argv[1], argv[2])) {
        printf("Error.\n");
        exit(1);
    } else {
        printf("Renamed successfully.\n");
    }

    return 0;
}
takatoh@nightschool $ ls *.txt
myfile.txt
takatoh@nightschool $ ./sample_9_7a myfile.txt yourfile.txt
Renamed successfully.
takatoh@nightschool $ ls *.txt
yourfile.txt

remove()

remove() はファイルを削除する。

int remove(char *ファイル名);

成功すると 0 を返し、エラーが発生すると 0 以外の値を返す。

#include
#include

int main(int argc, char *argv[])
{
    if (rename(argv[1], argv[2])) {
        printf("Error.\n");
        exit(1);
    } else {
        printf("Renamed successfully.\n");
    }

    return 0;
}
takatoh@nightschool $ ls *.txt
yourfile.txt
takatoh@nightschool $ ./sample_9_7b yourfile.txt
Remove successfully.
takatoh@nightschool $ ls *.txt
ls: *.txt にアクセスできません: そのようなファイルやディレクトリはありません

rewind()

rewind() はストリームの現在位置を先頭に巻き戻す。

void rewind(FILE *ストリーム);

rewind() に戻り値はない。開くことが出来たファイルはすべて巻き戻すこともできるから。
次のプログラムは、ファイルの内容を表示したあと、先頭まで巻き戻してもう一度表示する。

#include
#include

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

    if (argc != 2) {
        printf("Filename not given.\n");
        exit(1);
    }

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

    /* display file contents */
    while (!feof(fp)) {
        putchar(getc(fp));
    }

    /* rewind */
    rewind(fp);

    /* display once more */
    while (!feof(fp)) {
        putchar(getc(fp));
    }

    fclose(fp);

    return 0;
}
takatoh@nightschool $ ./sample_9_7c myfile
Hello
I'm takatoh
How are you?
�Hello
I'm takatoh
How are you?
�takatoh@nightschool $

なんかファイルの最後に変な文字が出力されてるな。何だろ?

[追記]

上の「変な文字」にいついてコメントで教えてもらった。詳しくはコメントを見て欲しいけど、要するに getc() が返した EOF を出力している、てことみたいだ。
次のように getc() の戻り値をチェックするように変更したらでなくなった。

#include
#include

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

    if (argc != 2) {
        printf("Filename not given.\n");
        exit(1);
    }

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

    /* display file contents */
    while (!feof(fp)) {
        ch = getc(fp);
        if (ch == EOF) {
            break;
        }
        putchar(ch);
    }

    /* rewind */
    rewind(fp);

    /* display once more */
    while (!feof(fp)) {
        ch = getc(fp);
        if (ch == EOF) {
            break;
        }
        putchar(ch);
    }

    fclose(fp);

    return 0;
}
takatoh@nightschool $ ./sample_9_7d myfile
Hello
I'm takatoh
How are you?
Hello
I'm takatoh
How are you?

ランダムアクセス

fread() はストリームの「現在位置」からデータを読み込む。任意の位置のデータを読み込むためにはこの「現在位置」を動かしてやる必要がある。
fseek() はそのための関数だ。

int fseek(FILE *ストリーム, long オフセット, int 開始位置);

fseek() は「開始位置」から「オフセット」バイトだけ「現在位置」を移動する。「開始位置」は次のマクロのどれかでなければいけない。

SEEK_SETファイルの先頭から
SEEK_CUR現在位置から
SEEK_ENDファイルの終わりから

これらのマクロは stdio.h で定義されている。

fseek() は成功すると 0 を返し、失敗すると 0 以外の値を返す。

ところで、ファイルの現在位置を知りたいときは ftell() を使う。

long ftell(FILE *ストリーム);

ftell() は成功すると現在位置を返し、失敗すると -1 を返す。

次のプログラムは、10 個のデータ(double型)をファイルに書き込み、ファイルを開き直す。そして何番目のデータを表示したいかをユーザーに尋ねて、それを表示する。

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

double d[10] = {
    10.23,
    19.87,
    1002.23,
    12.9,
    0.897,
    11.45,
    75.34,
    0.0,
    1.01,
    875.875
};

int main(void)
{
    long loc;
    double value;
    FILE *fp;

    /* write array data */
    if ((fp = fopen("myfile", "wb")) == NULL) {
        printf("Cannot open file.\n");
        exit(1);
    }

    if (fwrite(d, sizeof d, 1, fp) != 1) {
        printf("Error: at writing.\n");
        exit(1);
    }
    fclose(fp);

    /* re-open file */
    if ((fp = fopen("myfile", "rb")) == NULL) {
        printf("Cannot open file.\n");
        exit(1);
    }

    printf("What number of element? [0-9] ");
    scanf("%ld", &loc);
    if (fseek(fp, loc * sizeof(double), SEEK_SET)) {
        printf("Seek error.\n");
        exit(1);
    }

    fread(&value, sizeof(double), 1, fp);
    printf("Element %ld is %f\n", loc, value);

    fclose(fp);

    return 0;
}

47 行目で fseek() を使っている。loc に double 型のサイズをかけて、移動すべきバイト数を求めている。

実行例:

takatoh@nightschool $ ./sample_9_6
What number of element? [0-9] 4
Element 4 is 0.897000

バイナリデータの入出力

fread()とfwrite()

バイナリデータを読み書きするには、fread() と fwrite() 関数を使う。これらはどんなデータでもバイナリ表現を使って読み書きができる。一般的な形式は次のとおり。

size_t fread(void *バッファ, size_t サイズ, size_t 数値, FILE *ストリーム);
size_t fwrite(void *バッファ, size_t サイズ, size_t 数値, FILE *ストリーム);

fread() は「ストリーム」から「サイズ」バイトの大きさのオブジェクトを「数値」個読み込んで、「バッファ」の指す領域に格納する。戻り値は実際に読み込んだオブジェクトの数。これが「数値」よりも小さければ、エラーが発生したか、ファイルの終わりに達したことになる。どちらかは feof() と ferror() で調べられる。

fwrite() は逆のことを行う。「バッファ」から「サイズ」バイトの大きさのオブジェクトを「数値」個、「ストリーム」に書き込む。戻り値は書き込んだオブジェクト数で、「数値」よりも小さければエラーが発生したということだ。

void型ポインタ

上の、「void *バッファ」というのは、void 型のポインタだ。void 型のポインタは、型変換を行わずに任意の型のデータを指すことのできるポインタで、汎用ポインタ(generic pointer)と呼ばれる。
fread() や fwrite() ではどのような型のデータを扱うかわからないので(違う言い方をすると、どんな型のデータでも扱えるように)、void 型のポインタを使っている。

size_t

size_t 型は stdio.h で定義されている型で、この型の変数はコンパイラがサポートする最大オブジェクトの大きさを持つ値を保持できる。基本の型ではなく size_t 型を使うのは、コンパイラがそれぞれの環境の違いを吸収できるようにするため。

sizof

sizeof 演算子は型、あるいは変数の大きさをバイト数で返す。

sizeof(型);
sizeof 変数;

sizeof を型に対して使うときにはカッコで囲む必要があるけど、変数に対して使うときにはカッコが囲んでも囲まなくてもいい。

例1

次のプログラムは、10 この要素(double 型)をバイナリでファイルに書き込み、開きなおして読み込んで、画面に表示する。当然だけど、ファイルを開くときはバイナリモードで開く必要がある。

#include
#include

double d[10] = {
    10.23,
    19.87,
    1002.23,
    12.9,
    0.897,
    11.45,
    75.34,
    0.0,
    1.01,
    875.875
};

int main(void)
{
    int i;
    FILE *fp;

    /* write array data */
    if ((fp = fopen("myfile", "wb")) == NULL) {
        printf("Cannot open file.\n");
        exit(1);
    }

    for (i = 0; i < 10; i++) {
        if (fwrite(&d[i], sizeof(double), 1, fp) != 1) {
            printf("Error: at writing.\n");
            exit(1);
        }
     }
    fclose(fp);

    /* clear array */
    for (i = 0; i < 10; i++) {
        d[i] = -1.0;
    }

    /* read array data */
    if ((fp = fopen("myfile", "rb")) == NULL) {
        printf("Cannot open file.\n");
        exit(1);
    }
    for (i = 0; i < 10; i++) {
        if (fread(&d[i], sizeof(double), 1, fp) != 1) {
            printf("Error: at reading.\n");
            exit(1);
        }
    }
    fclose(fp);

    /* display array data */
    for (i = 0; i < 10; i++) {
        printf("%f\n", d[i]);
    }

    return 0;
}
takatoh@nightschool $ ./sample_9_5a
10.230000
19.870000
1002.230000
12.900000
0.897000
11.450000
75.340000
0.000000
1.010000
875.875000

ちなみに myfile ファイルを cat すると、

takatoh@nightschool $ cat myfile
�(\u$@��Q��3@�p=
�Q������)@NbX9��?fffff�&@�(\�R@)\�(�?_�@takatoh@nightschool $

となっている。バイナリだから人間には読めない。

例2

上の例では、配列の要素を1つずつ書き込み・読み込みしていたけど、配列はメモリ上に連続して確保されているので、1つの塊として1度で済ませることもできる。

#include
#include

double d[10] = {
    10.23,
    19.87,
    1002.23,
    12.9,
    0.897,
    11.45,
    75.34,
    0.0,
    1.01,
    875.875
};

int main(void)
{
    int i;
    FILE *fp;

    /* write array data */
    if ((fp = fopen("myfile", "wb")) == NULL) {
        printf("Cannot open file.\n");
        exit(1);
    }

    if (fwrite(d, sizeof d, 1, fp) != 1) {
        printf("Error: at writing.\n");
        exit(1);
    }
    fclose(fp);

    /* clear array */
    for (i = 0; i < 10; i++) {
        d[i] = -1.0;
    }

    /* read array data */
    if ((fp = fopen("myfile", "rb")) == NULL) {
        printf("Cannot open file.\n");
        exit(1);
    }
    if (fread(d, sizeof d, 1, fp) != 1) {
        printf("Error: at reading.\n"); exit(1);
    }
    fclose(fp);

    /* display array data */
    for (i = 0; i < 10; i++) {
        printf("%f\n", d[i]);
    }

    return 0;
}

30 行目で配列を一気に書き込み、49 行目では一気に読み込んでいる。結果は同じになる。

takatoh@nightschool $ ./sample_9_5b
10.230000
19.870000
1002.230000
12.900000
0.897000
11.450000
75.340000
0.000000
1.010000
875.875000

高レベルテキスト関数

fputs()

fputs() はストリームに文字列を書き込む。

int fputs(char *文字列, FILE *ストリーム);

fputs() は書き込みが成功すると負でない値を返し、エラーが発生すると EOF を返す。文字列の終端にあるヌル文字は書き込まれず、改行文字が追加されることもない。

fgets()

char *fgets(char *文字列, int 数値, FILE *ストリーム);

fgets() はストリームから文字を読み込み、文字列に書き込んでいく。この動作は、

  • 数値 – 1 個分の文字を読む
  • 改行文字に出会う
  • ファイルの終わりに達する

まで続けられる。どの場合にも、文字列の終端にはヌル文字が付け加えられる。
fgets() は成功すると文字列へのポインタを返し、エラーが発生するとヌルポインタを返す。

例1

次のプログラムは、キーボードから入力された文字列を読み込み、ファイルへ保存する。そしてファイルを開きなおして、内容を表示する。

#include
#include
#include

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

    /* check parameters */
    if (argc != 2) {
        printf("Specify file name\n");
        exit(1);
    }

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

    printf("Input string. Empty to quit.\n");
    do {
        printf(": ");
        gets(str);
        strcat(str, "\n");
        if (*str != '\n') {
            fputs(str, fp);
        }
    } while (*str != '\n');
    fclose(fp);

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

    /* read from file */
    do {
        fgets(str, 80, fp);
        if (!feof(fp)) {
            printf("%s", str);
        }
    } while (!feof(fp));
    fclose(fp);

    return 0;
}
takatoh@nightschool $ ./sample_9_4a sample_9_4a.txt
Input string. Empty to quit.
: Hello
: I'm takatoh
: How are you?
: 
Hello
I'm takatoh
How are you?
takatoh@nightschool $ cat sample_9_4a.txt
Hello
I'm takatoh
How are you?

fprintf()とfscanf()

これらは printf() と scnf() のストリーム版だ。コンソールから入出力する代わりに、ストリームから入出力する。

int fprintf(FILE *ストリーム, char *制御文字列, ...);
int fscanf(FILE *ストリーム, char *制御文字列, ...);

例2

次のプログラムは、double 値と int 値と文字列をコマンドラインで指定されたファイルに書き込み、それから、ファイルから読み込みなおして表示する。

#include
#include
#include

int main(int argc, char *argv[])
{
    FILE *fp;
    double ld;
    int d;
    char str[80];

    /* chech parameters */
    if (argc != 2) {
        printf("Specify file name\n");
        exit(1);
    }

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

    fprintf(fp, "%f %d %s", 12345.342, 1908, "hello");
    fclose(fp);

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

    fscanf(fp, "%lf%d%s", &ld, &d, str);
    printf("%f %d %s\n", ld, d, str);
    fclose(fp);

    return 0;
}
takatoh@nightschool $ ./sample_9_4b
Specify file name
takatoh@nightschool $ ./sample_9_4b sample_9_4b.txt
12345.342000 1908 hello
takatoh@nightschool $ cat sample_9_4b.txt
12345.342000 1908 hellotakatoh@nightschool $

書き込んだファイルの最後に改行文字がついてないせいで次のプロンプトが表示されちゃてるけど、画面に表示されるのと同じように、ファイルにも書き込まれているのがわかる。

練習問題9.3

1. コマンドラインで指定されたファイル(テキストまたはバイナリ)のバイト数を数え、その結果を表示するプログラムを作成してください。

#include
#include

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

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

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

    while (!feof(fp)) {
        ch = fgetc(fp);
        if (ferror(fp)) {
            printf("Error: at reading file.\n");
            exit(1);
        }
        count++;
    }

    if (fclose(fp) == EOF) {
        printf("Error: at closing file.\n");
        exit(1);
    }

    printf("%ld bytes\n", count - 1);

    return 0;
}

出力部分で – 1 しているのは、EOF も数えてしまっているから、それを差し引いている。

実行例:

takatoh@nightschool $ ./practice_9_3_1 sample_9_3
8925 bytes
takatoh@nightschool $ ls -l sample_9_3
-rwxrwxr-x 1 takatoh takatoh 8925  5月 10 19:11 sample_9_3

feof()とferror()

fgetc() が EOF を返した場合、エラーが起こったのかファイルの終わりに達したのか判定できない。バイナリファイルを読み込んでいる途中で EOF と同じ値が返ってきた時もファイルの終わりなのか有効なデータなのか判定できない。
これらを判定するためには、feof() と ferror() を使う。一般的な形式は次のとおり。

int feof(FILE *ストリーム);
int ferror(FILE *ストリーム);

feof() はストリームの終わりに達していると 0 以外の値(真)を返し、そうではければ 0 (偽)を返す。
ferror() はエラーが起これば 0 以外の値を返し、そうでなければ 0 を返す。

FILE *fp;
.
.
.
while (!feof(fp)) {
    ch = fgetc(fp);
    if (ferror(fp)) {
        printf("File error!\n");
        break;
    }
}

上のコードでは、feof(fp) が偽のあいだ while でループし、ストリームからデータを読み込むたびに ferror(fp) でエラーチェックをしている。

次のプログラムはファイルのコピーを行う。その際、完全なエラー検査を行う。

#include
#include

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

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

    /* open src file */
    if ((from = fopen(argv[1], "rb")) == NULL) {
        printf("Cannot open src file.\n");
        exit(1);
    }

    /* open dest file */
    if ((to = fopen(argv[2], "wb")) == NULL) {
        printf("Cannot open dest file.\n");
        exit(1);
    }

    /* copy file */
    while (!feof(from)) {
        ch = fgetc(from);
        if (ferror(from)) {
            printf("Error: at reading file.\n");
            exit(1);
        }
        if (!feof(from)) {
            fputc(ch, to);
        }
        if (ferror(to)) {
            printf("Error: at writing to file.\n");
            exit(1);
        }
    }

    if (fclose(from) == EOF) {
        printf("Error: at closing src file.\n");
        exit(1);
    }

    if (fclose(to) == EOF) {
        printf("Error: at closing dest file.\n");
        exit(1);
    }

    return 0;
}

実行例:

takatoh@nightschool $ ./sample_9_3
Usage: ./sample_9_3  
takatoh@nightschool $ ./sample_9_3 myfile myfile2
takatoh@nightschool $ cat myfile2
This is a test for file system.