プログラミングおよび演習 NO.9

Last-Modified: 2014.10.20

関数

今日は関数の使い方を習います。
プログラムの中で、同じ種類の処理(例えば、三角関数の計算、1からnまでの足し算、成績の集計計算など‥‥)を何回も実行したい場合を考えてみます。今までのやり方では、
 
 ‥‥
 同じ種類の処理1
 ‥‥
 同じ種類の処理2
 ‥‥
 同じ種類の処理3
 ‥‥

のように、処理が必要になるたびに同じコードを何回も書く必要があります。

これに対して、"同じ種類の処理"を、関数(サブルーチン)の形で用意しておきます。処理が必要になる度に、次のように何回でも、関数を呼び出して使うことにします。
 ‥‥
 関数の呼び出し --> 引数を渡す
 ‥‥           --> 関数の実行  

              <-- 戻り値を返す

 関数の呼び出し -->    
 ‥‥
 関数の呼び出し -->



このようにすれば、共通して使う部分を関数(サブルーチン)の形で一つだけ用意すれば済みます。 

関数呼び出しと関数処理の流れは次のようになります。

(1) 関数呼び出し

関数に渡すデータを引数に設定して、関数を呼び出します。関数呼び出しがあると、その地点で、呼び出し側の処理を一時中断して、関数の側に処理がジャンプします。

(2) 関数処理の開始

受け取った引数を用いて、関数本体の処理を開始します。関数の始りのの直後の実行文から、順番に処理を進めていきます。

(3) 関数処理の終了

関数の終わりのに到達するか、または、return文やexit文が実行されると、関数処理が終了します。

(4) 呼出し側の処理の再開

関数が終了すると、呼出した元の場所にジャンプして戻ってきます。その際、呼び出し側は、関数から戻り値を受け取ります。 その後、中断していた呼び出し側の処理が再開され、呼び出し地点の次の行の処理を進めていきます。

関数を使うメリット:

関数を使うことで、以下のようなメリットが生まれます。
  • 似通った内容の処理を何回も実行したい場合、プログラムの無駄を省くことができます。
  • 関数の形でルーチンをカプセル化することができ、関数全体の働きが分かれば、中身について知らなくて良くなります(プログラムの中身を隠蔽できる)。この結果、他人が見ても分かりやすい、修正や変更が容易で管理しやすいプログラム、が作れるようになります。
以上のような観点から、Cのプログラムではプログラム全体を、各々の働きに応じて、関数に分けて作るようにします。特に、大勢の人が共同作業で大きなプログラムを完成させる場合、プログラム全体を関数の集合体の形で構成していきます。こうすることにより、一人のプログラマーは、他の人が作ったルーチンを関数の形で利用するだけで良く、大きなプログラムに対応できるようになります。

関数は、

  (1)Cコンパイラが用意している標準ライブラリ関数、
  (2)ユーザが自分で作った関数、

の2種類に分けることができます。

(1)はこれまで習ってきたように、例えば

  入出力関数:printf(),scanf()
  ( ヘッダーファイル:stdio.h)
  や 
  算術関数:sin(),cos(),exp(),sqrt()
  (ヘッダーファイル:math.h )

などがそうです。それぞれの関数の種類に応じて、ヘッダーファイルをインクルードして、関数の型宣言をします。そうしておいて、必要なところで、

(1) 変数名a=関数名(引数1,引数2‥‥);

  のように関数を呼び出します。例えば、

  a=sin(0.5);

  のように関数を呼び出します。この時、sin()関数は引数0.5を受け取って、0.5のサインの値を計算します。その関数の計算結果が戻り値の形で呼び出し側に戻されて変数aに代入されます。

  c=2.0*sin(0.6)+3.0

のように、式の中の一部に関数を記述することもできます。その場合、該当箇所で関数呼び出しが行われ、戻り値が代入される形で式の計算が行われます。

以上は、戻り値を持つ関数の場合ですが、戻り値を持たない関数呼び出しは、

(2) 関数名(引数1,引数2‥‥);

のようになります。例えば、

  printf("Hello");

