abdelouafi
Administrator
إذا كنت ترغب في مشاهدة الصور ، يرجى النقر عليها
Blog
SUIVEZ NOTRE CHAINE YOUTUBE: قم بالتسجيل في قناتنا عبر هذا الرابط https://www.youtube.com/channel/UCCITRMWPcElh-96wCS3EyUg
devoir 1 math tronc commun
مجموعة من دروس و فروض جميع المستويات
دروس الإعدادي - دروس الثانوي الثأهيلي - دروس التعليم الابتدائي - فروض مختلف المستويات الدراسيةفضلا و ليس أمرا شارك هذه الصفحة مع أصدقائك:
La nécessité d'une allocation dynamique de la mémoire
C ++ prend en charge trois types de base d’allocation de mémoire, dont vous en avez déjà vu deux.
La plupart du temps, c'est très bien. Cependant, vous rencontrerez des situations dans lesquelles l'une de ces contraintes, voire les deux, causent des problèmes, généralement lorsqu'il s'agit d'une entrée externe (utilisateur ou fichier).
Par exemple, nous pouvons vouloir utiliser une chaîne pour contenir le nom d’une personne, mais nous ne savons pas combien de temps son nom s’applique jusqu’à ce qu’il soit saisi. Ou nous pouvons vouloir lire un certain nombre d’enregistrements à partir du disque, mais nous ne savons pas à l’avance combien il ya d’enregistrements. Nous pouvons également créer un jeu avec un nombre variable de monstres (qui changent avec le temps, à mesure que certains monstres meurent et que de nouveaux sont créés) tentent de tuer le joueur.
Si nous devons déclarer la taille de tout au moment de la compilation, le mieux que nous puissions faire est d’essayer de deviner la taille maximale des variables dont nous aurons besoin et nous espérons que cela suffira:
C'est une mauvaise solution pour au moins quatre raisons:
Premièrement, cela entraîne un gaspillage de mémoire si les variables ne sont pas réellement utilisées. Par exemple, si nous allouons 25 caractères pour chaque nom, mais que les noms ne comptent en moyenne que 12 caractères, nous utilisons plus de deux fois ce dont nous avons réellement besoin. Ou considérez le tableau de rendu ci-dessus: si un rendu utilise seulement 10 000 polygones, nous avons 20 000 polygones de mémoire non utilisés!
Deuxièmement, comment savoir quels bits de mémoire sont réellement utilisés? Pour les chaînes, c’est simple: une chaîne commençant par un \ 0 n’est clairement pas utilisée. Mais qu'en est-il de monstre [24]? Est-il vivant ou mort en ce moment? Cela nécessite de pouvoir distinguer les éléments actifs des éléments inactifs, ce qui ajoute à la complexité et peut utiliser davantage de mémoire.
Troisièmement, la plupart des variables normales (y compris les tableaux fixes) sont allouées dans une partie de la mémoire appelée pile. La quantité de mémoire de pile pour un programme est généralement assez petite - Visual Studio définit par défaut la taille de pile à 1 Mo. Si vous dépassez ce nombre, la pile débordera et le système d’exploitation fermera probablement le programme.
Sur Visual Studio, vous pouvez voir cela se produire lors de l'exécution de ce programme:
Être limité à seulement 1 Mo de mémoire poserait problème pour de nombreux programmes, en particulier ceux qui traitent des graphiques.
Quatrièmement, et surtout, cela peut conduire à des limitations artificielles et / ou des débordements de tableaux. Que se passe-t-il lorsque l'utilisateur essaie de lire 600 enregistrements à partir du disque, alors que nous n'avons alloué de la mémoire que pour 500 enregistrements au maximum? Soit nous devons donner à l’utilisateur une erreur, ne lire que les 500 enregistrements, ou (dans le pire des cas où nous ne gérons pas ce cas du tout), déborder du tableau d’enregistrements et regarder quelque chose de grave se produire.
Heureusement, ces problèmes sont facilement résolus via une allocation dynamique de la mémoire. L'allocation dynamique de mémoire est un moyen pour les programmes en cours de demander de la mémoire au système d'exploitation en cas de besoin. Cette mémoire ne provient pas de la mémoire de pile limitée du programme. Elle est plutôt allouée à partir d’un pool de mémoire beaucoup plus grand géré par le système d’exploitation appelé le segment de mémoire. Sur les machines modernes, le tas peut avoir une taille de gigaoctets.
Allocation dynamique de variables uniques
Pour allouer dynamiquement une seule variable, nous utilisons la forme scalaire (non-tableau) de l'opérateur new:
Dans le cas ci-dessus, nous demandons au système d’exploitation un nombre entier de mémoire. L'opérateur new crée l'objet à l'aide de cette mémoire, puis renvoie un pointeur contenant l'adresse de la mémoire allouée.
Le plus souvent, nous assignerons la valeur de retour à notre propre variable de pointeur pour pouvoir accéder ultérieurement à la mémoire allouée.
Nous pouvons alors déréférencer le pointeur pour accéder à la mémoire:
Si ce n’était pas le cas auparavant, il devrait maintenant être clair au moins un cas dans lequel les pointeurs sont utiles. Sans pointeur pour contenir l'adresse de la mémoire qui vient d'être allouée, nous n'aurions aucun moyen d'accéder à la mémoire qui vient d'être allouée pour nous!
Comment fonctionne l'allocation de mémoire dynamique?
Votre ordinateur dispose de mémoire (probablement en grande partie) disponible pour les applications. Lorsque vous exécutez une application, votre système d'exploitation la charge dans une partie de la mémoire. Cette mémoire utilisée par votre application est divisée en différentes zones, chacune ayant un objectif différent. Un domaine contient votre code. Un autre domaine est utilisé pour les opérations normales (suivi des fonctions appelées, création et destruction de variables globales et locales, etc.). Nous en reparlerons plus tard. Cependant, une grande partie de la mémoire disponible reste là, dans l'attente d'être distribuée aux programmes qui le demandent.
Lorsque vous allouez de la mémoire de manière dynamique, vous demandez au système d’exploitation de réserver une partie de cette mémoire à l’utilisation de votre programme. S'il peut répondre à cette demande, il retournera l'adresse de cette mémoire à votre application. À partir de ce moment, votre application peut utiliser cette mémoire à sa guise. Lorsque votre application a terminé avec la mémoire, elle peut la restituer au système d'exploitation pour être attribuée à un autre programme.
Contrairement à la mémoire statique ou automatique, le programme lui-même est chargé de demander et de disposer de la mémoire allouée dynamiquement.
Initialisation d'une variable allouée dynamiquement
Lorsque vous allouez une variable de manière dynamique, vous pouvez également l'initialiser via une initialisation directe ou uniforme (en C ++ 11):
Supprimer des variables simples
Lorsque nous en avons terminé avec une variable allouée dynamiquement, nous devons explicitement dire à C ++ de libérer la mémoire pour la réutiliser. Pour les variables simples, cela se fait via la forme scalaire (non-tableau) de l'opérateur delete:
Qu'est-ce que cela signifie d'effacer de la mémoire?
L'opérateur de suppression ne supprime rien réellement. Il renvoie simplement la mémoire pointée vers le système d'exploitation. Le système d'exploitation est alors libre de réaffecter cette mémoire à une autre application (ou à cette application ultérieurement).
Même s’il semble qu’on supprime une variable, ce n’est pas le cas! La variable de pointeur a toujours la même portée qu'auparavant et peut être affectée à une nouvelle valeur comme n'importe quelle autre variable.
Notez que la suppression d'un pointeur qui ne pointe pas vers la mémoire allouée dynamiquement peut entraîner des problèmes.
Pointeurs pendants
C ++ ne donne aucune garantie sur ce qu'il adviendra du contenu de la mémoire désallouée ou de la valeur du pointeur en cours de suppression. Dans la plupart des cas, la mémoire renvoyée au système d'exploitation contiendra les mêmes valeurs qu'avant le renvoi, et le pointeur restera dirigé sur la mémoire maintenant désallouée.
Un pointeur pointant vers une mémoire désallouée est appelé un pointeur suspendu. Le déréférencement ou la suppression d'un pointeur suspendu entraînera un comportement indéfini. Considérez le programme suivant:
Dans le programme ci-dessus, la valeur 7 précédemment attribuée à la mémoire allouée sera probablement toujours là, mais il est possible que la valeur de cette adresse mémoire ait changé. Il est également possible que la mémoire soit allouée à une autre application (ou à l’utilisation propre du système d’exploitation). Si vous essayez d’accéder à cette mémoire, le programme d’exploitation arrêtera le programme.
La désallocation de mémoire peut créer plusieurs pointeurs pendants. Prenons l'exemple suivant:
Tout d’abord, essayez d’éviter que plusieurs pointeurs pointent vers le même morceau de mémoire dynamique. Si cela n'est pas possible, précisez quel pointeur “possède” la mémoire (et est responsable de sa suppression) et quels sont ceux qui y ont juste accès.
Deuxièmement, lorsque vous supprimez un pointeur, si ce pointeur ne sort pas de sa portée immédiatement après, définissez le pointeur sur 0 (ou nullptr en C ++ 11). Nous parlerons davantage des pointeurs nuls et de la raison pour laquelle ils sont utiles dans un instant.
Règle: définissez les pointeurs supprimés sur 0 (ou nullptr dans C ++ 11) sauf s'ils sortent immédiatement de la portée.
L'opérateur new peut échouer
Lors de demandes de mémoire auprès du système d'exploitation, dans de rares circonstances, le système d'exploitation peut ne pas avoir de mémoire avec laquelle donner suite à la demande.
Par défaut, si new échoue, une exception bad_alloc est renvoyée. Si cette exception n’est pas correctement gérée (et elle ne le sera pas, puisque nous n’avons pas encore traité d’exception ou de gestion d’exception), le programme se terminera simplement (crash) avec une erreur d’exception non gérée.
Dans de nombreux cas, il est indésirable de créer une nouvelle exception (ou d’arrêter votre programme). Il est donc possible d’utiliser une autre forme de nouvelle qui peut être utilisée pour indiquer à new de renvoyer un pointeur nul si la mémoire ne peut pas être allouée. Ceci est fait en ajoutant la constante std :: nothrow entre le nouveau mot clé et le type d'allocation:
Dans l'exemple ci-dessus, si new ne parvient pas à allouer de la mémoire, il renverra un pointeur null au lieu de l'adresse de la mémoire allouée.
Notez que si vous essayez ensuite de déréférencer cette mémoire, il en résultera un comportement indéfini (votre programme plantera très probablement). Par conséquent, la meilleure pratique consiste à vérifier toutes les demandes de mémoire pour s'assurer qu'elles ont effectivement abouti avant d'utiliser la mémoire allouée.
Demander de nouvelles ressources mémoire n’échoue que rarement (et presque jamais dans un environnement de développement), c’est courant d’oublier de faire cette vérification!
Pointeurs nuls et allocation de mémoire dynamique
Les pointeurs nuls (pointeurs définis sur l'adresse 0 ou nullptr) sont particulièrement utiles pour traiter l'allocation de mémoire dynamique. Dans le contexte de l'allocation dynamique de mémoire, un pointeur null indique en principe «aucune mémoire n'a été allouée à ce pointeur». Cela nous permet de faire des choses comme l’allocation conditionnelle de mémoire:
La suppression d'un pointeur nul n'a aucun effet. Ainsi, les éléments suivants ne sont pas nécessaires:
Au lieu de cela, vous pouvez simplement écrire:
Si ptr est non nul, la variable allouée dynamiquement sera supprimée. Si c'est nul, rien ne se passera.
Fuites de mémoire
La mémoire allouée dynamiquement n'a effectivement pas de portée. Autrement dit, il reste alloué jusqu'à ce qu'il soit explicitement désalloué ou jusqu'à la fin du programme (et le système d'exploitation le nettoie, en supposant que votre système d'exploitation le fasse). Cependant, les pointeurs utilisés pour conserver les adresses mémoire allouées dynamiquement suivent les règles de portée des variables normales. Cette inadéquation peut créer des problèmes intéressants.
Considérons la fonction suivante:
Cette fonction alloue un entier de manière dynamique, mais ne le libère jamais avec delete. Étant donné que les pointeurs suivent les mêmes règles que les variables normales, lorsque la fonction se termine, ptr sort de la portée. Puisque ptr est la seule variable contenant l'adresse du nombre entier alloué dynamiquement, lorsque ptr est détruit, il n'y a plus de références à la mémoire allouée dynamiquement. Cela signifie que le programme a maintenant "perdu" l'adresse de la mémoire allouée dynamiquement. Par conséquent, cet entier alloué dynamiquement ne peut pas être supprimé.
Ceci s'appelle une fuite de mémoire. Les fuites de mémoire surviennent lorsque votre programme perd l'adresse d'un bit de mémoire allouée dynamiquement avant de la restituer au système d'exploitation. Lorsque cela se produit, votre programme ne peut pas supprimer la mémoire allouée dynamiquement, car il ne sait plus où il se trouve. Le système d’exploitation ne peut pas non plus utiliser cette mémoire, car celle-ci est toujours utilisée par votre programme.
Les fuites de mémoire consomment de la mémoire libre pendant l'exécution du programme, ce qui rend moins de mémoire disponible non seulement pour ce programme, mais également pour d'autres programmes. Les programmes présentant de graves problèmes de fuite de mémoire peuvent consommer toute la mémoire disponible, ce qui ralentit voire ralentit l’ensemble de la machine. Une fois le programme terminé, le système d’exploitation est capable de nettoyer et de «récupérer» toute la mémoire perdue.
Bien que les fuites de mémoire puissent provenir d'un pointeur sortant du cadre, il peut en résulter d'autres moyens. Par exemple, une fuite de mémoire peut se produire si une autre valeur est affectée au pointeur contenant l'adresse de la mémoire allouée dynamiquement:
Cela peut être corrigé en supprimant le pointeur avant de le réaffecter:
De manière connexe, il est également possible d’obtenir une fuite de mémoire via une double allocation:
L'adresse renvoyée par la deuxième affectation remplace l'adresse de la première affectation. Par conséquent, la première allocation devient une fuite de mémoire!
De même, vous pouvez éviter cela en vous assurant de supprimer le pointeur avant de réaffecter.
Conclusion
Les opérateurs new et delete nous permettent d'allouer de manière dynamique des variables uniques pour nos programmes.
La mémoire allouée dynamiquement n’a pas de portée et le restera jusqu’à ce que vous la désallouiez ou que le programme se termine.
Veillez à ne pas déréférencer les pointeurs pendants ou nuls.
Dans la leçon suivante, nous verrons comment utiliser new et delete pour allouer et supprimer des tableaux.
C ++ prend en charge trois types de base d’allocation de mémoire, dont vous en avez déjà vu deux.
- L'allocation de mémoire statique a lieu pour les variables statiques et globales. La mémoire pour ces types de variables est allouée une fois lorsque votre programme est exécuté et persiste tout au long de sa vie.
- L'allocation automatique de mémoire a lieu pour les paramètres de fonction et les variables locales. La mémoire pour ces types de variables est allouée lors de la saisie du bloc concerné et libérée lors de sa sortie, autant de fois que nécessaire.
- L'allocation de mémoire dynamique est le sujet de cet article.
- La taille de la variable / tableau doit être connue au moment de la compilation.
- L'allocation de mémoire et la désallocation se font automatiquement (lorsque la variable est instanciée / détruite).
La plupart du temps, c'est très bien. Cependant, vous rencontrerez des situations dans lesquelles l'une de ces contraintes, voire les deux, causent des problèmes, généralement lorsqu'il s'agit d'une entrée externe (utilisateur ou fichier).
Par exemple, nous pouvons vouloir utiliser une chaîne pour contenir le nom d’une personne, mais nous ne savons pas combien de temps son nom s’applique jusqu’à ce qu’il soit saisi. Ou nous pouvons vouloir lire un certain nombre d’enregistrements à partir du disque, mais nous ne savons pas à l’avance combien il ya d’enregistrements. Nous pouvons également créer un jeu avec un nombre variable de monstres (qui changent avec le temps, à mesure que certains monstres meurent et que de nouveaux sont créés) tentent de tuer le joueur.
Si nous devons déclarer la taille de tout au moment de la compilation, le mieux que nous puissions faire est d’essayer de deviner la taille maximale des variables dont nous aurons besoin et nous espérons que cela suffira:
Code:
char name[25]; // let's hope their name is less than 25 chars!
Record record[500]; // let's hope there are less than 500 records!
Monster monster[40]; // 40 monsters maximum
Polygon rendering[30000]; // this 3d rendering better not have more than 30,000 polygons!
C'est une mauvaise solution pour au moins quatre raisons:
Premièrement, cela entraîne un gaspillage de mémoire si les variables ne sont pas réellement utilisées. Par exemple, si nous allouons 25 caractères pour chaque nom, mais que les noms ne comptent en moyenne que 12 caractères, nous utilisons plus de deux fois ce dont nous avons réellement besoin. Ou considérez le tableau de rendu ci-dessus: si un rendu utilise seulement 10 000 polygones, nous avons 20 000 polygones de mémoire non utilisés!
Deuxièmement, comment savoir quels bits de mémoire sont réellement utilisés? Pour les chaînes, c’est simple: une chaîne commençant par un \ 0 n’est clairement pas utilisée. Mais qu'en est-il de monstre [24]? Est-il vivant ou mort en ce moment? Cela nécessite de pouvoir distinguer les éléments actifs des éléments inactifs, ce qui ajoute à la complexité et peut utiliser davantage de mémoire.
Troisièmement, la plupart des variables normales (y compris les tableaux fixes) sont allouées dans une partie de la mémoire appelée pile. La quantité de mémoire de pile pour un programme est généralement assez petite - Visual Studio définit par défaut la taille de pile à 1 Mo. Si vous dépassez ce nombre, la pile débordera et le système d’exploitation fermera probablement le programme.
Sur Visual Studio, vous pouvez voir cela se produire lors de l'exécution de ce programme:
Code:
int main()
{
int array[1000000]; // allocate 1 million integers (probably 4MB of memory)
}
Être limité à seulement 1 Mo de mémoire poserait problème pour de nombreux programmes, en particulier ceux qui traitent des graphiques.
Quatrièmement, et surtout, cela peut conduire à des limitations artificielles et / ou des débordements de tableaux. Que se passe-t-il lorsque l'utilisateur essaie de lire 600 enregistrements à partir du disque, alors que nous n'avons alloué de la mémoire que pour 500 enregistrements au maximum? Soit nous devons donner à l’utilisateur une erreur, ne lire que les 500 enregistrements, ou (dans le pire des cas où nous ne gérons pas ce cas du tout), déborder du tableau d’enregistrements et regarder quelque chose de grave se produire.
Heureusement, ces problèmes sont facilement résolus via une allocation dynamique de la mémoire. L'allocation dynamique de mémoire est un moyen pour les programmes en cours de demander de la mémoire au système d'exploitation en cas de besoin. Cette mémoire ne provient pas de la mémoire de pile limitée du programme. Elle est plutôt allouée à partir d’un pool de mémoire beaucoup plus grand géré par le système d’exploitation appelé le segment de mémoire. Sur les machines modernes, le tas peut avoir une taille de gigaoctets.
Allocation dynamique de variables uniques
Pour allouer dynamiquement une seule variable, nous utilisons la forme scalaire (non-tableau) de l'opérateur new:
Code:
new int; // dynamically allocate an integer (and discard the result)
Dans le cas ci-dessus, nous demandons au système d’exploitation un nombre entier de mémoire. L'opérateur new crée l'objet à l'aide de cette mémoire, puis renvoie un pointeur contenant l'adresse de la mémoire allouée.
Le plus souvent, nous assignerons la valeur de retour à notre propre variable de pointeur pour pouvoir accéder ultérieurement à la mémoire allouée.
Code:
int *ptr = new int; // dynamically allocate an integer and assign the address to ptr so we can access it later
Nous pouvons alors déréférencer le pointeur pour accéder à la mémoire:
Code:
*ptr = 7; // assign value of 7 to allocated memory
Si ce n’était pas le cas auparavant, il devrait maintenant être clair au moins un cas dans lequel les pointeurs sont utiles. Sans pointeur pour contenir l'adresse de la mémoire qui vient d'être allouée, nous n'aurions aucun moyen d'accéder à la mémoire qui vient d'être allouée pour nous!
Comment fonctionne l'allocation de mémoire dynamique?
Votre ordinateur dispose de mémoire (probablement en grande partie) disponible pour les applications. Lorsque vous exécutez une application, votre système d'exploitation la charge dans une partie de la mémoire. Cette mémoire utilisée par votre application est divisée en différentes zones, chacune ayant un objectif différent. Un domaine contient votre code. Un autre domaine est utilisé pour les opérations normales (suivi des fonctions appelées, création et destruction de variables globales et locales, etc.). Nous en reparlerons plus tard. Cependant, une grande partie de la mémoire disponible reste là, dans l'attente d'être distribuée aux programmes qui le demandent.
Lorsque vous allouez de la mémoire de manière dynamique, vous demandez au système d’exploitation de réserver une partie de cette mémoire à l’utilisation de votre programme. S'il peut répondre à cette demande, il retournera l'adresse de cette mémoire à votre application. À partir de ce moment, votre application peut utiliser cette mémoire à sa guise. Lorsque votre application a terminé avec la mémoire, elle peut la restituer au système d'exploitation pour être attribuée à un autre programme.
Contrairement à la mémoire statique ou automatique, le programme lui-même est chargé de demander et de disposer de la mémoire allouée dynamiquement.
Initialisation d'une variable allouée dynamiquement
Lorsque vous allouez une variable de manière dynamique, vous pouvez également l'initialiser via une initialisation directe ou uniforme (en C ++ 11):
Code:
int *ptr1 = new int (5); // use direct initialization
int *ptr2 = new int { 6 }; // use uniform initialization
Supprimer des variables simples
Lorsque nous en avons terminé avec une variable allouée dynamiquement, nous devons explicitement dire à C ++ de libérer la mémoire pour la réutiliser. Pour les variables simples, cela se fait via la forme scalaire (non-tableau) de l'opérateur delete:
Code:
// assume ptr has previously been allocated with operator new
delete ptr; // return the memory pointed to by ptr to the operating system
ptr = 0; // set ptr to be a null pointer (use nullptr instead of 0 in C++11)
Qu'est-ce que cela signifie d'effacer de la mémoire?
L'opérateur de suppression ne supprime rien réellement. Il renvoie simplement la mémoire pointée vers le système d'exploitation. Le système d'exploitation est alors libre de réaffecter cette mémoire à une autre application (ou à cette application ultérieurement).
Même s’il semble qu’on supprime une variable, ce n’est pas le cas! La variable de pointeur a toujours la même portée qu'auparavant et peut être affectée à une nouvelle valeur comme n'importe quelle autre variable.
Notez que la suppression d'un pointeur qui ne pointe pas vers la mémoire allouée dynamiquement peut entraîner des problèmes.
Pointeurs pendants
C ++ ne donne aucune garantie sur ce qu'il adviendra du contenu de la mémoire désallouée ou de la valeur du pointeur en cours de suppression. Dans la plupart des cas, la mémoire renvoyée au système d'exploitation contiendra les mêmes valeurs qu'avant le renvoi, et le pointeur restera dirigé sur la mémoire maintenant désallouée.
Un pointeur pointant vers une mémoire désallouée est appelé un pointeur suspendu. Le déréférencement ou la suppression d'un pointeur suspendu entraînera un comportement indéfini. Considérez le programme suivant:
Code:
#include <iostream>
int main()
{
int *ptr = new int; // dynamically allocate an integer
*ptr = 7; // put a value in that memory location
delete ptr; // return the memory to the operating system. ptr is now a dangling pointer.
std::cout << *ptr; // Dereferencing a dangling pointer will cause undefined behavior
delete ptr; // trying to deallocate the memory again will also lead to undefined behavior.
return 0;
}
Dans le programme ci-dessus, la valeur 7 précédemment attribuée à la mémoire allouée sera probablement toujours là, mais il est possible que la valeur de cette adresse mémoire ait changé. Il est également possible que la mémoire soit allouée à une autre application (ou à l’utilisation propre du système d’exploitation). Si vous essayez d’accéder à cette mémoire, le programme d’exploitation arrêtera le programme.
La désallocation de mémoire peut créer plusieurs pointeurs pendants. Prenons l'exemple suivant:
Code:
#include <iostream>
int main()
{
int *ptr = new int; // dynamically allocate an integer
int *otherPtr = ptr; // otherPtr is now pointed at that same memory location
delete ptr; // return the memory to the operating system. ptr and otherPtr are now dangling pointers.
ptr = 0; // ptr is now a nullptr
// however, otherPtr is still a dangling pointer!
return 0;
}
Tout d’abord, essayez d’éviter que plusieurs pointeurs pointent vers le même morceau de mémoire dynamique. Si cela n'est pas possible, précisez quel pointeur “possède” la mémoire (et est responsable de sa suppression) et quels sont ceux qui y ont juste accès.
Deuxièmement, lorsque vous supprimez un pointeur, si ce pointeur ne sort pas de sa portée immédiatement après, définissez le pointeur sur 0 (ou nullptr en C ++ 11). Nous parlerons davantage des pointeurs nuls et de la raison pour laquelle ils sont utiles dans un instant.
Règle: définissez les pointeurs supprimés sur 0 (ou nullptr dans C ++ 11) sauf s'ils sortent immédiatement de la portée.
L'opérateur new peut échouer
Lors de demandes de mémoire auprès du système d'exploitation, dans de rares circonstances, le système d'exploitation peut ne pas avoir de mémoire avec laquelle donner suite à la demande.
Par défaut, si new échoue, une exception bad_alloc est renvoyée. Si cette exception n’est pas correctement gérée (et elle ne le sera pas, puisque nous n’avons pas encore traité d’exception ou de gestion d’exception), le programme se terminera simplement (crash) avec une erreur d’exception non gérée.
Dans de nombreux cas, il est indésirable de créer une nouvelle exception (ou d’arrêter votre programme). Il est donc possible d’utiliser une autre forme de nouvelle qui peut être utilisée pour indiquer à new de renvoyer un pointeur nul si la mémoire ne peut pas être allouée. Ceci est fait en ajoutant la constante std :: nothrow entre le nouveau mot clé et le type d'allocation:
Code:
int *value = new (std::nothrow) int; // value will be set to a null pointer if the integer allocation fails
Dans l'exemple ci-dessus, si new ne parvient pas à allouer de la mémoire, il renverra un pointeur null au lieu de l'adresse de la mémoire allouée.
Notez que si vous essayez ensuite de déréférencer cette mémoire, il en résultera un comportement indéfini (votre programme plantera très probablement). Par conséquent, la meilleure pratique consiste à vérifier toutes les demandes de mémoire pour s'assurer qu'elles ont effectivement abouti avant d'utiliser la mémoire allouée.
Code:
int *value = new (std::nothrow) int; // ask for an integer's worth of memory
if (!value) // handle case where new returned null
{
// Do error handling here
std::cout << "Could not allocate memory";
}
Demander de nouvelles ressources mémoire n’échoue que rarement (et presque jamais dans un environnement de développement), c’est courant d’oublier de faire cette vérification!
Pointeurs nuls et allocation de mémoire dynamique
Les pointeurs nuls (pointeurs définis sur l'adresse 0 ou nullptr) sont particulièrement utiles pour traiter l'allocation de mémoire dynamique. Dans le contexte de l'allocation dynamique de mémoire, un pointeur null indique en principe «aucune mémoire n'a été allouée à ce pointeur». Cela nous permet de faire des choses comme l’allocation conditionnelle de mémoire:
Code:
// If ptr isn't already allocated, allocate it
if (!ptr)
ptr = new int;
La suppression d'un pointeur nul n'a aucun effet. Ainsi, les éléments suivants ne sont pas nécessaires:
Code:
if (ptr)
delete ptr;
Au lieu de cela, vous pouvez simplement écrire:
Code:
delete ptr;
Si ptr est non nul, la variable allouée dynamiquement sera supprimée. Si c'est nul, rien ne se passera.
Fuites de mémoire
La mémoire allouée dynamiquement n'a effectivement pas de portée. Autrement dit, il reste alloué jusqu'à ce qu'il soit explicitement désalloué ou jusqu'à la fin du programme (et le système d'exploitation le nettoie, en supposant que votre système d'exploitation le fasse). Cependant, les pointeurs utilisés pour conserver les adresses mémoire allouées dynamiquement suivent les règles de portée des variables normales. Cette inadéquation peut créer des problèmes intéressants.
Considérons la fonction suivante:
Code:
void doSomething()
{
int *ptr = new int;
}
Cette fonction alloue un entier de manière dynamique, mais ne le libère jamais avec delete. Étant donné que les pointeurs suivent les mêmes règles que les variables normales, lorsque la fonction se termine, ptr sort de la portée. Puisque ptr est la seule variable contenant l'adresse du nombre entier alloué dynamiquement, lorsque ptr est détruit, il n'y a plus de références à la mémoire allouée dynamiquement. Cela signifie que le programme a maintenant "perdu" l'adresse de la mémoire allouée dynamiquement. Par conséquent, cet entier alloué dynamiquement ne peut pas être supprimé.
Ceci s'appelle une fuite de mémoire. Les fuites de mémoire surviennent lorsque votre programme perd l'adresse d'un bit de mémoire allouée dynamiquement avant de la restituer au système d'exploitation. Lorsque cela se produit, votre programme ne peut pas supprimer la mémoire allouée dynamiquement, car il ne sait plus où il se trouve. Le système d’exploitation ne peut pas non plus utiliser cette mémoire, car celle-ci est toujours utilisée par votre programme.
Les fuites de mémoire consomment de la mémoire libre pendant l'exécution du programme, ce qui rend moins de mémoire disponible non seulement pour ce programme, mais également pour d'autres programmes. Les programmes présentant de graves problèmes de fuite de mémoire peuvent consommer toute la mémoire disponible, ce qui ralentit voire ralentit l’ensemble de la machine. Une fois le programme terminé, le système d’exploitation est capable de nettoyer et de «récupérer» toute la mémoire perdue.
Bien que les fuites de mémoire puissent provenir d'un pointeur sortant du cadre, il peut en résulter d'autres moyens. Par exemple, une fuite de mémoire peut se produire si une autre valeur est affectée au pointeur contenant l'adresse de la mémoire allouée dynamiquement:
Code:
int value = 5;
int *ptr = new int; // allocate memory
ptr = &value; // old address lost, memory leak results
Cela peut être corrigé en supprimant le pointeur avant de le réaffecter:
Code:
int value = 5;
int *ptr = new int; // allocate memory
delete ptr; // return memory back to operating system
ptr = &value; // reassign pointer to address of value
De manière connexe, il est également possible d’obtenir une fuite de mémoire via une double allocation:
Code:
int *ptr = new int;
ptr = new int; // old address lost, memory leak results
L'adresse renvoyée par la deuxième affectation remplace l'adresse de la première affectation. Par conséquent, la première allocation devient une fuite de mémoire!
De même, vous pouvez éviter cela en vous assurant de supprimer le pointeur avant de réaffecter.
Conclusion
Les opérateurs new et delete nous permettent d'allouer de manière dynamique des variables uniques pour nos programmes.
La mémoire allouée dynamiquement n’a pas de portée et le restera jusqu’à ce que vous la désallouiez ou que le programme se termine.
Veillez à ne pas déréférencer les pointeurs pendants ou nuls.
Dans la leçon suivante, nous verrons comment utiliser new et delete pour allouer et supprimer des tableaux.