共计 3495 个字符,预计需要花费 9 分钟才能阅读完成。
C 语言指针运算
指针运算就是对指针类型的变量做惯例数学运算,例如加减操作,实现地址的偏移。指针运算在 C 语言中是原生反对的,能够间接在指针变量上做加减,例如:
#include <stdio.h>
const int MAX = 3;
int main ()
{int var[] = {10, 100, 200};
int i, *ptr;
/* 指针中的数组地址 */
ptr = var;
for (i = 0; i < MAX; i++)
{printf("存储地址:var[%d] = %p\n", i, ptr );
printf("存储值:var[%d] = %d\n", i, *ptr );
/* 间接对指针做 ++ 操作,指向下一个地位 */
ptr++;
}
return 0;
}
后果
存储地址:var[0] = e4a298cc
存储值:var[0] = 10
存储地址:var[1] = e4a298d0
存储值:var[1] = 100
存储地址:var[2] = e4a298d4
存储值:var[2] = 200
C 语言指针运算犹如一把双刃剑,应用切当会起到事倍功半,有神之一手的成果,反之则会产生意想不到的 bug 而且很难排查。因为在做指针运算时是比拟形象的,具体偏移了多少之后指向到了哪里是十分不直观的,可能曾经偏离了构想中的地位而没有发现,运行起来就会呈现谬误。
例如这段 C 代码,找出数组中最小的元素:
#include <stdio.h>
int findMin(int *arr, int length) {
int min = *arr;
for (int i = 0; i <= length; i++) { // 留神这里是 i <= length,而不是 i < length
printf("i=%d v=%d\n", i, *(arr+i));
if (*(arr + i) < min) {min = *(arr + i);
}
}
return min;
}
int main() {int arr[] = {1, 2, 3, 4, 5};
int length = sizeof(arr) / sizeof(arr[0]);
printf("Min value is: %d\n", findMin(arr, length));
return 0;
}
数组中最小的是 1,可后果却是 0:
i=0 v=1
i=1 v=2
i=2 v=3
i=3 v=4
i=4 v=5
i=5 v=0
Min value is: 0
这是因为在 findMin
函数中循环条件是 i ≤ length
,超出数组大小多循环了一次,实际上数组曾经越界,而 C 语言的数组实际上就是指针,C 运行时认为这是在指针运算,所以不会报错,导致数组拜访到了其余内存地址,最终失去了一个谬误后果。
事实上有很多病毒和外挂的原理就是利用指针来拜访并批改程序运行时内存数据来达到目标。例如游戏外挂可能会搜寻和批改内存中的特定值,以扭转玩家的生命值、金钱或其余游戏属性。通过指针运算,外挂能够间接拜访这些内存地位并对其进行批改。而病毒可能应用指针运算来插入其本人的代码到一个运行中的程序,或者篡改程序的失常控制流,以达到其歹意目标。
在 C 语言之后的很多语言多多少少都对指针做了限度,例如 PHP 中的援用就可以看做是指针的简化版,而 Java 甚至罗唆移除了指针。
Go 指针运算
在 Go 中默认的一般指针也是指代的是一个内存地址,值相似 0x140000ac008
,但 Go 的一般指针不反对指针运算的,例如对指针做加法会报错:
a := 10
var p *int = &a
p = p + 1
报错
invalid operation: p + 1 (mismatched types *int and untyped int)
但 Go 还是提供了一种间接操作指针的形式,就是 unsafe.Pointer 和 uintptr。
uintptr 是一个整型,可了解为是将内存地址转换成了一个整数,既然是一个整数,就能够对其做数值计算,实现指针地址的加减,也就是地址偏移,相似跟 C 语言中一样的成果。
而 unsafe.Pointer 是一般指针和 uintptr 之间的桥梁,通过 unsafe.Pointer 实现三者的互相转换。
*T <-> unsafe.Pointer <-> uintptr
先看看这三位都长什么样:
func main() {
a := 10
var b *int
b = &a
fmt.Printf("a is %T, a=%v\n", a, a)
fmt.Printf("b is %T, b=%v\n", b, b)
p := unsafe.Pointer(b)
fmt.Printf("p is %T, p=%v\n", p, p)
uptr := uintptr(p)
fmt.Printf("uptr is %T, uptr=%v\n", uptr, uptr)
}
输入
a is int, a=10
b is *int, b=0x140000ae008
p is unsafe.Pointer, p=0x140000ae008
uptr is uintptr, uptr=1374390247432
举一个通过指针运算批改构造体的例子
type People struct {
age int32
height int64
name string
}
people := &People{}
fmt.Println(people)
// 将 people 一般指针转成 unsafe.Pointer 再转为 uintptr
// 前面再加上 height 字段绝对于构造体自身的偏移量,就失去了 height 的地址的 uintptr 值
// 再将 height 的 uintptr 值转成 unsafe.Pointer 赋值给 height 变量
// 所以当初 height 的类型是 unsafe.Pointer
height := unsafe.Pointer(uintptr(unsafe.Pointer(people)) + unsafe.Offsetof(people.height))
fmt.Printf("people addr is %v\n", unsafe.Pointer(people))
fmt.Printf("height is %T\n", height)
fmt.Printf("height addr is %v\n", height)
println("---")
// 应用类型转换,将 unsafe.Pointer 类型的 height 转换成 *int 指针
// 再通过最后面的 * 解援用,批改其值 身高 2 米 26
*((*int)(height)) = 226
fmt.Println(people)
// 同样的操作能够批改年龄和名字
age := unsafe.Pointer(uintptr(unsafe.Pointer(people)) + unsafe.Offsetof(people.age))
*((*int)(age)) = 18
name := unsafe.Pointer(uintptr(unsafe.Pointer(people)) + unsafe.Offsetof(people.name))
*((*string)(name)) = "小明"
fmt.Println(people)
输入
people: &{0 0}
people addr is 0x1400005e020
height is unsafe.Pointer
height addr is 0x1400005e028
---
people: &{0 226}
people: &{18 226}
people: &{18 226 小明}
再看一个操作,通过指针转换,将一个字节切片转成浮点数组:
package main
import (
"fmt"
"unsafe"
)
func main() {
// 假如咱们有一个字节切片,并且咱们晓得它是由浮点数示意的
byteSlice := []byte{0, 0, 0, 0, 0, 0, 240, 63} // 1.0 的 IEEE-754 示意
// 应用 unsafe 把字节切片转换为浮点数切片
floatSlice := (*[1]float64)(unsafe.Pointer(&byteSlice[0]))
fmt.Println(floatSlice)
}
输入
&[1]
这个过程不须要 Go 的类型查看,绕过了很多流程,相对来说性能会更高。
所以大体上通过 unsafe.Pointer 的指针运算会利用在如下几个方面:
- 性能优化 : 当性能是关键因素时,
unsafe
能够用来防止一些开销。例如,通过间接操作内存,能够防止切片或数组的额定调配和复制。 - C 语言交互 : 当应用 cgo 与 C 语言库交互时,
unsafe
包通常用于转换类型和指针。 - 自定义序列化 / 反序列化 : 在自定义的序列化或反序列化逻辑中,
unsafe
能够用于间接拜访构造的内存布局,能够进步性能。 - 实现非标准的数据结构 : 有时,特定的问题须要非标准的数据结构。
unsafe
容许你间接操作内存,能够用来实现一些 Go 的规范库中没有的数据结构。 - 反射 : 与反射联合时,
unsafe
能够用于拜访构造体的公有字段。