指针不显示 upupw_Go高级编程:指针和内存分配详解
点击上方蓝色“Go语言中文网”关注我们,设个星标,每天学习 Go 语言
定义
了解指针之前,先讲一下什么是变量。
每当我们编写任何程序时,我们都需要在内存中存储一些数据/信息。数据存储在特定地址的存储器中。内存地址看起来像0xAFFFF
(这是内存地址的十六进制表示)。
现在,要访问数据,我们需要知道存储它的地址。我们可以跟踪存储与程序相关的数据的所有内存地址。但想象一下,记住所有内存地址并使用它们访问数据会有非常困难。这就是为什么引入变量。
变量是一种占位符,用于引用计算机的内存地址,可理解为内存地址的标签。
什么是指针
指针是存储另一个变量的内存地址的变量。所以指针也是一种变量,只不过它是一种特殊变量,它的值存放的是另一个变量的内存地址。
在上面的例子中,指针p
包含值0x0001
,该值是变量的地址a
。
Go类型占用内存情况
unsafe包可以获取变量的内存使用情况
Go语言提供以下基本数字类型:
无符号整数:uint8,uint16,uint32,uint64
符号整数:int8,int16,int32,int64
实数:float32,float64 Predeclared
整数(依赖系统类型,跟系统有关):uint,int,uintptr (指针)
32位系统
uint=uint32 int=int32 uintptr为32位的指针
64位系统
uint=uint64 int=int64 uintptr为64位的指针
示例:
package main
import ("fmt""unsafe")
func main() {var uint8Value uint8var uint16Value uint16var uint32Value uint32var uint64Value uint64var int8Value int8var int16Value int16var int32Value int32var int64Value int64
var float32Value float32var float64Value float64
fmt.Println("uint8Value = Size:", unsafe.Sizeof(uint8Value)) //uint8Value = Size: 1 fmt.Println("uint16Value = Size:", unsafe.Sizeof(uint16Value)) //uint16Value = Size: 2 fmt.Println("uint32Value = Size:", unsafe.Sizeof(uint32Value)) //uint32Value = Size: 4 fmt.Println("uint64Value = Size:", unsafe.Sizeof(uint64Value))// uint64Value = Size: 8
fmt.Println("int8Value = Size:", unsafe.Sizeof(int8Value)) //int8Value = Size: 1 fmt.Println("int16Value = Size:", unsafe.Sizeof(int16Value))//int16Value = Size: 2 fmt.Println("int32Value = Size:", unsafe.Sizeof(int32Value))//int32Value = Size: 4 fmt.Println("int64Value = Size:", unsafe.Sizeof(int64Value)) //int64Value = Size: 8
fmt.Println("float32Value = Size:", unsafe.Sizeof(float32Value)) //float32Value = Size: 4 fmt.Println("float64Value = Size:", unsafe.Sizeof(float64Value))//float64Value = Size: 8
}
上面的是基本类型,接下来了解下复杂类型,以结构体类型为例
type Example struct { BoolValue bool IntValue int16 FloatValue float32}
该结构代表复杂类型。它代表7个字节,带有三个不同的数字表示。bool是一个字节,int16是2个字节,float32增加4个字节。但是,在此结构的内存中实际分配了8个字节。
所有内存都分配在对齐边界上,以最大限度地减少内存碎片整理。要确定对齐边界Go用于您的体系结构,您可以运行unsafe.Alignof函数。Go为64bit Darwin平台的对齐边界是8个字节。因此,当Go确定结构的内存分配时,它将填充字节以确保最终内存占用量是8的倍数。编译器将确定添加填充的位置。
什么是内存对齐呢?
内存对齐,也叫边界对齐(boundary alignment),是处理器为了提高处理性能而对存取数据的起始地址所提出的一种要求。编译器为了使我们编写的C程序更有效,就必须最大限度地满足处理器对边界对齐的要求。
从处理器的角度来看,需要尽可能减少对内存的访问次数以实现对数据结构进行更加高效的操作。为什么呢?因为尽管处理器包含了缓存,但它在处理数据时还得读取缓存中的数据,读取缓存的次数当然是越少越好!如上图所示,在采用边界对齐的情况下,当处理器需要访问a_变量和b_变量时都只需进行一次存取(图中花括号表示一次存取操作)。若不采用边界对齐,a_变量只要一次处理器操作,而b_变量却至少要进行两次操作。对于b_,处理器还得调用更多指令将其合成一个完整的4字节,这样无疑大大降低了程序效率。
以下程序显示Go插入到Example类型struct的内存占用中的填充:
package main
import ("fmt""unsafe")
type Example struct { BoolValue bool IntValue int16 FloatValue float32}
func main() { example := &Example{ BoolValue: true, IntValue: 10, FloatValue: 3.141592, }
exampleNext := &Example{ BoolValue: true, IntValue: 10, FloatValue: 3.141592, }
alignmentBoundary := unsafe.Alignof(example)
sizeBool := unsafe.Sizeof(example.BoolValue) offsetBool := unsafe.Offsetof(example.BoolValue)
sizeInt := unsafe.Sizeof(example.IntValue) offsetInt := unsafe.Offsetof(example.IntValue)
sizeFloat := unsafe.Sizeof(example.FloatValue) offsetFloat := unsafe.Offsetof(example.FloatValue)
sizeBoolNext := unsafe.Sizeof(exampleNext.BoolValue) offsetBoolNext := unsafe.Offsetof(exampleNext.BoolValue)
fmt.Printf("example Size: %d\n", unsafe.Sizeof(example))
fmt.Printf("Alignment Boundary: %d\n", alignmentBoundary)
fmt.Printf("BoolValue = Size: %d Offset: %d Addr: %v\n", sizeBool, offsetBool, &example.BoolValue)
fmt.Printf("IntValue = Size: %d Offset: %d Addr: %v\n", sizeInt, offsetInt, &example.IntValue)
fmt.Printf("FloatValue = Size: %d Offset: %d Addr: %v\n", sizeFloat, offsetFloat, &example.FloatValue)
fmt.Printf("Next = Size: %d Offset: %d Addr: %v\n", sizeBoolNext, offsetBoolNext, &exampleNext.BoolValue)
}
输出:
example Size: 8Alignment Boundary: 8BoolValue = Size: 1 Offset: 0 Addr: 0xc00004c080IntValue = Size: 2 Offset: 2 Addr: 0xc00004c082FloatValue = Size: 4 Offset: 4 Addr: 0xc00004c084Next = Size: 1 Offset: 0 Addr: 0xc00004c088
类型结构的对齐边界是预期的8个字节。
大小值显示将读取和写入该字段的内存量。正如所料,大小与类型信息一致。
偏移值显示进入内存占用的字节数,我们将找到该字段的开头。
地址是可以找到内存占用内每个字段的开头的地方。
我们可以看到Go在BoolValue和IntValue字段之间填充1个字节。偏移值和两个地址之间的差异是2个字节。您还可以看到下一个内存分配是从结构中的最后一个字段开始4个字节。
指针的使用
声明一个指针
使用以下语法声明类型为T的指针
var p *int
指针的零值是nil
。这意味着任何未初始化的指针都将具有该值nil
。让我们看一个完整的例子
package mainimport "fmt"
func main() {var p *int &p=1}
注意:当指针没有指向的时候,不能对(*point)进行操作包括读取,否则会报空指针异常。
示例:
package main
func main() {var p *int
*p = 1 //panic: runtime error: invalid memory address or nil pointer dereference}
解决方法即给该指针分配一个指向,即初始化一个内存,并把该内存地址赋予指针变量
示例:
import "fmt"
func main() {var p *intvar m int p = &m *p = 1 fmt.Println("m=", m) fmt.Println("p=", p)}
或还可以使用内置new()
函数创建指针。该new()
函数将类型作为参数,分配足够的内存以容纳该类型的值,并返回指向它的指针。
import "fmt"
func main() {var p *int
p = new(int) *p = 1 fmt.Println("p=", *p)}
初始化指针
您可以使用另一个变量的内存地址初始化指针。可以使用&
运算符检索变量的地址
var x = 100var p *int = &x
注意我们如何使用&
带变量的运算符x
来获取其地址,然后将地址分配给指针p
。
就像Golang中的任何其他变量一样,指针变量的类型也由编译器推断。所以你可以省略p
上面例子中指针的类型声明,并像这样写
var p = &a
取消引用指针
您可以*
在指针上使用运算符来访问存储在指针所指向的变量中的值。这被称为解除引用或间接
package mainimport "fmt"
func main() {var a = 100var p = &a
fmt.Println("a = ", a) fmt.Println("p = ", p) fmt.Println("*p = ", *p)}
输出:
a = 100p = 0xc00004c080*p = 100
您不仅可以使用*
运算符访问指向变量的值,还可以更改它。以下示例a
通过指针设置存储在变量中的值p
package mainimport "fmt"
func main() {var a = 1000var p = &a
fmt.Println("a (before) = ", a)
// Changing the value stored in the pointed variable through the pointer *p = 2000
fmt.Println("a (after) = ", a)}
输出:
a (before) = 1000a (after) = 2000
指针指向指针
指针可以指向任何类型的变量。它也可以指向另一个指针。以下示例显示如何创建指向另一个指针的指针
package mainimport "fmt"
func main() {var a = 7.98var p = &avar pp = &p
fmt.Println("a = ", a) fmt.Println("address of a = ", &a)
fmt.Println("p = ", p) fmt.Println("address of p = ", &p)
fmt.Println("pp = ", pp)
// Dereferencing a pointer to pointer fmt.Println("*pp = ", *pp) fmt.Println("**pp = ", **pp)}
Go中没有指针算术
如果您使用过C / C ++,那么您必须意识到这些语言支持指针算法。例如,您可以递增/递减指针以移动到下一个/上一个内存地址。您可以向/从指针添加或减去整数值。您也可以使用关系运算符比较两个三分球==
,<
,>
等。
但Go不支持对指针进行此类算术运算。任何此类操作都将导致编译时错误
package main
func main() {var x = 67var p = &x
var p1 = p + 1 // Compiler Error: invalid operation}
但是,您可以使用==
运算符比较相同类型的两个指针的相等性。
package mainimport "fmt"
func main() {var a = 75var p1 = &avar p2 = &a
if p1 == p2 { fmt.Println("Both pointers p1 and p2 point to the same variable.") }}
Go中传递简单类型
import "fmt"
func main() { p := 5 change(&p) fmt.Println("p=", p)//p= 0}func change(p *int) { *p = 0}
Go中所有的都是按值传递,对于复杂类型,传的是指针的拷贝
package main
import "fmt"
func main() {var m map[string]int m = map[string]int{"one": 1, "two": 2} n := m fmt.Printf("%p\n", &m) //0xc000074018 fmt.Printf("%p\n", &n) //0xc000074020 fmt.Println(m) // map[two:2 one:1] fmt.Println(n) //map[one:1 two:2] changeMap(m) fmt.Printf("%p\n", &m) //0xc000074018 fmt.Printf("%p\n", &n) //0xc000074020 fmt.Println(m) //map[one:1 two:2 three:3] fmt.Println(n) //map[one:1 two:2 three:3]}func changeMap(m map[string]int) { m["three"] = 3 fmt.Printf("changeMap func %p\n", m) //changeMap func 0xc000060240}
直接传指针 也是传指针的拷贝
package main
import "fmt"
func main() {var m map[string]int m = map[string]int{"one": 1, "two": 2} n := m fmt.Printf("%p\n", &m) //0xc000074018 fmt.Printf("%p\n", &n) //0xc000074020 fmt.Println(m) // map[two:2 one:1] fmt.Println(n) //map[one:1 two:2] changeMap(&m) fmt.Printf("%p\n", &m) //0xc000074018 fmt.Printf("%p\n", &n) //0xc000074020 fmt.Println(m) //map[one:1 two:2 three:3] fmt.Println(n) //map[two:2 three:3 one:1]}func changeMap(m *map[string]int) {//m["three"] = 3 //这种方式会报错 invalid operation: m["three"] (type *map[string]int does not support indexing) (*m)["three"] = 3 //正确 fmt.Printf("changeMap func %p\n", m) //changeMap func 0x0}
总结
Go 不能进行指针运算。
指针传递是很廉价的,只占用 4 个或 8 个字节。当程序在工作中需要占用大量的内存,或很多变量,或者两者都有,使用指针会减少内存占用和提高效率。
指针也是一种类型,不同于一般类型,指针的值是地址,这个地址指向其他的内存,通过指针可以读取其所指向的地址所存储的值。
函数方法的接受者,也可以是指针变量。简单类型和复杂类型在传递的时候不同,复杂类型传值或传指针都是指针拷贝。
只声明未赋值的变量,golang都会自动为其初始化为零值,基础数据类型的零值比较简单,引用类型和指针的零值都为nil,nil类型不能直接赋值,因此需要通过new开辟一个内存,或指向一个变量。
参考资料
http://golang.org/doc/faq#Pointers
https://www.callicoder.com/golang-pointers/
https://www.ardanlabs.com/blog/2013/07/understanding-pointers-and-memory.html
https://www.ardanlabs.com/blog/2013/07/understanding-type-in-go.html
喜欢本文的朋友,欢迎关注“Go语言中文网”,第一时间接收后续好文!
指针不显示 upupw_Go高级编程:指针和内存分配详解相关推荐
- 《PyQt5高级编程实战》自定义信号详解
自定义信号详解 1. 创建自定义信号 2. 让自定义信号携带值 3. 自定义信号的重载版本 4. 窗口间通信 5. 线程间通信 PyQt5中各个控件自带的信号已经能够让我们完成许多需求,但是如果想要更 ...
- Java多线程编程中Future模式的详解
转载自 https://www.cnblogs.com/winkey4986/p/6203225.html Java多线程编程中,常用的多线程设计模式包括:Future模式.Master-Worker ...
- python计算定积分_python编程通过蒙特卡洛法计算定积分详解
这篇文章主要介绍了python编程通过蒙特卡洛法计算定积分详解,具有一定借鉴价值,需要的朋友可以参考下. 想当初,考研的时候要是知道有这么个好东西,计算定积分...开玩笑,那时候计算定积分根本没有这么 ...
- python黑帽编程视频_Python黑帽编程 3.4 跨越VLAN详解
VLAN(Virtual Local Area Network),是基于以太网交互技术构建的虚拟网络,既可以将同一物理网络划分成多个VALN,也可以跨越物理网络障碍,将不同子网中的用户划到同一个VLA ...
- java一个方法排他调用_Java编程实现排他锁代码详解
一 .前言 某年某月某天,同事说需要一个文件排他锁功能,需求如下: (1)写操作是排他属性 (2)适用于同一进程的多线程/也适用于多进程的排他操作 (3)容错性:获得锁的进程若Crash,不影响到后续 ...
- 蚂蚁算法python_Python编程实现蚁群算法详解
简介 蚁群算法(ant colony optimization, ACO),又称蚂蚁算法,是一种用来在图中寻找优化路径的机率型算法.它由Marco Dorigo于1992年在他的博士论文中提出,其灵感 ...
- android 多闹钟实现代码,Android编程实现闹钟的方法详解
Android编程实现闹钟的方法详解 发布时间:2020-09-30 10:18:02 来源:脚本之家 阅读:75 作者:Jacob-wj 本文实例讲述了Android编程实现闹钟的方法.分享给大家供 ...
- ∑ n!(1! 2!)用c语言怎么编,数控车床编程教程,图文实例详解!
原标题:数控车床编程教程,图文实例详解! 第一节数控车床编程基础 一.数控车编程特点 (1) 可以采用绝对值编程(用X.Z表示).增量值编程(用U.W表示)或者二者混合编程. (2) 直径方向(X方向 ...
- 我的世界java版区块显示_我的世界手游区块显示指令分享:区块玩法操作详解[多图]...
我的世界手游区块是一个独特的机制,很多玩家对于区块是什么不太了解,区块显示指令以及区块的产生不是很熟悉,为了帮助到大家,今天小编就为大家带来我的世界手游区块显示指令分享:区块玩法操作详解的内容,希望大 ...
最新文章
- Doctype作用? 严格模式与混杂模式如何区分?它们有何意义
- 关于两个用于创建和销毁二维动态数组的宏
- 【Android】Home键
- 在.NET中使用DiagnosticSource
- JVM选项:-client vs -server
- shopify二次开发教程_详细教程:如何将Shopify的Storefront API与React和Redux结合使用...
- virtualbox禁用硬件虚拟化_Mac版Virtualbox6.1开启嵌套虚拟化
- zend studio配置php手册
- Ubuntu安装sqllite3并使用
- mysql怎么给sex设置默认值_记一次mysql优化操作
- java实战项目案例-附带视频教学
- idea 配置SVN
- Latex 给参考文献添加doi号和超链接
- js更新mysql数据库_更新javascript方法
- 西电微机系统课程设计步进电机开环控制系统
- ftp主动模式和被动模式的区别
- 金三银四产品人跳槽指南——找准定位,突破职业瓶颈
- 不是只有宇宙飞船能带你去火星,Go也可以!
- 台湾清华大学计算机网络--003 WLAN
- 与网络相连的计算机称为什么,网络把许多计算机连接在一起,而互联网则把许多( )通过路由器连接在一起。与网络相连的计算机常称为( )。...