[前へ]   [目次へ]   [次へ]

四則演算版計算機 デバッグ編(C/C++)


前回の四則演算版計算機の残ったバグ探しのデバッグ編です。
実践では「バグがある」なんて教えてはもらえないので、
実際にバグってもらわないことにはバグは見つかりません。

しかし、正常に動作することを確認しているだけではバグを全部取れません。
バグ探しに変な入力をイロイロ試してみる必要がありますが、闇雲にやってもキリがありません。

実は、バグにはある程度パターンがあります。
このパターンを多く知ることは、そのまま、バグを発見するのも、原因を究明するのも早くなります。

今後、イロイロな小さなプログラムを作っていく予定ですが、
それらに毎回様々なパターンでバグを混ぜていこうと思います。
それを自力で探すことは難しいかもしれませんが、探し出せるようになれば、
未知のパターンに遭遇した時の対処が早くなります。

バグにパターンがあるといっても、決して少なくないですし、それにキレイに属さない亜種みたいなのもあります。
未知のパターンに遭遇する可能性は決して低くありませんので、これは結構重要です。

さて、そろそろ本題です。
以下は前回のソースです。
<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
< 15>
< 16>
< 17>
< 18>
< 19>
< 20>
< 21>
< 22>
< 23>
< 24>
< 25>
< 26>
< 27>
< 28>
< 29>
< 30>
< 31>
< 32>
< 33>
< 34>
< 35>
< 36>
< 37>
< 38>
< 39>
< 40>
< 41>
< 42>
< 43>
< 44>
< 45>
< 46>
< 47>
< 48>
< 49>
#include <stdio.h>

