関数

ここまでの理解度チェック

理解度チェック

前回までの理解度の確認のために,簡単な練習問題を解いてみよう.

  1. while文を用いて,画面に 1 から 100 までの整数を一行で表示してみよう.
    実行例(出力値は正しいとは限らない):
    1 2 3 4 ... 98 99 100
    
  2. 同様の結果を,for文を用いて出力してみよう.
  3. 次に,カウンタ変数を実数に変更し,値の範囲を 0.0 から 100.0 まで,0.1 おきとして,表示してみよう.小数点以下第1位まで出力のこと.
    0.0 0.1 0.2 0.3 ... 99.8 99.9 100.0
    

目次

はじめに

多くのプログラミング言語では,(演劇の台本や,音楽の楽譜と同様に)処理の流れは基本的にソースコードの上から下へと逐次進行する.
これに加えて,条件分岐(if, switchなど)と繰り返し(for, whileなど)を組み合わせることによって,あらゆる処理の流れを実現可能であることが知られている.
また,必要な処理を機能ごとに分割し,その階層的な呼び出しの組み合わせで,大きなプログラム全体を組み立てることができる.

このような考え方はstructured programming(構造化プログラミング)と呼ばれている. C言語では,この「必要な処理を機能ごとに分割」する文法として「関数」が用いられる.

参考:C言語の関数は英語で function と呼ばれる.
「関数」より,「機能」という和訳のほうが理解しやすいかもしれない.

関数とは

構造化プログラムでは,さまざまな機能を部品(モジュール)として実装し,その呼び出し関係を階層的に記述することで処理を表現する. C言語では,関数と呼ばれる仕組みを使って,これを実現している.
関数はC言語のような手続き型プログラミング言語の根幹をなす機能である.
(BASIC, JavaScript, Python など,広く用いられている他の言語においてもサブルーチン,プロシージャ,副プログラムと呼ばれる同様の機能がある)

いろいろな場面で共通して用いられる動作(画面出力やキーボード入力)や,何度も繰り返される処理などはあらかじめ部品として作成しておき,必要に応じて使えるようにしておくと便利であり,効率的である.

これまで使用してきたprintf(), scanf() は,実は関数であるが,自分でこれらを作成する必要はなかったが,これは,画面表示やキーボード入力などはどのPCでも必要な基本的な動作であるため,自作せずとも初めから用意されているからである.
このような関数をライブラリ関数と言う.

ライブラリ関数は,必要なヘッダファイルの#include文の記述により,プログラム中から自由に呼び出す(call)ことができる.
例えば,printf(), scanf() 関数を使用するには,stdio.h (=Standard input/output)というヘッダを include することになっている.

今回は,ライブラリ関数の呼び出しだけでなく,自作で関数を定義する作り方(関数の宣言と定義)と,使い方(関数呼び出し)を学ぶ.

関数呼び出しの処理の流れ

プログラム中で,関数名の書かれている部分に処理が到達すると,元の関数内での処理から,呼び出した関数にジャンプする. これを関数の呼び出し(call)という.
呼び出した先の関数では,あらかじめ用意されたコードがが実行され,完了すると呼び出し元に処理が戻る (これをreturn,リターンするという).

簡単に図で描くと,このような流れで処理が進む.
この図では,ライブラリ関数である printf(), scanf()関数を,main() 関数から呼び出している.

function call
関数の呼び出しと,処理の流れ.

プログラミングを始めて一番最初に学んだ main() も,関数の一つである. main()関数は,OSで実行ファイルを指定したときに,処理が開始される「入口」に相当する特別な関数(エントリポイント)として規定されている.
したがって,C言語のプログラムは関数の集合であるといえる.

関数のつくりかた

関数の処理本体を定義 (definition)と呼ぶ.

関数定義の文法

戻り値の型名 関数名(型名 仮引数名1,型名 仮引数名2, ...)
{
    //
    // ここに処理を記述
    //

    return xxx;    // xxxが戻り値
}

関数とは,例えて言えば材料を渡すと,レシピに基づいて調理し,料理をアウトプットするようなものである.
C言語の文法では,

  1. 材料に相当するものは,引数(ひきすう)
  2. 調理法であるレシピは,関数の定義
  3. 出来上がった料理は,戻り値(もどりち)

