C言語編 第18章 理解の定着・小休止②

先頭へ戻る

この章の概要

この章の概要です。

理解の定着・小休止②

この章では、これまでの章内容の理解を再確認しましょう。 また、1章丸ごとを割くほどでも無い細かい部分について、少し触れていきます。

今回は、以下の範囲が対象です。 処理の流れの理解、関数による部品化がテーマとなります。

制御構造

プログラムの構造には、 順次構造分岐構造反復構造があります。 これらをまとめて、基本制御構造と呼びます。

順次構造は、次の命令、次の命令へと進んでいくだけの単純な構造です。

分岐構造(選択構造)は、何らかの条件によって、処理が途中で2方向以上に分かれる構造です。 C言語では、if文や switch文を使って表現します。

反復構造(ループ構造)は、同じ内容の処理を何度も繰り返す構造です。 C言語では、while文、for文、do文を使って表現します。 一応、goto文で作ることも可能ですが、C言語でそのようなことをする理由はほとんどありません。

論理値

論理値(ブール値)は、真(true)偽(false) という2つの値のいずれか一方を持つような値です。

例えば、「変数num の値が 100以上かどうか」という条件式があるとき、 実際に変数num の値が 200 であれば、「この条件式の結果は真である」と言えます。 同様に、変数num の値が 50 であれば、「この条件式の結果は偽である」と言います。

C言語においては、0 という整数が偽であり、0以外のあらゆる整数は真です。 このように、論理値はただの整数なので、int型で表現することができます。

真が 0以外、偽が 0 であることを、次のプログラムで確認できます。

#include <stdio.h>

int main(void)
{
    int boolean;
    char str[20];

    puts( "適当な整数を入力して下さい。" );
    fgets( str, sizeof(str), stdin );
    sscanf( str, "%d", &boolean );


    if( boolean ){
        /* 真とは条件を満たすことを言う */
        /* 真は 0以外なので、入力された値が 0以外のときにここに来る */
        puts( "真" );
    }
    else{
        /* 偽とは条件を満たさないことを言う */
        /* 偽は 0なので、入力された値が 0 のときにここに来る */
        puts( "偽" );
    }

    return 0;
}

実行結果:

適当な整数を入力して下さい。
8
真

分岐構造①(分岐するかしないか)

C言語で分岐構造を作る簡単な方法は、if文を使うことです。 まずは、条件式が真になるかどうかだけを判定する場合の例を挙げます。

#include <stdio.h>

int main(void)
{
    char str[40];
    int num;

    puts( "整数を入力して下さい。" );
    fgets( str, sizeof(str), stdin );
    sscanf( str, "%d", &num );

    if( num >= 100 ){
        puts( "100以上" );    /* 変数num が 100以上であれば、ここが実行される */
    }

    puts( "終了します。" );   /* ここは常に実行される */

    return 0;
}

実行結果:

整数を入力して下さい。
100
100以上
終了します。

if文の構造は次のようになります。

if( 条件式 ){
    条件を満たす場合に実行する処理1;
    条件を満たす場合に実行する処理2;
    :
}

実行する処理が1文だけであれば { } は省略できますが、省略しないことを勧めます。 後から新たな文を付け足すときに、{ } を付け忘れることによるバグを防ぐためです。 ただし、省略が可能であることは覚えておいて下さい。 他人が書いたプログラムを読むときには、必要な知識です。

条件式の部分ですが、次の表にあるような演算子を使用できます。

演算子 意味
< 左辺が右辺より小さい
> 左辺が右辺より大きい
<= 左辺が右辺以下
>= 左辺が右辺以上
== 左辺と右辺が同じ
!= 左辺と右辺が同じでない

先頭の4つは、大小関係を調べる演算子で、関係演算子と総称されます。 後ろの2つは、等しいか等しくないかを調べる演算子で、等価演算子と総称されます。

「以上」「以下」という言葉には、「イコール」が含まれることに注意して下さい。 また、これら演算子は数値に対して使うものですから、文字列の比較には使えません。 文字列同士を比較する場合には、strcmp関数という標準ライブラリ関数を使用します。

