当前位置: 首页 > news >正文

深入理解 Go 数组、切片、字符串

打个广告:欢迎关注我的微信公众号,在这里您将获取更全面、更新颖的文章!

alt

原文链接:深入理解 Go 数组、切片、字符串 欢迎点赞关注

前言

为什么在一篇文章里同时介绍数组、切片、字符串,了解这三个数据类型底层数据结构的同学一定知道,在go中这三个数据类型,底层有一定联系,切片和字符串底层都是基于数组实现的,字符切片和字符串之间还可以相互转换。

alt

数组

类型

数组是一种复合类型,具体类型是由:数组的长度 + 数组元素的类型决定的,下面的是两种数组类型:

[10]int // 长度为10的int数组
[11]int // 长度为11的int数组

只有长度和元素类型都相同才是同一类型。

访问

我们通过指向数组开头的指针、元素的数量以及元素类型占的空间大小表示数组。如果我们不知道数组中元素的数量,访问时可能发生越界;而如果不知道数组中元素类型的大小,就没有办法知道应该一次取出多少字节的数据,无论丢失了哪个信息,我们都无法知道这片连续的内存空间到底存储了什么数据:

alt

访问索引为1的元素只需要数组的起始地址偏移1个元素大小即可:P + 1 * size。

越界检查

Go 语言中可以在编译期间的静态类型检查判断数组越界,cmd/compile/internal/gc.typecheck1 会验证访问数组的索引:

func typecheck1(n *Node, top int) (res *Node) {
        switch n.Op {
        case OINDEX:
                ok |= ctxExpr
                l := n.Left  // array
                r := n.Right // index
                switch n.Left.Type.Etype {
                case TSTRING, TARRAY, TSLICE:
                        ...
                        if n.Right.Type != nil && !n.Right.Type.IsInteger() {
                                yyerror("non-integer array index %v", n.Right)
                                break
                        }
                        if !n.Bounded() && Isconst(n.Right, CTINT) {
                                x := n.Right.Int64()
                                if x < 0 {
                                        yyerror("invalid array index %v (index must be non-negative)", n.Right)
                                } else if n.Left.Type.IsArray() && x >= n.Left.Type.NumElem() {
                                        yyerror("invalid array index %v (out of bounds for %d-element array)", n.Right, n.Left.Type.NumElem())
                                }
                        }
                }
        ...
        }
}
  1. 访问数组的索引是非整数时,报错 “non-integer array index %v”;

  2. 访问数组的索引是负数时,报错 “invalid array index %v (index must be non-negative)";

  3. 访问数组的索引越界时,报错 “invalid array index %v (out of bounds for %d-element array)";

除了上面的检查外,在生成SSA期间还会插入越界检查指令,我们编写如下代码,然后生成SSA中间代码:

package check

func outOfRange() int {
        arr := [3]int{123}
        i := 4
        elem := arr[i]return elem
}

$ GOSSAFUNC=outOfRange go build array.go
dumped SSA to ./ssa.html

ssa.html 中间代码如下:

b1:...
    v22 (6) = LocalAddr <*[3]int> {arr} v2 v20
    v23 (6) = IsInBounds <bool> v21 v11 // 判断idx是否小于len
