第6章 関数

テキスト『新・明解C言語 入門編』第6章の内容に関する補足説明を提供します.

6-1 関数とは

main 関数とライブラリ関数

このセクションについての補足説明は現在のところありません.

関数とは

C言語での関数とは,いくつかの入力を受け取り,何らかの処理を行って, ひとつの値を返すものです. ただし,入力を何も受け取らない関数や(p.144), 値を返さない関数(p.142)もあります.

関数定義

関数本体の説明で「関数の中でのみ利用する変数」と述べているのは, 仮引数として宣言した変数とは別の変数です. こうした変数の例は List 6-2 にあります. この例題で定義している関数 max3 の中で, max という変数を宣言して利用しています. 仮引数として宣言された変数も,当然ながら,関数の中でのみ利用されます.

関数呼び出し

List 6-1 の max2 関数のように, 自分で定義する関数は main 関数よりも前に書きます. main 関数はソースプログラムの中で最後に書きます(p.147). 自作の関数を使うときには, それが使われる場所より前でその関数を定義しておきます. ただし,複雑なプログラムでは必ずしもこの順序が守られないかもしれません. その場合には,テキスト p.147 で説明されている 関数原型宣言(function prototype declaration)を最初に書きます.

三値の最大値

このセクションについての補足説明は現在のところありません.

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

このセクションについての補足説明は現在のところありません.

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

List 6-4 で,max2 関数は max4 関数の中で使われます. そのため,max2 関数は max4 関数より先に定義しておきます. この順序が逆になると,下図のように, clang でのコンパイルで警告が出ます. ただし,実行プログラムは作られ,正しく動作しています.

implicit declarration of function

テキスト p. 147 で説明されている関数原型宣言を書いておけば, コンパイルで警告は出ません.

function prototype declaration of the max2 function

値渡し

誤植が2か所あります.

テキスト p.140 での,値渡し (pass by value)の解説で, 「仮引数 x は実引数 a のコピーであり,仮引数 y は実引数 b のコピーです」 と書かれているのは, 「仮引数 x は実引数 a のコピーであり,仮引数 n は実引数 b のコピーです」 の誤りです(y でなく n).

テキスト p.141 での,List 6-6 の解説で, 「関数 power の実行が終了するときの n の値は -1 となりますが」 と書かれているのは, 「関数 power の実行が終了するときの n の値は 0 となりますが」 の誤りです(-1 ではなく 0). List 6-6 で定義されている power 関数の仮引数 n に, 実引数として 2 という値が代入されたとします. while 文の制御式 n-- > 0 の評価は, 後置減分演算子(--)が適用される前に行われます(テキスト p.79, p.87 参照). そのため,2 > 0 という式が評価され,評価値は 1(真)となります. 続いて,n の値は後置減分演算子によってひとつ減らされて,1 となります. ループ本体が実行されるとき,n の値は 1 です. ループ本体の実行が終わって, 再び制御式 n-- > 0 が評価されるときには, 1 > 0 という式が評価されることになります(評価値は 1). 続いて,後置減分演算子が適用され,n の値は 0 になります. 次に制御式が評価されるときには, 0 > 0 を評価することになるので,この評価値は 0 となり, ループ本体は実行されません. したがって,関数 power の実行が終了するときの n の値は 0 です.

6-2 関数の設計

値を返さない関数

このセクションについての補足説明は現在のところありません.

関数の汎用性

このセクションについての補足説明は現在のところありません.

仮引数を受け取らない関数

List 6-9 で,非負の整数を反転する仕組みはわかったでしょうか? テキストの実行例のように,正の整数 128 が入力されとしましょう. この値(厳密には,この値のコピー)は, main 関数の中で rev_int 関数(reverse integer という意味)に渡されます. 値を渡された rev_int 関数の中では,この値は仮引数 num に代入されます. 値の反転は do 文で実現されています. 最終的には反転した値が代入される変数 tmp は,最初は 0 で初期化されています. 1回目の do 文の実行(do 文のループは必ず1回は実行されます)で,


tmp = tmp * 10 + num % 10;

という文は,


0 * 10 + 128 % 10

を計算して tmp に代入することを意味します. 128 % 10 は 8 を返しますから,tmp には 8 が代入されます. 次の


num /= 10;

という文で,num の値が更新されます. 変数 num(更新前の値は 128)は int 型の整数ですから,10 で割ったあまりは切り捨てられ, 新たな num の値は 12 となります. 制御式 num > 0 は真なので,do 文のループ本体は再び実行されます. 今度は,


tmp = tmp * 10 + num % 10;

という文は,


8 * 10 + 12 % 10

を計算して tmp に代入することを意味します. 12 % 10 は 2 を返しますから,tmp には 82 が代入されます. 次の


