Comment expliquer les pointeurs C (déclaration vs opérateurs unaires) à un débutant?

141

J'ai eu le plaisir récent d'expliquer des pointeurs à un débutant en programmation C et je suis tombé sur la difficulté suivante. Cela peut ne pas sembler un problème du tout si vous savez déjà comment utiliser les pointeurs, mais essayez de regarder l'exemple suivant avec un esprit clair:

int foo = 1;
int *bar = &foo;
printf("%p\n", (void *)&foo);
printf("%i\n", *bar);

Pour le débutant absolu, le résultat peut être surprenant. À la ligne 2, il venait de déclarer que * bar était & foo, mais à la ligne 4, il s'avère que * bar est en fait foo au lieu de & foo!

La confusion, pourrait-on dire, provient de l'ambiguïté du symbole *: à la ligne 2, il est utilisé pour déclarer un pointeur. À la ligne 4, il est utilisé comme un opérateur unaire qui récupère la valeur sur laquelle pointe le pointeur. Deux choses différentes, non?

Cependant, cette «explication» n'aide pas du tout un débutant. Il introduit un nouveau concept en signalant une différence subtile. Cela ne peut pas être la bonne façon de l'enseigner.

Alors, comment Kernighan et Ritchie l'ont-ils expliqué?

L'opérateur unaire * est l'opérateur d'indirection ou de déréférencement; lorsqu'il est appliqué à un pointeur, il accède à l'objet vers lequel pointe le pointeur. […]

La déclaration du pointeur ip, int *ipest conçue comme un mnémonique; il dit que l'expression *ipest un int. La syntaxe de la déclaration d'une variable imite la syntaxe des expressions dans lesquelles la variable peut apparaître .

int *ipdevrait être lu comme " *ipretournera un int"? Mais pourquoi alors l'affectation après la déclaration ne suit-elle pas ce modèle? Que faire si un débutant souhaite initialiser la variable? int *ip = 1(lire: *iprenverra un intet le intest 1) ne fonctionnera pas comme prévu. Le modèle conceptuel ne semble tout simplement pas cohérent. Est-ce que j'ai râté quelque chose?


Edit: Il a essayé de résumer les réponses ici .

armin
la source
15
La meilleure explication est de dessiner des choses sur un papier et de les relier avec des flèches;)
Maroun
16
Quand j'ai dû expliquer la syntaxe des pointeurs, j'ai toujours insisté sur le fait que *dans une déclaration il y a un jeton signifiant "déclarer un pointeur", dans les expressions c'est l'opérateur de déréférencement, et que ces deux représentent des choses différentes qui se trouvent avoir le même symbole (identique à l'opérateur de multiplication - même symbole, signification différente). C'est déroutant, mais tout ce qui est différent de l'état actuel des choses va être encore pire.
Matteo Italia
40
peut-être que l'écrire comme cela int* barrend plus évident que l'étoile fait en fait partie du type, pas de l'identifiant. Bien sûr, cela vous pose différents problèmes avec des trucs peu intuitifs comme int* a, b.
Niklas B.
9
J'ai toujours pensé que l'explication K&R était stupide et inutile. La langue utilise le même symbole pour deux choses différentes et nous devons simplement nous en occuper. *peut avoir deux significations différentes selon le contexte. Tout comme la même lettre peut être prononcée différemment selon le mot dans lequel elle se trouve, il est donc difficile d'apprendre à parler plusieurs langues. Si chaque concept / opération avait son propre symbole, nous aurions besoin de claviers beaucoup plus grands, donc les symboles sont recyclés quand cela a du sens de le faire.
Art
8
J'ai rencontré le même problème plusieurs fois en enseignant le C à d'autres et, d'après mon expérience, il peut être résolu de la manière que la plupart des gens ici suggèrent. Tout d'abord, expliquez le concept de pointeur sans la syntaxe C. Ensuite, enseignez la syntaxe et mettez l'accent sur l'astérisque dans le cadre du type ( int* p), tout en avertissant votre élève de ne pas utiliser plusieurs déclarations dans la même ligne lorsque des pointeurs sont impliqués. Lorsque l'élève a complètement compris le concept des pointeurs, expliquez à l'élève que la int *psyntaxe est est équivalente, puis expliquez le problème avec plusieurs déclarations.
Theodoros Chatzigiannakis

Réponses:

43

Pour que votre élève comprenne la signification du *symbole dans différents contextes, il doit d'abord comprendre que les contextes sont effectivement différents. Une fois qu'ils comprennent que les contextes sont différents (c'est-à-dire la différence entre le côté gauche d'un devoir et une expression générale), il n'est pas trop difficile de comprendre les différences.

Expliquez d'abord que la déclaration d'une variable ne peut pas contenir d'opérateurs (démontrez-le en montrant que placer un symbole -ou +dans une déclaration de variable provoque simplement une erreur). Ensuite, montrez qu'une expression (c'est-à-dire sur le côté droit d'une affectation) peut contenir des opérateurs. Assurez-vous que l'élève comprend qu'une expression et une déclaration de variable sont deux contextes complètement différents.

Lorsqu'ils comprennent que les contextes sont différents, vous pouvez continuer en expliquant que lorsque le *symbole est dans une déclaration de variable devant l'identificateur de variable, cela signifie «déclarer cette variable comme un pointeur». Ensuite, vous pouvez expliquer que lorsqu'il est utilisé dans une expression (en tant qu'opérateur unaire), le *symbole est «l'opérateur de déréférencement» et il signifie «la valeur à l'adresse de» plutôt que sa signification antérieure.

Pour vraiment convaincre votre élève, expliquez que les créateurs de C auraient pu utiliser n'importe quel symbole pour désigner l'opérateur de déréférencement (c'est-à-dire qu'ils auraient pu utiliser à la @place), mais pour une raison quelconque, ils ont pris la décision de conception d'utiliser *.

Dans l'ensemble, il n'y a aucun moyen d'expliquer que les contextes sont différents. Si l'élève ne comprend pas que les contextes sont différents, il ne peut pas comprendre pourquoi le *symbole peut signifier différentes choses.

Pharap
la source
80

La raison pour laquelle la sténographie:

int *bar = &foo;

dans votre exemple peut être déroutant, c'est qu'il est facile de mal interpréter cela comme étant équivalent à:

int *bar;
*bar = &foo;    // error: use of uninitialized pointer bar!

quand cela signifie en fait:

int *bar;
bar = &foo;

Écrit comme ceci, avec la déclaration de variable et l'affectation séparées, il n'y a pas de tel potentiel de confusion, et l'utilisation du parallélisme de déclaration ↔ décrit dans votre devis K&R fonctionne parfaitement:

  • La première ligne déclare une variable bar, comme *barun int.

  • La deuxième ligne attribue l'adresse de fooà bar, faisant de *bar(an int) un alias pour foo(également un int).

Lors de l'introduction de la syntaxe du pointeur C aux débutants, il peut être utile de s'en tenir initialement à ce style de séparation des déclarations de pointeur des affectations, et d'introduire uniquement la syntaxe abrégée combinée (avec des avertissements appropriés sur son potentiel de confusion) une fois que les concepts de base de l'utilisation du pointeur dans C ont été correctement internalisés.

Ilmari Karonen
la source
4
Je serais tenté typedef. typedef int *p_int;signifie qu'une variable de type p_inta la propriété qui *p_intest un int. Ensuite, nous avons p_int bar = &foo;. Encourager quiconque à créer des données non initialisées et à les attribuer plus tard comme une habitude par défaut semble ... être une mauvaise idée.
Yakk - Adam Nevraumont
6
Ce n'est que le style cérébral des déclarations C; ce n'est pas spécifique aux pointeurs. considérez int a[2] = {47,11};que ce n'est pas une initialisation de l'élément (inexistant) a[2]eiher.
Marc van Leeuwen
5
@MarcvanLeeuwen D'accord avec les lésions cérébrales. Idéalement, *devrait faire partie du type, non lié à la variable, et vous pourrez alors écrire int* foo_ptr, bar_ptrpour déclarer deux pointeurs. Mais il déclare en fait un pointeur et un entier.
Barmar
1
Il ne s'agit pas seulement de déclarations / affectations «abrégées». Le problème se pose à nouveau au moment où vous souhaitez utiliser des pointeurs comme arguments de fonction.
armin
30

À court de déclarations

Il est bon de connaître la différence entre la déclaration et l'initialisation. Nous déclarons les variables en tant que types et les initialisons avec des valeurs. Si nous faisons les deux en même temps, nous l'appelons souvent une définition.

1. int a; a = 42;

int a;
a = 42;

Nous déclarons un intnommé a . Puis on l'initialise en lui donnant une valeur 42.

2. int a = 42;

Nous déclarons et intnommons a et lui donnons la valeur 42. Il est initialisé avec 42. Une définition.

3. a = 43;

Lorsque nous utilisons les variables, nous disons que nous opérons sur elles. a = 43est une opération d'affectation. Nous attribuons le nombre 43 à la variable a.

En disant

int *bar;

nous déclarons que bar est un pointeur vers un int. En disant

int *bar = &foo;

nous déclarons bar et l'initialisons avec l'adresse de foo .

Après avoir initialisé la barre, nous pouvons utiliser le même opérateur, l'astérisque, pour accéder à la valeur de foo et l'utiliser . Sans l'opérateur, nous accédons et opérons sur l'adresse vers laquelle pointe le pointeur.

En plus de cela, je laisse parler l'image.

Quoi

Une ASCIIMATION simplifiée sur ce qui se passe. (Et voici une version player si vous souhaitez mettre en pause etc.)

          ASCIIMATION

Morpfh
la source
22

La deuxième déclaration int *bar = &foo;peut être visualisée en images dans la mémoire comme,

   bar           foo
  +-----+      +-----+
  |0x100| ---> |  1  |
  +-----+      +-----+ 
   0x200        0x100

Voici maintenant barun pointeur de type intcontenant l'adresse &de foo. En utilisant l'opérateur unaire, *nous déférons pour récupérer la valeur contenue dans 'foo' en utilisant le pointeur bar.

EDIT : Mon approche avec les débutants est d'expliquer le memory addressd'une variable ie

Memory Address:Chaque variable est associée à une adresse fournie par le système d'exploitation. Dans int a;, &aest l'adresse de la variable a.

Continuez à expliquer les types de variables de base dans Cas,

Types of variables: Les variables peuvent contenir des valeurs de types respectifs mais pas des adresses.

int a = 10; float b = 10.8; char ch = 'c'; `a, b, c` are variables. 

Introducing pointers: Comme indiqué ci-dessus, les variables, par exemple

 int a = 10; // a contains value 10
 int b; 
 b = &a;      // ERROR

Il est possible d'assigner b = amais pas b = &a, car la variable bpeut contenir une valeur mais pas une adresse, nous avons donc besoin de pointeurs .

Pointer or Pointer variables :Si une variable contient une adresse, elle est appelée variable de pointeur. Utilisez *dans la déclaration pour indiquer qu'il s'agit d'un pointeur.

 Pointer can hold address but not value
 Pointer contains the address of an existing variable.
 Pointer points to an existing variable
Sunil Bojanapally
la source
3
Le problème est que lire int *ipcomme "ip est un pointeur (*) de type int" vous avez des ennuis en lisant quelque chose comme x = (int) *ip.
armin
2
@abw C'est quelque chose de complètement différent, d'où les parenthèses. Je ne pense pas que les gens auront du mal à comprendre la différence entre les déclarations et le casting.
bzeaman
@abw In x = (int) *ip;, obtenez la valeur en déréférençant le pointeur ipet convertissez la valeur en intquel que soit le type ip.
Sunil Bojanapally
1
@BennoZeeman Vous avez raison: le casting et les déclarations sont deux choses différentes. J'ai essayé de faire allusion au rôle différent de l'astérisque: 1er "ce n'est pas un int, mais un pointeur vers int" 2nd "cela vous donnera l'int, mais pas le pointeur vers int".
armin
2
@abw: Quelle est la raison pour laquelle l' enseignement int* bar = &foo;fait des charges plus de sens. Oui, je sais que cela pose des problèmes lorsque vous déclarez plusieurs pointeurs dans une seule déclaration. Non, je ne pense pas que cela compte du tout.
Courses de légèreté en orbite
17

En regardant les réponses et les commentaires ici, il semble y avoir un accord général sur le fait que la syntaxe en question peut être déroutante pour un débutant. La plupart d'entre eux proposent quelque chose dans ce sens:

  • Avant d'afficher du code, utilisez des diagrammes, des croquis ou des animations pour illustrer le fonctionnement des pointeurs.
  • Lors de la présentation de la syntaxe, expliquez les deux rôles différents du symbole astérisque . De nombreux didacticiels manquent ou éludent cette partie. La confusion s'ensuit ("Lorsque vous divisez une déclaration de pointeur initialisée en une déclaration et une affectation ultérieure, vous devez vous rappeler de supprimer la FAQ *" - comp.lang.c ) J'espérais trouver une approche alternative, mais je suppose que c'est le chemin à parcourir.

Vous pouvez écrire int* barau lieu de int *barpour souligner la différence. Cela signifie que vous ne suivrez pas l'approche K&R "déclaration mimics use", mais l' approche Stroustrup C ++ :

Nous ne déclarons *barpas être un entier. Nous déclarons barêtre un int*. Si nous voulons initialiser une variable nouvellement créée dans la même ligne, il est clair que nous avons affaire à bar, non *bar.int* bar = &foo;

Les inconvénients:

  • Vous devez avertir votre étudiant du problème de déclaration de pointeurs multiples ( int* foo, barvs int *foo, *bar).
  • Vous devez les préparer à un monde de souffrance . De nombreux programmeurs veulent voir l'astérisque à côté du nom de la variable, et ils prendront beaucoup de temps pour justifier leur style. Et de nombreux guides de style appliquent cette notation explicitement (style de codage du noyau Linux, guide de style NASA C, etc.).

Edit: Une approche différente qui a été suggérée, est de suivre la voie «mimique» de K&R, mais sans la syntaxe «abrégée» (voir ici ). Dès que vous omettez de faire une déclaration et une affectation dans la même ligne , tout paraîtra beaucoup plus cohérent.

Cependant, tôt ou tard, l'étudiant devra traiter les pointeurs comme arguments de fonction. Et des pointeurs comme types de retour. Et des pointeurs vers des fonctions. Vous devrez expliquer la différence entre int *func();et int (*func)();. Je pense que tôt ou tard, les choses s'effondreront. Et peut-être que plus tôt est mieux que plus tard.

armin
la source
16

Il y a une raison pour laquelle le style K&R favorise int *pet le style Stroustrup int* p; les deux sont valides (et signifient la même chose) dans chaque langue, mais comme Stroustrup l'a dit:

Le choix entre "int * p;" et "int * p;" ne concerne pas le bien et le mal, mais le style et l’accent. C a mis l'accent sur les expressions; les déclarations n'étaient souvent considérées que comme un mal nécessaire. C ++, d'autre part, met fortement l'accent sur les types.

Maintenant, puisque vous essayez d'enseigner C ici, cela suggère que vous devriez mettre davantage l'accent sur les expressions que sur les types, mais certaines personnes peuvent plus facilement mettre l'accent sur un accent plus rapidement que sur l'autre, et c'est à propos d'eux plutôt que du langage.

Par conséquent, certaines personnes trouveront plus facile de partir de l'idée que un int*est une chose différente de un intet partir de là.

Si quelqu'un ne Grok rapidement la façon de voir ce que les utilisations int* bard'avoir barcomme une chose qui n'est pas un int, mais un pointeur vers int, puis ils vont voir rapidement que *barest en train de faire quelque chose à bar, et le reste suivra. Une fois que vous avez fait cela, vous pouvez expliquer plus tard pourquoi les codeurs C ont tendance à préférerint *bar .

Ou pas. S'il y avait une façon dont tout le monde avait compris le concept pour la première fois, vous n'auriez eu aucun problème en premier lieu, et la meilleure façon de l'expliquer à une personne ne serait pas nécessairement la meilleure façon de l'expliquer à une autre.

Jon Hanna
la source
1
J'aime l'argument de Stroustrup, mais je me demande pourquoi il a choisi le symbole & pour désigner les références - un autre écueil possible.
armin
1
@abw Je pense qu'il a vu la symétrie si nous pouvons le faire, int* p = &anous pouvons le faire int* r = *p. Je suis à peu près sûr qu'il l'a couvert dans La conception et l'évolution de C ++ , mais cela fait longtemps que je n'ai pas lu cela, et j'ai bêtement prêté ma copie à quelqu'un.
Jon Hanna
3
Je suppose que tu veux dire int& r = *p. Et je parie que l'emprunteur essaie toujours de digérer le livre.
armin
@abw, oui c'est exactement ce que je voulais dire. Hélas, les fautes de frappe dans les commentaires ne soulèvent pas d'erreurs de compilation. Le livre est en fait une lecture assez rapide.
Jon Hanna
4
L'une des raisons pour lesquelles je préfère la syntaxe de Pascal (comme généralement étendue) à celle de C est qu'elle Var A, B: ^Integer;indique clairement que le type "pointeur vers entier" s'applique à la fois à Aet B. L'utilisation d'un K&Rstyle int *a, *best également réalisable; mais une déclaration comme int* a,b;, cependant, a l'air d'être aet bsont toutes les deux déclarées comme int*, mais en réalité, elle déclare acomme un int*et bcomme un int.
supercat
9

tl; dr:

Q: Comment expliquer les pointeurs C (déclaration vs opérateurs unaires) à un débutant?

R: ne le faites pas. Expliquez les pointeurs au débutant et montrez-leur comment représenter leurs concepts de pointeurs dans la syntaxe C après.


J'ai eu le plaisir récent d'expliquer des pointeurs à un débutant en programmation C et je suis tombé sur la difficulté suivante.

IMO, la syntaxe C n'est pas terrible, mais n'est pas non plus merveilleuse: ce n'est ni un grand obstacle si vous comprenez déjà les pointeurs, ni aucune aide pour les apprendre.

Par conséquent: commencez par expliquer les pointeurs et assurez-vous qu'ils les comprennent vraiment:

  • Expliquez-les avec des diagrammes encadrés et flèches. Vous pouvez le faire sans adresses hexadécimales, si elles ne sont pas pertinentes, affichez simplement les flèches pointant soit vers une autre boîte, soit vers un symbole nul.

  • Expliquez avec un pseudo-code: écrivez simplement l' adresse de foo et la valeur stockée dans bar .

  • Ensuite, lorsque votre novice comprend ce que sont les pointeurs, pourquoi et comment les utiliser; puis montrez le mappage sur la syntaxe C.

Je soupçonne que la raison pour laquelle le texte K&R ne fournit pas de modèle conceptuel est qu'ils comprenaient déjà les pointeurs et supposaient probablement que tous les autres programmeurs compétents à l'époque le faisaient aussi. Le mnémonique n'est qu'un rappel de la mise en correspondance du concept bien compris à la syntaxe.

Inutile
la source
En effet; Commencez par la théorie, la syntaxe vient plus tard (et ce n'est pas important). Notez que la théorie de l'utilisation de la mémoire ne dépend pas de la langue. Ce modèle de boîte et de flèches vous aidera avec des tâches dans n'importe quel langage de programmation.
oɔɯǝɹ
Voir ici quelques exemples (bien que google vous aidera également) eskimo.com/~scs/cclass/notes/sx10a.html
oɔɯǝɹ
7

Ce problème est quelque peu déroutant lorsque vous commencez à apprendre C.

Voici les principes de base qui pourraient vous aider à démarrer:

  1. Il n'y a que quelques types de base en C:

    • char: une valeur entière de 1 octet.

    • short: une valeur entière de 2 octets.

    • long: une valeur entière de 4 octets.

    • long long: une valeur entière de 8 octets.

    • float: une valeur non entière avec la taille de 4 octets.

    • double: une valeur non entière de 8 octets.

    Notez que la taille de chaque type est généralement définie par le compilateur et non par le standard.

    Les types entiers short, longet long longsont généralement suivies int.

    Cependant, ce n'est pas une obligation et vous pouvez les utiliser sans l'extension int.

    Alternativement, vous pouvez simplement énoncer int, mais cela pourrait être interprété différemment par différents compilateurs.

    Donc pour résumer ceci:

    • shortest identique short intmais pas nécessairement identique à int.

    • longest identique long intmais pas nécessairement identique à int.

    • long longest identique long long intmais pas nécessairement identique à int.

    • Sur un compilateur donné, intest soit short intou long intsoit long long int.

  2. Si vous déclarez une variable d'un certain type, vous pouvez également déclarer une autre variable pointant vers elle.

    Par exemple:

    int a;

    int* b = &a;

    Donc, en substance, pour chaque type de base, nous avons également un type de pointeur correspondant.

    Par exemple: shortet short*.

    Il y a deux façons de "regarder" la variable b (c'est probablement ce qui déroute la plupart des débutants) :

    • Vous pouvez considérer bcomme une variable de type int*.

    • Vous pouvez considérer *bcomme une variable de type int.

    Par conséquent, certaines personnes déclarent int* b, tandis que d'autres déclarent int *b.

    Mais le fait est que ces deux déclarations sont identiques (les espaces n'ont pas de sens).

    Vous pouvez utiliser soit bcomme pointeur vers une valeur entière, soit *bcomme valeur entière pointée réelle.

    Vous pouvez obtenir (lire) la valeur pointue: int c = *b.

    Et vous pouvez définir (écrire) la valeur pointue: *b = 5.

  3. Un pointeur peut pointer vers n'importe quelle adresse mémoire, et pas seulement vers l'adresse d'une variable que vous avez précédemment déclarée. Cependant, vous devez être prudent lorsque vous utilisez des pointeurs afin d'obtenir ou de définir la valeur située à l'adresse mémoire pointée.

    Par exemple:

    int* a = (int*)0x8000000;

    Ici, nous avons une variable apointant vers l'adresse mémoire 0x8000000.

    Si cette adresse mémoire n'est pas mappée dans l'espace mémoire de votre programme, toute opération de lecture ou d'écriture à l'aide *aentraînera probablement le blocage de votre programme, en raison d'une violation d'accès à la mémoire.

    Vous pouvez modifier en toute sécurité la valeur de a, mais vous devez faire très attention en modifiant la valeur de *a.

  4. Le type void*est exceptionnel dans le fait qu'il n'a pas de «type valeur» correspondant qui peut être utilisé (c'est-à-dire que vous ne pouvez pas déclarer void a). Ce type est utilisé uniquement comme un pointeur général vers une adresse mémoire, sans spécifier le type de données qui réside dans cette adresse.

barak manos
la source
7

Peut-être que le parcourir un peu plus facilite les choses:

#include <stdio.h>

int main()
{
    int foo = 1;
    int *bar = &foo;
    printf("%i\n", foo);
    printf("%p\n", &foo);
    printf("%p\n", (void *)&foo);
    printf("%p\n", &bar);
    printf("%p\n", bar);
    printf("%i\n", *bar);
    return 0;
}

Demandez-leur de vous dire ce qu'ils attendent de la sortie sur chaque ligne, puis demandez-leur d'exécuter le programme et de voir ce qui apparaît. Expliquez leurs questions (la version nue qui s'y trouve en suscitera certainement quelques-unes - mais vous pourrez vous soucier du style, de la rigueur et de la portabilité plus tard). Ensuite, avant que leur esprit ne se transforme en bouillie après avoir trop réfléchi ou qu'ils ne deviennent un zombie après le déjeuner, écrivez une fonction qui prend une valeur et la même qui prend un pointeur.

D'après mon expérience, il surmonte ce "pourquoi est-ce que cela s'imprime de cette façon?" bosse, puis montrant immédiatement pourquoi cela est utile dans les paramètres de fonction en jouant sur le terrain (en prélude à certains éléments de base de K&R comme l'analyse de chaînes / traitement de tableau) qui rend la leçon non seulement logique, mais collante.

La prochaine étape consiste à les amener à vous expliquer comment se i[0]rapporte à &i. S'ils peuvent le faire, ils ne l'oublieront pas et vous pourrez commencer à parler de structures, même un peu à l'avance, juste pour que cela pénètre.

Les recommandations ci-dessus sur les boîtes et les flèches sont également bonnes, mais elles peuvent également aboutir à une discussion approfondie sur le fonctionnement de la mémoire - ce qui est un discours qui doit avoir lieu à un moment donné, mais peut détourner l'attention du point immédiatement à portée de main. : comment interpréter la notation du pointeur en C.

zxq9
la source
C'est un bon exercice. Mais la question que je voulais soulever est une question syntaxique spécifique qui pourrait avoir un impact sur le modèle mental que les élèves construisent. Considérez ceci: int foo = 1;. Maintenant , c'est OK: int *bar; *bar = foo;. Ce n'est pas OK:int *bar = foo;
armin
1
@abw La seule chose qui a du sens, c'est ce que les élèves finissent par se dire. Cela signifie "en voir un, en faire un, en enseigner un". Vous ne pouvez pas protéger ou prédire la syntaxe ou le style qu'ils verront dans la jungle (même vos anciens dépôts!), Vous devez donc montrer suffisamment de permutations pour que les concepts de base soient compris indépendamment du style - et puis commencez à leur apprendre pourquoi certains styles ont été adoptés. Comme l'enseignement de l'anglais: expression de base, idiomes, styles, styles particuliers dans un certain contexte. Pas facile, malheureusement. En tout cas, bonne chance!
zxq9
6

Le type de l' expression *bar est int; ainsi, le type de la variable (et de l'expression) barest int *. Étant donné que la variable a un type pointeur, son initialiseur doit également avoir un type pointeur.

Il existe une incohérence entre l'initialisation et l'affectation des variables de pointeur; c'est juste quelque chose qui doit être appris à la dure.

John Bode
la source
3
En regardant les réponses ici, j'ai le sentiment que de nombreux programmeurs expérimentés ne peuvent même plus voir le problème . Je suppose que c'est un sous-produit de «apprendre à vivre avec des incohérences».
armin
3
@abw: les règles d'initialisation sont différentes des règles d'affectation; pour les types arithmétiques scalaires, les différences sont négligeables, mais elles sont importantes pour les types pointeur et agrégat. C'est quelque chose que vous devrez expliquer avec tout le reste.
John Bode
5

Je préfère le lire comme le premier *s'applique à intplus de bar.

int  foo = 1;           // foo is an integer (int) with the value 1
int* bar = &foo;        // bar is a pointer on an integer (int*). it points on foo. 
                        // bar value is foo address
                        // *bar value is foo value = 1

printf("%p\n", &foo);   // print the address of foo
printf("%p\n", bar);    // print the address of foo
printf("%i\n", foo);    // print foo value
printf("%i\n", *bar);   // print foo value
Grorel
la source
2
Ensuite, vous devez expliquer pourquoi int* a, bne fait pas ce qu'ils pensent faire.
Pharap
4
C'est vrai, mais je ne pense pas que cela int* a,bdevrait être utilisé du tout. Pour une meilleure lisibilité, mise à jour, etc ... il ne devrait y avoir qu'une seule déclaration de variable par ligne et jamais plus. C'est quelque chose à expliquer aux débutants aussi, même si le compilateur peut le gérer.
grorel
C'est l'opinion d'un homme cependant. Il y a des millions de programmeurs qui sont tout à fait d'accord pour déclarer plus d'une variable par ligne et le faire quotidiennement dans le cadre de leur travail. Vous ne pouvez pas cacher aux étudiants d'autres façons de faire les choses, il est préférable de leur montrer toutes les alternatives et de les laisser décider dans quelle direction ils veulent faire les choses, car s'ils deviennent un jour employés, ils devront suivre un certain style qui ils peuvent ou non être à l'aise avec. Pour un programmeur, la polyvalence est un très bon trait à avoir.
Pharap du
1
Je suis d'accord avec @grorel. Il est plus facile de penser *comme faisant partie du type et de simplement décourager int* a, b. Sauf si vous préférez dire que *ac'est de type intplutôt que de apointer vers int...
Kevin Ushey
@grorel a raison: int *a, b;ne devrait pas être utilisé. Déclarer deux variables avec des types différents dans la même instruction est une pratique plutôt médiocre et un bon candidat pour les problèmes de maintenance sur toute la ligne. C'est peut-être différent pour ceux d'entre nous qui travaillent dans le domaine embarqué où un int*et un intsont souvent de tailles différentes et parfois stockés dans des emplacements de mémoire complètement différents. C'est l'un des nombreux aspects du langage C qui serait le mieux enseigné car «c'est permis, mais ne le faites pas».
Evil Dog Pie
5
int *bar = &foo;

Question 1: Qu'est-ce que c'est bar?

Ans: C'est une variable pointeur (à taper int). Un pointeur doit pointer vers un emplacement mémoire valide et plus tard, il doit être déréférencé (* bar) à l'aide d'un opérateur unaire *afin de lire la valeur stockée à cet emplacement.

Question 2: Quel est &foo?

Ans: foo est une variable de type int .qui est stockée dans un emplacement mémoire valide et cet emplacement nous l'obtenons de l'opérateur &alors maintenant ce que nous avons est un emplacement mémoire valide &foo.

Donc, les deux sont réunis, c'est-à-dire que le pointeur avait besoin d'un emplacement mémoire valide et qui est obtenu par &fool'initialisation est bonne.

Maintenant, le pointeur barpointe vers un emplacement de mémoire valide et la valeur qui y est stockée peut être obtenue en le déréférençant, c'est-à-dire*bar

Gopi
la source
5

Vous devez signaler un débutant qui * a une signification différente dans la déclaration et dans l'expression. Comme vous le savez, * dans l'expression est un opérateur unaire, et * dans la déclaration n'est pas un opérateur et juste une sorte de syntaxe se combinant avec le type pour faire savoir au compilateur qu'il s'agit d'un type pointeur. il est préférable de dire un débutant, "* a une signification différente. Pour comprendre la signification de *, vous devriez trouver où * est utilisé"

Yongkil Kwon
la source
4

Je pense que le diable est dans l'espace.

J'écrirais (non seulement pour le débutant, mais aussi pour moi): int * bar = & foo; au lieu de int * bar = & foo;

cela devrait mettre en évidence la relation entre la syntaxe et la sémantique

rpaulin56
la source
4

Il a déjà été noté que * a plusieurs rôles.

Il existe une autre idée simple qui peut aider un débutant à comprendre les choses:

Pensez que "=" a également plusieurs rôles.

Lorsque l'affectation est utilisée sur la même ligne avec la déclaration, considérez-la comme un appel au constructeur et non comme une affectation arbitraire.

Quand tu vois:

int *bar = &foo;

Pensez que c'est presque équivalent à:

int *bar(&foo);

Les parenthèses ont préséance sur l'astérisque, ainsi "& foo" est beaucoup plus facilement attribué intuitivement à "bar" plutôt qu'à "* bar".

morfizm
la source
4

J'ai vu cette question il y a quelques jours, puis j'ai lu l'explication de la déclaration de type de Go sur le blog Go . Il commence par donner un compte rendu des déclarations de type C, ce qui semble être une ressource utile à ajouter à ce fil, même si je pense qu'il y a déjà des réponses plus complètes.

C a adopté une approche inhabituelle et intelligente de la syntaxe des déclarations. Au lieu de décrire les types avec une syntaxe spéciale, on écrit une expression impliquant l'élément déclaré et indique le type que cette expression aura. Donc

int x;

déclare x comme un int: l'expression 'x' aura le type int. En général, pour comprendre comment écrire le type d'une nouvelle variable, écrivez une expression impliquant cette variable qui s'évalue en un type de base, puis placez le type de base à gauche et l'expression à droite.

Ainsi, les déclarations

int *p;
int a[3];

déclarent que p est un pointeur vers int parce que '* p' a le type int, et que a est un tableau d'entiers parce que a [3] (ignorant la valeur d'index particulière, qui est punie pour être la taille du tableau) a le type int.

(Il décrit ensuite comment étendre cette compréhension aux pointeurs de fonction, etc.)

C'est une façon à laquelle je n'y ai pas pensé auparavant, mais cela semble être un moyen assez simple de tenir compte de la surcharge de la syntaxe.

Andy Turner
la source
3

Si le problème est la syntaxe, il peut être utile d'afficher un code équivalent avec template / using.

template<typename T>
using ptr = T*;

Cela peut ensuite être utilisé comme

ptr<int> bar = &foo;

Après cela, comparez la syntaxe normale / C avec cette approche C ++ uniquement. Ceci est également utile pour expliquer les pointeurs const.

MI3Guy
la source
2
Pour les débutants, ce sera beaucoup plus déroutant.
Karsten
Mon avis était que vous ne montriez pas la définition de ptr. Utilisez-le simplement pour les déclarations de pointeur.
MI3Guy
3

La source de confusion vient du fait que le *symbole peut avoir des significations différentes en C, selon le fait dans lequel il est utilisé. Pour expliquer le pointeur à un débutant, la signification de* symbole dans un contexte différent doit être expliquée.

Dans la déclaration

int *bar = &foo;  

le *symbole n'est pas l'opérateur d'indirection . Au lieu de cela, cela aide à spécifier le type d' barinformation du compilateur qui barest un pointeur versint un fichier . En revanche, lorsqu'il apparaît dans une instruction, le *symbole (lorsqu'il est utilisé comme opérateur unaire ) effectue une indirection. Par conséquent, la déclaration

*bar = &foo;

serait erroné car il attribue l'adresse de fooà l'objet qui barpointe vers, pas à barlui-même.

piratages
la source
3

"peut-être que l'écrire comme int * bar rend plus évident que l'étoile fait en fait partie du type, pas de l'identifiant." Moi aussi. Et je dis que c'est un peu comme Type, mais seulement pour un seul nom de pointeur.

"Bien sûr, cela vous pose différents problèmes avec des trucs peu intuitifs comme int * a, b."

Павел Бивойно
la source
2

Ici, vous devez utiliser, comprendre et expliquer la logique du compilateur, pas la logique humaine (je sais, vous êtes un humain, mais ici vous devez imiter l'ordinateur ...).

Quand tu écris

int *bar = &foo;

le compilateur regroupe comme

{ int * } bar = &foo;

Autrement dit: voici une nouvelle variable, son nom est bar, son type est un pointeur sur int et sa valeur initiale est &foo.

Et vous devez ajouter: les =ci - dessus indique une initialisation pas afféterie, alors que dans les expressions suivantes , *bar = 2;il est un afféterie

Modifier par commentaire:

Attention: en cas de déclaration multiple, le *n'est lié qu'à la variable suivante:

int *bar = &foo, b = 2;

bar est un pointeur vers int initialisé par l'adresse de foo, b est un int initialisé à 2, et dans

int *bar=&foo, **p = &bar;

bar en pointeur fixe vers int, et p est un pointeur vers un pointeur vers un int initialisé à l'adresse ou à la barre.

Serge Ballesta
la source
2
En fait, le compilateur ne le regroupe pas comme ça: int* a, b;déclare a comme pointeur vers an int, mais b comme an int. Le *symbole a simplement deux significations distinctes: dans une déclaration, il indique un type de pointeur et dans une expression, il s'agit de l'opérateur de déréférence unaire.
tmlen
@tmlen: Ce que je voulais dire, c'est que dans l'initialisation, le *in rattacha au type, de sorte que le pointeur soit initialisé alors que dans une affectaction la valeur pointée est affectée. Mais au moins tu m'as donné un joli chapeau :-)
Serge Ballesta
0

Fondamentalement, le pointeur n'est pas une indication de tableau. Le débutant pense facilement que le pointeur ressemble à un tableau. la plupart des exemples de chaînes utilisant le

"char * pstr" il ressemble à

"char str [80]"

Mais, choses importantes, le pointeur est traité comme un simple entier dans le niveau inférieur du compilateur.

Regardons des exemples:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv, char **env)
{
    char str[] = "This is Pointer examples!"; // if we assume str[] is located in 0x80001000 address

    char *pstr0 = str;   // or this will be using with
    // or
    char *pstr1 = &str[0];

    unsigned int straddr = (unsigned int)pstr0;

    printf("Pointer examples: pstr0 = %08x\n", pstr0);
    printf("Pointer examples: &str[0] = %08x\n", &str[0]);
    printf("Pointer examples: str = %08x\n", str);
    printf("Pointer examples: straddr = %08x\n", straddr);
    printf("Pointer examples: str[0] = %c\n", str[0]);

    return 0;
}

Les résultats seront comme ceci 0x2a6b7ed0 est l'adresse de str []

~/work/test_c_code$ ./testptr
Pointer examples: pstr0 = 2a6b7ed0
Pointer examples: &str[0] = 2a6b7ed0
Pointer examples: str = 2a6b7ed0
Pointer examples: straddr = 2a6b7ed0
Pointer examples: str[0] = T

Donc, fondamentalement, gardez à l'esprit que le pointeur est une sorte d'entier. présentant l'adresse.

cpplover - Slw Essencial
la source
-1

J'expliquerais que les ints sont des objets, tout comme les flottants, etc. Un pointeur est un type d'objet dont la valeur représente une adresse en mémoire (d'où la valeur par défaut d'un pointeur NULL).

Lorsque vous déclarez un pointeur pour la première fois, vous utilisez la syntaxe type-pointer-name. Il est lu comme un "nom de pointeur entier appelé qui peut pointer vers l'adresse de n'importe quel objet entier". Nous n'utilisons cette syntaxe que lors de la déclinaison, de la même manière que nous déclarons un int comme «int num1», mais nous n'utilisons «num1» que lorsque nous voulons utiliser cette variable, et non «int num1».

int x = 5; // un objet entier avec une valeur de 5

int * ptr; // un entier avec une valeur NULL par défaut

Pour faire pointer un pointeur sur l'adresse d'un objet, nous utilisons le symbole '&' qui peut être lu comme "l'adresse de".

ptr = & x; // maintenant la valeur est l'adresse de 'x'

Comme le pointeur n'est que l'adresse de l'objet, pour obtenir la valeur réelle conservée à cette adresse, nous devons utiliser le symbole '*' qui, lorsqu'il est utilisé avant un pointeur, signifie "la valeur à l'adresse pointée par".

std :: cout << * ptr; // affiche la valeur à l'adresse

Vous pouvez expliquer brièvement que « » est un «opérateur» qui renvoie différents résultats avec différents types d'objets. Lorsqu'il est utilisé avec un pointeur, l' opérateur « » ne signifie plus «multiplié par».

Il est utile de dessiner un diagramme montrant comment une variable a un nom et une valeur et un pointeur a une adresse (le nom) et une valeur et montrer que la valeur du pointeur sera l'adresse de l'int.

user2796283
la source
-1

Un pointeur n'est qu'une variable utilisée pour stocker des adresses.

La mémoire d'un ordinateur est composée d'octets (un octet se compose de 8 bits) disposés de manière séquentielle. Chaque octet a un nombre qui lui est associé, tout comme l'index ou l'indice dans un tableau, qui est appelé l'adresse de l'octet. L'adresse de l'octet commence de 0 à un de moins que la taille de la mémoire. Par exemple, disons dans un 64 Mo de RAM, il y a 64 * 2 ^ 20 = 67108864 octets. L'adresse de ces octets débutera donc de 0 à 67108863.

entrez la description de l'image ici

Voyons ce qui se passe lorsque vous déclarez une variable.

marques int;

Comme nous le savons, un int occupe 4 octets de données (en supposant que nous utilisons un compilateur 32 bits), le compilateur réserve donc 4 octets consécutifs de la mémoire pour stocker une valeur entière. L'adresse du premier octet des 4 octets alloués est appelée adresse des marques de variable. Disons que l'adresse de 4 octets consécutifs est 5004, 5005, 5006 et 5007, alors l'adresse des marques variables sera 5004. entrez la description de l'image ici

Déclaration des variables de pointeur

Comme déjà dit, un pointeur est une variable qui stocke une adresse mémoire. Comme toute autre variable, vous devez d'abord déclarer une variable de pointeur avant de pouvoir l'utiliser. Voici comment déclarer une variable de pointeur.

Syntaxe: data_type *pointer_name;

data_type est le type du pointeur (également appelé type de base du pointeur). nom_pointer est le nom de la variable, qui peut être n'importe quel identifiant C valide.

Prenons quelques exemples:

int *ip;

float *fp;

int * ip signifie que ip est une variable pointeur capable de pointer vers des variables de type int. En d'autres termes, une variable de pointeur ip peut stocker l'adresse des variables de type int uniquement. De même, la variable pointeur fp ne peut stocker que l'adresse d'une variable de type float. Le type de variable (également appelé type de base) ip est un pointeur vers int et le type de fp est un pointeur vers float. Une variable pointeur de type pointeur vers int peut être représentée symboliquement par (int *). De même, une variable pointeur de type pointeur vers float peut être représentée par (float *)

Après avoir déclaré une variable de pointeur, l'étape suivante consiste à lui attribuer une adresse mémoire valide. Vous ne devez jamais utiliser une variable de pointeur sans lui attribuer une adresse mémoire valide, car juste après la déclaration, elle contient une valeur de garbage et peut pointer vers n'importe où dans la mémoire. L'utilisation d'un pointeur non attribué peut donner un résultat imprévisible. Cela peut même provoquer le blocage du programme.

int *ip, i = 10;
float *fp, f = 12.2;

ip = &i;
fp = &f;

Source: thecguru est de loin l'explication la plus simple mais détaillée que j'aie jamais trouvée.

Cody
la source