If v23 → b2 b3 (likely) (6// 如果小于执行 b2,否则(越界)执行 b3

b2: ← b1-
    v26 (6) = PtrIndex <*int> v22 v21
    v27 (6) = Copy <mem> v20
    v28 (6) = Load <int> v26 v27 (elem[int])...
Ret v30 (+7)

b3: ← b1-
    v24 (6) = Copy <mem> v20
    v25 (6) = PanicBounds <mem> [0] v21 v11 v24 // panic 退出
Exit v25 (6)

切片

Go 语言切片可以看做对数组的封账,Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

数据结构

编译期间的切片是 cmd/compile/internal/types.Slice 类型的,但是在运行时切片可以由如下的 reflect.SliceHeader 结构体表示,其中:

  • Data 是指向数组的指针;

  • Len 是当前切片的长度;

  • Cap 是当前切片的容量,即 Data 数组的大小:

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

Data 是一片连续的内存空间,这片内存空间可以用于存储切片中的全部元素,数组中的元素只是逻辑上的概念,底层存储其实都是连续的,所以我们可以将切片理解成一片连续的内存空间加上长度与容量的标识。

alt

创建切片

使用make创建Slice

使用make来创建Slice时,可以同时指定长度和容量,创建时底层会分配一个数组,数组的长度即容量。

例如,语句slice := make([]int, 5, 10)所创建的Slice,结构如下图所示:

alt

该Slice长度为5,即可以使用下标slice[0] ~ slice[4]来操作里面的元素,capacity为10,表示后续向slice添加新的元素时可以不必重新分配内存,直接使用预留内存即可。

使用数组创建Slice

使用数组来创建Slice时,Slice将与原数组共用一部分内存。

例如,语句slice := array[5:7]所创建的Slice,结构如下图所示:

alt

切片从数组array[5]开始,到数组array[7]结束(不含array[7]),即切片长度为2,数组后面的内容都作为切片的预留内存,即capacity为5。

数组和切片操作可能作用于同一块内存,这也是使用过程中需要注意的地方。

切片扩容

使用append向Slice追加元素时,如果Slice空间不足,将会触发Slice扩容,扩容实际上重新一配一块更大的内存,将原Slice数据拷贝进新Slice,然后返回新Slice,扩容后再将数据追加进去。

例如,当向一个capacity为5,且length也为5的Slice再次追加1个元素时,就会发生扩容,如下图所示:

alt

当切片的容量不足时,我们会调用 runtime.growslice 函数为切片扩容,扩容是为切片分配新的内存空间并拷贝原切片中元素的过程,我们先来看新切片的容量是如何确定的:

func growslice(et *_type, old slice, cap int) slice {
        newcap := old.cap
        doublecap := newcap + newcap
        if cap > doublecap {
                newcap = cap
        } else {
                if old.len < 1024 {
                        newcap = doublecap
                } else {
                        for 0 < newcap && newcap < cap {
                                newcap += newcap / 4
                        }
                        if newcap <= 0 {
                                newcap = cap
                        }
                }
        }

在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;

  2. 如果当前切片的长度小于 1024 就会将容量翻倍;

  3. 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;

上述代码片段仅会确定切片的大致容量,下面还需要根据切片中的元素大小对齐内存,当数组中元素所占的字节大小为 1、8 或者 2 的倍数时,运行时会使用如下所示的代码对齐内存:

var overflow bool
    var lenmem, newlenmem, capmem uintptr
    switch {
    case et.size == 1:
            lenmem = uintptr(old.len)
            newlenmem = uintptr(cap)
            capmem = roundupsize(uintptr(newcap))
            overflow = uintptr(newcap) > maxAlloc
            newcap = int(capmem)
    case et.size == sys.PtrSize:
            lenmem = uintptr(old.len) * sys.PtrSize
            newlenmem = uintptr(cap) * sys.PtrSize
            capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
            overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
            newcap = int(capmem / sys.PtrSize)
    case isPowerOfTwo(et.size):
            ...
    default:
            ...
    }

runtime.roundupsize 函数会将待申请的内存向上取整,取整时会使用 runtime.class_to_size 数组,使用该数组中的整数可以提高内存的分配效率并减少碎片,我们会在内存分配一节详细介绍该数组的作用:

var class_to_size = [_NumSizeClasses]uint16{
    0,
    8,
    16,
    32,
    48,
    64,
    80,
    ...,
}

下面举个例子,说明一下扩容和内存对齐的效果:

var arr []int64
arr = append(arr, 12345)

fmt.Println(cap(arr)) // 6

上面代码里会触发 runtime.growslice 函数扩容 arr 切片并传入期望的新容量 5,这时期望分配的内存大小为 40 字节;不过因为切片中的元素大小等于 sys.PtrSize,所以运行时会调用 runtime.roundupsize 向上取整内存的大小到 48 字节,所以新切片的容量为 48 / 8 = 6。

切片copy

使用copy()内置函数拷贝两个切片时,会将源切片的数据逐个拷贝到目的切片指向的数组中,拷贝数量取两个切片长度的最小值,例如长度为10的切片拷贝到长度为5的切片时,将会拷贝5个元素,也就是说,copy过程中不会发生扩容。

func slicecopy(to, fm slice, width uintptr) int {
        if fm.len == 0 || to.len == 0 {
                return 0
        }
        n := fm.len
        if to.len < n {
                n = to.len
        }
        if width == 0 {
                return n
        }
        ...

        size := uintptr(n) * width
        if size == 1 {
                *(*byte)(to.array) = *(*byte)(fm.array)
        } else {
                memmove(to.array, fm.array, size)
        }
        return n
}

其他知识点

nil 切片和空切片

// nil切片
var s1 []int
    
// nil切片指针
s2 := new([]int)

// 空切片
s3 := make([]int0)
s4 := []int{}

sh1 := (*(*reflect.SliceHeader)(unsafe.Pointer(&s1)))
sh3 := (*(*reflect.SliceHeader)(unsafe.Pointer(&s3)))           

用debug看一下s1 s2 s3 s4的内存情况:

alt

通过debug可以发现,nil切片的data指针指向的是nil,空切片的data指针指向的是空数组:

alt

切片作为函数参数

在Go函数中,函数的参数传递均是值传递。那么,将切片通过参数传递给函数,其实质是复制了slice结构体对象,两个slice结构体的字段值均相等。正常情况下,由于函数内slice结构体的array和函数外slice结构体的array指向的是同一底层数组,所以当对底层数组中的数据做修改时,两者均会受到影响。

func main() {
    arr := []int{1,2,3}
    update(arr)
    fmt.Println(arr[1]) // 100
}

func update(arr []int) {
    arr[1] = 100
}

但是存在这样的问题:如果指向底层数组的指针被覆盖或者修改(copy、重分配、append触发扩容),此时函数内部对数据的修改将不再影响到外部的切片,代表长度的len和容量cap也均不会被修改。

func main() {
    arr := []int{1,2,3}
    update(arr)
    fmt.Println(len(arr)) // 3
}

func update(arr []int) {
    arr = append(arr, 4)
    fmt.Println(len(arr)) // 4
}

字符串

Go标准库builtin给出了所有内置类型的定义。 源代码位于src/builtin/builtin.go,其中关于string的描述如下:

// string is the set of all strings of 8-bit bytes, conventionally but not// necessarily representing UTF-8-encoded text. A string may be empty, but// not nil. Values of string type are immutable.type string string
解释

所以string是8比特字节的集合,通常但并不一定是UTF-8编码的文本。

另外,还提到了两点,非常重要:

  • string可以为空(长度为0),但不会是nil;

  • string对象不可以修改。

数据结构

字符串在 Go 语言中的接口其实非常简单,每一个字符串在运行时都会使用如下的 reflect.StringHeader 表示,其中包含指向字节数组的指针和数组的大小:

type StringHeader struct {
    Data uintptr
    Len  int
}

因为字符串作为只读的类型,我们并不会直接向字符串直接追加元素改变其本身的内存空间,所有在字符串上的写入操作都是通过拷贝实现的。

字符串拼接

Go 语言拼接字符串会使用 + 符号,底层实现是 runtime.concatstrings,它会先对遍历传入的切片参数,再过滤空字符串并计算拼接后字符串的长度。

func concatstrings(buf *tmpBuf, a []string) string {
        idx := 0
        l := 0
        count := 0
        for i, x := range a {
                n := len(x)
                if n == 0 {
                        continue
                }
                l += n
                count++
                idx = i
        }
        if count == 0 {
                return ""
        }
        if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {
                return a[idx]
        }
        s, b := rawstringtmp(buf, l)
        for _, x := range a {
                copy(b, x)
                b = b[len(x):]
        }
        return s
}

拼接的过程:

  1. 通过遍历计算新字符串的长度

  2. 生成新字符串

  3. 在通过遍历,将原字符串拷贝到新字符串中

字节串与字符数组互转

从字节数组到字符串的转换需要使用 runtime.slicebytetostring 函数,例如:string(bytes),该函数在函数体中会先处理两种比较常见的情况,也就是长度为 0 或者 1 的字节数组,这两种情况处理起来都非常简单:

func slicebytetostring(buf *tmpBuf, b []byte) (str string) {
        l := len(b)
        if l == 0 {
                return ""
        }
        if l == 1 {
                stringStructOf(&str).str = unsafe.Pointer(&staticbytes[b[0]])
                stringStructOf(&str).len = 1
                return
        }
        var p unsafe.Pointer
        if buf != nil && len(b) <= len(buf) {
                p = unsafe.Pointer(buf)
        } else {
                p = mallocgc(uintptr(len(b)), nilfalse)
        }
        stringStructOf(&str).str = p
        stringStructOf(&str).len = len(b)
        memmove(p, (*(*slice)(unsafe.Pointer(&b))).array, uintptr(len(b)))
        return
}

处理过后会根据传入的缓冲区大小决定是否需要为新字符串分配一片内存空间,runtime.stringStructOf 会将传入的字符串指针转换成 runtime.stringStruct 结构体指针,然后设置结构体持有的字符串指针 str 和长度 len,最后通过 runtime.memmove 将原 []byte 中的字节全部复制到新的内存空间中。

当我们想要将字符串转换成 []byte 类型时,需要使用 runtime.stringtoslicebyte 函数,该函数的实现非常容易理解:

func stringtoslicebyte(buf *tmpBuf, s string) []byte {
        var b []byte
        if buf != nil && len(s) <= len(buf) {
                *buf = tmpBuf{}
                b = buf[:len(s)]
        } else {
                b = rawbyteslice(len(s))
        }
        copy(b, s)
        return b
}

上述函数会根据是否传入缓冲区做出不同的处理:

  • 当传入缓冲区时,它会使用传入的缓冲区存储 []byte;

  • 当没有传入缓冲区时,运行时会调用 runtime.rawbyteslice 创建新的字节切片并将字符串中的内容拷贝过去;

上面的两种转换方式都进行了内存复制,会存在一些性能问题,还有一种通过unsafe包进行类型转换的方式,不涉及到内存拷贝,但是unsafe包并不推荐在生产环境使用,所以大家使用时要谨慎:

// string转ytes
func Str2sbyte(s string) (b []byte) {
    *(*string)(unsafe.Pointer(&b)) = s        // 把s的地址付给b
    *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&b)) + 2*unsafe.Sizeof(&b))) = len(s)        // 修改容量为长度
    return
}

