コンテンツにスキップ

第6章 関数

前章までは、画面に表示を行うときには printf 関数、puts 関数、putchar 関数を利用して、キーボードからの読込みを行うときには scanf 関数を利用してきた。

すなわち、入出力の処理を行うために、各関数に対して、「お願いしました!」と頼むばかりである。

しかし、このような "人まかせ" だけでは、作成できるプログラムには限界が生じる。

本章では、関数を作る方法や使う方法などを学習する。

6-1 関数とは

プログラムは、多くの部品の組合せで構成される。プログラムの部品の単位の一つが、本章で学習する関数である。

main 関数とライブラリ関数

これまで学習してきたすべてのプログラムは、Fig.6-1 に示す形式である。この中の赤色の部分は、main 関数(main function)と呼ばれる。

main 関数は1個だけ必要であり、プログラムの実行時には、その本体部が実行される。

前章までは、main 関数の中で、printf 関数、puts 関数、scanf 関数といった関数を利用してきた。

C言語によって標準で提供される、これらの関数は、ライブラリ関数(library function)と呼ばれる。

Fig.6-1 main関数 Fig6-1

関数とは

関数(function)は、自分でも作成できる。というよりも、どんどん関数を作っていかなければならない。まずは、次の関数の作成にチャレンジしよう。

二つの整数を受け取って、大きいほうの値を求めて返す関数。

この関数のイメージを、回路図の図で表したのが、Fig.6-2 である。

さて、printf 関数などのライブラリ関数は、中身を知らなくても、使い方さえかかれば、容易に使いこなせる "魔法の回路" のような存在である。

魔法の回路ともいえる関数を使いこなすには、提供する側と、使う側の両方の立場にたった、右記の二つの学習が必要である。

  • 関数の作り方 ... 関数定義
  • 関数の使い方 ... 関数呼出し

Info

function には、「機能」「作用」「働き」「仕事」「効用」「職務」「役目」などの意味がある。

Fig.6-2 二つの値の大きいほうの値を求める関数のイメージ Fig6-2

関数定義

まずは、関数の作り方を学習する。ここで考えている関数を、max2という名前で宣言するのが、Fig.6-3 である。

int max2(int a, int b)
{
    if (a > b)
        return a;
    else
        return b;
}

Fig.6-3 関数定義の構造 Fig6-3

この宣言は関数定義(function definition)と呼ばれ、多くのパーツで構成されている。

  • 関数頭部(function header) 関数の名前を含む仕様を記述する部分である。関数頭部という名前であるが、関数の "顔" と表現したほうが適切かもしれない。

①返却値型(return type) 関数が戻す値である返却値(return value)の型である。関数 max2 の場合、求めて返却するのが二つの int 型の大きいほうの値ですから、その型である int となっている。

②関数名(function name) 関数の名前である。この名前をもとに、他の部品から呼び出される。

③仮引数型並び(parameter type list) ()の中は、補助的な指示を受け取るための変数である仮引数(parameter)の宣言である。通常の変数の宣言と同様に、型と変数名(仮引数名)を宣言する。なお、本関数のように、複数の仮引数を受け取る場合は、各仮引数の宣言をコンマ , で区切って並べる。

!!! note 関数 max2 では、a と b のいずれもが int 型の仮引数として宣言されている。

  • 関数本体(function body) 関数の本体は、呼び出された際に実行する処理を記述した複合文である。関数の中でのみ利用する変数があれば、この複合文の中で宣言・利用するのが原則である(ただし、関数 max2 にはありません)。

なお、仮引数と同一名の変数は宣言できません(名前が衝突するからです)。

関数呼出し

関数の作り方=関数定義の概要が分かりました。次は、使い方=関数呼出しです。関数 max2 を定義して利用する List 6-1 のプログラムで理解していきましょう。

List 6-1

// 二つの整数の大きいほうの値を求める
#include <stdio.h>

//--- 大きいほうの値を返す ---//
int max2(int a, int b)
{
    if (a > b)
        return a;
    else
        return b;
}

int main(void)
{
    int n1, n2;

    puts("二つの整数を入力せよ。");
    printf("整数1:");   scanf("%d", &n1);
    printf("整数2:");   scanf("%d", &n2);

    printf("大きいほうの値は%dです。\n", max2(n1, n2));

    return 0;
}

実行例

二つの整数を入力せよ。
整数1:45□
整数2:83□
大きいほうの値は83です。

二つの整数を入力せよ。
整数1:37□
整数2:21□
大きいほうの値は37です。

二つの関数(関数 max2 と main 関数)が定義されている。プログラムが起動された際に実行されるのは main 関数である。

Note

main 関数より先頭側で定義されている max2 関数のほうが先に実行されることはありません。

関数を使う際に "関数を呼び出す" ことは、第1章で学習した(p.6)。関数 max2 を呼び出しているのが、プログラムの水色の式(右ページの Fig.6-4 の)部分である。

この式 max2(n1, n2) は、次の依頼と考えるとよいでしょう。

関数 max2 さん、int 型の整数値 n1 と n2 を渡しますので、それらの大きいほうの値を教えてください!

関数呼出しの際に、関数名の後ろに置く()は、関数呼出し演算子(function call operator)である。そのため、この式は、関数呼出し式(function call expression)となる。

Note

○○演算子を使う式は、○○式と呼ぶのでした(p.29)。

なお、関数呼出し演算子()の中に、補助的な指示である実引数(argument)を与えることや、実引数が2個以上ある場合に、コンマ , で区切ることは、第1章で学習済みです。

Fig.6-4 関数呼出しと値の返却 Fig6-4

関数呼出しが行われると、プログラムの流れは、その関数へと一気に移ります。具体的には、main 関数の実行が一時的に中断されて、関数 max2 の実行が開始されます。

その際、仮引数用の変数が生成された上で、実引数の値が代入されます。実行例①の場合、仮引数 a と b が作られて、実引数 n1 の値 45 と、n2 の値 83 が代入されます。

重要

関数呼出しが行われると、プログラムの流れは呼び出された関数に移る。その際、呼出し側が与えた実引数の値が、関数が受け取る仮引数に代入される。

仮引数への値の代入が終わると、関数本体の複合文が実行されます。

関数本体の実行中に、プログラムの流れが return 文(return statement)に出会うか、関数本体の末尾の } に到達すると、関数から抜け出して、呼び出した場所に戻ります。

すなわち、プログラムの流れは呼出し元に戻って、中断されていた main 関数の実行が再開されます。戻る際の《手みやげ》が、return の後ろに置かれた式の値(図の例では、式 b の値 83)です。

その返却値は、関数呼出し式の評価で得られる仕組みです。図の場合、で囲んだ関数呼出し式 max2(n1, n2) を評価した値が『int 型の 83』となります。

重要

return 文は、関数の実行を終了させて、プログラムの流れを呼出し元に戻すとともに値を返却する。その返却値は、関数呼出し式の評価によって得られる。

その結果、関数 max2 の返却値 83 が printf 関数に渡されて、その値が表示されます。

関数呼出し演算子の概要をまとめたのが、Table 6-1 です。

Table 6-1 関数呼出し演算子

関数呼出し演算子 x(arg) 関数 x に実引数 arg を渡して呼び出す(arg は 0 個以上の実引数をコンマで区切ったもの)。(返却値型が void でなければ)関数 x が返却した値を生成する。

Note

返却値型の void は、p.152 で学習します。

さて、呼出し側の実引数は、変数ではなく定数でも構いません。たとえば、次の関数呼出しは、変数 n1 と 5 の大きいほうの値を求めて返却します。

max2(n1, 5)    // n1と5の大きいほうの値を求める

return文と返却値

前ページで学習した return 文の構文図を Fig.6-5 に示しています。関数が返却するのは、return に続く式の値です。

構文図が示すように、return の後ろに置く式は0個か1個です。関数は2個以上の値を返却できないことが分かります。

Fig.6-5 return文の構文図 Fig6-5

単純な関数 max2 ですが、いろいろな実現法が考えられます。その一例が、Fig.6-6 です。

Note

a と b では、大きいほうの値を格納するための変数 max を使っています。関数の中でのみ利用する変数は、その関数中で宣言するのが原則です(p.143)。ただし、その変数の名前を、仮引数(この例では a と b)と同じにすることはできません。

Fig.6-6 関数max2の実現例 Fig6-6

さて、これら三つの関数が List 6-1 と異なるのは、return 文が1個だけという点です。関数の入口は一つです。出口がたくさんあると、プログラムの構造が把握しづらくなります。なるべく return 文を1個にして、出口を一本化したほうが好ましいと考えられます。

3値の最大値を求める関数

今度は、三つの整数の最大値を求める関数を作りましょう。その関数 max3 と、それを呼び出す main 関数とで構成されるプログラムを List 6-2 に示します。

List 6-2

// 三つの整数の最大値を求める
#include <stdio.h>

//--- 三つの整数の最大値を返す ---//
int max3(int a, int b, int c)
{
    int max = a;
    if (b > max) max = b;
    if (c > max) max = c;

    return max;
}

int main(void)
{
    int a, b, c;

    puts("三つの整数を入力せよ。");
    printf("整数a:");   scanf("%d", &a);
    printf("整数b:");   scanf("%d", &b);
    printf("整数c:");   scanf("%d", &c);

    printf("最大値は%dです。\n", max3(a, b, c));

    return 0;
}

実行例

三つの整数を入力せよ。
整数a:5□
整数b:3□
整数c:4□
最大値は5です。

