前回までの理解度の確認のために,簡単な練習問題を解いてみよう.
while
文を用いて,画面に 1 から 100 までの整数を一行で表示してみよう.
実行例(出力値は正しいとは限らない): 1 2 3 4 ... 98 99 100
for
文を用いて出力してみよう.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()
関数から呼び出している.
プログラミングを始めて一番最初に学んだ main()
も,関数の一つである.
main()
関数は,OSで実行ファイルを指定したときに,処理が開始される「入口」に相当する特別な関数(エントリポイント)として規定されている.
したがって,C言語のプログラムは関数の集合であるといえる.
関数の処理本体を定義 (definition)と呼ぶ.
関数定義の文法
戻り値の型名 関数名(型名 仮引数名1,型名 仮引数名2, ...)
{
//
// ここに処理を記述
//
return xxx; // xxxが戻り値
}
関数とは,例えて言えば材料を渡すと,レシピに基づいて調理し,料理をアウトプットするようなものである.
C言語の文法では,
と呼ばれる.
以下,関数を作るにあたってのルールである.
int, float
など)以外で,かつ,既に定義されている別の関数名(main, printf, scanf
など)と重複しないもの.void
と書く.return
文を使うと,その位置で関数の処理が終了し,呼び出し元に処理が戻る.この際に,戻り値と呼ばれる値を返すことができる.void
と書く.まず,以下のような簡単なプログラムを考える.
よく吠える犬がいます.吠える動作を,関数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 |
|
|
ここで,自作の calc()
という関数は,整数型の引数を2つとり,整数型の戻り値を返す関数としている.
main()
にたいして,ソースコードの上方で定義されている.
引数については,渡す側 main()
と,受け取る側 calc()
で,同じ個数,同じ順序,それぞれ同じ型で書く必要があり,
main
関数内での 関数呼び出し calc(a,b)
中の変数 a, b
を実引数(じつひきすう)calc
の定義に書かれている変数 c, d
を仮引数(かりひきすう)
と呼ぶ.
関数calc
内では,変数 int c
および int d
が「仮に」与えられたとして処理を記述することから,「仮」引数と呼ぶ.
関数が呼び出され処理がcalc
に移ってきた時点で,実引数の値(a,b)が,仮引数(c,d)にコピーされて渡されてくる.
仮引数のリストは,必ず型名と変数名をペアで書く必要がある.
通常の変数宣言のように,int c, d
と省略することはできず,int c, int d
と記述する必要がある.
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
による値の戻りをよく理解したうえで,練習問題に取り組んでみよう.
実行例: x = 10.5, y = 2.2 のとき norm = 115.09
main()
関数を変更してみよう.my_abs()
を作成してみよう.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;
}
なぜプロトタイプ宣言だけで良いかといえば,関数を呼び出す際には,差し当って,関数の名前,引数の型と個数,戻り値の型といった関数の形に関する情報があれば十分だからである.
関数本体の処理内容(定義)は,コンパイル後のリンク作業で結合され,実行ファイルが作られる.
この関数プロトタイプ宣言は,その関数の定義が「同じソースコード内には無い」場合に重宝される.
例えば,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)があり,ここに至って初めて値が定まり, 次々にリターンして(戻って)くる.
このように,再帰関数では処理の中で,必ず再帰呼び出しを終わらせるロジックを慎重に記述しなければならない.
void
キーワードを用いる.return
文で戻り値を指定するとともに,関数を終了させることができる.const
などを組み合わせて使う.void
ですか?int
です.ただ,コンパイラによっては void でもエラーを出さないものがあります.int main(int argc, char* argv[])
と書きます.doSomething()
や, アンダーバーで区切るcalc_some_value()
などがあります.const
をつけておくと良いです.以下の問それぞれに対応するプログラムを作成しなさい.
実数 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つの値を交換する関数,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;
}
戻り値として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
で割る.