// []byte转string
func Sbyte2str(b []byte) string {
    return *(*string)(unsafe.Pointer(&b))
}

参考

https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-array

https://books.studygolang.com/GoExpertProgramming/chapter01/1.2-slice.html

本文由 mdnice 多平台发布

相关文章:

深入理解 Go 数组、切片、字符串

打个广告&#xff1a;欢迎关注我的微信公众号&#xff0c;在这里您将获取更全面、更新颖的文章&#xff01; 原文链接&#xff1a;深入理解 Go 数组、切片、字符串 欢迎点赞关注 前言 为什么在一篇文章里同时介绍数组、切片、字符串&#xff0c;了解这三个数据类型底层数据结构…...

STM32下的HAL库U盘数据读写

最近在使用FreeRTOS操作U盘数据读取&#xff0c;与裸机开发区别不大&#xff0c;只要注意默认分配的栈大小就行 FATFS简介 FATFS 是一个完全免费开源的 FAT 文件系统模块&#xff0c;专门为小型的嵌入式系统而设计。它完全用标准 C 语言编写&#xff0c;所以具有良好的硬件平…...

JavaScript(20)——事件委托

事件委托是利用事件流的特征解决一些开发需求的知识技巧 优点&#xff1a;减少注册次数&#xff0c;提高程序性能 原理&#xff1a;利用事件冒泡的特点。 给父元素注册事件&#xff0c;当我们触发子元素的时候&#xff0c;会冒泡到父元素上&#xff0c;从而触发父元素事件。…...

