第23問の答え

答え

文字列リテラルを指すポインタの配列なので、こう書くのが正解!
  const char *names[] = {
    "Leo",
    "Yuki"
  };

解説

これは、「ポインタの配列」についての問題でした。一つ一つの要素がポインタなので、それぞれが別々のデータを指すことができます。

あれ?今回のプログラムは「名前の一覧」を格納してるから、文字列の配列になるんじゃないんですか?
意味合いとしては、そのとおりよ。でも、文字列の長さはみんな違うから、実装上は……
……あ、そっか。分かったかも!
いいわね。それじゃ続きをどうぞ。
はい。ポインタなら長さが同じになるから、文字列を指すポインタを配列にしてるんですね!
そういうこと!

配列は、同じサイズの要素を隙間なく並べたデータ構造です。例えば、次のような整数の配列があったとしましょう。

  int numbers[] = {
    123,
    456
  };

ここで、123456はどちらもint型の値なので、サイズも同じです。そのため、1つの配列にまとめることができています。このことは、次のように各要素を間接的に書き直してみれば明確になるでしょう。

  int number0 = 123;
  int number1 = 456;

  int numbers[] = {
    number0,
    number1
  };

さて、問題の穴埋め部分は次のような配列でした。

  /* ここに入るコードは? */ = {
    "Leo",
    "Yuki"
  };

一見すると「文字列の配列」のように見えますが、これが実際には「文字列を指すポインタの配列」なのです。このことが明確に分かるよう、各要素を間接的に書き直してみましょう。

  const char *name0 = "Leo";
  const char *name1 = "Yuki";

  /* ここに入るコードは? */ = {
    name0,
    name1
  };

"Leo""Yuki"は、文字列リテラルです(第05問でも出てきましたね)。実行時に書き換えられない文字列なので、これらを指すポインタの型はconst char *となります。

したがって、穴埋めの答えは次のようになります。

  const char *names[] = {
    "Leo",
    "Yuki"
  };
なるほど、これが「ポインタの配列」だというのは分かったんですけど……
質問がありそうね。
はい。問題のプログラムには「ポインタのポインタ」が出てきてましたよね。これって?
ああ、それはね……

問題のプログラムには、「ポインタの配列」を「ポインタのポインタ」として受け取る関数が出てきていました。

void GreetTo(int nameIndex, const char **ppNames) { /* ← ポインタのポインタとして受け取る */
  ……
}

int main(void) {
  const char *names[] = {
    ……
  };

  GreetTo(0, names); /* ← ポインタの配列を渡す */
  ……
}

C言語では、配列を引数にして関数を呼び出すと「配列の先頭要素のアドレス」が引き渡されます。この例では要素の型が「文字列を指すポインタ」なので、受け取る側は「文字列を指すポインタのポインタ」になるのです。

この動作は変数に代入するときも同じです。

  const char *names[] = {
    "Leo",
    "Yuki"
  };

  const char **ppNames = names; /* ← ポインタの配列を、ポインタのポインタに代入 */
  printf("Hello, %s!\n", ppNames[0]);
  printf("Hello, %s!\n", ppNames[1]);

少しややこしく感じるでしょうか?その場合は、整数の配列に戻って考えてみるとイメージがわいてくるかもしれません。

  int numbers[] = {
    123,
    456
  };

  int *pNumbers = numbers; /* ← 整数の配列を、整数のポインタに代入 */
  printf("Number = %d\n", pNumbers[0]);
  printf("Number = %d\n", pNumbers[1]);
なるほど〜。最初は難しく感じましたけど、こうやってシンプルに考えればなんとかなりそうです!
でしょ?まぁハッキリ言えば、const char *names[]っていう文法がややこしいだけなのよ。
あ、やっぱり。これ分かりづらいですよね。
もっと分かりやすくしたかったら、typedefを使うのも一つの方法ね。
例えば、文字列リテラルをこうやっておいて……
typedef const char *StringLiteral;
その配列をこんな感じにすれば分かりやすい!
  StringLiteral names[] = {
    "Leo",
    "Yuki"
  };
わ、ほんとだ分かりやすい!
でもね、「ポインタの配列」はよく出てくるデータ構造だから、文法に慣れておくことも大切よ。
はーい!
ここがポイント!
一見ややこしい「ポインタの配列」も、シンプルに考えればイメージしやすくなる!

ちなみに、main()関数でコマンドライン引数を受け取るときに使うargvも、今回の配列とよく似たデータ構造です。

int main(int argc, char *argv[]) {
  ……
}
コマンドライン引数は、第10問でも出てきたわね。
はい。でも、char *argv[]だと、引数なのに「ポインタのポインタ」になってないですけど……
この場合、char **argvって書くのと同じになるわ。
えええ?これと同じってこと?
int main(int argc, char **argv) {
  ……
}
これはね、関数の引数を書くときだけの特別ルールなのよ。
うわぁ、また混乱しそう。
ふふふ。でもね、char *argv[]と書けば「このポインタは配列を指すよ」っていう意図が伝わりやすくなるでしょ。
そっか、そのための特別ルールなんですね。

修正後のプログラム

main.c
#include <stdio.h>
#include <stdlib.h>

void GreetTo(int nameIndex, const char **ppNames) {
  printf("Hello, %s!\n", ppNames[nameIndex]);
}

int main(void) {
  const char *names[] = {
    "Leo",
    "Yuki"
  };

  GreetTo(0, names);
  GreetTo(1, names);

  return EXIT_SUCCESS;
}
実行結果
Hello, Leo!
Hello, Yuki!