GO-slice详解

news2024/11/18 19:57:41

GO-slice详解

简介

slice(切片)是go中常见和强大的类型,这篇文章不是slice使用简介,从源码角度来分析slice的实现,slice的一些迷惑的使用方式,同时也讲清楚一些问题。

slice的底层实现是数组,是对数组的抽象

官方有slice相关的博客

  1. https://go.dev/blog/slices-intro
  2. https://go.dev/blog/slices

slice分析

数据结构

源码:https://github.com/golang/go/blob/master/src/runtime/slice.go#L14

slice结构如下:

type slice struct {
	array unsafe.Pointer // 数组指针
	len   int  //切片长度
	cap   int // 切片容量
}

array:为是底层数组的指针

len:切片中已有元素的个数

cap:底层数组的长度

原理概述:

切片的底层实现是数组,len是切片的个数,cap是底层数组的长度,当往切片中追加元素的时候,len++,如果len>cap,就会触发切片扩容,扩容逻辑是算一个新的cap,并且创建一个新的底层数组,将原来的数据copy过来。并且创建一个新的切片(slice)。

可以从一个切片中创建一个新的切片,底层数组是同用的,修改切片元素的时候会影响到新的切片。

如果所示:

在这里插入图片描述

var s = make([]byte,5)对应的逻辑是,创建一个长度和容量为5的数组,如果所示

在这里插入图片描述

切片的创建方式

先看切片的创建方式,说这个问题之前,先看看切片的创建方式

声明

var vocabList []uint64

声明了一个[]uint64类型的切片,vocabList为切片的0值,

这得说一下go中nil

在 Go 中,nil 是指针、接口、映射、切片、通道和函数类型的零值,表示未初始化的值。

具体的可以看:https://go101.org/article/nil.html

回到代码,这表示nil值,它的len和cap都是0,和nil比较结果为true,这里要说,nil值对应的具体的类型是在上下文中编译器推导出来的

package main

func main() {
	var vocabList []uint64
	println(vocabList == nil)
}
// output:
// true

通过new创建

var vocabList = *new([]uint64)

new是内建函数,用来分配指定类型的内容,返回指向内存地址的指针,并且给此地址分配此类型的0值。

字面量创建

var vocabList = []uint64{1,2,3,4}

make

var vocabList = make([]uint64,10)

make接受三个参数,在创建的时候指定类型,长度,容量,不指定容量,默认和长度一样

代码如下:

func main() {
	var vocabList = make([]uint64,10)
	fmt.Printf("slice:%v,len:%d,cap:%d",vocabList,len(vocabList),cap(vocabList))
}
// output:
// slice:[0 0 0 0 0 0 0 0 0 0],len:10,cap:10

从切片或数组截取

var vocabList = resList[3:5]

两个切片公用一个底层数组,但如果新创建的切片扩容了,就不共用了。

问题分析

主要分析几个问题

nil切片和空切片的差异

nil切片是通过 new 和声明方式创建的切片,go会给他们nil值,如下面的代码所示:

var vocabList []int

vocabList == nil //true

空切片是通过make,字面量方式创建的长度为0的切片,

vocabList := make([]int,0)

vocabList == nil // false

vocabList1 = []int{}
vocabList1 == nil //false

具体可以看这篇文章:https://juejin.cn/post/6844903712654098446

我下面的代码和内容来于这篇文章

通过unsafe.Pointer可以将对应地址中的数据转换为任何符合go中类型的变量
在这里插入图片描述

可以看到,空切片的是有底层数组的,并且底层数组都一样,其实也可以说空切片执行了一个指定的地址空间,

这个地址空间在源码中有定义,当分配的大小为0的时候会返回这个地址,要说明的是这个地址空间不是固定的,不是写死的一个数,在不同的机器上运行会有不同的值。

源码:https://github.com/golang/go/blob/master/src/runtime/malloc.go#L948

在这里插入图片描述

