プログラミング序論 pageA1(update:2013/05/30)
[ index | prev | next ]

A1. プログラムの作り方

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

目的を満たせる仕様は色々考えられることでしょう。1つの仕様を実現する手順も複数在るでしょう。しかし、C言語に用意されたものだけで実現できるプログラムは限られます。制約された中で目的を達成できる仕様と手順を見つけなければなりません。

1.基本の形は覚えよう

C言語のプログラムはmain関数から実行が開始されます。まずはmain関数だけのプログラムで基本の形を覚えてください。

1-1.最小限の形

プログラムの改行や行頭の空白などもこの形で覚えてください。実行すると画面表示とかはしませんが、実行した側に0を戻します。

int main(void)
{
    return 0;
}

1-2. 標準出力を使う

標準入出力関数を使う場合はヘッダーファイルstdio.hの挿入(インクルード)を忘れてはいけません。printfは誰かが作った関数と呼ばれる形式のプログラムです。ここではそのプログラムを呼び出して「Hello」と画面に書き出してもらっています。printfへのデータ の渡しかたと何が戻ってくるかがstdio.hの中に書かれています。

#include<stdio.h>

int main(void)
{
    printf("Hello\n");
    return 0;
}

1-3. 変数を使う

データを覚えておく場所に名前をつけたのが変数です。データの表現形式を示すデータ型変数名が事前にわかれば機械語への翻訳の手間が減ります。従って、C言語では変数の定義はブロックの先頭でまとめて行います。
(※C99からは使う前であればどこでも定義できるようになりましたが、皆さんは先頭でまとめて行うようにしてください)

他人にも理解してもらうために、変数名は解り易い名前にしてください。自分だけしか使わない短いプログラムでは名前を考えるのは面倒なだけに思えるでしょう。しかし、大勢で作る大規模なプログラムの場合は解り易いことがとても重要です。

データ型はデータの表現方法をきめたものです。4つの基本の型char int float doubleはつづりまで覚えておきましょう。printfの書式指定子はおのおの%c %d %f %fです。

#include<stdio.h>

int main(void)
{
    double width = 12.3, height = 45.6;
    double area;

    area = width * height;
    printf("幅%fcm 高さ%fcmの長方形の面積は%fcm^2\n", width, height, area);

    return 0;
}

1-4. 標準入力を使う

標準入力から流れ込む文字列を解釈して指定されたデータ型の値に変換して読み込む標準関数scanfが用意されています。どんな変換をしてほしいかを示すのが最初の引数(入力書式)で、2番目以降はデータを格納する場所(メモリーアドレス)です。

データを入れる場所としては変数で用意した場所を使うのが普通で、変数の場所はアドレス演算子&を変数の前につけて表します。scanf関数は入力書式とアドレスだけ受け取って処理を行うので、 書式指定子と変数のデータ型が一致しなければいけません。ここで間違うと正しくデータが読めないだけでなく、変更してはいけないメモリーにまで書き込んでプログラムの暴走の原因になったりします。 4つの基本の型char int float doubleに対応したscanfの書式指定子はおのおの %c %d %f %lf です。

#include<stdio.h>

int main(void)
{
    double width, height;
    double area;
 
    printf("長方形の幅[cm]と高さ[cm]を入力してください");
    fflush(stdout);/*会話型プログラムなのでここで、必ず書き出してもらうため*/
    scanf("%lf%lf", &width, &height);
 
    area = width * height;
    printf("幅%fcm 高さ%fcmの長方形の面積は%fcm~2\n", width, height, area);

    return 0;
}

※fflush(stdout);は標準出力のバッファに溜まっている文字列を出力する指示でフラッシュと呼ばれる操作を行います。printfで標準出力のバッファに書き出す所までは必ず行われますが、バッファからの実際の出力は状況によって変わります。ここでは、scanfによる読み込みを知らせるためのメッセージを出力したいので、必ずscanfの前に書き出したいのです。会話型のプログラムでは、発言の順番を守る必要があるので、バッファにためておいて後から一気に書き出すのではだめなのです。 

コマンドプロンプトの様な端末プログラム上で実行すればprintfごとにフラッシュされます。しかし、通信が間にはいる様な実行ではフラッシュをしないとプロクラムの終了時点でフラッシュして終了となる場合があります。

