Go 1.19.4 数值处理、标准输入、数组-Day 04

1. 数据结构

1.1 类型的本质

什么是类型?如在内存中,有个0x63,表面看是个数字,但实际上计算机中存储都是二进制,16进制的0x63就是对应二进制01100011。那01100011到底表示什么意思呢?默认肯定是数字。那问题接踵而至,字符怎么办?如英文字母a,该如何表达呢?也需要对字符进行数字化,这个时候就需要建表(编码表),在表中指定字符对应的数字,以此来实现字符的数字化。

如0x63,定义它的类型为int,按照整型理解,为99,按照字符理解,就是c。

如0x63,定义它的类型为string,这个时候输出它的值,依然还是c,为什么?因为0x63对应10进制99,对照Unicode编码表(兼容ascii编码表)中,99对应的字符就是c。

如果0x63是byte类型或rune类型,在Go语言中,它是不同于整型的类型,但是展示出来同样是99。


还是那句话,到底什么类型,取决于我们怎么定义。

1.2 数值处理

1.2.1 取整

1.2.1.1 截取整数部分(整数除以整数)

在Go中,整数除以整数只会保留整数部分。
或者,把浮点转成整型,也只会保留整数部分。

package main

import "fmt"

func main() {
	fmt.Println(1/2, 3/2, 5/2)
	fmt.Println(-1/2, -3/2, -5/2)
	fmt.Println("-----------------------")
}
########## 调试结果 ##########
0 1 2
0 -1 -2
1.2.1.2 向上取整(math.Ceil)

取最大数

package main

import (
	"fmt"
	"math"
)

func main() {
	fmt.Println("===========调试结果===========")
	fmt.Println(math.Ceil(2.01), math.Ceil(2.5), math.Ceil(2.8))
	fmt.Println(math.Ceil(-2.01), math.Ceil(-2.5), math.Ceil(-2.8))
}
===========调试结果===========
3 3 3
-2 -2 -2
1.2.1.3 向下取整(math.Floor)

取最小数

package main

import (
	"fmt"
	"math"
)

func main() {
	fmt.Println("===========调试结果===========")
	fmt.Println(math.Floor(2.01), math.Floor(2.5), math.Floor(2.8))
	fmt.Println(math.Floor(-2.01), math.Floor(-2.5), math.Floor(-2.8))
}
===========调试结果===========
2 2 2
-3 -3 -3
1.2.1.4 四舍五入(math.Round)
package main

import (
	"fmt"
	"math"
)

func main() {
	fmt.Println("===========调试结果===========")
	fmt.Println(math.Round(2.01), math.Round(2.5), math.Round(2.8))
	fmt.Println(math.Round(-2.01), math.Round(-2.5), math.Round(-2.8))
	fmt.Println(math.Round(0.5), math.Round(1.5), math.Round(2.5), math.Round(3.5))
}
===========调试结果===========
2 3 3
-2 -3 -3
1 2 3 4

1.2.2 其它数值处理

1.2.2.1 取绝对值(math.Abs)
package main

import (
	"fmt"
	"math"
)

func main() {
	fmt.Println("===========调试结果===========")
	fmt.Println(math.Abs(-2.7))
}
===========调试结果===========
2.7
1.2.2.2 打印常数
package main

import (
	"fmt"
	"math"
)

func main() {
	fmt.Println("===========调试结果===========")
	fmt.Println(math.E, math.Pi)
}
===========调试结果===========
2.718281828459045 3.141592653589793
1.2.2.3 打印int16类型的最大值和最小值
package main

import (
	"fmt"
	"math"
)

func main() {
	fmt.Println("===========调试结果===========")
	fmt.Println(math.MaxInt16, math.MinInt16)
}
===========调试结果===========
32767 -32768
1.2.2.4 打印对数
package main

import (
	"fmt"
	"math"
)

func main() {
	fmt.Println("===========调试结果===========")
    // math.Log10(100),计算的是 100 的以 10 为底的对数
    // math.Log2(8),计算的是 8 的以 2 为底的对数
	fmt.Println(math.Log10(100), math.Log2(8))
}
===========调试结果===========
2 3
1.2.2.5 取最大值最小值
package main

import (
	"fmt"
	"math"
)

func main() {
	fmt.Println("===========调试结果===========")
	fmt.Println(math.Max(1, 2), math.Min(-2, 3))
}
===========调试结果===========
2 -2
1.2.2.6 幂(次方)
package main

import (
	"fmt"
	"math"
)