两者的差异:

  1. 嵌套在结构体中不容易发现

    package main
    
    type Word struct {
    	SenseIds []int
    }
    
    func main() {
    	word := Word{}
    	println(word.SenseIds == nil) //true
    
    	word1 := Word{
    		SenseIds: make([]int,0),
    	}
    	println(word1.SenseIds == nil) //false
    }
    
  2. json序列化

    package main
    
    import "encoding/json"
    
    type Word struct {
    	SenseIds []int `json:"sense_ids" `
    }
    
    func main() {
    	word := Word{}
    	marshal, err := json.Marshal(word)
    	if err != nil {
    		return
    	}
    	println(string(marshal)) //{"sense_ids":null}
    
    
    
    	word1 := Word{
    		SenseIds: []int{},
    	}
    	marshal1, err := json.Marshal(word1)
    	if err != nil {
    		return
    	}
    	println(string(marshal1)) //{"sense_ids":[]}
    }
    
    

    这个问题我深有体会

    在做一个需求的时候,看到编辑器报黄色提示,提示我将 var a = []int{} 改为var a []int,当然,go官方也是这么建议的。我就改了,然后一个接口就报错了。给我一顿找,发现json返回了null。

除此之外,没有别的区别。

切片共用底层数组

在做截取的时候,会创建一个新的slice,截取语法如下

bSlice := aSlice[start:stop:capacityIndex]
// satrt <= stop <= capacityIndex
//capacityIndex不是必须的,默认=原来切片的cap-startIndex
// 如果指定 新切片的容量为 capacityIndex-start

如图所示:

在这里插入图片描述

有了上面的例子,可以看如下代码

package main

import (
	"fmt"
)

func main() {
	slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
	s1 := slice[2:5]  
	fmt.Println("============= 1 ==============")
	fmt.Printf("%v len:%d  cap:%d \n",slice,len(slice),cap(slice))
	fmt.Printf("%v len:%d  cap:%d \n",s1,len(s1),cap(s1))


	s2 := s1[2:6:7]
	fmt.Println("============= 2 ==============")
	fmt.Printf("%v len:%d  cap:%d \n",slice,len(slice),cap(slice))
	fmt.Printf("%v len:%d  cap:%d \n",s1,len(s1),cap(s1))
	fmt.Printf("%v len:%d  cap:%d \n",s2,len(s2),cap(s2))
    
    // 到这里是正确的切片操作,slice,s1,s2通用底层数组

	s2 = append(s2, 100) //s2追加100,此时s2中len=cap,还没有触发扩容操作
	fmt.Println("============= 3 ==============")
	fmt.Printf("%v len:%d  cap:%d \n",slice,len(slice),cap(slice))
	fmt.Printf("%v len:%d  cap:%d \n",s1,len(s1),cap(s1))
	fmt.Printf("%v len:%d  cap:%d \n",s2,len(s2),cap(s2))
	
    
	s2 = append(s2, 200)// 200加不进去了,触发扩容操作,此时s2的底层数组和s1,slice不一样了
	fmt.Println("============= 3 ==============")
	fmt.Printf("%v len:%d  cap:%d \n",slice,len(slice),cap(slice))
	fmt.Printf("%v len:%d  cap:%d \n",s1,len(s1),cap(s1))
	fmt.Printf("%v len:%d  cap:%d \n",s2,len(s2),cap(s2))

	s1[2] = 20 // s1和slice底层数组还是一样的
	fmt.Println("============= 3 ==============")
	fmt.Printf("%v len:%d  cap:%d \n",slice,len(slice),cap(slice))
	fmt.Printf("%v len:%d  cap:%d \n",s1,len(s1),cap(s1))
	fmt.Printf("%v len:%d  cap:%d \n",s2,len(s2),cap(s2))
}
输出如下:
============= 1 ==============
[0 1 2 3 4 5 6 7 8 9] len:10  cap:10 
[2 3 4] len:3  cap:8 
============= 2 ==============
[0 1 2 3 4 5 6 7 8 9] len:10  cap:10 
[2 3 4] len:3  cap:8 
[4 5 6 7] len:4  cap:5 
============= 3 ==============
[0 1 2 3 4 5 6 7 100 9] len:10  cap:10 
[2 3 4] len:3  cap:8 
[4 5 6 7 100] len:5  cap:5 
============= 3 ==============
[0 1 2 3 4 5 6 7 100 9] len:10  cap:10 
[2 3 4] len:3  cap:8 
[4 5 6 7 100 200] len:6  cap:10 
============= 3 ==============
[0 1 2 3 20 5 6 7 100 9] len:10  cap:10 
[2 3 20] len:3  cap:8 
[4 5 6 7 100 200] len:6  cap:10 

源码分析

make创建切片

使用dlv或者go提供的汇编工具可以看到 make调用了什么函数
在这里插入图片描述

在这里插入图片描述

源码:https://github.com/golang/go/blob/master/src/runtime/slice.go#LL88C18-L88C18

