问 fun1 和 fun2 fun3分别输出什么,为什么?
func fun1() {
a := 2
c := (*string) (unsafe.Pointer(&a))
*c = "44"
fmt.Println(*c)
}
func fun2() {
a := "654"
c := (*string) (unsafe.Pointer(&a))
*c = "44"
fmt.Println(*c)
}
func fun3() {
a := 3
c := *(*string) (unsafe.Pointer(&a))
c = "445"
fmt.Println(c)
}
有疑问加站长微信联系(非本文作者)

实际工作中会用到这些吗?
一语中的,面试就喜欢挖些很无语的坑。
个人感觉面go的话,不太可能会问这些,面c问这些还是挺有可能的
你的说法我是不敢苟同的。你执行一下如下代码
你的回答没到点上,unsafe.Pointer 本身的定义就是任意指针类型,哪怕是 int 的转 string,转了之后,前者不会影响到后者。
你错了,会用到的
坐等令人信服的答案
你的答案根本不在点上
先说答案:
fun1() 如果不panic的话,是能输出44。但是有很大概率panic
fun2() 输出44
fun3() 输出445
原因如下:
首先,要理解go的string类型是什么
通过reflect包,我们可以知道,在Golang底层,string其实是struct:
type StringHeader struct { Data uintptr Len int }
其实一个string是一个保存了真实字符数组指针和长度的结构体
对于fun1 , a是一个int变量,unsafe.Pointer(&a) 是一个原生指针,指向的内存保存了一个int
c := (*string)(unsafe.Pointer(&a)) 将这个原生指针转换为了指向string的指针
注意c 现在是指向string的指针,换句话说,c指向的内存被当做了一个StringHeader 结构体来处理
但是c指向的内存,只分配了一个int的长度,并且这个int的值是2
所以目前c中 Data 值为2, Len是无效的,因为Len现在已经溢出了有效内存范围,其内的值是随机的(该溢出区域内存的原始值)
这时对 *c 进行赋值 "44", "44"是一个字符串常量,保存在只读的常量存储区中。
赋值时 其实是把常量字符串"44"的指针赋给了c的Data字段,把其长度2赋给了c的Len字段。 注意这里是一个野指针赋值,因为 Len 字段已经溢出到有效内存之外了,但是这里panic的概率很低,因为内存padding机制
然后输出 fmt.Println(*c) 这里,因为c的Len字段是溢出到无效内存中,所以如果其他指令有变量定义的操作,很可能把Len字段给覆盖掉 一旦Len被覆盖,那么其中保存的长度就不在是2,这时输出c,很大概率就会panic。 如果运气好,Len没有被覆盖,那么输出c,就能正常输出44
对于fun2,可以根据上述原理自行分析。因为a变量也是一个StringHeader,c其实与unsafe.Pointer(&a)指向同一个StringHeader。
对于fun3, 因为c本来就是一个新创建的string变量,也就是一个独立的StringHeader,没有复用变量a的内存,所以是正常的赋值和输出,自然不会 有问题。
简单说下我的理解:
不成功,原因在于 unsafe.Pointer 指向的内容在做类型转换时,新类型 B 占用的内存应该是不大于原类型A 的,而在这里,字符串"44" 所占用的内存(2个byte)是大于整数2所占用的内存(1个byte)的。
成功,都是2个byte
3 成功。把 a 这个变量变成了一个指向字符串类型的指针, c 作为这个指针指向的字符串,被赋值为 “445”。
在我实际测试fun1时,
如果这样写
总是可以正常输出
但如果这样写
就必然会panic
unsafe.Pointer 是不安全的指针,也就是原生的指针,这种情况下没有什么内容长度的检查,否则为什么要声明unsafe。
unsafe.Pointer 的使用其实就是类似于c/c++的指针,当你对代码的效率有很高要求的时候,可以使用它来减少一些内存拷贝的开销
你举的例子中,实际是做了两次的转换,才能正常的输出字符串 44,我说的并没有错,仅仅一次转换,且新类型的长度大于原类型的长度,是不行的。
没有什么两次转换。
你再仔细看看我写的,对同一个地址 &a 调用两次 unsafe.Pointer(&a) 就叫做两次转换?
实际上
只是因为我想看看a的地址而已。
我换一种写法
这次我用了一个无关变量b,没有什么两次转换了吧,但是输出*c仍然可以得到44,为什么呢?
因为我用了一个变量b把a后面的内存给保护起来了,因为有了变量b,所以a的地址被强转为StringHeader的时候,Len字段没有被其他指令给覆盖,所以能够正常输出
其实如果你C很熟的话,就不会有任何疑问了,这个就是C中的指针类型强转而已
比如说如下代码
输出的是Hello World?
通过强转成reflect.StringHeader 类型,就可以操作其内部的字符内存,直接修改某个字符的值。
其实 unsafe.Pointer 强转,就是获取一个变量的原生指针,也就是我们所谓的C指针,它指向此变量在内存中的首地址。
注意看如下代码
输出为
这段输出说明什么?
把变量a的地址强转为StringHeader指针,得到p
把变量a的地址墙砖为string指针,得到c
输出p是为了看内存中的数据
输出len(*c)是为了证明字符串的长度跟p的Len字段是一样的
不能直接输出c是因为这个字符串中的Data字段,也就是真实内存块的指针经常是无效的
把int变量a转为StringHeader指针p,输出得到
p的Data字段值是2,因为a原本就是2. p的Len字段是540179238 这是一个内存中的原本的值,因为Len字段溢出了。
把a转为string指针c,输出c的长度得到 540179238 证明目前c的长度跟p.Len是一致的
对c进行赋值之后,输出p和len(*c)得到
注意看p.Len也变成了2,因为字符串“44”长度就是2。而p.Data字段变成了7341052 ,这其实是字符串常量"44"在常量存储区里的内存地址 0x7003FC
再次对a赋值 a = 12321,输出p和len(*c) 得到
因为&a, p, c 这个三个地址是完全相同的。对a进行赋值,a的长度是int,跟p.Data的长度一致,所以p.Data被赋值成了12321,p.Len不变,所以len(*c) 也不变,都是2
注意这时不能输出*c,因为这时字符串的Data字段指向一个无效内存地址12321.
再看如下代码
对应输出为
b的内存地址比a打了8个字节,刚好匹配了p.Len字段。所以对*c进行赋值时,变量b也被写成了2.
受教了,谢谢。
为什么单单使用一个 fmt.Println(b) 就能把 a 的内存地址给保护起来呢?
你的回答相信对大家很有用
是否存在go版本的误差问题?我输出你 16 楼的例子中第一个 fmt,结果是: &{2 2}
相信和 go 版本无关,只是你初始化的那块内存的第二个字节处就是个 2
不是我的例子,是 ter 16 楼的例子
a := 2
p := (*reflect.StringHeader)(unsafe.Pointer(&a)) fmt.Println(p)
多执行几次呢,会不会是那块字节区域原本就是2
请 ter 继续为我们解答一下:为什么单单使用一个 fmt.Println(b) 就能把 a 的内存地址给保护起来呢?
不是
不是fmt.Println(b) 保护a的内存地址
是在a之后,紧接着又申请了一个b的内存空间大小,a和b的内存空间连在一起共16个字节,正好够stringHeader使用,b的作用就是在a的后边申请一个空间,将这个空间保护起来给a用。
和 fmt.Println 有什么关系?为什么执行了一次 fmt.Println ,就能内存共用?
和fmt.Println没关系,跟b:=3有关系,他之所以fmt.Println(b),就是单纯的打印出来而已,你不打印,,照样有用,,
结合这个反例
结果:注意 a,b的内存地址差了24位
不打印是没用的,我试过了,你也应该试试再打字。
请 ter 继续为我们解答一下:为什么单单使用一个 fmt.Println(b) 就能把 a 的内存地址给保护起来呢?
这种现象怎么解释。。a,b的内存不连续的时候,直接赋值,能赋值,那a之后的那8个字节是谁占用的呢??println函数内部吗?
不打印没用是因为,如果你不打印b,编译器优化的时候可能会把b这个变量给编译没了。
这样是最有用的,因为你对b进行了取地址操作,编译器认为你会使用b的地址进行读取,赋值等操作,一定会保护堆上b的内存不被占用。
如果你只是单纯的 fmt.Println(b) 有可能也是不行的,因为如果
编译器优化时一看,你这b赋了值,除了打印一下,再也不使用了,就给你优化成
所以实际上可能根本就不分配内存了
如果 a 和 b 内存不连续,那么中间肯定有其他变量使用了内存,
可能是其他 goroutine 刚好也在这时候申请了堆内存 (如果所有 goroutine 是共用堆的话,我猜测是共用堆的,因为用C写操作系统级线程是独立栈共用堆)
这种情况下,a与b中间的内存就被c给溢出了,这个就是我们常说的内存溢出。
Go的变量内存分配应该都是在堆上,因为可以返回临时变量的地址,或者是Go做了类似于.Net的封箱机制,可以把栈变量直接封装到堆上。
Go是有垃圾回收的,堆上的变量的生存周期要看垃圾回收的时间
C语言的变量分配规则非常简单,malloc分配的就是在堆上,直接定义的在函数中的局部变量一律都是在栈上。栈变量在函数返回时都会被清理掉,而堆变量必须手动dealloc
大兄弟,我还是希望你能把,fmt.Println(&b) 的时候,这个 b 的内存为什么会给 c := (*string)(unsafe.Pointer(&a)) 给用到 c 上面去了?
我还是希望你能把,fmt.Println(&b) 的时候,这个 b 的内存为什么会给 c := (*string)(unsafe.Pointer(&a)) 给用到 c 上面去了说下啊
a := 2 p := (*reflect.StringHeader)(unsafe.Pointer(&a)) fmt.Println(p)
你输出的是 &{2, 2} 这个跟版本无关,因为a只有8个字节,而强转成StringHeader要占用16个字节,后面8个字节映射到了未分配的堆内存里,这种情况下后面8个字节的值是未被初始化的,也就是说内存里原来是什么值,读出来就是什么值,这个值是随机的。你的程序每次运行这个值都可能会不同。连续几次运行都是相同的那也只是凑巧了,每次内存映射是一致的。如果你写1000个goroutine都执行这个代码,那输出的东西就会千奇百怪了
具体是谁申请分配了这8个字节内存,GC系统会有记录的,如果你确实想知道,可以写Dump文件,把堆全部写出来然后分析。runtime/debug 包里有写dump的函数
好的,谢谢,如果能再帮忙解答下 39 楼就更好了!
简单来说吧,变量a被定义时,其内存是在堆上分配了8个字节,因为a是int类型,占用8个字节
我们假设a的地址是0xc042012320,也就是这8个字节内存的首地址
变量b被定义时,发生同样的事情,b的地址是0xc042012328,长度也是8个字节
那么我们看一下当前堆的情况
0xc042012320----0xc04201232f 这16个字节的内存分别是a和b
当我们把a的指针unsafe强转为字符串指针c时,c这个指针的值仍然是0xc042012320,也就是它指向了跟a相同的内存空间
但是字符串对象在Go的底层其实是一个结构体
这个结构体包含两个成员变量,Data和Len,它们各自长8个字节,所以整个结构体长16个字节
因为这个c跟a内存首地址相同,但是c长16个字节,而a只有8个字节,那么c所指向的结构体就有一部分溢出了a的内存之外
再精确一点描述,c.Data 的地址也是 0xc042012320,因为它是c中的第一个成员变量
c.Len 的地址就是0xc042012328,因为Data长度是8字节,所以Len的地址就是Data的地址向后偏移8个字节
那么现在c.Len 的地址刚好跟b的地址是一样的
简单描述,就是c.Len溢出到了b的内存里,这个就是我们常说的内存溢出,缓冲区溢出之类的。
如果我们不定义变量b,那么变 0xc042012328 之后的内存就是未分配的,这个时候c就溢出到了未分配的内存里了。
这种情况下对c.Len 读取读的就是未分配内存,所以值是随机的。而对c.Len写入的话,写入本身是不会报错的,但是会导致两种情况:
0xc042012328 之后的内存一直未被分配。 这种情况代码通常可以正常运行,不会出错。因为堆的大小还是比较大的,远远超出几个变量的空间,所以这块内存虽然未分配,但是是有效的可以读写的。
0xc042012328 之后的内存被分配给了其他变量。这种情况就比较复杂了,会有两个类型甚至都可能不同的变量对同一块内存进行读写,出现各种奇怪的情况。原始的例子fun1会panic就是因为这个原因。
当然上面的例子里,有个引申问题,结构体的内存对齐,不过这个问题跟当前讨论内容无关,就不细说了
并不是因为调用了 fmt.Println(&b) 才导致b的内存被用到c里面。
b这个变量分配出来的时候就在a的后面,强转c的时候一定会溢出到b
调用 fmt.Println(&b) 是为了保证让编译器不把b给优化掉啊, 也能保证b这个变量不被提前GC回收掉
编译器在处理简单逻辑时有可能会把一些操作给优化掉,只要保证语言层面逻辑不发生变化就可以。
上述代码显示的对b进行取地址操作,并且把这个地址传递给了其他函数,编译器在编译的时候发现了这个,就一定会保证b这个变量确实被分配出来,并且不会马上销毁。
如果你写这样的代码
如果a和b变量在之后的代码中再也不使用,那编译器很可能会把它优化为
因为你调用时只是值传递,所以这样的逻辑上跟上面代码没有任何区别。
当然我对Go编译器的优化过程并不是非常了解,以上只是根据经验推测。
但实测下来编译器确实会做类似的优化,有可能一个变量,在编译器发现它完全没用了之后会立刻把它销毁掉把内存还给其他变量用的
你的这段代码就是典型例子
编译器在编译时发现b其实根本没用啊,所以压根就没生成这个变量。
这几行代码从逻辑上来说根本没有任何意义,所以直接被优化掉了
好的。
艺高人胆大
具体不知道,反正fun1(),我再vscode下调试,有panic(),提示,作为新手,不知道啥问题
今天又测试了下,上面我说的不对,刚才在 go1.10下测试了,fun1() 打印的是 *c的值,没任何问题。。。
如果知道c语言的void*指针和string的内存布局,这个问题其实不难,强烈建议看看这门课:
http://open.163.com/special/opencourse/paradigms.html
.unsafe.Pointer相当于void*,也就是无类型指针,可以指向任何地址的指针。
在go中,string的内存布局如下:
注意:
这里的的data就是一个指向string内容的指针,len是string的长度,在64位的系统中sizeof(data)==8, sizeof(len)==8, 一共占用16byte
。这里只解释fun1,其他函数自己按照理解去体会(以下解释默认在64位系统中)