Comment fonctionne la comparaison de pointeurs en C? Est-il correct de comparer des pointeurs qui ne pointent pas vers le même tableau?


33

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 pet le qpoint 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 ptet pxne pointent pas vers le même tableau (au moins dans ma compréhension).

Est également pt > pxdû 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 test supérieure à celle de x? C'est pourquoi pt > pxc'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 sbrkpeuvent ê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 de mallocn'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:

  1. p[0].p0 < p[0].p1
  2. p[1].p0 < p[1].p1
  3. p[2].p0 < p[2].p1

La réponse est 0, 1et 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 , et peut-être l' . 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)


17
Vous confondez peut - être ce qui est valable dans Cavec ce qui est sûr en C. 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).
Adrian Mole

13
En passant , vous ne devriez pas apprendre le C de K&R. Pour commencer, la langue a subi de nombreux changements depuis. Et, pour être honnête, l'exemple de code qu'il contient date d'une époque où la justesse plutôt que la lisibilité étaient appréciées.
paxdiablo

5
Non, il n'est pas garanti de fonctionner. Il peut échouer en pratique sur les machines avec des modèles de mémoire segmentée. Voir C a-t-il un équivalent de std :: less de C ++? Sur la plupart des machines modernes, cela fonctionnera malgré UB.
Peter Cordes

6
@Adam: Fermer, mais c'est en fait UB (à moins que le compilateur que l'OP utilisait, GCC, choisisse de le définir. Cela pourrait). Mais UB ne signifie pas "explose définitivement"; l'un des comportements possibles pour UB fonctionne comme vous l'espériez !! C'est ce qui rend UB si méchant; il peut fonctionner correctement dans une version de débogage et échouer avec l'optimisation activée, ou vice versa, ou s'arrêter en fonction du code environnant. La comparaison d'autres pointeurs vous donnera toujours une réponse, mais le langage ne définit pas ce que cette réponse signifiera (le cas échéant). Non, le plantage est autorisé. C'est vraiment UB.
Peter Cordes

3
@Adam: Oh oui, peu importe la première partie de mon commentaire, j'ai mal lu le vôtre. Mais vous prétendez que comparer d'autres pointeurs vous donnera toujours une réponse . Ce n'est pas vrai. Ce serait un résultat non spécifié , pas un UB complet. UB est bien pire et signifie que votre programme pourrait segfault ou SIGILL si l'exécution atteint cette instruction avec ces entrées (à tout moment avant ou après que cela se produise réellement). (Plausible uniquement sur x86-64 si l'UB est visible au moment de la compilation, mais en général tout peut arriver.) Une partie de l'intérêt de l'UB est de laisser le compilateur émettre des hypothèses "dangereuses" lors de la génération de asm.
Peter Cordes

Réponses:


33

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:

Lorsque deux pointeurs sont comparés, le résultat dépend des emplacements relatifs dans l'espace d'adressage des objets pointés. Si deux pointeurs vers des types d'objet pointent tous deux vers le même objet, ou si les deux pointent un au-delà du dernier élément du même objet tableau, ils se comparent égaux. Si les objets pointés sont des membres du même objet agrégé, les pointeurs vers les membres de la structure déclarés plus tard comparent les pointeurs supérieurs aux membres déclarés plus tôt dans la structure, et les pointeurs vers les éléments du tableau avec des valeurs d'indice supérieures comparent les pointeurs vers les éléments du même tableau avec des valeurs d'indice inférieures. Tous les pointeurs vers des membres du même objet union sont égaux.

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:

  • Il existe un modèle de mémoire plate où il existe une correspondance 1 à 1 entre une adresse et une valeur entière.
  • Que les valeurs de pointeur converties tiennent dans un type entier.
  • Que l'implémentation traite simplement les pointeurs comme des entiers lors de l'exécution de comparaisons sans exploiter la liberté donnée par un comportement non défini.
  • Qu'une pile est utilisée et que les variables locales y sont stockées.
  • Qu'un tas est utilisé pour extraire la mémoire allouée.
  • Que la pile (et donc les variables locales) apparaît à une adresse plus élevée que le tas (et donc les objets alloués).
  • Ces constantes de chaîne apparaissent à une adresse inférieure à celle du tas.

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.


3
@Shisui Même étant donné cela, vous ne devriez toujours pas dépendre des résultats. Les compilateurs peuvent devenir très agressifs en matière d'optimisation et utiliseront un comportement indéfini pour le faire. Il est possible que l'utilisation d'un compilateur différent et / ou de paramètres d'optimisation différents puisse générer une sortie différente.
dbush

2
@Shisui: Il arrivera en général de travailler sur des machines avec un modèle de mémoire plat, comme x86-64. Certains compilateurs pour de tels systèmes peuvent même définir le comportement dans leur documentation. Sinon, un comportement "insensé" peut se produire en raison de l'UB visible au moment de la compilation. (En pratique, je ne pense pas que quiconque veuille cela, donc ce n'est pas quelque chose que les compilateurs traditionnels recherchent et "essaient de casser".)
Peter Cordes

