プログラミング序論 page8(update:2017/06/20)
[ index | prev | next ]  まとめ main関数

8. 解り易いプログラムの書き方

プログラムは人が読み書きするものであり、製品としいてのプログラムは書いた本人でなくても容易に理解でき変更もできることが重要である。そのためには、できるだけ単純で解り易いプログラムを書くように心がけることが求められる。

ここでは、プログラムを解り易くするための変数とその名前の付け方プログラム構造が見えるようにする字下げの仕方について解説する。

※今回と次回でこれまでの復習をかねてプログラムの書き方、作り方の話をします。次々回以降では、複数の関数を使ったプログラムや、沢山のデータを扱う配列などの話に進みます。でも、その前に基礎を確かなものにしてほしい。

main関数

Cプログラムの中の値

8-1.基本の形は覚えよう(復習です)

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

最小限の形

プログラムの改行や行頭の空白などもこの形で覚えてください。コンパイルも問題なくでき実行可能ファイルが作れます。 実行すると画面表示とかはしませんが、実行した側に0を戻します。

int main(void)
{
    return 0;
}

※main関数の後のカッコ内には関数に渡すデータを記憶した引数を指定します。引数が無い場合は(void)と記述します。main関数を実行するときに引数を与えることもできますが序論では使いませんので,今は(void)のみとしておきます。

※mainの前に書かれたintはmain関数がint型の値を戻す関数であることを示しています。
多くの場合,正常終了を示すために0を戻します。ただし,実行環境によっては,この値は異なるかもしれません。

OSなのどの実行環境によっては戻り値を受け取らない環境もあります。この場合のmain関数は
void main(void)

    return;

のように,少し違う形で書かれることがあります。

標準出力を使う

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


stdio.h:標準 standard 入出力 input output ヘッダーファイルheader file
printf: print formatted でしょうか?

#include<stdio.h>

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

値の書き出し

printfには値を文字列に変換して書き出す機能があります。同じint型の値50でも文字コード50の1文字「2」、10進数の文字列「50」、16進数の文字列「32」などへの変換を書式指定子で指示することが可能です。

#include<stdio.h>

int main(void)
{
    printf("%c %d %x \n", 50, 50, 50);
    return 0;
}
/*実行結果
2 50 32    <<文字コード50の文字は「2」 50の10進数での表現「50」 50の16進数での表現「32」
*/

小数点の付く文字列への変換は%fです。

#include<stdio.h>

int main(void)
{
    printf("%f %f \n", 50.0, 50);
    return 0;
}
/*実行結果
50.000000 0.000000  <<double型の50.0は正しく文字列に変換されたが int型の50は可笑しな文字列に
*/

書式指定と2番目以降の引数の型は一致することが、正しく書き出される条件です。

※ここでは,書式指定子が「%f」だったのでint型の値50のメモリ上の0/1パターンをdouble型の0/1パターンとして誤って解釈したため出力は「0.000000」となってしまった。

変数を使う

データを書いておく場所が変数です。変数名で変数の区別を行います。データ型で変数の値とメモリに記憶される0/1パターンとの関係は決められています。

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

データ型は0/1のビットパターンでのデータ表現方法を決めたものです。4つの基本の型char int float doubleはつづりまで覚えておきましょう。

変数の定義はブロックの先頭でまとめて行うようにしましょう。用意する変数が事前にわかれば機械語への翻訳の手間が減ります。

※C89では先頭にまとめて定義する決まりでしたが、C99からは使う前であればどこでも定義できるようになりました。
しかし、皆さんは先頭でまとめて行うようにしてください

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

※ 
char : character 文字、記号、印象...
int : integer 整数
float : floating point number 浮動小数点数(2進数の浮動小数点数で実装されています)
double : double precision floating point number 倍精度浮動小数点数

データ型とprintfの書式指定

printfの書式指定子は

※なぜchar型やfloat型の場合が無いのか?
 printf関数を呼ぶときに、カッコ内に書かれた実引数の値がchar型の場合はint型に,float型の場合はdouble型に変換して仮引数を作る。この仮引数をprintf関数に渡すので,char型やfloat型の値は渡されない。これは可変長引数と呼ばれる引数の渡し方に原因がある。