とすると、文字列定数"Hello"を引数として、printf()関数が呼び出されます。
呼び出されたprintf()関数は、引数として受け取った文字列"Hello"を画面に表示します。

このように、関数呼び出しが行われると、

  ①引数(関数に受け渡すデータ)を渡して、関数を呼び出す。
   (このとき、呼び出し地点から、関数名で指定された関数本体に処理が分岐する)。
  ②関数は引数を貰って、関数本体の演算処理を行う。
  ③関数処理が終了すると、処理結果(戻り値)を持ち帰って、呼び出し地点に戻る。
   (上記の例では、変数aに関数の値が代入される)

のようにプログラム処理が進みます。

(2)のユーザが定義した関数についても、考え方は同じです。違う所は、関数本体の定義(関数の本体の作成)と、関数の型宣言(ヘッダファイルの部分)を自分で用意する必要があることです。具体的には、以下のようになります。

関数の呼び出し側(関数の使用)


・関数の型宣言

関数を呼び出す前に、以下のように関数の型宣言が必要です。これは変数を使う前に型宣言が必要であったのと同じです。変数の場合と違うのは、引数の型(引数は複数ある)と、戻り値の型の、各々について型宣言する必要があることです。
戻り値の型 関数名(引数1の型,引数2の型,‥‥);
例:float add(float,float);
型宣言をした後、プログラム本体で、上記(1)で説明した関数呼び出しのルールに従って次のような形で関数を呼び出します。

・関数の呼び出し

宣言した関数は、次のように呼び出すことができます。
①変数=関数(引数1,引数2,‥‥);(戻り値がある場合)
②関数(引数1,引数2,‥‥); (戻り値がない場合)
例えば、この後の練習問題に従って、戻り値がある関数addは、
………
a=200.0;
b=300.0;
c=add(a,b);
のように使うことができます。この例では 引数をa=200,b=300に設定して 関数add(a,b)をよびだす、戻り値(関数の計算結果)をcに代入する、という処理を行います。この例のように、引数に変数(或いは式)を用いてもよいし、
c=add(200.0,300.0);
のように定数を用いることもできます。あるいは、
c=50*add(200.0,b)-4.0;
のように、式の一部に関数を使うこともできます。

関数の定義(関数の本体、呼び出され側)

 次に、呼び出される関数の本体を次のように書きます。

   戻り値の型 関数名(引数1の型 引数1,引数2の型 引数2,‥‥)
   {      /* 関数の始まりのかっこ */
     変数や関数の型宣言;

     関数本体の実行文;
  
     return(戻り値);
   } 
   /* 関数の終りのかっこ */

   例:
   float add(float x, float y)
   {
      float z; 
  /* 関数の中で使う変数zの型宣言   */
      z=x+y;    /* 関数の中の演算処理        */
      return(z);  /* 戻値(計算結果)zを返す     */
   }

void型

 関数の引数が無かったり、もしくは戻り値が無い場合があります。
まず、引数がない場合は、引数が無いことがわかるように

   戻り値の型 関数名(void)

のように、引数にvoid(空という意味)を指定します。または

   戻り値の型 関数名()

のように()の中に何も書かないで使います。
同様に戻り値がない(return文の()の中が空、あるいはreturn文がない)場合は、

   void 関数名(‥‥)
   {
     ‥‥

     return;//或いはreturn文を書かない
   }

のように、戻り値の型をvoidに指定します。
関数を使うときの注意

  ●呼び出し側の引数と、呼び出された関数本体の型を一致させる。
   (もちろん、引数の数と順番も一致させる)


  ●関数の戻り値の型とreturn文の変数の型を一致させる。

 関数のプログラムがうまく動かない場合の殆どは、上記の間違いが原因です。

加減乗除算の関数を用いた、プログラムの例を以下に示します。

例9-1
#include<stdio.h>
#include<math.h>

/*  関数の型宣言  */
float add(float,float),sub(float,float),mul(float,float); 
float div(float,float),mag(float,float); 

