ストリームの話と、入出力装置、テキストファイル・バイナリファイル操作について 勉強しなおしの記録というか学習ノート-9日目-

Unreal Engin4すごい!(語彙力)ってなったので、少しいじりつつ、むかーしかじった事あるC,C++の学びなおしの学習ノート。この連載記事の詳しい趣旨と注意事項は1日目をご覧ください。

目次

printf()以外でコマンドプロンプト画面に文字を表示させる関数

今までprintf()関数で実行結果を表示させてきましたが、実は他にも文字を表示させる関数があります。

puts()関数

書式:puts(文字列);

標準出力に引数が指す文字列を書き込みます。この引数には文字列を格納した変数でも、文字列そのままでも取得できます。

ただし、取得できる引数は1つだけで固定されています。書式指定して、「文字と数値を組み合わせて表示」みたいなことはできません。

また、出力の際、末尾に自動的に改行文字(“\n”)が追加されます。

char* s = "あいうえお";
puts(s);

そのまま文字列を入れてしまってもOK。

puts("あいうえお");

実行結果↓

あいうえお

可変長引数

puts()関数はprintf()関数とは違い、引数の数が1つで固定されています。

printf()のような関数の、いくつ取るのかプログラマの任意で決まられる引数のことを、可変長引数と言います。

また、可変長引数を取るprintf()のような関数ことを、可変長引数関数と言います。

おまけ:標準Cライブラリから廃止されたgets()関数

じつは、puts()関数と対をなすように、標準入力から1行分の文字列を取り出す関数があったのですが、致命的な欠陥が見つかり、2011年に改訂されたC11以降の標準Cライブラリから廃止されました

詳しく知りたい方は↓のページが大変参考になるかと思います。

外部ページ:
gets関数|危険性と大体関数【gets_s/fgets/scanfによる代替処理】| MaryCore

printf(), scanf()を改めて見つめてみる — 標準出力装置と標準入力装置

今まで普通に『コマンドプロンプト』画面に、printf()関数を使って実行結果を表示(出力)させてきました。

ここで、少し立ち返って考えてみてください。

何故、printf()関数で指定した文字列が、ちゃんと『コマンドプロンプト』画面に出力されるのか、…いや、そもそも、なぜ『コマンドプロンプト』から、『コンパイル』の操作が行えて、実行ファイルを生成なんてできているのか…!?

実は、コンピューターのOSの方で標準出力装置標準入力装置として、何も指定しなくても予め入出力先が設定されているんです。

Windowsの場合は、コマンドプロンプトを標準出力装置、キーボードを標準入力装置として設定されています。

関数名説明入出力先
printf標準出力へと文字を出力するstdout(C言語でコマンドプロンプトを指す)
scanf標準入力からの入力を受け付けるstdin(C言語でキーボードを指す)

ちなみに先ほど出したputs()関数、gets()関数についても並べてみます。

関数名説明 入出力先
puts標準出力へと文字を1行で出力するstdout(C言語でコマンドプロンプトを指す)
gets標準入力からの1行での入力を受け付ける(C11からは廃止)stdin(C言語でキーボードを指す)

ストリーム –データの入出力の流れ

プログラムから見た入力~出力の流れや入出力先のことを、”ストリーム“と言います。

標準入出力(ストリーム)のお話を前の項でさせていただきましたが、鋭い方はもうすでになんとなくお察しなのではないでしょうか?

Q:「"予め決められているから、コマンドプロンプト画面に出力される"ってことは、
それ以外の入出力先も選択出来て、実は自由自在に入出力先を決められたりする?」
A:「します。」

“リダイレクト”と言って、OSに指示を出し任意で標準ストリームを変更することが可能です。

※個人のイメージが入ります。

ストリームを物流で例えると、トラックが、どの場所から荷物を受け取り(入力装置)、どの場所へと荷物を届けるのか(出力装置)、それがデフォルトで決まっていて、それに従って、荷物を運ぶトラックが行きかう。みたいな感じ。

