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な変数が破棄された場所を指し続けている」というようなことは起こらないはずです。)

課題11


C入門目次

前のページ  後のページ