Subterranean Flower

大学のプログラミング実習で生き残るためのいくつかのアドバイス

Author
古都こと
ふよんとえんよぷよぐやま!!!

新大学生のみなさま、まずは合格おめでとうございます。大学での新生活は新しい喜びと学びに満ちています。大いに楽しんでください。

さて、運良く(運悪く)情報科に、あるいはそれに類する学科に入ることになった方は、「プログラミング実習(演習)」という必修科目を受けることになると思います。必修科目のわりにこれがなかなか難しく、多くの学生が苦戦している様子を毎年見かけます。私もプログラミングの授業は何度かTA(ティーチングアシスタント)を担当して、様々な苦難を目にしました。

そこで、プログラミング実習を乗り切るためのいくつかのアドバイスを、この記事に記したいと思います。この記事が新入生の助けになれば幸いです。

プログラミング実習という授業について

学校にもよると思いますが、プログラミングの授業は、大まかには以下のような流れになると思います:

  1. PowerPointなどを用いて、今回学ぶ要素について先生が解説する
  2. 教科書などを参考にしつつ、授業時間内に練習問題を解いて提出する
  3. 次週までに宿題問題を解いて提出する

授業名に実習(演習)と名前がつくだけあって、自分でプログラムを書くことが非常に大きな割合を占めています。授業の自由度は高く、友達と相談したり、机間巡視をしている先生やTAに質問したり、気楽な姿勢で挑むことができる授業になっているかと思われます。

しかしそれでも挫折する人が後を絶たないというのが実情です。TAとしては、もっと気楽に質問していただき、研究室にも質問に訪れてほしいのですが、それはなかなかハードルが高く難しいようです。

使用するプログラミング言語はC言語やJava、あるいはRubyやPythonなど、多岐に渡ると思います。ここではC言語を前提として話を進めてきますが、他の言語でも通用する、一般的な話をできるだけメインにしていきます。

実習において気をつけたいこと

積極的に質問する

プログラミングは、非日常的な活動です。新しい発見に満ち溢れていますが、その分、わからないことも多く存在します。そういったときに、「なぜこうなっているのだろう」「どうしてこれで動かないのだろう」と自分で考えるというのは非常に大事なことです。

ですが、それが10分や20分かかるとなると話は別です。時間は有限ですし、疑問は無限に湧いてきます。そして自分だけで考えると間違った答えにたどり着くかもしれません。自分で考える、というのは時に有害となることがあります。

そして、プログラミングは積み重ねの技術です。授業のはじめの方で抱いた疑問をそのままにしていると、大学4年の終わりまで引きずることになります。「時間が経てば別の分野に行くからリセットされる」ということがありません。わからないことをわからないまま放置していると、だんだん積み上がってきて、最後には崩壊してしまいます。

なので、少し自分で考えて、わからないときはすぐに先生やTAに質問してみましょう。あまり本質的でない、素朴な疑問でも大丈夫です。例えば「forループで使う変数の名前は、なんで”i”なんですか?」などでもかまいません。とにかく頭に浮かんだ疑問は片っ端から解決していくことにしましょう。

本やネットで調べる

授業の指定教科書は軽視されがちですが、意外と重要なことが書いてあったりします。プログラミングの入門書は玉石混交で、中にはひどいものもありますが、授業の教科書として指定されているなら、そこまでひどいことは書いていないはずです。一度、教科書にも目を通してみましょう。

複数の参考書を利用するのも手です。ひとつの本だと多面的な視点が得られず、理解に苦労することがありますが、複数の参考書を見比べて、いろんな面から見てみると、案外簡単に理解できたりするものです。なお、参考書はわざわざ買わずとも、大学の図書館にたくさん置いてあるはずです。行き詰ったときには、図書館で借りてみましょう。

またネットで調べるという方法もあります。ネットには様々な情報が転がっていますし、質問サイトもあります。例えばプログラミング質問サイトでは、Stack Overflow(http://stackoverflow.com/)が有名です。どうしてもTAなどに相談しにくい、または先生やTAの知識レベルが信用できないとなったら、質問サイトを活用してみるのも手です。

プログラミングにおいて気をつけたいこと

エラーメッセージを読む

プログラミングにおいて困ることは、大きくふたつに分けられます。「思い通りに動かない」と「そもそも動作すらしない」です。慣れてくると「思い通りに動かない」に苦しめられることになるのですが、慣れるまでは「そもそも動作すらしない」に苦しめられることの方が多いと思います。

そもそも動作すらしない場合、画面にエラーメッセージが表示されます。エラーメッセージは、なぜ動かないかの答えそのものであることが多いです。しかしそれにもよらずエラーメッセージを全く読まない人というのは非常に多く存在します。

エラーメッセージを読むことで、初歩的なミスは簡単に解決することができます。単純な例を見てみましょう。

以下のプログラムは正しくコンパイルされません:

#include <stdio.h>

int main(void) {
  int a = 0;

  printf("a=%d, b=%d", a, b);
  return 0;
}

このプログラムは、aという変数の値と、bという変数の値を表示しようとするものです。

このときコンパイルメッセージは、例えばgccを使用した場合、以下のようになります:

$ gcc sample.c
sample.c: In function 'main':
sample.c:6:27: error: 'b' undeclared (first use in this function)
   printf("a=%d, b=%d", a, b);
                           ^
sample.c:6:27: note: each undeclared identifier is reported only once for each function it appears in

英語のメッセージですが、頑張って読んでみましょう。

$ gcc sample.c
sample.c: 'main'関数において、
sample.c:6:27: エラー: 'b'は未宣言です(この関数の中で初めての使用です)
   printf("a=%d, b=%d", a, b);
                           ^
sample.c:6:27: 注意: 各未宣言な識別子はそれが使用される関数ひとつにつき一度だけ報告されます

エラーは6行目27文字目における変数bが未宣言だと言っています。つまり「int b;」のような宣言がないと、このプログラムは動かないと言っているのです。もう一度プログラムを眺めてみると、たしかに変数bに関する宣言はありません。これが動かない原因のようです。プログラムを修正してみましょう。

#include <stdio.h>

int main(void) {
  int a = 0;
  int b = 1;

  printf("a=%d, b=%d", a, b);
  return 0;
}

これでコンパイルに通り、動作するようになりました!

しかしこういった宣言ミスはなかなか起こりません。未宣言エラーは実際には以下のようなケースが多いと思われます:

#include <stdio.h>

int main(void) {
  char user_name[] = "Koto Furumiya";

  printf("%s\n", user_nama);
  return 0;
}

このプログラムにおいても同様のエラーが発生します。

$ gcc sample.c
sample.c: In function 'main':
sample.c:6:18: error: 'user_nama' undeclared (first use in this function)
   printf("%s\n", user_nama);
                  ^~~~~~~~~
sample.c:6:18: note: each undeclared identifier is reported only once for each function it appears in

6行目18文字目で未宣言エラーが発生しています。この原因は非常に単純で、単なるタイプミスです。user_name変数を使用しようとしているのに、user_nama変数を表示しようとして、エラーになっています。

これはなかなか気づきません!しかしエラーメッセージをしっかり見ていれば、user_name変数を使用しているつもりなのに、変数が存在しないとエラーが表示される、つまりタイプミスをしている可能性があるのでは、と気づくことができます。

他の例も見てみましょう。例えば以下のようなプログラムがあるとします:

#include <stdio.h>

int main(void) {
  pirntf("%s\n", "Hello, world!");
  return 0;
}

このときgccのコンパイルでは以下のようなエラーが出ます:

$ gcc sample.c
sample.c: In function 'main':
sample.c:4:3: warning: implicit declaration of function 'pirntf' [-Wimplicit-function-declaration]
   pirntf("%s\n", "Hello, world!");
   ^~~~~~
Undefined symbols for architecture x86_64:
  "_pirntf", referenced from:
      _main in ccJZPnNt.o
ld: symbol(s) not found for architecture x86_64
collect2: error: ld returned 1 exit status

これを訳してみます。

$ gcc sample.c
sample.c: 'main'関数において
sample.c:4:3: 警告: 関数'pirntf'の暗黙的な宣言 [-Wimplicit-function-declaration]
   pirntf("%s\n", "Hello, world!");
   ^~~~~~
アーキテクチャx86_64に対する未定義のシンボル:
  "_pirntf", 以下から参照される:
      _main in ccJZPnNt.o
ld: アーキテクチャx86_64に対するシンボルが見つかりません
collect2: エラー: ldは終了ステータス1を返しました

今回はエラーがちょっと複雑ですね。なにやら「暗黙的」にprintf関数を使用していると警告されているようです。暗黙的ってなんだろう?なぜ?

エラーには専門的な言葉がたくさん出てくるので、全部を読む必要はありません。とりあえずprintf関数になにかがあるとわかれば、それで大丈夫です。……よくみてみるとprintfがpirntfとタイプミスされていて、そんな関数は存在しないと怒られているようです。これも簡単に修正することができますね。

次に以下のようなプログラムとエラーを見てみます:

#include <stdio.h>

int main(void) {
  for(int i = 0; i < 10; i++) {
    printf("%d\n", i);
  
  return 0;
}
$ gcc sample.c
sample.c: In function 'main':
sample.c:8:1: error: expected declaration or statement at end of input
 }
 ^

これは8行目に’}’ではなく、宣言(declaration)か式(statement)が期待されているというエラーです。こういった場合には大抵字面通りに捉えると混乱します。‘{‘‘}’などの括弧類が絡んでいるときはたいていの場合、文の開き忘れ・閉じ忘れです。この場合はプログラムをよく読むとfor文を閉じ忘れていることに気がつきます。

