ポインタと文字列

ポインタの活用例その2

文字列はchar型配列に保存する、というのは文字型と文字列の項で説明しました。
そして、ポインタと配列は似ている、というのもポインタと配列で説明しました。

ポインタを利用すれば、文字列はもう少し便利に扱うことができるようになります。


#include <stdio.h>

int main()
{
    char str[] = "ABCD";
    //char str[] = { 'A', 'B', 'C', 'D', '\0' };
    
    char *strP = "EFGH";

    printf("%s\n", str); //ABCD
    printf("%s\n", strP); //EFGH

    getchar();
}

5行目は今まで通りのchar型文字列です。
まずはこの処理を、メモリ上の処理に着目して詳しく追っていきます。

文字列リテラルは、プログラムの実行開始から終了まで常にメモリ上に存在する、という特徴があります。
配列の初期化時に文字列リテラルを指定すると、その文字列の長さ分(プラスNULL文字)のサイズを持つ配列が自動的に生成され、そこに一文字ずつ値がコピーされます。

char型配列の初期化子に文字列リテラルを指定した場合のメモリ上の処理

6行目のコメントに書いた通り、文字列リテラルでの初期化は、文字列を一文字ずつの文字に分解した初期化リストを指定するのと同等です。

次に、8行目の処理を見てみます。


char *strP = "EFGH";

文字列リテラルを評価すると、その文字列リテラルの先頭のアドレス(char型のポインタ)が返ってきます。
これをchar型のポインタ変数で受け取っています。

文字列リテラルをchar型ポインタ変数に代入

配列変数もポインタ変数も、「そのまま」書いた場合にはデータの先頭を示すポインタを返す、という点は共通しています。
そのため、printf関数の引数指定では全く同じ書き方ができるのです。
(10、11行目)
文字列の終端はどちらもNULL文字が目印です。

ちなみに、ポインタ変数も変数ですから、アドレスを保存しておく領域がメモリ上に確保されます。
この時のサイズは32bit環境ならば4バイトなので、上記のイメージ図では4マス消費しています。
(64bit環境では8バイト)

文字列のポインタのメリット

文字列のポインタが配列と同じように扱えたとしても、ただそれだけではメリットがありません。
配列にはない便利な点として、代入だけで文字列を別の文字列に変更可能です。


char str[] = "ABCD";
char *strP1 = "EFGH";
char *strP2;

//これはNG
str = "IJKL";

//これはOK
strP1 = "IJKLMNOPQRSTU";

//これもOK
strP2 = strP1;

6行目のような書き方はできないのは文字型と文字列の項で説明した通りです。
文字列配列に別の文字列を代入したい場合は、一文字ずつ書き換えていくか、関数(strcpy関数など)を使用する必要があります。

しかしポインタ変数ならばこれが可能です。
文字列リテラルを評価すると、その文字列へのポインタが返ってきます。
9行目の処理はそれをポインタ変数に格納しているだけですから、問題なく動くコードとなります。
もちろん、strP1が示す文字列も置き換わります。

文字列の長さに制限がないというのも大きな利点です。
9行目は最初に代入した文字列よりも長い文字列を再代入しています。

ポインタ変数の格納に必要なメモリ容量はint型と同じで、どれだけ長い文字列であっても4バイト(32bitのとき)の容量で位置を示せます。
文字列リテラルはプログラムの実行開始時にはすでにメモリ上に存在しているのですから、その場所をポインタに代入するだけで別の文字列を表示させることができます。
配列のときのように文字列を保存するために配列サイズをあらかじめ確保しておく必要もありません。
(文字列の終端はNULL文字で判断できます)

さらに、12行目のような書き方も問題ありません。
これはstrP2をstrP1と同じ文字列を指すように指定しています。
配列の時はこういう書き方はできませんでしたが、非常にシンプルに記述できます。

文字列のポインタのデメリット

文字列配列よりも利点が多い文字列のポインタですが、文字列配列ではできて文字列のポインタではできないこともあります。

それは、C言語では文字列リテラルは書き換えてはならないというルールがあるためです。


char str[] = "ABCD";
char *strP = "EFGH";

//これはOK
str[1] = 'Z';

//これはNG
strP[1] = 'Z';

//こうやってもNG
*(strP + 1) = 'Z';

文字列配列のときは、5行目のように文字列を書き換えることができました。
配列は、文字列リテラルとは別の場所に容量を確保し、そこに値を保存していますから、書き換えても問題ありません。

しかし、ポインタが指し示すのは文字列リテラル自身です。
文字列リテラルは書き換えてはならないというルールがあるため、8行目や11行目のような記述はできません。
strcpy等の文字列操作関数を用いても書き換えはできません。

実際にこのコードをコンパイルすると、問題なく動くこともあります。
しかしそれはたまたま動いただけの話で、常に問題がないとは限りません。
文字列リテラルを書き換えた時の動作は未定義なので、どのような動作を引き起こすかわからないのです。

そのため、書き換える可能性がある文字列はchar型配列、書き換える可能性がない文字列はポインタ、という使い分けが良いと思います。
文字列ポインタは書き換えはできませんが別の文字列リテラルを指すように変更することは容易なので、多くの場合はポインタを用いることになるでしょう。