Maison  >  Article  >  développement back-end  >  Aller à la pile de mécanismes de langage et aux pointeurs

Aller à la pile de mécanismes de langage et aux pointeurs

Go语言进阶学习
Go语言进阶学习avant
2023-07-24 16:10:54748parcourir

Cette série contient quatre articles, expliquant principalement les mécanismes et les concepts de conception derrière les pointeurs, les piles, les tas du langage Go, l'analyse d'échappement et la sémantique valeur/pointeur. Il s'agit du premier article de la série, expliquant principalement les piles et les pointeurs.

Introduction

Je ne vais pas dire de bonnes choses sur les pointeurs, c'est vraiment difficile à comprendre. S'il est mal utilisé, cela peut entraîner des bugs gênants et même des problèmes de performances. Cela est particulièrement vrai lors de l'écriture de logiciels simultanés ou multithread. Il n'est pas étonnant que de nombreux langages de programmation essaient d'éviter d'utiliser des pointeurs pour les programmeurs. Cependant, si vous utilisez le langage de programmation Go, les pointeurs sont inévitables. Ce n'est qu'en comprenant profondément les pointeurs que vous pourrez écrire du code propre, concis et efficace.

Bordure du cadre

La limite du cadre fournit un espace mémoire séparé pour chaque fonction, et la fonction est exécutée dans la limite du cadre. Les limites de cadre permettent aux fonctions de s'exécuter dans leur propre contexte et fournissent également un contrôle de flux. Les fonctions peuvent accéder directement à la mémoire dans le cadre via le pointeur de cadre, tandis que l'accès à la mémoire en dehors du cadre ne peut se faire qu'indirectement. Pour chaque fonction, si l'on souhaite pouvoir accéder à la mémoire hors du cadre, cette mémoire doit être partagée avec la fonction. Pour comprendre la mise en œuvre partagée, nous devons d'abord apprendre et comprendre les mécanismes et les contraintes permettant d'établir les limites du cadre.

Lorsqu'une fonction est appelée, un changement de contexte se produit entre deux limites de frame. De la fonction appelante à la fonction appelée, si des paramètres doivent être transmis lorsque la fonction est appelée, ces paramètres doivent également être transmis dans les limites du cadre de la fonction appelée. Dans le langage Go, les données sont transférées entre deux images par valeur.

L'avantage de transmettre les données par valeur est une bonne lisibilité. Lorsqu'une fonction est appelée, la valeur que vous voyez est la valeur qui a été copiée et reçue entre l'appelant de la fonction et l'appelé. C'est pourquoi j'associe le « passage par valeur » au WYSIWYG, car ce que vous voyez est ce que vous obtenez.

Regardons un morceau de code qui transmet des données entières par valeur :

Listing 1

package main


func main() {


   // Declare variable of type int with a value of 10.
   count := 10


   // Display the "value of" and "address of" count.
   println("count:\tValue Of[", count, "]\tAddr Of[", &count, "]")


   // Pass the "value of" the count.
   increment(count)


   println("count:\tValue Of[", count, "]\tAddr Of[", &count, "]")
}


//go:noinline
func increment(inc int) {


   // Increment the "value of" inc.
   inc++
   println("inc:\tValue Of[", inc, "]\tAddr Of[", &inc, "]")
}

Lorsque vous démarrez un programme Go, le runtime créera une coroutine principale pour exécuter tout le code d'initialisation, y compris le code de la fonction main(). Goroutine est un chemin d'exécution placé sur le thread du système d'exploitation et est finalement exécuté sur un certain noyau. À partir de Go 1.8, chaque goroutine allouera un bloc de mémoire contigu de 2 048 octets comme espace de pile. La taille de l'espace de pile initial a changé au fil des années et pourrait encore changer à l'avenir.

La pile est très importante car elle fournit l'espace mémoire physique pour les limites de trame de chaque fonction individuelle. Selon le listing 1, lorsque la coroutine principale exécute la fonction main(), l'espace de la pile est distribué comme suit :

Aller à la pile de mécanismes de langage et aux pointeurs

Figure 1

