结构体定义与内存布局
1.1定义、引言
Golang中的结构体是一种复合数据类型,用于将不同类型的数据组合在一起。
例如一个学生的学号、姓名、性别
type Student struct{
id int
name,sex string
}
我们可以通过unsafe.Sizeof(x) 来确定一个变量占用的内存字节数
对于64位机器,占用内存大小
| 类型 | 字节数 |
|---|---|
| bool | 1 |
| intN,uintN,floatN | N/8((eg:int32是4个字节) |
| int,uint | 8 |
| 指针 | 8 |
| map,func | 8 |
| string | 16 |
| 切片 | 24 |
fmt.Println(unsafe.Sizeof(int(0))) //8
fmt.Println(unsafe.Sizeof(uintptr(1))) //8
fmt.Println(unsafe.Sizeof([]int{})) //24
fmt.Println(unsafe.Sizeof(map[string]int{})) //8
fmt.Println(unsafe.Sizeof(string(""))) //16
fmt.Println(unsafe.Sizeof(bool(true))) //1
计算一个结构体占用内存的大小
type Example struct {
a bool
b int
c string
}
func main() {
fmt.Println(unsafe.Sizeof(Example{})) //32
}
将每个字段的字节数相加,得到 1 + 8 + 16 = 25 个字节
但是最终编译器输出32字节
这表明计算 结构体占用内存大小 并不是仅仅将结构体中每个字段的字节数相加得到的
而一定遵循某种规律
实际上,这是因为 在内存布局方面,Golang存在内存对齐机制。内存对齐是为了提高CPU访问内存的效率,确保数据存储在核实的内存地址上。
1.2什么是内存对齐
要在计算机中访问一个变量,需要访问它的内存地址
从理论上讲似乎对任何类型的变量的访问可以从任何地址开始
但是实际情况是:在访问特定类型变量的时候 通常在特定的内存地址访问,这就需要对这些数据在内存中存放的位置进行限制
各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个地排列,这就是对齐
1.3为什么需要内存对齐
- 有些CPU可以访问任意地址上的任意数据,而有些CPU只能在特定地址访问数据,因为不同硬件平台具有差异性,这样的代码就不具有移植性,如果在编译时,将分配的内存进行对齐,这就具有平台可移植性了(移植性就是可以跨平台编译)
- CPU访问内存时,并不是逐个字节进行访问,而是以字长为单位访问(例如以4为字长,那就是访问地址为0、4、8…的位置),例如64位的CPU字长是8字节,32位的CPU字长是4字节。
如果变量的地址没有对齐,可能需要多次访问才能完整读取到变量内容,而对齐后可能就只需要一次内存访问,因此内存对齐可以减少CPU访问内存的次数
1.4内存对齐规则
64位机器最大对齐数为8,32位机器最大对齐数是4
对齐的规则:
- 占用空间的地址从0开始
- 变量占用内存空间是m,取对齐数 k 为m与最大对齐数中的较小值,那么该变量的起始地址是k的倍数,向后数m个就是该变量占用的内存空间
- 对每个成员进行对齐后,占用的内存空间必须是最大对齐数的整数倍,如果不是,那么需要在后面填充空白字节以达到整数倍
还是上面的 Example 结构体,实际上它在内存中是这样存储的
- 对于a(bool),bool是1字节,小于最大对齐数,所以bool对齐数是1,起始地址是0,占用一个空间
- 对于b(int),int是8字节,等于最大对齐数,所以 int 对齐数是8,起始地址要从8的倍数开始,向下找,找到8,从8开始向下数8个
- 对于c(string),string是16字节,大于最大对齐数,所以string对齐数是8,起始地址要从8的倍数开始,向下找到16,从16开始向下数16个,到31
- 32是8的整数倍,所以占用内存字节数是32

1.5对齐数函数unsafe.Alignof()
unsafe.Alignof(x)的返回值是m,当变量进行内存对齐时,需要保证分配到x的内存地址能够整除m。实际上这里的m就是上面提到的对齐数
对于64位的机器
fmt.Println(unsafe.Alignof(bool(true))) //1-----min(1,8)
fmt.Println(unsafe.Alignof(int32(0))) //4-----min(4,8)
fmt.Println(unsafe.Alignof(int64(0))) //8-----min(8,8)
fmt.Println(unsafe.Alignof(string(""))) //8-----min(16,8)
匿名字段与嵌入类型
2.1匿名结构体
匿名结构体就是不用给结构体起名字,直接在函数中定义,只能调用一次
可以在定义时直接赋值,也可以先定义后赋值
func main() {
//在定义时直接赋值
person := struct {
name string
city string
age int
}{name: "小张", city: "江苏", age: 18}
fmt.Println(person)
//先定义后赋值
person2 := struct {
name string
city string
age int
}{}
person2.name = "小李"
person2.city = "山东"
person.age = 19
fmt.Println(person2)
}
2.2匿名字段
匿名字段是指结构体字段中没有名字 只有类型的成员
对匿名字段赋值时,必须按照顺序,并且结构体中的字段必须是唯一的
如果要得到成员值,因为成员没有名字,所以直接访问字段就可以
// 结构体中的成员在声明中 没有名字只有类型 称为匿名字段
// 字段是唯一的
type person struct {
string
int
}
func main() {
p1 := person{
"哈哈", //注意逗号
17,
}
fmt.Println(p1)
fmt.Println(p1.string, p1.int)
}
2.3嵌套结构体
嵌套结构体就是结构体B作为结构体A中的成员
如果结构体A匿名嵌套结构体B
要访问结构体B有两种方法
1.通过嵌套的匿名结构体字段访问其内部的字段
2.直接访问匿名结构体中的字段
对于第二种,在查询字段时,首先在结构体person中查询字段,如果没能查找到,再去匿名结构体address中查找
// 描述人的家庭住址
type address struct {
province, city string
}
// 人的基本信息
type person struct {
name, gender string
age int
address //匿名嵌套另外一个结构体
}
func main() {
p1 := person{
name: "小王子",
gender: "nan",
age: 18,
address: address{
province: "河北",
city: "衡水",
},
}
fmt.Println(p1) //{小王子 nan 18 {河北 衡水}}
//通过嵌套的匿名结构体字段访问其内部的字段
fmt.Println(p1.address.province) //河北
//直接访问匿名结构体中的字段
fmt.Println(p1.province) //河北
}
- 字段冲突
如果嵌套了多个结构体,并且多个结构体里面都有一个相同的字段
type address struct {
province string
city string
updateTime string
}
type email struct {
Addr string
updateTime string
}
type person struct {
name string
age int
address //匿名嵌套address
email //匿名嵌套email
}
结构体person中嵌套了两个结构体address和email,这两个结构体都包含字段updateTime
定义一个person结构体类型的变量
p1 := person{
name: "小王子",
age: 18,
address: address{
province: "河北",
city: "衡水",
updateTime: "0505",
},
email: email{
Addr: "21231313@qq.com",
updateTime: "0504",
},
}
如果直接访问匿名结构体中的字段,编译器会报错

所以如果想访问重复的字段,必须通过嵌套的匿名结构体字段访问其内部的字段
否则并不知道要找的是哪一个结构体中的字段,所以必须说明是哪个结构体中的字段
p1.address.updateTime = "0505"
p1.email.updateTime = "0504"
2.4结构体继承
通俗来讲就是结构体A中嵌套了结构体B,结构体B满足性质x,那么结构体A也满足性质x
比如下面这两个结构体
// 动物
type Animal struct {
Name string
}
// 小狗
type Dog struct {
Feet int //有feet条腿
Animal //匿名嵌套Animal结构体
}
// 给Animal加方法
func (a Animal) Speak() {
fmt.Printf("%s会说话\n", a.Name)
}
// 给Dog加方法
func (d Dog) Wang() {
fmt.Printf("%s会汪汪叫\n", d.Name) //或d.Animal.Name
}
func main() {
d1 := Dog{
Feet: 4,
Animal: Animal{
Name: "平安",
},
}
d1.Wang()
d1.Animal.Speak()
d1.Speak()
}
结构体Dog中匿名嵌套了结构体Animal,Speak是Animal的方法,Wang是Dog的方法,对于语句
d1.Wang()
d1.Animal.Speak()
d1.Speak()
都成立,表明结构体Dog继承了Animal的方法
1896

被折叠的 条评论
为什么被折叠?



