92
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Go言語のポインタについて、自分流の認識の仕方

Last updated at Posted at 2017-11-24

Go言語(Golang)を学んでいると避けることのできない要素であるポインタ。
僕はC言語を通ってこなかったエンジニアであるため、このポインタという考え方について最初はいまいちしっくり来ていませんでした。*とか&とか何なんだ状態です。
色々なサイトでGo言語のポインタの使用例や、C言語におけるポインタの考え方などを見ていく中で、自分流のGo言語におけるポインタの考え方を見つけたので、その内容を共有してみようと思います。

諸注意

投稿者はGo言語を業務等で使用したこともなく、勉強中の身です。そのため今回紹介させていただく認識方法については、コードリーディングや写経をする中で何をやっているかをふんわり理解するためのアプローチとして紹介させていただきます。
とはいえ間違った知識を拡散することは本意ではありませんので、「その認識の仕方はよくない」といった点がございましたらご指摘いただけますよう、お願いいたします。

ポインタ変数はモード切り替え機能付き変数

まずポインタ変数はモードチェンジ可能なハイテク変数であると捉えています。
使用できるモードは以下の2つ

  • アドレスモード
  • 間接参照モード

それぞれどういった機能を持つモードであるのかについて解説していきます。

アドレスモード

ますポインタ変数は以下のように定義します。

package main

func main() {
	var pointerSample *int
	sample := 3
	pointerSample = &sample
	println(pointerSample)
}

定義した状態では、ポインタ変数は アドレスモードとして扱われます。
アドレスモードの状態だとその変数は、他の変数が格納されている場所情報であるアドレスという値のみを格納する変数として振る舞います。
&マークを変数につけることで、変数からアドレス情報を取得することが可能となります。
この時の出力は以下の通りです。

$ go run main.go
0xc42003ff68

もし以下のように、普通の変数と同じ感覚で値を代入しようとするとコンパイルエラーが発生します。

package main

func main() {
	var pointerSample *int
	pointerSample = 3
	println(pointerSample)
}
./main.go:5:16: cannot use 3 (type int) as type *int in assignment

ではどうすればこのpointerSample変数を処理で活用できるような値として取り扱うことができるのか。ここで用いられるのがポインタ変数の間接参照モードです。

間接参照モード

最初のサンプルコードの最後の箇所を少しいじります。

package main

func main() {
	var pointerSample *int
	sample := 3
	pointerSample = &sample
  // ↓ *をつけることでモードチェンジされる
	println(*pointerSample)
}

この結果は以下の通り。慣れ親しんだ整数が表示されます。

$ go run main.go
3

代入されているのは変わらずsampleのアドレスではありますが、先頭に*をつけることで、pointerSampleがいつも扱っているようなint型変数として表示されるようになりました。

## アスタリスクの区別
僕ははじめ、この異なる使い方をされるアスタリスクに混乱していたのですが、この振る舞いから以下のように認識するようになりました。

*(アスタリスク)の使用箇所 効果
変数定義の際に型の前につける* ポインタ変数として定義する
定義されたポインタ変数の前につける* ポインタ変数を値として操作する(間接参照モードに切り替える)

何が嬉しいのか

個人的にポインタに限らず、理解が進まない原因となるのは、その機能を使えて何が嬉しいのか、が分からないということであると考えています。
上の説明だと「なら最初から普通の変数として定義すればいいじゃない」と思われるかと思います。全くもってその通りです。しかしこれが関数と変数をやりとりする際に必要となってきます。
## Go言語の関数の引数は値渡し
Go言語において、関数に対して引数を渡すとき、それらは値渡しとなります。
値渡しとは何なのか。以下の例を見ていただけるとわかりやすいかと思います。

package main

func main() {
	var number int
	number = 1
	tashizan(number)
	println(number)
}

// 渡された引数に10を足す関数
func tashizan(num int) {
	num += 10
}

渡された引数に10を足す関数であるtashizanに呼び出し元で定義して1を代入したnumberを渡しています。ひどい命名ですみません。
この時の出力は以下の通りとなります。

$ go run main.go
1

要するに関数の中で、呼び出し元から渡された引数をいじくりまわしても、呼び出し元における変数の状態には影響が及ばないわけです。
呼び出し元という天界から賜ったものは、呼び出し先という下界からは好き勝手に影響を与えることができないということです。
このtashizanメソッドを活用しようとする場合、以下のように書くことも可能です。

