プログラミング序論 page10(update:2017/08/21)
[ index | prev | next ]  まとめ Cの関数

10. 関数

ここまではプログラムを作るために原理的に必要な データ型,変数,演算子,制御構造 の話でした。
今回からは大規模で複雑な実用的なプログラムを作るための工夫の話が始まります。

(教科書 4章 関数)
制御構造で順次実行、条件分岐、繰り返しについて学んだ。これで原理的にはどんなプログラムでも書ける。  しかし、 大規模で複雑なプログラムが求められる様になると、プログラムの行数が増え全体を一度に把握できないようになる。

そこで、目的のプログラムを人間が一度に把握できる小さなプログラムに分割することにした。この小さなプログラムを順番に呼び出す(call)プログラムを作ることで 大規模なプログラムを作れるようにする。  呼ばれる側の小さなプログラムをサブルーチン(subroutine)、順番にサブルーチンを呼び出す側をメインルーチン(main routine)と言う。

C言語ではサブルーチンを規格化した関数(function)と呼ばれる仕組みをプログラムの単位としている。
従って,

C言語のプログラムは関数の集まりです。

 C言語の関数は、その中からさらに別の関数を呼ぶことができる。関数から関数を呼び出す階層の数に制限はない。関数内から自分の関数を呼び出す再起呼び出しさえ可能だ。 これらが、どのように実現されているかに注意して以下の説明を読んでほしい。

10-1. サブルーチン

プログラムを書いていると同じような記述が繰り返し出てくることが多い。下の例の0〜100の範囲の整数を2個入力する為の記述はほとんど同じdo-whileのプログラムを2回書いている。この2つのプログラムは1個にまとめられないだろうか?

#include<stdio.h>
int main(void)
{ int a,b; do{ printf("0..100の範囲の整数を入れてください>>"); fflush(stdout) scanf("%d", &a); }while( a < 0 || 100 < a); do{ printf("0..100の範囲の整数を入れてください>>"); fflush(stdout); scanf("%d", &b ); }while( b < 0 || 100 < b); printf("a+b=%d", a + b); return 0; }

Cのプログラムではこんなときに関数を作り以下のようにする。

#include<stdio.h>

/* 0-100の範囲の入力を読む。それ以外の入力に対しては再入力を行う関数 */ int read(void)
{
int data = -1;
do{
printf("0..100の範囲の整数を入れてください>>"); fflush(stdout);
scanf("%d", &data);
}while( data < 0 || 100 < data);
return data; /*dataの値を戻り値として帰る*/
}


int main(void)
{
int a,b;
a = read(); /*処理が関数readに跳び戻り値をもって帰ってくる*/
b = read(); /*処理が関数readに跳び戻り値をもって帰ってくる*/
printf("a+b=%d", a + b);
return 0;
}

main関数の中から関数readに処理が跳んでreadプログラムが実行されreturnで戻る。

関数はサブルーチン(subroution)の一種


サブルーチンは色々な場所から呼んで実行でき、実行終了で呼んだ場所に帰るプログラムである。

 

 

10-1-1. サブルーチンの仕組み

サブルーチンは色々な場所から呼ばれるので、戻り先は一つに決まらない。サブルーチンのプログラムでは呼び出した場所に戻る仕組みが必要になる。 

そこで、戻る場所(アドレス)をメモリに記録してからサブルーチンに跳び、戻る場所をメモリから読み出して戻る仕掛けになっている。

※現代の計算機の機械語には,このような処理を行う専用のサブルーチンの呼び出し命令callと戻る命令returnが用意されている。(プログラミング言語の進化に応じて機械語での対応も行われます)

 

サブルーチンを実行する手順

  1. サブルーチンの呼び出し(call)
      戻る命令の場所を記録し、 サブルーチンの先頭に跳ぶ
  2. サブルーチンプログラムの実行
  3. サブルーチンからの戻り(return)
      戻る命令の場所を読み出し、 その場所に跳ぶ

 

 

1) サブルーチンを呼ぶときは、次に実行するはずだった命令のアドレスをメモリーに書き込んで退避し、次に実行する命令の場所を飛び先のサブルーチンの番地に変更します。

