プログラミング序論 page7(update:2017/11/07)
[ index | prev | next ] 課題6 まとめ 制御構造

7. 制御構造 control structure

(教科書 3.処理の流れ)
C言語のプログラムは基本的には書かれた順に実行される(これを順次実行と言う)。 しかし、プログラムを順番に実行するだけでは、2次方程式の解法のような判別式の結果により計算が分かれる問題には対応できない。そこでプログラムを実行する順番を制御する構文が用意されている。

7-1. 実行の流れ execution sequence

C言語に用意された制御構造(実行順番の制御の為の構造)には、順次分岐繰り返しの3つが在る。 実行の流れを示すのによく使われるのが流れ図である。処理を示す四角形を線でつないで処理の順番(流れ)を示す。条件分岐はひし形で表す。順次、分岐、繰り返し を次のような図で表現できる。

流れ図(flow chart)

 

[メモ]

計算機は命令を読むアドレスをプログラムカウンタに記憶し、命令を読むたびにカウンタをアップすることで機械語を順番に実行する。「順次」の実行は次のように翻訳した機械語をアドレス順に並べるだけでいい。

「分岐」や「繰り返し」を実現する為に、プログラムカウンタの値を変更する次のような命令が用意されている。

  1. 無条件ジャンプ命令:プログラムカウンタの値を指定したアドレスに変更する。
  2. 条件付きジャンプ命令:計算結果が特定の条件を満たしたときにのみプログラムカウンタの値を変更する。

「分岐」は、次のような機械語の並びに翻訳できる。

「繰り返し」は、次のような機械語の並びに翻訳できる。

7-2. 条件分岐 conditional branch

7-2-1. if文 (if statement)

一般的な分岐を表す構文で、構文の形は以下のようにif単独の場合もelseと組み合わせる場合もある

1) if文

書式 if(条件式)文1;

 動作 

例:

if( a - b )
    printf( "aとbは等しくない\n" );

条件式を書く為の演算子

条件式としては、a-bは異常な書き方であり、通常は非等価演算子!=を用いてa != bと書く。

if( a != b )
    printf( "aとbは等しくない\n" );

条件式を書く為に用意された演算子には次のようなものがある。これらについて以下に説明する。

※何々演算子という名前は覚えなくていい。しかし、演算子の書式と戻り値は覚えること

等価演算子
書式 戻り値
==
e1 == e2

等号:  値が等しいなら戻り値1、異なる場合に0

!=
e1 != e2

不等号: 値が異なるなら戻り値1、等しい場合に0

関係演算子
relational operator
書式 戻り値
<
e1 < e2

e1e2なら戻り値1、異なる場合に0

<=
e1 <= e2

e1e2なら戻り値1、異なる場合に0

>
e1 > e2

e1e2なら戻り値1、異なる場合に0

>=
e1 >= e2

e1e2なら戻り値1、異なる場合に0

論理演算子については後述します。

※条件式の値が0を偽とする理由
計算機の演算回路は計算結果を記憶するアキュムレータ(Accumulator)フラグレジスタ(flag register)を持つことが多い。10-10の様な計算を行うと、アキュムレータには結果の値0が書き込まれ、フラグレジスタのゼロフラグが1になる。

機械語の条件分岐は多くの場合,フラグレジスタを見て行われる、if文は条件式の計算でゼロフラグが1になった時にその直後の命令に対応する機械語部分をスキップする 条件付きジャンプ命令に翻訳される。C言語が条件式の値が0か否かで判定するのは計算機の仕組み を反映した結果である。

フラグレジスタはステータスレジスタ(status register) とも言います

※機械語には条件判定用の計算でフラグレジスタのみを更新しアキュムレータを書き換えない命令が用意されていたりする。

※C99で論理型 _Bool が追加されるまでC言語には真偽を表わす専用のデータ型は無かった。代わりに整数型で値が0なら偽、0以外なら真としていた。

_Bool型は1バイトを使い値は0,1のみを取る。「stdbool.h」をインクルードすることで、別名 bool が使える。値もマクロでtrue、falseが使えるようになる。
(PascalやJavaには真偽を表わすデータ型、booleanが用意され、条件式はbooleanを戻す式に制限されている)

ブロック

ブロックは複数の文をまとめて1つの文として扱うもの。これを用いれば条件が真の時にまとまった複数の命令文を実行するプログラムを書くことができる。 複数の命令文をで囲んだブロックに置き換え, 次のようにif の直後に置くこと、条件式が真の場合にのみブロック内の文が実行される。

