C言語のきほん|ポインタで文字列リテラルを指す

文字列リテラルはそのまま指せる。でも、書き換えないという約束がとても大切。

これまで、ポインタで変数を指したり、配列を指したり、文字列を1文字ずつ操作したりしてきました。
その流れで考えると、次に気になってくるのが、文字列リテラルそのものをポインタで指す書き方です。

たとえば、これまでは次のように char 型配列を用意して、そこに文字列リテラルで初期化していました。

char text[] = "Hello";

この場合、text は配列なので、中身の文字を書き換えることができます。
前の項では、このような配列に対してポインタを使い、1文字ずつたどって文字を変換する方法を見ました。

一方で、C言語ではわざわざ配列を用意しなくても、文字列リテラルの先頭を直接ポインタで指すことができます。

const char *p = "Hello";

この書き方はとても便利です。
短い固定メッセージを扱うときや、読み取り専用の文字列を順番に表示したいときなどに、すっきり書けます。

ただし、ここでとても大切なのが、文字列リテラルは基本的に書き換えてはいけないという点です。
見た目は文字の並びなので、つい配列と同じ感覚で扱いたくなりますが、文字列リテラルは配列とは少し立場が違います。

この違いをあいまいにしたまま進むと、

  • 配列の文字列は書き換えられる
  • 文字列リテラルは読み取り専用として扱う

という重要な区別が見えにくくなってしまいます。

そこでここでは、ポインタで文字列リテラルを指すというテーマについて、

  • 文字列リテラルを直接ポインタに代入するとはどういうことか
  • なぜ const を付けるのか
  • 文字列配列との違いは何か
  • どういう場面で便利なのか

を、表や図も交えながら、やわらかく丁寧に整理していきます。

文字列リテラルをポインタで指すとは

まず、文字列リテラルとは、ソースコードの中に直接書かれた文字列のことです。
たとえば Hello や ABC や おはよう など、ダブルクォーテーションで囲まれたものが文字列リテラルです。

たとえば、次のような書き方があります。

const char *p = "Hello";
const char *p = "Hello";

この文では、文字列リテラル Hello の先頭文字 H のアドレスがポインタ p に代入されています。
つまり、p は文字列リテラルの先頭を指している状態です。

このとき、メモリ上のイメージとしては次のように考えられます。

要素中身
p文字列リテラルの先頭アドレス
*p'H'
*(p + 1)'e'
*(p + 2)'l'
*(p + 3)'l'
*(p + 4)'o'
*(p + 5)'\0'

つまり、配列をポインタでたどったときと同じように、文字列リテラルも先頭から1文字ずつ順番に読んでいけます。

ここだけ見ると、配列とほとんど同じように見えるかもしれません。
でも、書き換えられるかどうかという大事な違いがあります。

配列の文字列と文字列リテラルの違い

ここはとても大事なポイントです。
見た目が似ているので混同しやすいのですが、次の2つは意味が違います。

char str[] = "Hello";

const char *p = "Hello";

この違いを整理すると、次のようになります。

書き方何を作っているか書き換え
char str[] = "Hello";配列を作り、その中に文字をコピーするできる
const char *p = "Hello";文字列リテラルの先頭をポインタで指すしない、できないものとして扱う

最初の書き方では、配列 str が作られ、その中に H、e、l、l、o、\0 が入ります。
これは自分で用意した配列なので、str[0] = 'Y'; のような書き換えができます。

一方、2つ目の書き方では、自分で文字配列を作っているわけではありません。
ソースコード中の文字列リテラルを、そのままポインタで指しているだけです。
この文字列リテラルは、読み取り専用として扱うのが基本です。

なぜ const を付けるのか

文字列リテラルを指すときに、よく次のように書きます。

const char *p = "Hello";

ここで付いている const は、
p が指している先の文字は変更しません
という意味を表しています。

この const はとても大切です。
なぜなら、文字列リテラルは通常、書き換えてはいけないものだからです。

もし const を付けずに書くと、見た目としては変更できそうに見えてしまいます。
でも実際には、文字列リテラルを書き換えようとすると、処理系によっては異常終了したり、未定義動作になったりします。

