lesson7(update:2019/11/21) [課題7]


7.debug(虫取り)


[prev|next|index]

目次

[目次]


7.1 解り易いプログラム

 プログラムは誰にでも解り易いように書かなければいけません。 現在のソフトウエアは大勢の人が共同して作り、完成後も修正をしながら長く使うものです。従って書いた本人以外が見ても理解できるような書き方をしないと使い物になりません。解り易い書き方をすると、プログラムのミスも見つけやすくなります。

7.1.1 字下げ

 最低限、字下げをきちんとしましょう。字下げのやり方はプログラミング序論の教科書などを参考にしてください。 以下のプログラムは2次方程式の根を求めるものですが、字下げして無いので、人間には理解できません。こんなプログラムを他人に渡したらプログラマとしては信用されなくなると思っていいでしょう。

#include<stdio.h>
#include<math.h>
int main(void){
double a,b,c,d;
scanf("%lf%lf%lf",&a,&b,&c);
printf("%fx^2 %+fx %+f=0\n",a,b,c);
d=b*b-4*a*c;
if(d==0)printf("x=%f\n",-b/(2*a));
else if(d>0){
printf("x1=%f",-b/(2*a)+sqrt(d)/(2*a));
printf(" x2=%f\n",-b/(2*a)-sqrt(d)/(2*a));
}else{
printf("x1=%f %+fi",-b/(2*a),sqrt(-d)/(2*a));
printf(" x2=%f %+fi\n",-b/(2*a),-sqrt(-d)/(2*a));
}
return 0;
}

上記のプログラムはまずは字下げし、さらに空行や{ }なども入れて見やすい形にする必要があります。このようなことを自動で行う整形プログラムも存在しています。

7.1.2 コメントと変数名

 プログラムを見るときに重要なのは「何の為のプログラムなのか」です。このような情報はコメントとして書いておくのが適当です。

 次に「どのように処理するのか」ですが、理解してもらうためには変数の意味が分かるようにすることが重要です。簡単なコメントを書いておくと誤解される危険性が減ります。
 このプログラムでは二次方程式の係数を格納する変数をa,b,cとしていますが、これをx,y,zとしたら混乱を招くでしょう。同じように判別式(discriminant)は 英語の頭文字をとってdとしています。このように変数の名前1つでも分かりやすさは大きく変わります。

 さらにプログラムの流れについても概要が分かるようなコメントがあるといいでしょう。下記のプログラムではif文の分岐にもコメントを付けてみました。

※コメントを細部にまで付けるのは良くありません。コメントは目立たせる必要がある重要な部分にのみつけてください。

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

/*二次方程式の係数を標準入力から受け取り解を標準出力に書き出す*/
int main(void)
{
    double a,b,c;/*二次方程式の係数*/
    double d;/*判別式の値*/

    scanf("%lf%lf%lf",&a,&b,&c);
    printf("%fx^2 %+fx %+f=0\n",a,b,c);

    d=b*b-4*a*c;
    if(d==0)/*重解*/
    {   
        printf("x=%f\n",-b/(2*a));
    }
    else if(d>0)/*二個の実数解*/
    {
        printf("x1=%f",-b/(2*a)+sqrt(d)/(2*a));
        printf(" x2=%f\n",-b/(2*a)-sqrt(d)/(2*a));
    }
    else/*d<0で共役複素数の解*/
    {
        printf("x1=%f %+fi",-b/(2*a),sqrt(-d)/(2*a));
        printf(" x2=%f %+fi\n",-b/(2*a),-sqrt(-d)/(2*a));
    }
    return 0;
}

7.1.3 蛇足

 上のプログラムに付け足すものはあるでしょうか?

1)上のプログラムは二次方程式の係数を入力するとき、何も手がかりになるメッセージを出しません。これはプログラムがどのように使われるのかで、要不要が決まってきます。「二次方程式の係数を3個入れてください」とprintfで書きだすのは容易ですが、それは使う状況しだいでは邪魔になる場合もあります。

2)二次方程式の係数でa=b=0でc!=0の場合、解は存在しません。このような例外についてもプログラムで対応すべきかは、やはり使う状況しだいです。

3)a=0なら二次方程式ではないのでエラーと書き出して終了することも考えられます。

