LoginSignup
0
1

Goのポインタに関して図を用いて説明する

Last updated at Posted at 2022-08-06

初めに

Goのポインタを理解するのに苦戦したため、基本的な内容を自分なりにかみ砕いて投稿する。

■ 実行環境
The Go Playground
■ GOのバージョン
1.18

前提情報の整理

用語

  • メインメモリ(主記憶装置)・・・コンピュータのデータ記憶領域。
  • メモリアドレス・・・各メモリに割り振られるメモリのアドレス。
  • ポインタ(ポインタ変数)・・・メモリアドレスとその領域の型を格納する変数。
  • ポインタ型・・・ポインタの型。(Int型へのポインタ型、string型へのポイント型など)

※ポインタとポインタ変数に関しては、記事によって表現にブレがあるため、必ずしも今回の定義であるとは限らない。

ポインタは何を実現するものか

メモリアドレスを使用して、メモリにアクセスする手段である。
ポインタも変数であることに変わりはなく、別の変数のアドレスが保存されているアドレスというだけである。

なぜポインタに型が必要なのか

型によって必要なバイト数が異なるため。

変数とメモリ

コンピュータは、変数が定義された際にメインメモリに変数を格納する。
※本来データ型によって使用するメインメモリのバイト数は異なるが、今回は考慮しない。

var i int
イメージ図

SnapCrab_NoName_2022-4-6_9-13-29_No-00.png

101番のメモリに変数が入ったと思う。
この際の値はゼロ値である。

次に、101番のメモに格納されている変数に2を再代入してみる。

i = 2
イメージ図

SnapCrab_NoName_2022-4-6_9-16-37_No-00.png

101番の値が変更されていることが分かる。
このように、変数はメインメモリに格納されており、その中に値が入っている。

ポインタの宣言

  • ポインタのゼロ値はnilである。
  • %pは16進数でポインタが格納しているメモリアドレスを出力している。
  • %Tは型を出力している。

以下のコードでポインタを宣言できる。

基本形

var 変数名 *

func main() {
	// int型へのポインタを宣言
	var p *int
	// ポインタが格納しているゼロ値を出力
	fmt.Println(p)
	// int型へのポインタが格納しているメモリアドレスを出力
	fmt.Printf("ポインタ = %p", p)
	// ポインタの型を出力
	fmt.Printf("\n型 = %T\n", p)
}

// 出力結果
<nil>
ポインタ = 0x0
 = *int
イメージ図

SnapCrab_NoName_2022-4-8_12-42-55_No-00.png

ポインタは、宣言を行わずに変数の宣言と同時にメモリアドレスを代入しても作成可能である。

func main() {
	// 変数iに1を代入
	i := 1
	// 変数iのメモリアドレスを変数pに代入
	var p = &i
	// 変数pが格納しているメモリアドレスを出力
	fmt.Printf("ポインタ = %p", p)
	// 変数pの型を出力
	fmt.Printf("\n型 = %T\n", p)
}

// 出力結果
ポインタ = 0xc0000b8000
 = *int

ポインタにメモリアドレスを格納

上記のように宣言したポインタは、メモリアドレスを格納することができる。

  • &演算子はアドレス演算子と呼ばれ、メモリアドレスを指す。
  • *演算子を使用することで、メモリアドレスの値を参照できる。これをデリファレンスという。
func main() {
	// int型へのポインタを宣言
	var p *int
	// 変数iに1を代入
	i := 1
	// 変数iのメモリアドレスをポインタpに格納
	p = &i

	// ポインタが格納しているメモリアドレスを出力
	fmt.Println(p)
	// ポインタが格納しているメモリアドレスに格納されている変数の値を出力(すなわち変数iの値)
	fmt.Println(*p)
}

// 出力結果
0xc000018030
1
イメージ図

SnapCrab_NoName_2022-4-8_12-43-33_No-00.png

*演算子の使い方