そのため、

const char *p = "Hello";

のように書いて、読み取り専用であることをはっきり示すのが安全です。

表で整理すると、次のようになります。

書き方意味
char *p = "Hello";見た目として変更できそうに見えるが危険
const char *p = "Hello";指している文字列は変更しないと明示する

つまり const は、ただのお飾りではなく、
この文字列は読むだけです
という大切な約束なのです。

図:ポインタが文字列リテラルの先頭を指す

この図では、文字列リテラル Hello の各文字が横一列に並び、最後に \0 が付いています。
ポインタ p は先頭文字 H を指しており、そこから1文字ずつ順番に読み進めていけます。

この図で見てほしいのは、ポインタが配列を持っているわけではなく、すでに存在する文字列リテラルの先頭を指しているということです。
そして、この文字列は読み取り専用として扱う、という点も大切です。

サンプルプログラム

元のサンプルでは Hello を1文字ずつ表示していましたが、ここでは別のシンプルな例に変更してみましょう。
表示するメッセージも変え、コメントも日本語にします。

今回は、文字列リテラル ありがとう を1文字ずつではなく、英字の例でわかりやすく、
Sky
という文字列を1文字ずつ表示するプログラムにしてみます。

ファイル名:11_7_1.c

#include <stdio.h>

int main(void)
{
    const char *p = "Sky";   /* 文字列リテラルの先頭を指すポインタ */

    printf("文字を1つずつ表示します\n");

    while (*p != '\0') {     /* ナル文字に到達するまで繰り返す */
        printf("%c\n", *p);  /* 今ポインタが指している文字を表示する */
        p++;                 /* 次の文字へ進む */
    }

    return 0;
}

このプログラムで起きていること

このプログラムでは、まず次の1行がとても大事です。

const char *p = "Sky";

ここで、文字列リテラル Sky の先頭文字 S のアドレスがポインタ p に入ります。
つまり、最初の時点で p は S を指しています。

そのあと、

while (*p != '\0')

によって、今 p が指している文字が \0 でない限りループを続けます。

最初のループでは、

  • p は 'S' を指している
  • *p は 'S'
  • S を表示する
  • p++ で次の文字へ進む

次のループでは、

  • p は 'k' を指している
  • *p は 'k'
  • k を表示する
  • p++ で次へ進む

さらにその次は y を表示し、そのあと \0 に到達したらループを終えます。

流れを表にすると、次のようになります。

ループ回数p が指す文字表示される文字
1回目'S'S
2回目'k'k
3回目'y'y
4回目'\0'終了

このように、ポインタを1つずつ進めながら文字列リテラルを読むことができます。

実行結果

このプログラムの実行結果は、次のようになります。

文字を1つずつ表示します
S
k
y

文字列全体をまとめて表示するのではなく、ポインタで1文字ずつ取り出して表示しているところがポイントです。

文字列リテラルはなぜ書き換えてはいけないのか

ここは、学習の初期でとても大切な考え方です。

文字列リテラルは、ソースコード中に直接書かれた固定の文字列です。
多くの処理系では、これを読み取り専用の領域に置いたり、書き換えない前提で扱ったりします。

そのため、たとえば次のようなことはしてはいけません。

char *p = "Sky";
*p = 'A';

見た目には、p が指す先の文字を書き換えているように見えます。
でも、これは非常に危険です。
処理系によっては実行時エラーになったり、予測不能な動作になったりします。

だからこそ、文字列リテラルを指すときは const を付けて、

const char *p = "Sky";

と書くのです。

こうしておけば、もし誤って

*p = 'A';

のようなコードを書いたときに、コンパイラがエラーや警告で気づかせてくれます。
つまり const は、うっかり書き換えを防ぐ安全装置としても役立ちます。

図:文字列リテラルは読み取り専用として扱う

この図では、文字列リテラル ABC の各文字が並んでおり、ポインタ p が先頭を指しています。
その横に、読み取りはできるが書き換えはしない、という注意ラベルを配置します。

この図のポイントは、配列の文字列とは違って、文字列リテラルは参照のために使い、変更の対象にはしないということです。
ここをはっきり区別できると、配列と文字列リテラルの違いがよく見えるようになります。