func main() {
	fmt.Println("===========调试结果===========")
    // math.Pow(2, 3):2的3次方
    // math.Pow10(3):3的10次方
	fmt.Println(math.Pow(2, 3), math.Pow10(3))
}
===========调试结果===========
8 1000
1.2.2.7 取模
package main

import (
	"fmt"
	"math"
)

func main() {
	fmt.Println("===========调试结果===========")
	fmt.Println(math.Mod(5, 2), 5%2)
}
===========调试结果===========
1 1
1.2.2.8 开方
package main

import (
	"fmt"
	"math"
)

func main() {
	fmt.Println("===========调试结果===========")
	fmt.Println(math.Sqrt(2), math.Sqrt(3), math.Pow(2, 0.5))
}
===========调试结果===========
1.4142135623730951 1.7320508075688772 1.4142135623730951

1.3 标准输入-Scan

所有的标准输入都是字符串类型的数据。

语法:func fmt.Scan(a …any) (n int, err error)
含义:
(1)Scan
​​Scan​​ 是 Go 语言标准库 ​​fmt​​ 包中的一个函数,用于从标准输入(通常是键盘)读取文本,并将读取到的值根据提供的参数类型存储到对应的变量中。
当有多个值被读取时,按照空格分割存储。

(2)a
​​a​​ 是一个可变参数,表示要读取并存储的值的类型。

(3)n
​​n​​ 表示成功读取并存储的值的个数。

(4)err
​​err​​ 表示在读取过程中是否发生了错误,如果有错误的话,错误内容会自动赋值到err。

1.3.1 从控制台读取

package main

import (
	"fmt"
)

func main() {
    // 定义2个零值变量,此时这俩变量指向的存储0值的内存地址。
	var s1, s2 string

	// 从控制台的输入读取以空白字符分割的两个值,取内存地址,然后让s1和s2指向这俩内存地址,减少复制过程。
    // n:表示输入的几个值(输入的值的个数),这里对应s1和s2。err:看下面的if
	n, err := fmt.Scan(&s1, &s2)

	if err != nil { // 如果err不为空,测代表程序执行有错误。
		panic(err) // 执行到panic,输出err对应的错误内容,并且整个程序停止运行
	}
	fmt.Println("成功读取的项数为:", n)
	fmt.Printf("%T %[1]s, %T %[2]s\n", s1, s2)
}
===========调试结果===========
PS D:\个人\GO开发\20240313> go run .\main.go
111 222 // 这个是我们输入的,不是程序输出的
成功读取的项数为: 2
string 111, string 222

1.3.2 从控制台读取(提示用户输入)

package main

import "fmt"

func main() {
	var s1, s2 string

	n, err := fmt.Scan(&s1, &s2)
	if err != nil {
		panic(err)
	}
	fmt.Println("成功读取的项数为:", n)
	fmt.Printf("%T %[1]s, %T %[2]s\n", s1, s2)
	fmt.Println("---------------------------")

	var i1, i2 int
	fmt.Print("请输入两个整数:")
	// n和err在上面已经被定义过了,这里直接使用=覆盖原有的值即可
	n, err = fmt.Scan(&i1, &i2)
	if err != nil {
		panic(err)
	}
	fmt.Println("成功读取的项数为:", n)
	fmt.Printf("%T %[1]v, %T %[2]v\n", i1, i2)
}
===========调试结果===========
1 2
成功读取的项数为: 2       
string 1, string 2
---------------------------
请输入两个整数:123 456
成功读取的项数为: 2
int 123, int 456

如果需要频繁的重复使用err,除了上面覆盖值这种方式,还能这样:

package main

import "fmt"

func main() {
	// 先给n和err声明好
	var n int
	var err error

	var s1, s2 string
	n, err = fmt.Scan(&s1, &s2) // 直接调用就行
	if err != nil {
		panic(err)
	}
	fmt.Println("成功读取的项数为:", n)
	fmt.Printf("%T %[1]s, %T %[2]s\n", s1, s2)
	fmt.Println("---------------------------")

	var i1, i2 int
	fmt.Print("请输入两个整数:")

	// 直接调用就行。n可以使用_替代,但是这样就无法获取输出了多少值进来
	// err也可以使用_替代,但是这样就无法发现错误了(报错就直接响应0值)。所以按照规范,必须要写。
	_, err = fmt.Scan(&i1, &i2)
	if err != nil {
		panic(err)
	}
	//fmt.Println("成功读取的项数为:", n)
	fmt.Printf("%T %[1]v, %T %[2]v\n", i1, i2)
}

