見出し画像

C言語のポインタと文字列

 C言語を始めて、まず最初に混乱するのがポインタでしょう。
〔ここでは、C言語として話しますが、C++言語でも通じる内容です〕

 C言語は、文字列や配列をひと固まりのように扱えますが、
それはポインタを利用した、巧みな方法をであるため、混乱も起こります。

 char text[10];と書くと、文字が格納できる10個の箱が確保されます。
 正確には、textと名前のついた、char型の、サイズが10の、配列が確保されます。
 これに、
 text[0] = 'A';
 text[1] = 'B';
 text[2] = 'C';
 text[3] = '\0';
とすると「ABC」という文字が格納された文字列となります。


「'\0'」は文字列の終わりを示す特殊な値で、文字列には必ず必要です。
 この終わりを示す終端が必要なために、10個の箱には9文字しか格納できません。
 これも、C言語で良くハマる罠です。
 このようにC言語では、chra型の配列に、終端を付加するルールで、文字列を扱います。

〔現在では漢字や、多言語対応のために拡張されていますが、ここでは元来の8ビット限定の文字コードに絞って話を進めます〕

 このtextを他の関数に渡す場合、
 void set_name(const char* name);
のような関数があるとすれば、
 set_name(text);
と書けば渡せます。


 さて、受け取る側はあとで使うため、渡された文字列を保存しておきたいため、つぎのようなソースコードを書いたとします。

name.c
--------------------------------------------------

static const char* __name;

void set_name(const char* name)
{
	__name = name;
}

 渡されたnameを自身の静的変数に保持しています。あたかも文字列全体が保存できたかのように見えますが、これには問題があります。

 呼び出した側はつぎの処理のために、set_name(text);を呼び出したあとに、textの内容を書き換えたとします。
 すると、あとで使おうと保持しておいたname.cの__nameも、"XYZ"に変わってしまいます。
 このような罠にハマってしまったことは、一度や二度、経験があるのではないでしょうか。
 つぎのようなソースです。

main.c
--------------------------------------------------

int main(void)
{
  char text[10];

 text[0] = 'A';
 text[1] = 'B';
 text[2] = 'C';
 text[3] = '\0';

  set_name(text);

 text[0] = 'X';
 text[1] = 'Y';
 text[2] = 'Z';
 text[3] = '\0';
}

 C言語では、配列となっている変数の変数名から、[]部分を取り除き名前だけにすると、その配列の先頭を示すアドレスとなります。
 アドレスは、その変数が、どこに格納されているかを示すものです。
 この機能のおかげで、配列や文字列をひと固まりとして、スマートに扱えるのですが、混乱も招きます。
 set_name(text);では、textの先頭のアドレスしか渡せていません。

 受け取る側、set_name()の仮引数nameは、chra型のポインタ変数です。それを保持している静的変数の__nameも、同じchra型のポインタ変数です。
 ポインタ変数は、値が格納されている変数の『アドレス』を格納する変数です。
 main.cから渡されたtextの先頭アドレスを、ポインタ型の仮引数で受け取り、そのままポインタ型の静的変数にコピーしたにすぎません。
 実態はコピーされていないので、元の場所にある実態が変更されてしまうと、保持していたつもりの内容が、変わってしまいます。


 これを回避するにはname.c側にも領域を持ち、実態をコピーして保持しておく必要があります。
 つぎのようなソースになります。
 C言語には文字列の実態をコピーするには、このように関数を呼び出すか、一文字づつ代入していくループを書かないとできません。

name.c
--------------------------------------------------

static char __name[10];

void set_name(const char* name)
{
    strcpy(__name, name);
}


 さてこの処理だけでは、渡されるnameが9文字以上の場合、問題を起こします。
 用意されているのは9文字分の領域だけですが、それを超えてコピーされてしまいます。
 用意されている領域を超えた場所は、未知の領域で、そこに何かがコピーされた場合、どのような挙動がまき起こるか、予測不可能です。
 今すぐ不具合がは起こらずとも、あとあとになって不具合が起こることも多々あります。
 なので実際には、用意された領域を超えてコピーされないような、何らかの措置が必要です。
 これは、バッファオーバフローと言われる現象で、C言語が脆弱とされる要因のひとつです。


この話のポイント

・アドレスは、変数が格納されている場所を示す。
・ポインタ変数は、そのアドレスを格納する変数である。

・配列は、メモリ上の連続する場所に、領域がとられ、アドレスは連続する。
・配列は名前だけにすると、その先頭のアドレスになる。

・char型の配列に、最後に'\0'を付けるルールを適応すると、ひと固まりの文字列として扱える。


この記事が気に入ったらサポートをしてみませんか?