記事一覧

あやしいfor文

デバッグしやすくするにはif文を整理して分岐を減らしておくといいって教えてもらいましたけど、ループを書くときもコツってあるんですか?
そうねぇ……まずはif文と同じで、「やりがちな間違い」について知っておくのがいいかしら。
ループにも「やりがちな間違い」があるんですか?
いろいろとね。例えば……

次のfor文では変数xを「10」で初期化して、それが「0」になるまで「-2」で割るという処理をしようとしています。

  for (int x=10; x=!0; x/=-2) {
    printf("%+d\n", x);
  }

もし、これが正しいプログラムなら次のように表示されるはずです。

期待される実行結果
+10
-5
+2
-1

ところが、実際に実行してみると「+1」が表示され続けてしまい、いつまでもループが終わりません。

実際の実行結果(バグ)
+1
+1
+1
……(ループが止まらない!)
どこかを書き間違えているせいなのだけど、分かるかしら?
ええーっと、ちょっと待ってくださいね……
ふふふ。シンキングタイムね。
……ん?あぁ、分かりました!x=!0のところが書き間違いですね。
そうそう。正しくは?
x!=0です!
正解!ちなみに、今のはfor文だったけれど、while文にも同じ間違いはありそうね。
例えばこう書いても、やっぱりループは止まらない。
  int x = 10;
  while (x =! 0) {
    printf("%+d\n", x);
    x /= -2;
  }
なるほど。同じ動きになりそうですね……って、あれれ?
どうかした?
これって、!==!と書き間違えたんだから、そもそも文法エラーで実行できないのでは?
あー、それはね……

念のため、なぜx=!0がコンパイルできてしまうのか確認しておきましょう。

C言語に=!という演算子はありません。でも=!はありますね。そのため、この式はx = !0と解釈されているのです。!は否定を表すので!0の部分は1と書くのと同じであり、式全体としてはx = 1と代入するのと同じ意味になります。

また、代入を式と見たときの計算結果は、代入した値そのものになります。この式の場合は、計算結果が常に1になるということです。その結果、いつまでもループが終わらなくなってしまっていたのでした。

……と、原因を突き詰めていくと、どうしても細かい話になるのよね。
うーん。本当の原因は、単純に書き間違えたことだと思うんですけど。
そのとおりね。だから大切なのは、「動作がおかしいぞ」って気付いたときに……
そっか。どこが間違っているのか考えてデバッグしやすいように、プログラムを整理しておくことが大切なんでしたね。
そういうこと!

for文の使いどころ

さっき、for文のところをwhile文で書いたらこう、っていう例を見せてくれましたよね。この2つは何が違うんですか?
うん、そこは疑問に思うところよね。それじゃあ、for文とwhile文の違いについて確認しておきましょうか。
お願いします!

C言語のfor文は、だいたい次のような形をしています。

for (ループ変数を初期化; ループ条件をチェック; ループ変数を更新) {
  ……
}

これは、while文の「ループ条件」のすぐ近くに、「初期化」と「状態の更新」も書けるようにしたような文法だといえます。これと同等の意味をもつwhile文は、次のように書けます。

ループ変数を初期化;
while (ループ条件をチェック) {
  ……
  ループ変数を更新;
}
実際のところ、while文がこういう形に収まることは多いんじゃないかしら。
はい。そんな気がします。
その場合は、for文に置き換えられるはずね。

whileでもforでも、どちらを使ってもプログラムを書けるという状況は意外によくあります。これは、プログラムが分かりやすくなりそうなほうを選べるということです。

基本的には、for文を使ったほうが分かりやすいケースが多いでしょう。なぜなら、ループ変数に関する処理をまとめておけるからです。これにより、次の2つを区別しやすくなります。

  • どういう条件でループするのか
  • ループ中で何をするのか

視覚的に書くと、ループをこういう構造にできるということですね。

for (どういう条件でループするのか) {
  ループ中で何をするのか;
}
逆のことをいえば、上の構造を意識せずに書いたfor文は分かりづらくなりがちね。
例えば、このfor文はちょっとあやしい……
int sum;
int i;
for (i=1, sum=0; i<=10; sum+=i, i++);
うーん、なんだか分かりにくく感じます。行数は少ないのに……
そうよね。for文には、もっとプログラムを分かりやすくできるポテンシャルがあるの。それを引き出してあげましょう。
例えば、こう手直ししたらどうかしら?
int sum = 0;
for (int i=1; i<=10; i++) {
  sum += i;
}
わー、こんどは読みやすくなりましたよ。でも、なんで?

読みやすく手直ししたfor文では、括弧(())の内側にループ変数に関する処理だけが書かれています。変数sumはループ変数ではないので、中括弧({})の中に移動しました。これで、何をしているのかが読み取りやすいプログラムになったのではないでしょうか。

もしかすると、「これって、そんなに重要なこと?」と思ったかもしれません。でも、何十行もあるループを想像してみてください。プログラムが長く複雑になるほど、こういう小さな工夫によってデバッグしやすいプログラムになるものです。

ここがポイント!
while文がfor文でも書けそうなときは、プログラムが分かりやすくなりそうなほうを選びましょう。

for文と「無限ループ」

for文は、無限ループを書くときにもよく使われます。具体的には、次のような書き方をします。

for (;;) { /* ← 無限ループ */
  ……

  if (ループ条件をチェック) {
    break; /* ← ここでループ終了 */
  }

  ……
}

中括弧で囲まれた処理の、なかほどでbreakしていますね。無限ループでは、ここのif文の判定がループ条件にあたります(正確には、ループを終了する条件ですが)。

この書き方は、ループの入り口でループ条件を判定できない場合に便利ね。
そっか。だから「なかほどでbreak」なんですね。

ちなみに、こういうループは「n+1/2ループ(nと2分の1ループ)」などと呼ばれることもあります。通常のループが処理をn回繰り返すのだとしたら、こちらはあと1/2回多く処理するという意味ですね。

ループとunsignedの罠

ところで、signedunsignedの比較は要注意だっていうのは聞いたことあるかしら?
え?なんの話ですか?
例えばintは「符号あり」の値を表すからsignedな型ね。
はい。
で、unsigned intは「符号なし」よね。この2つの値を比較すると、思ったのと違う結果になることがあるのよ?
まさか、「-1」が「1」より大きいとか……いわないですよね?
あら、スルドイじゃない。

次の関数を見てください。for文を使って、引数xから「10」までの値を列挙しようとしています。

void valuesUpTo10(int x) {
  unsigned int y = 10;

  printf("Values:");
  for (int i=x; i<=y; i++) {
    printf(" %d", i);
  }
  printf("\n");
}

この関数をvaluesUpTo10(5);と呼び出すと、出力は次のようになります。