1
Comme si un compilateur voit qu'un chemin d'exécution mènerait <entre le mallocré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 une ud2instruction (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- voidfonction. godbolt.org est en panne en ce moment, semble-t-il, mais essayez de copier / coller int foo(){int x=2;}et notez l'absence d'unret
Peter Cordes

4
@Shisui: TL: DR: ce n'est pas un C portable, malgré le fait qu'il fonctionne bien sur Linux x86-64. Faire des hypothèses sur les résultats de la comparaison est tout simplement fou. Si vous n'êtes pas dans le thread principal, votre pile de threads aura été allouée dynamiquement en utilisant le même mécanisme que celui mallocutilisé 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 au malloc- dessus allouées dynamiquement. espace de rangement.
Peter Cordes

2
@PeterCordes: Ce qui est nécessaire, c'est de reconnaître divers aspects du comportement comme "éventuellement définis", de sorte que les implémentations puissent les définir ou non, à leur guise, mais doivent indiquer de manière testable (par exemple une macro prédéfinie) s'ils ne le font pas. De plus, au lieu de caractériser que toute situation où les effets d'une optimisation seraient observables en tant que «comportement indéfini», il serait beaucoup plus utile de dire que les optimiseurs peuvent considérer certains aspects du comportement comme «non observables» s'ils indiquent qu'ils faites-le. Par exemple, étant donné int x,y;, une implémentation ...
supercat

12

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.

Tout d'abord, je pensais que j'obtiendrais un type indéfini ou un type ou une erreur, car pt un px ne pointe pas vers le même tableau (du moins à ma connaissance).

Non, le résultat dépend de la mise en œuvre et d'autres facteurs imprévisibles.

Est également pt> px parce que les deux pointeurs pointent vers des variables stockées sur la pile et que la pile se développe, donc l'adresse mémoire de t est supérieure à celle de x? C'est pourquoi pt> px est vrai?

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.

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.

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.

Lorsque deux pointeurs sont comparés, le résultat dépend des emplacements relatifs dans l'espace d'adressage des objets pointés. ... Si les objets pointés sont des membres du même objet agrégé, ... les pointeurs vers des éléments de tableau avec des valeurs d'indice plus élevées comparent plus que les pointeurs aux éléments du même tableau avec des valeurs d'indice plus faibles.

Dans tous les autres cas, le comportement n'est pas défini.

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).


1
Plus important encore, avec tet xé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.
Peter Cordes

1
votre compilateur pourrait optimiser l'ensemble de la fonction, y compris les effets secondaires visibles Pas une surestimation: pour d'autres types d'UB (comme tomber de la fin d'une non- voidfonction), 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, gccne fait pas cela, seulement g++).
Peter Cordes

6

Puis demandé

p[0].p0 < p[0].p1
p[1].p0 < p[1].p1
p[2].p0 < p[2].p1

Évaluer. La réponse est 0, 1 et 0.

Ces questions se réduisent à:

  1. Est le tas au-dessus ou en dessous de la pile.
  2. Est le tas au-dessus ou en dessous de la section littérale de chaîne du programme.
  3. identique à [1].

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:

<empty>
text
rodata
rwdata
bss
< empty, used for heap >
...
stack
kernel

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.


3
Pas d'implémentation définie, non définie! Pensez-y de cette façon, les premières peuvent varier entre les implémentations mais les implémentations doivent documenter la façon dont le comportement est décidé. Ce dernier signifie que le comportement peut varier de quelque manière que ce soit et l'implémentation n'a pas à vous dire squat :-)
paxdiablo

1
@paxdiablo: Selon la justification des auteurs de la norme, "Comportement indéfini ... identifie également les zones d'extension de langage conforme possible: le réalisateur peut augmenter le langage en fournissant une définition du comportement officiellement indéfini." La justification dit en outre: "Le but est de donner au programmeur une chance de se battre pour créer de puissants programmes C qui sont également très portables, sans sembler rabaisser les programmes C parfaitement utiles qui se trouvent ne pas être portables, donc l'adverbe strictement." Les rédacteurs de compilateurs commerciaux le comprennent, mais certains autres rédacteurs de compilateurs ne le comprennent pas.
supercat

Il existe un autre aspect défini par l'implémentation; la comparaison du pointeur est signée , donc selon la machine / os / compilateur, certaines adresses peuvent être interprétées comme négatives. Par exemple, une machine 32 bits qui a placé la pile à 0xc << 28, afficherait probablement les variables automatiques à une adresse de bailleur que le tas ou les rodata.
mevets

