第26回
データ構造(5)~ポインタを使った文字列処理関数を作る

ポインタの走りすぎを回避する

文字列をコピーするstrcpy関数と文字列を連結するstrcat関数は、仕様上大きな欠点を抱えています。それを回避することを考えてみましょう。

他の変数領域を浸食する

仕様上の大きな欠点とは、受け側(strcpyでは第2引数、strcatでは第1引数)の配列に余裕がない場合、確保していた要素を超えて文字がコピーされてしまう――という点です。

例えば、要素数が10個の配列destに"Hello world!"という文字列を保持している配列srcの内容をstrcpy関数でコピーすると、"world"の"l"(小文字のL)のところでdestの余裕は尽きてしまい、"d!'\0'"の3バイト分がはみ出してしまいます。

「はみ出す」とは、確保されていた配列の最後に続くメモリ領域に値を書き込んでしまう――ということです。そこに他の変数の領域があれば、内容は勝手に変更されてしまいます。

strcat関数の場合も、第1引数の要素数が第2引数を連結しても大丈夫な余裕を持っていればよいのですが、そうでない場合には上述のstrcpy関数の場合と同じことが起こります。

意外なバグの原因

現在の処理系では、こういった場合コンパイル時に警告が出ますが、それでも実行形式ファイルは生成されます。

このような現象を「ポインタの走りすぎ」などと呼び、Cの抱える大きな弱点の1つと言われています。

Cのポインタが難しいと言われる要因の1つに、うっかりミスでポインタが走りすぎ、まったく関係のない変数の値を書き換えてしまうことがあります。プログラマーには、strcpyやstrcatで処理した文字列の隣にどのような変数が置かれているか分からないため、なかなかミスに気付かないのです。

このようなCの抱える問題を回避するためにC++ではstringクラスを用意し、文字列を単純なchar型配列ではなく、安全性を考慮したものにしています。C++についてもいずれ取り上げる予定ですが、ここではCのレベルでポインタの走りすぎを回避する方法を考えてみましょう。

strcat関数を作り替える

strcat関数では、リスト6のような形が考えられます。戻り値を保持するポインタretはstatic宣言します。
static char *ret;

関数の内部で宣言された変数は、関数の処理が終わると消滅します。しかしstaticキーワードを付けて宣言された変数は、関数の処理が終わっても存在し続けます。

さらにmalloc関数で必要な文字数+1(終端のNULLの分)バイトのメモリを確保します。malloc関数は、引数に示したバイト数分のメモリを確保してその先頭ポインタを返す関数です。使用するにはstdlib.hを取り込んでおかなければなりません。

p = (char *)malloc(strlen(s1) + strlen(s2) + 1);
関数の戻り値は、別途宣言したポインタ変数pに受け取ります。

ret = p;
として確保したメモリの先頭アドレスをretに保存しておき、pに対して第1引数(s1)の示す文字列をコピー、続いて第2引数(s2)の示す文字列を連結します。

これで、malloc関数で確保したメモリ領域に2つの文字列が順にコピーされるので、最後にメモリ領域の先頭アドレスを保存していたポインタretを返します。

処理の最初に2つの引数が示す文字列のバイト数を計算してメモリを確保するため、ポインタの走りすぎによって他の変数の領域を浸食することはありません。ただ、これはstrcat関数本来の仕様(第1引数に第2引数の文字列をつなぐ)とは異なり、第1引数と第2引数のそれぞれが示す文字列を連結した『新たな文字列』を返す形となります ※2
C++のstringクラスでは、malloc関数の代わりにnew演算子を使って新たな領域を確保するなど、Cのアバウトな仕様をカバーしています。リスト6はそれと似た仕組みをCで作ったものです

リスト6:ポインタの走りすぎに対処したstrcat関数
char * strcat(const char *s1, const char *s2)
{
  static char *ret;    /* 戻り値のポインタを静的に宣言 */
  char *p;             /* 作業用のポインタ */
  /* 必要なバイト数を確保する */
  p = (char *)malloc(strlen(s1) + strlen(s2) + 1);
  ret = p;    /* 戻り値に値を代入 */

  while (*s1 != '\0') {
    *p++ = *s1++;
  }
  while (*s2 != '\0') {
    *p++ = *s2++;
  }
  *p = '\0';
  return (ret);
}