課題
課題5の mycat1
では、ファイルからの入力を処理するには
シェルの redirect 機能である 「<
」 を利用して
標準入出力のリダイレクションではなく、ファイルからの入出力を直接 C プログラムから行うためにはプログラムの中でファイル操作を行わなければならない。 その操作を行うように mycat1.c を変更したプログラムを次に示す。
/* mycat2.c */ /* * 起動時に指定するファイル(1つ)の内容を * 標準出力に表示するプログラム */ #include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { int ch; FILE *fp; if(argc != 2) { printf("Which file do you want to see ?\n"); exit(1); } if (NULL == (fp = fopen(argv[1], "r"))) { printf("Cannot open file %s\n", argv[1]); exit(1); } while (EOF != (ch = fgetc(fp))) { fputc(ch, stdout); } fclose(fp); exit(0); }
このプログラムの流れは次のようになる。
起動時に与えられたパラメタの数を確認し、ファイル名が指定されているかどうかを確認する。 ファイル名が指定されていなかったり2つ以上指定されていた場合はエラーとして、 プログラムを終了する。
指定されたファイルの内容を読み込めるように、準備を行う。 この準備の事を「ファイルをオープンする」と呼ぶ。 オープンできなかった場合には、エラーメッセージを表示してプログラムを終了する。
オープンしたファイルから1文字ずつ読み込み、標準出力へ書き出す。
ここでは、問題 5-2 と同様 while
文を使ってみた。
読み込むものがなくなれば、「ファイルをクローズ」してプログラムを終了する。
最初にファイルからの入出力に関する部分として増えた部分を見ていく。
fopen
関数は、引数を2つ取る関数で、第1引数としてファイルの位置を指定し、
第2引数でどのようなモードでオープンするかを指定する。
ファイルがオープンできれば、ストリーム(stream)と結び付け、結果を FILE
型の
ポインタとして返す。
オープンできなかった場合は NULL
という値を返すので、
返り値が NULL かどうかを確認することでファイルオープンが成功したかどうかを判定することが可能である。
英語の stream には、「流れ」と言う意味があるが、ここで流れるものはデータである。 ストリームにデータを書き込んだり、ストリームからデータを読み込むことによって、 実際のファイルやデバイスからの入出力を実現しているのである。
課題5で取り上げた、標準入出力も実はストリームであり、プログラムの開始時に
自動的に準備されているものである。
標準入力はstdin
, 標準出力はstdout
、
そして標準エラー出力はstderr
という名前でアクセスすることができる。
第2引数として指定できるモードとしては次のようなものが用意されている。
r
ファイルからデータを読み込むためにオープンする。
w
ファイルにデータを書き込むためにオープンする。 指定されたファイルが、すでに存在していた場合は中身は消去される。
a
ファイルをデータを書き込むためにオープンする。 指定されたファイルが、すでに存在していた場合、 新たに書き込まれるデータは既存の内容の後ろに追加される。
これ以外にも、r+
, w+
, a+
というモードが
あり、読み書き両用にファイルをオープンする際に用いる。
さらに、b
という文字を(先頭部分以外に)追加することによって、
ストリームがバイナリモードであることを指示することもできる。
POSIX 準拠システム(本講義で使用している Linux を含む)では、
通常のテキストモードとバイナリモードの違いはないので、
この b
を指定しても無視されるだけであるが、
システムによっては、区別しているものもあるため、
移植を行う事が予想されるようなプログラムの場合は適切に指定することが必要である。
ファイルとストリームを接続する為にオープンするのに、fopen を用いたが
不要になったストリームとファイルの接続を解除するのにはfclose
関数を用いる。引数には fopen
で与えられた FILE 型のポインタを
指定する。
fgetc
は 引数のFILE 型のポインタで示されるストリームから1文字読み込む。
読み込めなかった場合は EOF を返す。
getchar
は fgetc(stdin)
に相当すると考えれば良い。
fputc
は、引数を2つ取り、最初の引数で指定された内容を、
2番目の引数である FILE 型のポインタで示されるストリームに出力する。
putchar(ch)
は fputc(ch, stdout)
に相当すると考えれば良い。
C で記述されたプログラムは基本的には main()
の最後まで実行しないと
終了しない。
実は、今までも何度か出てきている exit を用いるとこの振舞いを変更する事ができる。
プログラムを書いている場合、ある特定の条件の場合には、プログラムを途中で終了したいような場合も
多く存在する。(たとえば、カレンダ表示プログラムで「14月」がしていされた場合など)
このような場合には、exit
関数を用いる。
exit
関数は、呼び出されるとプログラムを正常に終了させる。
終了する前に、プログラム中でオープンされ、まだクローズしていないファイルを
クローズするなどの手続きを行う。詳しくは man 3 exit
を参照のこと。
一般に、プログラムの正常終了時には引数として 0 を、間違ったパラメタが 与えられるなどエラーのために異常終了する場合には 0以外を引数に指定する。
上で作成した mycat3.c
を元に、与えられたファイルの
先頭5行だけを表示するようなプログラムを作成し myhead.c
という名前で
保存せよ。
繰り返し処理を途中で終了するには、break
文を利用することができる。
コンピュータは内部的には電気信号の on/off で物事を記憶している。 この記憶の最小単位の事を 1bit(1ビット)と呼ぶ。
コンピュータの内部には、そのような記憶する場所が膨大にあり、CPU はその記憶領域(メモリ)を用いて作業を行っている。 すべての記憶領域は、アドレス(address)といわれる番号が割り振られ、管理されている。 いままで用いてきた変数は、ある一定のアドレス範囲に対して名前を割り振り、人間が理解しやすいようにしたものである。 この番地は 1bit ごとではなく、8bit をまとめた 1byte(1バイト)ごとにつけられている。 1バイトで表される情報量は 2×2×2×2×2×2×2×2 = 28 = 256 通りである。
コンピュータが行っているプログラムは、特定のアドレスで保持されている内容を参照したり、指定された内容を特定のアドレスに保存している事になる。
しかしながら、人間がアドレスで考えるのはたいへん(「52f4番地から4バイトの領域に、12345 という値を代入せよ」とか。)なので、
その領域について名前をつけることにしたものが変数である。
したがって、変数は、その型(サイズ)によって、特定のアドレス範囲を確保することになる。
例えば、int 型の変数 i
を宣言すると、次のように特定の範囲(赤い範囲)が
i
という名前で参照できることになる。
こうすると、「52f4番地から4バイトの領域に、12345 という値を代入せよ」の代わりに、 「int 型変数 iに 12345 を代入する」という表現ですむことになる。
変数の説明で少し触れたように、C では、この変数のアドレスを直接扱うことができる。
変数のアドレスを調べるためには、演算子&
を、printf の書式指定子としては %p
を用いるのであった。
/* address.c */ /* 変数のアドレスを表示する */ int main() { int i; char a[6]; for(i = 0; i < 6; ++i) { /* 変数 i のアドレスと、i の中身を表示 */ printf("&i = %p, i = %d\n", &i, i); } printf("\n"); /* 区切りのための空白行 */ for(i = 0; i < 6; ++i) { /* 配列変数 a の要素のアドレスを表示 */ printf("&a[%d] = %p\n", i, &a[i]); } }
問 7-1: address.c
を元に、int 型の配列の要素のアドレスがどうなっているかを調べるプログラムを作成し、自分の理解と一致しているかを確認せよ。
問 7-2: もし、配列の添字として宣言した要素数より大きな添字をつけると何が起こるかを考えてみよ。
ポインター(pointer)とは英和辞典を索くと「指す人(もの)」という意味を持つことがわかる。 C 言語で出てくるポインターとは「アドレスを入れる事ができる変数」の事である。 ポインタ型の変数を宣言する為には、
の用に「*
」をつけて宣言を行う。
宣言の際に前に書かれている型は、その内容であるアドレスに存在する変数の型、
言い換えると、そのポインタ変数でどの型の変数のアドレスを扱うのかを示している。
ポインタ変数の利用の仕方を順にあげる。
ポインタ変数に代入されているものはアドレス値そのものなので、通常変数のアドレスを代入することができる。 また、ポインタ変数同士だと、通常の変数と同様に利用することで、アドレスをやり取りすることができる。
int i; int *ptr, *a; ptr = &i; a = ptr;
とすると、ポインタ変数 ptr
, a
両方が、変数 i
のアドレスを持つことになる。
ポインタ変数に代入されているものは、アドレスそのものであるが、そのアドレスの内容を表示することもできる。
指し示しているアドレスの内容を参照するには、*
演算子をつける。
たとえば、ポインタ変数 ptr
の示している領域の内容を参照するには *ptr
とする。
次のプログラムを実行すると、j
には 10 が代入される。
int i, j; int *ptr; i = 10; /* 変数 i に 10 を代入 */ ptr = &i; /* ptr に i のアドレスを代入 */ j = *ptr; /* 変数 j に ptr で指し示されている領域 * (つまり、i)の内容を代入 */
問 7-3: 次のプログラムを実行するとどのような出力を得られるか?
/* prob7-3.c */ #include <stdio.h> int main() { char a[6] = "Hi"; char *p; p = &a[0]; /* a[0] のアドレスを p に代入 */ while (*p) { printf("%c\n", *p++); } }
6日目 | 表紙 | 8日目 |
2/16(試験期間)はどうする?