08 月 01 日(火)1-2h

入出力(1)

これまで,データの入出力については, 書式付き入出力関数 scanf( ) および printf( ) だけで対処してきた. しかし,問題によっては,書式付き入出力では不便な場合もある. そこで,今回および次回は,書式無し入出力関数について勉強しよう.

今回は,すべての入出力処理の基本となるバイト単位(文字単位)の入出力とする.

なお,書式付き入出力は,書式無し入出力を元にして実現できる.

これまでに学んだ入出力

本題の前に,既に理解している入出力処理について,事情を整理しておこう. これまでは,テキストデータ(文字列データ)の入出力しか取り扱って来なかった.

「えー, fprintf(fp, "%d", ...) とか fscanf(fp, "%lf", ...) とか, 数値データの入出力もやったじゃん?」 しかし,これらは実は, 数値データを文字列化して出力したり, 数字列データを入力して数値化していただけだ. つまり,数値データをそのままの形式で入出力していたわけではなかった, ということだ. 以前にも説明済み.

また,入力の場合,これまでは, 空白・改行で区切られた単語や数字さえ正しく入力できれば充分だった.

これはつまり,fscanf(fp, "%s %s ...", ...)fscanf(fp, "%d %d ...", ...) のこと. なお,文字列用の書式指定子 %s では, 空白文字を入力できない.

しかし,ファイルの内容がいつでもテキストデータである,とは限らない. たとえば音声や画像のデータファイルでは,大抵の場合, バイナリデータ(二進数データ)として記録されている. また,テキストファイルの場合であっても, 空白や改行の文字について,区切文字として無視するのではなく, 意味のあるデータとして利用したい場合もある. これらのような場合には,データを 1 バイトずつ入力する必要がある.

バイナリデータとテキストデータの違いは次の通り. たとえば,数 255 を記録する場合:


バイト単位の入出力

データを1バイトずつ入力するためには, もちろん fscanf(fp, "%c", ...) も使えるが, より簡単な次の関数を使うとよい:

#include <stdio.h>
int fgetc(FILE *fp)

この関数は, ファイルから1バイト分のデータを unsigned char 型として読み取り, int 型へ変換して返す. ただし,データを読み尽くした場合には,EOF を返す.

なぜ,1バイトの入力データなのに, わざわざ,int 型の4バイトへ変換するのか? unsigned char 型の1バイトのままでは, 入力データが 0xFF の場合に, EOF = -1 と区別できないからだ. (-1 ってのは, 1バイト signed char として表わすと 0xFF であり, 255 の 0xFF と区別できない. 一方,4バイト signed int では -1 = 0xFFFFFFFF となるので, 255 = 0x000000FF と区別できるわけだ.)

また,1バイトの出力としては,次の関数を使うとよい:

#include <stdio.h>
int fputc(int c, FILE *fp)

これは,fprintf(FILE *fp, "%c", unsigned char c) とほぼ同じものだ.


例:cp クローン

バイト単位入出力関数 fgetc( ) および fputc( ) の利用例として, ファイルの内容をコピーする関数 cp( ) の定義例を List 1 に示す. また,List 2 は, 書式付き入出力関数 fscanf( ) および fprintf( ) による定義例である. これらのリストを比較して, fgetc( )fscanf( ) の間の類似点・相違点を理解しよう.

List 1. ファイルコピー関数の定義例(バイト単位入出力版)
#include <stdio.h>

/* ファイルコピー関数
 * fin:入力のファイルポインタ
 * fout:出力のファイルポインタ
 */
void cp1(FILE *fin, FILE *fout)
{
	int  c;		// EOF を代入できるよう int 型

	while (1) {
		if ((c = fgetc(fin)) == EOF) break;
			// fgetc() の戻り値が EOF のとき,
			// 変数 c にも EOF が代入される
		fputc(c, fout);
	}
}

List 2. ファイルコピー関数の定義例(書式付き入出力版)
void cp2(FILE *fin, FILE *fout)
{
	unsigned char c;	// 文字しか代入しないので char 型

	while (1) {
		if (fscanf(fin, "%c", &c) == EOF) break;
			// 変数 c のアドレス &c を指定.
			// fscanf() の戻り値は EOF となるが,
			// 変数 c には EOF は代入されない
		fprintf(fout, "%c", c);
	}
}

