c systems notebook

Komplexe Zahlen

In vielen wissenschaftlichen und technischen Anwendungen reichen reelle Zahlen nicht aus, um natürliche Zusammenhänge zu beschreiben. Die Signalverarbeitung nutzt komplexe Zahlen zur Darstellung von Schwingungen, die Elektrotechnik arbeitet mit komplexen Impedanzen, also dem komplexen Widerstand von Schaltkreisen gegenüber Wechselstrom. In der numerischen Mathematik ermöglichen komplexe Funktionen elegante Lösungen für Probleme, die im Reellen kompliziert oder unlösbar wären. Die Berechnung der Mandelbrot-Menge, ein klassisches Beispiel fraktaler Geometrie, basiert vollständig auf Iterationen komplexer Zahlen.

C bietet (seit C99) optionale Unterstützung für komplexe Zahlen als eigenständige Datentypen. Diese direkte Sprachintegration erlaubt es dem Compiler, komplexe Arithmetik effizient zu implementieren und komplexe Konstanten bereits zur Übersetzungszeit auszuwerten. Wenn man portablen C-Code schreibt, darf man nicht voraussetzen, dass komplexe Berechnungen strikt IEEE-konform sind. (Quelle: Implementierungen, die den optionalen Annex G (›Floating-point extensions‹) unterstützen, stellen jedoch sicher, dass komplexe Arithmetik den IEEE-754-Semantiken folgt, einschließlich der konsistenten Behandlung von NaN, Unendlichkeiten und vorzeichenbehafteten Nullen.)

Existenztest mit Feature-Flag

Da komplexe Zahlen im C-Standard optional sind, sollte man bei portablen Programmen prüfen, ob die jeweilige Implementierung sie unterstützt. Dies geschieht über das Makro +__STDC_NO_COMPLEX__+, das vom Compiler definiert wird, wenn keine Unterstützung vorhanden ist:

#include <stdio.h>

int main() {
#ifndef __STDC_NO_COMPLEX__
    puts("Komplexe Zahlen werden vom Compiler unterstützt.");
#else
    puts("Komplexe Zahlen werden vom Compiler NICHT unterstützt.");
#endif
}

Da komplexe Zahlen auf praktisch allen modernen Compilern verfügbar sind, verzichten wir in den folgenden Beispielen auf diesen Test.

Deklaration und Initialisierung

C stellt drei komplexe Gleitkommatypen zur Verfügung:

  • float complex
  • double complex
  • long double complex

Jeder komplexe Typ besteht aus zwei Komponenten desselben reellen Gleitkommatyps, dem Realteil und dem Imaginärteil. (Quelle: ISO/IEC 9899:2024, § 6.2.5 Paragraph 13)

Die Deklaration folgt dem gewohnten Muster, wobei das Schlüsselwort complex zwischen Basistyp und Variablenname steht:

#include <stdio.h>
#include <complex.h>

int main() {
    float complex z1;
    double complex z2;
    long double complex z3;

    z1 = 3.0f + 4.0f*I;
    z2 = 1.5 + 2.5*I;
    z3 = 0.0L + 1.0L*I;
}

Der Header <complex.h> stellt das Makro I bereit, das die imaginäre Einheit repräsentiert. (Quelle: ISO/IEC 9899:2024, § 7.3.1 Introduction) Wird der Header nicht eingebunden, gibt es einen Compilerfehler.

Die Initialisierung erfolgt durch Addition eines reellen Werts mit einem imaginären Anteil, wobei der imaginäre Teil durch Multiplikation mit I gekennzeichnet wird.

Die Notation ist nahe an der mathematischen Konvention, wo man komplexe Zahlen als a + __b__i schreibt, wobei i die imaginäre Einheit darstellt. C übernimmt diese Darstellung nahezu wörtlich, ersetzt lediglich i durch I, um Kollisionen mit Variablennamen zu vermeiden. Der Compiler interpretiert x + y*I als komplexe Zahl mit Realteil x und Imaginärteil y.

I selbst ist ein komplexer Wert mit Realteil 0 und Imaginärteil 1. Die Multiplikation mit einem reellen Wert, wie 2.5*I verschiebt diesen in den Imaginärteil.

