※ 本記事は、過去に Qiitaでも投稿した記事である。
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プログラムの構造