1.3.3 使用scan注意事项

scan使用空格分隔字符,多余的会先暂存到内存中并往下顺延由下一个scan接收。
但是提交的内容数量不够时,scan就会一直等待输入。
连续的空格或换行符,都会被认为是1个空格。
在这里插入图片描述

1.4 标准输入-Scanf

语法:func Scanf(format string, a …interface{}) (n int, err error)
这里,format 是用于每个给定元素的不同格式,而 a…interface{} 是接收每个给定元素的参数列表。函数返回两个值:成功扫描的项目数 n 和可能遇到的错误 err。

功能和scan相同,只是要指定标准输入格式,建议还是按照和scan相同,使用空格为分隔符。

1.4.1 使用,作为分隔符(指定分隔符注意事项)

1.4.1.1 错误的使用方式

指定分隔符时(非空格),string类型的一定要要放在最后面,不然分隔符会被误认为字符串。

package main

import "fmt"

func main() {
	// 先给n和err声明好
	var n int
	var err error

	var name string
	var age int
	fmt.Print("请输入你的姓名和年龄:")
	n, err = fmt.Scanf("%s, %d", &name, &age) // 注意这里使用的,分隔
	if err != nil {
		panic(err)
	}
	fmt.Println("scanf接收到的有效值数量:", n)
	fmt.Printf("%T %[1]s, %T %[2]d", name, age)
}
===========调试结果===========
PS D:\个人\GO开发\20240313> go run .\main.go
请输入你的姓名和年龄:张三,16 // 这里报错原因就是因为scanf人为,也是字符串
panic: input does not match format

goroutine 1 [running]:
main.main()
        D:/个人/GO开发/20240313/main.go:15 +0x211
exit status 2

1.4.2 小练习:读取用户输入的两个浮点数和一个整数,然后打印这三个数的总和、平均值和乘积

package main

import (
	"fmt"
)

// 格式化输入练习
// 使用 fmt.Scanf 读取用户输入的两个浮点数和一个整数,然后打印这三个数的总和、平均值和乘积。
func a() {
	var (
		a, b float64
		c    int
	)
	fmt.Print("请输入两个浮点数和一个整数:")
	_, err := fmt.Scanf("%f %f %d", &a, &b, &c)
	if err != nil {
		fmt.Println("异常")
	} else {
		fmt.Printf("总和:%v\n平均值:%v\n乘积:%v", a+b+float64(c), (a+b+float64(c))/3, a*b*float64(c))

	}
}

func main() {
	a()
}
===========调试结果===========
PS D:\个人\GO开发\20240624> .\main.exe
请输入两个浮点数和一个整数:1.1 2.2 30
总和:33.3
平均值:11.1
乘积:72.60000000000001

2. 线性数据结构

在Go语言中,线性数据结构是指数据元素之间存在一对一关系的数据结构,即每个元素最多只有一个前驱和一个后继。
线性数据结构主要包括数组、切片、链表、栈、队列等。

2.1 线性表

线性表(简称表)
是一种抽象的数学概念,是一组元素的序列(有序)的抽象(存序列),它由有穷个元素组成(0个或任意个),并且只要是序列,就可以索引,一般从0开始,但go中是不支持负索引的。

为什么线性表还要分顺序表和链接表?是因为元素在内存中的物理存储方式有差异,使用上性能也有差异。

2.2 线性表的实现方式

2.2.1 顺序表

存储结构:
顺序表使用一大块连续的内存(内存空间编号是递增的)顺序存储表中的元素(有序存储),这样实现的表称为顺序表,或称连续表。

在顺序表中,元素的关系使用顺序表的存储顺序自然地表示。
顺序表中典型的代表就是:数组、切片、字符串等。
比如存储a b c,在内存中就是内存编号1存a,内存编号2存b,内存编号3存c,当然,实际存储的就是二进制数据。

特点:
由于元素是连续存储到内存中的,所以只要知道一个元素的地址或者顺序表的首地址,可以通过偏移量找到其中的某一个元素。
但顺序表的长度是固定的,不过可以通过动态的分配内存来改变其容量。

查找方式:
(1)使用内容查找
需要遍历,效率低下,该方式的操作复杂度为O(n),n为链表的长度,也就是数据的多少,数据越多越慢。

(2)使用索引
首地址+索引*元素字节数=元素内存位置
该方式的速度非常快,因为是固定公式+固定步骤就能算出来,和元素多少无关,被称为时间复杂度0(1)。