Die Typ-Suffixe (f, L) müssen bei den numerischen Literalen verwendet werden, um Typ-Konsistenz zu gewährleisten:

  • 4.0f*I erzeugt einen float complex-Wert, während
  • 2.5*I einen double complex-Wert ergibt.

Hinweis: Der C23-Standard erlaubt nun auch ›imaginary suffixes‹ wie 1.0i, 1.0I, 1.0j oder 1.0J, sodass das Multiplikationszeichen entfallen kann. GCC gibt im Pedantic-Modus jedoch weiterhin die Warnung ›imaginary constants are a C2Y feature or GCC extension [-Wpedantic]‹ aus, obwohl dies nach dem Standard nicht mehr erforderlich wäre. Deswegen wird im Buch weiterhin die längere Schreibweise mit Multiplikationszeichen verwendet, da alle Beispiele im Buch auch beim pedantischen Überprüfen keine Warnungen ergeben sollen.

Zugriff auf Komponenten

Hat man eine komplexe Zahl in einer Variablen gespeichert, kann man mit den Funktionen creal() und cimag() den Real- und Imaginärteil extrahieren:

#include <stdio.h>
#include <complex.h>

int main() {
    double complex z = 3.0 + 4.0*I;

    double r = creal(z);
    double i = cimag(z);

    printf("Realteil: %.1f\n", r);       // 📣 Realteil: 3.0
    printf("Imaginärteil: %.1f\n", i);   // 📣 Imaginärteil: 4.0
}

Die Funktionen creal(), crealf() und creall() extrahieren den Realteil, während cimag(), cimagf() und cimagl() den Imaginärteil liefern. (Quelle: ISO/IEC 9899:2024, § 7.3.9.1 The creal functions) Die Suffixkonvention folgt dem bekannten Muster: kein Suffix für double complex, Suffix f für float complex und Suffix l für long double complex. Der Rückgabetyp entspricht dem reellen Basistyp, creal() liefert double, crealf() liefert float.

Die creal*()-Funktionen erlauben nur Lesezugriff und Komponenten können dadurch nicht modifiziert werden. Eine Anweisung wie creal(z) = 5.0; ist syntaktisch ungültig. Um eine komplexe Zahl zu ändern, muss eine neue Zahl konstruiert werden.

Komplexe Zahlen ausgeben

Da C komplexe Zahlen nicht als eigene ›druckbare‹ Einheit behandelt, muss man sie in ihre Real- und Imaginärteile zerlegen und selbst formatieren. Es gibt kein spezielles Formatzeichen für komplexe Zahlen in printf().

Ein einfaches Beispiel zeigt, wie sich eine kleine Hilfsfunktion schreiben lässt, die die Darstellung im mathematisch üblichen Format a ± __b__i ausgibt:

#include <stdio.h>
#include <math.h>
#include <complex.h>

// Eigene Ausgabefunktion für komplexe Zahlen
static void print_complex(double complex z) {
    double r = creal(z);
    double i = cimag(z);

    printf("%.1f %c %.1fi\n", r, (i < 0.0 ? '-' : '+'), fabs(i));
}

int main() {
    print_complex(3.0 + 4.0*I);      // 📣 3.0 + 4.0i
    print_complex(3.0 - 4.0*I);      // 📣 3.0 - 4.0i
    print_complex(-3.0 + 4.0*I);     // 📣 -3.0 + 4.0i
    print_complex(-3.0 - 4.0*I);     // 📣 -3.0 - 4.0i
}

Mit dem Bedingungsoperator (i < 0.0 ? '-' : '+') wird abhängig vom Vorzeichen des Imaginärteils das passende Plus- oder Minuszeichen ausgewählt. fabs(i) liefert den Betrag, sodass bei negativen Imaginärteilen kein doppeltes Minus entsteht.

Dieses Beispiel zeigt zudem, dass komplexe Werte wie gewöhnliche Datentypen behandelt werden können: Sie lassen sich an Funktionen übergeben, aus Funktionen zurückgeben und in Ausdrücken verwenden. Der Compiler sorgt intern dafür, dass Real- und Imaginärteil korrekt übergeben und verarbeitet werden.