参考までに,List 3 は, List 1 および 2 をテストするためのプログラム (cp コマンドの機能限定版クローン p-cp) の定義例である.

List 3. ファイルコピープログラム p-cp.c
#include <stdio.h>
#include <stdlib.h>

void cp1(...) { ... }		// List 1

void cp2(...) { ... }		// List 2

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

	if (argc < 3) goto ERR_ARG;
	if ((fp1 = fopen(argv[1], "r")) == NULL) goto ERR_FP1;
	if ((fp2 = fopen(argv[2], "w")) == NULL) goto ERR_FP2;

	cp1(fp1, fp2);		// fgetc 版 cp
//	cp2(fp1, fp2);		// fscanf %c 版 cp	

	fclose(fp2);
	fclose(fp1);
	return (EXIT_SUCCESS);

ERR_FP2:
	fclose(fp1);
ERR_FP1:
ERR_ARG:
	fprintf(stderr, "コピー失敗\n");
	return (EXIT_FAILURE);
}

実行例:

$ cat  infile.txt		# 入力(コピー元)ファイルの内容を表示
  ooo rrr zzz
  o o r    z
  ooo r   zzz

$ ./p-cp  infile.txt  outfile.txt	# ファイルコピー

$ cat outfile.txt		# 出力(コピー先)ファイルの内容を表示
  ooo rrr zzz
  o o r    z
  ooo r   zzz				# 同じ内容になっている

入力と出力の内容をよく比較してみよう. 空白文字の個数まで完全に一致している. (fgetc 版でも fscanf %c 版でも動作は同じハズ.)

文字列入力 fscanf(..., "%s", ...) の場合, こうはならない. 疑うのなら, List 2 の cp2() 関数の "%c""%s" に変えて List 3 の p-cp コマンドを作り,実行してみよう. (もちろん,変数 char c についても 配列 char s[256] とかに変える必要あり.)

なお,上の実行例では,テキストファイルのコピーだったが, List 1 や List 2 の cp()(バイト単位の入出力)では, バイナリファイル(プログラム,画像,音声,等)のコピーも可能となる. (文字列単位の入出力だと正しくコピーできない.)


本日の課題

次の問題について,ソースファイルと実行例をレポートせよ.

  1. List 1 の関数 cp1( )そのまま利用して, 任意の個数のファイルを連結するプログラム cat の機能限定版クローン p-cat を作成せよ.
  2. 実行例:

    $ cat  infile.txt	# テスト用入力ファイルの内容を表示している
      ooo rrr zzz
      o o r    z
      ooo r   zzz
    
    $ ./p-cat  infile.txt	# 作成したプログラムでも同じ結果になるハズ
      ooo rrr zzz
      o o r    z
      ooo r   zzz
    
    $ ./p-cat  infile.txt  infile.txt  infile.txt	# 複数のファイルを連結している
      ooo rrr zzz
      o o r    z
      ooo r   zzz
    
      ooo rrr zzz
      o o r    z
      ooo r   zzz
    
      ooo rrr zzz
      o o r    z
      ooo r   zzz
    

    条件:厳守!

    ヒント: stdout 専用の cp() を作り直してしまうのは, ありがちな間違い. List 1 を変更せずに,そのまま再利用すること. (stdout もファイルポインタだ...fopen() は不要だが.) 問2の main() も見るがよい.

  3. 文字出力関数 fputc( ) を利用して, fputs(str, fp) および fprintf(fp, "%s", str) と同等な文字列出力関数 void myFputs(char *str, FILE *fp) を定義せよ.
  4. fputs() については,教科書の索引から調べよう.

    テスト用メイン関数:

    int main()
    {
    	char  *str = "Hello, world.\n";
    
    //	fputs(str, stdout);
    //	fprintf(stdout, "%s", str);
    	myFputs(str, stdout);
    	return (0);
    }
    

    実行例:

    $ ./myfputs
    Hello, world.
    
    $
    
レポート提出 注意事項: 以下の点についても厳しくチェックする:


(c) 2017, yanagawa@kushiro-ct.ac.jp