數據結構和算法(Golang實現)(4)簡單入門Golang-結構體和方法

結構體和方法

1、值,指針和引用

咱們如今有一段程序:算法

package main

import "fmt"

func main() {
    // a,b 是一個值
    a := 5
    b := 6

    fmt.Println("a的值:", a)

    // 指針變量 c 存儲的是變量 a 的內存地址
    c := &a
    fmt.Println("a的內存地址:", c)

    // 指針變量不容許直接賦值,須要使用 * 獲取引用
    //c = 4

    // 將指針變量 c 指向的內存裏面的值設置爲4
    *c = 4
    fmt.Println("a的值:", a)

    // 指針變量 c 如今存儲的是變量 b 的內存地址
    c = &b
    fmt.Println("b的內存地址:", c)

    // 將指針變量 c 指向的內存裏面的值設置爲4
    *c = 8
    fmt.Println("a的值:", a)
    fmt.Println("b的值:", b)

    // 把指針變量 c 賦予 c1, c1 是一個引用變量,存的只是指針地址,他們兩個如今是獨立的了
    c1 := c
    fmt.Println("c的內存地址:", c)
    fmt.Println("c1的內存地址:", c1)

    // 將指針變量 c 指向的內存裏面的值設置爲4
    *c = 9
    fmt.Println("c指向的內存地址的值", *c)
    fmt.Println("c1指向的內存地址的值", *c1)

    // 指針變量 c 如今存儲的是變量 a 的內存地址,但 c1 仍是不變
    c = &a
    fmt.Println("c的內存地址:", c)
    fmt.Println("c1的內存地址:", c1)
}

打印出:segmentfault

a的值: 5
a的內存地址: 0xc000016070
a的值: 4
b的內存地址: 0xc000016078
a的值: 4
b的值: 8
c的內存地址: 0xc000016078
c1的內存地址: 0xc000016078
c指向的內存地址的值 9
c1指向的內存地址的值 9
c的內存地址: 0xc000016070
c1的內存地址: 0xc000016078

那麼a,b是一個值變量,而c是指針變量,c1是引用變量。數組

若是&加在變量a前:c := &a,表示取變量a的內存地址,c指向了a,它是一個指針變量。數據結構

當獲取或設置指針指向的內存的值時,在指針變量前面加*,而後賦值,如:*c = 4,指針指向的變量a將會變化。併發

若是將指針變量賦予另一個變量:c1 := c,那另一個變量c1能夠叫作引用變量,它存的值也是內存地址,內存地址指向的也是變量a,這時候,引用變量只是指針變量的拷貝,兩個變量是互相獨立的。app

值變量能夠稱爲值類型,引用變量和指針變量均可以叫作引用類型。數據結構和算法

如何聲明一個引用類型的變量(也就是指針變量)呢?函數

咱們能夠在數據類型前面加一個*來表示:指針

var d *int

咱們之後只會以值類型,和引用類型來區分變量。code

2、結構體

有了基本的數據類型,還遠遠不夠,因此Golang支持咱們定義本身的數據類型,結構體:

// 結構體
type Diy struct {
    A int64   // 大寫導出成員
    b float64 // 小寫不能夠導出
}

結構體的名字爲Diy,使用type 結構體名字 struct來定義。

結構體裏面有一些成員Ab,和變量定義同樣,類型int64float64放在後面,不須要任何符號分隔,只須要換行便可。結構體裏面小寫的成員,在包外沒法使用,也就是不可導出。

使用結構體時:

// 新建結構體,值
    g := diy.Diy{
        A: 2,
        //b: 4.0, // 小寫成員不能導出
    }

    // 新建結構體,引用
    k := &diy.Diy{
        A: 2,
    }

    // 新建結構體,引用
    m := new(diy.Diy)
    m.A = 2

能夠按照基本數據類型的樣子使用結構體,上述創立的:

g := diy.Diy{
        A: 2,
        //b: 4.0, // 小寫成員不能導出
    }

是一個值類型的結構體。

你也可使用結構體值前面加一個&或者使用new來建立一個引用類型的結構體,如:

// 新建結構體,引用
    k := &diy.Diy{
        A: 2,
    }

    // 新建結構體,引用
    m := new(diy.Diy)
    m.A = 2

引用和值類型的結構體有何區別的?

咱們知道函數內和函數外的變量是獨立的,當傳參數進函數的時候,參數是值拷貝,函數裏的變量被約束在函數體內,就算修改了函數裏傳入的變量的值,函數外也發現不了。

但引用類型的變量,傳入函數時,雖然也是傳值,但拷貝的是引用類型的內存地址,能夠說拷貝了一個引用,這個引用指向了函數體外的某個結構體,使用這個引用在函數裏修改結構體的值,外面函數也會發現。

若是傳入的不是引用類型的結構體,而是值類型的結構體,那麼會完整拷貝一份結構體,該結構體和原來的結構體就沒有關係了。

