ファイル処理の最後の説明で、バッファ・オーバーフローと、安全な入力について説明。
安全な入力
一般的なC言語での文字列入力のプログラムは、以下の様なものがテキストにも書かれている。
// memory // [局所変数str][戻り番地][..........] void foo() { char str[ 10 ] ; scanf( "%s" , str ) ; }
バッファオーバーフロー
しかし、こういった処理は極めて危険である。 入力の際に、10文字以上のデータが入力された場合、 一般的な処理系では、str[] の配列の近辺に 「関数foo()の実行後に戻る処理の番地」が格納されている場合が多く、 文字列をはみ出るような入力があった場合、処理番地を書き換えられる可能性がある。 悪意のあるプログラマーは、はみ出す領域に、戻り番地を書き換えるデータと、悪意のある処理を 書くことで、想定外の処理を動かすかもしれない。 このテクニックをバッファオーバーフローと呼ぶ。
このため、最大文字制限の機能を使い、以下のように記述すべきである。
char str[ 10 ] ; scanf( "%9s" , str ) ;
しかし、scanf() には、空白を読み飛ばす機能により「入力が無い場合…」といった 処理が書きにくい、%d入力で実数の"."や文字列を間違って入力したときの処理などの 問題があって、scanf() 単体で複雑な入力に対応することは極めて難しい。
fgets() + sscanf()
このような場合によく使われるのが、fgets()とsscanf()である。
fgets() は、文字配列に1行分のデータ(行末文字"¥n"まで)を、文字配列に読み込む関数である。 また、sscanf() は、文字配列のデータから、scanf() と同じようにデータを読み込む。
FILE* fp ; if ( (fp = fopen( "data.txt" , "rt" )) != NULL ) { char buff[ 100 ] ; while( fgets( buff , sizeof( buff ) , fp ) != NULL ) { int x ; double y ; char z[100] ; if ( sscanf( buff , "%d%lf%s" , &x , &y , z ) != 3 ) break ; // x , y , z を使った処理 } fclose( fp ) ; }
fgets() は、第一引数に、読み込み先の配列アドレス、第2引数に最大読み込みバイト数、第3引数に 読み元のファイルを指定する。文字配列への読み込み時には、第2引数のサイズを超えて 読み込むことはしないので、バッファオーバフローの心配はない。 また、入力データが無い場合には、NULL を返す。
sizeof()は、引数部分の変数のバイト数を返す演算子。
sscanf() は、データの入力が、第一引数の文字列から読み込む以外は、 scanf(),fscanf() と同じ使い方。
注意:fgets では、入力が最大バイト数以下の場合、行末文字まで読み込む。
Tips: 入力が無かったら(空行なら)、標準入力(通常キーボード入力)なら
char buff[ 1024 ] ; while( fgets( buff , sizeof( buff ) , stdin ) != NULL ) ) { // stdin は、標準入力(通常キーボード) if ( buff[ 0 ] == '¥n' ) { // 入力が空行だった場合 } }
fscanf()にfprintfがあるように、sscanfに対してsprintfもある。
char buff[ 1024 ] ; sprintf( buff , "%d %5.1lf %s" , 12 , 34.5 , "abc" ) ; // buff = "12 34.5 abc"となる。
#defineマクロ
scanfで"%d"で数字を入力する際に、文字を入力されると、あとの処理が書きにくい場合が多い。 この場合、fgetsで入力し入力データが正しい文字を使っているかチェックしてから、 sscanf()を使うなどの対処をとることが多い。 ここで、#define マクロを使ってみる
#include <stdio.h> #define isdigit(C) ((C)>='0' && (C)<='9') void main() { char buff[ 1024 ] ; while( fgets( buff , sizeof( buff ) , stdin ) != NULL ) { char* pc ; for( pc = buff ; *pc != '¥0' && isdigit( *pc ) ; pc++ ) /*nothing*/ ; if ( *pc == '¥n' || *pc == '¥0' ) { int x ; sscanf( buff , "%d" , &x ) ; } } }
#define は、通常プログラム中の定数を分かりやすく使う場合に、使われる。
#define PI 3.14159265
ただし、#で始まる行は、C言語によって特殊で、C言語の解析の前に「プリプロセッサ」 でプログラムの内容を書き換える機能。
C言語のプログラムが機械語になるまで:
C言語(#行を含む) ↓ プリプロセッサ C言語(#行なし) ↓コンパイル 中間コード(printfなどの標準関数などが未解決) ↓ リンク処理 ← ライブラリ(標準関数などの中間コードをまとめたもの) ↓ 機械語
#define マクロでは、isdigit() の引数? C が、呼び出し部の *pc となり、 isdigit( *pc ) の部分は、以下のように書き換えられる。
((defineマクロ宣言)) #define isdigit(C) ((C)>='0' && (C)<='9') ((デファインマクロを使ったら)) isdigit(*pc) ↓ C ← *pc ((*pc)>='0' && (*pc)<='9')
ただし、#defineマクロは、プログラムのコンパイル前に、文字列として書き換えを行う。
// 例1 #define begin { #define end } void main() // まるでPASCALのような記述(^_^; begin printf( "Hello¥n" ) ; end // 正しく動くよ // 例2 #define ADD(X,Y) X + Y #define MUL(X,Y) X * Y void main() { printf( "%d" , MUL( 3 , ADD( 4 , 5 ) ) ) ; } // 3 * 4 + 5 に書き換えられるので、17になる。 // 3 * (4+5) の27にはならない。 // 普通の関数のように27の結果が欲しいなら、 // #define ADD(X,Y) ((X)+(Y)) // #define MUL(X,Y) ((X)*(Y)) // と書くべき。
開いた関数と閉じた関数
#defineマクロは、定義しておいた命令への書き換えなので、関数と同じように思うかもしれない。 ただし、機械語を生成する処理の前に書き換えるので、#define マクロで書き換えられる処理が 長い場合は、生成される機械語が大きくなる場合がある。 一方で、#defineマクロを使うと引数の受け渡しが無いので、 isdigit()のような簡単な処理の場合、 生成される機械語の処理が少し速い。
// 開いた関数FOO #define FOO(X) Xを使った処理 void main() { int x ; FOO(x) ; // xを使った処理 FOO(x) ; // xを使った処理 // FOOの中身が長い場合、FOOの機械語が2個作られる。 } // 閉じた関数foo int foo(int z) { zを使った処理 : : } void main() { int x ; foo(x) ; // z←x,fooを呼び出し foo(x) ; // z←x,fooを呼び出し // fooの機械語は1個だけ,引数の受け渡し処理が2個 }
#defineマクロのADD,MULの優先順位の問題が、"醜い"と思う人は、 C++で新しく導入された、"inline"関数を勉強すること。 #defineマクロを使わずに、開いた関数を簡単に記述できる。