今までやってきたのは、

 キーボードから入力
        ↓
 メモリに書き込み
     ↓
 その結果を読み込み
     ↓
 画面に出力

っていう流れでした。通常は、入力装置から出力装置へのストリームは、特に操作せずに「コードを書いて、コンパイルして、画面に表示」が行えてしまうので、意識することはないかと思われます。

ですが入出力の設定を操作して、ファイルや他のデバイスなどへの入出力ができるので、次の項でファイルへの入出力をご紹介してみようと思います。

ファイル操作

インターフェースが、GUICUIで違いますが、いつもの画面でテキストファイルを『作成』したり、文字を『書き込み』したり、いった動作と同じことを、コマンドプロンプト上でコードを入力するだけです。そこまで難しく考える必要はありませんよ。

C言語では、ファイルを操作するための関数が用意されています。

まずは、ファイルを入出力で使うために、ストリームにファイル情報を取り込まなくてはいけないわけですが、これにはFILE構造体を使用します。

FILE構造体 –ファイルの情報を格納しておくポインタ変数を生成する

FILE構造体とはデフォルトで定義されている構造体の型で、ファイル操作をする際にファイルの情報をポインタとして格納しておく変数を生成するためのものです。構造体ですが、”struct”を付けないで使えます。

書式:FILE *FILE型のポインタ変数名;

fopen() –ファイルを開く

テキストファイルに文字を書き込もうにも、閉じようにも、まずは何をするにも、開かなくては始まらないわけです。

書式:ファイルポインタ = fopen(操作したいファイル名, モード);

「操作したいファイル名」には、”ファイル名だけ”の場合は実行ファイルと同じフォルダのファイルを、そのフォルダへのパスも含めて指定します。そして、重要なのは第2引数にある、モードです。

モードでは、ファイルを開くときに「読み込み」、「書き込み」などの指定を行います。

モードを表すコード説明
r読み込みモード
ファイルがない場合NULLを返す。
w書き込みモード
ファイルがない場合は新しく作成し、存在している場合は、それまでの内容を破棄する。
a追加書き込みモード
ファイルがない場合は新しく作成する。
r+読み込み・書き込みモード
ファイルがない場合、NULLを返す。
w+読み込み・書き込みモード
ファイルがない場合は新しく作成し、存在している場合、それまでの内容を破棄する。
a+読み込み・追加書き込みモード。
ファイルがない場合は新しく作成し、存在している場合は追加する。

fopen()関数は、実行すると返り値にファイル情報を持つポインタが返されます。このポインタは、ファイルポインタと呼びます。

テキストファイル以外にも、バイナリファイルの選択もできます。

モードを表すコード説明
rbバイナリデータの読み込みモード
ファイルがない場合NULLを返す。
wbバイナリデータへの書き込みモード
ファイルがない場合は新しく作成し、存在している場合は、それまでの内容を破棄する。
abバイナリデータへの追加書き込みモード。
ファイルがない場合は新しく作成する。
rb+ または r+bバイナリデータへの読み込み・書き込みモード
ファイルがない場合NULLを返す。
wb+ または w+bバイナリデータへの読み込み・書き込みモード
ファイルがない場合は新しく作成し、ファイルが存在している場合、それまでの内容を破棄する。
ab+ または a+bバイナリデータへの読み込み・追加書き込みモード
ファイルがない場合は新しく作成し、存在している場合は追加する。

バイナリファイルとは、「1010」って感じの2進数のコードで書かれたファイルです。※人間には(多分)読めません。

テキストファイル以外のファイルがこれに分類されます。例えば 、実行可能ファイル(.exe)、画像ファイル(.png)、音楽ファイル(.mp3)などです。

この話について、過去記事で触れていますので、よろしければ併せてごらんください。

fclose() –ファイルを閉じる

書式:fclose(ファイルポインタ);

fopen()関数で開いたファイルを閉じるための関数です。引数に指定したファイルポインタが持つ情報が指すファイルを閉じます。

