Was sind die häufigsten undefinierten/nicht spezifizierten Verhaltensweisen für C, auf die Sie stoßen?[geschlossen]

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

  •  01-07-2019
  •  | 
  •  

Frage

Ein Beispiel für nicht spezifiziertes Verhalten in der Sprache C ist die Reihenfolge der Auswertung von Argumenten für eine Funktion.Es könnte von links nach rechts oder von rechts nach links sein, Sie wissen es einfach nicht.Dies würde sich darauf auswirken, wie foo(c++, c) oder foo(++c, c) wird ausgewertet.

Welches andere nicht spezifizierte Verhalten kann den ahnungslosen Programmierer überraschen?

War es hilfreich?

Lösung

Eine Frage eines Sprachjuristen.Hmkay.

Meine persönlichen Top3:

  1. Verstoß gegen die strenge Aliasing-Regel
  2. Verstoß gegen die strenge Aliasing-Regel
  3. Verstoß gegen die strenge Aliasing-Regel

    :-)

Bearbeiten Hier ein kleines Beispiel, das gleich doppelt schiefgeht:

(Angenommen, 32-Bit-Ints und Little Endian)

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

Dieser Code versucht, den Absolutwert einer Gleitkommazahl durch Bit-Twiddling mit dem Vorzeichenbit direkt in der Darstellung einer Gleitkommazahl zu ermitteln.

Das Ergebnis der Erstellung eines Zeigers auf ein Objekt durch Umwandlung von einem Typ in einen anderen ist jedoch kein gültiges C.Der Compiler geht möglicherweise davon aus, dass Zeiger auf verschiedene Typen nicht auf denselben Speicherblock verweisen.Dies gilt für alle Arten von Zeigern außer void* und char* (das Vorzeichen spielt keine Rolle).

Im obigen Fall mache ich das zweimal.Einmal, um einen Int-Alias ​​für den Float a zu erhalten, und einmal, um den Wert wieder in Float umzuwandeln.

Es gibt drei gültige Möglichkeiten, dasselbe zu tun.

Verwenden Sie während der Umwandlung einen char- oder void-Zeiger.Diese Alias ​​sind immer auf alles anwendbar und daher sicher.

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;
}

Verwenden Sie Memcopy.Memcpy akzeptiert ungültige Zeiger und erzwingt daher auch Aliasing.

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

Der dritte gültige Weg:Gewerkschaften verwenden.Dies ist ausdrücklich der Fall seit C99 nicht undefiniert:

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;
}

Andere Tipps

Mein persönliches, undefiniertes Verhalten ist, dass, wenn eine nicht leere Quelldatei nicht in einer neuen Linie endet, und definiert ist.

Ich vermute, es ist jedoch wahr, dass kein Compiler, den ich jemals sehen werde, eine Quelldatei anders behandelt hat, je nachdem, ob sie neu ist, abgeschlossen, außer einer Warnung zu emittieren. Es ist also nicht wirklich etwas, das nicht bewusstes Programmierer überraschen wird, außer dass sie von der Warnung überrascht werden könnten.

Für echte Portabilitätsprobleme (die meistens eher implementierungsabhängig als nicht spezifiziert oder nicht definiert sind, aber ich denke, das fällt in den Geist der Frage):

  • Char ist nicht unbedingt (UN) unterzeichnet.
  • Int kann jede Größe von 16 Bits haben.
  • Floats sind nicht unbedingt IEEE-formatiert oder konformant.
  • Ganzzahltypen sind nicht unbedingt zwei Komplement, und ganzzahliger arithmetischer Überlauf verursacht ein definiertes Verhalten (moderne Hardware wird nicht abstürzen, aber einige Compiler -Optimierungen führen zu einem Verhalten, das sich von Wickaround unterscheidet, obwohl dies das ist, was die Hardware tut. Zum Beispiel. Zum Beispiel. if (x+1 < x) kann wie immer optimiert werden, wenn x hat signiert Typ: siehe -fstrict-overflow Option in GCC).
  • "/", ". und ".." in einem #include haben keine definierte Bedeutung und kann von verschiedenen Compilern unterschiedlich behandelt werden (dies variiert tatsächlich, und wenn es schief geht, wird es Ihren Tag ruinieren).

Wirklich ernsthafte, die selbst auf der Plattform, auf der Sie entwickelt wurden, überraschend sein, da das Verhalten nur teilweise undefiniert / nicht spezifiziert ist:

  • POSIX -Threading und das ANSI -Speichermodell. Der gleichzeitige Zugriff auf Speicher ist nicht so gut definiert, wie Anfänger denken. Volatile tut nicht das, was Anfänger denken. Die Reihenfolge des Speicherzugriffs ist nicht so gut definiert, wie Anfänger denken. Zugriffe kann in bestimmte Richtungen über Gedächtnisbarrieren bewegt werden. Speicher -Cache -Kohärenz ist nicht erforderlich.

  • Profilerierungscode ist nicht so einfach, wie Sie denken. Wenn Ihre Testschleife keinen Einfluss hat, kann der Compiler einen Teil oder alle davon entfernen. Inline hat keinen definierten Effekt.

