Goで学ぶポインタとアドレス
Goってシンプルで書きやすいですよね。
しかし、シンプルなGoでもいくつか躓きやすいポイントがあると思っています。
その最初のポイントがポインタではないでしょうか。特に、ポインタの概念が存在しない言語から始めた人にとっては、なかなかとっつきにくいものだと思います。そこで今回は、なんとなく使っていたポインタを、ちゃんと理解するためのエントリを書きました。ポインタをちゃんと理解しようとすると、その前提として知らなければならないことが多々あり、そこから説明するので、やや遠回りをした説明になっています。
「これちげえじゃねえか」とか、「ここわかりにくいぜ」っていうのがあったら、ご教授ください。
※ 技術的な話は「です、ます」調よりも「である、だ」調の方が書きやすいので、以降は「である、だ」調で書きます。
前提知識Part
先ほど述べたとおり、ポインタを理解しようとすると、前提知識が必要になってくる。
まずは、その前提知識を説明したいと思う。
プログラムのコンパイルから実行までの流れ
何かしらの高級言語(GoとかJavaとか)で書かれたソースコードはそのままではそのプログラムをPCで実行することはできない。
ではどうするかというと、高級言語で書かれたソースコードをコンパイラでコンパイルし、コンピュータがプログラムを実行できるような形にする。
この「実行できるような形」は、バイナリーコードになった実行ファイルである。
変数とメモリとアドレス
ポインタを理解するには、まず変数とメモリとアドレスの関係を理解する必要がある。
ここで整理したいと思う。
- メモリは、1バイト毎に番号がつけられ、区別されている
- 変数は実行ファイルになると、番号が割り当てられる
- 変数は、メモリ上の該当の番号の区分に格納され、記憶される
- この変数に付与されるメモリの区分番号をアドレスという
ここでいうメモリ1番地とかがアドレスで、実際にはあとで説明するが、0x1040a0d0
みたいな感じの16進数で表される。
例えば、以下の様にする。
name := "太郎"
そうすると、コンパイルした時に、メモリ上のある場所に変数の値が格納される。
この メモリ上のある場所
が上記で説明した アドレス
というものである。
メモリ上に変数が格納される場所がアドレスである。
実際に格納されたアドレスを16進数で表示させることもできる。
詳しくはここを参照。
package main
import "fmt"
// Person は人間を表す構造体。
type Person struct {
Name string
Age int
}
func main() {
// ポインタ型の変数を宣言する
// pがポインタ変数
// *Personポインタ型
var p *Person
p = &Person{
Name: "太郎",
Age: 20,
}
fmt.Printf("変数pに格納されているアドレス :%p", p)
}
実行結果
変数pに格納されているアドレス :0x1040a0d0
参考 : メモリの仕組み - 苦しんで覚えるC言語
ポインタPart
ポインタ型とポインタ変数
ポインタという概念を学ぶ時に、よく以下のような説明を目にする。
- ポインタってのはメモリのアドレス情報のことだよ
- ポインタってのはアドレス情報を格納するための変数のことだよ
これらの説明はわかりやすいのだが、実際にコードを見た時には「結局どれがポインタなの?」ってなりがちだ。
その疑問ついて以下の記事が非常にわかりやすかったので、一読されるといいと思う。
C言語のポインタきらい - Qiita
上記の記事によれば、以下のコードの pが ポインタ変数
で、 *Person
がポインタ型になる。
コード例
package main
import "fmt"
// Person は人間を表す構造体。
type Person struct {
Name string
Age int
}
func main() {
// ポインタ型の変数を宣言する
// pがポインタ型変数
// *Personポインタ型
var p *Person
p = &Person{
Name: "太郎",
Age: 20,
}
fmt.Printf("p :%+v\n", p)
fmt.Printf("変数pに格納されているアドレス :%p", p)
}
実行結果
p :&{Name:太郎 Age:20}
変数pに格納されているアドレス :0x1040a0d0
pを表示すると、 &{Name:太郎 Age:20}
となることを覚えておいて欲しい。
&
については後ほど説明する。
ポインタ変数とは
メモリ上のアドレスを値として入れられる変数のこと
引用元 : ポインタ変数とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典
上記のコードでは、変数pがポインタ変数となり、実際にpにはアドレスが格納されている。(詳細は後述)
デリファレンス
&
を使うことで、ポインタ型を生成することができる。
Person型の変数pを &p
とすると、Personへのポインタである *Person型
の値を生み出すことができる。
&p
は、pのアドレスという。
package main
import "fmt"
// Person は人間を表す構造体。
type Person struct {
Name string
Age int
}
func main() {
// 値として、pに代入
p := Person{
Name: "太郎",
Age: 20,
}
fmt.Printf("最初のp :%+v\n", p)
p2 := p
p2.Name = "二郎"
p2.Age = 21
// pではなく
fmt.Printf("p2で二郎に書き換えを行なったはずのp :%+v\n", p)
// &pで*Person(Personのポインタ型)を生成する
// p3はpのアドレスが格納されている状態になる
p3 := &p
p3.Name = "二郎"
p3.Age = 21
fmt.Printf("p3で二郎に書き換えを行なったp :%+v\n", p)
}
実行結果
最初のp :{Name:太郎 Age:20}
p2で二郎に書き換えを行なったはずのp :{Name:太郎 Age:20}
p3で二郎に書き換えを行なったp :{Name:二郎 Age:21}
pはポインタではなく、Person型の値である。
p2 := p
は、Person型の値コピーしてp2に格納しているので、p2で書き換えを行っても、それがpに反映されることはない。これを値渡しという。
逆に、p3 := &p
は、*Person型(Personへのポインタである *Person型)をp3に格納しているので、p3はpのアドレス(Personへのポインタである *Person型)を持っていることになる。
従って、p3で書き換えを行うと、その変更はpに反映される。これを参照渡しという。
Goでは、構造体内のメソッド内で、構造体のフィールドの情報を変更するときには、この参照渡しをよく利用する。こことかが参考になる。
*Hoge型が格納された変数
&
を使うことで、ポインタ型を生成することができた。
では、&
を使って生成されたポインタ型を格納した変数はどう扱うか。
まずは、&
の復習もかねて、以下のコードを見てみよう。
package main
import "fmt"
func main() {
name := "太郎"
fmt.Printf("name :%v\n", name)
namePoint := &name
// namePointは、&nameが格納されているだけなので、stringへのポインタである *string型の値が格納されている。
fmt.Printf("namePoint :%v\n", namePoint)
// namePointが指している変数は、"*namePoint"という感じで、"*"をつけて表す。
fmt.Printf("namePoint :%v\n", *namePoint)
}
実行結果
name :太郎
namePoint :0x1040c128
namePoint :太郎
コードに示したように namePoint
には &name
が格納されている。
&
は、ポインタ型を生成するので&name
は、stringへのポインタである *string型
の値(アドレス)が格納されている。
よって、 namePoint
を表示すると *string型
の値である name
のアドレスが格納されていることがわかる。
では、namePoint
の元となっている name
の変数に格納されている値(ここでは「太郎」)は、どのように取得すれば良いか。
そのような場合は、 *namePoint
のように変数名の前に *
をつければ良い。
なお、ここが紛らわしいところなのだが、 *namePoint
自体も変数なので、これに代入することもできる。
例えば、以下のようなコードだ。
package main
import "fmt"
func main() {
name := "太郎"
fmt.Printf("name :%v\n", name)
namePoint := &name
// namePointは、&nameが格納されているだけなので、stringへのポインタである*string型の値が格納されている。
fmt.Printf("namePoint :%v\n", namePoint)
// namePointが指している変数は、"*namePoint"という感じで、"*"をつけて表す。
fmt.Printf("namePoint :%v\n", *namePoint)
*namePoint = "二郎"
// *namePointに値を代入することもできる。
fmt.Printf("*namePointに二郎を代入後の*namePoint :%v\n", *namePoint)
// 再代入したところで、namePointに格納されている*string型の値(アドレス)自体は、変わらない
fmt.Printf("*namePointに二郎を代入後のnamePoint :%v\n", namePoint)
// stringへのポインタである*string型の値(nameに格納されている値)を書き換えたので、nameの値も変更される。
fmt.Printf("*namePointに二郎を代入後のname :%v\n", name)
}
実行結果
name :太郎
namePoint :0x1040c128
namePoint :太郎
*namePointに二郎を代入後の*namePoint :二郎
*namePointに二郎を代入後のnamePoint :0x1040c128
*namePointに二郎を代入後のname :二郎
ここで注意すべきことは、
*namePoint
に値を代入すると、nameの値も書き変わるということだ。
これはなぜか?
*namePoint
には、 &name
(stringへのポインタである*string型の値が格納されているからであり、それを *namePoint = "二郎"
で書き換えているので、当然 name
の値も書き変わるということである。
まとめ
ポインタは確かにとっつきにくいかもしれないですが、Goを使用する上では必須ですし、使い方によっては非常に便利なものなので、ちゃんと理解して使っていきましょう。
参考
参考文献
松尾 愛賀 (2016/4/15)『スターティングGo言語』 翔泳社
Alan A.A. Donovan (著), Brian W. Kernighan (著), 柴田 芳樹 (翻訳)(2016/6/20)『プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)』丸善出版
参考にさせていただいたサイト
ポインタ変数とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典
Part4 誰もがつまずくポインタを完璧理解 | 日経 xTECH(クロステック)
【C言語入門】ポインタのわかりやすい使い方(配列、関数、構造体) | 侍エンジニア塾ブログ | プログラミング入門者向け学習情報サイト
もう一度基礎からC言語 第38回 プログラミングの周辺事項(1)~Cで書いたプログラムの仕組みと構造 Cプログラムの構造