と呼ばれる.
以下,関数を作るにあたってのルールである.

プログラム例:関数の定義,呼び出しと,引数

まず,以下のような簡単なプログラムを考える.

よく吠える犬がいます.吠える動作を,関数bark()を作成してみよう.
() をつけているのは,この名前が変数でなく関数であることを意味する.

#include <stdio.h>

// 新たに作成した自作関数の定義
void bark(void)
{
    printf("bow-wow!\n");   // 犬の鳴き声の onomatopée
    return;
}

// 元々のmain関数.プログラムに必要.
int main(void)
{
    bark();          //  関数 bark() の呼び出し (call)
    bark();
    bark();
    bark();
    bark();
    return 0;
}

吠える回数を引数として関数に渡す場合

#include <stdio.h>

// 引数のある関数
void bark(int n)   // 引数がある場合
{
    int i;
    for(i=0; i<n; i++) {
        printf("bow-wow!\n");
    }
    return;
}

int main(void)
{
    bark(5);          //  関数の呼び出し(call) with 引数
    return 0;
}

bark()関数内の処理・計算などで必要な数値はmain()からbark()に引数として渡すことができる.

プログラム例:戻り値

関数の戻り値は「あり」,「なし」のどちらかである.
先ほどの関数bark()は戻り値「なし」の例である. この場合,関数のブロックの終わり}で自動的,呼び出しもとのmain関数に処理が戻るため,return 文を省略することができた.

一方,以下の例では,関数pi()は,double型の実数を返す関数であるため, return ???; の行が必須である.

#include <stdio.h>

// double型の戻り値を返す関数
double pi(void)
{
    //  何か計算をする...
    return 3.141592;
}

int main(void)
{
    printf("pi = %20.18lf\n", pi() );
    return 0;     // 実は,これはmain関数の戻り値
}

このように,一つの機能を関数としてまとめることで,main関数がすっきりわかりやすくなる.
引数をとることにより,関数の動作をいろいろと変化させることができる.
また,戻り値により関数での処理結果を呼び出し元に伝えることができる.

練習問題

上記の円周率を返す関数pi()において,級数を用いてなるべく正確な円周率を計算してみよう.

例:π=4(1-1/3+1/5-1/7+1/9+...)

ヒント:円周率の計算を行う級数の公式は,数多く提案されている.以下のサイトおよび,理解できる範囲で収束の速い他の公式を用いても良い. 無限には計算できないので,適当なところ(小数点以下15桁程度)まで,計算すれば良い.

プログラム例:引数および戻り値がある例

二つの数a,bを渡すと,a2+b2 を計算して返す関数を見てみよう.

左の例では,すべての処理をmain関数内で行う.
一方,右の例では新たな関数 calc を自分でつくり,そこで計算処理をしている.

main関数のみ mainと,計算関数calc
#include <stdio.h>







int main(void)
{
    int a=3, b=5;  // 二個の整数
    int kekka;     // 計算結果をここに格納

    // ここで直接計算
    kekka = a*a + b*b;

    printf("Ans = %d\n", kekka);

    return 0;     // mainの終わり
}
#include <stdio.h>

int calc(int c, int d)   // 計算する関数を定義.
{
    int kekka = c*c + d*d;
    return kekka;        // 値を返すreturn文
}

int main(void)
{
    int a=3, b=5;
    int kekka;

    // 関数を呼び出して計算結果をkekkaに代入
    kekka = calc(a,b);  

    printf("Ans = %d\n", kekka);

    return 0;     // mainの終わり
}

ここで,自作の calc() という関数は,整数型の引数を2つとり,整数型の戻り値を返す関数としている.
main()にたいして,ソースコードの上方で定義されている

引数については,渡す側 main() と,受け取る側 calc() で,同じ個数,同じ順序,それぞれ同じ型で書く必要があり,

と呼ぶ.

関数calc内では,変数 int c および int d が「仮に」与えられたとして処理を記述することから,「仮」引数と呼ぶ.
関数が呼び出され処理がcalcに移ってきた時点で,実引数の値(a,b)が,仮引数(c,d)にコピーされて渡されてくる.

