Lecture 11 ポインタ(2)〜ファイルI/O

データを取得したり,計算した結果がでたら,何らかのメディアに保存する必要があります. みなさんもUSBメモリやメモリカード,ハードディスクなどにデータを保存していると思います. この作業はすべて,ソフトウェアが制御して行なっています. 計測したデータを保存するときには,「ファイル」という概念を用います. ここでは自分のプログラムから,ファイルを作ったり,既にあるファイルのデータを読んだりしてみよう.

ファイルシステムの基礎

ファイルを開く

プログラムの中でファイルを扱う方法はいくつかあるが,最初にファイルを開く必要がある.例えば,
    #include <stdio.h>
    FILE* fopen(char* ファイル名, char* モード);
という関数を用いてファイルを開く.ここで, ファイル名はその名の通り,ファイルの名称.モードは,開くときの状態を指し,以下の様な種類がある.
モード 意味 備考
r 読み込み用にテキストファイルを開く 既存ファイルが無ければエラー
w 書き込み用にテキストファイルを開く 既存ファイルがあれば先に消去
a 既存のテキストファイルの最後に追加 元の内容は変更されない
rb 読み込み用にバイナリファイルを開く テキストに準ずる
wb 書き込み用にバイナリファイルを開く テキストに準ずる
ab 既存のバイナリファイルの最後に追加 テキストに準ずる
上記以外にも,コンパイラによっては様々なモードがサポートされている.


FILE
は,ファイル構造体と呼ばれるもので,ファイルがオープンできると, fopenはファイル構造体へのポインタが返す. 以後のアクセスはすべて,このファイル構造体に対して行う.
失敗すると,NULLポインタを返す.これらを適切に判断しないとプログラムは正しく動かない. このため,以下のような誤り判定と処理を必ず入れなければならない.
#include <stdio.h>
#include <stdlib.h>
FILE* fp = NULL;
    ....
    if((fp=fopen("datfile.txt","r"))==NULL){
    	printf("エラー.ファイルが開けなかった¥n");
    	exit(1);
    }
【ヒント】 誤り判定をせず,開けなかったファイルに対して(NULLポインタに対して) データの読み書きを行うと,プログラムが暴走し,強制終了させられてしまうこともある. 従って誤り判定とその後の適切な処理は必須である.


ファイルへのデータの読み書き

ファイルを開いたら,そのファイルに対してデータの読み書きを行う.
    int fgetc(FILE* fp);
    int fputc(int 文字, FILE* fp);

fgetc() は,ファイルの現在の位置から 1バイト(unsigned charとして)読み込み,int型の値として返す. (返り値は int だが,読み込んが文字は下位バイトにあるため, char または unsigned char 型の変数に代入することができる) エラーの時(またはファイルの最後で読むものがなくなった時)は, EOF (End Of File, -1) を返す.

fputc() は,「文字」の下位バイトをファイルの 現在の位置 に unsigned char 型の値として書き込む. 書き込みが成功すると書き込んだ文字を,失敗すると EOF を返す.

ファイルを閉じる

ファイルの読み書きが終わったあとは,ファイルを閉じなければならない. ファイルを正しく閉じないと,ファイルにデータが書かれなかったり, ファイルそのものが壊れてしまい,読み書きできなくなったりするので注意が必要である.
    int fclose(FILE* fp);
ファイルポインタ fp には,開いた時に得たポインタを渡す. ファイルと閉じると,残っていたデータをファイルに書き込み, その後OS等でファイルが扱えるようになる.

ファイルの状態判定

fgetc() や fputc() では,エラーの時と,ファイル終了の時で返り値が同じである. これがどちらかなのか判定するには, feof() 関数や ferror()を使う.
    int feof(FILE* fp);
    int ferror(FILE* fp);

feof() は,現在の位置がファイルの終わりであれば true を,そうでなければ false を返す. ferror() は,ファイルにエラーが起きていれば true を,そうでなければ false を返す.

高レベルテキスト関数

文字ひとつひとつだけでなく,文字列の入出力を行うには,fputs()fgets() がある.
    int fputs(char* 文字列, FILE* fp);
    char* fgets(char* 文字列, int 文字列長さ, FILE* fp);
fputs() はエラーの時 EOF を返し,成功すると負でない値を返す.
fgets() は,成功すると文字列を,エラーの時ヌルポインタを返す.
    int fprintf(FILE* fp, char* 制御文字列, ...);
    int fscanf(FILE* fp, char* 制御文字列, ...);
fprintf()fscanf() は,いままで使った printf() と scanf() と同じ動作をする. 入出力が画面(コンソールという)に対して行われるのではなくファイルに対して行われるのが違いである.

