kind: reference search: int, double, uint8_t, Ganzzahldivision, Cast, struct, typedef, Array, Zeiger, Pointer, malloc, printf, Bits, Shift, Maske, fopen, Fehlermeldung, Segmentation fault

C zum Nachschlagen

Was die Aktivitäten in C immer wieder brauchen: Typen und Rechnen, Arrays und Structs, Funktionen aus math.h, Bits und Bytes, Zeiger, Dateien und was die Fehlermeldungen des Compilers sagen. Die Beispiele sind so geschrieben wie in PixelRAM, Raytracing, GIF und JPEG. Was PixelRAM selbst kann, steht im Handbuch PixelRAM, was raylib kann, unter raylib.

1. Ein Programm

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

#define W 320

// eine Funktion: Rückgabetyp, Name, Parameter mit Typ
double half(double x)
{
    return x / 2;
}

int main(void)
{
    int n = 7;
    printf("%d %f\n", n, half(n));
    return 0;
}
  • Jede Anweisung endet mit ;, Blöcke stehen in { … }. Kommentare: // bis zum Zeilenende oder /* … */.
  • #include <…> holt die Erklärungen einer Bibliothek: stdio.h für printf und Dateien, stdlib.h für malloc, free und exit, math.h für sqrt und Co., stdint.h für uint8_t und seine Verwandten. pixelram.h und raylib.h bringen stdbool.h, stdint.h und ihre eigenen Funktionen schon mit.
  • #define W 320 ersetzt vor dem Übersetzen jedes W durch 320. So stehen feste Größen an einer Stelle.
  • Eine Funktion muss oberhalb der Stelle stehen, an der sie aufgerufen wird, oder dort vorher angekündigt sein: double half(double x);, mit Semikolon statt Rumpf.
  • main ist der Anfang. Mit int main(int argc, char **argv) bekommt das Programm, was beim Start dahinter steht: argv[1] ist das erste Wort, argc die Zahl der Wörter einschließlich des Programmnamens. In den PixelRAM-Vorlagen gibst du es mit make ARGS=diskette.gif mit.

Dennis Ritchie (1941–2011), amerikanischer Informatiker bei den Bell Labs. Anfang der 1970er Jahre erfand er C, und damit schrieben er und Ken Thompson ihr Betriebssystem Unix neu, das es bis dahin nur in Assembler gab. Das Terminal im Workspace läuft unter Linux, und auch dessen Kern ist in C geschrieben.

1978 schrieb er mit Brian Kernighan das Buch The C Programming Language. Sein erstes Programm gibt hello, world aus, und seitdem beginnt fast jede Einführung in eine Programmiersprache so.

2. Zahlen und Rechnen

Typen

Typ Was darin steht Beispiel
int ganze Zahl, im Workspace 32 Bit: etwa ±2,1 Milliarden int x = 160;
long ganze Zahl, mindestens 32 Bit; Dateigrößen long pos = 0;
double Kommazahl mit etwa 16 Stellen double t = 0.5;
float Kommazahl mit etwa 7 Stellen; raylib rechnet damit float speed = 2.5f;
uint8_t ein Byte ohne Vorzeichen, 0 bis 255 uint8_t red = 255;
uint16_t, uint32_t 16 und 32 Bit ohne Vorzeichen; int16_t, int32_t mit uint32_t color = 0xff8800;
bool true oder false bool hit = false;
char ein Zeichen, eigentlich ein Byte; Text ist ein Array davon char *name = "Kugel";

Eine Variable, die in einer Funktion steht, gilt nur bis zum Ende ihres Blocks, und ihr Wert ist unbestimmt, bis du ihr einen gibst. Eine Variable außerhalb aller Funktionen gilt überall in der Datei und beginnt bei 0.

Rechnen

Ausdruck Ergebnis Warum
7 / 2 3 zwei ganze Zahlen: ganzzahlige Division, der Rest fällt weg
7 / 2.0 3.5 sobald eine Kommazahl beteiligt ist, wird mit Kommazahlen gerechnet
(double)x / W zwischen 0 und 1 der Cast macht aus x vorher eine Kommazahl
(int)(2.9) 2 der Cast zu int schneidet ab, er rundet nicht; (int)(-2.9) ist -2
7 % 3 1 der Rest der ganzzahligen Division
-7 % 3 -1 der Rest hat das Vorzeichen der linken Zahl; für einen Index, der nach vorn umläuft: ((i % n) + n) % n
x += 5, x++ kurz für x = x + 5 und x = x + 1
a > b ? a : b die größere Bedingung, dann der Wert für wahr, dann der für falsch

Wird ein Wert in einen kleineren Typ geschrieben, geht der Rest verloren: uint8_t c = 300; ergibt 44, denn nur die unteren 8 Bit passen hinein (300 − 256). Wo ein Farbwert über 255 kommen kann, begrenze ihn vorher, etwa mit v > 255 ? 255 : v.

math.h