仮引数のリストは,必ず型名と変数名をペアで書く必要がある.
通常の変数宣言のように,int c, d と省略することはできず,int c, int d と記述する必要がある.

練習問題

  1. 上記2つのプログラムをそれぞれ実行して,結果を比較しよう.(同じ結果になるか?)
  2. 右の例で,変数 kekka の宣言と kekka = calc(a,b); を削除したうえで,printf("結果 = %d\n", calc(a,b) ); と変更してみよう.
    同様の結果が得られただろうか?.
    関数での処理が終わり return すると,関数呼び出し部分が処理結果の数値に置き換わると考えれば良い.

いろいろな関数定義

例えば,引数として整数を2つ受けとり,その二乗和(ノルム)を整数型で一つ返す関数の形を確認してみよう.
関数の書き方の基本は以下の通りである.

#include <stdio.h>

int norm(int a, int b)    // 関数 norm() の定義.仮引数は整数2つ,戻り値の型は整数
{
    return a*a + b*b;
}

int main(void)
{
    int x = 1, y = 3;
    int z;

    //  関数呼び出し.
    //  関数での処理が終わりreturnすると,norm(x, y)の部分が数値に置き換わると考える.
    z = norm(x, y);

    printf("z = %d\n", z);
    return 0;
}

処理の流れ,引数間の値のコピーや,return による値の戻りをよく理解したうえで,練習問題に取り組んでみよう.

練習問題

  1. 上記のプログラムを改良し,整数でなく実数のノルムを計算するプログラムに変更しよう.
    変数の型を変更すると,ソースコードのどこに影響があるか考えよう.
    実行例:
    x = 10.5, y = 2.2 のとき
    norm = 115.09
    
  2. 二つの実数をキーボードから入力するよう,main()関数を変更してみよう.
  3. ノルムではなく絶対値(ノルムの平方根)を求める関数 my_abs() を作成してみよう.
    平方根の計算は,下記のようにmath.h をincludeして,sqrt() 関数を用いる.
    #include <math.h>
    
    int main(void)
    {
        double z;
        z = sqrt(2.0);   // sqrt()は,2.0の平方根を返す.
        return 0;
    }
    

いろいろな関数の記述例

引数の個数や型,および戻り値の型や有無については自由に決めてよい.
どのように使い分けるかは,関数にどのような動作をさせたいかによって決まる.
つまり,機能部品(モジュール)としての関数の設計に依存する.

関数定義の指針としては,一つの関数には,一つの機能を与え,その機能を表す適切な名前をつけるのが良い.
逆に,1つの関数に多くの処理を詰め込むことはよくない.

関数名については,一般に,関数には何らかの手続きを記述するので,その名前には動詞,または動詞+目的語を組み合わせることが多い.
例えば,do_something(), calc_velocity(), init_data(), load_from_file(), save_to_file(), show_result()などである.
別の流儀として,2番目以降の単語の先頭を大文字にして,doSomething(), calcVelocity(), initData() と命名する場合もある.

関数の役割上,引数や,戻り値が不要な場合は明示的に void と書く
以下に様々な引数,戻り値の組み合わせを例示する.

#include <stdio.h>

// 引数なし,戻り値無し の関数
void moji_hyouji(void)
{
    printf("単なる文字の表示です.\n");
    return;    //  戻り値「なし」なので,returnのみ記述.このreturnは省略可
}


// 引数あり,戻り値無し の関数
void wano_hyouji(int a, int b)
{
    int x = a + b;
    printf("wano_hyouji関数,和は %d です.\n", x);
    return;    // 戻り値「なし」.このreturnは省略可
}


// 引数あり,戻り値あり の関数
int wano_keisan(int a, int b)
{
    int x = a + b;
    return x;    // これは絶対必要
}


int main(void)
{
    int a=3, b=5;

    moji_hyouji();               // 引数なしでも関数呼び出しには()をつける
    wano_hyouji(a, b);
    int sum = wano_keisan(a, b);

    printf("wano_keisan関数,和は %d です.\n", sum);

    return 0;
}

練習問題

