2012年7月14日土曜日

ポインタ、アドレス、リファレンス(渡し)


この出力結果は、以下のコードを実行したものです。
尚、printfの改行の書式が文字化けしているかも知れません。
(\nはバックスラッシュ(\n)じゃなくて半角の円マークを使うのが元々のコードです。)


#include <stdio.h>

int main()

{
 int i = 3; // 普通の整数を表すint型
 printf( " i = %d \n", i ); //iの内容を表示
 printf( " &i = %d \n", &i); // iのアドレスを表示

 int* pi; // int型を指すことが出来るポインタ型の宣言

 //pi = i; // ポインタ型に内容を入れるのは間違い
 pi = &i; // &をiの前に付けてアドレスを入れる
  
 printf( " pi = %d  \n", pi ); // piの内容、つまりiのアドレス
 printf( "&pi = %d \n", &pi );// pi自体のアドレス
 printf( "*pi = %d  \n", *pi );// iの内容
 
 int &ri = i; // iのリファレンスを作る
 printf( " ri = %d  \n", ri ); //iの内容
 //printf( " *ri = %d  \n", *ri ); //*は不要
 printf( "&ri = %d  \n", &ri ); //ri、つまりiのアドレス


        getchar();

}

解説 

変数のアドレス
&変数名
という書式で得る事が出来ます。アドレスは変数がメモリ上の何処にあるかを表す番号です。

例)
int i = 3; // 普通の整数を表すint型
&i ; // 3ではなく、iのアドレスを表す

アドレスが具体的にどんな数になるかは、実行環境によってその都度変わるだろうし、普段意識する必要がない部分です。

ポインタはアドレスを格納する変数です。
普通、アドレスは整数による番号で表されるため、
何らかの整数が格納されているという意味では
int等の整数型と大差ありません。

しかしプログラマーもコンパイラも
それが単なる整数型じゃなくてアドレスを保存する
特別な整数型として認識しているため、
その型の利用の仕方に特別な手段が用意されています。
それが&、*、->等です。
また足し算の意味も
アドレスを直接足すのではなく型のサイズを単位とした移動量となります。

&を使えば変数のアドレスを得られるため、
その値を直接ポインタに格納できます。

冒頭に示した実行結果に
&i = 1572576
pi =  1572576
とあります。2つ同じ数字が並んでいます。
この結果は事前の代入
pi = &i
によってポインタpi = 変数iのアドレスとなったからです。

ポインタはそのままではアドレスですが、
*ポインタ
けば、アドレスが指す変数の内容を得ることが出来ます
つまり例中では
*piと書けば
iのアドレスではなくiそのものとなります。

ちなみに
ポインタ型の宣言、つまり例中の
int* pi;
においても*という記号を使うので間際らしいかも知れませが、型の宣言のための*と内容を得るための*は無関係です。強いて言えば言語開発者がポインタ関連だから同じ記号を使ったのでしょうか?覚えやすいかも知れませんが意味的には逆の作用ですね・・・。
int* pi;(ちなみにint *pi;という風にスペースの区切りを変えてもOK)の時は、int型をポインタ型にしますが、一旦アドレスを確保したポインタに対して再びアスタリスク(*)を付けると今度はint型に戻るのですね・・・。まぁ戻るといっても瞬間的な話であって*を付けなければポインタであり続けるのですが。


また、
*ポインタ は、それが指す変数の中身を知るだけではなく、操作する事ができます。

例)
int n = 10;
int* pn = &n;
*pn =50;

こうするとnの値も50となります。ポインタは変数のコピーではなくアドレスです。そして*を付けている間はそのアドレスが指す変数そのものになってしまという事です。

また、ポインタ型はポインタ型同士で代入が出来ます。
例えばさっきの例に続いて
int* pn2 = pn;
*pn = 99;
と書くと nの値は99になります。
つまりこの状況はnの値はnという変数自身とpnというポインタとpn2というポインタの合計、3つの変数からアクセスされ得る分けです。(ポインタも変数の一種です。)しかし依然としてintの値を確保したメモリ自体は一つしか存在していません。


その他、 ここでは説明しませんがポインタには newによるオブジェクトの確保という使い道 やインクリメントやデクリメントという、配列とセットで使う演算があります。


リファレンス(参照)は変数の別名です。
例えば
int i =3;
int& ri = i;
と書けばriをiとまったく同様に扱えます。ポインタと違って内容を得るの時に*riと書く必要はありません。また、ポインタではないのでアドレスを格納できないので
ri = &i;
とは書けません。ポインタと違ってリファレンスは、宣言すると同時にメモリ実体のある変数を入れる必要があり、アドレスやポインタ型変数を入れることは出来ません。だからポインタと違ってリファレンスが指す内容が実体のあるメモリか既に破壊された領域かを気にするケースも通常はないでしょう。