※引数の数が変えられる渡し方は可変長引数(可変個引数)と呼ばれます。printfや次のscanf関数でつかわれています。序論ではこの話は難しいので、興味があったらWikiなどを調べてみてください。可変長引数では実引数の型がcharやshortの場合はintにfloatの場合はdoubleに変換して仮引数が作られ,関数へはこの仮引数が渡されます。
 (引数で渡されるデータ型の種類を減らすことで,書式指定子の種類が少なくて済みます。)

 標準入力を使う

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

データを入れる場所としては変数で用意した場所を使うことが多い。

変数の場所はアドレス演算子&を変数名の前につけて表します。

scanf 関数は入力書式とアドレスだけ受け取って処理を行うので、 書式指定子と変数のデータ型が一致しなければいけません。ここで間違うと正しくデータが読めないだけでなく、変更してはいけないメモリーにまで書き込んでプログラムの暴走の原因になったりします。

基本の型char int float doubleに対応したscanfの書式指定子は %c %d %f %lf です。

※今度はfloatとdoubleを書式指定子%f%lfで区別する必要があります。scanfには値を代入するアドレスだけが渡されます。アドレスだけではデータ型が分からないので、書式でデータ型を示す必要が在るのです。
 float型変数のアドレスに書式指定子%lfで値を書き込むと、float型の領域4バイトに対してdouble型の0/1パターン8バイトを書き込んでしまい、変数以外の場所を書き換えてしまいます。

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

[注] 対話的プログラムでは printfの後に fflush(stdout); 

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

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


scanf : scan formatted  でしょうか?

8-2. 変数の使い方と変数名

プログラムを作る(設計する)とき最初にやることはどんな変数を用いるか考えることです。計算に必要なデータが何かを考えてデータを記録する場所として変数をまず用意し、次に変数を使った計算のプログラムを書きます。

プログラムを書いている途中で、新たな変数が必要なことや、用意した変数が要らないことに気が付くかもしれませんが、それは書いている途中で修正すればいい。まずは書きながら試行錯誤をしてください。

解り易い変数名で,適切な変数を用意すれば、プログラムも解り易く書くことができます。次に簡単な例題を用意したので見てください。

例題:1〜N までの整数の合計


目的:1〜N までの整数の合計を計算する。
仕様:Nの値はソースプログラムに書く。コンパイルし実行を行うと合計を標準出力に書き出す。

変数を使わないで書くと

1) 足し算の式をそのまま使う

それなりに解り易いプログラムです。しかし、Nが大きくなると破たんするプログラムの書き方です。

#include<stdio.h>
int main(void)
{
    printf("%d",1+2+3+4+5+6+7+8+9+10+11+12);
    return 0;
}
2) 和の公式を使う

この計算に限れば良く知られた公式が在るので、それを使うこともできます。

#include<stdio.h>
int main(void)
{
    printf("%d", (1+12)*12/2 ); 
    return 0;
}

しかし、これは解り易いプログラムとはいえません^^。

何よりも公式の意味が分からないと何を計算しているか解りません。プログラムの中で用いた公式については解り易くコメントで説明することが必要でしょう。また、公式が使えるような計算でしか利用できないので使える場面が限られてしまいます。

※このようなリテラルで書いた計算式は最適化により翻訳の段階で計算されてしまいそうです。出来上がった機械語のプログラムは答えを書き出すだけのものに成るかも。

変数を使うと

1) 合計値の変数 total を用意して、プログラムが何を計算するのかを解り易くする。

解り易い変数名が重要です。ここでは合計なので total としました。整数の合計は整数なので total のデータ型はint型とします。

#include<stdio.h>
int main(void)
{
    int total;/*合計を記録する変数*/

    total = 1+2+3+4+5+6+7+8+9+10+11+12 ;
    printf("%d", total);
 
    return 0;
}
2) 繰り返しの回数を数える変数 i (iterator反復子)を用意し繰り返しの構文を使う。

足し算の式を1+2+3+...N と書くのではNが大きくなるとプログラムが間違いやすくなるのは避けられません。繰り返しの構文を用いるとNが大きくても簡潔に書けます。

