関数が無いと始まらない!

こんにちは、めのんです!

お待たせしました!
本当は前回開設する予定だった関数をようやく今回やることになりました。

Cのプログラムというのは関数の集まりなんです。
PHPであれば関数が無くてもいきなり文を書けますが、Cの場合は関数が無ければ何もできません(オブジェクトなどの宣言ぐらいならできますけど)。

プログラムが開始するとmain関数が呼び出されます。
「『hello, world!』徹底解説」の回に解説しましたね。
このときも「main」という名前の関数が必要でした。

関数の宣言と定義

PHPでは必ずfunctionから始まります。
それに対してCの関数宣言はオブジェクトの宣言と同じような形式になっています。

int object;    // オブジェクトの宣言
int func();    // 関数の宣言

上の例では「func」という名前の関数を宣言しています。
丸括弧の部分を含めた「func()」を1行目のobjectの宣言と対比させると同じ形になっていますよね。
そして最初の「int」は関数の場合は返却値の型(PHPでいえば返り値の型です)であり、オブジェクトの場合はオブジェクトの型になります。

先ほどの例では関数を宣言しただけです。
関数の宣言では、どういう名前でどんな型の関数が存在するということだけをコンパイラに教えるだけしかできません。
実際の関数の振る舞いを定義するには、関数の本体を定義しなければなりません。

厳密なことをいうとオブジェクトや関数の定義も宣言の一種なのですが、ここではあまり深追いするのはやめておきます。

分離形式

先ほどの例ではfunc()の丸括弧の中には何も書きませんでした。
PHPではこのように丸括弧の中に何も書かないと、どんな型の実引数を何個渡してもかまわなかったかと思います。
そして、func_get_args関数を使って実際にどんな実引数が渡されたのかを調べますね。

Cの関数も同じで、関数の丸括弧に何も書かなければ、どんな型の実引数を何個でも渡すことができます。
ところが、Cにはfunc_get_args関数のようなものはありませんので、関数定義では次のように書くことで仮引数を宣言することができます。

int func(arg1, arg2)
  int arg1;
  double arg2;
{
}

このような書き方を「分離形式」といい、大昔のCにはこの書き方しかなかったそうです。
現在のCでも分離形式は使えるのですが、C99以降では非推奨になっています。

私は古いPHPについては知りませんが、おそらくCと似たような発展の仕方をしてきたのではないでしょうか?
PHPで関数の仮引数に型指定ができるようになったのはつい最近のことだと聞いています。
型指定がないと間違った実引数を渡していても何のチェックもしてくれませんよね。
昔のCもまさにそんな状況だったのです。

仮引数並びと関数原型

最近のPHPでは関数の仮引数に型指定を行えるようになっています。
それと同じように標準化以降のCでも仮引数の型指定が行えるようになっています。

int func(int args1, double arg2);

PHPの型指定とそっくりの書き方ですよね。
PHPだとこんな感じになりますね。

function func(int $arg1, float $arg2): int

functionで始めるかどうか、それと返却値の型を前に書くか後ろに書くかだけの違いです。
実際にはもっと細かい差はあるんですけど、最初の段階はその程度の理解で十分です。

さて、Cでは丸括弧の中に型指定付きで仮引数を並べたものを「仮引数並び」といいます。
仮引数並びを伴う関数宣言を「関数原型」といいますが、Cのプログラマーの間では「プロトタイプ」または「プロトタイプ宣言」の方が通りがいいようです。

関数原型については後ほどもう一度出てきますので、その際に使い方を解説します。

関数の呼び出し

Cのプログラムでの関数の呼び出し方はPHPと同じだと思ってかまいません。
関数名のあとに丸括弧を書き、その中に実引数をカンマで区切って並べます。

  $a = 123;
  $b = 4.56;
  func($a, $b);    // func関数の呼び出し
  int a = 123;
  double b = 4.56;
  func(a, b);

上の例を見ても、実引数に$が付くかどうかぐらいでそっくりの形をしています。

関数に渡した実引数は仮引数に代入するのと同じように振る舞います。
つまり、型が同じならそれでいいですし、型が異なれば暗黙的に型変換されます。
暗黙的に型変換できないぐらいかけ離れた型の実引数を渡そうとしたときはコンパイルエラーになります。

