C/C++ ポインタ入門
ポインタとは
ポインタとはアドレスの入れ物です
ただそれだけです
おしまい
ポインタとアドレス
わかっていれば本当にそれでおしまいなのですが、ではそのアドレスというのは何かというと、アドレスというのはメモリ上の場所を表す値で、ただの整数です
(実際には違いますが)単純に言うと 64KB のメモリのある環境ではアドレスは 0x0000 〜 0xffff になりますし、2GB のメモリのある環境ではアドレスは 0x00000000 〜 0x7fffffff になりますし、4GB のメモリのある環境ではアドレスは 0x00000000 〜 0xffffffff になります
また 8GB のメモリのある環境ではアドレスは 0x000000000 〜 0x7ffffffff になりますし、1TB のメモリのある環境ではアドレスは 0x00000000000 〜 0x3ffffffffff になります
16ビットの符号なし整数で表現できるのは 0 〜 64KB まで、32ビットの符号なし整数で表現できるのは 0 〜 4GB まで、64ビットの符号なし整数で表現できるのは 0 〜 16エクサバイトまでになりますね
このアドレスのビット幅のことを指して 16ビット環境、32ビット環境、64ビット環境などと呼んでいます
ポインタ型というのはこのアドレスを入れるためのものですから、16ビット環境、32ビット環境、64ビット環境などというのは正にポインタの大きさを表していると言ってもいいでしょう
(本当は違うけど)
ポインタとアドレス:その2
アドレスというのはメモリ上の場所を表す値であると言いましたが、これは具体的には、メモリの先頭から何バイト離れているかというバイト数のことです
ですから、アドレスとしての値が 1 ならメモリの先頭から 1バイト目の場所を指していますし、アドレスとしての値が 100 ならメモリの先頭から 100バイト目の場所を指しています
このようにアドレスというのはただの整数なので、ポインタも実はただの整数です
int a = 0; // a に入っている値は 0 int* b = 0; // b に入っている値は 0 char* c = 0; // c に入っている値は 0 int** d = 0; // d に入っている値は 0 int (*e)[10] = 0; // e に入っている値は 0 int (*f)() = 0; // f に入っている値は 0 void* g = 0; // g に入っている値は 0
普通の整数型に入っている数値もポインタに入っている数値も数値として見る限りはどちらも全く同じものです
普通の整数型に入っている数値とポインタに入っている数値とで何が違うのかというと、普通の整数型に入っている数値はそれが何を意味しているかはプログラマしか知らないけれども、ポインタに入っている数値がアドレスであるということはコンパイラも知っているというところです
コンパイラはポインタにアドレスが入っていることを知っているので、それを常にアドレスとして適切であるように扱います
つまりポインタとはコンパイラが特別扱いをしてくれる整数型のことです
ポインタとアドレス:その3
コンパイラが特別扱いをしてくれるとはいえ、ポインタはただの整数型とわかれば怖くはないですね
ここで具体的に考えるために今システムのメモリ全体が1つのファイルであるとしてみましょう
するとメモリのアドレスはシークオフセットということになりますね
ポインタはシークオフセットを入れている変数です
ただのシークオフセットなら益々何も怖いことはないですね
変数宣言をするということは、そのメモリというファイル上のどこかの場所のシークオフセットに名前を付けて覚えておくということです
そして変数に値を入れるということはその変数用のシークオフセットの場所に値を書き込むということです
変数から値を取り出すということはその変数用のシークオフセットの場所から値を読み出すということです
このような想定で次のコードを見てみましょう
int a = 10; int* p = &a; int b = *p; *p = 20;
↓
- シークオフセットを得て a の場所として覚える
- a の場所に 10 を書く
- シークオフセットを得て p の場所として覚える
- p の場所に a のシークオフセットを書く
- シークオフセットを得て b の場所として覚える
- p の場所から読みだした値を新たなシークオフセットとしてその場所にある値を読み出して b の場所に書く
- p の場所から読みだした値を新たなシークオフセットとしてその場所に 20 を書く
↓
ここで次のような関数があると考えてみると
// address_t : アドレスの型 // value_t : 任意の値の型 // 現在位置を得る address_t memory_tell(); // 現在位置を設定する void memory_seek( address_t p_to ); // 現在位置から指定されたバイト数のデータを読み込み、その分現在位置を進める value_t memory_read( size_t bytes_to_read ); // 現在位置に指定されたバイト数のデータを書き込み、その分現在位置を進める void memory_write( value_t value_to_write, size_t bytes_to_write ); // 指定位置から指定されたバイト数のデータを読み込む。現在位置には触らない value_t memory_read( address_t p_from, size_t bytes_to_read ); // 指定位置に指定されたバイト数のデータを書き込む。現在位置には触らない void memory_write( address_t p_to, value_t value_to_write, size_t bytes_to_write );
↓
上記のコードは次のように書けます
// int a = 10; address_t addressof_a = memory_tell(); // a を宣言 memory_write( 10, sizeof( int )); // a の場所に 10 を書く // int* p = &a; address_t addressof_p = memory_tell(); // p を宣言 memory_write( addressof_a, sizeof( address_t )); // p の場所に &a を書く // int b = *p; address_t addressof_b = memory_tell(); // b を宣言 address_t valueof_p = ( address_t )memory_read( addressof_p, sizeof( address_t )); // p の場所から読み出した値をシークオフセットとして int valueof_b = ( int )memory_read( valueof_p, sizeof( int )); // そこから値を読み出す memory_write( valueof_b, sizeof( int )); // b の場所にその値を書く // *p = 20; address_t valueof_p = ( address_t )memory_read( addressof_p, sizeof( address_t )); // p の場所から読み出した値をシークオフセットとして memory_write( valueof_p, 20, sizeof( int )); // その場所に 20 を書く
また別のコードでは次のようになります
int c[2] = { 0, 1 }; int* p = c; *p = 1; ++p; *p = 2; int d = p - c;
↓
- シークオフセットを得て c の場所として覚える
- c の場所に 0 と 1 を書く
- シークオフセットを得て p の場所として覚える
- p の場所に c のシークオフセットを書く
- p の場所から読みだした値を新たなシークオフセットとしてその場所に 1 を書く
- p の場所から読みだした値に p の指す先にあるものの大きさを1つ足して p の場所に書き戻す
- p の場所から読みだした値を新たなシークオフセットとしてその場所に 2 を書く
↓
// int c[2] = { 0, 1 }; address_t addressof_c = memory_tell(); // c を宣言 memory_write( 0, sizeof( int )); // c の場所に 0 を書く memory_write( 1, sizeof( int )); // c の場所に 1 を書く // int* p = c; address_t addressof_p = memory_tell(); // p を宣言 memory_write( addressof_c, sizeof( address_t )); // p の場所に c のアドレスを書く // *p = 1; address_t valueof_p = ( address_t )memory_read( addressof_p, sizeof( address_t )); // p の場所から読み出した値をシークオフセットとして memory_write( 1, sizeof( int )); // その場所に 1 を書く // ++p; address_t valueof_p = ( address_t )memory_read( addressof_p, sizeof( address_t )); // p の場所から読み出した値を valueof_p += sizeof( int ); // 一要素分先に進めて memory_write( addressof_p, valueof_p, sizeof( address_t )); // p の場所に書き戻す // *p = 2; address_t valueof_p = ( address_t )memory_read( addressof_p, sizeof( address_t )); // p の場所から読み出した値を memory_write( valueof_p, 2, sizeof( int )); // p の場所に 2 を書く // int d = p - c; address_t addressof_d = memory_tell(); // d を宣言 address_t valueof_p = ( address_t )memory_read( addressof_p, sizeof( address_t )); // p の場所から読み出した値から int valueof_d = ( int )( valueof_p - addressof_c ) / sizeof( int ); // c のアドレスを引いて要素のサイズで割って p と c の差分を要素数で求める memory_write( valueof_d, sizeof( int )); // d の場所に求めた要素数を書く
ここで ++p のところに注目してください
addressof_p のところから読みだした値 valueof_p を単純に +1 するのではなく、+sizeof( int ) としていますね
これは重要なところで、ポインタの加減算は常にポインタの型のサイズの単位で増減します
これはファイルで考えれば当たり前のことでしょう
ファイルに sizeof( int ) のデータを書き込んだらシークオフセットをその分だけ動かしますね
もしシークオフセットを動かすのが今書き込んだデータのサイズよりも小さかったら、次にそのシークオフセットの指すところに同じ大きさのデータを書き込んだら、直前に書き込んだデータが壊れてしまいますね
ですからシークオフセットは常に読み書きするデータのサイズの単位で増減します
ここで言うシークオフセットとはアドレスのことです
また読み書きするデータのサイズとはポインタの型のサイズのことです
つまり、ポインタの中身であるアドレスは常にポインタの型のサイズの単位で増減するということです
これは言い換えると、ポインタに指示する加減算のオペランドはバイト数ではなく要素数であるということです
ポインタに +1 をするとポインタの値であるアドレスは一要素分増えます
ポインタに -1 をするとポインタの値であるアドレスは一要素分減ります
アドレスを一要素分増やすのにポインタに +1 をします
アドレスを一要素分減らすにはポインタに -1 をします
またここで d = p - c のところに注目してください
d には p の値であるアドレスと c のアドレスとの差をそのまま入れているのではなく、その差を要素数に直してから入れていますね
これは二つのポインタの差分はバイト数ではなく要素数であるということを表しています
加減算に指定するのも要素数、差分が返すのも要素数です
ポインタは常にその型のサイズ単位で動くということを覚えましょう
ポインタの型
普通の整数に int や long などの型があるように、ポインタにも型があります
普通の整数の型とポインタの型との違うところは、普通の整数の型がその整数自身の型を決めるものであるのに対して、ポインタの型は指している場所にあるものの型を決めるものであるということです
int a; // a は int int* b; // b の指しているところにあるものは int char* c; // c の指しているところにあるものは char int** d; // d の指しているところにあるものはポインタ。そのポインタが指しているところにあるものは int int (*e)[10]; // e の指しているところにあるものは要素数10個の int の配列 int (*f)(); // f の指しているところにあるものは関数 void* g; // g の指しているところにあるものは何だかわからない
ポインタ自身には常にアドレスが入っているので、そのアドレスの指す先にあるものの型が何であろうとも、ポインタ自身には関係ありません
ファイルに入っているものが何であろうとシークオフセットを入れておく変数の型が変わったりはしませんからね
今、上記の a, b, c について考えてみると、a は a 自身が int であると宣言しています。a の大きさは sizeof( int ) になります
一方 b はポインタなので b 自身は int* であると宣言しています。b の大きさは sizeof( int* ) になります
また b が int* なので b に入っているアドレスが指す先にあるものは int であり、その大きさは sizeof( int ) です
c はポインタなので c 自身は char* であると宣言しています。c の大きさは sizeof( char* ) になります
また c が char* なので c に入っているアドレスが指す先にあるものは char であり、その大きさは sizeof( char ) です
例えば具体的に char が 1バイト、int が 4 バイト、ポインタが 8 バイトの環境で考えてみると、
a は int なので a 自身の大きさは 4 バイトです
一方 b はポインタなので b 自身の大きさは 8 バイトで、b に入っているアドレスが指す先にあるものは 4 バイトです
c はポインタなので c 自身の大きさは 8 バイトで、c に入っているアドレスが指す先にあるものは 1 バイトです
ここで a, b, c をそれぞれインクリメントしてみましょう。するとどうなるでしょうか
++a; ++b; ++c;
a はただの整数なので値が 1 増えます。元の値が 0 なら新しい値は 1 になります
一方 b はポインタなのでその値であるアドレスは一要素分増えます。b は int* なので sizeof( int ) 増えます。今 sizeof( int ) が 4 であるとすると、元の値が 0 なら新しい値は 4 になります
c もポインタなのでその値であるアドレスは一要素分増えます。c は char* なので sizeof( char ) 増えます。今 sizeof( char ) が 1 であるとすると、元の値が 0 なら新しい値は 1 になります
ここで、d, e もインクリメントしてみましょう
++d; ++e;
d はポインタなのでその値であるアドレスは一要素分増えます。d は int** なので sizeof( int* ) 増えます。今 sizeof( int* ) が 8 であるとすると、元の値が 0 なら新しい値は 8 になります
e もポインタなのでその値であるアドレスは一要素分増えます。e は int (*)[10] なので sizeof( int ) × 10個分増えます。今 sizeof( int ) が 4 であるとすると、元の値が 0 なら新しい値は 40 になります
ここで、f, g もインクリメントしてみましょう
++f; ++g;
しかしこれはどちらもエラーになります
f はポインタなのでその値であるアドレスを一要素分増やしたいところですが、f は関数を指すポインタなので、その要素は関数です。関数のサイズを知る方法は無いので、f はインクリメントできません
g もポインタなのでその値であるアドレスを一要素分増やしたいところですが、g は void* です。何を指しているのか決まっていないので要素のサイズを決定できません。ですから g もインクリメントできません
ポインタと配列
次のような配列があったとき
int c[4] = { 0, 1, 2, 3 };
その各要素にアクセスするには次のようにしますね
int c0 = c[0]; int c1 = c[1]; int c2 = c[2]; int c3 = c[3];
これが何をしているかを見てみましょう
// int c[4] = { 0, 1, 2, 3 }; address_t addressof_c = memory_tell(); // c を宣言 memory_write( 0, sizeof( int )); // c の場所に 0 を書く memory_write( 1, sizeof( int )); // c の場所に 1 を書く memory_write( 2, sizeof( int )); // c の場所に 2 を書く memory_write( 3, sizeof( int )); // c の場所に 3 を書く // int c0 = c[0]; address_t addressof_c0 = memory_tell(); // c0 を宣言 value_t valueof_c0 = memory_read( addressof_c + 0 * sizeof( int ), sizeof( int )); // c[0] の値を得て memory_write( addressof_c0, valueof_c0, sizeof( int )); // c0 の場所にその値を書く // int c1 = c[1]; address_t addressof_c1 = memory_tell(); // c1 を宣言 value_t valueof_c1 = memory_read( addressof_c + 1 * sizeof( int ), sizeof( int )); // c[1] の値を得て memory_write( addressof_c1, valueof_c1, sizeof( int )); // c1 の場所にその値を書く // int c2 = c[2]; address_t addressof_c2 = memory_tell(); // c2 を宣言 value_t valueof_c2 = memory_read( addressof_c + 2 * sizeof( int ), sizeof( int )); // c[2] の値を得て memory_write( addressof_c2, valueof_c2, sizeof( int )); // c2 の場所にその値を書く // int c3 = c[3]; address_t addressof_c3 = memory_tell(); // c3 を宣言 value_t valueof_c3 = memory_read( addressof_c + 3 * sizeof( int ), sizeof( int )); // c[3] の値を得て memory_write( addressof_c3, valueof_c3, sizeof( int )); // c3 の場所にその値を書く
配列の先頭アドレスから添え字に指定された要素の分だけ離れたところにアクセスしているわけです
配列と言っても何か特別なものがあるわけではなくただ要素が順番に並んでいるだけなので、そこにアクセスするときには先頭のアドレスと先頭からどのくらい離れているかというオフセットだけがわかればいいわけです
アドレスとオフセットだけあればいいなら別にポインタでも実現できそうですからやってみましょう
int* p = c; int p0 = *( p + 0 ); int p1 = *( p + 1 ); int p2 = *( p + 2 ); int p3 = *( p + 3 );
↓
// int* p = c; address_t addressof_p = memory_tell(); // p を宣言 memory_write( addressof_c, sizeof( address_t )); // p の場所に c のアドレスを書く // int p0 = p[0]; address_t addressof_p0 = memory_tell(); // p0 を宣言 address_t valueof_p = ( address_t )memory_read( addressof_p, sizeof( address_t )); // p の場所から読み出した値をシークオフセットとして value_t valueof_p0 = memory_read( valueof_p + 0 * sizeof( int ), sizeof( int )); // そこから値を得て memory_write( addressof_p0, valueof_p0, sizeof( int )); // p0 の場所にその値を書く // int p1 = p[1]; address_t addressof_p1 = memory_tell(); // p1 を宣言 address_t valueof_p = ( address_t )memory_read( addressof_p, sizeof( address_t )); // p の場所から読み出した値をシークオフセットとして value_t valueof_p1 = memory_read( valueof_p + 1 * sizeof( int ), sizeof( int )); // そこから値を得て memory_write( addressof_p1, valueof_p1, sizeof( int )); // p1 の場所にその値を書く // int p2 = p[2]; address_t addressof_p2 = memory_tell(); // p2 を宣言 address_t valueof_p = ( address_t )memory_read( addressof_p, sizeof( address_t )); // p の場所から読み出した値をシークオフセットとして value_t valueof_p2 = memory_read( valueof_p + 2 * sizeof( int ), sizeof( int )); // そこから値を得て memory_write( addressof_p2, valueof_p2, sizeof( int )); // p2 の場所にその値を書く // int p3 = p[3]; address_t addressof_p3 = memory_tell(); // p3 を宣言 address_t valueof_p = ( address_t )memory_read( addressof_p, sizeof( address_t )); // p の場所から読み出した値をシークオフセットとして value_t valueof_p3 = memory_read( valueof_p + 3 * sizeof( int ), sizeof( int )); // そこから値を得て memory_write( addressof_p3, valueof_p3, sizeof( int )); // p3 の場所にその値を書く
配列でもポインタでも、基準となるどこかのアドレスから指定された要素分だけ離れたところにアクセスしているというところは、まったく同じですね
実際 p[n] という表記は *( p + n ) という表記と全く同じ意味です
どちらもポインタと要素数を足し算した結果が指している場所にある値を表しています
ところで、足し算の左と右が入れ替わっても結果は同じですね
ですから *( p + n ) と *( n + p ) が同じように p[n] と n[p] も同じです
次の表記はどれも同じ意味です
p[3] 3[p] *( p + 3 ) *( 3 + p ) ( p + 3 )[0] 0[p + 3]
これは次のような表記ができるところからもわかります
1+1 == (int)&((char*)1)[1] 1+1 == (int)&1[(char*)1] 1+1 == (int)&*( 1+1 )
このようなことからも lhs[rhs] という表記が単純に [] の内と外を *( lhs + rhs ) の形に置き換えているだけだということがわかりますね
ポインタと配列:その2
次のような配列があったとき
int cc[3][4] = { { 0, 1, 2, 3 }, { 4, 5, 6, 7 }, { 8, 9, 10, 11 }, };
その各要素にアクセスするには次のようにしますね
int cc01 = cc[0][1]; int cc12 = cc[1][2]; int cc23 = cc[2][3];
これが何をしているかを見てみましょう
//int cc[3][4] = { // { 0, 1, 2, 3 }, // { 4, 5, 6, 7 }, // { 8, 9, 10, 11 }, //}; address_t addressof_cc = memory_tell(); // cc を宣言 memory_write( 0, sizeof( int )); // cc の場所に 0 を書く memory_write( 1, sizeof( int )); // cc の場所に 1 を書く memory_write( 2, sizeof( int )); // cc の場所に 2 を書く memory_write( 3, sizeof( int )); // cc の場所に 3 を書く memory_write( 4, sizeof( int )); // cc の場所に 4 を書く memory_write( 5, sizeof( int )); // cc の場所に 5 を書く memory_write( 6, sizeof( int )); // cc の場所に 6 を書く memory_write( 7, sizeof( int )); // cc の場所に 7 を書く memory_write( 8, sizeof( int )); // cc の場所に 8 を書く memory_write( 9, sizeof( int )); // cc の場所に 9 を書く memory_write( 10, sizeof( int )); // cc の場所に 10 を書く memory_write( 11, sizeof( int )); // cc の場所に 11 を書く // int cc01 = cc[0][1]; address_t addressof_cc01 = memory_tell(); // cc01 を宣言 value_t valueof_cc01 = memory_read( addressof_cc + ( 0*4 + 1 ) * sizeof( int ), sizeof( int )); // cc[0][1] の値を得て memory_write( addressof_cc01, valueof_cc01, sizeof( int )); // cc01 の場所にその値を書く // int cc12 = cc[1][2]; address_t addressof_cc12 = memory_tell(); // cc12 を宣言 value_t valueof_cc12 = memory_read( addressof_cc + ( 1*4 + 2 ) * sizeof( int ), sizeof( int )); // cc[1][2] の値を得て memory_write( addressof_cc23, valueof_cc23, sizeof( int )); // cc12 の場所にその値を書く // int cc23 = cc[2][3]; address_t addressof_cc23 = memory_tell(); // cc23 を宣言 value_t valueof_cc23 = memory_read( addressof_cc + ( 2*4 + 3 ) * sizeof( int ), sizeof( int )); // cc[2][3] の値を得て memory_write( addressof_cc23, valueof_cc23, sizeof( int )); // cc23 の場所にその値を書く
これをポインタで書くとどうなるでしょうか
まず p[n] は *( p + n ) と同じなので cc[0] は *( cc + 0 ) と書けますね
この *( cc + 0 ) を受け取るポインタの型は何でしょう
cc の要素の型ですから int でしょうか
しかし int は cc[0] の型ではなく cc[0][0] の型ですね
では int* でしょうか
しかし int* は &cc[0] の型ではなく &cc[0][0] の型ですね
まず cc が何なのかを考えましょう
cc は int が3×4個ある配列です
言い方を変えると int が4つの配列が3個ある配列です
つまり「int が4つの配列」が3個の配列ということですから、この配列の要素は「int が4つの配列」ですね
つまり cc の要素である cc[0] は「int が4つの配列」であるということです
その cc[0] を指すポインタは int が4つの配列を指すポインタということですから、それは int (*)[4] になります
int (*p)[4] = cc; int p01 = *( *( p + 0 ) + 1 ); int p12 = *( *( p + 1 ) + 2 ); int p23 = *( *( p + 2 ) + 3 );
↓
// int p01 = *( *( p + 0 ) + 1 ); address_t addressof_p01 = memory_tell(); // p01 を宣言 address_t valueof_p = ( address_t )memory_read( addressof_p, sizeof( address_t )); // p の場所から読み出した値をシークオフセットとして value_t valueof_p01 = memory_read( valueof_p + ( 0*4 + 1 ) * sizeof( int ), sizeof( int )); // そこから値を得て memory_write( addressof_p01, valueof_p01, sizeof( int )); // p01 の場所にその値を書く // int p12 = *( *( p + 1 ) + 2 ); address_t addressof_p12 = memory_tell(); // p12 を宣言 address_t valueof_p = ( address_t )memory_read( addressof_p, sizeof( address_t )); // p の場所から読み出した値をシークオフセットとして value_t valueof_p12 = memory_read( valueof_p + ( 1*4 + 2 ) * sizeof( int ), sizeof( int )); // そこから値を得て memory_write( addressof_p12, valueof_p12, sizeof( int )); // p12 の場所にその値を書く // int p23 = *( *( p + 2 ) + 3 ); address_t addressof_p23 = memory_tell(); // p23 を宣言 address_t valueof_p = ( address_t )memory_read( addressof_p, sizeof( address_t )); // p の場所から読み出した値をシークオフセットとして value_t valueof_p23 = memory_read( valueof_p + ( 2*4 + 3 ) * sizeof( int ), sizeof( int )); // そこから値を得て memory_write( addressof_p23, valueof_p23, sizeof( int )); // p23 の場所にその値を書く
p[n] と *( p + n ) が同じだったように *( *( p + n ) + m ) と p[n][m] も同じ意味です
ただしこの p は int* ではなく int (*)[M] であるというところが違います
ところで上記の例を見ておわかりの通り cc は int [3][4] ですが中身は int[12] と同じです
ですから下記のようにすることもできなくはありません
int* p = &cc[0][0]; int p01 = p[0*4+1]; int p12 = p[1*4+2]; int p23 = p[2*4+3];
↓
// int p01 = p[0*4+1]; address_t addressof_p01 = memory_tell(); // p01 を宣言 address_t valueof_p = ( address_t )memory_read( addressof_p, sizeof( address_t )); // p の場所から読み出した値をシークオフセットとして value_t valueof_p01 = memory_read( valueof_p + ( 0*4 + 1 ) * sizeof( int ), sizeof( int )); // そこから値を得て memory_write( addressof_p01, valueof_p01, sizeof( int )); // p01 の場所にその値を書く // int p12 = p[1*4+2]; address_t addressof_p12 = memory_tell(); // p12 を宣言 address_t valueof_p = ( address_t )memory_read( addressof_p, sizeof( address_t )); // p の場所から読み出した値をシークオフセットとして value_t valueof_p12 = memory_read( valueof_p + ( 1*4 + 2 ) * sizeof( int ), sizeof( int )); // そこから値を得て memory_write( addressof_p12, valueof_p12, sizeof( int )); // p12 の場所にその値を書く int p23 = p[2*4+3]; address_t addressof_p23 = memory_tell(); // p23 を宣言 address_t valueof_p = ( address_t )memory_read( addressof_p, sizeof( address_t )); // p の場所から読み出した値をシークオフセットとして value_t valueof_p23 = memory_read( valueof_p + ( 2*4 + 3 ) * sizeof( int ), sizeof( int )); // そこから値を得て memory_write( addressof_p23, valueof_p23, sizeof( int )); // p23 の場所にその値を書く
この通りまったく同じことです
ポインタと関数
関数が動作するとき、関数には必ずその関数専用のメモリ領域が存在しています
それは普通スタックというものですが、必ずしもスタックとは限りません
しかしともかく何らかの関数専用メモリ領域というものが存在します
そしてその関数に渡す引数はその関数専用のメモリ領域に置きますし、その関数のローカル変数もそこに置きます
この関数専用メモリ領域をファイルで考えると、関数が呼び出されたときの現在位置から先は好きに書き換えていいよ。関数への引数は現在位置の直前に置いておくよ。というようなことです
次のような関数があったとき、またシステム全体を一つのファイルであるとして考えてみると
void foo( int a, int b, int* presult ){ int result = 0; if( a > b ){ result = a; } else{ result = b; } *presult = result; }
↓
次のようになります
// void foo( int a, int b, int* presult ){ address_t current_address = memory_tell(); // 関数が呼び出されたときの現在位置を覚えておく address_t addressof_presult = current_address - sizeof( address_t ); // presult を宣言 address_t addressof_b = addressof_presult - sizeof( int ); // b を宣言 address_t addressof_a = addressof_b - sizeof( int ); // a を宣言 int valueof_a = ( int )memory_read( addressof_a, sizeof( int )); // a の値を得る int valueof_b = ( int )memory_read( addressof_b, sizeof( int )); // b の値を得る // int result = 0; address_t addressof_result = memory_tell(); // result を宣言 memory_write( 0, sizeof( int )); // result の場所に 0 を書く // if( a > b ){ if( valueof_a > valueof_b ){ // result = a; memory_write( addressof_result, valueof_a, sizeof( int )); // result の場所に a の値を書く // } } // else{ else{ // result = b; memory_write( addressof_result, valueof_b, sizeof( int )); // result の場所に a の値を書く // } } // *presult = result; int valueof_result = ( int )memory_read( addressof_result, sizeof( int )); // result の値を得る address_t valueof_presult = ( address_t )memory_read( addressof_presult, sizeof( address_t )); // presult の場所から読み出した値をシークオフセットとして memory_write( valueof_presult, valueof_result, sizeof( int )); // その場所に result の値を書く // } memory_seek( current_address ); // 元の位置の戻す
この foo を呼び出す関数 bar があったとき
void bar(){ int ab = 0; foo( 10, 20, &ab ); }
↓
これは次のようになります
// void bar(){ address_t current_address = memory_tell(); // 関数が呼び出されたときの現在位置を覚えておく // int ab = 0; address_t addressof_ab = memory_tell(); // ab を宣言 memory_write( 0, sizeof( int )); // ab の場所に 0 を書く // foo( 10, 20, &ab ); address_t current_address_before_foo = memory_tell(); // 現在位置を覚える memory_write( 10, sizeof( int )); // 現在位置に 10 を書く memory_write( 20, sizeof( int )); // 現在位置に 20 を書く memory_write( addressof_ab, sizeof( address_t )); // 現在位置に &ab を書く foo を呼ぶ memory_seek( current_address_before_foo ); // 元の位置の戻す // } memory_seek( current_address ); // 元の位置の戻す
このように関数にポインタを渡して値を返してもらうというのは、ファイルのシークオフセットを渡してそこに値を入れてもらうというのと同じことです
別に難しくも何ともないですね
ポインタと関数:その2
今、関数を呼び出すときには 10, 20, &ab の順番に渡しました
これは受け取る側が presult, b, a の順番で取り出しているからそれに合わせてそうしました
取りだす側が a, b, presult の順で取り出すなら &ab, 20, 10 の順番に渡す必要があります
どちらの順番にするべきでしょうか
また上記の例では元の位置に戻す処理を呼び出す側と呼び出された側と両方でやっていて無駄です
どちらがやっても同じことなのでどちらか一方だけがやればいいことですが、ではどちらがやるべきでしょうか
この引数を渡す順番・取り出す順番やアドレスを元に戻す責任などの決まりを関数の呼び出し規約(calling convention/calling sequence)と言います
conversion じゃないよ
コンパイルスイッチや cdecl, _cdecl, __cdecl, __stdcall, __pascal, __fastcall, __declspec(naked) などなどのコンパイラの予約語で指定している呼び出し規約というのはこの決まりごとのことです
ポインタと関数:その3
ところで関数からローカル変数のアドレスを返してはいけないということはどういうことかおわかりでしょうか
次のコードを見てください
void foo( int a, int** pp ){ int aaa[] = { 0, 1, 2, 3, 4, 5 }; *pp = &aaa[a]; }
↓
//void foo( int a, int** pp ){ address_t current_address = memory_tell(); // 関数が呼び出されたときの現在位置を覚えておく address_t addressof_pp = current_address - sizeof( address_t ); // pp を宣言 address_t addressof_a = current_address - sizeof( int ); // a を宣言 int valueof_a = ( int )memory_read( addressof_a, sizeof( int )); // a の値を得る // int aaa[] = { 0, 1, 2, 3, 4, 5 }; address_t addressof_aaa = memory_tell(); // aaa を宣言 memory_write( 0, sizeof( int )); // aaa の場所に 0 を書く memory_write( 1, sizeof( int )); // aaa の場所に 1 を書く memory_write( 2, sizeof( int )); // aaa の場所に 2 を書く memory_write( 3, sizeof( int )); // aaa の場所に 3 を書く memory_write( 4, sizeof( int )); // aaa の場所に 4 を書く memory_write( 5, sizeof( int )); // aaa の場所に 5 を書く // *pp = &aaa[a]; address_t valueof_pp = addressof_aaa + valueof_a; memory_write( addressof_pp, valueof_pp, sizeof( address_t )); // pp の場所に &aaa[a] を書く //} memory_seek( current_address ); // 元の位置の戻す
これを次のように呼び出してみるとどんなことが起きるでしょうか
int* p3 = NULL; foo( 3, &p3 ); int* p4 = NULL; foo( 4, &p4 ); int a3 = *p3; int a4 = *p4;
↓
// int* p3 = NULL; address_t addressof_p3 = memory_tell(); // p3 を宣言 memory_write( 0, sizeof( int )); // p3 の場所に 0 を書く // foo( 3, &p3 ); address_t current_address_before_foo1 = memory_tell(); // 現在位置を覚える memory_write( 3, sizeof( int )); // 現在位置に 3 を書く memory_write( addressof_p3, sizeof( address_t )); // 現在位置に &p3 を書く foo を呼ぶ memory_seek( current_address_before_foo1 ); // 元の位置の戻す // int* p4 = NULL; address_t addressof_p4 = memory_tell(); // p4 を宣言 memory_write( 0, sizeof( int )); // p4 の場所に 0 を書く // foo( 4, &p4 ); address_t current_address_before_foo2 = memory_tell(); // 現在位置を覚える memory_write( 4, sizeof( int )); // 現在位置に 4 を書く memory_write( addressof_p4, sizeof( address_t )); // 現在位置に &p4 を書く foo を呼ぶ memory_seek( current_address_before_foo2 ); // 元の位置の戻す // int a3 = *p3; address_t addressof_a3 = memory_tell(); // a3 を宣言 address_t valueof_p3 = ( address_t )memory_read( addressof_p3, sizeof( address_t )); // p3 の場所から読み出した値をシークオフセットとして int valueof_a3 = ( int )memory_read( valueof_p3, sizeof( int )); // その場所から読み出した値を memory_write( valueof_a3, sizeof( int )); // a3 の場所に書く // int a4 = *p4; address_t addressof_a4 = memory_tell(); // a4 を宣言 address_t valueof_p4 = ( address_t )memory_read( addressof_p4, sizeof( address_t )); // p4 の場所から読み出した値をシークオフセットとして int valueof_a4 = ( int )memory_read( valueof_p4, sizeof( int )); // その場所から読み出した値を memory_write( valueof_a4, sizeof( int )); // a4 の場所に書く
&p3 が 128 番地だったとしましょう
すると p3 自身の大きさがありますから p3 を宣言した後のシークオフセットは 128 + sizeof( int* ) 番地になりますね
そして、foo を呼び出すときに int と int* の分シークオフセットを進めるので、128 + sizeof( int* ) + sizeof( int ) + sizeof( int* ) 番地になります
ここで、sizeof( int ) を 4 バイト、sizeof( int* ) が 8 バイトであるとしてみると foo が呼び出された時点でのシークオフセットは 148 番地であることになります
foo では aaa を宣言していますから 148番地〜172番地までが aaa の領域になります
&aaa[3] は aaa + sizeof( int ) * 3 の位置ですから 148 + 12 で 160 番地になります
従って、最初の foo の呼び出しで p3 には 160 番地が入って戻ってくることになります
さて、foo から戻ってくると foo の中のローカル変数の分や foo に渡した引数の分は元に戻されるので、シークオフセットは 128 + sizeof( int* ) 番地、すなわち 136 番地に戻ります
ここで、p4 を宣言するので &p4 は 136 番地になります
そして、p4 自身の大きさの分シークオフセットを進めると 144 番地になります
ここでまた foo を呼び出すので int と int* の分シークオフセットを進めて、144 + sizeof( int ) + sizeof( int* ) で 156 番地になります
foo では 156番地〜180番地までが aaa の領域になります
さて、先ほど p3 には 160 番地が入っていましたね
ところが今その 160 番地は新しく呼び出された foo の aaa の領域になってしまいました
最初の呼び出しでは 160 番地には 3 が入っていましたが今は 2 になってしまいました
最初に関数専用のメモリ領域について「関数が呼び出されたときの現在位置から先は好きに書き換えていいよ」と言っていたのを覚えているでしょうか
これをもっと詳しく言うと「関数が呼び出される度にそのときそのときで現在位置がどこになっているのかはわからないけれども、どこであったとしてもともかく現在位置から先は好きに書き換えていいよ」ということなのです
呼び出されるたびにどこだかわからないというのですから、それはいつまでもそこにそのままあるものなどではなく、そのときだけの一時的なものなのです
この一時的というのが正確にはどのくらいなのかといえばそれは「その関数が呼び出されてから呼び出し元に戻るまでの間」です
つまり関数のローカル変数が有効なのはその関数の中だけです
だから「ローカル」というのです
ですからそのローカルなものを関数の外に出そうとしても壊れてしまうので、やってはいけませんということなのです
NULL ポインタ
アドレスが 1 ならメモリの先頭から 1バイト目の場所を指していますと言いましたが、ではアドレスが 0 ならメモリの先頭を指しているのでしょうか
そうであってもよさそうなものですが、違います
アドレスが 0 の場合だけは特別扱いになっていて、メモリの先頭を指しているのではなくどこも指していないものとして扱う。という決まりになっています
これが NULL ポインタです
NULL ポインタとはどこも指していないアドレスのことです
NULL ポインタはどこも指していないので NULL ポインタの指している先から値を得ようとすることは、エラーになりますし、NULL ポインタの指しているところに値を書き込もうとしてもエラーになります
ポインタとアセンブラ
ここではメモリ全体を一つのファイルと考えて、C/C++ のコードをファイル操作に置き換えて説明してきましたが、これは実はアセンブラがやっていることそのままです
ポインタやアドレスなどはアセンブラを覚えてしまえば全く当たり前のことになるので、新人さんはアセンブラも覚えてしましょう
アセンブラというと何か難しそうに感じるかもしれませんが、要はここに書いてあるファイル操作と同じ程度のことです
ファイル操作なら別に難しくもなんともないですよね
アセンブラに抵抗がなくなったら「C++プログラマのためのアセンブラ入門」でさらに理解を深めましょう
ポインタ変数の宣言
少し複雑になるとポインタ変数の宣言の読み方がわからなくなってしまうという人は「C/C++ 変数宣言の読み方入門」を読んでみるといいでしょう
規則を覚えてしまえば簡単ですよ