ファイル処理 | Programming Place Plus 新C++編

トップページ新C++編

このページの概要

このページでは、ファイルに書き込まれているデータを使ったり、データをファイルへ書き出したりする方法を取り上げます。ファイルの取り扱いができるようになると、プログラムを実行して得られた結果を取っておいたり、その結果を再びプログラムに読み込ませて再編集したりといったことも可能になり、実現できることの幅が一気に広がります。

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



ファイル

ここまでのページでは、電卓プログラムを題材にしながら、C++ の機能を解説してきました。まだ不満が残る状態かもしれませんが、電卓プログラムの話は終わりにして、このページから、ごく簡単な「蔵書リスト」のプログラムを題材としていきます。自分がこれまでに買った本の一覧表を作り、内容を閲覧、検索、編集できるようにします。本の追加もプログラムから行います。

この手のプログラムを作るにあたって必須となる知識が、このページのテーマである「ファイル (file)」です。ファイルといえば、ソースファイルもファイルですし、プログラムをビルドすると作られる実行ファイルもファイルですが、ここでは、データを保存しておく目的でファイルを使います。

これまでのプログラムは、実行結果が画面に表示されるだけですから、計算などで得られたデータは、プログラムの実行が終わると消失していました。もう1度同じデータが欲しいと思ったら、もう1度プログラムを実行しなおすしかありません。しかし蔵書リストのようなプログラムでは、実行を終えるたびにデータが消えてしまってはいけませんから、どこかにデータを保存しておく必要があります。その保存先が、ファイルということです。

ストリーム

ファイルを使った具体的なプログラムの話に入る前に、ストリーム (stream) という用語について触れておきます。

ストリームは「流れ」を意味する英単語ですが、ここでは「データの流れ」という意味になります。データの流れとはつまり、入力や出力のことです。

これまで、入力といえばキーボードからの入力、出力といえば画面への出力でした。ソースコード上では std::cin とか std::cout を使って表現しました。

std::cout << "xyz\n";  // 画面へ出力
std::cin >> str;       // キーボードから入力

データが流れているのだと捉えると、<<>> という記号を使っている意味も見えてくると思います。<< は左方向に、>> は右方向にデータが流れていく様子を表現しています。ここで std::cout は << の先に書かれているので、流れの先(つまりデータが行き着く先)を表しているといえます。つまり、行き着く先は画面ということです。std::cin の方も同様で、>> の左側に書かれているので、データの出どころを表していて、それはキーボードということになります。

std::cout は「画面」、std::cin は「キーボード」だという理解で進めてきましたが、これはデフォルトがそうなっているというだけのことです。話が難しくなるので詳しくは取り上げませんが、std::cout や std::cin が具体的には何であるのかを変更する方法があります。そこで、std::cout が表すものを「画面」と決めつけるのではなく、標準出力 (standard output) と呼びます。std::cin も「キーボード」だと決めつけるのではなく、標準入力 (standard input) と呼びます。今後はこのように表記します。

ファイルからの入力や、ファイルへの出力も同じように、データが流れるのだと考えます。標準出力への出力とファイルへの出力は、データが流れているという捉え方をするとまったく同じものです。違うのはデータの「行き着く先」だけです。入力も同様で、「出どころ」だけが異なります。実際、C++ ではファイルへの出力と入力を次のように書けるのですが、違うのは「行き着く先」と「出どころ」の部分だけであることがわかります。

std::cout << "xyz\n";  // 画面へ出力
ofs << "xyz\n";        // ファイルへ出力(ofs は std::ofstream型の変数)

std::cin >> str;       // キーボードから入力
ifs >> str;            // ファイルから入力 (ifs は std::ifstream型の変数)

コメントにある std::ofstreamstd::ifstream については、あとで実際にプログラムを書くときに取り上げます。

ファイルへ出力することをよく「ファイルに書き出す書き込む)」といい、ファイルから入力することをよく「ファイル(の内容)を読み込む読み取る)」といいます。