int main(void) /* メイン関数 */
{
    /*  変数の宣言 */
    float a,b,ans1,ans2,ans3,ans4,ans5; 
    
    /* ここからが実行文(プログラムの本体)  */
    printf("Input a=");fflush(0);scanf("%e",&a);
    printf("Input b=");fflush(0);scanf("%e",&b);
    
    ans1=add(a,b); /* 関数add()の呼び出し */
    ans2=sub(a,b); /* 関数sub()の呼び出し */
    ans3=mul(a,b); /* 関数mul()の呼び出し */
    ans4=div(a,b); /* 関数div()の呼び出し */
    ans5=mag(a,b); /* 関数mag()の呼び出し */

    printf("a+b=%f\n a-b=%f\n a*b=%f\n a/b=%f\n sqrt(a**2+b**2)=%f\n",
    ans1,ans2,ans3,ans4,ans5);
    
    return(0);
}

/* 和を求める関数 */
float add(float x, float y)
{
    float z;
    z=x+y;
    return(z);
}

/* 差を求める関数 */
float sub(float x, float y)
{
    float z;
    z=x-y;
    return(z);
}

/* 積を求める関数 */ 
float mul(float x, float y)
{
    float z;
    z=x*y;
    return(z);
}

/* 商を求める関数 */
float div(float x, float y) 
{
    float z;
    z=x/y;
    return(z);
}

/* 2つの数の2乗の和の平方根 */
float mag(float x, float y) 
{
    float z;
    z=sqrt(x*x+y*y);
    return(z);
}

演習問題9-1 (Revised : 2014/10/20)

例9-1において、

  ①七つの引数a,b,c,d,e,f,gの相加平均 (a+b+c+d+e+f+g)/7 を求める関数 amean(a,b,c,d,e,f,g)
  ②  〃   の中央値を求める関数 median(a,b,c,d,e,f,g)

を追加してください。
なお、レポートには、必ず、手計算の結果との比較、プログラムの動作説明、関数を用いることのメリットなどに関する考察を含めてください。

[中央値の補足説明] 中央値は、データを大きさの順に並べたとき、データの数を2等分する位置の変数の値をいいます。
例えば、データの個数が7個の場合、大きい方から数えて(小さい方から数えても同じ)4番目のデータになります。データの並べ替え(ソーティング)は第8回例8-4のプログラムを利用してください。

演習問題9-2 (Revised : 2012/10/23)

ウエスト周囲長と性別の2つの情報をキーボードからデータを入力すると、メタボリック症候群の可能性の有無を表示するプログラムを作成してください。メタボリック症候群の可能性を判定する関数を呼び出し、その戻り値をもとに判定結果を画面表示するプログラムを作ってください。詳細は以下の説明に従ってください。
実行結果は、手作業の結果と比較して正しく動作している事を確認して下さい。考察には手計算との照合結果とプログラムの動作説明を示してください。

[判定結果の表示]
メタボリック症候群の判定基準には、世界糖尿病連盟(IDF)と日本肥満学会(JASSO)の各々の機関が推奨している2種類の基準があります。
以下の判定条件のもとに、IDFかJASSOの基準のうちのいずれかでメタボリック症候群の可能性有と判断された場合、その基準名と共に「メタボリック症候群の可能性有、血圧や中性脂肪などの値を確認して下さい」と表示する関数を作ってください。なお、両方の基準で可能性有と判断された場合には、基準名を両方表示した上で「メタボリック症候群の可能性有、血圧や中性脂肪などの値を確認して下さい」と表示し、両方の基準が共に可能性を否定した場合には、「メタボリック症候群の可能性無」と表示すること。

[判定条件]
ウエスト周囲長(cm) と性別(Male:0,Female:1)を入力し、以下の条件を満たした時にメタボリック症候群の可能性有と判断する。

  世界糖尿病連盟(IDF)の基準
  ウエスト周囲長が  男性≧90cm、女性≧80cm のとき
 日本肥満学会(JASSO)の基準
  ウエスト周囲長が  男性≧85cm、女性≧90cm のとき

[関数の引数]
  ウエスト周囲長(cm) AC
  性別(Male:0,Female:1) Sex

[関数の呼び出し]
  Metabol(AC,Sex)