// Warum keine direkte Komponentennotation wie z.real oder z.imag? Komplexe Typen sind in C keine Strukturen, sondern atomare Typen. Ihre interne Repräsentation ist implementierungsdefiniert.(Quelle: ISO/IEC 9899:2024, § 6.2.5 Paragraph 13) Die Funktionen kapseln den Zugriff und garantieren Portabilität über verschiedene Plattformen hinweg. Ein Compiler könnte komplexe Zahlen als zwei aufeinanderfolgende Gleitkommazahlen im Speicher ablegen, ein anderer könnte eine verschachtelte Struktur oder eine spezielle Prozessor-Register-Darstellung wählen.

Konstruktion mit Makros

Nicht immer liegen Real- und Imaginärteil als Literale vor. Wenn man komplexe Zahlen aus berechneten Werten oder Variablen konstruieren möchte, stößt die Literal-Notation an ihre Grenzen. Für solche Fälle bietet <complex.h> Konstruktionsmakros an:

#include <stdio.h>
#include <complex.h>

int main() {
    double real_part = 5.0;
    double imag_part = 12.0;

    double complex z = CMPLX(real_part, imag_part);
    float complex zf = CMPLXF(3.0, 4.0);
    long double complex zl = CMPLXL(1.0, 1.0);

    printf("z = %.1f + %.1fi\n", creal(z), cimag(z));
    // 📣 z = 5.0 + 12.0i
    printf("zf = %.1f + %.1fi\n", crealf(zf), cimagf(zf));
    // 📣 zf = 3.0 + 4.0i
}

Die Makros CMPLX, CMPLXF und CMPLXL konstruieren komplexe Werte aus zwei reellen Argumenten. (Quelle: ISO/IEC 9899:2024, § 7.3.9.3 The CMPLX macros) Sie entsprechen den drei komplexen Typen: CMPLX erzeugt double complex, CMPLXF erzeugt float complex und CMPLXL erzeugt long double complex. Diese Makros sind in C als Konstantenausdrücke definiert, wenn ihre Argumente Konstantenausdrücke sind, was ihre Verwendung in Kontexten erlaubt, die Konstanten erfordern.

// Ein typischer Stolperstein ist die Annahme, CMPLX könne mit Integer-Argumenten aufgerufen werden. Obwohl dies syntaktisch zulässig ist -- Ganzzahlen werden implizit zu Gleitkommazahlen konvertiert -- kann dies zu Warnungen führen. Best Practice ist, Argumente explizit als Gleitkommazahlen zu schreiben, etwa CMPLX(5.0, 12.0) statt CMPLX(5, 12).

Mit dieser Schreibweise lassen sich auch von anderen komplexen Zahlen Real- oder Imaginärteil ändern:

double complex z = 3.0 + 4.0*I;
z = CMPLX(5.0, cimag(z));  // Realteil ändern, Imaginärteil beibehalten

Überladene Operatoren

In C gelten komplexe Typen (float _Complex, double _Complex, long double _Complex) als arithmetische Typen. Viele Operatoren sind für komplexe Typen überladen und implementieren die mathematischen Standardregeln. (Quelle: ISO/IEC 9899:2024, § 6.5.6 Additive operators) Daher funktionieren viele Operatoren wie bei reellen Zahlen.

Erlaubte arithmetische und Zuweisungsoperatoren für komplexe Typen sind:

  • Unär und binär (arithmetisch):

    • +z, -z (arithmetische Unär-Operatoren für arithmetische Typen).
    • z1 + z2, z1 - z2, z1 * z2, z1 / z2 (die üblichen arithmetischen Operatoren). Diese fallen unter die Multiplikations-/Additionsgruppen und nutzen die ›usual arithmetic conversions‹.
    • Prä- und Postfix {plus}{plus}z, z{plus}{plus}, --z, z-- (bei komplexen Zahlen entspricht {plus}{plus} dem Addieren von 1+0i, -- analog).
  • Zuweisung:

    • =
    • Kombinierte: +=, -=, *=, /=

Nicht erlaubt für komplexe Typen sind:

  • Restwertoperator,
  • alle bitweisen Operatoren,
  • Schiebeoperatoren,
  • relationale Vergleichsoperatoren <, >, \<=, >= existieren nicht für komplexe Zahlen
#include <stdio.h>
#include <complex.h>

