値渡しと参照渡し
関数へオブジェクトを渡す方法には、値渡しと参照渡しの2種類があります。値渡しは、オブジェクトの値が複製(コピー)され関数へ渡されます。プログラミング言語Cにおける関数呼び出しではすべて値渡しになります。なお、ポインターを渡すことでオブジェクトの参照を渡すことができるため、参照渡しとほぼ同じことが実現できます。
それぞれの渡し方による関数呼び出しの方法を説明する前に、値のコピーについて少し詳しくみてみましょう。Cにおける値渡しでは、実引数(関数呼び出しの引数)の値が仮引数(関数宣言の引数)に代入されます。
はじめに、int型のオブジェクトaとbを定義してみます。
int a = 1; int b;
aとbはまったく別のオブジェクトです。aの値をbに代入するには、代入演算子 = を使います。
b = a; // a == 1, b == 1
aの値がコピーされ、bに格納されました。aとbは別のオブジェクトですから、bの値を変更してもaには影響しません。
int a = 1; int b = a; b = 2; // bの値を変更してもaには影響しない。 printf("a=%d, b=%d\n", a, b); // a=1, b=2
構造体やポインターもまったく同じように代入できます。
// 構造体オブジェクトの代入 struct st { int x; int y; }; struct st s1 = { 1, 2 }; struct st s2 = s1; printf("s1.x=%d, s1.y=%d\n", s1.x, s1.y); // s1.x=1, s1.y=2 printf("s2.x=%d, s2.y=%d\n", s2.x, s2.y); // s2.x=1, s2.y=2 // ポインターの代入 int a = 1; int *p1 = &a; int *p2 = p1; printf("*p1=%d, *p2=%d\n", *p1, *p2); // *p1=1, *p2=1
ただし配列は、代入演算子による代入はできません。
int a[] = { 1, 2, 3 }; int b[3]; b = a; // エラー
この場合、aはポインター生成により int * へと変換されますので、左辺の型 int [3] とは一致しなくなります。配列aを配列bにコピーするためには、要素を一つずつ代入します。
for (int i = 0; i < 3; i++) { b[i] = a[i]; }
さて、本題の値渡しですが、これまで説明した代入によるコピーと同じやり方で値を関数に渡します。
void func(int a) { printf("a = %d\n", a); // a = 1 } int main(void) { int x = 1; func(x); return EXIT_SUCCESS; }
関数funcの仮引数aにはオブジェクトxの値がコピーされて渡されます。仮引数は実引数とは別のオブジェクトになりますので、関数funcの中でaの値を変更しても、オブジェクトxの値は変更されません。
void func(int a) { printf("a = %d\n", a); // a = 1 a = 2; printf("a = %d\n", a); // a = 2 } int main(void) { int x = 1; printf("x = %d\n", x); // x = 1 func(x); printf("x = %d\n", x); // x = 1 (2ではない) return EXIT_SUCCESS; }
次に、参照渡しと同等のことをしたい場合にどうすれば良いか説明をします。こちらはオブジェクトそのものを渡すのではなく、オブジェクトを参照する値であるポインターを使います。同じポインターを使ってオブジェクトを参照しますので、渡す側と受け取った側で同じオブジェクトを参照することになります。
void func(int *a) { printf("a = %d\n", *a); // a = 1 (2) *a = 2; printf("a = %d\n", *a); // a = 2 (3) } int main(void) { int x = 1; printf("x = %d\n", x); // x = 1 (1) func(&x); printf("x = %d\n", x); // x = 2 (4) ... 2に変わっている return EXIT_SUCCESS; }
まず関数mainでオブジェクトxを宣言します。初期化が終わったところでは、値は1になっています(1)。オブジェクトxのポインターを関数funcに渡し、funcでその参照先の値を確認すると、同じオブジェクトですので1となります(2)。このオブジェクトの値を2に変えると(3)、オブジェクトを宣言した関数main側でも同じように2に変わっていることが確認できます(4)。
このように通常の変数を使う場合とポインター変数を使う場合とでは異なる動作になりますので、関数を作るときにはどちらにするのか適切に判断する必要があります。一般的に、作ろうとしている関数が次のような場合に、ポインターを使います。
- 配列を受け取るとき
- 渡されるオブジェクトのサイズが大きい、あるいはサイズが不明で、コピーするのに無視できないコストがかかる可能性があるとき
- 渡されたオブジェクトをそのものを変更するとき
- 戻り値以外に値を返したいとき
配列を受け取るには、ポインターを使って次のように書きます。
// 配列を受け取る関数 void func(int *ary) { for (int i = 0; i < 2; i++) { printf("ary[%d]=%d\n", i, ary[i]); } } int main(void) { int ary[] = { 1, 2, 3 }; func(ary); return EXIT_SUCCESS; }
ここでは関数funcの宣言で、仮引数aryを int *ary と書きました。関数funcの呼び出しでintの配列を渡していますが、これはポインター生成によりintへのポインター、つまり int * となります。また、関数の仮引数では、int ary[] という書き方もできることになっていて、どちらの書き方をしてもまったく同じ意味になります。なお、[]内に数値を書いても無視され、単に int * と書いたものとみなされます。
// 次の3つはすべて同じ意味 void func(int *ary); void func(int ary[]); void func(int ary[3]);
関数で配列の配列(2次元配列)を受け取るときは次のようにします。
// 配列の配列を受け取る関数 // 配列の配列を受け取る関数 void func(int (*ary)[3]) { for (int i = 0; i < 2; i++) { for (int j = 0; j < 3; j++) { printf("ary[%d][%d]=%d\n", i, j, ary[i][j]); } } } int main(void) { int ary2[][3] = { { 1, 2, 3 }, { 4, 5, 6 } }; func(ary2); return EXIT_SUCCESS; }
関数funcに渡しているオブジェクトary2の型はint型の要素を持つ配列の配列ですが、ポインター生成により int (*)[3]、つまり「int型の要素を3つ持つ配列へのポインター」となります。この丸カッコ()は必須です。これがないとaryの前にある*よりも先に[]が解釈されてしまい、「intへのポインター型の要素を3つ持つ配列」になってしまいます。また、int (*ary)[3] は int ary[][3] と書いてもまったく同じです。
ただしいずれの場合でも、int (*ary)[3] の3は省略できません。配列aryの要素を参照するには ary[n][m](nとmは整数型)のようにします。このとき、型の大きさが明確でないと参照先の位置を特定することができないため、コンパイルするときにエラーになってしまいます。つまり、ary[n] の大きさをコンパイラに伝える必要があるということです。そのため ary[n] の型である int [3] の3は省略できないのです。
コラム●配列のサイズを知る
本文中では、int (*ary)[3] の3は省略できないということを説明しました。実際にサイズがどうなっているか確認してみましょう。関数funcを次のように書き換えてみます。
void func(int (*ary)[3]) { printf("%d %d %d %d\n", sizeof(int), sizeof(int *), sizeof(ary), sizeof(*ary)); }
実行結果は環境によって異なります。私の環境では、「4 4 4 12」と表示されました。
最初の2つは、型intのサイズと、intへのポインターのサイズを確認しています。ここではいずれも4バイト(32ビット)となっています。では3つめのaryはどうでしょうか。型は「int型の要素を3つ持つ配列へのポインター」で、結局のところポインターですのでサイズは4バイトです。4つめの*aryの型は「int型の要素を3つ持つ配列」ですので、int1つ分のサイズである4バイトが3つ分で12バイトあることが分かります。
コラム●文字列の初期化
配列の初期化は、一般的に次のようにします。
int ary[] = { 1, 2, 3 };
どんな型の配列でも同じように初期化できますが、charの配列の場合には文字列リテラルを使って初期化する方法もあります。
char str[] = "abc";
これは char str[] = { 'a', 'b', 'c', '\0' } と同じです。最後の \0 が不要の場合には char str[3] = "abc" と書くこともできます。この場合は char str[] = { 'a', 'b', 'c' } と同じです。
また、文字列リテラルを使った初期化には、次のようなものもあります。
char *pstr1 = "abc"; char *pstr2 = (char[]){ "abc" }; char *pstr3 = (const char[]){ "abc" };
配列を使った初期化(strの例)と似ていますが、動作が異なります。まず、いずれのオブジェクトも型はポインターになっており、値はすべて同じです。
しかし、最初の例(pstr1)では、文字列リテラルが使われています。文字列リテラルは基本的に変更できませんので、pstrの参照先の値を書き換えてはいけません。また、文字列リテラルは静的記憶域期間を持ちますので、このポインターpstr1を関数の戻り値にしても問題ありません。
2つ目の例(pstr2)では複合リテラルが使われています。複合リテラルは変更可能ですので、pstr2の参照先のオブジェクトは変更可能です。しかし、複合リテラルを関数の中で使った場合には自動記憶域期間を持ちますので、関数を抜けるとオブジェクトは捨てられてしまいます。したがって、pstr2をこのまま関数の戻り値に使ってはいけません。
さらに3つ目の例では、2つ目の例にconst修飾子をつけています。したがって、pstr3の参照先のオブジェクトを変更してはいけません。また、自動記憶域期間を持ちますので、これもpstr2と同様にこのまま関数の戻り値に使ってはいけません。
ポインターを使って関数から値を返す
引数と同じように戻り値でも、コピーを返したり、参照を返したりすることができます。
// int型オブジェクトのコピーを返す関数 int func() { int a = 1; return a; } int main(void) { int x; x = func(); printf("x = %d\n", x); // x = 1 return EXIT_SUCCESS; }
ポインターも同じように戻り値として使えますが、ポインターが参照するオブジェクトの記憶域期間(生存期間)に注意が必要です(連載第9回参照)。
// intへのポインターを返す関数(問題あり) int * func() { int a = 1; return &a; // オブジェクトaのポインターを返すが... } int main(void) { int *x; x = func(); printf("*x = %d\n", *x); // 結果は不定! return EXIT_SUCCESS; }
オブジェクトaの記憶域期間は関数funcの中だけです(自動記憶域期間)。つまり、関数mainに戻ってきたときには、オブジェクトaは捨てられてしまっています。関数mainでオブジェクトaのポインターを受け取っても、その参照先はすでに無効になっているため、結果がどのような値になっているか分かりません。もしかすると1かもしれませんが、まったく別の値かもしれません。それは実行してみるまでわかりませんし、ある環境で決まった値になったとしても、別の環境で実行すればまた別の値になるでしょう。これは解析しづらいバグの原因になります。
オブジェクトが有効かどうかを管理するのは、プログラマーであるあなたの役割です。ポインターを扱うときには、常に参照先のオブジェクトの記憶域期間を把握しておかなければなりません。
一般的に、オブジェクトの記憶域期間は一箇所で把握できるようにしておいたほうが、見通しのいいプログラムを書くことができます。この例では、関数funcの呼び出し側でオブジェクトを管理するとよいでしょう。つまり、まずオブジェクトを用意し、関数funcへオブジェクトの参照(ポインター)を渡し、戻ってきたところでオブジェクトを解放するということです。
// intへのポインターを受け取って値を設定する関数 void func(int *a) { *a = 1; } int main(void) { int x; func(&x); printf("x = %d\n", x); // x = 1 return EXIT_SUCCESS; }
オブジェクトxは自動記憶域期間を持ちますから、その範囲は関数mainの初めから終わりまでです。
このような利用方法は、関数から戻り値以外にも値を返したい場合にも使えます。例えば、関数の戻り値はエラーを、引数のポインターが参照するオブジェクトには計算結果を入れるようにします。
// 複数の結果を返す関数の例。 // エラーがない場合はresultに値が入り、戻り値として0以外が返る。 // エラーの場合は戻り値として0が返る。 int calc(int *result) { /* ... 何かの処理 ... */ if (/* 成功? */) { *result = /* 結果 */; return -1; } else { return 0; } }
関数ポインター
ポインターはオブジェクトだけではなく関数も参照することができます。引数も戻り値も持たない関数のポインター(関数ポインター)は、次のように宣言します。
// 引数と戻り値を持たない関数へのポインターpf void (*pf)(void);
pfの型は、void (*)(void) です。宣言の *pf や型の * を囲うカッコは省略できません。これがないと別の意味になってしまいます。
// 関数を参照するポインターpf1の宣言 void (*pf1)(void); // 引数を持たず、void *型の値を返す、関数pf2のプロトタイプ宣言 void *pf2(void);
次に、この関数へのポインターに値を設定します。ポインターに値を設定したら、そのポインターを使って関数を呼び出すことができます。
// 関数func void func(void) { /* ... */ } int main(void) { // ポインターpf void (*pf)(void); pf = &func; // 関数funcの呼び出し (*pf)(); return EXIT_SUCCESS; }
引数や戻り値の型が異なると、ポインターとしての型も異なりますので、比較したり代入したりできないことに注意してください。
- int (*pf3)(int) : int型の引数を1つとり、int型の値を返す関数へのポインタ : int型の引数を1つとり、int型の値を返す関数へのポインタ
- int (*pf4[])(int) : int型の引数を1つとり、int型の値を返す関数へのポインターの配列 : int型の引数を1つとり、int型の値を返す関数へのポインターの配列
関数ポインターの型は読みづらいので、typedefを使うとよいでしょう。
typedef void (*FUNC)(void); FUNC pf5 = &func;
なお、関数とポインターについてはいくつか特別なルールがあります。
まず、式の中に関数が現れると、関数へのポインターになります。したがって、pf = &func は、pf = func でも同じです。また、間接参照に使う単項*演算子は、参照先の型が関数の場合は、最終的に関数へのポインターとして解釈されます。関数呼び出し演算子 () は関数へのポインターを指定することに注意してください。つまり、(*pf)() は pf() と同じです。
以上のことをまとめると、関数funcと関数へのポインターpfについて、次のようになります。
// どちらでも同じ pf = &func; pf = func; // どれも関数funcの呼び出し (*pf)(); pf(); func();
malloc、free
これまでに説明したオブジェクトは、自動記憶域期間か静的記憶域期間を持っていました(連載第9回参照)。自動記憶域期間はブロックの開始から終了まで、静的記憶域期間はプログラムの開始から終了までです。ここでは、より細かく制御できる割付け記憶域期間について説明します。
他の2つの記憶域期間では、記述したコードの文法から記憶域期間やオブジェクトのサイズがわかるようになっていました。それに対して割付け記憶域期間では、mallocとfreeという関数を呼び出すことにより、実行時に決められるサイズの記憶域を確保したり解放したりできます。
mallocとfreeはライブラリとして提供されています。使用するためには、プログラムの先頭でstdlib.hをインクルードする必要があります。その中で次のように宣言されています。
void *malloc(size_t size); void free(void *ptr);
関数mallocには、記憶域のサイズを引数で指定します。size_t は処理系ごとに定義された型の1つで、符号なし整数型です。sizeof演算子の結果はsize_t型になります。戻り値の型は void * で、システム上で確保されたオブジェクトの先頭アドレスが返ります。
関数freeには、mallocで確保した記憶域のポインターを渡します。freeを呼び出すことでその記憶域を解放します。一旦解放された記憶域は使用できません。
では、サンプルコードで動きを確認してみましょう。mallocの戻り値とfreeの引数で使われている void *は、他のポインター型とキャストなしで変換できることに注意してくだい。
1: #include 2: #include 3: 4: int main(void) { 5: int *a = malloc(sizeof(int) * 10); 6: printf("a = %p\n", a); 7: free(a); 8: return EXIT_SUCCESS; 9: }
実行結果としてポインターの値が表示されますが、この値は実行時に決まりますので環境や実行時の構成によって異なります。
このサンプルコードではint型の10個分のサイズを持つオブジェクトを確保しています。確保したばかりのオブジェクトの値は特に決まっていません。意味のない値が設定されていますので、適切な値を設定して使いましょう。もし初期化としてオブジェクト全体を0で埋めるならば、ライブラリで用意されているmemset関数が使えます。
void *memset(void *s, int c, size_t n);
引数sは初期化する記憶域の先頭アドレス、cは初期化する値、nは記憶域のサイズです。また、memset関数を使うためにはstring.hをインクルードする必要があります。先の例で使ってみましょう。
1: #include <stdio.h> 2: #include <stdlib.h> 3: #include <string.h> 4: 5: int main(void) { 6: int *a = malloc(sizeof(int) * 10); 7: 8: // 0で埋める 9: memset(a, 0, sizeof(int) * 10); 10: // 値を確認 11: for (int i = 0; i < 10; i++) { 12: printf("*(a + %d) = %d\n", i, *(a + i)); 13: } 14: 15: free(a); 16: return EXIT_SUCCESS; 17: }
mallocには、似た機能を持つcallocとreallocという2つの関数が用意されています。
void *calloc(size_t n, size_t size); void *realloc(void *ptr, size_t size);
callocは大きさがsizeのオブジェクトn個分の領域を確保します。先の例で malloc(sizeof(int) * 10) としましたが、callocを使うと calloc(10, sizeof(int)) と書けます。また、callocは確保した領域が0で初期化されます。初期化する分だけmallocよりも時間がかかりますが、0で初期化することがわかっているのであればcallocを使うとよいでしょう。
reallocは、すでに確保済みのオブジェクトのサイズを変更します。引数ptrには確保済みのオブジェクト、引数sizeには新しい領域のサイズを指定します。元のオブジェクトの値は新しいオブジェクトに引き継がれます。元のオブジェクトと比べて小さくなった場合は切り詰められ、大きくなった場合は初期化されていない領域がうしろに追加されます。
malloc、calloc、reallocのいずれも、領域が確保できなかった場合は空ポインター(NULL)が返ります。この値のチェックは重要です。もしNULLが返ってきたことを確認せずに使用すると、その後の処理で不正な操作となるでしょう。
mallocとfreeは便利な関数ですが、ちょっとしたミスで不正な動作を引き起こしてしまいますので、よく注意しましょう。不正な動作を引き起こすよくあるミスは次のようなものです。
- 確保したバッファーより大きなデータを書き込む(通信やユーザー入力などで取得した値はサイズをチェックしてから書き込みます。他の記憶域期間のオブジェクトでも同じことが言えます)
- 同じポインターを渡して2回free関数を呼び出してしまう(freeしたポインターにはNULLを入れておくのが良い習慣です。free(NULL) は不正な動作ではなく、何も行われません)free(NULL) は不正な動作ではなく、何も行われません)
コラム●stdlib.hのインクルードし忘れに注意
C99より前は、関数を呼び出す前にプロトタイプ宣言がなかった場合、戻り値の型はintであると解釈されるようになっていました。したがって、stdlib.hをインクルードしなくてもmallocを呼び出すことができました。そこでもしstdlib.hのインクルードを忘れたとすると、C99以前のコンパイラはmallocの戻り値の型がintであると仮定します。
通常であれば、intからポインターへの代入を行おうとしてもコンパイラーが警告を出すでしょう。この警告に従って修正すれば問題ありません。ただ、歴史的な経緯からmallocの戻り値をキャストしている場合があります。その場合は警告が出なくなることがあり、問題に気づかないかもしれません。
C99ではプロトタイプ宣言がない関数を呼び出すことは禁止されています。たいていのコンパイラでは(たとえ戻り値をキャストしていても)コンパイル警告が出ますので、警告に従ってインクルードを見直しましょう。
コラム●freeすべきか
領域を確保するmallocと、領域を解放するfreeは、基本的にセットで扱われます。mallocで確保した領域は、freeで解放することが望ましいでしょう。
ただし、mallocで確保した領域を、freeで解放しないままプログラムを終了しても、たいていの場合は問題になりません。mallocで確保した領域は、プログラムが終了するとシステムが自動的に回収します。Cの規格でも、mallocで確保した領域をfreeするように求めていません。
とはいえ、mallocとfreeは対応付けられるように使うことが基本です。特に、サービスプログラムのように実行され続けるプログラムや、大量のメモリを使用するプログラムなどは、適切にfreeを呼び出さないとシステムのメモリを不必要に圧迫してしまいます。どのプログラムから呼び出されるかわからないライブラリなども、領域が不要になったらきちんとfreeを呼び出して解放すべきです。
現実のプログラムでは「freeしない方が良い」と考えられる場合もありますが、その場合でもシステムのことをきちんと把握した上で、問題ないこと確かめられたときのみfreeを省略するようにしましょう。
restrict 修飾子
restrict修飾子はコンパイラーによる最適化を助けるために、C99から用意されました。ある特定の場面で使用すると、コンパイラーがより動作の速い実行コードを生成することができるようになります。
restrictはポインターにのみ使用できます。restrictで修飾されたintへのポインターpは次のように宣言します。
int * restrict p = malloc(sizeof(int) * 10);
このように宣言した場合、ポインターpが参照するオブジェクトは、ポインターpからしかアクセスしないということをプログラマーが保証したことになります。
例として、Cライブラリに用意されているmemcpy関数を見てみましょう。memcpyのプロトタイプは次のようになっています。なお、memcpy関数を使うには、string.hをインクルードします。
void *memcpy(void * restrict s1, void * restrict s2, size_t n);
この関数は、s2が指すオブジェクトからs1が指すオブジェクトへ、サイズnだけコピーします。例えば次のように使います。
char a[4]; memcpy(a, "0123", 4);
オブジェクトaには、先頭から ‘0’ ‘1’ ‘2’ ‘3’ という文字が入ります。次に、aの要素の値を右に1つずらしてみましょう。例えば次のようにします。
memcpy(a + 1, a, 3);
確認してみると、’0’ ‘0’ ‘1’ ‘2’ という期待した結果に反して ‘0’ ‘0’ ‘1’ ‘1’ になりました。結果は環境ごとに異なりますので期待通りになる場合もありますが、いずれにせよmemcpyの使い方が間違っています。
restrictで修飾されたポインターは、参照先のオブジェクトがそのポインターからしか操作されないことを期待しています。ですから、memcpyの仮引数にある void * restrict s1 と void * restrict s2 では、s1とs2の参照するオブジェクトが別のものを指定しなければなりません。
言い換えれば、s1とs2が参照するオブジェクトは重なっていてはいけません。ここではs1とs2の操作対象(s1とs2の参照先から3つ目の引数で指定された大きさ3)が重なってしまっています。restrict修飾子がついている変数へ誤った指定をしているため、memcpyの使い方が間違っているということになるのです。
なぜこのような制約がついているのかというと、この制約をつけることでより高速に動作するような最適化が可能になるからです。問題になるケースは多くはないと思いますが、restrictの指定からくる制約に違反しているかどうかはコンパイラーではチェックされませんので、関数のプロトタイプ宣言をよく確認して使用するようにしましょう。
組み込み環境で使うポインター
Cがよく使われる領域である組み込み環境では、他の環境とは特別なポインターの使われ方をするときがあります。大まかには規格どおりの文法で問題ないのですが、組み込み環境の事情を考慮して、使えない機能があったり、より便利に使うための機能が備わっていたりします。
メモリが1MBに満たないような小規模の組み込み環境では、mallocやfreeが用意されていない場合があります。その場合は、自動記憶域期間や静的記憶域期間のオブジェクトでやりくりすることになります。開発環境によってはそのような場合にメモリの管理をしやすくなるような機能を用意しているものもありますので、よく調べてみましょう。
また、組み込み用CPUでは、メモリ上の特定のアドレスに特別な機能を用意している場合があります。例えば、「アドレス0x123を読み込むと外部のセンサーの値が取れる」とか、「アドレス0xABCに値を書き込むと音が出る」などです。このようなアドレスは、char a = *(0xABC) のように直接アドレスを指定する場合もありますが、多くのコンパイラではなんらかのアクセス手段が用意されています。ときには代入演算子を使ってはいけないというようなものもありますので、とにかく扱うデバイスと開発環境のマニュアルをよく読んで、その方法に従うことが大切です。
今回学んだこと
- 関数へオブジェクトを渡す方法には値渡しと参照渡しがあり、プログラミング言語Cではすべて値渡しになります
- 値渡しはオブジェクトのコピーを渡すため、関数側には同じ値の別のオブジェクトが渡ります
- プログラミング言語Cではポインターを使って参照渡しと同等のことが行えます
- 関数からポインターを返すこともできますが、オブジェクトの生存期間に注意が必要です
- ポインターは関数を参照することもできます
- malloc関数とfree関数で実行時に領域を確保したり解放したりできます
- restrictで修飾されたポインターは、別のオブジェクトで参照されないことを保証する必要があります
- 組み込み環境でもポインターは多く使われますが、特有の事情がある場合もあります
Copyright © ITmedia, Inc. All Rights Reserved.