Linux系统安装opencv

在Linux系统上安装OpenCV时&#xff0c;使用pip install opencv-python-headless是一个常见的选择。以下是安装OpenCV的步骤总结&#xff1a; 安装步骤&#xff1a; 更新系统软件包&#xff08;可选&#xff09;&#xff1a; sudo apt update sudo apt upgrade安装pip&#xf…...

nginx反向代理+nginx黑白名单+nginx负载均衡+平滑升级+配置jdk环境-7.30

一、反向代理 1.前端服务器配置 前端服务器&#xff1a;用于接收和响应客户端&#xff0c;代理另一台主机 Nginx 安装 (1).下载安装nginx [rootweb ~]# wget https://nginx.org/download/nginx-1.26.1.tar.gz (2).解压 [rootweb ~ ]# tar -zxvf nginx-1.26.1.tar.gz (3…...

C# Unity 面向对象补全计划 之 访问修饰符

本文仅作学习笔记与交流&#xff0c;不作任何商业用途&#xff0c;作者能力有限&#xff0c;如有不足还请斧正 本系列旨在通过补全学习之后&#xff0c;给出任意类图都能实现并做到逻辑上严丝合缝...

JAVA多线程设计模式 (多线程程序的衡量标准)

前言 如果你对一个程序的评价方式还停留在“这个程序写得不错”、“这个程序写得太差”的程度&#xff0c;这样很难看出你是否确实分析了程序的构造。不要只是“好不好”应该要有一个“好在哪里&#xff0c;不好在哪里”的评量标准。 一、下载地址 下载地址&#xff1a;JAVA…...

