プログラミング応用演習 第 3 回構造体, ファイル入出力
先週の出席確認へのコメント 暗号を破りたいが 平文の候補が多すぎる 人間の目で確認する代わりに どんなプログラムがあればよいか? 辞書を挙げた人が多かった 正しい着眼です 何億個もの平文候補が想定されるので 形態素解析や品詞判別を挙げた人もいます 辞書に近い回答で悪くはないのですが 平文候補ごとにあまり高機能なものを呼び出すと時間がかかる 0.1 秒の操作を 3 億回繰り返すと 約 1 年かかる アルファベット同士の関係 を挙げた人がいます : 辞書よりも少ない計算で英語らしさを判定できる可能性がある
今日のお題 構造体 構造体の宣言 構造体型変数の宣言 構造体の要素へのアクセス 構造体へのポインタ ファイル ファイルを開く ファイルの読み書き ファイルを閉じる EOFという値
構造体
構造体の宣言 構造体は いくつかの値をまとめて扱うための型である 構造体の要素をメンバと呼ぶ 以下のように宣言する struct 構造体の名前 { }; 型メンバ名 ; 変数などの宣言と同様
構造体の宣言の例 例えば : 学生の成績を管理したい 各学生は 名前 学籍番号 点数の 3 つの属性を持つ struct student { char name[30]; }; char id[9]; int score; // 名前 30 文字までの文字列 // 学籍番号 これも文字列 // 点数 こんな風に宣言しておくと struct student を型として使える
構造体型の変数の宣言 / メンバへのアクセス struct student x; // struct student 型の変数 x の宣言 型 構造体型の値に. とメンバ名を付すと そのメンバに アクセスできる printf("%s\n", x.name); printf("%s\n", x.id); printf("%d\n", x.score);
構造体のメモリイメージ メモリ上にメンバの記憶場所が並ぶ ただし メモリ上の一定の間隔に合わせて配置される #include <stdio.h> struct test { }; int i; double d; char s[10]; int main() { } struct test x; printf("%p\n",&x); printf("%p\n",&x.i); printf("%p\n",&x.d); printf("%p\n",x.s); printf("%lu\n",sizeof(struct test)); return 0; x CPU は多バイトのメモリアクセスをするときに メモリの番地が一定の値 ( 例えば 8) で割り切れるところからしか読めない構成になっていることがある これをアライメント (alignment) と呼ぶ アライメントのためのパディング (padding) &x.i &x.d x.s パディング 構造体の配列を考えると 構造体そのものもアライメントに合わせた大きさにする必要がある
typedef 構造体型を使うたびに 毎回 struct というキーワードを書くのは面倒なので 型に別名を付けることを推奨する C 言語では 以下のように宣言することで 型に別名をつけることができる typedef 既存の型名新しい型名 ; 例 ) typedef struct student Student; typedef char byte;
typedef と構造体 構造体の宣言と その構造体型の別名宣言を同時に行うことが多い : 以下のような書き方ができる typedef struct student { char name[30]; char id[9]; int score; } Student; typedef と同時に宣言するときは struct の後ろの構造体名を省略できる ここに書くのは 新しい型名
構造体へのポインタ 構造体へのポインタを考えることができる struct test *p; // 構造体 test へのポインタ変数の宣言 構造体へのポインタのポインタ値は 構造体の最初のメンバへのポインタ値と同じ x &x &x.i
構造体へのポインタとメンバへのアクセス ポインタの中身を表す * 演算子と 構造体のメンバを表す. 演算子は. の方が結合が強い *p.i は *(p.i) と解釈されてしまう p を構造体へのポインタ変数とするとき このポインタが指す構造体のメンバ i へのアクセスを (*p).i と書ける が いちいち括弧を書くのは面倒くさい (*p).i を p->i と書くことができる 次回 たくさん使います
共用体 構造体はメンバを全て同時に保持するデータ構造 共用体は メンバのいずれか一つを表すデータ構造 #include <stdio.h> union test { }; int i; double d; char s[10]; int main() { union test x; printf("%p\n",&x); printf("%p\n",&x.i); printf("%p\n",&x.d); printf("%p\n",x.s); printf("%lu\n",sizeof(union test)); return 0; 共用体 test の宣言 共用体 test 型の変数 x の宣言 &x.i } x &x.d いずれかひとつ なので メンバの記憶場所は共有される &x.s 共用体のどのメンバを使うかはプログラム中で管理する必要がある
共用体と構造体の組み合わせ例 typedef struct { int type; char name[30]; char id[9]; } Student; typedef struct { int type; char name[30]; int age; } Professor; typedef union { int type; Student s; Professor p; } Person; 学生を表す構造体 Student type には必ず 0 を入れると決めておく 教員を表す構造体 Professor type には必ず 1 を入れると決めておく 学生または教員を表す Person 学生なのか教員なのかは type を見れば分かる
ファイル
ファイルのオープン FILE stdio.h で定義される ファイル を表す構造体 ファイルのパスを文字列で与える FILE *fopen(const char *path, const char *mode); この const は fopen の中で書き換わることがない という宣言 返戻値は FILE 構造体へのポインタ ファイルが開けないときは NULL が返る ファイルの使い方を文字列で指定する例 ) "r" は読み出し専用 "w" は書き込み専用
ファイルの読み書き int fscanf(file *stream, const char *format,...); int fprintf(file* stream, const char *format,...); scanf および printf と同様に入出力する 第一引数に入出力先のファイルを指定する 空白文字を含む文字列の入力には fgets を使う char *fgets(char *s, int size, FILE *stream); 1byte ずつ読む fgetc という関数もある int fgetc(file *stream); 読む値は (1byte なので ) 0~255 の整数 ファイルの終端では ( 読む値がないことを表す ) -1 が返る
ファイルのクローズ int fclose(file *stream); 開いたファイルの FILE 構造体へのポインタを渡す 開けたファイルは 閉じてからプログラムを終了しましょう オープンに失敗した場合は クローズしなくてよい
EOF という値 End Of File ( ファイルの終わり ) という意味 stdio.h で -1 に定義されている記号定数... 整数 fscanf, scanf, fgetc でファイル読むとき ファイルの終端に至ると EOF が返される fgets は返戻値が char へのポインタ ファイルの終端では NULL が返る fclose に失敗したとき ( すでに閉じていたなど ) も EOF が返される
ファイルを使ったプログラム例 hello.c というファイルの行数 ( 改行の個数 ) を数えるプログラム #include <stdio.h> int main() { FILE *fp; int c,linecount; fp=fopen("hello.c","r"); if (fp == NULL) return 1; linecount=0; while((c=fgetc(fp))!= EOF) { if (c == '\n') linecount++; } fclose(fp); printf("%d\n",linecount); return 0; } ファイルを読み込み専用で開く ファイルが開けなければ終了 ファイルを閉じる 1byte 読み込む EOF でない限り繰り返す 読んだ文字が改行文字なら linecount を 1 増やす 改行文字の個数を表示する
最初から開いているファイル stdin stdout stderr 標準入力を表す 標準出力を表す 標準エラーを表す これらはオープンもクローズも無しに使うことができる printf は fprintf の第一引数に stdout を指定したものと同様 scanf は fscanf の第一引数に stdin を指定したものと同様 エラーメッセージは fprintf を使って stderr に出力するとよい
次回予告 再帰的構造体
出席確認のパズル fgets は文字列を 1 行ずつ読むので これを使ってファイルの行数を数えるプログラムを以下のように作った : #include <stdio.h> int main() { FILE *fp; } char s[100]; int linecount=0; if ((fp=fopen("hello.c","r")) == NULL) return 1; while(fgets(s,100,fp)!= NULL) linecount++; flose(fp); printf("%d\n",linecount); return 0; このプログラムは だいたいうまく動くのだが うまくいかないこともある どういう場合に正しくない値が表示されるか 問題点を指摘しなさい 提出方法 :sec02@sun.ac.jp 宛にメール提出期限 : 今日中 2017.04.12