どうやら、fopen()関数では、ストリームに組み込んだファイルが別のところで書き換えが起こってしまわないように、鍵をかけるようです。そして、このfclose()関数で再び書き込みができるようにする、という重要な役割があるようです。

fcloseの役割

一見、無意味なfopen関数ですが、ちゃんと役割があります。 WindowsやUnix(Linux)やMacなど、同時に複数のソフトが動く環境では、 もし、同じファイルを同時に2つのソフトで書き換えてしまうと、 どちらを反映すれば良いのかわからなくなってしまうので、 fopen関数で書き込みが出来るように開いたファイルには、 他のソフトで書き換えられないよう鍵をかけています。 fclose関数は鍵をはずして他のソフトから使えるようにします。 その他、ファイルを開いているときはメモリ内に保存し、 fclose関数が実行されたときに初めてディスクに書き込むことで、 高速化していることもあります。

引用元: テキストファイルの読み書き – 苦しんで覚えるC言語

テキストファイルの場合

fprintf() –ファイルへの書き込み

書式:fprintf(FILE型のポインタ変数, 文字列);

第1引数で指定したFILE型のポインタ変数に、第2引数で指定した文字列かchar型配列を対象の出力先へと書き込みます。ファイルへの書き込みで一番高性能なものがこのfprintf()関数だと思います。

#include <stdio.h>

void main() {
	FILE* f;
	f = fopen("test.txt", "w");
	fprintf(f, "こんにちは、世界!");
	fclose(f);
}

↑のCファイルをコンパイルして実行すると、そのCファイルと同じフォルダに”test.txt”というファイルが出現します。開くと↓のような内容が確認できます。

fscanf() –ファイルからの読み込み

書式:fscanf(FILE型のポインタ変数, 書式指定子群, 格納先の変数1,  格納先の変数2...);

第1引数で取得したファイルポインタから取得したファイル情報を出力し、第2引数で指定した書式指定子群の通りに、第3引数以降の変数へ格納していきます。

※注意!このfscanf()関数は、gets()関数と同じように、バッファオーバーフローを起こしやすい関数の1つです。

#include <stdio.h>

void main() {
	char s[1024]; // メモリ領域を十分に確保しておく
	FILE* f;
	if ((f = fopen("test.txt", "r")) == NULL) {
		printf("ファイルがありません!\n");
		exit(1);  //異常終了のサインを出して終了
	}
	while ((fscanf(f, "%s", s)) != EOF) {
		printf("%s", s);
	}
	fclose(f);
}

コラム:バッファオーバーフロー –メモリ領域(バッファ)が足りなくてデータがこぼれるバグ

バッファオーバーフローとは、何かから何かへデータを移す際に一時的にデータを保持するメモリ領域(バッファ)を、十分用意していなかったことが原因で起こる桁溢れ(オーバーフロー)です。

プログラムで何らかのデータを処理する場合、処理すべきデータや処理した後のデータを保持するためのメモリ領域が必要になります。これが「バッファ」です。
 ユーザーの入力したテキストを一文字ずつ処理したり、圧縮された画像データを伸長したり、といった処理を行うコードでは、処理対象となるデータを保持するためのメモリ領域を、関数のローカル変数として用意したり、malloc()関数を使ってヒープメモリから割り当てたりします。
 重要なことは、Cのコード上、これらのメモリ領域は固定長でしか記述できないということです。処理の途中で倍のサイズが必要になったからといって、勝手にサイズを倍にしてくれるような便利な仕組みはありません。必要であれば、倍のサイズのメモリ領域を確保し、データを移し替えて処理を続けるような仕組みを自分で実装する、それがC言語です。
 この固定長であるメモリ領域にデータをコピーする際、用意された領域の外まで書き込みを行ってしまうことを「バッファオーバーフロー」といいます。

引用元:Cでポピュラーな脆弱性とオーバーフロー(前編) (1/2)|もいちど知りたい、セキュアコーディングの基本(2) – @IT

