コンテンツへスキップ
Topos Uranos

要約

プログラムが計算したものは、プログラムが終わると消えてしまう。ただし、プログラムより長く残る場所に書き残しておけば別である。このレッスンはその場所、すなわちテキストファイルと、根本的に新しいことを何も学ばずにそれを使えるようにする考え方を紹介する。ファイルはストリームとして、キーボードや画面と同じ操作で読み書きされる。学ぶのは、std::ifstream と std::ofstream、ファイルを使う前に開けたかどうかを確かめること、そしてどこを探すかを決める作業フォルダである。さらに、正しい読み取りループ while (std::getline(in, line)) を、データを一つ余分に処理する eof() のループと対比し、その余分なデータが生じる正確な理由を見る。std::println による書き込み、追記モード、ブロックを出るときにストリームが閉じられることも扱う。最後に行の形式を扱う。フィールドをカンマで区切り、std::from_chars で検証し、形式の崩れた行をどう扱うかを仕様の中で決めることである。

学習目標

  1. 読み取り用または書き込み用にファイルを開き、使う前に開けたことを確かめる。その際、存在しないファイルと、別のフォルダで探されているファイルとを区別する。
  2. while (std::getline(in, line)) でファイルを一行ずつ読み、ストリームの状態を追って、while (!in.eof()) がなぜデータを一つ余分に処理するかを説明する。
  3. std::println(out, …) で結果をファイルに書き、内容を置き換えるか末尾に追記するかを選ぶ。
  4. カンマ区切りの値のファイルをレコードのベクターに読み込み、各行を検証し、不正な行をその行番号とともに報告する。

ファイルはストリームである

永続性

プログラムの変数はメモリに住み、プログラムのメモリはプログラムが終わると解放される。次の実行は何も覚えていない状態で始まる。あるデータが、それを生み出したプログラムより長く残るには、永続的な媒体に置かれなければならない。汎用の媒体がファイルである。名前をもち、ディスクに保存されたバイトの並びで、オペレーティングシステムが実行と実行のあいだ保持する。テキストファイルとは、そのバイトが文字であるファイルのことで、文字はレッスン テキストは ASCII ではない で見たように UTF-8 で表され、文字 '\n' で区切られた行に整理されている。もっとも素朴で、もっとも長持ちする形式である。どのエディタでも開け、どの言語でも読め、人が一目で確かめることもできる。

とはいえ、C++ はファイルをキーボードと別のものとしては扱わない。レッスン 入力・出力・エラー 以来、std::cin は入力のストリームである。順に消費される文字の供給源で、直前の操作が失敗したかどうかを記録する状態をもつ。読み取り用に開いたファイルは別の入力ストリームで、型は std::ifstream(input file stream から)である。書き込み用に開いたファイルは出力ストリームで、型は std::ofstream である。どちらもヘッダー <fstream> で宣言されている。したがって、レッスン 入力を正しく読む で std::cin について学んだこと、つまり演算子 >>、std::getline、エラー状態は、そのままファイルにも当てはまる。

開いて確かめる

ファイルはストリームを作るときに開かれ、ファイル名は波括弧の中に書く。std::ifstream in{"grades.txt"};。開くことは失敗しうる(ファイルが存在しない、読む権限がない、名前を書き間違えた)。しかも失敗してもプログラムは止まらず、メッセージも出ない。ストリームがエラー状態に置かれるだけである。エラー状態のストリームを条件として使うと false になる。したがって確認は if (!in) であり、最初の読み取りの前に行わなければならない。次のプログラムは存在しないファイルを開こうとする。

C++ のプログラム
#include <fstream>#include <iostream>#include <print>​int main(){    std::ifstream in{"grades.txt"};    if (!in) {        std::println(std::cerr, "grades.txt を開けませんでした");        return 1;    }    int first{};    in >> first;    std::println("最初の点数:{}", first);}
出力
grades.txt を開けませんでした

プログラムはコード 1 で終了しました。

三つの細部に注意したい。一つ目は std::cerr(<iostream> にある)、すなわちエラーストリームである。標準出力と同じく画面に書くが、別の経路なので、出力をファイルにリダイレクトしたときにエラーメッセージが結果と混ざらない。std::println は、書き込み先のストリームを第一引数として受け取る。二つ目は return 1; である。ゼロ以外の終了コードは、レッスン プログラムの解剖 で見たように、プログラムを実行した人に何かがうまくいかなかったことを知らせる。三つ目は、この確認がなかったら何が起きたかである。エラー状態のストリームに対する in >> first は何もせず、プログラムは平然と「最初の点数:0」と書いただろう。本物らしく見える偽の結果は、エラーメッセージより悪い。ここからこのレッスンの規則が出てくる。どのファイルも開いたときに確かめる。

相対パスと作業フォルダ

名前 "grades.txt" は、ファイルがどのフォルダにあるかを言っていない。このような名前は相対パスであり、作業フォルダを起点として解決される。作業フォルダとはプログラムを実行するときのフォルダのことで、実行ファイルのフォルダとも、ソースコードのフォルダとも限らない。次のプログラムは、フォルダ project にファイル data.txt と並べて保存されており、そのファイルを開いて最初の行を書く。これを二回実行した。最初は project から、次はその一つ上のフォルダからである。

ターミナル
$ cd project$ g++ -std=c++23 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -g -fsanitize=address,undefined -fno-sanitize-recover=undefined main.cpp -o main$ ./main最初の行:ファイルからこんにちは$ cd ..$ ./project/maindata.txt を開けませんでした

実行ファイルは同じで、ファイルも元の場所にある。変わったのは作業フォルダだけで、それとともに data.txt を探す場所が変わった。これが「ファイルが見つからない」のもっともよくある原因である。ボタンでプログラムを実行するエディタは、作業フォルダを独自に選ぶ(Visual Studio Code では通常エディタで開いているフォルダだが、タスクの設定による)。そのため、エディタが何をしているかがわかるまでは、ターミナルからデータのあるフォルダで実行するのがよい。逆の誘惑、つまりコードに "/home/ana/curso/data.txt" や "C:\\Users\\Ana\\data.txt" のような絶対パスを書くことは、一台のマシンで問題を解決し、ほかのすべてのマシンで問題を生む。別の人が実行したり、フォルダを移動したりした途端に、プログラムは動かなくなる。勧められるのは、作業フォルダがわかっている状態での相対パス、さらによいのは、ファイル名を利用者が与えるデータとすることである。