Funktion Was sie liefert
sqrt(x) die Wurzel
fabs(x) den Betrag einer Kommazahl; abs(n) aus stdlib.h für ganze Zahlen
floor(x), ceil(x), round(x) ab-, auf- und gerundet, als Kommazahl
fmod(x, y) den Rest einer Division von Kommazahlen
sin(a), cos(a), tan(a) Winkel im Bogenmaß: ein Vollkreis ist 2π, nicht 360
atan2(y, x) den Winkel des Punkts (x, y), von −π bis π; y kommt zuerst
pow(x, y) x hoch y
fmin(a, b), fmax(a, b) die kleinere, die größere Kommazahl
INFINITY unendlich: größer als jede Zahl, gut als »noch nichts getroffen«

π steht nicht in C selbst: #define PI 3.14159265358979 oben in die Datei; raylib bringt PI schon mit.

3. Verzweigen und wiederholen

if (x < 0)
    x = 0;
else if (x >= W)
    x = W - 1;

for (int y = 0; y < H; y++)        // y von 0 bis H - 1
    for (int x = 0; x < W; x++)
        set_pixel(x, y, color(x, y));

while (!should_close())
{
    // … ein Bild pro Durchgang …
}
  • Vergleiche: ==, !=, <, <=, >, >=; verknüpft mit && (und), || (oder), ! (nicht). = ist eine Zuweisung: if (x = 0) setzt x auf 0, und die Bedingung ist nie wahr.
  • Ohne geschweifte Klammern gehört nur die nächste Anweisung zu if, for oder while. Sobald es zwei sind, braucht es { … }.
  • break verlässt die Schleife, continue springt zum nächsten Durchgang.
  • switch (key) { case 1: …; break; default: …; } wählt nach einer ganzen Zahl; ohne break läuft es in den nächsten case weiter.

4. Arrays, Structs und Text

Arrays

int counts[16] = {0};             // 16 ganze Zahlen, alle 0
uint8_t palette[4][3] = {         // 4 Farben mit je Rot, Grün, Blau
    {0, 0, 0}, {255, 255, 255}, {255, 0, 0}, {0, 0, 255},
};
int map[H][W];                    // zweidimensional: erst die Zeile, dann die Spalte

counts[3] += 1;
palette[2][0] = 200;
map[y][x] = 1;
  • Die Indizes gehen von 0 bis Länge − 1. C prüft das nicht: counts[16] liest oder überschreibt Speicher, der jemand anderem gehört. Das Programm rechnet dann falsch oder stürzt ab, oft weit entfernt von der Stelle des Fehlers.
  • Ein Array kennt seine Länge nicht. Gib sie einer Funktion mit: void draw(int *values, int count).
  • Ein Array lässt sich nicht als Ganzes zuweisen; kopiert wird mit einer Schleife oder memcpy aus string.h.

Structs

typedef struct
{
    double x, y, z;
} vec;

vec add(vec a, vec b)
{
    return (vec){a.x + b.x, a.y + b.y, a.z + b.z};
}

vec eye = {0, 1.2, 5.5};
eye.y += 0.1;

Ein struct fasst Werte unter einem Namen zusammen, mit typedef bekommt er einen kurzen Typnamen. (vec){…} schreibt einen Wert dieses Typs direkt hin. Structs werden als Ganzes zugewiesen, übergeben und zurückgegeben, als Kopie: Ändert eine Funktion ihren Parameter a, bleibt das Original, wie es war.

Text ausgeben

Platzhalter für Beispiel und Ausgabe
%d int printf("%d x %d\n", w, h); → 320 x 180
%ld long printf("%ld Bytes\n", size);
%f, %.2f double, mit 2 Nachkommastellen printf("%.2f\n", 3.14159); → 3.14
%x, %02x, %08x hexadezimal, mit führenden Nullen auf 2 oder 8 Stellen printf("%02x\n", 10); → 0a
%s Text printf("Kann %s nicht öffnen.\n", path);
%c ein Zeichen printf("%c\n", 'G');

\n beendet die Zeile. Passt ein Platzhalter nicht zum Typ, druckt printf Unsinn; der Compiler warnt davor. In PixelRAM erscheint die Ausgabe in der Konsole der Seite (Handbuch PixelRAM).

Text ist ein Array von char, dessen letztes Zeichen eine 0 ist: "GIF" belegt vier Bytes. Vergleichen geht nicht mit ==, sondern mit strcmp(a, b) == 0 aus string.h.

5. Bits und Bytes

Ausdruck Was er tut Beispiel
a & b Und, Bit für Bit: lässt nur die Bits stehen, die in beiden gesetzt sind 0xb6 & 0x0f → 0x06, die unteren vier Bits
a | b Oder, Bit für Bit: setzt zusammen low | (high << 8)
a ^ b exklusives Oder: kippt die Bits, die in b gesetzt sind x ^ 1 wechselt zwischen gerade und ungerade
~a kippt alle Bits ~0x0f & 0xff → 0xf0
a << n schiebt um n Bits nach links: mal 2n 1 << 3 → 8
a >> n schiebt nach rechts: ganzzahlig durch 2n 0xb6 >> 4 → 0x0b, die oberen vier Bits
(a >> n) & 1 das Bit Nummer n, von rechts ab 0 gezählt (0xb6 >> 7) & 1 → 1
(1 << n) - 1 eine Maske mit den unteren n Bits (1 << 5) - 1 → 0x1f

0x schreibt eine Zahl hexadezimal: 0xff ist 255, jede Ziffer steht für vier Bits. Eine Zahl aus mehreren Bytes steht in GIF im Little Endian (das niedrige Byte zuerst, low | (high << 8)), in JPEG im Big Endian (das hohe zuerst, (high << 8) | low). Klammern lohnen sich: << bindet schwächer als +, & sogar schwächer als ==. 1 << n + 1 heißt 1 << (n + 1), und x & 1 == 0 heißt x & (1 == 0).

6. Speicher und Dateien

Zeiger

int x = 5;
int *p = &x;          // p ist die Adresse von x
*p = 7;               // schreibt 7 dorthin: x ist jetzt 7

uint8_t *file = malloc(size);   // size Bytes, die das Programm behalten darf
if (file == NULL)
    exit(1);                    // kein Speicher mehr frei
file[0] = 0x47;       // ein Zeiger wird wie ein Array benutzt
free(file);           // zurückgeben, wenn er nicht mehr gebraucht wird
  • &x ist die Adresse von x, *p der Wert an der Adresse p. Ein Array, das einer Funktion übergeben wird, kommt als Zeiger auf sein erstes Element an: void fill(uint8_t *data).
  • Soll eine Funktion eine Variable des Aufrufers ändern, bekommt sie deren Adresse: void swap(int *a, int *b), aufgerufen als swap(&x, &y).
  • malloc(n) aus stdlib.h holt n Bytes, die bleiben, bis free sie zurückgibt. Für ein Array von Werten: malloc(count * sizeof(double)). Was nach free noch benutzt wird, gehört niemandem mehr.
  • NULL ist ein Zeiger, der nirgendwohin zeigt. Wer über ihn liest oder schreibt, bringt ein Programm im Terminal zum Absturz; in einer PixelRAM-Seite geht es oft unbemerkt weiter, mit Unsinn.

Eine Datei lesen

So liest der GIF-Decoder eine ganze Datei in den Speicher:

FILE *f = fopen(path, "rb");      // "rb": lesen, Byte für Byte, wie es dasteht
if (!f)
{
    printf("Kann %s nicht öffnen.\n", path);
    exit(1);
}
fseek(f, 0, SEEK_END);            // ans Ende ...
long size = ftell(f);             // ... dort steht die Größe
fseek(f, 0, SEEK_SET);            // ... und zurück an den Anfang
uint8_t *data = malloc(size);
fread(data, 1, size, f);          // size Stücke zu je 1 Byte
fclose(f);

fopen gibt NULL zurück, wenn die Datei fehlt; deshalb die Prüfung. Zum Schreiben öffnest du mit "wb" und schreibst mit fwrite(data, 1, size, f) oder fprintf(f, …) wie mit printf.

7. Wenn etwas nicht klappt

Der Compiler nennt Datei, Zeile und Spalte: main.c:12:5: error: …. Lies die erste Meldung zuerst; die folgenden sind oft nur Folgen von ihr. Warnungen (warning:) verhindern das Übersetzen nicht, zeigen aber fast immer einen echten Fehler.

  • expected ';' at end of declaration, expected ';' after expression – An der genannten Stelle fehlt ein Semikolon. Zeigt sie auf den Anfang einer Zeile, fehlt es am Ende der Zeile davor.
  • use of undeclared identifier 'x' – Ein Tippfehler im Namen, oder x ist in einem anderen Block deklariert.
  • call to undeclared library function 'sqrt', call to undeclared function – Das #include fehlt, oder die Funktion steht weiter unten als ihr Aufruf.
  • expected '}', expected expression – Eine Klammer fehlt oder ist zu viel. Such von der genannten Zeile aus nach oben.
  • format specifies type 'int' but the argument has type 'double' – Der Platzhalter in printf passt nicht zum Wert, hier %f statt %d.
  • unused variable – Eine Warnung: Die Variable wird nie benutzt, oft wegen eines Tippfehlers an der Stelle, an der sie gebraucht wird.
  • Segmentation fault im Terminal, memory access out of bounds in der Konsole einer PixelRAM-Seite – Kein Fehler des Compilers, sondern des laufenden Programms: ein Index außerhalb eines Arrays, ein NULL-Zeiger oder Speicher nach free. In einer Webseite stürzt so ein Fehler oft gar nicht ab, sondern rechnet still mit falschen Werten weiter.

Rechnet das Programm falsch, ohne abzustürzen, hilft ein printf an der verdächtigen Stelle mehr als langes Hinsehen: Gib die Werte aus, die du dort erwartest, und vergleiche.

8. Zum Nachschlagen

  • cppreference: C: jede Funktion der Standardbibliothek mit Beispiel, sortiert nach Header (stdio.h, math.h, string.h …), und die Sprache selbst, auf Englisch.
  • Beej's Guide to C Programming: eine Einführung in C von vorn, mit Zeigern, Structs und Speicher ausführlich erklärt, frei zu lesen, auf Englisch.