ポインター (ポインター) は、Go 言語の 2 つの中心的な概念に分割できます。
- 型ポインタ。このポインタ型のデータを変更できます。ポインタは、データをコピーせずに直接データを転送するために使用できます。型ポインタをオフセットして操作することはできません。
- 開始要素への生のポインタ、要素の数、および容量で構成されるスライス。
Go 言語のポインタ型変数は、このような制約と分割の恩恵を受けて、ポインタ オフセットのない効率的なポインタ アクセスという特性を備えており、キー データの不正な変更の問題を回避します。同時に、ガベージ コレクションでは、オフセットされないポインターの取得とリサイクルも容易になります。
スライスは生のポインタよりも強力で安全です。スライスが範囲外になると、ランタイムはクラッシュを報告してスタックをポップアウトしますが、元のポインターはクラッシュするだけです。
C/ C++のポインター
C/C++ のポインターのことになると、特にポインターのオフセット、演算、変換に関して多くの人が「青ざめる」でしょう。
実はポインタはC/C++言語の非常に高いパフォーマンスの根幹であり、大きなブロックのデータを操作したり、オフセットを作成したりするときに便利で便利です。したがって、オペレーティング システムは依然として C 言語とポインタの特性を使用して記述されています。
C/C++ におけるポインタ批判の根本原因は、ポインタの操作とメモリ解放にあります。C/C++ の生のポインタは自由にオフセットでき、場合によってはオペレーティング システムのコア領域にオフセットされることもあります。更新や修復が頻繁に必要となるコンピュータ オペレーティング システムの本質は、ポインタの範囲外アクセスによって引き起こされる「バッファ オーバーフロー」の問題を解決することです。
ポインタを理解するには、ポインタ アドレス、ポインタ タイプ、およびポインタ値といういくつかの概念を理解する必要があります。これらについては、以下で詳しく説明します。
ポインタアドレスとポインタタイプを知る
ポインタ変数は任意の値のメモリ アドレスを指すことができ、ポインタ変数が指す値のメモリ アドレスは 32 ビット マシンと 64 ビット マシンでそれぞれ 4 バイトまたは 8 バイトを占有し、占有バイトのサイズは関係ありません。それが指す値のサイズで決まります。ポインタが定義されていて変数に割り当てられていない場合、そのデフォルト値は nil です。ポインター変数は、ptr と略されることがよくあります。
各変数には実行時にアドレスがあり、メモリ内の変数の位置を表します。 Go言語では変数名の前に&
演算子(プレフィックス)を付けて変数のメモリアドレスを取得(アドレス演算)し、以下のような形式となります。
ptr := &v
このうち、 v はアドレスを取得する変数を表し、変数 v のアドレスは変数 ptr で受け取ります、 ptr の型は*T
で、これを T のポインタ型といい、 *
ポインタを表します。
ポインターの実際の使用法は、次の例で理解できます。
package main
import (
"fmt"
)
func main() {
var cat int = 1
var str string = "banana"
fmt.Printf("%p %p", &cat, &str)
}
操作結果:
0xc042052088 0xc0420461b0
コードの説明は次のとおりです。
- 8 行目では整数変数 cat を宣言しています。
- 9 行目で文字列変数 str が宣言されています。
- 10 行目では、fmt.Printf の動詞
%p
を使用して cat 変数と str 変数のメモリ アドレスを出力します。ポインターの値は、16 進数の0x
がプレフィックスとして付けられたデータのセットです。
ヒント: 変数、ポインタ、およびアドレスの関係は、各変数がアドレスを持ち、ポインタの値がアドレスであるということです。
ポインタからポインタが指す値を取得します。
&
演算子を使用して共通変数のアドレスをフェッチし、変数のポインタを取得した後、ポインタに対して*
演算子を使用すると、ポインタの値をフェッチできます。コードは次のとおりです。
package main
import(
"fmt"
)
ファンクmain(){
//文字列の準備
var house = "Malibu Point 10880, 90265"
//文字列をアドレスにする, ptr型は*string
ptr := &house
//ptrの型を印刷
fmt.Printf("ptr type: %T\n", ptr)
//ptrのポインターアドレスを印刷
fmt.Printf("address: %p\n", ptr)
//ポインターを値操作する
value := *ptr
//値の型
fmt.Printf("value type: %T\n", value)
//ポインターの値は変数の値を指します
fmt.Printf("value: %s\n", value)
}
操作結果:
ptr type: *string
address: 0xc0420401b0
value type: string
value: Malibu Point 10880, 90265
コードの説明は次のとおりです。
- 10行目で文字列を用意して代入しています。
- 13 行目、文字列のアドレスを取得し、ポインタを変数 ptr に保存します。
- 16 行目、変数 ptr の型を出力します。その型は *string です。
- 19行目、ptrのポインタアドレスを出力します。アドレスは実行するたびに変わります。
- 22 行目では、ptr ポインター変数に対して値演算が実行され、変数値の型は文字列です。
- 行 25、値を取得した後の値のタイプを出力します。
- 28 行目で、value の値を出力します。
アドレス取得演算子&
と値取得演算子*
相補的な演算子のペアであり、 &
はアドレスを取得し、 *
アドレスに従ってアドレスが指す値を取得します。
変数、ポインタアドレス、ポインタ変数、フェッチアドレス、フェッチ値の相互関係と特性は以下のとおりです。
- 変数のアドレスを取得するには、
&
演算子を使用してこの変数のポインター変数を取得します。 - ポインタ変数の値はポインタアドレスです。
*
演算子を使用してポインター変数の値を取得し、ポインター変数が指す元の変数の値を取得します。
ポインタを使用して値を変更する
ポインタを介して値を取得できるだけでなく、値を変更することもできます。
複数の代入を使用して値を交換する方法はすでに説明しましたが、ポインタを使用して値を交換することもできます。
package main
import "fmt"
// 交換関数
func swap(a、b * int) {
// aのポインターの値を、一時変数tに代入します
t:=* a
// bのポインターの値を、aのポインターが指し示す変数に代入します
* a = * b
// aのポインターの値を、bのポインターが指し示す変数に代入します
* b = t
}
func main() {
// 2つの変数を準備し、1と2を割り当てます
x、y:=1、2
// 変数の値を交換する
swap(& x、& y)
// 変数の値を出力する
fmt.Println(x、y)
}
操作結果:
2 1
コードの説明は次のとおりです。
- 6 行目では、パラメータ a と b を使用して交換関数を定義しています。パラメータ a と b は両方とも *int ポインタ型です。
- 9 行目では、ポインタ a の値を取得し、その値を変数 t (この時点では int 型) に代入します。
- 12 行目では、b のポインタ値を取得し、それをポインタ a が指す変数に代入します。このときの
*a
の意味はポインタの値を取ることではなく、「aが指す変数」であることに注意してください。 - 15 行目で、t の値がポインタ b が指す変数に代入されます。
- 21行目では変数xとyの2つを用意し、それぞれ値1と2を代入しており、型はintとなっています。
- 24 行目では、x と y のアドレスをパラメータとして取り出し、swap() 関数に渡して呼び出します。
- 27行目、スワップが完了したらxとyの値を出力します。
*
演算子が右辺値として使用される場合、ポインターの値を取得することを意味し、左辺値として使用される場合、つまり代入演算子の左側に配置される場合、変数が指すことを意味します。ポインタによって に。実際、要約すると、 *
演算子の基本的な意味は、ポインタが指す変数を操作することです。演算が右辺値に対して行われる場合は、変数が指す値を取得し、演算が左辺値に対して行われる場合は、値を指す変数に設定します。
ポインタ値が swap() 関数で交換されるとどうなりますか?次のコードを参照できます。
package main
import "fmt"
func swap(a、b*int){
b、a = a、b
}
func main() {
x、y:= 1、2
swap(&x、&y)
fmt.Println(x、y)
}
操作結果:
1 2
交換は失敗したことが分かりました。上記のコードの swap() 関数は、a と b のアドレスを交換します。交換が完了すると、実際に a と b の変数値が交換されます。しかし、a と b に関連付けられた 2 つの変数は実際には関連しません。 2つの家が書かれたカードをテーブルに広げたようなもので、2つの家のカードを交換した後は、2つの家には何の影響も与えません。
例: ポインター変数を使用してコマンドラインから入力を取得する
Go 言語の組み込みフラグ パッケージはコマンド ライン パラメーターの解析を実装しており、フラグ パッケージによりコマンド ライン ツールの開発が容易になります。
次のコードは、いくつかのコマンド ライン命令と対応する変数を事前に定義し、実行時に対応するパラメーターを入力します。フラグ パッケージが解析された後にコマンド ライン データを取得できます。
[例] コマンドライン入力を取得します。
package main
// システムパッケージをインポートする
import(
flag,
fmt
)
// コマンドライン引数を定義する
Var mode = flag.String("mode", "", "process mode")
func main(){
// コマンドライン引数を解析する
flag.Parse()
// コマンドライン引数を出力する
fmt.Println(*mode)
}
このコードに main.go という名前を付け、次のコマンド ラインで実行します。
go run main.go –mode=fast
コマンドラインの出力は次のとおりです。
fast
コードの説明は次のとおりです。
- 行 10 では、flag.String までで、タイプが *string であるモード変数を定義します。次の 3 つのパラメータは次のとおりです。
- パラメータ名: コマンドラインにパラメータを入力するときにこの名前を使用します。
- パラメータ値のデフォルト値: フラグで使用される関数によって作成された変数タイプに対応し、String は文字列に対応し、Int は整数に対応し、Bool はブール値に対応します。
- パラメータの説明: -help を使用すると、説明に表示されます。
- 15 行目では、コマンド ライン パラメーターを解析し、結果を変数 mode に書き込みます。
- 18 行目、モード ポインタが指す変数を出力します。
mode という名前のコマンド ライン パラメーターは以前に flag.String で登録されているため、フラグの最下層はコマンド ラインを解析する方法を認識し、その値を mode*string ポインターに割り当てます。Parse 呼び出しが完了した後は、何も必要ありません。フラグから値を取得しますが、最終的な値は自身が登録したモードポインタを介して取得されます。コードの実行プロセスを次の図に示します。
Go 言語では、ポインター変数を作成する別の方法も提供しています。その形式は次のとおりです。
new(type)
一般的には次のように書きます。
str := new(string)
*str = "Go言語チュートリアル"
fmt.Println(*str)
new() 関数は、対応する型のポインターを作成でき、作成プロセスによってメモリが割り当てられ、作成されたポインターはデフォルト値を指します。