在计算机科学和算法分析中,时间复杂度是用来描述算法执行时间随输入数据规模增长而增长的性质。它常常用大O符号(O)来表示。
​​O(1)​​ 是时间复杂度的最低级别,表示算法的执行时间是一个常数,不会随着输入数据规模的增长而增长。换句话说,无论输入数据有多大,这个算法都会在相同或相似的时间内完成。
例如,访问数组或哈希表的元素通常是 ​​O(1)​​ 的时间复杂度,因为无论数组或哈希表中有多少元素,访问特定索引或键的操作通常都在常数时间内完成。

增加元素的方式:
(1)尾部:直接在尾部追加,代价最小。
(2)中间:从插入点开始,后面所有元素依次向后挪动。
(3)头部:从头开始,后面所有元素依次向后挪动,需要复制的数据较多(原有的0号索引位置数据复制到1号索引位置以此类推),开销较大。

修改元素的方式:
这里也可以使用索引或内容遍历,但依然是索引最优。

删除元素的方式:
也是遍历或索引。
使用索引,一步到位,立即删除。
但是要看元素所处位置:
元素在尾部,删除它,其他元素位置不变。
元素在头部或中间,删除它,其后元素需要向前复制。

应用场景:
适用于需要快速访问元素(要求读取效率),但不太需要频繁插入和删除元素的情况,或者变更都在元素尾部。

2.2.2 链接表

简称链表,是一种线性数据结构。

存储结构:
链表中的元素在物理内存中的存储位置分散开的,元素之间通过指针或链接相连。
如同操场上手拉手的小朋友,有序但排列随意。或者可以想象成一串带线的珠子,随意盘放在桌上。也可以离队、插队,也可以索引。

链表的特点:
链表的每个元素都包含数据部分和指向下一个元素的指针。
链表不受空间限制,插入和删除操作方便,不需要大量移动数据。
但访问链表中的元素通常需要从头开始遍历,因此访问效率较低。
和顺序表不同,就算知道元素地址或首地址,也无法通过偏移量推算出某个元素的地址,因为是无序存储的。

单向链表:
如1号元素>2号元素>3号元素,其中1号元素知道2号元素的位置,2号元素知道3号元素的位置,但是3号元素不知道2号元素的位置,2号元素也不知道1号元素的位置,这就是单向链表。

查找方式:
(1)使用内容查找
这种需要遍历,效率低下。

(2)使用索引查找
相对于顺序表来说,效率没那么高,但实际也很快,因为元素之间都会保存彼此的内存地址,所以通过索引,获取对应元素的内存地址查找对应的元素,也比较快。效率低是体现在时钟周期方面,因为要先找到第一个元素,去获取第二个元素的内存地址,以此内推直到找到所需元素。

增加元素的方式:
(1)尾部
利用最后一个元素的内存指针(简称尾部指针),调整该指针指向新插入的这个元素,并让原来老的这个元素指向新的这个元素的内存地址,效率很高。

(2)中间
先遍历查找,找到插入点,断开原来旧的链接,让新的元素链接两个旧的元素(调整指针地址),效率不高,需要遍历找到插入点。

(3)头部
同尾部相同,也是调整指针,效率很高。

修改元素的方式:
这里也可以使用索引或内容遍历,但依然是索引最优。

删除元素的方式:
也是遍历或索引。
使用索引,一步到位,立即删除。
但是要看元素所处位置:
元素在头部或尾部,删除它,其他元素之间的链接关系不变。
元素在中间,删除它,邻近的两个元素重新建立连接关系(单向链表单向,双向链表双向)。

应用场景:
适用于需要频繁插入和删除元素,但不太需要快速访问元素的情况,此时选择链接表才能获得最佳的性能。
只要是插入和删除的需求都很多,并且不是在尾部变更,不管读取多不多,都得选链接表。

2.2.3 顺序表和链接表如何选择

读取:
如果特别要求效率,那么顺序表加索引最好。

增加:
如果是在尾部追加内容多,那么使用顺序表是最好的。
如果是在中间插入内容多,那么使用链接表是最好的。

修改:
都行,如果使用索引比较多,那还是顺序表更合适。

删除:
如果总是删除最后一个元素,那么使用顺序表更好。
如果总是删除任意位置的元素,那么使用链接表更好。

2.3 常见的线性数据结构

  1. 数组
  2. 切片
  3. 链表
  4. 队列

3. 数组(array)

