web-dev-qa-db-fra.com

Quel est l'intérêt des pointeurs const?

Je ne parle pas de pointeurs vers des valeurs const, mais de pointeurs const eux-mêmes.

J'apprends le C et le C++ au-delà des trucs très basiques et jusqu'à aujourd'hui, j'ai réalisé que les pointeurs sont passés par valeur aux fonctions, ce qui est logique. Cela signifie qu'à l'intérieur d'une fonction, je peux faire pointer le pointeur copié vers une autre valeur sans affecter le pointeur d'origine de l'appelant.

Alors, quel est l'intérêt d'avoir un en-tête de fonction qui dit:

void foo(int* const ptr);

À l'intérieur d'une telle fonction, vous ne pouvez pas faire pointer ptr vers autre chose parce que c'est const et vous ne voulez pas qu'il soit modifié, mais une fonction comme celle-ci:

void foo(int* ptr);

Est-ce que le travail fonctionne aussi bien! car le pointeur est quand même copié et le pointeur de l'appelant n'est pas affecté même si vous modifiez la copie. Alors, quel est l'avantage de const?

145
R. Ruiz.

const est un outil que vous devez utiliser pour poursuivre un concept C++ très important:

Trouvez des bogues au moment de la compilation, plutôt qu'au moment de l'exécution, en demandant au compilateur d'appliquer ce que vous voulez dire.

Même si cela ne change pas la fonctionnalité, l'ajout de const génère une erreur de compilation lorsque vous faites des choses que vous ne vouliez pas faire. Imaginez la faute de frappe suivante:

void foo(int* ptr)
{
    ptr = 0;// oops, I meant *ptr = 0
}

Si tu utilises int* const, cela générerait une erreur de compilation car vous changez la valeur en ptr. Ajouter des restrictions via la syntaxe est une bonne chose en général. N'allez pas trop loin - l'exemple que vous avez donné est un cas où la plupart des gens ne prennent pas la peine d'utiliser const.

200
tenfour

Je tiens à utiliser niquementconst arguments car cela permet plus de vérifications du compilateur: si je réattribue accidentellement une valeur d'argument à l'intérieur de la fonction, le compilateur me mord.

Je réutilise rarement des variables, il est plus propre de créer de nouvelles variables pour contenir de nouvelles valeurs, donc essentiellement tous mes déclarations de variables sont const (sauf dans certains cas comme les variables de boucle où const empêcherait le code de fonctionner).

Notez que cela n'a de sens que dans la définition d'une fonction. Il n'appartient pas à la déclaration, ce que voit l'utilisateur. Et l'utilisateur ne se soucie pas si j'utilise const pour les paramètres à l'intérieur de la fonction.

Exemple:

// foo.h
int frob(int x);
// foo.cpp
int frob(int const x) {
   MyConfigType const config = get_the_config();
   return x * config.scaling;
}

Remarquez comment l'argument et la variable locale sont const. Ni nécessaire mais avec des fonctions encore plus grandes, cela m'a évité à plusieurs reprises de faire des erreurs.

75
Konrad Rudolph

Votre question touche à quelque chose de plus général: les arguments de fonction doivent-ils être const?

La constance des arguments de valeur (comme votre pointeur) est un détail d'implémentation, et il pas fait partie de la déclaration de fonction. Cela signifie que votre fonction est toujours la suivante:

void foo(T);

Il appartient entièrement à l'implémenteur de la fonction, qu'elle veuille utiliser la variable d'argument functions-scope de manière mutable ou constante:

// implementation 1
void foo(T const x)
{
  // I won't touch x
  T y = x;
  // ...
}

// implementation 2
void foo(T x)
{
  // l33t coding skillz
  while (*x-- = zap()) { /* ... */ }
}

Donc, suivez la règle simple pour ne jamais mettre const dans la déclaration (en-tête), et mettez-la dans la définition (implémentation) si vous ne voulez pas ou n'avez pas besoin de modifier la variable.

20
Kerrek SB

Le qualificatif de niveau supérieur const est ignoré dans les déclarations, de sorte que les déclarations de la question déclarent exactement la même fonction. En revanche, dans la définition (implémentation) le compilateur vérifiera que si vous marquez le pointeur comme const, il n'est pas modifié à l'intérieur du corps de la fonction.

Le mot-clé const en contient beaucoup, il est assez complexe. En règle générale, l'ajout d'un grand nombre de const à votre programme est considéré comme une bonne pratique de programmation, recherchez sur le Web "const correct" et vous trouverez de nombreuses informations à ce sujet.

Le mot clé const est un soi-disant "qualificatif de type", les autres sont volatile et restrict. Au moins volatile suit les mêmes règles (déroutantes) que const.


Tout d'abord, le mot clé const a deux objectifs. Le plus évident est de protéger les données (et les pointeurs) contre toute utilisation abusive intentionnelle ou accidentelle en les rendant en lecture seule. Toute tentative de modification d'une variable const sera repérée par le compilateur au moment de la compilation.

Mais il y a aussi un autre objectif dans tout système avec mémoire en lecture seule, à savoir garantir qu'une certaine variable est allouée à l'intérieur de cette mémoire - ce pourrait être EEPROM ou flash par exemple. Ce sont des mémoires non volatiles, NVM. Une variable allouée dans NVM suivra bien sûr toutes les règles d'une variable const.

Il existe plusieurs façons d'utiliser le mot clé const:

Déclarez une variable constante.

Cela peut être fait soit comme

const int X=1; or
int const X=1;

Ces deux formes sont complètement équivalentes . Ce dernier style est considéré comme un mauvais style et ne doit pas être utilisé.

La raison pour laquelle la deuxième ligne est considérée comme un mauvais style est probablement parce que les "spécificateurs de classe de stockage" tels que statiques et externes peuvent également être déclarés après le réel type, int static etc. Mais le faire pour les spécificateurs de classe de stockage est étiqueté comme une fonction obsolète par le comité C (ISO 9899 N1539 draft, 6.11.5). Par conséquent, pour des raisons de cohérence, il ne faut pas non plus écrire de qualificateurs de type de cette manière. Il ne sert à rien d'autre qu'à confondre le lecteur de toute façon.

Déclarez un pointeur sur une variable constante.

const int* ptr = &X;

Cela signifie que le contenu de "X" ne peut pas être modifié. Il s'agit de la façon normale de déclarer des pointeurs comme celui-ci, principalement dans le cadre des paramètres de fonction pour "const correct". Parce que 'X' n'a pas besoin d'être déclaré const, il peut s'agir de n'importe quelle variable. En d'autres termes, vous pouvez toujours "mettre à niveau" une variable vers const. Techniquement, C permet également de rétrograder de const à une variable simple par des transtypages explicites, mais cela est considéré comme une mauvaise programmation et les compilateurs donnent généralement des avertissements contre cela.

Déclarez un pointeur constant

int* const ptr = &X;

Cela signifie que le pointeur lui-même est constant. Vous pouvez modifier ce qu'il pointe, mais vous ne pouvez pas modifier le pointeur lui-même. Cela n'a pas beaucoup d'utilisations, il y en a quelques-unes, comme s'assurer qu'un pointeur pointé vers (pointer-to-pointer) n'a pas son adresse modifiée pendant qu'il est passé en paramètre à une fonction. Vous devrez écrire quelque chose de pas trop lisible comme ceci:

void func (int*const* ptrptr)

Je doute que de nombreux programmeurs C puissent obtenir le const et * juste là. Je sais [~ # ~] je [~ # ~] ne peux pas - j'ai dû vérifier auprès de GCC. Je pense que c'est pourquoi vous ne voyez que rarement cette syntaxe de pointeur à pointeur, même si elle est considérée comme une bonne pratique de programmation.

Des pointeurs constants peuvent également être utilisés pour garantir que la variable de pointeur elle-même est déclarée dans la mémoire morte, par exemple, vous pouvez déclarer une sorte de table de recherche basée sur un pointeur et l'allouer dans NVM.

Et bien sûr, comme indiqué par d'autres réponses, des pointeurs constants peuvent également être utilisés pour imposer la "correction constante".

Déclarez un pointeur constant vers des données constantes

const int* const ptr=&X;

Il s'agit des deux types de pointeurs décrits ci-dessus combinés, avec tous les attributs des deux.

Déclarez une fonction membre en lecture seule (C++)

Comme il s'agit du tag C++, je dois également mentionner que vous pouvez déclarer les fonctions membres d'une classe comme const. Cela signifie que la fonction n'est pas autorisée à modifier tout autre membre de la classe lorsqu'elle est appelée, ce qui empêche le programmeur de la classe d'erreurs accidentelles mais informe également l'appelant de la fonction membre qu'il ne gâchera rien en l'appelant. La syntaxe est:

void MyClass::func (void) const;
14
Lundin

Vous avez raison, pour l'appelant, cela ne fait absolument aucune différence. Mais pour l'auteur de la fonction, il peut s'agir d'un filet de sécurité "d'accord, je dois m'assurer de ne pas faire valoir ce mauvais point". Pas très utile mais pas inutile non plus.

C'est essentiellement la même chose que d'avoir un int const the_answer = 42 dans votre programme.

13
cnicutar

... aujourd'hui, j'ai réalisé que les pointeurs sont transmis par valeur aux fonctions, ce qui est logique.

(imo) cela n'a vraiment pas de sens par défaut. la valeur par défaut la plus raisonnable est de passer comme pointeur non réaffectable (int* const arg). c'est-à-dire que j'aurais préféré que les pointeurs soient passés comme arguments ont été implicitement déclarés const.

Alors, quel est l'avantage de const?

l'avantage est qu'il est assez simple et parfois peu clair lorsque vous modifiez l'adresse vers laquelle l'argument pointe, de sorte que vous pouvez introduire un bogue lorsqu'il n'est pas const assez facilement. la modification de l'adresse est atypique. il est plus clair de créer une variable locale si votre intention est de modifier l'adresse. de plus, la manipulation brute du pointeur est un moyen facile d'introduire des bogues.

il est donc plus clair de passer par une adresse immuable et de créer une copie (dans ces cas atypiques) lorsque vous souhaitez modifier l'adresse vers laquelle pointe l'argument:

void func(int* const arg) {
    int* a(arg);
    ...
    *a++ = value;
}

ajouter que local est pratiquement gratuit, et il réduit le risque d'erreurs, tout en améliorant la lisibilité.

à un niveau supérieur: si vous manipulez l'argument en tant que tableau, il est généralement plus clair et moins sujet aux erreurs que le client déclare l'argument en tant que conteneur/collection.

en général, l'ajout de const aux valeurs, arguments et adresses est une bonne idée car vous ne réalisez pas toujours les effets secondaires, que le compilateur applique avec joie. par conséquent, il est aussi utile que const que dans d'autres cas (par exemple, la question est similaire à `` Pourquoi devrais-je déclarer des valeurs const? ''). heureusement, nous avons aussi des références qui ne peuvent pas être réaffectées.

8
justin

Si vous faites des systèmes embarqués ou que vous programmez des pilotes de périphériques où vous avez des périphériques mappés en mémoire, les deux formes de `` const '' sont souvent utilisées, l'une pour empêcher la réaffectation du pointeur (car elle pointe vers une adresse matérielle fixe) et, si le périphérique le registre sur lequel il pointe est un registre matériel en lecture seule, puis un autre const détectera beaucoup d'erreurs au moment de la compilation plutôt qu'au moment de l'exécution.

Un registre de puce périphérique 16 bits en lecture seule pourrait ressembler à ceci:

static const unsigned short *const peripheral = (unsigned short *)0xfe0000UL;

Ensuite, vous pouvez facilement lire le registre du matériel sans avoir à recourir au langage d'assemblage:

input_Word = *peripheral;

6
Dan Haynes

int iVal = 10; int * const ipPtr = & iVal;

Tout comme une variable const normale, un pointeur const doit être initialisé à une valeur lors de la déclaration, et sa valeur ne peut pas être modifiée.

Cela signifie qu'un pointeur const pointera toujours vers la même valeur. Dans le cas ci-dessus, ipPtr pointera toujours vers l'adresse d'iVal. Cependant, étant donné que la valeur pointée est toujours non constante, il est possible de modifier la valeur pointée via le déréférencement du pointeur:

* ipPtr = 6; // autorisé, puisque pnPtr pointe vers un entier non const

5
jusathr

Votre question est vraiment de savoir pourquoi définir une variable comme un paramètre de pointeur const et pas seulement une fonction. Les mêmes règles s'appliquent ici que lorsque vous définissez une variable comme constante, s'il s'agit d'un paramètre à fonction ou d'une variable membre ou d'une variable locale.

Dans votre cas particulier, fonctionnellement, cela ne fait pas de différence comme dans de nombreux autres cas lorsque vous déclarez une variable locale comme const, mais cela impose une restriction selon laquelle vous ne pouvez pas modifier cette variable.

5
zar

La même question peut être posée pour tout autre type (pas seulement les pointeurs):

/* Why is n const? */
const char *expand(const int n) {
    if (n == 1) return "one";
    if (n == 2) return "two";
    if (n == 3) return "three";
    return "many";
}
5
pmg

Un exemple où un pointeur const est hautement applicable peut être démontré ainsi. Considérez que vous avez une classe avec un tableau dynamique à l'intérieur et que vous souhaitez passer l'accès utilisateur au tableau mais sans leur accorder les droits de modifier le pointeur. Considérer:

#include <new>
#include <string.h>

class TestA
{
    private:
        char *Array;
    public:
        TestA(){Array = NULL; Array = new (std::nothrow) char[20]; if(Array != NULL){ strcpy(Array,"Input data"); } }
        ~TestA(){if(Array != NULL){ delete [] Array;} }

        char * const GetArray(){ return Array; }
};

int main()
{
    TestA Temp;
    printf("%s\n",Temp.GetArray());
    Temp.GetArray()[0] = ' '; //You can still modify the chars in the array, user has access
    Temp.GetArray()[1] = ' '; 
    printf("%s\n",Temp.GetArray());
}

Ce qui produit:

Des données d'entrée
mettre des données

Mais si nous essayons ceci:

int main()
{
    TestA Temp;
    printf("%s\n",Temp.GetArray());
    Temp.GetArray()[0] = ' ';
    Temp.GetArray()[1] = ' ';
    printf("%s\n",Temp.GetArray());
    Temp.GetArray() = NULL; //Bwuahahahaa attempt to set it to null
}

On a:

erreur: lvalue requise comme opérande gauche de l'affectation // Drat déjoué à nouveau!

Il est donc clair que nous pouvons modifier le contenu du tableau, mais pas le pointeur du tableau. Bien si vous voulez vous assurer que le pointeur a un état cohérent lors de sa transmission à l'utilisateur. Il y a cependant un hic:

int main()
{
    TestA Temp;
    printf("%s\n",Temp.GetArray());
    Temp.GetArray()[0] = ' ';
    Temp.GetArray()[1] = ' ';
    printf("%s\n",Temp.GetArray());
    delete [] Temp.GetArray(); //Bwuahaha this actually works!
}

Nous pouvons toujours supprimer la référence mémoire du pointeur, même si nous ne pouvons pas modifier le pointeur lui-même.

Donc, si vous voulez que la référence de mémoire pointe toujours vers quelque chose (IE ne doit jamais être modifié, comme le fonctionnement actuel d'une référence), alors c'est très applicable. Si vous voulez que l'utilisateur ait un accès complet et le modifie, alors non-const est pour vous.

Modifier:

Après avoir noté le commentaire okorz001 de ne pas pouvoir affecter en raison de GetArray () étant un opérande de bonne valeur, son commentaire est tout à fait correct, mais ce qui précède s'applique toujours si vous deviez renvoyer une référence au pointeur (je suppose que je supposais que GetArray était référence), par exemple:

class TestA
{
    private:
        char *Array;
    public:
        TestA(){Array = NULL; Array = new (std::nothrow) char[20]; if(Array != NULL){ strcpy(Array,"Input data"); } }
        ~TestA(){if(Array != NULL){ delete [] Array;} }

        char * const &GetArray(){ return Array; } //Note & reference operator
        char * &GetNonConstArray(){ return Array; } //Note non-const
};

int main()
{
    TestA Temp;
    Temp.GetArray() = NULL; //Returns error
    Temp.GetNonConstArray() = NULL; //Returns no error
}

Reviendra dans le premier entraînant une erreur:

erreur: affectation de l'emplacement en lecture seule 'Temp.TestA :: GetArray ()'

Mais le second se produira gaiement malgré les conséquences potentielles sur le dessous.

De toute évidence, la question sera posée "pourquoi voudriez-vous renvoyer une référence à un pointeur"? Il existe de rares cas où vous devez attribuer de la mémoire (ou des données) directement au pointeur d'origine en question (par exemple, créer votre propre frontal malloc/free ou new/free), mais dans ces cas, il s'agit d'une référence non constante . Une référence à un pointeur const Je ne suis pas tombé sur une situation qui le justifierait (sauf peut-être comme des variables de référence const déclarées plutôt que des types de retour?).

Considérez si nous avons une fonction qui prend un pointeur const (contre une qui n'en a pas):

class TestA
{
    private:
        char *Array;
    public:
        TestA(){Array = NULL; Array = new (std::nothrow) char[20]; if(Array != NULL){ strcpy(Array,"Input data"); } }
        ~TestA(){if(Array != NULL){ delete [] Array;} }

        char * const &GetArray(){ return Array; }

        void ModifyArrayConst(char * const Data)
        {
            Data[1]; //This is okay, this refers to Data[1]
            Data--; //Produces an error. Don't want to Decrement that.
            printf("Const: %c\n",Data[1]);
        }

        void ModifyArrayNonConst(char * Data)
        {
            Data--; //Argh noo what are you doing?!
            Data[1]; //This is actually the same as 'Data[0]' because it's relative to Data's position
            printf("NonConst: %c\n",Data[1]);
        }
};

int main()
{
    TestA Temp;
    Temp.ModifyArrayNonConst("ABCD");
    Temp.ModifyArrayConst("ABCD");
}

L'erreur dans la const produit donc le message:

erreur: décrémentation du paramètre en lecture seule 'Données'

Ce qui est bien car nous ne voulons probablement pas le faire, sauf si nous voulons provoquer les problèmes indiqués dans les commentaires. Si nous éditons le décrément dans la fonction const, les événements suivants se produisent:

NonConst: A
Const: B

De toute évidence, même si A est "Data [1]", il est traité comme "Data [0]" car le pointeur NonConst a autorisé l'opération de décrémentation. Avec la const implémentée, comme l'écrit une autre personne, nous interceptons le bogue potentiel avant qu'il ne se produise.

Une autre considération principale est qu'un pointeur const peut être utilisé comme pseudo référence, en ce que la chose sur laquelle la référence pointe ne peut pas être modifiée (on se demande si c'est peut-être la façon dont il a été implémenté). Considérer:

int main()
{
    int A = 10;
    int * const B = &A;
    *B = 20; //This is permitted
    printf("%d\n",A);
    B = NULL; //This produces an error
}

Lors de la tentative de compilation, génère l'erreur suivante:

erreur: affectation de la variable en lecture seule 'B'

Ce qui est probablement une mauvaise chose si une référence constante à A était souhaitée. Si B = NULL est commenté, le compilateur nous laissera volontiers modifier *B et donc A. Cela peut ne pas sembler utile avec des nombres entiers, mais considérez si vous aviez une position unique d'une application graphique où vous vouliez un pointeur non modifiable qui s'y référait que vous pouviez faire circuler.

Son utilisation est variable (excusez le jeu de mots involontaire), mais utilisé correctement, c'est un autre outil dans la boîte pour aider à la programmation.

4
SSight3

Passer un pointeur const à une fonction n'a pas beaucoup de sens, car il sera de toute façon passé par valeur. Ce n'est qu'une de ces choses permises par la conception générale du langage. L'interdire simplement parce que cela n'a pas de sens ferait juste la spécification de la langue. plus grande.

Si vous êtes dans une fonction, c'est bien sûr un autre cas. Avoir un pointeur qui ne peut pas changer ce qu'il pointe est une assertion qui rend le code plus clair.

4
Anders Abel

Je suppose qu'un avantage serait que le compilateur peut effectuer des optimisations plus agressives à l'intérieur de la fonction sachant que ce pointeur ne peut pas changer.

Il évite également par exemple. passer ce pointeur à une sous-fonction qui accepte une référence de pointeur non const (et pourrait donc changer le pointeur comme void f(int *&p)), mais je suis d'accord, que l'utilité est quelque peu limitée dans ce cas.

4
MartinStettner

Les pointeurs n'ont rien de spécial où vous ne voudriez jamais qu'ils soient const. Tout comme vous pouvez avoir des valeurs constantes int des membres de la classe, vous pouvez également avoir des pointeurs constants pour des raisons similaires: vous voulez vous assurer que personne ne change jamais ce qui est pointé. Les références C++ corrigent quelque peu cela, mais le comportement du pointeur est hérité de C.

3
Mark B

Je crois que cela empêcherait le code d'incrémenter ou de décrémenter le pointeur dans le corps de la fonction.

3
Mike Christensen