【Golang進階】指針的詳細講解

来源:https://www.cnblogs.com/BlameKidd/archive/2020/04/15/12709216.html
-Advertisement-
Play Games

指針是一個代表著某個記憶體地址的值, 這個記憶體地址往往是在記憶體中存儲的另一個變數的值的起始位置. Go語言對指針的支持介於Java語言和 C/C++ 語言之間, 它既沒有像Java那樣取消了代碼對指針的直接操作的能力, 也避免了 C/C++ 中由於對指針的濫用而造成的安全和可靠性問題. 指針地址和變數 ...


指針是一個代表著某個記憶體地址的值, 這個記憶體地址往往是在記憶體中存儲的另一個變數的值的起始位置.

Go語言對指針的支持介於Java語言和 C/C++ 語言之間, 它既沒有像Java那樣取消了代碼對指針的直接操作的能力, 也避免了 C/C++ 中由於對指針的濫用而造成的安全和可靠性問題.

指針地址和變數空間

Go語言保留了指針, 但是與C語言指針有所不同. 主要體現在:

  • 預設值: nil.
  • 操作符 & 取變數地址, * 通過指針訪問目標對象.
  • 不支持指針運算, 不支持 -> 運算符, 直接用 . 訪問目標成員.

先來看一段代碼:

package main

import "fmt"

func main(){ 
	var x int = 99
	var p *int = &x

	fmt.Println(p)
}

當我們運行到 var x int = 99 時, 在記憶體中就會生成一個空間, 這個空間我們給它起了個名字叫 x, 同時, 它也有一個地址, 例如: 0xc00000a0c8. 當我們想要使用這個空間時, 我們可以用地址去訪問,也可以用我們給它起的名字 x 去訪問.

繼續運行到 var p *int = &x 時, 我們定義了一個指針變數 p , 這個 p 就存儲了變數 x 的地址.

所以, 指針就是地址, 指針變數就是存儲地址的變數.

接著, 我們更改 x 的內容:

package main

import "fmt"

func main() {
	var x int = 99
	var p *int = &x

	fmt.Println(p)

	x = 100

	fmt.Println("x: ", x)
	fmt.Println("*p: ", *p)
	
	*p = 999

	fmt.Println("x: ", x)
	fmt.Println("*p: ", *p)
}

可以發現, x*p 的結果一樣的.

其中, *p 稱為 解引用 或者 間接引用.

*p = 999 是通過藉助 x 變數的地址, 來操作 x 對應的空間.

不管是 x 還是 *p , 我們操作的都是同一個空間.

棧幀的記憶體佈局

首先, 先來看一下記憶體佈局圖, 以 32位 為例.

image

其中, 數據區保存的是初始化後的數據.

上面的代碼都存儲在棧區. 一般 make() 或者 new() 出來的都存儲在堆區

接下來, 我們來瞭解一個新的概念: 棧幀.

棧幀: 用來給函數運行提供記憶體空間, 取記憶體於 stack 上.

當函數調用時, 產生棧幀; 函數調用結束, 釋放棧幀.

那麼棧幀用來存放什麼?

  • 局部變數
  • 形參
  • 記憶體欄位描述值

其中, 形參與局部變數存儲地位等同

當我們的程式運行時, 首先運行 main(), 這時就產生了一個棧幀.

當運行到 var x int = 99 時, 就會在棧幀裡面產生一個空間.

同理, 運行到 var p *int = &x 時也會在棧幀里產生一個空間.

如下圖所示:

image

我們增加一個函數, 再來研究一下.

package main

import "fmt"

func test(m int){
	var y int = 66
	y += m
}

func main() {
	var x int = 99
	var p *int = &x

	fmt.Println(p)

	x = 100

	fmt.Println("x: ", x)
	fmt.Println("*p: ", *p)

	test(11)

	*p = 999

	fmt.Println("x: ", x)
	fmt.Println("*p: ", *p)
}

如下圖所示, 當運行到 test(11) 時, 會繼續產生一個棧幀, 這時 main() 產生的棧幀還沒有結束.

image

test() 運行完畢時, 就會釋放掉這個棧幀.

image

空指針與野指針

空指針: 未被初始化的指針.

var p *int