上記プログラムを実行して,処理の流れを確認しよう.

関数のプロトタイプ宣言

C言語のソースファイルは関数の集合であることはすでに述べたが,ソースコード中で関数を定義する場所と,呼び出しを記述する順序に注意が必要である.
というのは,コンパイラがソースファイルの構文解析を行う際に,ソースコードの上から下へとを行うため, 関数を呼び出す時点で,その関数名や引数についての情報が既出である必要がある.
(注:最近の賢いコンパイラは,ソースコード下方から関数の定義を探してくるものもある.)

例として,2つの整数の和を返す関数を考えてみよう.

#include <stdio.h>

int wa_no_keisan(int a, int b)    // 関数の定義
{
    return a+b;
}

int main(void)
{
    int x = 1, y = 3, z;
    z = wa_no_keisan(x, y);    // 関数の「呼び出し」
    printf("%d\n", z);
    return 0;
}

これはうまくコンパイルできるはずである.
次に,関数の中身は変えないで,main()wa_no_keisan()の記述順序を逆にしてみよう.
コンパイルするとどうなるだろうか.

//
//  書かれている内容は全く同一だが・・・
//
#include <stdio.h>

int main(void)
{
    int x = 1, y = 3, z;
    z = wa_no_keisan(x, y);    // 関数の「呼び出し」
    printf("%d\n", z);
    return 0;
}

int wa_no_keisan(int a, int b)    // 関数の定義
{
    return a+b;
}

この例は,関数の中身はそれぞれ先のコードと全く同じであるが,コンパイルエラーまたは警告が出る.
理由は,main() 関数内の wa_no_keisan() の呼び出しよりも下方に関数 wa_no_keisan() の定義が書かれている点である.

この例のように,関数の定義を呼び出す前に書くことが可能であれば良いが,関数が増えてくると呼び出しの依存関係が複雑になったり,また,2つの関数が処理でお互いを呼びあったりする場合は,どちらを先に書いてもエラーとなってしまう.

そこで,関数の形(戻り値の型,関数名,仮引数の個数と型)のみを,あらかじめ宣言する方法が用意されている.
これを関数のプロトタイプ宣言,という. (プロトタイプ=試作品,原型)

以下の例で確かめてみよう.

#include <stdio.h>

int wa_no_keisan(int a, int b);    // 関数のプロトタイプ宣言 「;」が必要.
 
int main(void)
{
    int x = 1, y = 3, z;
    z = wa_no_keisan(x, y);    // 関数の「呼び出し」
    printf("%d\n", z);
    return 0;
}

int wa_no_keisan(int a, int b)    // 関数の定義
{
    return a+b;
}

なぜプロトタイプ宣言だけで良いかといえば,関数を呼び出す際には,差し当って,関数の名前,引数の型と個数,戻り値の型といった関数の形に関する情報があれば十分だからである.
関数本体の処理内容(定義)は,コンパイル後のリンク作業で結合され,実行ファイルが作られる.

練習問題

  1. 上記のソースコードをそれぞれコンパイルしてみよう.
  2. プロトタイプ宣言の行を削除してコンパイルすると,どうなるか.
  3. プロトタイプ宣言の行を残して,関数定義を削除してコンパイルすると,どうなるか.

プロトタイプ宣言とヘッダファイル

この関数プロトタイプ宣言は,その関数の定義が「同じソースコード内には無い」場合に重宝される.
例えば,printf() の定義は皆さんが作っているソースコードの中には無いですね.
実は,おまじないの #include を記述している stdio.h という名前のヘッダファイル中に,printf() 関数のプロトタイプ宣言が書かれている.

また,printf()関数の定義は,事前にコンパイルされ,ライブラリーファイル(.a, .libファイル)としてまとめられており,これを自作のプログラムに結合する作業は,リンク処理で行われる.
通常はコンパイルすると,以下のように自動的に「リンク」処理まで行われる.

  1. プリプロセス:#include の展開や #define マクロの置き換え.
  2. コンパイル:.cppファイルごとにオブジェクトファイル(.obj, .o, 機械語の一部)をそれぞれ生成.(この時点では関数の定義は無くても良い,プロトタイプ宣言のみでOK)
    どこか(別のファイル)に関数があるものとしてコンパイルされる. 文法に誤りがあれば,この時点で「文法エラー」が発生する.
  3. リンク:複数のオブジェクトファイルを結合し,単一の実行ファイルを生成する.この際,必要に応じてライブラリファイル内の printf(), scanf() などの関数の機械語コードを結合する.
    関数の定義が見つからなければ,「リンクエラー」が出る.