実行結果
Values: 5 6 7 8 9 10
ふむふむ。xが「5」でyが「10」だから、forループでこうなりますね。
それじゃあ、valuesUpTo10(-5);と呼び出したらどうなるかしら?
なんだか嫌な予感がするなぁ……こんどはxが「-5」だから、こうじゃないんですか?
期待される実行結果
Values: -5 -4 -3 -2 -1 0 1 2 3 4 5 6 7 8 9 10
それがね、こうなってしまうのよ。
実際の実行結果(バグ)
Values:
1回もループしないってことですか?
そう。iyの値を比較している部分に問題があるの。
ちなみに、for文の代わりにwhile文を使っても結果は一緒。
void valuesUpTo10(int x) {
  unsigned int y = 10;

  printf("Values:");
  int i = x;
  while (i <= y) {
    printf(" %d", i);
    i += 1;
  }
  printf("\n");
}
無限ループに書き換えて比較の部分をif文にしても、やっぱり結果は変わらない。
void valuesUpTo10(int x) {
  unsigned int y = 10;

  printf("Values:");
  int i = x;
  for (;;) {
    if (i > y) {
      break;
    }
    printf(" %d", i);
    i += 1;
  }
  printf("\n");
}
つまり、forとかwhileとかifとかの違いは関係ないんですね。
signedunsignedを比較しているところが問題ってことね。
ええと。yunsigned intになってますね。ちょっと直してみていいですか?
yintにしてみたらどうかな。
void valuesUpTo10(int x) {
  int y = 10; /* ← unsigned を削除してみた */

  printf("Values:");
  for (int i=x; i<=y; i++) {
    printf(" %d", i);
  }
  printf("\n");
}
あ、出ましたよ!
実行結果
Values: -5 -4 -3 -2 -1 0 1 2 3 4 5 6 7 8 9 10
うん、直ったわね。何が原因だったかというと……

この関数では、変数iyの値を比較しています。前者の型はint、後者の型はunsigned intです。でも型が違うままでは値を比較できません。そこで、実際には比較する前に、両辺の値が同じ型に揃えられます。

このとき、intunsigned intに変換する処理が実行されます。そのため、intの符号が失われてしまうのです。具体的には、次のようになります。

  • intの値が0以上なら、unsigned intに変換後も同じ値になる
  • intの値がマイナスの場合は、unsigned intに変換すると、intで表現できる最大値よりも大きな値になってしまう

これにより、例えば-5 <= 10という比較をしているつもりが、4294967291 <= 10のような予想外な動きになっていたのです。

この問題は、実際の値がマイナスになるときだけ発生するっていうのが気を付けたいポイントね。
なるほど。だからvaluesUpTo10(5);は大丈夫で、valuesUpTo10(-5);とするとおかしな動作になったんですね。
そういうこと。ループ条件には値の大小を比較する式を書くことが多いけど、マイナスの値とunsignedを一緒に使わなければ大丈夫よ。
はーい!
ここがポイント!
マイナスの値を扱うときは、unsignedな値との比較に注意しましょう。

あやしいif文

ユキ先輩。プログラムには流れがあるっていう話を聞いて、if文の使い方が大事なんだなと感じたんですけど……
うん。if文は基本だけれど、意外に奥が深いものよ。
ですよね!そしたら、やっぱりコツみたいなものって、いろいろあるんじゃないですか?
そうね……C言語の解説書なんかでは、「やりがちな間違い」がよく紹介されてるわね。
「やりがちな間違い」ですか?
例えばね……

次のif文では、変数xの値が「-10」と「10」の間かどうかチェックしようとしています。

  int x = 123;
  if (-10 < x < 10) {
    puts("Yes");
  } else {
    puts("No");
  }

もし、これが正しいプログラムなら、Noと表示されるはずですね。でも、実際にはYesと表示されるでしょう。理由は、次のとおりです。

  • このif文は、-10 < x < 10の計算結果がtruefalseかをチェックする
  • 最初に、-10 < xの部分がtrueと判定される(x123なので)
  • 次に、これをもとの式に当てはめると1 < 10となる(C言語ではtrue1false0なので)
  • その結果、式全体の計算結果はtrueとなる

さらによく考えてみると、この式の計算結果はxがいくつでもtrueになることが分かります。-10 < xの部分は必ず10になり、どちらにしても10より小さいためです。

うわぁ。なんだか、思ってたのと違う計算になっちゃってますね……。
これはね、書き方を間違えているのよ。
じゃあ、正しい書き方があるってことですか?
もちろんよ!

上のif文は、次のように書き直せば期待したとおりに動きます。

  if ((-10 < x) && (x < 10)) {
    puts("Yes");
  } else {
    puts("No");
  }

-10 < x < 10-10 < xx < 10の2つの式に分けて、「AND条件(&&)」でつなげればいいんですね。

あ……はい、なるほど。答えを聞いたら、そりゃそうだって思っちゃいました。
そうよね。こういう書き間違いは、まぁいろいろあるわ。
えー、いろいろあるんですか……。
大丈夫、大丈夫よ。ぜんぶ覚えようとしなくてもいいの。
そうなんですか?よかったぁ〜。
ふふふ。こういう書き間違いをしたときって、だいたいは動作がおかしくなるでしょう?
はい、そうだと思います。
動作がおかしければ、「どこが間違ってるんだろう」って考えるんじゃないかしら?
そうですね、考えてデバッグしますね。
つまりね。間違いに気付くことさえできれば、あとは考えて直すだけなのよ。
えっと……そこが難しいところなのでは?
だから少しでもデバッグしやすくなるように、if文を整理するテクニックを覚えましょう!
なるほど!そっちがコツなんですね。

if文のネストを減らす

一般的に、分岐が増えるほどプログラムは複雑になっていきます。デバッグのことを考えれば、if文はできる限り少ないほうがいいでしょう。これは、「必要なif文」と「無駄なif文」を見極められるようになれば可能です。

要るのかどうかあやしいif文って、ネストのところに多いんじゃないかしら。
ほほう。

まずは、if文がネストしている部分に注目してみるのがおすすめです。次のプログラムを見てください。

  int x = 15;
  int y = 5;

  /* xはyで割り切れる? */
  bool isFactor = false;
  if (y != 0) {
    if (x % y == 0) {
      isFactor = true;
    }
  }

  /* 結果を表示 */
  puts(isFactor ? "Yes" : "No");
xyで割り切れるかどうかを、2つのif文で調べているわね。
ふむふむ。割り算をする前に、分母にくるyの値が0じゃないことを1つ目のifで確認してるんですね。
そう。この2つのifには、このとおりの順番で実行する意味があるということよ。
なるほど。

このように、「最初にこちら」「次にこちら」と順番に判定することがあるなら、if文をネストさせるのは自然な流れです。無理をしてif文の数を減らそうとする必要はないでしょう。

では、次のプログラムはどうでしょうか?

  int x = 15;

  /* xは3と5の倍数? */
  bool isMultiple = false;
  if (x % 3 == 0) {
    if (x % 5 == 0) {
      isMultiple = true;
    }
  }

  /* 結果を表示 */
  puts(isMultiple ? "Yes" : "No");
