C言語での “文字列リテラル” の扱い

C言語における文字列リテラルの扱いの解説ページのアイキャッチ

このページにはプロモーションが含まれています

このページでは、C言語における “文字列リテラル” について解説していきます。

早速ですが、下記の処理は実行しても問題ないですが("Good bye"'b''B' に変更しています)、

正常に動作する例
char str[] = "Good bye";
str[5] = 'B';

下記の処理は実行してはダメです。この理由がわかるでしょうか?(実行するとプログラムが落ちる可能性あり)

動作が異常になる例
char *str = "Good bye";
str[5] = 'B';

理由が気になる方は是非このページを読んでみてください!C言語における “文字列リテラル” の扱いについて理解することで、上記の2つの処理の違いが分かるようになります(ポインタの知識も必要ですが)。

試してみたら確かにプログラム落ちた…

でもどっちもそんなに処理変わらなさそうだけどな…

これは割とやりがちな間違いだよ!

やりがちだけど、結構原因を突き止めるのは難しい…

で、”動作が異常になる例” は割と結構やりがちな間違いです(私も最近しでかしました…)。いざ対処しようと思っても、この間違いの原因が分からないと、その対処法も思いつきにくいです。

ですが、このページを読んでいただければ、上記の原因が分かるので、同じ間違いをしたとしてもすぐに対処法を思いつくようになると思います!

まずは文字列リテラルの説明からしていきますが、上記の違いの原因だけを知りたい方は文字列リテラルを変更してはダメまでスキップしていただければと思います。

文字列リテラルとは

では、まずは “文字列リテラル” がどのようなものであるのかについて解説していきます。

文字列リテラルとは、ソースコード中に直接記述された文字列のことを言います。ちなみに、文字列に関わらず、ソースコード中に記述された値のことは “リテラル” と呼ばれます。つまり “文字列リテラル” とはリテラルの文字列バージョンとも捉えられますね!

例えば下記のようなソースコードにおいては、どれが文字列リテラルにあたるでしょうか?

文字列リテラルの例
#include <stdio.h>
#include <string.h>

int main(void) {
    char str1[256];
    char *str2 = "Hello";

    strcpy(str1, "Good bye");

    printf("str1 is %s, str2 is %s\n", str1, str2);

    return 0;
}

"Hello""Good bye" かな!

惜しい!"str1 is %s, str2 is %s\n"も文字列リテラルだね!

printf 内に記述されてる文字列も文字列リテラルなのか…

上記のソースコードには、下記の3つの文字列リテラルが存在します。

  • "Hello"
  • "Good bye"
  • "str1 is %s, str2 is %s\n"

printf 関数の第1引数に指定する文字列も、ソースコードに直接記述された文字列なので “文字列リテラル” になります。ですので、文字列リテラルは、おそらく皆さんがC言語を学びだした比較的早い段階から利用している “馴染み深い” ものであることが実感していただけると思います。

では、こういった文字列リテラルが、C言語プログラムにおいてどのようにして扱われているかについて解説していきたいと思います。

文字列リテラルはメモリ上に配置される

C言語においては(他のプログラミングもそうですが)、変数はメモリ上に配置され、そのメモリを利用することで様々な処理を実現することが可能です。

文字列リテラルも同様で、ソースコード中に記述された文字列リテラルはメモリ上に配置されます。

といっても、C言語には文字列型はありませんので、文字の配列(char 型の配列)がメモリ上に配置されるイメージです。

さらにC言語では、文字列の最後はヌル文字(\0)として扱うのが一般的ですので、それに合わせて文字列リテラルの後ろにはヌル文字が付加された状態で配置されることなります。

リテラル文字列がメモリ上に配置される様子を示す図

さらに、文字列リテラルは “プログラムの起動 〜 プログラムの終了” の間ずっとメモリ上に配置されたままになります(ローカル変数などは関数実行中のみメモリ上に配置される)。

ですので、プログラム起動中であれば、どのタイミングであっても文字列リテラルを利用することが可能です。