1
@mevets: La norme spécifie-t-elle une situation dans laquelle la signature des pointeurs dans les comparaisons serait observable? Je m'attendrais à ce que si une plate-forme 16 bits autorise des objets supérieurs à 32 768 octets, et arr[]est un tel objet, la norme exigerait une arr+32768comparaison plus grande que arrmême si une comparaison de pointeurs signée signalait le contraire.
supercat

Je ne sais pas; l'étalon C est en orbite dans le neuvième cercle de Dante, priant pour l'euthanasie. L'OP faisait spécifiquement référence à K&R et à une question d'examen. #UB est des débris d'un groupe de travail paresseux.
mevets

1

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:

extern int x[],y[];
int test(int i)
{
    int *p = y+i;
    y[0] = 4;
    if (p == x+10)
        *p = 1;
    return y[0];
}

Clang et gcc généreront du code qui retournera toujours 4 même s'il xy a dix éléments, yle suit immédiatement et iest nul, ce qui fait que la comparaison est vraie et p[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 par x[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'il xavait au moins 11 éléments, ce qui rendrait impossible cet accès à affecter y.

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.


0

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 ?


Techniquement, les tableaux ne sont pas vraiment une exception puisque vous ne déclarez pas arr[0], arr[1]etc séparément. Vous déclarez arrdans son ensemble, donc l'ordre des éléments de tableau individuels est un problème différent de celui décrit dans cette question.
paxdiablo

1
Les éléments de structure sont garantis en ordre, ce qui garantit que l'on peut utiliser memcpypour 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 un malloc()stockage alloué. La offsetofmacro 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 a char[], mais le Standard ne dit pas expressément que les octets d'une structure sont (ou peuvent être utilisés comme) un objet tableau.
supercat

-4

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 mallocou 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 malloclui-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 mallocne 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.

strcpycommencera à 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 p1pointeur a été initialisé à un bloc d'exactement 10 octets.

  • S'il ase 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 strcpyil 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, strcpycontinuera 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.


3
Cette réponse est malheureusement intrinsèquement invalide. Vous ne pouvez rien raisonner sur un comportement indéfini. La comparaison n'a pas besoin d'être effectuée au niveau de la machine.
Antti Haapala

6
Ghii, en fait non. Si vous regardez C11 Annexe J et 6.5.8, l'acte de comparaison lui-même est UB. Le déréférencement est une question distincte.
paxdiablo

6
Non, UB peut toujours être nuisible avant même que le pointeur ne soit déréférencé. Un compilateur est libre d'optimiser complètement une fonction avec UB en un seul NOP, même si cela change évidemment le comportement visible.
nanofarad

2
@Ghii, l'annexe J (le bit que j'ai mentionné) est la liste des choses qui sont un comportement indéfini , donc je ne sais pas comment cela prend en charge votre argument :-) 6.5.8 appelle explicitement la comparaison en tant qu'UB. Pour votre commentaire à supercat, il n'y a aucune comparaison en cours lorsque vous imprimez un pointeur, vous avez donc probablement raison de ne pas planter. Mais ce n'est pas ce que le PO demandait. 3.4.3est également une section à consulter: elle définit UB comme un comportement "pour lequel la présente Norme internationale n'impose aucune exigence".
paxdiablo

3
@GhiiVelte, vous continuez à dire des choses qui sont tout simplement erronées, même si cela vous est signalé. Oui, l'extrait de code que vous avez publié doit être compilé, mais votre affirmation selon laquelle il s'exécute sans accroc est incorrecte. Je vous suggère de lire la norme, en particulier (dans ce cas) 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 ".
paxdiablo

-5

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.


2
La pile de mots apparaît exactement zéro fois dans la norme C11. Et un comportement indéfini signifie que tout peut arriver (y compris le plantage du programme).
paxdiablo

1
@paxdiablo Ai-je dit que c'était le cas?
nickelpro

2
Vous avez mentionné les variables attribuées à la pile. Il n'y a pas de pile dans la norme, c'est juste un détail d'implémentation. Le problème le plus grave avec cette réponse est l'affirmation selon laquelle vous pouvez comparer des pointeurs sans risque d'accident - c'est tout simplement faux.
paxdiablo

1
@nickelpro: Si l'on souhaite écrire du code compatible avec les optimiseurs de gcc et clang, il faut passer par beaucoup de cerceaux idiots. Les deux optimiseurs rechercheront de manière agressive des opportunités de tirer des conclusions sur les éléments auxquels les pointeurs pourront accéder chaque fois que le Standard peut être modifié pour les justifier (et parfois même lorsqu'il n'y en a pas). Étant donné int x[10],y[10],*p;, si le code évalue y[0], puis évalue p>(x+5)et écrit *psans modification pdans l'intervalle, et enfin évalue à y[0]nouveau, ...
supercat

1
nickelpro, acceptez d'accepter d'être en désaccord, mais votre réponse est toujours fondamentalement erronée. Je compare votre approche à celle des personnes qui utilisent (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')plutôt que isalpha()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 :-)
paxdiablo
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.