在这里插入图片描述

切片的扩容规则

版本不同,扩容规则可能不一样,例子中go版本为:

在这里插入图片描述

代码如下:

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	ints := make([]int, 0) // 创建了一个长度为0的切片
	i := *(*[3]int)(unsafe.Pointer(&ints)) // 利用Pointer将slice转换为长度为3的int数组,此操作可以查看slice结构体中各个字段的数值
	fmt.Printf("slice1:%v \n",i)
	fmt.Printf("slice:%v \n",ints)

	var ints1 = append(ints, 1) // 追加一个元素
	i2 := *(*[3]int)(unsafe.Pointer(&ints1))
	fmt.Printf("slice2 %v \n",i2)   // slice对应的底层数组发生了扩容操作,底层数组已经变了
	fmt.Printf("slice2:%v \n",ints)
}

用dlv 查看它的汇编代码,看扩容操作调用了那些函数
在这里插入图片描述

源码链接:https://github.com/golang/go/blob/master/src/runtime/slice.go#LL157C10-L157C10

// 函数入参说明如下
//1. et 类型
//2. old 老切片
//3. cap 需要分配的指定容量,为了方便期间,调用这个函数的时候cap传递的都是老的slice的cap
func growslice(et *_type, old slice, cap int) slice { 
	if raceenabled { // 是否启动竞争检测
		callerpc := getcallerpc()
		racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, abi.FuncPCABIInternal(growslice))
	}
	if msanenabled { //内存检查,确保没有未初始化的内存被使用
		msanread(old.array, uintptr(old.len*int(et.size)))
	}
	if asanenabled { // 检查内存访问是否越界
		asanread(old.array, uintptr(old.len*int(et.size)))
	}

	if cap < old.cap {
		panic(errorString("growslice: cap out of range"))
	}

	if et.size == 0 {
	    // 正常是不会这样的,但为了安全还是处理了0的情况
		return slice{unsafe.Pointer(&zerobase), old.len, cap}
	}

	// 开始计算新的cap
	newcap := old.cap
	doublecap := newcap + newcap // 2倍
	if cap > doublecap { // 新的cap要是老的2倍
		newcap = cap
	} else {
		const threshold = 256
		if old.cap < threshold { // cap小于256,newCap为oldCap的两倍
			newcap = doublecap
		} else {

			for 0 < newcap && newcap < cap {
				// Transition from growing 2x for small slices
				// to growing 1.25x for large slices. This formula
				// gives a smooth-ish transition between the two.
				// 这个公式可以当超过256之后i,可以实现1.25到2倍的平滑过渡
				newcap += (newcap + 3*threshold) / 4  // 这个公式化简一下 newCap = oldCap*1.25 + 192(3/4*256)
			}
			// Set newcap to the requested cap when
			// the newcap calculation overflowed.
			if newcap <= 0 { // 防止溢出
				newcap = cap
			}
		}
	}
	
    // 下面的逻辑是对上面计算出来的newCap来做对齐操作,上面的计算不是真正的结果,下面还需要做内存对齐操作。
	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 == goarch.PtrSize:
		lenmem = uintptr(old.len) * goarch.PtrSize
		newlenmem = uintptr(cap) * goarch.PtrSize
		capmem = roundupsize(uintptr(newcap) * goarch.PtrSize)
		overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
		newcap = int(capmem / goarch.PtrSize)
	case isPowerOfTwo(et.size):
		var shift uintptr
		if goarch.PtrSize == 8 {
			// Mask shift for better code generation.
			shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
		} else {
			shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
		}
		lenmem = uintptr(old.len) << shift
		newlenmem = uintptr(cap) << shift
		capmem = roundupsize(uintptr(newcap) << shift)
		overflow = uintptr(newcap) > (maxAlloc >> shift)
		newcap = int(capmem >> shift)
	default:
		lenmem = uintptr(old.len) * et.size
		newlenmem = uintptr(cap) * et.size
		capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
		capmem = roundupsize(capmem)
		newcap = int(capmem / et.size)
	}

	// The check of overflow in addition to capmem > maxAlloc is needed
	// to prevent an overflow which can be used to trigger a segfault
	// on 32bit architectures with this example program:
	//
	// type T [1<<27 + 1]int64
	//
	// var d T
	// var s []T
	//
	// func main() {
	//   s = append(s, d, d, d, d)
	//   print(len(s), "\n")
	// }
	if overflow || capmem > maxAlloc {
		panic(errorString("growslice: cap out of range"))
	}

	var p unsafe.Pointer
    
    // 下面是分配新的数组

	if et.ptrdata == 0 { // 原slice底层数组为0,也就是nil切片,
		p = mallocgc(capmem, nil, false)
		// The append() that calls growslice is going to overwrite from old.len to cap (which will be the new length).
		// Only clear the part that will not be overwritten.
		memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem) // 然后使用memclrNoHeapPointers函数来清除新分配的内存
	} else {
		// Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
		p = mallocgc(capmem, et, true) // 分配新的底层数组
		if lenmem > 0 && writeBarrier.enabled { // 之前有数据,并且写屏障已经开启
			// Only shade the pointers in old.array since we know the destination slice p
			// only contains nil pointers because it has been cleared during alloc.
			bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem-et.size+et.ptrdata)
		}
	}
	// copy元素
	memmove(p, old.array, lenmem)
   // 创建新的切片返回
	return slice{p, old.len, newcap}
}

