同じような処理が何回も必要な場合、同じようなプログラムをだらだら記述することなく簡単にして見やすくするために、その処理の部分を関数として部品化し、必要な時にその部品を利用するようにすればスッキリしたプログラムになります。この、イメージ図を図1に示します。
図1
a = 5; |
a += 100;
a = a * 10 * sqrt(3); a = a + (0.5 * a); printf("%d\n",a); |
b = 20; |
b += 100;
b = b * 10 * sqrt(3); b = b + (0.5 * b); printf("%d\n",b); |
c = 520; |
d = c - 150; |
d += 100;
d = d * 10 * sqrt(3); d = d + (0.5 * d); printf("%d\n",d); |
a = 5; |
Keisan(a); |
b = 20; |
Keisan(b); |
c = 520; |
d = c - 150; |
Keisan(d); |
一つのプログラムには実にさまざまな処理があります。例えば次のような例を考えてみましょう。(※例で出てくる関数は筆者が仮想した関数名であり実際には存在しません。)
「携帯電話に登録してある電話帳をパソコンに取り込むプログラム」 必要な機能 ○パソコンと携帯電話との接続
これらの機能をそれぞれ関数にし、プログラムを組むと以下のようになります。 main( )
/*携帯電話からパソコンへの電話帳読み出し*/
/*取り出した電話帳をパソコンディスプレイへ表示*/
/*パソコンの電話帳ファイルへ保存*/
次に、各機能をまた細かく分割します。 (例)Connect_to_cellular関数(パソコンと携帯電話との接続) ○携帯電話とパソコンをつなぐケーブルが物理的につながっているかの確認
以上のように一つの関数(処理単位)をまた細かく分類することが出来ます。
|
上の携帯電話の例における関数群の親子関係を図にしてみたのが下の表です。右に行くほど階層が深くなっていきます。
main( ) 関数が一番親であることに注意して下さい。
main( )
| |---> Connect_to_cellular( ) | |---> check_cable( ) | | |---> check_voltage( ) | | | |---> read( ) | | | |---> write( ) | | | |---> printf( ) | | | ・・・(続く)・・・ | | |---> check_port( ) | | ・・・(続く)・・・ | |---> check_power( ) | |---> check_function_output_telnumber( ) | |---> ・・・(続く)・・・ | ・・・(続く)・・・ |---> Get_telephone_number( ) | |---> read_from_IO( ) | |---> ・・・(続く)・・・ | ・・・(続く)・・・ |---> Display_telephone_number( ) | |---> ・・・(続く)・・・ | ・・・(続く)・・・ | ・・・(続く)・・・ |
では、この関数化の問題を「車の工場」で考えてみましょう。車を組み立てる工場を一つのmain関数と見立てます。車を工場で組み立てるとき、鉄板、ガラス、ゴム、皮、電気コードなど、車を構成する原材料を納入してきて一つの工場で一から作ったのでは非常に効率が悪く、「エンジンの性能を上げる」といった要望が出たときにもすぐに対応することが出来ません。工場の製造ラインも大きく変更する必要があります。一方、エンジン、ボディ、シート、駆動パーツなどの製造を全て外部工場に委託し、本社工場ではパーツを組み立てて車を製造するようにすれば明らかに効率的です。また「性能の変更」にも本社工場の生産ラインの変更はほとんど無しに柔軟に対応することが出来ます。
また、プログラムの生産効率にも関係してきます。遊びではなく仕事としてプログラムを作成する立場になった場合、通常は大掛かりなプログラムになるため複数人の人間が協力して作ります。複数人で作る場合は作ろうとしているプログラムを機能のまとまりごとに分割してそれぞれのパーツの製作を一人一人に依頼します。この分割する単位を「関数」とすることで簡単に共同作業することが出来ます。
もう一つ、「(その1)プログラムを簡潔にする」にもかぶりますが、処理単位ごとに(分かりやすい関数名を用い)関数化してプログラムを記述することで、コメントを多用しなくてもわかりやすいプログラムを書くことが出来るようになります。
ここまで言っても、なかなかイメージがつかみ難いのではないでしょうか?この関数の利用方法に関しては経験がモノをいいます!!ですから、最初はとにかくプログラムを処理単位に分析する力を持ち、無意味だと感じる小さな処理単位でも関数化してみることです。経験を積むに従ってそれが自然になってきますから。
関数を自分で作る場合、次のような手順を用います。宣言と定義の合計二回書く必要があります。違いが分かりますか?
1)宣言部
【戻り値の型】 【関数名】(【引数】,【引数】,・・・); (例)
2)定義部 【戻り値の型】 【関数名】(【引数】,【引数】,・・・)
(例)
|
自分で関数を作る場合、宣言部(B)と定義部(D)にそれぞれ必要事項を記述するのを忘れずに!(下の例のプログラムは全く理解できなくても今は結構です)
コンパイラへの命令部(A)
(ヘッダファイルの読み込み指示など) (例)
|
関数・グローバル変数の宣言部〈B〉
(例)
|
main関数部(C)(実際にこの部分の記述通りの流れで処理が行われる)
(例)
|
関数定義部(D)
(自分で作った関数を表現する場所) (例)
|
ここで重要なのは、関数がエラーを起こした時の処理です。上の図では青色の部分です。初心者のうちはこのエラー処理が取っ付きにくくて、「エラーが起こってもワシは知らん!!」みたいな事を考えるかもしれません。確かに、エラーが起こっても何とかプログラムが動いているようなことが多々ありますが、プログラムが大きくなると「どこで計算間違いをしているのか」とか、そういうプログラム記述上の間違い(バグと言います)を探す(この探す作業をデバッグと言います)のに大変な苦労を強いられます。時間も掛かります。よって、頭が若いうちにエラー処理の概念を持っておくことは大切だと思います。ちなみに、エラー処理は関数利用時に限らず、普段でも必要に応じて付け加えましょう。
(注)エラーとは簡単に言うと「処理が失敗した」ことです。
さて、関数は宣言・定義と、二ヵ所で書かなくてはならないといいました。図2を見てもらえば分かりますが、プログラムは大きく分けて4つの部分に分けられます。C言語で書かれたプログラムを解読するコンパイラというものは、上から順番に読んでいき解読します。もし、(B)の部分で関数宣言をしなかったとしたら、コンパイラは(C)の部分で呼び出されたGetStringLength100関数の意味が分からなくてエラーを出します。つまり、main関数内で使用される自作の関数は、それよりも上の部分(B)であらかじめ「こういった関数がありますよ!詳細な定義は(D)の部分でしてますよ!」ということをコンパイラに教えておかなくてはなりません。では、「(B)での関数宣言を記述せず、いきなり(B)の部分に(D)の詳細な定義部を持ってきて記述すればどうか?」という事が思いついたあなたはプログラマーの素質があります(笑)結論から言うと「可能」です。しかし、そういった書き方は好ましくなく、ある意味で美しく無くなります。プログラムは芸術作品です!美しく仕上げましょう!ということで、芸術家を目指すあなたは、二ヵ所に書く運命にあります。(本当は、もっと深い意味があるのですが・・・)初心者の方はここまで言うと「?」となっているかもしれませんが、こういうのはとにかく「慣れ」です!習慣づけて慣れましょう!!
関数には「引数」と「戻り値」という二つの概念があります。簡単な例を数学の方程式で説明します。
y = ( x - 5 ) +( a - b)
この時、x、a、b は既知のもの、y は未知のもの(つまり、求めたい数字)とします。この方程式を解く時、すでに分かっている x、a、b の値をそれぞれの変数に代入することによって y の値が求まります。x、a、b といった、変数に代入する値をC言語では引数(ひきすう)と呼びます。また、方程式の計算の結果得られる値 y をC言語では戻り値(もどりち)と呼びます。何となく、分かりましたか?
では、上の方程式をC言語の文法を用い、関数で表現すればどうなるのでしょう?
宣言部 :
float Keisan(float ,float ,float );
main関数部 :
y = Keisan( x , a , b );
定義部 :
float Keisan(floatl,float
m,float n)
{
float kekka;
kekka = ( l - 5 ) + ( m - n);
return
kekka;
}
このように表現できます。
紫色の部分が引数、茶色の部分が戻り値となります。
引数はいくつでも並べることが出来、それぞれはコンマで区切ります。main関数内で関数を利用する際は引数の型を指定する必要はありません。関数定義部では青色文字部分を見てもらえば分かるように、敢えて、x、a、b 以外の変数名を使っています。もちろん、x、a、b を使ってもOKですが、ここから言える重要なことは、「実際にmain関数で使われる引数の変数名をそのまま使わなくても全く問題ない!」という事です。つまり、main関数と関数定義はある程度切り離して考えることが出来ます。また、main関数内では、上のプログラムのように与える引数が変数であっても良いし、
main関数部 :
y = Keisan( x , 3.14159 , 50
);
y = Keisan( 4 , 3.14159 , 50
);
のように、変数と実際の数字を組み合わせたり、数字のみ単独で使用したりして呼び出すことが出来ます!
こうして考えると、関数はブラックボックスであるという理由が分かると思います。
茶色の部分で return という言葉(キーワードの一つ)が出てきました。これは、読んで時のごとく【戻す】ことを意味します。return が使われると、その時点で関数を終了し、return に続く値(この例では kekka )などを結果として返します。returnは値を返すだけでなく、関数を終了する手段としても用いられるというわけです。例えば、エラーが起こった時に return を使い強制終了させることも出来ます。戻り値がある場合は必ず return をどこかに記述し、なお且つ上のプログラムの赤色部分のように戻り値の型を指定しておく必要があります。
さて、「関数が値を返す」とはどういうことでしょうか?言葉で説明してもイメージを掴みにくいと思いますので、とりあえず下に示した処理の流れを見てください。
y = Keisan( 10.5 , 6 , 5 );
↓
float Keisan(floatl,float
m,float n)
{
float kekka;
kekka = ( l - 5 ) + ( m - n);
return kekka;
}
↓
float Keisan( 10.5
, 6 ,
5 )
{
float kekka;
kekka = ( l - 5 ) + ( m - n);
return kekka;
}
↓
float Keisan( 10.5
, 6 ,
5 )
{
float kekka;
kekka = ( 10.5 - 5 ) +
( 6 - 5);
return kekka;
}
↓
float Keisan( 10.5
, 6 ,
5 )
{
float kekka;
kekka = 6.5;
return kekka;
}
↓
float Keisan( 10.5
, 6 ,
5 )
{
float kekka;
kekka = 6.5;
return 6.5;
}
↓
y = {Keisan( 10.5 , 6 , 5 ) = 6.5};
↓
y = 6.5;
見てもらえば分かるように、戻り値のある関数を呼び出すと結果的にその関数がある一つの値に置き換わります。この値が「戻り値」なのです。上の例では戻り値は6.5です。結果が6.5とちゃんと小数点型の値に置き換われるのは戻り値の型を float
Keisan(・・・) という具合に「浮動小数点型」として定義しているためです。これを int (整数型) とすると、小数点以下が切り捨てられ6になると考えられます。
よって「関数が値を返す」とは「関数がある一つの値に置き換わる」と覚えてください。
では、戻り値が必要無い場合はどうすれば良いのでしょうか?この場合、戻り値の型には void という特殊な型を指定します。これは、「無」を意味するキーワードです。戻り値が必要無い場合、関数に return は記述してはいけません!(当たり前ですが・・・)また、引数が必要無い場合は引数を指定する場所で void とだけ記述すれば良いのです。
(例)
void ConnectToInternet(void);/*引数無し、戻り値無し*/
void InputMyAddress(char buffer[200]);/*引数 文字列、戻り値なし*/
int PlayMusic(void);/*引数無し、戻り値 整数*/
上の例ではちょっと高度な関数名を想定してみました。授業では統計計算や数値計算などの面白くない関数ばかり作る運命にあるかもしれませんが、世の中には面白い処理をする関数が沢山あります。高度な知識が身についたら、絵を描く関数を作ったり、インターネットでメールを出す関数を自分で作ったり、実用的なものを作れるようになります。それはみなさんのやる気次第です。
あと、関数の名づけ方ですが、上の例のように英語(かローマ字)で分かりやすい関数名をつけることをお勧めします。
void a(int x,int y);
こんな関数があって、何をする関数か想像が付きますか?(笑)
最初は、キーボードに慣れてなくて打つのが面倒くさくて、ついつい短い関数名(変数名でも言えることですが)を付けがちです。しかし、上達するに従って「分かりやすい、長い関数(と 変数)名をつける」ように心がけるようにしてください。
※関数の名付け方ですが、アルファベットは大文字・小文字が区別されます。日本語は使えません!また、This
is a pen(・・・) といったように、スペース(空白)は関数名に使えませんが、This_is_a_pen のように代わりに通常は慣例として”_”(アンダースコア)記号を使います。
上記の説明では関数に「変数」ではなく「具体的な値」を与えることになります。
double x = 1.42;
double y = 356.003;
double z = 4.2;
double s;
s = sum_3( x , y , z );
↑とすると、関数側から見れば次のように解釈されます。↓
s = sum_3( 1.42 , 356.003 , 4.2 );
では、与えた引数から3を加算する関数add_3(
) を作ってみましょう。
int x = 4;
x = add_3( x ); |
main( ) 関数部 |
int add_3( int value)
{ return value + 3; } |
関数定義部 |
ここで、main( )関数部の次の部分は x が2回出てきて面倒臭くありませんか?
x = add_3( x );
これを
add_3( x );
に置き換えた場合、変数 x の値は関数実行後もオリジナルの値 4 のままです。
add_3( x );
とするだけで、変数 x の値を 3 加算した 7 にする方法はないのでしょうか。
この方法がこれからお話する「メモリのアドレスを介した情報の受け渡し」なのです。
さて、これは「スコープ(可視範囲)」という概念でしてC言語に備わっている一つのルールです。
プログラム中において変数は2ヶ所(関数の内部と外部)で定義することが出来ます。そして、それぞれに違った性質を与えます。
次のプログラムを見てください。
プログラム | test_1
の範囲 |
test_2
の範囲 |
test_3
の範囲 |
#include<stdio.h>
int test_1; void whoareyou(int);
|
|||
main( )
{ int test_2 = 4; whoareyou(test_2); } |
|||
void whoareyou(int test_3)
{ if(test_3 == 9) printf("猫\n"); else printf("I don't know\n"); } |
test_1,test_2,test_3 の各変数はそれぞれ違う場所で宣言しました。
test_1のようなmain( ) 関数の始まる前に宣言する変数をグローバル変数(または外部変数)と呼びます。
test_2,test_3のようなmain( ) 関数を始め、各種関数内で宣言する変数をローカル変数(または自動変数)と呼びます。
グローバル変数とはプログラム中どこからでもその値を知ることが出来る(アクセスできる)変数です。
ローカル変数とは変数を定義した関数内でしか値を知ることができない変数です。
これはテストに良く出るので覚えておいてください(笑)
上記の例で各変数を利用できる範囲を示したのが表中赤色の部分です。test_1 だけ全体に渡って見渡せることに注意してください。
こんなルールを用いるのには「メモリの有効活用」という側面もあります。ローカル変数の場合はその関数が処理している時間だけメモリを確保して利用します。よって、限られた物理的メモリを沢山の変数で使いまわせることにつながります。
「与えた引数から3を加算する関数add_3( )」という例を先ほど示しましたが、ここで出てきた問題(引数の変数値を直接操作する)は変数の存在するメモリのアドレスを関数に教えてあげることで解決できます。「アドレスを教える?」とはどういうことでしょうか?「ポインタ」の項目にもヒントがたくさん含まれていますのでピンと来ない人はもう一度読み返してください。
関数にアドレスを教えてあげると、通常はスコープの問題で見えない変数も読み書きできるようになります。なぜなら、どの関数からも原則としてそのプログラムに割り当てられたメモリ範囲全体にアクセスすることが可能だからです。例えばある一つの関数内でchar型変数「moji」を宣言したとします。その時メモリ上のアドレス3510番地から1バイトをこの変数用に確保しました。その関数内で変数「moji」を利用したい場合、「moji」としてやれば自動的に3510番地から1バイトの部分を読み書きしてくれます。しかし、違う関数内から変数「moji」の値を読み書きしたい場合に「moji」と指定しても「moji」がメモリ上のどこにあるのか「スコープ」の関係で分かりません。しかし、「moji」ではなく「3510番地から1バイト」と指定してやればその変数の値を読み書きすることが出来るのです。
以下に、入力値に3を加えた値を返す関数 add( ) を使ったプログラムを作ってみました。
#include<stdio.h>
int add(int ); void main(void)
int add(int num)
|
5
8
となります、
これと等価なプログラムを「引数にアドレスを与える方法」で作り変えてみました。2つ出来ましたが、結果は同じです。
#include<stdio.h>
void add(int *); void main(void)
void add(int *num)
|
#include<stdio.h>
void add(int *); void main(void)
void add(int *num)
|
上と下のプログラムの違いは、呼び出し元であるmain関数で、add関数に与えているアドレスが指し示す変数が通常変数であるかポインタ変数であるかの違いです。
上ではdataを通常の変数として宣言しています。よってadd関数を呼び出すときは引数にdata変数のアドレスを参照する修飾記号&を付けます。
逆に下の例ではdataをポインタ変数として宣言しています。つまり、dataはアドレスの入れ物です。よってadd関数を呼び出すときは修飾記号を付けません。
では、具体的にどういうやり取りがされているのか、時間を追って見ていきましょう。
void main(void)
{ int data = 5; printf("%d\n",data); add(&data); printf("%d\n",data); } |
int型変数dataが宣言され、値5が格納されます。
便宜上、data変数が存在するメモリ上のアドレスを62100番地と仮定します。 |
void main(void)
{ int data = 5; printf("%d\n",data); add(&data); printf("%d\n",data); } |
変数dataの値が画面に表示されます。 |
void main(void)
{ int data = 5; printf("%d\n",data); add(&data); printf("%d\n",data); } |
+3の計算するために関数addを呼び出します。おっと、修飾記号&が付いていますね。この場合、引数にadd(5);として数値を与えるのではなくアドレスを与えなければなりません。 |
void main(void)
{ int data = 5; printf("%d\n",data); add(62100); printf("%d\n",data); } |
コンピュータはこのように解釈します。 |
void add(int *num)
{ *num += 3; } |
呼び出し先の関数です。 |
void add(num = 62100)
{ *num += 3; } |
ポインタ変数numにアドレス62100番地が代入されました。 |
void add(int *num)
{ *62100= (*62100 → 5)+3; } |
62100番地に修飾記号*が付いていますね。この場合、右辺では62100番地にある値5を意味します。 |
void add(int *num)
{ *62100= 8; } |
62100番地に8が格納されます。この関数の処理はここで終わりなのでmain関数に戻ります。 |
void main(void)
{ int data = 5; printf("%d\n",data); add(&data); printf("%d\n",data); } |
変数dataには値8が格納されていますので8が画面に表示されます。 |
このように関数を呼び出す際引数にアドレスを与えることはプログラムをしていく中でよくあることです。特に、文字を扱ったり多くの数値データを扱うときなどは必須です。
ですから、理解できるまでひたすら反復練習を繰り返し慣れることが大切です。
「こういった機能が欲しい!」といった時、例えば音を鳴らしたいとか、四角形を描きたいとか、配列を数字の小さいもの順に並び替えたいという要望に応えてくれる関数が標準で結構用意されています。例えば、
int rand(void);
これは乱数(適当な整数値)を発生させる関数randです。引数は無し、戻り値は発生した整数値です。
こういった関数は標準のC言語関数として用意されているものでも数百あります。VisualC++のヘルプ機能でこれらの関数が辞書形式で説明してありますし、本屋さんでも辞書形式の関数ハンドブックなるものを置いてありますので一冊あったら重宝すると思います。ちなみに、これらのコンパイラ・メーカー側が用意している関数達を俗に「ライブラリ関数」と言います。
これらの関数ハンドブックにはその関数を使う際に必要な情報がたくさん載っています。
○引数とその意味
○戻り値
○関数が定義してあるヘッダファイル名
○関連する機能を持つ関数達の紹介
○使用例
このように最初から用意されている関数を利用する際に特に重要になってくるのが3つ目の「関数が定義してあるヘッダファイル名」になります。
#include<stdio.h>
と書きますよね?実はあれはヘッダファイルという、関数を定義してあるファイルを読み込む命令なのです。
stdio.h
このヘッダファイルは「STtanDard Input/Output」の頭文字から来たファイル名です。英語ではなんとなく難しいですが、「標準の入出力関数」という意味です。
入出力とはキーボードとコンピュータの関係を代表とする標準入力、ディスプレイとコンピュータの関係を代表とする標準出力からなります。
stdio.hファイルではprintf関数やscanf関数などの標準的な入出力関数を定義してあります。
では、さっきの乱数を発生させる関数randはどこに定義してあるのでしょうか?VisualC++のヘルプ機能で調べると次のように書いてありました。
疑似乱数を生成します。
int rand( void ); 関数 必要なヘッダー 互換性
戻り値 疑似乱数を返します。返すべきエラー値はありません。 解説 0 から RAND_MAX の範囲内の int 型疑似乱数を返します。srand 関数を使って疑似乱数ジェネレータを初期化してから、rand 関数を呼び出してください。 |
ヘルプで調べることでstdlib.hが必要なことが分かりました。
使うときは次のようにします。
#include<stdlib.h>
たったこれだけプログラムに追加するだけでrand関数が使えるようになります。
同様に、ヘッダファイルにもたくさんあります。理工系の人は計算に特化した関数を集めたmath.hなどは良く使うと思います。
Windows環境ではどのソフトもボタンがついてたり文字の大きさや背景色を変えれたりと、ソフトが見た目に華やかですよね。
学校の授業でやるモノクロの文字だけの世界とは程遠いように思えますが、少し経験を積みさえすればこういったマウスだけでも処理を進められる操作性の良いプログラムを組むことが出来るようになります。試しに今やって見ましょうか?
今から紹介する関数は俗に言う「メッセージボックス」というもので、コンピュータが質問してきてそれに利用者が応えるという機能を提供します。
プログラム自体はとても簡潔で簡単なものです。みなさんもアレンジしてメッセージボックスを活用してください。
関数名
int MessageBox( 0 (厳密には違いますので注意!) , 表示する文字列 , タイトル ,メッセージボックスのスタイルを定義する整数値 ); 必要なヘッダファイル Windows.h 戻り値 選択スタイルのメッセージボックスの場合、その選択した結果を数値として返す |
実際のプログラムです。
#include<stdio.h>
#include<windows.h> void main(void)
if(response == 1) MessageBox(0,"OKですな。","結果報告",0);
|
response変数にはメッセージボックスの選択結果を整数として格納しています。
実行結果は自分の目で確かめてください!
なお、このプログラムはVisualC++でないとコンパイルできません!!
というのは、LSIやgccといったコンパイラにwindows.hというものが定義されていないからです。
動かしたい人は学校でやりましょう!
また、Windows環境を駆使したプログラムが作成したくなった人は玉城先生のプログラミングの授業で先生に相談してください。
※問題が出来たら、各ボックスの右端の下矢印ボタンを押せば答えが出ます。
問題1
(1)値を3つ受け取り、それを表示する関数(Hyouji)、(2)整数の値を3つ受け取りその合計を返す関数(Goukei)、の二つを定義しましょう。