練習問題

  1. 一番最初に書いた Hello World! プログラムにおいて,#includeの行を削除してコンパイルしてみよう.
    どのようなエラーが出るだろうか.
  2. PCのドライブ,またはUSBメモリ上の開発環境のフォルダから stdio.h というファイル(複数ある?)を探し,その中からprintf, scanf関数のプロトタイプ宣言を探し出せ.

ライブラリ関数

一般的には,C言語の開発環境には「ライブラリ関数」と呼ばれる既成品の関数群が用意されている.
本来は,ライブラリ関数はコンパイラとは別の製品であるが,gccやbcc, Microsoft Cなど市販の or 無償利用できるコンパイラの多くは,ライブラリ関数とともに配布されている.
いままで用いてきた printf(), scanf()は,ライブラリ関数の代表例である.

ライブラリ関数を利用するには,そのプロトタイプ宣言が書かれたヘッダファイルを必要に応じて #include する必要がある.
これはほんの一例であるので,詳しくは各種メーカー提供のライブラリ関数リファレンスを参照のこと.

以下,いずれも外部サイトへのリンクです.

以下によく使われるライブラリ関数の例を示す.

  1. 標準入出力printf(), scanf()
  2. 標準ライブラリexit(), rand()
  3. 算術sqrt(), sin(), cos(), tan(), exp(), log()
  4. ファイル入出力 fopen(), fclose(), fprintf(), fscanf()
  5. 文字列操作 strcpy(), strlen(), atoi(), atof()
  6. メモリ malloc(), calloc(), free()
  7. 時間・時刻 time(), clock()

ファイル入出力や,文字列操作などは,別の回に説明する.

練習問題

1.算術関数

算術関数ライブラリを実際に使ってみよう.
math.h内で宣言されている sin, cos, tan 関数のプロトタイプは,

double sin(double x);
double cos(double x);
double tan(double x);
である.

下記のプログラムは,1周期分の正弦 sin の値を,数値として出力する.
このソースを実行出来たら,cos, tanについても同様に計算してみよう.

周期関数の引数は,ラジアン単位である事に注意.
わかりにくいようであれば,表示する際に,ラジアン→度に変換してみよう.

#include <stdio.h>
#include <math.h>       // これが必要

const double PI = 3.14159265358979;

int main(void)
{
    double theta;

    printf("theta , sin(theta) \n" );
    for(theta=0.0; theta<=2.0*PI; theta += 0.1*PI ) {
        printf("%lf , %lf\n", theta, sin(theta) );  // ラジアン表示.
    }
    return 0;
}

2.乱数

乱数を発生させる関数 rand() を使ってみよう.
この関数のプロトタイプは,

int rand(void);

である.
関数が呼ばれるたびに,0 から RAND_MAX(stdlib.h内で定義されている整数)以下のランダムな整数を返す.

#include <stdio.h>
#include <stdlib.h>       // これが必要

int main(void)
{
    for(int i=0; i<100; i++ ) {
        printf("%d\n", rand() );
    }
    return 0;
}

これを変更して,0.0 から 1.0 までの実数の乱数を発生させてみよう.
詳しい使い方は,ライブラリ関数リファレンスを参照のこと.


変数の有効範囲

ローカル と グローバル

関数の範囲は,波括弧{ }で表されるコードブロックで表される.
関数定義の中で宣言される変数は,その関数中でのみ有効であり,関数が終了し処理がブロック外に出ると,その変数は自動的にメモリ上から破棄され使用できなくなる.
このような変数は自動変数とよばれる. 通常の変数は,この自動変数となる.
(参考:自動変数の対義語としてstatic(静的)変数というのもある.)

