9.
関数を作る
関数を作る
・*:..☆ プロローグ ☆..:*・
それほど昔ではない頃のことです。ある初心者のプログラマーが、湖のそばでプログラムを作っていました。ところが手が滑って作っていた関数を湖に落としてしまったのです。プログラマーは困ってシクシクと泣きました。すると湖の中から神さまが出て来てきました。神さまはピカピカに光る金の関数を持っていました。
「お前が落としたのは、この金の関数か?」
「いいえ。わたしが落としたのは、そんなに立派な関数ではありません。」
神さまは、次に銀の関数を出しました。
「では、お前が落としたのは、この銀の関数か?」
「いいえ。そんなに整った関数でもありません。」
「では、お前が落としたのは、この関数か?」
神さまが三番目に見せたのは、ちょっと出来の悪い関数でした。
「そうです、そうです。拾って下さってありがとうございます。」
「そうか、そうか。お前は正直なプログラマーだな。」
神さまは感心して、金の関数も銀の関数も男にくれました。喜んだ男は、この事を友だちのプログラマーに話しました。友だちはうらやましがって、おれも金の関数をもらおう、と、湖へ出かけました。そして、わざと出来の悪い関数を湖に投げ、うそ泣きを始めました。すると、神さまが出て来て金の関数を見せました。
「お前が落としたのは、この金の関数か?」
「そうです。金の関数です。うっかり湖に落としてしまったんです。」
それを聞くと神さまは苦笑いをして、こう言いました。
「このうそつきの欲張り者め。でも、まあ、出来の悪い関数を使うくらいなら誰かの力をかりた方が良い。プログラミングとはそういうものさ。」
神さまは男に金の関数を渡すと、湖の中に戻って行きました。
最初にも述べましたが、C言語は、関数によって全体が構成されるプログラム言語です。そして、C言語のプログラムを作るという事は、関数を作るという事です。
一般的に、1つの関数は、それほど大きなものにはしません。コードが 1000 行にもなる関数は、かなり大きな関数であると言えるでしょう。作成するプログラムの大きさに比例して、関数が大きくなるわけではなく、関数の数が増えていくのが普通です。
関数の設計は非常に重要で、どのような機能の関数を作るかによって、開発効率、実行効率、メンテナンスの良し悪しが変わります。
ここでは、自分で関数を定義し、利用する方法について学習します。
9.1. 関数の定義
関数の定義
関数の定義は、次のように記述します。
戻り値型 関数名( 引数リスト )
{
処理
}
-
戻り値型
関数は、関数の呼び出し側に対し、1つの値を返すことが出来ます。これを戻り値と呼び、ここでは、その値のデータ型を指定します。
-
関数名
関数を識別するための名称です。
-
引数リスト
関数に渡されるデータが何であるかを定義します。
-
処理
変数宣言や文を記述します。
return 文と戻り値
return 文と戻り値
return 文は次のように記述します。
return;
または、
return 式;
return 文は、次の2つの機能を持っています。
- 関数の処理を終了し、呼び出し側に戻る。
- 戻り値を与える。
return 文が実行されると、それ以降に記述されている処理を実行せず、ただちに呼び出し側に戻ります。その際、return の右側に記述されている式の値が戻り値となります。この値のデータ型は、定義した戻り値型と一致していなければなりません。
関数が return 文を含まない場合には、最後の文が実行された時点で呼び出し側に戻ります。
戻り値型の指定は、省略してもかまいません。省略した場合には int であると見なされます。しかし、出来るだけ省略はせず、何らかのデータ型を指定した方が良いでしょう。
戻り値の無い関数を作成する場合には、void を指定します。その場合は、呼び出し側で戻り値を参照することはできません。また、return の右側には式を記述しません。
return文の使用例:
int sum( int x, int y )
{
return x + y;
}
void main()
{
printf( "12と16の合計は%dです\n", sum( 12, 16 ) );
}
引数リスト
引数リスト
呼び出し側から関数に渡されるデータを、仮引数、または単に引数、あるいは、パラメータと呼び、その定義を引数リストとして記述します。
引数の定義は、変数の宣言と同じように、データ型と引数名から成ります。2つ以上の場合にはカンマ‘,’で区切って記述します。
例:
( int x, int y, char ch, double d )
関数の呼び出し側では、引数リストに合わせて値を渡さなければなりません。渡される順番は、記述した順番と同じになります。
void myfunc( int x, int y, char c, double d )
{
printf( "x=%d, y=%d, c=%c, d=%f\n", x, y, c, d );
}
void main( void )
{
myfunc( 7, 13, 'P', 3.14 );
}
呼び出し側から関数に渡す値を、実引数、または単に引数と呼びます。
引数が必要の無い関数の引数リストは、空にするか void と記入します。
9.2. 戻り値
戻り値
戻り値のある関数の呼び出しでは、関数呼び出しの記述、つまり、関数名から小カッコ ( ) までが、戻り値になると考えてください。
int 型の関数であれば、その関数の呼び出しは、int 型の値と同じ意味を持ちます。
func_a, func_b, , , , の戻り値が int 型であるとすれば、次のようなコードを書くことが出来ます。
int i;
int x;
int a[10];
for( i = func_a() ; i < func_b() ; i += func_c() )
printf( "%d, %x", func_d(), func_e() );
switch( func_f() )
{
case 3:
x = func_g();
break;
case 4:
x = func_h();
break;
}
while( func_i() )
a[ func_j() ] = x;
戻り値のある関数を呼び出すのであっても、必ず戻り値を利用しなければならないわけではありません。戻り値を利用するかどうかは、呼び出し側にまかされます。
printf 関数は出力した文字数が戻り値になっていますが、あまり利用される事はありません。それは、単に文字列を出力するという目的で使用されることが多いからですが、出力された文字数を知りたいのであれば、戻り値から得られます。
以下の例では、printf 関数の戻り値を使用しています。
void main( void )
{
printf("は、%d文字です\n", printf("Hello World"));
}
ポインタの戻り値
ポインタの戻り値
ポインタを戻り値にする場合には、注意しなければなりません。戻り値のポインタが、関数を終了した後でも有効でなければならないからです。
以下の関数は問題なく使用できます。
int* larger( int* p1, int* p2 )
{
if( *p1 > *p2 )
return p1;
else
return p2;
}
戻り値のポインタは、引数として渡されたものであり、関数が終了した後でも有効です。
以下の関数は障害を引き起こします。
int* sum( int n1, int n2 )
{
int a = n1 + n2;
return &a;
}
戻り値のポインタは、関数内の変数 a のアドレスなので、関数が終了した後では使用できません。
関数の呼び出し側で、戻り値のポインタを使用しても正しい結果は得られません。
9.3. 引数
引数
引数は、呼び出し側の実体ではなく、値のコピーが渡されます。この方法を“値渡し”と呼びます。
呼び出し側に変数 x があり、その値が 6 であったとしましょう。ある関数に x を引数として渡した場合、関数側に渡されるのは、変数 x ではなく、値が 6 である x のコピーなのです。
これは、次のようなコードで確認できます。
void myfunc( int x )
{
x = 7;
}
void main( void )
{
int x = 6;
mufunc( x );
printf( "x = %d\n", x );
}
ここで、main で使用している変数名も、myfunc の引数名も、両方とも x としていますが、ここでの名前に関連はありません。x と y としても同じです。
変数 x は、初期値を 6 としています。その後、関数 myfunc を呼び出しています。myfunc では、引数 x に 7 を代入します。
myfunc を呼び出した後で、x の値を表示します。結果は“x = 6”と表示されます。
これは、myfunc の引数 x が、main の変数 x そのものではなく、x の値をコピーしたものであるからです。
ポインタの引数
ポインタの引数
引数には、ポインタ型を用いることも出来ます。
ポインタ型の引数を使用する理由は、主に次の2つです。
配列はポインタとして表されているので、関数に配列を渡すのであれば、ポインタ型が引数になります。
また、間接演算子‘*’を使用することによって、ポインタが指し示す先にアクセスできるので、呼び出し側の変数の値を変えることができます。
仮引数の定義では、ポインタ型を使用して int* pvalue のように記述するのですが、配列とポインタの関係から、次のようにも記述できます。
int pvalue[]
これは、記述方法が異なるだけでプログラムとしての違いはありません。
例:配列を渡す場合
void myprint( char* ptext )
{
printf( "%s\n", ptext );
}
void main( void )
{
char hello[] = "Hello World";
myprint( hello );
}
例:呼び出し側の変数の値を変える
void pow2( int* x )
{
*x = *x * *x;
}
void main( void )
{
int x = 3;
pow2( &x );
printf( "x = %d\n", x );
}
9.4. 再帰
再帰
関数は、その関数の処理の中で、自分自身を呼び出すことが出来ます。
以下は、1 から、指定された 1 以上の整数までの合計を求める関数です。
int nsum( int n )
{
if( n > 0 )
return nsum( n - 1 ) + n;
else
return 0;
}
void main( void )
{
printf( "sum = %d\n", nsum( 10 ) );
}
関数 nsum は、引数 n が 0 より大きければ、nsum( n - 1 ) の戻り値に n を加えた値を返します。nsum( n - 1 ) は、1 から n - 1 までの合計ですから、それに n を加えれば 1 から n までの合計が得られます。
引数 n が 0 以下の場合には、nsum を呼び出さずに 0 を返します。
関数を再帰的に使用する場合には、永遠に自分自身を呼び出すことが無いよう、注意しなければなりません。