#include <stdio.h>
#include <string.h>

int main(void)
{
    char str[6] = "Hello";

    if( strcmp( str, "Hello" ) == 0 ){
        puts( "一致しています。" );
    }

    return 0;
}

実行結果:

一致しています。

strcmp関数は、2つの文字列が一致していれば 0 を返し、1つ目の方が小さければ負数を、 1つ目の方が大きければ 0 より大きい値を返します。 ここで「小さい・大きい」というのは、辞書順で早く登場する文字が小さいという意味合いになります。 なお、strcmp関数を使うには、「#include <string.h>」という新たな行が必要になります

分岐構造②(真偽による2通りの処理)

条件を満たさない場合(偽の場合)にだけ実行するような処理が記述したければ、 else というキーワードを使って、次のような形の if文を作ります。

if( 条件式 ){
    条件を満たす場合に実行する処理;
}
else{
    条件を満たさない場合に実行する処理;
}

ここでも、中身が1文だけならば { }は省略できますが、省略しないことを勧めます。

次のプログラムは、標準入力から受け取った整数が偶数か奇数かを判定しています。

#include <stdio.h>
#include <string.h>

int main(void)
{
    char str[40];
    int num;

    puts( "整数を入力して下さい。" );
    fgets( str, sizeof(str), stdin );
    sscanf( str, "%d", &num );

    if( num % 2 == 0 ){
        puts( "偶数です" );
    }
    else{
        puts( "奇数です" );
    }

    return 0;
}

実行結果:

整数を入力して下さい。
7
奇数です

分岐構造③(if文による多方向分岐)

3方向以上へ分岐させたいときには、if と else を組み合わせる方法と、 後で説明する switch文による方法とがあります。 if と else による方法は次のようになります。

#include <stdio.h>

int main(void)
{
    char in;
    char str[10];

    puts( "質問の答えを入力して下さい。(y/n)" );
    fgets( str, sizeof(str), stdin );
    sscanf( str, "%c", &in );

    if( in == 'y' ){
        puts( "Yes" );
    }
    else if( in == 'n' ){
        puts( "No" );
    }
    else{
        puts( "Others" );
    }

    return 0;
}

実行結果:

質問の答えを入力して下さい。(y/n)
y
Yes

「else if」というのは2つのキーワードを組み合わせて使っているのであって、1つのキーワードという訳ではありません。 つまり、

    if( in == 'y' ){
        puts( "Yes" );
    }
    else if( in == 'n' ){
        puts( "No" );
    }
    else{
        puts( "Others" );
    }

と、

    if( in == 'y' ){
        puts( "Yes" );
    }
    else{
        /* A */
        if( in == 'n' ){
            puts( "No" );
        }
        else{
            puts( "Others" );
        }
    }

とでは、意味合いが同じであるとは限りません。 例えば、後者の方のコメント「A」の部分は、変数in が 'y' でさえ無ければいつでも実行されることになります。 しかしこの「A」の部分のような場所は、前者の方のプログラムには存在しません。

分岐構造④(switch文による多方向分岐)

多方向分岐のもう1つの手段は、switch文を使うことです。 switch文の構造は、次のようになります。

switch( 整数値 ){
    case 定数整数:
        定数に一致する場合に実行する処理;
        break;

    case 定数整数:
        定数に一致する場合に実行する処理;
        break;

    default:
        どの定数にも一致しない場合に実行する処理;
        break;
}

switch の直後に来る ( ) の中に整数値を記述し、その後に幾つかの caseラベルが続きます。 ラベルというのは、「○○○:」のような構造で、ソースコード上の場所を示す一種の目印のようなものです。

caseラベルには、整数の定数を記述します。 これが条件の整数値と一致すれば、その部分の処理が実行されます。 switch文の条件には、整数しか使えません

もし、どの caseラベルにも一致しなければ、 defaultラベルが実行されます。 defaultラベルについては省略が可能ですが、省略しないことを勧めますdefaultラベルが省略された場合は、どの caseラベルにも一致しなければ、何もせずに switch文全体から抜け出します