Fig.6-7 二つの関数と変数 Fig6-7

Fig.6-7 に示すように、関数が受け取る仮引数や、関数内で定義される変数は、それぞれの関数に独自のものである。関数 max3 の仮引数 a、b、c と、main 関数の変数 a、b、c は、たまたま名前が同一というだけであって、何の関係もありません。

『実引数と仮引数の変数名が同じでも大丈夫だろうか。』といった心配も無用です。

Note

関数 max3 を呼び出す際に、main 関数の a、b、c の値が、それぞれ関数 max3 の仮引数 a、b、c に渡されて代入されます。

Example

二つの int 型整数の小さいほうの値を返す関数を作成せよ。

int min2(int a, int b);
動作確認のための main 関数などを含むプログラムを作ること(以降の演習でも同様である)。

Example

三つの int 型整数の最小値を返す関数を作成せよ。

int min3(int a, int b, int c);
※ ; の意味は、p.157 で学習します。

関数の返却値を引数として関数に渡す

次は、プログラム中に(main 関数とは別に)関数を2個作成しましょう。List 6-3 に示すのは、二つの整数を読み込んで、その2乗値の差を求めて表示するプログラムです。

List 6-3

// 二つの整数の2乗値の差を求める
#include <stdio.h>

//--- nの2乗値を返す ---//
int sqr(int n)
{
    return n * n;
}

//--- aとbの差を返す ---//
int diff(int a, int b)
{
    return a > b ? a - b : b - a;  // 大きいほうから小さいほうを引く
}

int main(void)
{
    int x, y;

    puts("二つの整数を入力せよ。");
    printf("整数x:");   scanf("%d", &x);
    printf("整数y:");   scanf("%d", &y);

    printf("xの2乗とyの2乗の差は%dです。\n", diff(sqr(x), sqr(y)));

    return 0;
}

実行例

二つの整数を入力せよ。
整数x:4□
整数y:5□
xの2乗とyの2乗の差は9です。

Fig.6-8 関数呼出し式の評価 Fig6-8

二つの関数は、次のことを行います。 - 関数 sqr:仮引数 n に受け取った値の2乗値を求めて返却する。 - 関数 diff:仮引数 a と b に受け取った値の差を求めて返却する。

プログラム赤色部の関数呼出し式に着目しましょう。呼び出している関数が diff で、与えている二つの実引数は sqr を呼び出す関数呼出し式です。

実行例の場合、Fig.6-8 に示すように、関数呼出し式 sqr(x) と sqr(y) を評価した値は、16 と 25 となります。その二つの値 16 と 25 が、そのまま関数 diff を呼び出す際の実引数として渡されますので、関数呼出し式 diff(sqr(x), sqr(y)) は、diff(16, 25) となります。

この関数呼出し式を評価すると、関数 diff が返却する 9 が得られます。

main 関数では、その返却値をそのまま printf 関数に渡して表示を行っています。

Example

int 型整数の3乗値を返す関数を作成せよ。

int cube(int x);

自作の関数を呼び出す関数

これまで、main 関数の中で、ライブラリ関数や自作の関数を呼び出してきました。次は、自作の関数の中で、別の自作関数を呼び出すことにします。List 6-4 が、そのプログラム例です。

// 四つの整数の最大値を求める
#include <stdio.h>

//--- 大きいほうの値を返す ---//
int max2(int a, int b)
{
    return a > b ? a : b;
}

//--- 四つの整数の最大値を返す ---//
int max4(int a, int b, int c, int d)
{
    return max2(max2(a, b), max2(c, d));
}

int main(void)
{
    int n1, n2, n3, n4;

    puts("四つの整数を入力せよ。");
    printf("整数n1:");   scanf("%d", &n1);
    printf("整数n2:");   scanf("%d", &n2);
    printf("整数n3:");   scanf("%d", &n3);
    printf("整数n4:");   scanf("%d", &n4);

    printf("最も大きい値は%dです。\n", max4(n1, n2, n3, n4));

    return 0;
}

実行例

四つの整数を入力せよ。
整数n1:5□
整数n2:3□
整数n3:8□
整数n4:4□
最も大きい値は8です。

List 6-4

関数 max4 の赤色部では、関数 max2 を使って、次のように4値の最大値を求めています。

『a と b の大きいほうの値』と『c と d の大きいほうの値』の大きいほうの値

関数は、プログラムの《部品》です。もし部品を作るときに、それを実現するのに便利な部品があるのならば、どんどん使っていきましょう。

重要

関数はプログラムの部品である。部品を作るのに便利な部品があれば、それを積極的に利用する。

演習 6-4

int 型整数の4乗値を返す関数を作成せよ。

int pow4(int x);
関数の内部で、List 6-3 の関数 sqr を呼び出すこと。

値渡し

次は、べき乗を求める関数を作成します。n が整数であれば、x の n 乗は、x を n 回掛け合わせることで求められます。この考えに基づいて作ったのが、List 6-5 のプログラムです。

List 6-5

// べき乗を求める
#include <stdio.h>

//--- xのn乗を返す ---//
double power(double x, int n)
{
    double tmp = 1.0;

    for (int i = 1; i <= n; i++)
        tmp *= x;    // tmpにxを掛ける
    return tmp;
}

int main(void)
{
    double a;
    int b;

    printf("aのb乗を求めます。\n");
    printf("実数a:");   scanf("%lf", &a);
    printf("整数b:");   scanf("%d", &b);

    printf("%.2fの%d乗は%.2fです。\n", a, b, power(a, b));

    return 0;
}

実行例

aのb乗を求めます。
実数a:4.6□
整数b:3□
4.60の3乗は97.34です。

関数呼出しの際の、引数の受渡しについて考えていきます。右ページの Fig.6-9 に示すように、仮引数 x に実引数 a の値が代入され、仮引数 n に実引数 b の値が代入されます。

引数として《値》がやりとりされるメカニズムは、値渡し(pass by value)と呼ばれます。

さて、x の値の掛け合わせは、n の値を5、4、...、1 とカウントダウンしていくことでも行えます。そのように書き換えた関数 power が、List 6-6 です。

ループカウンタ用の変数 i が除去された結果、関数はコンパクトになっています。

ただし、n の値は、デクリメントされていく結果として、関数の終了時には -1 となります。そうすると、

仮引数 n の値を変更すると、実引数 b の値まで変更されてしまうのではないか?

と感じられるかもしれませんが、心配無用です。

Fig.6-9 関数呼出しにおける引数の値渡し Fig6-9

やりとりされるのは単なる値ですから、仮引数 n は実引数 b のコピーにすぎません。本のコピーをとって、そのコピーに赤鉛筆で何かを書き込んでも、もとの本には、何の影響もないのと同じ理屈です。

重要

関数間の引数の受渡しは、値渡しによって行われる。そのため、関数本体の中で仮引数の値を変更しても、実引数の値に影響が及ぶことはない。

関数 power から main 関数に戻った後の実引数 b の値は 3 のままです(-1 にはなりません)。

さて、関数 power の第1引数は double 型です。この引数に対して、次のように int 型の値を渡しても、べき乗は、正しく求められます("chap06/List0606a.c")。

power(5, 3);    // 5の3乗が求められる

実引数と仮引数の型が一致しないときには、暗黙の型変換が行われるのです。

重要

仮引数とは異なる型の実引数を渡すと、必要に応じて暗黙の型変換が行われる。

Note

すなわち、第1引数の 5 が、double 型の 5.0 へと格上げされた上で関数に渡されます。ただし、次のように、暗黙の型変換を行えない場合は、エラーとなります。

power("*", 5);   // エラー:文字列を数値に暗黙に変換することはできない

Example

1 から n までの全整数の和を求めて返却する関数を作成せよ。

int sumup(int n);

6-2 関数の設計

前節では、関数の定義と呼出しの基礎を学習しました。本節では、より本格的な関数の作り方などを学習していきます。

値を返さない関数

第4章では、記号文字を並べて三角形を表示するプログラムを作りました(p.105)。任意の個数だけ * を連続表示する関数を定義して、それを呼び出すことで『左下側が直角の直角二等辺三角形』を表示するのが、List 6-7 のプログラムです。

List 6-7

// 左下直角の直角二等辺三角形を表示(関数版)
#include <stdio.h>

//--- 記号文字'*'をn回連続して表示 ---//
void put_stars(int n)
{
    while (n-- > 0)
        putchar('*');
}

int main(void)
{
    int len;

    printf("左下直角二等辺三角形を作ります。\n");
    printf("短辺:");
    scanf("%d", &len);

    for (int i = 1; i <= len; i++) {
        put_stars(i);
        putchar('\n');
    }

    return 0;
}

実行例

左下直角二等辺三角形を作ります。
短辺:5□
*
**
***
****
*****

関数 put_stars は表示を行うだけであって、返却するものがありません。このような関数の返却値型は、void とします(void は、『空の』という意味です)。

重要

値を返却しない関数の返却値型は、void とする。

関数の汎用性

関数 put_stars を導入したおかげで、三角形表示の2重ループが1重ループに変更され、プログラムの見通しがよくなりました。それでは、右下側が直角の直角二等辺三角形を表示するプログラムを作りましょう。右ページの List 6-8 に示すのが、そのプログラムです。

List 6-8

// 右下直角の直角二等辺三角形を表示(関数版)
#include <stdio.h>

//--- 文字chをn回連続して表示 ---//
void put_chars(int ch, int n)
{
    while (n-- > 0)
        putchar(ch);
}