数组是属于顺序表的一种实现,主要特点如下:

  • 长度与容量不可变
    数组的长度或者说容量,在定义时必须指定,且之后无法修改。
    这个无法修改,指的是程序在运行过程中,无法通过追加、删除等操作改变数组的大小(长度或容量)。
  • 内容(元素)可变,但元素内存地址永远不会变。
  • 数组中元素的首地址,就是数组地址
  • 可索引
  • 值类型(深拷贝)
    元素完全复制。也就是说赋值或传递时会复制整个数组,会有额外的内存开销。
  • 顺序表
    所有元素都是一个接一个按顺序存储到内存中的。这里指的是存储间隔,如每8个字节存储一个元素。

3.1 定义方式

3.1.1 方式一:仅声明数组

package main

import "fmt"

func main() {
	// 错误的声明方式:在go中,这种方式是声明切片,不是数组。数组必须明确元素数量
	// var a0 []

	// 正确的声明方式,明确的给出了数组元素数量为3,但此时的a0数组是0值
    // 这里的int可以是go中支持的任意数据类型,但元素类型必须一致
	var a0 [3]int // 这里的[3]int,表示数组类型和可存储的元素数量
	// 数组可以使用len和cap来判断长度和容量
	fmt.Println(len(a0), cap(a0))
	// 输出的结果表示,该顺序表(数组),在内存中开辟了一段存放3个元素的连续空间,实际存储的元素为0值。
	// 注意:定义好的数组长度和容量(len=cap),在运行过程中是不可变的,但其中的元素可以改变。
}
=========调试结果=========
3 3

3.1.2 方式二:推荐

package main

import "fmt"

func main() {
	// 错误的定义方式,凡是带=的都是赋值,右边不能只写类型([3]int),还必须写字面常量(值)
	//var a1 = [3]int

	// 正确的定义方式,{}表示一个容器(容器类型字面量界定符),存放元素的。
    // 推荐写法,声明且初始化
	var ai = [3]int{} // 这里的[3]int{},表示一个字面常量,因在等号右边
	fmt.Println(len(ai), cap(ai), ai)
}
=========调试结果=========
3 3 [0 0 0]

3.1.3 方式三:不推荐

package main

import "fmt"

func main() {
	var a2 [3]int = [3]int{} // 声明且初始化,不推荐,写法太啰嗦
	fmt.Println(a2)
}
=========调试结果=========
[0 0 0]

3.1.4 方式四

package main

import (
	"fmt"
)

func main() {
	// 错误的定义方式,数组定义时必须明确长度,但是变量是可变的,违背了数组的特性。
	//var num = 3
	//var a3 = [num]int{}

	// 正确的定义方式,上面的问题,使用常量声明数组长度即可,因为常量不可变。
	const num = 3
	var a3 = [num]int{}
	fmt.Println(a3)
}
=========调试结果=========
[0 0 0]

3.1.5 方式五:自动推断长度

package main

import (
	"fmt"
)

func main() {
	var a4 = [...]int{1, 2, 3} // ...表示自动推断元素个数,实际的个数根据{}中定义的来定
	fmt.Println(a4)
}
=========调试结果=========
[1 2 3]

3.1.6 方式六:指定值存放的索引位置

package main

import (
	"fmt"
)

func main() {
    var a6 = [5]int{1: 20, 4: 30} // index:value
	fmt.Println(a6)
}
=========调试结果=========
[0 20 0 0 30]

3.1.7 定义二维数组

package main

import (
	"fmt"
)

func main() {
	a7 := [2][3]int{} // 这个表示行列,此处2行3列
	fmt.Println(a7)
}
=========调试结果=========
[[0 0 0] [0 0 0]]

//大概就是这个意思
[2] [3]int
[
  [0, 0, 0], // 第一行(第一维) [3]int
  [0, 0, 0], // 第二行(第二维) [3]int
]

// 定义元素的方式
package main

import (
	"fmt"
)

func main() {
	a7 := [2][3]int{1: {100, 200, 300}} // 在第2个(按索引)数组中添加元素
	fmt.Println(a7)
}
=========调试结果=========
[[0 0 0] [100 200 300]]

3.1.8 通过for循环定义二维数组

var a1 = [2][5]int{}
	for i := 0; i < 5; i++ {
		a1[0][i] = i + 1
	}
	fmt.Println(a1)
=========调试结果=========
[[1 2 3 4 5] [0 0 0 0 0]]

3.2 长度和容量函数

  • cap:即capacity,容量,表示给数组分配的内存空间可以容纳多少个元素。
  • len:即length,长度,指的是容器中目前有几个元素。