各ラベルで行う処理の終わりには、 break文があります。 break文が実行されると、switch文全体から抜け出します。 break文を caseラベルや defaultラベルのところで行う処理の終わりに書いておくことによって、 処理の終わりをきちんと検出して、switch文が抜け出せます。
ただし、switch文の } の直前、つまり一番最後の break文だけは無くても構いません。 あってもなくても、switch文の末尾に来ているので、自然に抜け出します。 しかし、これについても、個人的には省略しないことを勧めます。

switch文を使った分岐構造の例を挙げます。

#include <stdio.h>

int main(void)
{
    char in;
    char str[10];

    puts( "質問の答えを入力して下さい。(y/n)" );
    fgets( str, sizeof(str), stdin );
    sscanf( str, "%c", &in );

    switch( in ){
    case 'y':
        puts( "Yes" );
        break;

    case 'n':
        puts( "No" );
        break;

    default:
        puts( "Others" );
        break;
    }

    return 0;
}

実行結果:

質問の答えを入力して下さい。(y/n)
y
Yes

break文は省略することができます。 break文がないと、switch文から抜け出さず、次のラベル(他の caseラベルや defaultラベルのこと)にまで進んでいきます。 これをフォールスルーと言います。 フォールスルーは基本的には避けた方が無難です。

switch文の使い方の方針を挙げておきます。

論理演算

論理演算子を使うと、複数の条件を組み合わせたり、否定形を作ったりできます。 論理演算子には、以下の3種類があります。

論理演算子 名前 意味
&& 論理積演算子(論理AND演算子) ~かつ~のとき真
|| 論理和演算子(論理OR演算子) ~または~あるいはその両方のとき真
! 否定演算子(NOT演算子) 真と偽を入れ替える

次のプログラムで、それぞれの意味が確認できます。

#include <stdio.h>

int main(void)
{
    int a = 0;
    int b = 10;


    if( a < b && a % 2 == 0 ){
        puts( "a は b よりも小さく、a は偶数です。" );
    }
    if( a > b || a % 2 == 0 ){
        puts( "a は b よりも大きい、または、偶数です。" );
    }
    if( !a ){
        puts( "a は 0 です。" );
    }

    return 0;
}

実行結果:

a は b よりも小さく、a は偶数です。
a は b よりも大きい、または、偶数です。
a は 0 です。

条件式の結果に関してだけは、「真」は 1、「偽」は 0 であると明確に定められています。 しかし、「真」は 0以外であると考えておいた方が無難です。 !演算子で反転した結果も同様で、反転前が 0以外なら 0 に、反転前が 0 なら 1 になります。

ちなみに、!演算子>&&演算子>||演算子の順番で、実行の優先順位が設定されています。 (すべての演算子の優先順位の一覧は、こちらを参照して下さい)。

例えば、次のような if文は、意図しない結果を生みます。

if( !num % 2 == 1 ){
    printf( "%d は奇数ではありません。\n", num );
}

!演算子の優先順位は、%演算子よりも高く設定されているため、 このコードでは、変数num を反転した結果を 2 で割った余りと、1 とを比較することになります。 正しくは、次のように書きます。

if( !(num % 2) == 1 ){
    printf( "%d は奇数ではありません。\n", num );
}

実社会においての常識で、!演算子と %演算子がどちらが強いか判断することは難しいでしょう。 こういう曖昧な場面では、無理せず ( ) を使うべきです。 ( ) を使った結果、条件式が読みづらくなるようなら、そもそも条件式が複雑過ぎます。 条件式の一部を、いったん変数に格納してから使うといった工夫をして、簡単な判定文になるようにしましょう。

短絡評価

&&演算子や ||演算子を使う場合、短絡評価(ショートサーキット)という性質を知っておくべきです。 これは、複数の条件式を組み合わせている場合、&& や || の左側にある条件式を先に調べ、 その段階で、全体の結果が確定するのであれば、右側の条件式を調べないというものです