int main() {
    double complex z1 = 3.0 + 4.0*I;
    double complex z2 = 1.0 + 2.0*I;

    double complex sum = z1 + z2;
    double complex diff = z1 - z2;
    double complex prod = z1 * z2;
    double complex quot = z1 / z2;

    printf("Summe: %.1f + %.1fi\n", creal(sum), cimag(sum));
    // 📣 Summe: 4.0 + 6.0i
    printf("Differenz: %.1f + %.1fi\n", creal(diff), cimag(diff));
    // 📣 Differenz: 2.0 + 2.0i
    printf("Produkt: %.1f + %.1fi\n", creal(prod), cimag(prod));
    // 📣 Produkt: -5.0 + 10.0i
    printf("Quotient: %.2f + %.2fi\n", creal(quot), cimag(quot));
    // 📣 Quotient: 1.40 + 0.20i
}

Addition und Subtraktion erfolgen komponentenweise: (a + b i) + (c + d i) = (a + c) + (b + d)i. Multiplikation folgt der Formel (a + b i)(c + d i) = (ac - bd) + (ad + bc)i, die sich aus der Distributivität und der Eigenschaft i^2 = -1 ergibt. Division implementiert (a + b i) / (c + d i) = (a + b i)(c - d i) / (c^2 + d^2), wobei der Nenner mit dem Konjugierten erweitert wird.

// Die Überladung macht komplexe Zahlen zu erstklassigen Bürgern der Sprache.

Die Division durch Null bleibt undefiniertes Verhalten, analog zu reellen Gleitkommazahlen. Overflow und Underflow können auftreten, insbesondere bei Multiplikation und Division großer oder kleiner Werte. Implementierungen, die Annex G (›Floating-point extensions‹) unterstützen, folgen dabei den IEEE-754-Regeln für Gleitkommazahlen und erzeugen in solchen Fällen NaN oder Inf. Andere Implementierungen dürfen hiervon abweichen, sind aber in der Praxis meist kompatibel.

Vergleiche und Einschränkungen

Auch die Gleichheitsoperatoren == und != sind für komplexe Typen definiert und vergleichen beide Komponenten: (Quelle: ISO/IEC 9899:2024, § 6.5.9 Equality operators) Das heißt, die Werte sind dann gleich, wenn Real- und Imaginärteil jeweils gleich sind.

#include <stdio.h>
#include <complex.h>

int main() {
    double complex z1 = 3.0 + 4.0*I;
    double complex z2 = 3.0 + 4.0*I;
    double complex z3 = 3.0 + 5.0*I;

    printf("z1 == z2: %d\n", z1 == z2);  // 📣 z1 == z2: 1
    printf("z1 != z3: %d\n", z1 != z3);  // 📣 z1 != z3: 1

    // Achtung bei Gleitkomma-Vergleichen

    double complex z4 = (0.1 + 0.2)*I;
    double complex z5 = 0.3*I;
    printf("0.1 + 0.2i == 0.3i: %d\n", z4 == z5);
    // 📣 0.1 + 0.2i == 0.3i: 0
}

Der Vergleich unterliegt denselben Gleitkomma-Besonderheiten wie bei reellen Zahlen: Rundungsfehler können zu unerwartet ungleichen Werten führen. Das letzte Beispiel demonstriert dies, denn die Summe 0.1 + 0.2 ergibt im Binärsystem nicht exakt 0.3.

Relationale Operatoren wie <, >, <= und >= sind für komplexe Zahlen nicht definiert und führen zu Kompilierungsfehlern.

Logische Operatoren

In C gehören komplexe Zahlen zur Kategorie scalar type (so wie int oder double). Sie dürfen in logischen Ausdrücken stehen und ihre Wahrheit wird folgendermaßen bestimmt:

  • Eine komplexe Zahl ist ›wahr‹, wenn nicht beide Teile (Realteil und Imaginärteil) null sind.

Damit sind auch !, && und || bei komplexen Zahlen erlaubt.

  • Logische Negation: !z liefert einen int und ist 1, wenn z gleich 0 + 0i ist, 0, sonst.
  • UND-Operator: a && b prüft: Beide Operanden sind wahr (also ungleich 0 + 0i).
  • ODER-Operator: a || b prüft: Mindestens ein Operand ist wahr (also ungleich 0 + 0i).

Ein Beispiel:

double complex a = 2.0 + 0.0*I;
double complex b = 0.0 + 3.0*I;
double complex c = 0.0 + 0.0*I;