反復子に使う変数の名前は i, j, k などが良く使われます。特別な意味が無いのであれば i, j, k の順番で使ってください。(ループの回数を数えているので,この変数はカウンタ変数とも言います)

※反復子の変数名に i, j, k のような名前を使うのはプログラマー社会の一つのしきたりです。暗黙の了解事項を尊重することで、多くの人に読みやすいものとなります。

※初期の言語FORTRANではI,J,K,L,M,N の何れかで始まる名前の変数は整数型と決められていました。

#include<stdio.h>
int main(void)
{
    int total;
    int i;/*繰り返しの回数を数える変数*/

    total = 0; /*加算して行くので初めは0とする */
    for(i = 1; i <= 12; i++) /* i=1〜12の範囲で繰り返す */
    {
        total = total + i;
    }
    printf("%d", total);
 
    return 0;
}
3) 繰り返し回数も変数nにする。

N=12以外でも計算できるように、繰り返しの回数12の代わりに変数nを使うことにします。プログラムの変更は変数nの初期値の変更で済むようにします。

#include<stdio.h>
int main(void)
{
    int total;
    int n = 12; /* 繰り返しの回数 */
    int i;

    total = 0;
    for(i = 1; i <= n; i++)
    {
        total = total + i;
    }
    printf("%d", total); 

    return 0;
}
4) 繰り返し回数を入力できるようにする

新仕様:繰り返し回数Nの値を標準入力から入力すると合計を標準出力に書き出す。

この方が毎回ソースファイルを書き換えてコンパイルする手間が省けます。

繰り返し回数をnに代入するscanf 関数を追加するだけです。

#include<stdio.h>
int main(void)
{
    int total;
    int n; /* 繰り返しの回数 */
    int i;
    
    scanf("%d", &n);
    
    total = 0;
    for(i = 1; i <= n; i++)
    {
        total = total + i;
    }
    printf("%d", total); 

    return 0;
}
5) プログラムが応用できる

この形のプログラムなら書き換えも容易で、色々な合計計算に使えます。例えば1/1+1/2+...+1/N の計算は

#include<stdio.h>
int main(void)
{
    double total;/* 値が実数になるのでdouble型に変更 */
    int n;
    int i;
    
    scanf("%d", &n);
    total = 0;
    for(i = 1; i <= n; i++)
    {
        total = total + 1.0 / i;/* 1/iでは整数の割り算になるので1.0/iと実数の割り算にする*/
    }
    printf("%f", total); 

    return 0;
}

8-3. 見やすく書く

C言語のプログラムは自由書式なので書き方には大きな自由度が在ります。しかしまずは教科書と同じ書き方ができるようになってください。

区切りが分かるように書く

C言語のプログラムは文法規則に従ってコンパイラに読まれるため、特に区切のスペース文字を入れなくても正しく翻訳されることが多い。しかし、人間が見る場合は区切りが見難いと間違いが増えてしまう。労力を惜しまず半角スペースなどの区切文字を使ってください。

 
    scanf("%d",&n);
    total=0;
    for(i=1;i<=n;i++)
    {
        total=total+1.0/i;
    }
    printf("%d",total); 

と書いてもコンパイラは文句を言わない。※上のプログラムは少し詰まって見えると思います。

しかし、※下は演算子の両側やセミコロンやカンマの後に空白を入れている。

    
    scanf("%d", &n);
    total = 0;
    for(i = 1; i <= n; i++)
    {
        total = total + 1.0 / i;
    }
    printf("%d", total); 

と書く方が演算子などの区別が容易になって人間には読みやすい。

字下げ(インデント:indent)で制御構造を表現する

字下げのスタイルは色々あって、これが正解というものは無い。しかし、まずは教科書の字下げと同じ書き方ができるようになってください。

※字下げはプログラムの制御構造を示すために行うので、そもそも制御構造が解っていないとできません。逆に言えば、字下げがおかしいプログラムは、書いた人がプログラムの制御構造を理解していないことの結果です。

教科書65pリスト3_11

#include<stdio.h>

int main(void)
{
    int i, c;
 
    i = 1;
    while(1)
    {
        scanf("%d", &c);

        if(c == (-1))
        {
            break;
        }
        else if( c <= 32 || 127 <= c )
        {
            continue;
        }
        else
        {
            printf("%d : %x = %c\n", i, c,  c);
        }
        i++;
    }
 
    return 0;
}

