C ++ succinctement pointeurs, références et const-correct

Introduction aux pointeurs

Un pointeur n'est rien d'autre qu'une variable contenant une adresse mémoire. Lorsqu'il est utilisé correctement, un pointeur contient une adresse mémoire valide contenant un objet, compatible avec le type du pointeur. Comme les références en C #, tous les pointeurs d'un environnement d'exécution particulier ont la même taille, quel que soit le type de données pointé par le pointeur. Par exemple, lorsqu'un programme est compilé et exécuté sur un système d'exploitation 32 bits, un pointeur aura généralement 4 octets (32 bits)..

Les pointeurs peuvent pointer vers n'importe quelle adresse mémoire. Vous pouvez et aurez fréquemment des pointeurs sur des objets qui se trouvent sur la pile. Vous pouvez également avoir des pointeurs sur des objets statiques, sur des objets locaux et, bien sûr, sur des objets dynamiques (c'est-à-dire, alloués par tas). Quand les programmeurs qui ne connaissent que très peu les pointeurs y pensent, c'est généralement dans le contexte des objets dynamiques.

En raison de fuites potentielles, vous ne devez jamais allouer de mémoire dynamique en dehors d'un pointeur intelligent. La bibliothèque standard C ++ fournit deux pointeurs intelligents à prendre en compte: std :: shared_ptr et std :: unique_ptr.

En plaçant des objets de durée dynamique dans l’un d’eux, vous garantissez que lorsque le std :: unique_ptr, ou le dernier std :: shared_ptr qui contient un pointeur sur la mémoire qui sort de la portée, la mémoire sera correctement libérée avec la version correcte de delete (delete ou delete []) afin qu’elle ne coule pas. C'est le modèle RAII du chapitre précédent en action.

Deux choses peuvent se produire lorsque vous maîtrisez bien la RAII avec des pointeurs intelligents: l’allocation réussit et la mémoire est donc correctement libérée lorsque le pointeur intelligent devient hors de portée ou que l’attribution échoue. Dans ce cas, aucune mémoire n’est allouée et donc aucune fuite. En pratique, la dernière situation devrait être assez rare sur les PC et les serveurs modernes en raison de leur grande mémoire et de leur provision de mémoire virtuelle..

Si vous n'utilisez pas de pointeurs intelligents, vous demandez simplement une fuite de mémoire. Toute exception entre l'allocation de mémoire avec un nouveau ou un nouveau [] et la libération de la mémoire avec delete ou delete [] entraînera probablement une fuite de mémoire. Si vous ne faites pas attention, vous pouvez utiliser accidentellement un pointeur déjà supprimé mais non défini comme égal à nullptr. Vous accéderiez alors à un emplacement aléatoire de la mémoire et le traiteriez comme s'il s'agissait d'un pointeur valide..

La meilleure chose qui puisse arriver dans ce cas est que votre programme se bloque. Si ce n'est pas le cas, vous corrompez les données de manière étrange et inconnue et vous sauvegardez peut-être ces corruptions dans une base de données ou les transmettez sur le Web. Vous pourriez aussi ouvrir la porte à des problèmes de sécurité. Utilisez donc des pointeurs intelligents et laissez le langage gérer pour vous les problèmes de gestion de la mémoire..


Constateur Pointeur

Un pointeur constant prend la forme SomeClass * const someClass2 = & someClass1;. En d'autres termes, le * vient avant const. Le résultat est que le pointeur lui-même ne peut pas pointer vers autre chose, mais les données sur lesquelles il pointe restent mutables. Ce n'est pas susceptible d'être très utile dans la plupart des situations.

Pointeur vers const

Un pointeur sur const prend la forme const SomeClass * someClass2 = & someClass1;. Dans ce cas, le * vient après const. Le résultat est que le pointeur peut pointer vers d'autres éléments, mais vous ne pouvez pas modifier les données qu'il pointe. C'est un moyen courant de déclarer des paramètres que vous souhaitez simplement inspecter sans modifier leurs données..

Pointeur Const à Const

Un pointeur sur const prend la forme const SomeClass * const someClass2 = & someClass1;. Ici, le * est pris en sandwich entre deux mots-clés const. Le résultat est que le pointeur ne peut pas indiquer autre chose et que vous ne pouvez pas modifier les données auxquelles il pointe..

Const-Correctness et Fonctions membres const

Const-correctness consiste à utiliser le mot-clé const pour décorer les paramètres et les fonctions afin que la présence ou l'absence du mot-clé const traduise correctement les éventuels effets secondaires. Vous pouvez marquer une fonction membre const en plaçant le mot clé const après la déclaration des paramètres de la fonction..

Par exemple, int GetSomeInt (void) const; déclare une fonction membre const, une fonction membre qui ne modifie pas les données de l'objet auquel elle appartient. Le compilateur appliquera cette garantie. Cela garantira également la garantie que lorsque vous passez un objet dans une fonction qui le prend en tant que const, cette fonction ne peut appeler aucune fonction membre non-const de cet objet..

Il est plus facile de concevoir votre programme de manière à respecter const-correct, dès le début. Lorsque vous adhérez à const-correct, il devient plus facile d'utiliser le multithreading, car vous savez exactement quelles fonctions membres ont des effets secondaires. Il est également plus facile de rechercher des bogues liés à des états de données non valides. Les autres personnes qui collaborent avec vous sur un projet seront également informées des modifications potentielles des données de la classe lorsqu'elles appellent certaines fonctions membres..


le *, Et, et -> Les opérateurs

Lorsque vous travaillez avec des pointeurs, y compris des pointeurs intelligents, trois opérateurs sont intéressants: *, &, et ->.

L'opérateur d'indirection, *, référence un pointeur, ce qui signifie que vous travaillez avec les données pointées, au lieu du pointeur lui-même. Pour les paragraphes suivants, supposons que p_someInt est un pointeur valide sur un entier sans qualifications const..

La déclaration p_someInt = 5000000; n'attribuerait pas la valeur 5000000 à l'entier pointé. Au lieu de cela, le pointeur doit pointer sur l'adresse de mémoire 5000000, 0X004C4B40 sur un système 32 bits. Quel est à l'adresse mémoire 0X004C4B40? Qui sait? Ce pourrait être votre entier, mais il y a de fortes chances que ce soit autre chose. Si vous avez de la chance, c'est une adresse invalide. La prochaine fois que vous essayez d'utiliser p_someInt correctement, votre programme va planter. S'il s'agit d'une adresse de données valide, vous risquez alors de corrompre les données..

La déclaration * p_someInt = 5000000; assignera la valeur 5000000 au nombre entier désigné par p_someInt. C'est l'opérateur d'indirection en action; il prend p_someInt et le remplace par une valeur L qui représente les données à l'adresse indiquée (nous discuterons bientôt des valeurs L).

L'adresse-d'opérateur, &, récupère l'adresse d'une variable ou d'une fonction. Cela vous permet de créer un pointeur sur un objet local, que vous pouvez transmettre à une fonction qui souhaite un pointeur. Vous n'avez même pas besoin de créer un pointeur local pour le faire; vous pouvez simplement utiliser votre variable locale avec l'adresse-de l'opérateur devant elle comme argument, et tout fonctionnera parfaitement.

Les pointeurs sur les fonctions sont similaires aux instances de délégation en C #. Étant donné cette déclaration de fonction: double GetValue (int idx); ce serait le bon pointeur de fonction: double (* SomeFunctionPtr) (int);.

Si votre fonction a renvoyé un pointeur, dites comme ceci: int * GetIntPtr (void); alors ce serait le bon pointeur de fonction: int * (* SomeIntPtrDelegate) (void);. Ne laissez pas les doubles astérisques vous déranger; Rappelez-vous simplement le premier ensemble de parenthèses autour du nom du pointeur * et de la fonction afin que le compilateur l'interprète correctement comme un pointeur de fonction plutôt que comme une déclaration de fonction.

L'opérateur -> membre d'accès est ce que vous utilisez pour accéder aux membres de la classe lorsque vous avez un pointeur sur une instance de la classe. Il fonctionne comme une combinaison de l'opérateur d'indirection et du. opérateur d'accès membre. Alors p_someClassInstance-> SetValue (10); et (* p_someClassInstance) .SetValue (10); les deux font la même chose.

Valeurs L et Valeurs R

Ce ne serait pas C ++ si nous ne parlions pas au moins brièvement des valeurs L et des valeurs R. Les valeurs L sont appelées ainsi parce qu'elles apparaissent traditionnellement du côté gauche d'un signe égal. En d'autres termes, ce sont des valeurs qui peuvent être assignées à celles qui survivront à l'évaluation de l'expression courante. Le type de valeur L le plus connu est une variable, mais il inclut également le résultat de l'appel d'une fonction qui renvoie une référence à une valeur L.

Les valeurs R apparaissent généralement à droite de l'équation ou, peut-être plus précisément, sont-elles des valeurs qui ne pouvaient pas apparaître à gauche. Ce sont des choses telles que des constantes ou le résultat de l'évaluation d'une équation. Par exemple, a + b où a et b peuvent être des valeurs L, mais le résultat de leur addition constitue une valeur R ou la valeur renvoyée d'une fonction qui renvoie autre chose que void ou une référence à une valeur L..

Références

Les références agissent comme des variables sans pointeur. Une fois qu'une référence est initialisée, elle ne peut pas faire référence à un autre objet. Vous devez également initialiser une référence où vous la déclarez. Si vos fonctions prennent des références plutôt que des objets, vous ne supporterez pas le coût d'une construction de copie. Puisque la référence fait référence à l'objet, les modifications qui y sont apportées sont des modifications de l'objet lui-même.

Tout comme les pointeurs, vous pouvez également avoir une référence const. Sauf si vous devez modifier l'objet, vous devez utiliser des références const, car elles fournissent des vérifications du compilateur pour vous assurer que vous ne mutiez pas l'objet lorsque vous pensez ne pas l'être..

Il existe deux types de références: les références de valeur L et les références de valeur R. Une référence de valeur L est marquée d'un & ajouté au nom du type (par exemple, SomeClass &), tandis qu'une référence de valeur R est marquée par un && ajouté au nom du type (par exemple, SomeClass &&). Pour la plupart, ils agissent de la même manière; la principale différence est que la référence de valeur R est extrêmement importante pour déplacer la sémantique.


Pointeur et échantillon de référence

L'exemple suivant montre l'utilisation du pointeur et de la référence avec des explications dans les commentaires.

Exemple: PointerSample \ PointerSample.cpp

#comprendre  //// Voir ci-dessous le commentaire sur la première utilisation de assert () dans _pmain. // # define NDEBUG 1 #include  #include "… /pchar.h" en utilisant namespace std; void SetValueToZero (int & value) value = 0;  void SetValueToZero (int * valeur) * valeur = 0;  int _pmain (int / * argc * /, _pchar * / * argv * / []) int valeur = 0; const int intArrCount = 20; // Crée un pointeur sur int. int * p_intArr = new int [intArrCount]; // Crée un pointeur const sur int. int * const cp_intArr = p_intArr; // Ces deux instructions sont correctes car nous pouvons modifier les données pointées par un pointeur // const. // Définir tous les éléments sur 5. uninitialized_fill_n (cp_intArr, intArrCount, 5); // Définit le premier élément à zéro. * cp_intArr = 0; //// Cette instruction est illégale car nous ne pouvons pas modifier le pointeur const ////. // cp_intArr = nullptr; // Crée un pointeur sur const int. const int * pc_intArr = nullptr; // C'est bien parce que nous pouvons modifier ce qu'un pointeur sur const pointe sur //. pc_intArr = p_intArr; // Assurez-vous que nous "utilisons" pc_intArr. valeur = * pc_intArr; //// Cette instruction est illégale car nous ne pouvons pas modifier les données désignées par un //// pointeur sur const. // * pc_intArr = 10; const int * const cpc_intArr = p_intArr; //// Ces deux instructions sont illégales car nous ne pouvons pas modifier //// le pointeur const sur const ni les données sur lesquelles //// pointe. // cpc_intArr = p_intArr; // * cpc_intArr = 20; // Assurez-vous que nous "utilisons" cpc_intArr. valeur = * cpc_intArr; * p_intArr = 6; SetValueToZero (* p_intArr); // De , cette macro affichera un message de diagnostic si l'expression // entre parenthèses est évaluée à zéro. // Contrairement à la macro _ASSERTE, celle-ci sera exécutée lors de la génération de versions. Pour // le désactiver, définissez NDEBUG avant d'inclure le  entête. assert (* p_intArr == 0); * p_intArr = 9; int & r_first = * p_intArr; SetValueToZero (r_first); assert (* p_intArr == 0); const int & cr_first = * p_intArr; //// Cette instruction est illégale car cr_first est une référence const, //// mais SetValueToZero ne prend pas une référence const, mais une référence //// non-const, ce qui est logique si elle le souhaite //// modifier la valeur. // SetValueToZero (cr_first); valeur = cr_first; // On peut initialiser un pointeur en utilisant l'opérateur address-of. // Méfiez-vous simplement, car les variables non statiques locales deviennent // invalides lorsque vous quittez leur portée, de sorte que leurs pointeurs // deviennent invalides. int * p_firstElement = &r_first; * p_firstElement = 10; SetValueToZero (* p_firstElement); assert (* p_firstElement == 0); // Ceci appellera la surcharge SetValueToZero (int *) parce que nous // utilisons l'opérateur address-of pour transformer la référence // en un pointeur. SetValueToZero (& r_first); * p_intArr = 3; SetValueToZero (& (* p_intArr)); assert (* p_firstElement == 0); // Crée un pointeur de fonction. Remarquez comment nous devons mettre le nom de variable // entre parenthèses avec un * avant celui-ci. void (* FunctionPtrToSVTZ) (int &) = nullptr; // Définit le pointeur de fonction pour qu'il pointe vers SetValueToZero. Il sélectionne automatiquement la surcharge correcte. FunctionPtrToSVTZ = & SetValueToZero; * p_intArr = 20; // Appelez la fonction indiquée par FunctionPtrToSVTZ, c'est-à-dire // SetValueToZero (int &). FunctionPtrToSVTZ (* p_intArr); assert (* p_intArr == 0); * p_intArr = 50; // On peut aussi appeler un pointeur de fonction comme ceci. Ceci est // plus proche de ce qui se passe réellement dans les coulisses; // FunctionPtrToSVTZ est en train d'être dé-référencé, le résultat étant // la fonction sur laquelle on pointe, que // nous appelons ensuite à l'aide de la ou des valeurs spécifiées dans le deuxième ensemble de // parenthèses, c'est-à-dire * p_intArr ici. (* FunctionPtrToSVTZ) (* p_intArr); assert (* p_intArr == 0); // Assurez-vous que la valeur définie est 0 afin que nous puissions "l'utiliser". * p_intArr = 0; valeur = * p_intArr; // Supprime le tableau p_intArray à l'aide de l'opérateur delete [] puisqu'il s'agit d'un // tableau dynamique p_intArray. delete [] p_intArr; p_intArr = nullptr; valeur de retour; 

Volatil

Je mentionne volatile uniquement pour mettre en garde contre son utilisation. Comme const, une variable peut être déclarée volatile. Vous pouvez même avoir une constante volatile; les deux ne sont pas mutuellement exclusifs.

Voici la chose à propos de volatile: cela ne signifie probablement pas ce que vous pensez que cela signifie. Par exemple, ce n'est pas bon pour la programmation multithread. Le cas d'utilisation actuel de volatile est extrêmement étroit. Les chances sont, si vous mettez le qualificatif volatile sur une variable, vous faites quelque chose de terriblement faux.

Eric Lippert, membre de l'équipe de langage C # de Microsoft, a décrit l'utilisation de volatile comme suit: «Un signe que vous faites quelque chose de complètement fou: vous essayez de lire et d'écrire la même valeur sur deux threads différents sans verrouiller en place. "Il a raison, et son argument se poursuit parfaitement dans C++.

L'utilisation de volatile devrait être accueillie avec plus de scepticisme que l'utilisation de goto. Je dis cela parce que je peux penser à au moins une utilisation valide de goto à usage général: sortir d'une construction de boucle profondément imbriquée à la fin d'une condition non exceptionnelle. volatile, en revanche, n’est vraiment utile que si vous écrivez un pilote de périphérique ou écrivez du code pour un type de puce ROM. Sur ce point, vous devez vraiment bien connaître le standard de langage de programmation ISO / IEC C ++ lui-même, les spécifications matérielles de l'environnement d'exécution dans lequel votre code sera exécuté, et probablement le standard de langage ISO / IEC C également..

Remarque: Vous devez également connaître le langage d'assemblage du matériel cible. Vous pourrez ainsi consulter le code généré et vous assurer que le compilateur génère le code correct (PDF) pour votre utilisation de la technologie volatile..

J'ai ignoré l'existence du mot clé volatile et continuerai à le faire pour le reste de ce livre. C'est parfaitement sûr, car:

  • C'est une fonctionnalité linguistique qui n'entre en jeu que si vous l'utilisez réellement.
  • Son utilisation peut être évitée en toute sécurité par pratiquement tout le monde.

Une dernière remarque à propos de volatile: l'un des effets qu'il est très susceptible de produire est un code plus lent. Il était une fois, les gens pensaient que volatiles produisaient le même résultat que l'atomicité. Ce n'est pas. Lorsqu'il est correctement implémenté, atomicity garantit que plusieurs threads et plusieurs processeurs ne peuvent pas lire et écrire simultanément un bloc de mémoire accédé de manière atomique. Les mécanismes pour cela sont les verrous, les mutex, les sémaphones, les clôtures, les instructions spéciales du processeur, etc. La seule chose volatile est de forcer le processeur à extraire une variable volatile de la mémoire plutôt que d'utiliser toute valeur qu'il aurait pu mettre en cache dans un registre ou sur une pile. C'est la récupération de mémoire qui ralentit tout.

Conclusion

Les pointeurs et les références dérangent beaucoup de développeurs, ils sont également très importants dans un langage tel que C ++. Il est donc important de prendre votre temps pour comprendre le concept afin de ne pas avoir de problèmes par la suite. Le prochain article concerne le casting en C++.

Cette leçon représente un chapitre de C ++ Succinctly, un eBook gratuit de l’équipe de Syncfusion..