int main(void)
{
    int len;

    printf("右下直角二等辺三角形を作ります。\n");
    printf("短辺:");
    scanf("%d", &len);

    for (int i = 1; i <= len; i++) {
        put_chars(' ', len - i);
        put_chars('*', i);
        putchar('\n');
    }

    return 0;
}

実行例

右下直角二等辺三角形を作ります。
短辺:5□
    *
   **
  ***
 ****
*****

今回は、空白文字' 'の連続表示と、記号文字'*'の連続表示が必要です。その役目を担う関数 put_chars は、仮引数 ch に与えられた文字を、n 個連続して表示する関数です。

Note

文字定数が int 型であることは、第4章の p.86 で学習しました。関数間でやりとりする文字の引数も、char 型ではなく int 型とします。

main 関数の for 文では、関数 put_chars を次のように呼び出しています。

  • put_chars(' ', len - i); len - i 個の' 'の表示を依頼
  • put_chars('', i); i 個の''の表示を依頼

空白文字' 'の連続表示と、'*'の連続表示の両方が put_chars にゆだねられています。

前のプログラムの関数 put_stars は、表示できるのが'*'に限られていましたが、本プログラムの関数 put_chars は、任意の文字が表示できるという点で、汎用性が高い(使い道が広い)ものとなっています。

重要

関数はなるべく汎用性の高いものとしよう。

Example

警報を n 回連続して発する関数を作成せよ。

void alert(int n);

引数を受け取らない関数

List 6-9 を考えましょう。正の整数値を読み込んで、逆順に表示するプログラムです。

Note

本プログラムは、List 4-10(p.90)をベースにして書き換えたものです。

List 6-9

// 読み込んだ正の整数値を逆順に表示
#include <stdio.h>

//--- 正の整数を読み込んで返す ---//
int scan_pint(void)
{
    int tmp;

    do {
        printf("正の整数を入力せよ:");
        scanf("%d", &tmp);
        if (tmp <= 0)
            puts("\a正でない数を入力しないでください。");
    } while (tmp <= 0);
    return tmp;
}

//--- 非負の整数を反転した値を返す ---//
int rev_int(int num)
{
    int tmp = 0;

    if (num > 0) {
        do {
            tmp = tmp * 10 + num % 10;
            num /= 10;
        } while (num > 0);
    }
    return tmp;
}

int main(void)
{
    int nx = scan_pint();

    printf("反転した値は%dです。\n", rev_int(nx));

    return 0;
}

実行例

正の整数を入力せよ:-5□
正でない数を入力しないでください。
正の整数を入力せよ:128□
反転した値は821です。

関数 scan_pint は、正の整数値を読み込んで、その値を返す関数です。この関数は、受け取る仮引数がないため、()の中が void と宣言されています。

重要

引数を受け取らない関数は、仮引数型並びを void と宣言する。

呼出し側でも、関数呼出し演算子()の中を空にします(与える実引数がないからです)。

Note

C言語プログラムの『決まり文句』の一部である

int main(void)
が、main 関数が引数を受け取らないことの宣言であることが分かりました。

関数の返却値での初期化

main 関数冒頭の変数 nx の宣言に着目します。関数呼出し式 scan_pint() が、初期化子として与えられています。そのため、関数の返却値(関数 scan_pint の実行時にキーボードから読み込んだ非負の整数値)で、変数 nx が初期化されることが分かります。

Note

このように、プログラムの実行時に初期値が決まるタイプの初期化は、p.174 で学習する自動記憶域期間をもつオブジェクトに限られます。

ブロック有効範囲

関数 scan_pint と関数 rev_int の両方に、同じ識別子(名前)の変数 tmp がありますが、それぞれの関数に独自のものです(p.147)。

Fig.6-10 に示すように、関数 scan_pint 中の変数 tmp は関数 scan_pint に特有のものであり、関数 rev_int 中の変数 tmp は関数 rev_int に特有のものです。

Fig.6-10 関数内で宣言されたオブジェクト Fig6-10

変数や関数の識別子(名前)には、どこからどこまで通用するのかという範囲が決められています。その範囲を表すのが、有効範囲(scope)です。

ブロック(複合文)の中で宣言された変数の名前は、変数が宣言された場所から、その宣言を囲むブロック終端の}まで通用します(ブロックの外には通用しません)。

この有効範囲は、ブロック有効範囲(block scope)と呼ばれます。

重要

ブロック(複合文)の中で宣言された変数の名前には、宣言された場所から、その宣言を囲むブロック終端の}まで通用するブロック有効範囲が与えられる。

Example

画面に『こんにちは。』と表示する関数を作成せよ。

void hello(void);

ファイル有効範囲

有効範囲について、List 6-10 で学習を進めていきます。これは、5人の学生の点数を読み込んで、その最高点を求めて表示するプログラムです。

List 6-10

// 最高点を求める
#include <stdio.h>

#define NUMBER  5  // 学生の人数

int tensu[NUMBER];  // 配列の定義 ←①定義

int top(void);     // 関数topの関数原型宣言  ←A宣言

int main(void)
{
    extern int tensu[];     // 配列の宣言(省略可) ←②宣言

    printf("%d人の点数を入力せよ。\n", NUMBER);
    for (int i = 0; i < NUMBER; i++) {
        printf("%d : ", i + 1);
        scanf("%d", &tensu[i]);
    }
    printf("最高点=%d\n", top());

    return 0;
}

//--- 配列tensuの最大値を返す関数topの関数定義 ---//
int top(void)
{
    extern int tensu[];     // 配列の宣言(省略可) ←③宣言
    int max = tensu[0];

    for (int i = 1; i < NUMBER; i++)
        if (tensu[i] > max)
            max = tensu[i];
    return max;
}

実行例

5人の点数を入力せよ。
1:53□
2:49□
3:21□
4:91□
5:77□
最高点=91

点数用の配列 tensu が、main 関数と関数 top の外に位置する①で宣言されています。

このように、関数の外で宣言された識別子は、宣言された場所から、そのソースプログラムの終端まで名前が通用します。これが、ファイル有効範囲(file scope)です。

Note

関数 top の中で宣言されている変数 max には、宣言された場所から、その宣言を囲むブロック終端の}まで通用するブロック有効範囲(前ページ)が与えられます。

宣言と定義

さて、①で宣言された配列 tensu は、②と③でも宣言されています。先頭の①は、要素型が int 型で、要素数が NUMBER の配列 tensu を作り出す宣言です。

実体を作り出すための宣言は、次のニュアンスです。

定義(definition)でもある宣言 実体を作り出すための宣言 ①

一方、extern 付きの②と③は、『どこか別の箇所で作られている tensu を使います。』といった、次の宣言です。

定義ではない、単なる宣言 実体を使うための宣言 ②と③

Note

配列 tensu は、ファイル有効範囲が与えられているため、main 関数や関数 top の中では、わざわざ宣言しなくとも、ちゃんと利用できます。すなわち、②と③の宣言は、省略可能です。

関数原型宣言

私たち人間と同様に、コンパイラはプログラムを先頭から末尾へと読み進めます。そのため、関数 top を呼び出すコードCに出会ったときに、

関数 top は、引数を受け取らず、int 型の値を返す関数である。

という情報が(コンパイラにとっても、私たち人間にとっても)必要です。その情報を与えているのがAの宣言です。

この宣言は、関数の仕様ともいうべき、関数の返却値型/関数名/仮引数が記述されていることから、関数原型宣言(function prototype declaration)と呼ばれます。

Note

この宣言の末尾には、セミコロン ; が必要です。

関数の仕様に関する情報を、コンパイラやプログラムの読み手に与える関数原型宣言は、関数の実体を定義するわけではありません。すなわち、次のようになります。

B 関数 top の関数定義 ... 定義でもある宣言

A 関数 top の関数原型宣言 ... 定義ではない、単なる宣言

ちなみに、関数 top の仕様(返却値型や仮引数など)を変更する場合は、関数定義Bと関数原型宣言Aの両方を変更することになります。

さて、関数 top の関数定義B を、main 関数より前に配置しておけば、関数原型宣言Aは不要となります(コンパイラも私たちも、プログラムを先頭から読み進めるからです)。

関数の仕様を変更しても、その関数定義のみの変更ですみます。

一般的には、main 関数を最後に配置し、呼び出される側の関数を前側に配置したほうが、何かと都合がよいのです。

重要

呼び出される側の関数を前側に、呼び出す側の関数を後ろ側に配置しよう。

ヘッダとインクルード

関数を呼び出す際には、関数原型宣言が与える、関数の仕様というべき引数や返却値型などの情報が必要であることが分かりました。

それでは、printf 関数や scanf 関数などのライブラリ関数の関数原型宣言はどうなっているのでしょう。実は、これらの関数の関数原型宣言は、 の中に置かれています。

その情報を取り込むのが、C言語プログラムの決まり文句の一つである、

#include <stdio.h>     // ヘッダ<stdio.h>をインクルード

です。これは、#include 指令(#include directive)と呼ばれる特殊な指令です。

Note

前章で #define 指令を学習しました(p.124)。#define 指令や #include 指令などの、# で始まる指令は、通常の式や文とまったく異なります。

ライブラリ関数の関数原型宣言などが置かれたは、ヘッダ(header)と呼ばれ、それを #include 指令で取り込むことをインクルードするといいます。

Fig.6-11 に示すように、#include 指令の行が、そっくりそのままの内容と入れ替わる、というイメージです。

Note