ソースコードのかたちは同じになりますが、ファイルの場合には特有の難しさがあります。たとえば、a というファイルからデータを入力しようと思っていたが、実際には a というファイルは存在しないかもしれません(誰かが誤って消してしまったかもしれない)。b というファイルにデータを出力しようと思ったら、b の書き換えは許可されていないかもしれません(ファイルには「読み取り専用」のような設定ができます)。本格的なプログラムでは、こうした難しさにもきちんと向き合わなくてはなりませんが、最初からすべてを考慮することは難しいので、当面は厄介ごとは起こらない前提で話を進めていきます。

パス

もう1つ重要な用語として、パス (path) があります。

パスは、あるファイルがどこに置かれているのかをあらわす文字列のことです。ファイルはコンピュータの記憶装置(ハードディスクや SSD など)に保存されているわけですが、その具体的な所在地を表現しているもので、たとえば Windows では「C:¥Users¥readme.txt」といったふうに表現できます。この場合、Cドライブにある Users というフォルダの中にある readme.txt を表しています。

「Windows では」といったように、ほかの環境では異なる表現方法を取ることがあります。

フォルダという言葉が出ましたが、これと同じ意味で使われるディレクトリ (directory) という用語があります。プログラミングでは、ディレクトリという用語を使う場合が多いです。

絶対パスと相対パス

さきほどの「C:¥Users¥readme.txt」というパスの表記方法では、表現したいファイルの場所にいたるまでのすべてのディレクトリを ¥ で区切りながら書き並べています。このような方法は、絶対パス (absolute path) といいます。

絶対パスによる表記のルールには環境によって違いがあり、この例では C: の部分は Windows に特有のものです(ドライブレターといいます)。ディレクトリを区切る文字についても ¥ ではなく / を使う場合があります。

パスの表記方法にはもう1つ、相対パス (relative path) と呼ばれるものがあります。この方法では「今注目している場所(ディレクトリ)」という考え方を導入します。そのようなディレクトリのことを、カレントディレクトリ (current directory) と呼びます。

相対パスでは、カレントディレクトリを基準の位置として、そこからの経路を書き並べていくことでパスを表現します。たとえば、カレントディレクトリが「C:¥Users」であるとすれば、「C:¥Users¥readme.txt」のことを「readme.txt」と表現できますし、「C:¥Users¥Programs¥main.cpp」のことを「Programs¥main.cpp」と表現できます。

あるいは、カレントディレクトリの絶対パスをあらわす特別な表記「.」を用いて、「.¥readme.txt」とか「.¥Programs¥main.cpp」とも表現できます。

カレントディレクトリが「C:¥Users¥Programs」のときに、「C:¥Users¥readme.txt」を表したい場合はどうでしょう。この場合、まず1つ上のディレクトリに上がる必要があります。こういう場合には「..」という特別な表記方法を用います。相対パスの中にあらわれる「..」は、「1つ上のディレクトリ」という意味をもちます。そのため、「..¥readme.txt」と書けば、「C:¥Users¥readme.txt」を表すことになります。

ファイルに出力する (std::ofstream)

では、実際にファイルを使ったプログラムを書いてみます。

前に少し触れたとおり、ファイルを使ったプログラムには特有の難しさがあって、きちんと書こうと思うとなかなか大変です。最初からきちんとやるのは難しいですし、最低限のエラーチェックだけを入れるにしてもソースコードが大きくなって理解しづらくなるので、まずはすべてがうまくいく前提で書きます。エラーへの対策については、次のページで取り上げます。

次のプログラムは、test.txt という名前のファイルに “Hello.” を出力します。

#include <fstream>

int main()
{
    std::ofstream ofs("test.txt");
    ofs << "Hello.\n";
}

実行結果(標準出力):

実行結果(test.txt):

Hello.

プログラムは非常に短いですが、説明しなければならないことはたくさんあります。

ファイルをオープンする