ポインタに*演算子を付与して値を代入すると、ポインタが格納しているメモリアドレスにある変数の値を変更できる。

func main() {
	// int型へのポインタを宣言
	var p *int
	// 変数iに1を代入
	i := 1
	// 変数iのメモリアドレスをポインタpに格納
	p = &i

	// 変数iを出力
	fmt.Println(i)
	// ポインタに*演算子を使用して2を代入
	*p = 2
	// 変数iを出力
	fmt.Println(i)
}

// 出力結果
1
2
イメージ図

SnapCrab_NoName_2022-4-8_12-45-0_No-00.png

変数iが格納されているメモリのアドレスを代入したpに*演算子を使用して値を代入したことにより、iの値が変更されていることが分かる。

値渡しと参照渡し

値渡し

値渡しは、実引数に対して仮引数の値をコピーするというイメージである。
main関数内のx, yとpssValue内のx, yは異なるため、関数呼び出し後の変数の値は変更されていない。

func passValue(x, y int) {
	// mian関数とは別の変数xに、値1を格納する
	x++
	// mian関数とは別の変数yに、値5を格納する
	y = 5
	fmt.Println("関数内", x, y)
}

func main() {
	x := 1
	y := 2
	// 変数iの値を渡す
	passValue(x, y)
	fmt.Println("関数呼び出し後", x, y)
}

// 出力結果
関数内 2 5
関数呼び出し後 1 2
イメージ図

SnapCrab_NoName_2022-4-5_15-25-54_No-00.png

参照渡し

参照渡しは、実引数が仮引数を参照するイメージである。
変数x, yのポインタを仮引数に渡しているため、関数呼び出し後の変数の値が変更される。

func passRef(x, y *int) {
	// main関数内で宣言した変数x, yを参照する
	*x++
	*y = 5
	fmt.Println("関数内", *x, *y)
}

func main() {
	x := 1
	y := 2
	// passRef関数の実引数に、変数x, yのメモリアドレスを渡す
	passRef(&x, &y)
	fmt.Println("関数呼び出し後", x, y)
}

// 出力結果
関数内 2 5
関数呼び出し後 2 5
イメージ図

SnapCrab_NoName_2022-4-5_15-30-28_No-00.png

ポインタがあることのメリット

戻り値を返す必要がない場合がある

関数間でやりとりを行う際に、参照渡しを行うことによって、戻り値を返さずに変数の値を変更することができる。

メモリのバイト数の節約になる?

ポイント型よりバイト数が大きい値を値渡しする場合に、参照渡しをすることによってメモリのバイト数の節約になる可能性がある。
ポインタのサイズはデータ型によらず一定であり、関数にポインタを渡すのにかかる時間は1ナノ秒ほどと言われている。

細かい挙動

以下ではポインタの細かい挙動を記載している。

ポインタに*演算子を使用しないで値を代入しようとするとエラーになる。

func main() {
	var p *int
	// 変数iに1を代入
	i := 1
	// 変数iのメモリアドレスをポインタpに格納
	p = &i
	// ポインタに2を代入
	p = 2
}

// 出力結果
cannot use 2 (untyped int constant) as *int value in assignment

ポインタの使用用途は、あくまでもメモリアドレスを格納することにあると分かる。
値を代入して使用する場合は、変数を使用するべきである。

ポインタに別の型のメモリアドレスを代入する

また、int型へのポインタに別の型のメモリアドレスを代入するとエラーになる。

func main() {
	// 変数iに1を代入
	i := 1
	// 変数sに"GO"という文字列を代入
	s := "Go"
	// 変数pに変数iのメモリアドレスを格納
	var p = &i
	// 変数p(int型へのポインタ)に変数s(string型)のメモリアドレスを格納
	p = &s
}

// 出力結果
cannot use &s (value of type *string) as type *int in assignment

ポインタの型と同じ型の変数にメモリアドレスを代入