num /= 10;

という文で,num の値が更新されます. 変数 num の値は 12 でしたから,更新後の num の値は 1 です. 制御式 num > 0 は真なので,do 文のループ本体は再び実行されます. 今度は,


tmp = tmp * 10 + num % 10;

という文は,


82 * 10 + 1 % 10

を計算して tmp に代入することを意味します. 1 % 10 は 1 を返しますから,tmp には 821 が代入されます. 次の


num /= 10;

という文で,num の値が更新されます. 変数 num の値は 1 でしたから,更新後の num の値は 0 です. 制御式 num > 0 は偽なので,do 文のループ本体を抜けます. 最後に,


return tmp;

により,rev_int 関数は 821 という値を返します.

値の反転を行う do 文は,入力された値が正の数であるかどうかを確認する if 文に埋め込まれています. この確認は scan_pint 関数(scan positive integer という意味)で行っていますから, if 文はなくても動作します. わざわざ無駄なことをしているようですが, 別のプログラムで rev_int 関数を使うときに, いつも scan_pint 関数と共に使われるとは限りません. そのため,rev_int 関数に渡された値が正の整数かどうかを確認することには意味があります.

関数の返却値での初期化

List 6-9 では,main 関数の中で scan_pint 関数が呼び出され, それが返す値で変数 nx を初期化しています. 関数の返却値での初期化が可能なのは, 自動記憶域期間 (automatic storage duration)を持つ変数だけです. テキスト p.162 で学習するように, 静的記憶域期間(static storage duration)を持つ変数は, main 関数の実行前に初期化されますから, この時点で実行できない他の関数の返却値で初期化することはできません.

有効範囲

このセクションについての補足説明は現在のところありません.

ファイル有効範囲