このように、エラーメッセージをよく読むことで、どこにミスがあるのか、どんなミスをしているのか、ある程度予測がつくようになります。難しい言葉もたくさん出てくるので、エラーメッセージ全部を読む必要はありませんが、どこでエラーが起きているのかぐらいは見ておくと、楽になります。

プログラムを読みやすくする

膨大な数のエラーを乗り切って、ついにプログラムが動き出します!しかし今度は思った通りに動作しないのです。出力される値はぐちゃぐちゃで、全くのデタラメです。なぜこうなってしまうのでしょうか。どうすればいいのでしょうか。

意図しないプログラムの動作のことをバグといいます。バグは我々を苦しめる最大の要素で、プロのプログラマですら避けては通れぬ道なのです。しかしどうやってバグと戦っていけばいいのでしょうか。

ひとつの効果的な戦法としては、わかりやすいプログラムを書くという方法があります。プログラムは機械が読むだけではなく、人間が読むものでもあります。わかりやすいプログラムを書けば、それだけ人間のミスを減らし、バグを減らすことにつながります。

プログラムというのは書きながら何度も読みますし、将来的に昔書いたプログラムを読み返すこともあります。そのときに読みやすく書けていれば理解の助けになりますし、バグにも気づきやすくなります。どうすればいいのかわからなくなったときは、とにかく「読みやすい」「綺麗な」プログラムを目指せばいいでしょう。そうすれば、きっとバグの原因も見つかります。

わかりやすい変数名をつける

読みやすいプログラムを書く方法の一つとして、変数にわかりやすい名前をつけるというものがあります。a, b, cやx, y, zなど、短くわかりにくい変数名をつける人が多いですが、もう少しわかりやすい名前にすると、プログラムを読みやすくなります。

簡単な例を挙げてみましょう。以下のプログラムはBMIを計算するプログラムです:

#include <stdio.h>

int main(void) {
  double a = 160.0;
  double b = 50.0;
  double c = b / (a * a);

  printf("%g\n", c);

  return 0;
}

恐ろしいことに、この短いプログラムの中にすでにバグがあります!期待する出力は19.5ですが、実際に出力されるのは0.00195になります。いったいなぜでしょう。

これはプログラムを眺めていても解決しません。原因はもっと根源的な部分にあります。そもそもこのプログラムは、なんの説明もなしにいきなり出されると、なにをしているのかさっぱりわかりません。つまりこれはわかりにくい、悪いプログラムということになります。

これを書き換えてみましょう。変数の名前を変えて、以下のようにしてみます:

#include <stdio.h>

int main(void) {
  double height_meter = 160.0;
  double weight_kg = 50.0;
  double bmi = weight_kg / (height_meter * height_meter);

  printf("%g\n", bmi);

  return 0;
}

これは変数の名前を書き換えただけです。しかしよくみてみると……ああ!身長160メートル!そうです、BMIの計算では、身長はメートルを使うことになっています。つまり、身長の160.0の部分を、1.6に書き換えることで、正しい出力を得ることができます。

このようにして、変数名にしっかりした名前をつけることで、バグを未然に発見することが楽になります。特に古い教科書においては、変数名はaやb、xやyなどといった短いものが使用されることが多いですが、現代では特に短くする必要はありません。どんどん長い変数名をつけていきましょう。

変数名を工夫するコツは、必要な情報を名前に加えることです。例えば上の例では「単位」が重要な要素となりました。単位は間違って入力しやすいので、変数名に単位の情報を加えてやると安心です。

ループに使う変数名も同じです。例えば行列の要素を表示する、以下のようなプログラムがあるとします:

#include <stdio.h>

int main(void) {
  double matrix[3][3] = {
    {1.0, 2.0, 3.0},
    {4.0, 5.0, 6.0},
    {7.0, 8.0, 9.0}
  };

  for(int i = 0; i < 3; i++) {
    for(int j = 0; j < 3; i++) {
      printf("%g ", matrix[i][j]);
    }
    printf("\n");
  }

  return 0;
}

これもバグを含んでいます。実行すると、Segmentation faultと表示されます。これは内側のループでj++とするところをi++としているためです。ループを扱う時に、変数名に「i」や「j」を使うのはいいのですが、1文字だけだと、どうしても間違いやすくなります。もっと長い変数名をつけてみましょう。こういった行列の操作のときには、「row(行)」や「column(列)」といった変数名にするといいでしょう。

#include <stdio.h>

int main(void) {
  double matrix[3][3] = {
    {1.0, 2.0, 3.0},
    {4.0, 5.0, 6.0},
    {7.0, 8.0, 9.0}
  };

  for(int row = 0; row < 3; row++) {
    for(int column = 0; column < 3; column++) {
      printf("%g ", matrix[row][column]);
    }
    printf("\n");
  }

  return 0;
}