int main(void)
{
   
//変数宣言/定義
   
char buf[64];
   
int in1=0;//入力1格納変数
   
int in2=0;//入力2格納変数
   
int type=0;//演算種別格納用
   
int ans;//結果格納変数
   //入力1
   
printf("1つめの値を入力:");
   fgets(buf,63,stdin);
   sscanf(buf,
"%d",&in1);
   
//入力2
   
printf("2つめの値を入力:");
   fgets(buf,63,stdin);
   sscanf(buf,
"%d",&in2);
   
//演算種別入力
   
printf("演算種別を入力(1:+,2:-,3:*,4:/):");
   fgets(buf,63,stdin);
   sscanf(buf,
"%d",&type);
   
//計算処理(加算処理から変更)
   
ans=0;//ansは初期化していないので0を入れておく
   
switch(type){
   
case 1://加算
      
ans=in1+in2;
      
break;
   
case 2://減算
      
ans=in1-in2;
      
break;
   
case 3://乗算
      
ans=in1*in2;
      
break;
   
case 4://除算
      
ans=in1/in2;
      
break;
   
default://それ以外
      
puts("正しく入力されませんでした");
      
break;
   }
   
//出力処理
   
printf("結果:%d\n",ans);
   
//終了待ち
   
puts("Enterキーを押すと終了します。");
   getchar();
   
//終了
   
return 0;
まずは、初級編から。
ヒントとして「無限大」と言いましたが・・・

C/C++言語の整数型は基本的に「無限大」を表現できないため、
計算結果が「無限大」になると例外が発生します。

上のソースで無限大を生成する方法、お分かりでしょうか?
答えは、「0で割る」です。
除算は右辺値を左辺値から何度取れるか、という減算に置き換えることができます。
そうすると、右辺値が0、いくら0を減算しても左辺値は減りません。
0は基本的にあらゆる値から無限に取り出すことができるので、答えは無限大になります。
数学的には0で割るというのは禁止なんだそうです・・・

では、発動してみましょう。
上記のソースを実行して、以下のように入力します。

実行例:
1つめの値を入力:10
2つめの値を入力:0
演算種別を入力(1:+,2:-,3:*,4:/):4


ここまで入力すると異常終了してしまうはずです。
また、デバッガを使用している場合は36行目の位置で停止していることが分かるはずです。

異常終了した場合は、「 配列(C/C++) 」で解説した方法で「発生した問題」を調べてください。
前回は「0xc0000005」(アクセス違反)だった部分が今回は「0xc0000094」(0除算)になっていると思います。
これは、36行目の除算の右辺値が0だったため、答えが無限大になったことによります。

0による除算は一種の地雷であるため、注意が必要です。
特に、普通0は入らないだろう場所でたまたま0が入ってくると落ちるなどの現象が起きたりします。

修正策はただ一つ、右辺値が0の時に除算を実行しないように条件分岐することだけです。
右辺値が0だったら1など適当な値に書き換えたり、計算しないで進めるなどです。

今回は、エラーを表示して計算しないことで対処しようと思います。
<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
< 15>
< 16>
< 17>
< 18>
< 19>
< 20>
< 21>
< 22>
< 23>
< 24>
< 25>
< 26>
< 27>
< 28>
< 29>
< 30>
< 31>
< 32>
< 33>
< 34>
< 35>
< 36>
< 37>
< 38>
< 39>
< 40>
< 41>
< 42>
< 43>
< 44>
< 45>
< 46>
< 47>
< 48>
< 49>
< 50>
< 51>
< 52>
< 53>
< 54>
#include <stdio.h>

int main(void)
{
   
//変数宣言/定義
   
char buf[64];
   
int in1=0;//入力1格納変数
   
int in2=0;//入力2格納変数
   
int type=0;//演算種別格納用
   
int ans;//結果格納変数
   //入力1
   
printf("1つめの値を入力:");
   fgets(buf,63,stdin);
   sscanf(buf,
"%d",&in1);
   
//入力2
   
printf("2つめの値を入力:");
   fgets(buf,63,stdin);
   sscanf(buf,
"%d",&in2);
   
//演算種別入力
   
printf("演算種別を入力(1:+,2:-,3:*,4:/):");
   fgets(buf,63,stdin);
   sscanf(buf,
"%d",&type);
   
//計算処理(加算処理から変更)
   
ans=0;//ansは初期化していないので0を入れておく
   
switch(type){
   
case 1://加算
      
ans=in1+in2;
      
break;
   
case 2://減算
      
ans=in1-in2;
      
break;
   
case 3://乗算
      
ans=in1*in2;
      
break;
   
case 4://除算
      
if(in2){//(1)if
         
ans=in1/in2;
      }
      
else{//(2)in2が0
         
puts("0による除算はできません");
      }
      
break;
   
default://それ以外
      
puts("正しく入力されませんでした");
      
break;
   }
   
//出力処理
   
printf("結果:%d\n",ans);
   
//終了待ち
   
puts("Enterキーを押すと終了します。");
   getchar();
   
//終了
   
return 0;
36行目(1)は、条件分岐があります。
条件式は in2 だけですが、 条件分岐「if / else」 で解説したとおり、
数値を渡した場合は「0以外」が条件になります。
この条件をパスできるなら、 in2 が0でないことは保証されるので、除算をしても問題はありません。

39行目(2)は、36行目に対する else なので、 in2 が0の時に実行されます。
ここではエラーメッセージを表示して終わっておきます。
この時点で ans は switch 突入前に0に初期化したままなので、0です。
よって48行目の結果表示は0になります。

これらの条件分岐はそれぞれ1ステートメントしか処理がないので1行構文でも構いません。


さて、もう一つのバグ、上級編です。
ヒントとして「未初期化」を挙げました。
定義と同時に初期化していない変数は buf と ans しかないので、
このどちらかなのは分かるかと思います。

しかし、 buf も ans もすぐに fgets 関数や代入文で初期化されているように見えます。
これを初期化しないまま続ける方法・・・実はあります。
fgets 関数を失敗させるのです。
やり方は1文字目にファイル終端を表す「EOFマーカ」を入力します。(EOFは以前に一言だけ触れています( 1章「コンソール」 ))
fgets 関数は最初にEOFマーカに遭遇すると文字列を読み込まずに終了し、
ファイル終端またはエラー発生時の値である NULL を返します。

NULL は様々なところで出現してきますが、基本的に、「無効」「失敗」などの意で使われます。
これの英語読みは「ナル」ですが、 「Wikipedia」(外部リンク) によれば原典はドイツ語だそうで、ドイツ語読みでは「ヌル」だそうです。
当サイトではこれは「ナル」で統一しますが、両方は読みが違う同等のものであることは覚えておいてください。
また、文字列の時の「ナル文字」も同様で、「ヌル文字」と呼ばれることもあります。

さて、EOFは Ctrl+Z で入力することができます。やってみましょう。

実行例:
1つめの値を入力:^Z
2つめの値を入力:30
演算種別を入力(1:+,2:-,3:*,4:/):1
結果:30
Enterキーを押すと終了します。

1つめの値のところで Ctrl+Z を入力しました。(画面上は ^Z と表示)
・・・単に0として扱っただけのように見えます。
何が問題なのでしょうか?

実は、次にくる sscanf で10進整数として扱っているため、最初の文字が「数字」じゃないと発現しないんです。
数字が0だった場合も同様の結果なので、発現できる条件は9/256になります。
さらに、未初期化の時に入っている値は完全にランダムとはいえない上、
コンパイラによっては0等で埋め立てている場合があり、絶対に発現しない可能性もあります。

この例ではあまり問題になっていませんが、これを整数ではなく文字列として扱ったりすると
アクセス違反で落ちる可能性がぐ〜〜っと増えます。(未初期化の値に終端文字なんてついてませんから・・・)

こんなところでEOFなんて訳の分からない入力するような人間のために一々処理するのも面倒ですが、
インターネット等のように悪意を持った攻撃者がいるようなところからの入力はしっかり処理しておかないと、
セキュリティホールになります。(特にCGIは要注意、入力が容易なだけに見落としがちなのです)

対処は・・・初期化しておきましょう。
そうすればこのような現象は起きません。
ちなみに、上の条件で本当に fgets が buf を書き換えてないか調べたいなら、
適当な数字文字列で初期化しておけば、確実に現象を発現できます。

というわけで修正。
<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
< 15>
< 16>
< 17>
< 18>
< 19>
< 20>
< 21>
< 22>
< 23>
< 24>
< 25>
< 26>
< 27>
< 28>
< 29>
< 30>
< 31>
< 32>
< 33>
< 34>
< 35>
< 36>
< 37>
< 38>
< 39>
< 40>
< 41>
< 42>
< 43>
< 44>
< 45>
< 46>
< 47>
< 48>
< 49>
< 50>
< 51>
< 52>
< 53>
< 54>
#include <stdio.h>

int main(void)
{
   
//変数宣言/定義
   
char buf[64]="";//空文字列で初期化
   
int in1=0;//入力1格納変数
   
int in2=0;//入力2格納変数
   
int type=0;//演算種別格納用
   
int ans;//結果格納変数
   //入力1
   
printf("1つめの値を入力:");
   fgets(buf,63,stdin);
   sscanf(buf,
"%d",&in1);
   
//入力2
   
printf("2つめの値を入力:");
   fgets(buf,63,stdin);
   sscanf(buf,
"%d",&in2);
   
//演算種別入力
   
printf("演算種別を入力(1:+,2:-,3:*,4:/):");
   fgets(buf,63,stdin);
   sscanf(buf,
"%d",&type);
   
//計算処理(加算処理から変更)
   
ans=0;//ansは初期化していないので0を入れておく
   
switch(type){
   
case 1://加算
      
ans=in1+in2;
      
break;
   
case 2://減算
      
ans=in1-in2;
      
break;
   
case 3://乗算
      
ans=in1*in2;
      
break;
   
case 4://除算
      
if(in2){
         ans=in1/in2;
      }
      
else{
         puts(
"0による除算はできません");
      }
      
break;
   
default://それ以外
      
puts("正しく入力されませんでした");
      
break;
   }
   
//出力処理
   
printf("結果:%d\n",ans);
   
//終了待ち
   
puts("Enterキーを押すと終了します。");
   getchar();
   
//終了
   
return 0;

用意したバグは以上です。
ここでまだバグがあったりするとイロイロアレなのですが・・・

上級編はなかなか気付きにくいものですが、
CGI等のネットワークアプリケーションではこういう部分がセキュリティホールになりがちです。
いかなる入力にも備えるぐらいの気持ちでいなければ、普通に穴だらけなプログラムが出来上がってしまいます。

CGIアプリは結構簡単に作れたりしますが、その割にこのような穴を塞げる技術が要求されていたりするので、
「まともな」CGIアプリは実は全然簡単じゃなかったりします。
(Perlとかで作ったCGIアプリだってまた別の穴が満載です、CGIアプリって舐めたもんじゃないですよ・・・)

次回からは、演算種別の入力を数値から文字に変えてみたいと思います。

[前へ]   [目次へ]   [次へ]

プログラミング講座 総合目次

最終更新 2008/10/17