こんどはxが3と5の両方の倍数かどうかを調べているわね。2つのifの順番には……
あ、意味がないんですね。入れ替えてもおんなじだ。
そのとおり!こういうif文はね、1つにまとめてしまえばいいのよ。
もしかして!さっきも出てきた「AND条件(&&)」ですね?
つまり……
  /* xは3と5の倍数? */
  bool isMultiple = false;
  if ((x % 3 == 0) && (x % 5 == 0)) {
    isMultiple = true;
  }
正解よ!if文が1つ減ったわね。
やったー!
ちなみに、もう1つ減らせるのだけど……
いやいや、それだとifがなくなっちゃいますけど。
なくすのよ。
判定結果を変数に格納するだけなら、これでいいわね。
  /* xは3と5の倍数? */
  bool isMultiple = (x % 3 == 0) && (x % 5 == 0);
なんと!そもそもif文が要らない処理だったんですね。
ここがポイント!
「AND条件(&&)」をうまく使うと、if文のネストを減らせる場合があります。

if文で引数を判定する

じゃあ次は、if文と引数との関係について考えてみましょうか。
え?if文と引数ですか?

引数の値で処理を分岐させたいときにも、if文はよく使われます。次の関数を見てください。

/* 引数の値に応じたあいさつを表示する。
 *   1のとき: "Hello!"と表示
 *   2のとき: "Bye!"と表示
 */
void Say(int greetingIdentifier) {
  if (greetingIdentifier == 1) {
    puts("Hello!");
  }
  else {
    puts("Bye!");
  }
}
あやしいところはないかしら?
なんだか、コメントに書かれていることと処理内容がズレてる感じがしますね。

enumを使って次のように書き直してみれば、あやしい部分が浮き彫りになるでしょう。

typedef enum {
  Hello = 1,
  Bye = 2
} GreetingIdentifier;

void Say(GreetingIdentifier greetingIdentifier) {
  if (greetingIdentifier == Hello) {
    puts("Hello!");
  }
  else {
    puts("Bye!");
  }
}

引数が2の場合について、まともに判定していなかったことが分かるのではないでしょうか。これでは、引数が3の場合にも2と同じあいさつが表示されてしまいます。つまり、想定外の値が入力されたときにも、2が入力されたのと同じように動作してしまうのです。

その結果、引数の値に問題があってもなんとなく動作してしまいます。そのため、バグがあると認識するのが遅れることになるでしょう。

これはおそらく、引数の値が1でも2でもないケースについて、考えるのを忘れてしまったことが原因よ。

関数の定義域を明確にして、次のようにすれば状況は改善されます。

/* 引数の値に応じたあいさつを表示する。
 *   1のとき: "Hello!"と表示
 *   2のとき: "Bye!"と表示
 * 上記以外の値は入力禁止とする(入力時の動作は未定義)。
 */
void Say(int greetingIdentifier) {
  assert((greetingIdentifier == 1) || (greetingIdentifier == 2));

  if (greetingIdentifier == 1) {
    puts("Hello!");
  }
  else {
    puts("Bye!");
  }
}

コメントに1文追加したのが分かるでしょうか。これで、3を引数としてこの関数を呼び出してはいけないことが明確になりました。

実装としては、assertが追加されただけですね。
そうね。でも、この1行が書けるようになるとプログラムの品質はぐんと向上するのよ。

こうすれば、たとえ呼び出し側が誤って引数に3を指定したとしても、それが間違いだとすぐに気付くことができるでしょう。関数の誤った呼び出し方を、その入り口でシャットアウトしているからです。

if文のほうは何も変わっていませんね。
そうね。でも、すぐ上にassertがあるのがポイントよ。引数の値が1でも2でもないケースについて、しっかり考慮されていることが伝わるでしょう?
assert()については別のトピックで詳しく説明します。
ここがポイント!
引数をif文でチェックするときは、想定外の値が入力されるケースについても考えましょう。

if文をswitch文に置き換える

ちなみに、さきほどのif文は、次のようにswitch文で置き換えることもできます。

/* 引数の値に応じたあいさつを表示する。
 *   1のとき: "Hello!"と表示
 *   2のとき: "Bye!"と表示
 * 上記以外の値は入力禁止とする(入力時の動作は未定義)。
 */
void Say(int greetingIdentifier) {
  switch (greetingIdentifier) {

    case 1:
      puts("Hello!");
      break;

    case 2:
      puts("Bye!");
      break;

    default:
      assert(false);
  }
}

switch文もif文と同様、プログラムを分岐させるためのものです。複数の分岐を並べて書けるので、うまく使えばプログラムの流れを整理できるでしょう。

先々のことまで考えると、この書き方にはメリットがあるのよ。
あとからcaseを追加しやすいってことですよね。
正解!引数で3や4を受け付けられるように機能を拡張したくなったときも、これなら対応しやすいわね。

この書き方なら、将来の機能拡張が既存部分に与える影響を最小限にできるでしょう。引数の値のバリエーションが実際に「増えそうだな」と予想されるときは、こうしておければベターだといえます。

プログラムには流れがある

ユキ先輩。プログラムを分かりやすく書くために、気を付けると良いことってありますか?
そうねぇ……プログラムの流れを意識することかしら。
プログラムの流れ、ですか?詳しく教えてください!
じゃあ、いくつか例を挙げてみるわね。

まず、プログラム中の処理には、「正常時の処理」と「エラー(異常時の)処理」がありますね。

ソースコード上では、正常時の処理が中心にくるような書き方をするのがおすすめです。なぜなら、エラー処理は「だいたいの場合は発生しない状況」を扱う処理だからです。

正常時の処理を中心に据えておけば、そのプログラムをほかの人が見るときや、あとで自分が見たときに理解しやすいでしょう。

正常時の処理が中心にくるように、ですね。……けど、あまりイメージがわかないです。
そうね。それじゃぁ、次の「悪い例」を見てみましょう。
/* 指定されたファイルにテキストを書き込む。
 * - pText: テキスト
 * - pPathToFile: ファイルのパス
 * 正常に書き込めたらtrue、エラーが発生したらfalseを返す。
 */
bool WriteTextToFile(const char *pText, const char *pPathToFile) {
  assert(pText);
  assert(pPathToFile);

  FILE *fp = fopen(pPathToFile, "w");
  if (fp == 0) {
    return false; /* エラー:ファイルを開けなかった */
  }

  if (fputs(pText, fp) != EOF) {
    fclose(fp);
    return true; /* 正常 */
  }

  fclose(fp);
  return false; /* エラー:テキストを書き込めなかった */
}

この関数は、ファイルに文字列を書き込み、うまくいったらtrueを返すというものです。でも、正常時の流れがどこにあるのかが見つけにくくなってしまっています。

たしかに。なんか、入り組んでいる感じがしますね。
問題点は、いくつか考えられるわね。例えば……
  • エラーの判定に一貫性がありません。fopen()では失敗したかどうかを判定しているのに対し、fputs()では成功したかどうかを判定しています。
  • すべての処理に成功したとき、関数の最後までたどり着くことなくreturnしています。
  • fopen()は1カ所しかないのに、fclose()は2カ所で呼び出されています。
なるほど〜。
だけど、最大の問題は、書き方に「ポリシー」がないことよ!
書き方の「ポリシー」ですか!?

