デバッグコードはバグを検出するための仕組みだと分かったところで、実戦でも使える書き方を練習しましょう。ここまで例として使ってきた次の関数を、今回も使うことにします。
/* 整数の配列を小さい順にソートする。
* - pNumbers: ソートする配列の先頭アドレス
* - numberCount: ソートする要素数
* pNumbersが0(NULL)の場合の動作は未定義。
*/
void SortNumbers(int *pNumbers, size_t numberCount) {
/* ここに、ソートの実装 */
}
/* 整数の配列を小さい順にソートする。
* - pNumbers: ソートする配列の先頭アドレス
* - numberCount: ソートする要素数
* pNumbersが0(NULL)の場合の動作は未定義。
*/
void SortNumbers(int *pNumbers, size_t numberCount) {
/* バブルソート */
int moves = -1;
while (moves != 0) {
moves = 0;
for (size_t i=1; i<numberCount; i++) {
if (pNumbers[i - 1] > pNumbers[i]) {
int temp = pNumbers[i - 1];
pNumbers[i - 1] = pNumbers[i];
pNumbers[i] = temp;
moves += 1;
}
}
}
}
バブルソートは、先頭から順に隣り合う値を比較して、大小関係が逆だったら入れ替えていくソートです。この操作を繰り返し、入れ替えが1度も発生しなくなればソート完了です。値がフワフワと少しずつ移動していく様子が泡のようなので、「バブルソート」と呼ばれています。
ソートのアルゴリズムは今回の主題ではないので詳しい説明は省きますが、上の例は実際に動作するC言語の関数です。ぜひ、練習に使ってみてください。
バグが目立つようにする
#include <stdio.h>
#include <stdlib.h>
/* 整数の配列を小さい順にソートする。
* - pNumbers: ソートする配列の先頭アドレス
* - numberCount: ソートする要素数
* pNumbersが0(NULL)の場合の動作は未定義。
*/
void SortNumbers(int *pNumbers, size_t numberCount) {
/* 引数をチェック */
if (pNumbers) {
; /* OK */
} else {
; /* NG(pNumbersが0になっている) */
}
/* バブルソート */
int moves = -1;
while (moves != 0) {
moves = 0;
for (size_t i=1; i<numberCount; i++) {
if (pNumbers[i - 1] > pNumbers[i]) {
int temp = pNumbers[i - 1];
pNumbers[i - 1] = pNumbers[i];
pNumbers[i] = temp;
moves += 1;
}
}
}
/* 結果をチェック(配列の要素が小さい順に並んでいるか確認する) */
for (size_t i=1; i<numberCount; i++) {
if (pNumbers[i - 1] <= pNumbers[i]) {
; /* OK */
} else {
; /* NG(期待どおりにソートできていない) */
}
}
}
int main(void) {
/* ソートを実行 */
int numbers[] = {56, 0, -34, 2, 976, 2, 5, 55, -20, 48, 16};
size_t count = sizeof(numbers) / sizeof(int);
SortNumbers(numbers, count);
return EXIT_SUCCESS;
}
printf
でエラーメッセージを表示するとか?
abort()
、C++を使っているならterminate()
を使えばいいわね。1つやってみてくれる?
/* 引数をチェック */
if (pNumbers) {
; /* OK */
} else {
printf("NG:pNumbersが0です!\n");
abort();
}
ここまでできたら、実際にバグがあった場合に何が起こるのか体験しておくといいでしょう。やり方は、実際に「NG」になる呼び出し方をしてみるだけです。
今回の例では、第1引数を0(NULL
)にしてSortNumbers()
を呼び出してみればいいですね。実行すると、エラーメッセージが表示されて、プログラムが停止する様子を確認できます。
これで、バグが検出されたことが目立つようになりました。それがどのようなバグなのかも、メッセージを読めば分かります。でも、このメッセージの内容は、直前にあるif
文の条件式を見れば明らかですね。
そのため、ここで重要となる情報は「どのようなバグなのか」ではありません。代わりに、「どこで検出されたバグなのか」が表示されるように改良してみましょう。C言語では、__FILE__
と__LINE__
を使うのがおすすめです。
__FILE__
:いま見ているソースコードのファイル名を表すマクロ__LINE__
:いま見ているソースコードの行番号を表すマクロ
__FILE__
と__LINE__
の表示に直してみるわね。
/* 引数をチェック */
if (pNumbers) {
; /* OK */
} else {
printf("NG:%s (%d)\n", __FILE__, __LINE__);
abort();
}
/* 結果をチェック(配列の要素が小さい順に並んでいるか確認する) */
for (size_t i=1; i<numberCount; i++) {
if (pNumbers[i - 1] <= pNumbers[i]) {
; /* OK */
} else {
printf("NG:%s (%d)\n", __FILE__, __LINE__);
abort();
}
}
デバッグコードを開発中に限定する
ソフトウェア開発には、矛盾する2つの願いがあります。
- 開発中は問題があったら早く直したいので、バグは目立つようにしたい
- リリース後は、もしバグが残っていたとしても目立たないようにしたい
これは、ソフトウェアには2つのバージョンが必要だということを意味しています。「開発中のバージョン」と、「リリース用のバージョン」です。といっても、同じような開発を2回行わなければならないという意味ではありません。ソースファイルをコンパイルするときに、どちらのバージョンの実行ファイルを生成するか選べるようにするということです。
IDEを使っている人なら、開発中は「デバッグビルド」、最終的には「リリースビルド」を行えばいいと知っているのではないでしょうか。これは、2つのバージョンを選択するための仕組みです。
ここで、マクロをもう1つ覚えましょう。
NDEBUG
:リリース用のバージョンをコンパイルするときにだけ定義される
NDEBUG
は、C言語で標準的に利用できるマクロです。頭に「N」が付いているので、「Not DEBUG」という意味ですね。これと、条件付きコンパイルを組み合わせると、開発中バージョンにだけデバッグコードを付加できます。
具体的には、次のようにします。
#ifndef NDEBUG
ここにデバッグコード
#endif
ここで、#ifdef
ではなくて#ifndef
を使っている点に注目しましょう。こちらも「n」が付いているので、「if not defined」という意味ですね。つまり、「もしNDEBUG
が定義されていないなら」という条件により、開発中のバージョンのみでデバッグコードが有効になるのです。
#ifndef NDEBUG
」っていうお決まりのフレーズで、開発中にだけ有効なコードを埋め込めるということね。
/* 引数をチェック */
#ifndef NDEBUG
if (pNumbers) {
; /* OK */
} else {
printf("NG:%s (%d)\n", __FILE__, __LINE__);
abort();
}
#endif
/* 結果をチェック(配列の要素が小さい順に並んでいるか確認する) */
#ifndef NDEBUG
for (size_t i=1; i<numberCount; i++) {
if (pNumbers[i - 1] <= pNumbers[i]) {
; /* OK */
} else {
printf("NG:%s (%d)\n", __FILE__, __LINE__);
abort();
}
}
#endif
#ifndef NDEBUG
と#endif
で囲むだけでいいの。
このようなデバッグコードは、リリース用のバージョンでは「なかったこと」になります。そのため……
- たとえバグが残っていたとしても、目立ちにくくなる
という点のほかに、次のようなメリットも得られます。
- デバッグコードが、リリース後の実行速度を遅くする心配はない
- デバッグコードにより、リリース用の実行ファイルが大きくなる心配はない
条件付きコンパイルでデバッグコードを書くときは、注意すべき点もあります。それは、リリース後も必要となる機能の一部を、デバッグコードに含めてはならないということです。
もしも必要な機能を含めてしまうと、その部分がリリース用のバージョンで無視され、機能の欠損を招いてしまいます。デバッグコードはあくまでバグを検出するためのものと考え、それ以外のコードは含めないようにしましょう。