if(a!=b)
{
    /*複数の文をまとめたブロック*/
    printf("-------------\n");
    printf("aとbは等しくない\n");
    printf("-------------\n");
    /*忘れずに、字下げすること*/
}


2) if-else文

書式 if(条件式)文1;else 文2;

 動作 条件式の値が0以外の場合は、次の文1のみを実行し、0の場合は文2のみを実行する。

必ず文1か文2のいずれか1つが実行される。

注意:if文を書く時は誰が見ても解かり易く、間違いが無いように字下げをしてください

 
字下げ
(インデントindent):
 行の頭にスペースやタブを入れて書き出し位置を下げることでプログラムの制御構造を解りやすくする。

※タブの幅は4文字分だったり8文字分だったりと見る環境で変わリます。面倒でも初めはスペースで字下げしてください

ブロックを用いればまとまった処理も可能になる。文が1つしか無い場合でもブロックを使って書かれることが多い。この書き方の方が if 文の構造が強調されるだけでなく、様々なプログラムミスを防ぐ効果もある。

if(条件式)
{
  文1;
}
else
{
  文2;
}

3) 入れ子のif-else文

if-else文を入れ子にして3つ以上に分岐する処理(多重分岐)を記述できる。しかし、ifとelseの対応関係が明確になるように工夫しないと訳の解らないプログラムになってしまう。 次の書き方がよく使われる。

if(条件式1)
{
  文1;
}
else if(条件式2)
{
  文2;
}
else
{
  文3;
}

この形式ではif文は常に直後のelseと対応関係にある。

条件によって文1〜文3の何れかが必ず実行される。

※入れ子のif-else文を次のように字下げして書くこともできる。しかし,この書き方は,文1,文2,文3の何れか1つが実行される,3択であることが見えにくいので,あまり使われない。(文1だけが目立つてしまう)

if(条件式1)
{
  文1;
}
else 
{
  if(条件式2)
    {
        文2;
    }
    else
    {
      文3;
    }
}
 

7-2-2. 論理演算子logical operator

条件が複雑な場合に、複数の条件式を組み合わせる為に用意された演算子。

論理演算子
演算
書式
戻り値
&&
論理積
AND
e1 && e2

e1、e2がともに真(0以外)なら戻り値1、それ以外は0

(注)e1が0ならe2を調べないで戻り値0

||
論理和
OR
e1 || e2

e1、e2が何れか真(0以外)なら戻り値1、共に偽(0)なら0

(注)e1が真(0以外)ならe2を調べないで戻り値1

!
否定
NOT
! e

eが真(0以外)なら戻り値0、eが0なら1

注)演算子の優先順位には注意

優先度が高い ! < , <=, >, >= ==, != && || 優先度が低い

A || B && C は A ||( B && C)

注)条件式の評価手順にも注意 

 &&や||の結合規則は左から右

 さらに、C言語では左から評価していくが、右を評価する必要が無い場合は右を評価しない

例えば、 1<c/(1-b/a) を評価したいがaが0やaがbと等しいときの0除算のエラーは出したくない場合に条件式を

 a!=0 && 1!=b/a && 1<c/(1-b/a)

のように書くことが可能

1)aが0ならb/aの計算を行わないで0を戻す(偽)
2)aが0でなくてもaとbの値が等しければ三番目の式は評価しないで0を戻す(偽)

7-2-3. switch文 (switch statement)

整数値に応じて複数に分岐する場合にはswitch文を使うことができる。if文のような一般性は無いが、使える場合も多い。

使える条件:
整数を戻すの値が特定の定数式の値に等しいとき、各々の分岐先が決まるような分岐の場合。整数を戻す式は何でも構わない。しかし、定数式はプログラムをコンパイルする段階で値が決まる式のことなので、定数や定数の式(3+7など)などに限られる変数や変数を用いた式、関数などは定数式ではない。

※case の後には定数式しか書けない。

下の書式のように switch文の式の値がcaseの定数式の値と等しいとき、処理がそのcase直後に分岐する。等しいものが無い場合はdefaultへ分岐する。defaultを省略すると、等しいものが無い場合、何もせずにswitch文を終了する。

