第27問の答え

答え

このelseは「1ではないとき」に実行されるので、「2のとき」というコメントはおかしい!
  if (n == 1) {
    /* 1のとき */
    puts( "one" );
  } else {
    /* 2のとき */
    puts( "two" );
  }
ここは単純なif文なのでコメントを削除してスッキリさせつつ……
  if (n == 1) {
    puts( "one" );
  } else {
    puts( "two" );
  }
さらに、関数の定義域を明確にできるとベター!
/*
 * 引数が1のときは"one"、2のときは"two"と表示する。
 * それ以外のときの動作は未定義。
 */
void PrintOneOrTwo(int n) {
  assert((n == 1) || (n == 2));

  if (n == 1) {
    puts( "one" );
  } else {
    puts( "two" );
  }
}

解説

これは、コメントの書き方に関する出題でした。問題の関数では、if文のコメントに少しおかしなところがあります。

void PrintOneOrTwo(int n) {
  if (n == 1) {
    /* 1のとき */
    puts( "one" );
  } else {
    /* 2のとき */
    puts( "two" );
  }
}

「2のとき」というコメントがありますが、これが実際の動作と一致していないのが分かるでしょうか。

ここは(n == 1)という条件に対するelseだから……
「1ではないとき」っていうコメントじゃないとおかしいですね!
ふふふ。それじゃあ、やってみましょうか。
さすがに簡単ですよっ!
あれ?なんだか、すごく当たり前のことを書いてしまったような……
void PrintOneOrTwo(int n) {
  if (n == 1) {
    /* 1のとき */
    puts( "one" );
  } else {
    /* 1ではないとき */
    puts( "two" );
  }
}
そうね。まるでif文の説明みたいになってしまったわね。ここは思い切って、コメント自体をなくしてみるのはどう?
えっと。コメントを削除……と。
void PrintOneOrTwo(int n) {
  if (n == 1) {
    puts( "one" );
  } else {
    puts( "two" );
  }
}
あ、なんだかスッキリしましたよ!
ね。これくらいシンプルなif文なら、むしろコメントを書かないほうが読みやすいんじゃないかしら。
たしかに!ところで、僕はちょっと別の答えを予想してましたよ。
あら、そうなの?
はい。もともとの「2のとき」っていうコメントに合わせた動作にしたらいいのかなって。
こんな感じで、elseのところにifを追加すれば……
/* 引数が1のときは"one"、2のときは"two"と表示する */
void PrintOneOrTwo(int n) {
  if (n == 1) {
    puts( "one" );
  } else if (n == 2) {
    puts( "two" );
  }
}
仕様をストレートに実装したのね。とても良い答えよ!
やったー!
これなら、いつか1や2以外の値に対応したくなった場合にも、処理を追加しやすいわね。

さて。いつか1や2以外の値を追加したくなるかもしれないというのは、この関数が今のところ「1または2」しか受け付けないからですね。これは、関数に「定義域」が定められていることを意味します。定義域とは、引数として与えられる値の範囲のことです。

定義域の考え方は、実は数学で習う関数と大きく変わりません。

例えばね、三角関数の「tan」ってあるでしょ?
「サイン(sin)・コサイン(cos)・タンジェント(tan)」のタンジェントのことですか?
そうそう。そのタンジェントには「π/2」を与えられないじゃない?
いやいや、ユキ先輩!僕が文系出身だってことをお忘れですか?
あー、ごめんごめん。つまりね、関数というのは、意外に引数が制限されているものなのよ。
なるほど。そういう話なら分かります!
でね、それは数学でもプログラミングでも同じことなの。

では、問題の関数について確認してみましょう。

/* 引数が1のときは"one"、2のときは"two"と表示する */
void PrintOneOrTwo(int n) {
  ……
}

コメントされている仕様から、この関数の定義域は「1または2」だと分かります。そして、main()からは、次のように呼び出されています。

  PrintOneOrTwo(1);
  PrintOneOrTwo(2);

引数の値が定義域に収まっているので、期待どおりに動作しているということです。

もし、これが数学の関数なら、「1または2」以外の値を与えることはできません。でも実際には、「1または2」以外の値を引数にして関数を呼び出すプログラムを書くこともできますね。では、この関数の引数に3という値を与えたらどうなるでしょうか?

どうなると思う?
はい、分かりません!
正解!
ええっ?
だって、決められていないもの。「分からない」で正しいのよ。

今回の関数では、「1または2」以外の値を受け取ったときの動作が決められていませんでした。そのため、引数に3と書いた場合にどのような動作をするかは、「不明である」というのが仕様なのです。このような動作を「未定義(undefined)」といいます。

ふむふむ。つまり、「1または2以外の値を受け取った場合の動作は未定義」ということですかね。
そういうこと!
えっと……。そうしたら、引数に3を指定するのは無理ってことになりませんか?
そのとおりよ!「未定義」というのは、実質的には「使用禁止」のことね。
そっか!なんでも引数に指定していいわけじゃないんですね!
分かったみたいね。それじゃあ、何が未定義なのかが明確になるように、コメントを直してみましょうか。
やってみます!
未定義な動作を明確に……と。こんな感じかな?
/*
 * 引数が1のときは"one"、2のときは"two"と表示する。
 * それ以外のときの動作は未定義。
 */
void PrintOneOrTwo(int n) {
  ……
}
うん、いい感じ。これなら引数に3を指定してはいけないっていうことも分かるわね。
やったー。それにしても、C言語のクイズの答えで日本語を書くっていうのは、ちょっと意外だったかも。
ふふふ。そうかもね。
ここがポイント!
コメントには大事なことを書こう!関数に未定義の動作があるときは、コメントしておけば分かりやすい!

ちなみに。

関数の使い方をしっかりコメントに書いていたとしても、間違いを100%防げるわけではありません。今回の関数を、うっかり引数を3にして呼び出してしまうこともあるでしょう。

そこで、次のようにassertを入れておくのがおすすめです。こうすれば「1または2」以外の値が明確に禁止されるので、早い段階で間違いに気付けるようになるでしょう。

#include <assert.h>

void PrintOneOrTwo(int n) {
  assert((n == 1) || (n == 2)); /* ← 「1または2」以外の値を明確に禁止! */

  ……
}
レオ君が考えた答えと合わせて、こういう風に書くのもいいわね。
#include <assert.h>
#include <stdbool.h>

void PrintOneOrTwo(int n) {
  if (n == 1) {
    puts( "one" );
  } else if (n == 2) {
    puts( "two" );
  } else {
    assert(false); /* ← 「1または2」以外の値を明確に禁止! */
  }
}
採用された〜!
または、switch文で書き換えてもOKよ!
#include <assert.h>
#include <stdbool.h>

void PrintOneOrTwo(int n) {
  switch (n) {
    case 1:
      puts( "one" );
      break;
    case 2:
      puts( "two" );
      break;
    default:
      assert(false); /* ← 「1または2」以外の値を明確に禁止! */
  }
}

修正後のプログラム

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

/*
 * 引数が1のときは"one"、2のときは"two"と表示する。
 * それ以外のときの動作は未定義。
 */
void PrintOneOrTwo(int n) {
  assert((n == 1) || (n == 2));

  if (n == 1) {
    puts( "one" );
  } else {
    puts( "two" );
  }
}

int main(void) {
  PrintOneOrTwo(1);
  PrintOneOrTwo(2);

  return EXIT_SUCCESS;
}
実行結果
one
two