プログラミング序論 page9(update:2017/06/12)
[ index | prev | next ]

9. プログラムの作り方

何らかの目的を達成するため、プログラムを作りたいとします。しかし、C言語の文法を覚えただけではプログラムは作れないのです。どんな使い方のプログラムにするか仕様を考え、C言語に用意された仕組みを使って仕様を実現する手順(アルゴリズム)を組み上げなければなりません。この為、逆に手順が作れる仕様を考えることにもなります。

C言語に用意されたものだけで実現できる手順は限られます。制約された中で目的を達成できる仕様と手順を見つけなければなりません。

復習(
  C言語の式 
  制御構造 
 )

9-1.プログラムを作るために必要なこと

プログラムの目的

プログラム使用者がプログラムを実行することで実現したい目的は何かと言うことが一番重要です。

目的は単純明快にしましょう。複数の目的を同時に求めることはよくありません。

プログラムの仕様

仕様は何のデータをどのように与えると、何がどのように出力されるかを定義したものです。実行中のプログラムの動作を利用者側の視点で記述したものと言えるでしょう。

目的を満たすことのできる明快で単純な仕様を目指しましょう。プログラムの仕様は実現手順を無視しては作れません。複雑な仕様は避けるべきです。皆さんは仕様を書いてプログラムを作ってもらう立場になると思いますが、実現できない仕様を作らないためにも、プログラミングのことは十分知っておく必要があります。

※仕様は目的を十分達成できているか?これは重要な問題です。
プログラム作成を依頼した客と、プロクラムを作るプログラマが居るとします。客はプログラムで何が作れるかは良く知りませんから、人が手作業で行う手順で仕様を考えます。しかし、それではプログラムが作れないからと、プログラマはプログラムが可能な仕様を提案するでしょう。客は初めて聞くような仕様で自分の目的が果たせるかを判断しなければならなくなります。判断を誤ると役に立たないプログラムができてしまいます。

プログラムの設計

ここでは、仕様を満たすプログラムを作る過程を設計と呼ぶことにします。 C言語のプログラムの場合、設計は仕様を実現する手順作りです。設計の結果はプログラムのソースコードとして具体化されます。

設計の部分は手段も限られています。場合によっては仕様を満たすことは難しいこともあります。その場合は目的に合う作り易い仕様を考えることも必要です。

※仕様を満たす手順はフローチャート(流れ図)として記述することもできます。しかし、C言語のプログラムも処理の流れを記述するので、ローチャートの記述とソースコードの記述に本質的な違いはありません。

9-2.仕様と手順を考える 

さて、これまでC言語の文法を学んできましたが、これでプログラムが作れるかといえば作れません。 仕様を達成できる手順を見つけないといけないからです。

※C言語は手続き型と呼ばれる言語なので、プログラムは処理の手順を記述したものになります。

手順は見つかるとは限りません。しかし、これまで人間が人手でやっていたことをプログラムでやる場合は、人が行う手順を参考にすることもできます。手順は一通りとは限りませんから、プログラムしやすい手順を選ぶことも重要です。

今回は実際に簡単なプログラムを作る過程を例に手順を考えて行きましょう

9-2-0 目的40〜50人の得点から平均点や偏差値なんかを簡単に求める。

プログラムを作るのは何か目的があるはずです。ここでは目的を以下のようにしておきます。

目的: 40〜50人の得点から平均点や偏差値なんかを簡単に求める。

この目的に沿ったプログラムは色々考えられますから、まだこれだけではプログラムを作ることはできません。

仕様:どんな動作をするプログラムにするか。

プログラムを作るには、プログラムが動いているときの様子がどんなものなのか、イメージを作ることが次に必要です。このイメージを文書化して仕様とします。

イメージはいきなりは沸いてきませんから、人間が行う場合の手順を分析してみます。

  1. 計算用の紙を用意し表の形の罫線を引いておく。
  2. 得点データを表に書き込む
  3. 人数を数える
  4. 合計を求める
  5. 合計を人数で割って平均点を求める
  6. 得点と平均点の差を2乗して合計し人数で割ってから平方根を求め標準偏差とする
  7. 各人の得点から平均点を引いて、その差を標準偏差で割って10倍し50を加えたものを各人の偏差値とする

いきなり全部できるプログラムを作るのは難しそうです。そこで、まずは平均点の計算を行うプログラムを作ることにします。