これでプログラムがわかりやすくなり、バグも発見しやすくなります。内側のループでrow++としていたら明らかに間違いだとわかるでしょう!

コメントを入れる

変数を工夫することでプログラムの流れをわかりやすくすることができます。しかし変数名だけでは限界があります。そこでコメントの出番です。コメントを使うことで、プログラムに関係のないメモを書くことができます。

コメントは特にプログラムが大規模になってきたときに有効です。プログラムの処理の流れを読むとき、5行程度のプログラムなら、なんとか大まかな流れは読めますが、20行などになってくると、なかなか難しくなります。そんなときにコメントがあると、とても読みやすくなります。

実習の授業ではコメントを書かないという人が大多数ですが、できればコメントを書く癖をつけておきましょう。特にプログラミングに触れたてのときは、わかりやすい、綺麗なプログラムを書くことは困難です。試行錯誤しつつ行き当たりばったりで書くので、どうしてもぐちゃぐちゃしたプログラムになります。なので、プログラムをある程度綺麗に描こうと努力するのはもちろんですが、コメントを使ってプログラム全体の見通しを良くする方法も身につけておきましょう。

例えば以下のような読みにくい、長いプログラムがあるとしましょう:

#include <stdio.h>

int main(void) {
  int input_number;
  printf("Input a number -> ");
  scanf("%d", &input_number);

  int input_is_prime;

  if(input_number <= 1) { input_is_prime = 0; }
  else if(input_number == 2) { input_is_prime = 1; }
  else if(input_number % 2 == 0){ input_is_prime = 0; }
  else {
    for(int n = 3; n < input_number; n+=2) {
      if(input_number % n == 0) {
        input_is_prime = 0;
        break;
      }
    }
  }

  if(input_is_prime) {
    printf("%d is a prime number.\n", input_number);
  } else {
    printf("%d is not a prime number.\n", input_number);
  }

  return 0;
}

何をしているのか、非常にわかりづらいプログラムです。実はこれは、入力された数値が素数かどうかを判定するプログラムです。括弧がたくさんあって読みにくいですね。しかし頑張って工夫しても、なかなかシンプルになりません。

そこでコメントの出番になります。例えば以下のようにコメントを入れてみましょう:

#include <stdio.h>

int main(void) {
  // 数値を入力してもらう。
  int input_number;
  printf("Input a number -> ");
  scanf("%d", &input_number);

  // 入力された数値が素数がどうかを調べる。
  // ・入力が1以下のとき、素数ではない。
  // ・入力が2のとき、素数。
  // ・入力が偶数のとき、素数ではない。
  // ・入力がそれ以外のとき、input_number未満の奇数で割って素数かどうか調べる。
  int input_is_prime = 1;
  
  if(input_number <= 1) { input_is_prime = 0; }
  else if(input_number == 2) { input_is_prime = 1; }
  else if(input_number % 2 == 0) { input_is_prime = 0; }
  else {
    // n = 3、5、7、9、…で割って調べる。
    // ただしnはinput_number未満の数とする。
    for(int n = 3; n < input_number; n+=2) {
      // 割り切れたら素数ではない。
      if(input_number % n == 0) {
        input_is_prime = 0;
        break; // for文を抜け出す。
      }
    }
  }

  // 入力が素数がどうかによって表示を分ける。
  if(input_is_prime) {
    printf("%d is a prime number.\n", input_number);
  } else {
    printf("%d is not a prime number.\n", input_number);
  }

  return 0;
}

比較的読みやすくなりました。完璧とまではいきませんが、ある程度の流れは追いやすくなっているはずです。

この例では既に完成したプログラムにコメントを入れていきましたが、実際にはコメントを書きながらプログラムを書いて行く形になると思います。まず初めに大まかなコメントを入れて、それから書き始めるといいでしょう。例えば以下のような形からスタートすると良いと思います:

#include <stdio.h>

int main(void) {
  // 数値を入力してもらう。

  // 入力された数値が素数がどうかを調べる。

  // 入力が素数かどうかによって表示を分ける。

  return 0;
}

これは空っぽのプログラムですが、コメントがあることでだいたいの流れが作られています。あとは流れに沿って書いていくだけなので、非常に簡単になります。

コメントは、長いプログラム以外でも、もっと細かい規模で使うこともできます。例えば以下のようなプログラムの一部を考えましょう:

char user_name[20];

このとき、なぜ20文字なのでしょうか。5文字ではダメなのでしょうか。100文字にしてはいけないのでしょうか。少し不安になります。

しかしここにコメントがあったとします。例えば以下のようにです:

char user_name[20]; // 日本人の名前なら20文字もあれば十分入る。

このコメントから、20という数字は考えて決められているとわかります。5ではおそらく少ないのでしょうし、100では大きすぎるのでしょう。