ところで、関数の仮引数に配列型を指定した場合は配列そのものが値渡しされるわけではありません。
今回は詳しいことは省略しますが、いずれ近いうちに詳しく解説することにします。

return文

次に関数から値を返す方法について見てみましょう。
といっても、これもPHPと変わりません。
具体例として、2つの引数を受け取って合計を返すsum関数を作ってみることにします。

function sum(int $arg1, int $arg2): int
{
  return $arg1 + $arg2;
}
int sum(int arg1, int arg2)
{
  return arg1 + arg2;
}

これもそっくりですね。
CでもPHPと同じようにreturn文を使って値を返します。

返却値も仮引数と同じように、返却値の型のオブジェクトにreturn文で指定した式を代入するかのように振る舞います。
同じ型ならそのまま渡されますし、暗黙的な型変換ができるなら変換されます。
そうでなければコンパイルエラーになります。

ひとつだけ注意しないといけないのは、返却値の型に配列型を指定することはできない点です。
PHPでは普通にarray型を返しますので勝手が違うと思います。
Cでは複数の値を関数が返すことはできないのですが、どうしてもやりたい場合には別の方法があります。
引数に配列型を指定した場合とあわせて、これについても近いうちに解説することにします。

前方参照はできません!

Cでは、宣言を行う前にオブジェクトや関数を使おうとすると期待通りに振る舞ってくれません。
PHPだと次のような書き方は普通にできますよね。

<?php
main();

function main()
{
  echo sub(1.23) . PHP_EOL;
}

function sub(float $a): float
{
  return $a * 2;
}

Cの場合は次のように書いてもコンパイルエラーにはならないのですが、まともに動いてくれません。

#include <stdio.h>

int main(void)
{
  printf("%f\n", sub(1.23));
  return 0;
}

double sub(double a)
{
  return a * 2;
}

前もって宣言が無い状態でsub関数を呼び出しているので、この場合はsub関数は返却値の型がint型で仮引数並びが無いものとみなされます。

つまり次のように解釈されてしまいます。

int sub();

実際のsub関数はdouble型の引数を受け取ってdouble型を返しますので矛盾していますね。
どんな結果になるのか、実際にやってみてもいいと思います。
ぜんぜん見当違いな結果になると思いますよ。

勝手に解釈された関数の形式がたまたま実際の定義と合致しているなら期待通りに動きますが、そもそも仮引数並びを書かないのは非推奨ですし、宣言無しに関数を呼び出すのもやっぱり非推奨なんです。

こうした間違いをおかしても、コンパイラによっては(そしてコンパイルオプションによっては)警告さえ無しにコンパイルされてしまうことがあります。
Cはプログラマーは万能で間違わないことを前提にしている言語なのです!

先行宣言

では、必ず関数を呼び出すより前に関数を定義すればいいのかというとそれも不便です。
こういう場合は関数の先行宣言を行います。
先行宣言という用語は私が考えたものではありませんが規格の用語でもありません。
でもよく使われる表現だと思います。

具体的にどうするかというと、関数を呼び出す前に中身の無い宣言だけをしておくのです。
先ほどの例でいえばこんな風に書き換えます。

#include <stdio.h>

double sub(double a);

int main(void)
{
  printf("%f\n", sub(1.23));
  return 0;
}

double sub(double a)
{
  return a * 2;
}

これで問題なく動くようになったと思います。

このときに役に立っているのが関数原型なんです。
関数原型があるから、その関数がどんな名前で、どんな型の引数をどんな順序で何個受け取って、どんな型の値を返すのかをコンパイラが知ることができるのです。

駆け足でしたがCの関数について見てきました。
実際に関数を使いこなすまでには、もっといろんなことを習得しないといけません。
PHPでも参照渡しとかコールバック関数とかいろいろ学ばないといけないことが多いと思います。
Cにも似たようなことがあるんです。

それでは今回の解説は以上です。
次回は何をやるか決めていませんが、そろそろポインタをやらないといけないですね。
ちょっと考えてみます。