今川館

都内勤務の地味OLです

ポインタがわからない①

わたしがプログラミングを初めて覚えたのはJavaだったけど、以来、JavaScriptPythonなど、ポインタを使わない言語しか経験が無い。
だから、Goの学習で一番苦戦しているのが「ポインタ」である。

まず、何がわからないのかもわからない。

暗中模索というか、完全に手探りで色々試してみるうちに、

  1. 星とか&とかからして何がなにやらという状態。「変数は箱、ポインタはメモリの番地」とか言われてもさっぱり理解不能(苦笑)
  2. ローカル変数の宣言に*を付けるべきか?
  3. 関数のレシーバーないし引数に*を付けるべきか?
  4. 関数の戻り値に*を付けるべきか?
  5. 変数の頭にも*を付けて利用できることを知って、頭がパァ?

やっとここまで進んできたという状態である。
自分の理解のためにも、ここまで理解できたことをメモしておくことにする。

アドレス演算子とポインタの間接参照

まず、&と*の呼び名からして識別困難だった。(記号なのでぐーぐらびりてーが低いし)

Goでこれらを使って自分なりに説明してみたのが以下。

var p Point // pはPoint型
var q *Point // qはPointのポインタ型 (普通の型とポインタ型は区別される)

p = Point{10, 20} // pにPointのオブジェクトを作って代入
q = &p // qに、アドレス演算子&を使ってpのポインタを作って代入
r := *q // 間接演算子*を使って、...代入

&のことを「アドレス演算子」、*のことを「間接演算子」と呼ぶらしい。

Goのドキュメントでは「間接演算子」という言葉は出てこなかったので、MSDNから言葉を拝借した。

間接演算子: *

単項間接演算子 (*) はポインターを逆参照します。つまり、ポインター値を左辺値に変換します

Goプログラミング言語仕様 - golang.jp

*T型(ポインタ)であるxをオペランドとするポインタの間接参照*xは、xによって指されるT型の値を表します。

代入したとき

ではこの後、qのフィールド(q.x, q.y)を書き換えるとp, rには影響するだろうか?

var p Point
var q *Point

p = Point{10, 20}
q = &p
r := *q

q.x, q.y = 7, 8
fmt.Println(p, q, r) // ここで何が出力されるか?

わたしは「{7 8} {7, 8} {7, 8}」が出力されると思ったが、実際は

{7 8} &{7 8} {10 20}

何と!? 間接参照すると本体を複製して代入するようだ。
それと、ポインタと普通の型は出力の表記も区別され、ポインタだと先頭に「&」がついている。

関数を通したとき

次に、関数を定義するときに引数や戻り値にポインタ型を指定するとどうなるのか確かめた。

point := func(p Point) Point { return p }
point2 := func(p *Point) *Point { return p }
point3 := func(p *Point) Point { return *p }

L := Point{1, 2}
M := point(L)
N := point2(&M)
O := point3(N)

M.x, M.y = -5, -6
fmt.Println(L, M, N, O) // ここで何が出力されるか?

これを実行すると何が表示されるだろうか?

まず、関数を定義すると引数はコピーされるという話は知っていたので、LとMは別物でMとNが同じものを指すだろうと思った。

FAQ - golang.jp

関数パラメータは値渡しか?

C言語系の言語と同様にGo言語ではすべて値渡しです。すなわち関数は常に値をパラメータに代入する代入ステートメントがあるかのように、渡されたもののコピーを受け取ります。たとえば関数にint型の値を渡すとint型のコピーが作成され、ポインタの値を渡すとポインタのコピーが作成されますが、ポインタが指し示すデータがコピーされるのではありません。

なので、わたしは「{1 2} {-5 -6} {-5 -6} {-5 -6}」が出力されると思っていたが、実際は

{1 2} {-5 -6} &{-5 -6} {1 2}

やはり間接演算子でポインタの本体の方を引っ張り出すと複製するルールは変わらないようだ。
それと、ポインタ型の変数は先頭に「&」をつけて出力される。

学んだこと

  1. Point型と*Point型は別物と区別され、後者は先頭に「&」をつけて表記される。
  2. 間接演算子*を使うと、ポインタの本体をコピーする。

いやぁ、難しい・・