こういった風に、コメントでは、「この部分は何か(What)」よりも、「この部分はなぜこうなっているか(Why)」を書いた方が役立つと、世間一般では言われています。あまりこの説に縛られるのも毒ですが、ひとつの方針として心の中に持っておくと、あなたの助けになるでしょう。

例えば以下のようなプログラムを書いたとします:

#include <stdio.h>

int main(void) {
  int array[] = {10, 2, 4, 8, 6, 3, 1, 14, 13, 11};
  int length = 10;

  // バブルソート。配列をソート(並び替え)する。
  // バブルソートは効率が悪く処理時間が非常に長くかかるアルゴリズムだが、
  // 今回は10要素程度なのでバブルソートでも一瞬で終わる。
  // もっと多い10000などの要素数になったら他のアルゴリズムを検討した方が良い。
  for(int i = 0; i < length - 1; i++) {
    for(int j = length - 1; j > i; j--) {
      if(array[j - 1] > array[j]) {
        int temp = array[j - 1];
        array[j - 1] = array[j];
        array[j] = temp;
      }
    }
  }

  // ソートした配列の中身を表示する。
  for(int index = 0; index < length; index++) {
    printf("%d ", array[index]);
  }
  printf("\n");

  return 0;
}

これは配列をソート(並び替え)するプログラムです。アルゴリズムにはバブルソートというものを採用しています。そしてコメントには「なぜ(Why)」が書かれています。コメントから、このソート方法は遅いが、今回の場合は全く問題ないということがわかります。また、1万要素の配列をソートする場合はこのアルゴリズムは避けた方がいいということもわかります。プログラムを読んだだけではわからない情報が、コメントからたくさん読み取ることができます。

このようにして、プログラムにコメントをつけることで、追加の情報を付与して、読み手(ここでは自分自身)にとってわかりやすく仕上げることができます。

もっとコメントを入れる

先ほどもコメントについて言及しましたが、一般論についてでした。大学の実習授業においては、コメントにはもっと違った役割があります。

例えば一般的には以下のコメントは価値のないものとされています:

i++; // iを1増やす。i = i + 1と同じ。

このコメントに価値がないとされるのは、これがプログラマにとって常識だからです。i++がiを1増やすのは当たり前のことで、このコメントはなんら新しい情報をもたらさないからです。

しかしプログラミング初心者にとっては違います。i++がiを1増やすことは当たり前ではありません。全く新しい概念です。ですから、このコメントにも価値があります。あなたの理解の助けになるのですから。

つまり、以下のようなコメントを書いてもいいということです:

// これは、i = 0を初期値として、i < 3のとき、
// iを1ずつ増やして実行することを意味する。
// つまり、i = 0のとき、i = 1のとき、i = 2のときについて、
// それぞれprintf("%d\n", i)を実行する。
for(int i = 0; i < 3; i++) {
  printf("%d\n", i);
}

このコメントにも価値があります。for文に初めて出会った場合、それが何をしているのかというのは一見してわかりません。そのときコメントがあれば、理解の助けになります。

授業内で使用するコメントは、あまり一般論に縛られる必要はありません。ノートがわりに様々なことを書いてもいいですし、思ったことをそのまま書いてもかまいません。とにかく何かしらの情報をプログラムに付け加えるということが重要になります。

役に立つ情報になるのなら、例えば以下のようなコメントでもかまいません:

// 出た!悪魔の二重ループ!
// なぜ動いているのか、原理はよくわからない。
// とにかく行列の中身をすべて表示してくれる。
for(int row = 0; row < 3; row++) {
  for(int column = 0; column < 3; column++) {
    printf("%d ", matrix[row][column]);
  }
  printf("\n");
}

このコメントからは、あなたが「よくわかっていない」ということがわかります。しかしこれは「正常に動く」ということもわかります。これもコメントとして有益です。何も書かないよりかは、はるかに多くの情報を得ることができるのですから。

コメントは「うまく動いていない」部分についても有効です。宿題課題のプログラムを書いていて、うまく解けずにふて寝して朝起きたら、普通の人間は、何があったか全く忘れてしまいます。そんなときにもコメントがあると、何がうまくいっていないのか、思い出すことができます。

double input;
scanf("%d", &input);
printf("%d", input); // 全く違う数字が表示される…

こうしていれば、翌日の朝になっても、どこが動かなかったのか思い出すことができますし、スムーズに作業を再開できるはずです。

コメントを書くときに、あまり「かっこよくしよう」だとか、「ダサくないコメントを書こう」だとか、そういうことを気にする必要はありません。とにかく追加の情報となるものなら、何でも書いていいのです。何もコメントを書いていないよりかは、かっこ悪いコメントが書いてある方が、有益で、わかりやすいのですから。

ただし、実習授業内において、の話です。学校の外部に出すプログラムなどでは、こういうコメントは控えましょう。

printfデバッグをする

変数の名前などを工夫して、コメントも入れて、プログラムの流れをわかりやすくしても、それでもなおバグというものは多く発生するものです。これまでは遠回りしてきましたが、そろそろバグに直接立ち向かうための武器が必要です。

