Dans K&R (The C Programming Language 2nd Edition) chapitre 5, je lis ce qui suit:
Premièrement, les pointeurs peuvent être comparés dans certaines circonstances. Si
p
et leq
point aux membres du même réseau, les relations alors comme==
,!=
,<
,>=
, etc. fonctionnent correctement.
Ce qui semble impliquer que seuls les pointeurs pointant vers le même tableau peuvent être comparés.
Mais quand j'ai essayé ce code
char t = 't';
char *pt = &t;
char x = 'x';
char *px = &x;
printf("%d\n", pt > px);
1
est imprimé à l'écran.
Tout d' abord, je pensais que je recevrais non défini ou un certain type ou d'une erreur, parce que pt
et px
ne pointent pas vers le même tableau (au moins dans ma compréhension).
Est également pt > px
dû au fait que les deux pointeurs pointent vers des variables stockées sur la pile et que la pile augmente, donc l'adresse mémoire de t
est supérieure à celle de x
? C'est pourquoi pt > px
c'est vrai?
Je suis plus confus lorsque malloc est introduit. Également dans K&R au chapitre 8.7, ce qui suit est écrit:
Il y a toujours une hypothèse, cependant, que les pointeurs vers différents blocs renvoyés par
sbrk
peuvent être comparés de manière significative. Ceci n'est pas garanti par la norme qui permet des comparaisons de pointeurs uniquement dans un tableau. Ainsi, cette version demalloc
n'est portable que parmi les machines pour lesquelles la comparaison générale des pointeurs est significative.
Je n'ai eu aucun problème à comparer des pointeurs pointant vers l'espace malloculé sur le tas à des pointeurs pointant vers des variables de pile.
Par exemple, le code suivant a bien fonctionné et a 1
été imprimé:
char t = 't';
char *pt = &t;
char *px = malloc(10);
strcpy(px, pt);
printf("%d\n", pt > px);
Sur la base de mes expériences avec mon compilateur, 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.
Pourtant, je suis confus par ce que je lis dans K&R.
La raison pour laquelle je demande, c'est parce que mon prof. en fait une question d'examen. Il a donné le code suivant:
struct A { char *p0; char *p1; }; int main(int argc, char **argv) { char a = 0; char *b = "W"; char c[] = [ 'L', 'O', 'L', 0 ]; struct A p[3]; p[0].p0 = &a; p[1].p0 = b; p[2].p0 = c; for(int i = 0; i < 3; i++) { p[i].p1 = malloc(10); strcpy(p[i].p1, p[i].p0); } }
Qu'est-ce que cela évalue:
p[0].p0 < p[0].p1
p[1].p0 < p[1].p1
p[2].p0 < p[2].p1
La réponse est 0
, 1
et 0
.
(Mon professeur inclut l'avertissement sur l'examen que les questions sont pour un environnement de programmation Ubuntu Linux 16.04, version 64 bits)
(note de l'éditeur: si SO autorisait plus de balises, cette dernière partie justifierait x86-64 , linux et peut-être l' assemblage . Si le point de la question / classe était spécifiquement les détails d'implémentation du système d'exploitation de bas niveau, plutôt que le C. portable)
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).Réponses:
Selon la norme C11 , les opérateurs relationnels
<
,<=
,>
et>=
ne peuvent être utilisés sur des pointeurs vers des éléments du même tableau ou un objet struct. Ceci est expliqué dans la section 6.5.8p5:Notez que toutes les comparaisons qui ne satisfont pas à cette exigence invoquent un comportement indéfini , ce qui signifie (entre autres) que vous ne pouvez pas dépendre des résultats pour être répétables.
Dans votre cas particulier, pour la comparaison entre les adresses de deux variables locales et entre l'adresse d'une adresse locale et une adresse dynamique, l'opération a semblé "fonctionner", mais le résultat pourrait changer en apportant une modification apparemment sans rapport avec votre code ou même de compiler le même code avec différents paramètres d'optimisation. Avec un comportement indéfini, ce n'est pas parce que le code peut se bloquer ou générer une erreur qu'il le fera .
Par exemple, un processeur x86 fonctionnant en mode réel 8086 possède un modèle de mémoire segmentée utilisant un segment 16 bits et un décalage 16 bits pour créer une adresse 20 bits. Donc, dans ce cas, une adresse ne se convertit pas exactement en un entier.
Les opérateurs d'égalité
==
et!=
cependant n'ont pas cette restriction. Ils peuvent être utilisés entre deux pointeurs vers des types compatibles ou des pointeurs NULL. Donc, l'utilisation de==
ou!=
dans vos deux exemples produirait un code C valide.Cependant, même avec
==
et!=
vous pourriez obtenir des résultats inattendus mais toujours bien définis. Voir Une comparaison d'égalité de pointeurs indépendants peut-elle être évaluée comme vraie? pour plus de détails à ce sujet.En ce qui concerne la question d'examen donnée par votre professeur, elle fait un certain nombre d'hypothèses erronées:
Si vous deviez exécuter ce code sur une architecture et / ou avec un compilateur qui ne satisfait pas ces hypothèses, vous pourriez obtenir des résultats très différents.
En outre, les deux exemples présentent également un comportement indéfini lorsqu'ils appellent
strcpy
, car l'opérande droit (dans certains cas) pointe vers un seul caractère et non une chaîne terminée par null, ce qui entraîne la lecture de la fonction au-delà des limites de la variable donnée.la source
<
entre lemalloc
résultat et une variable locale (stockage automatique, c'est-à-dire la pile), il pourrait supposer que le chemin d'exécution n'est jamais pris et simplement compiler la fonction entière en uneud2
instruction (déclenche une illégalité -instruction exception que le noyau gérera en fournissant un SIGILL au processus). GCC / clang le font en pratique pour d'autres types d'UB, comme tomber de la fin d'une non-void
fonction. godbolt.org est en panne en ce moment, semble-t-il, mais essayez de copier / collerint foo(){int x=2;}
et notez l'absence d'unret
malloc
utilisé pour obtenir plus de mémoire du système d'exploitation, il n'y a donc aucune raison de supposer que vos variables locales (pile de threads) sont aumalloc
- dessus allouées dynamiquement. espace de rangement.int x,y;
, une implémentation ...Le principal problème avec la comparaison de pointeurs à deux tableaux distincts du même type est que les tableaux eux-mêmes n'ont pas besoin d'être placés dans un positionnement relatif particulier - l'un pourrait se retrouver avant et après l'autre.
Non, le résultat dépend de la mise en œuvre et d'autres facteurs imprévisibles.
Il n'y a pas nécessairement de pile . Lorsqu'il existe, il n'a pas besoin de grandir. Ça pourrait grandir. Il pourrait être non contigu d'une manière bizarre.
Regardons la spécification C , §6.5.8 à la page 85 qui traite des opérateurs relationnels (c'est-à-dire les opérateurs de comparaison que vous utilisez). Notez que cela ne concerne pas directement
!=
ou==
comparaison.La dernière phrase est importante. Bien que je réduise certains cas non liés pour économiser de l'espace, il y a un cas qui est important pour nous: deux tableaux, ne faisant pas partie du même objet struct / agrégat 1 , et nous comparons des pointeurs à ces deux tableaux. C'est un comportement indéfini .
Alors que votre compilateur vient d'insérer une sorte d'instruction machine CMP (comparer) qui compare numériquement les pointeurs, et vous avez eu de la chance ici, UB est une bête assez dangereuse. Littéralement, tout peut arriver - votre compilateur pourrait optimiser l'ensemble de la fonction, y compris les effets secondaires visibles. Il pourrait engendrer des démons nasaux.
1 Les pointeurs dans deux tableaux différents qui font partie de la même structure peuvent être comparés, car cela relève de la clause où les deux tableaux font partie du même objet agrégé (la structure).
la source
t
etx
étant défini dans la même fonction, il n'y a aucune raison de supposer quoi que ce soit sur la façon dont un compilateur ciblant x86-64 disposera les sections locales dans le cadre de la pile pour cette fonction. La pile qui croît vers le bas n'a rien à voir avec l'ordre de déclaration des variables dans une fonction. Même dans des fonctions distinctes, si l'une pouvait s'aligner dans l'autre, les habitants de la fonction "enfant" pouvaient toujours se mélanger avec les parents.void
fonction), g ++ et clang ++ font vraiment cela en pratique: godbolt.org/z/g5vesB ils Supposons que le chemin d'exécution ne soit pas pris car il conduit à UB et compilez ces blocs de base en une instruction illégale. Ou à aucune instruction du tout, juste en passant silencieusement à n'importe quel asm suivant si jamais cette fonction était appelée. (Pour une raison quelconque,gcc
ne fait pas cela, seulementg++
).Ces questions se réduisent à:
Et la réponse à ces trois questions est «mise en œuvre définie». Les questions de votre prof sont fausses; ils l'ont basé dans une mise en page Unix traditionnelle:
mais plusieurs unités modernes (et systèmes alternatifs) ne sont pas conformes à ces traditions. A moins qu'ils n'aient préfacé la question par "à partir de 1992"; assurez-vous de donner un -1 sur l'eval.
la source
arr[]
est un tel objet, la norme exigerait unearr+32768
comparaison plus grande quearr
même si une comparaison de pointeurs signée signalait le contraire.Sur presque toutes les plates-formes modernes à distance, les pointeurs et les entiers ont une relation d'ordre isomorphe, et les pointeurs vers les objets disjoints ne sont pas entrelacés. La plupart des compilateurs exposent cet ordre aux programmeurs lorsque les optimisations sont désactivées, mais la norme ne fait aucune distinction entre les plates-formes qui ont un tel ordre et celles qui n'en ont pas et n'exigent pas que toute implémentation expose un tel ordre au programmeur même sur des plates-formes qui le feraient. Définissez-le. Par conséquent, certains rédacteurs de compilateurs effectuent différents types d'optimisations et d '"optimisations" en se basant sur l'hypothèse que le code ne comparera jamais l'utilisation d'opérateurs relationnels sur des pointeurs à différents objets.
Selon la justification publiée, les auteurs de la norme voulaient que les implémentations étendent le langage en spécifiant comment elles se comporteront dans les situations que la norme qualifie de "comportement indéfini" (c'est-à-dire lorsque la norme n'impose aucune exigence). ), ce qui serait utile et pratique. , mais certains rédacteurs du compilateur préfèrent supposer que les programmes n'essaieront jamais de profiter de quoi que ce soit au-delà des exigences de la norme, plutôt que de permettre aux programmes d'exploiter utilement les comportements que les plateformes pourraient prendre en charge sans frais supplémentaires.
Je ne connais aucun compilateur de conception commerciale qui fasse quelque chose de bizarre avec les comparaisons de pointeurs, mais au fur et à mesure que les compilateurs se tournent vers le LLVM non commercial pour leur back-end, ils sont de plus en plus susceptibles de traiter de manière absurde du code dont le comportement avait été spécifié plus tôt. compilateurs pour leurs plateformes. Un tel comportement n'est pas limité aux opérateurs relationnels, mais peut même affecter l'égalité / l'inégalité. Par exemple, même si la norme spécifie qu'une comparaison entre un pointeur vers un objet et un pointeur "juste après" vers un objet immédiatement précédent comparera égal, les compilateurs basés sur gcc et LLVM sont enclins à générer du code absurde si les programmes effectuent de telles comparaisons.
Comme exemple d'une situation où même la comparaison d'égalité se comporte de manière absurde dans gcc et clang, considérons:
Clang et gcc généreront du code qui retournera toujours 4 même s'il
x
y a dix éléments,y
le suit immédiatement eti
est nul, ce qui fait que la comparaison est vraie etp[0]
écrite avec la valeur 1. Je pense que ce qui se passe est qu'une passe d'optimisation réécrit la fonction comme si elle*p = 1;
avait été remplacée parx[10] = 1;
. Ce dernier code serait équivalent si le compilateur était interprété*(x+10)
comme équivalent à*(y+i)
, mais malheureusement une étape d'optimisation en aval reconnaît qu'un accès àx[10]
ne serait défini que s'ilx
avait au moins 11 éléments, ce qui rendrait impossible cet accès à affectery
.Si les compilateurs peuvent obtenir cette "création" avec un scénario d'égalité de pointeur décrit par la norme, je ne leur ferais pas confiance pour s'abstenir de devenir encore plus créatif dans les cas où la norme n'impose pas d'exigences.
la source
C'est simple: la comparaison des pointeurs n'a pas de sens car les emplacements de mémoire des objets ne sont jamais garantis dans le même ordre que vous les avez déclarés. L'exception est les tableaux. & array [0] est inférieur à & array [1]. C'est ce que K&R souligne. Dans la pratique, les adresses des membres struct sont également dans l'ordre dans lequel vous les déclarez. Aucune garantie à ce sujet .... Une autre exception est si vous comparez un pointeur pour égal. Lorsqu'un pointeur est égal à un autre, vous savez qu'il pointe vers le même objet. Peu importe ce que c'est. Mauvaise question d'examen si vous me demandez. Selon Ubuntu Linux 16.04, environnement de programmation de version 64 bits pour une question d'examen? Vraiment ?
la source
arr[0]
,arr[1]
etc séparément. Vous déclarezarr
dans son ensemble, donc l'ordre des éléments de tableau individuels est un problème différent de celui décrit dans cette question.memcpy
pour copier une partie contiguë d'une structure et affecter tous les éléments qui s'y trouvent et n'affecter rien d'autre. La norme est bâclée quant à la terminologie quant aux types d'arithmétique de pointeur pouvant être effectués avec des structures ou unmalloc()
stockage alloué. Laoffsetof
macro serait plutôt inutile si l'on ne pouvait pas utiliser le même type d'arithmétique de pointeur avec les octets d'une structure qu'avec achar[]
, mais le Standard ne dit pas expressément que les octets d'une structure sont (ou peuvent être utilisés comme) un objet tableau.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:
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é,
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 .
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.
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:
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 :
result
explicite par souci de clarté , et nous l' imprimons pour forcer le compilateur à calculer ce qui serait autrement redondant, du code mort.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:
Alors que cette instruction doit être compilée et exécutée:
... comme cela doit:
... 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 :
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:
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
ousbrk
. 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èresbrk
; si de manière plus évidente , fréquente , sur des entités plus disparates , plus critique - et pertinente également sur des plateformes où celamalloc
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
, enle 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:
pour ça:
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 dea
, 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 alorsstrcpy
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é parmalloc
.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:
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:
Si ce n'était pas vrai, la programmation telle que nous la connaissons - et nous l'aimons - n'aurait pas été possible.
la source
3.4.3
est également une section à consulter: elle définit UB comme un comportement "pour lequel la présente Norme internationale n'impose aucune exigence".C11 6.5.6/9
, en gardant à l'esprit que le mot "doit" indique une exigenceL "Lorsque deux pointeurs sont soustraits, les deux doivent pointer vers des éléments du même objet tableau, ou un après le dernier élément de l'objet tableau ".Les pointeurs ne sont que des entiers, comme tout le reste d'un ordinateur. Vous pouvez absolument les comparer avec
<
et>
produire des résultats sans provoquer le plantage d'un programme. Cela dit, la norme ne garantit pas que ces résultats ont une signification en dehors des comparaisons de tableaux.Dans votre exemple de variables allouées à la pile, le compilateur est libre d'allouer ces variables à des registres ou à des adresses de mémoire de pile, et dans n'importe quel ordre. Les comparaisons telles que
<
et>
donc ne seront pas cohérentes entre les compilateurs ou les architectures. Cependant,==
et!=
ne sont pas si restreints, la comparaison de l' égalité des pointeurs est une opération valide et utile.la source
int x[10],y[10],*p;
, si le code évaluey[0]
, puis évaluep>(x+5)
et écrit*p
sans modificationp
dans l'intervalle, et enfin évalue ày[0]
nouveau, ...(ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')
plutôt queisalpha()
parce que quelle implémentation sensée aurait ces caractères discontinus? L'essentiel est que, même si aucune implémentation que vous connaissez ne pose problème, vous devriez coder autant que possible selon la norme si vous appréciez la portabilité. J'apprécie cependant le label "standards maven", merci pour cela. Je peux mettre mon CV :-)