文字列とポインタ
見た目が同じで中身が違う 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 バイトを守るためです。
では、配列で受けた名前とリテラルを指すポインタを、並べて動かしてみましょう。
要件
- 名前は char name[32] に %31s で読み込む
- const char *label には "score" を持たせる
- 名前の先頭 1 文字を *name で取り出して表示する
- label + 2 を使って 3 文字目から先を表示する
- name の先頭を * で書き換えて伏せ字にし、書き換えた結果を表示する
- 出力は「名前は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です"