バグに直接対処する方法のひとつとして、printfデバッグと呼ばれるものがあります。これは名前の通り、printf関数を使って値を出力し、デバッグ(バグ取り)をする手法です。単純な方法ですが、実に効果的です。

実例を見てみましょう。以下のようなプログラムがあるとします:

#include <stdio.h>

int main(void) {
  double input;

  // 値を入力してもらう。
  printf("Input a number -> ");
  scanf("%d", &input);

  // 3乗を計算して表示する
  double cube = input * input * input;
  printf("%g\n", cube);

  return 0;
}

こんなに簡単なプログラムなのに、バグが潜んでいます。これを実行して、値を入力しても、でたらめな値が出力されるだけです。

$ ./a.out 
Input a number -> 10
0

このとき、どこが悪いのかを調べてみましょう。やり方は簡単で、printf関数を使って、とりあえず値を出力してみるのです。今回は「そもそもinputを正確に受け取れているか?」という点が怪しそうです。一度input変数を表示してみましょう。

#include <stdio.h>

int main(void) {
  double input;

  // 値を入力してもらう。
  printf("Input a number -> ");
  scanf("%d", &input);

  // デバッグ用
  printf("input = %g\n", input);

  // 3乗を計算して表示する
  double cube = input * input * input;
  printf("%g\n", cube);

  return 0;
}

デバッグ用のprintf関数を追加しました。これで実行してみます。

$ ./a.out 
Input a number -> 10
input = 6.95314e-310
0

inputの値がものすごいことになっています!つまり、これはinput変数を正確に受け取れていないということです。よって原因はscanf関数にある可能性が高いです。scanf関数について、教科書などで調べてみましょう。すると、double型で受け取る時には、「%d」ではなく「%lf」を使用するということがわかりました。さっそく修正してみましょう。

#include <stdio.h>

int main(void) {
  double input;

  // 値を入力してもらう。
  printf("Input a number -> ");
  scanf("%lf", &input);

  // デバッグ用
  printf("input = %g\n", input);

  // 3乗を計算して表示する
  double cube = input * input * input;
  printf("%g\n", cube);

  return 0;
}
$ ./a.out 
Input a number -> 10
input = 10
1000

これで正確に動きました!あとは不要になったデバッグ用のprintf関数をコメントアウトしておきましょう。全く消してしまうよりかは、後のためにコメントとして残しておく方が無難です。

#include <stdio.h>

int main(void) {
  double input;

  // 値を入力してもらう。
  printf("Input a number -> ");
  scanf("%lf", &input);

  // デバッグ用
  // printf("input = %g\n", input);

  // 3乗を計算して表示する
  double cube = input * input * input;
  printf("%g\n", cube);

  return 0;
}

このようにして、printf関数を用いて途中の値を出力することで、バグの原因を特定し、修正することができました。

printfデバッグは簡単で強力な手法です。悩んだときには頼ってみましょう。課題で行き詰ったときに、プログラムとひたすらにらめっこしている学生もたびたび見受けられますが、そういうときにはとりあえず手を動かしてprintfデバッグしてみた方が解決できることが多いです。

もうひとつ例を見てみましょう。以下のプログログラムは、1^2+2^2+3^2+…+input_number^2を計算するものです:

#include <stdio.h>

int main(void) {
  // 数値を入力してもらう。
  int input_number;
  printf("Input a number -> ");
  scanf("%d", &input_number);

  // 1^2 + 2^2 + 3^2 + ... + input_number^2を計算する。
  int total = 0;
  for(int i = 0; i < input_number; i++) {
    total *= i * i;
  }

  // 結果を表示する。
  printf("%d\n", total);

  return 0;
}

しかしこれを実行しても「0」としか表示されません。何がいけないのでしょうか。printfデバッグで調べてみましょう。

#include <stdio.h>

int main(void) {
  // 数値を入力してもらう。
  int input_number;
  printf("Input a number -> ");
  scanf("%d", &input_number);

  // 1^2 + 2^2 + 3^2 + ... + input_number^2を計算する。
  int total = 0;
  for(int i = 0; i < input_number; i++) {
    total *= i * i;

    // デバッグ用
    printf("i = %d\n", i);
    printf("i * i = %d\n", i * i);
    printf("total = %d\n\n", total);
  }

  // 結果を表示する。
  printf("%d\n", total);

  return 0;
}

デバッグ用にprintfを挿入しました。ループ計算の時は、「ループに使った変数i」「使用した計算」「結果を代入した変数」の3つを出力すれば、だいたい問題が絞り込めます。さらに色々な処理をしているなら、それも全部出力してみると原因を特定しやすくなるかもしれません。

これを実行してみましょう。

$ ./a.out 
Input a number -> 3
i = 0
i * i = 0
total = 0