switch(式){
    case 定数式1:<<リテラルもしくはリテラル間の簡単な演算式。コンパイル時点で値が定まっている式。
       文1;
       ..
       break;<<switch文から抜ける
    case 定数式2:<<caseの後の:はコロンなので見間違えないようにしてください
       文2;
            <<breakが無いと処理は順番に下に移る
    default:<<上のどのcaseにもあてはまらない場合にここに来る(defaultは省略可)
}
 

変数cに一文字読み込んでその値で分岐し演算子+−*/の場合と数字の場合のみメッセージを書き出す

int c;
c = getchar( );
switch( c )
{
    case '+':
    case '-':
    case '*':
    case '/':
        printf("%cは演算子\n",c);
        break;
    case '0':
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
        printf("%cは数字\n",c);
}

7-3. 繰り返し iteration

if文だけではプログラムの前に戻れないため繰り返し同じ部分を実行することはできません。しかし、次に実行する命令文の場所を自由に変更するgoto文と組み合わせればこれが可能になります。

C言語では例外的な場合を除いてgoto文は使わないことになっています。(実行の順番がgoto文で複雑に変化するプログラムは俗に「スパゲッティ-・プログラム」と呼ばれ、解かり難いプログラムの書き方として批判されてきました)

i = 0;
LOOP:;/*空文にラベル名をつけている*/
if(i < 10){
    printf("%d\n", i);
    i = i + 1;
    goto LOOP;/*ラベルLOOPへ跳ぶ*/
}

ifとgotoの組み合わせは自由度がありすぎて、プログラムの構造が読みにくくなるので、
C言語には下図の繰り返し専用の構文が用意されています。

7-3-1. while文

もっとも一般的な繰り返しの構文はwhile文で

while(条件式) 文 

 動作:条件式の値が真(0以外)なら文を実行し再び条件式の判定を行う。この結果、条件式の値が偽:0になるまで文を繰り返し実行する。

 ※文を実行することで条件式の値が変化しない場合は無限ループになる。

 while(条件式){文1; 文2;... }

 上記の様に文をブロックにすることでまとまったプログラムを繰り返し実行できる。 前のgotoを使った例は次の様に書き換えると簡潔なプログラムになる。

int i = 0;/*初期設定*/
while( i <= 10)/* 条件式 繰り返しの判定*/
{
   /*繰り返す範囲がわかるように字下げする*/
   printf("%d\n", i);
   i++;/*再設定 i=i+1と同じ処理*/
}

7-3-2. for文

while文は初期設定、条件式、再設定がまとまっていない欠点がある。これに対してfor文 は3つの要素をまとめて記述するため繰り返しの条件が把握しやすい利点がある。 ただし、これらの要素の記述が長い場合には、逆にわかりにくくなることもある。

 書式 for(初期設定式; 条件式; 再設定式) 

 動作:初期設定式を実行後、条件式の値が真(0以外)なら文を実行する。再設定式を実行後、再び条件式の判定を行う。while文と同様に文を複文にすることでまとまったプログラムを繰り返し実行できる。

 書式 for(初期設定式;条件式;再設定式) { 文1;文2;文3; } 

int i;
for( i = 0; i < 10 ; i++ )/*繰り返しの初期設定、条件式、再設定が1つにまとまっている*/
{  
   /*繰り返す範囲がわかるように字下げする*/
   printf("%d\n", i);
}

※プログラムでは数を数えるとき0から始めることが多い。
物の数を数えるときは1から始めることが多いので違和感があるのではないだろうか?しかし、物ではなくて状態と考えれば、何もない状態0から始めるのは逆に自然である。

注意」forでは初期設定式や条件式や再設定式の記述を省略することが許されています。
for( ; i < 10 ; i ++)初期設定式を省略したfor文です
for( i = 1;  ;i++ ) 省略された条件式部分は真とする約束なので i が1から増加する無限ループとなる
for( ; ; ) 無限ループの構文で良く使われます。while(1)も同様に無限ループの構文でよく使われる。

7-3-3. do-while文

繰り返しの文を実行後に条件判定をする構文です。

 do 文 ; while(条件式);

 あるいは

do {文1;文2; ...} while(条件式);

動作: 文を実行後、条件式を判定し結果が真であれば文の実行を繰り返す。 while文やfor文は繰り返しの文を実行する前に条件判定を行うのに対してこの構文は繰り返しの文の後で条件を判定します。最低でも1回は繰り返し部分が実行されることになります。

