C言語の基本|文字列リテラルをポインタで扱う

文字列リテラルはポインタで読める。だからこそ、書き換えないという約束を守ろう。

これまで、ポインタを使って変数や配列を指し、メモリ上に並んだ要素を順番にたどる方法を学んできました。文字列についても、char型配列の先頭へポインタを合わせ、\0まで一文字ずつ処理できます。

ここで新しく確認したいのが、ソースコードへ直接書いた文字列リテラルをポインタで指す方法です。

const char *p = "Hello";

この書き方では、pが文字列リテラルHelloの先頭文字を指します。pを一文字ずつ進めれば、配列をたどるときと同じように、H、e、l、l、oの順番で文字を読み取れます。

ただし、文字列リテラルには重要な約束があります。文字列リテラルは読み取り専用として扱い、内容を書き換えてはいけません。同じ文字列に見えても、char型配列として用意した文字列と、文字列リテラルを指すポインタでは、変更できるかどうかが異なります。

この記事では、次の内容を順番に見ていきます。

  • 文字列リテラルをポインタで指す仕組み
  • const char *で宣言する理由
  • char型配列との違い
  • p++で文字列リテラルを読み進める方法
  • 読み取り専用の文字列が向いている場面

文字列リテラルを安全に扱うには、ポインタで読めることと、指している文字は変更しないことをセットで理解するのが大切です。

文字列リテラルとは

文字列リテラルとは、ソースコードの中にダブルクォーテーションで囲んで直接記述した文字列です。

"Hello"
"C Language"
"System Ready"

見た目は一つの文字列ですが、内部では複数の文字が順番に並び、最後に終端を表す\0が置かれています。たとえば文字列リテラルCODEは、次の要素で構成されます。

位置中身役割
0'C'1文字目
1'O'2文字目
2'D'3文字目
3'E'4文字目
4'\0'文字列の終端

画面に表示される文字数は4文字ですが、終端の\0を含むため、メモリ上では5つのchar型要素が並んでいます。

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

文字列リテラルの先頭は、次のようにconst char *型のポインタで指せます。

const char *p = "CODE";

この宣言によって新しい編集用のchar型配列が作られるわけではありません。pには、文字列リテラルCODEの先頭文字'C'を指すポインタが代入されます。

ポインタ式と文字の対応は、次のようになります。

ポインタ式読み取れる文字
*p'C'
*(p + 1)'O'
*(p + 2)'D'
*(p + 3)'E'
*(p + 4)'\0'

pが先頭を指しているため、*pで現在の文字を読み取れます。p + 1は一文字先、p + 2は二文字先を指します。さらにp++を実行すれば、p自身を次の文字へ進められます。

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

この図から分かること

文字列リテラルCODEには、C、O、D、E、\0が順番に並んでいます。pが保存するのは文字列全体ではなく、先頭文字'C'の場所です。*pで現在の文字を読み、p++で次へ進めます。ただし、この並びは読み取り専用として扱い、*pへの代入で変更してはいけません。

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

次の2つは、どちらもHelloという文字列を扱いますが、作られるものと変更の可否が異なります。

char text[] = "Hello";
const char *p = "Hello";

char text[] = "Hello";では、textというchar型配列が確保され、その配列へH、e、l、l、o、\0が格納されます。textは自分で用意した配列なので、各要素を書き換えられます。

text[0] = 'Y';

この代入後、textの内容はYelloになります。

一方、const char *p = "Hello";では、pが文字列リテラルの先頭を指します。編集用の配列を作って文字をコピーしているわけではありません。指している文字列は読み取り専用として扱います。

比較項目char text[] = "Hello";const char *p = "Hello";
用意されるもの文字を格納するchar型配列文字列リテラルを指すポインタ
先頭の参照text[0]または*text*p
文字の変更できるしない
ポインタによる走査できるできる
主な用途編集する文字列読むだけの固定文字列

どちらも先頭から\0まで順番に読めます。大きな違いは、指している文字を書き換えてよいかどうかです。

なぜconstを付けるのか

文字列リテラルを指すときは、次の形を使います。

const char *p = "Hello";