package main

func main() {
	var number int
	number = 1
	number = tashizan(number)
	println(number)
}

// 引数に10を足した値を返すようにする。
func tashizan(num int) int {
	return num + 10
}

単純な処理においてはこのような対応することも可能ですが、複雑な処理をしようとした時、どうしても呼び出し先で引数の値を変化させたいケースが出てきます。その時に活用されるのがポインタ変数となるわけです。

package main

func main() {
	var number int
	number = 1
  // number変数のアドレスを関数に渡す。
	tashizan(&number)
	println(number)
}

// ポインタ変数として引数を受け取る
func tashizan(num *int) {
	*num += 10
}

このように書き換えることによって、関数の中で渡された引数に影響を与えることができるようになるわけです。

やっていること

tashizan(&number)のようにtashizan関数に対して、numberのアドレス、つまりnumberの本体が格納されている場所情報を渡しています。
そのアドレスを関数側でポインタ変数として受け取り、間接参照モードに切り替えて足し算を行なっているわけです。
*num += 10(*num = *num + 1)によって、呼び出し元のnumberの値が変更されていることが分かります。

俗な例え

これをもう少し身近な例えで抽象的に説明してみましょう。
通常の値渡しとポインタ変数を用いた渡し方はそれぞれ

  • メールでのファイル転送
  • オンラインファイル共有サービス

の関係に似ています。

## ストーリー
AさんとBさんはOshigoto.xlsxというファイルを共同で編集しています。
Aさんは自分の担当する分の作業を完了したので、続きの編集作業をBさんにお願いしようとしています。

  • Aさん = 関数の呼び出し元
  • Bさん = 呼び出される関数

と例えて読み進めてみてください。

値渡しのケース

Aさんが続きの作業を頼むために、自分の作業を行なったOshigoto.xlsxをBさんにメールで送信したとします。
Bさんは転送されたOshigoto.xlsxに自分の作業を反映させていくわけですが、その作業はAさんの保持するOshigoto.xlsxには何の影響も与えません
なぜならBさんに転送されたOshigoto.xlsxはAさんの持つOshigoto.xlsxのコピーがメール転送されたにすぎないからです。
Bさんの作業をAさんの持つOshigoto.xlsxに反映させようとした場合、

1. Bさんが作業を完了したら、Aさんに修正したOshigoto.xlsxを送り返す

func bsan(oshigoto int) int {
	return oshigoto + 10
}

2. Aさんは送り返されたOshigoto.xlsxの修正された箇所を自分の持つOshigoto.xlsxに反映する。あるいは上書き保存する

func main() {
	var oshigoto int
	oshigoto = 1
	oshigoto = bsan(oshigoto)
	println(oshigoto)
}

といったプロセスが必要になるわけです。これを何度も行うとなるとめんどくさいですし、反映ミスなども発生しかねません。
あいだに挟んだサンプルコードの通り、メール転送 ≒ 値渡しといったイメージです。

ポインタ渡しのケース

このめんどくさい共同作業はオンラインファイル共有サービス(Google Driveなど)を用いることで改善されます。
オンライン上にOshigoto.xlsxを配置し、自分の担当分の作業が完了したら、そのファイルにアクセスできるURLをBさんに伝えます

func main() {
	var oshigoto int
	oshigoto = 1
	bsan(&oshigoto)
	println(oshigoto)
}

BさんはそのURLにアクセスし、直接ファイルを修正することが可能となります。

func bsan(oshigoto *int) {
	*oshigoto += 10
}

ファイルに施された修正はそのままリアルタイムでオンライン上のファイルに反映されます。
これでAさんはメールでファイルを転送したり、送り返されてきたファイルを自分の持つファイルに反映するという雑務から解放されるというわけです。
オンラインファイル共有を用いた共同作業 ≒ ポインタ渡しとなります。

まとめ

いかがだったでしょうか。技術的な知識の未熟から、かなりふんわりした伝え方になってしまいましたが、ポインタの取り扱いに困っている初心者の方のお役に少しでも立てば嬉しく思います。
また、ここの伝え方は間違っている、など見受けられた際はコメントいただければ対応をさせていただきますので、是非ご指摘ください。
閲覧いただき、ありがとうございました。

92
51
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
92
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?