由于数组创建时就必须确定的元素个数,且不能改变长度,所以不需要预留多余的内存空间,因此cap和len对数组来说一样。

3.3 索引

Go语言不支持负索引。
通过[index]来获取该位置上的值。
索引范围就是[0, 长度-1],可以理解为包前不包后。

3.4 修改元素

package main

import (
	"fmt"
)

func main() {
	a1 := [...]int{10, 20, 30}
	fmt.Println(a1)
	// 修改元素
	a1[0] += 100
	fmt.Println(a1)
}
=========调试结果=========
[10 20 30]
[110 20 30]

3.5 索引遍历元素

3.5.1 for循环

package main

import (
	"fmt"
)

func main() {
	var a0 = [5]int{1, 3, 5, 7, 9}
	for i := 0; i < len(a0); i++ {
		fmt.Println("Index:", i, "Value:", a0[i])
	}
}
=========调试结果=========
Index: 0 Value: 1
Index: 1 Value: 3
Index: 2 Value: 5
Index: 3 Value: 7
Index: 4 Value: 9

3.5.2 for range

package main

import (
	"fmt"
)

func main() {
	var a0 = [5]int{1, 3, 5, 7, 9}
	for i, v := range a0 {
		fmt.Println(i, v)
        
        //fmt.Println(i, v, a0[i])// 还可以这样,但是没必要,加了a0[i]
	}
}
=========调试结果=========
0 1
1 3
2 5
3 7
4 9

3.6 内存模型

package main

import (
	"fmt"
)

func main() {
	var a0 = [5]int{1, 3, 5, 7, 9}
	for i := 0; i < len(a0); i++ {
		fmt.Printf("数组内存地址: %p, 元素索引: %d, 元素值: %d, 元素内存地址: %p\n", &a0, i, a0[i], &a0[i])
	}
}
=========调试结果=========
数组内存地址: 0xc00000e3c0, 元素索引: 0, 元素值: 1, 元素内存地址: 0xc00000e3c0
数组内存地址: 0xc00000e3c0, 元素索引: 1, 元素值: 3, 元素内存地址: 0xc00000e3c8
数组内存地址: 0xc00000e3c0, 元素索引: 2, 元素值: 5, 元素内存地址: 0xc00000e3d0
数组内存地址: 0xc00000e3c0, 元素索引: 3, 元素值: 7, 元素内存地址: 0xc00000e3d8
数组内存地址: 0xc00000e3c0, 元素索引: 4, 元素值: 9, 元素内存地址: 0xc00000e3e0

通过上面输出的结果可以发现:
首先看元素内存地址:
0xc00000e3c0
0xc00000e3c8
0xc00000e3d0
0xc00000e3d8
0xc00000e3e0
可以发现:c0、c8、d0、d8、e0,都是每8个字节存放一个元素(16进制,逢16进1),充分的证明了顺序表是顺序存储的。
同时c0,也表示了该数组的首地址。

内存编址举例:
假如首地址为:00000000。内存中16进制按照字节编址,就是0到f(1个字节一个地址),一共16个地址,每一个字节表示一个偏移量。
假设我存了一个字符a,那么此时a会存储到0中,实际内存地址为000000000。
如果此时又存储了一个字符b,那么实际的内存地址为000000008(16进制偏移量为8)。
继续存,那么实际的内存地址为000000010。

上面每个元素间隔8个字节,正好64位,符合int类型定义。

3.6.1 修改元素值后,内存地址是否会改变

package main

import (
	"fmt"
)

func main() {
	var a0 = [5]int{1, 3, 5, 7, 9}
	for i := 0; i < len(a0); i++ {
		fmt.Printf("数组内存地址: %p, 元素索引: %d, 元素值: %d, 元素内存地址: %p\n", &a0, i, a0[i], &a0[i])
	}
	fmt.Println("----------------------")

	a0[0] = 1000
	fmt.Printf("数组内存地址: %p, 元素值: %d, 元素内存地址: %p\n", &a0, a0[0], &a0[0])
}
=========调试结果=========
数组内存地址: 0xc00000e3c0, 元素索引: 0, 元素值: 1, 元素内存地址: 0xc00000e3c0
数组内存地址: 0xc00000e3c0, 元素索引: 1, 元素值: 3, 元素内存地址: 0xc00000e3c8
数组内存地址: 0xc00000e3c0, 元素索引: 2, 元素值: 5, 元素内存地址: 0xc00000e3d0
数组内存地址: 0xc00000e3c0, 元素索引: 3, 元素值: 7, 元素内存地址: 0xc00000e3d8
数组内存地址: 0xc00000e3c0, 元素索引: 4, 元素值: 9, 元素内存地址: 0xc00000e3e0
----------------------
数组内存地址: 0xc00000e3c0, 元素值: 1000, 元素内存地址: 0xc00000e3c0