すべての関数の外側で定義される変数を 外部変数 (external variable)あるいは グローバル変数 (global variable)と呼びます(この用語はテキストにはありません). List 6-10 での配列 tensu は,変数でなく配列ですが, main 関数および top 関数の外側で定義されているので, 外部変数(グローバル変数)に相当します. 外部変数は,それにアクセスするどの関数の定義よりも前で宣言します. 通常は,List 6-10 での配列 tensu のように, プリプロセッサのコマンド(# で始まる)の直後に書きます.

外部変数は,宣言された場所以降の, プログラム中のどの関数からでもアクセスできます. これに対して, ブロックの内部(たとえば,main 関数の内部)で宣言された変数は, そのブロックの内部でのみ使用できます. ブロックの外側からアクセスすることはできません.

宣言と定義

List 6-10 で配列 tensu の宣言に使われている extern という宣言は, テキスト p.162 で説明されている 記憶域クラス指定子 (storage class specifier)のひとつです.

関数の外側で 定義 された外部変数は, それを使う関数の中で 宣言 します. 関数の中で外部変数を宣言するときに, extern を先頭につけます. List 6-10 はこの原則に従っています. 変数と定義の違いは,今の段階では少しわかりにくいかもしれません. 変数の実体を作り出す,すなわち, メモリ上の記憶領域が変数に割り当てられるとき,それを定義と呼びます. 第10章の List 10-2 で,変数に割り当てられた領域のアドレスを表示します.

ただし,外部変数が,それを使用する関数より前に, 同一のソースファイルの中で定義されている場合には, 関数内でその外部変数を宣言する必要はありません. したがって,List 6-10 では,main 関数および top 関数の中での配列 tensu の宣言は省略できます.

省略可能ならば,なぜ extern 宣言などというものが必要なのでしょうか? このテキストで学習する範囲では,その必要性はほとんどありません. しかし,より進んだプログラミングでは,これが必要になってきます.

このテキストで学習する範囲では, ソースプログラムはひとつのファイルでできており, このファイルをコンパイルして実行プログラムを作っています. より進んだプログラミングでは, ソースプログラムが複数のファイルから構成され, それらのファイルを別々にコンパイルしてからリンクさせるということが行われます.

たとえば,ソースプログラムが part1.c と part2.c という2つのファイルから構成されているとします. そして,グローバル変数 glb が,part1.c の中で定義されているとします. このグローバル変数を part2.c の中で使用するには, part2.c の中で,extern をつけてこの変数を宣言すればよいのです. たとえば,part2.c の冒頭部分で,


#include <stdio.h>

extern int glb;

と書くことで, part2.c の中でもグローバル変数 glb を使用できるようになります.

関数原型宣言

テキスト p.147 ページにある,関数の配置についての推奨 「呼び出される側の関数を前に,呼び出す側の関数を後ろに配置しよう」 に従えば,関数原型宣言は不要になります.

しかし,プログラムが複雑になると,この原則に従うのは難しくなります. たとえば,自作の関数定義がたくさんあり, 自作関数が自作関数を呼び出すということが頻繁に生じると, 関数定義の配置を決めることは面倒になります. それならば,関数原型宣言を書いてしまった方が簡単です. 以下の書籍の p.177 では,「関数プロトタイプを書くスタイルが正当」と述べています.

林晴比古 (2003) 新訂 新C言語入門 ビギナー編 ソフトバンククリエイティブ

ヘッダとインクルード

多くの Unix 系 OS での標準の設定では, ソースプログラムのコンパイル時に読み込まれるヘッダ <stdio.h> は, ヘッダファイル


/usr/include/stdio.h

です.ディレクトリ構造で一番上に位置するルートディレクトリの中に usr というディレクトリがあります. その下に include というディレクトリがあり, ヘッダファイル stdio.h はここに含まれています.

このファイルの中に記述されている putchar 関数の関数原型宣言を見るには, 文字列を検索する grep コマンドを使うとよいでしょう. ヘッダファイルは /usr/include/stdio.h であるとします. ターミナルで,


grep putchar /usr/include/stdio.h

と入力して [return] キーを押してください(putchar の前後に半角スペース). ヘッダファイル stdio.h の中で,文字列 putchar を含む行が表示されます. 下図では,一番上に putchar 関数の関数原型宣言が表示されています.

grep putchar /usr/include/stdio.h

関数の汎用性

このセクションについての補足説明は現在のところありません.

配列の受渡し

List 6-11 の main 関数では, 英語と数学の点数の入力を促す表示を行うために printf 関数を使用しています. 出力される「英語」という文字と「数学」という文字の位置を揃えるために, 「数学」での printf 関数に与える書式文字列の中に, 4つの半角空白が含まれています. 二重引用符で囲まれた書式文字列の直前にも半角空白が入っていますが, これはもちろん出力されません. プログラム中での「英語」と「数学」の位置をそろえるためにこうしているだけです.

配列の受渡しについて正確に理解するには,テキストにも書かれているように, ポインタ(pointer)の学習が必要です. 第10章の p.280 に,ここと同じ「配列の受渡し」というセクションがあります. それまで,テキストでも書かれているように, とりあえずの理解をしておいてください. ポインタを学習すると,max_of 関数の関数頭部は


int max_of(int *v, int n)

と書くのが本来の書き方だとわかります.

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

本章第1節で学習したように, 「関数間の引数の受渡しは,値渡しによって行われる」(p.140)のでした. たとえば,List 6-5 では,main 関数は自身が保持している変数 a と b の値をコピーして power 関数に渡していました. そのため,渡された値を power 関数が変更しても, main 関数が保持している a および b の値には影響しないのでした.

これに対して,何らかの関数を呼び出して配列を渡す場合には, コピーではなくもとの配列そのものが渡されます. よって,呼び出された関数において配列の値が書き換えられると, 呼び出した側が保持していた配列の値も書き換えられます. 同一の配列を2つの関数が共有しているからです.

正確には,配列の受渡しにおいてもコピーが行われているのですが, コピーされているのはポインタ(配列が記録されている記憶域のアドレス)であるため, 配列そのものが受渡しされていることと同じになります. ポインタについては第10章で学習します.

関数に配列を渡して,その関数が配列の内容を変更した後, 表示のコントロールは main 関数が行います(List 6-12 のように,表示用の関数を別に呼び出すことはあります). 表示は特定の配列に依存する部分が大きいからです. たとえば,関数に渡される配列の名前は様々ですから, 配列名の表示は main 関数がコントロールすべきです. 配列を操作する関数は,どのような名前の配列を渡されても, 同じ操作を行うようにします.

const 型修飾子(type qualifier)について, 誤解しやすい点を2つ注意しておきます.

const 型修飾子は, 型(たとえば int 型)を修飾して「読み出し専用」であることを意味します. List 6-12 での print_array 関数の仮引数宣言では, 「const int 型」の配列 v が宣言されています. 型を修飾しているのですから,「const int 型」の配列であって, const の「int 型の配列」ではないので注意してください.

もうひとつの誤解しやすい点は, 関数の仮引数での const 型修飾子は, その関数がその引数の値を変更しないということを述べているのであって, const 型の引数を渡すことを要求しているのではないということです. たとえば,List 6-12 では, main 関数での2つの配列 ary1 および ary2 は int 型であって, const int 型ではありません. これらの配列を,const 型修飾子があるため配列の要素の値を変更しない, print_array 関数に渡しています.

線形探索(逐次探索)

ある目的を達成するための手順のことを アルゴリズム (algorithm)と呼びます. IT 用語辞典 e-Words(http://e-words.jp/)では, アルゴリズムの意味を以下のように説明しています.

アルゴリズムとは、ある特定の問題を解く手順を、 単純な計算や操作の組み合わせとして明確に定義したもの。 数学の解法や計算手順なども含まれるが、 ITの分野ではコンピュータにプログラムの形で与えて実行させることができるよう定式化された、 処理手順の集合のことを指すことが多い。

データの中から特定のものを探すことを 探索 (search)と呼びます, List 6-13 から List 6-15 では,最も基本的な探索アルゴリズムである, 線形探索(linear search)を学びます.

もう慣れたとは思いますが,配列の要素の数と, 末尾要素の添字はひとつずれていることに注意してください. たとえば,List 6-14 の main 関数では, int vx[NUMBER + 1] と宣言していますから, 配列 vx の要素数は 5 + 1 = 6 です. この配列の末尾の要素には 番兵 (sentinel)が格納され,vx[5] としてアクセスできます. 検索対象となるデータは vx[0] から vx[4] でアクセスできます.

番兵を置き忘れると,key と一致する要素はいつまでも見つかりませんから, 無限ループになってしまいます(配列の要素数を超えて, 存在しない要素にアクセスすることになるので, どのような動作になるのかはわかりません). もし無限ループに入ってしまったら,ターミナルで, コントロールキーを押しながら [C] キーを同時に押してください. プロセスを止めることができます

List 6-15 の for 文はとてもすっきりしていますね. この for 文は,v[i] が key と一致しない限り, i をひとつずつ増やしながらループを回しています. 検索対象となったデータのどこかで key が見つかるか, 見つからずに番兵(key が格納されています)までたどり着くと, ループを抜けます. 番兵までたどり着いたときには,i の値は n になっています.

List 6-15 の for 文を,もう少し List 6-14 と似たものにするなら, 以下のように書くこともできます. ループの継続条件(制御式)が空白なので「無限ループ」になっています. どこかで key が見つかると,break 文でループを抜けます. 配列の最後の要素に番兵を置くことを忘れないように.


for (i = 0; ; i++)
    if (v[i] == key)
       break;

演習 6-11 は少し難しいですが,よい問題です. がんばってプログラムを書いてみてください. List 6-14 の実行例のようにデータ(1, 7, 5, 7, 2, 4, 7)と 検索キー(探す値:7)を入力すると,


7は3個含まれます。
vx[1] = 7
vx[3] = 7
vx[6] = 7

と表示されるようにしてください.

多次元配列の受渡し

演習 6-12 には誤植があります(初版で確認). 行列 a および b の積を格納する行列 c は,3行3列ではなく4行4列です. したがって,関数 mut_mul の宣言は以下のようになります.


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

この問題は演習 5-10(p.127)の関数版です. 数学での行列(matrix)を扱っています. 高校数学のカリキュラムから行列が消えてしまいましたから, 知らなければこの問題はスキップしてかまいません.

演習 6-13 は演習 5-12(p.127)の関数版です. 『解きながら学ぶC言語』(初版)で提示されている解答が想定されているならば, ポインタを学習した後でないとこの問題は解決できません.スキップしてください. 直観的に言えば,データは3次元配列として記録されており, ここから2次元配列の部分を取り出して関数に渡す必要があります. 『解きながら学ぶC言語』(初版)で提示されている解答には少しおかしな部分があり, 以下のような警告が出ます.ただし,実行プログラムは作られ,動作します.

incompatible pointer types

警告が出ないようにするには, 関数 mat_add および mat_print に配列(正確には,int[3] へのポインタ)を渡す部分を, 以下のように修正します.


mat_add(&tensu[0][0], &tensu[1][0], sum, 4);

mat_print(&tensu[i][0], 4);

6-3 有効範囲と記憶域期間

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

このセクションについての補足説明は現在のところありません.

記憶域期間

有効範囲(scope)という概念と, 記憶域期間(storage duration)という概念を, 混同しないように注意してください. これらの区別を理解するために,List 6-18 での変数 sx に注目してください. この変数は関数 func の中で宣言されていますから, この関数の中だけで有効です. 他の場所(func 関数の外)から参照することはできません. しかし,宣言で static が付けられていますから, この変数(sx)はプログラムの終了時まで保持されています.

テキスト p.162 の最後にある「この宣言は,代入ではなく初期化を意味しているのですから」 という文は,解説としておかしいと思います. 初期化を意味しているという点では,static int sx = 0; と, 次の行の int ax = 0; は同じです. 変数 sx の初期化が1度だけであるのは,static が付けられているからです.

演習 6-15 で作成する関数 put_count の宣言は,


void put_count(void)

としてください.テキスト(初版)では仮引数が空白になっています. 修正してください.