※プログラムを自動的に字下げしてくれる整形ツールがあります。
誰が書いたプログラムでも同じ書式に変換して、プログラムの見た目を統一してくれます。しかし、まずは自分でプログラムの制御構造を表現できるようになってからツールは使ってほしいと思います。

整形ツールはプログラムの間違いを未然に防ぐ効果が在ります。

8-4 宿題

課題7(初期ファイルp7.c

JISコード表を書き出すp7.cのプログラムはこのままでもコンパイルと実行が可能です。実行結果は次の様になります。

00:  01:  02:  03:  04:  05:  06:  07:  08:  09:  0A:  0B:  0C:  0D:  0E:  0F:  
10:  11:  12:  13:  14:  15:  16:  17:  18:  19:  1A:  1B:  1C:  1D:  1E:  1F:  
20:  21:! 22:" 23:# 24:$ 25:% 26:& 27:' 28:( 29:) 2A:* 2B:+ 2C:, 2D:- 2E:. 2F:/ 
30:0 31:1 32:2 33:3 34:4 35:5 36:6 37:7 38:8 39:9 3A:: 3B:; 3C:< 3D:= 3E:> 3F:? 
40:@ 41:A 42:B 43:C 44:D 45:E 46:F 47:G 48:H 49:I 4A:J 4B:K 4C:L 4D:M 4E:N 4F:O 
50:P 51:Q 52:R 53:S 54:T 55:U 56:V 57:W 58:X 59:Y 5A:Z 5B:[ 5C:\ 5D:] 5E:^ 5F:_ 
60:` 61:a 62:b 63:c 64:d 65:e 66:f 67:g 68:h 69:i 6A:j 6B:k 6C:l 6D:m 6E:n 6F:o 
70:p 71:q 72:r 73:s 74:t 75:u 76:v 77:w 78:x 79:y 7A:z 7B:{ 7C:| 7D:} 7E:~ 7F:  
80:  81:  82:  83:  84:  85:  86:  87:  88:  89:  8A:  8B:  8C:  8D:  8E:  8F:  
90:  91:  92:  93:  94:  95:  96:  97:  98:  99:  9A:  9B:  9C:  9D:  9E:  9F:  
A0:  A1:。 A2:「 A3:」 A4:、 A5:・ A6:ヲ A7:ァ A8:ィ A9:ゥ AA:ェ AB:ォ AC:ャ AD:ュ AE:ョ AF:ッ 
B0:ー B1:ア B2:イ B3:ウ B4:エ B5:オ B6:カ B7:キ B8:ク B9:ケ BA:コ BB:サ BC:シ BD:ス BE:セ BF:ソ 
C0:タ C1:チ C2:ツ C3:テ C4:ト C5:ナ C6:ニ C7:ヌ C8:ネ C9:ノ CA:ハ CB:ヒ CC:フ CD:ヘ CE:ホ CF:マ 
D0:ミ D1:ム D2:メ D3:モ D4:ヤ D5:ユ D6:ヨ D7:ラ D8:リ D9:ル DA:レ DB:ロ DC:ワ DD:ン DE:゙ DF:゚ 
E0:  E1:  E2:  E3:  E4:  E5:  E6:  E7:  E8:  E9:  EA:  EB:  EC:  ED:  EE:  EF:  
F0:  F1:  F2:  F3:  F4:  F5:  F6:  F7:  F8:  F9:  FA:  FB:  FC:  FD:  FE:  FF: 

 しかし、とても人間が読める形にはなっていません。区切文字や字下げを追加して教科書のようなプログラムに整形しなさい。教科書の字下げに合わせるためにブロック{ }を追加してもかまわない。

※整形する時にエラーを追加しないように注意すること。

おまけ

回答もすぐに見られるようにした練習問題。習っていない内容(配列や構造体)も含んでいます。そこは推理してすすめてもいいし、跳ばしてもかまいません。

練習問題1
●標準入出力の利用
●字下げ

練習問題2
●分岐と繰り返し


[ index | prev | next ]