計算プログラムでは,処理結果を単に画面に表示するだけでなく,そのデータ永続的に(=PCの電源を切っても)保存しておきたい場合がある.
また,処理に先立って計算に必要な数値・条件や,入力データなどをディスク上から変数などに読み込んでから処理を開始する場合も多い.
実用的なデータ処理プログラムでは,このような手順が一般的である.
コンピュータにおいてはこのようなデータの永続的な保管機能を実現するハードウエアを 補助記憶装置と呼ぶ.
これらはディスクやドライブ,ストレージとも呼ばれる.
補助記憶装置の具体的な例として,ハードディスク(HDD)や,ソリッドステートディスク(SSD), SDメモリ, USBメモリ, Blue-ray disk, DVD, CD-ROM(古くは MO, PD, FDD, 磁気テープ, パンチカード・・・)などが挙げられる.
この中には,名称に「メモリ」とつくものがあるが,PC内蔵メモリ(主記憶装置)ではなく補助記憶装置である.
Windows, Mac OS, UNIX など,ほとんどのオペレーティングシステム (OS) では,ディスク上にデータを保存する際に,「ファイル」という管理単位を用いており,1つのファイルに1つの名前(ファイル名)をつけて識別している.
ファイルの種類は非常に多岐にわたり,例えば Windows OS ではファイル名の後に続く「拡張子」により区別している. (UNIXなどの他のOSでは,拡張子が重要ではない場合もある.)
以下,WindowsOSで使用されている主な拡張子の例:
今回の実習では,Cで書いたプログラムから
方法を学ぶ.
まず,ファイルには大きく分けて「テキストファイル」と「バイナリファイル」の2種類がある.
ただし,いずれも,ディスク上に0または1の並びが書かれているだけなので,コンピュータにとって本質的な差異は無い.
テキストエディタで開いて人が読めるものはテキストファイルであると考えてよい.
C言語でのファイル入出力の操作は,以下の3つの手順が必ずセットとなる.
このパターンは毎回,決まっているので,覚えよう.
(stdio.h 内の f
から始まるファイル関連のライブラリ関数を使う.)
fopen()
関数fgetc(), fputc()
fgets(), fputs()
fscanf(), fprintf()
fread(), fwrite()
fclose()
この3つの手順(開く,読み書き,閉じる)が必ずこの順序で用いられる.
ファイル操作では,この基本操作に加えて,
feof(), ferror(), fseek()
など,多くの関数を組み合わせて用いられる.
ファイルは「ファイル名」によって識別された複数のデータ集合であるから,まず,どのファイルに対して読み書きするかを識別するために,ファイルポインタと呼ばれるポインタ変数を定義する.
ファイルポインタは,ファイル 1 つに付き 1 個必要である.
ファイルポインタ変数を複数用いれば,複数のファイルを同時に扱うことができる.
プログラムの中でファイルを扱う際には,最初に「ファイルを開く」,最後に「ファイルを閉じる」必要がある.
fopen()
関数を使う.fclose()
関数を使う.プログラム例
#include <stdio.h>
int main(void)
{
FILE *fp; // ファイルポインタ.
fp = fopen(ファイル名, モード);
// ... ここで,読み書き処理 ...
fclose(fp); // ファイルを閉じる
return 0;
}
fopen()
関数の引数の説明
fopen
関数の第 1 引数「ファイル名」は,ディスク上でのファイルの名称を表す文字列である.
ファイル名の箇所には,"datafile.txt"
のように文字列定数を書いてもよいが,文字列変数char fname[] = "datafile.txt";
のように,文字列変数としておく方が良い.
fopen
関数の第 2 引数「モード」は,同じく文字列であるが,以下に示すいずれかのモードを指定する.
読み込み or 書き込み or 追記モードと,テキストファイル or バイナリファイルの組み合わせを指定する.
モード | 意味 | 備考 | |
ファイルが存在する場合 | ファイルが存在しない場合 | ||
"r" | 読み込み専用でファイルを開く.read | 正常終了 | エラー |
"w" | 書き込み専用で新規ファイルを開く.write | 元ファイルが消去され,新たに作成 | ファイルが新規作成される |
"a" | 追加書き込み専用でファイルを開く.append | 元のファイルの末尾に追記 | ファイルが新規作成される |
"r+" | 読み書き用でファイルを開く. | 正常終了 | エラー |
"w+" | 読み書き用で新規ファイルを開く. | 元ファイルが消去され,新たに作成 | ファイルが新規作成される |
"a+" | 読み込み+追加書き込み用でファイルを開く. | 元のファイルの末尾に追記 | ファイルが新規作成される |
fopen()
でバイナリファイルを開く場合は,"rb","wb"
のように,モードの文字列に b
をつける.
(b
を省略した場合はテキストモードとみなされる.)
テキストモードとバイナリモードでは,改行コードの扱いが異なる.
開きたいファイルの性質を考慮して,正しいモードで開く必要がある.
テキストファイルをバイナリモードで開くことも可能であり,その逆も(無意味だが)可能であるが,正しい動作をしない可能性がある.
OSによっては,モード設定が意味をなさない場合もある.
最後に,ファイルの読み書きが終わったあとは,必ず fclose()
関数を用いてファイルを閉じなければならない.
ファイルを閉じないと,ファイルにデータが書かれなかったり,ファイルそのものが壊れてしまう場合がある.
fclose()
で閉じたファイルは,プログラム中のそれ以降の処理では読み書き処理ができない.
再度,読み書きする場合は,また fopen()
関数を用いてファイルを開きなおす必要がある.
fopen()
は,ファイルのオープンに成功すると,そのファイルを操作するためのファイルポインタを戻り値として返す.
逆に,「指定したファイルが無い」とか,「ディスクが満杯」,「別のプログラムで開いてロックされている」など,なんらかの原因によりオープンに失敗すると,NULL
ポインタを返す.
このため,以下のような fopen()
の戻り値のチェック処理を必ず入れる必要がある.
ファイルが開けなかった場合で,処理の続行が不可能の場合は,exit()
関数でプログラムを終了させると良い.
main()
関数内であれば,return
でも良い.
#include <stdio.h>
#include <stdlib.h> // exit() 関数用
int main(void)
{
FILE *fp;
const char fname[] = "datafile.txt"; // ファイル名を表す文字列.
fp = fopen(fname, "w");
if( fp == NULL ) {
printf("Cannot open %s !\n", fname);
exit(1); // ファイル処理が続行出来ないので,ここで強制終了
}
// ここで読み書き処理をする
fclose(fp); // ファイルを閉じる
return 0;
}
fopen()
とif
を同じ行にまとめて
FILE *fp;
if( (fp = fopen(fname, "w") ) == NULL ) {
と書いたり,ファイルポインタの定義とfopen()
を同じ行にまとめて
FILE *fp = fopen(fname, "w");
if( fp == NULL ) ...
と書いても良い.
fp = fopen(fname, "w")
の式全体の値が,代入された値と等値になるため,この式全体を条件式内で if( (fp = fopen(fname, "w") ) == NULL )
と比較することができる.if( !fp )
と書く例もあるが,NULL==0 を前提とするため,あまりお勧めできない.
fopen
が NULL
を返し,エラーメッセージが正しく表示されることを確かめよう.)
ファイルを無事に開くことができたら,ファイルポインタを使ってデータの読み書きを行う. まず,データを「読む」,「書く」の意味(データの流れる方向)については,
と覚えておこう.「読む」「書く」の主語は,自分の書いたプログラム,と考えればよい.
以下,順にファイル入出力関数の使い分けおよび実例を学ぼう.
fgetc(), fputc()
.テキストファイル・バイナリファイルともに使用可fgets(), fputs()
,テキストファイルのみ使用可fscanf(), fprintf()
,テキストファイルのみ使用可fread(), fwrite()
,基本的にバイナリファイル.(実はテキストファイルもOK)FILE
ポインタには,現在操作しているファイルの「どこを読み書きしているか」の情報が含まれる.
読み込み・書き込みモードでファイルを開いた直後は「ファイルの先頭」を指し示している.
追加モードでファイルを開いた場合は「ファイルの最後」を指し示しており,ここから書き込みが行われる.
ファイル関連の関数でファイル読み書きをすると,ファイルを読み書きしている位置は自動的にインクリメントされる.
ファイル読み書きの基本単位は 1 バイト ( byte ) であり,1 バイトづつ読み書きする関数が用意されている.
int fgetc(FILE* fp);
int fputc(int 書き出す値, FILE* fp);
fgetc()
int
型の値として返す.EOF
(End Of File, -1と定義されている) を返す.
fputc()
EOF
を返す.
fgetc(), fputc()
関数は,ファイルに書かれている内容を 1byte = 8bit の数値として取り出す/書き出すだけである.
テキストファイル・バイナリファイルを問わずいかなるファイルでも読み書きできる.
その反面,1バイトごとの処理であるので,大量のデータ読み書きにおいては処理速度が遅く,効率が悪い.
ファイルから1バイトごとに読み込む例.
exeファイルと同じフォルダーに,sample.txt という名前のファイルが必要.
テキストファイル,バイナリファイルどちらも実行可能.
#include <stdio.h>
int main(void)
{
// ファイルを開く
FILE *fp = fopen("sample.txt", "r");
if( ! fp ) { // fp == NULL と等値.
printf("File open error\n");
return 1; // exit()でもOK
}
char c;
while((c = fgetc(fp)) != EOF){ // ファイルから 1 byte読み込んで c に代入するとともに,EOFを検出
printf("%d\n", c);
}
fclose(fp);
return 0;
}
ファイルから 「行」単位で文字列の入出力を行うには,fputs()
関数や,fgets()
関数を使用する.
注:「行」という考え方がそもそもテキストファイルであるので,この関数はテキストファイル用である.
fgets()
は,ファイルに書かれている内容が文字・数値にかかわらず文字(文字コード)として読み込んでくる.
例:ファイルに100と書かれていた場合,数値の100でなく,文字列の「1」「0」「0」として,3文字読み込む.
int fputs(char* 文字列, FILE* fp);
char* fgets(char* 文字列バッファ, int 文字列バッファのサイズ, FILE* fp);
fputs()
EOF
を返す.
fgets()
NULL
ポインタを返す.
テキストファイル入出力で一番柔軟性が高いのは,fprintf(), fscanf()
関数である.
書き出し(出力)int fprintf(FILE* fp, char* 書式指定文字列, 変数...);
読み込み(入力)int fscanf(FILE* fp, char* 書式指定文字列, 変数...);
fprintf()
と fscanf()
は,画面・キーボード入出力で使う printf()
や scanf()
と,ほぼ同じ引数である.
違いは,入出力先がファイルとなるため,第1引数にファイルポインタが追加されている点である.
注:テキストファイルに書かれている内容は基本的に文字・文字列である. 例えばファイル内に「100」と書かれていても,整数の百ではなく,文字の「いち,ぜろ,ぜろ」に過ぎない.
この場合,fscanf
を使用して読み込むと,書式指定文字(%f
や%d
)により,scanf
と同様,文字を数値に変換してくれる.
以下のプログラムを順次実行してみよう.
fprintf_sample.txt という名前のファイルが生成されているはずである.
エディタなどで開いて中身を確かめよう.
fprintf()
の出力は画面ではなく,ファイルに対して行われるため,画面には何も表示されない.
書き出しの例
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE* fp;
const char fname[] = "fprintf_sample.txt";
/***********************************************/
/* 書き込みモードでテキストファイルを開く */
/***********************************************/
fp = fopen(fname, "w");
if( fp == NULL) {
printf("File Open Error\n");
exit(1);
}
/***********************************************/
/* fprintfの例 */
/***********************************************/
int k = 10;
fprintf(fp, "%d ,%d ,%d \n", k, k+10, k+20); /* カンマ区切りで数値を出力する例. */
fprintf(fp, "%X\n", k*10); /* 16進数で数値を出力する例. */
fprintf(fp, "My name is %s\n", fname); /* 文字列を出力する例. */
fprintf(fp, "This is a pen.\n"); /* 文字列を出力する例. */
fprintf(fp, "\n");
fclose(fp); /* これを忘れてはいけない. */
return 0;
}
次に,読み込みの例を示す.
この実行には,上記の出力ファイル fprintf_sample.txt が必要.
読み込みの例
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE* fp;
const char fname[] = "fprintf_sample.txt";
/***********************************************/
/* 読み込みモードでテキストファイルを開く */
/***********************************************/
fp = fopen(fname, "r");
if( fp == NULL) {
printf("File Open Error\n");
exit(1);
}
/***********************************************/
/* fscanfの例 */
/***********************************************/
char buf[100]; /* 読み込み用バッファ */
while( fscanf(fp, "%s", buf) != EOF) { /* EOF = end of file を表す記号 */
printf("%s\n", buf);
}
fclose(fp); /* これを忘れてはいけない. */
return 0;
}
bmp, jpeg画像ファイル,mp3, wav音声ファイル,mp4, mov動画ファイルなどは人が直接読むための文字列ではないため,テキスト入出力関数では扱えない.
このようなデータを扱う場合,以下のバイナリ入出力関数を用いる必要がある.
バイナリファイルは,メモリ上に置かれたデータ(0,1 の並び)を,そのままディスク上に保存するファイル形式であり,配列などの保存にとても便利である.
また,テキストファイル処理に必要な行末の探索処理や,数値⇔文字列の変換などを行わないため,読み書きが非常に高速である.
(テキストファイルの読み書きより,数十倍~数百倍速いことが多い.)
int
型の整数 5 のメモリ上での表現は,ビッグエンディアンでは 00 00 00 05 だが,リトルエンディアンでは 05 00 00 00 となる.
エンディアン方式が異なるPC間でバイナリファイルをやり取りする場合はバイトの並び(バイトオーダー)が逆順になっているので,その変換が必要となる.
バイナリファイルの読み書き
size_t fread(void* buffer, size_t データ1個のサイズ, size_t 個数, FILE* fp);
size_t fwrite(void* buffer, size_t データ1個のサイズ, size_t 個数, FILE* fp);
(1)
void* は,voidポインタであり,「どんな型でも指すことができる汎用ポインタ」である.
従って,第1引数には,char,int, float や doubleなどの単純型や,配列,構造体など,ありとあらゆる変数のアドレスを渡すことができる.
(2)
size_t 型は,「処理系がもつ最大の値を格納できる整数」と定義され,一般に符号なし整数である.
これらの関数は,buffer
を先頭アドレスとする配列データなどを,指定されたバイト数だけファイルに読み書きする関数である.
読み書きするデータの総バイト数は,「データ1個のサイズ」×「個数」バイトとなる.
「データ1個のサイズ」については,機種により変化することがあるので,定数で 2 や 4 と書くより,sizeof()
演算子を用いて,sizeof(int)
やsizeof(float)
と書いておくほうが良い.
バイナリファイルの出力例
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
const int N = 5; // 配列の要素数
float a[5] = {0.0, 1.2, -5.3, 99.99, 10000}; // float 配列
FILE* fp;
const char fname[] = "data.bin"; // ファイル名
fp = fopen(fname, "wb"); // バイナリ書き込みモードで開く
if( fp == NULL ) {
printf("Cannot open %s \n", fname);
exit(1);
}
// バイナリ出力
fwrite(a, sizeof(float), N, fp);
fclose(fp); // ファイルを閉じる
printf("%s was written.\n", fname);
return 0;
}
は,float
型配列 5 個分 = 20 byte のデータをファイルに書き出す.
(sizeof
は,「変数1個分のサイズを返す」演算子である.
ここでは sizeof(float) == 4
であるが,定数で「4」と書かずに,このように記述しておくことにより,プログラムの意図をはっきり示すだけでなく,将来の移植性も担保できる.)
float
型配列を書き出し,書き出されたファイル名 data.bin のファイルサイズを確認しよう.予想したサイズになっているだろうか.fread()
関数で 4バイトずつfloat
型変数に5回読み込んで,画面に表示してみよう.float r;
for(...) { // 5回繰り返す
fread(&r, sizeof(float), 1, fp); // float 1 個分を,バイナリ形式で読み込む.
printf("r = %f\n", r);
}
または,
const int N = 5;
float r[N];
fread(&r[0], sizeof(float), N, fp); // float 5 個分を,バイナリ形式で一括読み込み.
一つのファイル内に,何文字,何行のデータが含まれているかは,ファイルを実際に読み込んでみなければわからない場合がある.
そのような場合は,何らかの方法でファイルの末尾を検出する必要がある.
feof()
関数int feof(FILE* fp);
feof()
は,現在の位置がファイルの終わりであれば 0以外の値 を,そうでなければ 0 を返す.
ファイルの中身にどれだけデータが書かれているか不明の場合に,while
文と組み合わせて大いに役立つ関数である.
ファイルの終端まで 1byte ずつ読む例
sometext.txt をダウンロードして,実行ファイルと同じフォルダに保存すること.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE* fp;
fp = fopen("sometext.txt", "r");
if( fp == NULL ) {
printf("File open error\n");
exit(1);
}
char c;
while ( ! feof(fp) ) {
fscanf(fp, "%c", &c); // fgetc() でもOK
printf("%d , %c\n", c, c); // 1文字読み込んで,10進数と文字で表示.
}
fclose(fp);
return 0;
}
上記のプログラムを作成し,実行してみよ.
ダウンロードした sometext.txt と,出力結果を見比べよう.
ここでは,実際のファイル入出力のサンプルプログラムを例示する.
パターンは決まっているので,サンプルを各自で実行してみて,定石を覚えてしまうと良い.
テキストファイル myfile.txt を行単位で読み込む例.
プログラム実行時に,このファイルが実行ファイルと同じフォルダ内に無いとエラーになる.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE* fp;
const char fname[] = "myfile.txt"; // ファイル名
// 読み込みモードでテキストファイルを開く
fp = fopen(fname, "r");
if( fp == NULL) {
printf("File Open Error\n");
exit(1);
}
///////////////////////////////////////////////////
// fscanfの例
// 先頭から最後まで1行づつ読み込んで画面に表示
///////////////////////////////////////////////////
const int N = 100; // 読み込みバッファのサイズ.1行の文字数より大きい値にしておく
char buffer[N]; // 読み込みバッファ. ここに 1 行分のデータを格納する
while( ! feof(fp) ) {
fscanf(fp, "%[^\n]%*c", buffer); // 1 行読み込み.fgetsでもOK
printf("%s\n", buffer); // 読み込んだ1行を文字列で表示,確認
}
fclose(fp);
return 0;
}
ファイルの中身と,画面に表示される文字とを比べてみよう.
データファイル data.csv 右クリックでダウンロード.
まずこのファイルをエディタで開いて,中身の構造を確認しよう.テキストファイルである.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE* fp;
const char fname[] = "data.csv"; // ファイル名
// 読み込みモードでテキストファイルを開く
fp = fopen(fname, "r");
if( fp == NULL) {
printf("File Open Error\n");
exit(1);
}
int num;
while( fscanf(fp, "%d,", &num) != EOF ) { // camma 区切りの数値を1個読み込む
printf("%d\n", num); // 読み込んだ値を表示
}
fclose(fp);
return 0;
}
以下のプログラムを作成し,実行してみよ. 出力される myfile.bin は,どのようなファイルとなるか.
#include <stdio.h>
#include <stdlib.h>
// 配列を表示する関数
void disp_array(int data[], int n)
{
int i;
for(i=0; i<n; i++) {
printf("data[%d] = %d\n", i, data[i]);
}
}
// 配列をゼロクリアする関数
void clear(int data[], int n)
{
int i;
for(i=0; i<n; i++) {
data[i] = 0;
}
}
int main(void)
{
FILE* fp;
const char fname[] = "myfile.bin"; // ファイル名
// 書き込みモードでバイナリファイルを開く
if( (fp = fopen(fname, "wb")) == NULL) {
printf("File Open Error\n");
exit(1);
}
const int N = 5; // データ個数
int data[N] = {10, 20, 30, 40, 50}; // データ本体
if( fwrite(data, sizeof(int), N, fp) != N) {
printf("write error\n");
exit(1);
}
fclose(fp);
// ファイルに保存したので,配列の中身をいったんゼロクリア
clear(data, N);
/*************************** ここでいったん区切り *****************************************/
printf("Before reading:\n");
disp_array(data, N);
// 読み込みモードでバイナリファイルを開く
if( (fp = fopen(fname, "rb")) == NULL) {
printf("Open Error\n");
exit(1);
}
if( fread(data, sizeof(int), N, fp) != N) {
printf("read error\n");
exit(1);
}
fclose(fp);
printf("After reading:\n");
disp_array(data, N);
return 0;
}
以下の問それぞれに対応するプログラムを作成しなさい.
ファイルのダウンロードは,リンクを右クリック,「名前を付けてリンクを保存」などをクリックして,実行ファイルと同じフォルダに保存する.
まず,テキストファイルhello.txtをダウンロードする.
プログラムからこのファイルを開き,fscanf()
関数で「1文字」ずつ読み込み,
ファイル終端まですべての文字について,
「文字:そのアスキーコード16進数」を画面に表示せよ.
表示できない文字は,文字コードのみ表示すること.
(ヒント:ライブラリ関数isprint()
を調べてみよ.)
実行例.以下の値になるとは限らない H : 0x48 e : 0x65 l : 0x6C l : 0x6C o : 0x6F : 0x12 W : 0x55 ...
フォルダー内の任意のテキストファイル(例えばcppソースファイル)の名前をキーボードから入力すると,
そのファイルを1行ごとに読み込み,各行の先頭に4桁の10進数で行番号を付加し,画面に出力するプログラムを作成せよ.
ヒント1:行番号は,printf()
内の書式指定子を "%04d"
とする.
ヒント2:fscanf(), fgets(), fgetc()
いろいろなバージョンで作成してみよう.
【実行例】 Input a filename to read:test.cpp <-キーボードから入力 0001 #include <stdio.h> 0002 0003 int main(void) 0004 { … 0022 return 0; 0023 }