总结如下:

  1. 确定新容量,cap小于256,直接2倍,大于256,新容量=老容量*1.25 * 3/4 * 256
  2. 用新容量来做内存对齐操作
  3. 分配新数组
  4. copy数组
  5. 创建切片返回

还有一点:

append的操作汇编并没有调用函数,在汇编层面就做了,直接往底层数组添加元素,只有数组已经满的情况下才会触发扩容操作

内存对齐相关东西之后在说

我们来一个例子来验证一下上面的代码逻辑:

package main

import "fmt"

func main() {
	var s = []int{}

	oldCap := cap(s)

	for i := 0; i < 2048; i++ {
		s = append(s, i)

		newCap := cap(s)

		if newCap != oldCap {
			// 追加元素,当容量发生变化的时候,打印,扩容之前的元素,cap,导致扩容的元素,和扩容之后的cap
			fmt.Printf("[%d -> %4d] cap = %-4d    after append %-4d  cap = %-4d\n", 0, i-1, oldCap, i, newCap)
			oldCap = newCap
		}
	}
}
// outPut
[0 ->   -1] cap = 0     |  after append 0     cap = 1   
[0 ->    0] cap = 1     |  after append 1     cap = 2   
[0 ->    1] cap = 2     |  after append 2     cap = 4   
[0 ->    3] cap = 4     |  after append 4     cap = 8   
[0 ->    7] cap = 8     |  after append 8     cap = 16  
[0 ->   15] cap = 16    |  after append 16    cap = 32  
[0 ->   31] cap = 32    |  after append 32    cap = 64  
[0 ->   63] cap = 64    |  after append 64    cap = 128 
[0 ->  127] cap = 128   |  after append 128   cap = 256    // 256之前都是2倍
[0 ->  255] cap = 256   |  after append 256   cap = 512    // 1.25 * 256 + 192 = 512
[0 ->  511] cap = 512   |  after append 512   cap = 848    // 1.25 * 512 + 192 = 832
[0 ->  847] cap = 848   |  after append 848   cap = 1280
[0 -> 1279] cap = 1280  |  after append 1280  cap = 1792
[0 -> 1791] cap = 1792  |  after append 1792  cap = 2560

copy函数的使用

copy函数底层调用的是

在这里插入图片描述

底层数组共用,那copy函数就可以完成一下几种操作

  1. 移动slice中的元素

    func main() {
    	var vocabs = []int{1,2,3,4,5,6,7,8,9}
    	// 现在将5去除掉,将5之后的移动到前面 
    	copy(vocabs[4:],vocabs[5:])
    	fmt.Printf("%v",vocabs)
    }
    //outPut
    [1 2 3 4 6 7 8 9 9]
    
  2. slice合并

    func main() {
    	ints := make([]int, 10)
    	i1 := make([]int,0, 5)
    	for i := 0; i < 5; i++ {
    		i1 = append(i1, i)
    	}
    
    	i2 := make([]int,0, 5)
    	for i := 5; i < 10; i++ {
    		i2 = append(i2, i)
    	}
    	
    
    	copy(ints,i1) // 从i1全部复制到ints中
    	copy(ints[len(i1):],i2) // 将i2复制到ints的len(i1)位置开始一直到结束的数组中
    
    	fmt.Printf("%v\n",i1)
    	fmt.Printf("%v\n",i2)
    	fmt.Printf("%v\n",ints)
    }
    // output
    [0 1 2 3 4]
    [5 6 7 8 9]
    [0 1 2 3 4 5 6 7 8 9]
    
  3. 长度不够的copy,依dist为准

    package main
    
    import "fmt"
    
    func main() {
    	ints := make([]int, 3)
    	i1 := make([]int,0, 10)
    	for i := 0; i < 10; i++ {
    		i1 = append(i1, i)
    	}
    	copy(ints,i1)
    	fmt.Printf("%v",ints)
    }
    //outPut:
    [0 1 2]
    

