ついついやってしまう単純なミス
(文字列操作その1)
奥が深い文字列と配列変数とその操作
慣れてくるとやってしまうミスです。下のソースを見てみてください。
1:#include <stdio.h>
2:#include <stdlib.h>
3:#include <string.h>
4:#include <memory.h>
5:
6:int main( int argc , char **argv )
7:{
8: char
*p_string ;
9:
10: p_string
= NULL ;
11: p_string
= (char *)malloc( strlen( argv[1] ));
12: strcpy(
p_string , argv[1] );
13: fprintf(
stderr , "input [%s]\n" , p_string ) ;
14: free(
p_string );
15:
16: return
0 ;
17:}
このプログラムは、コマンドラインの引数の第1引数で与えられた文字列の長さを調べて、文字列バッファに格納しそれを出力しています。
一見どこも変ではないような気がします。実際コンパイルもリンクも正常に通ります。しかし、実行してみると不正な処理となってしまって
プログラムが不具合を起こします。
個人で行う分には深刻な不具合ではないのですが、実際の業務システムのプログラムの一部であったりしたばあい、該当プログラムが停止
してしまうことによって、システムに深刻な影響を与えかねません。
これを避けるために、特にC言語等で文字列操作のプログラムロジックを組む場合は細心の注意をはらいたいものです。
では何がいけないのか? 順を追ってみてみましょう。
ポイントとしては3つ。調べた文字列の長さ・必要なバッファの確保の仕方。それにバッファに格納する時。この3つを抑えましょう。
それではまず「調べた文字列の長さ」です。調べるべきデータはコマンドライン引数から取得するので、argv[1] という変数に格納されています。
この変数はポインタで渡される参照専用の変数ですので、この変数に対して値の設定などは行ってはいけません。
11行目にある strlen( argv[1] ) という部分がありますが、ここで文字列の長さを調べることになります。
この関数が返す戻り値は、引数( argv[1] ) の文字列の長さをバイト単位で返します。 例えば "テスト" という文字を引数で与えた場合は
戻り値が"6" で返されます。"テスト" というのは2バイト文字ですので2バイト×3で計6バイトとなります。
とりあえずここはこれで抑えておきましょう。
次に文字列を格納するバッファ(領域)を確保しましょう。始めから大きな領域をプログラム内で確保しておく手もあります。
例: static char buff[ 1024 ] ;
この場合はバッファの確保などいちいち行わなくてすむのですが、格納すべき文字列が3バイトとか少なかったり、逆に長いデータ等を扱うとき
いちいちプログラム修正をしなければならず無駄が多い上に不効率的なプログラムになってしまいます。
そこで malloc() という関数を使うわけですが、この関数は指定したサイズ分の領域を確保してくれるかんすうです。細かい事をいうと
確保するだけで、元々そこにかくのうされていたなんでもないデータはそのまま残っているということを覚えておきましょう。
実際に領域の確保の仕方は以下の通りです。 例では、10バイトの大きさの領域を確保しています。
例: p_string = (char *)malloc( 10 ) ;
p_string というのは文字列型のポインタ変数です。malloc() 関数により、10バイト、システムで確保され、その先頭アドレスを
返すという処理です。 (char *) という部分はキャストといい、C言語で日常的に使用します。 malloc() 関数で返されるアドレスは
文字列型とは決まっていないので、その都度明示的に格納先の型に合わせて変換させてやる必要があり、これを「キャストする」といいます。
では上記ソースから見てみてみましょう。 p_string = (char *)malloc( strlen(argv[1]) );
という部分です。コマンドライン引数で与えられた文字列長を strlen(argv[1]) というように長さをしらべ、その値を malloc() 関数の引数として
与え、ここで動的に必要領域を確保しています。 仮に今はデータをして "test" という文字列を受け取ったとかていします。
これで p_string という変数は "test" という文字列を格納するための4バイトの領域を持ったことになります。ここまではいいでしょう。
問題になるのはこれからです。さて、では確保された領域にデータを格納します。格納するデータは "test" という文字列4バイトです。
これを p_string という変数に格納します。 strcpy( p_string , argv[1] ) ; という部分です。
strcpy()
という関数は二つのパラメータを指定します。strcpy( para1 , para2 ) で、第1パラメータには格納先の変数を。
第2パラメータには格納すべきデータが入った変数を指定します。この関数のポイントは第2パラメータで指定した変数の終端文字NULLまでを
コピーするというてんにあります。
ここも注意が必要です。終端文字までをコピーするということで、例えば "test" という文字列をコピーする場合、strlen() 関数を使用して
長さを調べると4バイト必要になります。そこで4バイトの領域を確保してコピーすればいい。というわけにはいかないのです。
C言語で文字列というのは、文字データ
"test" に合わせて終端文字 NULL( 0x00 )が終わりに付加されてそれで文字列というのです。
ここがこのプログラムの最大のポイントであり、C言語でプログラムを組むのに一番注意が必要なところでもあります。
実際には "test" という文字列を別の変数にコピーする場合は
"test" という文字列長4バイトに加えて、
終端文字の1バイトを加えた計5バイトが必要になります。
もし上記の点をウッカリミスで忘れていた場合? もし、4バイトで領域を確保してあったところに、10バイトのデータをコピーしてしまった
場合。どうなるでしょう。 確保していた4バイトを越えて6バイト分のデータがシステム上に上書きされてしまうことになってしまいます。
C言語のコンパイラはコンパイル時にあらかじめこのようなチェックは出来ません。 データが不正に上書きされてしまうと、もし
上書きされた所を他のプログラムが参照していたとしたら? もし上書きしたところがシステムで重大なプログラムが使用していた部分だった
としたら? どうなるでしょう。 一つのプログラムの小さな不正な処理で、システムが深刻なダメージを受けかねないのです。
この点をプログラマは十分に理解し、プログラムを組む必要があるのです。
話が飛んでしまいましたが、上記のプログラムコードは前述の事が起こる「不正なプログラム」なのです。
ではどこをどうすればいいのでしょうか?
ポイントは領域の確保の仕方にあります。 "test" という文字列をコピーするので、4バイトに合わせて、終端文字1バイトが
必要になるので、 次のように変更します p_string = (char *)malloc( strlen(argv[1]) +1 ) ;
文字列長を strlen() 関数で調べて、それに+1という形で終端文字分のサイズを加えたものを malloc() 関数の引数として与えています。
それによって、確保される領域サイズは5バイトとなるので、上記のような不正な処理は起こらずに清むのです。
実は文字列を扱うときにはさらに注意するべき点がいくつかありますが、それはおいおい説明していきます。
それでは、修正後のプログラムは下のようになります。 上のプログラムはエラーが起こりますが、文字列とその操作方法を十分に
理解していないと、不具合解明が非常に困難になります。その点を十分理解しておきましょう。
1:#include <stdio.h>
2:#include <stdlib.h>
3:#include <string.h>
4:#include <memory.h>
5:
6:int main( int argc , char **argv )
7:{
8: char
*p_string ;
9:
10: p_string
= NULL ;
11: p_string
= (char *)malloc( strlen( argv[1] )+1);
12: strcpy(
p_string , argv[1] );
13: fprintf(
stderr , "input [%s]\n" , p_string ) ;
14: free(
p_string );
15:
16: return
0 ;
17:}