constは、pが指している先のchar型データを、このポインタを通して変更しないことを示します。つまり、const char *pは、読み取り専用の文字を指すポインタとして扱えます。

宣言意味
char *pchar型データを指すポインタ
const char *p変更しないchar型データを指すポインタ

constが付いているため、次のような書き換えをしようとすると、コンパイラが問題を検出できます。

const char *p = "Hello";
*p = 'Y';   /* constで保護された文字は変更できない */

このコードは、指している先を書き換えようとしているため、コンパイルエラーや診断の対象になります。constは、文字列リテラルを誤って変更するコードを早い段階で見つけるための安全装置になります。

ここで、constが付いていてもp++は実行できます。

p++;

constで変更できないのはpが指す先の文字です。p自身が保存する位置は変更できるため、次の文字へ移動しながら読み進められます。

操作実行できるか理由
printf("%c", *p);できる指している文字を読む処理だから
p++;できるp自身を次の位置へ進める処理だから
*p = 'X';できない指している文字を変更する処理だから

constを付けずに文字列リテラルを指す危険性

C言語では、次のようなコードを見かけることがあります。

char *p = "Hello";

処理系によってはこの宣言を受け付けることがありますが、文字列リテラルの内容を書き換えてよいという意味にはなりません。

char *p = "Hello";
*p = 'Y';   /* 実行してはいけない */

C言語では文字列リテラルを書き換えようとすると未定義動作になる

文字列リテラルを変更しようとすると、C言語では未定義動作になります。実行時に異常終了する場合もあれば、一見動いたように見える場合もあり、結果を予測できません。

なぜ、文字列リテラルを書き換えるとダメなのか、それは文字列リテラルは、実行時に書き換えられないメモリ領域へ配置されることがあるからです。問題なのは、ポインタだから書き換えられないのではなく、「ポインタの指している先が文字列リテラルだから書き換えてはいけない」という点です。

そのため、文字列リテラルを指す場合は、最初からconst char *を使って変更しない意図を明確にします。

const char *p = "Hello";

図2:char型配列と文字列リテラルの違い

この図から分かること

char型配列wordには文字列の各文字が格納されるため、word[0]のように配列要素を変更できます。const char *pは文字列リテラルの先頭を指しますが、指している文字は変更しません。同じSkyという表示でも、編集用の配列か、読み取り専用として扱う文字列リテラルかによって役割が異なります。

ポインタで文字列リテラルを読み進める

文字列リテラルは変更できませんが、先頭から順番に読み取ることはできます。基本となるループは次のとおりです。

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

このループでは、次の処理を繰り返します。

  1. *pで現在の文字を読み取る
  2. printfで現在の文字を表示する
  3. p++で次の文字へ進む
  4. *pが\0になったら終了する

配列をポインタでたどるときと同じ流れですが、文字列リテラルでは参照と移動だけを行い、*pへの代入は行いません。

文字列リテラルを一文字ずつ表示する

次のプログラムでは、文字列リテラルWaveの先頭をconst char *型のポインタで指し、一文字ずつ改行して表示します。

固定文字列を先頭から一文字ずつ表示するプログラム

ファイル名:11_7_1.c

#include <stdio.h>

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

    printf("固定文字列を一文字ずつ表示します\n");

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

    return 0;
}
実行結果の例
固定文字列を一文字ずつ表示します
W
a
v
e

最初に注目したいのは、次の宣言です。

const char *p = "Wave";

pには、文字列リテラルWaveの先頭文字'W'を指すポインタが入ります。constが付いているため、pを通して文字を書き換えないことも宣言から分かります。

while文の条件では、現在の文字が\0ではないことを確認します。

while (*p != '\0')

ループの中では、%cを使って*pを一文字として表示します。その後、p++で次のchar型要素へ進みます。

ポインタの動きを表にすると、次のようになります。

ループpが指す位置*p実行する処理
1回目先頭'W'Wを表示して次へ進む
2回目1文字先'a'aを表示して次へ進む
3回目2文字先'v'vを表示して次へ進む
4回目3文字先'e'eを表示して次へ進む
終了時4文字先'\0'while文を終了する