问题解答

  1. nil 切片可以添加元素吗?

    可以

    nil切片就是切片声明,追加的时候切片长度为0,会引发扩容操作,扩容的时候会给分配一个新的数组。

  2. nil切片和空切片有区别吗?

    nil切片有两种方式

    • 声明
    • new创建

    空切片有两种:

    • 字面量创建但没有任何的元素
    • make创建长度指定为0

    使用方式除了下面两点没有别的区别:

    1. 嵌套结构体,不显性创建为nil
    2. json序列化会为null
  3. slice扩容规则

    说到前面:它在确定cap之后有内存对齐操作

    1. 小于256,是原cap的2倍
    2. 大于256,是原来的1.25倍+3/4*256

到这里就结束了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/633872.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

(转载)基于蚁群算法的三维路径规划(matlab实现)

1 理论基础 1.1 三维路径规划问题概述 三维路径规划指在已知三维地图中&#xff0c;规划出一条从出发点到目标点满足某项指标最优&#xff0c;并且避开了所有三维障碍物的三维最优路径。现有的路径规划算法中&#xff0c;大部分算法是在二维规划平面或准二维规划平面中进行路…

微服务框架

流量入口Nginx 在上图中可以看到&#xff0c;Nginx作为整个架构的流量入口&#xff0c;可以理解为一个外部的网关&#xff0c;它承担着请求的路由转发、负载均衡、动静分离等功能。作为一个核心入口点&#xff0c;Nginx肯定要采用多节点部署&#xff0c;同时通过keepalived来实…

(八)CSharp-泛型类和参数约束(1)

一、C# 中的泛型 泛型&#xff08;generic&#xff09;特性可以让多个类型共享一组代码。 泛型类型不是类型&#xff0c;而是类型的模板。 C# 提供了5种类型&#xff1a;类、结构、接口、委托和方法。 泛型类 泛型的主要优点&#xff1a; 性能 类型转换时&#xff0c;非泛型的…

2018~2019 学年第二学期《信息安全》考试试题(B 卷)

北京信息科技大学 2018 ~2019 学年第 2 学期 《信息安全》课程期末考试试卷 B 课程所在学院:计算机学院 适用专业班级:计科 1601-06&#xff0c;重修 考试形式:(闭卷) 一. 选择题(本题满分 10 分&#xff0c;共含 10 道小题&#xff0c;每小题 1 分) 网络中存在的安全漏洞主…

虚拟环境创建、配置及激活

虚拟环境创建、配置及激活 前言 一、虚拟环境是什么&#xff1f; 虚拟环境&#xff08;Virtual Environment&#xff09;是在计算机上使用特定版本的编程语言&#xff08;如python 3.9&#xff09;和其所需包及依赖项的一种方法(如pandas 2.4)&#xff0c;它可以被看作是一个隔…

基于html+css的图展示121

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

chatgpt赋能python:Python中如何快速删除字符串

Python中如何快速删除字符串 在Python编程中&#xff0c;字符串操作是非常常见的。有时候我们需要从字符串中删除一些无用的字符&#xff0c;以便更方便地处理数据。在本文中&#xff0c;将介绍Python如何快速删除字符串。 删除特定字符 Python中可以使用replace()函数快速替…

【深度学习炼丹大杀器——mlrunner初体验(以mmdetection为例)】

深度学习炼丹大杀器——mlrunner初体验&#xff08;以mmdetection为例&#xff09; 自动化炼丹&#xff0c;告别手动运行的烦恼~ 0.引言 了解深度学习的人都知道&#xff0c;炼丹是一种很玄学的事&#xff0c;并且还存在以下问题&#xff1a; 效率&#xff1a;在训练模型时&…

Seata服务端的启动过程 学习记录

1.ServerRunner ServerRunner类实现了CommandLineRunner与DisposableBean接口&#xff0c;将会在Spring容器启动和关闭的时间&#xff0c;分别执行 run 和 destory 方法。 而seata服务端的启动过程&#xff0c;都藏在run方法中 2.整体流程 io.seata.server.Server#start pu…