ヘッダの実現方法などは、処理系によって異なります。個々のヘッダが、それぞれ単独のファイルで供給されるという保証もありません。『ヘッダファイル』ではなく、『ヘッダ』という用語で呼ばれるのは、そのためです。

Fig.6-11 ヘッダのインクルード Fig6-11

たとえば、putchar 関数の関数原型宣言は、 ヘッダ中で、次のように宣言されています。

int putchar(int __c);    // putchar関数の関数原型宣言の一例

Note

仮引数の名前は、処理系によって異なります。また、関数原型宣言では、仮引数の名前は省略できることになっているため、

int putchar(int);
と宣言されていることもあります。

なお、入出力をまったく行わないプログラムでは、#include は不要です。

関数の汎用性

プログラムに戻りましょう。関数 top の仕様を説明すると、次のようになります。

int 型配列 tensu の先頭 NUMBER 個の要素の最大値を求めて、その値を返却する。

プログラムの中身を知らない人が、この説明を聞いたら、次のような疑問を抱くことになるでしょう。

『配列 tensu って何?』

『NUMBER の値って、いくつなの?』

これらの疑問の答えは、関数 top 以外のコードを読むことによってしか得られません。すなわち、関数 top は、自身の関数以外の変数やマクロなどに依存している、すなわち、独立していないということです。

本プログラムで取り扱っているのは、単一科目の点数用配列 tensu です。しかし、英語の点数と、数学の点数の各々の最高点を求める必要性が将来的に生じるかもしれません。また、英語が選択科目で、数学が必修科目であって、それぞれの人数が異なる場合を考えなければならないこともあるでしょう。

そうすると、関数 top では、まったくの "お手上げ" となってしまいます。

重要

関数の外で定義された変数などの情報に依存する関数を作るべきではない。

任意の配列が取り扱える関数を作っていきましょう。

  • 任意の配列が取り扱える 配列 tensu だけではなく、任意の配列を取り扱える必要があります。
  • 異なる要素数の配列に対応できる 最大値を求めるべき配列の要素数が NUMBER すなわち 5 であるとは限りません。処理対象となる配列の要素数(ここでは人数)も、自由に指定できる必要があります。

このような条件を満たす関数を作っていきましょう。

配列の受渡し

List 6-11 に示すのが、前ページで考えた問題を解決するプログラムです。

List 6-11

// 英語の点数と数学の点数の最高点を求める
#include <stdio.h>

#define NUMBER  5   // 学生の人数

//--- 要素数nの配列vの最大値を返す ---//
int max_of(int v[], int n)
{
    int max = v[0];

    for (int i = 1; i < n; i++)
        if (v[i] > max)
            max = v[i];
    return max;
}

int main(void)
{
    int eng[NUMBER];   // 英語の点数
    int mat[NUMBER];   // 数学の点数

    printf("%d人の点数を入力せよ。\n", NUMBER);
    for (int i = 0; i < NUMBER; i++) {
        printf("[%d] 英語:", i + 1);    scanf("%d", &eng[i]);
        printf("    数学:");          scanf("%d", &mat[i]);
    }
    int max_e = max_of(eng, NUMBER);    // 英語の最高点
    int max_m = max_of(mat, NUMBER);    // 数学の最高点

    printf("英語の最高点=%d\n", max_e);
    printf("数学の最高点=%d\n", max_m);

    return 0;
}

実行例

    5人の点数を入力せよ。
    [1] 英語:53□
        数学:82□
    [2] 英語:49□
        数学:35□
    [3] 英語:21□
        数学:72□
    [4] 英語:91□
        数学:35□
    [5] 英語:77□
        数学:12□
    英語の最高点=91
    数学の最高点=82
    ```

main 関数では二つの配列が定義されています。eng は英語の点数用で、mat は数学の点数用です(それぞれの最高点は、変数 max_e と変数 max_m に格納します)。

点数の最高点を求めるのが、関数 max_of です。まずは、関数頭部に着目しましょう。

```c
int max_of(int v[], int n)

このように、配列を受け取る仮引数は、『型名 引数名 []』という形式で宣言しておき、要素数は別の仮引数(この場合は n)として受け取るのが基本です。

Note

すなわち、要素型は一意に決まるものの、要素数は自由です。数学と英語の配列の要素数は、いずれも NUMBER すなわち 5 ですが、この関数 max_of 自体は要素数に依存しません(すなわち、前のプログラムの問題点が解決しています)。

この関数を呼び出す赤色部の第1実引数は、単なる eng です。このように、呼び出す側の実引数は、(添字演算子 [] を付けずに)配列の名前だけとします (Fig.6-12) 。

もちろん、第2引数に与えている NUMBER は、配列 eng の要素数です。

Fig.6-12 関数呼出しにおける配列の受渡し Fig6-12

図に示すように、関数呼出し式 max_of(eng, NUMBER) で呼び出された関数 max_of の中では、仮引数の配列 v は、実引数の配列 eng そのものとなります。たとえば、v[0] は eng[0] を表し、v[1] は eng[1] を表します。

Note

もちろん、max_of(mat, NUMBER) で呼び出された際の関数 max_of の中では、仮引数の配列 v は、事実上、実引数の配列 mat そのもの、ということになります。

このようになる原理は、第10章で詳しく学習します。

呼び出された関数 max_of の仕様を説明すると、次のようになります。

受け取った配列の要素の最大値を求めて、その値を返却する。

受け取るのは、int の配列でさえあれば、体重の配列や身長の配列など何でもよく、要素数も自由です。

関数 top のように、tensu や NUMBER といった、関数の外の情報に縛られません。他の部品に依存しない、というということは、独立性が高い、ということです。

重要

関数を設計するときは、なるべく独立性が高くなるようにしよう。

配列の受渡しと const 型修飾子

前ページでは、次のことを学習しました。

重要

呼び出された側の関数で仮引数として受け取った配列は、呼び出した側で与えた実引数の配列そのものである。

ということは、受け取った配列の要素に値を代入すれば、それが呼出し側の配列に反映されるはずです。List 6-12 のプログラムで確認しましょう。

List 6-12

// 配列の全要素をゼロにする
#include <stdio.h>

//--- 要素数nの配列vの要素に0を代入 ---//
void set_zero(int v[], int n)
{
    for (int i = 0; i < n; i++)
        v[i] = 0;
}

//--- 要素数nの配列vの全要素を表示して改行 ---//
void print_array(const int v[], int n)
{
    printf("{ ");
    for (int i = 0; i < n; i++)
        printf("%d ", v[i]);
    printf("}\n");
}

int main(void)
{
    int ary1[] = {1, 2, 3, 4, 5};
    int ary2[] = {3, 2, 1};

    printf("ary1 = ");    print_array(ary1, 5);  //←①
    printf("ary2 = ");    print_array(ary2, 3);

    set_zero(ary1, 5);      // 配列ary1の全要素に0を代入 ←②
    set_zero(ary2, 3);      // 配列ary2の全要素に0を代入

    printf("両配列の全要素に0を代入しました。\n");
    printf("ary1 = ");    print_array(ary1, 5);  //←③
    printf("ary2 = ");    print_array(ary2, 3);

    return 0;
}

実行結果

ary1 = { 1 2 3 4 5 }
ary2 = { 3 2 1 }
両配列の全要素に0を代入しました。
ary1 = { 0 0 0 0 0 }
ary2 = { 0 0 0 }

このプログラムには、main 関数の他に、二つの関数が定義されています。

  • 関数 set_zero: 配列 v の全要素に 0 を代入する。※要素の値を更新する。
  • 関数 print_array: 配列 v の全要素の値を表示する。 ※要素の値を更新しない。

main 関数では、これらの関数を呼び出して、配列 ary1 と ary2 の両方に対して、次の処理を行っています。

① 全要素の値の表示 → ② 全要素への 0 の代入 → ③ 全要素の値の表示

関数 set_zero が、仮引数として受け取った配列 v に値を代入した結果、呼出し側の実引数の配列 ary1 と ary2 の要素の値が更新されている(全要素が 0 になっている)ことが、実行結果から確認できます。

そうすると、関数に対して配列を渡すときは、

渡す配列の要素の値を勝手に書き換えられると困るのだが、大丈夫だろうか。

と不安を感じることになってしまいます。

しかし、心配は無用です。受け取った配列を関数内で書き換えられないようにする手段が用意されているからです。

関数 print_array のように、仮引数に const という型修飾子(type qualifier)を置いて宣言するだけで、受け取った配列の要素の値は、書込みが不能となります。

重要

仮引数に受け取った配列の要素の値を読み取るだけで書き込まないのであれば、その仮引数は const 付きで宣言する(呼出し側も安心して呼び出せる)。

Note

関数 print_array 内に、次のようなコードがあればエラーとなります("chap06/List0612x.c")。

v[1] = 5;    // エラー:const宣言された配列の要素には代入できない
また、配列の要素に対して値を書き込む関数 set_zero の仮引数 v の宣言に const 型修飾子を置くと、エラーとなります。

ここまでの学習で、List 6-11(p.160)の関数 max_of が受け取る仮引数 v も、const 型修飾子を付けて宣言すべきであることが分かりました。

ここで、単純な実験をします。関数 set_zero を呼び出す②の箇所を、次のコードに置き換えて実行してみましょう("chap06/List0612a.c")。

set_zero(ary1, 3);    // 配列ary1の先頭3要素に0を代入
set_zero(ary2, 2);    // 配列ary2の先頭2要素に0を代入