このプログラムでは、pが文字列リテラルの先頭から終端まで移動します。各位置の文字を読むだけなので、constの約束を守った安全な処理になっています。

pと*pの役割を区別する

文字列リテラルをたどるときは、pと*pの違いを意識すると理解しやすくなります。

書き方意味
p現在の文字が置かれている場所
*p現在の場所にある文字
p++pを次の文字の場所へ進める
*p != '\0'現在の文字が終端ではないか調べる

たとえば、最初のpは'W'の場所を指し、*pは文字'W'を表します。p++の後はpが'a'の場所を指し、*pは'a'になります。

このように、pは場所、*pはその場所にある文字として読むと、ループ内の動きを追いやすくなります。

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

この図から分かること

pは最初に'W'を指し、*pで文字を読み取ったあと、p++で'a'へ進みます。同じ流れでv、eまで表示し、\0に到達すると処理を終了します。p自身は移動できますが、指している文字列リテラルの内容は一度も変更していません。

読む処理と書き換える処理を使い分ける

文字列の用途に応じて、const char *とchar型配列を使い分けます。

やりたいこと適した形理由
固定メッセージを表示するconst char *p = "Ready";文字を変更する必要がないため
文字列を一文字ずつ読むconst char *p = "Wave";p++で安全に走査できるため
先頭文字を変更するchar text[] = "Wave";配列要素を書き換えられるため
大文字・小文字を変換するchar text[] = "Wave";各文字を変更する必要があるため

固定の案内文、メニュー名、状態表示など、内容を変えずに読むだけならconst char *が適しています。宣言を見るだけで、この文字列は変更しないという意図も伝わります。

一方、文字の置換や変換が必要なら、char型配列を用意します。

char text[] = "Wave";
text[0] = 'C';

この場合、配列textの内容はCaveになります。

覚え方としては、次の2つに分けると分かりやすいです。

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

文字列リテラルを扱うときの注意点

文字列リテラルをポインタで扱うときは、次の点を確認しましょう。

指している文字を書き換えない

文字列リテラルは読み取り専用として扱います。const char *を使い、*pへの代入を行わないようにします。

const char *p = "Ready";

\0を越えて読まない

文字列リテラルにも最後に\0があります。p++で読み進めるときは、\0を終了条件にします。

while (*p != '\0')

\0の先を文字列の一部として読み続けてはいけません。

p++を忘れない

while文の中でp++を実行しないと、pは同じ文字を指し続けます。条件が変わらないため、無限ループになる可能性があります。

変更が必要ならchar型配列を用意する

読み始めたあとで文字の変更が必要になった場合は、文字列リテラルを無理に書き換えるのではなく、最初からchar型配列として用意します。

char message[] = "Ready";
message[0] = 'S';

このようにすれば、変更可能な配列の範囲内で安全に文字を書き換えられます。

const char *の読み方を整理する

const char *pは、次のように分けて考えると理解しやすくなります。

部分意味
char指している要素はchar型
*ppはchar型要素を指すポインタ
constpを通して指している要素を変更しない

つまり、const char *pは、変更しないchar型データを指すポインタです。

文字列リテラルへ使う場合は、次の形を基本として覚えておきましょう。

const char *p = "固定文字列";

pを進めながら文字を読むことはできますが、*pを使った書き換えは行いません。

文字列リテラルを安全に扱うために覚えておきたいこと

覚えておきたいこと内容
文字列リテラルソースコードへ直接書いた固定文字列
const char *p = "Hello";pが文字列リテラルの先頭を指す
*p現在指している文字を読み取る
p++p自身を次の文字へ進める
\0文字列の終端を表す
const指している文字を変更しないことを明示する
char型配列文字を編集したい場合に使用する

文字列リテラルとchar型配列は、どちらもポインタで先頭から\0までたどれます。しかし、文字列リテラルは読み取り専用として扱い、char型配列は必要に応じて編集できるという違いがあります。

ポインタで文字列リテラルを扱うときは、pで現在の場所を管理し、*pで文字を読み、p++で次へ進む流れを意識してみてください。そして、文字を変更しないという意図をconstで明確にすることが、安全で読みやすいコードにつながります。