このように、プログラムは色んなことを考えてどんどん複雑なものにすることができます。でもそれはプログラムを解りにくくして、ミスを増やす原因になります。蛇足とならないかよく考えてください。

 1−3)のようなことはプログラムを作る前に要求仕様として全て決めなければいけません。プログラムで全てを解決することはできません。仕様は単純にして、プログラムは解り易いものにしましょう。

[目次]


7.2 コンパイルエラー

 コンパイラ はソースプログラムを読みながら機械語に翻訳しますが、このときソースプログラムが決められた文法どおりに書かれていることを前提としています。見方を変えると機械的に翻訳できるような文法でソースプログラムを書くように決めたともいえます。

7.2.1 セミコロンがありません

 コンパイラはソースプログラムを読んでいく途中で、予想したものが見つからず、翻訳作業が継続できなくなったときコンパイルエラーを出して、翻訳を放棄します。例えばC言語の場合、文の終わりはセミコロンで示されますが、文が終わるはずなのにセミコロンでないものがあると、そこで「セミコロンが無い」とエラーを出します。

int main(void)
{
    return 0
}

 上のプログラム(ソースファイルp7c.c)をコンパイルすると

p7c.c(4) : error C2143: 構文エラー : ';' が '}' の前にありません。

とエラーを出します。これはp7c.cの4行目まで 読んだところで、セミコロンを探していたのに、ブロックの閉じ括弧を見つけてしまいこんなはずではないとエラーをだしているのです。

 注意してほしいのはコンパイラは間違が明らかになった場所を示して、文法エラーを指摘するだけだということです。間違いを修正する方法は教えてくれません。なぜなら、文法に合わせるための書き換えは無数に存在しコンパイラはどのように修正すべきか判断できないからです。上の例では、プログラマーが4行目で指摘されたエラーを見て3行目のreturn 0の後にセミコロンを入れる必要があります。

※コンパイルエラーは役に立つ。
プログラムを作っていてコンパイルエラーが取れずに悩むことは多いと思います。しかし、真の原因を突き止めることなく、単にエラーが出ないようにソースを書き換えるようなことをしてはいけません。問題を見えないように隠しただけでは、プログラムの実行時にエラーを出したり、不正な動作をしたり、より深刻な結果を招くだけです。

7.2.2 文字 '0x81' は認識できません。

 ソースファイルで日本語の文字を使っているとよくあるエラーです。特に'0x81'と'0x40'のセットは2バイトの空白文字でテキストエディターでも表示は通常の空白と見分けが付かないので注意が必要です。似たような例にダブルクオテーション「”」があり、この場合は「"」との見分けが困難です。エディターの上では文字幅で見分けるしかないでしょう。下記はprintfの前の空白とprintf関数の引数文字列を閉じる部分の文字が間違っています。

#include<stdio.h>

