Go
プログラミング
ポインタ

Goで学ぶポインタとアドレス


Goで学ぶポインタとアドレス

Goってシンプルで書きやすいですよね。

しかし、シンプルなGoでもいくつか躓きやすいポイントがあると思っています。

その最初のポイントがポインタではないでしょうか。特に、ポインタの概念が存在しない言語から始めた人にとっては、なかなかとっつきにくいものだと思います。そこで今回は、なんとなく使っていたポインタを、ちゃんと理解するためのエントリを書きました。ポインタをちゃんと理解しようとすると、その前提として知らなければならないことが多々あり、そこから説明するので、やや遠回りをした説明になっています。

「これちげえじゃねえか」とか、「ここわかりにくいぜ」っていうのがあったら、ご教授ください。

※ 技術的な話は「です、ます」調よりも「である、だ」調の方が書きやすいので、以降は「である、だ」調で書きます。


前提知識Part

先ほど述べたとおり、ポインタを理解しようとすると、前提知識が必要になってくる。

まずは、その前提知識を説明したいと思う。


プログラムのコンパイルから実行までの流れ

何かしらの高級言語(GoとかJavaとか)で書かれたソースコードはそのままではそのプログラムをPCで実行することはできない。

ではどうするかというと、高級言語で書かれたソースコードをコンパイラでコンパイルし、コンピュータがプログラムを実行できるような形にする。

この「実行できるような形」は、バイナリーコードになった実行ファイルである。


変数とメモリとアドレス

ポインタを理解するには、まず変数とメモリとアドレスの関係を理解する必要がある。

ここで整理したいと思う。


  • メモリは、1バイト毎に番号がつけられ、区別されている

  • 変数は実行ファイルになると、番号が割り当てられる

  • 変数は、メモリ上の該当の番号の区分に格納され、記憶される

  • この変数に付与されるメモリの区分番号をアドレスという

図にするとこんな感じ

メモリと変数.png

ここでいうメモリ1番地とかがアドレスで、実際にはあとで説明するが、0x1040a0d0 みたいな感じの16進数で表される。

参考 : 変数とメモリの関係 - 苦しんで覚えるC言語

例えば、以下の様にする。

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)』丸善出版


参考にさせていただいたサイト

バイナリーコード(ばいなりーこーど)とは - コトバンク

変数とメモリの関係 - 苦しんで覚えるC言語

メモリの仕組み - 苦しんで覚えるC言語

C言語のポインタきらい - Qiita

ポインタ変数とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典

Part4 誰もがつまずくポインタを完璧理解 | 日経 xTECH(クロステック)

C言語ポインタの基礎 - Qiita

Goのポインタ - はじめてのGo言語

【C言語入門】ポインタのわかりやすい使い方(配列、関数、構造体) | 侍エンジニア塾ブログ | プログラミング入門者向け学習情報サイト

もう一度基礎からC言語 第38回 プログラミングの周辺事項(1)~Cで書いたプログラムの仕組みと構造 Cプログラムの構造

Go言語の構造体の値渡しとポインタ渡しの動作を確認してみる - Qiita