※プログラムカウンター:次に実行する機械語のアドレス(場所)を示すための記憶装置。
機械語を実行するたびに値がカウントアップする。アドレス順に機械語を並べることで順次実行が行える。

2) サブルーチンから戻るときは、次に実行する命令の場所を退避しておいた戻り先番地に変更します。

※サブルーチンを容易にするため機械語には、「call:コール」と「return:リターン」の命令が用意されています。「call:コール」は現在のプログラムカウンタの値をメモリに記憶してから指定されたアドレスをプログラムカウンタに書き込みます。「return:リターン」はcallで記憶したアドレスを読みだして、プログラムカウンタに書き込みます。

サブルーチンから別のサブルーチンを呼ぶためには複数の戻り先番地を記憶する必要がある。この為に後述したスタックと呼ばれる仕組みが使われます

10-2. C言語の関数呼び出し

C言語では関数という形に規格化されたサブルーチンがプログムの単位となる。

関数は呼び出したところに戻って来るという最低限の機能の他に呼ぶ側と呼ばれる側の間でデータ交換する為に引数(argument/parameter)戻り値(return value)の仕組みを持 つ。

10-2-1.引数

呼び出す関数に,呼ぶ側のデータを渡す仕組み。

実引数と仮引数

関数のカッコ(  )の内側に書かれた記述を引数と呼ぶが,役割の違いで実引数と仮引数の2つに区別される。

#include<stdio.h>
/*関数maxの定義*/
int max(int a, int b)/*ここでa,bが仮引数、値は呼び出し側で代入済*/
{
if( a < b ) return b;
else return a;
}

int main(void)
{
int a, b;
printf("7と9で大きい方の値は%d", max(7, 9) );/*7と9が実引数*/
printf("7+2と9で大きい方の値は%d", max(7 + 2, 9) );/*7+2と9が実引数*/ fflush(stdout);
//
scanf("%d%d", &a, &b);
printf("%d,%dで大きい方は%d", a, b, max(a, b) );/*aとbが実引数*/
return 0;
}

実引数argument 関数の呼び出し記述において( )内にカンマで区切って並べられたリテラル、変数、数式、関数呼び出しなど仮引数に代入する値を与えるもの。
仮引数に代入可能な値となるものは全てが使える。

仮引数parameter 関数の定義の頭部で( )内に変数として定義されたもの
仮引数 は呼び出し側で実引数の値を代入済の変数。
関数が仮引数の値を変更しても呼び出した側が影響を受けることはない。実引数に仮引数と同じ名前の変数 を使っても、このことは成り立つ。

※日本語表記では引数とまとめられているが、英語表記ではargument(実引数)、parameter(仮引数)のように区別して呼ぶことがある。

関数呼び出しでの引数の処理

 1)仮引数の生成(呼び出し側で行う)
関数を呼びだすメインルーチンでは、呼び出し側に記述された実引数の値を求め、新たに仮引数のメモリ領域を確保して値を代入する。

2)仮引数の利用(関数側で行う)
仮引数を値が代入済みの変数として使い,関数側の処理を実行する。

 この様に値を代入した変数を新しく作って渡すやり方を値による呼び出しとか値渡し(call by value)と言う。

実際の機械語プログラムはどうなっているのか

関数を呼ぶ側は仮引数の名前を知らなくていい

仮引数の名前は呼ばれる関数の中でのみ有効な名前である。次のプログラムのように実引数と仮変数が同じ名前でもアドレスが異なる別物であることを確認できる。

#include<stdio.h>
int max(int a, int b)
{
    printf("max(): &a=%08x &b=%08x\n", &a, &b);

    if( a < b)return b;
    else return a;
}
int main(void)
{
    int a = 10;
    int b = 20;
    printf("main(); &a=%08x &b=%08x\n", &a, &b); 
    printf("max(%d,%d)=%d", a, b, max(a, b));
    return 0;
}
/*実行結果 実引数と仮引数 名前は同じでもアドレスが異なる
main(); &a=0042f8b0 &b=0042f8ac
max(): &a=0042f8a4 &b=0042f8a8
max(10,20)=20
*/