基于html+css的图展示120

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

【JMeter压力测试】通过jmeter压测surging

目录 前言 环境 下载配置源码 JMeter和JDK下载 JDKJmeter安装 Jmeter非GUI运行压测 结尾 前言 surging是异构微服务引擎&#xff0c;提供了模块化RPC请求通道&#xff0c;引擎在RPC服务治理基础之上还提供了各种协议&#xff0c;并且还提供了stage组件&#xff0c;以便针…

最新版CleanMyMac X4.13.4中文版Mac清理软件

cleanmymac是一款强大的Mac系统垃圾清理工具,可以清除Mac系统多余的语言包,系统缓存,应用程序!可智能清理mac磁盘垃圾和多余语言安装包&#xff0c;快速释放电脑内存&#xff0c;轻松管理和升级Mac上的应用。同时CleanMyMac X可以强力卸载恶意软件&#xff0c;修复系统漏洞&…

EXCEL函数2(统计函数,逻辑函数及其余函数)

统计函数 1、COUNT&#xff08;单元格范围&#xff09;&#xff1a; 计算单元格范围的行数&#xff0c;比如用光标选中一定范围内的单元格&#xff0c;那么只要单元格里面有值&#xff0c;那么count函数便会将有值的单元格的数量统计出来 2、COUNTA&#xff08;单元格范围&am…

msf渗透测试学习-与永恒之蓝漏洞案例

MSF是Metasploit Framework的缩写&#xff0c;是一款广泛使用的渗透测试工具&#xff0c;具有强大的攻击功能。它提供了一个模块化的平台&#xff0c;通过将各种攻击载荷、漏洞利用和辅助工具组装在一起&#xff0c;可用于模拟各种攻击&#xff0c;测试系统安全性&#xff0c;也…

Task Add-in Sample (C#)

下例显示了用 C# 编写Task Add-in 的完整源代码。 使用 C# 类库 &#xff08;.NET Framework&#xff09; 创建 Visual Studio 中的项目。实现 IEdmAddIn5。在“任务属性”对话框中创建自定义页。自定义任务详细信息页面。 注意&#xff1a; 若要填充下面的 GUID 属性&#x…

【linux】登录root账户时报错Sorry, that didn‘t work. Please try again.抱歉,这不管用,请再试一次

一、问题背景 登录其他普通账户的GUI桌面&#xff0c;发现都很正常&#xff0c;但是登录管理员账户root的桌面&#xff0c;重启之后一段时间正常&#xff0c;过一段时间就会出现登录报错Sorry, that didn’t work. Please try again. 二、解决办法——配置文件的解析 下面给出…

由于找不到msvcp120.dll丢失的解决方法,计算机丢失msvcp120.dll修复教程

在打开游戏或者软件的时候&#xff0c;计算机提示由于找不到msvcp120.dll&#xff0c;无法继续执行此代码怎么办呢&#xff1f;msvcp120.dll是一个动态链接库&#xff08;DLL&#xff09;文件&#xff0c;其作用是提供一些常用的C函数和类库&#xff0c;以便在Windows操作系统上…

高手都是如何做 Mysql 慢 SQL 优化

tip&#xff1a;作为程序员一定学习编程之道&#xff0c;一定要对代码的编写有追求&#xff0c;不能实现就完事了。我们应该让自己写的代码更加优雅&#xff0c;即使这会费时费力。 &#x1f495;&#x1f495; 推荐&#xff1a;体系化学习Java&#xff08;Java面试专题&#…

2017~2018学年《信息安全》考试试题(A2卷)

北京信息科技大学&#xff0c;2017~2018 学年第二学期《信息安全》考试试题&#xff08;A 卷&#xff09; 适用专业班级&#xff1a;计科15级 重修课程所在学院&#xff1a;计算机学院 考试形式&#xff1a;闭卷 一、单选题&#xff08;本题满分20分&#xff0c;共含10 道小题…

《Java从入门到精通》学习笔记

Java从入门到精通学习笔记 第一章 初识java a) Java是一种通过解释方式来执行的语言。 b) Java语言编写的程序既是编译型&#xff0c;又是解释型的。编译只进行一次&#xff0c;而解释在每次运行程序时都会进行。 c) JDK下载安装 i. path:jdk/bin ii. calsspath:jdk/jre/lib ii…