6日目 8日目

7日目

目標

課題


課題 6

ファイルからの入出力

課題5の mycat1 では、ファイルからの入力を処理するには シェルの redirect 機能である 「<」 を利用して

mycat1 < filename
とする必要があった。これを課題4で扱ったコマンドライン引数を用いて リダイレクト記号(<)を用いることなく実現する方法について考える。

標準入出力のリダイレクションではなく、ファイルからの入出力を直接 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("表示するファイル名を1つ指定してください\n");
		exit(1);
	}

	if (NULL == (fp = fopen(argv[1], "r"))) {
		printf("ファイル %s を開けません\n", argv[1]);
		exit(1);
	}

	while (EOF != (ch = fgetc(fp))) {
		fputc(ch, stdout);
	}
	fclose(fp);
	exit(0);
}

このプログラムの流れは次のようになる。

  1. 起動時に与えられたパラメタの数を確認し、ファイル名が指定されているかどうかを確認する。 ファイル名が指定されていなかったり2つ以上指定されていた場合はエラーとして、 プログラムを終了する。

  2. 指定されたファイルの内容を読み込めるように、準備を行う。 この準備の事を「ファイルをオープンする」と呼ぶ。 オープンできなかった場合には、エラーメッセージを表示してプログラムを終了する。

  3. オープンしたファイルから1文字ずつ読み込み、標準出力へ書き出す。 ここでは、for ではなく、新しく while文を使ってみた。

  4. 読み込むものがなくなれば、「ファイルをクローズ」してプログラムを終了する。

最初にファイルからの入出力に関する部分として増えた部分を見ていく。


fopen

fopen関数は、引数を2つ取る関数で、第1引数としてファイルの位置を指定し、 第2引数でどのようなモードでオープンするかを指定する。 ファイルがオープンできれば、ストリーム(stream)と結び付け、結果を FILE型の ポインタとして返す。 オープンできなかった場合は NULLという値を返すので、 返り値が NULL かどうかを確認することでファイルオープンが成功したかどうかを判定することが可能である。

英語の stream には、「流れ」と言う意味があるが、ここで流れるものはデータである。 ストリームにデータを書き込んだり、ストリームからデータを読み込むことによって、 実際のファイルやデバイスからの入出力を実現しているのである。

課題5で取り上げた、標準入出力も実はストリームであり、プログラムの開始時に 自動的に準備されているものである。 標準入力はstdin, 標準出力はstdout、 そして標準エラー出力はstderrという名前でアクセスすることができる。

第2引数として指定できるモードとしては次のようなものが用意されている。

これ以外にも、r+, w+, a+ というモードが あり、読み書き両用にファイルをオープンする際に用いる。

さらに、bという文字を(先頭部分以外に)追加することによって、 ストリームがバイナリモードであることを指示することもできる。 POSIX 準拠システム(本講義で使用している Linux を含む)では、 通常のテキストモードとバイナリモードの違いはないので、 この bを指定しても無視されるだけであるが、 システムによっては、区別しているものもあるため、 移植を行う事が予想されるようなプログラムの場合は適切に指定することが必要である。


fclose

ファイルとストリームを接続する為にオープンするのに、fopen を用いたが 不要になったストリームとファイルの接続を解除するのにはfclose 関数を用いる。引数には fopenで与えられた FILE 型のポインタを 指定する。


fgetc

fgetcは 引数のFILE 型のポインタで示されるストリームから1文字読み込む。 読み込めなかった場合は EOF を返す。 getchar は fgetc(stdin)に相当すると考えれば良い。


fputc

fputcは、引数を2つ取り、最初の引数で指定された内容を、 2番目の引数である FILE 型のポインタで示されるストリームに出力する。 putchar(ch) は fputc(ch, stdout)に相当すると考えれば良い。


while

課題5では、ファイルの内容を読み出すのに forを使ったが、 課題6では、while文を用いた。

while 文は次のような構成になっている。

 while (繰り返しを行うかどうかの判定文){
     <繰り返し処理>
 }

英語の while には、「…するうち」という意味があるので、 「()で与えられた条件が成立する間、繰り返し処理を行う」というふうに 読み下だすと覚えやすいかも知れない。 このようにある条件文が成立する間の繰り返し処理には、for文ではなく この while文が使われている場合が多い。


