文字操作・入出力ライブラリの使い方を理解しよう:目指せ! Cプログラマ(13)(2/2 ページ)
文字を操作する関数、入出力に関する関数など、Cでよく使われるライブラリについて理解を深めよう。
入出力ライブラリの使い方(stdio.h)
入出力(インプット/アウトプット)の機能はstdio.hをインクルードすると使えるようになります。標準ライブラリで提供されているのはファイル入出力と標準入出力です。ただし標準入出力はファイルにマッピングされたものとして扱われますので、ファイルを操作するように扱うことができます。
ファイルへの入出力は、オープン、読み書き、クローズという手順になります。まずオープンとクローズを行う関数は次のようになっています。
FILE * fopen ( const char * restrict filename, const char * restrict mode ); int fclose ( FILE * stream );
fopen関数にはファイル名とオープンモードを渡します。オープンモードには次のものがあります。
- r : 読み取りモード
- w : 書き込みモード(ファイルがすでに存在した場合は長さを0に切り詰める)
- a : 追加モード(ファイルがすでに存在した場合は終了位置に追加する)
どのモードにも + と b を後ろに付けられます。+ を指定すると更新モードになり、先頭の文字が r、w、a のいずれの場合でも読み書き可能になります。b を指定するとバイナリモードになり、指定しないとテキストモードになります。バイナリモードとテキストモードのもっとも大きな違いは、行を意識して操作するかどうかです。
例えば次のような組み合わせで使います。
- r+ : 更新モード(読み書き可能)
- wb : バイナリ書き込みモード(ファイルがすでに存在した場合は長さを0に切り詰める)
- a+b : バイナリ更新モード("ab+" でも同じ)
fopen関数の戻り値とfclose関数の引数で使われているFILE型は、1つのファイルを表したものです。fopenで得たFILEオブジェクトを使って読み書きを行い、終わったらfcloseにそのオブジェクトを渡して処理を終了します。
読み書きには「直接入出力」「文字入出力」「書式付き入出力」の3種類があります。初めの直接入出力がもっとも低いレベルの操作で、柔軟性はありますが抽象度が低いため使いづらく、後ろに行くほど抽象度が高くて使いやすくなっています。
直接入出力はシンプルに読み出し(fread)と書き込み(fwrite)の2つの関数が提供されています。
size_t fread ( void * restrict ptr, size_t size, size_t mem, FILE * restrict stream ); size_t fwrite ( const void * restrict ptr, size_t size, size_t mem, FILE * restrict stream );
プログラムでは標準入力、標準出力、標準エラー出力という特別なFILEポインター型のオブジェクトが用意されています。これらはプログラムからはファイルと同じように扱えますが、オープン(fopen)しなくても入出力ができます。これらの入出力がどこに割り当てられるかは実行時に決まります。
これを利用して、標準入力から得た文字を画面へ表示するプログラムを作ってみましょう。
#include <stdlib.h> #include <stdio.h> int main(void) { char buf[10]; // 標準入力stdinからの入力 size_t size; size = fread(buf, sizeof(buf[0]), sizeof(buf) / sizeof(buf[0]), stdin); // 標準出力stdoutへの出力 fwrite(buf, sizeof(buf[0]), size, stdout); return EXIT_SUCCESS; }
標準入力からプログラムへ値を渡すにはキーボードを使う方法もありますが、次のようにするのが簡単です。
- コマンドプロンプト(Windows環境の場合。Linuxなどでは端末エミュレーター)を起動する。
- echo 標準入力へ渡す文字列 | 実行ファイル を実行する。
例えば、Windowsでこのプログラムをコンパイルしてtest.exeを用意したとすると、次のようにします。
C:> echo abc|test.exe abc C:>
freadとfwriteの2つ目の引数(size)は、渡した配列の要素1つ分の大きさです。sizeof(char) でも構いませんが、bufオブジェクトの型を変更しても問題ないように、sizeof(buf[0]) としています。3つ目の引数は要素の数です。
戻り値は、読み込み、あるいは書き出しに成功した要素の数になります。つまり "abc" を渡した場合はfreadの結果として4(echoコマンドの仕様により改行 '\n' が後ろに追加される)が返り、入力された分だけ文字をfwriteでコンソールに出力しています。
次に文字入出力は、文字単位で入出力が行えます。いくつかバリエーションがありますが、主に使われるのは次の4つの関数です。
// 文字読み込み int fgetc ( FILE *stream ); // 文字列読み込み char * fgets ( char * restrict s, int n, FILE * restrict stream ); // 文字書き出し int fputc ( int c, FILE * stream ); // 文字列書き出し int fputs ( const char * restrict s, FILE * stream );
標準入力から読み込んだ文字列を標準出力へ書き出すプログラムは次のようになります。
#include <stdlib.h> #include <stdio.h> int main(void) { char buf[10]; char *result; // 標準入力からの入力 result = fgets(buf, sizeof(buf) / sizeof(buf[0]), stdin); // 標準出力への出力 if (result != NULL) { fputs(buf, stdout); } return EXIT_SUCCESS; }
fgets関数は、2つ目の引数(n)より1だけ小さいサイズの文字を読み込み、最後に文字列の終了を表す '\0' を追加します。従ってこのプログラムでは9文字を読み込めます。それより長い部分は捨てられます。また、fgets関数は読み込みエラーの時に NULL が返りますので、そのチェックが必要です。
C:> echo abc|test.exe abc C:> echo abcefghijklmn|test.exe abcdefghi C:>
fputs関数では、fwrite関数のときのようにサイズの指定が必要ない点に注目してください。直接入出力ではデータを単なるバイト列として扱いましたが、文字入出力では文字列('\0' で終わる)として扱います。
最後に書式付き入出力を説明します。
int fscanf ( FILE * restrict stream, const char * restrict format, ... ); int fprintf ( FILE * restrict stream, const char * restrict format, ... );
fscanf関数が読み込み、fprintf関数が書き出しになります。いずれも2つ目の引数formatに書式を指定し、書式に従った入出力が行えます。fscanf関数の書式とfprintf関数の書式は同じではありませんが、書式の文字列は % で始まる文字列(変換指定子)を含み、これにより書式を指定するということは同じです。
- d : 10進整数
- x : 16進整数
- f : 浮動小数点数
- c : 文字あるいは文字の並び(fscanf)、文字(fprintf)
- s : 非空白文字の並び(fscanf)、文字配列あるいは文字列(fprintf)
さらに % と変換指定子の間に、さらに細かい変換の指定できます。これらの指定は省略可能です。
fscanf関数の場合:
- 代入抑止(入力を読み捨てるかどうか)
- 最大フィールド幅(入力値の幅の制限)
- 長さ修飾子
fprintf関数の場合:
- フラグ(変換指定の意味を修飾する)
- 最小フィールド幅(結果が短い場合に空白を詰める)
- 精度
- 長さ修飾子
標準入力から入力された値を標準出力へ出力するプログラムは次のようになります。"%10s %f%2s" では、順に、長さ10の文字列、浮動小数点数、長さ2の文字列として、続く引数へ各値を読み込むことを意味しています。
"type=[%s]: %.2f%s\n" では、順に、文字列、小数点数以下2桁の浮動小数点数、文字列として、続く引数の各値を表示することを意味しています。
#include <stdio.h> #include <stdlib.h> int main(void) { char type[10 + 1]; float value; char unit[2 + 1]; int count; // 入力 count = fscanf(stdin, "%10s %f%2s", type, &value, unit); // 出力 if (count != EOF) { fprintf(stdout, "type=[%s]: %.2f%s\n", type, value, unit); } return EXIT_SUCCESS; }
実行例は次のようになります。
C:> echo distance 10.3km|test.exe type=[distance]: 10.30km C:> echo price 1000USD|test.exe type=[price]: 1000.00US C:>
入力された文字列がfscanf関数によって書式通りに分解され、それぞれの変数へ代入されます。fscanf関数が失敗すると戻り値はEOFになりますので、そのチェックが必要です。成功した場合は入力されたパラメーターの数が入ります。
ところで、実行例の2つ目の結果の最後が、USDではなくUSになっています。これは、fscanf関数の書式指定で最大フィールド幅を2(%2s の部分)と指定したからです。文字列の場合は受け取るオブジェクトのサイズが決まっていますから、%s や %c を指定する際には最大フィールド幅を忘れずに設定してください。
ここまでで3種類の入出力について説明しました。これらにはいくつかバリエーションがあります。状況によってはそれらを使用してもよいでしょう。
fgetc
- getc : fgetcのマクロ版(fgetcは関数)
- getchar : getcの入力を標準入力に限定
fputc
- putc : fputcのマクロ版(fputcは関数)
- putchar : putcの出力を標準出力に限定
fgets
- gets : 使ってはいけない(後述)
fputs
- puts : fputsの出力を標準出力に限定
fscanf
- scanf : fscanfの入力を標準入力に限定
- sscanf : fscanfの入力が文字列
fprintf
- printf : fprintfの出力を標準出力に限定
- sprintf : 使ってはいけない(後述)
- snprintf : 結果を標準出力ではなく配列に書き込む
さらに、fscanf関数およびそのバリエーション、fprintf関数及びそのバリエーションには、その可変個の引数をva_listに変更し、関数名の頭にvがついたバージョンが存在します。va_listについては今後説明します。
文字列操作とセキュリティ
文字列操作の説明の中で、Cにおいて文字列操作が簡単ではない理由として、可変長の文字列を簡単に扱う方法がないことを挙げました。もちろん長さが変わる文字列を扱えないわけではありませんが、言語としてサポートされていないことはプログラマが注意しなければならず、慣れていてもちょっとしたことで大きな問題を発生させてしまいます。
そういった問題は、コンピュータが登場した初期の頃には単なるバグとして片付けられていましたが、インターネットが普及した現代ではセキュリティ問題と結び付けられることが多くなっています。例えば、strcpy関数はコピー先のバッファの容量を確認せずに上書きしてしまうため、使い方によっては意図しないところまで書き換えてしまいます。
char abc[] = "abc"; char str[3]; strcpy(str, abc);
ご存知の通り、この例における配列abcの長さは4となります。'a', 'b', 'c' の3文字に加えて、文字列の終わりを示す '\0' があるからです。これを3文字分の配列strにコピーしてしまうと、領域外へデータを書き込んでしまうことになります。これはバッファオーバーフロー(あるいはバッファオーバーラン)というよく知られた問題の一種です。このコード片では文字列abcがリテラルで指定されているのでコンパイル時点で長さが決まっていますが、外部から入力された文字列を扱うのであれば、実行時にきちんと数えなければなりません。
このような注意すべき点はすでにいくつか知られていますので、ここで紹介します。
(1) gets関数やsprintf関数は使わない
gets関数やsprintf関数はバッファオーバーフローを避けるのが難しい関数です。これらの関数は受け取った入力や処理の結果を配列に格納しますが、配列のサイズを超えていても書き込もうとしてしまいます。当然、配列を超えないことが分かっていて呼び出す分には問題ありませんが、実際のところそれは難しいため、セキュリティを気にする必要があるプログラムでは、使わないほうが無難です。
代わりにfgets関数やsnprintf関数を使いましょう。これらの関数は結果を受け取る配列の大きさを指定でき、それを超えるものは捨てられます。あるいは、これらの関数の問題点を解消する関数を用意している環境もあります。とはいえ、それらの代わりとなる関数を使えば何をやっても安心というわけではありません。常に気を配ってプログラミングする必要があります。
gets関数やsprintf関数以外にも、バッファオーバーフローが発生しやすい関数があります。結果のサイズが分かっている場合はかまいませんが、そうでない場合は代わりの関数を使ってください。
- gets : fgets、gets_s を使う
- sprintf : snprintf、sprintf_s を使う
- strcat : strncat、strcat_s を使う
- strcpy : strncpy、strcpy_s を使う
Visual C++ 2005以降、あるいは最新のC11規格(ISO/IEC 9899:2011)をサポートする環境であれば、_s で終わる関数を使用できますので、これも検討してみてください。
(2) fscanf関数の %c、%s は必ず最大フィールド幅を指定する
文字列のところでも説明しましたが、fscanfで文字配列や文字列を受け取る %s や %c を指定する際には、最大フィールド幅を必ず指定してください。そうしないとバッファオーバーフローが簡単に発生します。
環境によっては、これが守られなかった場合にエラーとして報告してくれる関数を別に用意しているものもあります(WindowsやC11における fscanf_s など)。そのような関数を使うのも良いでしょう。
(3) fprintf関数やfscanf関数の書式指定に外部からの入力を使わない
fprintf関数やfscanf関数では、書式指定文字列とそれに対応した後続の引数のセットで処理されますが、この対応関係が正しくない場合、エラーを引き起こすことがあります。具体的には、変換指定子の方が後続の引数より多くなったり、引数の型が正しくなかったりすると、未定義の動作が引き起こされます。
// 変換指示子は2つだが、後続の引数は1つ? fprintf(file, "%d,%d", i); // 変換指示子は%sだが、型はint? int i = 10; fprintf(file, "value=%s", i);
後続の引数の数は、変換指示子の数と同じか、それより多くなければなりません。これが守られない場合、例のような未定義の動作を引き起こすばかりではなく、悪意あるコードが任意の位置にデータを書き込むこともできてしまいます。
エラーや未定義動作を防ぐには、書式指定文字列に入力された値を使わず、あらかじめ決まった値に固定することです。たとえfprintf関数でたったひとつの文字列を出力する場合でも fprintf(file, str) とせず、fprintf(file, "%s", str) のようにします。
数を扱うライブラリ
数を扱うライブラリにはいくつかの種類があり、数の種類によって分かれています。
- 論理:stdbool.h
- 整数:stdint.h , inttypes.h
- 浮動小数点数:float.h , fenv.h
- 複素数:complex.h
これらには、標準的に使われるよりも詳細な型の定義や、その型の特性を表すマクロの定義などが含まれています。例えば stdbool.h で定義される _Bool や、stdint.h で定義される int8_t などです。
数を演算するためのライブラリが math.h、tgmath.h、stdlib.h で提供されます。提供されている機能は多岐にわたりますので、代表的なものだけ説明します。
math.h
- 無限大やNaN(Not a Number)の判定: isfinite、isinf、isnan など
- 三角関数、双曲線関数: sin、cos、tan、asin、asinh など
- 指数関数: exp、log、log10、modf など
- べき乗関数、絶対値関数: abs、pow、sqrt など
- 誤差、ガンマ関数: erf、erfc、lgamma など
- 最近接整数関数: ceil、floor、round、trunc など
- 剰余関数: fmod、remainder、reqmquo など
- 実数操作関数: copysign、nan など
- 最大、最小、差: fmax、fmin、fdim など
- 浮動小数点乗算、加算: fma など
- 比較: isgreater、isless
stdlib.h
- 数値変換関数: atoi、atol、atoll、strtod、strtof など
- 乱数: rand、srand
- 整数算術関数: abs、div など
math.hに含まれる関数は、型によっていくつかのバリエーションが用意されています。例えば、sin関数は引数にdoubleを受け取りますが、floatの場合は sinf、long doubleの場合はsinl、さらに複素数用に、これら3つの関数の頭にcがついたもの(csin、csinf、csinl)が存在します。
もし何らかの理由でこれらの関数に渡す引数の型を変えようとすると、これを使っている関数をすべて対応するものに変えなければならず、とても面倒なことになります。long intで扱っていたがそれを超える値を扱いたくなったのでlong long intにするとなると、すべてのsin(やそれに類する関数)をsinlに変更しなければなりません。この手間を減らすため、math.hの代わりにtgmath.hを使うと、引数の型に応じて自動的に関数を使い分けてくれるようになっています。
以降では、上で挙げたリストの中でもよく使われる、数値変換関数と乱数について説明します。
数値変換関数は、文字列と数値を相互に変換するためのです。ato で始まるものと strto で始まるものの2種類があります。
int atoi ( const char * str ); long int strtol ( const char * restrict ptr, char ** restrict endptr, int base );
atoiには文字列を渡すと、変換結果の整数が返ります。例えば atoi("1") では1になります。もちろん atoi("0") ならば0になるのですが、整数に変換できない文字列を渡しても0が返ります。従って atoi("a") は0になります。このように ato で始まる関数は変換がエラーになったかどうかの判別ができないので、エラーの判定をしたい場合は strtol を使います。
strtol は1つ目の引数に変換する文字列の先頭を渡し、2つ目の引数で変換が終わった位置を受け取ります。3つ目の引数は基数(10進数や16進数などの数字の部分)です。
#include <stdlib.h> #include <stdio.h> int main(void) { char str[] = "40000km"; char *endptr; long int result; // 変換結果:result=40000, endptr="km" result = strtol(str, &endptr, 10); if (str != endptr) { printf("result=%ld, endptr=\"%s\"\n", result, endptr); } else { printf("error: >>>%c<<<\n", *endptr); } return EXIT_SUCCESS; }
変換に失敗すると戻り値は0になり、2つ目の引数で得られる値が1つ目の引数の値と同じになります。
atoi と strtol には、戻り値の型によっていくつか関数が用意されていますので、それらを使うとより大きな値や浮動少数にも変換できます。
- int: atoi
- long int: atol、strtol
- double: strtod
- float: strtof
- long long int: atoll、strtoll
- unsigned long int: strtoul
- unsigned long long int: strtoull
次は乱数です。
Cの標準ライブラリで提供している乱数は、疑似乱数です。srandに乱数の種(シード)を与えて初期化し、randで乱数値を得ます。
void srand ( unsigned int seed ); int rand ( void );
rand関数は0からRAND_MAXマクロで定義された値までの間の値を返します。疑似乱数ですので、seedに同じ値を与えれば、決まった数列が得られることに注意してください。プログラムの実行ごとに違った乱数列を得たいのであれば、実行ごとに異なる種、例えば時刻や、キーボードからのランダムな入力などを与える必要があります。
また、Cにおける乱数は、乱数の生成アルゴリズムが決められていないことにも注意してください。一般的にCの実行時ライブラリで提供される乱数は、高速ですがあまり質の高くない乱数列を生成するケースが多いようです。簡単なゲームなどではそれでも問題ないと思いますが、特にセキュリティに厳しい場面で使う場合にはそれでは要件を満たさないケースもあると思いますので、そのような場合には別の乱数発生器を検討してみてください。
最後に、limits.hを紹介します。
limits.hでは、整数型の表現できる範囲を表すマクロが提供されます。INT_MAXは型intが表現できる最大の値を、INT_MINは逆に型intが表現できる最小の値を表します。
#include <stdlib.h> #include <stdio.h> #include <limits.h> int main(void) { printf("max:%d,min:%d\n", INT_MAX, INT_MIN); return EXIT_SUCCESS; }
結果は環境ごとに異なります。私の環境では次のようになりました。
max:2147483647,min:-2147483648
他の型のために次のようなマクロがあります。
- signed char: SCHAR_MAX、SCHAR_MIN
- unsigned char: UCHAR_MAX
- char: CHAR_MAX、CHAR_MIN
- short int: SHRT_MAX、SHRT_MIN
- unsigned short int: USHRT_MAX
- int: INT_MAX、INT_MIN
- unsigned int: UINT_MAX
- long int: LONG_MAX、LONG_MIN
- unsigned long int: ULONG_MAX
- long long int: LLONG_MAX、LLONG_MIN
- unsigned long long int: ULLONG_MAX
intやlong intのような型は、すべて符号付きです。つまりintはsigned intでもあります。ところがcharは、単にcharとして現れたときに、それが符号付きなのか、あるいは符号無しなのかは、環境ごとに決められます。従って、charだけにはcharとは別にsigned charの最大最小が用意されています。
これらのマクロの値は環境ごとに異なりますが、どの環境でも保証される絶対値での最小の値が決められており、SCHAR_MAXは127、SCHAR_MINは-127となります。つまりsigned charには、-127から127の整数が必ず格納できます。逆に言えば、それ以上に大きな値が格納できるかどうかは、環境によるわけです。他に、UCHAR_MAXは255、INT_MAXは32,767(2の15乗-1)、INT_MINは-32,767、LONG_MAXは2,147,483,647(2の31乗-1)となっています。
今回学んだこと
- 文字列はナル文字 '\0' で終わる文字の並びです。
- 文字列操作に関連する機能はstring.hで提供されています。
- 文字操作はctype.h、wctype.hで提供されています。
- 文字操作の関数の一部はロケールの設定によって動作が変わります。
- 入出力機能はstdio.hで提供されています。
- 入出力の種類には「直接入出力」「文字入出力」「書式付き入出力」があります。
- 数を扱う機能はstdbool.h、stdint.h、float.h、complex.hなどで提供されています。
- 擬似乱数を生成する機能はstdlib.hで提供されます。
- 整数型の表現できる範囲はlimits.hで定義されています。
Copyright © ITmedia, Inc. All Rights Reserved.