[聚合文章] 基础技能树-24 结构体

.Net 2017-11-16 1 阅读

本节内容

  • 匿名字段与继承
  • 名称遮蔽(成员访问优先级)
  • 结构体内存布局

匿名字段与继承

当一个结构体包含其它类型的时候,它会把原来的类型展开,所以它的内存布局包含了所有展开的结果,展开的是类型基本的结构。

名称遮蔽(成员访问优先级)

名称遮蔽是很典型的语法糖。go支持这种结构。

package main

import "fmt"

type (
    file struct {
        name string
    }

    log struct {
        name string
    }

    data struct {
        file
        log
    }
)

func main() {
    d := data{}

    //d.name = "name"

    d.file.name = "file"
    d.log.name = "log"

    fmt.Printf("%+v\n", d)
}

定义了三个结构体, data 里面嵌入了file和log结构,但是只给了类型没有给字段的名字,这其实就是一个语法糖,真实写法就是用类型做名字:

type (
    data struct {
        file file
        log log
    }
)

只不过,字段名字和类型相同,所以就忽略掉了,忽略掉的好处就是我们可以直接用短名去访问嵌入的名字,看上去短名访问效应。

这地方就有个所谓的名称遮蔽问题,如果名字一样,高优先级的会遮蔽掉低优先级的。一般是自己的优先,没有的话找嵌入字段,嵌入字段从数据结构上来说类似于继承的东西,如果两个都有,它没有办法确定哪个,因为go没有真正意义上的继承概念,python有多继承的概念,它的规则是从左到右,从下到上。那么你就必须明确操作谁。

结构体内存布局

$ cat test.go
package main

import (
    "unsafe"
    "fmt"
)

type Data struct {
    a byte         // |a,c .....| b | d | x |
    c byte
    b int
    d int
    x struct{}
}

func main() {
    d := Data{
        a: 0x1,
        b: 0x100,
        c: 0x2,
        d: 0x3,
    }

    fmt.Println(d, unsafe.Sizeof(d))
}
$ go build -gcflags "-N" -o test test.go
$ gdb test
$ l
$ l
$ l
$ b 24
$ r
$ x/4xg &d #输出4组
$ x/8xb &d #输出a和c的内存数据,后面6组是垃圾数据

第一组除了1以外补了3个0,我们管这种东西叫做数据对齐,数据对齐有些时候有特定的要求,很多的内存优化策略和CPU对数据的访问要求你的地址是偶数,它的对齐规则比较简单,不管结构体里面有多少个字段,它会找出最长的基本类型,补位凑成最长的长度。数据对齐是平台的要求,这就带来一个问题,如果不注意的话你的内存开销比别人多很多。

有些时候仅仅是字段排列顺序的差别,可能带来差异。比如Data请求使用,而且在堆上内存分配,每次分配差8个字节,如果对象非常大的情况下内存开销就会有很大的不同,尤其是字段类型比较多的情况下这种差别可能会更大。

我们在不同的语言里对字段的对齐是不一样的,比如像c语言、go语言必须手工对齐,手工调整顺序,编译器不会帮你调整顺序,因为这很它的指针和偏移位置有关系。但是像Java或者C#语言,编译器会帮你最佳的排列组合,因为像Java或者C#不允许做指针访问的。大部分支持指针操作的语言它不会帮你做自动对齐处理的。这个时候你做比较复杂数据结构的时候而且实例数量非常多的时候你得考虑清楚你这种排列是不是最优的。

另外对齐还有个麻烦是空结构体。

关于零长度的数据类型一直是很特殊的东西,当零长度的数据作为结构体成员出现的时候,那么本身不占用任何空间。当作为数组类型的时候,这个数组其实并不存在,也就是并没有为这个数组分配内存空间,多数语言的空值都是用一个全局变量,也就是所有空值都是指向这个全局变量,所以你对这个数组的操作是没有意义的。数组只保留两个基本属性。

空结构体在最后一个的时候,我们取x的地址会取到别的地方的地址。所以当空结构体在最后一个的情况下,编译器会强制的把它当作1byte的数据类型来处理。这样虽然不能对x进行操作,但是取x地址的时候肯定是属于结构体的内存,而不会指向结构体以外的内存。你对x的指针引用可能不会让垃圾回收器对这个东西造成误解。这东西就是避免垃圾回收器造成误解,因为没有这个东西你的内存指向就越界了。如果空结构体放中间,对它取地址的话无非就指向下一个字段而已,肯定不会越界。

当我们学习一门语言时候,语言规范里面肯定不会告诉你很多细节,这些细节甚至和语言规范产生冲突,语言规范会告诉你按照最长的基本类型进行对齐。很多时候代码的异常行为并不是语言规范造成的,有可能是你的运行时选择的版本有关系。

注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。