流れが分かりやすい書き方

プログラムの流れが分かりやすくなる、おすすめのポリシーを紹介するわね。

  • 正常でもエラーでも、関数の最後まで処理を到達させる。
  • if文では処理が成功したかどうかを判定し、エラー処理が必要な場合はelseに収める。

このポリシーにしたがうと、上の関数は次のようになります。

bool WriteTextToFile(const char *pText, const char *pPathToFile) {
  assert(pText);
  assert(pPathToFile);

  bool bSuccess = false;

  FILE *fp = fopen(pPathToFile, "w");
  if (fp) {
    if (fputs(pText, fp) != EOF) {
      bSuccess = true;
    }

    fclose(fp);
  }

  return bSuccess;
}

どうでしょう。正常時の流れがよく見えるようになったのではないでしょうか。正常時の流れが見えれば、その関数がどのようなアルゴリズムで書かれているのかが明確になります。

ほんとだ。処理の流れが分かりやすくなりましたね!
それに、複雑さが軽減されて、改造が必要なときも手を付けやすいソースコードになったんじゃないかしら。

上の例でも、ごく自然な流れで、fopen()fclose()の呼び出しが1度ずつになりました。さらに、エラーが発生したとき、実は「falseを返す」以外に何もしていなかったということが明らかになりました。

ポリシーにしたがって書くようにすると、いろいろなメリットがあるんですね。
そうね。でも、これですべてOKってことでもないのよ。

この書き方は、必ずしも万能というわけではありません。判定すべきことが増えてくると、ifのネストが深くなってしまうのです。処理をサブルーチンに分ければネストの問題は解決できますが、ほかの書き方をしたくなるケースも出てくるでしょう。

毎回同じポリシーにするってわけにはいかないんですね。

もう一つの書き方

もう一つ、別のポリシーを紹介するわね。

  • 正常時は、関数の最後まで処理を到達させる。
  • エラーを検出したら、即座にreturnする。

このポリシーにしたがうと、先ほどの関数は次のように書けます。

bool WriteTextToFile(const char *pText, const char *pPathToFile) {
  assert(pText);
  assert(pPathToFile);

  FILE *fp = fopen(pPathToFile, "w");
  if (fp == 0) {
    return false;
  }

  if (fputs(pText, fp) == EOF) {
    fclose(fp);
    return false;
  }

  fclose(fp);

  return true;
}

エラーケースが多数考えられる関数は、こちらのポリシーで書いたほうがシンプルになることがよくあります。そもそも、if文のネストが深くならないのでこちらのほうが好き、という人もいるかもしれません。

ただし、プログラムの「構造化」という面では、returnを何度も書くのは好ましいとはいえません。この例のfclose()ように、同じ処理を2回以上書かなければならない場面も出てきます。

それでも、一定のポリシーにしたがって書かれたプログラムは読みやすくなるものよ。
書き方に「ポリシー」がないと、プログラムが必要以上に複雑になっちゃうってことですね!
そういうこと。

基本的には、1つ目に紹介したポリシーで書くようにするのがおすすめです。それで、ちょっとやりづらいなと感じたときは、2つ目のポリシーを試してみてください。

ここがポイント!
プログラムを書くときは、処理の流れを意識しよう。一定のポリシーにしたがって書けば、読みやすくなります。

第30問の答え

答え

次のように、再帰呼び出しを使うのが正解!
int SumListValues(const ListNode *pNode) {
  if (pNode == 0) {
    return 0;
  }

  return pNode->value + SumListValues(pNode->pNext);
}

解説

これは、「再帰呼び出し(recursive call)」に関する出題でした。再帰呼び出しとは、ある関数の処理中で、その関数を再び呼び出す手法のことです。

問題の関数は、次のような形をしていました。

int SumListValues(const ListNode *pNode) {
  ……
}

この関数は、引数pNodeで連結リストのノードを受け取ります。そして、そのノード以降の値の合計を返すというものです。

値の合計を再帰呼び出しで求めるには、次の2つに分けて考えればいいですね。

  • pNodeの値(pNode->value
  • pNodeの次のノード(pNode->pNext)以降の値の合計

この2つを足せば、pNode以降の値の合計になるはずです。

次のノード以降の値を合計するところで再帰呼び出しを使うのよ。つまり、自分自身を呼び出すということね。
あ、分かったかも!
2つに分けて、自分自身を呼び出す……と、できた!
int SumListValues(const ListNode *pNode) {
  return pNode->value + SumListValues(pNode->pNext);
}
ふむふむ。それじゃあ、実行してみましょうか。
はーい!……ぐはぁ、クラッシュしました。
そうよね。でも、おしいところまできてるのよ。

自分自身を無制限に呼び出し続けようとすれば、いずれプログラムはクラッシュしてしまいます。そのため、再帰呼び出しは「どこでストップするか」が重要です。

今のクラッシュは、ノードが4つしか存在しないのに対し、5回目の呼び出しを行ったために発生したものでした。このとき、引数は0(NULL)になっていたはずです。最後のノードに対するpNode->pNextには、0が格納されているためです。

  ListNode node4 = { 40, 0 }; /* ← 最後のノードのpNextは0(NULL) */
  ListNode node3 = { 30, &node4 };
  ListNode node2 = { 20, &node3 };
  ListNode node1 = { 10, &node2 };
引数pNodeが0だったときに、自分自身を呼び出さないようにすればいいわね。
ふむふむ。じゃあif文を使って……
int SumListValues(const ListNode *pNode) {
  if (pNode == 0) {
    /* ここでストップさせたい! */
  }

  return pNode->value + SumListValues(pNode->pNext);
}
えっと、呼び出しをストップさせるのにbreakは使えないですよね?
うん、ループじゃないものね。ここではreturnが使えるわよ。
そっか!pNodeが0のときは値がないから……
int SumListValues(const ListNode *pNode) {
  if (pNode == 0) {
    return 0;
  }

  return pNode->value + SumListValues(pNode->pNext);
}
こうやって0を返せばいいんですね!
正解!これでクラッシュせずに、合計を求められるようになったはずよ。
なんだか、すごくシンプルなプログラムになっちゃいましたね。
そう見えるのは、レオ君が仕組みを理解したからよ。
えっ?
再帰呼び出しを知らない人から見れば、これは難解なプログラムだと思うわ。
たしかに……じゃあ、普通にループを使ったほうが人に優しいってことになりませんか?
そうかもしれないわね。でもね、やっぱり再帰呼び出しが必要っていう場面もあるのよ。

今回の題材は、連結リスト構造でした。そのノードは、次のような形をしています。

typedef struct ListNode {
  int value;
  struct ListNode *pNext;
} ListNode;

ノードが一列につながるため、ループで処理できることも多いでしょう。

では、データがツリー構造だった場合はどうでしょうか。例えば、つながりが左右2方向に枝分かれする「二分木(binary tree)」のノードは、次のような形をしています。

typedef struct TreeNode {
  int value;
  struct TreeNode *pLeft;
  struct TreeNode *pRight;
} TreeNode;

ツリー内の値を合計したいと思ったとき、whileループやforループを使うのは無理があります。でも、次のように再帰呼び出しを使えば、プログラムはシンプルになります。

int SumTreeValues(const TreeNode *pNode) {
  if (pNode == 0) {
    return 0;
  }

  return pNode->value + SumTreeValues(pNode->pLeft) + SumTreeValues(pNode->pRight);
}

これは、値の合計を次の3つに分けて考え、足し合わせているのです。

  • pNodeの値(pNode->value
  • pNodeの左側のノード(pNode->pLeft)以降の値の合計
  • pNodeの右側のノード(pNode->pRight)以降の値の合計
データが一列に並んでいればループで処理できるけど、枝分かれしているなら再帰呼び出しって感じですかね?
そうね。そういうイメージでいいと思うわ。
ここがポイント!
再帰呼び出しを使いこなせば、いろいろな構造のデータをシンプルに処理できる!

さて。

ここまで読んでいただき、ありがとうございました!C言語にまつわる全30問のクイズ、いかがだったでしょうか?現実の問題解決に役立てられる部分が少しでもあったなら、とても嬉しいです。

本シリーズの問題は、読み物としても楽しんでいただけるよう心がけて制作しました。もし「途中から読んだよ」という場合は、ぜひ「問題一覧」に戻って、ほかの問題にもチャレンジしてみてください!

僕ももう1回、第01問から見直してみたいです!
そうね。時間をあけてから見直すと、何か新しい発見があるかもしれないわね!

修正後のプログラム

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

typedef struct ListNode {
  int value;
  struct ListNode *pNext;
} ListNode;

int SumListValues(const ListNode *pNode) {
  if (pNode == 0) {
    return 0;
  }

  return pNode->value + SumListValues(pNode->pNext);
}

int main(void) {
  ListNode node4 = { 40, 0 };
  ListNode node3 = { 30, &node4 };
  ListNode node2 = { 20, &node3 };
  ListNode node1 = { 10, &node2 };

  printf("Sum: %d\n", SumListValues(&node1));

  return EXIT_SUCCESS;
}
実行結果
Sum: 100

第30問

さあレオ君、いよいよ最後の問題よ!
もう最後か〜。けっこう楽しかったのになぁ……
あら、嬉しいことを言うじゃない。
ユキ先輩のおかげで、僕もちょっとレベルアップできた気がします!
よかったよかった。それじゃあラスト1問、はりきっていきましょうか!
はーい!

以下は、第29問の答えのプログラムです。数値の並びを連結リスト(linked list)構造で表現し、その合計を関数SumListValues()で求めています。この関数ではループが使われていますが、ループを使わない方法に置き換えたいと考えています。

どう書き直せばいいか、分かりますか?
main.c
#include <stdio.h>
#include <stdlib.h>

typedef struct ListNode {
  int value;
  struct ListNode *pNext;
} ListNode;

int SumListValues(const ListNode *pNode) {
  int sum = 0;
  for (const ListNode *p = pNode; p; p = p->pNext) {
    sum += p->value;
  }

  return sum;
}

int main(void) {
  ListNode node4 = { 40, 0 };
  ListNode node3 = { 30, &node4 };
  ListNode node2 = { 20, &node3 };
  ListNode node1 = { 10, &node2 };

  printf("Sum: %d\n", SumListValues(&node1));

  return EXIT_SUCCESS;
}
実行結果
Sum: 100
第29問は正しいプログラムを、別の正しいプログラムに書き換える問題だったわよね。
はい、そうでした。
そこで、さらに別の方法で正しいプログラムを書いてみよう!っていうのが今回の問題よ。
なるほど。でも、ループを使わない方法なんて、本当にあります?
あるのよ。数値の並びは10203040でしょう?
はい。つまり、求めたいのは「10203040の合計」ってことですよね。
そうね。それを最初の1つと残りの3つを分けて考えたら、「10」と「203040の合計」の2つになるわよね。
え……はい、なります。
その2つを足したら、求めたい合計になるのよ。
ええっ?それって解決になってます?
あら、何かおかしなことを言ったかしら?
だって、「203040の合計」のほうは、結局ループしないと求められないですよ。
そこはもう1回、最初の1つと残りを分けるのよ。
ん?「20」と「3040の合計」ってことですか?
そう!そうやって2つに分ければ、足し算できるでしょ?
あのー、何をどうすればいいのか、さっぱり分からないです……
うん。ちょっと手応えがあったほうが最終問題っぽいでしょ。
そ、そうですね……
じゃあ、ヒントね。合計を求める関数には何を渡しているかしら?
えっと、呼び出し元はmain()関数にあって……ここだな。
  printf("Sum: %d\n", SumListValues(&node1));
引数としてnode1、つまり1つ目のノードのアドレスを渡してます。
すると、1つ目以降の合計が分かるのよね。そこに2つ目のノードのアドレスを渡したらどうなるかしら?
それって、こういうこと?
  printf("Sum: %d\n", SumListValues(&node2));
そっか。この場合は2つ目以降の合計になるんですね!
そういうこと。だんだん答えに近付いてきたんじゃない?
そうなんですか?もうちょっと考えてみます!
この関数から自分自身を呼び出すようにすれば……

第29問の答え

答え

pがループ変数になっているので、こう書くのが正解!
int SumListValues(const ListNode *pNode) {
  int sum = 0;
  for (const ListNode *p = pNode; p; p = p->pNext) {
    sum += p->value;
  }

  return sum;
}

解説

今回は、「ループ変数」を見極める練習問題でした。ループ変数とは、ループが継続する条件となる変数のことです。その値が一定の条件を満たす間だけ、値を更新しながら処理を繰り返します。

もっとも分かりやすいループ変数は、次のようなものでしょう。

  for (int i=0; i<10; i++) {
    ……
  }

ここでのループ変数はiです。条件(10より小さい)を満たす間だけ、値を更新(1ずつ増加)しながら処理を繰り返しています。

では、これから書き直そうとしているwhileループを確認してみましょう。

int SumListValues(const ListNode *pNode) {
  const ListNode *p = pNode;
  int sum = 0;

  while (p) {
    sum += p->value;
    p = p->pNext;
  }

  return sum;
}
どれがループ変数なのか、分かるかしら?
whileの括弧のところを見れば分かりそうだなー。
  while (p) {
    ……
  }
ズバリ、pじゃないですか?
そのとおり!ここまでは難しくないわね。
はい。大丈夫でした。
それじゃあ、これをどうやってforに書き換えるかってことなのだけど……
わくわく。
それにはwhileforの関係性を理解するのがいいわね。

forは、whileでループする際のよくあるパターンを書きやすく拡張したようなものです。多くの場合、whileループは次のような形をしています。

  ループ変数を初期化;
  while (ループ条件をチェック) {
    ……
    ループ変数を更新;
  }

これをforループで表現すると、次のようになります。

  for (ループ変数を初期化; ループ条件をチェック; ループ変数を更新) {
    ……
  }

このように、ループ変数に関する記述をまとめられるのがforの便利なところです。うまく使えば、プログラムを読みやすくできるでしょう。

ということは、このよくあるforループは……
  for (int i=0; i<10; i++) {
    ……
  }
このwhileループの変形だったのか!
  int i=0;
  while (i < 10) {
    ……
    i++;
  }
分かったみたいね!この2つが完全に同じ処理だとまではいわないけれど、やろうとしていることは一緒なのよ。
なるほど!って感じです。

この考え方を、問題のwhileループにあてはめてみましょう。ループ変数がpだということは、すでに分かっています。したがって、「ループ変数を初期化」「ループ条件をチェック」「ループ変数を更新」という3つの処理は、それぞれ次の部分だといえます。

int SumListValues(const ListNode *pNode) {
  const ListNode *p = pNode; /* ← ループ変数を初期化 */
  int sum = 0;

  while (p) { /* ← ループ条件をチェック */
    sum += p->value;
    p = p->pNext; /* ← ループ変数を更新 */
  }

  return sum;
}
ここまで見極められれば、あとはforに変形できるんじゃないかしら?
そうですね。できそうな気がします。
3つの処理をforの書き方に合わせて……っと。うーん、これでいいのかなぁ……
int SumListValues(const ListNode *pNode) {
  int sum = 0;
  for (const ListNode *p = pNode; p; p = p->pNext) {
    sum += p->value;
  }

  return sum;
}
そう!正解よ。
よかったぁ!あと、やっぱりプログラムが読みやすくなった気がしますね。
ループ変数に関係する部分が1行にまとまったし、行数も減ったものね。
あのー、1つ質問してもいいですか?
もちろん!
じつは、forの「ループ条件をチェック」のところが、これでいいのか自信がなかったんです。
pとだけ書いたところね。これは条件式なのよ。whileifなんかの括弧の中と同じ書き方ね。
じゃあ、ここにはどんな条件でも指定していいってことですか?
いいのよ。もしかして……今までi<10みたいな書き方しかしたことがなかったのかしら?
そうなんです!だって、forは範囲が決まってるときに使うループじゃないですか。
あー、そういうことね。それは逆なのよ。
逆……ですか?
forwhileと同じで、どんなループに使ってもいいの。ただ、あらかじめ範囲が決まっているループはforを使うと書きやすいっていうことね。
なるほどー。逆ってそういうことだったんですね。

さて、ここまではforループのおすすめの書き方を紹介しました。でも、ただwhileforに書き換えるというだけなら、別の方法もあります。

例えば、無限ループとbreakを組み合わせた、次のような書き方が考えられます。

  ループ変数を初期化;
  for (;;) { /* ← 無限ループ */
    if (ループ条件をチェック) {
      break;
    }
    ……
    ループ変数を更新;
  }

これは、「条件を満たす間だけ処理を繰り返す」のではなく、「条件を満たした時点で処理を終える」ということですね。そのため、「ループ条件をチェック」の部分に入る式は、whileの場合の否定形になります。

今回の問題にあてはめると、次のようになるでしょう。

int SumListValues(const ListNode *pNode) {
  const ListNode *p = pNode;
  int sum = 0;

  for (;;) {
    if (p == 0) {
      break;
    }

    sum += p->value;
    p = p->pNext;
  }

  return sum;
}
もともとのwhileより、行数が増えちゃいましたね。
そうね。それにパッと見たときに、どれがループ変数なのか判別しづらいんじゃないかしら。
そうですね。じゃあ、無限ループは使わないほうがいいってことですか?
そんなことはないのよ。breakの前後どちらにも処理が必要なときは、無限ループが便利ね。
つまり、こんな感じ。
  for (;;) {
    前半の処理:

    if (ループ条件をチェック) {
      break;
    }

    後半の処理;
  }
そっか。ループを抜ける条件を、処理の途中で判定したい場合もあるってことですね。
そういうこと。今回の問題では、その必要はなかったわね。

では、とにかく行数を減らすことを優先させたらどうなるでしょうか?例えば、次のように書くことも可能でしょう。

int SumListValues(const ListNode *pNode) {
  int sum = 0;
  for (const ListNode *p = pNode; p; sum += p->value, p = p->pNext);

  return sum;
}
ええっ!これでも同じ結果になるんですか?
そうなのよ。
中括弧がないですけど……
中括弧の中にあった処理は、カンマで区切って括弧の中に入れてあるでしょ。
あ、ほんとだ。それで行数が減ってるんですね。
そうね。でも、その分だけ読みやすくなったといえるかしら?
うーん。どういう処理をしているのかが、ちょっと分かりづらい気がします。
そうよね。頑張って1行にまとめようとしすぎて、かえって読みづらくなってしまった例といえるわね。
そうですねぇ。
ここがポイント!
ループを作るときは、どれがループ変数なのかを見極めよう!

修正後のプログラム

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

typedef struct ListNode {
  int value;
  struct ListNode *pNext;
} ListNode;

int SumListValues(const ListNode *pNode) {
  int sum = 0;
  for (const ListNode *p = pNode; p; p = p->pNext) {
    sum += p->value;
  }

  return sum;
}

int main(void) {
  ListNode node4 = { 40, 0 };
  ListNode node3 = { 30, &node4 };
  ListNode node2 = { 20, &node3 };
  ListNode node1 = { 10, &node2 };

  printf("Sum: %d\n", SumListValues(&node1));

  return EXIT_SUCCESS;
}
実行結果
Sum: 100

第29問

数値の並びを連結リスト(linked list)構造で表現し、その合計を求めるプログラムを作りました。プログラム中にwhileループが1つありますが、これをforループで置き換えたいと考えています。

どう書き直せばいいか、分かりますか?
main.c
#include <stdio.h>
#include <stdlib.h>

typedef struct ListNode {
  int value;
  struct ListNode *pNext;
} ListNode;

int SumListValues(const ListNode *pNode) {
  const ListNode *p = pNode;
  int sum = 0;

  while (p) {
    sum += p->value;
    p = p->pNext;
  }

  return sum;
}

int main(void) {
  ListNode node4 = { 40, 0 };
  ListNode node3 = { 30, &node4 };
  ListNode node2 = { 20, &node3 };
  ListNode node1 = { 10, &node2 };

  printf("Sum: %d\n", SumListValues(&node1));

  return EXIT_SUCCESS;
}
実行結果
Sum: 100
ここまでいろいろな問題を出してきたけど……
はい。
目的が同じプログラムでも、書き方は1つじゃないっていうことは分かったかしら?
もちろん!正解はいくつもあるってことですよね。
そのとおり!今回は、それをそのまま問題にしてみたわ。
えっと。つまり、別の正解を考えるってことですか?
そういうこと!……まず、連結リストは分かる?
たしか、「数珠つなぎ」になってるデータ構造だったと思います。
そうね。一つ一つの「数珠」にあたる部分を「ノード(node)」というのだけど……
あ、はい。プログラム中にありますね。
ここにノードが4つあって、それぞれに数値が入ってる……
  ListNode node4 = { 40, 0 };
  ListNode node3 = { 30, &node4 };
  ListNode node2 = { 20, &node3 };
  ListNode node1 = { 10, &node2 };
10203040っていう数値の並びになっているわね。
それって、node1node2node3node4の順番でつながってるってことですか?
そうよ。ノードの構造体を見れば分かると思うけど。
構造体は……これか。
typedef struct ListNode {
  int value;
  struct ListNode *pNext;
} ListNode;
数値をvalueに格納しつつ、pNextがポインタだから、これで次のノードにつながるんですね!
うん。そうやってできる「数珠つなぎ」が、連結リスト構造ね。
なるほど〜!
さてさて。ここまで分かったら、問題にとりかかれるんじゃないかしら?
おっと、そうでした。
whileが使われているのは、この関数か……
int SumListValues(const ListNode *pNode) {
  const ListNode *p = pNode;
  int sum = 0;

  while (p) {
    sum += p->value;
    p = p->pNext;
  }

  return sum;
}
このループをよく見て、forで書く場合の正解を考えてみてね。
はーい。やってみます!
ループの条件になっている変数に注目してみましょう!

利用形態を意識したデザイン

関数を作るときは、使う人の視点で使用目的を考えておくといい」と言ってましたけど、いまひとつピンとこないので詳しく教えてもらえますか?
ああ……それはね、できる限り「使いやすいかどうか」を意識するということよ。関数は作る回数よりも、使う回数の方が多いからね。
でも、どうすれば使いやすくなるんでしょう?
関数のプロトタイプ宣言を見ただけで、使い方のイメージがわくようにデザインできるといいわね。
プロトタイプ宣言で使い方のイメージがわくように……ですか!?
そう。C言語では、関数を使う(呼び出す)側から、その関数の中身(実装)が見えないことも多いでしょ?
……そういうことかぁ。
じゃあ。1つ例をあげてみるわね。

今回は、アルファベットの小文字を大文字にするToUpper()という関数を作るケースについて考えてみましょう。

この場合、例えば次のようなデザインが考えられるのではないでしょうか。

/* codeで、文字コードを受け取る。
 * それがアルファベットの小文字だった場合は、大文字に変換して返却する。
 * そうでない場合は、codeをそのまま返却する。
 */
int ToUpper(int code);
/* pCodeで、変換対象の文字コードのアドレスを受け取る。
 * 文字コードがアルファベットの小文字だった場合は、大文字に変換してpCodeのアドレスに格納する。
 * そうでない場合は、何もしない。
 */
void ToUpper(int *pCode);
/* lowerCodeで、文字コードを受け取る。
 * それがアルファベットの小文字だった場合は、大文字に変換してpUpperCodeのアドレスに格納する。
 * そうでない場合は、lowerCodeをそのままpUpperCodeのアドレスに格納する。
 */
void ToUpper(int lowerCode, char *pUpperCode);

さて、どのデザインを採用すると使いやすくなるでしょうか?

うわぁ。ぜんぜんピンとこないです。
あらら。そうしたら、「どんな風に使いたいか」と考えてみるといいわね。
そっかぁ。やってみます!
だいたいの関数には、使い方のパターンがあるはずよ。
ええと……アルファベットの小文字を大文字にしたいケースって、たぶん文字列ごと変換したいことが多いんじゃないでしょうか。例えば、こんな感じで!
サンプルコード
/* 文字列を大文字に変換する */
void ConvertToUpper(char *pText) {
  size_t length = strlen(pText);

  for (int i=0; i<length; i++) {
    pText[i] = ToUpper(pText[i]);
  }
}

ConvertToUpper()は、文字列に含まれるアルファベットの小文字をすべて大文字に変換する関数です。その実装では、ループ中でToUpper()を使うことになるでしょう。変換前の文字コードを1つずつ渡して、変換後の文字コードを戻り値で返してもらえれば、自然な流れで処理できます。

バッチリね!この関数の場合は、おそらくこのような使い方が典型的といえるわね。
(やった〜!)……というわけで、ToUpper()のデザインは、こちら(1つ目)を採用することになりますね。
/* codeで、文字コードを受け取る。
 * それがアルファベットの小文字だった場合は、大文字に変換して返却する。
 * そうでない場合は、codeをそのまま返却する。
 */
int ToUpper(int code);
うんうん。これ以外(2つ目や3つ目)のプロトタイプだと、使いづらそうよね。
ここがポイント!
関数を作るときは、利用形態(どんな風に使いたいか)を意識してデザインしましょう。

標準関数は使いやすくできている

ちなみに、ToUpper()と同じ目的で使われるものとして、toupper()という標準関数があります。こちらのプロトタイプも、こうなっています。

int toupper(int c);

同じですね。使うときのことを考えてデザインしてくれていると分かります。

へー。標準関数って、ちゃんと使いやすく考えられているんですね!

C++のクラスはどうする?

C++では、関数よりもクラス(class)の単位で機能を提供したいケースが多いでしょう。その場合、使いやすいかどうかはクラス全体のデザインに左右されます。

そのクラスのインスタンスを生成してから破棄するまでの典型的な使い方を想定して、メンバー関数ごとのプロトタイプを決めるのがいいでしょう。

意図した使い方をしてもらうには?

ところで、「きっとこんな風に使いたいだろう」と想定して関数を作ったとしても、みんなが同じように使ってくれるとは限らないですよね?
そうね。典型的な使い方について考えるときはサンプルコードを書くことになると思うから、せっかくなら、それをヘッダーファイルのコメントやドキュメントなんかに掲載しておくといいんじゃないかしら。
なるほど〜。目に付きやすい場所にあれば、実際に使う人がサンプルコードをもとに実装できますね。
予想外の使われ方によるトラブルも避けやすくなるわね。

第28問の答え

答え

数値を0で書き始めると8進数になってしまう!
PlanetInfo planets[] = {
  { 004879, "Mercury" }, /* ← 先頭が0だと8進数! */
  { 012104, "Venus" },
  { 012742, "Earth" },
  { 006779, "Mars" },
  { 139820, "Jupiter" }, /* ← こちらは普通に10進数 */
  { 116460, "Saturn" },
  { 050724, "Uranus" },
  { 049244, "Neptune" },
};
10進数を表したいときは、先頭を0にしない書き方が正解!
PlanetInfo planets[] = {
  {   4879, "Mercury" },
  {  12104, "Venus" },
  {  12742, "Earth" },
  {   6779, "Mars" },
  { 139820, "Jupiter" },
  { 116460, "Saturn" },
  {  50724, "Uranus" },
  {  49244, "Neptune" },
};

解説

これは、数値の表記法についての出題でした。C言語では通常の10進数のほかに、8進数や16進数による表現が可能です。

16進数は第25問でも出てきたけど、よく使う表現よね。
あ、例の0xで始まるやつですね。(そっか、よく使うのか……)
でね、ちょっと分かりにくいのだけど、0で始まる数値は8進数なのよ。
並べて書くと、こんな感じ。
  int v10 = 100; /* 10進数 */
  int v8 = 0100; /* 8進数(10進数で64)*/
  int v16 = 0x100; /* 16進数(10進数で256) */
ええっ!ということは……
ここに並んでる整数は、10進数と8進数が混ざっちゃってるのか。
PlanetInfo planets[] = {
  { 004879, "Mercury" },
  { 012104, "Venus" },
  { 012742, "Earth" },
  { 006779, "Mars" },
  { 139820, "Jupiter" },
  { 116460, "Saturn" },
  { 050724, "Uranus" },
  { 049244, "Neptune" },
};
どれも普通の10進数に見えるのにね。
これって、数字の桁数をきれいに揃えてプログラムを書きたかっただけですよね?
そういうこと。だから、けっこうやってしまいがちな間違いといえるわね。
じゃあ、きれいに書けないですねー。
あら、そんなことはないわよ。
例えば、こんな風にすれば……
PlanetInfo planets[] = {
  {   4879, "Mercury" },
  {  12104, "Venus" },
  {  12742, "Earth" },
  {   6779, "Mars" },
  { 139820, "Jupiter" },
  { 116460, "Saturn" },
  {  50724, "Uranus" },
  {  49244, "Neptune" },
};
そっか。0で埋めるんじゃなくて、スペースを空ければよかったんですね!
そう。これでコンパイルできるようになったはずよ。

さて。今回の問題は、「コンパイルエラーになる」という点でした。でも、8進数を使うといつもエラーになるわけではありません。

16進数では、先頭の0xのあとは0から9までの数字とaからfまでの英字、合わせて16種類の文字を使いますね。8進数では、先頭の0のあと、0から7までの8種類の文字を使います。これに対して、問題のプログラムでは偶然89も使われていたためエラーになったのです。

普通の数字のように見えて、しかもエラーにならないときもあるって、困るじゃないですか。
そうなのよ。これはC言語の、ちょっと気を付けないといけないところね。

ちなみに、後発のプログラミング言語には表記法が改善されているものもあります。例えば、Swiftでは次のように書きます。

  let v10 = 100; // 10進数
  let v2 = 0b100; // 2進数(10進数で4)
  let v8 = 0o100; // 8進数(10進数で64)
  let v16 = 0x100; // 16進数(10進数で256)
ヘぇ〜。この書き方なら間違えにくいですね。
逆に、この書き方じゃないからC言語では間違えやすいっていうのも分かるわね。
たしかに!
ここがポイント!
数値の先頭を0にする書き方には要注意。それは8進数です!
ところで、8進数って本当に必要なんですか?
あー、それは疑問に思うわよね。私は使ったことがないわ。
なんとっ!
16進数が、1桁あたり4ビットなのは分かるかしら?
えっと、charとか8ビットの値が16進数で2桁だから、そうなりますね。
そうね。で、最近のコンピューターは32ビットとか64ビットが主流でしょう?
はい。
32ビットや64ビットは、4ビットで割り切れるから、16進数と相性がいいのよ。
ふむふむ。だから16進数はよく使うんですね。
でね。8進数は、1桁が3ビットなの。
まさかの奇数!
あはは。で、32ビットや64ビットは、3ビットじゃ割れないってわけ。
なるほど。だから8進数はあまり使わないんですね。
ところが!世の中には36ビットや48ビットのコンピューターもあるのよ。なんと3ビットで割れるのよ。
うわぁ!
つまり、8進数が便利な世界でプログラミングをしてる人もいるってことね。

修正後のプログラム

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

typedef struct {
  uint32_t diameter; /* [km] */
  const char *pName;
} PlanetInfo;

PlanetInfo planets[] = {
  {   4879, "Mercury" },
  {  12104, "Venus" },
  {  12742, "Earth" },
  {   6779, "Mars" },
  { 139820, "Jupiter" },
  { 116460, "Saturn" },
  {  50724, "Uranus" },
  {  49244, "Neptune" },
};

int main(void) {
  int n = sizeof(planets) / sizeof(planets[0]);
  for (int i=0; i<n; i++) {
    printf("Diameter of %s: %dkm\n", planets[i].pName, planets[i].diameter);
  }

  return EXIT_SUCCESS;
}
実行結果
Diameter of Mercury: 4879km
Diameter of Venus: 12104km
Diameter of Earth: 12742km
Diameter of Mars: 6779km
Diameter of Jupiter: 139820km
Diameter of Saturn: 116460km
Diameter of Uranus: 50724km
Diameter of Neptune: 49244km

第28問

太陽系の惑星ごとの直径を、名前とともに一覧表示するプログラムを作ろうとしています。でも、コンパイルエラーで動きません。

何が間違っているか分かりますか?
main.c
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

typedef struct {
  uint32_t diameter; /* [km] */
  const char *pName;
} PlanetInfo;

PlanetInfo planets[] = {
  { 004879, "Mercury" },
  { 012104, "Venus" },
  { 012742, "Earth" },
  { 006779, "Mars" },
  { 139820, "Jupiter" },
  { 116460, "Saturn" },
  { 050724, "Uranus" },
  { 049244, "Neptune" },
};

int main(void) {
  int n = sizeof(planets) / sizeof(planets[0]);
  for (int i=0; i<n; i++) {
    printf("Diameter of %s: %dkm\n", planets[i].pName, planets[i].diameter);
  }

  return EXIT_SUCCESS;
}
期待される実行結果
Diameter of Mercury: 4879km
Diameter of Venus: 12104km
Diameter of Earth: 12742km
Diameter of Mars: 6779km
Diameter of Jupiter: 139820km
Diameter of Saturn: 116460km
Diameter of Uranus: 50724km
Diameter of Neptune: 49244km
水星(Mercury)から海王星(Neptune)までの、8つの惑星のデータが並んでますね。
データを構造体の配列で表現しようとしているのが分かるかしら?
はい。PlanetInfoが構造体で、その配列がplanetsですね。
構造体のほうは……
typedef struct {
  uint32_t diameter; /* [km] */
  const char *pName;
} PlanetInfo;
diameterっていうのが惑星の「直径」を表してて、単位が「キロメートル(km)」なわけですね。
そうそう。で、pNameが惑星の「名前」ね。
データを表示しているところは……
  int n = sizeof(planets) / sizeof(planets[0]);
  for (int i=0; i<n; i++) {
    printf("Diameter of %s: %dkm\n", planets[i].pName, planets[i].diameter);
  }
forループで構造体の中身を1つずつ表示してるのは分かるんですけど……
うん。
変数nのところは、何を計算をしてるんですか?
配列全体のサイズsizeof(planets)を、要素1つ分のサイズsizeof(planets[0])で割ってるわよね。
たしかに。ということは……
  int n = sizeof(planets) / sizeof(planets[0]);
分かった!nは配列の要素数ってことですね!
うん、正解!これはね、C言語ではよく使う計算なのよ。
そうなんですか?この行が怪しいと思ったんだけどなぁ……
なーんだ、そういうことだったのね。
えっと、じゃあ、実際にコンパイルしてみれば何か分かりますかね?
そうね。コンパイルエラーの内容をよく確認すればね。
ですよね。やってみます!
配列の内側にある数字の書き方に注目してみましょう!