消息队列使用

消息队列在分布式系统中扮演着非常重要的角色,其主要用途包括但不限于以下几点: 解耦: 消息队列可以减少应用程序之间的直接依赖,使系统各部分更加独立。例如,在一个电子商务网站中,订单系统可以将订单信息发送到消息队列中,而不必直接调用库存系统。库存系统可以从队列…...

Windows 10+Visual Studio2019 Opencv-C++安装配置

前往官网下载需要的版本Releases - OpenCVhttps://opencv.org/releases/ 1.下载并解压OpenCV 我选择4.6.0&#xff0c;点击windows版本&#xff0c;进行下载 2.配置项目属性 打开你的Visual Studio 2019项目 -> 右击项目名&#xff0c;选择“属性” 注&#xff1a;整个配…...

百易云资产管理运营系统 comfileup.php 文件上传致RCE漏洞复现(XVE-2024-18154)

0x01 产品简介 百易云资产管理运营系统,是专门针对企业不动产资产管理和运营需求而设计的一套综合解决方案。该系统能够覆盖资产的全生命周期管理,包括资产的登记、盘点、评估、处置等多个环节,同时提供强大的运营分析功能,帮助企业优化资产配置,提升运营效率。 0x02 漏…...

【学习笔记】Redis学习笔记——第17章 集群

第17章 集群 17.1 节点 使用cluster meet命令将其他节点添加到当前节点集群中 17.1.1 启动节点 通过cluster-enabled设置来决定是否启用集群 17.1.2 集群数据结构 每个节点都保存了自己及其他节点的IP端口号等信息 17.1.3 cluster meet命令的实现 是收到命令的节点和新…...

Vue - CSS基础学习