そうすると、0 が代入されるのが、配列 ary1 は先頭 3 個の要素、配列 ary2 は先頭 2 個の要素のみとなります。

関数 set_zero と print_array の仮引数 n は、『配列の要素数』というよりも、『処理対象の要素の個数』であることが分かりました。

Note

関数 set_zero のコメントは、次のようになっています。

要素数 n の配列 v の要素に 0 を代入 ... A
より正確に記述するのであれば、次のようになります。
配列 v の先頭 n 個の要素に 0 を代入 ... B
一般的には、Aのような表現を使うのが普通です。本書では、主としてAを使いますが、Bの表現も使っています。

線形探索(逐次探索)

配列内に、ある値の要素が存在するかどうか、存在するのであれば、どの要素なのかを調べる関数を作りましょう。List 6-13に示すのが、そのプログラムです。

List 6-13

// 線形探索(逐次探索)

#include <stdio.h>

#define NUMBER     5       // 要素数
#define FAILED    -1       // 探索失敗

//--- 要素数nの配列vからkeyと一致する要素を探索 ---//
int search(const int v[], int key, int n)
{
    int i = 0;

    while (1) {
        if (i == n)
            return FAILED;      // 探索失敗
        if (v[i] == key)
            return i;           // 探索成功
        i++;
    }
}

int main(void)
{
    int ky, idx;
    int x[NUMBER];

    for (int i = 0; i < NUMBER; i++) {
        printf("x[%d] : ", i);
        scanf("%d", &x[i]);
    }
    printf("探す値 : ");
    scanf("%d", &ky);

    idx = search(x, ky, NUMBER);    // 要素数NUMBERの配列xからkyを探索

    if (idx == FAILED)
        puts("\a探索に失敗しました。");
    else
        printf("%dは%d番目にあります。\n", ky, idx + 1);

    return 0;
}

実行例

① x[0] : 8
  x[1] : 5
  x[2] : 7
  x[3] : 4
  x[4] : 2
  探す値 : 4
  4は4番目にあります。

② x[0] : 8
  x[1] : 5
  x[2] : 7
  x[3] : 4
  x[4] : 2
  探す値 : 1
  探索に失敗しました。

関数searchは、要素数nのint型配列vから、値がkeyの要素を探索する関数です。

Note

前ページまでに学習した内容を反映しています。すなわち、 * 受け取る配列は、要素型がint型であることは一意に決まるものの、要素数は任意である。 * 探す値は任意である。 * 関数の動作は、関数外部の情報に依存しない(探索失敗を表すマクロFAILEDを除く)。 * 仮引数の受け取った配列の要素の値は、読み取るだけなのでconst付きで宣言されている。

Fig.6-13に示すように、探索は成功する場合と失敗する場合があります。関数searchが返却するのは、成功時は見つけた要素の添字iで、失敗時はFAILEDすなわち-1です。

Fig.6-13 逐次探索 Fig6-13

Fig.6-13 逐次探索 * a 4を探索(探索成功) * b 1を探索(探索失敗)

図に示すように、探索は、配列の要素を先頭から順に走査することで行います。走査を行うwhile文は、制御式が1ですから、無限ループの構造です(p.92)。

ただし、次のいずれかの終了条件が満たされたときにループを抜け出します。

① 探すべき値が見つからず、末端を通り越した(i == nが成立)。 → 探索失敗 ② 探すべき値を見つけた(v[i] == keyが成立)。 → 探索成功

配列の先頭から順に走査して、目的とするものと同じ値をもつ要素を見つける一連の手続きは、線形探索(linear search)あるいは逐次探索(sequential search)と呼ばれます。

Column 6-1 インライン関数

関数定義の際に、inlineという関数指定子を前置すると、その関数はインライン関数(inline function)となります。インライン関数とは、高速に動作する可能性がある関数です(実際に高速に動作するようにコンパイルするかどうかは、処理系にゆだねられます)。

//--- インライン関数の定義の一例 ---//
inline int max2(int a, int b)
{
    return a > b ? a : b;
}

注意

なお、関数の結合性が通常の関数とは異なるなど、利用にあたっては注意を要します(関数の結合性については、入門書である本書の学習の範囲外です)。

番兵法

繰返しのたびに行う終了条件①と②の二つの判定は、手軽であるとはいえ、何度となく積み重なると、その負荷は小さくありません。

配列の要素数に余裕があるとして、解決法を考えていきましょう。

探索すべき要素の並びの直後であるv[n]に、探索すべき値keyを格納します(Fig.6-14の赤い要素です)。そうすると、配列の本来の要素内に目的とする値がない場合も、v[n]まで走査したところで、必ずkeyが見つかって、終了条件②が必ず満たされます。

このことは、判定①が不要であることを示しています。

Fig.6-14 逐次探索(番兵法) Fig6-14

Fig.6-14 逐次探索(番兵法) * a 4を探索(探索成功) * b 1を探索(探索失敗)

末端に追加したデータを番兵(sentinel)と呼び、それを用いた手続きを番兵法と呼びます。 番兵を導入すると、繰返し終了のための判定を簡略化できます。

番兵法を使って書き換えたのが、右ページのList 6-14のプログラムです。

まずは、関数searchに着目します。微妙ですが、いろいろと変更されています。

  • 配列を受け取る仮引数の宣言 配列を受け取る仮引数vの宣言では、オリジナルのプログラムで置かれていたconst型修飾子が取り去られています。関数本体で、配列の要素v[n]に書込みを行うからです。

  • 繰返し終了条件の判定 while文の中のif文が、2個から1個に減っています(①が削除されて②のみが残っています)。このことは、繰返しの終了条件の判定回数が半分になることを示しています。

  • 繰返し終了後の判定 while文終了後に実行する文Aに、条件演算子?:による判定が追加されています。これは、見つけた要素(値がkeyと等しい要素)が、もともと配列中に存在した要素なのか(図a)、それとも、番兵として追加した要素なのか(図b)の判定です。

List 6-14

// 逐次探索(番兵法)

#include <stdio.h>

#define NUMBER     5       // 要素数
#define FAILED    -1       // 探索失敗

//--- 要素数nの配列vからkeyと一致する要素を探索(番兵法) ---//
int search(int v[], int key, int n)
{
    int i = 0;

    v[n] = key;      // 番兵を格納

    while (1) {
        if (v[i] == key)
            break;       // 探索成功
        i++;
    }
    return i < n ? i : FAILED;
}

int main(void)
{
    int ky, idx;
    int x[NUMBER + 1];    // 要素数はNUMBERではなく、NUMBER + 1と宣言されている

    for (int i = 0; i < NUMBER; i++) {
        printf("x[%d] : ", i);
        scanf("%d", &x[i]);
    }
    printf("探す値 : ");
    scanf("%d", &ky);

    if ((idx = search(x, ky, NUMBER)) == FAILED)
        puts("\a探索に失敗しました。");
    else
        printf("%dは%d番目にあります。\n", ky, idx + 1);

    return 0;
}

実行例

① x[0] : 8
  x[1] : 5
  x[2] : 7
  x[3] : 4
  x[4] : 2
  探す値 : 4
  4は4番目にあります。

② x[0] : 8
  x[1] : 5
  x[2] : 7
  x[3] : 4
  x[4] : 2
  探す値 : 1
  探索に失敗しました。

そのため、この関数が返却するのは、次の値となります。

  • 探索成功時(図a) i
  • 探索失敗時(図b) FAILED

次は、main関数に着目します。この関数も、いろいろな変更が施されています。

Bでは、配列の要素数が、NUMBERではなく、NUMBER + 1と宣言されています。これは、番兵を格納するために1要素だけ余分に必要だからです。

次は、判定結果を表示するCの箇所に着目します。

if ((idx = search(x, ky, NUMBER)) == FAILED)

if文の制御式は、構造が複雑です。この式をFig.6-15を見ながら理解しましょう。

Fig.6-15 代入式と等価式の評価 Fig6-15

二つの演算子=と==が使われています。演算を優先させるための()が置かれていますので、この式の評価は、次の2段階で行われます。

① 代入演算子=による代入 関数searchの返却値が変数idxに代入されます。

② 等価演算子==による等価性の判定 左オペランドの代入式idx = search(x, ky, NUMBER)と、右オペランドのFAILEDが等しいかどうかの判定が行われます。

代入式を評価して得られるのは、代入後の左オペランドの値ですから、この制御式を日本語で表現すると、次のようになります。

関数呼出し式の返却値をidxに代入して、代入後のidxがFAILEDと等しければ···

Note

式idx = search(x, ky, NUMBER)を囲む()は、省略できません。等価演算子==の優先度が代入演算子=よりも高いからです。

複雑な式でしたが、熟練者が好んで使う表現です。

重要

関数fが返却する値を変数vに代入するとともに、それがxと等しいかどうかを判定する式は、次のように表現できる。

(v = f(...)) == x

Note

このような式を使った判定は、if文だけでなく、while文やfor文などでも多用されます。

さて、関数search中のwhile文による繰返しは、for文を使って書き換えるとプログラムがすっきりします。List 6-15に示すのが、そのプログラムです。

List 6-15

//--- 要素数nの配列vからkeyと一致する要素を探索(番兵法:for文) ---//
int search(int v[], int key, int n)
{
    int i;

    v[n] = key;      // 番兵を格納

    for (i = 0; v[i] != key; i++)
        ;
    return i < n ? i : FAILED;
}

Note

このfor文は、keyと同じ値の要素に出会うまでiをインクリメントします。ループ本体として、何も行うことがないため、空文となっています。

Example

要素数がnであるintの配列vの要素の最小値を返す関数を作成せよ。

int min_of(const int v[], int n);

Example

要素数がnであるintの配列vの要素の並びを反転する関数を作成せよ。

void rev_intary(int v[], int n);

List 5-8(p.123)と演習5-4(p.127)を参考にすること。

Example

要素数がnであるintの配列v2の並びを反転したものを配列v1に格納する関数を作成せよ。

void intary_rcpy(int v1[], const int v2[], int n);

Example

要素数nの配列v内のkeyと等しい全要素の添字を配列idxに格納する関数search_idxを作成せよ。返却するのはkeyと等しい要素の個数とする。

int search_idx(const int v[], int idx[], int key, int n);

たとえば、vに受け取った配列の要素が{1, 7, 5, 7, 2, 4, 7}でkeyが7であれば、idxに{1, 3, 6}を格納した上で3を返却する。

多次元配列の受渡し

前章のList 5-15(p.134)は、二つの2次元配列の全要素の和を求めるプログラムでした。

和を求める部分と、表示を行う部分の、それぞれを関数として独立させて実現したのが、List 6-16のプログラムです。

List 6-16

// 4人の学生の3科目のテスト2回分の合計を求めて表示(関数版)

#include <stdio.h>

//--- 4行3列の行列aとbの和をcに格納する ---//
void mat_add(const int a[4][3], const int b[4][3], int c[4][3])
{
    for (int i = 0; i < 4; i++)
        for (int j = 0; j < 3; j++)
            c[i][j] = a[i][j] + b[i][j];
}

//--- 4行3列の行列mを表示 ---//
void mat_print(const int m[4][3])
{
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 3; j++)
            printf("%4d", m[i][j]);
        putchar('\n');
    }
}

int main(void)
{
    int tensu1[4][3] = { {91, 63, 78}, {67, 72, 46}, {89, 34, 53}, {32, 54, 34} };
    int tensu2[4][3] = { {97, 67, 82}, {73, 43, 46}, {97, 56, 21}, {85, 46, 35} };
    int sum[4][3];         // 合計

    mat_add(tensu1, tensu2, sum);     // 2回分の点数の合計を求める

    puts("1回目の点数"); mat_print(tensu1);    // 1回目の点数を表示
    puts("2回目の点数"); mat_print(tensu2);    // 2回目の点数を表示
    puts("合計点");     mat_print(sum);       // 合計点を表示

    return 0;
}

実行結果

1回目の点数
  91  63  78
  67  72  46
  89  34  53
  32  54  34
2回目の点数
  97  67  82
  73  43  46
  97  56  21
  85  46  35
合計点
 188 130 160
 140 115  92
 186  90  74
 117 100  69

本プログラムでは、二つの関数が受け取るすべての配列が、4行3列の配列として宣言されているため、要素数を受け取るための仮引数は宣言されていません。

Note

関数間の多次元配列の受渡しでは、最も高い次元の要素数のみを、配列とは別の引数としてやりとりするのが一般的です(Column 6-2:右ページ)。

Example

4行3列の行列aと3行4列の行列bの積を、4行4列の行列cに格納する関数を作成せよ。

void mat_mul(const int a[4][3], const int b[3][4], int c[4][4]);

Example

2回分の点数を3次元配列に格納するようにList 6-16を書き換えたプログラムを作成せよ。

Column 6-2  多次元配列の受渡し

n次元の多次元配列を受け取る関数の仮引数は、次のルールに基づいて宣言します。

  • n次元の要素数は省略可能(宣言しても無視されるため、別の引数として受け取る)。
  • (n - 1)次元以下の要素数は、定数として宣言する。

そのため、1次元配列~3次元配列を受け取る5個の典型的な宣言例は、次のようになります。

void func1(int v[],          int n);    // 要素型はintで、    要素数は別の引数n
void func2(int v[][3],       int n);    // 要素型はint[3]で、  要素数は別の引数n
void func3(int v[][2][3],    int n);    // 要素型はint[2][3]で、要素数は別の引数n

このように、任意に指定できるのは、最も高い次元の要素数のみです。そのことを利用して作成したプログラムをList 6C-1に示します。

List 6C-1

// n行3列の2次元配列の全構成要素に同一値を代入

#include <stdio.h>

//---int[3]型を要素型とする要素数nの配列mの全構成要素にvを代入 ---//
void fill(int m[][3], int n, int v)
{
    for (int i = 0; i < n; i++)
        for (int j = 0; j < 3; j++)
            m[i][j] = v;
}

//---int[3]型を要素型とする要素数nの配列mの全構成要素の値を表示 ---//
void mat_print(const int m[][3], int n)
{
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < 3; j++)
            printf("%4d", m[i][j]);
        putchar('\n');
    }
}

int main(void)
{
    int no;
    int x[2][3] = {{0}};      // 2行3列:要素型はint[3]型で要素数は2
    int y[4][3] = {{0}};      // 4行3列:要素型はint[3]型で要素数は4

    printf("全構成要素に代入する値:");
    scanf("%d", &no);

    fill(x, 2, no);          // xの全構成要素にnoを代入
    fill(y, 4, no);          // yの全構成要素にnoを代入

    printf("--- x ---\n");    mat_print(x, 2);
    printf("--- y ---\n");    mat_print(y, 4);

    return 0;
}

関数fillと関数mat_printが受け取る引数mの2次元の要素数(行数)は省略されており、1次元の要素数(列数)が3となっています。そのため、これらの関数に対しては、行数は任意で、列数が3の配列を渡せます(本プログラムでは、2行3列の配列と、4行3列の配列を渡しています)。

有効範囲と記憶域期間

有効範囲と識別子の可視性

本節の最初に考えるのは、List 6-17のプログラムです。同一の名前をもつ変数xが3箇所で宣言されています(宣言順にx、x、xと色分けしています)。

List 6-17

// 識別子の有効範囲を確認する

#include <stdio.h>

int x = 75;            // Aファイル有効範囲

void print_x(void)
{
    printf("x = %d\n", x);
}

int main(void)
{
    int x = 999;           // Bブロック有効範囲

    print_x();             // ①

    printf("x = %d\n", x); // ②

    for (int i = 0; i < 5; i++) {
        int x = i * 100;       // Cブロック有効範囲
        printf("x = %d\n", x); // ③
    }

    printf("x = %d\n", x); // ④

    return 0;
}

実行結果

x = 75
x = 999
x = 0
x = 100
x = 200
x = 300
x = 400
x = 999

まず、Aで宣言されたxに着目します。初期化子75が与えられた変数xは、関数の外で宣言・定義されているため、ファイル有効範囲が与えられます。

関数print_xの中で、"x"といえば、このxのことですから、関数print_xを実行すると、次のように表示されます。

x = 75               ... 表示されるのはxの値

①では、その関数print_xを呼び出していますので、最初に上記の表示が行われます。

次に、Bで宣言されたxに着目します。main関数の関数本体であるブロック(複合文)の中で宣言されているため、このxには、ブロック有効範囲が与えられます(名前が通用するのは、main関数の終端の}までです)。

ということは、②では同じ名前のxとxの二つが存在することになります。このような状況で適用されるのが、次の規則です。

重要

ファイル有効範囲とブロック有効範囲をもつ同じ名前の変数が存在する場合は、ブロック有効範囲の名前が見えて、ファイル有効範囲の名前は隠される。

つまり、②での"x"はxのことであり、その値が、次のように表示されます。

x = 999               ... 表示されるのはxの値

その表示に続くfor文のループ本体の赤い部分では、3番目のxが宣言・定義されています。宣言位置がブロック内ですから、このxにはブロック有効範囲が与えられます。

そうすると、隠されているx以外に、xとxが存在することになります。ここで適用されるのは、次の規則です。

重要

ブロック有効範囲をもつ同じ名前の変数が存在する場合、より内側のものが見えて、より外側のものが隠される。

そのため、for文のループ本体のブロック内で"x"といえば、xのことになります。

Fig6-io

そのfor文は5回の繰返しを行いますから、③では、xの値が次のように表示されます。

x = 0                 ... 表示されるのはxの値
x = 100
x = 200
x = 300
x = 400

for文が終了すると、もはやxの名前は通用しません。そのため、最後のprintf関数の呼出し④では、xの値が、次のように表示されます。

x = 999               ... 表示されるのはxの値

これで、本プログラムの挙動が理解できました。

Note

宣言された識別子は、その名前を書き終わった直後から有効となります。そのため、Bの宣言を

int x = x;
と書き換えても、=の右側の初期化子のxは、ここで宣言しているxのことであって、プログラム冒頭のAで宣言されたxではない、ということです。そのため、xは75ではなく、自身の値である不定値で初期化されます("chap06/List0617a.c")。

記憶域期間

オブジェクト(変数)は、必ずしもプログラムの開始から終了まで存在し続けるのではありません。オブジェクトの生存期間(寿命)を表すのが、記憶域期間(storage duration)という考え方です。

具体的なことを、右ページのList 6-18のプログラムで学習していきましょう。 関数funcの本体では、二つの変数sxとaxが宣言されています。ただし、sxの宣言には staticという記憶域クラス指定子(storage duration specifier)が置かれています。

そのためでしょうか、同じ値で初期化して、インクリメントを同じように行っているにもかかわらず、axとsxの値が異なります。

自動記憶域期間(automatic storage duration)

関数の中で、記憶域クラス指定子staticを付けずに定義されたオブジェクト(変数)には、次の性質の自動記憶域期間が与えられます。