まず、ファイルを取り扱うためには、ファイルをオープン(開く) という手順を踏まなければなりません。本やノートに書き込むためには、まず開かなければいけないのと同じことです。オープンされていないファイルへ書き込む方法はありません。これは読み込みの場合でも同様ですが、やり方が少し異なります(読み込みの方は、あとで取り上げます)。

書き込みのためにファイルをオープンするには、std::ofstream型 の変数を宣言して、オープンしたいファイルのパスを指定します。それが、std::ofstream ofs("test.txt"); という文がしていることです。"test.txt" は相対パスによる表記で、「カレントディレクトリの test.txt」を意味しています。なお、std::ofstream を使うためには、#include <fstream> という記述が必要です。

カレントディレクトリは、Visual Studio からプログラムを実行した場合は、Visual Studio のプロジェクトファイル(拡張子が .pxproj のファイル)が置かれている場所です。Windows のエクスプローラーから実行ファイルを実行した場合には、その実行ファイルが置かれているディレクトリがカレントディレクトリです。

実のところ、パスの表記方法のルールは処理系定義ということになっているため1、どういう記述なら問題なくて、どういう記述ではパスとして認識してくれないかは、処理系次第です。とはいえほとんどの場合、相対パス、絶対パスのどちらでも受け付けてくれます。また、ディレクトリの区切りとして / が使えることが多いです。Windows では区切りに \ を使うことがありますが、これはエスケープシーケンスのために使われる文字でもあるため、"\\" のように2つ重ねなければなりません。

【C言語プログラマー】早い話、fopen関数と同じだということです。

std::ofstream によって、書き込み先のファイルが無事にオープンされると、そのファイルの中身は空になります。これは意外に思えるかもしれませんが、これからデータを書き込もうとするわけですから、余計なものがない状態から始まってくれたほうが都合が良いことがほとんどです。もし、ファイルの中身はそのままに、その後ろに書き足したいだとか、中身を部分的に書き換えたいといった場合には、少しやり方を変える必要があります。この件は、ページをあらためて取り上げます。

なお、std::ostream によって、書き込み先のファイルをオープンしようとしたとき、指定したファイルが存在しなかった場合には、自動的に空のファイルが作成されます。つまり、ファイルが存在しないことはエラーの原因にはなりません。この挙動は std::ofstream、つまり出力のためのオープンの場合です。入力の場合には、これからデータを読もうとしているのですから、ファイルが存在しないことはエラーになります。

【C言語プログラマー】fopen関数の第2引数に “w” を指定したときと同じ挙動です。

ファイルへ書き込む

ファイルへの出力は std::cout や std::ostringstream と同じで、<< を使います。宣言した std::ofstream型の変数に対して << を使って、ofs << "hello" とか ofs << 123 のように書きます。

ファイルをクローズする

ファイルの処理は、クローズ(閉じる) という手順を踏んで終了します。C++ ではこの手順を明示的に書くこともできますし、暗黙的(自動的)にさせることもできます。明示的にクローズするには、以下の1文を書きます。

ofs.close();    // ofs は std::ofstream型の変数

ファイルがクローズされると、オープンする前の状態に戻ります。したがって、クローズ後には書き込みの処理は行えません。

【C言語プログラマー】std::ofstream型の変数の生存期間が終わるとき、ファイルは自動的にクローズされます。これを実現する仕組みは、C++ のデストラクタという機能です。デストラクタは、オブジェクトが生存期間を終えるときに自動的に呼び出される特殊な関数で、この中で fclose関数に相当する処理が行われています。しかし自動的に呼び出される仕組みになったことで、C言語でよく推奨される fclose関数の戻り値を使ったエラーチェック(C言語編第39章)が行えないという問題があります(std::ofstream のデストラクタは、エラーを握りつぶしてしまいます2)。エラーチェックを行うのならば、デストラクタによってクローズされる前に、明示的にクローズする必要がありますが、close関数の戻り値は void です。そのため、close関数の呼び出し後にさらに、if (ofs.fail())if (!ofs) のようにして判定しなければなりません。

ファイルから入力する (std::ifstream)

