Erste Schritte
Hello World
Jedes C-Programm startet in main(). #include <stdio.h> bindet die Standard-I/O-Bibliothek ein (printf, scanf). main gibt 0 bei Erfolg zurück, ungleich Null bei Fehlschlag. Das Schlüsselwort void deklariert explizit, dass main keine Parameter annimmt.
#include <stdio.h>
int main(void) {
printf("Hello, World!\n");
return 0;
}Variablen & Typen
C ist statisch typisiert. Häufige Typen: int, float, double, char. float benötigt das f-Suffix. char[] ist ein String (null-terminiertes Array). long und short sind Größenmodifikatoren. unsigned bedeutet nicht-negativ. Größen variieren je nach Plattform; verwenden Sie <stdint.h> für feste Breiten.
int age = 30;
float height = 5.7f;
double pi = 3.14159;
char grade = 'A';
char name[] = "Alice";
long big = 100000L;
unsigned int count = 42;
printf("%s is %d\n", name, age);Eingabe & Ausgabe
scanf benötigt die Adresse (&) der Variablen, um Eingaben zu speichern. Begrenzen Sie immer die String-Eingabelänge (%49s für einen 50-Zeichen-Puffer), um Pufferüberläufe zu verhindern. scanf stoppt das Lesen von Strings bei Leerzeichen; verwenden Sie fgets für ganze Zeilen.
int n;
printf("Enter a number: ");
scanf("%d", &n);
printf("You entered %d\n", n);
char name[50];
printf("Enter name: ");
scanf("%49s", name); // limit to prevent overflow
printf("Hi, %s!\n", name);printf-Formatbezeichner
Formatbezeichner steuern die Ausgabe: %d int, %f float, %c char, %s string, %x hex, %p pointer. Breite und Genauigkeit (z.B. %5.2f) steuern Ausrichtung und Dezimalstellen. Nichtübereinstimmung von Bezeichner und Typ verursacht undefiniertes Verhalten.
printf("%d\n", 42); // integer
printf("%f\n", 3.14); // float/double
printf("%.2f\n", 3.14159); // 3.14 (2 decimals)
printf("%c\n", 'A'); // char
printf("%s\n", "hello"); // string
printf("%x\n", 255); // ff (hex)
printf("%5d\n", 42); // right-aligned, width 5
printf("%-5d|\n", 42); // left-alignedPräprozessor & Header
Der Präprozessor läuft vor der Kompilierung. #include fügt Header-Dateien ein, #define erstellt Makros und Konstanten. Verwenden Sie immer Include-Guards (#ifndef/#define/#endif) in Headern, um doppelte Einbindung zu verhindern. Makros sind Textsubstitution—verwenden Sie Klammern um Parameter.
#include <stdio.h> // system header
#include "myheader.h" // local header
#define PI 3.14159
#define SQUARE(x) ((x) * (x))
#ifndef GUARD_H
#define GUARD_H
// header content
#endifStrings & String.h
String-Grundlagen
C-Strings sind null-terminierte char-Arrays. strlen zählt Zeichen vor '\0'; sizeof gibt die Puffergröße zurück. strcpy kopiert bis zum Null-Terminator—stellen Sie sicher, dass das Ziel groß genug ist, um Überläufe zu vermeiden.
#include <string.h>
char s[20] = "Hello";
printf("Length: %zu\n", strlen(s)); // 5
printf("Size: %zu\n", sizeof(s)); // 20
char dest[20];
strcpy(dest, s); // copy
printf("%s\n", dest); // HelloVerkettung & Vergleich
strcat hängt an (Ziel muss Platz haben). strcmp vergleicht lexikografisch: gibt 0 zurück bei Gleichheit, negativ wenn erste < zweite, positiv wenn erste > zweite. Verwenden Sie niemals == zum Vergleichen von Strings (das vergleicht Zeiger, nicht Inhalt).
#include <string.h>
char s[30] = "Hello";
strcat(s, ", World!"); // s = "Hello, World!"
printf("%s\n", s);
int cmp = strcmp("apple", "banana");
// returns <0 if a<b, 0 if equal, >0 if a>b
if (strcmp(s, "Hello") == 0) {
printf("Equal!\n");
}sprintf & sscanf
sprintf formatiert in einen String-Puffer (wie printf, aber in einen String). sscanf parst einen String in Variablen (wie scanf, aber aus einem String). Verwenden Sie snprintf statt sprintf, um Pufferüberläufe durch Angabe der Maximalgröße zu verhindern.
char buf[100];
int age = 30;
char name[] = "Alice";
sprintf(buf, "%s is %d years old", name, age);
printf("%s\n", buf);
int a, b;
sscanf("10 20", "%d %d", &a, &b);
printf("a=%d, b=%d\n", a, b); // a=10, b=20strchr, strstr & strtok
strchr findet ein Zeichen, strstr findet einen Teilstring. strtok teilt einen String nach Trennzeichen, modifiziert aber den Originalstring (fügt Null-Terminatoren ein) und ist nicht Thread-sicher—übergeben Sie NULL bei nachfolgenden Aufrufen, um die Tokenisierung fortzusetzen.
#include <string.h>
char s[] = "Hello, World!";
char *p = strchr(s, 'W'); // find first 'W'
printf("%s\n", p); // World!
char *sub = strstr(s, "World");
printf("%s\n", sub); // World!
char tokens[] = "a,b,c";
char *tok = strtok(tokens, ",");
while (tok) {
printf("%s\n", tok);
tok = strtok(NULL, ",");
}fgets & sichere Eingabe
fgets ist die sichere Methode zum Lesen von Strings—es nimmt ein Größenlimit, um Überläufe zu verhindern. Im Gegensatz zu scanf liest es Leerzeichen. Der Zeilenumbruch ist im Ergebnis enthalten; strcspn findet und entfernt ihn. Bevorzugen Sie immer fgets gegenüber gets (das aus C11 entfernt wurde).
char line[100];
printf("Enter text: ");
fgets(line, sizeof(line), stdin);
// removes trailing newline
line[strcspn(line, "\n")] = 0;
printf("You said: %s\n", line);Zahlen & Mathematik
Ganzzahltypen & Grenzen
Verwenden Sie <stdint.h> für Typen mit fester Breite (int32_t, int64_t), wenn genaue Größen wichtig sind. <limits.h> bietet INT_MAX, INT_MIN usw. für plattformspezifische Grenzen. Das LL-Suffix markiert long-long-Literale. Größen von int/long variieren je nach Plattform.
#include <stdint.h>
#include <limits.h>
int32_t a = 100;
int64_t big = 9223372036854775807LL;
uint8_t byte = 255;
printf("INT_MAX = %d\n", INT_MAX); // 2147483647
printf("INT_MIN = %d\n", INT_MIN); // -2147483648
printf("UINT_MAX = %u\n", UINT_MAX); // 4294967295Gleitkommazahlen
float ist 4 Byte (6-7 Stellen Genauigkeit), double ist 8 Byte (15-16 Stellen). Vergleichen Sie Floats niemals mit == aufgrund von Rundungsfehlern—verwenden Sie fabs(a - b) < epsilon. <float.h> bietet DBL_MAX, DBL_EPSILON für Grenzen und Genauigkeit.
#include <float.h>
double d = 3.141592653589793;
float f = 3.14f;
printf("DBL_MAX = %e\n", DBL_MAX);
printf("DBL_EPSILON = %e\n", DBL_EPSILON);
if (d == 0.1 + 0.2) {
// likely false! floating point imprecision
}Mathematische Funktionen
<math.h> bietet Standard-Mathematikfunktionen. pow und sqrt geben double zurück. fabs ist die Float-Version von abs (abs ist für int). Linken Sie mit -lm auf einigen Systemen. Für Finanzberechnungen vermeiden Sie Gleitkommazahlen—verwenden Sie stattdessen Integer-Cents.
#include <math.h>
double x = 2.5;
pow(x, 3); // 15.625
sqrt(x); // 1.581
fabs(-5.0); // 5.0
floor(3.7); // 3.0
ceil(3.2); // 4.0
fmod(10.5, 3); // 1.5
exp(1); // 2.718 (e^1)
log(2.718); // 1.0 (natural log)Zufallszahlen
rand() gibt eine pseudo-zufällige int von 0 bis RAND_MAX zurück. Seeden Sie mit srand() einmal beim Programmstart (mit time(NULL)). rand() % N hat Modulo-Verzerrung und geringe Qualität; für ernsthafte Verwendung lesen Sie /dev/urandom oder verwenden Sie eine Drittanbieter-PRNG-Bibliothek.
#include <stdlib.h>
#include <time.h>
srand(time(NULL)); // seed once at start
int r = rand() % 100; // 0-99
int dice = rand() % 6 + 1; // 1-6
float fr = (float)rand() / RAND_MAX; // 0.0 - 1.0Typumwandlung & Casting
Casting ist (typ)wert. Ganzzahldivision schneidet ab—verwenden Sie einen Float-Operanden, um ein Float-Ergebnis zu erhalten. atoi/atof konvertieren Strings zu Zahlen, prüfen aber keine Fehler; bevorzugen Sie strtol/strtod, die Parse-Fehler über errno melden.
int i = 65;
char c = (char)i; // 'A'
double d = 3.99;
int truncated = (int)d; // 3
// Implicit promotion
int a = 5;
double result = a / 2.0; // 2.5 (promoted to double)
int bad = a / 2; // 2 (integer division)
// String to number
int n = atoi("42");
double f = atof("3.14");Kontrollfluss
If / Else
if/else if/else ist die Standardbedingung. C behandelt 0 als false und jeden Nicht-Null-Wert als true. Verwenden Sie Klammern auch für einzelne Anweisungen, um Bugs beim späteren Hinzufügen von Zeilen zu verhindern. Es gibt keinen Boolean-Typ in C89; C99 fügt _Bool und <stdbool.h> hinzu.
int score = 85;
if (score >= 90) {
printf("A\n");
} else if (score >= 80) {
printf("B\n");
} else if (score >= 70) {
printf("C\n");
} else {
printf("F\n");
}Switch
switch springt zu einem passenden case-Label. Verwenden Sie immer break, um Fall-Through zu verhindern (case 6 und 7 teilen sich Code absichtlich). default behandelt nicht übereinstimmende Werte. switch funktioniert nur mit Ganzzahl- und char-Typen, nicht mit Strings oder Floats.
int day = 3;
switch (day) {
case 1: printf("Mon\n"); break;
case 2: printf("Tue\n"); break;
case 3: printf("Wed\n"); break;
case 6:
case 7: printf("Weekend\n"); break;
default: printf("Invalid\n");
}For-Schleife
Die for-Schleife hat init; bedingung; update. sizeof(nums)/sizeof(nums[0]) berechnet die Array-Länge zur Kompilierzeit. Die Deklaration von i innerhalb der for-Schleife erfordert C99 oder neuer. Der Schleifenkörper wird nullmal ausgeführt, wenn die Bedingung anfangs false ist.
for (int i = 0; i < 5; i++) {
printf("%d\n", i);
}
// Iterate an array
int nums[] = {10, 20, 30};
int n = sizeof(nums) / sizeof(nums[0]);
for (int i = 0; i < n; i++) {
printf("%d\n", nums[i]);
}While & Do-While
while prüft vor der Ausführung (kann nullmal laufen). do-while führt den Körper zuerst aus, dann wird geprüft (läuft mindestens einmal). do-while ist ideal für Eingabevalidierung und Menü-Schleifen, bei denen die Eingabeaufforderung erscheinen muss, bevor die Bedingung geprüft werden kann.
int count = 0;
while (count < 3) {
printf("%d\n", count++);
}
int x;
do {
printf("Enter positive: ");
scanf("%d", &x);
} while (x <= 0); // runs at least onceBreak, Continue & goto
break verlässt die nächste Schleife/den nächsten switch; continue springt zur nächsten Iteration. C hat kein labeled break, daher ist goto die idiomatische Methode, um tief verschachtelte Schleifen zu verlassen. goto ist sonst verpönt, aber akzeptabel für Cleanup-Muster und verschachtelte Schleifenabbrüche.
for (int i = 0; i < 10; i++) {
if (i == 3) continue; // skip 3
if (i == 7) break; // stop at 7
printf("%d ", i); // 0 1 2 4 5 6
}
// goto for breaking nested loops
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (found) goto done;
}
}
done: printf("exited\n");Funktionen
Definieren & Aufrufen
Funktionen müssen vor der Verwendung deklariert (Prototyp) oder definiert werden. void-Rückgabetyp bedeutet kein Rückgabewert. const char *name bedeutet, dass die Funktion den String nicht modifiziert. C übergibt Argumente als Wert; verwenden Sie Zeiger, um Pass-by-Reference zu simulieren.
int add(int a, int b) {
return a + b;
}
void greet(const char *name) {
printf("Hello, %s!\n", name);
}
int main(void) {
int sum = add(3, 4);
greet("Alice");
return 0;
}Rekursion
Rekursion ruft sich selbst mit einer kleineren Eingabe auf. Jede rekursive Funktion benötigt einen Basisfall zum Stoppen. Das naive fib oben ist O(2^n)—exponentiell. Verwenden Sie Memoisierung oder Iteration für Effizienz. Tiefe Rekursion kann den Call-Stack überlaufen lassen.
int factorial(int n) {
if (n <= 1) return 1; // base case
return n * factorial(n - 1); // recursive case
}
int fib(int n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
// factorial(5) == 120Funktionszeiger
Funktionszeiger speichern die Adresse einer Funktion und ermöglichen Callbacks und dynamischen Dispatch. Die Syntax int (*op)(int, int) deklariert einen Zeiger auf eine Funktion, die zwei ints annimmt und int zurückgibt. Verwendet in qsort, Event-Handlern und Plugin-Systemen.
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int (*op)(int, int) = add;
printf("%d\n", op(3, 4)); // 7
op = sub;
printf("%d\n", op(3, 4)); // -1
// As a parameter
int apply(int (*f)(int, int), int a, int b) {
return f(a, b);
}Variadische Funktionen
Variadische Funktionen akzeptieren eine variable Anzahl von Argumenten mit <stdarg.h>. va_start initialisiert, va_arg holt das nächste Argument, va_end räumt auf. Sie benötigen eine Möglichkeit, die Anzahl zu kennen (z.B. ein Zählparameter oder ein Sentinel-Wert). printf funktioniert so.
#include <stdarg.h>
int sum(int count, ...) {
va_list args;
va_start(args, count);
int total = 0;
for (int i = 0; i < count; i++) {
total += va_arg(args, int);
}
va_end(args);
return total;
}
// sum(3, 10, 20, 30) == 60static & inline
static auf eine Funktion/Variable beschränkt sie auf die aktuelle Übersetzungseinheit (Datei). static auf eine lokale Variable lässt sie über Aufrufe hinweg bestehen (wie eine globale, aber begrenzt). inline schlägt dem Compiler vor, den Funktionskörper einzubetten; moderne Compiler ignorieren es und entscheiden selbst.
// static: internal linkage (file-local)
static int counter = 0;
static int next_id(void) { return ++counter; }
// inline: hint to expand inline
static inline int square(int x) { return x * x; }
// static local: persists across calls
int call_count(void) {
static int n = 0;
return ++n;
}Arrays & Zeiger
Arrays
Arrays haben feste Größe, sind null-indiziert und zusammenhängend im Speicher gespeichert. sizeof(arr)/sizeof(arr[0]) berechnet die Länge, funktioniert aber nur bei echten Arrays, nicht bei Zeigern (Arrays zerfallen zu Zeigern, wenn sie an Funktionen übergeben werden, wobei die Größeninformation verloren geht).
int nums[5] = {1, 2, 3, 4, 5};
printf("%d\n", nums[0]); // 1
printf("%d\n", nums[4]); // 5
int len = sizeof(nums) / sizeof(nums[0]); // 5
// Array of strings
char *fruits[] = {"apple", "banana", "cherry"};
printf("%s\n", fruits[1]); // bananaZeiger
Zeiger speichern Speicheradressen. & holt die Adresse, * dereferenziert. Initialisieren Sie Zeiger immer (verwenden Sie NULL, falls noch nicht zugewiesen). Das Dereferenzieren eines NULL- oder nicht initialisierten Zeigers ist undefiniertes Verhalten (normalerweise ein Absturz). Prüfen Sie vor dem Dereferenzieren auf NULL.
int x = 10;
int *ptr = &x; // ptr holds address of x
printf("%p\n", (void*)ptr); // address
printf("%d\n", *ptr); // 10 (dereference)
*ptr = 20; // modify x through pointer
printf("%d\n", x); // 20
int *p = NULL; // null pointer (points to nothing)
if (p) { /* safe to dereference */ }Zeigerarithmetik
Zeigerarithmetik skaliert nach der Elementgröße: p+1 bewegt sich zum nächsten Element, nicht zum nächsten Byte. Das macht p[i] äquivalent zu *(p+i). Das Subtrahieren zweier Zeiger desselben Arrays ergibt die Elementanzahl. Zeigerarithmetik ist nur innerhalb eines Arrays gültig.
int arr[] = {10, 20, 30, 40, 50};
int *p = arr; // points to arr[0]
printf("%d\n", *p); // 10
printf("%d\n", *(p + 1)); // 20
printf("%d\n", *(p + 2)); // 30
p += 3; // now points to arr[3]
printf("%d\n", *p); // 40
int diff = (p - arr); // 3 (number of elements)Arrays vs. Zeiger
Array-Namen zerfallen zu Zeigern, wenn sie an Funktionen übergeben oder in Ausdrücken verwendet werden, wobei die Größeninformation verloren geht. Deshalb müssen Sie die Array-Länge separat übergeben. sizeof(arr) gibt die volle Array-Größe nur, wenn arr ein echtes Array ist, kein Zeiger.
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr; // arr decays to &arr[0]
// These are equivalent
printf("%d\n", arr[2]);
printf("%d\n", ptr[2]);
printf("%d\n", *(arr + 2));
// But sizeof differs
printf("%zu\n", sizeof(arr)); // 20 (5 * 4 bytes)
printf("%zu\n", sizeof(ptr)); // 8 (pointer size)Mehrdimensionale Arrays
2D-Arrays sind Arrays von Arrays, zeilenweise gespeichert. grid[i][j] greift auf Zeile i, Spalte j zu. Beim Übergeben an Funktionen muss die Anzahl der Spalten angegeben werden: void foo(int arr[][3], int rows). Für dynamische 2D-Arrays verwenden Sie Arrays von Zeigern.
int grid[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
printf("%d\n", grid[0][1]); // 2
printf("%d\n", grid[1][2]); // 6
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", grid[i][j]);
}
printf("\n");
}Structs & Unions
Structs
Structs gruppieren verwandte Variablen unterschiedlicher Typen. Auf Member wird mit dem Punkt-Operator (.) zugegriffen. Initialisieren Sie mit Klammernotation. Structs werden als Wert übergeben (kopiert); übergeben Sie als Zeiger (struct Point *), um Kopieren zu vermeiden und das Original zu modifizieren.
struct Point {
int x;
int y;
};
struct Point p = {3, 4};
printf("(%d, %d)\n", p.x, p.y); // (3, 4)
p.x = 10;
p.y = 20;
printf("(%d, %d)\n", p.x, p.y); // (10, 20)typedef
typedef erstellt einen Alias für einen Typ, sodass Sie Student statt struct Student schreiben können. Es wird häufig mit Structs verwendet, um die Syntax zu vereinfachen. typedef kann auch Funktionszeiger-Typen aliasen, was Callbacks viel lesbarer macht.
typedef struct {
char name[50];
int age;
float gpa;
} Student;
Student s = {"Alice", 20, 3.8};
printf("%s: %d, GPA %.1f\n", s.name, s.age, s.gpa);
// typedef for other types
typedef unsigned long ulong;
typedef int (*CompareFn)(const void*, const void*);Zeiger auf Structs
Wenn Sie einen Zeiger auf ein Struct haben, verwenden Sie den Pfeil-Operator (->), um auf Member zuzugreifen. ptr->x ist eine Abkürzung für (*ptr).x. Übergeben Sie Struct-Zeiger an Funktionen für Effizienz (vermeidet Kopieren großer Structs) und um Modifikation zu erlauben.
typedef struct {
int x, y;
} Point;
Point p = {3, 4};
Point *ptr = &p;
// Arrow operator (->) for pointer members
printf("%d\n", ptr->x); // 3
ptr->y = 10;
printf("%d\n", p.y); // 10
// Equivalent: (*ptr).xUnions
Unions legen mehrere Typen über denselben Speicher—nur ein Member ist gleichzeitig gültig. Das Setzen eines Members überschreibt die anderen. Nützlich für Type Punning (Bits neu interpretieren) und Speichereinsparung, wenn nur einer von mehreren Typen gleichzeitig benötigt wird.
union Value {
int i;
float f;
char bytes[4];
};
union Value v;
v.i = 65;
printf("%d\n", v.i); // 65
printf("%c\n", v.bytes[0]); // 'A' (same memory)
v.f = 3.14f;
printf("%d\n", v.i); // reinterpreted bits!Bit-Felder & Enums
Bit-Felder packen mehrere kleine Felder in ein einzelnes int und sparen Speicher (häufig in Protokollen und Hardware-Registern). Enums definieren benannte Ganzzahlkonstanten (0, 1, 2... standardmäßig). Verwenden Sie Enums statt #define für besseres Debugging und Typsicherheit.
struct Flags {
unsigned int bold : 1;
unsigned int italic : 1;
unsigned int size : 6; // 0-63
};
struct Flags f = {1, 0, 12};
printf("bold=%d, size=%d\n", f.bold, f.size);
enum Color { RED, GREEN, BLUE };
enum Color c = GREEN;
printf("%d\n", c); // 1Speicherverwaltung
malloc & free
malloc reserviert Heap-Speicher und gibt einen void-Zeiger zurück (oder NULL bei Fehlschlag). Prüfen Sie immer auf NULL. Jedes malloc muss mit einem free gepaart werden, um Speicherlecks zu vermeiden. Das Setzen des Zeigers auf NULL nach free verhindert Use-after-free-Bugs.
#include <stdlib.h>
int *arr = malloc(5 * sizeof(int));
if (arr == NULL) {
fprintf(stderr, "malloc failed\n");
return 1;
}
for (int i = 0; i < 5; i++) arr[i] = i * 2;
free(arr); // release memory
arr = NULL; // avoid dangling pointercalloc & realloc
calloc reserviert und nullt Speicher (sicherer als malloc, das Müll enthält). realloc ändert die Größe: Es kann den Block verschieben und einen neuen Zeiger zurückgeben. Wenn realloc fehlschlägt, gibt es NULL zurück, aber der Originalblock ist noch gültig—verwenden Sie einen temporären Zeiger, um Lecks zu vermeiden.
#include <stdlib.h>
// calloc: zero-initialized
int *arr = calloc(5, sizeof(int)); // all zeros
// realloc: resize
arr = realloc(arr, 10 * sizeof(int));
if (!arr) { /* handle failure, original still valid */ }
free(arr);Stack vs. Heap
Stack-Speicher ist automatisch (mit Funktionsaufrufen reserviert/freigegeben) und schnell, aber begrenzt (oft 1-8 MB). Heap-Speicher wird manuell über malloc/free verwaltet, viel größer, aber langsamer und anfällig für Lecks. Verwenden Sie Stack für kleine, kurzlebige Daten; Heap für große oder langlebige Daten.
// Stack: automatic, fast, limited size
int local_var = 42;
int arr[100]; // on the stack
// Heap: manual, large, slower
int *heap_arr = malloc(1000000 * sizeof(int));
// Stack frame is freed when function returns
// Heap memory persists until explicitly freedSpeicherlecks & hängende Zeiger
Speicherlecks entstehen, wenn Sie den einzigen Zeiger zu reserviertem Speicher verlieren (k önnen ihn nicht freigeben). Hängende Zeiger zeigen auf freigegebenen Speicher—Dereferenzieren ist undefiniertes Verhalten. Doppeltes Freigeben ist ebenfalls undefiniert. Tools wie Valgrind und AddressSanitizer erkennen diese Bugs.
// Memory leak: lost the pointer, can't free
void leak(void) {
int *p = malloc(100 * sizeof(int));
// function returns without free -> leaked!
}
// Dangling pointer: using freed memory
int *p = malloc(sizeof(int));
free(p);
*p = 42; // UNDEFINED BEHAVIOR!
// Double free: also undefined
free(p); // crash likelyDynamische Arrays & Strings
Dynamische Reservierung ermöglicht das Erstellen von Strings/Arrays, deren Größe zur Laufzeit bestimmt wird. Der Aufrufer ist für das Freigeben des Speichers verantwortlich. Reservieren Sie immer strlen+1 für Strings (den Null-Terminator). Dieses Muster (reservieren, zurückgeben, Aufrufer gibt frei) ist häufig in C-APIs.
#include <stdlib.h>
#include <string.h>
// Dynamic string copy
char *dup_str(const char *s) {
char *copy = malloc(strlen(s) + 1); // +1 for null
if (copy) strcpy(copy, s);
return copy; // caller must free
}
char *name = dup_str("Alice");
printf("%s\n", name);
free(name);Datei-I/O
fopen & fclose
fopen öffnet eine Datei und gibt einen FILE-Zeiger zurück (oder NULL bei Fehlschlag). Modi: r (lesen), w (schreiben/abschneiden), a (anhängen), r+ (lesen/schreiben), b (binär). Prüfen Sie immer auf NULL. fclose leert Puffer und schließt die Datei. fgets liest eine Zeile sicher.
#include <stdio.h>
FILE *f = fopen("data.txt", "r");
if (!f) {
perror("fopen failed");
return 1;
}
char line[256];
while (fgets(line, sizeof(line), f)) {
printf("%s", line);
}
fclose(f);fprintf & fscanf
fprintf und fscanf funktionieren wie printf/scanf, aber auf Dateien. fscanf ist fragil—Formatmissverständnisse verursachen Probleme. Für robustes Parsen lesen Sie Zeilen mit fgets und parsen Sie dann mit sscanf. Schließen Sie Dateien immer, wenn Sie fertig sind, um Puffer zu leeren und Ressourcen freizugeben.
FILE *f = fopen("output.txt", "w");
fprintf(f, "Name: %s\n", "Alice");
fprintf(f, "Age: %d\n", 30);
fclose(f);
FILE *in = fopen("output.txt", "r");
char name[50];
int age;
fscanf(in, "Name: %49s\n", name);
fscanf(in, "Age: %d\n", &age);
printf("%s, %d\n", name, age);
fclose(in);fread & fwrite (Binär)
fread/fwrite lesen/schreiben rohe Bytes—ideal für Binärdaten und Structs. Die Argumente sind: Puffer, Elementgröße, Anzahl, Datei. Binärdateien sind kompakt, aber nicht über Architekturen hinweg portabel (Endianness, Struct-Padding). Öffnen Sie Binärdateien immer mit 'b'-Modus.
typedef struct { int id; float score; } Record;
Record r = {1, 95.5f};
FILE *f = fopen("data.bin", "wb");
fwrite(&r, sizeof(Record), 1, f);
fclose(f);
Record r2;
FILE *in = fopen("data.bin", "rb");
fread(&r2, sizeof(Record), 1, in);
printf("id=%d, score=%.1f\n", r2.id, r2.score);
fclose(in);fseek, ftell & rewind
fseek bewegt die Dateiposition: SEEK_SET (vom Anfang), SEEK_CUR (relativ), SEEK_END (vom Ende). ftell gibt die aktuelle Position zurück. rewind ist eine Abkürzung für fseek(f, 0, SEEK_SET). Diese ermöglichen wahlfreien Zugriff in Dateien, nützlich für Datenbanken und indizierte Lookups.
FILE *f = fopen("data.txt", "r");
fseek(f, 0, SEEK_END); // jump to end
long size = ftell(f); // get position = file size
printf("Size: %ld bytes\n", size);
rewind(f); // back to start
// or: fseek(f, 0, SEEK_SET);
fseek(f, 10, SEEK_SET); // 10 bytes from start
char c = fgetc(f);
printf("Char at 10: %c\n", c);
fclose(f);stderr & Standard-Streams
Jedes C-Programm hat drei Streams: stdin (Tastatur), stdout (Bildschirm), stderr (Bildschirm, ungepuffert). Das Schreiben von Fehlern nach stderr trennt sie von der normalen Ausgabe und ermöglicht Umleitung: program 2> errors.log. stderr ist ungepuffert, sodass Meldungen vor Abstürzen erscheinen.
#include <stdio.h>
// Three standard streams: stdin, stdout, stderr
fprintf(stdout, "Normal output\n");
fprintf(stderr, "Error: something went wrong\n");
int c;
while ((c = fgetc(stdin)) != EOF) {
fputc(c, stdout); // echo input
}
// stderr is unbuffered (appears immediately)
// stdout is line-buffered (flushes on newline)Präprozessor & Makros
#define-Konstanten & Makros
#define erstellt Textsubstitutions-Makros. Konstanten wie PI verbessern Lesbarkeit und Wartbarkeit. Funktionsähnliche Makros müssen Parameter in Klammern setzen, um Vorrang-Bugs zu vermeiden: SQUARE(2+3) ohne Klammern wäre 2+3*2+3=11. Bevorzugen Sie const-Variablen und inline-Funktionen gegenüber Makros.
#define MAX_SIZE 100
#define PI 3.14159
#define VERSION "2.0"
// Function-like macro
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int area = SQUARE(5); // 25
int big = MAX(3, 7); // 7Bedingte Kompilierung
Bedingte Kompilierung (#if, #ifdef, #ifndef) schließt Code zur Kompilierzeit ein/aus. Dies wird für plattformspezifischen Code, Debug-Builds und Feature-Flags verwendet. #ifdef prüft, ob ein Makro definiert ist; #if wertet seinen Wert aus. #elif und #else bieten Alternativen.
#define DEBUG 1
#if DEBUG
printf("Debug: x=%d\n", x);
#endif
#ifdef _WIN32
// Windows-specific code
#elif defined(__linux__)
// Linux-specific code
#endif
#ifndef BUFFER_SIZE
#define BUFFER_SIZE 1024
#endifInclude-Guards
Include-Guards verhindern doppelte Einbindung von Headern, was Neudefinitionsfehler verursachen würde. Das #ifndef/#define/#endif-Muster ist Standard-C. #pragma once ist eine einfachere, weit unterstützte Alternative (nicht standard, aber funktioniert auf allen gängigen Compilern).
// myheader.h
#ifndef MYHEADER_H
#define MYHEADER_H
struct Point { int x, y; };
void init_point(struct Point *p);
#endif // MYHEADER_H
// Alternative (non-standard but widely supported):
#pragma once#pragma & Compiler-Hinweise
#pragma bietet compilerspezifische Direktiven. #pragma once ist ein einfacherer Include-Guard. #pragma pack steuert das Struct-Speicherlayout (wichtig für binäre Protokolle). __attribute__ (GCC/Clang) annotiert Funktionen für Optimierung, Deprekation und Warnungen.
#pragma once // include guard
#pragma pack(1) // struct packing (no padding)
#pragma GCC diagnostic ignored "-Wunused-variable"
// Common pragmas
#pragma message("Compiling " __FILE__)
// C99 __attribute__ (GCC/Clang)
__attribute__((deprecated)) void old_func(void);
__attribute__((noreturn)) void fatal(void);Stringifizierung & Token-Pasting
# (Stringifizierung) wandelt ein Makro-Argument in ein String-Literal. ## (Token-Pasting) verkettet Tokens zu einem neuen Bezeichner. Das zweistufige STR/XSTR-Muster stellt sicher, dass Makros vor der Stringifizierung expandiert werden. Diese werden in Codegenerierung und Logging-Makros verwendet.
#define STR(x) #x
#define XSTR(x) STR(x)
#define CONCAT(a, b) a##b
printf("%s\n", STR(Hello World)); // "Hello World"
printf("%s\n", XSTR(VERSION)); // expands VERSION first
int CONCAT(foo, bar) = 42; // creates variable foobar
printf("%d\n", foobar); // 42Bit-Operationen
Grundlegende bitweise Operatoren
Bitweise Operatoren manipulieren einzelne Bits. AND (&) maskiert Bits (behält nur gesetzte Bits), OR (|) setzt Bits, XOR (^) schaltet Bits um, NOT (~) invertiert alle Bits. Linksshift (<<) multipliziert mit Zweierpotenzen, Rechtsshift (>>) dividiert (für unsigned). Verwenden Sie immer unsigned Typen für Bit-Manipulation — signed Rechtsshift ist implementierungsdefiniert (kann sign-extend). Bit-Operationen sind extrem schnell (ein einzelner CPU-Zyklus) und werden in Flags, Hardware-Registern, Komprimierung und Kryptografie verwendet. Binäre Literale (0b-Präfix) sind C23/C++14; verwenden Sie hex (0x) oder dezimal in älterem C.
#include <stdio.h>
int main() {
unsigned int a = 0b1100; // 12
unsigned int b = 0b1010; // 10
// AND: both bits must be 1
printf("%u\n", a & b); // 8 (0b1000)
// OR: either bit is 1
printf("%u\n", a | b); // 14 (0b1110)
// XOR: bits differ (exclusive or)
printf("%u\n", a ^ b); // 6 (0b0110)
// NOT: flip all bits
printf("%u\n", ~a); // 4294967283 (on 32-bit)
// Left shift: multiply by 2^n
printf("%u\n", a << 2); // 48 (12 * 4)
// Right shift: divide by 2^n (unsigned)
printf("%u\n", a >> 1); // 6 (12 / 2)
return 0;
}Bits setzen, löschen & umschalten
Bit-Flags packen mehrere boolesche Optionen in eine einzelne Ganzzahl und sparen Speicher. Die drei Kernoperationen: SET (|= mask), CLEAR (&= ~mask), TOGGLE (^= mask), CHECK (& mask). Verwenden Sie #define mit (1 << n) für lesbare Flag-Namen. Dieses Muster ist allgegenwärtig in der Systemprogrammierung (Dateiberechtigungen, Geräte-Steuerung, Konfigurationsoptionen). Zum Beispiel verwenden Unix-Dateiberechtigungen (rwxr-xr-x = 0755) Bit-Flags. Verwenden Sie immer unsigned Ganzzahlen für Flags, um Sign-Extension-Probleme zu vermeiden. Dies ist speichereffizienter als ein Array von Bools (1 Bit vs. 8 Bits pro Flag).
#include <stdio.h>
// Flag definitions (powers of 2)
#define FLAG_READ (1 << 0) // 0b0001
#define FLAG_WRITE (1 << 1) // 0b0010
#define FLAG_EXECUTE (1 << 2) // 0b0100
#define FLAG_ADMIN (1 << 3) // 0b1000
int main() {
unsigned int permissions = 0;
// SET a bit (OR with mask)
permissions |= FLAG_READ | FLAG_WRITE; // 0b0011
// CHECK if a bit is set (AND, compare to 0)
if (permissions & FLAG_READ) {
printf("Read permission granted\n");
}
// CLEAR a bit (AND with inverted mask)
permissions &= ~FLAG_WRITE; // 0b0001
// TOGGLE a bit (XOR with mask)
permissions ^= FLAG_EXECUTE; // 0b0101 (execute now on)
permissions ^= FLAG_EXECUTE; // 0b0001 (execute now off)
// SET multiple bits at once
permissions = FLAG_READ | FLAG_EXECUTE | FLAG_ADMIN;
printf("Permissions: 0x%X\n", permissions); // 0xD
return 0;
}Bit-Manipulations-Tricks
Bit-Tricks nutzen die binäre Darstellung für Geschwindigkeit aus. x & 1 testet gerade/ungerade (schneller als Modulo). x & (x-1) löscht das niedrigste gesetzte Bit — nützlich zum Prüfen von Zweierpotenzen und Zählen von Bits. __builtin_popcount (GCC/Clang) oder __popcnt (MSVC) zählen gesetzte Bits in einer Anweisung auf modernen CPUs. XOR-Swap (a^=b; b^=a; a^=b) vermeidet eine temporäre Variable, ist aber langsamer auf modernen CPUs und weniger lesbar — vermeiden Sie ihn. Der Trick 'auf nächste Zweierpotenz aufrunden' propagiert das höchste gesetzte Bit zu allen niedrigeren Bits und addiert dann 1. Diese Tricks sind nützlich in Embedded-Systemen, Game-Engines und performance-kritischem Code.
#include <stdio.h>
int main() {
int x = 42;
// Check if odd/even (faster than x % 2)
if (x & 1) printf("odd\n"); else printf("even\n");
// Check if power of 2 (only one bit set)
// x & (x-1) clears the lowest set bit
if (x && !(x & (x - 1))) printf("power of 2\n");
// Count set bits (popcount / Hamming weight)
unsigned int n = 0b10110110;
int count = 0;
while (n) { count += n & 1; n >>= 1; }
printf("Set bits: %d\n", count); // 6
// Or use __builtin_popcount(n) (GCC/Clang)
// Swap two values without temp (XOR swap)
int a = 5, b = 10;
a ^= b; b ^= a; a ^= b;
// a=10, b=5 (avoid in practice — less readable)
// Get lowest set bit
unsigned int lowest = x & (-x); // isolates lowest 1-bit
// Round up to next power of 2
unsigned int v = 5;
v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++;
return 0;
}Bit-Felder in Structs
Bit-Felder packen mehrere kleine Werte in ein einzelnes Struct und sparen Speicher. Die Doppelpunkt-Syntax (unsigned int field : N) gibt die Bitbreite an. Der Compiler übernimmt Bit-Extraktion/-Einfügung automatisch. Dies ist nützlich für speicherbeschränkte Systeme, Netzwerkprotokolle und Hardware-Register-Mapping. Das Layout von Bit-Feldern ist jedoch implementierungsdefiniert (Byte-Reihenfolge, Alignment, Padding) — verwenden Sie keine Bit-Felder für plattformübergreifende Binärkompatibilität. Verwenden Sie explizite Bit-Maskierung (#define + & |) für portable binäre Formate. Unbenannte Felder (: 5) fügen Padding hinzu. Die Gesamtgröße wird auf das Alignment des Structs aufgerundet.
#include <stdio.h>
// Bit fields: pack multiple small fields into one int
struct Date {
unsigned int day : 5; // 0-31 (5 bits)
unsigned int month : 4; // 0-15 (4 bits)
unsigned int year : 12; // 0-4095 (12 bits)
unsigned int is_leap : 1; // 0 or 1 (1 bit)
}; // Total: 22 bits (padded to 32)
struct Flags {
unsigned int visible : 1;
unsigned int editable : 1;
unsigned int locked : 1;
unsigned int : 5; // unnamed padding (5 bits)
unsigned int priority : 4; // 0-15
};
int main() {
struct Date d = { 15, 6, 2024, 0 };
printf("Size: %zu bytes\n", sizeof(d)); // 4 bytes
printf("Date: %u/%u/%u\n", d.day, d.month, d.year);
struct Flags f = { .visible = 1, .editable = 0, .locked = 1, .priority = 7 };
printf("Size: %zu bytes\n", sizeof(f)); // 4 bytes
return 0;
}Praktische Bit-Manipulation (RGB-Farbe)
Das Packen mehrerer Werte in eine Ganzzahl ist häufig in Grafik, Netzwerk und Embedded-Systemen. RGB-Farben packen drei 8-Bit-Kanäle in 24 Bits (0xRRGGBB). Linksshift (<<) positioniert jeden Kanal, OR (|) kombiniert sie. Rechtsshift (>>) und Maskierung (& 0xFF) extrahieren einzelne Kanäle. Dies spart Speicher (1 int vs. 3 Bytes) und ermöglicht atomare Operationen. Dasselbe Muster gilt für Netzwerk-Byte-Reihenfolge, Hardware-Register-Zugriff und Datenkomprimierung. Verwenden Sie immer Typen mit fester Breite (uint8_t, uint32_t) für Portabilität — int-Größe variiert je nach Plattform.
#include <stdio.h>
#include <stdint.h>
// Pack RGB into a single 32-bit integer (0xRRGGBB)
uint32_t make_color(uint8_t r, uint8_t g, uint8_t b) {
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
}
// Extract components
uint8_t get_red(uint32_t color) { return (color >> 16) & 0xFF; }
uint8_t get_green(uint32_t color) { return (color >> 8) & 0xFF; }
uint8_t get_blue(uint32_t color) { return color & 0xFF; }
// Blend two colors (50/50 mix)
uint32_t blend(uint32_t c1, uint32_t c2) {
uint8_t r = (get_red(c1) + get_red(c2)) / 2;
uint8_t g = (get_green(c1) + get_green(c2)) / 2;
uint8_t b = (get_blue(c1) + get_blue(c2)) / 2;
return make_color(r, g, b);
}
int main() {
uint32_t red = make_color(255, 0, 0); // 0xFF0000
uint32_t blue = make_color(0, 0, 255); // 0x0000FF
uint32_t purple = blend(red, blue); // 0x7F007F
printf("Red: 0x%06X\n", red);
printf("Blue: 0x%06X\n", blue);
printf("Mix: 0x%06X (R=%d G=%d B=%d)\n",
purple, get_red(purple), get_green(purple), get_blue(purple));
return 0;
}Signal-Verarbeitung
Grundlegende Signal-Verarbeitung
Signale sind Software-Interrupts, die an einen Prozess gesendet werden (z.B. sendet Ctrl+C SIGINT, Division durch Null sendet SIGFPE). signal() registriert eine Handler-Funktion. In einem Handler sind nur async-signal-safe Funktionen erlaubt — printf, malloc und die meisten stdlib-Funktionen sind NICHT sicher, weil das Hauptprogramm mitten in einem Aufruf unterbrochen werden kann. Verwenden Sie write() für die Ausgabe. Häufige Signale: SIGINT (Ctrl+C), SIGTERM (Terminierungsanforderung), SIGKILL (erzwungenes Beenden, kann nicht abgefangen werden), SIGSEGV (Segfault), SIGALRM (Timer). Bevorzugen Sie sigaction() gegenüber signal() für Portabilität und Kontrolle.
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
// Signal handler function (must match signature)
void handler(int sig) {
// WARNING: only async-signal-safe functions allowed here!
// printf is NOT safe — use write() instead
const char *msg = "Caught SIGINT\n";
write(STDOUT_FILENO, msg, 14);
}
int main() {
// Register handler for Ctrl+C (SIGINT)
signal(SIGINT, handler);
// Ignore SIGINT entirely
// signal(SIGINT, SIG_IGN);
// Reset to default behavior (terminate)
// signal(SIGINT, SIG_DFL);
printf("PID %d waiting. Press Ctrl+C...\n", getpid());
while (1) {
sleep(1);
}
return 0;
}sigaction (portable Signal-Verarbeitung)
sigaction() ist die moderne, portable Methode zum Behandeln von Signalen (signal()-Verhalten variiert je nach Plattform). Der sa_sigaction-Handler erhält ein siginfo_t mit Details: si_pid (Sender-PID), si_uid (Sender-UID), si_signo (Signalnummer), si_code (Grund). Das SA_SIGINFO-Flag aktiviert den drei-Argumente-Handler. sa_mask blockiert bestimmte Signale während der Handler-Ausführung (verhindert verschachtelte Interrupts). Andere Flags: SA_RESTART (unterbrochene Syscalls automatisch neu starten), SA_NOCLDWAIT (keine Zombie-Kinder). Verwenden Sie sigaction() immer in Produktionscode — signal() ist auf einigen Plattformen unzuverlässig.
#include <stdio.h>
#include <signal.h>
#include <string.h>
void handler(int sig, siginfo_t *info, void *context) {
// siginfo_t provides details about the signal
printf("Signal %d from PID %d\n", sig, info->si_pid);
}
int main() {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = handler; // use sa_sigaction (not sa_handler)
sa.sa_flags = SA_SIGINFO; // enable siginfo_t parameter
// Block other signals during handler execution
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGQUIT); // block SIGQUIT during handler
// Register (more portable than signal())
sigaction(SIGINT, &sa, NULL);
// Send a signal to self
raise(SIGINT); // like kill(getpid(), SIGINT)
printf("Done\n");
return 0;
}Signale senden & Alarm
alarm(seconds) plant ein SIGALRM nach der angegebenen Zeit — nützlich für Timeouts. pause() blockiert, bis ein Signal eintrifft. volatile sig_atomic_t ist die einzige sichere Methode, Daten zwischen einem Signal-Handler und Hauptcode zu teilen — volatile verhindert Compiler-Optimierung, sig_atomic_t garantiert atomaren Zugriff. kill(pid, signal) sendet ein Signal an einen anderen Prozess. raise(sig) sendet ein Signal an sich selbst. SIGKILL (9) und SIGSTOP können nicht abgefangen oder ignoriert werden — sie funktionieren immer. SIGTERM (15) ist die höfliche Terminierungsanforderung (Programme können sie abfangen, um aufzuräumen). Verwenden Sie alarm() für einfache Timeouts; verwenden Sie setitimer()/timer_create() für mehr Kontrolle.
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
volatile sig_atomic_t got_alarm = 0;
void alarm_handler(int sig) {
got_alarm = 1; // safe: sig_atomic_t is atomic
}
int main() {
signal(SIGALRM, alarm_handler);
// Set a timer: deliver SIGALRM after 3 seconds
alarm(3);
printf("Waiting for alarm...\n");
// Wait for the alarm
while (!got_alarm) {
pause(); // sleep until any signal arrives
}
printf("Alarm fired!\n");
// Send signal to another process
// kill(pid, SIGTERM); // request termination
// kill(pid, SIGKILL); // force kill (can't be caught)
// Send signal to self
raise(SIGUSR1);
return 0;
}Referenz häufiger Signale
Das Verstehen von Signalen ist essenziell für Unix-Programmierung. SIGKILL (9) und SIGSTOP können nicht abgefangen werden — sie sind das letzte Mittel. SIGTERM ist das Standard-Signal für ordnungsgemäßes Herunterfahren (fangen Sie es ab, um Zustand zu speichern). SIGINT ist Ctrl+C (interaktiver Interrupt). SIGCHLD feuert, wenn ein Kindprozess endet — wenn Sie nicht wait() dafür aufrufen, wird das Kind zum Zombie. Das Setzen von SIGCHLD auf SIG_IGN erntet Kinder automatisch (oder verwenden Sie SA_NOCLDWAIT). SIGPIPE feuert beim Schreiben in eine geschlossene Pipe/Socket — die meisten Server ignorieren es (signal(SIGPIPE, SIG_IGN)) und prüfen stattdessen write()-Rückgabewerte. Verwenden Sie _exit() (nicht exit()) in Signal-Handlern — exit() führt atexit-Handler aus, die möglicherweise nicht signal-safe sind.
#include <signal.h>
// Common POSIX signals:
// SIGINT (2) - Ctrl+C interrupt (terminate)
// SIGQUIT (3) - Ctrl+\ quit (core dump)
// SIGKILL (9) - Force kill (CANNOT be caught/ignored)
// SIGSEGV (11) - Segmentation fault (invalid memory access)
// SIGPIPE (13) - Write to broken pipe
// SIGTERM (15) - Termination request (graceful shutdown)
// SIGSTOP (19) - Pause process (CANNOT be caught/ignored)
// SIGCONT (18) - Resume paused process
// SIGCHLD (17) - Child process exited
// SIGALRM (14) - Timer alarm
// SIGUSR1 (10) - User-defined signal 1
// SIGUSR2 (12) - User-defined signal 2
// SIGFPE (8) - Arithmetic error (divide by zero)
// SIGBUS (7) - Bus error (misaligned access)
// Graceful shutdown pattern:
void cleanup_handler(int sig) {
// Save state, close files, release resources
// Then exit cleanly
_exit(0); // use _exit in signal handlers (not exit)
}
// In main:
signal(SIGTERM, cleanup_handler);
signal(SIGINT, cleanup_handler);
// Prevent zombie children:
signal(SIGCHLD, SIG_IGN); // auto-reap childrenSelf-Pipe-Trick (Signal-sicheres Aufwecken)
Der Self-Pipe-Trick löst ein grundlegendes Problem: Signal-Handler können keine komplexe Arbeit sicher ausführen, aber Sie müssen in Ihrer Hauptschleife auf Signale reagieren. Die Lösung: Der Handler schreibt ein Byte in eine Pipe, und die Hauptschleife verwendet select()/poll(), um es zu erkennen. Dies integriert Signale sicher in die Event-Schleife. Der Handler ruft nur write() auf (async-signal-safe). Moderne Alternativen: signalfd() (Linux-spezifisch, wandelt Signale direkt in Dateideskriptoren um) oder pselect() (blockiert Signale atomar während select). Dieses Muster wird in Event-gesteuerten Servern (nginx, Redis) verwendet, um Signale ohne Race-Conditions zu behandeln.
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
int pipe_fd[2]; // [0]=read, [1]=write
void handler(int sig) {
// Write one byte to the pipe — wakes up select()/poll()
write(pipe_fd[1], &sig, sizeof(sig));
}
int main() {
pipe(pipe_fd);
// Make read end non-blocking
fcntl(pipe_fd[0], F_SETFL, O_NONBLOCK);
signal(SIGINT, handler);
signal(SIGTERM, handler);
printf("Waiting (select-based)...\n");
while (1) {
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(pipe_fd[0], &readfds);
// select() blocks until pipe is writable (signal received)
int ready = select(pipe_fd[0] + 1, &readfds, NULL, NULL, NULL);
if (ready > 0 && FD_ISSET(pipe_fd[0], &readfds)) {
int sig;
read(pipe_fd[0], &sig, sizeof(sig));
printf("Handled signal %d in main loop\n", sig);
if (sig == SIGTERM) break;
}
}
return 0;
}Prozess-Fork & Exec
fork()-Grundlagen
fork() erstellt eine exakte Kopie des aktuellen Prozesses — der einzige Unterschied ist der Rückgabewert: 0 im Kind, Kind-PID im Elternprozess. Beide Prozesse setzen ab dem fork()-Aufruf fort. Das Kind erhält eine Kopie des Eltern-Speichers (Copy-on-Write optimiert dies). Prüfen Sie immer alle drei Fälle: pid < 0 (Fehler), pid == 0 (Kind), pid > 0 (Eltern). waitpid() blockiert, bis das Kind endet, und ruft seinen Status ab. WIFEXITED prüft, ob es normal beendet wurde, WEXITSTATUS holt den Exit-Code. Wenn Sie nicht wait() aufrufen, wird das Kind zum Zombie, bis es geerntet wird.
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork(); // create a child process
if (pid < 0) {
perror("fork failed");
return 1;
} else if (pid == 0) {
// CHILD process (fork returned 0)
printf("Child: PID=%d, Parent PID=%d\n",
getpid(), getppid());
sleep(2);
printf("Child exiting\n");
return 42; // child exit code
} else {
// PARENT process (fork returned child's PID)
printf("Parent: PID=%d, Child PID=%d\n",
getpid(), pid);
int status;
waitpid(pid, &status, 0); // wait for child
if (WIFEXITED(status)) {
printf("Child exited with code %d\n",
WEXITSTATUS(status));
}
}
return 0;
}exec-Familie (Prozess-Image ersetzen)
exec ersetzt das aktuelle Prozess-Image durch ein neues Programm — die PID bleibt gleich, aber Code, Daten und Stack werden ersetzt. exec gibt nur bei Fehlschlag zurück. Die Namenskonvention: 'l' = Listen-Argumente (variadisch, NULL-terminiert), 'v' = Vektor/Array von Argumenten, 'p' = PATH nach ausführbarer Datei durchsuchen, 'e' = benutzerdefinierte Umgebung. Das erste Argument ist konventionsgemäß der Programmname (argv[0]). fork()+exec() ist die Unix-Methode zum Starten von Programmen — fork erstellt den Prozess, exec lädt das neue Programm. Diese Trennung ermöglicht das Einrichten von Dateideskriptoren, Umgebung und Signalen zwischen fork und exec.
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// CHILD: replace self with a new program
// exec never returns on success (only on failure)
// execlp: search PATH, list arguments
execlp("ls", "ls", "-la", "/tmp", NULL);
// execvp: search PATH, array of arguments
char *args[] = {"ls", "-la", "/tmp", NULL};
execvp("ls", args);
// execl: full path, list arguments
execl("/bin/ls", "ls", "-la", NULL);
// Only reached if exec failed
perror("exec failed");
_exit(1);
} else {
wait(NULL); // parent waits for child
printf("Child finished\n");
}
return 0;
}
// exec variants:
// execl (path, arg1, arg2, ..., NULL) — list args, full path
// execlp (file, arg1, arg2, ..., NULL) — list args, search PATH
// execv (path, argv[]) — array args, full path
// execvp (file, argv[]) — array args, search PATH
// execve (path, argv[], envp[]) — array args, custom envZombie- & Waisenprozesse
Zombies entstehen, wenn ein Kind endet, aber der Elternprozess nicht wait() aufgerufen hat — der Kernel behält den Prozesstabelleneintrag (PID, Exit-Status), bis er geerntet wird. Zombies verschwenden PIDs und können die Prozesstabelle erschöpfen. Lösung: Rufen Sie immer wait() für Kinder auf, oder setzen Sie SIGCHLD auf SIG_IGN (Kernel erntet automatisch). Waisen entstehen, wenn der Elternprozess vor dem Kind endet — init/systemd (PID 1) adoptiert die Waise und erntet sie, wenn sie endet. Das Double-Fork-Muster (fork, Kind forked erneut, erstes Kind endet) erstellt einen Daemon, der automatisch zu init reparentiert wird, vom Terminal entkoppelt. Überwachen Sie Zombies mit 'ps aux | grep Z' oder 'top'.
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
printf("Child PID=%d\n", getpid());
_exit(0); // child exits immediately
}
// If parent doesn't wait(), child becomes a ZOMBIE
// (process table entry remains until reaped)
sleep(5); // parent sleeps — child is now a zombie
// Run 'ps' during this window to see the zombie (state 'Z')
// Reap the zombie:
int status;
waitpid(pid, &status, 0);
printf("Zombie reaped\n");
// Orphan: if parent exits before child
pid_t pid2 = fork();
if (pid2 == 0) {
sleep(3); // parent will exit first
printf("Orphan adopted by init (PID 1), new parent=%d\n",
getppid());
_exit(0);
}
// Parent exits immediately — child becomes orphan
// init/systemd (PID 1) adopts and reaps it
// Prevent zombies: ignore SIGCHLD
// signal(SIGCHLD, SIG_IGN); // kernel auto-reaps children
// Or use SA_NOCLDWAIT with sigaction
return 0;
}Daemon-Prozess-Erstellung
Daemons sind Hintergrundprozesse, die ohne Terminal laufen (z.B. Webserver, Datenbanken). Die Daemonisierungsschritte: fork+exit zum Abkoppeln von der Shell, setsid() zum Erstellen einer neuen Session (kein kontrollierendes Terminal), erneut forken zur Sicherheit, chdir('/'), um ein Dateisystem nicht zu blockieren, umask für vorhersagbare Dateiberechtigungen setzen und stdio schließen/umleiten nach /dev/null. Der Double-Fork ist eine Unix-Konvention, um zu verhindern, dass der Daemon ein Terminal über open() zurückgewinnt. Moderne Systeme bieten systemd-Service-Dateien für Daemon-Verwaltung, aber das Verstehen manueller Daemonisierung ist wichtig für Embedded-Systeme und portablen Code. Loggen Sie in Dateien (nicht stdout), da stdout /dev/null ist.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
void daemonize() {
// 1. Fork and exit parent (child continues in background)
pid_t pid = fork();
if (pid > 0) exit(0); // parent exits
if (pid < 0) exit(1);
// 2. Create new session (detach from controlling terminal)
setsid();
// 3. Fork again (prevent reacquiring a terminal)
pid = fork();
if (pid > 0) exit(0);
if (pid < 0) exit(1);
// 4. Change working directory to / (don't hold filesystem)
chdir("/");
// 5. Set umask to 0 (full control over file permissions)
umask(0);
// 6. Close standard file descriptors (detach from terminal)
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
// 7. Redirect them to /dev/null (in case code writes to them)
open("/dev/null", O_RDWR); // fd 0 = stdin
dup(0); // fd 1 = stdout
dup(0); // fd 2 = stderr
}
int main() {
daemonize();
// Now running as a daemon (background, no terminal)
while (1) {
// Daemon work here (e.g., log to file, listen on socket)
sleep(60);
}
return 0;
}Inter-Prozess-Kommunikation (IPC) Überblick
IPC lässt Prozesse kommunizieren. Pipes sind am einfachsten (Eltern-Kind, unidirektional). Named Pipes (FIFOs) funktionieren zwischen unverwandten Prozessen über einen Dateisystempfad. Shared Memory ist am schnellsten (Zero-Copy), erfordert aber Synchronisation (Semaphoren/Mutexes). Sockets sind am flexibelsten (bidirektional, netzwerkfähig). Message Queues bieten strukturierte, nachrichtenbegrenzte Kommunikation. Signale sind minimal (nur eine Zahl). Wählen Sie nach Bedarf: Pipes für einfache Eltern-Kind-Kommunikation, Shared Memory für Hochleistungsdatenaustausch, Sockets für Netzwerkkommunikation. System V IPC (shmget, semget) ist älter; POSIX IPC (shm_open, sem_open) ist sauberer, aber weniger universell verfügbar.
#include <stdio.h>
// C provides several IPC mechanisms:
// 1. PIPES: unidirectional byte stream between parent/child
// pipe(fd) creates fd[0]=read, fd[1]=write
// Only works between related processes (fork)
// 2. NAMED PIPES (FIFOs): like pipes but have a filesystem path
// mkfifo("/tmp/myfifo", 0666);
// Works between unrelated processes
// 3. SHARED MEMORY: fastest IPC (both processes access same RAM)
// shmget/shmat (System V) or shm_open/mmap (POSIX)
// 4. MESSAGE QUEUES: structured messages (not byte stream)
// msgget/msgsnd/msgrcv (System V) or mq_open (POSIX)
// 5. SEMAPHORES: synchronization (not data transfer)
// semget/semop (System V) or sem_open (POSIX)
// 6. SOCKETS: bidirectional, works across machines (network)
// socket/bind/listen/accept/connect
// 7. SIGNALS: minimal data (just signal number)
// kill(pid, SIGUSR1)
// Choosing IPC:
// - Same machine, related processes → pipes
// - Same machine, unrelated processes → named pipes, shared memory
// - Different machines → sockets
// - Need synchronization → semaphores, mutexes
// - Need structured messages → message queuesPipes & IPC
Anonyme Pipes (Eltern-Kind)
Pipes bieten unidirektionale Kommunikation zwischen verwandten Prozessen (durch fork erstellt). pipe(fd) erstellt zwei Dateideskriptoren: fd[0] zum Lesen, fd[1] zum Schreiben. Kritisch: Schließen Sie das ungenutzte Ende in jedem Prozess — der Elternprozess schließt das Lese-Ende, das Kind schließt das Schreib-Ende. Wenn das Schreib-Ende nicht geschlossen wird, blockiert das read() des Kindes für immer (wartet auf mehr Daten). read() gibt 0 (EOF) nur zurück, wenn alle Schreib-Enden geschlossen sind. Pipes haben einen festen Puffer (typischerweise 64KB) — write() blockiert, wenn der Puffer voll ist. Pipes sind ideal für Eltern-Kind-Kommunikation und das Verketten von Shell-Befehlen (ls | grep).
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main() {
int fd[2]; // fd[0]=read end, fd[1]=write end
pipe(fd); // create pipe
pid_t pid = fork();
if (pid == 0) {
// CHILD: read from pipe
close(fd[1]); // close unused write end
char buf[256];
int n = read(fd[0], buf, sizeof(buf));
buf[n] = '\0';
printf("Child received: %s", buf);
close(fd[0]);
} else {
// PARENT: write to pipe
close(fd[0]); // close unused read end
const char *msg = "Hello from parent!\n";
write(fd[1], msg, strlen(msg));
close(fd[1]); // close write end → child's read returns 0 (EOF)
wait(NULL);
}
return 0;
}Named Pipes (FIFOs)
Named Pipes (FIFOs) sind Pipes mit einem Dateisystem-Namen — sie funktionieren zwischen unverwandten Prozessen. mkfifo() erstellt die Pipe-Datei; open() blockiert, bis sowohl ein Leser als auch ein Schreiber verbunden sind (eingebaute Synchronisation). FIFOs bleiben bestehen, bis unlink() aufgerufen wird (im Gegensatz zu anonymen Pipes, die verschwinden, wenn Prozesse enden). Sie sind nützlich für einfache IPC zwischen separaten Programmen. Das Blockierverhalten bei open() stellt sicher, dass der Schreiber nicht startet, bis ein Leser bereit ist. Verwenden Sie O_NONBLOCK für nicht-blockierende Opens. FIFOs sind unidirektional — für bidirektionale Kommunikation verwenden Sie zwei FIFOs oder Sockets. Named Pipes werden häufig in Shell-Skripten und Systemdiensten verwendet.
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
// Process A (writer):
int writer_main() {
mkfifo("/tmp/myfifo", 0666); // create named pipe
int fd = open("/tmp/myfifo", O_WRONLY);
write(fd, "Hello via FIFO!", 15);
close(fd);
return 0;
}
// Process B (reader) — can be a completely separate program:
int reader_main() {
int fd = open("/tmp/myfifo", O_RDONLY);
char buf[256];
int n = read(fd, buf, sizeof(buf));
buf[n] = '\0';
printf("Received: %s\n", buf);
close(fd);
return 0;
}
// Named pipes persist in the filesystem (use unlink to remove):
// unlink("/tmp/myfifo");
// open() blocks until BOTH a reader and writer are connected
// (unless O_NONBLOCK is used)Shared Memory (schnellste IPC)
Shared Memory ist die schnellste IPC — beide Prozesse mappen denselben physischen RAM, sodass Datenübertragung Zero-Copy ist. shmget() erstellt ein Segment, shmat() bindet es in den Adressraum des Prozesses ein, shmdt() trennt es, shmctl(IPC_RMID) zerstört es. Der kritische Vorbehalt: Shared Memory bietet KEINE Synchronisation — wenn beide Prozesse gleichzeitig darauf zugreifen, kommt es zu Datenrennen. Sie MÜSSEN Semaphoren, Mutexes oder andere Synchronisation verwenden, um den Zugriff zu koordinieren. ftok() generiert einen Schlüssel aus einem Dateipfad (beide Prozesse müssen sich auf den Schlüssel einigen). Zerstören Sie Shared Memory immer, wenn Sie fertig sind (es bleibt nach Prozessende bestehen und leakt Speicher). POSIX Shared Memory (shm_open/mmap) ist eine sauberere Alternative.
#include <stdio.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <string.h>
#include <unistd.h>
#define SHM_SIZE 1024
int main() {
key_t key = ftok("/tmp/shmfile", 65); // generate unique key
// Create shared memory segment
int shmid = shmget(key, SHM_SIZE, 0666 | IPC_CREAT);
pid_t pid = fork();
if (pid == 0) {
// CHILD: attach and read
char *shared = (char *)shmat(shmid, NULL, 0);
sleep(1); // wait for parent to write
printf("Child reads: %s\n", shared);
shmdt(shared); // detach
} else {
// PARENT: attach and write
char *shared = (char *)shmat(shmid, NULL, 0);
strcpy(shared, "Hello from shared memory!");
printf("Parent wrote to shared memory\n");
shmdt(shared);
wait(NULL);
// Destroy shared memory after use
shmctl(shmid, IPC_RMID, NULL);
}
return 0;
}
// WARNING: shared memory has NO synchronization!
// Use semaphores or mutexes to prevent race conditions.dup2 & Umleitung
dup2(oldfd, newfd) macht newfd zu einer Kopie von oldfd — so funktioniert Shell-Umleitung. Um stdout zu einer Pipe umzuleiten: dup2(pipe_write, STDOUT_FILENO) — jetzt geht printf/write zu stdout in die Pipe. Um stdin von einer Pipe umzuleiten: dup2(pipe_read, STDIN_FILENO) — jetzt kommt scanf/read von stdin aus der Pipe. Genau so implementiert die Shell Pipes (ls | sort), Umleitung (ls > file) und Eingabe (sort < file). Nach dup2 schließen Sie den originalen fd (er wurde dupliziert). Dieses Muster ist fundamental zum programmatischen Erstellen von Unix-Pipelines und wird von Shells, popen() und Prozessverwaltungsbibliotheken verwendet.
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
// Implement shell-like pipeline: ls | sort
int main() {
int pipefd[2];
pipe(pipefd);
pid_t pid1 = fork();
if (pid1 == 0) {
// First child: ls (writes to pipe instead of stdout)
close(pipefd[0]); // close read end
dup2(pipefd[1], STDOUT_FILENO); // stdout → pipe write
close(pipefd[1]); // close original (dup'd)
execlp("ls", "ls", NULL);
}
pid_t pid2 = fork();
if (pid2 == 0) {
// Second child: sort (reads from pipe instead of stdin)
close(pipefd[1]); // close write end
dup2(pipefd[0], STDIN_FILENO); // stdin → pipe read
close(pipefd[0]); // close original (dup'd)
execlp("sort", "sort", NULL);
}
// Parent: close both ends and wait
close(pipefd[0]);
close(pipefd[1]);
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 0);
return 0;
}popen (High-Level-Pipe)
popen() ist ein High-Level-Wrapper um fork+pipe+exec+shell — es führt einen Befehl über /bin/sh aus und gibt ein FILE* zum Lesen der Ausgabe ('r') oder Schreiben der Eingabe ('w') zurück. Es ist viel einfacher als manuelles fork/pipe/exec, läuft aber durch eine Shell, also übergeben Sie NIEMALS nicht vertrauenswürdige Eingaben (Shell-Injection-Risiko). Verwenden Sie fgets/fprintf auf dem zurückgegebenen FILE* wie eine reguläre Datei. pclose() schließt die Pipe und wartet, bis das Kind endet (gibt seinen Status zurück). Für nicht vertrauenswürdige Eingaben verwenden Sie fork+execvp direkt (keine Shell). popen ist perfekt für schnelle Skripte, Systemadministrationstools und das Lesen von Befehlsausgaben. Für bidirektionale Kommunikation verwenden Sie socketpair() oder zwei Pipes.
#include <stdio.h>
#include <stdlib.h>
int main() {
// popen opens a process with a pipe (like shell command | ...)
FILE *fp = popen("ls -la /tmp", "r");
if (fp == NULL) {
perror("popen failed");
return 1;
}
// Read command output line by line
char buf[256];
while (fgets(buf, sizeof(buf), fp) != NULL) {
printf(">> %s", buf);
}
pclose(fp); // closes pipe and waits for child
// Writing to a process (like ... | command):
FILE *wp = popen("grep hello", "w");
fprintf(wp, "hello world\n");
fprintf(wp, "goodbye\n");
pclose(wp); // grep outputs "hello world"
return 0;
}
// popen is simpler than fork+pipe+exec but:
// - Runs via /bin/sh (shell injection risk with user input!)
// - Less control over the child process
// - Use fork+exec directly for untrusted inputMakefile & Build-Tools
Grundlegende Makefile-Struktur
Make automatisiert die Kompilierung. Ein Makefile hat Regeln: target (zu erstellende Datei), prerequisites (Abhängigkeiten) und recipe (Shell-Befehle, TAB-eingerückt). Variablen (CC, CFLAGS) zentralisieren die Konfiguration. Automatische Variablen: $@ (Zielname), $< (erste Voraussetzung), $^ (alle Voraussetzungen). Pattern Rules (%.o: %.c) verallgemeinern die Kompilierung für alle Quelldateien. .PHONY deklariert Ziele, die keine Dateien sind (clean, all, install). Die erste Regel ist die Standardregel (make ohne Argumente erstellt 'all'). Make verfolgt Datei-Zeitstempel — es erstellt nur neu, wenn eine Voraussetzung neuer ist als das Ziel. Dieses inkrementelle Bauen spart Zeit bei großen Projekten.
# Makefile — build automation for C/C++ projects
# Rule syntax: target: prerequisites
# recipe (must start with TAB, not spaces)
# Variables
CC = gcc
CFLAGS = -Wall -Wextra -g -O2
TARGET = myapp
SRCS = main.c utils.c parser.c
OBJS = $(SRCS:.c=.o) # substitute .c with .o
# Default target (first rule)
all: $(TARGET)
# Link object files into executable
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
# Compile each .c to .o
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# Clean build artifacts
clean:
rm -f $(OBJS) $(TARGET)
# Run the program
run: $(TARGET)
./$(TARGET)
# Phony targets (not files)
.PHONY: all clean run
# Usage:
# $ make — builds 'all' (the default)
# $ make clean — removes build files
# $ make run — builds and runs
# $ make -j4 — parallel build (4 jobs)Automatische Variablen & Pattern Rules
Automatische Variablen machen Makefiles prägnant und wartbar. $@ (Ziel), $< (erste Voraussetzung) und $^ (alle Voraussetzungen) sind die häufigsten. Pattern Rules (%.o: %.c) lassen Sie eine Regel für alle Quelldateien schreiben — das % passt auf jeden String. $(wildcard) findet Dateien, die einem Glob entsprechen, $(patsubst) transformiert Strings — zusammen entdecken sie Quellen automatisch. Das @-Präfix unterdrückt die Ausgabe des Befehls. Static Pattern Rules (target: %.o: %.c) gelten für eine spezifische Liste. Das Verstehen dieser Features eliminiert repetitive Regeln und macht Makefiles skalierbar für große Projekte. Verwenden Sie immer TAB (keine Leerzeichen) für Recipe-Einrückung — Make ist streng darin.
# Automatic variables in recipes:
# $@ — the target filename
# $< — the first prerequisite
# $^ — all prerequisites (no duplicates)
# $+ — all prerequisites (with duplicates)
# $? — prerequisites newer than the target
# $* — the stem (matching % part)
# Example showing all automatic variables:
program: main.o utils.o
@echo "Target: $@" # program
@echo "First dep: $<" # main.o
@echo "All deps: $^" # main.o utils.o
@echo "Newer deps: $?" # (whichever changed)
gcc -o $@ $^
# Pattern rule: compile any .c to .o
%.o: %.c
gcc -c $< -o $@
# $< = source (.c file), $@ = target (.o file)
# Static pattern rule (specific files):
$(OBJS): %.o: %.c
gcc -c $< -o $@
# Built-in functions:
SRCS = $(wildcard src/*.c) # find all .c files
OBJS = $(patsubst src/%.c,build/%.o,$(SRCS)) # path substitution
DIRS = $(sort $(dir $(SRCS))) # unique directoriesAbhängigkeiten & Header-Dateien
Das Verfolgen von Header-Abhängigkeiten ist entscheidend — ohne es löst das Ändern einer .h-Datei keine Neukompilierung der .c-Dateien aus, die sie einbinden, was zu veralteten Builds führt. Die Lösung: gcc -MMD -MP generiert .d-Dateien, die alle Abhängigkeiten auflisten (inklusive Header). -include zieht diese in das Makefile ein. -MP fügt Pseudo-Ziele für Header hinzu (verhindert Fehler, wenn ein Header gelöscht wird). Dies ist der Standardansatz für C/C++-Projekte. Ohne dies müssten Sie jede Header-Abhängigkeit manuell auflisten — unhandlich für große Projekte. Der erste Build hat keine .d-Dateien (das '-' in -include unterdrückt den Fehler); sie werden während der Kompilierung erstellt und bei nachfolgenden Builds verwendet.
# When a header file changes, dependent .c files must recompile
# Make doesn't track this automatically — use gcc -MMD
CC = gcc
CFLAGS = -Wall -MMD -MP # generate .d dependency files
SRCS = $(wildcard src/*.c)
OBJS = $(SRCS:.c=.o)
DEPS = $(OBJS:.o=.d) # dependency files
all: myapp
myapp: $(OBJS)
$(CC) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# Include auto-generated dependency files
-include $(DEPS) # '-' means don't error if missing
clean:
rm -f $(OBJS) $(DEPS) myapp
.PHONY: all clean
# How it works:
# 1. gcc -MMD creates main.d next to main.o
# 2. main.d contains: main.o: main.c utils.h parser.h
# 3. -include pulls these in, so Make knows header dependencies
# 4. If utils.h changes, main.o rebuilds automaticallyMakefile für Multi-Verzeichnis-Projekt
Echte Projekte erstrecken sich über mehrere Verzeichnisse. Dieses Makefile entdeckt Quellen automatisch (wildcard), ordnet sie einem Build-Verzeichnis zu (patsubst) und erstellt Verzeichnisse nach Bedarf. Die |-Syntax erstellt Order-Only-Prerequisites — $(BUILDDIR) wird vor der Kompilierung erstellt, aber seine Zeitstempeländerung löst keine Neuerstellung aus (ohne | würde das Erstellen des Verzeichnisses jedes Mal alles neu erstellen). -Iinclude sagt gcc, wo Header zu finden sind. -MMD generiert Abhängigkeitsdateien im Build-Verzeichnis. Diese Struktur hält Quell-, Build- und Binärverzeichnisse getrennt — einfach zu bereinigen (rm -rf build) und verschmutzt nicht den Quellbaum. Für sehr große Projekte ziehen Sie CMake oder Meson in Betracht.
# Project structure:
# project/
# src/ — source files
# include/ — header files
# build/ — object files (created by make)
# bin/ — final executable
CC = gcc
CFLAGS = -Wall -Iinclude -g
SRCDIR = src
INCDIR = include
BUILDDIR = build
BINDIR = bin
TARGET = $(BINDIR)/myapp
SRCS = $(wildcard $(SRCDIR)/*.c)
OBJS = $(patsubst $(SRCDIR)/%.c,$(BUILDDIR)/%.o,$(SRCS))
DEPS = $(OBJS:.o=.d)
all: $(TARGET)
$(TARGET): $(OBJS) | $(BINDIR)
$(CC) -o $@ $^
# Order-only prerequisite: create build dir before compiling
$(BUILDDIR)/%.o: $(SRCDIR)/%.c | $(BUILDDIR)
$(CC) $(CFLAGS) -MMD -c $< -o $@
$(BUILDDIR):
mkdir -p $(BUILDDIR)
$(BINDIR):
mkdir -p $(BINDIR)
-include $(DEPS)
clean:
rm -rf $(BUILDDIR) $(BINDIR)
.PHONY: all clean
# Order-only prerequisites (| syntax) create directories
# without triggering rebuilds when the dir timestamp changesCMake-Grundlagen (Alternative zu Make)
CMake ist ein Meta-Build-System — es generiert Makefiles (oder Ninja, Visual Studio, Xcode-Projekte) aus einer CMakeLists.txt-Datei. Es ist der De-facto-Standard für C/C++-Projekte, weil es plattformübergreifende Kompilierung, Abhängigkeitserkennung und IDE-Integration behandelt. Schlüsselbefehle: project() setzt den Projektnamen, add_executable() definiert ein Build-Ziel, target_include_directories() fügt Header-Pfade hinzu, target_link_libraries() linkt Bibliotheken. Out-of-Source-Builds (mkdir build && cd build && cmake ..) halten den Quellbaum sauber. CMake erkennt Compiler und Flags pro Plattform automatisch. Für neue C/C++-Projekte bevorzugen Sie CMake gegenüber rohen Makefiles — es ist wartbarer und portabler.
# CMakeLists.txt — CMake is a cross-platform build generator
# It generates Makefiles (or Ninja, VS, Xcode projects)
cmake_minimum_required(VERSION 3.10)
project(MyApp C)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra")
# Add executable from source files
add_executable(myapp src/main.c src/utils.c src/parser.c)
# Include directory
target_include_directories(myapp PRIVATE include)
# Link a library
target_link_libraries(myapp m) # math library (-lm)
# Build type flags
set(CMAKE_C_FLAGS_DEBUG "-g -O0")
set(CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG")
# Usage:
# $ mkdir build && cd build
# $ cmake .. # generate Makefiles
# $ make # build
# $ make install # install (optional)
# $ cmake .. -DCMAKE_BUILD_TYPE=Debug # debug build
# Out-of-source builds keep source tree clean
# CMake is the standard for C/C++ cross-platform projectsFunktionszeiger Vertieft
Funktionszeiger-Syntax & Verwendung
Funktionszeiger speichern die Adresse einer Funktion und ermöglichen Runtime-Dispatch. Die Syntax int (*fp)(int, int) ist notorisch verwirrend — lesen Sie sie als 'fp ist ein Zeiger auf eine Funktion, die (int, int) annimmt und int zurückgibt'. typedef vereinfacht dies: typedef int (*math_func)(int, int) erstellt einen lesbaren Alias. Funktionsnamen zerfallen zu Zeigern (wie Array-Namen), sodass 'add' und '&add' äquivalent sind. Funktionszeiger ermöglichen Callbacks, Event-Handler, Strategy-Pattern und Dispatch-Tabellen (Arrays von Funktionszeigern für switch-ähnlichen Dispatch). Sie sind die Grundlage von qsort's Komparator und GUI-Event-Systemen.
#include <stdio.h>
// Function pointer syntax: return_type (*name)(param_types)
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int main() {
// Declare a function pointer
int (*operation)(int, int);
// Assign (function name decays to pointer)
operation = add; // or &add
printf("5 + 3 = %d\n", operation(5, 3)); // 8
operation = subtract;
printf("5 - 3 = %d\n", operation(5, 3)); // 2
// typedef for readability
typedef int (*math_func)(int, int);
math_func fn = add;
printf("Result: %d\n", fn(10, 20));
// Array of function pointers (dispatch table)
math_func ops[] = {add, subtract};
printf("op[0](4,2)=%d op[1](4,2)=%d\n",
ops[0](4, 2), ops[1](4, 2));
return 0;
}Callbacks (qsort-Beispiel)
qsort ist das klassische Beispiel für Funktionszeiger als Callbacks. Der Komparator empfängt const void*-Zeiger (generisch) und gibt eine Ganzzahl zurück, die die Reihenfolge angibt. qsort ruft Ihren Komparator auf, um die Elementreihenfolge zu entscheiden — Sie steuern das Sortierverhalten, indem Sie verschiedene Funktionen übergeben. Dies ist das Strategy-Pattern in C: Der Algorithmus (qsort) ist fest, aber die Vergleichslogik wird injiziert. void* ermöglicht generische Programmierung (jeden Typ sortieren). Der Komparator muss eine reine Funktion sein (keine Seiteneffekte) und konsistent (wenn a<b und b<c dann a<c). Dieses Muster wird in der gesamten C-Standardbibliothek verwendet (bsearch, atexit, signal).
#include <stdio.h>
#include <stdlib.h>
// Comparator function for qsort
// Returns: negative if a<b, 0 if equal, positive if a>b
int compare_asc(const void *a, const void *b) {
return (*(int *)a - *(int *)b);
}
int compare_desc(const void *a, const void *b) {
return (*(int *)b - *(int *)a);
}
int main() {
int arr[] = {5, 2, 8, 1, 9, 3, 7, 4, 6};
int n = sizeof(arr) / sizeof(arr[0]);
// qsort takes a function pointer as the comparator
qsort(arr, n, sizeof(int), compare_asc);
printf("Ascending: ");
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
// Same array, different comparator (descending)
qsort(arr, n, sizeof(int), compare_desc);
printf("Descending: ");
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
return 0;
}Structs mit Funktionszeigern (OOP in C)
C kann OOP mit Structs und Funktionszeigern simulieren — so funktionieren C++-vtables intern. Das Struct enthält einen Zeiger auf eine 'vtable' (virtuelle Funktionstabelle), die Funktionszeiger hält. Jede 'Unterklasse' (Circle, Square) hat ihre eigene vtable mit ihren Implementierungen. Das Casten eines Circle* zu Shape* ermöglicht Polymorphismus — print_shape() ruft die richtige area()-Funktion über die vtable auf. Dieses Muster wird in echtem C-Code verwendet: Linux-Kernel (Gerätetreiber), GObject (GTK) und SQLite. Es bietet Kapselung, Vererbung (via Struct-Einbettung) und Polymorphismus. Während im Vergleich zu C++ verbose, gibt es volle Kontrolle über Speicherlayout und virtuellen Dispatch.
#include <stdio.h>
// Simulating OOP with structs + function pointers
typedef struct Shape Shape;
// Virtual function table (vtable)
typedef struct {
double (*area)(Shape *);
double (*perimeter)(Shape *);
void (*describe)(Shape *);
} ShapeVTable;
struct Shape {
const ShapeVTable *vtable; // pointer to virtual functions
char name[32];
};
// Circle implementation
typedef struct {
Shape base; // inherit from Shape
double radius;
} Circle;
double circle_area(Shape *s) {
return 3.14159 * ((Circle *)s)->radius * ((Circle *)s)->radius;
}
double circle_perimeter(Shape *s) {
return 2 * 3.14159 * ((Circle *)s)->radius;
}
static const ShapeVTable circle_vtable = {
circle_area, circle_perimeter, NULL
};
Circle *circle_create(double r) {
Circle *c = malloc(sizeof(Circle));
c->base.vtable = &circle_vtable;
strcpy(c->base.name, "Circle");
c->radius = r;
return c;
}
// Polymorphic function (works with any Shape)
void print_shape(Shape *s) {
printf("%s: area=%.2f, perimeter=%.2f\n",
s->name, s->vtable->area(s), s->vtable->perimeter(s));
}Event-gesteuerte Programmierung mit Callbacks
Funktionszeiger ermöglichen Event-gesteuerte Architektur in C — das Publish/Subscribe-Muster. Handler registrieren sich via on_event() (Subscribe), und emit_event() ruft alle registrierten Handler auf (Publish). Dies entkoppelt Event-Produzenten von Konsumenten — der Emitter weiß nicht, was die Handler tun. Dieses Muster ist fundamental für GUI-Frameworks (Button-Klick → Handler), Game-Engines (Kollision → Callback) und asynchrone I/O (Daten bereit → Lese-Handler). Die Handler-Signatur (Event-Name + void* data) ist generisch genug für jeden Event-Typ. In der Produktion fügen Sie Fehlerbehandlung hinzu (was, wenn ein Handler abstürzt?), Prioritätsreihenfolge und die Möglichkeit zum Abbestellen. So funktionieren libuv, libevent und Node.js unter der Haube.
#include <stdio.h>
// Event system using function pointers
typedef void (*EventHandler)(const char *event, void *data);
// Simple event emitter
#define MAX_HANDLERS 10
static EventHandler handlers[MAX_HANDLERS];
static int handler_count = 0;
void on_event(EventHandler handler) {
if (handler_count < MAX_HANDLERS) {
handlers[handler_count++] = handler;
}
}
void emit_event(const char *event, void *data) {
for (int i = 0; i < handler_count; i++) {
handlers[i](event, data); // call each registered handler
}
}
// Concrete handlers
void log_handler(const char *event, void *data) {
printf("[LOG] Event: %s\n", event);
}
void alert_handler(const char *event, void *data) {
if (strcmp(event, "error") == 0) {
printf("[ALERT] Error occurred!\n");
}
}
int main() {
// Register handlers (subscribe)
on_event(log_handler);
on_event(alert_handler);
// Emit events (publish)
emit_event("click", NULL);
emit_event("error", NULL);
emit_event("scroll", NULL);
return 0;
}Funktionszeiger-Fallen
Funktionszeiger haben mehrere Fallen. Das Aufrufen eines NULL-Funktionszeigers stürzt ab (Segfault) — prüfen Sie immer vor dem Aufruf auf NULL. Das Casten auf die falsche Signatur ist undefiniertes Verhalten (die Aufrufkonvention kann abweichen). Das Vergleichen von Funktionszeigern auf Gleichheit ist gültig (gleiche Funktion), aber Ordnung (<, >) ist undefiniert. Verwenden Sie typedef konsistent — Funktionszeiger-Syntax ist fehleranfällig, und typedefs machen Deklarationen lesbar und wartbar. In C sind Funktionszeiger die einzige Möglichkeit, Runtime-Polymorphismus und Callbacks zu erreichen, daher ist ihre Beherrschung essenziell. C++ fügt std::function, Lambdas und virtuelle Funktionen als sicherere Alternativen hinzu.
#include <stdio.h>
// PITFALL 1: Calling a NULL function pointer (crash!)
void bad_call() {
void (*fp)(void) = NULL;
fp(); // SEGFAULT — always check for NULL
if (fp) fp(); // safe
}
// PITFALL 2: Wrong signature (undefined behavior)
void takes_int(int x) { printf("%d\n", x); }
void wrong_sig() {
void (*fp)(void) = (void (*)(void))takes_int; // WRONG cast
fp(); // UB: missing argument, garbage value
}
// PITFALL 3: Function pointer to a local function (dangling)
typedef int (*callback_t)(int);
callback_t get_callback() {
// Returning pointer to local function is OK (functions aren't local)
// But returning a pointer to a local VARIABLE is not
return NULL; // functions have static storage, safe to return
}
// PITFALL 4: Comparing function pointers
int f1(int x) { return x; }
int f2(int x) { return x; }
void compare_fps() {
int (*p1)(int) = f1;
int (*p2)(int) = f1;
if (p1 == p2) printf("Same function\n"); // OK
// Comparing p1 == f2 is valid but they're different functions
}
// GOOD: Always use typedef for complex function pointers
// typedef int (*comparator_t)(const void *, const void *);
// This makes declarations readable and consistentVariable Argumente (varargs)
Grundlegende variadische Funktionen (stdarg)
Variadische Funktionen akzeptieren eine variable Anzahl von Argumenten mit stdarg.h. va_list hält die Argumentliste, va_start initialisiert sie (erfordert den letzten benannten Parameter vor ...), va_arg holt das nächste Argument mit einem angegebenen Typ, va_end räumt auf. Die Funktion muss wissen, wie viele Argumente zu lesen sind — entweder über einen Zählparameter (wie printfs Formatstring) oder einen Sentinel-Wert (NULL-Terminator). Das '...' muss immer der letzte Parameter sein. va_arg prüft keine Typen — das Übergeben des falschen Typs ist undefiniertes Verhalten. So funktionieren printf, fprintf und execl.
#include <stdio.h>
#include <stdarg.h>
// Variadic function: takes variable number of arguments
// The '...' must be the LAST parameter
int sum(int count, ...) {
va_list args; // argument list type
va_start(args, count); // initialize (needs last named param)
int total = 0;
for (int i = 0; i < count; i++) {
int val = va_arg(args, int); // get next argument (as int)
total += val;
}
va_end(args); // cleanup
return total;
}
int main() {
printf("%d\n", sum(3, 10, 20, 30)); // 60
printf("%d\n", sum(5, 1, 2, 3, 4, 5)); // 15
printf("%d\n", sum(0)); // 0
return 0;
}
// The 'count' parameter tells the function how many args follow.
// Without it, the function can't know when to stop.Benutzerdefiniertes printf implementieren
vprintf/vfprintf/vsprintf sind variadische Helfer, die ein va_list statt ... annehmen — sie lassen Sie benutzerdefinierte printf-ähnliche Funktionen bauen. Das log_msg-Beispiel umschließt printf mit einem Log-Level-Präfix. Das print_values-Beispiel zeigt, wie gemischte Typen behandelt werden: Übergeben Sie ein Typ-Tag vor jedem Wert und switchen Sie auf das Tag, um va_arg mit dem richtigen Typ aufzurufen. Dies ist notwendig, weil va_arg den exakten Typ erfordert — es gibt keine Runtime-Typinformation. Das Typ-Tag-Muster wird in polymorphen C-APIs verwendet (z.B. SQLites Bind-Funktionen). Passen Sie va_arg-Typen immer genau an — int vs long, float vs double (Floats werden in varargs zu double befördert).
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
// Custom logging function with format string
void log_msg(const char *level, const char *format, ...) {
printf("[%s] ", level);
va_list args;
va_start(args, format);
// vprintf: like printf but takes va_list instead of ...
vprintf(format, args);
va_end(args);
printf("\n");
}
// Variadic function with mixed types
void print_values(int count, ...) {
va_list args;
va_start(args, count);
for (int i = 0; i < count; i++) {
int type = va_arg(args, int); // type tag
switch (type) {
case 0: // int
printf("int: %d\n", va_arg(args, int));
break;
case 1: // double
printf("double: %f\n", va_arg(args, double));
break;
case 2: // string
printf("string: %s\n", va_arg(args, char *));
break;
}
}
va_end(args);
}
int main() {
log_msg("INFO", "User %s logged in from %s", "Alice", "192.168.1.1");
log_msg("ERROR", "Failed to open %s (code %d)", "config.txt", 13);
print_values(2, 0, 42, 2, "hello", 1, 3.14);
return 0;
}Sentinel-terminierte variadische Funktionen
Sentinel-terminierte variadische Funktionen verwenden einen speziellen Wert (normalerweise NULL), um das Ende der Argumente zu markieren, statt einer Zählung. Dies ist sauberer für String-lastige APIs — der Aufrufer muss Argumente nicht zählen. Die exec-Familie (execl, execlp) verwendet NULL als Sentinel. Der Nachteil: Wenn der Aufrufer das NULL vergisst, liest die Funktion Garbage-Speicher (undefiniertes Verhalten). Einige Compiler (GCC) unterstützen __attribute__((sentinel)), um vor fehlenden Sentinels zu warnen. Dokumentieren Sie immer, dass NULL erforderlich ist. Der Puffergrößen-Parameter verhindert Pufferüberläufe — übergeben Sie immer die Zielgröße und prüfen Sie Grenzen vor strcat.
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
// Sentinel-terminated: last argument is NULL
void concat_strings(char *dest, size_t size, ...) {
va_list args;
va_start(args, size);
dest[0] = '\0'; // start with empty string
size_t used = 0;
while (1) {
char *s = va_arg(args, char *);
if (s == NULL) break; // sentinel — stop reading
size_t len = strlen(s);
if (used + len < size) {
strcat(dest, s);
used += len;
}
}
va_end(args);
}
int main() {
char result[256];
concat_strings(result, sizeof(result),
"Hello, ", "world", "! ", "How are you?", NULL);
printf("%s\n", result);
// Hello, world! How are you?
// Common C APIs using sentinels:
// execl("/bin/ls", "ls", "-l", NULL); // exec family
// sqlite3_exec(db, sql, callback, NULL, NULL);
return 0;
}Variadische Argumente weiterleiten
Das Weiterleiten variadischer Argumente erfordert va_copy (nicht Zuweisung) — va_list kann ein opaker Typ sein, der nicht mit = kopiert werden kann. va_copy lässt Sie die Argumentliste mehrfach durchlaufen (z.B. zuerst messen, dann drucken). Das LOG-Makro verwendet __VA_ARGS__, um alle Argumente an fprintf weiterzuleiten. Die ##__VA_ARGS__ GCC-Erweiterung entfernt das vorangehende Komma, wenn keine variadischen Argumente bereitgestellt werden (sodass LOG("msg") ohne nachfolgendes Komma funktioniert). Dieses Muster ist allgegenwärtig in C-Logging-Makros. C99 erfordert mindestens ein Argument vor ...; C11/C23 und GCC erlauben null. Für typsichere Alternativen in C++ verwenden Sie variadische Templates oder std::format.
#include <stdio.h>
#include <stdarg.h>
// Wrapper that forwards varargs to another function
// Use va_copy for the copy (needed for multiple passes)
void custom_printf(const char *format, ...) {
va_list args1, args2;
va_start(args1, format);
va_copy(args2, args1); // copy for second use
// First pass: count characters that would be printed
int len = vsnprintf(NULL, 0, format, args1);
printf("[len=%d] ", len);
// Second pass: actually print
vprintf(format, args2);
va_end(args1);
va_end(args2);
}
// Macro forwarding (common pattern for logging)
#define LOG(fmt, ...) \
fprintf(stderr, "[LOG] %s:%d: " fmt "\n", \
__FILE__, __LINE__, ##__VA_ARGS__)
// The ## operator removes the comma if __VA_ARGS__ is empty
// (GCC extension, widely supported)
int main() {
custom_printf("Value: %d, Name: %s\n", 42, "test");
LOG("Simple message"); // no extra args
LOG("With value: %d", 100); // with args
return 0;
}Variadische Makros (C99)
C99 variadische Makros verwenden __VA_ARGS__, um alle Argumente zu erfassen, die auf das ... in der Makrodefinition passen. ##__VA_ARGS__ (GCC-Erweiterung, jetzt Standard in C20) entfernt das Komma, wenn keine variadischen Argumente übergeben werden. Das COUNT-Makro verwendet einen cleveren Trick: Es ordnet N Argumente N, 5, 4, 3, 2, 1 zu und die N-te Position ergibt die Zählung. Das do { ... } while (0)-Idiom in ASSERT lässt das Makro sich wie eine einzelne Anweisung verhalten (sicher in if/else ohne Klammern). # stringifiziert Makro-Argumente. Variadische Makros sind essenziell für Logging, Debugging und generische Programmierung in C. Sie sind die Grundlage vieler Bibliotheks-APIs und des Logging-Systems des Linux-Kernels.
#include <stdio.h>
// C99 variadic macros: __VA_ARGS__ captures all extra args
#define DEBUG_PRINT(fmt, ...) \
printf("DEBUG: " fmt "\n", ##__VA_ARGS__)
#define MAX(...) (max_of(__VA_ARGS__))
// Assert macro with message
#define ASSERT(cond, fmt, ...) \
do { \
if (!(cond)) { \
fprintf(stderr, "Assertion failed: %s\n" fmt "\n", \
#cond, ##__VA_ARGS__); \
exit(1); \
} \
} while (0)
// Count arguments (GCC __VA_OPT__ or recursive macros)
#define COUNT(...) COUNT_N(__VA_ARGS__, 5, 4, 3, 2, 1)
#define COUNT_N(_1, _2, _3, _4, _5, N, ...) N
// Stringification of all args
#define STR(...) #__VA_ARGS__
int main() {
DEBUG_PRINT("x = %d", 42);
DEBUG_PRINT("no args"); // ## removes comma
ASSERT(x > 0, "x was %d", x);
printf("Count: %d\n", COUNT(a, b, c)); // 3
printf("Stringified: %s\n", STR(hello, world)); // "hello, world"
return 0;
}Bit-Manipulations-Tricks
Häufige Bit-Tricks
Bit-Manipulation arbeitet direkt auf binären Darstellungen. n & 1 prüft das niedrigstwertige Bit auf gerade/ungerade. Linksshift (<<) multipliziert mit 2; Rechtsshift (>>) dividiert. XOR-Swap vermeidet eine temporäre Variable, ist aber weniger lesbar. n & (n-1) löscht das niedrigste gesetzte Bit, nützlich für Zweierpotenz-Prüfungen und Popcount. Diese Tricks sind schnell, aber priorisieren Sie Lesbarkeit in Anwendungscode.
// Check if odd
int is_odd(int n) { return n & 1; }
// Multiply/divide by powers of 2
int doubled = x << 1; // x * 2
int halved = x >> 1; // x / 2
int times8 = x << 3; // x * 8
// Swap without temp variable
void swap(int *a, int *b) {
*a ^= *b; *b ^= *a; *a ^= *b;
}
// Check if power of 2
int is_pow2(int n) { return n > 0 && (n & (n-1)) == 0; }
// Count set bits (population count)
int popcount(unsigned int n) {
int count = 0;
while (n) { n &= (n-1); count++; }
return count;
}Bit-Flags & Masken
Bit-Flags packen mehrere boolesche Optionen in eine einzelne Ganzzahl und sparen Speicher. Jedes Flag ist eine Zweierpotenz (ein Bit). OR (|) setzt Flags, AND (&) prüft Flags, XOR (^) schaltet um, AND NOT (&= ~) löscht. Dieses Muster ist allgegenwärtig in der Systemprogrammierung: Dateiberechtigungen (O_RDONLY, O_CREAT), Socket-Optionen und GPU-Zustand. Verwenden Sie benannte Konstanten für Lesbarkeit.
#define FLAG_READ (1 << 0) // 0x01
#define FLAG_WRITE (1 << 1) // 0x02
#define FLAG_EXECUTE (1 << 2) // 0x04
// Set flags
unsigned int perms = FLAG_READ | FLAG_WRITE;
// Check if flag is set
if (perms & FLAG_WRITE) { /* write allowed */ }
// Toggle a flag
perms ^= FLAG_EXECUTE;
// Clear a flag
perms &= ~FLAG_APPEND;
// Check if ALL flags in mask are set
int has_all = (perms & (FLAG_READ|FLAG_WRITE))
== (FLAG_READ|FLAG_WRITE);Bit-Felder in Structs
Bit-Felder packen kleine Werte in eine minimale Anzahl von Bits innerhalb von Structs. Die Doppelpunkt-Syntax gibt die Bitbreite an. Dies spart Speicher für Datenstrukturen mit vielen kleinen Feldern (Datum, Flags, Hardware-Register). Das Layout von Bit-Feldern ist jedoch implementierungsdefiniert: Byte-Reihenfolge, Padding und Alignment variieren zwischen Compilern. Vermeiden Sie Bit-Felder für portable Datenformate; verwenden Sie stattdessen explizite Bit-Masken.
// Pack multiple small fields into one int
struct Date {
unsigned int day : 5; // 0-31 (5 bits)
unsigned int month : 4; // 0-15 (4 bits)
unsigned int year : 23; // 0-8M (23 bits)
}; // Total: 32 bits = 4 bytes
struct Date today = { 21, 6, 2025 };
printf("Size: %zu bytes\n", sizeof(today)); // 4
today.day = 15;
if (today.month == 12) {
today.year++;
today.month = 1;
}Endianness-Konvertierung
Endianness bestimmt die Byte-Reihenfolge: Little-Endian (x86, ARM-Standard) speichert LSB zuerst; Big-Endian (Netzwerk, einige MIPS) speichert MSB zuerst. Netzwerkprotokolle verwenden Big-Endian (Network-Byte-Order). Verwenden Sie htonl/ntohl für portablen Netzwerkcode. Manuelle Byte-Swaps mit Shifts und Masken funktionieren auf jeder Plattform. Erkennen Sie Endianness zur Kompilierzeit mit __BYTE_ORDER__ für optimierte Code-Pfade.
#include <arpa/inet.h> // htonl, ntohl
// Host to network byte order (big-endian) and back
uint32_t host_val = 0x12345678;
uint32_t net_val = htonl(host_val);
uint32_t back = ntohl(net_val);
// Manual byte swap (portable)
uint32_t swap32(uint32_t v) {
return ((v & 0xFF000000) >> 24) |
((v & 0x00FF0000) >> 8) |
((v & 0x0000FF00) << 8) |
((v & 0x000000FF) << 24);
}
// Detect endianness at compile time
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
printf("Little-endian system\n");
#endifBitweise Hacks
Branchless Bit-Hacks vermeiden bedingte Sprünge für Performance in engen Schleifen. Der abs-Trick verwendet arithmetischen Rechtsshift, um eine Maske zu erstellen. next_pow2 füllt alle Bits unterhalb des höchsten gesetzten Bits und addiert dann 1. Bit-Umkehr verwendet Teile-und-Herrsche: Tausche Nibbles, dann Paare, dann einzelne Bits. Diese sind nützlich in Kryptografie, Hashing und DSP. Moderne CPUs haben oft eingebaute Befehle (POPCNT, LZCNT), die schneller sind.
// Absolute value without branching
int abs_val(int n) {
int mask = n >> (sizeof(int)*8 - 1);
return (n ^ mask) - mask;
}
// Round up to next power of 2
unsigned int next_pow2(unsigned int n) {
n--;
n |= n >> 1; n |= n >> 2;
n |= n >> 4; n |= n >> 8;
n |= n >> 16;
return n + 1;
}
// Reverse bits in a byte
uint8_t reverse_byte(uint8_t b) {
b = (b >> 4) | (b << 4);
b = ((b & 0xCC) >> 2) | ((b & 0x33) << 2);
b = ((b & 0xAA) >> 1) | ((b & 0x55) << 1);
return b;
}Signal-Verarbeitung Vertieft
Signal-Mengen & Blockierung
sigprocmask blockiert Signale, sodass sie in die Warteschlange gestellt (nicht verloren) und später zugestellt werden. Dies schützt kritische Abschnitte vor Unterbrechung. SIG_BLOCK fügt zur Maske hinzu, SIG_UNBLOCK entfernt, SIG_SETMASK ersetzt. Verwenden Sie sigpending, um wartende Signale zu prüfen. Blockieren Sie Signale nur kurz; langes Blockieren kann wichtige Ereignisse verpassen. Signal-Masken sind prozessbezogen und werden über fork vererbt.
#include <signal.h>
sigset_t mask, oldmask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);
// Block signals (they will be queued)
sigprocmask(SIG_BLOCK, &mask, &oldmask);
// Critical section - signals are deferred
do_critical_work();
// Unblock - pending signals are now delivered
sigprocmask(SIG_SETMASK, &oldmask, NULL);
// Check for pending signals
sigpending(&mask);
if (sigismember(&mask, SIGINT)) {
printf("SIGINT is pending\n");
}Sichere Signal-Handler
Signal-Handler müssen async-signal-safe sein: Verwenden Sie nur reentrante Funktionen (write, _exit, signal). Vermeiden Sie printf, malloc und die meisten Bibliotheksfunktionen—sie können mitten in einer Operation unterbrochen werden und den Zustand korrumpieren. Verwenden Sie volatile sig_atomic_t für Handler-gesetzte Flags. SA_RESTART startet unterbrochene Systemaufrufe automatisch neu. sigaction wird gegenüber signal für portables, wohldefiniertes Verhalten bevorzugt.
#include <signal.h>
#include <unistd.h>
// Only safe type in handlers
volatile sig_atomic_t got_signal = 0;
void handler(int sig) {
// ONLY use async-signal-safe functions!
// write() is safe; printf() is NOT
const char msg[] = "Signal received\n";
write(STDERR_FILENO, msg, sizeof(msg)-1);
got_signal = 1;
}
int main() {
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART; // Restart interrupted syscalls
sigaction(SIGINT, &sa, NULL);
while (!got_signal) pause();
return 0;
}Self-Pipe & signalfd
signalfd (Linux) konvertiert Signale in Dateideskriptoren und integriert sie in Event-Schleifen (epoll, select). Blockieren Sie das Signal zuerst, dann erstellen Sie das signalfd. Der Self-Pipe-Trick ist portabel: Der Handler schreibt ein Byte in eine Pipe, und die Hauptschleife liest es. Beide Ansätze verlagern die Signal-Verarbeitung aus dem eingeschränkten Handler-Kontext in normalen Code, wo Sie jede Funktion sicher aufrufen können.
#include <sys/signalfd.h>
// signalfd: handle signals as file descriptors (Linux)
int setup_signalfd() {
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);
sigprocmask(SIG_BLOCK, &mask, NULL); // Must block first
int fd = signalfd(-1, &mask, SFD_CLOEXEC);
return fd;
}
// In event loop:
struct signalfd_siginfo si;
read(signalfd_fd, &si, sizeof(si));
printf("Got signal %d\n", si.ssi_signo);
// Self-pipe trick (portable):
// Write to pipe in handler, read in main loop
int pipefd[2];
void pipe_handler(int sig) {
write(pipefd[1], &sig, sizeof(sig));
}Timer & SIGALRM
setitimer liefert SIGALRM in Intervallen. ITIMER_REAL verwendet Echtzeit; ITIMER_VIRTUAL verwendet CPU-Zeit; ITIMER_PROF verwendet CPU- + Systemzeit. Der Timer wiederholt sich, bis er abgebrochen wird. Für modernen Code bevorzugen Sie timer_create mit SIGEV_THREAD für pro-Thread-Timer, oder verwenden Sie einen dedizierten Timer-fd (timerfd_create auf Linux) zur Integration in Event-Schleifen. Behandeln Sie das Signal immer, um Standardterminierung zu vermeiden.
#include <sys/time.h>
volatile sig_atomic_t timer_fired = 0;
void timer_handler(int sig) { timer_fired = 1; }
int main() {
struct sigaction sa = {0};
sa.sa_handler = timer_handler;
sigaction(SIGALRM, &sa, NULL);
// Set interval timer: 2 sec initial, 1 sec repeat
struct itimerval timer;
timer.it_value.tv_sec = 2;
timer.it_interval.tv_sec = 1;
setitimer(ITIMER_REAL, &timer, NULL);
int count = 0;
while (count < 5) {
pause();
if (timer_fired) {
printf("Timer %d\n", ++count);
timer_fired = 0;
}
}
return 0;
}Signale senden
kill sendet ein Signal an einen Prozess per PID. kill(0, sig) sendet an die gesamte Prozessgruppe. raise sendet ein Signal an den aufrufenden Prozess. sigqueue sendet ein Signal mit angehängten Daten (siginfo). Verwenden Sie immer waitpid, um Kindprozesse zu ernten und den Exit-Status zu prüfen. WIFSIGNALED unterscheidet Signal-Tod von normalem Exit. Das Senden von SIGTERM (nicht SIGKILL) ermöglicht ordnungsgemäßes Herunterfahren.
#include <signal.h>
#include <sys/wait.h>
pid_t child = fork();
if (child == 0) {
while (1) { sleep(1); }
} else {
sleep(3);
kill(child, SIGTERM); // Send SIGTERM to child
int status;
waitpid(child, &status, 0);
if (WIFSIGNALED(status))
printf("Killed by signal %d\n", WTERMSIG(status));
}
// Send signal to self
raise(SIGSTOP); // Stop (resume with SIGCONT)
// Send to process group
kill(0, SIGUSR1); // 0 = own process group
// Send with data (sigqueue)
union sigval value = { .sival_int = 42 };
sigqueue(child, SIGUSR1, value);Prozessverwaltung Vertieft
fork & exec-Muster
Das fork-exec-Muster erstellt einen Kindprozess (fork) und ersetzt sein Image durch ein neues Programm (exec). fork dupliziert den Prozess; exec lädt ein neues Programm. Das Kind muss _exit (nicht exit) bei exec-Fehlschlag aufrufen, um das Leeren von Eltern-Puffern zu vermeiden. waitpid blockiert, bis das Kind endet. WEXITSTATUS extrahiert den Exit-Code. So führen Shells Befehle aus.
#include <unistd.h>
#include <sys/wait.h>
int run_program(const char *path, char *const args[]) {
pid_t pid = fork();
if (pid < 0) { perror("fork"); return -1; }
if (pid == 0) {
// Child: exec replaces process image
execvp(path, args);
perror("execvp"); // Only on failure
_exit(127);
}
// Parent: wait for child
int status;
waitpid(pid, &status, 0);
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}
// Usage
char *args[] = {"ls", "-la", "/tmp", NULL};
int result = run_program("ls", args);Daemonisierung
setsid erstellt eine neue Session und Prozessgruppe und trennt vom kontrollierenden Terminal. Das Double-Fork-Muster verhindert, dass der Daemon ein Terminal zurückgewinnt. Nach der Daemonisierung schließen Sie Standard-Dateideskriptoren und leiten nach /dev/null oder Log-Dateien um. chdir zu / verhindert blockierende Unmounts. umask(0) stellt vorhersagbare Dateiberechtigungen sicher. Dies ist das Standard-Daemon-Muster.
#include <unistd.h>
int main() {
pid_t child = fork();
if (child > 0) _exit(0); // Parent exits
if (child < 0) return 1;
// First child: create new session
setsid();
// Second fork (prevent reacquiring terminal)
pid_t grandchild = fork();
if (grandchild > 0) _exit(0);
// Daemon process
chdir("/");
umask(0);
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
while (1) {
sleep(60);
// Do daemon work
}
return 0;
}posix_spawn-Alternative
posix_spawn ist eine effizientere Alternative zu fork+exec auf Systemen ohne MMU (Embedded) oder mit großem Speicher-Footprint (fork kopiert Page-Tabellen). Es kombiniert Prozesserstellung und exec in einem Aufruf, wobei Dateiaktionen (Umleitung, Schließen) atomar angewendet werden. Verwenden Sie posix_spawn, wenn Sie den Kindzustand zwischen fork und exec nicht modifizieren müssen. Es ist POSIX-Standard und auf Linux, macOS und den meisten Unix-Systemen verfügbar.
#include <spawn.h>
#include <sys/wait.h>
extern char **environ;
int spawn_child(const char *cmd, char *const argv[]) {
pid_t pid;
posix_spawn_file_actions_t actions;
posix_spawn_file_actions_init(&actions);
posix_spawn_file_actions_addclose(&actions, STDIN_FILENO);
posix_spawnattr_t attr;
posix_spawnattr_init(&attr);
int ret = posix_spawnp(&pid, cmd, &actions, &attr, argv, environ);
posix_spawn_file_actions_destroy(&actions);
posix_spawnattr_destroy(&attr);
if (ret != 0) return -1;
int status;
waitpid(pid, &status, 0);
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}Zombie-Verhinderung
Zombie-Prozesse entstehen, wenn ein Kind endet, bevor der Elternprozess wait aufruft. Verhindern Sie sie durch: (1) Behandeln von SIGCHLD mit waitpid in einer Schleife (WNOHANG vermeidet Blockieren), (2) Setzen von SIGCHLD auf SIG_IGN (Kernel erntet automatisch), oder (3) Double-Forking, sodass das Enkelkind verwaist und von init (PID 1) adoptiert wird, der es automatisch erntet. Speichern und stellen Sie errno in Signal-Handlern immer wieder her.
#include <sys/wait.h>
#include <signal.h>
// Reap zombies with SIGCHLD handler
void sigchld_handler(int sig) {
int saved_errno = errno;
while (waitpid(-1, NULL, WNOHANG) > 0) {
// Reap all available zombies
}
errno = saved_errno;
}
// Setup: handle SIGCHLD
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa, NULL);
// Alternative: explicitly ignore (auto-reap)
// signal(SIGCHLD, SIG_IGN);
// Double-fork to prevent zombies
pid_t inter = fork();
if (inter == 0) {
if (fork() == 0) {
// Grandchild does the work
execlp("sleep", "sleep", "10", NULL);
_exit(1);
}
_exit(0); // First child exits immediately
}
waitpid(inter, NULL, 0);Ressourcenlimits
setrlimit legt Ressourcenlimits für einen Prozess fest: CPU-Zeit (RLIMIT_CPU), virtueller Speicher (RLIMIT_AS), Dateigröße (RLIMIT_FSIZE), offene Dateien (RLIMIT_NOFILE), Stack-Größe und Core-Dump-Größe. Das Soft-Limit wird durchgesetzt; das Hard-Limit ist die Obergrenze. Überschreiten der CPU-Zeit sendet SIGXCPU; Überschreiten des Speichers lässt malloc fehlschlagen. Verwenden Sie Limits in Kindprozessen, um Ressourcenerschöpfung durch Bugs oder Angriffe zu verhindern.
#include <sys/resource.h>
void set_limits() {
struct rlimit lim;
// Limit CPU time to 10 seconds
lim.rlim_cur = 10; lim.rlim_max = 10;
setrlimit(RLIMIT_CPU, &lim);
// Limit memory to 256 MB
lim.rlim_cur = 256*1024*1024;
lim.rlim_max = 256*1024*1024;
setrlimit(RLIMIT_AS, &lim);
// Limit open files
lim.rlim_cur = 32; lim.rlim_max = 64;
setrlimit(RLIMIT_NOFILE, &lim);
}
void show_limits() {
struct rlimit lim;
getrlimit(RLIMIT_NOFILE, &lim);
printf("Max files: soft=%lu, hard=%lu\n",
lim.rlim_cur, lim.rlim_max);
}Pipes & IPC Vertieft
Anonyme Pipes
Anonyme Pipes bieten unidirektionale Kommunikation zwischen Eltern- und Kindprozessen. Schließen Sie immer das ungenutzte Ende: Der Schreiber muss das Lese-Ende schließen und umgekehrt. Das Schließen des Schreib-Endes signalisiert dem Leser EOF (read gibt 0 zurück). Pipes haben einen festen Puffer (typischerweise 64KB); Schreibvorgänge blockieren, wenn voll. Pipes sind nur für verwandte Prozesse (Eltern-Kind). Für unverwandte Prozesse verwenden Sie Named Pipes (FIFOs) oder Sockets.
#include <unistd.h>
int main() {
int pipefd[2];
pipe(pipefd);
pid_t pid = fork();
if (pid == 0) {
// Child: read from pipe
close(pipefd[1]); // Close unused write end
char buf[256];
ssize_t n = read(pipefd[0], buf, sizeof(buf)-1);
buf[n] = '\0';
printf("Child received: %s", buf);
close(pipefd[0]);
} else {
// Parent: write to pipe
close(pipefd[0]); // Close unused read end
const char *msg = "Hello from parent!\n";
write(pipefd[1], msg, strlen(msg));
close(pipefd[1]); // EOF signal to reader
wait(NULL);
}
return 0;
}Named Pipes (FIFOs)
Named Pipes (FIFOs) sind spezielle Dateien, die als Pipes zwischen unverwandten Prozessen fungieren. mkfifo erstellt die Datei; open blockiert, bis sowohl ein Leser als auch ein Schreiber vorhanden sind. Verwenden Sie O_NONBLOCK für nicht-blockierende Opens. FIFOs bleiben im Dateisystem bestehen, bis sie unlinkt werden. Sie sind nützlich für einfache IPC zwischen unabhängigen Programmen, aber für komplexe Kommunikation ziehen Sie Unix-Domain-Sockets oder Message-Queues in Betracht.
#include <sys/stat.h>
#include <fcntl.h>
// Create a named pipe (persists in filesystem)
mkfifo("/tmp/myfifo", 0666);
// Writer process
int fd = open("/tmp/myfifo", O_WRONLY);
write(fd, "Hello FIFO", 10);
close(fd);
// Reader process (can be unrelated to writer)
int fd = open("/tmp/myfifo", O_RDONLY);
char buf[256];
ssize_t n = read(fd, buf, sizeof(buf));
close(fd);
// Non-blocking open
int fd = open("/tmp/myfifo", O_RDONLY | O_NONBLOCK);
// Clean up
unlink("/tmp/myfifo");Shared Memory
Shared Memory ist die schnellste IPC: Prozesse mappen denselben physischen Speicher in ihre Adressräume. shm_open erstellt ein POSIX-Shared-Memory-Objekt; mmap mappt es. Änderungen sind sofort für alle Mapper sichtbar. Verwenden Sie Semaphoren oder Mutexes (mit PTHREAD_PROCESS_SHARED) zur Synchronisation. Unmappen und unlinken Sie immer, um Lecks zu vermeiden. Shared Memory ist ideal für große Daten; der Overhead ist nur das anfängliche Mapping.
#include <sys/mman.h>
#include <fcntl.h>
// Create shared memory object
int fd = shm_open("/my_shm", O_CREAT | O_RDWR, 0666);
ftruncate(fd, 4096); // Set size
// Map into process address space
char *shared = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
close(fd); // Can close after mmap
// Write data (visible to other processes)
sprintf(shared, "Shared data at %p", (void*)shared);
// Synchronize (flush to backing store)
msync(shared, 4096, MS_SYNC);
// Unmap and clean up
munmap(shared, 4096);
shm_unlink("/my_shm");Unix-Domain-Sockets
Unix-Domain-Sockets bieten bidirektionale, stream-orientierte IPC auf derselben Maschine. Sie sind schneller als TCP (kein Netzwerk-Overhead) und unterstützen das Übergeben von Dateideskriptoren zwischen Prozessen via SCM_RIGHTS. Verwenden Sie SOCK_STREAM für zuverlässige Streams, SOCK_DGRAM für Datagramme. Der Socket-Pfad ist ein Dateisystem-Eintrag; unlinken Sie vor bind, um 'Address in use'-Fehler zu vermeiden. Unix-Sockets sind die Basis von Docker, X11 und systemd-Kommunikation.
#include <sys/socket.h>
#include <sys/un.h>
// Server
int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/mysocket");
unlink("/tmp/mysocket");
bind(sfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sfd, 5);
int cfd = accept(sfd, NULL, NULL);
char buf[256];
read(cfd, buf, sizeof(buf));
write(cfd, "Response", 8);
close(cfd);
// Client
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/mysocket");
connect(sock, (struct sockaddr*)&addr, sizeof(addr));
write(sock, "Request", 7);
close(sock);
unlink("/tmp/mysocket");Message Queues
POSIX-Message-Queues bieten prioritätsgeordnete, nachrichtenbasierte IPC. Jede Nachricht hat eine Priorität; Nachrichten mit höherer Priorität werden zuerst empfangen. mq_send und mq_receive sind atomar für einzelne Nachrichten. Verwenden Sie O_NONBLOCK für nicht-blockierenden Betrieb oder mq_timedreceive für Timeouts. Message-Queues bleiben bestehen, bis sie unlinkt werden, im Gegensatz zu Pipes, die mit Prozessen sterben. Sie sind ideal für Aufgabenverteilung und Event-Benachrichtigung.
#include <mqueue.h>
// Create/open a message queue
struct mq_attr attr = {
.mq_maxmsg = 10,
.mq_msgsize = 256
};
mqd_t mq = mq_open("/my_queue", O_CREAT | O_RDWR, 0666, &attr);
// Send a message (priority-based)
mq_send(mq, "Hello MQ", 8, 1); // priority 1
// Receive a message (highest priority first)
char buf[256];
unsigned int prio;
ssize_t n = mq_receive(mq, buf, sizeof(buf), &prio);
printf("Received (priority %u): %.*s\n", prio, (int)n, buf);
// Non-blocking receive
mqd_t mq_nb = mq_open("/my_queue", O_RDONLY | O_NONBLOCK);
// Timed receive
struct timespec ts = { .tv_sec = time(NULL) + 5 };
mq_timedreceive(mq, buf, sizeof(buf), &prio, &ts);
mq_close(mq);
mq_unlink("/my_queue");Makefile & Build Vertieft
Automatische Variablen & Patterns
Automatische Variablen machen Makefiles prägnant und wartbar. Pattern Rules (%.o: %.c) definieren, wie jede Datei erstellt wird, die auf ein Pattern passt. -MM generiert Abhängigkeitsdateien (.d), die Header-Abhängigkeiten verfolgen, sodass das Bearbeiten eines Headers die Neukompilierung abhängiger .c-Dateien auslöst. Die -include-Direktive bindet Abhängigkeitsdateien ein, falls vorhanden. Dies ist die Grundlage robuster C/C++-Build-Systeme.
# Automatic variables:
# $@ = target name
# $< = first prerequisite
# $^ = all prerequisites
# $? = prerequisites newer than target
# Pattern rule: compiles any .c to .o
%.o: %.c %.h
$(CC) $(CFLAGS) -c $< -o $@
OBJS = main.o utils.o parser.o
program: $(OBJS)
$(CC) $(LDFLAGS) $^ -o $@ $(LDLIBS)
# Automatic dependency generation
%.d: %.c
@$(CC) -MM $< > $@
-include $(OBJS:.o=.d)
clean:
rm -f $(OBJS) programVariablen & Bedingte
Verwenden Sie := für sofortige Auswertung (schneller, vorhersagbar) und = für träge Auswertung (erlaubt Vorwärtsreferenzen). ?= setzt eine Variable nur, wenn nicht gesetzt, und erlaubt Benutzer-Overrides von der Kommandozeile. Bedingte (ifeq, ifdef) ermöglichen Debug/Release-Builds. Der Q-Trick schaltet Befehls-Echo stumm, außer VERBOSE ist gesetzt. MAKECMDGOALS enthält die Ziele von der Kommandozeile.
# = recursive (evaluated when used)
# := simple (evaluated when defined)
# ?= conditional (only if not already set)
CC := gcc
CFLAGS ?= -Wall -Wextra -O2
DEBUG := $(filter debug,$(MAKECMDGOALS))
ifeq ($(DEBUG),debug)
CFLAGS += -g -DDEBUG -O0
else
CFLAGS += -DNDEBUG
endif
ifdef VERBOSE
Q =
else
Q = @
endif
build:
$(Q)$(CC) $(CFLAGS) -c main.cFunktionen & Textverarbeitung
Make-Funktionen ermöglichen Texttransformation: patsubst für Pattern-Ersetzung, filter zum Auswählen von Dateien, wildcard für Globbing, foreach für Iteration. Die shell-Funktion führt Befehle zur Parse-Zeit aus—nützlich zum Einbetten von Versionsinformationen. Substitutionsreferenzen ($(VAR:.c=.o)) sind eine prägnante Alternative zu patsubst für einfache Suffix-Änderungen.
SRCS = main.c utils.c parser.c
# Substitution
OBJS = $(patsubst %.c,%.o,$(SRCS)) # main.o utils.o parser.o
# Or shorthand:
OBJS = $(SRCS:.c=.o)
# Filter files by extension
C_SRCS = $(filter %.c,$(wildcard *.c))
# Add prefix
FLAGS = $(addprefix -I,$(INCLUDE_DIRS))
# Foreach
DIRS = src lib include
CREATE = $(foreach dir,$(DIRS),mkdir -p $(dir);)
# Shell function
GIT_HASH = $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
CFLAGS += -DGIT_HASH=\"$(GIT_HASH)\"Unterverzeichnisse & rekursives Make
Rekursives Make (Unterverzeichnis-Makefiles) ist traditionell, kann aber langsam und fehleranfällig mit parallelen Builds sein. Der nicht-rekursive Ansatz (einzelnes Makefile mit vpath) wird für Korrektheit und Geschwindigkeit bevorzugt. Wenn Sie rekursives Make verwenden, übergeben Sie Variablen explizit und verwenden Sie MAKECMDGOALS, um Ziele weiterzugeben. Für große Projekte ziehen Sie CMake oder Meson statt rohem Make in Betracht für bessere Abhängigkeitsverfolgung und IDE-Unterstützung.
SUBDIRS = lib src tests
.PHONY: all $(SUBDIRS) clean test
all: $(SUBDIRS)
# Pass variables to sub-makes
$(SUBDIRS):
$(MAKE) -C $@ $(MAKECMDGOALS)
# Parallel build: make -j4
clean: $(SUBDIRS)
rm -f *.o program
# Non-recursive alternative (single Makefile)
vpath %.c src:lib
vpath %.h include
CFLAGS += -Iinclude -Ilib
program: main.o lib/utils.o
$(CC) $^ -o $@CMake-Integration
CMake generiert Makefiles (oder Ninja, VS, Xcode-Projekte) aus einer deklarativen CMakeLists.txt. Modernes CMake verwendet zielbasierte Befehle (target_include_directories, target_link_libraries) statt globaler Variablen. Generator-Ausdrücke ($<$<CONFIG:Debug>:...) ermöglichen pro-Konfigurations-Flags. CMake ist der De-facto-Standard für C/C++-Projekte mit besserer IDE-Integration und plattformübergreifender Unterstützung als rohe Makefiles.
# CMakeLists.txt (modern CMake 3.15+)
cmake_minimum_required(VERSION 3.15)
project(MyApp C)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
add_executable(myapp src/main.c src/utils.c)
target_include_directories(myapp PRIVATE include)
target_compile_options(myapp PRIVATE -Wall -Wextra)
# Per-configuration flags
target_compile_options(myapp PRIVATE
$<$<CONFIG:Debug>:-g -O0 -DDEBUG>
$<$<CONFIG:Release>:-O2 -DNDEBUG>
)
# Find and link library
find_package(MATH REQUIRED)
target_link_libraries(myapp PRIVATE m)
install(TARGETS myapp DESTINATION bin)Debuggen mit GDB
Start & Breakpoints
Kompilieren Sie mit -g, um Debug-Symbole einzubetten, und -O0, um Optimierungen zu deaktivieren (sonst werden Variablen möglicherweise wegoptimiert). break setzt Breakpoints bei Funktionen, Zeilen oder Bedingungen. watch (Daten-Breakpoints) lösen aus, wenn sich eine Variable ändert—mächtig zum Finden von Speicherkorruption. Bedingte Breakpoints (break func if cond) feuern nur, wenn die Bedingung wahr ist, nützlich für Schleifen. tbreak ist ein einmaliger Breakpoint.
# Compile with debug symbols
gcc -g -O0 -o program program.c
# Start GDB
gdb ./program
gdb --args ./program arg1 arg2
# Breakpoints
(gdb) break main # Function
(gdb) break 42 # Line 42
(gdb) break file.c:50 # Specific file
(gdb) break func if x > 10 # Conditional
(gdb) tbreak main # Temporary (removed after hit)
(gdb) watch x # Break when x changes
# Manage breakpoints
(gdb) info breakpoints
(gdb) delete 2 # Delete breakpoint #2
(gdb) disable 1 # Temporarily disable
(gdb) enable 1Schrittweise & Inspektion
next springt über Funktionsaufrufe; step tritt in sie ein. finish läuft bis zum Ende der aktuellen Funktion. print-Formate: /x (hex), /c (char), /s (string), /t (binär). Der @-Operator druckt Array-Slices: arr@5 zeigt 5 Elemente. display druckt Variablen bei jedem Stop automatisch. backtrace zeigt den Call-Stack; frame N schaltet den Kontext, um diesen Frame zu inspizieren.
(gdb) run # Start program
(gdb) continue # Continue to next breakpoint
(gdb) next # Step over (no function entry)
(gdb) step # Step into (enters functions)
(gdb) finish # Run until function returns
(gdb) until 50 # Run until line 50
# Inspect variables
(gdb) print x # Print variable
(gdb) print *ptr # Dereference pointer
(gdb) print arr[0]@5 # First 5 elements
(gdb) print/x x # Print in hex
(gdb) display x # Auto-print at each stop
(gdb) info locals # All local variables
# Backtrace
(gdb) backtrace # Call stack
(gdb) frame 2 # Switch to frame #2
(gdb) up / down # Navigate stackSpeicher untersuchen
Der examine (x)-Befehl inspiziert rohen Speicher. Das Format gibt Anzahl, Anzeigeformat und Einheitsgröße an. x/10i $pc disassembliert 10 Anweisungen ab dem Programmzähler. x/s behandelt Speicher als null-terminierten String. info proc mappings zeigt das virtuelle Speicherlayout (text, data, heap, stack, Shared Libraries). Dies ist essenziell zum Debuggen von Pufferüberläufen und Speicherkorruption.
# x/[count][format][size] address
(gdb) x/10x &arr # 10 words in hex
(gdb) x/20cb &str # 20 bytes as chars
(gdb) x/4xw ptr # 4 words in hex
(gdb) x/s str # String
# Format: x(hex) d(decimal) u(unsigned) o(octal)
# t(binary) f(float) a(addr) i(instr) c(char) s(string)
# Size: b(byte) h(half/2) w(word/4) g(giant/8)
# Disassemble
(gdb) disassemble main
(gdb) disassemble /r main # With raw bytes
# Memory map
(gdb) info proc mappings # Process memory layoutCore Dumps
Core Dumps erfassen den Prozesszustand zum Absturzzeitpunkt für Post-Mortem-Debugging. Aktivieren Sie sie mit ulimit -c unlimited. Laden Sie die Core-Datei mit gdb program core. Der backtrace zeigt, wo der Absturz auftrat; info locals zeigt Variablenwerte. Für multithreaded Programme zeigt thread apply all bt alle Thread-Zustände—essenziell für Deadlock-Analyse.
# Enable core dumps
ulimit -c unlimited
echo "core.%p" > /proc/sys/kernel/core_pattern
# Run program until it crashes
./program # Produces core.12345
# Analyze core dump
gdb ./program core.12345
(gdb) bt # Backtrace at crash
(gdb) bt full # With local variables
(gdb) frame 0 # Top frame (crash location)
(gdb) info locals # Variables at crash
(gdb) print *ptr # What was the pointer?
# Multithreaded
(gdb) info threads # List all threads
(gdb) thread 3 # Switch to thread 3
(gdb) thread apply all bt # Backtraces for ALL threadsGDB-Skripte & Automatisierung
.gdbinit automatisiert häufige Einstellungen beim Start. define erstellt benutzerdefinierte Befehle für wiederkehrende Aufgaben. commands hängt Aktionen an Breakpoints (z.B. eine Variable loggen und fortfahren). GDB unterstützt Python-Scripting für komplexe Analyse: Testläufe automatisieren, Datenstrukturen visualisieren oder Statistiken extrahieren. Python-Skripte können über das gdb-Modul auf GDB-Internas zugreifen. Verwenden Sie Skripte, um Debugging-Workflows teamübergreifend zu standardisieren.
# .gdbinit file (loaded on startup)
set pagination off
set print pretty on
set print element 0 # No limit on string display
# Define custom commands
define print_array
set $i = 0
while $i < $arg0
printf "[%d] = %d\n", $i, $arg1[$i]
set $i = $i + 1
end
end
# Usage: print_array 10 my_array
# Commands attached to breakpoints
break main
commands 1
silent
printf "x = %d\n", x
continue
end
# Python scripting (GDB 7+)
python
import gdb
gdb.execute("break main")
gdb.execute("run")
val = gdb.parse_and_eval("x")
print(f"x = {int(val)}")
endSpeicher-Alignment & Bitfelder
Struct-Alignment & Padding
Der Compiler fügt Padding ein, sodass jedes Member natürlich aligned ist (typischerweise nach seiner Größe: char=1, short=2, int=4, double=8). Das Neuordnen von Membern von der größten zur kleinsten minimiert Padding. Verwenden Sie offsetof, um das Layout zu inspizieren. Auf 64-Bit-Systemen benötigen Zeiger 8-Byte-Alignment. Übermäßiges Padding verschwendet Speicher und verschlechtert Cache-Performance. Ordnen Sie Struct-Member immer nach absteigender Größe.
#include <stddef.h>
// Without alignment consideration (padded)
struct Bad {
char a; // 1 byte + 7 padding
double b; // 8 bytes
char c; // 1 byte + 7 padding
}; // sizeof = 24
// Reordered for efficiency
struct Good {
double b; // 8 bytes
char a; // 1 byte
char c; // 1 byte + 6 padding
}; // sizeof = 16
printf("Bad: a=%zu b=%zu c=%zu total=%zu\n",
offsetof(struct Bad, a),
offsetof(struct Bad, b),
offsetof(struct Bad, c),
sizeof(struct Bad));Alignment kontrollieren
C11 alignas gibt das minimale Alignment für Typen oder Variablen an—nützlich für SIMD (16/32-Byte-Alignment) und DMA. Gepackte Structs (__attribute__((packed)) oder #pragma pack) entfernen alles Padding, sparen Platz, können aber den Zugriff verlangsamen (unaligned Speicherzugriff kann auf einigen Architekturen fehlschlagen). Verwenden Sie packed für Netzwerkprotokolle und Dateiformate, wo exaktes Layout wichtig ist. Packen Sie niemals Structs, die schnellen Zugriff benötigen.
#include <stdalign.h>
// C11 explicit alignment
struct alignas(16) Aligned16 {
int data[4]; // 16 bytes, 16-byte aligned
};
// Check alignment
printf("Alignment of int: %zu\n", alignof(int)); // 4
printf("Alignment of struct: %zu\n",
alignof(struct Aligned16)); // 16
// Packed struct (no padding) - GCC/Clang
struct __attribute__((packed)) Packed {
char a;
int b; // No padding after a
char c;
}; // sizeof = 6
// MSVC packed
#pragma pack(push, 1)
struct PackedMSVC { char a; int b; char c; };
#pragma pack(pop)Flexible Array-Member
Flexible Array-Member (C99) erlauben einem Struct, ein variabel langes Array als letztes Member zu haben. Reservieren Sie mit malloc(sizeof(struct) + gewünschte_länge). Das Array teilt sich die einzelne Reservierung, sodass ein free alles freigibt. Dies ist effizienter und sauberer als ein separater Zeiger + malloc. Häufig in dynamischen Arrays, Strings und Netzwerk-Paket-Headern. sizeof(struct) schließt das flexible Array aus.
#include <stdlib.h>
// C99 flexible array member (last member with no size)
struct Buffer {
size_t size;
char data[]; // Flexible array
};
// Allocate with space for data
size_t data_len = 1024;
struct Buffer *buf = malloc(sizeof(struct Buffer) + data_len);
buf->size = data_len;
memset(buf->data, 0, data_len);
// Use data
strcpy(buf->data, "Hello");
// Single free (no separate allocation for data)
free(buf);Union Type Punning
Unions legen Member über denselben Speicher und ermöglichen Type Punning (Bits als anderen Typ neu interpretieren). Das Lesen eines Union-Members anders als des zuletzt geschriebenen ist in C erlaubt (implementierungsdefiniert). Union Type Punning ist unter Strict Aliasing legal, im Gegensatz zu Zeiger-Casting. Anonyme Unions (C11) exponieren Member direkt ohne Member-Namen. Verwenden Sie Unions für getaggte Varianten und den Zugriff auf IEEE-754-Float-Internas.
#include <stdio.h>
// Union: members share the same memory
union Data {
int i;
float f;
char bytes[4];
};
int main() {
union Data d;
d.i = 0x41424344;
printf("As int: %d\n", d.i);
printf("As float: %f\n", d.f);
printf("As bytes: %02x %02x %02x %02x\n",
d.bytes[0], d.bytes[1], d.bytes[2], d.bytes[3]);
// Safe type punning (legal in C)
union { float f; int i; } u;
u.f = 3.14f;
printf("Float bits: 0x%08x\n", u.i);
// Anonymous unions (C11)
struct Variant {
int type;
union { int i; float f; char *s; };
};
}Speicherlayout & Endianness
Endianness bestimmt die Byte-Reihenfolge im Speicher: Little-Endian (x86, ARM-Standard) speichert LSB zuerst; Big-Endian speichert MSB zuerst. Beim Schreiben portabler binärer Formate serialisieren Sie mit expliziten Shifts statt memcpy. Verwenden Sie dump_hex, um rohen Speicher beim Debuggen zu inspizieren. Netzwerkprotokolle verwenden Big-Endian (Network-Byte-Order); verwenden Sie htonl/ntohl für portablen Code. Testen Sie immer auf beiden Endianness, wenn Sie portable binäre I/O schreiben.
#include <stdint.h>
void dump_hex(void *ptr, size_t len) {
unsigned char *bytes = ptr;
for (size_t i = 0; i < len; i++) {
printf("%02x ", bytes[i]);
if ((i + 1) % 16 == 0) printf("\n");
}
}
int main() {
uint32_t val = 0x12345678;
printf("Value: 0x%08x\n", val);
printf("Bytes: ");
dump_hex(&val, sizeof(val));
// Little-endian: 78 56 34 12 (LSB first)
// Big-endian: 12 34 56 78 (MSB first)
// Serialize to big-endian (network order)
unsigned char be[4];
be[0] = (val >> 24) & 0xFF;
be[1] = (val >> 16) & 0xFF;
be[2] = (val >> 8) & 0xFF;
be[3] = val & 0xFF;
// Deserialize from big-endian
uint32_t recovered = (be[0]<<24)|(be[1]<<16)|(be[2]<<8)|be[3];
printf("Recovered: 0x%08x\n", recovered);
return 0;
}Variadische Funktionen Vertieft
va_list-Grundlagen
Variadische Funktionen verwenden va_list, um auf variable Argumente zuzugreifen. va_start initialisiert die Liste mit dem letzten benannten Parameter. va_arg holt das nächste Argument mit dem angegebenen Typ. va_end räumt auf. Der Aufrufer muss Anzahl und Typen mitteilen (z.B. verwendet printf Formatbezeichner). Variadische Funktionen haben keine Typsicherheit—nicht übereinstimmende Typen verursachen undefiniertes Verhalten.
#include <stdarg.h>
// Variadic function: variable number of arguments
int sum(int count, ...) {
va_list args;
va_start(args, count); // Initialize with last named param
int total = 0;
for (int i = 0; i < count; i++) {
total += va_arg(args, int); // Get next argument
}
va_end(args); // Cleanup
return total;
}
// Usage
int result = sum(3, 10, 20, 30); // 60
int result2 = sum(5, 1, 2, 3, 4, 5); // 15Sentinel-terminierte Variadics
Ein Sentinel-Wert (oft NULL) markiert das Ende der Argumentliste und eliminiert die Notwendigkeit eines Zählparameters. Dies ist häufig in C-APIs wie execl. GCC __attribute__((sentinel)) warnt, wenn das letzte Argument nicht NULL ist. Dokumentieren Sie immer den erwarteten Sentinel. Der Nachteil ist, dass der Sentinel nicht als gültiger Datenwert erscheinen kann.
#include <stdarg.h>
// Sentinel value marks the end
void log_messages(const char *first, ...) {
va_list args;
va_start(args, first);
const char *msg = first;
while (msg != NULL) { // NULL is the sentinel
printf("%s\n", msg);
msg = va_arg(args, const char *);
}
va_end(args);
}
// Usage: NULL terminates the list
log_messages("Error 1", "Error 2", "Error 3", NULL);
// GCC attribute to enforce sentinel
void log_messages(const char *first, ...)
__attribute__((sentinel));vfprintf & Format-Strings
vprintf/vfprintf/vsnprintf akzeptieren ein va_list statt ..., was benutzerdefinierte printf-ähnliche Funktionen ermöglicht. Verwenden Sie immer vsnprintf (begrenzt) statt vsprintf, um Pufferüberläufe zu verhindern. Leiten Sie das va_list direkt weiter. Dieses Muster wird in Logging-Bibliotheken, Fehlerberichten und benutzerdefinierten Formatierern verwendet. Die Format-String-Schwachstelle (benutzergesteuertes Format) ist ein Sicherheitsrisiko—übergeben Sie niemals Benutzereingaben als Format.
#include <stdarg.h>
#include <stdio.h>
// Custom printf-like function
void logf(const char *format, ...) {
va_list args;
va_start(args, format);
// Add timestamp prefix
printf("[LOG] ");
vprintf(format, args); // Pass va_list to vprintf
printf("\n");
va_end(args);
}
// Write to string with vsnprintf
void format_msg(char *buf, size_t size, const char *fmt, ...) {
va_list args;
va_start(args, fmt);
vsnprintf(buf, size, fmt, args); // Safe: bounded
va_end(args);
}
// Usage
logf("User %s logged in (id=%d)", username, id);
char msg[256];
format_msg(msg, sizeof(msg), "Error %d: %s", code, text);Funktionszeiger & Callbacks
Funktionszeiger ermöglichen Callbacks und Polymorphismus in C. Die Syntax return_type (*name)(params) deklariert einen Zeiger auf eine Funktion. qsort verwendet einen Vergleichs-Callback für generisches Sortieren. Arrays von Funktionszeigern implementieren Dispatch-Tabellen (Alternative zu switch). Stellen Sie immer sicher, dass die Callback-Signatur exakt übereinstimmt. Funktionszeiger sind die Grundlage von Event-Handlern, Plugins und dem Strategy-Pattern in C.
#include <stdlib.h>
// Function pointer type
typedef int (*compare_fn)(const void *, const void *);
// Comparison function for qsort
int cmp_int(const void *a, const void *b) {
return *(const int *)a - *(const int *)b;
}
int main() {
int arr[] = {5, 2, 8, 1, 9, 3};
size_t n = sizeof(arr) / sizeof(arr[0]);
// qsort takes a function pointer
qsort(arr, n, sizeof(int), cmp_int);
// Array of function pointers
double (*ops[])(double, double) = {
add, subtract, multiply, divide
};
double result = ops[2](10.0, 3.0); // multiply
// Function pointer as parameter
void apply(int *arr, size_t n, int (*fn)(int)) {
for (size_t i = 0; i < n; i++) arr[i] = fn(arr[i]);
}
apply(arr, n, square);
return 0;
}Variadische Makros
Variadische Makros (__VA_ARGS__) akzeptieren variable Argumente, nützlich für Logging und Debugging. __VA_OPT__ (C2x) behandelt den Null-Argument-Fall durch bedingtes Einfügen des Kommas. Die ##__VA_ARGS__ GCC-Erweiterung entfernt das vorangehende Komma, wenn keine Argumente übergeben werden. Debug-Makros, die in Release-Builds zu nichts kompilieren, eliminieren Overhead ohne Codeänderungen. Schützen Sie Format-Strings immer, um Format-String-Angriffe zu verhindern.
// C99 variadic macros
#define LOG(fmt, ...) printf("[LOG] " fmt "\n", __VA_ARGS__)
// Usage
LOG("Value: %d", x);
LOG("User %s, age %d", name, age);
// GCC __VA_OPT__ (C99/C2x): handle zero arguments
#define LOG2(fmt, ...) printf("[LOG] " fmt "\n" __VA_OPT__(,) __VA_ARGS__)
// Count arguments (GCC extension)
#define COUNT(...) NARG_(__VA_ARGS__, 5, 4, 3, 2, 1)
#define NARG_(_1, _2, _3, _4, _5, N, ...) N
// Debug macro that compiles out in release
#ifdef NDEBUG
#define DEBUG(fmt, ...) ((void)0)
#else
#define DEBUG(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__)
#endif
// ## removes comma if no variadic args (GCC extension)
DEBUG("just a message");
DEBUG("value = %d", x);Verwandte C-Snippets
Copy-paste ready code for common tasks.
Pointer Basics
Declare pointers, dereference, and walk an array with pointer arithmetic.
Memory Management
Allocate, resize, and free heap memory with malloc, realloc, and free.
String Operations
Use string.h helpers for length, copy, concat, compare, and tokenize.
File I/O
Open, read, write, and close files using the stdio FILE API.
Structs
Group related fields with typedef and pass by pointer for mutation.
Function Pointers
Store function addresses for callbacks and dispatch tables.
Preprocessor Macros
Define object-like and function-like macros with conditional compilation.
Bit Operations
Set, clear, toggle, and test bits with bitwise operators and flags.
Was this helpful?