Und wie ich denke, Nils erwähnt im Vorbeigehen:

  • Verstoß gegen die strenge Aliasing -Regel.

Etwas durch einen Zeiger auf etwas zu teilen. Ich werde einfach aus irgendeinem Grund nicht kompilieren ... :-)

result = x/*y;

Mein Favorit ist das:

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

Um einige Kommentare zu beantworten, ist es ein undefiniertes Verhalten gemäß dem Standard. Wenn man dies sieht, darf der Compiler etwas bis zum Format Ihrer Festplatte tun. Siehe zum Beispiel Dieser Kommentar hier. Der Punkt ist nicht, dass Sie sehen können, dass ein Verhalten möglich ist. Aufgrund des C ++ - Standards und der Art und Weise, wie die Sequenzpunkte definiert sind, ist diese Codezeile tatsächlich ein undefiniertes Verhalten.

Zum Beispiel, wenn wir hatten x = 1 Was würde das gültige Ergebnis vor der obigen Zeile danach sein? Jemand bemerkte, dass es sein sollte

x wird durch 1 erhöht

Also sollten wir x == 2 danach sehen. Dies gilt jedoch nicht eigentlich nicht. zum zugrunde liegenden Problem. Ich denke, dies liegt im Wesentlichen daran, dass der Compiler die beiden Zuordnungsanweisungen in beliebiger Reihenfolge bewerten darf, sodass er das tun kann x++ zuerst oder der x = Erste.

Ein weiteres Problem, auf das ich gestoßen bin (was definiert, aber definitiv unerwartet ist).

Char ist böse.

  • signiert oder nicht signiert, je nachdem, was der Compiler fühlt
  • nicht als 8 Bit beauftragt

Ich kann nicht zählen, wie oft ich das Printf -Format -Spezifizierer so korrigiert habe, um ihren Argument zu entsprechen. Jede Nichtübereinstimmung ist ein undefiniertes Verhalten.

  • Nein, Sie dürfen keine passieren int (oder long) zu %x - ein unsigned int ist nötig
  • Nein, Sie dürfen keine passieren unsigned int zu %d - ein int ist nötig
  • Nein, Sie dürfen nicht a passieren size_t zu %u oder %d - verwenden %zu
  • Nein, Sie dürfen keinen Zeiger mit drucken %d oder %x - verwenden %p und zu einem gegossen void *

Ein Compiler muss Ihnen nicht mitteilen, dass Sie eine Funktion mit der falschen Anzahl von Parametern/falschen Parametypen aufrufen, wenn der Funktionsprototyp nicht verfügbar ist.

Ich habe viele relativ unerfahrene Programmierer gesehen, die von Multi-Charakter-Konstanten gebissen wurden.

Dies:

"x"

ist ein String -Literal (der vom Typ ist char[2] und verfällt char* in den meisten Kontexten).

Dies:

'x'

ist eine gewöhnliche Charakterkonstante (die aus historischen Gründen vom Typ ist int).

Dies:

'xy'

ist auch eine vollkommen rechtliche Charakterkonstante, aber sein Wert (der noch vom Typ ist int) ist implementierungsdefiniert. Es ist eine fast nutzlose Sprachfunktion, die hauptsächlich dazu dient, Verwirrung zu verursachen.

Die Klangentwickler haben einige gepostet Tolle Beispiele Vor einiger Zeit sollte in einem Beitrag jeder C -Programmierer lesen. Einige interessante, die zuvor nicht erwähnt wurden:

  • Signierter Ganzzahlüberlauf - Nein, es ist nicht in Ordnung, eine signierte Variable über das Maximum hinaus zu wickeln.
  • Dereferenzieren ein Nullzeiger - Ja, das ist undefiniert und kann ignoriert werden, siehe Teil 2 des Links.

Die EE hier haben gerade entdeckt, dass A >>-2 ein bisschen belastet ist.

Ich nickte und sagte ihnen, es sei nicht natürlich.

Stellen Sie sicher, dass Sie Ihre Variablen immer initialisieren, bevor Sie sie verwenden! Als ich gerade mit C angefangen hatte, verursachte ich eine Reihe von Kopfschmerzen.

Verwenden der Makroversionen von Funktionen wie "max" oder "isupper". Die Makros bewerten ihre Argumente zweimal, sodass Sie unerwartete Nebenwirkungen erhalten, wenn Sie Max (++ I, J) oder Isupper (*P ++) aufrufen

Das obige ist für Standard C. In C ++ sind diese Probleme weitgehend verschwunden. Die MAX -Funktion ist jetzt eine Vorlagenfunktion.

Vergessen hinzufügen static float foo(); In der Header -Datei nur, um schwimmende Punktausnahmen zu erhalten, die ausgeworfen werden, wenn sie 0,0F zurückgeben;

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top