標準入出力のファイルポインタ:stdin, stdout, stderr

実はC言語では,常にオープンされたファイルが存在する.それは標準入出力とよばれるもので, 今まで scanf() や printf() で無意識に使用していたものである. fscanf() や fprintf() のファイルポインタに,stdin や stdout と書くと, 標準入出力への入出力となる.

入出力のリダイレクト

標準入出力を使ったプログラムは,OSの機能により,実行時に入出力先を切り替えることができる. 例えば,標準出力に "Hello, world!" と出力するプログラム,hello の場合,
 hello > hello.txt
とすると,普段は画面に出力される文字列を,hello.txt というファイルに書き出すことができる. stderrは,エラー出力用のファイルポインタで,通常は stdout と同じ場所を示しているが, リダイレクトには応答しないポインタである.

練習:以下のプログラムを作ってみよう

    1. "Hello, world" とファイル output.txt にリダイレクトで書き込んでみよう.
    2. 上記のプログラムを標準出力ではなくて,明示的にファイルに書き込むプログラムにしてみよう.
    3. 上で作った output.txt から,リダイレクトを使って標準入力として文字列を読み込み,画面に表示してみよう.
    4. 上のプログラムを明示的にファイルから読み込むものに変えよう.




バイナリデータ入出力

データの読み書きには,fscanf()やfprintf()を使ったテキスト入出力は便利だが, それだけでは十分ではない.例えば,画像,音声,動画のデータなどは数値の集まりであり, 文字ではないため,人がそのまま読むことは出来ない.このようなデータを扱う場合, バイナリデータ入出力を用いる. 例えば,
    size_t fread(void* buffer, size_t サイズ, size_t 個数, FILE* fp);
    size_t fwrite(void* buffer, size_t サイズ, size_t 個数, FILE* fp);
はそれぞれ,メモリに格納されたデータを,そのメモリイメージのまま,ファイルに読み書きする関数である. 読み書きするデータの大きさは,「サイズ」バイトのデータを「個数」個,すなわち, 「サイズ☓個数」バイトである.

void* は,ボイドポインタといい,型にとらわれない汎用のポインタを示す. 従って,このbufferは,char だけでなく,float や doubleなどの データへのポインタも置くことができる.
size_t は,コンパイラがもつ最大の値を格納できる変数と定義されていて, 多くの場合,unsigned long と同等である.

構造体

C言語には新しい型(のように扱える)を作れる機能があります. それを構造体 (structure)と呼びます. 構造体は,様々な種類のデータをまとめて管理出来るのでとても便利な機能です.

 構造体は,複数のデータを含み,ユーザが定義することで新しいデータ型が作れる機能です. データ(あるいは情報)は,それぞれ何らかの関連があることが多く,この様なデータをまとめて 管理できると大変に都合が良い.例えば,友人の連絡先データベースを作る場合,「氏名」「電話」 「住所」などの情報をまとめて一人分のデータとできれば便利である.Cでは,このような 「複合データ型(aggregate data type)」を, を構造体を用いて作成でき, これを「複合データ構造体(complex data structure)」などと呼こともあります.


構造体の定義

構造体は,メンバと呼ばれる,互いに関連のある2つ以上の変数で構成される複合データ型である. 配列がどの要素も同じ型を持つのに対して,構造体はそれぞれのメンバが異なる型を持つことができる. 構造体を定義する一般的な形式は次の通り,
    struct タグ名 {
        型 メンバ 1;
        型 メンバ 2;
        型 メンバ 3;
        ・・・
        型 メンバ N;
    } 変数名;

例えば,住所録の場合,
    struct AddressRecord {
        char name[40];
        short postal_code1;
        short postal_code2;
        char address[80];
        char phone;
    } card;

Cの場合の説明

Cでは,この例の場合,AddressRecord は「タグ名」で,struct AddressRecord とすると「型名」になる.その後にある,card は struct AddressRecord 型の変数である.
* struct AddressRecord は,int などと同じ型名であって変数ではない.

C++の場合の説明 ... 拡張子が.cppの場合はこちら

C++では,Cと比べて構造体の解釈が拡張され,AddressRecordは「タグ名」ではなくて,「型名」として 使えるようになった.従って,変数(インスタンス)を作るときにも,
    struct AddressRecord card;
ではなくて,
    AddressRecord card;
でよく,typedef を使う必要もなくなった.


バイナリファイルI/Oとの親和性

バイナリファイルI/Oは,メモリイメージそのままをファイルに読み書きするので, 様ざなま種類のデータを束ねられる構造体との親和性が非常に高い. ある意味,テキストモードでデータを読み書きするよりも遥かに簡単である.