バイナリファイルの場合

バイナリファイルを(ある程度)人間が読めるようにするためのもので、バイナリエディタというものがあります。バイナリエディタでは大抵16進数に変換されて表示されます。

「ここら辺のことを解説するのにバイナリエディタ必要かな」と思ってたら、以外とVisual Studioに突っ込んだら読み込んでくれたので、これで行きます。

fwrite() –バイナリファイルへの書き込み

書式:fwrite(データ, 1つのデータのサイズ, データの数, ファイルポインタ);

第1引数にはバイナリファイルに書き込みたいデータ(今回は16進数の配列を指定しました)、
第2引数には書き込みたいデータ一つのサイズ(今回の場合はchar型の配列なのでsizeof(char)と指定しました)、
第3引数のデータの数にはデータの項目数(今回は4つ要素を持つ配列なので4)で、
最後の第4引数には、ファイルポインタを指定します。

#include <stdio.h>
#include <stdlib.h>

void main() {
	char hex[] = { 0xaa, 0xbb, 0xcc,0xdd };
	FILE* f;
	if ((f = fopen("test.bin", "wb")) == NULL) {
		printf("ファイルを開けませんでした\n");
		exit(1);
	}
	fwrite(hex, sizeof(char), sizeof(4), f);
	fclose(f);
}

実行結果(Visual Studio 2019 Commnityで表示させたもの)↓

fread()関数 –バイナリファイルの読み込み

書式:fread(データ, 1つのデータのサイズ, データの数, ファイルポインタ);

バイナリデータ以外でもそうですが、中身のわからないファイルを読み込む場合、ファイルのデータの大きさによって、確保するメモリ領域を変えてデータを読み込まなければ、先ほどから頻出するバッファオーバーフローになってしまうわけなのです。

「メモリ領域を変えて」というのは、以前の記事でも見たmalloc系の関数を使えば良さそうですが、肝心のファイルのデータの大きさはどうやって調べればいいのかというと、 fseek()、ftell()関数というものがあります。

そのファイルの中身を先行して調べるための関数も使った例※このコードは一週間で身につくC言語の基本|第6日目:ファイルの読み書きに掲載されているコードを大いに参考に(割とパクら)させていただきました↓

#include <stdio.h>
#include <stdlib.h>

void main() {
        int i, size;
	char* hex_data;
	FILE* f;
	if ((f = fopen("test.bin", "rb")) == NULL) {
		printf("ファイルを開けませんでした\n");
		exit(1);
	}
	fseek(f, 0, SEEK_END);
	size = ftell(f);
	hex_data = (char*)malloc(sizeof(char) * size);
	fseek(f, 0, SEEK_SET);
	fread(hex_data, sizeof(char), size, f);
	fclose(f);
	for (i = 0; i < size; i++) {
		printf("%x", hex_data[i]);
	}
	free(hex_data);
}

実行結果↓

ffffffaaffffffbbffffffccffffffdd

char型だと、他のバイトに格納されているものが全部1で埋まるんですかね?

fseek()関数 –ファイルを開始位置から移動バイト数移動する

書式:fseek(ファイルポインタ, 移動バイト数, 開始位置);

第1引数にファイルポインタを、第2引数に移動したい位置の第3引数の開始位置から数えたバイト数分の位置を、第3引数に、基準となる位置を表すキーワードを入れます。キーワード一覧は↓のとおりです。

基準位置説明
SEEK_SETファイルの始め
SEEK_CURその時点のファイル位置表示子の値
SEEK_ENDファイルの終わり

ファイルの位置表示子とは、ストリームを制御するための情報の1つで、その時点でアクセスしているストリームの位置情報を保持しています。

ftell()関数 –現在の位置の値をバイト数で返す

書式:ftell(ファイルポインタ);