exit

今までのプログラムでは、プログラムは必ず main()の最後まで実行しないと 終了しなかった。 プログラムを書いている場合、ある特定の条件の場合には、プログラムを途中で終了したいような場合も 多く存在する。(たとえば、カレンダ表示プログラムで「14月」がしていされた場合など) このような場合には、exit関数を用いる。

exit関数は、呼び出されるとプログラムを正常に終了させる。 終了する前に、プログラム中でオープンされ、まだクローズしていないファイルを クローズするなどの手続きを行う。詳しくは man 3 exitを参照のこと。

一般に、プログラムの正常終了時には引数として 0 を、間違ったパラメタが 与えられるなどエラーのために異常終了する場合には 0以外を引数に指定する。

問題 6

上で作成した mycat3.cを元に、与えられたファイルの 先頭5行だけを表示するようなプログラムを作成し myhead.cという名前で 保存せよ。

繰り返し処理を途中で終了するには、break文を利用することができる。


課題 7

ポインタ

メモリとアドレス、変数の関係

コンピュータは内部的には電気信号の on/off で物事を記憶している。 この記憶の最小単位の事を 1bit(1ビット)と呼ぶ。

コンピュータの内部には、そのような記憶する場所が膨大にあり、CPU はその記憶領域(メモリ)を用いて作業を行っている。 すべての記憶領域は、アドレス(address)といわれる番号が割り振られ、管理されている。 いままで用いてきた変数は、ある一定のアドレス範囲に対して名前を割り振り、人間が理解しやすいようにしたものである。 この番地は 1bit ごとではなく、8bit をまとめた 1byte(1バイト)ごとにつけられている。 1バイトで表される情報量は 2×2×2×2×2×2×2×2 = 28 = 256 通りである。

コンピュータが行っているプログラムは、特定のアドレスで保持されている内容を参照したり、指定された内容を特定のアドレスに保存している事になる。 しかしながら、人間がアドレスで考えるのはたいへん(「04番地から4バイトの領域に、12345 という値を代入せよ」とか。)なので、 その領域について名前をつけることにしたものが変数である。 したがって、変数は、その型(サイズ)によって、特定のアドレス範囲を確保することになる。 int 型の変数や char 型の配列はそれぞれ次のような形で確保されていたと考えれば良い。

memory map

こうすると、「04番地から4バイトの領域に、12345 という値を代入せよ」の代わりに、 「int 型変数 iに 12345 を代入する」という表現ですむことになる。


C では、この変数のアドレスを直接扱うことができる。 変数のアドレスを調べるためには、演算子&を用いる。 次のようなプログラムを使うと、変数のアドレスを調べることができる。

/* 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]);
	}
}

実行するとどうなるだろうか? はじめの for で出力される内容を見ると、変数の中身は変わっても、その位置であるアドレスは変化していないことが分かる。 また、次の forで出力される内容をみると、配列要素のアドレスは連続している事も分かる。

問 7-1: int 型の配列の要素のアドレスはどのようになっているであろうか?

問 7-2: 宣言した要素数より大きな添字をつけると何が起こるか?


ポインター変数

ポインター(pointer)とは英和辞典を索くと「指す人(もの)」という意味を持つことがわかる。 C 言語で出てくるポインターとは「アドレスを入れる事ができる変数」の事である。 ポインタ型の変数を宣言する為には、

int *ptr;

の用に「*」をつけて宣言を行う。 宣言の際に前に書かれている型は、その内容であるアドレスに存在する変数の型、 言い換えると、そのポインタ変数でどの型の変数のアドレスを扱うのかを示している。

ポインタ変数の利用の仕方を順にあげる。

問 7-3: 次のプログラムを実行するとどのような出力を得られるか?

/* prob7-3.c */
#include <stdio.h>

int main() 
{
	char a[6] = "Hello";
	char *p;

	p = &a[0]; /* a[0] のアドレスを p に代入 */

	while (*p) {
		printf("%c\n", *p++);
	}
}

6日目 表紙 8日目

tacha@tack.fukui-med.ac.jp
$Id$