関数(1)

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

理解度チェック

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

  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 など,広く用いられている他の言語においてもサブルーチン,プロシージャ,副プログラムと呼ばれる同様の機能がある)

プログラムに限らず,いろいろな場面で必要となる処理や,コード内で共通して用いられる動作(画面出力やキーボード入力),何度も繰り返される処理などは,あらかじめ作成しておき,必要に応じて使えるようにしておくと便利であり,作業効率も良い.
機械加工で例えれば,物を製作する際にネジやバネ自体の製作からはじめることはせず,既存の規格品を利用するのが普通である. その場合は,ネジの直径やピッチなど規格を調べて,それに合うように設計する.

C言語の具体例として,これまで使用してきたprintf(), scanf() は,実は関数であるが,自分でこれらを作成する必要はなかった. なぜならば,画面表示やキーボード入力などは基本的な動作であるため,自作せずとも初めから用意(=コーディングおよびデバッグ済)されている.
このような関数をライブラリ関数と言う.

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

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

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

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

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

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

プログラミングを始めて一番最初に学んだ main() も,関数の一つである. したがって,C言語のプログラムは「関数の集合である」といえる.

main()関数は,OSで実行ファイルを指定したときに,「この関数から処理が開始される」入口に相当する特別な関数(エントリポイント)として規定されている.

関数のつくりかた

まず,いくつかの用語を説明するが,プログラムの具体例を見ると理解しやすい.
ソースコード中に書かれる関数の処理本体を定義 (definition)と呼ぶ.

関数定義の文法

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

    return xxx;    // xxxが戻り値
}

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

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

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

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

まず,以下のような簡単なプログラムを考える.
それぞれ順を追って,プログラムを作成・コンパイルして,動作させてみよう.

#include <stdio.h>

int main(void)
{
    printf("bow-wow!\n");   // 犬の鳴き声の onomatopée
    return 0;
}

よく吠える犬では,以下のようになる.

#include <stdio.h>

int main(void)
{
    printf("bow-wow!\n");
    printf("bow-wow!\n");
    printf("bow-wow!\n");
    printf("bow-wow!\n");
    printf("bow-wow!\n");
    return 0;
}

同じ処理が並んでいるので,この部分(吠える動作)を,関数barkを作成してみよう.

#include <stdio.h>

// 新たに作成した自作関数の「定義」
void bark(void)
{
    printf("bow-wow!\n");
    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;
}

吠える回数をキーボードから入力

#include <stdio.h>

// 引数をとる関数
void bark(int n)
{
    int i;    // ローカル変数
    for(i=0; i<n; i++) {
        printf("bow-wow!\n");
    }
    return;
}

int main(void)
{
    int n;    // ローカル変数
    printf("n=? ");
    scanf("%d", &n);

    bark(n);          //  関数の呼び出し(call) with 引数
    return 0;
}

実行例
n=? 100     <- 苦情レベルの,とてもよく吠える犬.
bow-wow!
・
・
・
bow-wow!

この例のように,異なる関数内では,変数が別の空間に存在するため相互参照できない.(この例では,ローカル変数のin
そのため,bark関数内の処理・計算などで必要な数値はmainからbarkに引数として渡し,その結果は戻り値としてmainに返す必要がある.
野球で言えば,キャッチボールのようなイメージとなる.

プログラム例:戻り値

関数の戻り値は「あり」,「なし」のどちらかである.
先ほどの関数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()において,級数を用いてなるべく正確な円周率を計算してみよう.

ヒント:円周率の計算を行う級数の公式は,数多く提案されている.以下のサイトおよび,理解できる範囲で収束の速い他の公式を用いても良い.
無限には計算できないので,適当なところ(小数点以下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() と,受け取る側 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つの関数に多くの処理を詰め込むことはよくない.

例えば,1つの関数に「データの入力」と「計算処理」と「出力」を詰め込むのは×.
この場合は,それぞれの機能を3つの関数に分割するべきである.
Simple is the best!

関数名については,一般に,関数には何らかの手続きを記述するので,その名前には動詞,または動詞+目的語を組み合わせることが多い.
例えば,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;
}

練習問題

上記プログラムを実行して,処理の流れを確認しよう.
特に,関数を呼び出す・呼び出される側において

  1. 関数呼び出し時の引数の渡し方
  2. 関数での引数の受け取り方
  3. 戻り値の返し方
  4. 返された戻り値の受け取り方

に注目. 引数の個数・型や,戻り値の有無は,双方で整合していなければならない.

まとめ

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()などがあります.

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

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

  1. まずは小手調べ・・・ scanf() 関数を使って,変数aにキーボードから整数を入力させ,a を関数 func() に引数として渡し,関数 func() 内でその3乗を計算して,返すプログラムを作成せよ.

    ヒント:

    #include ...
    
    ??? func(...)
    {
        // 関数内では,画面表示はしないこと
    	return ???;
    }
    
    int main(void)
    {
        int a;       // 整数型の変数 a
    
        printf("a = ");
        scanf(...);
    
        printf("a*a*a = ??? \n", func(a) );    // ここで関数を呼び出すこと
    	
        return 0;
    }
    

  2. 問題1の変数 a を,「整数型」から「実数型」に変更せよ.
    たとえば,0.5を与えると,0.125を画面に表示するようにせよ.
    ヒント:変数 a の定義を int から float に変えるだけでは足りない.影響する箇所を順次,変更する必要がある.


  3. キーボードから正の整数 n を入力すると,n の約数のうち,(n 以外の)最大の値を計算し,画面に出力するプログラムを作成せよ.
    この際,最大の約数を計算する部分は関数とすること.
    ヒント:整数を引数,計算結果を戻り値とする関数
    関数名は,max_yakusu() とする.(この関数内では計算のみ行い,画面表示は行わないこと.)

    実行例:
    n=? 10
    5
    
    n=? 9
    3
    
    n=? 7
    1       
    #include ...
    
    ??? max_yakusu(...)
    {
        // 関数内では,画面表示はしないこと.計算結果を戻り値として返す.
        return ???;
    }
    
    int main(void)
    {
        int n;
    
        printf(...);
        scanf(...);
    
        // ここで関数を呼び出す    
        printf(???);
    
        return 0;
    }
    

    ヒント:最大の約数の求め方は,元の数 n を,それより小さい数 n-1, n-2, ... , 2, 1 で順に割って,最初に割り切れた数です.

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

    実行例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;
    }