i = 1
i * i = 1
total = 0

i = 2
i * i = 4
total = 0

0

出力結果から、「i*iに問題はない」「totalに代入する時に0になっている」ことがわかります。よってtotalの代入式をよく見てみましょう……よく見ると「+=」とすべきところが「*=」となっています!これでは何度やっても0になるのは仕方ありません。

そしてさらにもうひとつのバグも見つかります。iは「1からinput_numberまで」のはずですが、出力を見ると「0からinput_number-1まで」となっており、ひとつずれています。これは変数の初期化とループ条件が間違っているということです。つまり、「i = 0」の部分を「i = 1」に、「i < input_number」の部分を「i <= input_number」に修正することで直すことができます。

#include <stdio.h>

int main(void) {
  // 数値を入力してもらう。
  int input_number;
  printf("Input a number -> ");
  scanf("%d", &input_number);

  // 1^2 + 2^2 + 3^2 + ... + input_number^2を計算する。
  int total = 0;
  for(int i = 1; i <= input_number; i++) {
    total += i * i;

    // デバッグ用
    //printf("i = %d\n", i);
    //printf("i * i = %d\n", i * i);
    //printf("total = %d\n\n", total);
  }

  // 結果を表示する。
  printf("%d\n", total);

  return 0;
}

これで修正できました!実行してみると、正しい出力が得られます。

このようにして、printfデバッグをうまく使えば、簡単にバグの特定ができます。自分が書いたプログラムがうまく動かないときは、画面を眺めてうんうん唸るよりも、とりあえずprintfデバッグに手を出して見ることをおすすめします。

[上級者向け]中間変数を使う

授業が先に進んで来ると、プログラムがどんどん複雑化していきます。複雑なプログラムは流れが追いにくく、バグが発生しても気づくことが難しくなります。

複雑なプログラムは、中間変数を使うことで、ある程度わかりやすくできます。わかりやすいプログラムを書くことができれば、バグの発見が簡単になります。

例えば以下のようなプログラムがあるとします:

#include <stdio.h>
#include <math.h>

int main(void) {
  double p1_x = 1.0;
  double p1_y = 1.0;
  double p2_x = 2.0;
  double p2_y = 2.0;

  // 2点間の距離を計算する。
  double distance = sqrt((p1_x - p2_x) * (p1_x - p2_x) + (p1_y - p2_y) * (p1_y - p2_y));

  printf("%g\n", distance);

  return 0;
}

このプログラムは点(1, 1)と点(2, 2)の2点間の距離を計算するもので、実行すると「1.41421」と表示されます。しかしなかなか大規模な計算式になっていて、タイプミスなどをしてしまいそうです。このような部分はバグの温床となります。

こういったときには中間変数を増やして、いくつかに分割してやると読みやすくなります。

#include <stdio.h>
#include <math.h>

int main(void) {
  double p1_x = 1.0;
  double p1_y = 1.0;
  double p2_x = 2.0;
  double p2_y = 2.0;

  // 2点間の距離を計算する。
  double diff_x_square = (p1_x - p2_x) * (p1_x - p2_x);
  double diff_y_square = (p1_y - p2_y) * (p1_y - p2_y);
  double distance = sqrt(diff_x_square + diff_y_square);

  printf("%g\n", distance);

  return 0;
}

動作は変わりませんが、だいぶ読みやすくなります。もっと細かく分けてもいいかもしれませんが、そこは個人の裁量次第です。

中間変数の使用は、if文においても有効です。例えば以下のようなプログラムがあるとします:

#include <stdio.h>

int main(void) {
  char input_char = 'c';

  // input_charがアルファベットあるいは数であることを確かめる。
  if((input_char >= 'a' && input_char <= 'z') ||
     (input_char >= 'A' && input_char <= 'Z') ||
     (input_char >= '0' && input_char <= '9')) {
    printf("%c is an alphabet or digit.\n", input_char);
  } else {
    printf("%c is neither an alphabet nor digit.\n", input_char);
  }

  return 0;
}

これは文字がアルファベットあるいは数であることを確かめるプログラムですが、if文の中がすごいことになっています。こういった巨大な条件式は、プログラムを書いているときは大丈夫なのですが、あとで読む時になると、なにをやっているのか少しわかりにくいです。

これも中間変数を用いることで読みやすくすることができます。

#include <stdio.h>

int main(void) {
  char input_char = 'c';

  int input_is_lowercase = (input_char >= 'a' && input_char <= 'z');
  int input_is_uppercase = (input_char >= 'A' && input_char <= 'Z');
  int input_is_alphabet = (input_is_lowercase || input_is_uppercase);

  int input_is_digit = (input_char >= '0' && input_char <= '9');

  // input_charがアルファベットあるいは数であることを確かめる。
  if(input_is_alphabet || input_is_digit) {
    printf("%c is an alphabet or digit.\n", input_char);
  } else {
    printf("%c is neither an alphabet nor digit.\n", input_char);
  }

  return 0;
}