バイナリファイルの読み書きの例.以下のプログラムを作成し,実行してみよ.
struct Juushoroku {
    char name[16];
    char age;
};

int main(void)
{
    Juushoroku data[10] = {
        {"Ichiro",40},
        {"Jiro",34},
        {"Saburo",24},
        {"Shiro",12},
        {"Goro",6},
    };
	
    // バイナリファイル書き出し

    // バイナリファイル読み込み
}



Quiz 11(提出課題ではありません)

課題は,数多くのトレーニングを積むことで,個人の実力を養成する目的で出しているものです.その趣旨を理解し「自分の為」になるよう適切に実施しなさい.

以下の問それぞれに対応するプログラムを作成しなさい.

    1. 以下のリンクからテキストファイル(inputdat.csv)をダウンロードせよ. このファイルを読み込み,一行ごとに数値の平均値を行の最後に加え, これを新たなファイルに書き込むプログラムを作成せよ.平均値の計算は関数で行うこと. 入力用のファイルは【inputdat.csv】 また出力するファイル名は,outputdat.csv とする.
      【ヒント】上記データには5x5の数値データが格納されている。今はその数を知っていることにしてよい。
      【ヒント】最初に画面に表示するようにプログラムをつくり, うまく出来るようになったら同じ内容をファイルに書くようにすれば 比較的短時間でできる.


    2. ファイルを読み込み,各行の先頭に行番号をつけて出力するプログラムを作成せよ. ファイル名はキーボードから入力する. (printf内の書式指定子を”%03d”とすると,実行結果のように0付きの3けたで出力される.)
      【実行例】 
      >11-2
      ファイル名=11-2.cpp 
      001 #include <stdio.h> 
      002 #include <stdlib.h> 
      003 int main(void) 
      004 { 
          … 
      
      022     return 0; 
      023 } 
      

    3. "output.csv"というファイルにデータを出力するoutput関数を作成せよ. ただし,main関数を含む以下の関数は変更してはならない.
      #include <stdio.h>
      #include <stdlib.h> 
      #include <math.h>
      const int DSIZE = 1000;
      const int CSIZE = 2;
      typedef double DataSet[DSIZE][CSIZE];
      
      double f(double n){ 
          return sin(0.01*n)/(0.1*n+1.0); 
      } 
      
      void calc(DataSet n){ 
          for(int i=0; i<DSIZE; i++){ 
              double x = (double)i; 
              n[i][0] = x; 
              n[i][1] = f(x); 
          } 
      } 
      
      void output(DataSet data){ 
      //ここを記入 
      }
      
      int main(void) 
      { 
          DataSet data;
          calc(data);
          output(data); 
          return 0; 
      } 
      

    4. "encripted.txt"はある英文を暗号化した物である. これを読み込み,デコードした物を"output.txt"として出力せよ. なお,暗号化規則は英文の各文字をASCIIコード表に従って-0x01シフトした物である. 入力用のファイルは【encripted.txt】 ただしmain文は以下の通りとせよ.
      int main(int argc, const char * argv[]) 
      {
          const int N = 1000;
          char dat[N]; 
          int lng; 
          
          read(dat,N,lng); 
          decord(dat,lng); 
          write(dat); 
      
          return 0; 
      } 
      

    5. あるクラス全員の数学のテストスコアが記述されたテキストファイル"test_result.txt"を読み込み、 平均点を計算したのち、その結果を表示しなさい このとき 条件1:平均を計算する過程は関数average()を作成すること 条件2:読み込むファイルのファイル名をこちらが指定できるよう、ファイル名を入力する過程を入れること (こちらが用意したテキストファイルで正しい答えが表示されるかを確認する) 入力用のファイルは【test_result.txt】
      【ヒント】読み込んだデータは文字なので、atoi()関数を使用する
      【ヒント】atoi()を使わずに、scanf()を使ってもいいです


    6. ポインタの配列を用いて, 月の英語名称のデータテーブルを作り, データテーブル全体をプリントする関数を作れ. また,月ごとのデータを入れ替える swap_db()関数を作れ.
      char* month[] = {
          "January",
          "February",
          ....
      };




Assignment(課題)11

課題の提出は Online Judge にて行います.

  • 提出期限があります.できるだけ早めにやりましょう.
  • Online Judge は普通の課題提出方法とは異なり,良い点が取れるまで何度でも挑戦できます.良い点が取れるまで頑張ってやろう.
  • わからないことは,早めに Teams で聞こう.問題は自分で解決すること.
  • それでもどうしてもわからないときは,毎週月曜日の2時限目にZoomによるオフィスアワーを用意しています. ZoomのリンクはOh-o! meijiで確認してください.
  • Online Judge のページはこちら