例えば、次のように書くと、&&演算子の左側にある「num == 0」が偽であれば、func関数の呼び出しが省略されることになります。

if( num == 0 && func() ){
}

func関数の中身が非常に大きい場合、実行速度を向上させる効果が見込めます。 しかし、func関数が必ず実行しなければならないものだとすれば、バグの原因になり得ます。 func関数を必ず実行させるためには、if文から独立させるべきです。

短絡評価をうまく使うことで効率を向上できる可能性はありますが、不必要な効率化は避けるべきです。 一般に、十分な性能が出ているプログラムを、無理に効率向上させるべきではありません。

while文によるループ構造

ループ構造を作る構文の中で、一番単純なものは while文です。 while文の構造は次のようになります。

while( 条件式 ){
    ループさせる処理
}

{ } の中身が1文だけであれば、{ } は省略できますが、省略しないことを勧めます。 while文の流れを整理すると、次のようになります。

  1. 「条件式」を調べる。「真」なら2へ。「偽」なら4へ。
  2. 「ループさせる処理」を実行する。3へ。
  3. 「ループさせる処理」が全て終了したら1へ戻る。
  4. while文を終了させて、次の処理へ進む。

したがって、1~3の部分がループしていることになります。 もし、初めて1に来た時点で、条件式が偽になっている場合は、ループ内の処理は1度も実行されません

次のプログラムは、標準入力から受け取った文字を、10回出力します。

#include <stdio.h>

int main(void)
{
    char c;
    char str[20];
    int count;

    puts( "1文字入力して下さい。" );
    fgets( str, sizeof(str), stdin );
    sscanf( str, "%c", &c );

    /* 10回出力する */
    count = 0;
    while( count < 10 ){
        printf( "%c\n", c );
        count = count + 1;
    }

    return 0;
}

実行結果:

1文字入力して下さい。
y
y
y
y
y
y
y
y
y
y
y

for文によるループ構造

ループ構造を作る構文の中で、一番多機能なのが for文です。 for文の構造は次のようになります。

for( 初期設定式; 条件式; 再設定式 ){
    ループさせる処理
}

やはり、{ } の中身が1文だけであれば、{ } は省略できますが、省略しないことを勧めます。 for文の流れを整理すると、次のようになります。

  1. 「初期設定式」を実行。2へ。
  2. 「条件式」を調べる。「真」なら3へ。「偽」なら6へ。
  3. 「ループさせる処理」を実行する。4へ。
  4. 「ループさせる処理」が全て終了したら5へ。
  5. 「再設定式」を実行。2へ戻る。
  6. for文を終了させて、次の処理へ進む。

次のプログラムは、標準入力から受け取った文字を、10回出力します。

#include <stdio.h>

int main(void)
{
    char c;
    char str[20];
    int i;

    puts( "1文字入力して下さい。" );
    fgets( str, sizeof(str), stdin );
    sscanf( str, "%c", &c );

    /* 10回出力する */
    for( i = 0; i < 10; ++i ){
        printf( "%c\n", c );
    }

    return 0;
}

実行結果:

1文字入力して下さい。
y
y
y
y
y
y
y
y
y
y
y

for文では、インクリメント演算子デクリメント演算子を使う機会が多くあります。 インクリメントは +1 すること、デクリメントは -1 することを指します。

「++num」の場合と「num++」の場合とでは、結果が異なります。

「++num」のように、変数の前に「++」と書く場合を前置インクリメントと呼び、 「まず +1 を行ってから、次へ進む」という意味になります。 従って、変数num の値が元々 10 だった場合、まず +1 されて 11 になった後、代入などの次の処理を行うことになります。

「num++」のように、変数の後に「++」と書く場合を後置インクリメントと呼び、 「次に行うことを先に行ってから、+1 を行う」という意味になります。 従って、変数num の値が元々 10 だった場合、まず 10 のまま代入が行われ、その後で +1 されます。

前置と後置の違いは、デクリメント演算子の場合でも全く同様です。 前置と後置は、代入を伴わず単体で使う場合には意味は変わりません。 代入が組み合わさったときに初めて違いが生じます。 代入がなければ、次の4つは同じ意味になります。