[関数の戻り値]
  メタボリック症候群の可能性無し  0
  IDFの基準でメタボリック症候群の可能性有り 1
  JASSOの基準でメタボリック症候群の可能性有り 2
  IDFおよびJASSOの両方の基準でメタボリック症候群の可能性有り 3

■main関数


いままで、プログラムの本体を

  int main(void)
  {
     ‥‥
  }


のように書くように説明してきましたが、その理屈は説明していませんでした。
実は、mainも基本的には今日説明した関数と全く同じ意味の関数にすぎません。ただし、mainは、プログラムの一番最初に、mainという名前の関数から実行を始めるという約束のある、特別な意味のある関数です。
 このため、main関数は、一般の関数と、次のような違いがあります。

①main関数は、プログラムの中に必ずないといけません。これに対して、一般の関数はあっても無くてもかまいません(前回まで練習してきたプログラムは、main以外の関数を使っていませんでした)。
②main関数をプログラムのどこかで呼び出す必要はありません(プログラムの開始と同時にmainが自動的に呼び出されるため)。これに対して、一般の関数は、関数呼び出しがあった場合にのみ実行されます。

 今日の関数の説明で、main関数の各々の意味が理解できたでしょうか?(void の意味を含めて)。

変数の有効範囲と記憶クラス

●関数の使い方に関連して、変数の有効範囲や記憶クラスについて説明します。
これらは、関数を正しく理解するための重要事項ですので、是非、気を抜かないでやって貰いたいと思います。
 また、今週のところまでをきちんと理解して貰うと、これまでとりあえず説明が難しいので、 理屈を後回しにしてきた所が、全て明らかになります。 プログラムの最初から最後まで、何故そのように書くかということが、 理屈を含めて理解して貰えると思います。

■記憶クラス

まず始めに変数の記憶クラスについて説明します。記憶クラスの違いは、以下のようなデータの保持形態の違いを意味します。

自動変数(auto)
自動変数が関数の中で使われている場合、関数の実行が終わると変数の中身は消えてしまう。 また、初期値を指定しないと、適当な数(いい加減な数)が入る(注意が必要)。 例えば、整数型の自動変数aは
auto int a;

のように宣言します。ただし、autoを省略してもよいことになっていますので、普段は

  int a;

と書きます。今まで例題で使ってきた変数は全て自動変数です。

静的変数(static)

静的変数は、関数の実行が終了しても変数の中身は記録されて残っています。また、初期値を指定しない場合、最初の値を0に初期化します。整数型の静的変数aは

  static int a;

と宣言することにより使用できます。

以上の他に、レジスタ変数や外部変数がありますが、使用頻度は低いので、ここでは説明を省略します。

■有効範囲

C言語では、変数が何処で宣言されているかによって、その変数の有効範囲が決まります。 大きく分けて、プログラム全体にわたって共通して使える①グローバル変数と、 各々の関数の中だけで使える②ローカル変数の2つがあります。

①グローバル変数(プログラム全体で有効)

以下の例のように、main()を含めた関数の外側(ファイルの先頭部分)に、宣言して使います。
#include<stdio.h>

int a; /*整数型変数a( グロ-バル変数)の宣言*/
       /*以下のプログラム全体に渡って有効*/   
int main(void)
{
  ‥‥
}
int func(‥)
{
 ‥‥
}

以上のようにグローバルで宣言した変数は、プログラム中のどの関数からも使うことができます。 グローバル変数の記憶クラスは、静的変数と同じ扱いになります(この場合、static を先頭につける必要はありません。 また、初期値が指定されていない場合、自動的に0に初期化します)。

利点: 通常は関数のデータの受け渡しは、引数や戻り値を用いるが、グローバル変数を使えば、 引数や戻り値を使わなくてもプログラムが書ける。プログラムを簡単に作りたい場合に便利。

欠点: グローバル変数を多用すると、変数の重複使用などプログラムミスを引き起こす原因になる。

②ローカル(ブロック内)変数(関数内やブロック内だけで有効)

関数の引数部や、関数の始まりと終わりの括弧の中(ブロック内)で宣言された変数はローカル変数になります。

#include<stdio.h>

