ここでは、Go言語のポインタ(Pointers)について基本的なところを見ていきたいと思います。
ポインタはメモリ上のアドレスを扱います。C言語などをやっている人にはおなじみの処理だと思います。
すべての値はメモリ上に保存されています。このメモリ内のすべての場所にアドレスが割り当てられています。このアドレスとそこに格納されている値との関係をポインタの処理でみていきましょう。
ポインタのアドレス演算子の&とポインタ型の*
それではGoのポインタをやって行きましょう。
ポインタを扱うときの特徴としては、アドレス演算子の&(アンパサンド)とポインタ型の*(アスタリスク)を使うところです。
次のような形で利用します。(といっても、これだけじゃ意味がわかりませんが…)
&変数
*変数
*型名
&では、変数の値が格納されているメモリ上のアドレスを提供します。*を伴った変数は、そのアドレスの場所にある値を操作することになります。また、*で型名を作るとポインタ型となります。
具体的なコードで見て行きましょう。
package main
import "fmt"
func main() {
var n int = 10
fmt.Println("n:", n)
fmt.Println("&n:", &n)
fmt.Printf("type:%T\n", n)
fmt.Printf("type:%T\n", &n)
var p *int = &n
// p := &n
fmt.Println("p:", p)
fmt.Println("*p:", *p)
fmt.Println("*&n:", *&n)
fmt.Printf("type:%T\n", p)
fmt.Printf("type:%T\n", *p)
fmt.Printf("type:%T\n", *&n)
*p = 99
fmt.Println("n:", n)
}
実行結果を見ながら説明して行きましょう。型名を確認しながらのコードになっています。
結果は次のように表示されます。
n: 10
&n: 0xc0000b2008
type:int
type:*int
p: 0xc0000b2008
*p: 10
*&n: 10
type:*int
type:int
type:int
n: 99
まずint型の変数nに10を代入して初期化しています。
Println()でnを表示しています。これは、これまでやってきた通常の出力で10が表示されています。
次は、&nとして変数nにアドレス演算子&をつけたものを出力しています。出力すると、メモリのアドレスの「0xc0000b2008」が表示されています。このアドレスは各環境で異なります。
次にPrintf()でnと&nの型を出力していますが、int型とポインタ型のintとそれぞれ異なっているということが確認できます。
次にポインタ型のintでpを宣言しています。これはコメントアウトのように省略宣言の形でもいいです。ここに、&nを代入しています。これはnのアドレスを示していますから「0xc0000b2008」ということになります。
この変数pを出力すると、アドレスを代入したので当然「0xc0000b2008」が出力されています。
次にこの変数pに*をつけて*pを出力しています。この*(アスタリスク)を付けることで、アドレスに格納されている値を示すことになります。アドレスは元々の出発点のnのアドレスであり、そのnに格納されているのは「10」でしたので、ここでも「10」が出力されているのがわかります。
次は試しに同様のことを&nに*をつけて出力しています。アドレスの差している値を扱うわけですから、こちらも「10」が出力されています。
これら3つの型を調べて出力すると、ポインタ型のint、int型、int型となっているのが確認できます。
*pはここまではアドレスに「10」が入っていたのが確認できました。この*pに「99」を代入しています。
そしてnをPrintln()で出力しています。pのアドレスは「0xc0000b2008」であり、これはnのアドレスを代入したものでした。先ほどの*pでアドレス「0xc0000b2008」にある値を「99」に書き換えました。これは同じアドレスであるnの値を書き換える操作になります。ですから、ここではnは「10」と出力されずに「99」と出力されています。
ポインタの図解
このコードのポインタの処理の流れを図に示すと次のようなイメージになります。
上のコードの処理と見比べてみるとイメージを掴む参考にはなるのかと思います。
値が渡されているところ、アドレスが渡されているところ、アドレスの示す先の値を示しているところ、それを変更しているところ、という処理の流れになっていると思います。
このようにポインタは、&と*でメモリ上のアドレスを利用したデータの参照をすることができます。
ポインタを利用した値の共有の確認
もう少しポインタを触ってみましょう。
ポインタを使用すると、メモリ上に格納されている値を共有することができます。
それによって、多くのデータを渡さなくてもよくなったり、特定のメモリ上のデータを変更したりすることができます。
次のコードで見てみましょう。
package main
import "fmt"
func main() {
x := 10
fmt.Println("処理前 &x", &x)
fmt.Println("処理前 x", x)
foo(&x)
fmt.Println("処理後 &x", &x)
fmt.Println("処理後 x", x)
}
func foo(y *int) {
fmt.Println("処理前 y", y)
fmt.Println("処理前 *y", *y)
*y = 99
fmt.Println("処理後 y", y)
fmt.Println("処理後 *y", *y)
}
変数xに10を代入して初期化しています。
アドレスと値をPrintln()で出力したあとに、xのアドレスを引数に持ったfoo()を実行します。これは別で定義しています。
foo()を実行した後に、xのアドレスと値を再度表示します。
foo()関数の定義を続けて行っています。アドレスの引数y(ポインタ型のint)に持った関数です。yのアドレスと格納された値*yを出力し、*yを99に書き換えて、アドレスと値を出力しています。
実行するとこうなります。
処理前 &x 0xc000012068
処理前 x 10
処理前 y 0xc000012068
処理前 *y 10
処理後 y 0xc000012068
処理後 *y 99
処理後 &x 0xc000012068
処理後 x 99
foo()の呼び出し時点で処理前はxの値は、アドレスがポインタで引き継がれているので、xと*yで共有されています。foo()内の*yの値が変更された処理の後は、その値がxにも共有されて処理後の出力になっています。これも、アドレスがポインタで引き継がれているためです。
このように、ポインタで値が共有されて処理されているのが確認できると思います。
ポインタは、渡された値がアドレスなのか、値そのものなのかを確認するのが良いでしょう。
最後に
ここではGo言語のポインタの基本について扱いました。C言語を学んでいる人には理解しやすいでしょう。
ポインタはメモリ上のアドレスを扱います。
アドレス演算子&を使ってメモリアドレスを取得します。*を使ってアドレスの差している値を参照します。また、*はポインタ型を宣言します。
ポインタの操作と通常の変数の操作との違いを確認しましょう。