仮引数(パラメータ)と実引数(アーギュメント)
混同されがちですが、分けておく価値のある2つの言葉です。
- **仮引数(パラメータ)**は関数の定義に書かれた変数です -
int square(int n)のint n。 - **実引数(アーギュメント)**は呼び出し側で与える値です -
square(4)の4。
関数を呼び出すと、そのパラメータが新しいローカル変数として作られ、引数がそこへコピーされます。このコピーこそ、このページ全体の主題です。
C言語は常に値で渡す
これがルールであり、C言語に例外はありません。関数は引数のコピーを受け取ります。パラメータへの代入はコピーだけを変えます。
value は依然として 42 のままです。関数は x という名のコピーを受け取り、そのコピーをゼロにし、関数が戻るときにコピーは消えました。パラメータの名前を value に変えても何も変わりません - 名前が両者をつなぐわけではなく、それらは別々の変数です。
これはあらゆる型について当てはまります。int、double、char、ポインタ、そして構造体全体(フィールドごとにコピーされます)も同じです。Python、Java、JavaScript から来た人は、それらの言語がオブジェクトの参照を値渡ししているため、関数の中でオブジェクトを変更すると外からも見えることに注意してください。C言語にそれはありません - オブジェクトそのものをコピーします。
値渡しは制限ではなく、機能です。関数が誤って呼び出し側のデータを壊すことがないので、はるかに考えやすくなります。
古典的な失敗:swap
教科書的な実演は、2つの値を交換しようとする関数です。
ロジックは正しく、確かに a と b を交換しています。しかし a と b はコピーなので、関数が戻った瞬間にその成果は捨てられます。
ポインタで参照渡しを模倣する
関数に呼び出し側の変数を変えさせるには、値ではなくその変数のアドレスを渡します。そのアドレス自体は値渡しされ - ポインタがコピーされ - ますが、コピーも元のオブジェクトを指しているので、それを通して書き込めば呼び出し側のメモリに届きます。
3つの構文要素が、この考え全体を担っています。
- パラメータリストの
int *aは、aを「intへのポインタ」として宣言します。 - 呼び出し側の
&xはxのアドレスを生み出します。 - 関数内の
*aは「aが指しているint」という意味で、読むことも代入することもできます。
これがC言語プログラマの言う「参照渡し」ですが、正確に言う価値があります。これも依然として値渡しです。コピーされるのはポインタです。a 自体への代入(a = NULL;)はコピーだけを変え、*a への代入が呼び出し側の変数を変えます。ポインタがこのモデルを詳しく扱っています。
代償として、呼び出し箇所を見ても変数が変わりうるかどうか分からなくなります - だからこそ & が必須なのです。swap(x, y) はコンパイルできず、swap(&x, &y) と書かなければなりません。その & が、この呼び出しが x を変更しうるという目に見える印です。
複数の結果を返す
return が生み出す値は1つなので、追加の出力はポインタのパラメータを通って運ばれます。よくある形は、戻り値をステータスコードとし、本当の結果はポインタ越しに書き込むというものです。
呼び出しが失敗したとき q と r は以前の値を保つ点に注意してください - 関数は何も書かずに戻ったからです。それが呼び出し側の守るべき契約であり、出力を使う前にステータスをチェックする理由です。
もう1つの手は、両方のフィールドを持つ構造体を返すことです。ポインタを完全に避けられますし、値が本当に一緒に属しているなら、そのほうが良い設計であることも多いです。
配列は別物
配列はコピーのルールに従わず、これがほとんど全員をつまずかせます。配列を渡すと、それは先頭要素へのポインタに**成り下がり(decay)**ます - つまり関数が受け取るのはアドレスであって、データのコピーではありません。
呼び出し側の配列が本当に変わりました。しかもどこにも & はありません - この文脈では data がすでにアドレスであるからです。帰結が3つあります。
パラメータリストでは int a[] と int *a はまったく同じ意味です。 int a[100] ですらそうで、サイズは無視されます。int a[] と書くと意図は伝わりますが、「配列がコピーされている」と読んではいけません。
関数の中の sizeof は間違った答えを返します。 これがそのバグです。
main では sizeof(data) は20バイトで、長さは5と出ます。関数の中では sizeof(a) はポインタのサイズ - 64ビットシステムなら8 - なので、「長さ」は2になります。配列のサイズは、そこでは単に手に入らないのです。長さは必ず別の引数として渡しましょう。 最近のコンパイラは -Wall でこれを警告してくれます。
書き込まないなら const を使いましょう。 const int a[] は関数が読むだけだと宣言し、コンパイラがそれを強制し、読み手はそれを信頼できます。
int sum(const int a[], int n); /* 配列を変更しないと約束する */
文字列も配列なので同じことが当てはまります。void greet(const char *name) は、文字列を変更せずに読む関数の標準的なシグネチャです。
コストはどうなのか
大きな構造体を値で渡すと、呼び出しのたびに全バイトがコピーされます。フィールドがいくつかの構造体ならそれで問題なく、ポインタが加える間接参照より速いことすらあります。大きなものにはポインタを渡し、関数が読むだけならそれを const と印しましょう。
struct Config { char name[64]; int flags[32]; double weights[128]; };
void applyConfig(const struct Config *cfg); /* 呼び出しごとの512バイトのコピーがない */
const struct Config *cfg は「変更しない Config へのポインタ」という意味で、ポインタの効率とコピーの安全性を兼ね備えます。
よくある間違い
- 素のパラメータが呼び出し側の変数を変えると期待すること。 変えられません。ポインタを渡しましょう。
- 呼び出し側で
&を忘れること。 ポインタを求めるswapに対するswap(x, y)は型エラーです。コンパイラが捕まえてくれますが、プロトタイプがスコープにある場合だけです。 - 関数の中で
*を忘れること。 2つのポインタパラメータに対するa = b;は、値ではなくローカルのコピーを入れ替えます。書きたいのは*a = *b;です。 - 配列パラメータに
sizeofを使うこと。 常に誤りです。長さを渡しましょう。 - ローカル変数へのポインタを返すこと。 関数が戻るとローカルは死ぬので、呼び出し側は宙ぶらりんのポインタを持つことになり、未定義動作です。値で返すか、呼び出し側が用意したバッファに書き込むか、
mallocで確保しましょう。 - ポインタのパラメータを
NULLかチェックしないこと。 渡されたものを何でもデリファレンスする関数は、ヌルの引数でクラッシュします。
よくある質問
C言語に参照渡しはありますか?
ありません。C言語のモードはただ1つ、値渡しだけです。関数は常に各引数のコピーを受け取るので、パラメータへの代入が呼び出し側の変数に触れることはありません。参照渡しに見えるものは、ポインタを値渡ししているのです - ポインタはコピーされますが、それでも呼び出し側のオブジェクトを指しているので、関数はポインタ越しにそれを変更できます。
C言語で配列を関数に渡すには?
パラメータを int a[] または int *a と書き - この2つは同じ意味です - 長さを別の引数として渡します:int sum(const int a[], int n)。配列はコピーされず、関数は先頭要素へのポインタを受け取るので、a[i] への変更は呼び出し側から見えます。
C言語で配列パラメータに sizeof が効かないのはなぜですか?
パラメータの実体がポインタだからです。関数の中の sizeof(a) は配列のサイズではなくポインタのサイズ(通常8バイト)を返すので、sizeof(a)/sizeof(a[0]) は要素数ではなく 2 のような値になります。長さは必ず明示的に渡しましょう。
C言語の関数が複数の値を返すには?
追加の結果にはポインタを渡し、それを通して書き込みます:int divide(int a, int b, int *quotient, int *remainder) は成功フラグを返しつつ、両方の出力を埋めます。もう1つの手は、値をまとめた構造体を返すことです。