※繰り返しの条件が後に記述されているので,プログラムが分かりにくいことも在り,私はdo-whileはあまり使いません。

7-3-4. breakとcontinue文

break :exit form loop or switch.
continue:skip 1 iteration of loop.

while、forは繰り返し部分の前で条件判定、do-whileは後で判定します。しかし、場合によっては途中で判定したい場合も出てきます。 この様な場合にもgoto文を使わなくていいようにbreak文やcontinue文が用意されています。

break文

これはswitch分でも使われますが,whileやfor等の繰り返しループからの脱出にも使えます。while,for,do-while文から出て,次の文へ処理を移す命令です。

continue文

これはループからの脱出ではなくて、whileやdo while文なら次の処理を条件式へ移す命令。for文の場合は再設定式へ移す。 breakが繰り返し文の外へのgotoであるのに対して、continueは繰り返し文の残りをスキップするgotoです。

         

breakやcontinueが用意されているので、goto文は序論レベルではを使う必要がありませんし、使いません。

ループの中間で判定を行う 例題

連続して整数値を読み合計と平均を計算する。読込みの終端は数値に変換できない入力で示される。

..
int total, count, data;
total = 0;
count = 0;
while(1) /*条件が常に真となりwhileは無限ループとなる*/
{
    printf("%d番目のデータ>", count + 1); fflush(stdout);
    if( scanf("%d",&data) != 1) break;/*ここで条件判定してwhileループから抜ける*/
    total += data;
    count++;
}
if(count == 0)printf("データが入力されていません");
else printf("データ数:%d個 合計:%d 平均:%f \n", count, total, (double)total / count);
..

breakを使わないで書くと条件判定の前に在った入力要求のprintf部分を次のように2回書くことになる。

..
int total, count, data;
total = 0;
count = 0;
printf("%d番目のデータ>",count+1); fflush(stdout);
while(scanf("%d", &data) == 1)
{
    total += data;
    count++;
    printf("%d番目のデータ>",count+1); fflush(stdout);
}
if(count == 0)printf("データが入力されていません");
else printf("データ数:%d個 合計:%d 平均:%f \n", count, total, (double)total / count);
..

※カンマ演算子を使って条件式にprintfまで入れてしまうことも可能だが、それでは真の条件が見ずらくなる。

[メモ]printf関数の次のfflush(stdout);はprintfがバッファに書き出したものを、ただちに相手先に出力するための指示。端末エミュレータのコマンドプロンプトなどではprintfの出力はただちに画面に描きだされる設定なので不要です。しかし、通信などが間に入るような場合にはfflush(stdout);を書いておくほうが確実です。

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

行の頭にスペースやタブを入れて書き出し位置を下げることでプログラムの制御構造を解りやすくすることを俗に「字下げをする」といいます。プログラムを解りやすく読みやすくするためには重要なことなので必ず行ってください。大規模なプログラムを複数の人間で作成する場合、自分が間違えないためにも、仲間のプログラムを理解するためにも字下げは重要です。また、プログラムの内容を解りやすくするためには空行を入れるなどすることも効果的です。

 字下げの仕方は決まっているわけではありませんが、まずは教科書の字下げを参考にしてください。

7-4-1. 教科書の字下げ

教科書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;
}

7-5 課題6.(初期ファイル p6.c) 

0から99までの100個の数値を縦横10個に並べて書き出す

分岐や繰り返しの構文を用いて実行結果が次のようになること。

  0  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 55 56 57 58 59
 60 61 62 63 64 65 66 67 68 69
 70 71 72 73 74 75 76 77 78 79
 80 81 82 83 84 85 86 87 88 89
 90 91 92 93 94 95 96 97 98 99

ヒント:

1)プログラムは100回の繰り返しと、繰り返しの中で数値を書き出し、1桁目が9の条件が成立したら改行とするなど、流れ図を書いて考えを整理してから書き始めるのが良い。

2)数値は見やすいように桁を揃え、空白で区切りたい。

変数の値を空白を含めて3桁で表示したい場合は「%3d」のように書式指定をする。1桁や2桁にしかならない場合は先頭に空白が入って3文字になる。

printf("%3d", x);

あるいは数値を2文字で書き出して、その後に1文字空白を書いてもいい。

3)変数xの下1桁が9の判定は「(x%10)==9」を計算することで可能です

if( (x%10) == 9 ) printf("\n");

[ index | prev | next ]