関数「内」で宣言される変数のほかに,関数「外」でも変数の宣言をすることができる.
関数の外で宣言された変数はどの関数にも属さないため,(その変数が宣言された行以降の)全ての関数から使用できる.

#include <stdio.h>

//  関数外での変数宣言=グローバル変数
int global = 100;
float data = 2.99;
    
int main(void)
{
    int i;     // 関数内での変数宣言

    i=1;           // これはもちろんOK
    data = 5.11;   // これもOK
    return 0;
}

変数が利用可能なソースコード中の範囲を「変数のスコープ」と呼び,その範囲は変数が定義された場所で決まり,

と呼ぶ.
ローカル変数,グローバル変数は,それぞれ局所変数,大域変数とも呼ばれる.

注:関数の仮引数は,見かけ上関数のブロック { } の外側にあるが,これは関数内でのみ有効なローカル変数である.

グローバル変数の宣言と有効範囲

グローバル変数は,どの関数でも変更可能なので,あちらこちらで自由に参照できるので,一見すると非常に便利に思えるが,実は副作用の方が大きい.
特に,プログラムの規模が大きくなるにつれ,次第にその管理が難しくなることから,安易に使わない方が良い.
関数間でデータをやり取りする場合は,引数や戻り値を適切に受渡しするのが良い.

グローバル変数の使用が適している例として,プログラム全体で共通して使用する定数や,円周率や重力加速度などの物理定数などがある.
このような定数の場合は,定数を誤って変更しないように const を追加で指定しておくと安全である.

#include <stdio.h>

// この位置では,定義前なので変数は使用不能.
// global_val = 1.0;    // これはエラー


//  グローバル変数.定義した行以降で有効
const double pi = 3.14159265358979;   // const 指定すると,値の変更がコンパイルエラーとなり安全.
float global_val = 0;


float area(float r)    // 仮引数 r は,関数 area のローカル変数
{
    float A;        //  ローカル変数.
    A = pi * r * r;   //  グローバル変数 pi を使用
    return A;
}
    
void func(void)
{
    float r = 10.0;  // ローカル変数.関数area()内の r とは同名でも全く別物
    float L; 
    L = 2.0 * pi * r;
    global_val = 100;   //  グローバル変数に代入.この変更はコード全体に影響する.
}

int main(void)
{
    ...
}

引数の渡しかた

関数に引数を渡す方には,3種類ある(すべて重要).

1.値渡し(call by value)
通常の渡し方.呼ばれた関数の仮引数に,実引数の値がコピーされて渡される.
従って,仮引数をどれだけ変更しても,実引数(元の変数)には影響を与えない

2.アドレス渡し (call by address)
値のコピーではなく,値のある場所(アドレス)を渡す方法である.
仮引数の値を変更すると,呼び出し元の実引数の値も変更される.
つまり,引数として渡された変数の値を参照・変更できる.
return による戻り値では,1つしか値を返せない)

3.参照渡し(call by reference)
C++ の拡張機能である.仮引数は,呼び出し側の実引数の「参照」が渡される.
アドレス渡しと同じく関数中で仮引数の値を変更すると,呼び出し元の実引数の値も変更される.
(ソースコードの見た目は「値渡し」に見えるが,本質は「アドレス渡し」である.*&を書かなくても済む.)

値渡し アドレス渡し(ポインタ変数) 参照渡し,C++のみ
#include <stdio.h>

void clear(int x)
{
    // 仮引数を変更
    x = 0;
}

int main(void)
{
    int a = 100;

    printf("a=%d\n",a);

    clear(a);

    printf("a=%d\n",a);

    return 0;
}
#include <stdio.h>

void clear(int *px)   // アドレス渡し
{
    // ポインタ変数に*をつける
    *px = 0;
}

int main(void)
{
    int a = 100;

    printf("a=%d\n",a);

    clear(&a); // アドレスを渡す

    printf("a=%d\n",a);

    return 0;
}
#include <stdio.h>

void clear(int& x) // 参照渡し
{
    // x は,main()中の a の「参照」
    x = 0;
}

int main(void)
{
    int a = 100;

    printf("a=%d\n",a);

    clear(a);  // 値渡しと同じ?

    printf("a=%d\n",a);

    return 0;
}

