今回はC++のファイル入出力について説明します。プログラムの典型的な動作は、外部から入力されたデータを処理し、その結果を外部へ出力することです。外部とのインターフェースはいろいろありますが、基本となるのがファイル入出力です。C++でファイル入出力を行う場合、「ストリーム (stream)」というデータを介してファイルにアクセスします。ストリームはファイルとプログラムの間でやりとりされるデータの流れという意味で使われています。
ストリームはファイルと 1 対 1 に対応していて、ファイルからデータを入力する場合は、ストリームを経由してデータが渡されます。逆に、ファイルへデータを出力するときも、ストリームを経由して行われます。C++の場合、ストリームを表すクラスが複数用意されていて、用途によって適当なクラスを選びます。そして、そのクラスから生成されたインスタンスがストリームの実体となり、データを入出力するメンバ関数を使ってファイルにアクセスします。
通常のファイルは、ストリームを生成しないとアクセスすることはできません。ただし、標準入出力はC++の起動時にストリームが自動的に生成されるので、簡単に利用することができます。一般に、キーボードからの入力を「標準入力 (standard input)」、画面への出力を「標準出力 (standard output)」といいます。
C++の場合、基本的な入力ストリームを表すクラスが istream で、出力ストリームを表すクラスが ostream です。ヘッダ iostream をインクルードすると、どちらのストリームも利用することができます。下表に標準入出力を表す変数名を示します。
| 変数名 | ファイル |
|---|---|
| cin | 標準入力 |
| cout | 標準出力 |
| cerr | 標準エラー出力 |
| clog | 標準エラー出力 |
標準エラー出力 (standard error) は、エラーメッセージなどを出力するためのストリームです。標準出力と同様にデータを画面に出力しますが、標準出力とは独立しているので、標準出力がファイルにリダイレクトされていても、エラーメッセージを画面に表示することができます。cerr はデータをバッファリングしませんが、clog はバッファリングを行います。
入力ストリームからデータを受け取る場合、入力演算子 >> を使うと簡単です。
入力ストリーム >> 変数; 入力ストリーム >> 変数a >> ... >> 変数z;
入力演算子は入力ストリームからデータを読み込み、それを変数のデータ型に合わせて変換して、その結果を変数にセットします。入力演算子は入力ストリームの参照を返すので、>> をつなげて複数のデータを入力することができます。このとき、空白文字 (空白、タブ、改行など) がデータの区切り文字になります。
簡単な例を示しましょう。
リスト : 入力演算子の使用例 (sample2801.cpp)
#include <iostream>
using namespace std;
int main()
{
int i;
cin >> i;
cout << i << endl;
double j;
cin >> j;
cout << j << endl;
string s;
cin >> s;
cout << s << endl;
}
$ clang++ sample2801.cpp $ ./a.out 12345 <-- 入力 12345 1.2345 <-- 入力 1.2345 helloWorld <-- 入力 helloWorld $ ./a.out 123 1.234 hello_world <-- 入力 123 1.234 hello_world
行全体を読み込む場合は関数 getline を使うと簡単です。
入力ストリーム& getline(入力ストリーム, string 変数名);
string の容量は自動的に拡張されるので、メモリなどのシステム資源が許す限り、長い文字列でも読み込むことができます。
簡単な例題として、標準入力からデータを読み込み、それをそのまま標準出力へ書き出すプログラムを作ってみましょう。次のリストを見てください。
リスト : ファイルの表示 (sample2802.cpp)
#include <iostream>
using namespace std;
int main()
{
string buff;
while (getline(cin, buff))
cout << buff << endl;
}
$ clang++ sample2802.cpp
$ ./a.out
abc <-- 入力
abc
def <-- 入力
def
123 <-- 入力
123
456 <-- 入力
456
$ ./a.out < sample2802.cpp
#include <iostream>
using namespace std;
int main()
{
string buff;
while (getline(cin, buff))
cout << buff << endl;
}
キーボードからデータを入力する場合、最後は CTRL-D を入力してください。ストリームは operator bool が多重定義されいて、条件式でストリームを評価すると、ストリームの状態をチェックすることができます。たとえば、ファイルの終端に到達した場合、ストリームを条件式で評価すると false を返すので、while ループを終了することができます。また、出力をリダイレクトすれば、テキストファイルをコピーすることもできます。
getline は関数だけではなく、入力ストリームのメンバ関数にも用意されています。
istream& getline(char* buff, streamsize n); istream& getline(char* buff, streamsize n, char delimit);
引数 buff が文字列を格納するバッファ、n が読み込む文字数の最大値です。streamsize は入出力処理で転送されるバイト数や、バッファの大きさを表すデータ型で、実体は符号付き整数です。
getline は入力ストリームから文字を読み込みんでバッファに格納します。最大で n - 1 文字、またはファイルの終了か改行文字まで読み込みます。区切り文字 delimit が指定されている場合は、その文字までの文字列を入力します。改行文字や区切り文字はバッファに格納されません。最後にヌル文字を付加します。
メンバ関数の getline を使う場合、プログラムは次のようになります。
リスト : メンバ関数 getline の使用例 (sample2803.cpp)
#include <iostream>
using namespace std;
int main()
{
const int M = 256;
char buff[M];
while (cin.getline(buff, M))
cout << buff << endl;
}
このほかにも、istream には便利なメンバ関数が多数用意されています。興味のある方はC++のリファレンスマニュアルをお読みください。
標準入出力を使わずにファイルにアクセスする場合、次の 3 つの操作が基本になります。
「ファイルをオープンする」とは、アクセスするファイルを指定して、それと 1 対 1 に対応するストリームを生成することです。入出力関数は、そのストリームを経由してファイルにアクセスします。C++の場合、ファイルのオープンはストリームを生成するコンストラクタで行います。ファイルをリードオープンするときはクラス ifstream を、ライトオープンするときはクラス ofstream を使います。なお、これらのクラスを使う場合はヘッダファイル fstream をインクルードしてください。
ifstream& istream("ファイル名");
ofstream& istream("ファイル名");
ifstream, ofstream は istream, ostream を継承しているので、出力演算子や入力演算子を使ってデータの入出力を行うことができます。このほかにも、便利なメンバ関数が多数用意されています。
オープンしたファイルは必ずクローズします。C++の場合、この操作をデストラクタで行います。ストリームをクローズするとともに、それに対応するファイルも自動的にクローズされます。なお、メンバ関数 close で明示的にファイルをクローズすることもできます。
簡単な使用例を示します。
リスト : ファイルのオープンとクローズ (sample2804.cpp)
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
ifstream fin("testin.txt");
if (!fin) {
cerr << "testin.txt is not found\n";
return 1;
}
ofstream fout("testout.txt");
if (!fout) {
cerr << "Can't open testout.txt\n";
return 1;
}
char c;
while (fin.get(c)) fout.put(c);
}
最初に testin.txt を ifstream でオープンします。if 文で変数 fin をチェックして結果が false の場合、エラーメッセージを出力してプログラムを終了します。次に、出力先のファイルを ofstream でオープンします。オープンできない場合はエラーメッセージを出力して終了します。
あとは while ループで testin.txt からデータを読み込み、それを testout.txt へ書き込みます。main が終了するとき、fin と fin のデストラクタが起動されて、ファイルは自動的にクローズされます。
get と put は文字単位で入出力を行うメンバ関数です。
istream& get(char&); ostream& put(char);
get は入力ストリームから 1 文字読み込んで引数の変数に格納します。put は引数の文字を出力ストリームに出力します。
それでは実行してみましょう。
$ cat testin.txt foo bar baz oops hello world $ clang++ sample2804.cpp $ ./a.out $ cat testout.txt foo bar baz oops hello world
ところで、引数のない get も用意されています。これはC言語の標準ライブラリ関数 getchar と同じで、読み込んだ文字を int (0 - 255) で返します。ファイルの終端に到達した場合は EOF (-1) を返します。引数のない get を使うと、while ループの部分は次のようになります。
リスト : get の使用例 int c; while ((c = fin.getc()) != EOF) fout.put(c);
C言語ユーザであれば、こちらの方がわかりやすいかもしれません。get は多重定義されているので、これら以外にもいろいろな使用方法があります。詳細はC++のリファレンスをお読みください。
一般に、テキストファイルの「行」は改行文字で区切られます。改行文字は OS によって異なります。'\n' (0x0a) までを 1 行とする仕様は Unix 系 OS で、Windows は '\r\n' (0x0d, 0x0a) の 2 バイトで改行を表します。
この違いを吸収するため、Window 用のC/C++ではファイルのアクセスモードに「テキストモード」と「バイナリモード」が用意されています。次に示すように、テキストモードでは改行コードの変換が行われます。
読み込み時: '\r\n' ─→ '\n' 書き込み時: '\n' ─→ '\r\n'
バイナリモードの場合、改行コードの変換は行われません。Unix 系 OS 用のC/C++では、テキストモードとバイナリモードに違いはありません。すべてバイナリモードとして扱われます。
C/C++の場合、次のように main の引数を指定することができます。
int main(int argc, char *argv[]);
argv には実行したコマンド名とコマンドラインで与えられた引数、argc には argv に格納された要素数が渡されます。
簡単な例を示しましょう。
リスト : コマンドライン引数の表示 (sample2805.cpp)
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
for (int i = 0; i < argc; i++)
cout << argv[i] << " ";
cout << endl;
}
sample2805.cpp は変数 argv の内容を表示するだけです。4 つの引数を与えて起動すると、次のように表示されます。
$ clang++ sample2805.cpp $ ./a.out foo bar baz oops ./a.out foo bar baz oops
argv[0] には実行したコマンド名 (./a.out) がセットされるので、argc はコマンド名と引数の個数を足した値 5 になります。
簡単な例として、複数のファイルを連結するコマンド concat を作ってみましょう。
リスト : ファイルの表示 (concat.cpp)
#include <iostream>
#include <fstream>
using namespace std;
void concat(const char* filename)
{
ifstream fin(filename);
if (fin) {
char c;
while (fin.get(c)) cout.put(c);
}
}
int main(int argc, char* argv[])
{
for (int i = 1; i < argc; i++)
concat(argv[i]);
}
for ループで argv からファイル名を取得して関数 concat を呼び出します。concat は filename を ifstream でオープンします。ファイルが見つからない場合は無視して、何も出力しないことにします。あとは、while ループで 1 文字読み込み、それを cout に出力するだけです。
簡単な実行例を示します。$ cat foo.txt foo $ cat bar.txt bar $ cat baz.txt baz $ clang++ concat.cpp $ ./a.out foo.txt bar.txt baz.txt foo bar baz
文字 (バイト) や行以外の単位で入出力を行うメンバ関数に read と write があります。
istream& read(char* buff, streamsize n); ostream& write(const char* buff, streamsize n);
read は入力ストリームからデータを n バイト読み込んで配列 buff に格納します。末尾にヌル文字を付加することはありません。途中でファイルの終端に到達した場合、そこでデータの入力を終了します。実際に読み込んだデータ数はメンバ関数 gcount で求めることができます。write は配列 buff に格納されているデータを n バイト出力ストリームに書き出します。
簡単な例として、read と write を使ってファイルをコピーするプログラムを作りましょう。次のリストを見てください。
リスト : ファイルのコピー (sample2806.cpp)
#include <iostream>
#include <fstream>
using namespace std;
int main(int argc, char* argv[])
{
if (argc != 3) {
cerr << "arguments error\n";
return 1;
}
ifstream fin(argv[1]);
if (!fin) {
cerr << "Can't open " << argv[1] << endl;
return 1;
}
ofstream fout(argv[2]);
if (!fout) {
cerr << "Can't open " << argv[2] << endl;
return 1;
}
const int M = 8; // テストのため小さな値に設定
char buff[M];
while (fin.read(buff, M))
fout.write(buff, M);
if (fin.gcount() > 0)
fout.write(buff, fin.gcount());
}
$ clang++ sample2806.cpp $ ./a.out sample2806.cpp sample2806.txt $ cmp sample2806.cpp sample2806.txt $
入出ストリームを変数 fin に、出力ストリームを変数 fout にセットします。それから、大きさ M の配列 buff を用意して fin.read(buff, M) でデータを読み込みます。M バイト読み込んだ場合、ストリーム fin は真を返すので、fout.write(buff, M) でデータを出力ストリームに書き込みます。
read で M バイト読み込むことができなかった場合、fin は偽を返すので while ループを終了します。fin.gcount() で読み込んだデータ数をチェックして、データがあるならば write で残りのデータを書き込みます。
なお、次のように do - while 文を使ってプログラムすることもできます。
リスト : do - while 文を使う場合
do {
fin.read(buff, M);
fout.write(buff, fin.gcount());
} while (fin.gcount() == M);
この場合、ストリームの状態をチェックしないで、読み込んだデータ数でファイルの終端に到達したことを確認することになります。
入出力演算子 <<, >> は、ストリームの状態を操作するための関数を呼び出すことができます。これを「マニュピレータ (manipulator)」といいます。たとえば、改行を出力する endl もマニュピレータの一つです。基本的なマニュピレータはヘッダファイル iostream に定義されていますが、引数付きのマニュピレータを使うときはヘッダファイル iomanip をインクルードしてください。
boolalpha は真偽値を true, false で表すマニュピレータです。bool 型データは true, false で表示され、入力するときも true, false を使用することができます。元に戻す場合は noboolalpha を指定してください。
リスト : boolalpha の使用例 (sample2807.cpp)
#include <iostream>
using namespace std;
int main()
{
cout << true << endl;
cout << false << endl;
cout << boolalpha;
cout << true << endl;
cout << false << endl;
bool a;
cin >> a;
cout << a << endl;
cin >> boolalpha >> a;
cout << a << endl;
}
$ clang++ sample2807.cpp $ ./a.out 1 0 true false 1 <-- 入力 true false <-- 入力 false
入力演算子 >> は空白文字を読み飛ばしますが、空白文字を読み込みたい場合は noskipws を指定します。元に戻す場合は skipws を指定してください。
リスト : noskipws の使用例 (sample2080.cpp)
#include <iostream>
using namespace std;
int main()
{
char a, b, c;
cin >> a >> b >> c;
cout << "(" << a << ")" << endl;
cout << "(" << b << ")" << endl;
cout << "(" << c << ")" << endl;
cin >> noskipws >> a >> b >> c;
cout << "(" << a << ")" << endl;
cout << "(" << b << ")" << endl;
cout << "(" << c << ")" << endl;
}
$ clang++ sample2808.cpp $ ./a.out a b c d e (a) (b) (c) ( ) (d) ( )
最初に cin から 3 文字読み込みます。この場合、空白文字を読み飛ばすので、変数 a, b, c には文字 a, b, c がセットされます。次に、nokipws を指定して文字を読み込むと、空白文字はスキップされないので、a に空白文字、b に d、c に空白文字がセットされます。
整数の入出力は 10 進数がデフォルトですが、hex や oct を指定すると 16 進数や 8 進数で入出力を行うことができます。10 進数に戻すときは dec を指定します。
リスト : hex と oct の使用例 (sample2809.cpp)
#include <iostream>
using namespace std;
int main()
{
int a = 256;
// cout << showbase; 基数表示
// cout << uppercase; 英大文字で表示
cout << a << endl;
cout << hex << a << endl;
cout << oct << a << endl;
int b;
cin >> hex >> b;
cout << hex << b << endl;
cin >> oct >> b;
cout << oct << b << endl;
}
$ clang++ sample2809.cpp $ ./a.out 256 100 400 ffff ffff 7777 7777
showbase を指定すると、接頭辞に 0 (8 進数) や 0x (16 進数) が付加されます。uppercase を指定すると、英大文字を使って 16 進数を表示します。取り消す場合は noshowbase, nouppercase を指定してください。
データを表示するフィールドの幅は setw(n) で指定することができます。
リスト : フィールド幅の指定 (sample2810.cpp)
#include <iostream>
#include <iomanip>
using namespace std;
int main()
{
cout << setw(4) << 10 << endl;
cout << setw(4) << 100 << endl;
cout << setw(4) << 10000 << endl;
cout << setfill('*');
cout << setw(4) << 1 << endl;
cout << setw(4) << 10 << endl;
cout << left;
cout << setw(4) << 1 << endl;
cout << setw(4) << 10 << endl;
}
$ clang++ sample2810.cpp $ ./a.out 10 100 10000 ***1 **10 1*** 10**
setw(4) でフィールド幅を 4 に指定します。10 ではフィールド幅に満たないので、右詰めに出力されています。もし、フィールド幅に収まらない場合は、10000 のように指定を無視して数値を出力します。フィールド幅を文字で埋めたい場合は、setfill(文字) で指定します。左詰めにしたい場合は left を指定します。右詰めに戻したい場合は right を指定してください。
C言語の場合、printf で浮動小数点数を出力する書式は %e, %f, %g がありますが、C++の場合はデフォルトが %g で、scientific を指定すると %e になり、fixed を指定すると %f と同じ表示になります。
簡単な例を示しましょう。
リスト : 浮動小数点数の表示 (sample2811.cpp)
#include <iostream>
using namespace std;
int main()
{
double a = 1234.5678912;
double b = 1.23456789e10;
cout << a << endl;
cout << b << endl;
cout << scientific;
cout << a << endl;
cout << b << endl;
cout << fixed;
cout << a << endl;
cout << b << endl;
}
$ clang++ sample2811.cpp $ ./a.out 1234.57 1.23457e+10 1.234568e+03 1.234568e+10 1234.567891 12345678900.000000
浮動小数点数は精度を設定することができます。デフォルトの値は 6 で、scientific と fixed は小数点以下の桁数、デフォルトでは最大有効桁数 (整数部分 + 小数点以下の部分) になります。表示する桁数ににあわせて、四捨五入が行われることに注意してください。
精度は setpercision で指定します。簡単な使用例を示します。
リスト : setpercision の使用例 (sample2812.cpp)
#include <iostream>
#include <iomanip>
using namespace std;
int main()
{
double a = 1234.5678912;
double b = 1.23456789e10;
cout << setprecision(8);
cout << a << endl;
cout << b << endl;
cout << scientific;
cout << a << endl;
cout << b << endl;
cout << fixed;
cout << a << endl;
cout << b << endl;
cout.setf(ios_base::fmtflags(0), ios_base::floatfield);
cout << a << endl;
cout << b << endl;
}
$ clang++ sample2812.cpp $ ./a.out 1234.5679 1.2345679e+10 1.23456789e+03 1.23456789e+10 1234.56789120 12345678900.00000000 1234.5679 1.2345679e+10
浮動小数点数の表示をデフォルトに戻すには、ストリームのメンバ関数 setf で floatfield を fmtflags(0) に指定してください。
C++は文字列 (string) をストリームとして扱うことができます。istringstream が入力用のストリームで、ostringstream が出力用のストリームです。文字列ストリームを使用するときはヘッダファイル sstream をインクルードしてください。
簡単な使用例を示しましょう。
リスト : 文字列ストリーム (sample2813.cpp)
#include <iostream>
#include <sstream>
using namespace std;
int main()
{
istringstream s1("1234 5678 9abc def0");
ostringstream s2;
int x;
while (s1 >> hex >> x) s2 << x << " ";
cout << s2.str() << endl;
}
$ clang++ sample2813.cpp $ ./a.out 4660 22136 39612 57072
変数 s1 に入力用の文字列ストリーム、変数 s2 に出力用の文字列ストリームをセットします。while 文で、s1 から 16 進数の整数値を読み込み、それを 10 進数で s2 に書き込みます。書き込まれたデータはメンバ関数 str で取り出すことができます。
なお、最近の規格 (C++11) から数値 (整数と浮動小数点数) を文字列に変換する関数 to_string が導入されました。逆に、文字列から数値に変換する関数に stoi や stod などがあります。stoi は文字列を int に、stod は double に変換します。これらの関数を使用するときはヘッダファイル string をインクルードしてください。
簡単な使用例を示します。
リスト : 数値と文字列の変換 (sample2814.cpp)
#include <iostream>
#include <string>
using namespace std;
int main()
{
string buff;
while (getline(cin, buff)) {
int n = stoi(buff);
double d = stod(buff);
cout << to_string(n) << endl;
cout << to_string(d) << endl;
}
}
$ clang++ sample2814.cpp $ ./a.out 123456789 <-- 入力 123456789 123456789.000000 1.23456789 <-- 入力 1 1.234568 1e10 <-- 入力 1 10000000000.000000
最後に簡単な例題として、ファイルを行単位で連結するコマンドを作りましょう。Unix 系 OS には paste というコマンドがありますが、今回作成するプログラムは paste の簡易バージョンで、空白文字を挟んで連結することにします。動作例を図に示します。
$ cat file1.txt abcde fghij klmno $ cat file2.txt ABCDE FGHIJ KLMNO 12345 $ ./mypaste file1.txt file2.txt abcde ABCDE fghij FGHIJ klmno KLMNO 12345
mypaste.cpp は 2 つのファイル file1.txt と file2.txt の各行を連結して標準出力へ出力します。この場合、2 つのファイルを同時にオープンしなければいけませんが、近代的なプログラミング言語であれば特別なことをしなくても複数のファイルを扱うことができます。
2 つのファイルをリードモードでオープンし、入力ストリームを別々の変数 file1, file2 にセットします。変数 file1 に fgets を適用すれば、ファイル 1 から 1 行分データをリードすることができます。同様に、変数 file2 に fgets を適用すれば、ファイル 2 からデータをリードできるのです。あとは、文字列を連結して標準出力へ出力すればいいわけです。
ただし、一つだけ注意点があります。それは、2 つのファイルの行数は同じとは限らないということです。つまり、どちらかのファイルが先に終了する場合があるのです。この場合は、残ったファイルをそのまま出力します。処理内容を下図に示します。
↓
┌───────┐
│file 1,2 open │
└───────┘
├←────┐
↓ │
┌─────┐ EOF┌─────┐ │
┌←─│file2 出力│←─│file1 read│ │
│ └─────┘ └─────┘ │
│ ↓ │
│ ┌─────┐ EOF┌─────┐ │
│ │文字列出力│←─│file2 read│ │
│ └─────┘ └─────┘ │
│ ↓ ↓ │
│ ┌─────┐ ┌──────┐ │
├←─│file1 出力│ │連結して出力│ │
│ └─────┘ └──────┘ │
│ ↓ │
↓ └─────┘
└──────────────┐
↓
図 : 処理の流れ
ファイル 1 が終了した場合は、ファイル 2 をそのまま出力します。ファイル 2 が終了した場合は、ファイル 1 をそのまま出力しますが、その前に入力したファイル 1 のデータが残っているので、読み込んだデータを出力することをお忘れなく。
それでは、プログラムを作りましょう。次のリストを見てください。
リスト : 行の結合 (mypaste.cpp)
#include <iostream>
#include <fstream>
using namespace std;
void output_file(ifstream& fin)
{
int c;
while ((c = fin.get()) != EOF) cout.put(c);
}
void paste(ifstream& fin1, ifstream& fin2)
{
string buff1, buff2;
while (true) {
if (!getline(fin1, buff1)) {
output_file(fin2);
break;
}
if (!getline(fin2, buff2)) {
cout << buff1 << endl;
output_file(fin1);
break;
}
cout << buff1 << " " << buff2 << endl;
}
}
int main(int argc, char *argv[])
{
if (argc < 3) {
cerr << "Not enough arguments\n";
return 1;
}
ifstream fin1(argv[1]);
if (!fin1) {
cerr << "Can't open " << argv[1] << endl;
return 1;
}
ifstream fin2(argv[2]);
if (!fin2) {
cerr << "Can't open " << argv[2] << endl;
return 1;
}
paste(fin1, fin2);
}
main で引数の個数をチェックし、ファイル名が 2 つ指定されていない場合はエラーを表示して終了します。次に、ファイル args[1] と args[2] をオープンします。オープンできない場合はエラーを表示して終了します。それから、入力ストリームを関数 paste に渡して呼び出します。
関数 paste は簡単です。fin1 から getline で 1 行読み込みます。ファイルが終了した場合は関数 output_file で fin2 を出力してから break で while ループを脱出します。次に、fin2 から 1 行読み込みます。ファイルが終了した場合は、fin1 の文字列 buff1 を出力してから output_file で fin1 を出力します。そうでなければ、文字列 buff1 と buff2 を空白文字を挟んで出力します。
関数 output_file も簡単で、ファイルの最後まで 1 文字ずつ get で読み込んで、それを put で出力するだけです。これでプログラムは完成です。興味のある方は実際にプログラムを動かしてみてください。