一、元素及属性 CSS 是为 web 内容添加样式的代码。 style标签 1.语法 1.除了选择器部分&#xff0c;每个规则集都应该包含在成对的大括号里&#xff08;{}&#xff09;。 2.在每个声明里要用冒号&#xff08;:&#xff09;将属性与属性值分隔开。 3.在每个规则集里要用分号…...

python setup.py install

有三种方法安装pyinstaller&#xff0c;而我唯独用下面这种最方便&#xff0c;又成功。 下载源码安装包&#xff0c;然后解压安装包&#xff0c;打开cmd并进入安装包解压目录&#xff0c;可以看到该目录下有一个setup.py的文件。 执行命令&#xff1a;python setup.py instal…...

论文解读:DiAD之SG网络

目录 一、SG网络功能介绍二、SG网络代码实现 一、SG网络功能介绍 DiAD论文最主要的创新点就是使用SG网络解决多类别异常检测中的语义信息丢失问题&#xff0c;那么它是怎么实现的保留原始图像语义信息的同时重建异常区域&#xff1f; 与稳定扩散去噪网络的连接&#xff1a; S…...

Prometheus+Grafana 监控平台实践-搭建常用服务监控告警

前言 Prometheus 是一个开放性的监控解决方案,通过各种 Exporter 采集当前主机/服务的数据,和 Grafana 相结合可以实现强大的监控和可视化功能 本篇将分享使用 docker compose 构建 Prometheus+Grafana,并监控之前文章所搭建的主机&服务,分享日常使用的一些使用经验 文…...

leaflet加载天地图:卫星底图(影响地图) 和 路网底图(矢量地图)【webgis】

文章目录 引言I 申请Key1.1 应用创建1.2 账号认证II 使用申请的key进行相关的服务调用2.1 服务API2.2 初始化地图组件2.3 加载影像底图和影像注记2.4 地理编码查询引言 自己没有对应的地图服务器和地理查询服务器的时候,使用天地图来实现业务的快速支撑。 基于天地图使用Leaf…...

微软蓝屏事件:全球网络安全与系统稳定性的警示

文章目录 每日一句正能量前言探讨软件更新流程中的风险管理和质量控制机制软件更新的风险风险管理策略质量控制措施测试流程缺陷识别实施质量控制结论 提供预防类似大规模故障的最佳方案或应急响应对策设计冗余系统实施灾难恢复计划建立高可用架构应急响应对策利用自动化工具和…...

【51单片机仿真】基于51单片机设计的整数/小数计算器系统仿真源码文档——文末资料下载

演示: 摘要 本项目设计并实现了一种基于51单片机的多功能计算器系统。系统采用STC89C52单片机作为主控制器,结合LCD显示模块、矩阵键盘等外设,实现了基本的整数、小数算术运算功能,包括加、减、乘、除、取模等。本文详细介绍了系统的硬件设计和软件实现,并对系统的功能和…...

ubuntu安装dockergitlab

#更换阿里云进行配置GPG 密钥 curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add - sudo add-apt-repository \ "deb [archarm64] https://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable" #如果出现错…...

自定义视图提示:提升Laravel用户体验的秘诀

自定义视图提示&#xff1a;提升Laravel用户体验的秘诀 在Laravel框架中&#xff0c;视图提示是一种向用户显示友好信息的方式&#xff0c;它们可以是表单输入后的错误信息、成功通知或其他重要的用户反馈。自定义视图提示不仅可以增强用户体验&#xff0c;还可以使应用程序的…...

(二)TensorRT-LLM | 模型导出(v0.20.0rc3)

0. 概述 上一节 对安装和使用有个基本介绍。根据这个 issue 的描述&#xff0c;后续 TensorRT-LLM 团队可能更专注于更新和维护 pytorch backend。但 tensorrt backend 作为先前一直开发的工作&#xff0c;其中包含了大量可以学习的地方。本文主要看看它导出模型的部分&#x…...

