Wenn Sie für den C-Standard einen Funktionszeiger auf einen Funktionszeiger eines anderen Typs umwandeln und diesen dann aufrufen, handelt es sich um ein undefiniertes Verhalten . Siehe Anhang J.2 (informativ):
Das Verhalten ist unter folgenden Umständen undefiniert:
- Mit einem Zeiger wird eine Funktion aufgerufen, deren Typ nicht mit dem Typ kompatibel ist, auf den gezeigt wird (6.3.2.3).
Abschnitt 6.3.2.3, Absatz 8 lautet:
Ein Zeiger auf eine Funktion eines Typs kann in einen Zeiger auf eine Funktion eines anderen Typs konvertiert werden und wieder zurück; Das Ergebnis muss mit dem ursprünglichen Zeiger verglichen werden. Wenn ein konvertierter Zeiger zum Aufrufen einer Funktion verwendet wird, deren Typ nicht mit dem Typ kompatibel ist, auf den verwiesen wird, ist das Verhalten undefiniert.
Mit anderen Worten, Sie können einen Funktionszeiger auf einen anderen Funktionszeigertyp umwandeln, ihn wieder zurücksetzen und aufrufen, und die Dinge werden funktionieren.
Die Definition von kompatibel ist etwas kompliziert. Es ist in Abschnitt 6.7.5.3, Absatz 15 zu finden:
Damit zwei Funktionstypen kompatibel sind, müssen beide kompatible Rückgabetypen 127 angeben .
Darüber hinaus müssen die Parametertyplisten, sofern beide vorhanden sind, in der Anzahl der Parameter und in der Verwendung des Ellipsen-Terminators übereinstimmen. entsprechende Parameter müssen kompatible Typen haben. Wenn ein Typ eine Parametertypliste hat und der andere Typ von einem Funktionsdeklarator angegeben wird, der nicht Teil einer Funktionsdefinition ist und eine leere Bezeichnerliste enthält, darf die Parameterliste keinen Ellipsenabschluss haben und der Typ jedes Parameters muss mit dem Typ kompatibel sein, der sich aus der Anwendung der Standardargument-Promotions ergibt. Wenn ein Typ eine Parametertypliste hat und der andere Typ durch eine Funktionsdefinition angegeben wird, die eine (möglicherweise leere) Bezeichnerliste enthält, müssen beide in der Anzahl der Parameter übereinstimmen. und der Typ jedes Prototypparameters muss mit dem Typ kompatibel sein, der sich aus der Anwendung der Standardargumentwerbung auf den Typ des entsprechenden Bezeichners ergibt. (Bei der Bestimmung der Typkompatibilität und eines zusammengesetzten Typs wird angenommen, dass jeder mit Funktion oder Array-Typ deklarierte Parameter den angepassten Typ und jeder mit qualifiziertem Typ deklarierte Parameter die nicht qualifizierte Version seines deklarierten Typs hat.)
127) Wenn beide Funktionstypen "alter Stil" sind, werden Parametertypen nicht verglichen.
Die Regeln zum Bestimmen, ob zwei Typen kompatibel sind, sind in Abschnitt 6.2.7 beschrieben, und ich werde sie hier nicht zitieren, da sie ziemlich lang sind, aber Sie können sie im Entwurf des C99-Standards (PDF) lesen .
Die relevante Regel hier ist in Abschnitt 6.7.5.1, Absatz 2:
Damit zwei Zeigertypen kompatibel sind, müssen beide identisch qualifiziert sein und beide müssen Zeiger auf kompatible Typen sein.
Da a void*
nicht mit a kompatibel iststruct my_struct*
, ist ein Funktionszeiger vom Typ void (*)(void*)
nicht mit einem Funktionszeiger vom Typ kompatibel void (*)(struct my_struct*)
, so dass diese Umwandlung von Funktionszeigern ein technisch undefiniertes Verhalten ist.
In der Praxis können Sie jedoch in einigen Fällen sicher mit Casting-Funktionszeigern davonkommen. In der x86-Aufrufkonvention werden Argumente auf den Stapel übertragen, und alle Zeiger haben dieselbe Größe (4 Byte in x86 oder 8 Byte in x86_64). Das Aufrufen eines Funktionszeigers läuft darauf hinaus, die Argumente auf dem Stapel zu verschieben und einen indirekten Sprung zum Ziel des Funktionszeigers zu machen, und es gibt offensichtlich keine Vorstellung von Typen auf der Ebene des Maschinencodes.
Dinge, die Sie definitiv nicht tun können:
- Zwischen Funktionszeigern verschiedener Aufrufkonventionen umwandeln. Sie werden den Stapel durcheinander bringen und im besten Fall abstürzen, im schlimmsten Fall lautlos mit einer riesigen Sicherheitslücke klaffen. In der Windows-Programmierung geben Sie häufig Funktionszeiger weiter. Win32 erwartet , dass alle Callback - Funktionen , die verwenden
stdcall
Aufrufkonvention (welche die Makros CALLBACK
, PASCAL
und WINAPI
alle erweitern). Wenn Sie einen Funktionszeiger übergeben, der die Standard-C-Aufrufkonvention ( cdecl
) verwendet, führt dies zu einer Unrichtigkeit.
- In C ++ zwischen Funktionselementzeigern und regulären Funktionszeigern umwandeln. Dies löst häufig C ++ - Neulinge aus. Klassenmitgliedsfunktionen haben einen versteckten
this
Parameter, und wenn Sie eine Elementfunktion in eine reguläre Funktion this
umwandeln , gibt es kein zu verwendendes Objekt, und wiederum führt dies zu einer großen Beeinträchtigung.
Eine andere schlechte Idee, die manchmal funktioniert, aber auch undefiniertes Verhalten ist:
- Casting zwischen Funktionszeigern und regulären Zeigern (z. B. Casting von a
void (*)(void)
nach a void*
). Funktionszeiger haben nicht unbedingt die gleiche Größe wie normale Zeiger, da sie auf einigen Architekturen möglicherweise zusätzliche Kontextinformationen enthalten. Dies wird unter x86 wahrscheinlich in Ordnung sein, aber denken Sie daran, dass es sich um ein undefiniertes Verhalten handelt.