仮引数を作るために、関数を呼ぶ側で必要なのは仮引数のデータ型と並び順

10-2-2 戻り値 return value

関数を呼んだ側にデータを渡す仕組み。

C言語では 関数から戻るときに一個以下のデータを戻す。 戻す値の型は関数の頭部で関数の型として宣言され、戻す値は関数の中でreturnの後に書かれた式の値となる。後述の関数の定義のところで説明します。

※戻り値の記憶場所は戻り値の型に応じて決めている。多くの場合、記憶場所として主メモリではなくてレジスタが使われる。

基本的に関数を呼ぶことによって関数を呼んだ側が受ける影響は戻り値のみ

※ポインターを使って、この制約を迂回する方法はポインターのところで説明します。

10-2-3. スタック(stack)

C言語では、関数を呼ぶたびに戻り先記憶用と仮引数用のメモリーを必要とする。空いているメモリーからうまくこのような場所を割り当てる仕組みが必要だ。ここで、スタックと呼ばれる仕組みが使われている。さらに、関数内の変数の割り当ても同じスタックが使われる。

スタック(stack)とは干し草・書類などの山を意味する単語である。貯めるときは上に上にと積み上げ、使うときは上から順番に取り崩してゆく。

メモリー割り当てでのスタックは、始めに連続した番地の空きメモリーの山を用意しておく。メモリーが必要になったときは、空きメモリーの山の上から、使用中のメモリーの山へメモリーを積んでゆく。以下に例題のプログラムとメモリー使用状況の模式図を示す。

int func2(char a,char b)
{
    int n;
    n = a - b;
    return n;
}
int main(void)
{
    char a = 'Z';
    int d;
    d = func2(a, 'A');
    return d;
}

1. 実行はmain関数から始まる

始めに変数a,dのメモリーを用意する

int main(void)
{
    char a = 'Z';
    int d;
    d = func2(a, 'A');
    return d;
}

 
変数aは...00B番地のメモリー。文字「Z」のコード値で初期化する。
変数dは...00C番地から4バイトのメモリー。場所と変数名dを対応付けしただけで、メモリーの中身はもとのまま。
このため変数dの値は定まっていない。これを未定とか不定と言う。

2. 関数を呼び出し

呼び出しでは仮引数a,bと戻り先番地を格納するメモリーを用意する

int main(void)
{
    char a='Z';
    int d;
    d=func2(a,'A');
    return d;
}


実引数の値を求め仮引数用のメモリーに書き込む。
メモリーに戻り先のアドレスを書き込んで関数func2のプログラムに跳ぶ。

3. 関数func2の実行

func2ではint型の変数nを使うので、新たに4バイトのメモリーを割り当てる。

int func2(char a,char b)
{
    int n;
    n = a - b;
    return n;
}


a-bの計算でASCIIコードの場合’Z'-'A'は25となる。この値が変数nに書き込まれる。
return n;でこのnの値を戻り値とし、
戻り先アドレスを読みだして、呼び出し側のプログラムに戻る。

※関数の呼び出しや変数の定義でスタックのメモリーが使用される。もし,スタックに用意したメモリー以上にメモリーを使うと,スタック・オーバーフローを起こして実行時エラーとなる。特に,再帰呼び出しの場合には注意が必要。

4 .関数func2から戻ると

戻ってくると戻り値をdに代入するが、関数を呼ぶために用意したメモリーは用済みなので空きメモリーの山に戻す。

int main(void)
{
    char a = 'Z';
    int d;
    d = func2(a, 'A');
    return d;
}

スタックポインター

使用中メモリーとか空メモリーとかの山を実際に作ることはできない。しかし上のスタックの模式図で空きメモリを使用中のメモリーの山にひっくり返して重ねると次の図のようになり、メモリーの移動は要するに使用中領域と空領域の境目の移動である。この境目のメモリー番地を記憶するためにスタックポインターと呼ばれる記憶装置が用意されている。

関数は呼ばれたときのスタックポインターの位置を基準にして仮引数や関数内の変数の番地を基準からの相対的番地として利用する。