int main(void)
{
   printf("こんにちは); 
    return 0;
}

このソースファイルp7d.cをコンパイルすると次のようなエラーが指摘されました

p7d.c(5) : error C2018: 文字 '0x81' は認識できません。
p7d.c(5) : error C2018: 文字 '0x40' は認識できません。
p7d.c(5) : error C2001: 定数が 2 行目に続いています。
p7d.c(6) : error C2143: 構文エラー : ')' が 'return' の前にありません。

上2行のエラーは空白文字を指摘しています。しかし、下の2行は「こんにちは」の文字列が閉じていないことによるエラーで直接に「”」を指摘してはくれません。このあたりが難しいところです。

7.2.3 コンパイルは前処理の後で行われます

  ソースコードを見てC言語の文法に合っているように見えても、マクロの記述に誤りがあると前処理後のプログラムは文法的に不正なものに書き換わって コンパイルエラーになることがあります。

#include<stdio.h>
#define PI 3.1415;

int main(void)
{
    double r;
    scanf("%lf",&r);
    printf("半径%fの円の面積は%f",r,PI*r*r);
    return 0;
}

上のファイルp7x.cをコンパイルすると

p7x.c(8) : error C2143: 構文エラー : ')' が ';' の前にありません。
p7x.c(8) : error C2100: 間接指定演算子 (*) の使い方が正しくありません。
p7x.c(8) : error C2059: 構文エラー : ')'
p7x.c(8) : warning C4552: '*' : 演算子にプログラム上の作用がありません。作用を持つ演算子を使用してください

のように8行目でエラーを指摘されます。ここで、8行目を何度見直しても指摘されたような間違があるとは思えません。しかし、ここでPIはマクロで3.1415に置き換えられるように見えますが実は3.1415;で置き換えられ、セミコロンが入るためにエラーが出ているのです。

7.2.4 警告を無視してはいけません

 警告はもしかしたらプログラムミスかもしれない部分を指摘してくれるものです。翻訳自体は行われて実行可能ですが、指摘の意味を理解せずに無視してはいけません。

#include<stdio.h>

int main(void)
{
    int total,a;
    while(1){
        if(scanf("%d",&a)!=1)break;
        total+=a;
    }
    printf("入力の合計は%d",total);
    return 0;
}
/*
実行結果
10
20
30
a
入力の合計は29825166 <<値がおかしい?
*/

上のプログラムはコンパイル時点では下の警告2行が出ますが、翻訳はされて実行もできます。しかし、結果は入力の合計を書き出してくれません。

p7y.c(6) : warning C4127: 条件式が定数です。
p7y.c(8) : warning C4700: 値が割り当てられていないローカルな変数 'total' に対して参照が行われました

ここで1行目の警告はwhile文でわざと無限ループにするために定数1を入れたことによるものです。では2行目は何でしょうか?これは変数totalに初期値が代入されていないのに、参照しているけどいいのかと警告しているのです。
 関数の中で定義された局所変数はスタックのメモリーに割り当てられますが、このときメモリとの対応は決めても、そのメモリーの値には何もしません。メモリーを割り当てただけで、メモリーの値はそのままなのでtotalの値を読み出すと、そのときのメモリーの値が読み出されます。つまりtotalの最初の値はそのときしだいでどんな値かわからないのです。合計を求める場合はtotal=0;とはじめにtotalを初期化することが必要です。

[目次]


7.3 実行結果がおかしい

 さあコンパイルを通って実行可能になったら、そこからが虫取りの本番です。ですがここでは簡単に項目を挙げるだけにしておきます。下記の作業が簡単になるようにプログラムは分かりやすい書き方をしておきましょう

7.3.1 テスト

 まずはいろんな使い方をして、思い通りに動いているのかテストしましょう。間違いを見つけて修正したら、はじめからテストをやり直しましょう。修正で新たなミスが発生することはよくあることです。

7.3.2 printfを入れて途中結果を見よう

途中の変数の値を表示するprintf文を入れて、実行中の値の変化を調べます。バファリングされると困るので

printf("%d",x);
fflush(stdout);

のようにフラッシュもしておくといいでしょう。この方法は面倒に見えて実はかなり効果的な手法です。

 

例題:

 ファイル名の読み込みでscanfの代わりにfgetsを用いると改行コードまで読み込んでしまい、ファイルが開けなかった問題の例を以下に示します。

注意:このエラーを起こすプログラムは実行しないでください。計算機の異常動作を引き起こすことがあります。

 1)ファイルの中身を画面に書き出すプログラムを作りました。ここで、ファイル名の読み込みにscanf関数を用いています。

ファイルの#include<stdio.h>

int main(void)
{
	FILE *p;
	int c;
	char fname[100];
	scanf("%s",fname);
	p=fopen(fname,"r");
	while((c=fgetc(p))!=EOF)
		putchar(c);
	return 0;
}
/* 
 正常に実行しファイルの中身を画面出力
*/

2) しかし、scanfはバッファーオーバーランの危険性があるのでfgets関数に変えてみます。

#include<stdio.h>

int main(void)
{
	FILE *p;
	int c;
	char fname[100];
	/*scanf("%s",fname);*//*scanfではバッファー溢れの危険あり*/
	fgets(fname,100,stdin);
	p=fopen(fname,"r");
	while((c=fgetc(p))!=EOF)
		putchar(c);
	fclose(p);
	return 0;
}
/* 実行結果
ファイル名を入力した後で
-----実行時エラー----
*/

3)何処でエラーになるのか、printf関数を挟み込んで実行してみます。

#include<stdio.h>

