Golang中奇怪的指针行为解析

Golang中奇怪的指针行为解析 你好,我在使用指针时遇到了奇怪的行为。有人能解释一下这是如何运作的吗?

package main

import "fmt"

func addAndCheck(candidates []int, candidateIndex int, target int, currentTest []int, l int, finalSolution *[][]int) {
	currentTest = append(currentTest, candidates[candidateIndex])
	if target == 0 {
		fmt.Println("currentTest --> ", currentTest)
		*finalSolution = append(*finalSolution, currentTest)
		fmt.Println("current finalResult --> ", *finalSolution)
		return
	}
	if target < 0 {
		return
	}
	for i := candidateIndex; i < l; i++ {
		//fmt.Println(target, candidates[i], i, target-candidates[i], currentTest )
		addAndCheck(candidates, i, target-candidates[i], currentTest, l, finalSolution)
	}
}

func combinationSum(candidates []int, target int) [][]int {
	solution := make([][]int, 0)

	for i, k := range candidates {
		currentTest := make([]int, 0)
		addAndCheck(candidates, i, target-k, currentTest, len(candidates), &solution)
	}
	return solution
}

func main() {
	a := combinationSum([]int{2, 3, 5}, 8)
	fmt.Println("finalResult --> ", a)
}

如你从代码中所见,我将对我有效的结果追加到 finalResult 数组中。奇怪的事情发生在数组的第一个被推入的元素上。以下是我在程序执行期间打印 currentTestfinalResult 时收到的输出。

currentTest → [2 2 2 2] current finalResult → [[2 2 2 2]] currentTest → [2 3 3] current finalResult → [[2 2 2 5] [2 3 3]] currentTest → [3 5] current finalResult → [[2 2 2 5] [2 3 3] [3 5]] finalResult → [[2 2 2 5] [2 3 3] [3 5]]

第一个被推入的切片 [2 2 2 2] 怎么会变成 [2 2 2 5] 呢?

提前感谢你的帮助。


更多关于Golang中奇怪的指针行为解析的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

感谢您的详细解答

更多关于Golang中奇怪的指针行为解析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


当你对 currentTest 进行追加操作时,并不能保证每次都会获得新的内存。在达到很高的容量之前,append 的当前实现会加倍容量,所以你从容量 0 开始,第一次追加得到 1,然后是 2,接着是 4。

你的算法深入 addAndCheck 函数 4 层,但在第 3 层之后,currentTest 的容量是 4,而不是 3。在第 4 层调用 addAndCheck 的第一次迭代中,产生了 []int{2, 2, 2, 2},将该切片追加到 finalResult,然后返回。现在第 3 层的 addAndCheck 继续其循环,并通过调用另一层 addAndCheck 来测试 5,该调用将 5 追加到 currentTest[:3](这与追加到 finalResult 的实际切片是同一个,其中包含 []int{2, 2, 2, 2}),因此你得到了 []int{2, 2, 2, 5}

这里有一个使用 printTest 函数的相同示例,它展示了切片内存和递归的情况:https://play.golang.org/p/_dZAN1u7-4Z

一个修复方法是使用三索引切片来对 currentTest 进行切片,这样在第 8 行追加时总是会创建一个新的切片:

currentTest = append(currentTest[:len(currentTest):len(currentTest)], candidates[candidateIndex])

https://play.golang.org/p/pex2uKYuKAO

这个问题涉及Go语言中切片底层数组的共享机制。当您使用append修改切片时,如果底层数组容量不足,会创建新的数组,否则会复用原有数组。在您的代码中,currentTest切片在递归过程中被多个结果共享并修改。

关键问题出现在这一行:

*finalSolution = append(*finalSolution, currentTest)

这里存储的是currentTest切片的引用(底层数组指针),而不是数据的副本。当后续递归调用修改currentTest时,之前存储的结果也会被修改。

以下是具体解释和修复方案:

问题分析:

  1. 第一次找到[2 2 2 2]时,将其追加到finalSolution
  2. 后续递归继续使用同一个currentTest切片
  3. 当找到[2 3 3]时,append操作可能修改了底层数组
  4. 由于[2 2 2 2]和后续结果共享底层数组,导致第一个结果被意外修改

解决方案: 创建切片的副本后再存储:

func addAndCheck(candidates []int, candidateIndex int, target int, currentTest []int, l int, finalSolution *[][]int) {
	currentTest = append(currentTest, candidates[candidateIndex])
	if target == 0 {
		// 创建切片的副本
		result := make([]int, len(currentTest))
		copy(result, currentTest)
		*finalSolution = append(*finalSolution, result)
		return
	}
	if target < 0 {
		return
	}
	for i := candidateIndex; i < l; i++ {
		addAndCheck(candidates, i, target-candidates[i], currentTest, l, finalSolution)
	}
}

或者使用更简洁的append方式创建副本:

func addAndCheck(candidates []int, candidateIndex int, target int, currentTest []int, l int, finalSolution *[][]int) {
	currentTest = append(currentTest, candidates[candidateIndex])
	if target == 0 {
		// 使用append创建新切片
		*finalSolution = append(*finalSolution, append([]int{}, currentTest...))
		return
	}
	if target < 0 {
		return
	}
	for i := candidateIndex; i < l; i++ {
		addAndCheck(candidates, i, target-candidates[i], currentTest, l, finalSolution)
	}
}

修改后的输出将是:

finalResult →  [[2 2 2 2] [2 3 3] [3 5]]

这个问题的核心是理解Go语言切片的三要素:指针、长度和容量。当多个切片共享同一个底层数组时,对其中一个切片的修改可能会影响其他切片,特别是在使用append且容量足够时不会重新分配数组的情况下。

回到顶部