最後の「+=」は「+」と「=」の複合形で、複合代入演算子と呼ばれます。 これは「-=」「*=」「/=」「%=」といった具合に、それぞれ存在しています。 これらは、プログラムを少ない記述で簡潔に書くために用意されており、構文糖(シンタックスシュガー)と呼ばれています。

do文によるループ構造

ループ構造を作る構文の中で、特殊な構造を持つのが do文です。 do文の構造は次のようになります。

do{
    ループさせる処理
} while( 条件式 );

やはり、{ } の中身が1文だけであれば、{ } は省略できますが、省略しないことを勧めます。 do文の流れを整理すると、次のようになります。

  1. 「ループさせる処理」を実行する。2へ。
  2. 「ループさせる処理」が全て終了したら3へ。
  3. 「条件式」を調べる。「真」なら1へ。「偽」なら4へ。
  4. do文を終了させて、次の処理へ進む。

次のプログラムは、標準入力から受け取った文字を、10回出力します。

#include <stdio.h>
#include <string.h>

int main(void)
{
    char c;
    char str[20];
    int count;

    puts( "1文字入力して下さい。" );
    fgets( str, sizeof(str), stdin );
    sscanf( str, "%c", &c );

    /* 10回出力する */
    count = 0;
    do{
        printf( "%c\n", c );
        ++count;
    } while( count < 10 );

    return 0;
}

実行結果:

1文字入力して下さい。
y
y
y
y
y
y
y
y
y
y
y

while文や for文との比較のために、あえて同じサンプルを持ち出しましたが、 このような while文や for文で自然に書けるループを、do文で書くのは悪い使い方です。 一般的に、do文が形作る「後判定ループ」は、 while文や for文が作る「前判定ループ」よりも、読みづらくなる傾向があります。 もちろん、do文の方が自然な場合もありますが、そうでない限り、あえて do文を使う理由はありません。

無限ループ

無限ループは、条件式が偽になることが決してないようなループです。 無限ループは、決して抜け出すことがなく、従ってそのループは終わることがないか、 あるいは、後で紹介するジャンプ文を使って強制的に終了させるかします。

無限ループは、次のいずれかの方法で作ります。

while( 1 ){
    printf( "!!!\n" );
}
for( ;; ){
    printf( "!!!\n" );
}

これ以外の方法でも作れない訳ではありませんが、変な書き方をやめておきましょう。

ジャンプ文

ジャンプ文(跳躍文)は、処理の流れを一気に変えてしまうもので、 break文continue文goto文return文の4つがあります。

break文は、それが記述されている箇所から一番近いループ、または switch文から抜け出します。 次のサンプルプログラムでは、無限ループから抜け出すために break文を利用しています。

#include <stdio.h>
#include <string.h>

int main(void)
{
    char password[16] = "password";
    char try_password[16];
    char str[16];


    while( 1 ){
        puts( "正しいパスワードを入力して下さい。" );
        fgets( str, sizeof(str), stdin );
        sscanf( str, "%s", try_password );

        if( strcmp( try_password, password ) == 0 ){
            break;
        }
    }

    puts( "入力に成功しました。" );

    return 0;
}

実行結果:

正しいパスワードを入力して下さい。
passward
正しいパスワードを入力して下さい。
password
入力に成功しました。

break文は、ネストしたループや switch文から、一気に抜け出すようなことができません。 そういう場合には、goto文を利用できます。 goto文は、指定したラベルまでジャンプする命令です。 ただし、同じ関数内でなければなりません

#include <stdio.h>

int main(void)
{
    int i;
    int j;


    for( i = 1; i <= 9; ++i ){
        for( j = 1; j <= 9; ++j ){
            printf( "%d ", i * j );

            /* 答えが 50 を超えるものが現れたら、終了させたい */
            if( i * j > 50 ){
                printf( "\n" );		/* 途中で抜け出すので、改行しておく */
                goto loop_end;
            }
        }
        printf( "\n" );
    }
loop_end:    /* goto文はここへ飛んでくる */

    return 0;
}

