Cによるプログラミング入門11
関数に関するあれこれ
関数はCプログラムの柱です。今回は、関数に関して、いくつか補足をします。
●関数の中で関数を使う
関数の中で関数を使っても何も問題ありません。たとえば、
void hello(void){
printf("hello\n");
}
では、helloという関数の定義の中でprintfという関数を使っていますね。これで、まったく問題がなかったわけです。
printfは(普通は)コンパイラに付いてくる関数(標準関数などといいます)ですが、自作の関数を別の自作の関数内で使っても問題ありません。たとえば、次のようなプログラムを書くこともできます。
/* hellox1.c */
#include <stdio.h>
void hello(void){
printf("hello\n");
}
void hello2(void){
int i;
for(i = 0; i < 5; i++)
hello();
}
int main(void)
{
hello2();
return 0;
}
Fig.1 hellox1.exeの実行画面
hello2の内部で、helloを5回呼び出しているわけです。
●関数を定義する順序
hellox1.cで、helloとhello2の順番を逆にし、
void hello2(void){
int i;
for(i = 0; i < 5; i++)
hello();
}
void hello(void){
printf("hello\n");
}
とすると、コンパイルできなくなります。
hello2の定義の中で使われるhelloの定義が、hello2の後にあるので、コンパイラが困ってしまうのです。
●プロトタイプ宣言
この場合でも、hello2の定義の前に
void hello(void);
と書くと、エラーではなくなります。これは、「関数helloのプロトタイプ宣言」などとよばれるもので、これにより「あとでhelloという関数を定義するよ」という意味になるのです。
完全なプログラム例を書くと、たとえば、次のようになります。
/* hellox2.c */
#include <stdio.h>
/* helloのプロトタイプ宣言 */
void hello(void);
void hello2(void){
int i;
for(i = 0; i < 5; i++)
hello();
}
void hello(void){
printf("hello\n");
}
int main(void)
{
hello2();
return 0;
}
たとえば、mainの中で「mainの後に定義を書いた関数」を使う場合は、mainの前にそのプロトタイプ宣言をしておけば、「どの関数(どのような関数)を使うか」明示することになるわけです。
/* hellox3.c */
#include <stdio.h>
void hello(void);
void hello2(void);
int main(void)
{
hello2();
return 0;
}
void hello(void){
printf("hello\n");
}
void hello2(void){
int i;
for(i = 0; i < 5; i++)
hello();
}
コードが長くなって初心者には嫌な感じがするかもしれません。しかし、プログラムの頭の方を見れば、登場人物(関数)がわかるので、プログラムが見やすくなったということもできるのです。
●引数の値渡し
ところで、次のようなプログラムをコンパイルし、実行すると何が出力されると思いますか。
/* addx1.c */
#include <stdio.h>
void add_one(int x){
x++;
}
int main(void)
{
int m = 100;
add_one(m);
printf("mの中身は%dです。\n", m);
return 0;
}
出力は次のようになります。
Fig.2 addx.exeの実行画面
ちょっと考えると、はじめにmが定義され、そのときに100が格納されますが、add_oneに引数として渡されると、add_oneの内部で1増やされて、mの値は101になりそうです。(「++」は「1増やせ」という命令でした。)しかし、そうなっていません。なぜでしょうか。
「add_oneの中のx」はadd_oneの仮引数とよばれる変数です。仮引数には引数として与えられる値がコピーされ、それが関数内で使われるのでした。実際、mainの中の「add_one(m);」とある行で、「mainの中のm」の値が「add_oneの中のx(add_oneの仮引数)」にコピーされ、add_oneの中身が実行されるのです。その中身は、「x++;」でたしかに「xを1増やせ」ですが、これは「mainの中のm」のコピーにすぎません。コピーの値を増やしてもオリジナルの値には何の変化もないのです。
このように、「仮引数は引数のコピーになる」という性質を「引数の値渡し」などと言います。ちなみに、「仮引数を変化させると引数そのものも変化する」ようなものを「引数の参照渡し」などと言います。しかし、Cは、あくまでも値渡しであって、参照渡しではないのです。つまり、これがCの特徴でもあるのです。
ちなみに、add_oneは、結局、外部に何の影響も与えない、無意味な関数です。これは、あくまで、説明用関数と考えてください。
●ポインタ引数
それでは、次のようなプログラムを実行するとどうなるでしょう。
/* addx2.c */
#include <stdio.h>
void add_one(int* x){
(*x)++;
}
int main(void)
{
int m = 100;
add_one(&m);
printf("mの中身は%dです。\n", m);
return 0;
}
結果は次のようになります。
Fig.3 addx2.exeの実行画面
なぜだかわかりますか?
まず、add_oneの仮引数は「int x」ではなく「int* x」となっています。これは、引数として「int」ではなく「int*」、つまり、「整数」ではなく「整数を入れる変数のアドレス」を受け取るという意味です。そして、関数の定義では、*xを1増やしています。これは、「xが保持するアドレスにある変数」、つまり、xが指す変数の中身を1つ増やせという意味ですね。
mainの中では、mという変数を定義しています。そして「add_one(&m);」で、「mainの中のm」のアドレスを、引数としてadd_oneに与えているのです。このとき、「mainの中のm」のアドレスは、「add_oneの仮引数x」にコピーされます。したがって、「add_oneの仮引数x」の保持するアドレスにある変数は「mainの中のm」ということになります。そして、add_oneの中で「xの保持するアドレスにある変数」の値を1つ増やしているので、結局、「mainの中のm」値が1増えるのです。
引数をポインタにしても、引数の値が仮引数にコピーされるという事実にかわりはありません。しかし、ポインタを使うことによって、関数の外部にある変数の内容を変更することができるのです。
実は、そのような例を見たことがあります。そうです。scanfです。これは
scanf("%d", &x);
のようにして、ユーザの入力した値をxに格納する関数でした。実際、これでxの値を変更することになるわけですね。
逆に、アドレスを受け取るようにしないと、その変数の中身を変更できない(つまり、ユーザの入力値を格納することなどができない)のです。
●関数内の変数
それでは、次のようなプログラムを実行するとどうなると思いますか。
/* hensu1.c */
#include <stdio.h>
void showx(void){
int x = 100;
x++;
printf("xの中身は%dです。\n", x);
}
int main(void)
{
showx();
showx();
showx();
return 0;
}
実行すると次のようになります。
Fig.4 hensu1.exeの実行画面
xは、showxという関数内部で定義された変数です。そして、これは、関数が実行されるたびに生成され、使われ、関数が終了すると破棄されるものなのです。xは、showxを実行するごとに生成され、100が与えられ、1増やされ、出力されるので、Fig.4のような実行結果になるわけです。
●staticな変数
しかし、xの定義の前にstaticを付けて、xをstatic変数にすると、xは、「一度だけ生成され、プログラムが終了するまで保持される変数」、つまり「関数の実行自体に関係なく保持される変数」になります。
例を見てみましょう。
/* hensu2.c */
#include <stdio.h>
void showx(void){
static int x = 100;
x++;
printf("xの中身は%dです。\n", x);
}
int main(void)
{
showx();
showx();
showx();
return 0;
}
Fig.5 hensu2.exeの実行画面
上の実行例を見て、関数内で定義したstaticな変数の振る舞いを理解してください。xは、一度だけ定義され、そのときに100という値を与えられます。その後、shgowxを実行するたびに、1増やされて表示されるので、Fig.5のような実行画面になるのです。
(注:static変数を関数外部で定義することもありますが、ここでは考えません。)
入門8の最後にあるような配列を使って、「おみくじをする関数omikuji」を書くとすると、次のようなものを思いつくかもしれません。(入門9のものとも比較してください。)
void omikuji(void){
char* kekka[5] = {"今日は大吉ですぜ。",
"今日は吉です。",
"今日は普通かな。",
"今日は、う〜ん、凶。",
"今日は、すまん、大凶だ。"};
printf("%s\n", kekka[rand() % 5]);
}
しかし、関数を呼び出すたびに、kekkaという配列の要素を初期化するのは少し無駄な気もします。実際には、それは目に見えるようなものではないのですが、気になるなら、kekkaをstaticにしてしまえばよいわけです。そうすれば、配列要素の初期化は、1度だけになるはずです。
void omikuji(void){
static char* kekka[5] =
{"今日は大吉ですぜ。", "今日は吉です。",
"今日は普通かな。",
"今日は、う〜ん、凶。",
"今日は、すまん、大凶だ。"};
printf("%s\n", kekka[rand() % 5]);
}
細かいことを言うと、「""で囲まれた文字列リテラル」は、「メモリ上のどこかに場所が確保され、そこに文字列が格納さたもの」のアドレスを表わすのでした。この「文字列リテラルの文字列が置かれる場所」は、プログラムが終了するまで保持されることになっています。そのため、その場所のアドレスをstaticな変数が保持していても、問題はないのです。(つまり、「文字列リテラルの文字列が先に破棄され、staticな変数が破棄された場所を指し続けている」というようなことは起こらないはずです。)