Go to list of users who liked
More than 5 years have passed since last update.
Goで学ぶポインタとアドレス
Goってシンプルで書きやすいですよね。
しかし、シンプルなGoでもいくつか躓きやすいポイントがあると思っています。
その最初のポイントがポインタではないでしょうか。特に、ポインタの概念が存在しない言語から始めた人にとっては、なかなかとっつきにくいものだと思います。そこで今回は、なんとなく使っていたポインタを、ちゃんと理解するためのエントリを書きました。ポインタをちゃんと理解しようとすると、その前提として知らなければならないことが多々あり、そこから説明するので、やや遠回りをした説明になっています。
「これちげえじゃねえか」とか、「ここわかりにくいぜ」っていうのがあったら、ご教授ください。
※ 技術的な話は「です、ます」調よりも「である、だ」調の方が書きやすいので、以降は「である、だ」調で書きます。
前提知識Part
先ほど述べたとおり、ポインタを理解しようとすると、前提知識が必要になってくる。
まずは、その前提知識を説明したいと思う。
プログラムのコンパイルから実行までの流れ
何かしらの高級言語(GoとかJavaとか)で書かれたソースコードはそのままではそのプログラムをPCで実行することはできない。
ではどうするかというと、高級言語で書かれたソースコードをコンパイラでコンパイルし、コンピュータがプログラムを実行できるような形にする。
この「実行できるような形」は、バイナリーコードになった実行ファイルである。
変数とメモリとアドレス
ポインタを理解するには、まず変数とメモリとアドレスの関係を理解する必要がある。
ここで整理したいと思う。
- メモリは、1バイト毎に番号がつけられ、区別されている
- 変数は実行ファイルになると、番号が割り当てられる
- 変数は、メモリ上の該当の番号の区分に格納され、記憶される
- この変数に付与されるメモリの区分番号をアドレスという
ここでいうメモリ1番地とかがアドレスで、実際にはあとで説明するが、0x1040a0d0
みたいな感じの16進数で表される。
例えば、以下の様にする。
name:="太郎"
そうすると、コンパイルした時に、メモリ上のある場所に変数の値が格納される。
このメモリ上のある場所
が上記で説明したアドレス
というものである。
メモリ上に変数が格納される場所がアドレスである。
実際に格納されたアドレスを16進数で表示させることもできる。
詳しくはここを参照。
packagemainimport"fmt"// Person は人間を表す構造体。typePersonstruct{NamestringAgeint}funcmain(){// ポインタ型の変数を宣言する// pがポインタ変数// *Personポインタ型varp*Personp=&Person{Name:"太郎",Age:20,}fmt.Printf("変数pに格納されているアドレス :%p",p)}
実行結果
変数pに格納されているアドレス :0x1040a0d0
ポインタPart
ポインタ型とポインタ変数
ポインタという概念を学ぶ時に、よく以下のような説明を目にする。
- ポインタってのはメモリのアドレス情報のことだよ
- ポインタってのはアドレス情報を格納するための変数のことだよ
これらの説明はわかりやすいのだが、実際にコードを見た時には「結局どれがポインタなの?」ってなりがちだ。
その疑問ついて以下の記事が非常にわかりやすかったので、一読されるといいと思う。
C言語のポインタきらい - Qiita
上記の記事によれば、以下のコードの pがポインタ変数
で、*Person
がポインタ型になる。
コード例
packagemainimport"fmt"// Person は人間を表す構造体。typePersonstruct{NamestringAgeint}funcmain(){// ポインタ型の変数を宣言する// pがポインタ型変数// *Personポインタ型varp*Personp=&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のアドレスという。
packagemainimport"fmt"// Person は人間を表す構造体。typePersonstruct{NamestringAgeint}funcmain(){// 値として、pに代入p:=Person{Name:"太郎",Age:20,}fmt.Printf("最初のp :%+v\n",p)p2:=pp2.Name="二郎"p2.Age=21// pではなくfmt.Printf("p2で二郎に書き換えを行なったはずのp :%+v\n",p)// &pで*Person(Personのポインタ型)を生成する// p3はpのアドレスが格納されている状態になるp3:=&pp3.Name="二郎"p3.Age=21fmt.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型が格納された変数
&
を使うことで、ポインタ型を生成することができた。
では、&
を使って生成されたポインタ型を格納した変数はどう扱うか。
まずは、&
の復習もかねて、以下のコードを見てみよう。
packagemainimport"fmt"funcmain(){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 :0x1040c128namePoint :太郎
コードに示したようにnamePoint
には&name
が格納されている。&
は、ポインタ型を生成するので&name
は、stringへのポインタである*string型
の値(アドレス)が格納されている。
よって、namePoint
を表示すると*string型
の値であるname
のアドレスが格納されていることがわかる。
では、namePoint
の元となっているname
の変数に格納されている値(ここでは「太郎」)は、どのように取得すれば良いか。
そのような場合は、*namePoint
のように変数名の前に*
をつければ良い。
なお、ここが紛らわしいところなのだが、*namePoint
自体も変数なので、これに代入することもできる。
例えば、以下のようなコードだ。
packagemainimport"fmt"funcmain(){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 :0x1040c128namePoint :太郎*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プログラムの構造
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme