Les pointeurs sont un concept qui, pour beaucoup, peut être déroutant au début, en particulier lorsqu'il s'agit de copier des valeurs de pointeur autour et de référencer toujours le même bloc de mémoire.
J'ai trouvé que la meilleure analogie est de considérer le pointeur comme un morceau de papier avec une adresse de maison et le bloc de mémoire auquel il fait référence comme la maison réelle. Toutes sortes d'opérations peuvent ainsi être facilement expliquées.
J'ai ajouté du code Delphi ci-dessous et quelques commentaires le cas échéant. J'ai choisi Delphi car mon autre langage de programmation principal, C #, ne présente pas des choses comme les fuites de mémoire de la même manière.
Si vous ne souhaitez apprendre que le concept de haut niveau des pointeurs, vous devez ignorer les parties étiquetées "Disposition de la mémoire" dans l'explication ci-dessous. Ils sont destinés à donner des exemples de ce à quoi pourrait ressembler la mémoire après les opérations, mais ils sont de nature plus bas niveau. Cependant, afin d'expliquer avec précision le fonctionnement réel des dépassements de mémoire tampon, il était important que j'ajoute ces diagrammes.
Avis de non-responsabilité: à toutes fins utiles, cette explication et les exemples de dispositions de mémoire sont considérablement simplifiés. Il y a plus de frais généraux et beaucoup plus de détails que vous devez savoir si vous devez gérer la mémoire à un niveau bas. Cependant, pour expliquer la mémoire et les pointeurs, il est suffisamment précis.
Supposons que la classe THouse utilisée ci-dessous ressemble à ceci:
type
THouse = class
private
FName : array[0..9] of Char;
public
constructor Create(name: PChar);
end;
Lorsque vous initialisez l'objet maison, le nom donné au constructeur est copié dans le champ privé FName. Il y a une raison pour laquelle il est défini comme un tableau de taille fixe.
En mémoire, il y aura des frais généraux associés à l'allocation de maison, je vais l'illustrer ci-dessous comme ceci:
--- [ttttNNNNNNNNNN] ---
^ ^
| |
| + - le tableau FName
|
+ - frais généraux
La zone "tttt" est en surcharge, il y en aura généralement plus pour différents types de runtimes et de langues, comme 8 ou 12 octets. Il est impératif que les valeurs stockées dans cette zone ne soient jamais modifiées par autre chose que l'allocateur de mémoire ou les routines du système principal, sinon vous risquez de planter le programme.
Allouer de la mémoire
Demandez à un entrepreneur de construire votre maison et donnez-vous l'adresse de la maison. Contrairement au monde réel, l'allocation de mémoire ne peut pas être indiquée où allouer, mais trouvera un endroit approprié avec suffisamment de place et rendra l'adresse à la mémoire allouée.
En d'autres termes, l'entrepreneur choisira l'endroit.
THouse.Create('My house');
Disposition de la mémoire:
--- [ttttNNNNNNNNNN] ---
1234Ma maison
Gardez une variable avec l'adresse
Écrivez l'adresse de votre nouvelle maison sur un morceau de papier. Ce document vous servira de référence pour votre maison. Sans ce morceau de papier, vous êtes perdu et ne pouvez pas trouver la maison, sauf si vous y êtes déjà.
var
h: THouse;
begin
h := THouse.Create('My house');
...
Disposition de la mémoire:
h
v
--- [ttttNNNNNNNNNN] ---
1234Ma maison
Copier la valeur du pointeur
Écrivez simplement l'adresse sur une nouvelle feuille de papier. Vous avez maintenant deux morceaux de papier qui vous mèneront à la même maison, pas deux maisons séparées. Toute tentative de suivre l'adresse d'un papier et de réorganiser les meubles de cette maison donnera l'impression que l'autre maison a été modifiée de la même manière, sauf si vous pouvez détecter explicitement qu'il s'agit en fait d'une seule maison.
Remarque C'est généralement le concept que j'ai le plus de mal à expliquer aux gens, deux pointeurs ne signifie pas deux objets ou blocs de mémoire.
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := h1; // copies the address, not the house
...
h1
v
--- [ttttNNNNNNNNNN] ---
1234Ma maison
^
H2
Libérer la mémoire
Démolissez la maison. Vous pouvez ensuite plus tard réutiliser le papier pour une nouvelle adresse si vous le souhaitez, ou l'effacer pour oublier l'adresse de la maison qui n'existe plus.
var
h: THouse;
begin
h := THouse.Create('My house');
...
h.Free;
h := nil;
Ici, je construis d'abord la maison et saisis son adresse. Ensuite, je fais quelque chose à la maison (utilisez-le, le ... code, laissé comme exercice pour le lecteur), puis je le libère. Enfin, j'efface l'adresse de ma variable.
Disposition de la mémoire:
h <- +
v + - avant gratuit
--- [ttttNNNNNNNNNN] --- |
1234Ma maison <- +
h (pointe maintenant nulle part) <- +
+ - après gratuit
---------------------- | (notez que la mémoire peut encore
xx34Ma maison <- + contient des données)
Pointeurs pendants
Vous dites à votre entrepreneur de détruire la maison, mais vous oubliez d'effacer l'adresse de votre feuille de papier. Quand plus tard vous regardez le morceau de papier, vous avez oublié que la maison n'est plus là et allez la visiter, avec des résultats infructueux (voir aussi la partie sur une référence invalide ci-dessous).
var
h: THouse;
begin
h := THouse.Create('My house');
...
h.Free;
... // forgot to clear h here
h.OpenFrontDoor; // will most likely fail
L'utilisation h
après l'appel à .Free
pourrait fonctionner, mais ce n'est que pure chance. Très probablement, il échouera, chez un client, au milieu d'une opération critique.
h <- +
v + - avant gratuit
--- [ttttNNNNNNNNNN] --- |
1234Ma maison <- +
h <- +
v + - après libre
---------------------- |
xx34Ma maison <- +
Comme vous pouvez le voir, h pointe toujours vers les restes des données en mémoire, mais comme il n'est peut-être pas complet, l'utiliser comme avant peut échouer.
Fuite de mémoire
Vous perdez le morceau de papier et ne pouvez pas trouver la maison. Cependant, la maison est toujours debout quelque part, et lorsque vous souhaitez plus tard construire une nouvelle maison, vous ne pouvez pas réutiliser cet endroit.
var
h: THouse;
begin
h := THouse.Create('My house');
h := THouse.Create('My house'); // uh-oh, what happened to our first house?
...
h.Free;
h := nil;
Ici, nous avons remplacé le contenu de la h
variable par l'adresse d'une nouvelle maison, mais l'ancienne est toujours debout ... quelque part. Après ce code, il n'y a aucun moyen d'atteindre cette maison et elle restera debout. En d'autres termes, la mémoire allouée restera allouée jusqu'à la fermeture de l'application, point auquel le système d'exploitation la supprimera.
Disposition de la mémoire après la première allocation:
h
v
--- [ttttNNNNNNNNNN] ---
1234Ma maison
Disposition de la mémoire après la deuxième allocation:
h
v
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
1234Ma maison 5678Ma maison
Une façon plus courante d'obtenir cette méthode consiste simplement à oublier de libérer quelque chose, au lieu de l'écraser comme ci-dessus. En termes Delphi, cela se produira avec la méthode suivante:
procedure OpenTheFrontDoorOfANewHouse;
var
h: THouse;
begin
h := THouse.Create('My house');
h.OpenFrontDoor;
// uh-oh, no .Free here, where does the address go?
end;
Une fois cette méthode exécutée, il n'y a pas de place dans nos variables où l'adresse de la maison existe, mais la maison est toujours là.
Disposition de la mémoire:
h <- +
v + - avant de perdre le pointeur
--- [ttttNNNNNNNNNN] --- |
1234Ma maison <- +
h (pointe maintenant nulle part) <- +
+ - après avoir perdu le pointeur
--- [ttttNNNNNNNNNN] --- |
1234Ma maison <- +
Comme vous pouvez le voir, les anciennes données sont laissées intactes en mémoire et ne seront pas réutilisées par l'allocateur de mémoire. L'allocateur garde une trace des zones de mémoire qui ont été utilisées et ne les réutilisera que si vous le libérez.
Libérer la mémoire mais conserver une référence (désormais invalide)
Démolissez la maison, effacez l'un des morceaux de papier mais vous avez également un autre morceau de papier avec l'ancienne adresse dessus, lorsque vous allez à l'adresse, vous ne trouverez pas de maison, mais vous pourriez trouver quelque chose qui ressemble aux ruines d'un.
Peut-être trouverez-vous même une maison, mais ce n'est pas la maison à laquelle vous avez initialement reçu l'adresse, et donc toute tentative de l'utiliser comme si elle vous appartenait pourrait échouer horriblement.
Parfois, vous pourriez même trouver qu'une adresse voisine a une maison assez grande qui occupe trois adresses (rue principale 1-3), et votre adresse va au milieu de la maison. Toute tentative de traiter cette partie de la grande maison à 3 adresses comme une petite maison unique pourrait également échouer horriblement.
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := h1; // copies the address, not the house
...
h1.Free;
h1 := nil;
h2.OpenFrontDoor; // uh-oh, what happened to our house?
Ici, la maison a été démolie, grâce à la référence h1
, et bien qu'elle ait h1
été effacée également, h2
elle a toujours l'ancienne adresse obsolète. L'accès à la maison qui n'est plus debout peut ou non fonctionner.
Il s'agit d'une variante du pointeur suspendu ci-dessus. Voir son agencement de mémoire.
Dépassement de tampon
Vous déplacez plus de choses dans la maison que vous n'en avez peut-être, en déversant dans la maison ou la cour voisine. Lorsque le propriétaire de la maison voisine rentrera plus tard à la maison, il trouvera toutes sortes de choses qu'il considérera comme les siennes.
C'est la raison pour laquelle j'ai choisi un tableau de taille fixe. Pour préparer le terrain, supposons que la deuxième maison que nous allouons sera, pour une raison quelconque, placée avant la première en mémoire. En d'autres termes, la deuxième maison aura une adresse inférieure à la première. De plus, ils sont attribués côte à côte.
Ainsi, ce code:
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := THouse.Create('My other house somewhere');
^-----------------------^
longer than 10 characters
0123456789 <-- 10 characters
Disposition de la mémoire après la première allocation:
h1
v
----------------------- [ttttNNNNNNNNNN]
5678Ma maison
Disposition de la mémoire après la deuxième allocation:
h2 h1
vv
--- [ttttNNNNNNNNNN] ---- [ttttNNNNNNNNNN]
1234Mon autre maison quelque part
^ --- + - ^
|
+ - écrasé
La partie qui provoquera le plus souvent un crash est lorsque vous écrasez des parties importantes des données que vous avez stockées qui ne devraient vraiment pas être modifiées au hasard. Par exemple, cela pourrait ne pas être un problème si des parties du nom de la maison h1 ont été modifiées, en termes de plantage du programme, mais l'écrasement de la surcharge de l'objet se bloquera très probablement lorsque vous essayez d'utiliser l'objet cassé, comme le fera écraser les liens qui sont stockés vers d'autres objets dans l'objet.
Listes liées
Lorsque vous suivez une adresse sur un morceau de papier, vous arrivez dans une maison, et dans cette maison, il y a un autre morceau de papier avec une nouvelle adresse, pour la prochaine maison de la chaîne, etc.
var
h1, h2: THouse;
begin
h1 := THouse.Create('Home');
h2 := THouse.Create('Cabin');
h1.NextHouse := h2;
Ici, nous créons un lien entre notre maison et notre cabine. Nous pouvons suivre la chaîne jusqu'à ce qu'une maison n'ait aucune NextHouse
référence, ce qui signifie que c'est la dernière. Pour visiter toutes nos maisons, nous pourrions utiliser le code suivant:
var
h1, h2: THouse;
h: THouse;
begin
h1 := THouse.Create('Home');
h2 := THouse.Create('Cabin');
h1.NextHouse := h2;
...
h := h1;
while h <> nil do
begin
h.LockAllDoors;
h.CloseAllWindows;
h := h.NextHouse;
end;
Disposition de la mémoire (ajouté NextHouse en tant que lien dans l'objet, noté avec les quatre LLLL dans le diagramme ci-dessous):
h1 h2
vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
1234Home + 5678Cabin +
| ^ |
+ -------- + * (pas de lien)
En termes de base, qu'est-ce qu'une adresse mémoire?
Une adresse mémoire n'est en termes de base qu'un nombre. Si vous considérez la mémoire comme un grand tableau d'octets, le tout premier octet a l'adresse 0, le suivant l'adresse 1 et ainsi de suite. C'est simplifié, mais assez bon.
Donc, cette disposition de la mémoire:
h1 h2
vv
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
1234Ma maison 5678Ma maison
Pourrait avoir ces deux adresses (la plus à gauche - est l'adresse 0):
Ce qui signifie que notre liste de liens ci-dessus pourrait ressembler à ceci:
h1 (= 4) h2 (= 28)
vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
1234Home 0028 5678Cabine 0000
| ^ |
+ -------- + * (pas de lien)
Il est typique de stocker une adresse qui "ne pointe nulle part" comme une adresse zéro.
En termes de base, qu'est-ce qu'un pointeur?
Un pointeur n'est qu'une variable contenant une adresse mémoire. Vous pouvez généralement demander au langage de programmation de vous donner son numéro, mais la plupart des langages de programmation et des exécutions essaient de cacher le fait qu'il y a un nombre en dessous, simplement parce que le nombre lui-même n'a pas vraiment de sens pour vous. Il est préférable de considérer un pointeur comme une boîte noire, c'est-à-dire. vous ne savez pas ou ne vous souciez pas vraiment de la façon dont il est réellement mis en œuvre, tant qu'il fonctionne.