実行結果:

1 2 3 4 5 6 7 8 9
2 4 6 8 10 12 14 16 18
3 6 9 12 15 18 21 24 27
4 8 12 16 20 24 28 32 36
5 10 15 20 25 30 35 40 45
6 12 18 24 30 36 42 48 54

goto文の乱用は、プログラムを非常に読みにくいものにしてしまいます。 上のサンプルのように、break文だけでは抜け出せないような深いネストから抜け出すとき(ただし、ネストを深くし過ぎないように注意を払うのが先です)や、 エラー処理を1箇所にまとめる場合にだけ、goto文を使うのが適切でしょう。

continue文は、現在の回のループを打ち切って、次の回のループへ進ませる命令です。 for文の場合は、「再設定式」が実行されてから、次へ進みます。 次の回のループへ進んだとき、当然「条件式」がチェックされます。 これが偽になれば、ループ全体を終えることになります。

なお、continue文は、break文とは違って、switch文とは何の関係もありません。

次のプログラムは、奇数の入力だけを無視しながら、入力された値を標準出力へ出力します(負数を入力すると終了します)。

#include <stdio.h>

int main(void)
{
    int num;
    char str[40];


    while( 1 ){
        puts( "整数を入力して下さい。" );
        fgets( str, sizeof(str), stdin );
        sscanf( str, "%d", &num );

        /* 負数が入力されたら終了 */
        if( num < 0 ){
            break;
        }

        /* 奇数は無視 */
        if( num % 2 == 1 ){
            continue;
        }

        printf( "%d\n", num );
    }

    return 0;
}

実行結果:

整数を入力して下さい。
5
整数を入力して下さい。
7
整数を入力して下さい。
10
10
整数を入力して下さい。
-1

return文は、関数から抜け出し呼び出し元へ戻す命令です。 このとき、戻り値の型が void型でなければ、戻り値を1個だけ指定することができます。 関数についてはこの後で触れます。

関数

関数は、何らかの処理を行うために必要な命令文を書き並べたものです。 それは「プログラム」でも同じことなのですが、「関数」はもっと小さな単位と考えられます。 C言語のプログラムは、多数の関数を組み合わせて記述するというスタイルを取っています。

関数の定義は、次のような形式で記述します。

戻り値の型 関数名( 引数の型と名前, 引数の型と名前, … )  /* 引数は 0個以上 */
{
    /* 関数の本体 */

    return /* 戻り値 */		/* 戻り値がなければ return文自体が不要 */
}

引数は、関数を呼び出すときに、その関数に指定する情報のことです。 呼び出す側としては引数のことを実引数といい、呼び出される側としては仮引数といいます。
逆に、戻り値(返却値)は、関数から返される情報のことです。

ジャンプ文のところで登場した return文は、戻り値を指定する役割を兼ねています。 戻り値の型が void型の場合は、戻り値が無いことを表しています。 この場合、return文は戻り値を指定する必要はなく、単体で「return;」と書けます

関数を新たに作る場合、関数プロトタイプを記述すべきです

#include <stdio.h>

double calcAreaOfTriangle(double base, double height);  /* 関数プロトタイプ */


int main(void)
{
    double base, height, area;


    base   = 3.0;
    height = 5.0;
    area   = calcAreaOfTriangle( base, height );
    printf( "底辺:%f  高さ:%f  面積:%f\n", base, height, area );

    base   = 7.0;
    height = 4.0;
    area   = calcAreaOfTriangle( base, height );
    printf( "底辺:%f  高さ:%f  面積:%f\n", base, height, area );

    base   = 4.4;
    height = 3.6;
    area   = calcAreaOfTriangle( base, height );
    printf( "底辺:%f  高さ:%f  面積:%f\n", base, height, area );

    return 0;
}

/*
    三角形の面積を求める。
    引数
        base:	底辺の長さ。
        height:	高さ。
    戻り値
        面積。
*/
double calcAreaOfTriangle(double base, double height)
{
    return ( base * height / 2 );
}

実行結果:

底辺:3.000000  高さ:5.000000  面積:7.500000
底辺:7.000000  高さ:4.000000  面積:14.000000
底辺:4.400000  高さ:3.600000  面積:7.920000

関数プロトタイプ(関数原型)は、関数の仮引数の並びを、以下のルールに沿って記述したものです。

  1. 引数がないときには、空ではなく void と記述する
  2. 引数が固定個数あるときには、その型を書き並べる(関数宣言の場合は、名前を省略してもよい)
  3. 引数が可変個のときには、固定の部分は上記に従い、可変個部分は ... と記述する

関数プロトタイプの記法を使えば、 コンパイラは、仮引数と実引数の対応が一致していないことを検出して、エラーを報告することができるようになります。 安全性を高めるため、常に、関数プロトタイプを使うようにして下さい。

関数化する意味

関数を使うと何が良いのでしょうか。

1つに、同じような処理を1箇所にまとめることで、コードの再利用ができるようにするという点があります。 これまでにも繰り返し書いてきましたが、同じ意味の同じコードを複数書く行為は避けるべきことです。 後になって修正を加えたくなったとき、同じコードがある箇所を全て探し出して、1つ1つ修正しなくてはならなくなります。 1つでも修正し損なったら、バグの原因になります。

そこで、何度も繰り返し登場するような共通の処理は、関数化しておき、その関数を呼び出すようにすると良いのです。 「似ているけれど、使う値が若干違う」処理の場合には、引数や戻り値の仕組みを使って対応できます。

同じ意味のコードが関数として1箇所にまとめられていれば、 コードの修正が必要になったときには、関数の中身だけを直せば済みます。


また、処理を関数化しておくと、 その関数を呼び出す側は、処理の詳細を理解する必要がなくなるというのも利点の1つです。

例えば、printf関数とか scanf関数とかの中身を詳しく理解している人は、ほとんどいないと思いますが、それでも大いに活用できている訳です。 他人が作った関数は、中身を知らなくても、一応使えるということです。 このとき、関数に名前があるということも、理解の大きな助けになっています。

便利な関数を作れば、それを他の人達に使ってもらうことができます。 数学の難しい公式を使って結果を導き出す処理といった場合でも、関数にまとめてしまえば、使う側としては非常に楽になります。 例えば、数学が得意な人が関数化して、苦手な人はそれを使わせてもらう。ということも考えられます。


このように、関数の利点は幾つかありますが、結局のところ「部品化」というのが最大のポイントです。 同じ処理は「部品」としてまとめ、分かりやすい名前を与えてやるのです。 そうすれば、繰り返し同じようなコードを書く必要がなくなり、かつ中身を詳しく知らなくても使うことができます。


練習問題

まとめとして、多めに練習問題を用意しました。★の数は難易度を表します。

問題① int型の引数を受け取り、その絶対値を返す関数を作成して下さい。[★★]

問題② 2つの int型の引数a と b を受け取り、b が a の約数かどうかを判定する関数を作成して下さい。[★★]

問題③ 次のプログラムで、while文によるループから決して抜け出すことができない理由を答えて下さい。[★]

#include <stdio.h>

int main(void)
{
    char str[40];
    int num = 1;

    while( num = 1 ){
        puts( "整数を入力して下さい。" );
        fgets( str, sizeof(str), stdin );
        sscanf( str, "%d", &num );
    }

    return 0;
}

問題④ 次のプログラムを実行すると、何が出力されるでしょう。[★]

#include <stdio.h>

int main(void)
{
    int num = -5;

    if( num > 0 )
        if( num == 10 )
            puts( "aaa" );
    else {
        puts( "bbb" );
    }

    return 0;
}

問題⑤ 次のプログラムを実行すると、何が出力されるでしょう。[★]

#include <stdio.h>

int main(void)
{
    int i;

    for( i = 0; i < 5; ++i );
        printf( "%d\n", i );

    return 0;
}

