Quel est le comportement commun indéfini / non spécifié pour C que vous rencontrez? [fermé]

StackOverflow https://stackoverflow.com/questions/98340

  •  01-07-2019
  •  | 
  •  

Question

Un exemple de comportement non spécifié en langage C est l’ordre d’évaluation des arguments d’une fonction. Cela peut être laissé à droite ou de droite à gauche, vous ne savez tout simplement pas. Cela affecterait la manière dont foo(c++, c) ou foo(++c, c) sera évalué.

Quel autre comportement non spécifié existe-t-il qui peut surprendre le programmeur non averti?

Était-ce utile?

La solution

Une question d’avocat spécialiste des langues. Hmkay.

Mon top3 personnel:

  1. violation de la règle de crénelage strict
  2. violation de la règle de crénelage strict
  3. enfreint la règle d'aliasing stricte

    : -)

Modifier Voici un petit exemple qui se trompe deux fois:

(supposons que les bits 32 bits et le petit endian)

float funky_float_abs (float a)
{
  unsigned int temp = *(unsigned int *)&a;
  temp &= 0x7fffffff;
  return *(float *)&temp;
}

Ce code tente d'obtenir la valeur absolue d'un float en mélangeant bit avec le bit de signe directement dans la représentation d'un float.

Cependant, le résultat de la création d'un pointeur sur un objet en convertissant d'un type à un autre n'est pas valide. Le compilateur peut supposer que les pointeurs sur des types différents ne pointent pas sur le même bloc de mémoire. Ceci est vrai pour tous les types de pointeurs sauf void * et char * (la signification ne compte pas).

Dans le cas ci-dessus, je le fais deux fois. Une fois pour obtenir un alias int pour le float a et une fois pour convertir la valeur en float.

Il existe trois façons valables de faire la même chose.

Utilisez un caractère ou un pointeur vide pendant la distribution. Ceux-ci font toujours référence à tout, ils sont donc en sécurité.

float funky_float_abs (float a)
{
  float temp_float = a;
  // valid, because it's a char pointer. These are special.
  unsigned char * temp = (unsigned char *)&temp_float;
  temp[3] &= 0x7f;
  return temp_float;
}

Utilisez memcopy. Memcpy utilise des pointeurs vides, de sorte qu'il forcera également le crénelage.

float funky_float_abs (float a)
{
  int i;
  float result;
  memcpy (&i, &a, sizeof (int));
  i &= 0x7fffffff;
  memcpy (&result, &i, sizeof (int));
  return result;
}

Troisième moyen valable: utiliser des unions. Ceci est explicitement non indéfini depuis C99:

float funky_float_abs (float a)
{
  union 
  {
     unsigned int i;
     float f;
  } cast_helper;

  cast_helper.f = a;
  cast_helper.i &= 0x7fffffff;
  return cast_helper.f;
}

Autres conseils

Mon comportement personnel non défini préféré est que, si un fichier source non vide ne se termine pas par une nouvelle ligne, le comportement n'est pas défini.

Je suppose que c'est vrai, cependant, aucun compilateur que je verrai n'aura traité un fichier source différemment selon qu'il est ou non terminé par une nouvelle ligne, autre que pour émettre un avertissement. Donc, ce n'est pas vraiment quelque chose qui va surprendre les programmeurs non conscients, à part cela, ils pourraient être surpris par l'avertissement.