通过上述输出可以看到,0索引的元素,已经被改成了1000,但实际还是存储到原来的内存地址中的。

3.6.2 内存中,go对与字符串的处理方式

package main

import (
	"fmt"
)

func main() {
	var a0 = [4]string{"abc", "d", "asdfghjklpoiuytre", "q"}
	for i := 0; i < len(a0); i++ {
		fmt.Printf("数组内存地址: %p, 元素索引: %d, 元素值: %s, 元素内存地址: %p\n", &a0, i, a0[i], &a0[i])
	}
}
=========调试结果=========
数组内存地址: 0xc000026080, 元素索引: 0, 元素值: abc, 元素内存地址: 0xc000026080
数组内存地址: 0xc000026080, 元素索引: 1, 元素值: d, 元素内存地址: 0xc000026090
数组内存地址: 0xc000026080, 元素索引: 2, 元素值: asdfghjklpoiuytre, 元素内存地址: 0xc0000260a0
数组内存地址: 0xc000026080, 元素索引: 3, 元素值: q, 元素内存地址: 0xc0000260b0

从上面的输出可以看到,字符串的存储,每个元素之间间隔了16个字节,因为80、90、a0、b0。

但实际上我们说一个字符占一个字节,可为啥第三个元素长度超过了16,内存间隔依然是16个字节呢?实际处理逻辑如下:
首先开辟n个16个字节大小的内存空间,这个空间大小是不可改变的,并且内存空间并没有真的存储字符串,而是存储着一些元数据和一个指针,因为字符串很容易超过16字节长度,这个指针,指向了真正存储字符串的堆的地址(堆内存,字符串存储在堆中的),16个字节用来存储地址,是足够的。
这就是为啥第三个元素超过了16字节,依然能存储的原因,把字符串当成int来处理。

3.7 值类型(赋值与复制)

在Go语言中,值类型(Value Types)是指变量直接存储数据的值,而不是存储指向数据的指针或引用。当为值类型的变量分配一个新值时,这个新值会被直接复制到变量中,而不会影响其他变量。

值类型的数据有:

  • 布尔型。
  • 整型系列。
  • 浮点型。
  • 复数。
  • 字符串。
  • 字节型。
  • 符文型(rune)。
  • 数组。
  • 结构体。
    由零个或多个任意类型的值组成的聚合类型,虽然结构体本身是引用类型,但当结构体的字段全部为值类型时,结构体的行为类似于值类型。

3.7.1 示例一

package main

import "fmt"

func showAddr(arr [5]int) {
	fmt.Printf("arr %v %p", arr, &arr)
}

func main() {
	var a0 = [5]int{1, 3, 5, 7, 9}
	fmt.Printf("a0 %v %p\n", a0, &a0)

	// 把a0赋值给a1
	var a1 = a0
	fmt.Printf("a1 %v %p\n", a1, &a1)
	fmt.Println("a0和a1的内容是否相同:", a0 == a1, "a0和a1的内存地址是否相同", &a0 == &a1)

	// 把a0通过函数传参的方式,传给showAddr函数
	showAddr(a0)
}
=========调试结果=========
a0 [1 3 5 7 9] 0xc00000e3c0
a1 [1 3 5 7 9] 0xc00000e420
a0和a1的内容是否相同: true a0和a1的内存地址是否相同 false
arr [1 3 5 7 9] 0xc00000e480

通过上面的结果,可以看到a0、a1、arr的内容都相同,但是内存地址都不一样,这是什么原因?

首先就内容来说,整个代码运行过程中,并没有去修改内容,所以3个数组的内容都相同。

为什么内存地址不同?
首先看这段代码:var a1 = a0。
在其他语言中,这段代码可以理解为a1和a0共用一个内存地址,实际内存中,就一份数据,但Go不是这样的。
3c0、420、480,充分的说明了3个数组元素的首地址都不一样,说明在内存中,有3个副本保存着相同的数据。
也就是说a1 = a0,相当于是在内存中,重新开辟了一块内存空间,来存储a1对应的内容。
所以,在go中的话,类似var a1 = a0这种赋值语句,就是完全复制,也可以称为值复制。