int main(void)
{
	FILE *p;
	int c;
	char fname[100];
	/*scanf("%s",fname);*//*scanfではバッファー溢れの危険あり*/
	fgets(fname,100,stdin);
	printf("fgetsで読み込んファイル名は%s",fname);
	fflush(stdout);
	p=fopen(fname,"r");
	printf("fopenで開いたファイル名のポインターは%u",(int)p);
	fflush(stdout);
	while((c=fgetc(p))!=EOF)
		 putchar(c);
	fclose(p);
	return 0;
}
/* 実行結果
fgetsで読み込んファイル名はaa.c
fopenで開いたファイル名のポインターは0
-----この後で実行時エラー----
*/

4)ファイル名は読み込めているようですが、ファイルは開けずにファイルポインターがゼロになっています。不思議ですが、scanfでは正常に実行できているのでfnameに読み込んでいる値が違うとしか思えません。そこでscanfとfgetsで同じものを読み込んでfnameの中身を数値として比べてみました。

#include<stdio.h>
int main(void)
{
	int c;
	char fname[100];
	
	scanf("%s",fname);
	printf("scanfの場合 fname=%s\n",fname);
  
	for(c=0;c<100;c++){/*配列の範囲内で先頭から文字列終端の0が見つかるまでを書き出す*/
		if(fname[c]==0)break;/*文字列の終端でブレイク*/
		printf("fname[%d]=%3u ",c,fname[c]);
	}
	printf("\n\n");
	fflush(stdout);
	
	fflush(stdin);/*標準入力のフラッシュ*/
	/*scanfが読み込まなかった改行コードなどを捨てる為に行った*/
	
	fgets(fname,100,stdin);
	printf("fgetsの場合 fname=%s\n",fname);
  
	for(c=0;c<100;c++){/*配列の範囲内で先頭から文字列終端の0が見つかるまでを書き出す*/
		if(fname[c]==0)break;/*文字列の終端でブレイク*/
		printf("fname[%d]=%3u ",c,fname[c]);
	}
	fflush(stdout);
	
	return 0;
}
/*
aa.c << キー入力してEnterキー
scanfの場合 fname=aa.c
fname[0]= 97 fname[1]= 97 fname[2]= 46 fname[3]= 99 

aa.c << キー入力してEnterキー
fgetsの場合 fname=aa.c

fname[0]= 97 fname[1]= 97 fname[2]= 46 fname[3]= 99 fname[4]= 10 
fgetsでは最後に10が追加されている。10はEnterキーで入った改行文字のコード 

for(c=0;c<100;c++) if(fname[c]=='\n')fname[c]=0;
として、改行コードを取り除く必要がある
*/

 

7.3.3 プログラムはうまく関数に分割しておきましょう。

 長いmain関数にすべてを詰め込んだようなプログラムは、問題点の発見も修正も非常に大変なものになります。 数十行程度の関数に分けておけば、関数ごとにテストし虫取りが行えます。この後、虫の取れた関数を組み合わせることで、信頼性の高いプログラムを作れます。

7.3.4 ソースコードをよく見ましょう

ソースコードを読みながら変数の値がどうなるか、紙の上に記録しながらプログラムの流れを追っていくのもいい方法です。ただし、長いプログラムでは大変な作業になるので、前記のように確かめる範囲が狭く なるように関数に分けておくことがトテモ大事です。

[目次]


7.4 演習課題

課題7 デバッグ問題

このファイル(p07.c)を使ってプログラムを作成すること。
講義後半にファイルをアップします。

10人の身長デ−タ
data[10]={ 173.8,168.2,180.3,166.0,189.7,167.4,159.8,170.5,167.5,167.4}
を背の高い順に並べ換えた後で書き出すプログラムです。正しく動くように虫を取って下さい。
※どこが間違っていたのか分かるようにコメントをつけること。
 

#include<stdio.h>
#define num=10; 
int  i;
double data[num]={ 173.8,168.2,180.3,166.0,189.7,167.4,159.8,170.5,167.5,167.4}

int main(void)
{
    for(i=1;i<=num;i++){
       max=i;
       for(j=i+1;j<=num;j++)/*最大の要素の場所を捜す*/
         if (data[max]<data[j])max=j;
       data[i]=data[max];data[max]=data[i];/*入れ換え*/
    }
    for(i=1;i<=num;i++)printf("%7.1f ",data[i]);

    return 0;
}
 

ホームワーク(第7回)

配列を用いたプログラム課題

提出締切:11月27日(水)

提出先:情報棟2F 就職室 レポートボックス


[prev|next|index]