2.仕様と手順(アルゴリズム)を考える 

さて、これまでC言語の文法を学んできましたが、これでプログラムが作れるかといえば作れません。 プログラムの目的を達成できるC言語で可能な処理手順を見つけないといけないからです。
(C言語のような手続き型と呼ばれる言語は処理手順をプログラム命令の記述順で示します)

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

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

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

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

この目的に沿ったプログラムは色々考えられますから、まだこれだけではプログラムを作ることはできません。プログラムを作るには、プログラムが動いているときの様子がどんなものなのか、イメージを作ることが次に必要です。このイメージを文書化して仕様書とします。次に仕様を満たす処理手順を考えてプログラムを書きます。もし、手順が作れないときは 作れそうな仕様に考え直すことにします。

1)プログラムで何をしたいのか目的を決める
2)どんな動作をするプログラムにするか、仕様書を作る。
3)仕様書を満足する処理手順を考えプログラムを書く。

仕様を考えるほどのイメージはいきなりは沸いてきませんから、仕様を作るために、人間が行う場合の手順を分析してみます。

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

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

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

素直なアルゴリズム

計算の手順をアルゴリズム:Algorithmと言います。人間が行う 手順を素直にまねたアルゴリズムをフローチャートにすると以下のようになるでしょう。

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

上記の素直なアルゴリズムは学んだ範囲のC言語の知識ではプログラムにするのは難しそうです。そこで簡単に手順が作れる仕様から初めて少づつ目的 に沿った仕様に近づけることにしましょう。

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言語のプログラミングはC言語に用意された手段を使って目的を達成する手順を作ることなのです。応用の利く手順は色々 ありますから他人のプログラムをたくさん読んで自分の頭の中に入れておくと役に立ちます。

 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;
}

 

プログラムの仕様

ここでの目的は平均点を簡便に求めるプログラムを作ることです。しかし、プログラムの仕様は実現手順を無視しては作れません。皆さんは仕様を書いてプログラムを作ってもらう立場になると思いますが、実現できない仕様を作らないためにも、プログラミングのことは十分知っておく必要があります。

もう一つの課題は「仕様書は目的を十分達成できているか?」です。これは大きな問題です。プログラム作成を依頼した客と、プロクラムを作るプログラマが居るとします。客はプログラムで何が作れるかは良く知りませんから、人が手作業で行う手順で仕様を考えます。しかし、それではプログラムが作れないからと、プログラマはプログラムが可能な仕様書を提案するでしょう。

客は初めて聞くような仕様で自分の目的が果たせるかを判断しなければならなくなります。判断を誤ると役に立たないプログラムができてしまいます。上の例でも、人数を数える必要がある仕様では成績処理に時間がかかってしまいます。

3.仕様の追加

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;
}

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

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;
}

 

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;
}

 


以下は蛇足です。 あくまでも参考程度に見てください。

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

4.偏差値を計算する

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

    R2=煤iMi-X)2

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

    σ=√(R2/(count-1))

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

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);

   if(count==1)
   {
      printf("得点1個では標準偏差を計算できません");
      return 0;
   }

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

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

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

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

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

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

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

仕様書案 4: 40〜50人の得点の平均点、標準偏差、偏差値を求める。
得点をスペースで区切って順次入力する。
  入力された得点が0から100の範囲外なら、入力得点を無視し、警告メッセージを出力する
得点入力の終わりは数値に変換できない入力で示される。
入力が終わると平均点と標準偏差が出力される。
  得点が1件も入力されなかった場合は「得点が入力されませんでした」と出力し終了する。
  得点が1件しか入力されなかった場合は「得点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);
   if(count==1)
   {
      printf("得点1個では標準偏差を計算できません");
      return 0;
   }

   r2=total2 -2*average*total +average*average*count;
   sd=sqrt(r2/(count-1));
   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言語には多数のデータを記憶する配列と呼ばれるデータ型が用意されています。さらに、いくつかのデータをまとめたデータ型を作る構造体と呼ばれるものもあります。 配列を用いて入力データを記憶しておけば、偏差値の計算プログラムも解りやすくて誤差も少ない形に書くことができます。
 配列や構造体などのデータを整理して旨くまとめる仕組みを使うことで、初めて多数のデータを扱う実用的なプログラムを作れます。


[ index | prev | next ]