プログラムの流れが宣言を通過する際に、オブジェクトが生成される。宣言を囲むブロックの終端である}を通過するときに、そのオブジェクトは役目を終えて破棄される。

初期化子が与えられずに宣言されると、初期値は不定値となる。

これは、ブロックの中でのみ生きるはかない命です。変数axは、

int ax = 0;      // この宣言を通過する際に0で初期化される

の宣言を通過する際に、生成されると同時に初期化されます。

静的記憶域期間(static storage duration)

関数の中でstaticを付けて宣言されたオブジェクトや、関数の外で宣言・定義されたオブジェクトには、次の性質の静的記憶域期間が与えられます。

プログラムの開始時、具体的にはmain関数の実行開始前の準備段階でオブジェクトが生成されて、プログラムの終了時に破棄される。

初期化子が与えられずに宣言されると、自動的に0で初期化される。

これは、永遠の命ともいうべきものです。

静的記憶域期間が与えられたオブジェクトは、main関数の実行が開始される前に初期化が行われます。そのため、変数sxは、

static int sx = 0;      // この宣言を通過する際に0で初期化されない

の宣言を通過するたびに初期化されるのではありません(0で初期化されるのは、main関数の実行が開始される前の準備段階の1回限りです)。

List 6-18

// 自動記憶域期間と静的記憶域期間

#include <stdio.h>

int fx = 0;            // 静的記憶域期間+ファイル有効範囲

void func(void)
{
    static int sx = 0;  // 静的記憶域期間+ブロック有効範囲
    int      ax = 0;    // 自動記憶域期間+ブロック有効範囲

    printf("%3d%3d%3d\n", ax++, sx++, fx++);
}

int main(void)
{
    int i;

    puts(" ax sx fx");
    puts("---------");
    for (i = 0; i < 10; i++)
        func();
    puts("---------");

    return 0;
}

実行結果

 ax sx fx
---------
  0  0  0
  0  1  1
  0  2  2
  0  3  3
  0  4  4
  0  5  5
  0  6  6
  0  7  7
  0  8  8
  0  9  9
---------

2種類の記憶域期間の性質をまとめたのが、Table 6-2です。

Table 6-2 オブジェクトの記憶域期間

自動記憶域期間 静的記憶域期間
生 成 プログラムの流れが宣言を通過するとき
初期化 明示的に初期化しなければ不定値
破 棄 その宣言を含むブロックを抜け出すとき

Note

関数の中で、記憶域クラス指定子autoまたはregisterを付けて宣言・定義された変数に対しても自動記憶域期間が与えられます(autoはあってもなくても同じであり、付ける必要はありません)。

auto int ax = 0;        // int ax = 0;と同じ
また、register記憶域クラス指定子を付けて
register int ax = 0;
と宣言すると、コンパイラに対して、『変数axを、主記憶よりも(高速な)レジスタに格納したほうがよい。』というヒントが与えられます(その結果、演算が高速になることが期待できます)。

ただし、レジスタの個数には限りがありますし、コンパイル技術が進歩した現在では、どの変数をレジスタに格納すればよいのかを、コンパイラ自身が判断して最適化します(レジスタに格納する変数を、プログラムの実行時に動的に変えるものまであります)。

もはやregister宣言を行う意味はなくなりつつあります。

それでは、Fig.6-16を見ながらプログラムの挙動を考えていきましょう。

Fig6-sa

Note

この図では、次のように色分けしています。 静的記憶域期間をもつ変数:赤 自動記憶域期間をもつ変数:黒

Fig.6-16 オブジェクトの生成と破棄 Fig6-16

a main関数実行開始直前の状態です。 静的記憶域期間をもつfxとsxとが、記憶域上に生成されて0で初期化されます。 なお、これらの変数は、プログラムの実行を通じて、同じ場所に存在し続けます。

b main関数の実行が開始します。自動記憶域期間をもつ変数iが生成されます。

c main関数から関数funcが呼び出され、自動記憶域期間をもつ変数axが生成されて0で初期化されます。ここで、ax、sx、fxの値が 0 0 0 と表示されます。その後、これら三つの変数はインクリメントされますから、それらの値は1、1、1となります。

d 関数funcの実行終了とともにaxが破棄されます。

e main関数では、変数iをインクリメントして、再び関数funcを呼び出します。このとき、変数axが生成されて0で初期化されます。ここで、三つの変数の値が 0 1 1 と表示されます。表示後に、これらの変数はインクリメントされ、それぞれ、1、2、2となります。

main関数は関数funcを10回呼び出しますが、永遠の寿命を与えられているfxとsxは、そのたびに値がインクリメントされていきます。

一方、関数funcの中でしか生きることのできないaxは、毎回生成されて0に初期化されるため、その値は10回とも0と表示されます。

g main関数の終了と同時に、変数iは役目を終えて破棄されます。

これで、プログラムの挙動が理解できました。

Note

これまでバラバラの箱と考えてきた変数が、記憶域の一部であることが分かりました。そのあたりの詳細は、第10章で学習します。

静的記憶域期間が与えられたオブジェクトが暗黙のうちに0で初期化されることを、プログラムで確認しましょう。List 6-19に示すのが、そのプログラムです。

List 6-19

// 静的記憶域期間をもつオブジェクトの暗黙の初期化を確認

#include <stdio.h>

int fx;                // 0で初期化される

int main(void)
{
    static int    si;      // 0で初期化される
    static double sd;      // 0.0で初期化される
    static int    sa[5];   // 全要素が0で初期化される

    printf("fx = %d\n", fx);
    printf("si = %d\n", si);
    printf("sd = %f\n", sd);

    for (int i = 0; i < 5; i++)
        printf("sa[%d] = %d\n", i, sa[i]);

    return 0;
}

実行結果

fx = 0
si = 0
sd = 0.000000
sa[0] = 0
sa[1] = 0
sa[2] = 0
sa[3] = 0
sa[4] = 0

静的記憶域期間を与えられるint型変数fxとsi、double型変数sd、intの配列の全要素sa[0]~sa[4]のすべてが0(あるいは0.0)で初期化されることが確認できます。

Example

静的記憶域期間が与えられたdouble型配列の全要素が0.0で初期化されることを確認するプログラムを作成せよ。

Example

呼び出された回数を表示する関数put_countを作成せよ。(右に示すのは、関数put_countを3回呼び出した実行結果である)。

void put_count(void);
実行結果
put_count:1回目
put_count:2回目
put_count:3回目

まとめ

Tip

● ひとまとまりの手続きは、プログラムの部品である関数として実現する。関数は返却値型/関数名/仮引数型並びによって特徴付けられる。引数を受け取らない関数は、仮引数型並びをvoidとする。

● 関数本体は複合文(ブロック)である。関数に特有の変数は、関数本体の中で宣言する。

● 関数呼出しは、関数呼出し演算子()を用いた『関数名(実引数の並び)』の形式で行う。実引数がない場合は()の中を空とする。複数の実引数がある場合はコンマで区切る。

● 関数呼出しが行われると、プログラムの流れは呼び出された関数に移る。

● 引数の受渡しは値渡しによって行われ、実引数の値が仮引数に代入される。そのため、受け取った仮引数の値を変更しても、実引数の値に反映されることはない。値渡しのメリットを活かすと、関数はコンパクトで効率のよいものとなる可能性がある。

● 関数内でreturn文を実行するか、関数本体の実行が終了すると、プログラムの流れが呼出し元に戻る。返却値型がvoidでない関数は、呼出し元へと戻る際に、単一の値を返却する。

● 関数呼出し式を評価すると、関数によって返却された値が得られる。

● 変数や関数の実体を作り出す宣言は定義でもある宣言で、そうでない宣言は定義ではない宣言である。

● プログラムを実行すると、main関数の本体部が実行される。main関数以外の関数が先に実行されることはない。

● 呼び出される側の関数を前方で定義し、呼び出す側の関数を後方で定義すると都合がよい。前方で定義されていない関数を呼び出すには、関数の返却値型や仮引数の型や個数を記述した関数原型宣言が必要である。

Fig6-18

● 関数は、できるだけ汎用性や独立性の高い仕様となるように設計すべきである。

● C言語が提供するprintf関数、scanf関数などの関数は、ライブラリ関数と呼ばれる。

などのヘッダには、ライブラリ関数の関数原型宣言などが含まれている。ヘッダの内容は、#include指令によってインクルードする(取り込む)。

● 配列を受け取る仮引数は、『型名 変数名[]』の形式で宣言し、要素数は別の引数として受け取る。なお、受け取った配列要素の値を参照するだけで書き換えないのであれば、配列を受け取る仮引数にはconstを付けて宣言する。

● 配列を走査して、目的とする値をもつ要素を見つける手続きを、線形探索あるいは逐次探索という。番兵法の併用も可能である。

● 関数の外で定義された変数は、ファイル終端まで名前が通用するファイル有効範囲をもち、関数の中で定義された変数は、ブロック終端まで名前が通用するブロック有効範囲をもつ。

● 異なる有効範囲をもつ同一名の変数が存在する場合、より内側のものが見えて外側のものが隠される。

● 関数の外で定義されたオブジェクトと、staticを伴って関数の中で定義されたオブジェクトは、プログラムの開始から終了まで生きる静的記憶域期間をもつ。明示的に初期化されない場合は0で初期化される。

● 関数の中でstaticを伴わずに定義されたオブジェクトは、ブロックの終端まで生きる自動記憶域期間をもつ。明示的に初期化されない場合は不定値で初期化される。

Fig6-17

演習問題

  1. 配列を逆順にコピーする関数
  2. 指定した値と一致する要素を探索する関数
  3. 素数判定関数
  4. ニュートン法による平方根計算

演習問題7-1:配列を逆順にコピーする関数

問題の説明

ある配列の内容を逆順にして別の配列にコピーする関数を作成してください。関数は以下の仕様に従ってください。 - 関数名: intary_rcpy - 引数: コピー先配列、コピー元配列、要素数 - 戻り値: なし(void)

期待される結果

出力例:

元の配列: 10 20 30 40 50
逆順コピー配列: 50 40 30 20 10

ヒント

  • 配列の添字は0から始まることを忘れないでください
  • 逆順にコピーするには、コピー元配列の最後の要素からコピーを始めます
  • コピー元配列を変更せず読み取りのみを行うため、const修飾子を使用します
  • コピー元配列の添字がn-i-1となることに注意してください(例:nが5の場合、i=0のとき添字は4)
  • プログラム中の int size = sizeof(src) / sizeof(src[0]); は配列の要素数を計算するための標準的な方法です:
  • sizeof(src) は配列全体のバイトサイズを返します
  • sizeof(src[0]) は配列の1要素のバイトサイズを返します
  • 全体サイズを要素サイズで割ることで、要素数が得られます
  • この方法は静的配列にのみ有効であることに注意してください
  • 関数構造
    void intary_rcpy(int v1[], const int v2[], int n)
    
  • 引数の説明
  • v1[]:コピー先配列(結果が格納される配列)
  • const int v2[]:コピー元配列(constで読み取り専用として指定され、元の配列は変更されない)
  • n:配列の要素数
  • 戻り値
  • void(戻り値なし)

演習問題7-2:指定した値と一致する要素を探索する関数

問題の説明

配列内で指定した値と一致するすべての要素の添字を別の配列に格納し、一致した要素の個数を返す関数を作成してください。関数は以下の仕様に従ってください。 - 関数名: search_idx - 引数: 探索対象の配列、添字を格納する配列、探索する値、要素数 - 戻り値: 一致した要素の個数(int)

期待される結果

出力例1:

配列: 1 7 5 7 2 4 7
探す値: 7
7は3個見つかりました。
その添字は 1 3 6です。

配列: 10 20 30 40 50
探す値: 35
35は見つかりませんでした。

ヒント

  • 一致した要素の個数を変数でカウントしながら処理を進めます
  • 一致した要素の添字は、添字保存用配列に格納します
  • 添字保存用配列のインデックスは、見つかった要素の数(count)で管理します
  • 探索対象の配列は変更しないため、const修飾子を使用します
  • プログラム中の int size1 = sizeof(array1) / sizeof(array1[0]); は配列の要素数を計算するための標準的な方法です:
  • sizeof(array1) は配列全体のバイトサイズを返します
  • sizeof(array1[0]) は配列の1要素のバイトサイズを返します
  • 全体サイズを要素サイズで割ることで、要素数が得られます
  • この方法はコンパイル時にサイズが決まる静的配列にのみ有効です

  • 関数構造

    int search_idx(const int v[], int idx[], int key, int n)
    

  • 引数の説明
  • const int v[]:探索対象の配列(読み取り専用)
  • int idx[]:発見した要素のインデックスを格納するための配列
  • int key:探索する値
  • n:配列の要素数
  • 戻り値
  • int(一致した要素の個数)

演習問題7-3:素数判定関数

問題の説明

与えられた数が素数かどうかを判定する関数を作成し、指定された範囲内のすべての素数を表示するプログラムを作成してください。関数は以下の仕様に従ってください。 - 関数名: is_prime - 引数: 判定する整数値 - 戻り値: 素数なら1、そうでなければ0(int)

期待される結果

入力例1:

範囲を入力してください
最小値: 10
最大値: 50

出力例1:

10から50までの素数:
11 13 17 19 23 29 31 37 41 43 47

入力例2:

範囲を入力してください
最小値: 2
最大値: 10

出力例2:

2から10までの素数:
2 3 5 7

ヒント

  • 素数は1より大きい自然数で、1とその数自身以外に約数を持たない数です
  • 効率的な素数判定のためには、以下の最適化が重要です:
  • 2は特別に処理する(2は唯一の偶数の素数)
  • 2以外の偶数は素数ではないため、すぐに判定できる
  • 約数の存在チェックは、その数の平方根までで十分です(なぜなら、n = a × b と表せる場合、a と b の少なくとも一方は√n 以下になるため)
  • 3以上の約数チェックは奇数のみで十分です(偶数の約数性はすでに確認済みのため)
  • ステップごとの処理:
  • 特殊ケースの判定: n ≤ 1 は素数ではない、n = 2 は素数
  • 偶数判定: n > 2 かつ n が偶数なら素数ではない
  • 奇数での割り算チェック: 3, 5, 7, ... と奇数のみでチェックし、√n まで調べる
  • どの数でも割り切れなければ素数と判定
  • 関数の返り値はブール値の代わりに整数(1または0)を使います
  • i * i <= n は整数オーバーフローを避けるため、大きな数の場合は i <= sqrt(n) と書くこともできます(その場合は math.h のインクルードが必要)
  • 関数構造
    int is_prime(int n)
    
  • 引数の説明
  • int n:素数かどうかを判定する整数値
  • 戻り値
  • int(素数なら1、素数でなければ0を返す、論理値のような使い方)

演習問題7-4:ニュートン法による平方根計算

問題の説明

ニュートン法(反復法)を使って浮動小数点数の平方根を計算する関数を実装してください。関数は以下の仕様に従ってください。 - 関数名: my_sqrt - 引数: 平方根を求める正の実数 - 戻り値: 平方根の近似値(double)

期待される結果

入力例1:

平方根を計算する正の数を入力: 2

出力例1:

√2の近似値 = 1.414213562
標準ライブラリ値 = 1.414213562
相対誤差 = 1.570092459e-16

入力例2:

平方根を計算する正の数を入力: 100

出力例2:

√100の近似値 = 10
標準ライブラリ値 = 10
相対誤差 = 0

入力例3:

平方根を計算する正の数を入力: -4

出力例3:

負の数の平方根は実数では定義されません

ヒント

  • ニュートン法は、関数のゼロ点を求める数値計算法の一つで、反復によって近似解を求めます
  • ニュートン法の数学的背景:
  • 平方根計算では、f(x) = x² - a というf(x) = 0となる点x = √aを探します
  • ニュートン法は、接線の方程式を使って次の近似点を見つけます:x_(n+1) = x_n - f(x_n)/f'(x_n)
  • f'(x) = 2x なので、漸化式は x_(n+1) = x_n - (x_n² - a)/(2x_n) = (x_n + a/x_n)/2 となります
  • 実装のステップ:
  • 特殊ケースの処理: 負の値は-1を返す、0は0を返す
  • 初期推測値の設定: 元の値自体を使うのが一般的(他には a/2 などでも可)
  • 反復計算: 漸化式 x_(n+1) = (x_n + a/x_n)/2 を使って推測値を更新
  • 収束判定: |x_n² - a| < ε となるまで反復(εは許容誤差)
  • 収束判定は単純に「前回値と今回値の差」ではなく、「推測値の二乗と元の値の差」を使うべきです
  • 浮動小数点数の比較では、絶対誤差(定数εとの比較)より相対誤差(値に応じたεの調整)を使うと精度が向上します
  • 本問題では DBL_EPSILON * x を使用して、値の大きさに応じた誤差範囲を設定しています
  • DBL_EPSILONは、1.0に足したときに1.0と区別できる最小の値(約2.2e-16)です
  • ニュートン法は二次収束するため、精度が2倍になるごとに必要な反復回数が急速に減少します
  • 特殊なケース(負の数、ゼロ)は別途処理する必要があります
  • 0除算を避けるために、x == 0の場合は特別に処理しています
  • 収束が保証されるよう、初期値の選択にも注意しましょう。なぜ x を初期値として選んでいるのか考えてみましょう
  • 反復回数を記録して表示することで、アルゴリズムの効率性を確認できます
  • #include <float.h> が必要な理由は、浮動小数点の精度を表す定数 DBL_EPSILON を使用するためです
  • 数学的な収束条件 |guess * guess - x| > DBL_EPSILON * x の意味について考えてみましょう
  • プログラム中では以下のように表現されています:
    fabs(guess * guess - x) > DBL_EPSILON * x
    
  • fabs() は絶対値を計算する関数で、この条件が真である限り(誤差が許容範囲より大きい限り)ループが継続します。
  • 計算結果の出力と検証には以下のコードを使用しています:
    printf("√%.10gの近似値 = %.10g\n", value, my_sqrt(value));
    printf("標準ライブラリ値 = %.10g\n", sqrt(value));
    printf("相対誤差 = %.10g\n", fabs(my_sqrt(value) - sqrt(value)) / sqrt(value));
    
  • これにより、自作の平方根関数の結果を標準ライブラリの sqrt() 関数と比較し、相対誤差を計算しています
  • %.10g は浮動小数点数を10桁の精度で表示するための書式指定子です
  • 相対誤差は |自作関数の結果 - ライブラリ関数の結果| / ライブラリ関数の結果 で計算されます
  • 関数構造
    double my_sqrt(double x)
    
  • 引数の説明
  • double x:平方根を計算する正の実数
  • 戻り値
  • double(平方根の近似値)