ポインタの型と同じ型の変数にメモリアドレスは代入できるのかを検証する。

func main() {
	var x, y int
	x = &y
}

// 出力結果
cannot use &y (value of type *int) as type int in assignment

int型とint型へのポインタは異なる型であるためエラーになる。

同じ型であれば、再代入することが可能である。

func main() {
	// int型へのポインタを宣言
	var p *int
	// 変数iに1を、変数inに2を代入
	i, in := 1, 2
	// 変数p(int型へのポインタ)に変数i(int型)のメモリアドレスを格納
	p = &i

	// 変数iのメモリアドレスを格納しているポインタpに2を代入
	*p = 2
	// 変数p(int型へのポインタ)に変数in(int型)のメモリアドレスを格納
	p = &in
	// 変数inのメモリアドレスを格納しているポインタpに2を代入
	*p = 3
	// 変数i、inを出力
	fmt.Println(i, in)
}

// 出力結果
2 3

メモリにアクセスしているため、変数i, inのどちらの値も変更されていることが分かる。

デフォルト値を持っているポインタはデリファレンスできない。

また、ポインタ型のデフォルト値はnilであり、nilのポインタをデリファレンスしようとするとエラーになる。

func main() {
	var i *int
	fmt.Println(*i)
}
// エラー内容
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x47aab6]

goroutine 1 [running]:
main.main()
	/tmp/sandbox3795421833/prog.go:11 +0x16

構造体のフィールドにポインタが使用されている場合

構造体のフィールドにポインタが使用されている場合はヘルパー関数を使用する。
以下の例だとエラーになる。

例1
func main() {
	type name struct {
		firstName string
		lastName  *string
	}
	s := name{"Johnson", "Noah"}
	fmt.Println(s)
}
// ./prog.go:14:23: cannot use "Noah" (untyped string constant) as *string value in struct literal
例2

初期化時にアドレス演算子(&)を使用してみる

func main() {
	type name struct {
		firstName string
		lastName  *string
	}
	s := name{"Johnson", &"Noah"}
	fmt.Println(s)
}

// ./prog.go:14:24: invalid operation: cannot take address of "Noah" (untyped string constant)

上記は文字数リテラルNoahにアドレス演算子を使用している。
しかし、文字数リテラルは不変であるためアドレス演算子を使用できないためエラーになっている。
そのため以下のヘルパー関数を使用するとポインタ型のフィールドの値を定義することができる。

func main() {
	type name struct {
		firstName string
		lastName  *string
	}
	s := name{"Johnson", stringp("Noah")}
	fmt.Println(s) // {Johnson 0xc000096020}
	fmt.Println(*s.lastName)
}

func stringp(s string) *string {
	return &s
}

stringp関数の引数sは変数であり、ポインタを使用することができるので正常に動作する。

関数にポインタ型を渡した場合の挙動

関数にポインタ型引数に代入された値が関数が終了しても残って欲しい場合はデリファレンスして値を変更する必要がある。
下記だと、xを代入しているが関数が終了すると値は引き継がれない。
それは以下の理由がある。

  • 関数fで渡している引数がポインタのコピーであるため
func f(p *int) {
	x := 10
	p = &x
	fmt.Println(p, *p) // 0xc000012028 10
}
func main() {
	var i *int
	f(i)
	fmt.Println(i) // <nil>
}

では、先ほどの問題を解決した下記だとどうだろうか。

func f(p *int) {
	x := 10
	p = &x
}
func main() {
	i := 1
	f(&i)
	fmt.Println(i) // 1
}

これでも変数iの値は変わっていない。
これは関数fで受け取ったポインタをデリファレンスして値を変更していないためである。
上記を解決したコードは下記である。

func f(p *int) {
	*p = 10
}
func main() {
	i := 1
	f(&i)
	fmt.Println(i) // 10
}

これで、関数にポインタを渡して変更することができる。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1