※初めに目的を決めました、これは仕様をいろいろと変えていくときの基準とするためです。仕様の良し悪しは目的に合うかどうかで判断できます。目的を満たせる範囲であればプログラム可能な仕様を探すことができます。

手順:まずは素直なアルゴリズム

仕様そのままの素直な 手順をフローチャートにすると以下のようになるでしょう。

計算の手順をアルゴリズム:Algorithmと言います

 

仕様ではプログラムにどのように得点を入力するか示さなければいけませんが、学んだC言語の範囲では表計算ソフトの様に表を表示して書き込んでもらうことはできません。たくさんの得点を 記憶できるC言語のデータ型「配列」はまだ学んでいませんから、得点を記憶する場所にも困ります。いきなり得点の入力でつまづいてしまいます。

上記のアルゴリズムは学んだ範囲のC言語の知識ではプログラムにするのは難しそうです。

簡単に手順が作れる仕様から初めて少しづつ目的 に沿った仕様に近づけることにしましょう。

9-2-1. 3人の得点の平均点を求める

得点の数が決まっていないし数が多いのがとにかく難しいので、数は3個と決めれば容易なはずです。 入力も標準入力から数値をスペースで区切った文字列で受け取ることにします。

仕様案1a: 3人の得点(整数値)の平均点を求める
3個の得点をスペースで区切った文字列をキー入力すると平均点を出力する。

プログラムにすると次のように書けます。

#include<stdio.h>

int main(void)
{
   /*計算用紙の各欄に相当する変数を用意*/
   int a, b, c;/*3人の得点*/
   int count=3;/* 人数:仕様で定数3 */
   int total;/*合計*/
   double average;/*平均値*/
 
   /*得点の入力*/
   scanf("%d%d%d", &a, &b, &c);

   /*合計の計算*/
   total = a + b + c;

   /*平均点の計算*/
   average = (double)total / count; /*人数も合計も整数です。実数の割り算にするためにキャスト */

   /*平均点の出力*/
   printf("平均点は%4.1f", average);
 
   return 0;
}

平均点を出すときの割り算が実数の割り算になるように注意する必要があります。でも何とかプログラムにできました。

 アルゴリズムの工夫

上のアルゴリズムでははじめから人数が決まっていないといけません。さらに、人数が増えると得点を記憶しておく変数が増えて煩雑で判り難いプログラムになるのは明らかです。

得点 を全て覚えておこうとすると、得点の数だけ変数が必要になります。得点を覚えておかなくても済むアルゴリズムは無いでしょうか? 

プログラムを見ると、得点は合計の計算に使われるだけです。従って、得点の変数は合計に加えた後は用済みです。得点を1個読んでは合計に加え,これを必要な回数だけ繰り返すことにします。得点の変数は同じものが使いまわせます。

繰り返しの構文を使い、得点の入力と合計への加算を繰り返し行うことにします。アルゴリズムを変更して

プログラムにすると以下のようになります。

#include<stdio.h>

int main(void)
{
   
   int a;/*1人の得点、入力のときに一時的に使用し使いまわす*/
   int count = 3;/*ここは仕様で定数*/
   int total;
   double average;
   int i;/*for文の繰り返し回数を数える変数:反復子と呼ぶ*/
 
   /*合計の計算中で得点を入力する*/
   total = 0;/*加算して行くのではじめに0で初期化が必要*/
   for( i = 0; i < count; i++)
   {
       scanf("%d", &a);/*得点1個の入力*/
       total += a;/*得点を合計に加算*/
   }

   average = (double)total / count;
   printf("平均点は%4.1f", average);
 
   return 0;
}

どうでしょう。この形なら得点を入れる変数は使いまわすので人数さえ決まっていれば何人でも対応可能です。ただし、この手順は誰でも考え付くといったものではありません。

C言語に用意された手段を使って仕様を達成する手順(アルゴリズム)を作ることがプログラミングなのです。応用の利く手順は色々 ありますから他人のプログラムをたくさん読んで自分の頭の中に入れておくと役に立ちます。

 9-2-2. 40〜50人の得点の平均点を求める

残された問題は人数が未定であることへの対応です。人数はプログラムが実行されるときに何らかの形で示されるはずですが、仕様にはどのように示されるかが書かれていません。 ここでは、プログムで実行できそうなものを仕様に追加して みます。

仕様案1b: 40〜50人の得点の平均点を求める。
はじめに人数を入力
続いて得点をスペースで区切って順次入力する
人数分を入力すると平均点が出力される。

これは簡単です。前のプログラムで人数を格納した変数countへscanfを使って人数を入力すれば済みます。

#include<stdio.h>

int main(void)
{
   
   int a;
   int count;/*人数*/
   int total;
   double average;
   int i;

   printf("まず人数を入力してください");
   fflush(stdout);
   scanf("%d", &count);
   
   total = 0;
   for( i = 0; i < count; i++)
   {
       scanf("%d", &a);
       total += a;
   }

   average = (double)total / count;
   printf("平均点は%4.1f", average);
 
   return 0;
}

仕様の改善

目的は

目的: 40〜50人の得点から平均点や偏差値なんかを簡単に求める。

でした。仕様の初めに人数を入力するには、プログラムの利用者が人数を数える必要があるので、手間がかかります。数え間違うかもしれません。そこでscanf 関数の特性を利用して、仕様を次のように改善することにします。

仕様案1c: 40〜50人の得点の平均点を求める。
得点をスペースで区切って順次入力する。
得点入力の終わりは数値に変換できない入力で示される。例:「end」のような文字列の入力
入力が終わると平均点が出力される。

scanfの戻り値でデータが幾つ変換できたか分かります。これを使えばデータ終端が検出できるので、上の仕様を以下のアルゴリズムで実現します。


データ数が不定なのでwhileでループを作りデータが読めないときにループから出ることにします。

#include<stdio.h>

int main(void)
{
   
   int a;
   int count;
   int total;
   double average;

   count = 0;/*人数は0で初期化しておく*/
   total = 0;
   while( scanf("%d",&a) == 1 )/*scanfは入力が1個読めたときは1を戻す*/
   { 
      count ++;
      total += a;
   }

   average = (double)total / count;
   printf("平均点は%4.1f", average);
 
   return 0;
}

 

9-3.仕様の追加

9-3-1 最大値、最小値

ちょっと機能を追加してみましょう

仕様案 2a: 
40〜50人の得点の平均値、最大値、最小値を求める。
得点をスペースで区切って順次入力する。
得点入力の終わりは数値に変換できない入力で示される。
入力が終わると平均点、最大値、最小値が出力される。

得点が1個ならその値が最大値かつ最小値です。

#include<stdio.h>

int main(void)
{
   
   int a;
   int count;
   int total;
   double average;
   
   int max,min;/*最大値と最小値*/

   count = 0;
   total = 0;
   while( scanf("%d", &a) == 1 )
   { 
      if(count == 0){
          max = min = a; /*1個目は最大値かつ最小値*/
      }else if( max < a ){
          max = a; /*最大値より入力が大きい場合、最大値を更新*/
      }else if( a < min ){
          min = a; /*最小値より入力が小さい場合、最小値を更新*/
      }
      
      count ++;
      total += a;
   }

   average = (double)total / count;
   printf( "平均点は%4.1f", average);
   printf( " 最大値は%3d 最小値は%3d" ,max, min);
 
   return 0;
}

最大値と最小値の変数を用意して、入力のたびに必要なら更新するアルゴリズムで仕様を達成しています。
 

9-3-2. 不正な得点が入力されたりした場合の動作を仕様に追加

ここまでのプログラム は正常な使われ方をするのが前提でしたが、使い方が分からないなどの理由で不正な得点が入力された場合、あるいはまったく得点が入力されなかった場合の仕様を追加してみます。

仕様案 2b: 
40〜50人の得点の平均値を求める。
得点をスペースで区切って順次入力する。
  入力された得点が0から100の範囲外なら、入力得点を無視し、警告メッセージを出力する
得点入力の終わりは数値に変換できない入力で示される。
入力が終わると平均点が出力される。
  得点が1件も入力されなかった場合は「得点が入力されませんでした」と出力し終了する。

#include<stdio.h>

int main(void)
{
   
   int a;
   int count;
   int total;
   double average;
   
   count = 0;
   total = 0;
   while( scanf("%d", &a) == 1)
   { 
      if( a < 0 || 100 < a)
      {
          printf("得点%dは不正な値です", a);
          fflush(stdout);
          continue;
      }
      count ++;
      total += a;
   }

   if(count == 0)
   {
      printf("得点が入力されませんでした");
      return 0;
   }

   average = (double)total / count;
   printf("平均点は%4.1f", average);
 
   return 0;
}

 

