文字列とポインタ

見た目が同じで中身が違う 2 つ

入門では、文字列は「\0 で終わる char の配列」だと学びました。ここにポインタが加わると、よく似た 2 つの書き方が出てきます。

char name[] = "Tanaka"; /* 1 */ const char *label = "Tanaka"; /* 2 */

どちらも printf("%s", ...) で同じ表示になります。しかし中身は別物です。

1 は、"Tanaka" の 7 バイト分(末尾の \0 を含みます)を 写し取った配列 です。name はその配列の実体で、書き換えられます。

2 は、プログラムのどこかに 1 つだけ置かれている "Tanaka" という 文字列リテラルの番地 を label に入れただけです。実体は増えていません。label はただの矢印です。

リテラルは書き換えてはいけない

この違いが最初に効くのが書き換えです。

name[0] = 'K'; /* 配列の写しなので問題なく通る */ label[0] = 'K'; /* 文字列リテラルの書き換え。やってはいけない */

文字列リテラルは、多くの環境で読み取り専用の領域に置かれます。そこへ書き込むとプログラムは異常終了します。しかもコンパイル時には止まらず、実行してはじめて落ちるので、原因を探しにくい部類のバグです。

だから、文字列リテラルを指すポインタには const を付けます。const char *label と書いておけば、label[0] = 'K'; はコンパイルの時点でエラーになります。実行時に落ちるバグを、コンパイル時のエラーに前倒しできるわけです。

進めれば途中から見える

文字列も char の並びなので、ポインタ演算がそのまま効きます。

const char *label = "score"; printf("%s\n", label + 2); /* ore */

label + 2 は 3 文字目を指す番地です。%s はそこから \0 まで表示するので、途中から表示されます。文字列を切り出すのに新しい領域を作る必要はなく、どこを指すかを変えるだけ で済む、というのが C の文字列の考え方です。次章で自作する strlen や strcpy も、この発想の上に建っています。

どこまでが文字列か

%s が止まる場所は \0 だけです。長さを別に持っているわけではありません。だから \0 を消してしまうと、%s はメモリの続きを読み続けて、たまたま \0 に当たるまで止まりません。配列の大きさを超えて読むので、何が出るかは分かりませんし、落ちることもあります。

配列で受けるときは、終端の \0 の分も数えて大きさを決めます。char name[32] に入れられる文字は 31 文字までです。scanf%31s のように上限を書くのは、この 1 バイトを守るためです。

では、配列で受けた名前とリテラルを指すポインタを、並べて動かしてみましょう。

要件

  1. 名前は char name[32] に %31s で読み込む
  2. const char *label には "score" を持たせる
  3. 名前の先頭 1 文字を *name で取り出して表示する
  4. label + 2 を使って 3 文字目から先を表示する
  5. name の先頭を * で書き換えて伏せ字にし、書き換えた結果を表示する
  6. 出力は「名前はTanakaです」「先頭の文字はTです」「ラベルはscoreです」「ラベルの3文字目からはoreです」「伏せ字にすると*anakaです」の形にする

入出力例

main("Tanaka") → "名前はTanakaです 先頭の文字はTです ラベルはscoreです ラベルの3文字目からはoreです 伏せ字にすると*anakaです" main("Suzuki") → "名前はSuzukiです 先頭の文字はSです ラベルはscoreです ラベルの3文字目からはoreです 伏せ字にすると*uzukiです" main("Yamada") → "名前はYamadaです 先頭の文字はYです ラベルはscoreです ラベルの3文字目からはoreです 伏せ字にすると*amadaです"

ヒント

生田 陸人
ゆめさくエンジニア / 現役ソフトウェアエンジニア
編集 LuaGate編集部
main.c
学習モード

メモ

文字列とポインタ

⌘S で保存