---やや細かい話-------
ポインタが指すアドレスが必ずしも他の変数のアドレスではなく、newによって独自に確保したオブジェクトである可能性があるのに対し、リファレンスは確実に他の変数のです。アクセス手段こそ普通の変数と同じですが、どこか別の場所にも同じ内容を指す変数があるとうい事には注意が必要です。
さらに言えば、ポインタの様にアドレスは代入できませんが、ポインタが指す内容は代入可能です。
例)
int *pi = new int(10);
int& ri = *pi;
ですから、使い方によってはリファレンスが指す内容が消失している場合もあり得ます。
----------------------

リファレンスもややこしい事に?あるいは覚えやすくするために?
 型名の後に付ける&はリファレンスの作成を意味しますが
既に出来ている変数の前に付ける&はアドレスを得るアドレス演算子として働くので&の意味を混同しないで下さい。
(例)
int i = 3;
int &ri = i;
//これで変数iがriという名前でも扱えるようになった
&ri; //←これは3という値が保存されているアドレス
&i; //←これも3という値が保存されている&riと同じアドレス

この時、
ri =5にするとiも5になるし、
i=5にしてもriも5になります。
別の複数の名前が一つの変数の実体をいじれるというところはポインタと同様です。

関数引数のリファレンス

ここで問題
void func(int& i)
今使われた&の意味はどっちでしょうか?
iのアドレスでしょうか?
それともiは何かのリファレンスなのでしょうか?
見出しでバレバレかも知れませんが答えはリファレンスです。


 一般的に関数の引数では、容量の大きい構造体やクラスオブジェクトの無駄な生成を避けるために、あるいは渡されたオブジェクトの値をいじる時にリファレンスを使います。
その際、呼び出し元の変数を変更したいのかしたくないのか用途をはっきりさせるために、変更しない場合は
void func(const int& i)
という風にconstを付けるのが一般的です。
constを付けて初期化された変数は値を参照はできますが変更はできないので、関数内部でiの値を変更するコードを書くとコンパイルエラーとなります。

ちなみにconstの利用は関数内部に限った話ではありません。
constは定数を作る時に利用できます。
例)
const float pai = 3.141592f;
pai = 0; //コンパイルエラー


関数引数の文法と仕組み

単純に言えば、関数の引数においてもそれが呼びされた時点で
(型名 変数名)
という風に()の中で型の宣言に応じて変数の生成が行われます
つまり
 void func(int i)
という風に&を付けなければint iをここで生成します。その際、呼び出し元の引数の内容を新しく生成した変数のメモリにコピーします。intではピンと来ないでしょうがクラス型の場合コピーコンストラクタが呼ばれる事になります。こうして値こそ呼び出し元の引数と同じだけれど、メモリアドレスは別な部分に全く新しい変数が生成される分けです(どうせ関数のスコープを抜ければ解体されるのに)。

難しく聞こえたかも知れませんがこれは何も特別なことではなく、
int a = 3;
int b = a;
とやった時に、bはaからのコピーで初期化した分けです。
同様に
func(a);
を呼んでやれば
void func(int i)  
{
//この時点で int i = a;
//が発生している
}
という話です。

一方、
void func(int& i)
という風に&を付けてリファレンス型にしておくと余計な変数は生成せずに呼び出し元の引数のリファレンスが渡されます。(あるいは、 呼び出し元の引数のリファレンスを生成したと言い換えてもいいかも知れません。)

先程の例で言うと
int a =3;
int b& = a; //bはaのリファレンス
なのと同じように

func(a);
を呼んだ時に

void func(int& i)  
{
//この時点で int& i = a; が発生してる。
//つまりaのリファレンスiが作られた
}
という事になります。


2 件のコメント:

匿名 さんのコメント...

ありゃ もう更新はしないんですかね?
ちくちく見てたんですが

ほべるば さんのコメント...

おお、閲覧者が居て、嬉しい様な恥ずかしいようなw。
ポインタ、アドレス、リファレンス(渡し)
に関しては更新予定はありませんが
C++の記事全体で言えば…やっぱり今のところは書く予定がありませんw(しかしコピーコンストラクタの様なC++が勝手に生産するコンストラクタの話ぐらいは書いておこうかと思う事が時々あります。)

現在はDirect3D11のチュートリアルの翻訳を密かにリメイク中です。
今後の更新予定としてはDirect3D11関連が多くなると思います。