前回までの理解度の確認のために,簡単な練習問題を解いてみよう.
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 など,広く用いられている他の言語においてもサブルーチン,プロシージャ,副プログラムと呼ばれる同様の機能がある)
プログラムに限らず,いろいろな場面で必要となる処理や,コード内で共通して用いられる動作(画面出力やキーボード入力),何度も繰り返される処理などは,あらかじめ作成しておき,必要に応じて使えるようにしておくと便利であり,作業効率も良い.
機械加工で例えれば,物を製作する際にネジやバネ自体の製作からはじめることはせず,既存の規格品を利用するのが普通である.
その場合は,ネジの直径やピッチなど規格を調べて,それに合うように設計する.
C言語の具体例として,これまで使用してきたprintf()
, scanf()
は,実は関数であるが,自分でこれらを作成する必要はなかった.
なぜならば,画面表示やキーボード入力などは基本的な動作であるため,自作せずとも初めから用意(=コーディングおよびデバッグ済)されている.
このような関数をライブラリ関数と言う.
ライブラリ関数は,必要なヘッダファイルの#include
文の記述により,プログラム中から自由に呼び出す(call)ことができる.
例えば,printf()
, scanf()
関数を使用するには,stdio.h (=Standard input/output)というヘッダを include することになっている.
今回は,ライブラリ関数の呼び出しだけでなく,自作の関数を定義する「作り方(関数の宣言と定義)」と,「使い方(関数呼び出し)」を学ぶ.
プログラム中で関数名の書かれている部分に処理が到達すると,元の関数内での処理から,呼び出した関数にジャンプする.
(これを関数の呼び出し(call)という.)
呼び出した先の関数では,あらかじめ用意されたコードがが実行され,完了すると呼び出し元に処理が戻る
(これをreturn,リターンという).
簡単に図で描くと,このような流れで処理が進む.
この図では,ライブラリ関数である printf(), scanf()
関数を,main()
関数から呼び出している(=呼び出し元という).
プログラミングを始めて一番最初に学んだ main()
も,関数の一つである.
したがって,C言語のプログラムは「関数の集合である」といえる.
main()
関数は,OSで実行ファイルを指定したときに,「この関数から処理が開始される」入口に相当する特別な関数(エントリポイント)として規定されている.
まず,いくつかの用語を説明するが,プログラムの具体例を見ると理解しやすい.
ソースコード中に書かれる関数の処理本体を定義 (definition)と呼ぶ.
関数定義の文法
戻り値の型名 関数名(型名 仮引数名1,型名 仮引数名2, ...)
{
//
// ここに処理を記述
//
return xxx; // xxxが戻り値
}
関数とは,例えて言えば「材料」を渡すと,「レシピ」に基づいて調理し,「料理」をアウトプットするようなものである.
C言語の文法では,
と呼ばれる.
以下,関数を作るにあたってのルールである.
int, float
など)以外で,かつ,既に定義されている別の関数名(main, printf, scanf など)以外.void
と書く.return
文を使うと,そこで関数の処理が終了し,呼び出し元に処理が戻る.この際に,戻り値と呼ばれる値を返すことができる.void
と書く.まず,以下のような簡単なプログラムを考える.
それぞれ順を追って,プログラムを作成・コンパイルして,動作させてみよう.
#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!
この例のように,異なる関数内では,変数が別の空間に存在するため相互参照できない.(この例では,ローカル変数のi
とn
)
そのため,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 |
|
|
自作の calc
という「関数」は,ソースコードの上方で定義されており,
整数型の「引数」を2つとり,整数型の「戻り値」を返す関数としている.
引数については,渡す側 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つの関数に多くの処理を詰め込むことはよくない.
例えば,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;
}
上記プログラムを実行して,処理の流れを確認しよう.
特に,関数を呼び出す・呼び出される側において
に注目. 引数の個数・型や,戻り値の有無は,双方で整合していなければならない.
void
キーワードを用いる.return
文で戻り値を指定するとともに,関数を終了させることができる.void
ですか?int
です.ただ,コンパイラによっては void でもエラーを出さないものがあります.int main(int argc, char* argv[])
と書きます.doSomething()
や, アンダーバーで区切るcalc_some_value()
などがあります.以下の問それぞれに対応するプログラムを作成しなさい.
まずは小手調べ・・・
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;
}
問題1の変数 a
を,「整数型」から「実数型」に変更せよ.
たとえば,0.5を与えると,0.125を画面に表示するようにせよ.
ヒント:変数 a の定義を int
から float
に変えるだけでは足りない.影響する箇所を順次,変更する必要がある.
キーボードから正の整数 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 で順に割って,最初に割り切れた数です.
実数 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;
}