這時如果我們想要對其取值操作 *p, 會報錯.

野指針: 被一片無效的地址空間初始化.

var p *int = 0xc00000a0c8

指針變數的記憶體存儲

表達式 new(T) 將創建一個 T 類型的匿名變數, 所做的是為 T 類型的新值分配並清零一塊記憶體空間, 然後將這塊記憶體空間的地址作為結果返回, 而這個結果就是指向這個新的 T 類型值的指針值, 返回的指針類型為 *T.

new() 創建的記憶體空間位於heap上, 空間的預設值為數據類型的預設值. 如: p := new(int)*p0.

package main

import "fmt"

func main(){
	p := new(int)
	fmt.Println(p)
	fmt.Println(*p)
}

這時 p 就不再是空指針或者野指針.

我們只需使用 new() 函數, 無需擔心其記憶體的生命周期或者怎樣將其刪除, 因為Go語言的記憶體管理系統會幫我們打理一切.

接著我們改一下*p的值:

package main

import "fmt"

func main(){
	p := new(int)
	
	*p = 1000
	
	fmt.Println(p)
	fmt.Println(*p)
}

這個時候註意了, *p = 1000 中的 *pfmt.Println(*p) 中的 *p 是一樣的嗎?

大家先思考一下, 然後先來看一個簡單的例子:

var x int = 10
var y int = 20
x = y

好, 大家思考一下上面代碼中, var y int = 20 中的 yx = y 中的 y 一樣不一樣?

結論: 不一樣

var y int = 20 中的 y 代表的是記憶體空間, 我們一般把這樣的稱之為左值; 而 x = y 中的 y 代表的是記憶體空間中的內容, 我們一般稱之為右值.

x = y 表示的是把 y 對應的記憶體空間的內容寫到x記憶體空間中.

等號左邊的變數代表變數所指向的記憶體空間, 相當於操作.

等號右邊的變數代表變數記憶體空間存儲的數據值, 相當於操作.

在瞭解了這個之後, 我們再來看一下之前的代碼.

p := new(int)

*p = 1000

fmt.Println(*p)

所以, *p = 1000 的意思是把1000寫到 *p 的記憶體中去;

fmt.Println(*p) 是把 *p的記憶體空間中存儲的數據值列印出來.

所以這兩者是不一樣的.

如果我們不在main()創建會怎樣?

func foo() {
	p := new(int)

	*p = 1000
}

我們上面已經說過了, 當運行 foo() 時會產生一個棧幀, 運行結束, 釋放棧幀.

那麼這個時候, p 還在不在?

p 在哪? 棧幀是在棧上, 而 p 因為是 new() 生成的, 所以在 上. 所以, p 沒有消失, p 對應的記憶體值也沒有消失, 所以利用這個我們可以實現傳地址.

對於堆區, 我們通常認為它是無限的. 但是無限的前提是必須申請完使用, 使用完後立即釋放.

函數的傳參

明白了上面的內容, 我們再去瞭解指針作為函數參數就會容易很多.

傳地址(引用): 將地址值作為函數參數傳遞.

傳值(數據): 將實參的值拷貝一份給形參.

無論是傳地址還是傳值, 都是實參將自己的值拷貝一份給形參.只不過這個值有可能是地址, 有可能是數據.

所以, 函數傳參永遠都是值傳遞.

瞭解了概念之後, 我們來看一個經典的例子:

package main

import "fmt"

func swap(x, y int){
	x, y = y, x
	fmt.Println("swap  x: ", x, "y: ", y)
}

func main(){
	x, y := 10, 20
	swap(x, y)
	fmt.Println("main  x: ", x, "y: ", y)
}

結果:

swap  x:  20 y:  10
main  x:  10 y:  20

我們先來簡單分析一下為什麼不一樣.

首先當運行 main() 時, 系統在棧區產生一個棧幀, 該棧幀里有 xy 兩個變數.

當運行 swap() 時, 系統在棧區產生一個棧幀, 該棧幀裡面有 xy 兩個變數.

運行 x, y = y, x 後, 交換 swap() 產生的棧幀里的 xy 值. 這時 main() 里的 xy 沒有變.

swap() 運行完畢後, 對應的棧幀釋放, 棧幀里的x y 值也隨之消失.

