Démarrage
Hello World
Chaque programme C démarre dans main(). #include <stdio.h> apporte la bibliothèque d'E/S standard (printf, scanf). main retourne 0 en cas de succès, non-zéro en cas d'échec. Le mot-clé void déclare explicitement que main ne prend pas de paramètres.
#include <stdio.h>
int main(void) {
printf("Hello, World!\n");
return 0;
}Variables et types
C est typé statiquement. Types courants : int, float, double, char. float a besoin du suffixe f. char[] est une chaîne (tableau terminé par null). long et short sont des modificateurs de taille. unsigned signifie non négatif. Les tailles varient par plateforme ; utilisez <stdint.h> pour des largeurs fixes.
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);Entrée et sortie
scanf a besoin de l'adresse (&) de la variable pour stocker l'entrée. Limitez toujours la longueur d'entrée de chaîne (%49s pour un tampon de 50 caractères) pour empêcher le débordement de tampon. scanf arrête de lire les chaînes à l'espace blanc ; utilisez fgets pour les lignes complètes.
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);Spécificateurs de format printf
Les spécificateurs de format contrôlent la sortie : %d int, %f float, %c char, %s chaîne, %x hex, %p pointeur. La largeur et la précision (par ex. %5.2f) contrôlent l'alignement et les décimales. L'inadéquation entre spécificateur et type cause un comportement indéfini.
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éprocesseur et en-têtes
Le préprocesseur s'exécute avant la compilation. #include colle les fichiers d'en-tête, #define crée des macros et constantes. Utilisez toujours des include guards (#ifndef/#define/#endif) dans les en-têtes pour empêcher la double inclusion. Les macros sont de la substitution de texte — utilisez des parenthèses autour des paramètres.
#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
#endifChaînes et String.h
Bases des chaînes
Les chaînes C sont des tableaux de char terminés par null. strlen compte les caractères avant '\0' ; sizeof retourne la taille du tampon. strcpy copie jusqu'au terminateur null — assurez-vous toujours que la destination est assez grande pour éviter le débordement.
#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); // HelloConcaténation et comparaison
strcat ajoute (la destination doit avoir de la place). strcmp compare lexicographiquement : retourne 0 si égal, négatif si premier < second, positif si premier > second. N'utilisez jamais == pour comparer des chaînes (cela compare des pointeurs, pas du contenu).
#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 et sscanf
sprintf formate dans un tampon de chaîne (comme printf mais vers une chaîne). sscanf analyse une chaîne dans des variables (comme scanf mais depuis une chaîne). Utilisez snprintf au lieu de sprintf pour empêcher le débordement de tampon en spécifiant la taille max.
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 et strtok
strchr trouve un caractère, strstr trouve une sous-chaîne. strtok divise une chaîne par délimiteurs mais modifie la chaîne originale (insère des terminateurs null) et n'est pas thread-safe — passez NULL sur les appels suivants pour continuer la tokenisation.
#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 et entrée sûre
fgets est la façon sûre de lire des chaînes — il prend une limite de taille pour empêcher le débordement. Contrairement à scanf, il lit les espaces. Le newline est inclus dans le résultat ; strcspn le trouve et le supprime. Préférez toujours fgets à gets (qui est retiré depuis C11).
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);Nombres et maths
Types entiers et limites
Utilisez <stdint.h> pour les types à largeur fixe (int32_t, int64_t) quand la taille exacte compte. <limits.h> fournit INT_MAX, INT_MIN, etc. pour les limites spécifiques à la plateforme. Le suffixe LL marque les littéraux long long. Les tailles de int/long varient par plateforme.
#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); // 4294967295Virgule flottante
float fait 4 octets (6-7 chiffres de précision), double fait 8 octets (15-16 chiffres). Ne comparez jamais les flottants avec == à cause des erreurs d'arrondi — utilisez fabs(a - b) < epsilon. <float.h> fournit DBL_MAX, DBL_EPSILON pour les limites et la précision.
#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
}Fonctions mathématiques
<math.h> fournit les fonctions math standard. pow et sqrt retournent double. fabs est la version float de abs (abs est pour int). Liez avec -lm sur certains systèmes. Pour les calculs financiers, évitez la virgule flottante — utilisez des centimes entiers à la place.
#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)Nombres aléatoires
rand() retourne un int pseudo-aléatoire de 0 à RAND_MAX. Semez avec srand() une fois au démarrage du programme (en utilisant time(NULL)). rand() % N a un biais de modulo et une faible qualité ; pour un usage sérieux, lisez /dev/urandom ou utilisez une bibliothèque PRNG tierce.
#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.0Conversion de type et cast
Le cast est (type)valeur. La division entière tronque — utilisez un opérande flottant pour obtenir un résultat flottant. atoi/atof convertissent les chaînes en nombres mais ne font aucune vérification d'erreur ; préférez strtol/strtod qui rapportent les erreurs d'analyse via errno.
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");Flux de contrôle
If / Else
if/else if/else est la conditionnelle standard. C traite 0 comme faux et toute valeur non-zéro comme vrai. Utilisez des accolades même pour les instructions uniques pour empêcher les bugs lors de l'ajout de lignes plus tard. Il n'y a pas de type booléen en C89 ; C99 ajoute _Bool et <stdbool.h>.
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 saute vers un label de case correspondant. Utilisez toujours break pour empêcher le fall-through (les cas 6 et 7 partagent du code intentionnellement). default gère les valeurs non correspondantes. switch ne fonctionne que sur les types entier et char, pas les chaînes ou flottants.
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");
}Boucle for
La boucle for a init ; condition ; update. sizeof(nums)/sizeof(nums[0]) calcule la longueur du tableau à la compilation. Déclarer i à l'intérieur de la boucle for nécessite C99 ou ultérieur. Le corps de la boucle s'exécute zéro fois si la condition est initialement fausse.
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 et do-while
while vérifie avant d'exécuter (peut tourner zéro fois). do-while exécute le corps d'abord, puis vérifie (tourne au moins une fois). do-while est idéal pour la validation d'entrée et les boucles de menu où le prompt doit apparaître avant que la condition puisse être vérifiée.
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 et goto
break sort de la boucle/switch la plus proche ; continue saute à l'itération suivante. C n'a pas de break étiqueté, donc goto est la façon idiomatique de sortir de boucles profondément imbriquées. goto est autrement déconseillé mais acceptable pour les motifs de nettoyage et les sorties de boucles imbriquées.
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");Fonctions
Définir et appeler
Les fonctions doivent être déclarées (prototype) ou définies avant usage. Le type de retour void signifie pas de valeur de retour. const char *name signifie que la fonction ne modifiera pas la chaîne. C passe les arguments par valeur ; utilisez des pointeurs pour simuler le passage par référence.
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;
}Récursion
La récursion s'appelle elle-même avec une entrée plus petite. Toute fonction récursive a besoin d'un cas de base pour s'arrêter. Le fib naïf ci-dessus est O(2^n) — exponentiel. Utilisez la memoization ou l'itération pour l'efficacité. La récursion profonde peut déborder la pile d'appels.
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) == 120Pointeurs de fonction
Les pointeurs de fonction stockent l'adresse d'une fonction, permettant les callbacks et la dispatch dynamique. La syntaxe int (*op)(int, int) déclare un pointeur vers une fonction prenant deux ints et retournant int. Utilisé dans qsort, les gestionnaires d'événements, et les systèmes de plugins.
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);
}Fonctions variadiques
Les fonctions variadiques acceptent un nombre variable d'arguments en utilisant <stdarg.h>. va_start initialise, va_arg récupère l'argument suivant, va_end nettoie. Vous avez besoin d'un moyen de connaître le compte (par ex., un paramètre de compte ou une valeur sentinelle). printf fonctionne de cette façon.
#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 et inline
static sur une fonction/variable la limite à l'unité de traduction courante (fichier). static sur une variable locale la fait persister à travers les appels (comme une globale mais à portée). inline suggère au compilateur d'embarquer le corps de la fonction ; les compilateurs modernes l'ignorent et décident eux-mêmes.
// 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;
}Tableaux et pointeurs
Tableaux
Les tableaux sont de taille fixe, indexés depuis zéro, et stockés de manière contiguë en mémoire. sizeof(arr)/sizeof(arr[0]) calcule la longueur mais ne fonctionne que sur de vrais tableaux, pas des pointeurs (les tableaux décroissent en pointeurs quand passés aux fonctions, perdant l'info de taille).
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]); // bananaPointeurs
Les pointeurs stockent des adresses mémoire. & obtient l'adresse, * déréférence. Initialisez toujours les pointeurs (utilisez NULL si non encore assigné). Déréférencer un pointeur NULL ou non initialisé est un comportement indéfini (généralement un crash). Vérifiez NULL avant de déréférencer.
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 */ }Arithmétique de pointeurs
L'arithmétique de pointeurs est mise à l'échelle par la taille de l'élément : p+1 va à l'élément suivant, pas à l'octet suivant. Cela rend p[i] équivalent à *(p+i). Soustraire deux pointeurs du même tableau donne le compte d'éléments. L'arithmétique de pointeurs n'est valide qu'à l'intérieur d'un tableau.
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)Tableaux vs pointeurs
Les noms de tableaux décroissent en pointeurs quand passés aux fonctions ou utilisés dans des expressions, perdant l'information de taille. C'est pourquoi vous devez passer la longueur du tableau séparément. sizeof(arr) donne la taille complète du tableau uniquement quand arr est un vrai tableau, pas un pointeur.
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)Tableaux multidimensionnels
Les tableaux 2D sont des tableaux de tableaux, stockés row-major. grid[i][j] accède à la ligne i, colonne j. Au passage aux fonctions, le nombre de colonnes doit être spécifié : void foo(int arr[][3], int rows). Pour les tableaux 2D dynamiques, utilisez des tableaux de pointeurs.
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 et unions
Structs
Les structs regroupent des variables apparentées de types différents. Les membres sont accédés avec l'opérateur point (.). Initialisez avec la notation accolade. Les structs sont passés par valeur (copiés) ; passez par pointeur (struct Point *) pour éviter la copie et pour modifier l'original.
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 crée un alias pour un type, donc vous pouvez écrire Student au lieu de struct Student. C'est couramment utilisé avec les structs pour simplifier la syntaxe. typedef peut aussi aliaser des types de pointeurs de fonction, rendant les callbacks beaucoup plus lisibles.
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*);Pointeurs vers structs
Quand vous avez un pointeur vers un struct, utilisez l'opérateur flèche (->) pour accéder aux membres. ptr->x est un raccourci pour (*ptr).x. Passez des pointeurs de struct aux fonctions pour l'efficacité (évite de copier de grands structs) et pour permettre la modification.
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
Les unions superposent plusieurs types sur la même mémoire — un seul membre est valide à la fois. Définir un membre écrase les autres. Utile pour le type punning (réinterpréter des bits) et économiser de la mémoire quand un seul de plusieurs types est nécessaire à la fois.
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 fields et enums
Les bit fields packent plusieurs petits champs dans un seul int, économisant de la mémoire (courant dans les protocoles et registres matériels). Les enums définissent des constantes entières nommées (0, 1, 2... par défaut). Utilisez les enums au lieu de #define pour un meilleur débogage et une meilleure sécurité de type.
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); // 1Gestion de mémoire
malloc et free
malloc alloue de la mémoire heap et retourne un pointeur void (ou NULL en cas d'échec). Vérifiez toujours NULL. Chaque malloc doit être apparié avec un free pour éviter les fuites de mémoire. Définir le pointeur à NULL après free empêche les bugs use-after-free.
#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 et realloc
calloc alloue et met à zéro la mémoire (plus sûr que malloc qui a des déchets). realloc redimensionne : il peut déplacer le bloc, retournant un nouveau pointeur. Si realloc échoue, il retourne NULL mais le bloc original est encore valide — utilisez un pointeur temp pour éviter les fuites.
#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
La mémoire stack est automatique (allouée/libérée avec les appels de fonction) et rapide mais limitée (souvent 1-8 Mo). La mémoire heap est gérée manuellement via malloc/free, beaucoup plus grande, mais plus lente et sujette aux fuites. Utilisez stack pour les petites données éphémères ; heap pour les grandes ou longues données.
// 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 freedFuites de mémoire et pointeurs pendants
Les fuites de mémoire surviennent quand vous perdez le seul pointeur vers la mémoire allouée (impossible de la libérer). Les pointeurs pendants pointent vers de la mémoire libérée — les déréférencer est un comportement indéfini. Double-free est aussi indéfini. Des outils comme Valgrind et AddressSanitizer détectent ces 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 likelyTableaux et chaînes dynamiques
L'allocation dynamique vous permet de créer des chaînes/tableaux dont la taille est déterminée à l'exécution. L'appelant est responsable de libérer la mémoire. Allouez toujours strlen+1 pour les chaînes (le terminateur null). Ce motif (allouer, retourner, l'appelant libère) est courant dans les APIs C.
#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);E/S fichiers
fopen et fclose
fopen ouvre un fichier et retourne un pointeur FILE (ou NULL en cas d'échec). Modes : r (lecture), w (écriture/troncature), a (ajout), r+ (lecture/écriture), b (binaire). Vérifiez toujours NULL. fclose vide les tampons et ferme le fichier. fgets lit une ligne sûrement.
#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 et fscanf
fprintf et fscanf fonctionnent comme printf/scanf mais sur des fichiers. fscanf est fragile — les inadéquations de format causent des problèmes. Pour un parsing robuste, lisez les lignes avec fgets puis parsez avec sscanf. Fermez toujours les fichiers quand terminé pour vider les tampons et libérer les ressources.
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 et fwrite (binaire)
fread/fwrite lisent/écrivent des octets bruts — idéal pour les données binaires et les structs. Les arguments sont : tampon, taille d'élément, compte, fichier. Les fichiers binaires sont compacts mais non portables à travers les architectures (endianness, padding de struct). Ouvrez toujours les fichiers binaires avec le mode « b ».
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 et rewind
fseek déplace la position du fichier : SEEK_SET (depuis le début), SEEK_CUR (relatif), SEEK_END (depuis la fin). ftell retourne la position courante. rewind est un raccourci pour fseek(f, 0, SEEK_SET). Ils permettent l'accès aléatoire dans les fichiers, utile pour les bases de données et les lookups indexés.
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 et flux standard
Chaque programme C a trois flux : stdin (clavier), stdout (écran), stderr (écran, non tamponné). Écrire les erreurs vers stderr les sépare de la sortie normale, permettant la redirection : program 2> errors.log. stderr est non tamponné donc les messages apparaissent avant les crashes.
#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éprocesseur et macros
Constantes et macros #define
#define crée des macros de substitution de texte. Les constantes comme PI améliorent la lisibilité et la maintenabilité. Les macros de type fonction doivent envelopper les paramètres dans des parenthèses pour éviter les bugs de précédence : SQUARE(2+3) sans parenthèses serait 2+3*2+3=11. Préférez les variables const et les fonctions inline aux macros.
#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); // 7Compilation conditionnelle
La compilation conditionnelle (#if, #ifdef, #ifndef) inclut/exclut du code à la compilation. C'est utilisé pour le code spécifique à une plateforme, les builds de débogage, et les feature flags. #ifdef vérifie si une macro est définie ; #if évalue sa valeur. #elif et #else fournissent des alternatives.
#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
Les include guards empêchent la double inclusion d'en-têtes, ce qui causerait des erreurs de redéfinition. Le motif #ifndef/#define/#endif est du C standard. #pragma once est une alternative plus simple et largement supportée (non standard mais fonctionne sur tous les compilateurs majeurs).
// 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 et indications du compilateur
#pragma fournit des directives spécifiques au compilateur. #pragma once est un include guard plus simple. #pragma pack contrôle la disposition mémoire des structs (important pour les protocoles binaires). __attribute__ (GCC/Clang) annote les fonctions pour l'optimisation, la dépréciation, et les avertissements.
#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);Stringification et token pasting
# (stringification) transforme un argument de macro en littéral de chaîne. ## (token pasting) concatène des tokens en un nouvel identifiant. Le motif à deux niveaux STR/XSTR assure que les macros sont étendues avant la stringification. Ils sont utilisés dans la génération de code et les macros de logging.
#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); // 42Opérations bit à bit
Opérateurs bit à bit de base
Les opérateurs bit à bit manipulent les bits individuels. AND (&) masque les bits (garde uniquement les bits définis), OR (|) définit les bits, XOR (^) bascule les bits, NOT (~) inverse tous les bits. Le décalage gauche (<<) multiplie par des puissances de 2, le décalage droit (>>) divise (pour les non signés). Utilisez toujours des types non signés pour la manipulation de bits — le décalage droit signé est défini par l'implémentation (peut étendre le signe). Les opérations bit à bit sont extrêmement rapides (un cycle CPU unique) et utilisées dans les flags, les registres matériels, la compression, et la cryptographie. Les littéraux binaires (préfixe 0b) sont C23/C++14 ; utilisez hex (0x) ou décimal dans les C plus anciens.
#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;
}Définir, effacer et basculer des bits
Les bit flags packent plusieurs options booléennes dans un seul entier, économisant de la mémoire. Les trois opérations principales : SET (|= mask), CLEAR (&= ~mask), TOGGLE (^= mask), CHECK (& mask). Utilisez #define avec (1 << n) pour des noms de flag lisibles. Ce motif est omniprésent en programmation système (permissions de fichiers, contrôle de périphérique, options de configuration). Par exemple, les permissions de fichiers Unix (rwxr-xr-x = 0755) utilisent des bit flags. Utilisez toujours des entiers non signés pour les flags pour éviter les problèmes d'extension de signe. C'est plus économe en mémoire qu'un tableau de bools (1 bit vs 8 bits par 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;
}Astuces de manipulation de bits
Les astuces de bits exploitent la représentation binaire pour la vitesse. x & 1 teste pair/impair (plus rapide que modulo). x & (x-1) efface le bit défini le plus bas — utile pour vérifier les puissances de 2 et compter les bits. __builtin_popcount (GCC/Clang) ou __popcnt (MSVC) comptent les bits définis en une instruction sur les CPUs modernes. L'échange XOR (a^=b; b^=a; a^=b) évite une variable temporaire mais est plus lent sur les CPUs modernes et moins lisible — à éviter. L'astuce « arrondir à la puissance de 2 supérieure » propage le bit défini le plus haut vers tous les bits inférieurs, puis ajoute 1. Ces astuces sont utiles dans les systèmes embarqués, les moteurs de jeu, et le code critique en performance.
#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 fields dans les structs
Les bit fields packent plusieurs petites valeurs dans un seul struct, économisant de la mémoire. La syntaxe deux-points (unsigned int field : N) spécifie la largeur de bits. Le compilateur gère l'extraction/insertion de bits automatiquement. C'est utile pour les systèmes à mémoire contrainte, les protocoles réseau, et le mappage de registres matériels. Cependant, la disposition des bit fields est définie par l'implémentation (ordre des octets, alignement, padding) — n'utilisez pas les bit fields pour la compatibilité binaire multiplateforme. Utilisez le masquage de bits explicite (#define + & |) pour les formats binaires portables. Les champs non nommés (: 5) ajoutent du padding. La taille totale est arrondie à l'alignement du struct.
#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;
}Manipulation de bits pratique (couleur RGB)
Packer plusieurs valeurs dans un seul entier est courant en graphisme, réseau, et systèmes embarqués. Les couleurs RGB packent trois canaux 8 bits dans 24 bits (0xRRGGBB). Le décalage gauche (<<) positionne chaque canal, OR (|) les combine. Le décalage droit (>>) et le masquage (& 0xFF) extraient les canaux individuels. Cela économise de la mémoire (1 int vs 3 octets) et permet des opérations atomiques. Le même motif s'applique à l'ordre des octets réseau, à l'accès aux registres matériels, et à la compression de données. Utilisez toujours des types à largeur fixe (uint8_t, uint32_t) pour la portabilité — la taille de int varie par plateforme.
#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;
}Gestion des signaux
Gestion de signaux de base
Les signaux sont des interruptions logicielles envoyées à un processus (par ex., Ctrl+C envoie SIGINT, division par zéro envoie SIGFPE). signal() enregistre une fonction gestionnaire. À l'intérieur d'un gestionnaire, seules les fonctions async-signal-safe sont autorisées — printf, malloc, et la plupart des fonctions stdlib ne sont PAS sûres car le programme principal peut être interrompu en plein appel. Utilisez write() pour la sortie. Signaux courants : SIGINT (Ctrl+C), SIGTERM (requête de terminaison), SIGKILL (kill forcé, ne peut pas être attrapé), SIGSEGV (segfault), SIGALRM (timer). Préférez sigaction() à signal() pour la portabilité et le contrôle.
#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 (gestion de signaux portable)
sigaction() est la façon moderne et portable de gérer les signaux (le comportement de signal() varie par plateforme). Le gestionnaire sa_sigaction reçoit un siginfo_t avec des détails : si_pid (PID émetteur), si_uid (UID émetteur), si_signo (numéro de signal), si_code (raison). Le flag SA_SIGINFO active le gestionnaire à trois arguments. sa_mask bloque les signaux spécifiés pendant l'exécution du gestionnaire (empêche les interruptions imbriquées). Autres flags : SA_RESTART (redémarre automatiquement les appels système interrompus), SA_NOCLDWAIT (pas d'enfants zombies). Utilisez toujours sigaction() dans le code de production — signal() est non fiable sur certaines plateformes.
#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;
}Envoyer des signaux et alarm
alarm(seconds) planifie un SIGALRM après le temps spécifié — utile pour les timeouts. pause() bloque jusqu'à ce qu'un signal arrive. volatile sig_atomic_t est la seule façon sûre de partager des données entre un gestionnaire de signal et le code principal — volatile empêche l'optimisation du compilateur, sig_atomic_t garantit l'accès atomique. kill(pid, signal) envoie un signal à un autre processus. raise(sig) envoie un signal à vous-même. SIGKILL (9) et SIGSTOP ne peuvent pas être attrapés ou ignorés — ils fonctionnent toujours. SIGTERM (15) est la requête de terminaison polie (les programmes peuvent l'attraper pour nettoyer). Utilisez alarm() pour des timeouts simples ; utilisez setitimer()/timer_create() pour plus de contrôle.
#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;
}Référence des signaux courants
Comprendre les signaux est essentiel pour la programmation Unix. SIGKILL (9) et SIGSTOP ne peuvent pas être attrapés — ils sont le dernier recours. SIGTERM est le signal standard d'arrêt gracieux (attrapez-le pour sauvegarder l'état). SIGINT est Ctrl+C (interruption interactive). SIGCHLD se déclenche quand un enfant sort — si vous ne wait() pas pour lui, l'enfant devient un zombie. Définir SIGCHLD à SIG_IGN auto-reap les enfants (ou utilisez SA_NOCLDWAIT). SIGPIPE se déclenche à l'écriture vers un pipe/socket fermé — la plupart des serveurs l'ignorent (signal(SIGPIPE, SIG_IGN)) et vérifient les valeurs de retour de write() à la place. Utilisez _exit() (pas exit()) dans les gestionnaires de signaux — exit() exécute les gestionnaires atexit qui peuvent ne pas être signal-safe.
#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 childrenAstuce du self-pipe (réveil signal-safe)
L'astuce du self-pipe résout un problème fondamental : les gestionnaires de signaux ne peuvent pas faire de travail complexe sûrement, mais vous devez répondre aux signaux dans votre boucle principale. La solution : le gestionnaire écrit un octet vers un pipe, et la boucle principale utilise select()/poll() pour le détecter. Cela intègre les signaux à la boucle d'événements sûrement. Le gestionnaire appelle uniquement write() (async-signal-safe). Alternatives modernes : signalfd() (spécifique à Linux, transforme les signaux en descripteurs de fichiers directement) ou pselect() (bloque atomiquement les signaux pendant select). Ce motif est utilisé dans les serveurs événementiels (nginx, Redis) pour gérer les signaux sans conditions de course.
#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;
}Fork de processus et exec
Bases de fork()
fork() crée une copie exacte du processus courant — la seule différence est la valeur de retour : 0 dans l'enfant, PID de l'enfant dans le parent. Les deux processus continuent depuis l'appel fork(). L'enfant obtient une copie de la mémoire du parent (copy-on-write optimise cela). Vérifiez toujours les trois cas : pid < 0 (erreur), pid == 0 (enfant), pid > 0 (parent). waitpid() bloque jusqu'à ce que l'enfant sorte et récupère son statut. WIFEXITED vérifie s'il est sorti normalement, WEXITSTATUS obtient le code de sortie. Si vous ne wait() pas, l'enfant devient un zombie jusqu'à être récolté.
#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;
}Famille exec (remplacer l'image du processus)
exec remplace l'image du processus courant par un nouveau programme — le PID reste le même, mais le code, les données, et la pile sont remplacés. exec ne retourne qu'en cas d'échec. La convention de nommage : « l » = liste d'arguments (variadique, terminée par NULL), « v » = vecteur/tableau d'arguments, « p » = cherche dans PATH l'exécutable, « e » = environnement personnalisé. Le premier argument est conventionnellement le nom du programme (argv[0]). fork()+exec() est la façon Unix de lancer des programmes — fork crée le processus, exec charge le nouveau programme. Cette séparation permet de configurer les descripteurs de fichiers, l'environnement, et les signaux entre fork et 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 envProcessus zombies et orphelins
Les zombies surviennent quand un enfant sort mais le parent n'a pas appelé wait() — le noyau garde l'entrée de la table des processus (PID, statut de sortie) jusqu'à être récolté. Les zombies gaspillent des PIDs et peuvent épuiser la table des processus. Solution : wait() toujours pour les enfants, ou définissez SIGCHLD à SIG_IGN (le noyau auto-reap). Les orphelins surviennent quand le parent sort avant l'enfant — init/systemd (PID 1) adopte l'orphelin et le récolte à sa sortie. Le motif double-fork (fork, l'enfant fork encore, le premier enfant sort) crée un daemon automatiquement reparenté à init, se détachant du terminal. Surveillez les zombies avec « ps aux | grep Z » ou « 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;
}Création de processus daemon
Les daemons sont des processus d'arrière-plan qui tournent sans terminal (par ex., serveurs web, bases de données). Les étapes de daemonization : fork+exit pour se détacher du shell, setsid() pour créer une nouvelle session (pas de terminal de contrôle), fork encore pour la sécurité, chdir('/') pour éviter de tenir un système de fichiers, définir umask pour des permissions de fichiers prévisibles, et fermer/rediriger stdio vers /dev/null. Le double-fork est une convention Unix pour empêcher le daemon de réacquérir un terminal via open(). Les systèmes modernes fournissent des fichiers de service systemd pour la gestion des daemons, mais comprendre la daemonization manuelle est encore important pour les systèmes embarqués et le code portable. Loggez vers des fichiers (pas stdout) puisque stdout est /dev/null.
#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;
}Vue d'ensemble de la communication inter-processus (IPC)
L'IPC laisse les processus communiquer. Les pipes sont les plus simples (parent-enfant, unidirectionnels). Les pipes nommés (FIFOs) fonctionnent entre processus non apparentés via un chemin du système de fichiers. La mémoire partagée est la plus rapide (zero-copy) mais nécessite une synchronisation (sémaphores/mutex). Les sockets sont les plus flexibles (bidirectionnels, réseau-capables). Les files de messages fournissent une communication structurée, à frontières de messages. Les signaux sont minimaux (juste un nombre). Choisissez selon vos besoins : pipes pour parent-enfant simple, mémoire partagée pour le partage de données haute performance, sockets pour la communication réseau. System V IPC (shmget, semget) est plus ancien ; POSIX IPC (shm_open, sem_open) est plus propre mais moins universellement disponible.
#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 et IPC
Pipes anonymes (parent-enfant)
Les pipes fournissent une communication unidirectionnelle entre processus apparentés (créés par fork). pipe(fd) crée deux descripteurs de fichiers : fd[0] pour lire, fd[1] pour écrire. Critique : fermez l'extrémité inutilisée dans chaque processus — le parent ferme l'extrémité de lecture, l'enfant ferme l'extrémité d'écriture. Si l'extrémité d'écriture n'est pas fermée, le read() de l'enfant bloque indéfiniment (en attente de plus de données). read() retourne 0 (EOF) uniquement quand toutes les extrémités d'écriture sont fermées. Les pipes ont un tampon fixe (typiquement 64 Ko) — write() bloque si le tampon est plein. Les pipes sont idéaux pour la communication parent-enfant et le piping de commandes shell (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;
}Pipes nommés (FIFOs)
Les pipes nommés (FIFOs) sont des pipes avec un nom du système de fichiers — ils fonctionnent entre processus non apparentés. mkfifo() crée le fichier pipe ; open() bloque jusqu'à ce qu'un lecteur et un écrivain soient connectés (synchronisation intégrée). Les FIFOs persistent jusqu'à être unlink()'és (contrairement aux pipes anonymes qui disparaissent quand les processus sortent). Ils sont utiles pour une IPC simple entre programmes séparés. Le comportement bloquant sur open() garantit que l'écrivain ne démarre pas avant qu'un lecteur soit prêt. Utilisez O_NONBLOCK pour des ouvertures non bloquantes. Les FIFOs sont unidirectionnels — pour une communication bidirectionnelle, utilisez deux FIFOs ou des sockets. Les pipes nommés sont couramment utilisés dans les scripts shell et les services système.
#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)Mémoire partagée (IPC la plus rapide)
La mémoire partagée est l'IPC la plus rapide — les deux processus mappent la même RAM physique, donc le transfert de données est zero-copy. shmget() crée un segment, shmat() l'attache à l'espace d'adressage du processus, shmdt() le détache, shmctl(IPC_RMID) le détruit. La mise en garde critique : la mémoire partagée ne fournit AUCUNE synchronisation — si les deux processus y accèdent simultanément, vous obtenez des data races. Vous DEVEZ utiliser des sémaphores, mutex, ou autre synchronisation pour coordonner l'accès. ftok() génère une clé depuis un chemin de fichier (les deux processus doivent se mettre d'accord sur la clé). Détruisez toujours la mémoire partagée quand terminé (elle persiste après que les processus sortent, fuyant de la mémoire). La mémoire partagée POSIX (shm_open/mmap) est une alternative plus propre.
#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 et redirection
dup2(oldfd, newfd) fait de newfd une copie de oldfd — c'est ainsi que fonctionne la redirection shell. Pour rediriger stdout vers un pipe : dup2(pipe_write, STDOUT_FILENO) — maintenant printf/write vers stdout va dans le pipe. Pour rediriger stdin depuis un pipe : dup2(pipe_read, STDIN_FILENO) — maintenant scanf/read depuis stdin vient du pipe. C'est exactement comment le shell implémente les pipes (ls | sort), la redirection (ls > file), et l'entrée (sort < file). Après dup2, fermez le fd original (il a été dupliqué). Ce motif est fondamental pour construire des pipelines Unix programmatiquement et est utilisé par les shells, popen(), et les bibliothèques de gestion de processus.
#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 (pipe de haut niveau)
popen() est un wrapper de haut niveau autour de fork+pipe+exec+shell — il exécute une commande via /bin/sh et retourne un FILE* pour lire la sortie (« r ») ou écrire l'entrée (« w »). C'est beaucoup plus simple que fork/pipe/exec manuel mais tourne via un shell, donc NE JAMAIS passer d'entrée non fiable (risque d'injection shell). Utilisez fgets/fprintf sur le FILE* retourné comme un fichier régulier. pclose() ferme le pipe et attend que l'enfant sorte (retourne son statut). Pour une entrée non fiable, utilisez fork+execvp directement (pas de shell). popen est parfait pour les scripts rapides, les outils d'administration système, et la lecture de sortie de commande. Pour une communication bidirectionnelle, utilisez socketpair() ou deux 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 et outils de build
Structure de Makefile de base
Make automatise la compilation. Un Makefile a des règles : target (fichier à construire), prerequisites (dépendances), et recipe (commandes shell, indentées avec TAB). Les variables (CC, CFLAGS) centralisent la configuration. Variables automatiques : $@ (nom de target), $< (premier prérequis), $^ (tous les prérequis). Les règles de motif (%.o: %.c) généralisent la compilation pour tous les fichiers source. .PHONY déclare les targets qui ne sont pas des fichiers (clean, all, install). La première règle est le défaut (make sans args construit « all »). Make suit les timestamps des fichiers — il ne reconstruit que si un prérequis est plus récent que la target. Cette construction incrémentale économise du temps sur les grands projets.
# 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)Variables automatiques et règles de motif
Les variables automatiques rendent les Makefiles concis et maintenables. $@ (target), $< (premier prérequis), et $^ (tous les prérequis) sont les plus courantes. Les règles de motif (%.o: %.c) vous laissent écrire une règle pour tous les fichiers source — le % correspond à n'importe quelle chaîne. $(wildcard) trouve les fichiers correspondant à un glob, $(patsubst) transforme les chaînes — ensemble ils découvrent automatiquement les sources. Le préfixe @ supprime l'echo de la commande. Les règles de motif statiques (target: %.o: %.c) s'appliquent à une liste spécifique. Comprendre ces fonctionnalités élimine les règles répétitives et fait que les Makefiles passent à l'échelle sur de grands projets. Utilisez toujours TAB (pas d'espaces) pour l'indentation des recipes — Make est strict à ce sujet.
# 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 directoriesDépendances et fichiers d'en-tête
Suivre les dépendances d'en-tête est crucial — sans cela, modifier un fichier .h ne déclenche pas la recompilation des fichiers .c qui l'incluent, menant à des builds périmés. La solution : gcc -MMD -MP génère des fichiers .d listant toutes les dépendances (y compris les en-têtes). -include tire ceux-ci dans le Makefile. -MP ajoute des targets factices pour les en-têtes (empêche les erreurs si un en-tête est supprimé). C'est l'approche standard pour les projets C/C++. Sans cela, vous devriez lister manuellement chaque dépendance d'en-tête — ingérable pour les grands projets. Le premier build n'aura pas de fichiers .d (le « - » dans -include supprime l'erreur) ; ils sont créés pendant la compilation et utilisés sur les builds suivants.
# 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 de projet multi-répertoires
Les vrais projets s'étendent sur plusieurs répertoires. Ce Makefile découvre automatiquement les sources (wildcard), les mappe vers un répertoire de build (patsubst), et crée les répertoires au besoin. La syntaxe | crée des prérequis order-only — $(BUILDDIR) est créé avant la compilation, mais son changement de timestamp ne déclenche pas de rebuild (sans |, créer le répertoire ferait tout rebuild à chaque fois). -Iinclude dit à gcc où trouver les en-têtes. -MMD génère les fichiers de dépendance dans le répertoire de build. Cette structure garde les répertoires source, build, et binaire séparés — facile à nettoyer (rm -rf build) et ne pollue pas l'arborescence source. Pour les très grands projets, envisagez CMake ou Meson.
# 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 changesBases de CMake (alternative à Make)
CMake est un méta-système de build — il génère des Makefiles (ou Ninja, Visual Studio, projets Xcode) depuis un fichier CMakeLists.txt. C'est le standard de facto pour les projets C/C++ car il gère la compilation multiplateforme, la détection de dépendances, et l'intégration IDE. Commandes clés : project() définit le nom du projet, add_executable() définit une target de build, target_include_directories() ajoute les chemins d'en-tête, target_link_libraries() lie les bibliothèques. Les builds out-of-source (mkdir build && cd build && cmake ..) gardent l'arborescence source propre. CMake détecte automatiquement les compilateurs et flags par plateforme. Pour les nouveaux projets C/C++, préférez CMake aux Makefiles bruts — c'est plus maintenable et portable.
# 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 projectsPlongée dans les pointeurs de fonction
Syntaxe et usage des pointeurs de fonction
Les pointeurs de fonction stockent l'adresse d'une fonction, permettant la dispatch à l'exécution. La syntaxe int (*fp)(int, int) est notoirement confuse — lisez-la comme « fp est un pointeur vers une fonction prenant (int, int) retournant int ». typedef simplifie cela : typedef int (*math_func)(int, int) crée un alias lisible. Les noms de fonction décroissent en pointeurs (comme les noms de tableau), donc « add » et « &add » sont équivalents. Les pointeurs de fonction permettent les callbacks, gestionnaires d'événements, motifs de stratégie, et tables de dispatch (tableaux de pointeurs de fonction pour une dispatch type switch). Ils sont la fondation du comparateur de qsort et des systèmes d'événements GUI.
#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;
}Rappels (exemple qsort)
qsort est l'exemple classique de pointeurs de fonction comme rappels (callbacks). Le comparateur reçoit des pointeurs const void* (génériques) et renvoie un entier indiquant l'ordre. qsort appelle votre comparateur pour décider de l'ordre des éléments — vous contrôlez le comportement de tri en passant différentes fonctions. C'est le pattern strategy en C : l'algorithme (qsort) est fixe, mais la logique de comparaison est injectée. void* permet la programmation générique (trier tout type). Le comparateur doit être une fonction pure (sans effets de bord) et cohérente (si a<b et b<c alors a<c). Ce pattern est utilisé partout dans la bibliothèque standard C (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 avec pointeurs de fonction (POO en C)
Le C peut simuler la POO en utilisant des structs avec des pointeurs de fonction — c'est ainsi que fonctionnent les vtables de C++ en interne. La struct contient un pointeur vers une 'vtable' (table de fonctions virtuelles) qui regroupe des pointeurs de fonction. Chaque 'sous-classe' (Circle, Square) a sa propre vtable avec ses implémentations. Convertir un Circle* en Shape* permet le polymorphisme — print_shape() appelle la bonne fonction area() via la vtable. Ce pattern est utilisé dans le vrai code C : le noyau Linux (pilotes de périphériques), GObject (GTK) et SQLite. Il fournit l'encapsulation, l'héritage (via l'inclusion de struct) et le polymorphisme. Bien que verbeux par rapport à C++, il donne un contrôle total sur la disposition mémoire et la distribution virtuelle.
#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));
}Programmation événementielle avec des rappels
Les pointeurs de fonction permettent l'architecture événementielle en C — le pattern publish/subscribe. Les gestionnaires s'enregistrent via on_event() (subscribe), et emit_event() appelle tous les gestionnaires enregistrés (publish). Cela découple les producteurs d'événements des consommateurs — l'émetteur ne sait pas ce que font les gestionnaires. Ce pattern est fondamental pour les frameworks GUI (clic de bouton → gestionnaire), les moteurs de jeu (collision → rappel) et les E/S asynchrones (données prêtes → gestionnaire de lecture). La signature du gestionnaire (nom d'événement + void* data) est assez générique pour tout type d'événement. En production, ajoutez la gestion d'erreurs (que faire si un gestionnaire plante ?), l'ordonnancement par priorité et la capacité de se désabonner. C'est ainsi que fonctionnent libuv, libevent et Node.js en interne.
#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;
}Pièges des pointeurs de fonction
Les pointeurs de fonction ont plusieurs pièges. Appeler un pointeur de fonction NULL provoque un crash (segfault) — vérifiez toujours NULL avant d'appeler. Convertir vers la mauvaise signature est un comportement indéfini (la convention d'appel peut différer). Comparer des pointeurs de fonction pour l'égalité est valide (même fonction), mais l'ordonnancement (<, >) est indéfini. Utilisez typedef de manière cohérente — la syntaxe des pointeurs de fonction est sujette aux erreurs, et les typedef rendent les déclarations lisibles et maintenables. En C, les pointeurs de fonction sont le seul moyen d'obtenir le polymorphisme à l'exécution et les rappels, donc les maîtriser est essentiel. C++ ajoute std::function, les lambdas et les fonctions virtuelles comme alternatives plus sûres.
#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 consistentArguments variables (varargs)
Fonctions variadiques de base (stdarg)
Les fonctions variadiques acceptent un nombre variable d'arguments en utilisant stdarg.h. va_list contient la liste d'arguments, va_start l'initialise (requiert le dernier paramètre nommé avant ...), va_arg récupère l'argument suivant avec un type spécifié, va_end fait le nettoyage. La fonction doit savoir combien d'arguments lire — soit via un paramètre de comptage (comme la chaîne de format de printf) soit via une valeur sentinelle (terminateur NULL). Le '...' doit toujours être le dernier paramètre. va_arg ne fait pas de vérification de type — passer le mauvais type est un comportement indéfini. C'est ainsi que fonctionnent printf, fprintf et 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.Implémenter un printf personnalisé
vprintf/vfprintf/vsprintf sont des assistants variadiques qui prennent une va_list au lieu de ... — ils permettent de construire des fonctions personnalisées de type printf. L'exemple log_msg enveloppe printf avec un préfixe de niveau de log. L'exemple print_values montre comment gérer les types mixtes : passez une étiquette de type avant chaque valeur, puis faites un switch sur l'étiquette pour appeler va_arg avec le bon type. Cela est nécessaire car va_arg requiert le type exact — il n'y a pas d'information de type à l'exécution. Le pattern d'étiquette de type est utilisé dans les APIs C polymorphiques (par exemple, les fonctions bind de SQLite). Faites toujours correspondre exactement les types va_arg — int vs long, float vs double (les floats sont promus en double dans les varargs).
#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;
}Fonctions variadiques terminées par sentinelle
Les fonctions variadiques terminées par sentinelle utilisent une valeur spéciale (généralement NULL) pour marquer la fin des arguments, au lieu d'un comptage. C'est plus propre pour les APIs riches en chaînes — l'appelant n'a pas besoin de compter les arguments. La famille exec (execl, execlp) utilise NULL comme sentinelle. L'inconvénient : si l'appelant oublie le NULL, la fonction lit de la mémoire invalide (comportement indéfini). Certains compilateurs (GCC) supportent __attribute__((sentinel)) pour avertir des sentinelles manquantes. Documentez toujours que NULL est requis. Le paramètre de taille de tampon prévient les débordements de tampon — passez toujours la taille de destination et vérifiez les bornes avant 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;
}Transfert d'arguments variadiques
Le transfert d'arguments variadiques requiert va_copy (pas l'affectation) — va_list peut être un type opaque qui ne peut pas être copié avec =. va_copy permet de parcourir la liste d'arguments plusieurs fois (par exemple, d'abord pour mesurer, puis pour imprimer). La macro LOG utilise __VA_ARGS__ pour transférer tous les arguments à fprintf. L'extension GCC ##__VA_ARGS__ supprime la virgule précédente lorsqu'aucun argument variadique n'est fourni (ainsi LOG("msg") fonctionne sans virgule finale). Ce pattern est omniprésent dans les macros de log C. C99 requiert au moins un argument avant ... ; C11/C23 et GCC permettent zéro. Pour des alternatives de type sûr en C++, utilisez les templates variadiques ou 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;
}Macros variadiques (C99)
Les macros variadiques C99 utilisent __VA_ARGS__ pour capturer tous les arguments correspondant au ... dans la définition de macro. ##__VA_ARGS__ (extension GCC, désormais standard en C20) supprime la virgule lorsqu'aucun argument variadique n'est passé. La macro COUNT utilise une astuce intelligente : elle mappe N arguments vers N, 5, 4, 3, 2, 1 et la Nème position donne le comptage. L'idiome do { ... } while (0) dans ASSERT fait se comporter la macro comme une seule instruction (sûr dans if/else sans accolades). # transforme les arguments de macro en chaîne. Les macros variadiques sont essentielles pour la journalisation, le débogage et la programmation générique en C. Elles sont la base de nombreuses APIs de bibliothèque et du système de journalisation du noyau Linux.
#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;
}Astuces de manipulation de bits
Astuces de bits courantes
La manipulation de bits opère directement sur les représentations binaires. n & 1 vérifie le bit le moins significatif pour pair/impair. Le décalage à gauche (<<) multiplie par 2 ; le décalage à droite (>>) divise. Le XOR swap évite une variable temporaire mais est moins lisible. n & (n-1) efface le bit défini le plus bas, utile pour les vérifications de puissance de 2 et le popcount. Ces astuces sont rapides mais privilégient la lisibilité dans le code d'application.
// 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;
}Drapeaux et masques de bits
Les drapeaux de bits regroupent plusieurs options booléennes dans un seul entier, économisant de la mémoire. Chaque drapeau est une puissance de 2 (un bit). OR (|) définit les drapeaux, AND (&) vérifie les drapeaux, XOR (^) bascule, AND NOT (&= ~) efface. Ce pattern est omniprésent en programmation système : permissions de fichiers (O_RDONLY, O_CREAT), options de socket et état du GPU. Utilisez des constantes nommées pour la lisibilité.
#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);Champs de bits dans les structs
Les champs de bits regroupent de petites valeurs dans un nombre minimal de bits au sein des structs. La syntaxe deux-points spécifie la largeur de bits. Cela économise de la mémoire pour les structures de données avec beaucoup de petits champs (dates, drapeaux, registres matériels). Cependant, la disposition des champs de bits dépend de l'implémentation : l'ordre des octets, le remplissage et l'alignement varient selon les compilateurs. Évitez les champs de bits pour les formats de données portables ; utilisez des masques de bits explicites à la place.
// 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;
}Conversion d'endianness
L'endianness détermine l'ordre des octets : little-endian (x86, ARM par défaut) stocke le LSB en premier ; big-endian (réseau, certains MIPS) stocke le MSB en premier. Les protocoles réseau utilisent le big-endian (ordre des octets réseau). Utilisez htonl/ntohl pour du code réseau portable. Les permutations d'octets manuelles avec des décalages et des masques fonctionnent sur toute plateforme. Détectez l'endianness à la compilation avec __BYTE_ORDER__ pour des chemins de code optimisés.
#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");
#endifAstuces bit à bit
Les astuces bit à bit sans branchement évitent les sauts conditionnels pour la performance dans les boucles serrées. L'astuce abs utilise le décalage arithmétique à droite pour créer un masque. next_pow2 remplit tous les bits en dessous du bit défini le plus haut, puis ajoute 1. L'inversion de bits utilise diviser pour régner : permuter les nibbles, puis les paires, puis les bits simples. Ces astuces sont utiles en cryptographie, hachage et DSP. Les CPU modernes ont souvent des instructions intégrées (POPCNT, LZCNT) qui sont plus rapides.
// 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;
}Gestion des signaux avancée
Ensembles de signaux et blocage
sigprocmask bloque les signaux afin qu'ils soient mis en file d'attente (non perdus) et délivrés plus tard. Cela protège les sections critiques contre l'interruption. SIG_BLOCK ajoute au masque, SIG_UNBLOCK retire, SIG_SETMASK remplace. Utilisez sigpending pour vérifier les signaux en file d'attente. Ne bloquez les signaux que brièvement ; un blocage long peut manquer des événements importants. Les masques de signaux sont par processus et hérités à travers fork.
#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");
}Gestionnaires de signaux sûrs
Les gestionnaires de signaux doivent être async-signal-safe : utilisez uniquement des fonctions réentrantes (write, _exit, signal). Évitez printf, malloc et la plupart des fonctions de bibliothèque — elles peuvent être interrompues en milieu d'opération et corrompre l'état. Utilisez volatile sig_atomic_t pour les drapeaux définis par le gestionnaire. SA_RESTART redémarre automatiquement les appels système interrompus. sigaction est préféré à signal pour un comportement portable et bien défini.
#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 et signalfd
signalfd (Linux) convertit les signaux en descripteurs de fichiers, les intégrant dans les boucles d'événements (epoll, select). Bloquez d'abord le signal, puis créez le signalfd. L'astuce du self-pipe est portable : le gestionnaire écrit un octet dans un pipe, et la boucle principale le lit. Les deux approches déplacent la gestion des signaux hors du contexte restreint du gestionnaire vers du code normal, où vous pouvez appeler n'importe quelle fonction en toute sécurité.
#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));
}Minuteurs et SIGALRM
setitimer délivre SIGALRM à intervalles. ITIMER_REAL utilise le temps réel ; ITIMER_VIRTUAL utilise le temps CPU ; ITIMER_PROF utilise le temps CPU + système. Le minuteur se répète jusqu'à annulation. Pour le code moderne, préférez timer_create avec SIGEV_THREAD pour des minuteurs par thread, ou utilisez un fd de minuteur dédié (timerfd_create sur Linux) pour l'intégration avec les boucles d'événements. Gérez toujours le signal pour éviter la terminaison par défaut.
#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;
}Envoi de signaux
kill envoie un signal à un processus par PID. kill(0, sig) envoie à tout le groupe de processus. raise envoie un signal au processus appelant. sigqueue envoie un signal avec des données attachées (siginfo). Utilisez toujours waitpid pour récupérer les processus enfants et vérifier le statut de sortie. WIFSIGNALED distingue la mort par signal de la sortie normale. Envoyer SIGTERM (pas SIGKILL) permet un arrêt en douceur.
#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);Gestion de processus avancée
Patterns fork & exec
Le pattern fork-exec crée un processus enfant (fork) et remplace son image par un nouveau programme (exec). fork duplique le processus ; exec charge un nouveau programme. L'enfant doit appeler _exit (pas exit) en cas d'échec d'exec pour éviter de vider les tampons du parent. waitpid bloque jusqu'à ce que l'enfant se termine. WEXITSTATUS extrait le code de sortie. C'est ainsi que les shells exécutent les commandes.
#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);Daemonisation
setsid crée une nouvelle session et un nouveau groupe de processus, se détachant du terminal de contrôle. Le pattern double-fork empêche le daemon de réacquérir un terminal. Après la daemonisation, fermez les descripteurs de fichiers standard et redirigez vers /dev/null ou les fichiers de log. chdir vers / empêche de bloquer les démontages. umask(0) assure des permissions de fichiers prévisibles. C'est le pattern standard de daemon.
#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;
}Alternative posix_spawn
posix_spawn est une alternative plus efficace à fork+exec sur les systèmes sans MMU (embarqué) ou avec de grandes empreintes mémoire (fork copie les tables de pages). Il combine la création de processus et exec en un seul appel, avec des actions de fichier (redirection, fermeture) appliquées atomiquement. Utilisez posix_spawn lorsque vous n'avez pas besoin de modifier l'état de l'enfant entre fork et exec. C'est standard POSIX et disponible sur Linux, macOS et la plupart des systèmes Unix.
#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;
}Prévention des zombies
Les processus zombies se produisent lorsqu'un enfant se termine avant que le parent n'appelle wait. Prévenez-les en : (1) gérant SIGCHLD avec waitpid dans une boucle (WNOHANG évite le blocage), (2) configurant SIGCHLD à SIG_IGN (le noyau récupère automatiquement), ou (3) double-forkant afin que le petit-enfant soit orphelin et adopté par init (PID 1), qui le récupère automatiquement. Sauvegardez et restaurez toujours errno dans les gestionnaires de signaux.
#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);Limites de ressources
setrlimit impose des limites de ressources à un processus : temps CPU (RLIMIT_CPU), mémoire virtuelle (RLIMIT_AS), taille de fichier (RLIMIT_FSIZE), fichiers ouverts (RLIMIT_NOFILE), taille de pile et taille de core dump. La limite souple est appliquée ; la limite dure est le plafond. Dépasser le temps CPU envoie SIGXCPU ; dépasser la mémoire fait échouer malloc. Utilisez les limites dans les processus enfants pour prévenir l'épuisement des ressources dû aux bugs ou aux attaques.
#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 avancés
Pipes anonymes
Les pipes anonymes fournissent une communication unidirectionnelle entre processus parent et enfant. Fermez toujours l'extrémité inutilisée : l'écrivain doit fermer l'extrémité de lecture, et vice versa. Fermer l'extrémité d'écriture signale EOF au lecteur (read renvoie 0). Les pipes ont un tampon fixe (généralement 64 Ko) ; les écritures bloquent quand il est plein. Les pipes sont uniquement pour les processus liés (parent-enfant). Pour les processus non liés, utilisez des pipes nommés (FIFOs) ou des 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;
}Pipes nommés (FIFOs)
Les pipes nommés (FIFOs) sont des fichiers spéciaux qui agissent comme des pipes entre des processus non liés. mkfifo crée le fichier ; open bloque jusqu'à ce qu'un lecteur et un écrivain soient présents. Utilisez O_NONBLOCK pour des ouvertures non bloquantes. Les FIFOs persistent dans le système de fichiers jusqu'à leur unlink. Ils sont utiles pour de l'IPC simple entre programmes indépendants, mais pour une communication complexe, envisagez les sockets de domaine Unix ou les files de messages.
#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");Mémoire partagée
La mémoire partagée est l'IPC le plus rapide : les processus mappent la même mémoire physique dans leurs espaces d'adressage. shm_open crée un objet de mémoire partagée POSIX ; mmap le mappe. Les changements sont immédiatement visibles à tous les mappeurs. Utilisez des sémaphores ou des mutex (avec PTHREAD_PROCESS_SHARED) pour la synchronisation. Démappez et unlink toujours pour éviter les fuites. La mémoire partagée est idéale pour les grandes données ; le surcoût n'est que le mappage initial.
#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");Sockets de domaine Unix
Les sockets de domaine Unix fournissent un IPC bidirectionnel orienté flux sur la même machine. Ils sont plus rapides que TCP (pas de surcoût réseau) et supportent le passage de descripteurs de fichiers entre processus via SCM_RIGHTS. Utilisez SOCK_STREAM pour des flux fiables, SOCK_DGRAM pour des datagrammes. Le chemin du socket est une entrée du système de fichiers ; unlink avant bind pour éviter les erreurs d'adresse déjà utilisée. Les sockets Unix sont la base de la communication Docker, X11 et systemd.
#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");Files de messages
Les files de messages POSIX fournissent un IPC basé sur des messages ordonnés par priorité. Chaque message a une priorité ; les messages de priorité plus élevée sont reçus en premier. mq_send et mq_receive sont atomiques pour les messages simples. Utilisez O_NONBLOCK pour un fonctionnement non bloquant ou mq_timedreceive pour des délais d'attente. Les files de messages persistent jusqu'à leur unlink, contrairement aux pipes qui meurent avec les processus. Elles sont idéales pour la distribution de tâches et la notification d'événements.
#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 avancés
Variables automatiques et patterns
Les variables automatiques rendent les Makefiles concis et maintenables. Les règles de pattern (%.o: %.c) définissent comment construire tout fichier correspondant à un pattern. -MM génère des fichiers de dépendances (.d) qui suivent les dépendances d'en-tête, afin que modifier un en-tête déclenche la recompilation des fichiers .c dépendants. La directive -include inclut silencieusement les fichiers de dépendances s'ils existent. C'est la base des systèmes de build C/C++ robustes.
# 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) programVariables et conditionnelles
Utilisez := pour l'évaluation immédiate (plus rapide, prévisible) et = pour l'évaluation paresseuse (permet les références en avant). ?= définit une variable uniquement si non définie, permettant les remplacements utilisateur depuis la ligne de commande. Les conditionnelles (ifeq, ifdef) permettent les builds debug/release. L'astuce Q silencie l'écho des commandes sauf si VERBOSE est défini. MAKECMDGOALS contient les cibles de la ligne de commande.
# = 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.cFonctions et traitement de texte
Les fonctions Make permettent la transformation de texte : patsubst pour le remplacement de pattern, filter pour sélectionner des fichiers, wildcard pour le globbing, foreach pour l'itération. La fonction shell exécute des commandes au moment de l'analyse — utile pour intégrer des informations de version. Les références de substitution ($(VAR:.c=.o)) sont une alternative concise à patsubst pour les changements simples de suffixe.
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)\"Sous-répertoires et make récursif
Le make récursif (Makefiles de sous-répertoires) est traditionnel mais peut être lent et sujet aux erreurs avec les builds parallèles. L'approche non récursive (Makefile unique avec vpath) est préférée pour la correction et la vitesse. Si vous utilisez le make récursif, passez les variables explicitement et utilisez MAKECMDGOALS pour propager les cibles. Pour les grands projets, envisagez CMake ou Meson au lieu de Make brut pour un meilleur suivi des dépendances et le support IDE.
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 $@Intégration CMake
CMake génère des Makefiles (ou projets Ninja, VS, Xcode) à partir d'un CMakeLists.txt déclaratif. Le CMake moderne utilise des commandes basées sur les cibles (target_include_directories, target_link_libraries) au lieu de variables globales. Les expressions de générateur ($<$<CONFIG:Debug>:...) permettent des drapeaux par configuration. CMake est le standard de facto pour les projets C/C++, avec une meilleure intégration IDE et un support multiplateforme que les Makefiles bruts.
# 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)Débogage avec GDB
Démarrage et points d'arrêt
Compilez avec -g pour intégrer les symboles de débogage et -O0 pour désactiver les optimisations (sinon les variables peuvent être optimisées). break définit des points d'arrêt aux fonctions, lignes ou conditions. watch (points d'arrêt de données) se déclenche quand une variable change — puissant pour trouver la corruption de mémoire. Les points d'arrêt conditionnels (break func if cond) ne se déclenchent que lorsque la condition est vraie, utile pour les boucles. tbreak est un point d'arrêt à usage unique.
# 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 1Pas à pas et inspection
next passe par-dessus les appels de fonction ; step entre dedans. finish exécute jusqu'à la fin de la fonction courante. Formats print : /x (hex), /c (char), /s (string), /t (binaire). L'opérateur @ imprime des tranches de tableau : arr@5 montre 5 éléments. display imprime automatiquement les variables à chaque arrêt. backtrace montre la pile d'appels ; frame N bascule le contexte pour inspecter cette frame.
(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 stackExamen de la mémoire
La commande examine (x) inspecte la mémoire brute. Le format spécifie le comptage, le format d'affichage et la taille d'unité. x/10i $pc désassemble 10 instructions depuis le compteur de programme. x/s traite la mémoire comme une chaîne terminée par null. info proc mappings montre la disposition de la mémoire virtuelle (text, data, heap, stack, bibliothèques partagées). C'est essentiel pour déboguer les débordements de tampon et la corruption de mémoire.
# 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
Les core dumps capturent l'état du processus au moment du crash pour le débogage post-mortem. Activez-les avec ulimit -c unlimited. Chargez le fichier core avec gdb program core. Le backtrace montre où le crash s'est produit ; info locals montre les valeurs des variables. Pour les programmes multithreadés, thread apply all bt montre tous les états de threads — essentiel pour l'analyse des interblocages.
# 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 threadsScripts GDB et automatisation
.gdbinit automatise les paramètres courants au démarrage. define crée des commandes personnalisées pour les tâches répétitives. commands attache des actions aux points d'arrêt (par exemple, journaliser une variable et continuer). GDB supporte le scripting Python pour l'analyse complexe : automatiser les exécutions de test, visualiser les structures de données ou extraire des statistiques. Les scripts Python peuvent accéder aux internals de GDB via le module gdb. Utilisez des scripts pour standardiser les flux de travail de débogage au sein d'une équipe.
# .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)}")
endAlignement mémoire et bitfields
Alignement et remplissage de struct
Le compilateur insère du remplissage afin que chaque membre soit naturellement aligné (généralement à sa taille : char=1, short=2, int=4, double=8). Réorganiser les membres du plus grand au plus petit minimise le remplissage. Utilisez offsetof pour inspecter la disposition. Sur les systèmes 64 bits, les pointeurs nécessitent un alignement sur 8 octets. Un remplissage excessif gaspille de la mémoire et nuit aux performances de cache. Ordonnez toujours les membres de struct par taille décroissante.
#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));Contrôle de l'alignement
alignas de C11 spécifie l'alignement minimal pour les types ou variables — utile pour SIMD (alignement 16/32 octets) et DMA. Les structs packed (__attribute__((packed)) ou #pragma pack) suppriment tout remplissage, économisant de l'espace mais ralentissant potentiellement l'accès (l'accès mémoire non aligné peut fauter sur certaines architectures). Utilisez packed pour les protocoles réseau et les formats de fichiers où la disposition exacte compte. Ne packez jamais les structs qui nécessitent un accès rapide.
#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)Membres de tableau flexibles
Les membres de tableau flexibles (C99) permettent à une struct d'avoir un tableau de longueur variable comme dernier membre. Allouez avec malloc(sizeof(struct) + desired_length). Le tableau partage l'allocation unique, donc un seul free libère tout. C'est plus efficace et plus propre qu'un pointeur séparé + malloc. Courant dans les tableaux dynamiques, les chaînes et les en-têtes de paquets réseau. sizeof(struct) exclut le tableau flexible.
#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);Type punning par union
Les unions superposent les membres dans la même mémoire, permettant le type punning (réinterpréter les bits comme un type différent). Lire un membre d'union autre que le dernier écrit est autorisé en C (défini par l'implémentation). Le type punning par union est légal sous l'aliasing strict, contrairement au cast de pointeur. Les unions anonymes (C11) exposent les membres directement sans nom de membre. Utilisez les unions pour les variants étiquetés et l'accès aux internals des floats IEEE 754.
#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; };
};
}Disposition mémoire et endianness
L'endianness détermine l'ordre des octets en mémoire : little-endian (x86, ARM par défaut) stocke le LSB en premier ; big-endian stocke le MSB en premier. Lors de l'écriture de formats binaires portables, sérialisez avec des décalages explicites au lieu de memcpy. Utilisez dump_hex pour inspecter la mémoire brute pendant le débogage. Les protocoles réseau utilisent le big-endian (ordre des octets réseau) ; utilisez htonl/ntohl pour du code portable. Testez toujours sur les deux endianness lors de l'écriture d'E/S binaires portables.
#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;
}Fonctions variadiques avancées
Bases de va_list
Les fonctions variadiques utilisent va_list pour accéder aux arguments variables. va_start initialise la liste avec le dernier paramètre nommé. va_arg récupère l'argument suivant avec le type spécifié. va_end fait le nettoyage. L'appelant doit communiquer le comptage et les types (par exemple, printf utilise des spécificateurs de format). Les fonctions variadiques manquent de sécurité de type — les types mismatchés causent un comportement indéfini.
#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); // 15Variadiques terminées par sentinelle
Une valeur sentinelle (souvent NULL) marque la fin de la liste d'arguments, éliminant le besoin d'un paramètre de comptage. C'est courant dans les APIs C comme execl. __attribute__((sentinel)) de GCC avertit si le dernier argument n'est pas NULL. Documentez toujours la sentinelle attendue. L'inconvénient est que la sentinelle ne peut pas apparaître comme une valeur de données valide.
#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 et chaînes de format
vprintf/vfprintf/vsnprintf acceptent une va_list au lieu de ..., permettant des fonctions personnalisées de type printf. Utilisez toujours vsnprintf (borné) au lieu de vsprintf pour prévenir les débordements de tampon. Transférez la va_list directement. Ce pattern est utilisé dans les bibliothèques de journalisation, le rapport d'erreurs et les formateurs personnalisés. La vulnérabilité de chaîne de format (format contrôlé par l'utilisateur) est un risque de sécurité — ne passez jamais l'entrée utilisateur comme 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);Pointeurs de fonction et rappels
Les pointeurs de fonction permettent les rappels et le polymorphisme en C. La syntaxe return_type (*name)(params) déclare un pointeur vers une fonction. qsort utilise un rappel de comparaison pour le tri générique. Les tableaux de pointeurs de fonction implémentent des tables de distribution (alternative au switch). Assurez-vous toujours que la signature du rappel correspond exactement. Les pointeurs de fonction sont la base des gestionnaires d'événements, des plugins et du pattern strategy en 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;
}Macros variadiques
Les macros variadiques (__VA_ARGS__) acceptent des arguments variables, utiles pour la journalisation et le débogage. __VA_OPT__ (C2x) gère le cas zéro argument en incluant conditionnellement la virgule. L'extension GCC ##__VA_ARGS__ supprime la virgule précédente lorsqu'aucun argument n'est passé. Les macros de débogage qui se compilent en rien dans les builds release éliminent le surcoût sans changements de code. Protégez toujours les chaînes de format pour prévenir les attaques par chaîne de format.
// 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);Snippets C associés
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?