9-3-3 素直なアルゴリズムに近づける

ここまで学んだ知識を用いてmain関数に全てをつめ込んだプログラムを作ることが原理的には可能です。しかし、そんなプログラムは長くなるほど解りにくいものになってしまいます。

そこで、まだ学んでいませんが配列や関数を使って素直なアルゴリズムに近い形で作ってみました。
 大規模なプログラムを解り易く作るには、関数や配列は必須です。

#include<stdio.h>
#define MAXLENGTH 50

/*配列dataに最大でlength個の得点を読みむ。戻り値は読み込んだ数*/
int readData(int data[], int length)
{
    int count = 0;
    int a;
    while( count < length)
    {
        if( scanf("%d",&a) != 1)break;
        if( a < 0 || 100 < a)
        {
            printf("得点%dは不正です\n",a);
            fflush(stdout);
            continue;
        }
        data[count] = a;
        count++;
    }
    return count;
}

/*配列dataの要素length個の平均値を戻す*/
double calcAverage(int data[], int length)
{
    int i;
    int total = 0;

    for(i = 0; i < length; i++) total += data[i];

    return (double)total / length;
}

/*main関数は素直なアルゴリズムの手順に近い形にしました*/
int main(void)
{
    int a[MAXLENGTH];/*得点データを格納する配列*/
    int count;
    double average;

    count = readData(a, MAXLENGTH);
    if(count == 0)
    {
        printf("得点が入力されませんでした");
        return 0;
    }
    
    average = calcAverage(a, count);
    printf("平均点は%4.1f", average);

    return 0;
}

 

9-4.偏差値を計算する

以下は参考程度に見てください。これまで学んだ範囲内でも偏差値を求めるプログラムを作れることを示します。しかし、本来は配列や関数を使って素直なアルゴリズムで作るべきものです。

偏差値の計算には標準偏差が必要です。標準偏差の計算は通常はまず平均値Xを求め、次に各データと平均値の差を二乗して合計した二乗残渣を求めます。i番目のデータをMiとし狽データの番号iでの合計とすれば二乗残渣R2が次のような式で 求められます。

    R2=煤iMi-X)2

標準偏差σはこれをデータ数で割って平方根を取って次式で計算されます。

    σ=√(R2/(count))

標準偏差は各得点の平均値からのばらつきの大きさを示す量です。この場合、受験者が1名だと標準偏差は0です。ここで使う標準偏差は標本標準偏差ではなく母集団の標準偏差を使いました。

仕様案 3: 
40〜50人の得点の平均点と標準偏差を求める。
得点をスペースで区切って順次入力する。
  入力された得点が0から100の範囲外なら、入力得点を無視し、警告メッセージを出力する
得点入力の終わりは数値に変換できない入力で示される。
入力が終わると平均点と標準偏差が出力される。
  得点が1件も入力されなかった場合は「得点が入力されませんでした」と出力し終了する。

9-4-1 標準偏差の計算手順を考えよう

ここまでの平均値の計算プログラムでは、平均値を 求めた時点で各入力値Miは失われている。従って、上記のプログラムに書き加えて標準偏差を計算するのは不可能に思えるかも知れません。しかし、二乗残渣を分解して少し変形すると

 R2=煤iMi-X)2
=(Mi2 - 2*X*Mi + X2)
=熱i2 - 2*X*熱i+X2*狽P

ここで熱iはデータの合計total、狽Pはデータ数countだから、後は熱i2があれば二乗残渣の計算が可能ですね 。合計と同様に予めデータの二乗和を計算しておけば二乗残渣が計算できることになります。下記はこの方法で標準偏差を計算するプログラムです。

#include<stdio.h>
#include<math.h> /* math.hは平方根、指数関数、対数関数、三角関数などのヘッダーファイル*/

int main(void)
{
   
   int a;
   int count;
   int total;
   int total2;/*得点の2乗の合計*/

   double average;
   double r2;/*二乗残渣*/
   double sd;/*標準偏差standard deviation*/
   
   count=0;
   total=0;
   total2=0;

   while(scanf("%d",&a)==1)
   { 
      if(a<0 || 100<a)
      {
          printf("得点%dは不正な値です",a);
          fflush(stdout);
          continue;
      }
      count++;
      total+=a;
      total2+=a*a;
   }

   if(count==0)
   {
      printf("得点が入力されませんでした");
      return 0;
   }

   average=(double)total/count;
   printf("平均点は%4.1f",average);

   r2=total2 -2*average*total +average*average*count;
   sd=sqrt(r2/(count));
   printf("標準偏差は%4.1f",sd);
 
   return 0;
}