次は入力です。さきほどのサンプルプログラムで出力された test.txt を読み込んで、その内容を標準出力へ出力するようにします。

#include <fstream>
#include <iostream>
#include <string>

int main()
{
    std::ifstream ifs("test.txt");
    std::string s {};
    std::getline(ifs, s);
    std::cout << s << "\n";
}

実行結果(標準出力):

Hello.

test.txt の内容:

Hello.

入力の場合も、まずファイルをオープンしなければなりません。そのためには、std::ifstream型の変数を宣言し、入力したいファイルのパスを指定します。std::ofstream と同様、#include <fstream> の記述が必要です。

std::ofstream と std::ifstream とでは、オープンに関する挙動に違いがあります。std::ofstream ではオープンされたファイルの中身が空になりますが、std::ifstream では何も変化しません。また、std::ofstream では指定したファイルが存在しなかったときには自動的に空のファイルが作成されますが、std::ifstream ではエラーになります。

【C言語プログラマー】std::ifstream の挙動は、fopen関数の第2引数に “r” を指定したときと同じです。std::ofstream は “w” と同じです。

この挙動の違いは、出力と入力の違いから来るものです。出力の場合は、これから新規のデータを書き込もうとしているので、「元あったデータはいらない」「ファイルがなければ新規で作る」という動作を取ります。入力の場合は、既存のデータを読み取りたいので、「元あったデータはそのまま」「ファイルが無いのなら読み込みようがないのでエラー」という動作を取ります。

無事にファイルがオープンできたら、std::getline(ifs, s); のようにして1行分の内容を読み込んで、変数s に格納します。もちろん ifs >> s; も可能ですが、こちらだと空白文字のところで読み込みを止めてしまいます(「文字列の入力」のページを参照)。

ファイルの終わりまで読み込む

ファイルの内容が複数行にわたっている場合、繰り返し読み込みを行えば2行目以降を読むことができます。しかし、どこで終わればいいのかが問題になります。

while (/* どこかで打ち切らないと・・・ */) {
    std::string s {};
    std::getline(ifs, s);   // 新しい行を読み込む
    std::cout << s << "\n";
}

ファイルの終わりまで読み込み済みであるかどうかは、ifs.eof() で問い合わせることができますifs は std::ifstream型の変数)。この式から得られるものは、すでにファイルの終わりまで読み込みを終えていたら true、終えていなかったら false です。なお「eof」とは「end of file(ファイルの終わり)」を略したもので、ファイル処理の話の中でよく出てくる言葉です。

ifs.eof() を使うと次のように書けます。

#include <fstream>
#include <iostream>
#include <string>

int main()
{
    std::ifstream ifs("test.txt");

    while (ifs.eof() == false) {  // ファイルの終わりまで読み込み済みなら終了する
        std::string s {};
        std::getline(ifs, s);     // 1行分読み込む
        std::cout << s << "\n";
    }
}

test.txt の内容:

1st line.
2nd line.
3rd line.

実行結果(標準出力):

1st line.
2nd line.
3rd line.

ifs.eof() == false は、先のページで解説する ! という演算子を使って !ifs.eof() とも書けます。

まとめ


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク


練習問題

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

出力先のファイルのパスを、絶対パスで指定した場合と、相対パスで指定した場合とで、実際にファイルが作られる場所がどのように変わるか試してください。

ビルドして得られた実行ファイルをさまざまな場所にコピーして、そこで実行してみましょう。

解答・解説

問題2 (基本★★)

標準入力から入力された文字列を、ファイルへ書き出していくプログラムを作成してください。“exit” が入力されるまで繰り返すものとします。

解答・解説

問題3 (応用★★)

ファイルの行数をカウントするプログラムを作成してください。

解答・解説

問題4 (応用★★)

次のように、各行に3つの整数が、空白で区切られながら記述されているファイルを読み込み、各行の整数の合計値を標準出力へ出力するプログラムを作成してください。

203 1100 95
635 55 300
1400 2 16
80 65 54

解答・解説


解答・解説ページの先頭



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る