【技術記事】Goで学ぶポインタとアドレス

※ 本記事は、過去に Qiitaでも投稿した記事である。

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