Vous pouvez voir sur la figure 1 cette partie de la pile de la fonction principale a été encadré est sorti. Cette partie est appelée « cadre de pile » et ce cadre représente la limite de la fonction principale sur la pile. Le cadre est créé lorsque la fonction appelée est exécutée. Vous pouvez également voir que le nombre de variables est placé dans le cadre de la fonction main() à l'adresse mémoire de 0x10429fa4.

La figure 1 illustre également un autre point intéressant : toute la mémoire de pile située en dessous du cadre actif est indisponible. Seule la mémoire de pile située au-dessus du cadre actif est disponible. La frontière entre l’espace de pile disponible et l’espace de pile indisponible doit être clarifiée.

Adresse

Le but d'une variable est d'attribuer un nom à une adresse mémoire spécifique, rendant le code plus lisible et vous aidant à analyser les données que vous traitez. Si vous avez une variable et que vous pouvez stocker sa valeur en mémoire, il doit y avoir une adresse dans l'adresse mémoire qui stocke cette valeur. Dans la ligne 9 du code, la fonction main() appelle la fonction intégrée println() pour afficher la valeur et l'adresse du nombre de variables.

Listing 2

println("count:\tValue Of[", count, "]\tAddr Of[", &count, "]")

Il n'est pas surprenant d'utiliser l'opérateur & pour obtenir l'adresse de l'emplacement mémoire où se trouve une variable, d'autres langages utilisent également cet opérateur. Si votre code s'exécute sur un ordinateur 32 bits, tel que Go Playground, le résultat sera similaire à ce qui suit :

清单3

count:  Value Of[ 10 ]  Addr Of[ 0x10429fa4 ]

函数调用

接下来的第 12 行代码,main() 函数调用 increment() 函数。

清单4

increment(count)

调用函数意味着协程需要在栈上构建出一块新的栈帧。但是,事情有点复杂。要想成功地调用函数,在发生上下文切换时,数据需要跨越帧边界传递到新的帧范围内。具体一点来说,函数调用的时候,整型值会被复制和传递。通过第 18 行代码、increment() 函数的声明,你就可以知道。

清单5