ポインタ渡しの例と参照渡しの例に,同じアンパサンド(アンド)記号 & が使われているが,異なる用法なので注意.
(同様に,アスタリスク * 記号も掛け算と同じ記号であるが,まったく意味が異なる.さらに細かく言えば,アドレス渡しの例内の2つの * も,異なる役割である.)
ポインタ変数の使い方については,ポインタの回で詳しく説明する.

練習問題

上記のプログラムをそれぞれ実行して,結果を比較しよう.
特に,値渡しと参照渡しでは,main 関数は全く同じであるが,結果が異なるので注意!

再帰関数

C言語では関数の中で自分を呼び出すことができる.
参考書などでは定番の書法であるが,実用的な使い道はさほど多くない.
例えば,以下は階乗の計算の例である.

#include <stdio.h>

int kaijo(int x)
{
    int n = 0;

    if(x < 0)
        n = 0;           // エラー処理
        
    else if(x == 0)
        n = 1;           // 0! = 1

    else
        n = x * kaijo(x-1); // 再帰呼び出し

    return n;
}


int main(void)
{
    for(int i=0; i<10; i++)
        printf("%d! = %d\n", i, kaijo(i));
    return 0;
}

階乗の計算は,例えば,
5! = 5*4*3*2*1
である.これは,
5! = 5 * 4!
に等しい.
一般項で考えると,
x! = x * (x-1)!
となる.
つまり,計算過程に,自分自身を含むような処理の場合に再帰関数を用いることができる.

関数の再帰呼び出しにおいては,条件分岐に再帰呼び出しではない処理(この場合は x==0return が 1)があり,ここに至って初めて値が定まり, 次々にリターンして(戻って)くる.
このように,再帰関数では処理の中で,必ず再帰呼び出しを終わらせるロジックを慎重に記述しなければならない.

練習問題

  1. 上記のプログラムをそれぞれ実行して,結果を確認しよう.
  2. 大きな整数(20〜30程度以上)の階乗計算にトライしてみよう.何か問題が起こる?
    次に,どのようにしたら解決できるだろうか? 方針を立てるために,デバッグ技法を活用してみよう.

まとめ

Q and A

Q and A
そもそも関数って必要ですか?
全部 main() に書いてはいけないのですか?
単発の小さな使い捨てプログラムであればmainだけで良い,という考え方もわからなくはないですが,やはり処理の過程,つながりや階層を順序だてて記述,理解するには関数が必須です.(構造化プログラミング)
また,ソースコードを資産として考えた時,関数単位でまとめておかないと,どのソースコードがデバック・検証済か,逆に作りかけでエラーが含まれているか,の区別がわからなくなるので,やはり関数は必須です.
仮引数と実引数の変数名は同じでもよいですか?
構いません.異なる関数内のローカル変数は,同名でも全く別の変数として扱われます.
main()関数の戻り値はvoidですか?
いいえ,必ずintです.ただ,コンパイラによっては void でもエラーを出さないものがあります.
main()関数の引数は void ですか?
voidでもOKですが,正式にはint main(int argc, char* argv[])と書きます.
関数名のつけ方はどうしたらよいですか?
処理内容を端的に表す命名が良いです. 書き方のスタイルは,単語開始文字を大文字で書く方法 doSomething() や, アンダーバーで区切るcalc_some_value()などがあります.
値渡し,アドレス渡し,参照渡しの使い分けはどう考えればよいですか?
渡した先の関数で値を変更させたい場合はアドレス渡し or 参照渡しが必須です.それ以外は値渡しです.
(原則として,配列はアドレス渡しです.巨大な配列を値渡しすると,処理効率が悪いからです.また,構造体は値渡しになります.)
グローバル変数はなぜ使わないほうが良いのでしょうか
グローバル変数はどの関数からでも変更できてしまうので,ソースコードのどこで値が変化するかが管理しにくいからです.
逆に,プログラム全体に影響するような値(動作モードなど)や,物理定数(円周率やネイピア数)の保持には最適ですが,値が不用意に変更されないように,constをつけておくと良いです.
プロトタイプ宣言の必要性が分かりません
複数のソースファイルを含むような中規模以上のプロジェクトや,大人数で共同作業するプログラム開発では必須です.
どのような機能の関数を,どのように組み合わせてソフトウエアを構築するかは,ソフトウエアの設計として,とても重要です.
プログラムの全体設計を先に済ませ,必要となる部品の仕様(=関数のプロトタイプ)を先に決めて,そのあと関数本体のコーディング・デバッグを書くような工程もあります.
また,市販のライブラリ関数などを使用する場合も,ソースコードは提供されないのが一般的ですので,ヘッダファイルのインクルードやライブラリの結合などには必要な技法となります.