配列の文字列と比べてみよう

ここで、配列として用意した文字列と、文字列リテラルを指すポインタを比べておくと理解しやすいです。

配列として用意する場合

char word[] = "Sky";

この場合は、word という配列が作られ、その中に S、k、y、\0 がコピーされます。
そのため、次のような変更ができます。

word[0] = 'A';

文字列リテラルを直接指す場合

const char *p = "Sky";

この場合は、文字列リテラルの先頭を p が指しているだけです。
そのため、文字の変更はしません。

この違いを表で整理すると、次のようになります。

比較項目char 配列const char *
実体自分で用意した配列文字列リテラルを指す
文字の変更できるしない
主な用途編集したい文字列読み取り専用の文字列

この違いが見えると、どちらの書き方を選ぶべきか判断しやすくなります。

ポインタを進めて読む流れは同じ

配列か文字列リテラルかの違いはありますが、
ポインタを進めながら1文字ずつ読む
という流れ自体は同じです。

たとえば、

while (*p != '\0') {
    printf("%c\n", *p);
    p++;
}

という形は、文字列処理の基本形です。

ここでやっていることは、次の通りです。

  1. *p で今の文字を見る
  2. 必要なら表示や判定をする
  3. p++ で次の文字へ進む
  4. \0 に着いたら終わる

この基本形をしっかり押さえておくと、
文字数を数える
特定の文字を探す
文字列を比較する
といった処理の理解にもつながっていきます。

図:ポインタを進めながら文字列リテラルを読む

この図では、文字列リテラル Sun の各文字が横に並び、ポインタ p が S から u、n、\0 へと順番に進んでいく様子を表します。
各位置で *p によって今の文字を読み取り、表示したあと p++ で次へ進む流れを示します。

この図で見てほしいのは、文字列リテラルは書き換えないものの、順番に読んでいく処理にはとても自然に使えるという点です。

const char * の意味をもう少し整理する

const char *p という書き方は、最初は少し読みにくく感じるかもしれません。
でも意味を分けるとわかりやすいです。

  • char
    → 文字を表す型
  • *p
    → p はその文字を指すポインタ
  • const
    → 指している先の文字を変更しない

つまり、

p は char 型の文字を指すポインタであり、その指している文字は変更しない

という意味になります。

表にすると、次のように整理できます。

宣言意味
char *pchar 型の文字を指すポインタ
const char *p読み取り専用の char 型文字列を指すポインタ

学習のこの段階では、
文字列リテラルを指すなら const char * を使う
と覚えておくと、とてもわかりやすいです。

どんなときに便利か

文字列リテラルを直接ポインタで指す書き方は、次のような場面で便利です。

  • 固定のメッセージを扱いたいとき
  • 文字列を読むだけで、変更する必要がないとき
  • 簡潔にポインタによる文字列走査を書きたいとき

たとえば、メニュー表示やメッセージ表示、定型文の扱いなどでは、
わざわざ char 配列を作らずに const char * を使うと、意図がはっきりします。

一方で、文字を変換したり書き換えたりしたいなら、char 配列として用意するほうが適しています。
つまり、

  • 読むだけなら const char *
  • 書き換えるなら char 配列

という使い分けが大切です。

ポインタで文字列リテラルを指す感覚をつかもう

ここまでの内容で、まず押さえたいポイントは次の通りです。

覚えたいこと内容
文字列リテラルは直接ポインタで指せるconst char *p = "Hello"; の形で書ける
ポインタは先頭文字を指す*p で今の文字を読める
p++ で次の文字へ進める1文字ずつ順番に読める
\0 で終わりを判断する文字列処理の基本
文字列リテラルは書き換えないconst を付けて明示する

この感覚がつかめると、文字列リテラルはただの固定メッセージではなく、
先頭から順番に読み取れる文字の並びとして見えてきます。

そして同時に、配列として用意した文字列との違いも見えてきます。
同じようにポインタでたどれても、書き換えてよいものと、読み取り専用で扱うものは違うのです。

この区別ができるようになると、C言語の文字列の扱いがずっとすっきり理解できるようになります。