2004年度前期 IT教育基礎論特論B
手続き型プログラミングパラダイムを例にとり、 C言語を使用してプログラムの記述方法およびその解釈を学習する。 ただし、本講義の内容はC言語の言語仕様に添う形となっているが、 手続き型プログラミングパラダイムに基づく プログラムの記述方法の学習を目的としており、 C言語の言語仕様を学習することを目的としているわけではない。 C言語に関しては、各自、別途学習すること。
なんらかのプログラミング言語によりプログラムを記述し、 コンピュータ上で実行するには、 そのプログラミング言語により、プログラムをどのように記述し、 また記述されたプログラムがどのように実行(解釈)されるかを理解する必要がある。
ここでは、手続き型プログラミングパラダイムに基づくプログラミング言語、 具体的にはC言語を例に取り、 プログラムの記述方法、ならびにその実行方法について学習する。
基本式とは、 そのプログラミング言語により提供される基本的な機能の実行を 定義するものであった。 手続き型プログラミング言語における基本式は、 1つの手続きの実行を定義するものであり、 C言語では、演算式や関数呼び出し式などを記述することができる。
演算式とは、なんらかの演算子による演算を行う式である。 C言語では、「+」や「-」などの四則演算や、 「&&(論理積)」や「||(論理和)」などの論理演算、 「==(等号)」や「>(大なり)」などの比較演算を記述することができる。
例えば、2つの数値、「3」と「2」との 足し算を行いたい場合、 以下のように演算式を記述することができる。
3+2;
ただし、C言語の場合、 BASICやLISPのように与えられた式のみを直接実行することはでず、 1つのプログラムには特別な関数であるmain()関数が必ず1つ存在し、 プログラムを実行すると先ず、 main()関数が呼び出されるというルールがあるため、 上記の演算式を実行するには、 実際には以下のようなプログラムを作成する必要がある。
/* simple-operation.c */ int main(int argc, char **argv) { 3+2; } |
またこのプログラムは、 「3+2」の演算をコンピュータ内部で行うだけであり、 何らかの結果を画面等に出力するものではないため、 このプログラムを(コンパイルして)実行しても、 以下のように見た目には何も起こらないことに注意する必要がある (何も起こらないのが正しい実行結果)。
$ gcc -o simple-operation simple-operation.c # 作成したプログラムファイルのコンパイル $ ./simple-operation $ |
C言語では、数学的な演算を行う式以外に、 変数への値の代入も代入演算子による演算式として記述する。 例えば、変数aに値として「3」を代入する演算式は、 以下のように記述することができる。
a=3;
ただし、この場合も、main()関数を用意する必要があり、 さらに、C言語では変数を使用する場合には予めその変数の宣言が必要であるため、 実際に実行可能なプログラムは以下のようになる。
/* simple-assignment.c */ int main(int argc, char **argv) { int a; a=3; } |
このプログラムも、 コンピュータ内部で変数aに値が代入されるだけであり、 何らかの結果を画面等に出力するものではないため、 このプログラムを実行しても以下のように何も起こらない。
$ ./simple-assignment $ |
C言語では、 なんらかの機能を実行するための手続きに名前をつけたものを関数と呼び、 その関数を実行するための手続きを関数呼び出し式として記述する。 関数呼び出し式を記述する場合、 関数名の後ろに括弧をつけ、 引数がある場合にはその引数を括弧の中に記述する。 もし、複数の引数がある場合には「,」で区切って記述する。
例えば、引数として与えられた文字列を標準出力(画面)に出力する手続きとして printf()関数が用意されており、 これによる文字列"Hello, world!"の出力は以下のように記述できる。
printf("Hello, world!\n");
この場合もmain()関数を用意する必要があり、 またC言語では、予め用意されている関数を使用する場合でも、 その使用方法を定義したヘッダファイル(インクルードファイル)を プログラム中で読み込む必要があるため、 上記の関数呼び出し式を実行するには、 実際には以下のようなプログラムを作成する必要がある。
/* simple-function-call.c */ #include |
このプログラムを実行するとprintf()関数が呼び出され、 以下のように、引数として指定された文字列が画面に出力される。
$ ./simple-function-call Hello, world! $ |
ただし、C言語における関数は、 数学でいうところの関数とは異なることに注意が必要である。 数学における関数とは、 何らかの入力値に対して何らかの値を出力として返す機能をもったものであり、 これによる外界への作用はない。 これに対してC言語における関数とは、 あくまで手続きに名前を付けたものであり、 入力値が必要のない関数や、入力値に対する値を返さない関数、 外界へ作用を及ぼす関数がある。 例えば、標準入力からの入力を行うscanf()関数は、 引数として与えられた値を入力値とするだけでなく、 関数の外界である標準入力を行い、 さらに、引数として与えられた変数の格納領域に基づき、 変数の値を変更する機能を実行する。
組み合わせ法とは、 複数の機能を組み合わせ、 より複雑な機能を実現する合成物を作る方法であった。 手続き型プログラミング言語では、 基本的には手続きとしての基本式を組み合わせることになるが、 C言語では、1つの式の中で演算式の組み合わせによる合成と、 また複数の式の組み合わせによる合成が可能である。
演算式の合成とは、1つの式の中で複数の演算を組み合わせることである。 例えば、「2」と「3」を足した上で、さらに「4」を足すという演算は、 2つの加算演算を組み合わせて以下のように記述することができる。
2+3+4;
このとき、各演算子には結合規則と優先順位とがあり、 これにより演算順序が決まる。 例えば、加算演算子「+」の結合規則は左から右となっており、 上記の例の場合、先ず「2+3」を実行し「5」を得、 次に「5+4」を実行し「9」を得る。
これに対し「2+3*4」と記述した場合は、 加算演算子よりも乗算演算子の方が優先順位が高いため、 先ず「3*4」を実行し「12」を得、 次に「2+12」を実行し結果として「14」を得る。 もし、「2」と「3」を足したものに4を乗じたいのであれば、 以下のように括弧をつけて演算順序を明示的に記述する必要がある。
(2+3)*4;
また多くの演算子が、 同じ優先順位の場合には左から右へ(→)と演算を行う結合規則であるのに対し、 代入演算子などの一部の演算子の結合規則は右から左へ(←)となっている。 例えば以下のように代入演算を組み合わせて記述した場合、 演算は右から左に実行されるため、 先ず、変数bに「1」が代入され、その結果として「1」を得、 次に変数aにその「1」が代入されることになる。
a = b = 1;
これは、以下のプログラムを実行することによって確認することができる。
/* multiple-assignment.c */ #include |
この実行結果は以下のようになり、 変数a、変数bともに1が代入されていることがわかる。
$ ./multiple-assignment As a result of a = b = 1, value of a is 1 and b is 1. $ |
ただし、代入演算の場合、演算を行った結果は代入された値そのものになるため、 以下のように括弧をつけて演算順序を変えることはできない。
(a = b) = 1; /* このような記述はできない */
演算式の合成における、主な演算子の結合規則と、 演算子間の優先順位を表1に示す。
演算子 | 結合規則 | 優先順位 |
---|---|---|
[] , -> , . , ++(後置) , --(後置) | → |
高 ↑ ↓ 低 |
! , ++(前置) , --(前置) | ← | |
* , / , % | → | |
+ , - | → | |
< , <= , > , >= | → | |
== , != | → | |
&& | → | |
|| | → | |
= , += , -= , *= , /= , %= , | ← | |
, | → |
手続き型プログラミング言語では、 複数の式を組み合わせ、 複雑な機能を実現する手続きを定義することができる。 手続き型プログラミング言語における手続きの合成法は、 大きく分けて逐次実行、条件分岐、 繰り返しの3種類がある。
手続き型プログラミング言語における逐次実行とは、 図1に示すように、2つ以上の手続きを順番に記述し、 これを上から逐次的に実行する方法である。 これにより、複数の手続きを組み合わせ、 複雑な機能を合成することができる。
![]() |
C言語では、逐次実行を行う手続きの合成を ブロックとして定義することができる。 ブロックは「{」で始まり、「}」で終了し、 その中に複数の式を記述することができる。 例えば、複数の文字列を標準出力に出力する場合、 以下のように、printf()関数の呼び出し式を繰り返し記述すればよい。
{ printf("Hello.\n"); printf("How are you?\n"); }
ブロックとして合成された手続きを プログラム中に記述すると以下のようになる。
/* sequential-by-block.c */ #include |
ただし、実際にはmain()関数自身が 1つのブロックを定義したものであるので、 特に他の部分と分ける必要がなければ、 以下のように記述してもよい。
/* sequential-by-block.c */ #include |
このプログラムを実行すると以下のようになり、 プログラム内で記述された順番に手続きが実行されていることが確認できる。
$ ./sequential-by-block Hello. How are you? $ |
上記の例では、手続きとしての個々の式が独立したものであったが、 変数を利用することにより、個々の式を関連付けて、 複雑な続きを合成することができる。 例えば、変数を用意し、これに演算式により求まった値を格納し、出力する場合、 以下のように記述することができる。
{ int a; a = 2 + 3; printf("Answer is %d.\n", a); }
ただし、変数を利用するために、その変数を用意するための変数宣言式は、 ブロックの先頭に記述しなければならないことに注意する必要がある。 実際のプログラムは以下のようになる。
/* variable-use.c */ #include |
この実行結果は以下のようになり、 演算式に実行結果として得られた値を出力できていることが確認できる。
$ ./variable-use Answer is 5. $ |
一般に、新たに作成された合成物の中で使用する変数を その合成物の中だけで有効とすることを変数の隠蔽と呼ぶ。 隠蔽された変数は、その合成物の中でしか操作することができない。 逆に、隠蔽された変数に対する操作は合成物の中に閉じており、 その合成物の外に同じ名前の変数があっても影響を与えない。 このように変数を隠蔽することにより、 一次的に利用する変数などを合成物の中で自由に用意することが可能となり、 便利である。
C言語におけるブロックも手続きの合成物であり、 ブロックの中で新たに変数を宣言することにより、 その中に隠蔽された変数を用意することができる。 例えば次のプログラムでは、 ブロックの中に別のブロックを定義し、 外側のブロックと内側のブロックとで同じ名前の変数を用意し、 それぞれ使用している。
{ /* 外側のブロック */ int a = 1; /* 変数を用意し、値を代入 */ { /* 内側のブロック */ int a = 2; /* 変数を用意し、値を代入 */ printf("Value of a is %d\n", a); /* 変数の値を参照 */ } printf("Value of a is %d\n", a); /* 変数の値を参照 */ }
実際のプログラムファイルの内容は、以下のようになる。
/* variable-hiding.c */ #include |
この例では、内側のブロックで、 外側のブロックで使用されている変数と同じ名前の変数に値を代入している。 しかしながら、内側のブロックで使用される変数は、 内側のブロックの中で新たに用意した変数であるため、 内側のブロック内に隠蔽されており、 外側のブロックで使用されている変数には影響を与えない。
このプログラムを実行すると以下のようになり、 変数が隠蔽されていることを確認できる。
$ ./variable-hiding Value of a is 2 Value of a is 1 $ |
条件分岐とは、図2に示すように、 条件式と各条件に応じた手続きを記述し、 プログラムの実行時には、 条件式の実行結果に応じて複数の手続きの中から1つの手続きを選択し、 実行するものである。 これにより複数の手続きの選択的実行を行う手続きを合成することができる。
![]() |
C言語では、条件分岐による手続きの選択的実行のために、 if文やswitch〜case文を利用することができる。 例えばif文では、与えられた条件式の真偽に基づき、 手続き(式またはブロック)を選択し、実行することができ、 これを利用して以下のようなプログラムを記述することができる。
if( x > 0 ) y = x; else y = -x;
この例では、変数xの値が0より大きいか否かに応じて、 大きければ変数xの値をそのまま変数yに代入し、 そうでなければ変数xの符号を逆にした値を代入する手続きを 合成している。
ただし、この例では、条件分岐により実行される手続きを式により与えているが、 この記述ではどこからどこまでが条件分岐による実行であるかわかりづらいので、 式の代わりに、以下のようにブロックを使用して記述するとよいだろう。
if( x > 0 ) { y = x; } else { y = -x; }
ここで注意しなければならないのは、 C言語において新たな合成物を定義できるのはブロックのみであり、 条件分岐を実現する if文やswitch〜case文では、 手続きを合成することはできるが、 これにより新たな合成物を定義することはできないことである。 すなわち、if文などにより合成された手続きは、 基本式と同様の扱いとなる。 従って、if文はブロック内に挿入される形で、 例えば、以下のようなプログラムを記述することができる。
/* conditional-branch.c */ #include |
この実行結果は以下のようになり、 入力された値に応じて異なる手続きが実行されることがわかる。
$ ./conditional-branch Inpur a number: 10 Absolute of 10 is 10. $ ./conditional-branch Inpur a number: -7 Absolute of -7 is 7. $ |
繰り返しとは、条件式と繰り返し対象となる手続きを記述し、 条件が成立するまで手続きを繰り返し実行するものである。 これにより、複数回、同様の手続きを実行する手続きを合成することができる。 また繰り返しには、図3に示すように、 (a)繰り返し対象となる手続きの前に条件式を実行し、 場合によっては1回も手続きを実行しない前判定繰り返しと、 (b)繰り返し対象となる手続きを実行した後に条件式を実行し、 少なくとも1回は手続きを実行する後判定繰り返しの2種類がある (ただし、実際の手続き型プログラミング言語の多くは、 プログラムの書き方により繰り返し手続きの途中で条件式を実行し、 繰り返しを中断する手続きを定義することもできる)。
|
|
C言語では、前判定繰り返しによる手続きを合成するために for文やwhile文、 後判定繰り返しによる手続きを合成するために do〜while文を利用することができる。 例えば、while文を利用して、 以下のようなカウントダウンを行うプログラムを記述することができる。
while( i != 0 ) { printf("%d, ", i); fflush(stdout); sleep(1); i--; }
ただしC言語の場合、 for文やwhile文などによる手続きの合成も if文などによる条件分岐と同様、 新たな合成物を定義するわけではなく、 基本式と同様の扱いとなることに注意する必要がある。 従って実際のプログラムは、ブロック内に挿入する形で、 例えば以下のように記述することができる。
/* loop.c */ #include |
このプログラムの実行結果は以下のようになり、 数を減算する手続きが繰り返されることが確認できる。
$ ./loop Input a number: 3 3, 2, 1, Go! $ |
抽象化とは、新しく作られた合成物に名前をつけ、 新しく1つの機能として利用できるようにするものであった。 手続き型プログラミング言語では、 新しく合成された手続きに対し名前を付け、 新たな手続きとして利用することができる。
C言語では、合成された手続きに名前をつけ、 新たな手続きとして利用できるようにするために、 合成された手続きであるブロックに対し名前を付け、 新たな関数を定義する機能を持つ。
例えば、以下のように変数aと変数bの値を足し、 その結果を変数cに代入する手続きがあるとする。
{ c = a + b; }
これにadd()という名前をつけ、新たな関数として定義する場合、 以下のように記述できる。
add(){ c = a + b; }
これにより、関数add()を自由に使用することができるようになる。
しかしながら、この関数の定義では、 足し合わせる2つの数値が、 変数aおよび変数bに代入されていなければならず、 またその演算結果も変数cに格納されていることを 知っていなければならず、不便である。 このためにC言語では、 関数を呼び出す際に与える複数のパラメータを引数として定義し、 またその関数を実行した結果として得られる1つの値を 戻り値として定義することができる。
引数を定義するには、以下のように、 関数定義の括弧の中に定義したい引数を指定する。
add(int a, int b){ c = a + b; }
引数で指定された変数は、 ブロックの内部で用意された変数と同様にブロック内に隠蔽され、 これにより、 この手続きの中でどのような名前の変数が使われているかを知らなくとも、 引数として2つの値を与えればよいことさえ知っていればよくなる。
また戻り値を指定する場合、以下のように、 関数名の前に戻り値の型を指定し、 また、手続きの実行を終了する部分に return文により戻り値を指定する。
int add(int a, int b){ c = a + b; return c; }
ただし、この場合、演算結果は関数の戻り値として得られるため、 変数cはこの手続きの内部でのみ参照できればよいので、 この変数もブロックの中で用意することでブロック内に隠蔽することで、 外部に余計な影響を与える心配がなくなるため、 実際には以下のように記述するとよい。
これにより、この手続きによって得られる結果が どのような名前の変数に格納されるかを知らなくとも、 関数を呼び出した結果得られる値を参照すればよいことだけを 知っていればよくなる。
以上のように定義された関数を利用するには、以下のように、 その関数の名前と引数を与え、実行結果を参照すればよい。
result = add(1, 2);
以上の関数定義およびその呼び出し方法に基づき、 実際のプログラムは以下のように記述することができる。
/* add.c */ #include <stdio.h> int add(int a, int b) { int c; c = a + b; return c; } int main(int argc, char **argv) { int i, j; int result; printf("Input first number: "); scanf("%d", &i); printf("Input second number: "); scanf("%d", &j); result = add(i, j); printf("Adding %d to %d makes %d.\n", i, j, result); } |
このプログラムの実行結果は以下のようになり、 新しく定義した関数を自由に利用できることがわかる。
$ ./add Input first number: 1 Input second number: 2 Adding 1 to 2 makes 3. $ |
関数の定義の中で、その関数自身を利用することを関数の再帰呼び出しと呼ぶ。
例えば、階乗n !は一般に以下のように定義されるが、
n ! = | { | n * (n -1) * (n -2) * ... * 1 | (n > 1) |
1 | (n = 1) | ||
これは、以下のように定義することもできる。
n ! = | { | n * (n -1) ! | (n > 1) |
1 | (n = 1) | ||
この定義に基づき、 n の階乗を求めるfact()関数をC言語で定義する場合、 関数の再帰呼び出しを利用して以下のように記述することができる。
int fact(int n) { if ( n > 1 ) return n * fact(n - 1); else return 1; } |
以下のプログラムをC言語により作成し、実行せよ。 レポートには、作成したプログラムとその実行結果を示すこと。
2つの整数3、5の平均を求め、標準出力に出力せよ。
2つの整数x 、y を引数にとり、 その平均を求めるaverrage()関数を定義し、 これを利用して、標準入力から入力された2つの値の平均を求め、 標準出力に出力せよ。 結果には、3と5を入力した結果を示せ。
整数x を引数にとり、 その絶対値を求めるabsolutes()関数を定義し、 これを利用して、標準入力から入力した値の絶対値を求め、 標準出力に出力せよ。 結果には、-5を入力した結果を示せ。
2つの整数x 、y を引数にとり、 その大きいほうの数値を返すlarger()関数を定義し、 これを利用して標準入力から入力された2つの値のうち、 大きいほうの値を標準出力に出力せよ。 結果には、3と5を入力した結果を示せ。
上記のabsolutes()およびlarger()を定義するとともに、 これらを使用して、 2つの整数x 、y を引数にとり、 絶対値が大きいほうのもとの数値を返す abs_larger()関数を定義し、 これを利用して標準入力から入力された2つの値のうち、 絶対値が大きいほうの値を標準出力に出力せよ。 結果には、3と-5を入力した結果を示せ。
標準入力から入力された値が3の倍数であるか否かを判別し、 3の倍数である場合には3の倍数であることを示すメッセージを出力し、 3の倍数ではない場合には、 3の倍数ではないことを示すメッセージを出力するとともに 3で割った余りを出力せよ。 実行結果には、126および511を入力した結果を示せ。
1からn までの総和を求める関数は、 以下のように帰納的に定義することができる。
sum(n ) = |
|
= | { |
|
= n + sum(n -1) | (n > 1) | 1 | (n = 1) | |||||||||||||
この定義に基づき、 1からn までの総和を求める関数sum(n)を定義し、 1から10までの総和を求めよ。
以下の実行結果のように、標準入力から文字列"yes"を入力するまで、 質問を繰り返すプログラムを do〜while文を使用して作成せよ。
$ ./spoiled-child Will you give me a candy? no Will you give me a candy? no Will you give me a candy? no Will you give me a candy? no Will you give me a candy? yes Thank you! $
ただし、標準入力から文字列の入力を行う場合、 例えば、char型の配列変数sを用意し、 scanf()関数を使用して、 以下のように記述できる。
char s[255]; ... scanf("%s", s); ...
また、2つの文字列が等しいかどうかを比較する場合、 比較演算子「=」ではなく、strcmp()関数を使用する。 strcmp()関数の使用法に関しては、 オンラインマニュアルを参照のこと。
(1)のプログラムを改良し、標準入力から文字列"yes"または"Yes"を入力するか、 もしくは、5回、別の文字列が入力されるまで、 質問を繰り返すプログラムを作成せよ。
日曜日から土曜日までを0から6と定義し、 y 年m 月d 日において、 1月と2月を前の年の13月、14月とすると、 Zeller(ツェラー)の公式により、 以下の式で得られる値を7で割った余りにより、 各曜日に対応した値を得ることができる。
y + | [ | y | ] | - | [ | y | ] | + | [ | y | ] | + | [ | 13 * m + 8 | ] | + d |
4 | 100 | 400 | 5 |
ただし、[x ] はx を超えない最大の整数を意味する。
これに基づき、標準入力から入力された年、月、日から 曜日名を出力するプログラムを作成せよ。 ただし、標準入力からの入力時には、 1月や2月もそのまま入力できることとする。 また、[x ] の求め方に関しては、 各演算を整数で行えば小数点以下を切り捨てるため、特に気にしなくともよい。 出力結果には、2004年5月31の結果を示せ。