スポンサーリンク

文字列リテラルはアドレスとして扱われる

また、メモリ上に配置されたデータには、そのデータがメモリ上のどの位置に存在するかが分かるようにアドレスが割り振られます。

文字列リテラルもメモリ上に配置されるわけですのでアドレスが割り振られることになります。

例えば、下の図はアドレス 0xA0 に "Hello" が、アドレス 0xA6 に "Good bye" が配置された様子を示しています(アドレス値は16進数表記しています)。

文字列リテラルにメモリが割り振られる様子を示す図

さらに、C言語のソースコードで文字列リテラルを記述した場合、その文字列リテラルは、その文字列リテラルの “先頭アドレス” に置き換えて扱われることになります。

文字列リテラルがアドレスと置き換えられて扱われる様子を示す図

イメージとしては、C言語で配列名を記述した際に、その配列名が配列の “先頭アドレス” に置き換えて扱われるのと同様です。

例えば下記のように printf を実行すると、文字列リテラル "Hello" の先頭アドレスが表示されます。

文字列リテラルを表示
printf("%p", "Hello");

また、下記のように printf の引数に文字列リテラル "Hello" を指定した場合は、"Hello" の先頭アドレスが printf 関数に渡されることになります。

文字列リテラルを文字列として表示
printf("Hello");

そして、printf は、その渡されたアドレスからデータを読み込み(つまり、"Hello" を読み込み)、それを標準出力に表示してくれるというわけです。

また、文字列リテラルはアドレスとして扱われますので、文字列リテラルに対する比較は、アドレスに対する比較になります。

例えば下記では "Hello""Good bye" の比較を行なっていますが、これは文字列としての比較ではなく、"Hello" の先頭アドレスと "Good bye" の先頭アドレスの比較を行なっていることになります。

文字列リテラルを用いた比較
if ("Hello" < "Good bye") {
    printf("Good bye の方が後ろのアドレスにあります\n");
};

要は上記の if 文では、"Hello" よりも "Good bye" がアドレス的に後ろ側に配置されている場合に成立することになります。まずこんな比較は行わないと思いますし、文字列自体の比較を行いたいのであれば strcmp を利用するようにしましょう。

さらに、ここからはちょっと特殊な使い方ですが、文字列リテラルはアドレスとして扱われるので、文字列リテラルに対して足し算を行うようなこともできます。

例えば、下記のような計算もできてしまいます。printf で表示されるのは何になるでしょうか?

文字列リテラルへの足し算
char *adr = "Good bye" + 5;
printf("%c", *adr);

ゲゲっ

何やってるか意味不明…

文字列リテラルがアドレスとして扱われることを思い出すと分かりやすいかも!

何やってるかよく分からない計算ですね…。ただ、文字列リテラルがアドレスとして扱われることを考慮するとどんな計算が行われるのか分かりやすいのではないかと思います。

前述の通り文字列リテラルは、文字列リテラルの先頭アドレスに置き換えて扱われます。

ですので、上記の計算では、"Good bye" の先頭アドレス + 5 が実行されることになります。つまり、この計算結果の代入先である adr "Good bye" の第 5 文字の 'b' を指すことになります。

したがって、上記の printf で表示されるのは 'b' になります。

また、先頭アドレスに置き換えて扱われるのは配列名も同様です。で、配列名の場合、添字演算子([])を用いて配列の特定の要素にアクセスすることができますよね?

これと同様に、文字列リテラルにも添字演算子([])を利用することができます。これにより、文字列リテラルの特定の要素(文字)にアクセスすることができます。

例えば、下記は "Good bye" の第 5 要素(つまり 'b')を取得し、その文字を printf で表示する処理になります。

文字列リテラルへの添字演算子
printf("%c", "Good bye"[5]);

また、同様に文字列リテラルに対して間接演算子(*)を使用することもできます。例えば下記では、"Good bye" の先頭アドレスのデータ(つまり文字 'G')を間接演算子(*)により取得し、その文字を printf で表示する処理になります。