int main(void)
{
    int a,y; /*ロ-カル変数*/
  ‥‥
   y=func(a);
  ‥‥
}


int func(int x) /* 引数xはローカル変数*/
{
     int a, z; /*ロ-カル変数*/
     ‥‥
     ‥‥
     return (z)/*戻り値zはローカル変数*/
}

上図のように宣言されたローカル変数は、 関数のブロックの中(関数の始まりの{と終りの}で 囲まれた範囲、上図で色で示した範囲)だけで使うことができます。

ローカル変数の記憶クラスの指定を省略した場合、 自動変数になります (静的変数にしたい場合、static を先頭につけます)。

ローカル変数の宣言文で変数の初期値を指定しなかった場合、

  静的変数(static) の場合 : 自動的に初期値を零にする
  自動変数(auto) の場合 :  初期値の設定は行わない(何が入っているかは分からない)

の違いがあることに注意してください。

異なる関数の中で, 同じ名前のローカル変数が使われている場合、各々は独立したまったく別の変数であることに注意してください (上図の例において、関数mainの中のaと関数funcの中のaは、名前が同じだけで全く別の変数)。

利点/欠点: グローバル変数の逆の利点/欠点がある。グローバル変数は、 プログラム全体で共通的に使用するデータや、サイズの大きい配列データなど、に用います。その他の場合は、 プログラムのモジュール化やカプセル化の考えにたって、なるべくローカル変数を使うようにします。


■関数の型宣言の場所と有効範囲

以下は、例9-1の関数magの宣言と呼び出し部分を抜粋したものです。そこでは、

#include<stdio.h>

int main(void)
{
  
float mag(float,float);
  ‥‥
}

のように、関数magの型宣言を呼び出す関数(上記の場合はmain関数)の中に書いています。 この場合、関数magの型宣言はmain関数の中だけで有効です。 言い換えると、main関数以外の関数からmag関数を呼び出す場合、呼び出す関数のそれぞれに、関数の型宣言が必要です。これに対して、例9-1のように、

#include<stdio.h>

float mag(float,float);

int main(void)
{

  ‥‥
}


として、main関数の外に書くこともできます。この場合、関数magの型宣言はプログラム全体にわたって有効です。 すなわち、上記のように宣言すると、main以外のどの関数からも関数magを呼び出すことができます。



以下では、品物の税抜き価格と消費税率を与えた時に、税込み価格を計算する関数(TotalPrice)を使うプログラムについて説明します。最初の例9-2では、関数データの受け渡し方法として、通常の引数(税抜き価格と消費税率の2つの引数)と戻り値(税込み価格)を用いて実現しています。

例9-2(ローカル変数を用いた関数データの受け渡し)(Revised : 2013/10/21)
#include<stdio.h>
#include<math.h>

int main(void)
{
    int TotalPrice(int, int);
    int price,taxrate,total;

    printf("Before-tax price="); fflush(0); scanf("%d",&price);
    printf("Rate of tax(%%)="); fflush(0); scanf("%d",&taxrate);

    total=TotalPrice(price,taxrate);
    printf("Tax-included price=%d\n",total);
    
    return(0);
}

int TotalPrice(int x, int y) 
{
    int z;
    z=x+x*y*0.01;
    return(z);
}

次に以下の例9-3は、グローバル領域に宣言した変数や関数を用いる形に、例9-2を書き換えたプログラムです。すなわち、例9-2の関数TotalPriceは引数と戻り値を介してデータの受け渡しをしましたが、例9-3の関数TotalPriceは引数と戻り値を使わずに、グローバル変数price,taxrate,totalを介してデータの受け渡しをしています。 また、関数TotalPriceの型宣言はグローバル領域に宣言しています。
例9-2と例9-3の 二つのプログラムの実行結果は全く同じになります。

例9-3(グローバル領域に宣言した変数や関数を用いる形に書き変えた場合)(Revised : 2013/10/21)
#include<stdio.h>
#include<math.h>

void TotalPrice(void);
int price,taxrate,total;

