ポインタの続きです。今回は参照 (リファレンス)、多段階のポインタ、関数へのポインタなどについて説明します。
前回はポインタを使って参照呼びと同じ動作を実現しました。C++の場合、参照呼びは「参照 (リファレンス)」を使うと簡単に実現することができます。参照は変数や値に別名をつける働きをします。参照は次のように定義します。
データ型& 別名 = 変数;
データ型の後ろに & を付けて、その後ろに別名を書きます。そして、= のあとに変数を指定します。これで、指定した変数を別名でアクセスすることができます。
簡単な例を示しましょう。
リスト : 参照 (sample60.cpp) #include <iostream> using namespace std; int main() { int a = 10; int& aa = a; // aa は a を参照 (a の別名) cout << "a = " << a << ", aa = " << aa << endl; a = 20; cout << "a = " << a << ", aa = " << aa << endl; aa = 30; cout << "a = " << a << ", aa = " << aa << endl; }
$ clang++ sample60.cpp $ ./a.out a = 10, aa = 10 a = 20, aa = 20 a = 30, aa = 30
int& aa = a; により aa は a を参照することができます。a の値を書き換えると aa の値も更新されます。逆に、aa の値を書き換えると a の値も更新されます。
参照の動作はポインタとよく似ていますが、ポインタのようにアドレスを変更することはできません。たとえば、aa++ のように参照をインクリメントしても、ポインタのようにアドレスがインクリメントされるのではなく、参照先の値 (変数 a の値) がインクリメントされます。
また、参照を定義するとき、参照先のデータが実際に存在しないといけません。たとえば、ポインタは int* aa; のように定義することができますが、参照の定義で int& aa; のように参照先の変数を省略することはできません。コンパイルエラーになります。このように、参照は初期設定が必要で、設定したあとは参照先を変更することができないので、ポインタよりも安全性が高いといえるでしょう。
C++の場合、関数の仮引数に参照を指定すると、参照呼びを実現することができます。たとえば、値を交換する swap は次のようになります。
リスト : 値の交換 (sample61.cpp) #include <iostream> using namespace std; void swap(int& x, int& y) { int tmp = x; x = y; y = tmp; } int main(void) { int a = 10; int b = 20; swap(a, b); cout << "a = " << a << endl; cout << "b = " << b << endl; return 0; }
$ clang++ sample61.cpp $ ./a.out a = 20 b = 10
関数の仮引数に & を付けると、その引数は参照呼びになります。main() では swap(a, b) を呼び出していますが、swap の仮引数 x, y は参照なので、変数 a, b の参照 (アドレス) が x, y に渡されます。そして、x, y の値を更新すると、呼び出し元の変数 a, b の値も更新されます。また、swap(a, 20) のような呼び出しはコンパイルエラーになります。
なお、C++の参照は「参照呼び」だけではなく、いろいろなところでポインタの代わりに使うことができます。
C/C++の場合、関数の引数に int a[10] のような配列を宣言することはできますが、実際に配列の領域が確保されて要素がコピーされることはありません。引数 a に渡されるのは配列の先頭アドレスだけです。このとき、添字は無視されます。意味を持つのは多次元配列を渡すときです。
関数に配列を渡す場合、引数の宣言はポインタを使って int *a とするか、[ ] の中の数字を省略して int a[] とします。どちら方法でも引数 a には配列の先頭アドレスが渡されます。簡単な例を示しましょう。
リスト : 配列と関数 (sample62.cpp) #include <iostream> using namespace std; void foo(int a[], int n) // int* a でもよい { for (int i = 0; i < n; i++) cout << a[i] << " "; cout << endl; a[0] = 100; } int main() { int a[8] = {1, 2, 3, 4, 5, 6, 7, 8}; foo(a, 8); for (int i = 0; i < 8; i++) cout << a[i] << " "; cout << endl; }
$ clang++ sample62.cpp $ ./a.out 1 2 3 4 5 6 7 8 100 2 3 4 5 6 7 8
関数 foo は受け取った配列 a の内容を表示します。配列の大きさは引数 n で受け取ります。最後に a[0] の値を 100 に書き換えます。foo を呼び出したあと、配列 a の内容を表示すると、先頭要素が 100 に書き換えられていて、配列 a の先頭アドレスが関数 foo に渡されていることがわかります。
なお、Cスタイル文字列は文字の配列なので、文字列を関数に渡す場合も、配列と同様に先頭アドレスが渡されることになります。これに対し、string 型は値がコピーされます。次のリストを見てください。
リスト : string を関数に渡す場合 (sample63.cpp) #include <iostream> using namespace std; void foo(string s) { s += "world\n"; cout << s; } void bar(string& s) { s += "world\n"; cout << s; } int main() { string s = "hello "; foo(s); cout << s << endl; bar(s); cout << s << endl; }
$ clang++ sample63.cpp $ ./a.out hello world hello hello world hello world
関数 foo の引数は string s なので、foo を呼び出すとき string 型のデータはコピーされます。したがって、foo の処理で "world\n" を連結しても、元の変数の値を書き換えることはありません。関数 bar のように string& s とすると参照呼びになるので、値のコピーは行われません。"world\n" を連結すると、元の変数の値も更新されます。
番地 0x68000 0x69000 0x70000 ┬─┬─┬─┬─┬ ┬─┬─┬─┬─┬ ┬─┬─┬─┬─┬ メモリ │ 0x69000 │~│ 0x70000 │~│ 0x100000 │ ┴─┴─┴─┴─┴ ┴─┴─┴─┴─┴ ┴─┴─┴─┴─┴ 変数 q │ ↑変数 p│ ↑ 変数 a └─────┘ └─────┘ 図 : 多段階のポインタ
C/C++の場合、ポインタを指すポインタを作ることができます。上図を見てください。変数 q はポインタです。q はポインタ p を指しています。p は変数 a を指しています。つまり、q は p を経由して変数 a を指し示しているのです。これをC++のプログラムで表すと、次のようになります。
リスト : 多段階のポインタ (sample64.cpp) #include <iostream> using namespace std; int main() { int a = 100; int b = 1000; int* p = &a; int** q = &p; cout << a << endl; cout << *p << endl; cout << **q << endl; **q = 200; cout << a << endl; cout << *p << endl; cout << **q << endl; *q = &b; cout << b << endl; cout << *p << endl; cout << **q << endl; }
$ clang++ sample64.cpp $ ./a.out 100 100 100 200 200 200 1000 1000 1000
C/C++の場合、ポインタを指し示すポインタは、経由するポインタと同じ数だけ * を追加します。変数 q はポインタ p を経由して変数 a を指し示すので int** q; となります。2 つのポインタを経由するのであれば、int*** q; と宣言します。
ポインタ q はポインタ p を指し示すので、初期化は変数 p のアドレスをセットします。p の値は変数 a のアドレスなので、q にセットしてはいけません。もし q = p; とプログラムすると、データ型が合わずコンパイルエラーになります。
これで、**q とすることで変数 a の値にアクセスすることができます。**q =200; のように値を代入すると、変数 a の値を書き換えることができます。また、*q とすることで変数 p の値にアクセスすることができます。このとき、*q の値を書き換えると、ポインタ q と p は変数 a ではなく、別の値を指し示すことになります。このように、ポインタを操作するときには細心の注意が必要になります。
C/C++はポインタを配列に格納することができます。
データ型* 配列名[大きさ];
配列名の前に * を付けると、配列の要素はポインタになります。たとえば、Cスタイル文字列を格納する配列は次のように定義することができます。
リスト : 配列に文字列を格納する (sample65.cpp) #include <iostream> using namespace std; int main() { char a[] = "foo"; char b[] = "bar"; char c[] = "baz"; char d[] = "hello, world"; char* msg[] = {a, b, c, d}; /* char *msg[] = { "foo", "bar", "baz", "hello, world" }; */ for (int i = 0; i < 4; i++) { cout << msg[i] << endl; } }
$ clang++ sample65.cpp $ ./a.out foo bar baz hello, world
変数 a, b, c, d は文字列で、それを格納する配列が msg です。msg は char* msg[] と宣言し、{a, b, c, d} で初期化します。これはコメントにあるように、直接文字列を指定することもできますが、clang++ ではワーニングが表示されます。C++では素直に string 型の配列を使ったほうがよさそうです。
リスト : string 型の配列 string msg[] = { "foo", "bar", "baz", "hello, world" };
これで、msg の要素は文字列を指し示すアドレスが格納されます。つまり、msg[1] は "foo" で、msg[3] は "hello, world" になります。
もちろん、文字列以外のポインタも格納することができます。次のリストを見てください。
リスト : 配列に int 型の配列を格納する (sample66.cpp) #include <iostream> using namespace std; int main() { int a[] = {1, 2}; int b[] = {3, 4, 5}; int c[] = {6, 7, 8, 9}; int* d[] = {a, b, c}; /* int d[3][4] = { {1, 2}, {3, 4, 5}, {6, 7, 8, 9} }; */ cout << *d[0] << endl; cout << *(d[0] + 1) << endl; cout << d[1][0] << endl; cout << d[2][3] << endl; }
$ clang++ sample66.cpp $ ./a.out 1 2 3 9
配列 a, b, c を定義し、それらを配列 d にセットします。この場合、d は int* d[] と宣言します。これで d の要素は各配列の先頭アドレスになります。なお、この場合はコメントのように多次元配列で定義したほうが簡単ですね。int d[n][m] と定義した場合、d[0] は d[0][0] の先頭アドレス、d[i] (0 < n) は d[i][0] の先頭アドレスになります。
C/C++の場合、関数名はコードの先頭アドレスを表します。そのアドレスを格納する変数が「関数へのポインタ」になります。基本的な考え方は他のポインタと同様に簡単なのですが、関数へのポインタを表す構文がちょっと複雑になります。関数へのポインタの構文を示します。
返り値のデータ型 (*変数名)(引数のデータ型 仮引数名, ...); 返り値のデータ型 (*変数名)(引数のデータ型 仮引数名, ...) = 関数名;
関数プロトタイプと同じような形式で、関数名のかわりに変数名を指定します。そして、名前の前に * を付けます。これでこの変数は関数へのポインタになります。仮引数名は省略してもかまいません。呼び出し方は (*変数名)(実引数, ...) とするか、変数名(実引数, ... ) とします。本稿では後者の方法で呼び出すことにします。
それでは、簡単な例を示しましょう。次のリストを見てください。
リスト : 関数へのポインタ (sample67.cpp) #include <iostream> using namespace std; double identity(double x) { return x; } double square(double x) { return x * x; } double cube(double x) { return x * x * x; } double sumof(double (*func)(double), int n, int m) { double sum = 0; for (; n <= m; n++) { sum += func(n); } return sum; } int main(void) { double (*func)(double); func = identity; cout << func(1.2345) << endl; func = square; cout << func(1.2345) << endl; func = cube; cout << func(1.2345) << endl; cout << sumof(identity, 1, 100) << endl; cout << sumof(square, 1, 100) << endl; cout << sumof(cube, 1, 100) << endl; // 配列 double (*func_tbl[])(double) = {identity, square, cube}; for (int i = 0; i < 3; i++) cout << func_tbl[i](6.789) << endl; }
$ clang++ sample67.cpp $ ./a.out 1.2345 1.52399 1.88137 5050 338350 2.55025e+07 6.789 46.0905 312.909
関数 identity は引数 x をそのまま返します。このような関数を「恒等関数」といいます。関数 square は引数 x を二乗し、関数 cube は三乗を計算します。どの関数も引数の型は double で、返り値の型も double です。これらの関数へのポインタは double (*func)(double) で表すことができます。
func = identity でポインタ func に identity のアドレスを代入し、func(1.2345) と呼び出すとポインタ経由で identity を呼び出すことができます。同様に、関数 square と cube を func にセットすれば、func(1.2345) で square や cube を呼び出すことができます。
C/C++の場合、関数へのポインタを使って、関数を他の関数の引数に渡すことができます。関数 sumof の第 1 引数 func は引数が double で返り値が double の関数を表します。func に identity を渡せば、n から m までの和を求めることができます。square を渡せば二乗の和を、cube を三乗の和を求めることができます。
sumof のように、関数を引数として受け取る関数を「高階関数 (higher order function)」と呼びます。関数型言語の場合、高階関数は特別なものではく、ごく普通に使用する機能です。C言語の場合、高階関数を使う機会は少ないと思いますが、C言語の標準ライブラリには配列をソートする qsort や、配列を二分探索する bsearch などが用意されています。また、C++の標準ライブラリには「関数オブジェクト」を受け取る高階関数が定義されています。関数オブジェクトはオブジェクト指向機能の回で説明します。
また、double (*func_tbl[])(double) とすれば、関数へのポインタを格納する配列を定義することができます。初期化も簡単で、{identity, square, cube} のように関数名を記述するだけです。呼び出し方も簡単で、func_tbl[0](1.2345) とすれば、func_tbl[0] に格納されている関数を呼び出すことができます。
関数へのポインタはデータ型が複雑になるので、typedef で別名を付けると便利です。typedef の書式を示します。
typedef データ型 別名;
簡単な例を示しましょう。
リスト : typedef の使用例 (sample68.cpp) #include <iostream> using namespace std; typedef unsigned int uint; typedef uint* uintp; int main() { uint x = 12345; uintp p = &x; cout << x << endl; cout << p << endl; cout << *p << endl; }
$ clang++ sample68.cpp $ ./a.out 12345 0x7fffcdfb685c 12345
typedef unsigned int uint で unsigned int に uint という別名を付けます。そして、uint* に uintp という別名を付けます。これで、uint は unsigned int と同じデータ型、uintp は unsigned int* と同じデータ型を表すことができます。
typedef で関数へのポインタに別名を付ける場合、他のポインタとは違って書式はちょっと複雑になります。以下に構文を示します。
typedef 返り値のデータ型 名前(引数のデータ型 仮引数名, ...); 名前* 変数名 = 関数名;
typedef 返り値のデータ型 (*名前)(引数のデータ型 仮引数名, ...); 名前 変数名 = 関数名;
typedef の場合、定義した "名前" または "(*名前)" の部分の "名前" が新しいデータ型名になります。最初の構文は、typedef で関数のデータ型に新しい名前を付けるもので、関数へのポインタを定義しているわけではないことに注意してください。したがって、関数へのポインタを宣言するときは、名前の後ろに * を付ける必要があります。二番目の構文は * を付けて定義しているので、名前 変数; と宣言すれば関数へのポインタになります。
簡単な例を示しましょう。次のリストを見てください。
リスト : 関数へのポインタ (sample69.cpp) #include <iostream> using namespace std; // 別名の定義 typedef double (*Func)(double); // 恒等関数 double identity(double x) { return x; } // 二乗 double square(double x) { return x * x; } // 三乗 double cube(double x) { return x * x * x; } // 総和 double sumof(Func func, int n, int m) { double sum = 0; for (; n <= m; n++) { sum += func(n); } return sum; } int main(void) { Func func_tbl[] = {identity, square, cube}; for (int i = 0; i < 3; i++) { cout << func_tbl[i](1.2345) << endl; cout << sumof(func_tbl[i], 1, 100) << endl; } return 0; }
$ clang++ sample69.cpp $ ./a.out 1.2345 5050 1.52399 338350 1.88137 2.55025e+07
double を受け取り double を返す関数へのポインタに typedef で別名 Func を付けます。すると、関数 sumof の引数のデータ型は Func func と表すことができ、func(実引数) で引数 func の関数を呼び出すことができます。関数へのポインタを格納する配列 func_tbl も簡単に定義することができます。
C++の場合、void は関数が値を返さないことを表すために使いますが、もうひとつ void ポインタ (void*) という使い方があります。void* は任意のデータ型のポインタを表していて、ポインタであれば void* 型の変数に代入することができます。
ただし、void* 型はどのようなデータ型かコンパイラが判断できないため、void* 型変数への代入、void* 同士の比較、他のポインタへの明示的な変換以外の操作を行うとコンパイルエラーになります。
C/C++で明示的に型変換を行うことを「キャスト (cast)」といいます。C++で void* を他のポインタにキャストするには static_cast を使います。C言語の場合、void * はどんな型のポインタにもキャストせずに代入することができますが、C++ではキャストが必要になることに注意してください。
簡単な例を示しましょう。
リスト : void* の使用例 (sample6a.cpp) #include <iostream> using namespace std; int main() { int a = 100; double b = 1.2345; void* p = &a; cout << p << endl; int* q = static_cast<int*>(p); cout << *q << endl; p = &b; cout << p << endl; double* r = static_cast<double*>(p); cout << *r << endl; }
$ clang++ sample6a.cpp $ ./a.out 0x7fffe4b0968c 100 0x7fffe4b09680 1.2345
変数 p は void* 型なので、int a, double b のアドレスを代入することができます。そして、static_cast<データ型*> で、void* を指定した データ型* にキャストすることができます。ただし、データ型を間違ってキャストすると、おかしな動作が起きる危険性があります。キャストを使うときには十分にご注意くださいませ。
最後に標準ライブラリ <cstdlib> にある関数 qsort と bsearch の使い方を簡単に説明しておきましょう。
<cstdlib> void qsort(void* buff, size_t size, size_t n, int (*comp)(const void*, const void*)); void *bsearch(void* key, void* buff, size_t size, size_t n, int (*comp)(const void*, const void*));
引数 buff が操作対象となる配列へのポインタ、size は配列の大きさ、n は要素の大きさ (バイト数)、comp が比較関数へのポインタ、bsearch の key が検索するキーへのポインタです。size_t は無符号整数を表すデータ型です。bsearch は見つけた要素へのポインタを返し、見つからない場合は 0 を返します。
これらの関数は汎用的に作られているので、配列の先頭アドレスは void* で、比較関数の引数も、要素を値渡しするのではなく要素へのポインタ (void*) を渡すようになっています。関数内部では、要素の位置をアドレス (たとえば char *ptr) で管理していて、次の要素は ptr に要素の大きさ n を加えて求めます。たとえば int であれば 4 を加え、double であれば 8 を加えます。
要素を比較するときは、そのアドレスを比較関数に渡します。比較関数は要素のデータ型がわかっているので、比較するのは簡単です。comp(a, b) の返り値は a == b ならば 0 を、a < b ならば負の整数、a > b ならば正の整数とします。
const で宣言された変数は値を変更することができません。たとえば、const int a = 10; と宣言したあと、a = 20; のように代入するとコンパイルエラーになります。ポインタの場合、たとえば const int *p であれば *p = 100 のような代入はエラーになりますが、p++ とか p = &a; のように p が保持しているアドレスを変更することは可能です。つまり、ポインタが参照している変数の値を更新できない、という意味になります。
簡単な例を示しましょう。次のリストを見てください。
リスト : qsort と bsearch (sample6b.cpp) #include <iostream> #include <cstdlib> using namespace std; const int N = 10; int buff[N] = {5, 6, 4, 7, 3, 8, 2, 9, 1, 0}; int comp(const void* x, const void* y) { const int* p = static_cast<const int*>(x); const int* q = static_cast<const int*>(y); return *p - *q; } int main() { qsort(buff, N, sizeof(int), comp); for (int i = 0; i < N; i++) cout << buff[i] << " "; cout << endl; // int key = 2; void* p = bsearch(&key, buff, N, sizeof(int), comp); cout << p << endl; key = 7; p = bsearch(&key, buff, N, sizeof(int), comp); cout << p << endl; key = 10; p = bsearch(&key, buff, N, sizeof(int), comp); cout << p << endl; }
int 型の配列 buff を qsort で昇順に並べ替えて、そのあと bsearch で検索します。比較関数は int comp(const void* x, const void* y) と宣言します。関数の中で x と y を static_cast で const int* にキャストします。あとは、キャストした変数 p, q を使って *p - *q を返すだけです。
それでは実行してみましょう。
$ clang++ sample6b.cpp mhiroi@DESKTOP-FQK6237:~/cpp$ ./a.out 0 1 2 3 4 5 6 7 8 9 0x564a594e3078 0x564a594e308c 0
正常に動作していますね。
ここで参照呼びについてもう少しだけ詳しく説明しましょう。参照呼びは、呼び出し先 (callee) の仮引数の値を更新すると、それが呼び出し元 (caller) の変数にも直ちに反映されるような呼び出し方のことをいいます。値呼びと参照呼びは関数呼び出しにおける仮引数と実引数の対応を表したもので、渡される値のデータ型とは関係ありません。
C言語はポインタを使って参照呼びと同じことができます。次のリストを見てください。
リスト : 値の交換 #include <stdio.h> void swap(int *a, int *b) { int tmp = *a; *a = *b; *b = tmp; } int main() { int a = 10; int b = 20; swap(&a, &b); printf("a = %d, b = %d\n", a, b); return 0; }
& 演算子で変数 a, b のアドレスを取り出し、関数 swap の仮引数 x, y に渡します。これで関数 main の変数 a, b の値を swap で書き換えることができますが、これを「参照呼び」とはいいません。C言語は値呼びであり、あくまでも変数のアドレスを値渡ししているだけなのです。
swap を呼び出す箇所は次のようにコンパイルされます。
リスト : swap を呼び出す箇所のアセンブリコード (C言語 : gcc) movl $10, -8(%ebp) movl $20, -4(%ebp) leal -4(%ebp), %eax movl %eax, 4(%esp) leal -8(%ebp), %eax movl %eax, (%esp) call _swap
movl はデータの転送命令、leal はアドレスの転送命令です。leal で局所変数のアドレスをレジスタ EAX にセットし、それを movel でスタックに積んでいることがわかります。
ここで、変数に値を代入する式 a = 10, b = 20 に注目してください。= の右側を「右辺式」、左側を「左辺式」といいます。一般に、左辺式は変数やその定義を表していて、右辺式は値を生成する式になります。値呼びは右辺の式 (値) が渡されると考えてください。C言語の場合、アドレスを取り出す演算子 &a と &b は swap の仮引数 x と y の値を表す右辺式と考えることができます。
これに対し、参照呼びは左辺式 (変数) が渡されると考えてください。たとえば、値を交換する swap はC++の「参照」を使うと次のようになります。
リスト : 値の交換 #include <iostream> using namespace std; void swap(int& x, int& y) { int tmp = x; x = y; y = tmp; } int main(void) { int a = 10; int b = 20; swap(a, b); cout << "a = " << a << endl; cout << "b = " << b << endl; return 0; }
C++は値呼びが基本ですが、関数の仮引数に & を付けると、その引数は参照呼びになります。main() では swap(a, b) を呼び出していますが、swap の仮引数 x, y は参照呼びなので、変数 a, b の参照 (アドレス) が x, y に渡されます。そして、x, y の値を更新すると、呼び出し元の変数 a, b の値も更新されます。
swap を呼び出す箇所は次のようにコンパイルされます。
リスト : swap を呼び出す箇所のアセンブリコード (C++ : g++) movl $10, -8(%ebp) movl $20, -4(%ebp) leal -4(%ebp), %eax movl %eax, 4(%esp) leal -8(%ebp), %eax movl %eax, (%esp) call __Z4swapRiS_
C言語と同様に leal で局所変数のアドレスをレジスタ EAX にセットし、それを movel でスタックに積んでいます。けっきょく、C++の参照も変数のアドレスを仮引数に渡すことで実現しているのですが、それを処理系が自動的に行ってくれるところが「参照呼び」のメリットです。C++ の場合はコンパイル時に型チェックも行われるので、C言語のようにポインタを使って参照呼びを実現するよりも安全といえるでしょう。
このほかにも参照呼びをサポートしている言語があります。たとえば Pascal は値呼びが基本ですが、関数や手続きの仮引数に var を付けると参照呼びになります。Pascal はこの機能を「変数引数」または「変数パラメータ (variable parameter)」と呼んでいます。
スクリプト言語では Perl が参照呼びです。Perl の場合、実引数は $_ という特別な配列に格納されていて、その要素を書き換えると、呼び出し元の変数の値を書き換えることができます。Perl では $_ から値を取り出して局所変数にセットすることで「値呼び」と同様の動作になります。