文字列リテラルへの添字演算子
printf("%c", *"Good bye");

正直実践上は、文字列リテラルへの足し算や添字演算子によるアクセスを行うことはまずないです。あくまでも、文字列リテラルがアドレスとして扱われている(配列名と同様に扱われている)ことを理解していただくための例になります。

文字列リテラルはポインタで指すことができる

文字列リテラルはアドレスとして扱われるのですから、当然ポインタで指すことが可能です。C言語では、メモリ上に存在するもの(アドレスが割り振られているもの)は、ポインタでなんでも指すことができてしまいます。

文字列リテラルをポインタで指す様子を示す図

例えば下記の処理を実行すると、文字列リテラル "Good bye" の先頭アドレスをポインタで指すことができます(文字列リテラルを指す場合、ポインタ変数の型は char * を使用します)。

文字列リテラルをポインタで指す
char *str = "Good bye";

さらに、ポインタでは間接演算子(*)や添字演算子([])を用いて、そのポインタの指す先のデータを取得することができます。

例えば下記では、間接演算子(*)と添字演算子([])を用いて、ポインタが指す先から 5 要素後ろのデータ、つまり "Good bye" の第 5 文字を取得して表示する例になります。

ポインタが指すデータを取得する
char *str = "Good bye";
printf("%c\n", str[5]);
printf("%c\n, *(str + 5)

文字列リテラルを変更してはダメ

取得だけでなく、ポインタでは間接演算子(*)や添字演算子([])を用いてポインタの指す先のデータを変更することも可能です。

つまり、ポインタが文字列リテラルを指す場合、間接演算子(*)や添字演算子([])を用いて文字列リテラルを変更しようとすることができてしまうことになります。

例えば下記のような処理は、"Good bye" の第 5 文字を 'b' から 'B' に変更するものになります。

文字列リテラルの変更
char *str = "Good bye";
str[5] = 'B';

ポインタから文字列リテラルを変更する様子を示す図

ですが、上記を実行すると、ほとんどの環境ではプログラムが異常終了することになると思います。私の環境では下の図のように EXC_BAD_ACCESS というエラーが発生し、その時点でプログラムが異常終了します。

文字列リテラルの変更時の動作例を示す図

前述の通り、C言語では、ポインタで文字列リテラルを指せば、その文字列リテラルを変更しようとすることができてしまいます。ですが、その文字列リテラルの変更は禁止されており、文字列リテラルを変更しようとした場合、多くの環境では上記のようにプログラムが異常終了するようになっています。

もし変更できてしまうと、例えば下記では printfGood bye ではなく Food bye と表示されるでしょうね。

文字列リテラルが変更できてしまうと...
char *str = "Good bye";
str[0] = 'F';
printf("Good bye");

前述の通り、文字列リテラルはアドレスとして扱われます。上記の最初の二行では、"Good bye" の先頭アドレスから最初の文字が 'F' に書き換えられることになります。さらに、printf には "Good bye" の先頭アドレスが渡され、そこからの文字列が表示されることになりますが、そのアドレスのデータは既に書き換えられているので、書き換え後の "Food bye" が表示されることになります。

文字列リテラルが変更されてしまう度プログラムが正常に動作しないことを示す図

"Good bye" を表示してほしいとソースコードを書いているのに "Food bye" と表示されてしまったら明らかにプログラムの動作としておかしいです。

そういうことがないように、文字列リテラルの変更は禁止されています。

なるほど…

つまり、このページの最初に見せた “動作が異常になる例” の処理を実行しちゃいけないのは、

文字列リテラルを変更しようとしているからだね!

だとしたら、”正常に動作する例” の処理も文字列リテラルを変更しているように見えるけど、

こっちが問題ないのはなんでなの?

“正常に動作する例” の場合、文字列リテラル自体は変更してないからだよ

この点について解説していくね!

ここまでの解説の通り、文字列リテラルは変更してはダメです。

ですが、下記も文字列リテラルを変更しているように見えますが、こちらは問題ありません。これは何故でしょう?

文字列リテラルの変更??
char str[] = "Good bye";
str[5] = 'B';

C言語やポインタに慣れてない人には分かりにくいかもしれませんが、実は上記では文字列リテラル自体の変更は行っていません。

まず1行目でどのような処理が行われるのかについて考えてみましょう!

C言語では、変数宣言を行うと、必要なサイズ分のメモリがその変数用にメモリ上に確保されます。その確保されたメモリは、const 指定を行なっていない限りはプログラム内で自由自在に変更することができます。

上記の場合は、char 型のサイズ *  ("Good bye" の文字数 + 1) 分のメモリが確保されます(+1 は文字列の終端を表すヌル文字用)。なので、要は char str[9]; と変数宣言しているのと同様にメモリが確保されます。

さらに、上記の1行目の場合は初期化を行なっていますので、右辺の文字列リテラルが  str にコピーされることになります。つまり、配列 str には下の図のように文字が格納されることになります(詳しくいうと、文字列リテラルと変数は異なるメモリ領域に配置されることになりますが、簡単のためその区別はせずに図を書いてます)。

配列に文字列リテラルをコピーする様子を示す図

ここでポイントなのは、変数宣言によって確保されたメモリに対して文字列リテラルがコピーされている点です。さらに、そのメモリは前述の通りプログラム内で自由自在に変更可能です。

つまり2行目の代入は、その自由自在に変更可能なメモリに対して実行されることになります。ですので、二行目の代入を実行しても文字列リテラルは一切変更されることはありません。

文字列リテラルが変更されない様子を示す図

文字列リテラルが変更されないのですから、上記の処理は全く問題なく動作することになります。

つまり、下記の1行目ではポインタ変数の宣言でアドレスを格納するためのメモリが確保され、そのメモリに右辺の文字列リテラル "Good bye" の先頭アドレスが格納されるだけです。文字列をコピーするためのメモリは確保されません。

動作が異常になる例
char *str = "Good bye";
str[5] = 'B';

str は文字列リテラルを指していますので、2行目の代入では、strの指す文字列リテラルの第 5 文字を直接変更することになります。

一方で、下記では前述の通り、右辺の文字列リテラル "Good bye" をコピーするための配列(メモリ)が確保され、初期化によりその配列に "Good bye" がコピーされることになります。

正常に動作する例
char str[] = "Good bye";
str[5] = 'B';

ですので、2行目の代入では、文字列リテラルではなく、その確保されたメモリ(配列 str)の第 5 文字が変更されることになります。

割と同じような処理に見えますが、動作としては全く異なるので注意してください。

スポンサーリンク

文字列リテラルを安全に利用する

ここまで解説してきたように、文字列リテラルはポインタにより変更することができてしまいます(文字列リテラルに直接間接演算子や添字演算子を用いて変更することもできますが、まずやらない処理なのでここでは考慮しないものとします)。

前述の通り、文字列リテラルを変更するとプログラムの動作が異常になるため、安全に文字列リテラルを使用するためには、文字列リテラルを変更しないようにプログラミングする必要があります。

そのためのポイントを、ここから解説していきたいと思います。

変更する可能性がある場合はポインタで指さない

まず、ポインタの指す先の文字列を変更する可能性がある場合、そもそもそのポインタでは文字列リテラルを指さないようにするべきです。

要は、ポインタで指すのではなく、配列に文字列を格納するようにします。

例えば下記では、ポインタの指す先の文字列を変更することになるので、そもそもこのポインタで文字列リテラルを指しているのが良くないです。

ポインタで指した文字列リテラルを変更する
char *str;
str = "Good bye";
str[5] = 'B';

ですので、下記のように配列を用意し、その配列に strcpy 等で文字列リテラルをコピーするようにしましょう。

配列にコピー後に変更する
char str[256];
strcpy(str, "Good bye");
str[5] = 'B';

前述の通り、変数宣言した配列は自由自在に変更可能ですので、中に文字列リテラルをコピーした文字列が格納されていようがどれだけ変更してもオーケーです。

変更する可能性がない場合は const 指定する

変更する可能性がない場合は、ポインタで文字列リテラルを指してしまっても問題ないです。

ですが、より安全に文字列リテラルを扱うためには、ポインタの変数宣言時に const 指定をした方が良いです。

const 指定されたポインタにおいては、そのポインタの指すデータ(この場合は文字列リテラル)を変更しようとした場合、コンパイルエラーが発生するようになります。

ポインタ変数へのconst指定
const char *str = "Good bye";
str[5] = 'B'; /* ここでコンパイルエラー */

下記は私の環境で発生したコンパイルエラーの文言です。

error: read-only variable is not assignable

ですので、間違って文字列リテラルを変更するような処理を記述した場合、コンパイルエラーにより、すぐにその変更を検知することができます。

プログラムが異常終了する際は、その異常終了が発生することで、ある程度は検知することはできるかもしれませんが、環境によっては異常終了せずに文字列リテラルの変更に気づかない可能性もあります。ですので、const 指定した方がより確実に文字列リテラルの変更を検知することができます。

const 指定については下記ページで詳しく解説していますので、使い方や効果などを詳しく履帯方は是非読んでみてください。

constの解説ページアイキャッチ 【C言語】constの使い方・メリットを解説

スポンサーリンク

まとめ

このページでは、C言語における “文字列リテラル” の扱いについて解説しました!

普段何気なく使っている文字列リテラルですが、ポインタで指すことができ、さらにポインタから文字列リテラルを変更しようとするとプログラムが異常終了するので、実は扱いには注意が必要です。

ただし、今まで通り、文字列リテラルは何気なく使用するので良いです。便利なのでたくさん使って良いです。ただ、是非今回解説した内容は頭の片隅にでも置いておくようにしてください。

“文字列リテラルを変更するとプログラムが異常終了してしまう” ということを覚えておくだけで、実際にプログラムが異常終了した場合に、どの処理を疑えば良いかどうかのヒントになると思います。

特に文字列リテラルをポインタで指すような処理があった場合、そのポインタに const を付加してみることで、コンパイル時のエラーや警告から、文字列リテラルが変更されていないかどうかをすぐに確認することができると思いますので、是非 const も活用してみてください!

オススメの参考書(PR)

C言語学習中だけど分からないことが多くて挫折しそう...という方には、下記の「スッキリわかるC言語入門」がオススメです!

まず学習を進める上で、参考書は2冊持っておくことをオススメします。この理由は下記の2つです。

  • 参考書によって、解説の仕方は異なる
  • 読み手によって、理解しやすい解説の仕方は異なる

ある人の説明聞いても理解できなかったけど、他の人からちょっと違った観点での説明を聞いて「あー、そういうことね!」って簡単に理解できた経験をお持ちの方も多いのではないでしょうか?

それと同じで、1冊の参考書を読んで理解できない事も、他の参考書とは異なる内容の解説を読むことで理解できる可能性があります。

なので、参考書は2冊持っておいた方が学習時に挫折しにくいというのが私の考えです。

特に上記の「スッキリわかるC言語入門」は、他の参考書とは違った切り口での解説が豊富で、他の参考書で理解できなかった内容に対して違った観点での解説を読むことができ、オススメです。題名の通り「なぜそうなるのか?」がスッキリ理解できるような解説内容にもなっており、C言語入門書としてもかなり分かりやすい参考書だと思います。

もちろんネット等でも色んな観点からの解説を読むことが出来ますので、分からない点は別の人・別の参考書の解説を読んで解決していきましょう!もちろん私のサイトも参考にしていただけると嬉しいです!

入門用のオススメ参考書は下記ページでも紹介していますので、こちらも是非参考にしていただければと思います。

https://daeudaeu.com/c_reference_book/

同じカテゴリのページ一覧を表示