int main(void)
{
    printf("Before-tax price="); fflush(0); scanf("%d",&price);
    printf("Rate of tax(%)="); fflush(0); scanf("%d",&taxrate);

    TotalPrice();
    printf("Tax-included price=%d\n",total);
 
    return(0);
}

void TotalPrice(void) 
{
    total=price+price*taxrate*0.01;

    return;
}



演習問題 9-3

例9-2、9-3の各々のプログラムの動作を確認して説明してください。特に、関数への変数の受け渡し方や計算結果の受け取り方に注目してそれぞれのプログラムの動作を説明し、その上で両者の出力が同じになる理由を述べてください。


演習問題 9-4  (Revised : 2013/10/21)
#include <stdio.h>
int gi,si;

int main(void)
{
    int i,ai=0;
    void count();

    printf("\t gi\t si\t ai\n");
    for(i=0; i<3; i++){ 
        count();
        gi=gi+1;
        si=si+1;
        ai=ai+1;
        printf("main()\t gi= %d\t si=%d\t ai=%d\n",gi,si,ai);
    }
    return(0);
}

void count(void)
{
    int i, ai=0;
    static int si;

    for(i=0; i<3; i++){
        gi=gi+5;
        si=si+5;
        ai=ai+5;
        printf("count()\t gi= %d\t si=%d\t ai=%d\n",gi,si,ai);
    }
    return;
}

(1) 上記のプログラムを実行し、表示結果を説明しなさい。特に、gi,si,aiの三つの変数は、同じ計算を行っていることに注意し、宣言文の位置が違うだけで、三者三様の結果が表示される理由について、プログラムの動作説明を含めながら詳細に説明をしなさい。

(2) main関数内およびcount関数内の各々の自動変数aiの初期化について、以下のように変更するとどうなるかを答えなさい。具体的には、以下の3通りについて試し、各々の結果について、その理由をつけてレポートにまとめなさい。
 ①count関数内のaiだけ0に初期化し、main関数のaiは宣言のみする場合
 ②main関数内のaiだけ0に初期化し、count関数のaiは宣言のみする場合
 ③main関数内とcount関数内のaiは両方とも初期化せずに宣言のみする場合

(3) プログラムを最初の状態に戻した後で、count関数内の静的変数siの 宣言文を取り除くとどうなるか試し、 レポートには結果とともにその理由について述べなさい。




●インクルード文の意味

今まで、標準関数である入出力関数を使うときは、

  #include<stdio.h>
  /*このinclude文の意味は、stdio.hというファイルをここに挿入するという意味です。*/

また数学関数を使う時は、

  #include<math.h>

のように、必要なヘッダファイル(stdio.hやmath.h)をインクルード(挿入)してから、使うように説明してきました。 何故、インクルードしないと使えないのでしょうか?その答えは、次のようになります。 実は、stdio.hやmath.hの中身は、各々が用意しているいろいろな関数の型宣言が書いてあります。

例えば、math.hの中身は以下のような宣言文が並んでいます。

これを見ると、math.hでどんな算術関数が使えるか(どんな関数が宣言されているか)、 また各々の関数の引数の型、戻り値の型、の約束を知ることができます。

  ‥‥
  double acos(double _x);
  double asin(double _x);
  double atan(double _x);
  double atan2(double _y, double _x);
  double ceil(double _x);
  double cos(double _x);
  double cosh(double _x);
  double exp(double _x);
  double fabs(double _x);
  double floor(double _x);
  double fmod(double _x, double _y);
  double frexp(double _x, int *_pexp);
  double ldexp(double _x, int _exp);
  double log(double _y);
  double log10(double _x);
  double modf(double _x, double *_pint);
  double pow(double _x, double _y);
  double sin(double _x);
  double sinh(double _x);
  double sqrt(double _x);
  double tan(double _x);
  double tanh(double _x);
  ‥‥


math.hがどこにあるかというと、/usr/include  というようなディレクトリの中にあります。コンピュータ管理者の設定によって場所が違います。興味のある人は一度中身を覗いてみてください。ただし、これはあくまでも自分のコンピュータで行ってください。センターのコンピュータではあまりやらないでください(初心者の人は特に)。誤って消してしまったり、書き換えたりしてしまうと困るからです。