Le nombre de questions affichées ici montre clairement que les gens ont des problèmes assez fondamentaux à se faire une idée précise des arithmétiques.
Je suis curieux de savoir pourquoi. Ils ne m'ont jamais vraiment causé de problèmes majeurs (même si je les ai découverts au Néolithique). Afin de mieux répondre à ces questions, j'aimerais savoir ce que les gens trouvent difficile.
Donc, si vous vous débattez avec des pointeurs, ou si vous étiez récemment, mais que tout à coup vous avez compris, quels sont les aspects des pointeurs qui vous ont posé problème?
Je soupçonne que les gens vont un peu trop loin dans leurs réponses. Une compréhension de la planification, des opérations de l'unité centrale ou de la gestion de la mémoire au niveau de l'assemblage n'est pas vraiment nécessaire.
Lorsque j'enseignais, les lacunes suivantes dans la compréhension des élèves étaient la source de problèmes la plus courante:
La plupart de mes étudiants ont pu comprendre un dessin simplifié d'une partie de la mémoire, généralement la section des variables locales de la pile dans la portée actuelle. Donner généralement des adresses de fiction explicites aux différents endroits a aidé.
En résumé, je pense que si vous voulez comprendre les pointeurs, vous devez comprendre les variables et ce qu’elles sont réellement dans les architectures modernes.
Lorsque j'ai commencé à travailler avec eux, le problème le plus important que j'ai rencontré était la syntaxe.
int* ip;
int * ip;
int *ip;
sont tous les mêmes.
mais:
int* ip1, ip2; //second one isn't a pointer!
int *ip1, *ip2;
Pourquoi? parce que la partie "pointeur" de la déclaration appartient à la variable et non au type.
Et puis le déréférencement de la chose utilise une notation très similaire:
*ip = 4; //sets the value of the thing pointed to by ip to '4'
x = ip; //hey, that's not '4'!
x = *ip; //ahh... there's that '4'
Sauf si vous avez réellement besoin d'un pointeur ... vous utilisez une esperluette!
int *ip = &x;
Hourra pour la cohérence!
Ensuite, apparemment juste pour être saccadés et prouver leur habileté, beaucoup de développeurs de bibliothèques utilisent des pointeurs à pointeurs, et s’ils attendent un tableau de ces choses, pourquoi ne pas simplement passer un pointeur à cela aussi .
void foo(****ipppArr);
pour appeler cela, j'ai besoin de l'adresse du tableau de pointeurs en pointeurs en pointeurs:
foo(&(***ipppArr));
Dans six mois, lorsque je devrai gérer ce code, je passerai plus de temps à essayer de comprendre ce que tout cela signifie que de réécrire à partir de la base. (ouais, j'ai probablement mal interprété cette syntaxe - ça fait un moment que je n'ai rien fait en C. Cela me manque un peu, mais je suis un peu massochiste)
Une bonne compréhension des pointeurs nécessite une connaissance de l'architecture de la machine sous-jacente.
De nos jours, de nombreux programmeurs ne savent pas comment leur machine fonctionne, tout comme la plupart des gens qui savent conduire une voiture ne savent rien du moteur.
Lorsqu'ils traitent avec des indicateurs, les personnes qui se perdent se retrouvent généralement dans l'un des deux camps. Je suis (suis?) Dans les deux.
array[]
C'est la foule qui ne sait pas comment traduire de notation de pointeur à notation de tableau (ou ne sait même pas qu'ils sont même liés). Voici quatre manières d'accéder aux éléments d'un tableau:
int vals[5] = {10, 20, 30, 40, 50};
int *ptr;
ptr = vals;
array element pointer
notation number vals notation
vals[0] 0 10 *(ptr + 0)
ptr[0] *(vals + 0)
vals[1] 1 20 *(ptr + 1)
ptr[1] *(vals + 1)
vals[2] 2 30 *(ptr + 2)
ptr[2] *(vals + 2)
vals[3] 3 40 *(ptr + 3)
ptr[3] *(vals + 3)
vals[4] 4 50 *(ptr + 4)
ptr[4] *(vals + 4)
L'idée ici est que l'accès aux tableaux via des pointeurs semble assez simple et direct, mais une tonne de choses très compliquées et intelligentes peuvent être faites de cette façon. Certains d'entre eux peuvent laisser des programmeurs expérimentés en C/C++ confus, encore moins des débutants inexpérimentés.
reference to a pointer
Et pointer to a pointer
This est un excellent article qui explique la différence et sur lequel je vais citer et voler du code :)
Comme petit exemple, il peut être très difficile de voir exactement ce que l'auteur voulait faire si vous rencontriez quelque chose comme ceci:
//function prototype
void func(int*& rpInt); // I mean, seriously, int*& ??
int main()
{
int nvar=2;
int* pvar=&nvar;
func(pvar);
....
return 0;
}
Ou, dans une moindre mesure, quelque chose comme ceci:
//function prototype
void func(int** ppInt);
int main()
{
int nvar=2;
int* pvar=&nvar;
func(&pvar);
....
return 0;
}
En fin de compte, que résolvons-nous réellement avec tout ce charabia? Rien.
Nous avons maintenant vu la syntaxe de ptr-to-ptr et ref-to-ptr. Y a-t-il des avantages de l'un sur l'autre? J'ai peur, non. L'utilisation de l'un des deux, pour certains programmeurs ne sont que des préférences personnelles. Certains utilisateurs de ref-to-ptr disent que la syntaxe est "plus propre", tandis que d'autres, qui utilisent ptr-to-ptr, disent que la syntaxe ptr-to-ptr est plus claire pour ceux qui lisent ce que vous faites.
Cette complexité et l'interchangeabilité apparente (apparente audacieuse) avec les références, qui est souvent un autre inconvénient des pointeurs et une erreur des nouveaux arrivants, complique la compréhension des pointeurs. Il est également important de comprendre, pour finir, que les pointeurs de références sont illégaux en C et C++ pour des raisons confuses qui vous entraînent dans la sémantique lvalue
-rvalue
.
Comme le soulignait une réponse précédente, il arrive souvent que ces programmeurs de renom se croient intelligents en utilisant ******awesome_var->lol_im_so_clever()
et la plupart d'entre nous sont probablement coupables d'avoir écrit de telles atrocités, mais ce n'est tout simplement pas bon. code, et ce n'est certainement pas maintenable.
Eh bien, cette réponse s’est avérée plus longue que ce que j’avais espéré ...
Je blâme la qualité des matériaux de référence et des personnes qui enseignent, personnellement; la plupart des concepts en C (mais en particulier les pointeurs) sont tout simplement enseignés mal. Je continue de menacer d’écrire mon propre livre C (intitulé La dernière chose dont le monde a besoin est un autre livre sur le langage de programmation C), mais je n’ai ni le temps ni la patience de le faire. Je reste donc ici et jette des citations aléatoires de la norme sur des personnes.
Il y a aussi le fait que lorsque C a été conçu à l'origine, c'était supposé vous avez compris l'architecture de la machine à un niveau assez détaillé simplement parce qu'il n'y avait aucun moyen de l'éviter dans votre travail quotidien (mémoire si serrés et les processeurs étaient si lents, vous deviez comprendre comment ce que vous avez écrit affectait les performances.
Il existe un excellent article qui soutient l'idée que les pointeurs sont difficiles à lire sur le site de Joel Spolsky - The Perils of JavaSchools .
[Disclaimer - Je ne suis pas un hater Java en soi .]
La plupart des choses sont plus difficiles à comprendre si vous n'êtes pas ancré dans la connaissance "en dessous". Quand j'ai enseigné la CS, cela a été beaucoup plus facile lorsque j'ai initié mes étudiants à la programmation d'une "machine" très simple, un ordinateur décimal simulé avec des opcodes décimaux dont la mémoire était constituée de registres et d'adresses décimales. Ils mettraient des programmes très courts pour, par exemple, ajouter une série de chiffres pour obtenir un total. Ensuite, ils allaient un pas pour regarder ce qui se passait. Ils pourraient maintenir la touche "entrée" enfoncée et la regarder courir "rapidement".
Je suis sûr que presque tout le monde sur SO se demande pourquoi il est utile d’être aussi élémentaire. Nous oublions ce que c’était de ne pas savoir programmer. Jouer avec un ordinateur aussi jouet met en place des concepts sans que vous ne pouvez pas programmer, comme l'idée que le calcul est un processus pas à pas, utilisant un petit nombre de primitives de base pour construire des programmes, et le concept de variables de mémoire en tant qu'endroits où sont stockés des nombres, dans lesquels L’adresse ou le nom de la variable est distinct du nombre qu’elle contient. Il existe une distinction entre l’heure à laquelle vous entrez dans le programme et l’heure à laquelle elle "s'exécute". J’apprécie d’apprendre à programmer comme passant par une série de "bumps", tels que des programmes très simples, puis des boucles et des sous-routines, puis des tableaux, puis des E/S séquentielles, puis des pointeurs et une structure de données. Tous ces éléments sont beaucoup plus faciles à apprendre en se référant à ce que l'ordinateur fait réellement.
Enfin, lorsque vous parvenez à C, les pointeurs sont confus, bien que K & R ait très bien expliqué. La façon dont je les ai appris en C était de savoir comment les lire - de droite à gauche. Comme quand je vois int *p
dans ma tête je dis "p
pointe vers un int
". C a été inventé comme un pas en avant par rapport au langage de l’Assemblée et c’est ce qui me plaît le plus: il est proche de ce "terrain". Les pointeurs, comme toute autre chose, sont plus difficiles à comprendre si vous n’avez pas cette base.
Je n'ai pas eu d'indication avant d'avoir lu la description dans K & R. Jusque-là, les indicateurs n'avaient aucun sens. J'ai lu un tas de trucs où les gens disaient "N'apprends pas de pointeurs, ils sont déroutants et vont te faire mal à la tête et te donner des anévrismes", alors je l'ai longtemps évité et j'ai créé cet air inutile de concept difficile. .
Sinon, surtout ce que je pensais être, pourquoi voudriez-vous une variable dont vous devez tenir compte pour obtenir la valeur, et si vous voulez lui attribuer des éléments, vous devez faire des choses étranges pour que les valeurs disparaissent en eux. Je pensais que le but d'une variable est de stocker une valeur, alors pourquoi quelqu'un voulait compliquer les choses me dépassait. "Donc, avec un pointeur, vous devez utiliser le *
opérateur pour obtenir sa valeur ??? Quel genre de variable maladroite est-ce? ", pensai-je. Inutile, sans jeu de mots.
La raison pour laquelle c'était compliqué était que je ne comprenais pas qu'un pointeur était un adresse à quelque chose. Si vous expliquez que c'est une adresse, que c'est quelque chose qui contient une adresse à autre chose, et que vous pouvez manipuler cette adresse pour faire des choses utiles, je pense que cela pourrait effacer la confusion.
Une classe qui nécessitait l'utilisation de pointeurs pour accéder/modifier des ports sur un PC, l'utilisation de l'arithmétique de pointeur pour adresser différents emplacements de mémoire et l'examen de code C plus complexe qui modifiait leurs arguments m'avaient dissuadé de l'idée que les pointeurs étaient inutiles.
Voici un exemple de pointeur/tableau qui m'a donné une pause. Supposons que vous ayez deux tableaux:
uint8_t source[16] = { /* some initialization values here */ };
uint8_t destination[16];
Et votre objectif est de copier le contenu de uint8_t à partir de la destination source en utilisant memcpy (). Devinez lequel des objectifs suivants atteint cet objectif:
memcpy(destination, source, sizeof(source));
memcpy(&destination, source, sizeof(source));
memcpy(&destination[0], source, sizeof(source));
memcpy(destination, &source, sizeof(source));
memcpy(&destination, &source, sizeof(source));
memcpy(&destination[0], &source, sizeof(source));
memcpy(destination, &source[0], sizeof(source));
memcpy(&destination, &source[0], sizeof(source));
memcpy(&destination[0], &source[0], sizeof(source));
La réponse (Alerte Spoiler!) Est TOUS. "destination", "& destination", et "& destination [0]" ont tous la même valeur. "& destination" est un type différent des deux autres, mais sa valeur est toujours la même. Il en va de même pour les permutations de "source".
En passant, je préfère personnellement la première version.
Je devrais commencer par dire que C et C++ ont été les premiers langages de programmation que j'ai appris. J'ai commencé avec le C, puis j'ai suivi le C++ à l'école, puis je suis retourné au C pour le maîtriser.
La première chose qui m'a dérouté au sujet des indicateurs lors de l'apprentissage du C était simple:
char ch;
char str[100];
scanf("%c %s", &ch, str);
Cette confusion était principalement due au fait que l'on avait commencé à utiliser la référence à une variable pour les arguments OUT avant que les pointeurs ne me soient correctement présentés. Je me souviens d’avoir omis d’écrire les premiers exemples dans C pourNuls parce qu’ils étaient trop simples. seulement pour ne jamais obtenir le premier programme que j'ai écrit au travail (probablement à cause de cela).
Ce qui était déroutant à ce sujet était ce que &ch
voulait vraiment dire pourquoi str
n'en avait pas besoin.
Une fois que je me suis familiarisé avec cela, je me souviens d’avoir été confus au sujet de l’allocation dynamique. J'ai réalisé à un moment donné qu'avoir des pointeurs sur des données n'était pas extrêmement utile sans allocation dynamique, alors j'ai écrit quelque chose comme:
char * x = NULL;
if (y) {
char z[100];
x = z;
}
pour essayer d'allouer dynamiquement de l'espace. Ça n'a pas marché. Je n'étais pas sûr que cela fonctionnerait, mais je ne savais pas comment cela pourrait fonctionner autrement.
J'ai appris plus tard sur malloc
et new
, mais ils me semblaient vraiment être des générateurs de mémoire magiques. Je ne savais pas comment ils pourraient fonctionner.
Quelque temps plus tard, on m'enseignait à nouveau la récursivité (je l'avais appris par moi-même auparavant, mais j'étais en classe maintenant) et j'ai demandé comment cela fonctionnait sous le capot - où étaient stockées les variables séparées. Mon professeur a dit "sur la pile" et beaucoup de choses sont devenues claires pour moi. J'avais entendu le terme auparavant et j'avais déjà implémenté des piles de logiciels. J'avais entendu d'autres parler de "la pile" depuis longtemps, mais je l'avais oublié.
À peu près à la même époque, j’ai aussi réalisé qu’utiliser des tableaux multidimensionnels en C pouvait être très déroutant. Je savais comment ils fonctionnaient, mais ils étaient tellement faciles à s’embrouiller que j’ai décidé de travailler avec eux chaque fois que je le pouvais. Je pense que le problème ici était principalement syntaxique (en particulier en passant à ou en renvoyant des fonctions).
Depuis que je suis en train d’écrire C++ à l’école pour la prochaine année ou deux, j’ai beaucoup d’expérience dans l’utilisation de pointeurs pour les structures de données. Ici, j'ai eu une nouvelle série de problèmes - mélanger les pointeurs. J'aurais plusieurs niveaux de pointeurs (des choses comme node ***ptr;
) me faire trébucher. Je déréférencer un pointeur le mauvais nombre de fois et finalement avoir recours au calcul du nombre de *
J'avais besoin d'essais et d'erreurs.
À un moment donné, j'ai appris comment fonctionnait le tas d'un programme (en quelque sorte, mais suffisamment bon pour qu'il ne m'empêche plus de dormir la nuit). Je me souviens avoir lu que si vous regardez quelques octets avant le pointeur que malloc
renvoie sur un système donné, vous pouvez voir la quantité de données effectivement allouée. J'ai réalisé que le code dans malloc
pouvait demander plus de mémoire au système d'exploitation et que cette mémoire ne faisait pas partie de mes fichiers exécutables. Avoir une idée de travail décente sur le fonctionnement de malloc
est vraiment utile.
Peu de temps après, j'ai suivi un cours d'assemblée, ce qui ne m'a pas appris autant de choses sur les pointeurs que la plupart des programmeurs le pensent probablement. Cela m'a amené à réfléchir davantage sur le type d'assemblage que mon code pourrait être traduit. J'avais toujours essayé d'écrire du code efficace, mais j'avais maintenant une meilleure idée de la marche à suivre.
J'ai aussi pris quelques cours où j'ai dû écrire LISP . En écrivant LISP, je n'étais pas aussi soucieux d'efficacité que de C. J'avais très peu d'idée du contenu de ce code s'il était compilé, mais je savais qu'il semblait utiliser beaucoup de symboles nommés locaux (variables). les choses beaucoup plus facile. À un moment donné, j'ai écrit du code de rotation d'arborescence AVL dans un peu de LISP, que j'ai eu beaucoup de difficulté à écrire en C++ en raison de problèmes de pointeur. J'ai réalisé que mon aversion pour ce que je pensais être un excès de variables locales m'avait empêché d'écrire cela et plusieurs autres programmes en C++.
J'ai aussi suivi un cours sur les compilateurs. Dans cette classe, je suis passé au matériel avancé et ai appris à propos de statiqueniqueaffectation (SSA) et des variables mortes, ce qui n'est pas le cas important, sauf que cela m’a appris que tout compilateur décent fera un travail décent en traitant des variables qui ne sont plus utilisées. Je savais déjà que plus de variables (y compris les pointeurs) avec les types corrects et les bons noms me permettraient de garder les choses droites dans ma tête, mais maintenant je savais aussi que les éviter pour des raisons d'efficacité était encore plus stupide que mes professeurs moins férus de micro-optimisation moi.
Donc pour moi, connaître un peu la structure de la mémoire d'un programme m'a beaucoup aidé. Réfléchir à la signification de mon code, à la fois symboliquement et matériellement, m'aide. L'utilisation de pointeurs locaux de type correct aide beaucoup. J'écris souvent un code qui ressemble à:
int foo(struct frog * f, int x, int y) {
struct leg * g = f->left_leg;
struct toe * t = g->big_toe;
process(t);
de sorte que si je vis un type de pointeur, le problème est très clair par l'erreur du compilateur. Si j'ai fait:
int foo(struct frog * f, int x, int y) {
process(f->left_leg->big_toe);
et si un type de pointeur était incorrect, l’erreur du compilateur serait beaucoup plus difficile à comprendre. Je serais tenté de recourir à des essais et des erreurs dans ma frustration, et probablement aggraver les choses.
J'avais mon "pointeur" sur certains programmes de téléphonie en C. Je devais écrire un émulateur d’échange AXE10 en utilisant un analyseur de protocole qui ne comprenait que le C classique. Tout était axé sur la connaissance des pointeurs. J'ai essayé d'écrire mon code sans eux (hé, j'étais "pré-pointeur" m'a coupé un peu du mou) et j'ai complètement échoué.
Pour moi, la clé pour les comprendre était l'opérateur & (adresse). Une fois que j'ai compris que &i
signifiait "l'adresse de i" puis comprenait que *i
voulait dire "le contenu de l'adresse indiquée par i" est venu un peu plus tard. Chaque fois que j'écrivais ou lisais mon code, je répétais toujours ce que "&" voulait dire et ce que "*" voulait dire et je finis par les utiliser intuitivement.
À ma honte, j'ai été forcé de VB puis Java, donc ma connaissance du pointeur n'est pas aussi précise qu'elle l'était auparavant, mais je suis heureux de l'être "). post-pointeur ". Ne me demandez pas d’utiliser une bibliothèque qui demande que je comprenne * * p, cependant.
En regardant en arrière, il y a quatre choses qui m'ont vraiment aidé à comprendre enfin les indicateurs. Avant cela, je pouvais les utiliser, mais je ne les comprenais pas bien. Autrement dit, je savais que si je suivais les formulaires, j'obtiendrais les résultats souhaités, mais je ne comprenais pas tout à fait le "pourquoi" des formulaires. Je me rends compte que ce n'est pas exactement ce que vous avez demandé, mais je pense que c'est un corollaire utile.
Écrire une routine qui prenait un pointeur sur un entier et modifiait celui-ci. Cela m'a donné les formes nécessaires pour construire des modèles mentaux de fonctionnement des pointeurs.
Allocation de mémoire dynamique unidimensionnelle. La détermination de l'allocation de mémoire 1-D m'a fait comprendre le concept du pointeur.
Allocation de mémoire dynamique à deux dimensions. La détermination de l'allocation de mémoire 2D a renforcé ce concept, mais m'a aussi appris que le pointeur lui-même nécessite un stockage et doit être pris en compte.
Différences entre les variables de pile, les variables globales et la mémoire heap. Comprendre ces différences m'a appris les types de mémoire sur lesquels les pointeurs se dirigent.
Chacun de ces éléments nécessitait d'imaginer ce qui se passait à un niveau inférieur - la construction d'un modèle mental qui satisfaisait tous les cas que je pouvais penser de jeter. Cela a pris du temps et des efforts, mais cela en valait la peine. Je suis convaincu que pour comprendre les indicateurs, vous devez construire ce modèle mental sur la façon dont ils fonctionnent et dont ils sont mis en œuvre.
Revenons maintenant à votre question initiale. Sur la base de la liste précédente, il y avait plusieurs éléments que j'avais du mal à saisir à l'origine.
Voici une non-réponse: Utilisez cdecl (ou c ++ décl) pour le comprendre:
eisbaw@leno:~$ cdecl explain 'int (*(*foo)(const void *))[3]'
declare foo as pointer to function (pointer to const void) returning pointer to array 3 of int
La principale difficulté avec les pointeurs, du moins pour moi, est que je n’ai pas commencé par C. j’ai commencé avec Java. Toute la notion de pointeurs était vraiment étrangère jusqu’à quelques cours au collège où je devais connaître C. C’est alors que j’ai appris moi-même les bases du C et comment utiliser les pointeurs dans leur sens le plus fondamental. Même dans ce cas, chaque fois que je lis le code C, je dois rechercher la syntaxe du pointeur.
Donc, dans mon expérience très limitée (1 an réel + 4 au collège), les pointeurs me déroutent parce que je n'ai jamais eu à l'utiliser réellement dans une salle de classe. Et je peux comprendre les étudiants qui commencent maintenant CS avec Java au lieu de C ou C++. Comme vous l'avez dit, vous avez appris des indicateurs à l'époque "néolithique" et vous l'utilisez probablement depuis Pour nous, nouveaux venus, l’idée d’allouer de la mémoire et de faire de l’arithmétique de pointeur est vraiment étrangère, car toutes ces langues ont fait abstraction de cela.
P.S. Après avoir lu l'essai Spolsky, sa description de 'JavaSchools' ne ressemblait en rien à ce que j'ai vécu à l'université de Cornell (2005-2009). J'ai pris les structures et la programmation fonctionnelle (SML), les systèmes d'exploitation (C), les algorithmes (stylo et papier), et toute une série d'autres cours qui n'étaient pas enseignés en Java. Cependant, toutes les classes d'introduction et toutes les options ont toutes été effectuées dans Java), car il est utile de ne pas réinventer la roue lorsque vous essayez de faire quelque chose de plus haut que de mettre en œuvre une table de hachage avec des pointeurs.
J'avais programmé en c ++ pendant environ 2 ans, puis converti en Java (5 ans) sans jamais regarder en arrière. Cependant, lorsque j'ai récemment dû utiliser des éléments natifs, j'ai découvert (avec étonnement) que je n'avais rien oublié des pointeurs et que je les trouvais même faciles à utiliser. C’est un contraste frappant avec ce que j’avais vécu il ya 7 ans lorsque j’avais essayé de saisir le concept. Donc, je suppose que comprendre et aimer sont une question de maturité de programmation? :)
OR
Les pointeurs sont comme un vélo, une fois que vous avez trouvé comment travailler avec eux, vous ne pouvez pas l’oublier.
Globalement, difficile à saisir ou non, l’idée du pointeur est TRÈS pédagogique et je pense qu’il devrait être compris par tout programmeur, qu’il programme ou non sur une langue.
Ils ajoutent une dimension supplémentaire au code sans modification significative de la syntaxe. Penses-y:
int a;
a = 5
Il n’ya qu’une chose à changer: a
. Tu peux écrire a = 6
et les résultats sont évidents pour la plupart des gens. Mais considérons maintenant:
int *a;
a = &some_int;
Il y a deux choses à propos de a
qui sont pertinentes à des moments différents: la valeur réelle de a
, le pointeur et la valeur "derrière" le pointeur. Vous pouvez changer a
:
a = &some_other_int;
...et some_int
est toujours là quelque part avec la même valeur. Mais vous pouvez aussi changer ce qui est indiqué:
*a = 6;
Il y a un fossé conceptuel entre a = 6
, qui n'a que des effets secondaires locaux, et *a = 6
, ce qui pourrait affecter bien d’autres choses ailleurs. Mon point ici est pas que le concept d'indirection est intrinsèquement délicat, mais que parce que vous pouvez faire les deux la chose immédiate et locale avec a
ou l'indirect chose avec *a
... c'est peut-être ce qui déroute les gens.
Les pointeurs (ainsi que certains autres aspects du travail de bas niveau), exigent que l'utilisateur enlève la magie.
La plupart des programmeurs de haut niveau aiment la magie.
Je pense qu'une des raisons pour lesquelles les indicateurs C sont difficiles est qu'ils associent plusieurs concepts qui ne sont pas vraiment équivalents; pourtant, comme ils sont tous mis en œuvre à l'aide de pointeurs, les gens peuvent avoir du mal à démêler les concepts.
En C, les pointeurs sont utilisés pour, entre autres choses:
En C, vous définiriez une liste chaînée d’entiers comme celle-ci:
struct node {
int value;
struct node* next;
}
Le pointeur n’est là que parce que c’est le seul moyen de définir une structure de données récursive en C, alors que le concept n’a vraiment rien à voir avec des détails aussi simples que les adresses mémoire. Considérez l'équivalent suivant en Haskell, qui n'exige pas l'utilisation de pointeurs:
data List = List Int List | Null
Plutôt simple: une liste est soit vide, soit formée d'une valeur et du reste de la liste.
Voici comment appliquer une fonction foo
à chaque caractère d'une chaîne en C:
char *c;
for (c = "hello, world!"; *c != '\0'; c++) { foo(c); }
Bien que nous utilisions également un pointeur comme itérateur, cet exemple présente très peu de points communs avec le précédent. La création d'un itérateur que vous pouvez incrémenter est un concept différent de la définition d'une structure de données récursive. Aucun de ces concepts n’est spécialement lié à l’idée d’une adresse mémoire.
Voici une signature de fonction réelle trouvée dans glib :
typedef struct g_list GList;
void g_list_foreach (GList *list,
void (*func)(void *data, void *user_data),
void* user_data);
Whoa! C'est tout à fait une bouchée de void*
's. Et tout est juste pour déclarer une fonction qui itère sur une liste pouvant contenir tout type de chose, appliquant une fonction à chaque membre. Comparez-le à la façon dont map
est déclaré dans Haskell:
map::(a->b)->[a]->[b]
C’est beaucoup plus simple: map
est une fonction qui prend une fonction qui convertit un a
en un b
et l’applique à une liste de a
à donne une liste de b
. Comme dans la fonction C g_list_foreach
, map
n'a pas besoin de savoir quoi que ce soit dans sa propre définition sur les types auxquels il sera appliqué.
Pour résumer:
Je pense que les pointeurs C seraient beaucoup moins déroutants si les gens apprenaient d'abord les structures de données récursives, les itérateurs, le polymorphisme, etc. en tant que concepts séparés, puis apprenaient comment les pointeurs peuvent être utilisés pour mettre en œuvre ces idées. en C , plutôt que de mélanger tous ces concepts dans un seul sujet de "pointeurs".
Les pointeurs sont difficiles à cause de l'indirection.
Les pointeurs permettent de gérer la différence entre un descripteur d'objet et un objet lui-même. (ok, pas forcément des objets, mais vous savez ce que je veux dire, ainsi que mon esprit)
À un moment donné, vous devrez probablement faire face à la différence entre les deux. Dans les langages modernes de haut niveau, cela devient la distinction entre copie par valeur et copie par référence. Quoi qu'il en soit, c'est un concept qui est souvent difficile à comprendre pour les programmeurs.
Cependant, comme cela a été souligné, la syntaxe permettant de traiter ce problème en langage C est laide, incohérente et déroutante. Finalement, si vous essayez vraiment de le comprendre, un pointeur aura un sens. Mais lorsque vous commencez à traiter avec des indicateurs, et ainsi de suite, ad nauseum, cela devient vraiment déroutant pour moi ainsi que pour les autres personnes.
Une autre chose importante à retenir à propos des pointeurs est qu'ils sont dangereux. C est un langage de maître programmeur. Cela suppose que vous sachiez ce que vous faites et que vous ayez ainsi le pouvoir de tout gâcher. Bien que certains types de programmes doivent encore être écrits en C, la plupart des programmes ne le sont pas. Si vous avez un langage qui fournit une meilleure abstraction de la différence entre un objet et son handle, je vous suggère de l'utiliser.
En effet, dans de nombreuses applications C++ modernes, il est fréquent que toute arithmétique de pointeur requise soit encapsulée et abstraite. Nous ne voulons pas que les développeurs utilisent l'arithmétique de pointeur dans tous les sens. Nous voulons une API centralisée et bien testée qui effectue l’arithmétique de pointeur au niveau le plus bas. Les modifications de ce code doivent être effectuées avec le plus grand soin et des tests approfondis.
Il était une fois ... Nous avions des microprocesseurs 8 bits et tout le monde écrivait dans Assembly. La plupart des processeurs incluaient un type d'adressage indirect utilisé pour les tables de saut et les noyaux. Lorsque des langages de niveau supérieur sont apparus, nous avons ajouté une fine couche d’abstraction et les avons appelés indicateurs. Au fil des ans, nous nous sommes de plus en plus éloignés du matériel. Ce n'est pas forcément une mauvaise chose. On les appelle les langues de niveau supérieur pour une raison. Plus je peux me concentrer sur ce que je veux faire au lieu de détailler la façon dont cela est fait, mieux c'est.
Il semble que de nombreux étudiants ont un problème avec le concept d’indirection, en particulier lorsqu’ils rencontrent le concept d’indirection pour la première fois. Je me souviens que lorsque j'étais étudiant, parmi les 100 étudiants de mon cursus, seule une poignée de personnes comprenait vraiment les indications.
Le concept d'indirection n'est pas quelque chose que nous utilisons souvent dans la vie réelle, c'est donc un concept difficile à saisir au départ.
Je pense que cela nécessite une base solide, probablement au niveau de la machine, avec une introduction à un code de machine, à un assemblage, et à la manière de représenter les éléments et la structure de données dans la RAM. Cela prend un peu de temps, des devoirs ou de la résolution de problèmes, et de la réflexion.
Mais si une personne connaît des langages de haut niveau au début (ce qui n’a rien d’inconvénient, un charpentier utilise un hache. Une personne qui doit scinder atom utilise autre chose. Nous avons besoin de personnes qui sont charpentiers, et nous avons des gens qui étudient les atomes) et cette personne connaissant le langage de haut niveau reçoit une introduction de 2 minutes aux pointeurs, puis il est difficile de s’attendre à ce qu’il comprenne l’arithmétique de pointeur, les pointeurs en pointeurs, un ensemble de pointeurs vers des chaînes de tailles variables, et un tableau de caractères, etc. Une base solide de bas niveau peut aider beaucoup.
Le problème que j'ai toujours eu (principalement en autodidacte) est le "quand" pour utiliser un pointeur. Je peux comprendre la syntaxe utilisée pour construire un pointeur, mais j'ai besoin de savoir dans quelles circonstances un pointeur doit être utilisé.
Suis-je le seul à avoir cet état d'esprit? ;-)
Je viens tout juste d'avoir le moment de clic du pointeur, et j'ai été surpris de l'avoir trouvé déroutant. C’était plus que tout le monde en parle tellement, que j’imaginais qu’il y avait de la magie noire.
La façon dont j'ai eu c'était ça. Imaginez que toutes les variables définies se voient attribuer un espace mémoire au moment de la compilation (sur la pile). Si vous souhaitez un programme capable de gérer des fichiers de données volumineux tels que des fichiers audio ou des images, vous ne voudriez pas d'une quantité de mémoire fixe pour ces structures potentielles. Donc, vous attendez le moment de l'exécution pour affecter une certaine quantité de mémoire à la conservation de ces données (sur le tas).
Une fois que vous avez vos données en mémoire, vous ne voulez pas les copier tout autour de votre bus de mémoire à chaque fois que vous souhaitez exécuter une opération dessus. Supposons que vous souhaitiez appliquer un filtre à vos données d'image. Vous avez un pointeur qui commence au début des données que vous avez affectées à l'image, puis une fonction parcourt ces données et les modifie à la place. Si vous ne saviez pas ce que vous faites, vous ferez probablement des doublons de données, comme vous les avez exécutés tout au long de l'opération.
Au moins c'est ce que je vois pour le moment!
Parlant en tant que novice C++ ici:
Le système de pointeur a mis un certain temps à digérer, pas nécessairement à cause du concept mais à cause de la syntaxe C++ relative à Java. Quelques choses que j'ai trouvées déroutantes sont:
(1) déclaration de variable:
A a(1);
vs.
A a = A(1);
vs.
A* a = new A(1);
et apparemment
A a();
est une déclaration de fonction et non une déclaration de variable. Dans d’autres langues, il n’ya qu’une façon de déclarer une variable.
(2) L'esperluette est utilisée de différentes manières. Si c'est
int* i = &a;
alors le & a est une adresse mémoire.
OTOH, si c'est
void f(int &a) {}
alors le & a est un paramètre passé par référence.
Bien que cela puisse sembler trivial, cela peut être déroutant pour les nouveaux utilisateurs - je viens de Java et Java est un langage avec une utilisation plus uniforme des opérateurs
(3) relation tableau-pointeur
Une chose qui est un peu frustrant à comprendre est qu’un pointeur
int* i
peut être un pointeur sur un int
int *i = &n; //
ou
peut être un tableau pour un int
int* i = new int[5];
Et pour simplifier les choses, les pointeurs et les tableaux ne sont pas interchangeables dans tous les cas et les pointeurs ne peuvent pas être passés en tant que paramètres de tableau.
Ceci résume quelques-unes des frustrations de base que j'avais avec C/C++ et ses pointeurs, que l'OMI est grandement aggravée par le fait que C/C++ possède toutes ces bizarreries spécifiques au langage.
Pointeurs .. hah .. tout sur le pointeur dans ma tête, c’est qu’il donne une adresse mémoire où les valeurs réelles de quelle que soit sa référence .. donc pas de magie à ce sujet .. si vous apprenez quelque chose d’Assemblée, vous n’auriez pas tant de peine à apprendre comment les pointeurs fonctionnent .. allez les gars ... même dans Java tout est une référence ..
Le principal problème des gens ne comprend pas pourquoi ont-ils besoin de pointeurs. Parce qu'ils ne sont pas clairs sur la pile et le tas. Il est bon de commencer par un assembleur 16 bits pour x86 avec un mode mémoire minuscule. Cela a aidé beaucoup de gens à se faire une idée de la pile, du tas et de "l'adresse". Et octet :) Les programmeurs modernes ne peuvent parfois pas vous dire combien d'octets vous devez adresser à un espace 32 bits. Comment peuvent-ils avoir une idée des pointeurs?
Le deuxième moment est la notation: vous déclarez le pointeur comme *, vous obtenez l'adresse comme & et ce n'est pas facile à comprendre pour certaines personnes.
Et la dernière chose que j'ai vue était un problème de stockage: ils comprenaient le tas et la pile mais ne pouvaient pas se faire une idée du "statique".
Personnellement, je n'ai pas compris le pointeur, même après mon diplôme et après mon premier emploi. La seule chose que je savais, c'est que vous en avez besoin pour les listes chaînées, les arbres binaires et pour passer des tableaux en fonctions. C'était la situation même à mon premier emploi. Ce n’est que lorsque j’ai commencé à donner des interviews que je comprenais que le concept du pointeur était profond et avait une utilité et un potentiel considérables. Ensuite, j'ai commencé à lire K & R et à écrire mon propre programme de test. Tout mon objectif était axé sur l'emploi.
À ce moment-là, j’ai trouvé que les pointeurs ne sont vraiment ni mauvais ni difficiles s’ils sont enseignés correctement. Malheureusement, lorsque j'ai appris le C lors de l'obtention du diplôme, notre enseignant externe n'était pas au courant du pointeur et même les devoirs utilisaient moins de pointeurs. Au niveau supérieur, l’utilisation du pointeur ne permet en fait de créer des arbres binaires et une liste chaînée. En pensant que vous n'avez pas besoin de bien comprendre les pointeurs pour travailler avec eux, supprimez l'idée de les apprendre.