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

先頭へ戻る

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

理解の定着・小休止②

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

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

制御構造

プログラムの処理の構造(流れ方)について、最も基本的なものとして基本制御構造があります。 基本制御構造は、順次構造分岐構造反復構造という3つの構造から構成されます。

順次構造は、単純に上から下へ流れるだけといった構造です。

分岐構造(選択構造)は、何らかの条件によって、処理が途中で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文だけであれば { } は省略できますが、省略しないことを勧めます。 後から新たな文を付けたしたときに、{ } も付け加えることを忘れる可能性があるからです。
ただし、省略が可能であることは覚えておいて下さい。 他人が書いたプログラムを読むときには、必要な知識です。

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

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

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

1文字同士の比較には使えます。実は、C言語の「文字」の正体は「数値」なのです。 ところが、文字列となると話が複雑になってきます。 この辺りはもっとずっと先で説明します。

文字列同士を比較する場合には、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文が抜け出せます。 ただし、一番最後の break文だけは無くても構いません。 しかし、これについても省略しないことを勧めます。

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 になります。

ちなみに、!演算子>&&演算子>||演算子の順番で、実行の優先順位が設定されています。 C言語では、優先順位の問題は幾つかの場面で現れてきます。 例えば、次のような if文は意図しない結果を生みます。

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

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

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

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

なお、先ほどの間違った方のコードでは、変数num の値が 0 のときにだけ、if文が真となり、printf関数が呼び出されます。 2 で割った余りが 1 になるためには、変数num を反転した結果が奇数でなくてはなりません。 反転した結果が 1(真)になるには、反転前が 0(偽)である必要があるということになります。

短絡評価

短絡評価(ショートサーキット)は、複数の条件式を組み合わせる場合に、 全体としての結果が確定する段階で、それ以上の条件の判定を取りやめることで、効率を向上させる機能(性質)です。 具体的には、以下の2点が短絡評価に当たります。

例えば、次のように書くと、&&演算子の左側にある「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文は、関数から抜け出し呼び出し元へ戻す命令です。このとき、戻り値を1個だけ指定することができます。 関数についてはこの後で触れます。

関数

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

関数は、次のような形式になっています。

戻り値の型 関数名( 引数の型と名前, 引数の型と名前, … )  /* 引数は 0個以上 */
{
	/* 関数の本体 */
	
	return /* 戻り値 */		/* 戻り値がなければ return文自体が不要 */
}

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

ジャンプ文のところで登場した return文は、戻り値を返す役割を兼ねています。 戻り値の型が void型であれば、return文は戻り値を伴う必要はなく、単体で「return;」と書けます

void というのは、「何もない」ことを表すキーワードで、引数の型や戻り値の型に使用できます。 変数を宣言するときには使えません。

第31章では、これら以外の用途で void が登場します。

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

#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つに、同じような処理を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通りを作成してみて下さい。


解答ページはこちら

参考リンク

更新履歴

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

'2015/8/22 リンク先が間違っている箇所があったのを修正。

'2009/5/30 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