[メモ] 
なぜスタック領域の高位番地から低位番地に向かってデータを積んでゆくのでしょう。理由を考えつきますか?
整数型データを記憶するには4バイト使います。int型変数nのアドレス&nとして示されるのは4つのメモリーアドレスの内のどの位置でしょうか?

[メモ] 主メモリーとレジスタ

パソコンの例ですが...データを記憶する場所は主メモリー の他にCPU内部にレジスタと呼ばれる少数だが高速に読み書きが可能なメモリーがある。例えばバス133MHz、主メモリ256M、CPUがPIII1G(内部クロックはバスの8倍)、キャッシュがL1= 32K,L2=256Kでは単純な演算でレジスター演算1ns、キャッシュを使うと数ns、主メモリーでは十数nsと遅くなる。

C言語では関数の戻り値は1個以下に決められているので戻り値の記憶には 高速なレジスタを使うのが普通。このほかの一時的な変数もレジスタに空があればレジスタを用いるほうが高速に演算できる。

 

10-3 関数プロトタイプ(function prototype)宣言

関数が使う仮引数の名前や変数の名前と、その処理の手順などは関数を呼ぶ側は知る必要が無い。

例えば、printf 関数がどんな名前の変数を使っているかや、具体的な処理手順などは気にせず呼び出してきたし、それで不都合は起きなかったはずです。

関数を呼ぶ手順から分かるように関数を呼ぶ側で必要な情報は下記の3つです。  

●関数名 function name

C言語では関数は関数名のみで区別される。従って、関数名は一意識別できる名前でなくてはいけない。関数名に対応した機械語プログラムが書かれたアドレスが関数呼び出しの跳び先アドレスとなる。

※printfのような標準関数のプログラムをライブラリーから取り出してプログラムに付け足し、その場所をprintfを呼び出しているcall命令のとび先アドレスに書き込んだりするのがリンク作業です。

●仮引数並び parameters

仮引数の型と引数名を「,」で区切って並べたもの。引数が無い場合はvoidと記述。仮引数を作るとき、引数並びと同じ順番にメモリーを割り当てる決まりなので、仮引数を作る側では型と順番のみが必要

※呼び出し側は仮引数の名前は必要としない。この為、関数プロトタイプでは名前の省略が許される。

●戻り値のデータ型 returen type

関数が戻すデータを記憶する場所は戻り値の型に応じて決められている。 従って、戻り値のデータの型が値の受取には必要になる。

戻り値は1個以下であり、関数が値を戻さない場合はvoidと型を指定する。

※戻り値を呼び出し側が無視し、使わない場合も有る

これらの情報は関数の定義から得られるが、関数のプロトタイプ宣言で与えることも可能。(定義については後述)

プロトタイプ宣言(function prototype declaration)

関数プロトタイプは関数を呼びだすために必要な情報を記述するものです。書式は

戻り値のデータ型 関数名(引数並び);

※プロトタイプ宣言では引数並びの引数名は省略可能。

int add(int,int);
/*引数の変数名はプロトタイプでは省略できる*/
/*しかし、意味の分かる名前を書くのが親切*/
double complex_abs(double re,double im); /*プロトタイプ宣言の仮引数名は,本当の仮引数名である必要はない*/

コンパイラは関数を呼び出す記述より前に関数のプロトタイプ宣言あるいは関数定義があることを要求する。これはコンパイルの手間を 省く為である。この結果2個の関数が相互に呼び出す場合はプロトタイプ宣言が必須になる。

int B(int);/*この関数Bのプロトタイプ宣言がないとコンパイルできない*/
int A(int n){
if(n==0)return 1;
else return n*B(n-1);/*プロトタイプ宣言から関数Bの呼び出しに必要な情報を得る*/
}
int B(int n){ /*こちらは関数Bの定義*/
if(n==0)return 1;
else return n+A(n-1);/*関数Aの定義から呼び出しに必要な情報が得られる*/
}


main関数については、通常は呼び出しを書くことはありませんしプロトタイプ宣言を書くこともありません。

10-4 関数定義 function definition

