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;
}
なぜプロトタイプ宣言だけで良いかといえば,関数を呼び出す際には,差し当って,関数の名前,引数の型と個数,戻り値の型といった関数の形に関する情報があれば十分だからである.
関数本体の処理内容(定義)は,コンパイル後のリンク作業で結合され,実行ファイルが作られる.
この関数プロトタイプ宣言は,その関数の定義が「同じソースコード内には無い」場合に重宝される.
例えば,printf()
の定義は皆さんが作っているソースコードの中には無いですね.
実は,おまじないの #include
を記述している stdio.h という名前のヘッダファイル中に,printf
関数のプロトタイプ宣言が書かれている.
また,printf()
関数の定義は,事前にコンパイルされ,ライブラリーファイル(.a, .libファイル)としてまとめられており,これを自作のプログラムに結合する作業は,リンク処理で行われる.
通常はコンパイルすると,以下のように自動的に「リンク」処理まで行われる.
#include
の展開や #define
マクロの置き換え.printf(), scanf()
などの関数の機械語コードを結合する.#include
の行を削除してコンパイルしてみよう.printf, scanf
関数のプロトタイプ宣言を探し出せ.
一般的には,C言語の開発環境には「ライブラリ関数」と呼ばれる既成品の関数群が用意されている.
本来は,ライブラリ関数はコンパイラとは別の製品であるが,gccやbcc, Microsoft Cなど市販の or 無償利用できるコンパイラの多くは,ライブラリ関数とともに配布されている.
いままで用いてきた printf(), scanf()
は,ライブラリ関数の代表例である.
ライブラリ関数を利用するには,そのプロトタイプ宣言が書かれたヘッダファイルを必要に応じて #include
する必要がある.
これはほんの一例であるので,詳しくは各種メーカー提供のライブラリ関数リファレンスを参照のこと.
以下,いずれも外部サイトへのリンクです.
以下によく使われるライブラリ関数の例を示す.
printf(), scanf()
exit(), rand()
sqrt(), sin(), cos(), tan(), exp(), log()
fopen(), fclose(), fprintf(), fscanf()
strcpy(), strlen(), atoi(), atof()
malloc(), calloc(), free()
time(), clock()
ファイル入出力や,文字列操作などは,別の回に説明する.
算術関数ライブラリを実際に使ってみよう.
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;
}
乱数を発生させる関数 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種類ある(すべて重要).
return
による戻り値では,1つしか値を返せない)
*
や&
を書かなくても済む.)
値渡し | アドレス渡し(ポインタ変数) | 参照渡し,C++のみ |
|
|
|
ポインタ渡しの例と参照渡しの例に,同じアンパサンド(アンド)記号 &
が使われているが,異なる用法なので注意.
(同様に,アスタリスク *
記号も掛け算と同じ記号であるが,まったく意味が異なる.さらに細かく言えば,アドレス渡しの例内の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==0
で return
が 1)があり,ここに至って初めて値が定まり, 次々にリターンして(戻って)くる.
このように,再帰関数では処理の中で,必ず再帰呼び出しを終わらせるロジックを慎重に記述しなければならない.
const
などを組み合わせて使う.const
をつけておくと良いです.以下の問それぞれに対応するプログラムを作成しなさい.
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;
}
先のソースコードを,プロトタイプ宣言を利用する形に書き換えよ.
つまり,main()
関数がソースコードの上方に配置されるようにすること.
#include <stdio.h>
// ここに関数2つのプロトタイプ宣言を追加
// 最初にmain関数
int main(void)
{
// main 関数は変更しないこと
int x = 1, y = 3;
...
return 0;
}
/////////////////////////
// この下に関数の定義
/////////////////////////
void swap_val(...) // 値渡しバージョン
{
...
}
void swap_ref(...) // 参照渡しバージョン
{
...
}
おまけ:余力+時間があれば,swap_val(), swap_ref()
を別のソースファイル(.cpp)に移動させ,かつ,プロトタイプ宣言を別途ヘッダファイル(.h)にしてみよう.コンパイル時には,2つのソースファイルを同時にコンパイラに渡すこと.
戻り値として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;
}
乱数関数 rand()
を使って,整数の乱数を大量に発生させ,その数が偶数である確率を求めよ.
同様に,その数が素数である確率を推定せよ.
N = 10000;
またはそれ以上として,繰り返し乱数を発生させ,その値が偶数 or 素数であった回数を数え,最後にN
で割る.