問題⑥ 標準入力から受け取った西暦年が、閏年かどうか判定するプログラムを作成して下さい。[★★★]
※閏年の定義を正確に知っていますか?「4年に1度」だけでは不正解です。

問題⑦ int型の引数を受け取り、桁数を返す関数を作成して下さい。[★★★]

問題⑧ 次のプログラムは、どうなると終了しますか?[★]

#include <stdio.h>

int main(void)
{
    char str[40];
    int num;

    do {
        puts( "整数を入力して下さい。" );
        fgets( str, sizeof(str), stdin );
        sscanf( str, "%d", &num );
    }while( num );

    return 0;
}

問題⑨ 標準入力から 10個の整数を受け取り、奇数だけの平均値、偶数だけの平均値を求めるプログラムを作成して下さい。[★★★]

問題⑩ 次のプログラムは間違っています。間違いを指摘して下さい。[★]

#include <stdio.h>

void getFloatFromStdin(void);

int main(void)
{
    printf( "%f\n", getFloatFromStdin() );

    return 0;
}


/*
    標準入力から float型の値を受け取る。
*/
void getFloatFromStdin(void)
{
    char str[40];
    float f;

    puts( "浮動小数点数を入力して下さい。" );
    fgets( str, sizeof(str), stdin );
    sscanf( str, "%f", &f );

    return f;
}

問題⑪ 次のプログラムは間違っています。間違いを指摘して下さい。[★]

#include <stdio.h>

int main(void)
{
    double d = 2.0;

    switch( d ){
    case 1.0:
        puts( "a" );
        break;
    case 2.0:
        puts( "b" );
        break;
    default:
        /* 何もしない */
        break;
    }

    return 0;
}

問題⑫ 次のプログラムを、論理演算子を使って、同じ意味になるように書き変えて下さい。[★★]

#include <stdio.h>

int main(void)
{
    int a = 10;
    int b = 5;


    if( a == 10 ){
        if( b == 5 ){
            puts( "OK" );
        }
    }

    return 0;
}

問題⑬ 次のソースコード中の4箇所のコメント部分に、適切なジャンプ文を当てはめて下さい。[★]
(goto文や return文の場合には、適切なラベル名や戻り値を伴うものとします)。

#include <stdio.h>

int main(void)
{
    char str[40];
    int num;

    while( 1 ){
        puts( "1以上の整数を入力して下さい。0 で終了します。" );
        fgets( str, sizeof(str), stdin );
        sscanf( str, "%d", &num );

        if( num == 0 ){
            /* A */;
        }
        if( num < 0 ){
            puts( "負数は受け付けません。入力し直して下さい。" );
            /* B */;
        }

        switch( num % 2 ){
        case 0:
            puts( "偶数です。" );
            /* C */;
        case 1:
            puts( "奇数です。" );
            break;
        default:
            puts( "エラー:ここに来ることはないはず!" );
            /* D */;
        }
    }
end_loop:
    puts( "終了します。" );

    return 0;
}

問題⑭ 問題⑬のプログラムにおいて、4箇所のコメント部分に、「決して当てはめることができないジャンプ文」を答えて下さい。[★★]

問題⑮ 30 から始めて、3ずつ減らしながら、0 までの各値を出力するようなループを作って下さい。[★★]
余裕があれば、for文、while文、do文、goto文による方法の全4通りを作成してみて下さい。


解答ページはこちら

参考リンク

更新履歴

'2018/2/13 全面的に文章を見直し、修正を行った。

'2018/2/5 「==」や「!=」を関係演算子と表記していたのを、等価演算子に改めた。

'2018/1/31 画面という表現を、標準出力に改めた。

'2018/1/28 「小数」という表現を、「浮動小数点数」に置き換えた。

'2017/6/9 C99 で関数プロトタイプが必須だとする記述を削除。
古い書き方は廃止予定になっているが、新しい関数プロトタイプを必須とはしていない。

▼更に古い更新履歴を展開


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS

前の章へ(第17章 処理の流れを制御する)

次の章へ(第19章 数の表現方法)

C言語編のトップページへ

Programming Place Plus のトップページへ