并且上面的函数传参,大概也是这样的道理,调用showAddr(a0)函数时,会在该函数的内部再造一个副本(这里这样说明只是为了方便理解)。
也就是说,showAddr(a0),看似是在操作a0,实际是在内存空间中重新开辟了一块新的内存,复制a0的数据存过来而已,也就是操作的这个新的副本。

3.7.2 示例二

package main

import "fmt"

func showAddr(arr [5]int) [5]int { // 看这里
	fmt.Printf("arr %v %p\n", arr, &arr)
	arr[0] = 10000
	fmt.Printf("arr %v %p\n", arr, &arr)

	return arr
}

func main() {
	var a0 = [5]int{1, 3, 5, 7, 9}
	fmt.Printf("a0 %v %p\n", a0, &a0)

	var a1 = a0
	fmt.Printf("a1 %v %p\n", a1, &a1)
	fmt.Println("a0和a1的内容是否相同:", a0 == a1, "a0和a1的内存地址是否相同", &a0 == &a1)

	a8 := showAddr(a0) // 看这里
	fmt.Println("a0 ", a0)
	fmt.Printf("a8 %v %p\n", a8, &a8)
}
=========调试结果=========
a0 [1 3 5 7 9] 0xc00000e3c0
a1 [1 3 5 7 9] 0xc00000e420
a0和a1的内容是否相同: true a0和a1的内存地址是否相同 false
arr [1 3 5 7 9] 0xc00000e4b0  // 看这里
arr [10000 3 5 7 9] 0xc00000e4b0 // 看这里
a0  [1 3 5 7 9] // 看这里
a8 [10000 3 5 7 9] 0xc00000e480 // 看这里

这里主要是想说明,在函数中使用return返回值,也是值复制。
如在showAddr函数中,通过arr[0] = 10000,然后通过return返回值,用a8(a8 := showAddr(a0))来接住该返回值,最终虽然a8的值和arr相同,但内存地址也是完全不同的,只是a8是arr的副本而已(return时,对值又做了一次复制)。

3.7.3 频繁的值复制对系统带来的影响

同一份数据被频繁的赋值,会对系统内存造成一个极大的浪费,严重影响系统性能。
那如果在使用数组过程中有频繁赋值的需求怎么办?使用切片代替数组。

3.7.4 总结

使用赋值表达式时,会形成一次值复制,形成一个副本。
使用函数传参或者函数返回值时,也会形成一次值复制,形成一个副本。
这就是值类型。

3.8 小练习

3.8.1 创建一个整数数组并使用循环填充数组元素,找出数组中的最大值和最小值,并打印结果

package main

import (
	"fmt"
	"math/rand"
	"time"
)

// 数组练习
func a() {
	var a = [10]int{}
	r := rand.New(rand.NewSource(time.Now().UnixNano()))
	for i := 0; i < 10; i++ {
		a[i] = r.Intn(10)
	}
	fmt.Println(a)
	var maxnums, minnums int = 1, 1
	for _, v := range a {
		if v > maxnums {
			maxnums = v
		} else if v < minnums {
			minnums = v
		}
	}
	fmt.Println(maxnums, minnums)

}

func main() {
	a()
}
================== 调试结果 ==================
[1 8 1 9 6 9 2 6 8 4]
9 1

3.8.2 编写一个函数,接受一个整数数组,返回数组中所有元素的和。

package main

import (
	"fmt"
)

func sumArray(array [5]int) int {
	sum := 0
	for i := 0; i < len(array); i++ {
		sum += array[i]
	}
	return sum
}

func main() {
	i := sumArray([5]int{1, 2, 3, 4, 5})
	fmt.Println(i)
}
==========调试结果==========15

3.8.3 编写一个函数,接受一个整数数组,返回一个新的数组,该数组是原数组的反转。

package main

import "fmt"

func oldArray(array [5]int) [5]int {
	newArray := [5]int{}
	for i, v := range array {
		newArray[len(newArray)-(i+1)] = v
	}
	return newArray
}

func main() {
	i := oldArray([5]int{1, 2, 3, 4, 5})
	fmt.Println(i)
}
===============调试结果===============
[5 4 3 2 1]

3.8.4 编写一个函数,接受一个整数数组和一个整数,将数组中所有等于该整数的元素替换为0。

package main

import (
	"fmt"
)

func a(arry [5]int, num int) {
	for i, v := range arry {
		if v == num {
			arry[i] = 0
		}
	}
	fmt.Println(arry)
}

func main() {
	a([5]int{1, 2, 3, 4, 3}, 3)
}
==========调试结果==========
[1 2 0 4 0]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值