だいぶすっきりしました。変数の数は大幅に増えましたが、if文自体はシンプルになりました。

[上級者向け]関数を活用する

基礎課題をすべて提出して、応用課題などに手を出し始めると、プログラムが非常に複雑になってきます。複数の処理が入り乱れるからです。そういったときは関数を利用して処理を分割すれば、プログラムをシンプルに保つことができます。

しかし、関数の利用は、躊躇する学生が多いようです。それは「関数」という概念自体が難しいからでしょう。関数を用いて解決できる問題よりも、関数を用いることで発生する問題の方が多いとなると、なかなか手を出せません。それでも、関数というものは非常に便利なので、もし余裕があれば、ぜひ利用してみてください。

関数を使うと簡単になる例を紹介します。例えば以下のような複雑怪奇なプログラムがあるとします:

#include <stdio.h>

int main(void) {
  int matrix[2][2] = {
    {2, 3},
    {5, 7}
  };

  // 行列のすべての要素が素数かどうか調べる。
  int matrix_is_prime = 1;
  for(int row = 0; row < 2; row++) {
    for(int column = 0; column < 2 ; column++) {
      int value = matrix[row][column];
      if(value <= 1) { matrix_is_prime = 0; }
      else if(value > 2 && value % 2 == 0) { matrix_is_prime = 0; }
      else {
        for(int n = 3; n < value; n += 2) {
          if(value % n == 0) { matrix_is_prime = 0; }
        }
      }
    }
  }

  if(matrix_is_prime) {
    printf("prime\n");
  } else {
    printf("not prime\n");
  }

  return 0;
}

これは非常に読みにくいプログラムで、何をやっているのか、さっぱりわかりません。しかしこのままだとおそらく大量のバグに悩まされることになります。何とかしないといけません。

そこで関数を用いて処理を分割すれば、プログラムが綺麗になります。主に処理が複雑な部分を関数にすれば、だいたい綺麗になります。今回は素数判定部分を切り出せば簡単になりそうな気がします。実際にやってみましょう。

#include <stdio.h>

// 素数ならば1を返す。
// 素数でなければ0を返す。
int is_prime(int num) {
  if(num <= 1) { return 0; }
  else if(num == 2) { return 1; }
  else if(num % 2 == 0) { return 0; }
  else {
    for(int i = 3; i < num; i += 2) {
      if(num % i == 0) { return 0; }
    }
  }

  return 1;
}

// 素数でなければ1を返す。
// 素数であれば0を返す。
int is_not_prime(int num) {
  return !is_prime(num);
}

int main(void) {
  int matrix[2][2] = {
    {2, 3},
    {5, 7}
  };

  // 行列のすべての要素が素数かどうか調べる。
  int matrix_is_prime = 1;
  for(int row = 0; row < 2; row++) {
    for(int column = 0; column < 2 ; column++) {
      if(is_not_prime(matrix[row][column])) {
        matrix_is_prime = 0;
      }
    }
  }

  if(matrix_is_prime) {
    printf("prime\n");
  } else {
    printf("not prime\n");
  }

  return 0;
}

だいぶ綺麗になりました!まだまだ2重ループなどがあり複雑ではありますが、先ほどよりかは比較的読みやすいはずです。このように、関数で処理を切り出し分割することで、プログラムを綺麗に保つことができます。

関数は奥が深く、様々な場面で利用することができます。関数については、記事「JavaScriptの関数で何ができるのか、もう一度考える」でも詳しく解説しているので、そちらも読んでみてください。

まとめ

  • 実習において気をつけたいこと
    • 積極的に質問する
      • 自分で考えることも大事ですが、せっかくなのですから先生やTAに質問してみましょう。
    • 本やネットで調べる
      • 複数の本を参照したり、ネットの質問サイトを活用してみましょう。
  • プログラミングにおいて気をつけたいこと
    • エラーメッセージを読む
      • エラーメッセージには多くの情報が含まれています。あなたの助けになるかもしれません。
    • プログラムを読みやすくする
      • 読みやすいプログラムはバグを減少させます。できるだけ読みやすく書いてみましょう。
    • わかりやすい変数名をつける
      • 変数名がわかりやすければ、ミスも見つけやすくなります。
    • コメントを入れる
      • コメントがあれば、プログラムを読むだけではわからない情報を手に入れることができます。
    • もっとコメントを入れる
      • あまりかっこよくはないコメントでも実習授業では大いに役立ちます。なんでもコメントに書いてみましょう。
    • printfデバッグをする
      • printfで途中の値を出力すれば、バグの原因が何か見えてくるかもしれません。
    • [上級者向け]中間変数を使う
      • 中間変数を使えば、複雑な式を複数に分割することができます。
    • [上級者向け]関数を活用する
      • 関数を使えば、長いプログラムを細かく分けることができます。