所以, 當運行 fmt.Println("main x: ", x, "y: ", y) 這句話時, 其值依然沒有變.

接下來我們看一下參數為地址值時的情況.

傳地址的核心思想是: 在自己的棧幀空間中修改其它棧幀空間中的值.

而傳值的思想是: 在自己的棧幀空間中修改自己棧幀空間中的值.

註意理解其中的差別.

繼續看以下這段代碼:

package main

import "fmt"

func swap2(a, b *int){
	*a, *b = *b, *a
}

func main(){
	x, y := 10, 20
	swap(x, y)
	fmt.Println("main  x: ", x, "y: ", y)
}

結果:

main  x:  20 y:  10

這裡並沒有違反 函數傳參永遠都是值傳遞 這句話, 只不過這個時候這個值為地址值.

這個時候, xy 的值就完成了交換.

我們來分析一下這個過程.

首先運行 main() 後創建一個棧幀, 裡面有 x y 兩個變數.

運行 swap2() 時, 同樣創建一個棧幀, 裡面有 a b 兩個變數.

註意這個時候, a b 中存儲的值是 x y 的地址.

當運行到 *a, *b = *b, *a 時, 左邊的 *a 代表的是 x 的記憶體地址, 右邊的 *b 代表的是 y 的記憶體地址中的內容. 所以這個時候, main() 中的 x 就被替換掉了.

所以, 這是在 swap2() 中操作 main() 里的變數值.

現在 swap2() 再釋放也沒有關係了, 因為 main() 里的值已經被改了.


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • BUAA OO 第二單元總結 Part 1 設計策略 這三次作業採用了 主線程獲取請求,多級調度器逐級分派,電梯模擬運行的策略 。具體來說,主線程實例化 類,通過阻塞讀取方式獲得請求 ,之後將請求分配給調度器 ,調度器負責處理請求(既可以自己處理,也可以分配給其他子調度器處理),每一個電梯與一個 綁 ...
  • 北航OO(2020)第二單元博客作業 [TOC] 設計策略分析(多線程視角) 本單元的三次作業中,我採用了相似的策略:採用輸入線程與電梯線程通過線程安全的調度器進行交互的方式。這種方式基本屬於生產者 消費者模式。在調度器的設計方面,我主要採用synchronized關鍵字結合wait和notify方 ...
  • 前言 這是一個基於中小型企業或團隊的架構設計。 不考慮大廠。有充分的理由相信,大廠有絕對的實力來搭建一個相當複雜的環境。 中小型企業或團隊是個什麼樣子? 開發團隊人員配置不全,部分人員身兼開發過程上下游的數個職責; 沒有專職的維護人員,或者維護人員實力不足以完全掌控生產和開發環境。 這種情況下,過於 ...
  • 無處不在的線程,多線程,阻塞隊列,併發 編程世界無新鮮事,看你翻牆翻得厲不厲害 場景:現在的軟體開發迭代速度(一周一更新,甚至一天一發佈)真是太快了,今天進行軟體更新的時候,看到了有趣的現象,這不就是線程池,ThreadPoolExecutor,阻塞隊列,任務(下載和安裝)最好的案例嘛!經常看到很多 ...
  • redis為什麼那麼快?結論有三點,大家都知道,這裡主要是分析。 首先第一點 redis是記憶體訪問的,所以快 當然這個大家都知道,所以不是重點 io密集型和cpu密集型 一般我們把任務分為io密集型和cpu密集型 io密集型 IO密集型指的是系統的CPU性能相對硬碟、記憶體要好很多,此時,系統運作,大 ...
  • 一、製作Nine-Patch圖片 1.含義:一種被特殊處理的png圖片,能夠指定哪些區域可以被拉伸,哪些區域不可以被拉伸。 2.首先先製作一個佈局 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xml ...
  • 實例級別的鎖 實例代碼 @Slf4j public class AddCompareDemo { private int a, b; public void add() { for (int i = 0; i < 10000; i++) { a++; b++; } } public void com ...
  • ThreadLocal ThreadLocal 適用於變數線上程間隔離,而在方法或類間共用的場景。 代碼 1 @RestController 2 public class ThreadLocalController { 3 private static final ThreadLocal<Strin ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...