Donc, pour les problèmes de véritable portabilité (qui dépendent principalement de la mise en œuvre plutôt que non spécifiés ou non définis, mais je pense que cela tombe dans l'esprit de la question):

  • le caractère n'est pas nécessairement (non) signé.
  • int peut avoir n'importe quelle taille à partir de 16 bits.
  • Les floats ne sont pas nécessairement au format IEEE ni conformes.
  • les types entiers ne sont pas nécessairement complémentaires et un débordement arithmétique d’entier provoque un comportement indéfini (le matériel moderne ne plantera pas, mais certaines optimisations du compilateur entraîneront un comportement différent de l’enveloppe, même si c'est ce que fait le matériel. Par exemple if (x+1 < x) peut être optimisé comme toujours false lorsque x a signé le type: voir option -fstrict-overflow dans GCC).
  • " / " ;, ". & "; et " .. " dans un #include n’a pas de signification définie et peut être traité différemment par différents compilateurs (cela varie en fait, et si cela ne va pas, cela gâchera votre journée).

Celles vraiment sérieuses qui peuvent surprendre même sur la plate-forme sur laquelle vous avez évolué, car le comportement n'est que partiellement indéfini / non spécifié:

  • Le threading POSIX et le modèle de mémoire ANSI. L'accès simultané à la mémoire n'est pas aussi bien défini que le pensent les novices. volatile ne fait pas ce que pensent les novices. L'ordre des accès en mémoire n'est pas aussi bien défini que le pensent les novices. Les accès peuvent être déplacés à travers des barrières de mémoire dans certaines directions. La cohérence du cache mémoire n'est pas requise.

  • Le code de profilage n’est pas aussi facile que vous le pensez. Si votre boucle de test n'a aucun effet, le compilateur peut en supprimer une partie ou la totalité. inline n'a pas d'effet défini.

Et, comme je pense que Nils a mentionné en passant:

  • VIOLER LA RÈGLE STRICT ALIASING.

Diviser quelque chose par un pointeur sur quelque chose. Ne compilera pas pour une raison quelconque ...: -)

result = x/*y;

Mon préféré est le suivant:

// what does this do?
x = x++;

Pour répondre à certains commentaires, il s’agit d’un comportement indéfini selon la norme. Voyant cela, le compilateur est autorisé à faire n'importe quoi jusqu'au formatage de votre disque dur inclus. Voir, par exemple, ce commentaire ici . Le fait n'est pas que vous pouvez voir qu'il existe une attente raisonnable possible de certains comportements. En raison de la norme C ++ et de la façon dont les points de séquence sont définis, cette ligne de code constitue en réalité un comportement indéfini.

Par exemple, si nous avions x = 1 avant la ligne ci-dessus, quel serait le résultat valide par la suite? Quelqu'un a commenté qu'il devrait être

  

x est incrémenté de 1

donc nous devrions voir x == 2 après. Cependant, ce n'est pas vrai, vous trouverez des compilateurs qui ont x == 1 après, ou peut-être même x == 3. Vous devriez regarder de près l'assembly généré pour voir pourquoi, mais les différences sont dues au problème sous-jacent. Pour l’essentiel, je pense que c’est parce que le compilateur est autorisé à évaluer les deux instructions d’assignations dans l’ordre de son choix. Il peut donc exécuter le x++ d’abord, ou le x = d’abord.

Un autre problème que j'ai rencontré (qui est défini, mais vraiment inattendu).

le caractère est diabolique.

  • signé ou non signé en fonction de ce que ressent le compilateur
  • pas obligatoire en tant que 8 bits

Je ne peux pas compter le nombre de fois que j'ai corrigé les spécificateurs de format printf pour qu'ils correspondent à leur argument. Toute incompatibilité est un comportement non défini .

  • Non, vous ne devez pas passer un int (ou long) à %x - un unsigned int est requis
  • Non, vous ne devez pas passer un %d à size_t - un %u est requis
  • Non, vous ne devez pas transmettre de %zu à %p ou void * - utilisez <=>
  • .
  • Non, vous ne devez pas imprimer de pointeur avec <=> ou <=> - utilisez <=> et transformez-le en un <=>

Un compilateur n'a pas à vous dire que vous appelez une fonction avec un nombre incorrect de paramètres / types de paramètres incorrects si le prototype de la fonction n'est pas disponible.

J'ai vu beaucoup de programmeurs relativement peu expérimentés se faire piquer par des constantes multi-caractères.

Ceci:

"x"

est un littéral de chaîne (de type char[2] et se décomposant en char* dans la plupart des contextes).

Ceci:

'x'

est une constante de caractère ordinaire (qui, pour des raisons historiques, est de type int).

Ceci:

'xy'

est également une constante de caractère parfaitement légal, mais sa valeur (qui est toujours du type <=>) est définie par l'implémentation. C'est un langage presque inutile qui sert principalement à semer la confusion.

Les développeurs de Clang ont publié des d'excellents exemples il y a quelque temps, dans un article que tout programmeur C devrait lire. Quelques points intéressants non mentionnés auparavant:

  • Débordement d'entier signé - non, il n'est pas correct d'envelopper une variable signée au-delà de son maximum
  • Déréférencement d'un pointeur NULL - oui, cela n'est pas défini et peut être ignoré, voir la partie 2 du lien.

Les EE viennent de découvrir qu'un > > -2 est un peu lourd.

J'ai acquiescé et leur ai dit que ce n'était pas naturel.

Assurez-vous de toujours initialiser vos variables avant de les utiliser! Quand je venais de commencer avec C, cela m'a causé un certain nombre de maux de tête.

Utilisation des versions de macro de fonctions telles que " max " ou & "; isupper &" ;. Les macros évaluent leurs arguments deux fois. Vous obtiendrez ainsi des effets secondaires inattendus lorsque vous appelez max (++ i, j) ou isupper (* p ++)

Ce qui précède concerne le standard C. En C ++, ces problèmes ont en grande partie disparu. La fonction max est maintenant une fonction basée sur un modèle.

en oubliant d’ajouter static float foo(); dans le fichier d’en-tête, uniquement pour que des exceptions en virgule flottante soient émises lorsqu’il renverrait 0.0f;

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top