定義は宣言の一種です

 

宣言:(declaration)

変数名や関数名のような識別子を示し、その特性を記述したものを宣言という。
プロトタイプ宣言はコンパイラ が関数の呼び出し部分の翻訳に必要な情報を事前に示すものです。宣言されたからと言って使う場面が出てこなければ、この情報は使われません。

変数についても同様の記述があります。次のような外部変数の宣言です。

extern int a;/* aは整数型の変数で、このプログラムファイルの外かもしれないが、どこかにある。*/

コンパイラはこの記述があると変数aに関係する処理は、変数aの使うメモリー番地を後からリンク処理等で設定可能な機械語に翻訳します。変数aのメモリー割り当ては行いません。変数aが使われていなければ、この記述は何の効果も有りません。コンパイルで警告が出ることもない。

定義:(definition)

変数名や関数名のような識別子に対応した機械語コードやデータ領域を作る宣言を定義と言います
関数定義は関数の具体的な処理内容を示し、機械語プログラムへの翻訳を要求します。定義であれば、機械語に翻訳されて必ず機械語コードの一部と成ります。

変数についても

int b;/*整数型の変数bをメモリーに割り当てよ。*/

のように、これまで使ってきた記述は変数用にメモリー割り当てを要求する定義です。使わない変数を定義した場合にはコンパイルで警告が出ます。

※定義は宣言の一種として分類されるので、上記の変数bの記述は単に宣言と呼ぶことの方が多い。

10-4-1関数定義の書式