練習問題(授業時間内に実習)

以下の問それぞれに対応するプログラムを作成しなさい.

  1. 実数 r の n 乗(= rn ,n は正の整数)を求める関数my_power()を作成し,動作をチェックするプログラムを作成せよ.
    (math.hのライブラリ関数に pow() 関数があるが,これを使用しないで自作せよ)
    r, n はキーボードから入力させる.

    実行例1:
    r=? 1.1
    n=? 2
    ans = 1.21000
    
    実行例2:
    r=? 2
    n=? 10
    ans = 1024.00000
    #include ...
    
    ??? my_power(...)
    {
        // 関数内では,画面表示はしないこと.計算結果を戻り値として返す.
        return ???;
    }
    
    int main(void)
    {
        float r;
        int n;
    
        printf(...);
        scanf(...);
    
        // ここで関数を呼び出す    
        printf("ans = ???", ???);
    
        return 0;
    }
    

  2. 2つの整数を引数にとり,この2つの値を交換する関数,swap を2種類(値渡しと参照渡し)作成してみよう.
    なお,作成した swap 関数が正しく動作することを確認するため,main 関数は以下の通りとすること.
    ヒント:値渡しでは関数に値のコピーが渡されるため,うまくswap(入替え)ができない.参照渡しならOK.

    #include <stdio.h>
    
    void swap_val(...)    // 値渡しバージョン
    {
        ...
    }
    
    void swap_ref(...)    // 参照渡しバージョン
    {
        ...
    }
    
    int main(void)
    {
        //  main 関数は変更しないこと
        int x = 1, y = 3;
        printf("Before swap: x = %d, y = %d \n", x, y);
    
        swap_val(x, y);
        printf("After swap_val() (by value)    : x = %d, y = %d \n", x, y);
    
        swap_ref(x, y);
        printf("After swap_ref() (by reference): x = %d, y = %d \n", x, y);
    
        return 0;
    }
    
  3. 戻り値としてboolを返す関数もよく使われる.
    引数などの値などが,ある条件に当てはまっているかどうか=「~であるか否か」を判定して,bool 値を返す関数には Is????(), is_????のように,is(be動詞)から始まる名前をつけることが多い.
    以下の例を参考に,素数の判定を行う関数と,偶数・奇数の判定を行う関数を作成してみよう.

    #include <stdio.h>
    
    bool IsPrime(int n)
    // 素数の判定
    {
        ...
        return ????    // true or false
    }
    
    
    bool IsEven(int n)
    // 偶数の判定
    {
        ...
        return ????    // true or false
    }
    
    		
    int main(void)
    {
        int n;
        printf("n=?");
        scanf("%d", &n);
        
        // 奇数・偶数の判定
        if( IsEven(n) ) {   // IsEven(n) == true  と書いても良い.
            printf("even.\n");
        } else {
            printf("odd.\n");
        }
    
        // 素数の判定
        if( IsPrime(n) ) {   // IsPrime(n) == true  と書いても良い.
            printf("prime number.\n");
        } else {
            printf("non-prime number.\n");
        }
        return 0;
    }
    
  4. 乱数関数 rand() を使って,整数の乱数を大量に発生させ,その数が偶数である確率を求めよ.
    同様に,その数が素数である確率を推定せよ.

    ヒント
    試行回数を整数N = 10000; またはそれ以上として,繰り返し乱数を発生させ,その値が偶数 or 素数であった回数を数え,最後にNで割る.
    RAND_MAXは,32000程度の数であるため,厳密ではないが,試行回数を増やすほど真値に漸近してゆくと考える.