fseek()で移動した現在位置をバイト数で返します。もし、fseek()の第3引数で”SEEK_END”を指定し、第2引数が”0″であれば、最後から0バイト目なわけなので、そのファイルのサイズと等しくなります。

コマンドライン引数

突然ですが!main()関数には、実は引数を入れることが出来ます。

「ん?main()に引数を入れるように記述しても、main()ってプログラムの最初に呼び出されるよね?どこから引数を入れるの?」

そう思われるのも仕方ないでしょうが、いるじゃないですか、main()よりも前に動いてるプログラムが…

windowsだとコマンドプロンプトがそれにあたります。

C言語には、アプリケーション(.exeファイルとか)を起動する時にファイル名を渡す機能があります。それをコマンドライン引数というのです。

ちなみに、この“コマンドライン”というのは、ご存じコマンドプロンプト上の入力行の事です。

int argc, char *argv[] –暗黙の了解的な記述のルール

コマンドライン引数を受け取る書式:
    void main(int argc, char *argv[]){
       /*
        略
       */
    }

他の引数名で記述しても受け取ることが出来るのですが、昔からこの引数名で記述することが好まれます。

第1引数のargcにはコマンドラインから渡された引数の数、第2引数のargvにはコマンドライン引数で渡された文字列が格納されます。

↓のページを参考に、Sleep()関数を使って一時停止してみてます。

外部ページ:
sleep|処理を一時停止する 複数の方法【C言語/C++/Swift/Objective-C】|MaryCore

#include <stdio.h>
#include <windows.h>

void main(int argc, char* argv[]) {
	int count = 0; 
	char any[256];
	printf("第1引数: %s\n", argv[count]);
	count++;
	puts("何か入力してください");
	scanf("%s", any);
	while (count < argc) {
		printf("第%d引数: %s\n", count+1, argv[count]);
		count++;
	}
	Sleep(10000);//すぐに閉じないように10秒ほど停止させてます。
}

これをコンパイルして生成されたexeファイル(sample.exe)を、コマンドプロンプト上ではなく、直接ダブルクリックで起動して、以前作った適当なexeファイル(calc.exe)ファイルをドラッグして入れてみました。↓

おまけ:sprintf()関数とsscanf()関数

こういったタイプの入出力もあるんですよ、っていうおまけ的にご紹介しておきます。

sprintf()関数 –char型配列への出力

書式:sprintf(char型配列, 書式指定子付き文字列, 変数, 変数...);

第1引数のchar型配列へ、第2の書式付き文字列を第3引数以降の値を組み込んで出力します。

printf()関数とは、可変長引数関数という点が同じですね。ただ、こちらのsprintf()関数は、出力先が画面ではなく、char型の配列です。

#include <stdio.h>

void main() {
	char s[256];
	sprintf(s, "こっそりとsprintf()を実行しました。");
	printf("printf()実行時初めて画面に出力されます。先ほど、%s",s);
}

実行結果↓

printf()実行時初めて画面に出力されます。先ほど、こっそりとsprintf()を実行しました。

sscanf()関数 –char型配列の文字列からの入力

書式:sscanf(char型配列, 書式指定子群, 変数, 変数...);

第1引数のchar型配列の文字列を取得し、第2引数に指定した書式指定子群の仕様で分解した第1引数を、第3引数以降の変数に格納します。

こちらは文字列からの書式つきの出力というと、よくわからないと思いますので、例を見て掴んでください。

char* s = "ウィル マリオ シーザー";
char names[3][10] = { {""}, {""}, {""} };

sscanf(s, "%s %s %s", names[0], names[1], names[2]);
printf("%s %s %s", names[2], names[1], names[0]);

実行結果↓

シーザー マリオ ウィル

あとがき

ご意見・ご感想・ご質問、また、ここ間違ってるよとか、もっといい方法あるよといったご指摘などございましたら、お手数ですがコメント欄やtwitterアカウントほろほろり(@_horo_horori)へお願いしますm(_ _)m

参考にさせていただいたページ・サイト一覧

Pocket

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください