見出し画像

続C言語教室 - 第3回 ファイル入力 その2

前回、キーボードやファイルから1行を読み込む scanf と fscanf を取り上げました。

続C言語教室 - 第2回 ファイル入力

そこでも書いたのですが、実はあまり scanf は使いません。scanf で行を入力するには予め書式がわかっている必要があり、その書式が正確に一致していないとうまく入力することが出来ないからです。エラーとして処理するためにも、どんな入力が行われたのかが分からなければ適切な判断も難しいですし、結果をメッセージとして出力することも出来ません。

そして最も致命的なことは、読み込む内容によってはプログラムで使っているメモリが想定しない形で書き換えられてしまう恐れがあることです。

#include <stdio.h>
void main() {
  char buf[128]; // 128は適当
  scanf("%s", buf);
  printf("%s\n", buf); // 入力内容を確認するため
}

このように入力された文字列を「適当に」用意した文字型配列変数に入れてもらうわけですが、もし入力された文字列が「適当」よりも長い場合でも問答無用で入れられてしまうのです。その結果、配列として確保した変数を超えたメモリが使われてしまうので、他の変数の値が変わってしまったり、プログラムがそのまま動き続けることができない事態が起こってしまうかもしれません。

C言語は言語の機能として、個別のメモリに関して面倒を見てくれることはありません。関数側で渡されたポインタの先がどうなっているのか知る方法もありませんし、何らかのエラーになることもありません。ですから関数を呼び出す側は、それを承知の上で必要なメモリを確保してから関数を呼び出さないといけません。

そんな事を言っても、何かの根拠がない限り、キーボードやファイルから入力される文字列がどんな長さまでなのかがわかるのは難しいですよね。そしてその根拠は環境によって変わったりします。そこでこのような関数を「呼び出してはいけない」というのが最近の掟です。


さて、それではどうするかナンですが、普通は fgets を使って1行を文字列として読み込んでしまいます。

#include <stdio.h>

void main() {
  char buffer[100]; // 100は適当
  FILE *file = fopen("example.txt", "r");
  while (fgets(buffer, sizeof(buffer), file)) {
    printf("%s", buffer); // 読み込んだ内容を確認する
  }
  fclose(file);
}

fgets は引き数に入力された文字列を受け取る文字列配列(文字型変数へのポインタ)、読み込む最大の長さ、そしてFILE構造体へのポインタを渡します。ここで重要なのは「最大の長さ」を渡すことで、これにより渡した文字型配列の領域を超えてしまうことを防げます。もし、この長さよりも1行が長くても、そこまでしか入力されないのです。また fgets はファイルを読み終わって、もう読むことが出来ない場合にFALSEを返すので(そうでなければTRUE)、while などを使って処理を繰り返すことができます。

このようにして長い行に対しても分割して入力することで安全に読み込むことが出来るのですが、行が分割されたときやファイルの最後に改行がない場合の処理が大丈夫かなどを、自分で処理を書く必要はあります。

さて、これで安全に1行を手に入れることが出来たら、この文字列に含まれる文字をひとつずつ確認して必要な処理を続けます。空白などの区切り文字や文字種などを確かめた後に、atoi などで整数に直したり、sscanf という文字列に対して使える scanf と同じような関数が用意されているので、適切な型の変数に代入していけば良いのです。もし適切ではない文字が含まれていれば、そこで処理を中断するなりして、どの部分に問題があるのかを標準エラー出力などに出しておけば、プログラムを使う人にとっても親切です。

ところで、ファイルではなくキーボードからの入力に対して使う gets という関数もあるのですが、残念なことに関数の引き数は文字型変数へのポインタのみで「最大の長さ」がありません。ですから想定以上に長い文字列が入力されてしまうことが考えられるので、これは「使ってはいけない」関数です。ですからキーボードから1行を入力したい場合には、他の関数を使う必要があり、最近の処理系であれば getline であるとか、gets_s なんかを使えというのですが、どれが使えるのかは処理系によって微妙に異なります。無難なのは fgets のFILE構造体に標準入力(stdin)を渡す方法かもしれません。

「そんなに長い1行なんてあるの?」という人もいるかもしれませんが、実は「改行コードの罠」という問題があります。特にファイルとして渡された場合、プログラムを実行しているOSと異なる改行コードを使っているファイルだと1行を入力する関数は改行コードを認識せずにファイル全体を1行として読み取ってしまうことがあり得るのです。もちろん誤ってテキストではなくてバイナリが入っているファイルを読ませても似たようなリスクはあるわけです。

fgets を使って処理を書くと行が分割されたり、最後の1行の処理を書くのが割と面倒ですし、そもそも「正しい改行コード」を前提とする必要があります。だったらいっそのこと1文字ずつ読み取って自分で処理を書いたほうが安心という考え方もありますよね。そこで今度は1文字ずつ読む方法もしらべてみましょうか。


もう少しいろいろなサンプルコードを載せないとちょっと理解しにくいところもあるかもしれませんね。ただファイル処理はもう少しアレコレがあるので、一通り済ませてから復習といきますか。

ヘッダ画像は、Copilotに描いてもらいました。

#C言語 #プログラミング講座 #入力 #scanf #fscanf #sscanf #gets #fgets #行 #改行コード #バッファオーバーラン

いいなと思ったら応援しよう!

kzn
頂いたチップは記事を書くための資料を揃えるために使わせていただきます!