【磁盘】每天掌握一个Linux命令 - iostat

目录 【磁盘】每天掌握一个Linux命令 - iostat工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景 注意事项 【磁盘】每天掌握一个Linux命令 - iostat 工具概述 iostat&#xff08;I/O Statistics&#xff09;是Linux系统下用于监视系统输入输出设备和CPU使…...

将对透视变换后的图像使用Otsu进行阈值化,来分离黑色和白色像素。这句话中的Otsu是什么意思?

Otsu 是一种自动阈值化方法&#xff0c;用于将图像分割为前景和背景。它通过最小化图像的类内方差或等价地最大化类间方差来选择最佳阈值。这种方法特别适用于图像的二值化处理&#xff0c;能够自动确定一个阈值&#xff0c;将图像中的像素分为黑色和白色两类。 Otsu 方法的原…...

2021-03-15 iview一些问题

1.iview 在使用tree组件时&#xff0c;发现没有set类的方法&#xff0c;只有get&#xff0c;那么要改变tree值&#xff0c;只能遍历treeData&#xff0c;递归修改treeData的checked&#xff0c;发现无法更改&#xff0c;原因在于check模式下&#xff0c;子元素的勾选状态跟父节…...

Keil 中设置 STM32 Flash 和 RAM 地址详解

文章目录 Keil 中设置 STM32 Flash 和 RAM 地址详解一、Flash 和 RAM 配置界面(Target 选项卡)1. IROM1(用于配置 Flash)2. IRAM1(用于配置 RAM)二、链接器设置界面(Linker 选项卡)1. 勾选“Use Memory Layout from Target Dialog”2. 查看链接器参数(如果没有勾选上面…...

【C语言练习】080. 使用C语言实现简单的数据库操作

080. 使用C语言实现简单的数据库操作 080. 使用C语言实现简单的数据库操作使用原生APIODBC接口第三方库ORM框架文件模拟1. 安装SQLite2. 示例代码:使用SQLite创建数据库、表和插入数据3. 编译和运行4. 示例运行输出:5. 注意事项6. 总结080. 使用C语言实现简单的数据库操作 在…...

Springboot社区养老保险系统小程序

一、前言 随着我国经济迅速发展&#xff0c;人们对手机的需求越来越大&#xff0c;各种手机软件也都在被广泛应用&#xff0c;但是对于手机进行数据信息管理&#xff0c;对于手机的各种软件也是备受用户的喜爱&#xff0c;社区养老保险系统小程序被用户普遍使用&#xff0c;为方…...

Python ROS2【机器人中间件框架】 简介

销量过万TEEIS德国护膝夏天用薄款 优惠券冠生园 百花蜂蜜428g 挤压瓶纯蜂蜜巨奇严选 鞋子除臭剂360ml 多芬身体磨砂膏280g健70%-75%酒精消毒棉片湿巾1418cm 80片/袋3袋大包清洁食品用消毒 优惠券AIMORNY52朵红玫瑰永生香皂花同城配送非鲜花七夕情人节生日礼物送女友 热卖妙洁棉…...

C# winform教程(二)----checkbox

一、作用 提供一个用户选择或者不选的状态&#xff0c;这是一个可以多选的控件。 二、属性 其实功能大差不差&#xff0c;除了特殊的几个外&#xff0c;与button基本相同&#xff0c;所有说几个独有的 checkbox属性 名称内容含义appearance控件外观可以变成按钮形状checkali…...

用神经网络读懂你的“心情”:揭秘情绪识别系统背后的AI魔法

用神经网络读懂你的“心情”:揭秘情绪识别系统背后的AI魔法 大家好,我是Echo_Wish。最近刷短视频、看直播,有没有发现,越来越多的应用都开始“懂你”了——它们能感知你的情绪,推荐更合适的内容,甚至帮客服识别用户情绪,提升服务体验。这背后,神经网络在悄悄发力,撑起…...