func increment(inc int) {

如果你回过头来再次看第 12 行代码函数 increment() 的调用,你会发现 count 变量是传值的。这个值会被拷贝、传递,最后存储在 increment() 函数的栈中。记住,increment() 函数只能在自己的栈内读写内存,因此,它需要 inc 变量来接收、存储和访问传递的 count 变量的副本。

就在 increment() 函数内部代码开始执行之前,协程的栈(站在一个非常高的角度)应该是像下图这样的:

Aller à la pile de mécanismes de langage et aux pointeurs

图 2 

你可以看到栈上现在有两个帧,一个属于 main() 函数,另一个属于 increment() 函数。在 increment() 函数的帧里面,你可以看到 inc 变量,它的值 10,是函数调用时拷贝、传递进来的。变量 inc 的地址是 0x10429f98,因为栈帧是从上至下使用栈空间的,所以它的内存地址较小,这只是具体的实现细节,并没任何意义。重要的是,协程从 main() 的栈帧里获取变量 count 的值,并使用 inc 变量将该值的副本放置在 increment() 函数的栈帧里。

increment() 函数的剩余代码显示 inc 变量的值和地址。

清单6

inc++
println("inc:\tValue Of[", inc, "]\tAddr Of[", &inc, "]")

第 22 行代码输出类似下面这样:

清单7

inc:    Value Of[ 11 ]  Addr Of[ 0x10429f98 ]

执行这些代码之后,栈就会像下面这样:

Aller à la pile de mécanismes de langage et aux pointeurs

图 3

第 21、22 行代码执行之后,increment() 函数返回并且 CPU 控制权交还给 main() 函数。第 14 行代码,main() 函数会再次显示 count 变量的值和地址。

清单8

println("count:\tValue Of[",count, "]\tAddr Of[", &count, "]")

上面例子完整的输出会像下面这样:

count:  Value Of[ 10 ]  Addr Of[ 0x10429fa4 ]
inc:    Value Of[ 11 ]  Addr Of[ 0x10429f98 ]
count:  Value Of[ 10 ]  Addr Of[ 0x10429fa4 ]

main() 函数栈帧里,变量 count 的值在调用 increment() 函数前后是相同的。

函数返回

当函数返回并且控制权交还给调用函数时,栈上的内存实际上会发生什么?回答是:不会发生任何事情。当 increment() 函数返回时,栈上的空间看起来像下面这样:

Aller à la pile de mécanismes de langage et aux pointeurs

Figure 4

Sauf que le cadre de pile créé pour la fonction incrément() devient indisponible, la distribution de la pile est fondamentalement la même que celle de la figure 3. En effet, le cadre de la fonction main() devient le cadre actif. Aucun traitement n'est effectué sur le frame de pile de la fonction incrément().

Lorsque la fonction reviendra, nettoyer le cadre de la fonction vous fera perdre du temps, car vous ne savez pas si cette mémoire sera à nouveau utilisée. Cette mémoire ne fera donc aucun traitement. Chaque fois qu'une fonction est appelée, les frames allouées sur la pile seront effacées lorsqu'une frame est nécessaire. Cela se fait lors de l'initialisation des variables stockées dans le cadre. Étant donné que toutes les valeurs sont initialisées à leurs valeurs zéro correspondantes, la pile se nettoie correctement à chaque fois que la fonction est appelée.

Valeur partagée

Et s'il est très important que la fonction incrément() exploite directement la variable count stockée dans le cadre de la fonction main() ? Cela nécessite l'utilisation de pointeurs ! Le but des pointeurs est de partager des valeurs entre fonctions Même si la valeur n'est pas dans le cadre de sa propre fonction, la fonction peut la lire et l'écrire.

Si vous n’avez pas le concept de partage en tête, vous n’utiliserez probablement pas de pointeurs. Lors de l’apprentissage des pointeurs, il est important d’utiliser un vocabulaire clair plutôt que de simplement mémoriser des opérateurs ou une syntaxe. Alors, rappelez-vous que les pointeurs sont destinés à être partagés et lorsque vous lisez du code, lorsque vous pensez au « partage », vous devez penser à l'opérateur &.

Type de pointeur

Qu'il soit personnalisé par vos soins ou livré avec le langage Go, pour chaque type déclaré, le type de pointeur correspondant peut être obtenu en fonction de ces types pour le partage. Par exemple, le type intégré int, le type de pointeur correspondant est *int. Si vous déclarez vous-même le type User, le type de pointeur correspondant est *User.

Tous les types de pointeurs ont les mêmes caractéristiques. Premièrement, ils commencent par le symbole * ; deuxièmement, ils occupent le même espace mémoire et représentent tous deux une adresse, utilisant 4 ou 8 octets de longueur pour représenter une adresse. Sur une machine 32 bits (comme une aire de jeux), un pointeur nécessite 4 octets de mémoire ; sur une machine 64 bits (comme votre ordinateur), il nécessite 8 octets de mémoire.

规范里有说明,指针类型可以看成是类型字面量,这意味着它们是有现有类型组成的未命名类型。

间接访问内存

让我们来看一段代码,这段代码展示了函数调用时按值传递地址。main() 和 increment() 函数的栈帧会共享 count 变量:

清单10

package main


func main() {


   // Declare variable of type int with a value of 10.
   count := 10


   // Display the "value of" and "address of" count.
   println("count:\tValue Of[", count, "]\t\tAddr Of[", &count, "]")


   // Pass the "address of" count.
   increment(&count)


   println("count:\tValue Of[", count, "]\t\tAddr Of[", &count, "]")
}


//go:noinline
func increment(inc *int) {


   // Increment the "value of" count that the "pointer points to". (dereferencing)
   *inc++
   println("inc:\tValue Of[", inc, "]\tAddr Of[", &inc, "]\tValue Points To[", *inc, "]")
}

基于原来的代码有三处改动的地方,第 12 行是第一处改动:

清单11

increment(&count)

现在,第 12 行代码拷贝、传递的并非 count 变量的值,而是变量的地址。可以认为,main() 函数与 increment() 函数是共享 count 变量的。这是 & 操作符起的作用。

重点理解,现在依旧是传值,唯一不同的是现在传递的是地址而不是一个整型数据。地址也是一个值,是函数调用时会跨帧边界发生拷贝和传递的内容。

因为地址会发生拷贝和传递,在 increment() 函数里面需要一个变量接收和存储该地址值。所以在第 18 行声明了整型的指针变量。

清单12

func increment(inc *int) {

如果你传递的是 User 类型值的地址,变量就应该声明成 *User。尽管指针变量存储的是地址,也不能传递任何类型的地址,只能传递与指针类型相一致的地址。关键在于,共享值的原因是因为接收函数能够对值进行读写操作。只有知道值的类型信息才能够进行读写操作。编译器会保证只有与指针类型相一致的值才能够实现函数间共享。

调用 increment() 函数时候,栈空间就像下面这样:

Aller à la pile de mécanismes de langage et aux pointeurs

图 5

当一个地址作为值执行按值传递之后,你可以从图 5 看出栈是如何分布的。现在,increment() 函数帧空间里面的指针变量指向 count 变量,该变量在 main() 函数的帧空间里。

通过使用指针变量,increment() 函数可以间接对 count 变量执行读写操作。

清单 13

*inc++

这一次,字符 * 充当操作符,与指针变量搭配使用。使用 * 操作符是“获取指针指向的值”的意思。指针变量允许在帧外对函数帧内的内存进行间接访问。有时候,间接的读写操作也称为解引用。increment() 函数必须有指针变量,才能够对其他函数帧空间执行间接访问。

执行第 21 行代码之后,栈空间分布如图 6 所示。

Aller à la pile de mécanismes de langage et aux pointeurs

图 6 

程序最后输出:

清单 14

count:  Value Of[ 10 ]              Addr Of[ 0x10429fa4 ]
inc:    Value Of[ 0x10429fa4 ]      Addr Of[ 0x10429f98 ]   Value Points To[ 11 ]
count:  Value Of[ 11 ]              Addr Of[ 0x10429fa4 ]

你可以看到,指针变量 inc 的值和 count 变量的地址是相同的。这将建立起共享关系,允许在帧外执行内存的间接访问。在 increment() 函数里,一旦通过指针执行了写操作,改变也会体现在 main() 函数里。

指针变量并不特别

指针变量并不特别,它们和其他变量一样也是变量,有内存地址和值。正巧的是,无论指针变量指向的值的类型如何,所有的指针变量都有同样的大小和表现形式。唯一困惑的是使用 * 字符充当操作符,用来声明指针类型。如果你能分清指针类型声明和指针操作,你就没有那么困惑了。

总结

这篇文章描述了设计指针背后的目的和 Go 语言中栈和指针的工作机制。这是理解 Go 语言机制、设计哲学的第一步,也对编写一致性且可读性的代码提供一些指导作用。

总结一下,通过这篇文章你能学习到的知识:

1.La limite du cadre fournit un espace mémoire séparé pour chaque fonction, et la fonction est exécutée dans la plage du cadre 2.Lorsque la fonction est appelée, le contexte bascule entre les deux cadres ; 3.L'avantage du passage par valeur est une bonne lisibilité ; 4.La pile est importante car elle fournit un espace mémoire physique accessible pour la limite de trame de chaque fonction ; 5. le cadre actif n'est pas disponible, seuls le cadre actif et la mémoire de pile au-dessus sont utiles 6.Appeler la fonction signifie que la coroutine ouvrira un nouveau cadre de pile sur la mémoire de pile 7.À chaque fois ; une fonction est appelée, lorsqu'un cadre est utilisé, la mémoire de pile correspondante sera initialisée 8.Le but de la conception des pointeurs est de réaliser le partage de valeur entre les fonctions, même si la valeur n'est pas dans la fonction elle-même ; stack frame, il peut également être lu et écrit ; 9.Pour chaque type, qu'il soit auto-défini ou intégré au langage Go, il existe un type de pointeur correspondant 10.Passed ; L'utilisation de variables de pointeur permet un accès indirect à la mémoire en dehors du cadre de la fonction 11.Par rapport à d'autres variables, les variables de pointeur n'ont rien de spécial car ce sont aussi des variables, avec des adresses et des valeurs mémoire.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer