Quelle question provocante!
Même un survol rapide des réponses et des commentaires dans ce fil révèlera à quel point votre requête apparemment simple et directe se révèle émotive .
Cela ne devrait pas être surprenant.
Il est incontestable que les malentendus autour du concept et de l'utilisation des pointeurs représentent une cause prédominante de graves échecs dans la programmation en général.
La reconnaissance de cette réalité est facilement évidente dans l'omniprésence des langues conçues spécifiquement pour répondre, et de préférence pour éviter les défis que les pointeurs introduisent complètement. Pensez C ++ et autres dérivés de C, Java et ses relations, Python et d'autres scripts - simplement comme les plus importants et les plus répandus, et plus ou moins ordonnés en fonction de la gravité du problème.
Développer une compréhension plus profonde des principes sous-jacents doit donc être pertinent pour chaque individu qui aspire à l' excellence en programmation - en particulier au niveau des systèmes .
J'imagine que c'est précisément ce que votre professeur veut démontrer.
Et la nature de C en fait un véhicule pratique pour cette exploration. Moins clairement que l'assemblage - mais peut-être plus facilement compréhensible - et encore beaucoup plus explicitement que les langages basés sur une abstraction plus profonde de l'environnement d'exécution.
Conçu pour faciliter la traduction déterministe de l'intention du programmeur en instructions que les machines peuvent comprendre, C est un langage de niveau système . Bien que classé comme de haut niveau, il appartient vraiment à une catégorie «moyenne»; mais comme il n'en existe pas, la désignation de «système» doit suffire.
Cette caractéristique est largement responsable d'en faire un langage de choix pour les pilotes de périphériques , le code du système d'exploitation et les implémentations intégrées . En outre, une alternative à juste titre privilégiée dans les applications où l'efficacité optimale est primordiale; où cela signifie la différence entre la survie et l'extinction, et est donc une nécessité par opposition à un luxe. Dans de tels cas, la commodité attrayante de la portabilité perd tout son attrait, et opter pour les performances de manque de lustre du dénominateur le moins commun devient une option impensablement préjudiciable .
Ce qui rend C - et certains de ses dérivés - assez spécial, c'est qu'il permet à ses utilisateurs un contrôle complet - quand c'est ce qu'ils souhaitent - sans leur imposer les responsabilités associées lorsqu'ils ne le font pas. Néanmoins, il n'offre jamais plus que la plus fine des isolations de la machine , c'est pourquoi une utilisation correcte exige une compréhension rigoureuse du concept de pointeurs .
En substance, la réponse à votre question est sublimement simple et d'une douceur satisfaisante - pour confirmer vos soupçons. À condition , cependant, que l'on attache la signification requise à chaque concept dans cette déclaration:
- Les actes d'examen, de comparaison et de manipulation des pointeurs sont toujours et nécessairement valables, tandis que les conclusions tirées du résultat dépendent de la validité des valeurs contenues, et n'ont donc pas besoin de l' être.
Le premier est à la fois invariablement sûr et potentiellement approprié , tandis que le second ne peut jamais être approprié lorsqu'il a été établi comme sûr . Étonnamment - pour certains - , établir la validité de ce dernier dépend et l' exige .
Bien sûr, une partie de la confusion provient de l'effet de la récursivité intrinsèquement présente dans le principe d'un pointeur - et des défis posés pour différencier le contenu de l'adresse.
Vous avez tout à fait correctement supposé,
Je suis amené à penser que n'importe quel pointeur peut être comparé à n'importe quel autre pointeur, indépendamment de l'endroit où ils pointent individuellement. De plus, je pense que l'arithmétique du pointeur entre deux pointeurs est très bien, peu importe où ils pointent individuellement parce que l'arithmétique utilise simplement les adresses mémoire stockées par les pointeurs.
Et plusieurs contributeurs l'ont affirmé: les pointeurs ne sont que des chiffres. Parfois quelque chose de plus proche des nombres complexes , mais toujours pas plus que des nombres.
L'acrimonie amusante dans laquelle cette affirmation a été reçue ici révèle plus sur la nature humaine que sur la programmation, mais reste digne d'être notée et développée. Peut-être le ferons-nous plus tard ...
Comme un commentaire commence à faire allusion; toute cette confusion et cette consternation découlent de la nécessité de discerner ce qui est valable de ce qui est sûr , mais c'est une simplification excessive. Nous devons également distinguer ce qui est fonctionnel et ce qui est fiable , ce qui est pratique et ce qui peut être approprié , et plus encore: ce qui est approprié dans une circonstance particulière de ce qui peut être approprié dans un sens plus général . Sans parler de; la différence entre conformité et convenance .
À cette fin, nous devons d'abord comprendre précisément ce qu'un pointeur est .
- Vous avez démontré une solide emprise sur le concept et, comme certains autres, vous pouvez trouver ces illustrations avec condescendance simpliste, mais le niveau de confusion évident ici exige une telle simplicité de clarification.
Comme plusieurs l'ont souligné: le terme pointeur n'est qu'un nom spécial pour ce qui est simplement un index , et donc rien de plus qu'un autre nombre .
Cela devrait déjà être évident, compte tenu du fait que tous les ordinateurs traditionnels contemporains sont des machines binaires qui fonctionnent nécessairement exclusivement avec et sur nombres . L'informatique quantique peut changer cela, mais cela est hautement improbable et elle n'est pas arrivée à maturité.
Techniquement, comme vous l'avez noté, les pointeurs sont plus précis adresses; un aperçu évident qui introduit naturellement l'analogie gratifiante de les corréler avec les «adresses» des maisons ou des parcelles dans une rue.
Dans un modèle de mémoire plate : toute la mémoire du système est organisée en une seule séquence linéaire: toutes les maisons de la ville se trouvent sur la même route, et chaque maison est identifiée de manière unique par son seul numéro. Délicieusement simple.
Dans schémas segmentés : une organisation hiérarchique des routes numérotées est introduite au-dessus de celle des maisons numérotées afin que des adresses composites soient requises.
- Certaines implémentations sont encore plus compliquées, et la totalité des «routes» distinctes n'a pas besoin de résumer en une séquence contiguë, mais rien de tout cela ne change quoi que ce soit sur le sous-jacent.
- Nous sommes nécessairement capables de décomposer chaque lien hiérarchique de ce type en une organisation plate. Plus l'organisation est complexe, plus nous devrons franchir de cerceaux pour ce faire, mais cela doit être possible. En effet, cela s'applique également au «mode réel» sur x86.
- Sinon, la mise en correspondance des liens avec les emplacements ne serait pas bijective , car une exécution fiable - au niveau du système - exige qu'elle DOIT être.
- plusieurs adresses ne doivent pas correspondre à des emplacements de mémoire singuliers, et
- les adresses singulières ne doivent jamais correspondre à plusieurs emplacements de mémoire.
Nous amenant à la torsion supplémentaire qui transforme l'énigme en un enchevêtrement fascinant et compliqué . Ci-dessus, il était opportun de suggérer que les pointeurs sont des adresses, par souci de simplicité et de clarté. Bien sûr, ce n'est pas correct. Un pointeur n'est pas une adresse; un pointeur est une référence à une adresse , il contient une exception de code d'opération invalide une adresse . Comme l'enveloppe arbore une référence à la maison. Contempler cela peut vous amener à entrevoir ce que signifiait la suggestion de récursivité contenue dans le concept. Encore; nous avons seulement tant de mots, et parlons des adresses de références aux adresses tel ou tel, stagne bientôt la plupart des cerveaux à un . Et pour la plupart, l'intention est facilement obtenue du contexte, alors revenons à la rue.
Les postiers de notre ville imaginaire ressemblent beaucoup à ceux que nous trouvons dans le monde «réel». Personne ne risque de subir un accident vasculaire cérébral lorsque vous parlez ou demandez au sujet d' une invalide adresse, mais chaque dernier rechignent quand vous leur demandez d'agir sur cette information.
Supposons qu'il n'y ait que 20 maisons sur notre rue singulière. Prétendre en outre qu'une âme malavisée ou dyslexique a adressé une lettre, très importante, au numéro 71. Maintenant, nous pouvons demander à notre porteur Frank, s'il existe une telle adresse, et il rapportera simplement et calmement: non . On peut même l'attendre d'estimer dans quelle mesure en dehors de la rue cet endroit se trouverait si elle a exist: environ 2,5 fois plus loin que la fin. Rien de tout cela ne lui causera d'exaspération. Cependant, si nous lui demandions de remettre cette lettre, ou de ramasser un article à cet endroit, il est susceptible d'être assez franc au sujet de son mécontentement et de son refus de se conformer.
Les pointeurs ne sont que des adresses et les adresses ne sont que des chiffres.
Vérifiez la sortie des éléments suivants:
void foo( void *p ) {
printf(“%p\t%zu\t%d\n”, p, (size_t)p, p == (size_t)p);
}
Appelez-le sur autant de pointeurs que vous le souhaitez, valides ou non. S'il vous plaît ne pas poster vos résultats si elle échoue sur votre plate - forme, ou votre (contemporaine) compilateur se plaint.
Maintenant, comme les pointeurs ne sont que des nombres, il est inévitablement valable de les comparer. Dans un sens, c'est précisément ce que votre professeur démontre. Toutes les déclarations suivantes sont parfaitement valables - et correctes! - C, et une fois compilé s'exécutera sans rencontrer de problèmes , même si aucun pointeur n'a besoin d'être initialisé et les valeurs qu'ils contiennent peuvent donc être indéfinies :
- Nous calculons seulement
result
explicite par souci de clarté , et nous l' imprimons pour forcer le compilateur à calculer ce qui serait autrement redondant, du code mort.
void foo( size_t *a, size_t *b ) {
size_t result;
result = (size_t)a;
printf(“%zu\n”, result);
result = a == b;
printf(“%zu\n”, result);
result = a < b;
printf(“%zu\n”, result);
result = a - b;
printf(“%zu\n”, result);
}
Bien sûr, le programme est mal formé lorsque a ou b n'est pas défini (lire: pas correctement initialisé ) au moment du test, mais cela n'a absolument rien à voir avec cette partie de notre discussion. Ces extraits, ainsi que les déclarations suivantes, sont garantis - par le «standard» - pour être compilés et exécutés sans faille, nonobstant l' IN validité de tout pointeur impliqué.
Les problèmes surviennent uniquement lorsqu'un pointeur non valide est déréférencé . Lorsque nous demandons à Frank de venir chercher ou livrer à l'adresse invalide et inexistante.
Étant donné tout pointeur arbitraire:
int *p;
Alors que cette instruction doit être compilée et exécutée:
printf(“%p”, p);
... comme cela doit:
size_t foo( int *p ) { return (size_t)p; }
... les deux suivants, en contraste frappant, seront toujours facilement compilés, mais échoueront à moins que le pointeur ne soit valide - par lequel nous voulons simplement dire ici qu'il fait référence à une adresse à laquelle la présente application a été autorisée :
printf(“%p”, *p);
size_t foo( int *p ) { return *p; }
Quelle est la subtilité du changement? La distinction réside dans la différence entre la valeur du pointeur - qui est l'adresse, et la valeur du contenu: de la maison à ce numéro. Aucun problème ne survient jusqu'à ce que le pointeur soit déréférencé ; jusqu'à ce qu'il soit tenté d'accéder à l'adresse vers laquelle il est lié. En essayant de livrer ou de récupérer le colis au-delà du tronçon de la route ...
Par extension, le même principe s'applique nécessairement à des exemples plus complexes, y compris la nécessité susmentionnée d' établir la validité requise:
int* validate( int *p, int *head, int *tail ) {
return p >= head && p <= tail ? p : NULL;
}
La comparaison relationnelle et l'arithmétique offrent une utilité identique pour tester l'équivalence et sont équivalentes en principe - en principe. Cependant , ce que les résultats de ce calcul ne signifierait , est une autre affaire tout à fait - et précisément la question traitée par les citations que vous avez inclus.
En C, un tableau est un tampon contigu, une série linéaire ininterrompue d'emplacements de mémoire. La comparaison et l'arithmétique appliquées aux pointeurs qui font référence à des emplacements dans une telle série singulière sont naturellement et évidemment significatives les unes par rapport aux autres et à ce `` tableau '' (qui est simplement identifié par la base). Précisément, la même chose s'applique à chaque bloc alloué via malloc
ou sbrk
. Parce que ces relations sont implicites , le compilateur est en mesure d'établir des relations valides entre elles et peut donc être sûr que les calculs fourniront les réponses attendues.
L'exécution d'une gymnastique similaire sur des pointeurs qui référencent des blocs ou des tableaux distincts n'offre pas une telle utilité inhérente et apparente . D'autant plus que toute relation existant à un moment donné peut être invalidée par une réaffectation qui suit, dans laquelle celle-ci est très susceptible de changer, voire d'être inversée. Dans de tels cas, le compilateur n'est pas en mesure d'obtenir les informations nécessaires pour établir la confiance qu'il avait dans la situation précédente.
Vous , cependant,tant que programmeur, peut avoirtelle connaissance! Et dans certains cas, ils sont obligés d'exploiter cela.
Il SONT donc des circonstances où même c'est tout à fait VALIDE et parfaitement CORRECTE.
En fait, c'est exactement ce que malloc
lui-même doit faire en interne lorsque vient le temps d'essayer de fusionner des blocs récupérés - sur la grande majorité des architectures. La même chose est vraie pour l'allocateur de système d'exploitation, comme celui derrière sbrk
; si de manière plus évidente , fréquente , sur des entités plus disparates , plus critique - et pertinente également sur des plateformes où cela malloc
ne l'est peut-être pas. Et combien de ceux-ci ne sont pas écrits en C?
La validité, la sécurité et le succès d'une action sont inévitablement la conséquence du niveau de compréhension sur lequel elle est fondée et appliquée.
Dans les citations que vous avez proposées, Kernighan et Ritchie abordent un problème étroitement lié, mais néanmoins distinct. Ils définissent les limites de la langage et expliquent comment vous pouvez exploiter les capacités du compilateur pour vous protéger en détectant au moins les constructions potentiellement erronées. Ils décrivent les longueurs auxquelles le mécanisme peut - est conçu - aller pour vous aider dans votre tâche de programmation. Le compilateur est votre serviteur, vous êtes le maître. Un maître sage, cependant, est celui qui connaît intimement les capacités de ses divers serviteurs.
Dans ce contexte, un comportement indéfini sert à indiquer un danger potentiel et la possibilité de préjudice; ne pas impliquer un destin imminent et irréversible, ni la fin du monde tel que nous le connaissons. Cela signifie simplement que nous - «signifiant le compilateur» - ne sommes pas en mesure de faire de conjectures sur ce que cette chose peut être, ou représenter et pour cette raison, nous choisissons de nous laver les mains de la question. Nous ne serons pas tenus responsables de toute mésaventure pouvant résulter de l'utilisation ou de la mauvaise utilisation de cette installation .
En effet, il dit simplement: "Au-delà de ce point, cow - boy : vous êtes seul ..."
Votre professeur cherche à vous montrer les nuances les plus fines .
Remarquez le grand soin qu'ils ont apporté à l'élaboration de leur exemple; et comment fragile il est encore . En prenant l'adresse a
, en
p[0].p0 = &a;
le compilateur est contraint d'allouer le stockage réel pour la variable, plutôt que de le placer dans un registre. Cependant, étant une variable automatique, le programmeur n'a aucun contrôle sur l' endroit où cela est attribué, et donc incapable de faire une conjecture valide sur ce qui le suivrait. C'est pourquoi a
il faut définir zéro pour que le code fonctionne comme prévu.
Changer simplement cette ligne:
char a = 0;
pour ça:
char a = 1; // or ANY other value than 0
rend le comportement du programme non défini . Au minimum, la première réponse sera désormais 1; mais le problème est bien plus sinistre.
Maintenant, le code invite au désastre.
Bien qu'il soit toujours parfaitement valide et même conforme à la norme , il est maintenant mal formé et bien qu'il soit certain de le compiler, son exécution peut échouer pour diverses raisons. Pour l' instant , il y a de multiples problèmes - aucune dont le compilateur est capable de reconnaître.
strcpy
commencera à l'adresse de a
, et ira au- delà pour consommer - et transférer - octet après octet, jusqu'à ce qu'il rencontre une valeur nulle.
Le p1
pointeur a été initialisé à un bloc d'exactement 10 octets.
S'il a
se trouve être placé à la fin d'un bloc et que le processus n'a pas accès à ce qui suit, la toute prochaine lecture - de p0 [1] - provoquera un segfault. Ce scénario est peu probable sur l'architecture x86, mais possible.
Si la zone au-delà de l'adresse de a
est accessible, aucune erreur de lecture ne se produit, mais le programme n'est toujours pas sauvé du malheur.
Si un octet zéro se produit dans les dix en commençant à l'adresse de a
, il peut toujours survivre, car alors strcpy
il s'arrêtera et au moins nous ne subirons pas de violation d'écriture.
S'il n'est pas en défaut pour lecture incorrecte, mais qu'aucun octet zéro ne se produit dans cette plage de 10, strcpy
continuera et tentera d' écrire au-delà du bloc alloué par malloc
.
Si cette zone n'appartient pas au processus, le défaut de segmentation doit être immédiatement déclenché.
La encore plus désastreuse - et subtile --- situation se produit lorsque le bloc suivant est la propriété du processus, pour l'erreur ne peut pas être détectée, aucun signal ne peut être soulevée, et il peut « paraître » encore « travail » , alors qu'il remplacera en fait d' autres données, les structures de gestion de votre allocateur ou même du code (dans certains environnements d'exploitation).
C'est pourquoi les bogues liés au pointeur peuvent être si difficiles à suivre . Imaginez ces lignes enfouies profondément dans des milliers de lignes de code étroitement liées, que quelqu'un d'autre a écrites, et vous êtes invité à parcourir.
Néanmoins , le programme doit encore être compilé, car il reste parfaitement valide et conforme au standard C.
Ces types d'erreurs, aucune norme et aucun compilateur ne peuvent protéger les imprudents. J'imagine que c'est exactement ce qu'ils ont l'intention de vous apprendre.
Les paranoïaques cherchent constamment à changer la nature du C pour éliminer ces possibilités problématiques et ainsi nous sauver de nous-mêmes; mais c'est faux . C'est la responsabilité que nous sommes obligés d' accepter lorsque nous choisissons de poursuivre le pouvoir et d'obtenir la liberté que nous offre un contrôle plus direct et complet de la machine. Les promoteurs et les poursuivants de la perfection dans la performance n'accepteront jamais moins.
Portabilité et généralité qu'elle représente est une considération fondamentalement distincte et tout ce que la norme cherche à traiter:
Ce document précise la forme et établit l'interprétation des programmes exprimés dans le langage de programmation C. Son but est de: promouvoir la portabilité , la fiabilité, la maintenabilité et l'exécution efficace des programmes en langage C sur une variété de systèmes informatiques .
C'est pourquoi il est parfaitement approprié de le garder distinct de la définition et des spécifications techniques du langage lui-même. Contrairement à ce que beaucoup semblent penser, la généralité est antithétique à exceptionnelle et exemplaire .
De conclure:
- Examiner et manipuler les pointeurs eux-mêmes est invariablement valable et souvent fructueux . L'interprétation des résultats peut être ou non significative, mais la calamité n'est jamais invitée tant que le pointeur n'est pas déréférencé ; jusqu'à ce qu'une tentative d' accès à l'adresse liée à soit effectuée.
Si ce n'était pas vrai, la programmation telle que nous la connaissons - et nous l'aimons - n'aurait pas été possible.
C
avec ce qui est sûr enC
. Il est toujours possible de comparer deux pointeurs au même type (vérifier l'égalité, par exemple), en utilisant l'arithmétique des pointeurs et en comparant>
et<
n'est sûre que lorsqu'elle est utilisée dans un tableau donné (ou un bloc de mémoire).