これでヤッターできたと言えるでしょうか? 今回は二乗残渣の計算に、入力データを残さなくて済む方法を採用しましたが実はこの計算方法では、計算誤差が大きくなる欠点があるのです。また計算方法が一般的な手順と異なるためにコメントなどをしっかり書かないとプログラムを理解してもらえないでしょう。

※初期のプログラマブル電卓などでは、このような手順のプログラムが少ないメモリーでも動くので、実際に使われていました。

9-4-2 偏差値の計算手順を考えよう

偏差値は次の計算式で与えられます。

i番目の偏差値=50+10*( i番目のデータ:Mi − 平均値:X ) /標準偏差:σ 

今度こそ入力値Miが必要です。配列を使うしか方法は無いでしょうか? いえいえ、一度データを入れてもらって標準偏差まで求めた後で、再度同じデータを入れてもらえばいいのです。 仕様を工夫することで可能です。

仕様案 4: 40〜50人の得点の平均点、標準偏差、偏差値を求める。
得点をスペースで区切って順次入力する。
  入力された得点が0から100の範囲外なら、入力得点を無視し、警告メッセージを出力する
得点入力の終わりは数値に変換できない入力で示される。
入力が終わると平均点と標準偏差が出力される。
  得点が1件も入力されなかった場合は「得点が入力されませんでした」と出力し終了する。
この後で再度得点をスペースで区切って順次入力するとその偏差値を計算して書き出す。
数値に変換できない文字列を与えると偏差値の計算を終了してプログラムも終了する。

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

int main(void)
{
   
   int a;
   int count;
   int total; 
   int total2;
    
   double average;
   double r2;
   double sd;
   
   count=0;
   total=0;
   total2=0;
   while(scanf("%d",&a)==1)
   { 
      if(a<0 || 100<a)
      {
          printf("得点%dは不正な値です",a);
          fflush(stdout);
          continue;
      }
      count++;
      total+=a;
      total2+=a*a;
   }

   if(count==0)
   {
      printf("得点が入力されませんでした");
      return 0;
   }

   average=(double)total/count;
   printf("平均点は%5.1f",average);
   fflush(stdout);

   r2=total2 -2*average*total +average*average*count;
   sd=sqrt(r2/(count));
   printf("標準偏差は%4.1f",sd);
   fflush(stdout);

    printf("偏差値を計算できます。再度得点を入力してください\n");
    fflush(stdout);

    while( scanf("%d",&a)!=1)getchar();/*最初の得点が読めるまで空読み*/
    do{
        printf("%d の偏差値は %4.1fです\n",a,50+10*(a-average)/sd );
        fflush(stdout);
    }while( scanf("%d",&a)==1);

    return 0;
}
/* 実行結果

95 100 90 82 81 82 75 90 79 60 80 end<入力
合計は914 平均値は83.090909
標準偏差10.746670
偏差値を計算できます。再度データを入力してください
95 100 75 60 quit<入力
95 の偏差値は 61.1です
100 の偏差値は 65.7です
75 の偏差値は 42.5です
60 の偏差値は 28.5です

*/

 後で学ぶことになりますが、C言語には多数のデータを記憶する配列と呼ばれるデータ型が用意されています。さらに、いくつかのデータをまとめたデータ型を作る構造体と呼ばれるものもあります。 配列を用いて入力データを記憶しておけば、偏差値の計算プログラムも解りやすくて誤差も少ない形に書くことができます。
 配列や構造体などのデータを整理して旨くまとめる仕組みを使うことで、初めて多数のデータを扱う実用的なプログラムを作れます。

課題8(初期ファイルp8.c 参考 ReDo.zip

ReDo.zipファイルをダウンロードして解凍。これは、皆さんの先輩が作ったコーディング過程を解説付きで再現するツールです。フォルダの中のReDo.jarをダブルクリックで起動しそのウインドウにp8.xmlをドラッグアンドドロップすると再現がスタートします。

これを見て、レポートツールで初期ファイルp8.cから、ここで紹介した平均点を求めるプログラムを作ってレポートしてください。間違いを再現する必要はありません^^。


[ index | prev | next ]