printf("a && b -> %d\n", a && b); // 1, beide ungleich 0
printf("a && c -> %d\n", a && c); // 0, c ist 0+0i
printf("a || c -> %d\n", a || c); // 1, a ist ungleich 0
printf("!a -> %d\n", !a);         // 0, weil a != 0

Typkonvertierungen

Komplexe und reelle Typen können stellenweise gemischt werden. C definiert sowohl implizite als auch explizite Konvertierungsregeln. (Quelle: ISO/IEC 9899:2024, § 6.3.1.4 Real and complex)

Ein Beispiel macht das deutlich:

#include <stdio.h>
#include <complex.h>

int main() {
    // Implizit: reell zu komplex
    double complex z1 = 5.0;
    printf("z1: %.1f + %.1fi\n", creal(z1), cimag(z1));
    // 📣 z1: 5.0 + 0.0i

    // Implizit: Integer zu komplex
    double complex z2 = 7;
    printf("z2: %.1f + %.1fi\n", creal(z2), cimag(z2));
    // 📣 z2: 7.0 + 0.0i

    // Gemischte Arithmetik
    double complex z3 = 3.0 + 4.0*I;
    double complex result = z3 + 2.0;
    printf("z3 + 2.0: %.1f + %.1fi\n", creal(result),
                                       cimag(result));
    // 📣 z3 + 2.0: 5.0 + 4.0i

    // Explizit: komplex zu reell (nur Realteil)
    double real_part = (double)z3;
    printf("Realteil von z3: %.1f\n", real_part);
    // 📣 Realteil von z3: 3.0
}

Reelle Werte werden implizit zu komplexen Zahlen konvertiert, indem der Realteil auf den gegebenen Wert gesetzt und der Imaginärteil auf Null gesetzt wird. Integer-Werte durchlaufen zunächst die übliche Integer-zu-Gleitkomma-Konvertierung, bevor sie zu komplexen Zahlen werden. Dies ermöglicht natürliche gemischte Ausdrücke wie z + 2.0, ohne explizite Casts zu erfordern.

Die Konvertierung von komplex zu reell ist nicht implizit und erfordert einen expliziten Cast. Der Cast extrahiert nur den Realteil, der Imaginärteil geht verloren. Dies ist eine bewusste Designentscheidung: Der Verlust von Information soll explizit sein, um Programmierfehler zu vermeiden. Wer versehentlich eine komplexe Zahl einer reellen Variablen zuweist, erhält einen Compilerfehler.

Bei gemischten Ausdrücken folgt C den üblichen arithmetischen Konvertierungen. (Quelle: ISO/IEC 9899:2024, § 6.3.1.8 Usual arithmetic conversions) Wird ein float complex-Wert mit einem double-Wert addiert, wird der float complex-Wert zu double complex befördert, der double-Wert wird zu double complex konvertiert, und das Ergebnis ist double complex. Die Regel lautet: Der schwächere Typ wird zum stärkeren befördert, wobei long double > double > float und komplex die entsprechende Rangfolge erbt.

#include <stdio.h>
#include <complex.h>

int main() {
    float complex zf = 1.0f + 2.0f*I;
    double d = 3.0;

    // Ergebnis ist double complex
    double complex result = zf + d;
    printf("float complex + double: %.1f + %.1fi\n", creal(result),
                                                     cimag(result));
    // 📣 float complex + double: 4.0 + 2.0i
}

Ein häufiger Fehler ist die Annahme, dass komplexe und reelle Typen frei austauschbar seien. Best Practice ist, Typen bewusst zu wählen und Konvertierungen explizit zu machen, wenn Informationsverlust auftritt.

Funktionen aus <complex.h>

Über einfache Arithmetik hinaus benötigt man oft spezialisierte Operationen. Die Standardbibliothek stellt eine umfangreiche Funktionssammlung bereit, die in <complex.h> definiert ist. (Quelle: ISO/IEC 9899:2024, § 7.3 Complex arithmetic)

Funktion Kurzbeschreibung
creal(), crealf(), creall() Liefert den Realteil der komplexen Zahl
cimag(), cimagf(), cimagl() Liefert den Imaginärteil der komplexen Zahl
conj(), conjf(), conjl() Gibt die konjugiert komplexe Zahl zurück
cabs(), cabsf(), cabsl() Betrag (Modul) der komplexen Zahl
carg(), cargf(), cargl() Argument (Phase) in Radiant
cproj(), cprojf(), cprojl() Projektion auf die Riemann-Sphäre
cexp(), cexpf(), cexpl() Exponentialfunktion (e^z^)
clog(), clogf(), clogl() Natürlicher Logarithmus
cpow(), cpowf(), cpowl() Potenz z^w^ = exp(w × log(z))
csqrt(), csqrtf(), csqrtl() Quadratwurzel (Hauptzweig)
csin(), csinf(), csinl() Sinus einer komplexen Zahl
ccos(), ccosf(), ccosl() Kosinus einer komplexen Zahl
ctan(), ctanf(), ctanl() Tangens einer komplexen Zahl
casin(), casinf(), casinl() Arcus-Sinus
cacos(), cacosf(), cacosl() Arcus-Kosinus
catan(), catanf(), catanl() Arcus-Tangens
csinh(), csinhf(), csinhl() Hyperbolischer Sinus
ccosh(), ccoshf(), ccoshl() Hyperbolischer Kosinus
ctanh(), ctanhf(), ctanhl() Hyperbolischer Tangens
casinh(), casinhf(), casinhl() Inverser hyperbolischer Sinus
cacosh(), cacoshf(), cacoshl() Inverser hyperbolischer Kosinus
catanh(), catanhf(), catanhl() Inverser hyperbolischer Tangens

Alle Funktionen existieren in typspezifischen Varianten: <name> für double complex, <name>f für float complex, <name>l für long double complex.

Praxisbeispiel: Mandelbrot-Menge

Die Mandelbrot-Menge ist ein klassisches Beispiel für die Anwendung komplexer Zahlen. Sie entsteht durch eine einfache, wiederholte Rechnung (Iteration), die für jeden Punkt c in der komplexen Ebene durchgeführt wird.

Man beginnt mit z~0~ = 0 und berechnet dann immer wieder: z~n+1~ = z~n~^2^ + c

Das bedeutet: Man nimmt das vorherige Ergebnis z, quadriert es und addiert die Zahl c dazu. Diese Rechnung wird viele Male wiederholt.

Wenn der Betrag von z (also |z~n~|) nach vielen Wiederholungen nicht immer weiter wächst, sondern innerhalb eines bestimmten Bereichs bleibt, dann gehört der Punkt c zur Mandelbrot-Menge. Wird z dagegen immer größer, liegt c außerhalb der Menge.

Die native Unterstützung komplexer Zahlen macht die Implementierung nahezu selbsterklärend:

#include <complex.h>
#include <stdio.h>

int main() {
    const int width = 80, height = 25, max_iter = 20;
    for (int py = 0; py < height; py++) {
        for (int px = 0; px < width; px++) {
            double complex c = (px * 3.5 / width - 2.5)
                               + (py * 2.0 / height - 1.0) * I;
            double complex z = 0;
            int iter = 0;
            while (cabs(z) < 2.0 && iter < max_iter) {
                z = z * z + c;
                iter++;
            }
            putchar(iter == max_iter ? '*' : ' ');
        }
        putchar('\n');
    }
}

Die Konstante 2.0 bei cabs(z) < 2.0 ist mathematisch begründet und ist der Fluchtradius.

Die Ausgabe:


                                                       *
                                                   * *****
                                                    ******
                                           *     *   ****
                                             ** *************
                                             *********************
                                          ***********************
                                         **************************
                               *  * *    *************************
                              *********  **************************
                             *************************************
                          ***************************************
                          ***************************************
                             *************************************
                              *********  **************************
                               *  * *    *************************
                                         **************************
                                          ***********************
                                             *********************
                                             ** *************
                                           *     *   ****
                                                    ******
                                                   * *****
                                                       *

C macht diesen Code nicht nur lesbar, sondern auch effizient. Der Compiler kann die komplexe Multiplikation z * z in optimierte Maschinenbefehle übersetzen, die spezialisierte Gleitkomma-Einheiten nutzen. Die explizite Berechnung wäre fehleranfälliger und möglicherweise langsamer, da manuelle Optimierungen oft schlechter sind als moderne Compiler-Transformationen.