內置的數據類型切片slice和字典map都是引用類型,不須要任何額外操做,因此傳遞這兩種類型做爲函數參數,是比較危險的,開發的時候須要謹慎操做。

3、方法

結構體能夠和函數綁定,也就是說這個函數只能被該結構體使用,這種函數稱爲結構體方法:

// 引用結構體的方法,引用傳遞,會改變原有結構體的值
func (diy *Diy) Set(a int64, b float64) {
    diy.A = a
    diy.b = b
    return
}

// 值結構體的方法,值傳遞,不會改變原有結構體的值
func (diy Diy) Set2(a int64, b float64) {
    diy.A = a
    diy.b = b
    return
}

只不過在之前函數的基礎上func Set(a int64, b float64),變成了func (diy *Diy) Set(a int64, b float64),只不過在函數裏面,可使用結構體變量diy裏面的成員。

上面表示值類型的結構體diy Diy可使用Set2方法,引用類型的結構體diy *Diy可使用Set方法。

若是是這樣的話,咱們每次使用結構體方法時,都要注意結構體是值仍是引用類型,幸運的是Golang操碎了心,每次使用一個結構體調用方法,都會自動將結構體進行類型轉換,以適配方法。好比下面:

// 新建結構體,值
    g := diy.Diy{
        A: 2,
        //b: 4.0, // 小寫成員不能導出
    }

    g.Set(1, 1)
    fmt.Printf("type:%T:%v\n", g, g) // 結構體值變化

    g.Set2(3, 3)
    fmt.Printf("type:%T:%v\n", g, g) // 結構體值未變化

    // 新建結構體,引用
    k := &diy.Diy{
        A: 2,
    }
    k.Set(1, 1)
    fmt.Printf("type:%T:%v\n", k, k) // 結構體值變化
    k.Set2(3, 3)
    fmt.Printf("type:%T:%v\n", k, k) // 結構體值未變化

結構體g是值類型,原本不能調用Set方法,可是Golang幫忙轉換了,咱們毫無感知,而後值類型就變成了引用類型。同理,k是引用類型,照樣可使用Set2方法。

前面咱們也說過,函數傳入引用,函數裏修改該引用對應的值,函數外也會發現。

結構體的方法也是同樣,不過範圍擴散告終構體自己,方法裏能夠修改結構體自己,可是若是結構體是值,那麼修改後,外面的世界是發現不了的。

3、關鍵字 new 和 make

關鍵字new主要用來建立一個引用類型的結構體,只有結構體能夠用。

關鍵字make是用來建立和初始化一個切片或者字典。咱們能夠直接賦值來使用:

e := []int64{1, 2, 3}                 // slice
    f := map[string]int64{"a": 3, "b": 4} // map

可是這種直接賦值相對粗暴,由於咱們使用時可能不知道數據在哪裏,數據有多少。

因此,咱們在建立切片和字典時,能夠指定容量大小。看示例:

s := make([]int64, 5)
    s1 := make([]int64, 0, 5)
    m1 := make(map[string]int64, 5)
    m2 := make(map[string]int64)
    fmt.Printf("%#v,cap:%#v,len:%#v\n", s, cap(s), len(s))
    fmt.Printf("%#v,cap:%#v,len:%#v\n", s1, cap(s1), len(s1))
    fmt.Printf("%#v,len:%#v\n", m1, len(m1))
    fmt.Printf("%#v,len:%#v\n", m2, len(m2))

運行後:

[]int64{0, 0, 0, 0, 0},cap:5,len:5
[]int64{},cap:5,len:0
map[string]int64{},len:0
map[string]int64{},len:0

切片可使用make([],佔用容量大小,所有容量大小)來定義,你能夠建立一個容量大小爲5,可是實際佔用容量爲0的切片,好比make([]int64, 0, 5),你預留了5個空間,這樣當你切片append時,不會由於容量不足而內部去分配空間,節省了時間。

若是你省略了後面的參數如make([]int64, 5),那麼其等於make([]int64, 5,5),由於這時所有容量大小就等於佔用容量大小。內置語言caplen能夠查看所有容量大小,已經佔用的容量大小。

同理,字典也能夠指定容量,使用make([],容量大小),可是它沒有所謂的佔用容量,它去掉了這個特徵,由於咱們使用切片,可能須要五個空白的初始值,可是字典沒有鍵的狀況下,預留初始值也沒做用。省略容量大小,表示建立一個容量爲0的鍵值結構,當賦值時會自動分配空間。

4、內置語法和函數,方法的區別

函數是代碼片斷的一個封裝,方法是將函數和結構體綁定。

可是Golang裏面有一些內置語法,不是函數,也不是方法,好比appendcaplenmake,這是一種語法特徵。

語法特徵是高級語言提供的,內部幫你隱藏瞭如何分配內存等細節。

系列文章入口

我是陳星星,歡迎閱讀我親自寫的 數據結構和算法(Golang實現),文章首發於 閱讀更友好的GitBook

相關文章
相關標籤/搜索