戻り値のデータ型 関数名(仮引数並び)
{
   /* function body */
   変数の宣言・定義をまとめて最初に書く;
   関数の処理を命令文で記述
   return 戻り値/*戻り値がない場合はreturn;のみでよい。*/

関数の本体部分(function body)  と で括ってブロックにまとめて記述する。先頭で変数の定義などの宣言が行える。続いて処理プロ グラムを記述する。 関数から戻る出口にはreturn文を置く。

変数の定義 

ブロック内で定義された変数はブロックに処理が移動したときにメモリーに割り当てられ、ブロックから出るとき割り当てが解除される。

関数の場合も関数の本体部分のブロックで定義された変数は関数が呼ばれたときにメモリーが割り当てられ、関数から戻るとき割り当てが解除される。このため、関数内に定義した変数は関数の実行中でだけ有効な変数となる。 (次週説明するstatic変数はこの限りではない)

return文

returnは関数を呼んだ側に戻る指示文。戻す値をreturn直後の式で与える。この式の値は戻り値のデータ型と一致すことが望ましいが、戻り値のデータ型に自動変換できるデータ型でもよい。

 returnは複数書いてもいい。

int max(int a,int b){
  if(a<b)return b;
  else return a;
} 

returnに続けて書かれた式の値が関数の戻り値となる。以下のような例は何れも可。

return 1;/*値1を戻す*/
return a;/*変数aの値を戻す*/
return a+b;/*変数a,bの加算式の値を戻す*/
return sqrt(a);/*関数sqrtの戻り値を戻す*/

... プログラム例 

#include<math.h>
/*ルートの計算でsqrtを使うのでmath.hが必要*/
double complex_abs(double re,double im)
{
    double t;
    t=re*re+im*im;
    return sqrt(t);/*関数sqrtの戻り値を戻す*/
}

... 実行可能なプログラム例  (プログラムとして実行可能にするにはmain関数が必要)

#include<stdio.h>

void printMark(int n)/*データは仮引数nに代入して渡される*/
{
while(0<n){
printf("*");
n--;/*ここで仮引き数nを変更しているが、main関数の変数i(実引数)は変化しない*/
}
printf("\n");
}
int main(void)/*mainも関数であり、プログラムの実行はmain関数の呼び出しである。*/
{
int i,max=10;
for(i=1;i<=max;i++)
printMark(i);/*実引数iの値を仮引数nに代入し、printMark関数を呼び出す*/
return 0; }
/*
実行結果
*
**
***
****
*****
******
*******
********
*********
**********

*/


10-5. 再帰呼び出し

C言語では関数を呼ぶことが自由にできる。例えば下記の例の様に関数factorialの中からfactorialを呼び出す再帰呼び出しも可能。

プログラムを実行する時に、関数から別の関数を呼び出して行くと, 場合によっては最初の関数が処理の途中であるにも係らず再度呼び出されることも起り得る。下は単純化された例だが、C言語においてはこの様な再帰を行って もプログラムは正常に実行される。これは関数を呼び出す度にスタックに変数を積み上げて行く仕組みにより可能となっている。同じ関数が呼び出される場合でも呼び出しのたびに新しい仮引数や変数が新たな場所のメモリーを使って用意される。


図の説明:

#include<stdio.h>

int factorial(int n)
{
    printf("n=%d &n=%x\n", n, &n);
    if(n == 0)return 1;
    else return n * factorial(n - 1);
}

int main(void)
{
    factorial(3);
    return 0;
}

/*実行結果
 仮引数nの番地を見ると
 同じnと言う名前でも呼び出される度に違うメモリーに割り当てられている
n=3 &n=13fee0
n=2 &n=13fed4
n=1 &n=13fec8
n=0 &n=13febc
 ※仮引数nのアドレス&nの値が12ずつ減っています。
  スタックが高位アドレスから低位アドレスへとデータを積んでいるのが解ります
*/

10-6. Cプログラムの全体構造

Cのプログラムは関数の集合体で、一番上のメインルーチンはmain関数として書く決まりになっている。つまり、プログラムは初期化後,main関数の頭から実行を行い,main関数のreturnで戻ったら,後始末の後でプログラムは実行完了する。

※プログラムで実行される関数はmain関数だ。

10-6-1. Cプログラムの実行手順

  1. OSがプログラムをメモリー上に展開する
  2. 初期化を行う。
  3. main関数を呼び出す
  4. main関数から戻ったら必要な後始末を行う
  5. main関数の戻り値をOSに渡して終了

プログラムの実行は上記の様にOSからのmain関数の呼び出しである。他の関数はmain関数からの呼び出しで実行されることになる。

10-6-2. main関数

main関数の形:
main関数はANSIでは次の様に決められている
 int main(void)あるいは
 int main(int argc、char *argv[ ] )/*この形はポインターの所で説明*/

C99からは処理系定義のmain関数も使える。
この場合は引数、戻り値とも何でもいい。引数も戻り値も取らない処理系であれば 
 void main(void)
を処理系で定義したmain関数としてもいい。しかし、処理系依存になるため他の処理系でコンパイル・エラーとなることも覚悟しなければならない

※以下の書き方は許容されることが多い
 main( )
しかし、これは戻り値がint型で引数の情報が無い(可変長引数で引数の数や型は不明の)main関数となる。

※C言語では、戻り値のデータ型が省略された場合はint型、

※C言語では、仮引数並びが省略されると引数の情報が無い関数。
このときコンパイラは
可変長引数の関数と見なして翻訳する。機械語プログラムは実引数の値を全てスタックに積んでから関数を呼びだす形になる。ここでコンパイラは引数の数や型のチェックは行わない。さらに、呼び出された関数の側で、スタックに積まれた値を使っていなくても、コンパイラは何も言わない。

 ※可変長引数
printfやscanf関数では実引数の数が可変になっていた。ここで、使われている引数の渡し方を可変長引数という。実引数の値は既定実引数拡でchar、short、int型ならint型に、float、double型ならdouble型に型変換してスタックに積まれる。

引数を受け取る側では、仮引数の数や型が不明で、引数の情報が無いため、通常は可変長引数型の前に別の引数を置いて、これで可変長引数部分の型や数の情報を受け渡す。printfやscanfの最初の引数が書式を示す文字列だったことを思い出してほしい。
printf関数の宣言 int printf(const char* format, ... )
で、ドット3個の「... 」が可変個引数の表記となる。この表記は引数並びの最後にしか使えない。

序論の範囲では可変長引数の関数を使うことはあっても、作ることは要求しません。作るには多くの知識が必要です。

※CとC++の違い
C++言語では引数が無い場合は通常 f(  ) のように記述する。これはC言語の f(void) に対応する。
C++言語では可変個引数のみの関数は f( ... ) のように記述する。
注意:C言語のC99規格では f( )のような引数並びの省略は非推奨

10-6-3 分割コンパイル

C言語は構造化言語といわれています。大規模なプログラムを作成する為には、プログラムを一つの塊ではなくて複数の部品の集合体として記述し、プログラムの構造が見て取れるようなものにすることが不可欠です。C言語では前章で説明した、 if、while、forなどの制御構文を使うことでプログラムの実行順番 の把握が容易になります。しかし、これだけでは大規模なプログラムはまだ作れません。

プログラムが大規模化すると、プログラムの全てを一度に把握することは不可能になります。この様な場合の対応手段は一度に把握が必要な範囲を小さくすることです。そこで、要求される仕事を幾つかのサブプログラムに分け、個々のサブプログラムがきちんと役割を果たせば、それを組み合わせた全体が目的の動作をするようにします。C 言語は 関数によってこの様なサブプログラムの作成を容易に行える代表的な構造化言語です。

サブプログラムの作成では他のサブプログラムやその組み合わせ方などは考える必要が無いようにして、要求された仕事だけ考えれば済むようにしなければいけません。これが成功すれば、個々のサブプログラムを他のプログラマに任せることも可能になります。C言語では、各プログラマに割り当てたサブプログラムを個別にコンパイルしておき、後で一つのプログラムにリンクしてまとめることが可能です。このようなプログラムの作成手順を分割コンパイルと呼びます。

※何れソフトウエア工学で構造化分析・設計について話を聞く機会があるかもしれません。C言語は構造化プログラミングを実践し易いように作られているのです。

10-7 課題

課題9.(初期ファイルp9.c
 2つのint型引数を受け取って、最大公約数を戻す関数gcdを作る

初期ファイルを元にして最大公約数を求める関数gcdの定義を書きなさい。

※最大公約数を求めるアルゴリズム(手順)としては下記のユークリッドの互除法が有名。
必要なら第4の関数を作ってもいいし、再起呼び出しを使ってもいい。
しかし、そんなことはしなくても繰り返しの構文で書ける。

#include<stdio.h>
  
int gcd(int,int); /*最大公約数を求める関数のプロトタイプ宣言 prototype declaration*/

void printox(int a,int b) /*○×表示用関数*/
{
    if(a == b)
    {
        printf("%d ○\n",a);
    }
    else
    {
        printf("%d ×\n",a);
    }
    return;
}
/*テスト用main関数*/
int main(void)
{
    int a = 48, b = 1024, c = 17, d = 0;
    printox( gcd( a, b), 16);
    printox( gcd( b, a), 16);
    printox( gcd( a, c), 1);
    printox( gcd( d, a), 48);
    return 0;
}

/*関数gcdの定義を記述しなさい Write the definition of function gcd*/

実行すると。プログラムが正しければ出力は

16 ○
16 ○
1 ○
48 ○

最大公約数を求めるアルゴリズム

最大公約数を求めるアルゴリズムとして,下記のユークリッドの互除法がよく知られています。

  1. 前提条件:2つの0以上の整数が仮引数で与えられる。
     
  2. 大きい方をm, 小さい方をn とする。(n<=m となるようにする)
    ※ 仮引数をm,nとしてもいいし、仮引数は別の名前にして変数m,nを定義してもいい。
    ※ 2つの変数(例えばx, y)の値を入れ替えるには、もう一つ別の変数(例えばz)を用意する必要があります。
    z=x; x=y; y=z;の順番で代入を行えばx,yの値が入れ替わる。x=y; y=x; とするとx,y共に元のyの値になってしまう。
     
  3. n==0なら最大公約数はmである
    ※nがゼロの場合は mの値を戻り値としてreturn
     
  4. m割るnの余りをrとする。
    ※ 余り(剰余算)の演算子は %
  5. m=n; n=r; として3に戻る。 
    ※ 3〜5が前判定の繰り返しなのでwhileなどの繰り返しの構文が使える

[ index | prev | next ]