Partie 1 - Bases de C¶
Support présentationCette partie a pour objectif de rafraîchir vos connaissances en C et d’aborder des notions utiles pour la suite du cours ou déjà vues dans d’autres cours (allocation de la mémoire sur la pile ou le tas, représentation des variables en binaire, etc.). Ce n’est ni un cours complet ni un cours pour débutants en informatique : nous couvrirons les bases du langage pour les exploiter dans le cours de programmation système. Je considère, ici et dans le reste du cours, que vous avez des bases en algorithmique et en programmation (peu importe le langage) et une pratique minimale du terminal.
Voir les ressources pour des cours et liens/indications vers la documentation.
Voir les outils que nous utiliserons dans ce cours.
Qu’est-ce que le C ?¶
Le C est un langage de programmation impératif et compilé, inventé dans les années 1970 par Dennis Ritchie. Il est considéré aujourd’hui comme un langage de bas niveau, comparé aux langages modernes, et de haut niveau comparé à l’assembleur.
Dennis Ritchie - Concepteur du C |
Brian Kernighan - a contribué à la formalisation, la diffusion et la normalisation du C |
Malgré ses ≈50 ans, il reste très utilisé grâce à sa vitesse d’exécution et à sa portabilité, et le restera très probablement pendant de nombreuses années. Zig et Rust se présentent comme des alternatives modernes au C. Rust est d’ailleurs le 3e langage à rejoindre le développement du noyau Linux, après l’assembleur et le C.
Il est notamment employé pour le développement de systèmes d’exploitation, de pilotes ou de systèmes embarqués.
Normes du C¶
Le langage C a connu plusieurs normes qui ont permis d’améliorer le langage et de donner une direction dans l’implémentation des compilateurs. Un certain niveau de rétrocompatibilité avec les versions précédentes est maintenu dans la plupart des cas.
C K&R (1978)
Première version non standardisée, présentée dans le livre « The C Programming Language » de Brian Kernighan et Dennis Ritchie.
ANSI C / C89 (1989) – ISO C / C90 (1990)
Première normalisation officielle par l’ANSI. Ajout des prototypes de fonctions, de mots-clés et meilleure portabilité du code. Présentée dans le livre « The C Programming Language 2nd edition ».
C99 (1999)
Mise à jour majeure avec, entre autres, les commentaires
//, les déclarations dans les boucles, les types entiers de taille fixe (<stdint.h>), les tableaux de taille variable (VLA), les initialisations désignées, et une meilleure prise en charge du standard IEEE pour les flottants.C11 (2011)
Mise à jour majeure avec un support standardisé des threads (
<threads.h>), des opérations atomiques (<stdatomic.h>), de nouveaux mots-clés et des améliorations de sécurité.C17/C18 (2018)
Révision mineure de C11, corrections sans nouvelles fonctionnalités.
C23/C2x (finalisé en 2023, publié en 2024)
Modernisation du langage et meilleure portabilité avec le C++.
C2y (202?)
En cours de développement avec pour objectifs d’améliorer la sécurité et l’interopérabilité.
Comme pour le C++, le comité de standardisation du langage C définit les évolutions du langage. Les développeurs de compilateurs (comme GCC, Clang, MSVC, …) doivent ensuite implémenter ces spécifications. Il peut donc exister un décalage de plusieurs années entre la publication d’une norme et sa prise en charge complète dans les compilateurs (voir C compiler support et C++ compiler support).
Hello World!¶
Le premier programme classique pour débuter dans un langage est d’afficher Hello World! dans la console.
Voici un exemple en C :
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int main(void) {
5 printf("Hello World!\n");
6 return EXIT_SUCCESS;
7 }
Voici le détail ligne par ligne des différentes étapes :
1// import de printf
2// (stdio → standard input output, entrée sortie standard)
3#include <stdio.h>
4// import de EXIT_SUCCESS (0 → fin normale)
5// et EXIT_FAILURE (1/autre → erreur)
6// (stdlib → standard library, bibliothèque standard)
7#include <stdlib.h>
8
9// le point d'entrée du programme :
10// la première fonction appelée à l'exécution
11int main(void) {
12 // écrit un message sur la sortie standard (stdout)
13 printf("Hello World!\n");
14
15 // fin du programme
16 // retourne un code de succès au système d'exploitation
17 return EXIT_SUCCESS;
18}
Le C étant un langage compilé, le code source écrit par le programmeur doit être traduit en langage machine avant de pouvoir l’exécuter. Ce processus est effectué par le compilateur : il traduit le C en assembleur, l’assembleur produit un fichier objet (binaire intermédiaire), puis l’éditeur de liens produit un exécutable prêt à être lancé par le système d’exploitation. Voir la partie Compilation de la page Outils pour plus de détails.
Pour compiler cet exemple avec GCC (GNU Compiler Collection) :
gcc -std=c2x -Wall -Wextra -pedantic hello.c -o hello
-std=c2x: indique au compilateur d’utiliser la norme C23 ;-Wall: active les avertissements classiques (erreurs de base, variables inutilisées, etc.) ;-Wextra: active encore plus d’avertissements utiles ;-pedantic: signale toute construction non conforme strictement à la norme ;-o: change le nom du programme créé (a.outpar défaut).
Ces options permettent de rendre le compilateur plus bavard : elles ne modifient pas le comportement du programme, mais aident à détecter des erreurs ou mauvaises pratiques dès la compilation.
Une fois le programme compilé, nous pouvons l’exécuter avec :
$ ./hello
Hello World!
Les variables¶
Une variable se compose de quatre éléments :
un type (qui détermine la nature et la taille de la donnée) ;
un nom (l’identifiant que vous choisissez) ;
un espace mémoire réservé (dont la taille dépend du type) identifié grâce à son adresse ;
une valeur (optionnelle à l’initialisation, mais obligatoire avant toute utilisation) stockée dans l’espace mémoire réservé.
Les variables sont déclarées sous la forme :
// Déclaration
type nom_variable;
// Déclaration et initialisation (fortement recommandé)
type nom_variable = valeur;
Pour le nom de la variable :
il doit commencer par une lettre ou un
_(underscore) ;il ne peut contenir que des lettres, chiffres et underscores ;
il ne doit pas être un mot-clé du langage (ex. :
int,return,if, etc.).
Par convention, on utilise souvent le snake_case (ma_variable) en C (convention utilisée dans ce cours).
Pour la valeur :
elle doit correspondre au type déclaré ;
utiliser une variable non initialisée peut conduire à un comportement indéfini ;
la valeur peut être modifiée si la variable n’est pas déclarée constante avec le mot-clé
const.
Pour le type, voici un sous-ensemble des principaux types en C que nous utiliserons dans ce cours (voir liste plus complète sur Wikipédia) :
1#include <stdbool.h> // pour importer bool (inutile en C23, à partir de GCC 13)
2#include <stdio.h>
3#include <stdlib.h>
4
5int main(void) {
6
7 // booléen
8 bool b = true;
9 printf("bool b = %d\t(taille: %zu octet(s))\n", b, sizeof(b));
10
11 // caractère : signé ou non selon la plateforme,
12 // utiliser signed char / unsigned char pour l'imposer
13 // (attention : utiliser des apostrophes simples 'A' et non "A")
14 char c = 'A';
15 printf("char c = '%c'\t(taille: %zu octet(s))\n", c, sizeof(c));
16
17 // entier signé
18 // ou non signé avec unsigned
19 int i = 42;
20 printf("int i = %d\t(taille: %zu octet(s))\n", i, sizeof(i));
21
22 // entier non signé pour les tailles et indices
23 // ou signé avec ssize_t
24 // (spécifique aux fonctions système et mémoire)
25 size_t s = sizeof(int);
26 printf("size_t s = %zu\t(taille: %zu octet(s))\n", s, sizeof(s));
27
28 // nombre à virgule flottante
29 // float et double
30 double d = 3.1415926535;
31 printf("double d = %1.2lf\t(taille: %zu octet(s))\n", d, sizeof(d));
32
33 // pointeur vers entier
34 int *p = &i;
35 printf("int *p = %p\t(pointe vers i = %d)\n", (void *)p, *p);
36
37 // pointeur générique (void *)
38 void *vp = &c; // peut pointer vers n'importe quoi
39 printf("void *vp = %p\t(pointe vers char c)\n", vp);
40
41 return EXIT_SUCCESS;
42}
L’opérateur sizeof renvoie la taille en mémoire, en octets (bytes ; 1 octet = 8 bits), de la variable ou du type donné.
Pour une variable, on peut écrire sizeof x ou sizeof(x) ; pour un type, les parenthèses sont obligatoires : sizeof(int).
Sortie (dépendant de l’architecture de votre machine, 32 bits ou 64 bits) :
bool b = 1 (taille: 1 octet(s))
char c = 'A' (taille: 1 octet(s))
int i = 42 (taille: 4 octet(s))
size_t s = 4 (taille: 8 octet(s))
double d = 3.14 (taille: 8 octet(s))
int *p = 0x7ffdcec93f54 (pointe vers i = 42)
void *vp = 0x7ffdcec93f52 (pointe vers char c)
Avec le mot-clé const, une variable devient une constante et ne peut plus être modifiée :
const int my_int = 42;
my_int = 4; // Erreur : assignment of read-only variable 'my_int'
La sortie suivante montre les adresses et valeurs en mémoire pour ces variables (le programme qui l’affiche n’est pas donné, seules les déclarations le sont ; les adresses et l’ordre des variables en mémoire dépendent du compilateur et de ses options) :
// #include <stdint.h> pour int32_t et int64_t
bool b = true;
unsigned char uc = 200;
int i1 = 255;
int i2 = 256;
int i3 = 2147483647;
int32_t i32 = 1500;
int64_t i64 = 345623132;
float f = 3.1415926f;
double d = -12345.6789;
bool b = true (base=0x7ffe6c04df72, taille=1 octet)
Idx Offset Adresse Hex Dec Binaire
--- ------ ------------------ ----- ----- ---------
0 +0 0x7ffe6c04df72 0x01 1 0000 0001
unsigned char uc = 200 (base=0x7ffe6c04df73, taille=1 octet)
Idx Offset Adresse Hex Dec Binaire
--- ------ ------------------ ----- ----- ---------
0 +0 0x7ffe6c04df73 0xC8 200 1100 1000
int i1 = 255 (base=0x7ffe6c04df74, taille=4 octets)
Idx Offset Adresse Hex Dec Binaire
--- ------ ------------------ ----- ----- ---------
0 +0 0x7ffe6c04df74 0xFF 255 1111 1111
1 +1 0x7ffe6c04df75 0x00 0 0000 0000
2 +2 0x7ffe6c04df76 0x00 0 0000 0000
3 +3 0x7ffe6c04df77 0x00 0 0000 0000
int i2 = 256 (base=0x7ffe6c04df78, taille=4 octets)
Idx Offset Adresse Hex Dec Binaire
--- ------ ------------------ ----- ----- ---------
0 +0 0x7ffe6c04df78 0x00 0 0000 0000
1 +1 0x7ffe6c04df79 0x01 1 0000 0001
2 +2 0x7ffe6c04df7a 0x00 0 0000 0000
3 +3 0x7ffe6c04df7b 0x00 0 0000 0000
int i3 = 2147483647 (base=0x7ffe6c04df7c, taille=4 octets)
Idx Offset Adresse Hex Dec Binaire
--- ------ ------------------ ----- ----- ---------
0 +0 0x7ffe6c04df7c 0xFF 255 1111 1111
1 +1 0x7ffe6c04df7d 0xFF 255 1111 1111
2 +2 0x7ffe6c04df7e 0xFF 255 1111 1111
3 +3 0x7ffe6c04df7f 0x7F 127 0111 1111
int32_t i32= 1500 (base=0x7ffe6c04df80, taille=4 octets)
Idx Offset Adresse Hex Dec Binaire
--- ------ ------------------ ----- ----- ---------
0 +0 0x7ffe6c04df80 0xDC 220 1101 1100
1 +1 0x7ffe6c04df81 0x05 5 0000 0101
2 +2 0x7ffe6c04df82 0x00 0 0000 0000
3 +3 0x7ffe6c04df83 0x00 0 0000 0000
int64_t i64= 345623132 (base=0x7ffe6c04df88, taille=8 octets)
Idx Offset Adresse Hex Dec Binaire
--- ------ ------------------ ----- ----- ---------
0 +0 0x7ffe6c04df88 0x5C 92 0101 1100
1 +1 0x7ffe6c04df89 0xCA 202 1100 1010
2 +2 0x7ffe6c04df8a 0x99 153 1001 1001
3 +3 0x7ffe6c04df8b 0x14 20 0001 0100
4 +4 0x7ffe6c04df8c 0x00 0 0000 0000
5 +5 0x7ffe6c04df8d 0x00 0 0000 0000
6 +6 0x7ffe6c04df8e 0x00 0 0000 0000
7 +7 0x7ffe6c04df8f 0x00 0 0000 0000
float f = 3.1415926f (base=0x7ffe6c04df84, taille=4 octets)
Idx Offset Adresse Hex Dec Binaire
--- ------ ------------------ ----- ----- ---------
0 +0 0x7ffe6c04df84 0xDA 218 1101 1010
1 +1 0x7ffe6c04df85 0x0F 15 0000 1111
2 +2 0x7ffe6c04df86 0x49 73 0100 1001
3 +3 0x7ffe6c04df87 0x40 64 0100 0000
double d = -12345.6789 (base=0x7ffe6c04df90, taille=8 octets)
Idx Offset Adresse Hex Dec Binaire
--- ------ ------------------ ----- ----- ---------
0 +0 0x7ffe6c04df90 0xA1 161 1010 0001
1 +1 0x7ffe6c04df91 0xF8 248 1111 1000
2 +2 0x7ffe6c04df92 0x31 49 0011 0001
3 +3 0x7ffe6c04df93 0xE6 230 1110 0110
4 +4 0x7ffe6c04df94 0xD6 214 1101 0110
5 +5 0x7ffe6c04df95 0x1C 28 0001 1100
6 +6 0x7ffe6c04df96 0xC8 200 1100 1000
7 +7 0x7ffe6c04df97 0xC0 192 1100 0000
Les colonnes Hex/Dec/Binaire sont des vues du même octet ; l’ordre des octets dans la mémoire (indice 0,1,2,3,…) dépend de l’endianness (l’ordre dans lequel les octets doivent être lus).
Les entiers signés sont en complément à deux (d’où INT_MAX = 0x7FFFFFFF).
float/double suivent IEEE-754 : val = (-1)^s × (1+mantisse) × 2^(exp-bias) (hors cas spéciaux).
L’alignement explique les frontières d’adresses « propres » (multiples de 4 pour int/float, multiples de 8 pour int64_t/double).
LSB (Least Significant Bit/Byte) : poids faible - le bit (ou l’octet) qui compte le moins, côté droit.
MSB (Most Significant Bit/Byte) : poids fort - le bit (ou l’octet) qui compte le plus, côté gauche.
Endianness : on voit partout que l’octet d’indice 0 (adresse la plus basse) contient le poids faible de la valeur :
i1 = 255 → FF 00 00 00;i2 = 256 → 00 01 00 00. La machine est donc en little-endian. En big-endian, on aurait00 00 00 FFet00 00 01 00, soit l’ordre le plus « naturel » pour un humain.Unités d’allocation : chaque ligne d’octet a une adresse qui s’incrémente de +1. Les types >1 octet occupent un bloc contigu (4 pour int, 8 pour int64_t/double, etc.).
Alignement : les bases respectent les alignements usuels x86_64 :
int/float à des adresses multiples de 4 (…74, …78, …7C, …80, …84) ;
int64_t/double à des adresses multiples de 8 (…88, …90).
Le compilateur peut insérer du padding (octets de remplissage) ou réordonner légèrement les variables pour respecter ces contraintes. C’est le cas du
float f, placé aprèsint32_t i32et avantint64_t i64alors qu’il est déclaré après ce dernier.
Sur l’image ci-dessus, on retrouve pour chaque variable son adresse et la valeur de celle-ci en binaire.
Les bool et char occupent 1 octet en mémoire, les int 4 octets, donc 4 blocs les uns à la suite des autres et les double 8 octets.
Un pointeur contient une adresse : il occupe donc la même taille quel que soit le type pointé, que ce soit un pointeur sur un int ou sur un bool (8 octets sur une machine 64 bits, 4 en 32 bits).
Les octets¶
La mémoire est un ensemble de blocs de 8 bits appelés octets et repérés par une adresse.
Il existe plusieurs manières de représenter les octets :
en binaire :
0000 0010, avec des bits valant 0 ou 1 ;en hexadécimal :
0x02, avec un chiffre de 0 à F pour chaque groupe de 4 bits (appelé nibble) ;en décimal :
2, avec des chiffres de 0 à 9.
Les adresses (et plus généralement le contenu de la mémoire) sont représentées en hexadécimal car chaque octet correspond exactement à 2 chiffres hexadécimaux, ce qui rend la lecture des octets directe :
en décimal, un octet va de
0à255, soit 1 à 3 chiffres ;en hexadécimal, un octet va de
0x00à0xFF, soit toujours 2 chiffres.
Entrées - sorties¶
Sorties¶
Comme vu précédemment, printf permet d’afficher des informations dans la console (la sortie standard ou stdout).
La fonction printf est définie dans la bibliothèque standard <stdio.h> (doc, man 3 printf).
int printf(const char *restrict format, ...);
format: chaîne de caractères à formater ;...: éléments à afficher, dans l’ordre d’apparition des spécificateurs de format dans la chaîneformat.
Retourne le nombre de caractères affichés ou une valeur négative en cas d’erreur.
Elle affiche du texte formaté à partir d’une chaîne de format (premier argument) pouvant contenir des spécificateurs de format (%d par exemple), suivie des valeurs à afficher.
Voici quelques spécificateurs de format courants :
%c:char;%d:int;%u:unsigned int;%ld:long;%f:float/double;%lf:double(pourscanf) ;%s:char *;%zu:size_t;%p:void *(il faut convertir le pointeur en(void *), voir Conversions explicites (cast)).
On peut préciser une largeur minimale : %04d complète avec des 0 jusqu’à 4 caractères, %4d avec des espaces (voir l’exemple de la doc).
C’est une largeur minimale : une valeur plus longue est affichée en entier.
printf écrit sur la sortie standard. Pour écrire sur la sortie d’erreur (stderr), on utilise fprintf(stderr, ...), qui prend en premier argument le flux sur lequel écrire.
La sortie standard est mise en tampon : le texte n’est souvent affiché qu’à la fin d’une ligne (\n). fflush(stdout) force l’affichage immédiat, utile par exemple avant une saisie quand le message ne se termine pas par \n.
Entrées¶
Pour récupérer des données de l’utilisateur depuis le clavier (entrée standard ou stdin), nous pouvons utiliser, entre autres, scanf (doc, man 3 scanf).
int scanf(const char *restrict format, ...);
format: format des éléments à lire ;...: pointeurs vers les éléments à modifier.
Retourne le nombre d’éléments lus et modifiés, ou EOF (End Of File) si la fin de l’entrée est atteinte avant la première conversion.
Elle récupère l’entrée utilisateur grâce au format demandé et à l’adresse de la variable où stocker la valeur lue.
L’adresse d’une variable est donnée avec & (par exemple, &my_int) et permet à la fonction scanf d’aller directement modifier l’espace mémoire de la variable avec l’entrée utilisateur (voir Les pointeurs).
int age;
printf("Quel est votre âge ? ");
scanf("%d", &age);
printf("Vous avez %d ans.\n", age);
Les spécificateurs de format sont les mêmes que pour printf, à quelques exceptions près : dans scanf, %f attend un float * et %lf un double *.
scanf peut être suffisant pour lire des entiers ou flottants simples, mais il présente plusieurs limites que nous verrons avec les chaînes de caractères.
Exemple d’entrées-sorties (Input/Output ou IO en anglais), qui utilise puts (affiche une chaîne suivie d’un \n) ainsi que fgets et strcspn, présentés dans la section Les chaînes de caractères :
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4
5int main(void) {
6 char nom[100];
7 puts("Votre nom ?");
8 fgets(nom, sizeof(nom), stdin); // fgets pour chaînes
9 nom[strcspn(nom, "\n")] = '\0'; // retire le \n
10
11 int age;
12 puts("Quel est votre âge ?");
13 scanf("%d", &age); // scanf pour nombres
14
15 printf("Bonjour %s, %d ans.\n", nom, age);
16 return EXIT_SUCCESS;
17}
Les opérateurs¶
Les opérateurs en C sont les suivants :
Dans la liste ci-dessous, la notation a @ b indique où se placent les opérandes, @ représentant l’opérateur.
Opérateurs arithmétiques (
a @ b), pour effectuer des opérations mathématiques :addition :
+soustraction :
-multiplication :
*division :
/modulo :
%(reste de la division entière)int a = 7; int b = 3; printf("%d\n", a + b); // 10 printf("%d\n", a % b); // 1
Opérateurs de comparaison (
a @ b), qui retournent un booléen (0 ou 1) :inférieur à :
<inférieur ou égal à :
<=supérieur à :
>supérieur ou égal à :
>=égal à :
==différent de :
!=
Opérateurs logiques (
a @ b), pour manipuler des valeurs booléennes vrai/faux (0 ou 1) :non logique :
!(!a, inverse la valeur booléenne)et logique :
&&ou logique :
||
Opérateurs d’affectation (
a @ b), pour modifier la valeur d’une variable :affectation simple :
=addition puis affectation :
+=soustraction puis affectation :
-=multiplication puis affectation :
*=division puis affectation :
/=modulo puis affectation :
%=
Incrémentation / décrémentation (
@apour préfixe,a@pour postfixe) :incrémente de 1 :
++décrémente de 1 :
--int i = 1; printf("%d\n", ++i); // affiche 2, puis i vaut 2 printf("%d\n", i++); // affiche 2, puis i vaut 3
Opérateurs bit à bit (
a @ b), pour manipuler les bits individuels dans les variables entières (utilisés en programmation bas niveau) :ou bit à bit :
|et bit à bit :
&ou exclusif (XOR) :
^non bit à bit :
~(~a)décalage à gauche :
<<décalage à droite :
>>unsigned int x = 5; // binaire 0101 unsigned int y = x << 1; // décalage à gauche : 1010 (10 en décimal)
Opérateur conditionnel (ternaire)
condition ? a : b: vautasi la condition est vraie,bsinon.int x = -4; int abs_x = (x >= 0) ? x : -x; // 4
Comme en mathématiques, les opérateurs ont des priorités.
Les parenthèses peuvent être utilisées pour forcer l’ordre d’évaluation.
La priorité et l’associativité (ordre entre opérateurs de même priorité, par exemple de gauche à droite pour a - b - c) sont fixées par la norme.
En revanche, l’ordre d’évaluation des opérandes n’est pas spécifié (dans f() + g(), f ou g peut être appelée en premier), et modifier deux fois une variable dans une même expression (i++ + i++) est un comportement indéfini.
Avec les nombres flottants, l’ordre des opérations compte : (a + b) + c et a + (b + c) peuvent donner des résultats différents à cause des arrondis.
Conversions explicites (cast)¶
L’opérateur de conversion (cast) (type) expression convertit une valeur vers un autre type :
int a = 7;
int b = 2;
double moyenne = (double)a / b; // 3.5 (sans le cast : division entière, 3)
size_t n = 10;
int i = (int)n; // size_t → int
printf("%p\n", (void *)&a); // conversion en void * pour %p
Une conversion peut perdre de l’information (partie décimale, valeur trop grande pour le type cible) : elle indique au compilateur, et au lecteur, que ce choix est volontaire.
De même, un littéral entier peut porter un suffixe qui fixe son type : 60u est un unsigned int, 60l un long.
Contrôle du flux¶
Le contrôle du flux permet de diriger l’exécution du programme selon des conditions.
Les instructions if, else if et else permettent d’exécuter des blocs de code en fonction de conditions.
if (condition1) {
// instructions si condition1 vraie
} else if (condition2) {
// instructions si condition2 vraie
} else {
// instructions si aucune condition vraie
}
Exemple :
int a = 5;
if (a != 8) {
printf("a n'est pas égal à 8\n");
}
if (a % 2 == 0) {
printf("a est pair\n");
} else {
printf("a est impair\n");
}
if (a > 10) {
printf("a est plus grand que 10\n");
} else if (a > 3) {
printf("a est entre 4 et 10\n");
} else {
printf("a est 3 ou moins\n");
}
La structure switch permet de sélectionner une action parmi plusieurs selon la valeur d’une variable entière ou d’un caractère.
switch (variable) {
case valeur1:
// instructions
break;
case valeur2:
// instructions
break;
...
default:
// instructions par défaut
}
Sans break, l’exécution se poursuit dans les case suivants (fall-through) jusqu’au prochain break ou à la fin du switch.
Exemple :
char grade = 'B';
switch (grade) {
case 'A':
printf("Excellent\n");
break;
case 'B':
// pas de break
// donc affiche le Bien du cas 'C'
case 'C':
printf("Bien\n");
break;
case 'D':
printf("Passable\n");
break;
default:
printf("Grade inconnu\n");
}
Les boucles¶
Les boucles permettent de répéter une série d’instructions tant qu’une condition est remplie.
Le langage C propose trois types principaux de boucles : for, while et do...while.
La boucle for est souvent utilisée quand le nombre d’itérations est connu à l’avance.
Elle se compose de trois parties :
initialisation ;
condition (test à chaque tour) ;
mise à jour (à chaque fin d’itération).
for (initialisation; condition; mise à jour) {
// instructions à répéter
}
Exemple :
for (int i = 0; i < 5; i++) {
printf("i vaut %d\n", i);
}
La boucle while teste la condition avant chaque itération. Si la condition est fausse dès le départ, le bloc ne sera jamais exécuté.
while (condition) {
// instructions à répéter
}
Exemple :
int i = 0;
while (i < 5) {
printf("i vaut %d\n", i);
i++;
}
La boucle do...while est similaire à while, mais la condition est testée après l’exécution du bloc.
Le corps de la boucle est donc exécuté au moins une fois, même si la condition est fausse.
do {
// instructions à répéter
} while (condition);
Exemple :
int i = 0;
do {
printf("i vaut %d\n", i);
i++;
} while (i < 5);
Une boucle peut être interrompue (break) ou une itération sautée (continue) :
break: sort immédiatement de la boucle :for (int i = 0; i < 10; i++) { if (i == 5) { break; } printf("%d ", i); } // affiche : 0 1 2 3 4
continue: saute à l’itération suivante (ignore les instructions restantes du bloc courant) :for (int i = 0; i < 5; i++) { if (i == 2) { continue; } printf("%d ", i); } // affiche : 0 1 3 4
Les fonctions¶
Une fonction permet de regrouper un ensemble d’instructions sous un même nom afin de pouvoir les réutiliser facilement.
En C, une fonction se compose :
d’un type de retour (le type de la valeur renvoyée) ;
d’un nom ;
de paramètres (éventuellement) ;
d’un corps (le bloc d’instructions à exécuter).
La déclaration (ou prototype) indique au compilateur l’existence de la fonction :
void bonjour(void);
int get_number(void);
int addition(int a, int b);
La définition fournit le corps de la fonction :
void bonjour(void) {
printf("Bonjour !\n");
}
int get_number(void) {
return 42;
}
int addition(int a, int b) {
return a + b;
}
Une fonction doit être déclarée avant son appel. Pour appeler une fonction, on écrit son nom suivi des arguments entre parenthèses :
bonjour();
int number = get_number();
printf("Numéro : %d\n", number);
int result = addition(3, 4);
printf("Résultat : %d\n", result);
Note
Avant C23, sans le void dans la déclaration des paramètres, un appel comme bonjour("coucou") compile sans erreur.
Il est donc fortement recommandé de toujours écrire (void) pour une fonction sans paramètre.
On place souvent les prototypes de fonction en haut du fichier .c ou dans un fichier .h.
#include <stdio.h>
#include <stdlib.h>
// Déclaration/Prototype
int carre(int n);
int main(void) {
int x = 5;
printf("Le carré de %d est %d\n", x, carre(x));
return EXIT_SUCCESS;
}
// Définition
int carre(int n) {
return n * n;
}
La première fonction de votre programme appelée à l’exécution est la fonction spéciale main (le code de démarrage de la libc s’exécute avant et l’appelle).
Elle constitue le point d’entrée du programme.
Cette fonction peut être définie de plusieurs manières :
// sans arguments
int main(void)
int main() // préférer la forme explicite avec void
// ou
// avec argc et argv comme arguments
int main(int argc, char **argv)
int main(int argc, char *argv[])
int main(int argc, char *argv[argc + 1])
Les paramètres du main correspondent aux arguments passés par la ligne de commande :
argc(argument count) : un entier qui représente le nombre d’arguments passés en ligne de commande, y compris le nom du programme lui-même ;argv(argument vector) : un tableau de chaînes de caractères (pointeurs verschar, voir Les pointeurs) contenant chacun des arguments passés au programme.
La forme char *argv[argc + 1] indique que le tableau contient argc + 1 éléments : la norme garantit que argv[argc] vaut NULL, ce qui marque la fin des arguments.
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[argc + 1]) {
printf("Nombre d'arguments : %d\n", argc);
for (int i = 0; i < argc; i++) {
printf("Argument %d : %s\n", i, argv[i]);
}
return EXIT_SUCCESS;
}
$ gcc -std=c2x -Wall -Wextra -pedantic exemple.c -o exemple
$ ./exemple abc 123 a1b2c3 3.14
Nombre d'arguments : 5
Argument 0 : ./exemple
Argument 1 : abc
Argument 2 : 123
Argument 3 : a1b2c3
Argument 4 : 3.14
La fonction main a pour type de retour int ; la valeur renvoyée est transmise au système d’exploitation :
return EXIT_SUCCESS;(oureturn 0;) indique que le programme s’est terminé normalement ;return EXIT_FAILURE;(ou un autre entier non nul) signale une erreur.
Depuis C99, en l’absence de return à la fin de main, le programme renvoie 0.
Les tableaux¶
Un tableau en C est une structure permettant de stocker plusieurs valeurs de même type en mémoire contiguë.
Dans la figure ci-dessous, la partie haute de gauche montre un int sur 4 octets, aux adresses 0x7f...00 à 0x7f...03, avec sa valeur en binaire.
La partie basse de gauche en donne une représentation simplifiée : le int commence à l’adresse 0x7f...00 (et occupe les 3 octets contigus suivants), et ses 4 octets convertis en décimal donnent 9.
Pour représenter un tableau de 5 int (partie droite), nous réservons 5 × 4 octets en mémoire (soit 20 octets), qui commencent ici à l’adresse 0x7f...00 ; le dernier int commence à l’adresse 0x7f...10 et finit donc à l’adresse 0x7f...13.
Déclaration d’un tableau d’entiers en C :
// tableau de 5 entiers
int tableau[5];
// avec initialisation
int notes[5] = {10, 12, 15, 9, 14};
int zeros[3] = {0}; // les autres éléments valent aussi 0
int valeurs[6] = {
[1] = 12,
[3] = 45,
[5] = 78,
}; // les autres valeurs sont initialisées à 0
// en laissant le compilateur trouver la taille
int autres[] = {1, 2, 3, 4}; // taille = 4
La taille du tableau doit être connue à la compilation : elle doit donc être donnée directement ou définie avec une constante du préprocesseur, #define TAILLE 5 (voir Préprocesseur).
Danger
En C99, les VLA (Variable Length Arrays) ont été introduits dans la norme puis mis en optionnels dans la norme C11 (les compilateurs ne sont donc pas obligés de les implémenter depuis C11, comme MSVC sous Windows qui ne les implémente pas).
Ils permettent d’allouer des tableaux sur la pile (stack) avec une taille inconnue à la compilation (variable, entrée utilisateur, lecture dans un fichier, etc.). Ils peuvent donc causer des dépassements de pile (stack overflow, voir gestion mémoire). Ils sont peu portables et ne peuvent pas être utilisés comme champs de structures.
Si vous voulez un tableau de taille inconnue à la compilation, préférez l’allocation dynamique sur le tas (heap) que nous verrons plus bas avec malloc.
On accède aux éléments d’un tableau par leur indice, en commençant à 0 :
printf("Première note : %d\n", notes[0]);
notes[2] = 16; // modifier la 3e note
for (int i = 0; i < 5; i++) {
printf("Note %d : %d\n", i, notes[i]);
}
Avertissement
Attention : il n’y a pas de vérification des limites du tableau. L’accès hors limites provoque un comportement indéfini.
On peut déclarer des tableaux de tableaux avec plusieurs dimensions :
// tableau 1D
// [indice]
int tab1d[5] = {10, 12, 15, 9, 14};
// tableau 2D
// [lignes][colonnes]
int matrice[3][2] = {
{1, 2},
{3, 4},
{5, 6},
};
printf("%d\n", matrice[1][0]); // affiche 3
// tableau 3D
// [profondeur][lignes][colonnes]
int cube[2][3][4] = {
{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
},
{
{13, 14, 15, 16},
{17, 18, 19, 20},
{21, 22, 23, 24},
},
};
printf("%d\n", cube[1][2][3]); // affiche 24
// indices : profondeur=1, ligne=2, colonne=3
// tableau 4D
// [t][z][y][x] (x = dimension la plus à droite)
int hyper[2][2][2][3] = {
{
// t = 0
{{1, 2, 3}, {4, 5, 6}}, // z=0 : y=0 puis y=1
{{7, 8, 9}, {10, 11, 12}}, // z=1
},
{
// t = 1
{{13, 14, 15}, {16, 17, 18}},
{{19, 20, 21}, {22, 23, 24}},
},
};
printf("%d\n", hyper[1][1][1][2]); // affiche 24 (t=1,z=1,y=1,x=2)
Le passage d’un tableau en paramètre d’une fonction est présenté dans la section Tableaux et fonctions.
Les chaînes de caractères¶
Une chaîne de caractères est un tableau de char terminé par un caractère nul (\0).
Déclaration d’une chaîne :
// utilisation similaire à celle d'un tableau
char nom1[6] = {'A', 'l', 'i', 'c', 'e', '\0'};
// utilisation plus concise et recommandée
char nom2[] = "Bob"; // équivalent à {'B','o','b','\0'}
printf("Nom : %s\n", nom2);
// affiche :
// Nom : Bob
La bibliothèque standard (<string.h>) propose plusieurs fonctions pour manipuler les chaînes (voir man 3 nom_fonction) :
#include <string.h>
char nom[20] = "Alice";
char nom_complet[40];
// copie nom → nom_complet
strcpy(nom_complet, nom);
// concaténation
strcat(nom_complet, " Dupont");
// longueur de "Alice" = 5
// strlen ignore le \0
size_t longueur = strlen(nom);
// comparaison
if (strcmp(nom, "Alice") == 0) {
printf("C'est bien Alice !\n");
}
Attention : la taille de la chaîne doit inclure le \0 et toute chaîne doit finir par \0.
scanf peut être suffisant pour lire des entiers ou flottants simples, mais il présente plusieurs limites :
%ss’arrête au premier blanc (espace, tabulation, retour à la ligne) et, sans largeur maximale (%99s), peut déborder du tableau ;il peut laisser des caractères non lus dans le tampon d’entrée (
stdin), comme le\naprès unscanf("%d", ...), ce qui peut perturber les lectures suivantes ;il est difficile à utiliser pour des saisies plus complexes ou robustes (détection d’erreurs, chaînes multi-mots, etc.).
Pour des lectures plus sécurisées et robustes, on peut utiliser fgets, pour lire une ligne entière depuis l’entrée utilisateur (jusqu’au retour à la ligne).
char *fgets(char *buffer, int taille, FILE *flux);
buffer: tableau où stocker la ligne lue ;taille: taille du tableau ;flux: flux à lire (stdinpour l’entrée standard).
Retourne buffer si la lecture réussit, NULL en fin de fichier ou en cas d’erreur (buffer n’est alors pas modifié de façon fiable).
La fonction fgets :
lit une ligne depuis le flux et la stocke dans
buffer;s’arrête après
taille - 1caractères ou après le\n;ajoute
\0à la fin si la lecture réussit ;conserve le retour à la ligne (
\n) s’il a été lu.
char nom[100];
printf("Entrez votre nom : ");
if (fgets(nom, sizeof(nom), stdin) != NULL) {
printf("Bonjour %s !", nom); // Attention, le \n est conservé !
}
Pour supprimer le retour à la ligne :
nom[strcspn(nom, "\n")] = '\0';
strcspn(nom, "\n") renvoie l’indice du premier \n (ou la longueur de la chaîne s’il n’y en a pas) : on y place le \0 de fin.
Pour convertir une chaîne en entier, atoi(chaine) (<stdlib.h>) renvoie l’entier lu au début de la chaîne (0 si aucun chiffre).
strtol fait la même chose mais permet de détecter les erreurs (voir man 3 strtol).
Les pointeurs¶
Un pointeur est une variable qui contient l’adresse mémoire d’une autre variable.
Pour déclarer un pointeur vers un type T, on écrit T *mon_pointeur.
Pour obtenir l’adresse d’une variable, on utilise & : mon_pointeur = &ma_variable donne à mon_pointeur l’adresse de ma_variable.
On peut ensuite modifier la valeur à l’adresse pointée avec l’opérateur de déréférencement * : *mon_pointeur = 5.
int x = 42;
// p pointe vers x
int *p = &x;
// %p pour afficher une adresse avec le cast en (void *)
printf("Adresse de x : %p\n", (void *)&x);
printf("Adresse contenue dans p : %p\n", (void *)p);
printf("Valeur pointée par p : %d\n", *p);
printf("Modification de *p\n");
*p = 5;
printf("Valeur pointée par p : %d\n", *p);
printf("Valeur de x : %d\n", x);
Sortie :
Adresse de x : 0x7fff907fa4dc
Adresse contenue dans p : 0x7fff907fa4dc
Valeur pointée par p : 42
Modification de *p
Valeur pointée par p : 5
Valeur de x : 5
Dans la figure ci-dessous, my_int est un int stocké à l’adresse 0x7f...00, il a pour valeur 9.
my_ptr est un pointeur sur int stocké à l’adresse 0x7f...08 dont la valeur est l’adresse de my_int (0x7f...00).
Nous utilisons principalement 2 opérateurs avec les pointeurs :
&: donne l’adresse d’une variable ;*: utilisé dans deux cas :à la déclaration d’une variable : il indique que la variable est un pointeur vers un type ;
devant une variable pointeur (hors déclaration) : il permet de déréférencer le pointeur, c’est-à-dire d’accéder à la valeur stockée à l’adresse pointée.
Les pointeurs permettent, entre autres, de modifier une variable dans une fonction (passage par adresse) :
void doubler(int *n) {
*n = *n * 2;
}
int main(void) {
int a = 10;
doubler(&a);
printf("%d\n", a); // affiche 20
return EXIT_SUCCESS;
}
Dans la plupart des expressions, le nom d’un tableau est converti implicitement en pointeur vers son premier élément :
int tab[3] = {1, 2, 3};
int *p = tab; // équivalent à &tab[0]
printf("%d\n", *(p + 1)); // affiche 2
printf("%d\n", tab[1]); // équivalent
*(p + 1) utilise l’arithmétique des pointeurs : on avance d’un élément de la taille du type pointé.
Ici p est un pointeur sur int, donc lorsque l’on fait p + 1, le compilateur comprend ajoute 1 * sizeof(int) à l'adresse contenue dans p.
Nous n’avons pas besoin de lui dire de combien d’octets avancer, le compilateur le saura de lui-même :
int tab[3] = {1, 2, 3};
int *p = tab;
// affiche : sizeof(int) = 4
printf("sizeof(int) = %zu\n", sizeof(int));
// affiche : sizeof(tab) = 12
// sizeof d'un tableau renvoie sizeof(type des éléments) * nombre d'éléments
printf("sizeof(tab) = %zu\n", sizeof(tab));
// affiche : sizeof(p) = 8
printf("sizeof(p) = %zu\n", sizeof(p));
// affiche : tab : 0x...9c
// 9c en hex → 156 en dec
printf("tab : %p\n", (void *)tab); // ou &tab[0]
// affiche : p : 0x...9c
// 9c en hex → 156 en dec
printf("p : %p\n", (void *)p);
// affiche : &tab[1] : 0x...a0
// a0 en hex → 160 en dec
printf("&tab[1] : %p\n", (void *)(&tab[1]));
// affiche : p + 1 : 0x...a0
// a0 en hex → 160 en dec
printf("p + 1 : %p\n", (void *)(p + 1));
// affiche : &tab[2] : 0x...a4
// a4 en hex → 164 en dec
printf("&tab[2] : %p\n", (void *)(&tab[2]));
// affiche : p + 2 : 0x...a4
// a4 en hex → 164 en dec
printf("p + 2 : %p\n", (void *)(p + 2));
Dans l’exemple, l’octet de poids faible de l’adresse du tableau (et de p) vaut 0x9c (156 en décimal).
Pour l’adresse de tab[1] (ou p + 1), il vaut 0xa0 (160), soit 4 octets plus loin (la taille d’un int étant de 4).
Pour l’adresse de tab[2] (ou p + 2), il vaut 0xa4 (164), soit encore 4 octets plus loin.
On peut donc parcourir un tableau avec un pointeur :
for (int *ptr = tab; ptr < tab + 3; ptr++) {
printf("%p %d\n", (void *)ptr, *ptr);
}
Sortie :
0x...9c 1
0x...a0 2
0x...a4 3
Tableaux et fonctions¶
Un tableau passé en paramètre d’une fonction est converti en pointeur vers son premier élément : la fonction ne connaît donc pas sa taille, qu’il faut passer en paramètre. Les deux écritures suivantes sont équivalentes :
void inverser_signes(int *tab, size_t n);
// ou :
void inverser_signes(int tab[], size_t n);
void inverser_signes(int *tab, size_t n) {
for (size_t i = 0; i < n; ++i) {
tab[i] = -tab[i];
}
}
// affichage : const car la fonction ne modifie pas le tableau
void afficher_tab(const int *tab, size_t n);
int tab[4] = {1, -2, 3, -4};
inverser_signes(tab, 4); // tab vaut {-1, 2, -3, 4}
Comme le tableau n’est pas copié, la fonction modifie directement le tableau de l’appelant.
Bonnes pratiques : utiliser size_t pour les tailles et const si la fonction ne modifie pas le tableau.
Pour un tableau 2D, il faut préciser la taille de la 2e dimension (le nombre de colonnes), pour que le compilateur sache calculer l’adresse de m[i][j] :
void afficher_mat(int m[][2], size_t lignes);
// ou (équivalent) : pointeur vers des tableaux de 2 int
void afficher_mat(int (*m)[2], size_t lignes);
int matrice[3][2] = {{1, 2}, {3, 4}, {5, 6}};
afficher_mat(matrice, 3);
Pointeurs de pointeurs¶
Un pointeur peut aussi pointer vers un pointeur :
int x = 5;
int *p = &x;
int **pp = &p;
printf("%d\n", **pp); // affiche 5
Un pointeur peut ne pointer vers rien : il vaut alors NULL (ou nullptr, à partir de C23) :
int *p1 = NULL;
int *p2 = nullptr; // à partir de C23 (GCC 13)
Il est prudent de tester qu’un pointeur n’est pas NULL avant de l’utiliser :
if (p1 != NULL) {
printf("%d\n", *p1);
}
Passage par valeur, passage par adresse¶
En C, les paramètres passés aux fonctions sont des copies des valeurs des variables données à la fonction. On parle de passage par valeur ou passage par copie :
int doubler(int a) {
return a * 2;
}
int main(void) {
int ma_variable = 5;
// ici ma_variable est copiée à l'appel de la fonction
// le résultat est retourné par la fonction
ma_variable = doubler(ma_variable);
return EXIT_SUCCESS;
}
Cependant, on peut vouloir modifier directement les variables (par exemple plusieurs à la fois), ou réserver la valeur de retour à un code d’erreur. On parle alors de passage par adresse :
void doubler(int *a) {
*a *= 2;
}
int main(void) {
int ma_variable = 5;
// ici on donne l'adresse de ma_variable (adresse qui sera copiée)
// le résultat est directement modifié par la fonction
doubler(&ma_variable);
return EXIT_SUCCESS;
}
Pointeurs de fonctions¶
Il est possible de créer des pointeurs sur fonction avec la syntaxe type_retour (*nom_pointeur)(types_parametres) :
int addition(int a, int b) {
return a + b;
}
int soustraction(int a, int b) {
return a - b;
}
int operation(int a, int b, int (*op)(int, int)) {
return op(a, b);
}
int main(void) {
// affiche : 2 + 3 = 5
printf("2 + 3 = %d\n", operation(2, 3, addition));
// affiche : 2 - 3 = -1
printf("2 - 3 = %d\n", operation(2, 3, soustraction));
return EXIT_SUCCESS;
}
Pointeur générique¶
void * est un pointeur générique vers un objet (mémoire de données) de n’importe quel type.
Vous rencontrerez beaucoup de fonctions prenant en entrée ou retournant en sortie un void *.
Par exemple, malloc et free, présentés plus loin (voir Gestion mémoire / Allocation dynamique) :
void *malloc(size_t size);
void free(void *ptr);
mallocprend en entrée une taille en octets et retourne un pointeur vers la mémoire allouée. Pour éviter de créer une fonctionmallocpour chaque type imaginable, elle retourne unvoid *, qui peut être converti vers n’importe quel type de pointeur ;free, de la même manière, prend en entrée un pointeur vers n’importe quel type pour libérer la mémoire pointée.
Les structures¶
Une structure permet de regrouper plusieurs variables (de types éventuellement différents) sous un même nom. C’est un moyen de créer ses propres types de données. Les variables d’une structure sont appelées champs ou membres de la structure.
On utilise le mot-clé struct :
struct Point {
int x;
int y;
};
Cela déclare un nouveau type struct Point avec deux champs, x et y, auxquels on accède avec l’opérateur ..
struct Point p1;
p1.x = 3;
p1.y = 4;
// ou initialisation directe
struct Point p2 = {5, 6};
// ou initialisation avec les noms de champs :
struct Point p3 = {.x = 5, .y = 4};
// accès
printf("x : %d, y : %d\n", p2.x, p2.y);
Lorsqu’une structure est passée à une fonction, celle-ci est copiée :
// copie p à chaque appel
void afficher_point(struct Point p) {
printf("x = %d, y = %d\n", p.x, p.y);
}
Pour une petite structure comme Point (8 octets), cette copie ne coûte presque rien.
En revanche, pour les structures volumineuses, ou pour les modifier, on passe plutôt un pointeur (const si on ne modifie pas la structure), afin d’éviter une surconsommation de mémoire et une perte de temps de calcul :
// passage par adresse, seul le pointeur est copié
void afficher_point(const struct Point *p) {
printf("x = %d, y = %d\n", p->x, p->y);
}
void deplacer_point(struct Point *p) {
p->x += 1;
p->y += 1;
}
Avec un pointeur sur une structure, on n’utilise plus . pour accéder à un champ mais -> : p->x est équivalent à (*p).x.
Pour simplifier l’utilisation des structures, nous pouvons utiliser typedef :
typedef struct {
int x;
int y;
} Point;
Point p = {1, 2}; // on écrit simplement Point
Ici, la structure est anonyme : le type s’appelle uniquement Point et struct Point n’existe pas.
Pour pouvoir utiliser les deux noms (nécessaire par exemple pour une structure qui contient un pointeur vers elle-même), on donne aussi une étiquette (tag) à la structure :
typedef struct Point {
int x;
int y;
} Point;
struct Point p1 = {1, 2}; // les deux écritures
Point p2 = {3, 4}; // désignent le même type
typedef struct Noeud {
int valeur;
struct Noeud *suivant; // Noeud n'est pas encore défini ici
} Noeud;
Une structure peut contenir d’autres structures :
typedef struct {
Point coin_haut_gauche;
Point coin_bas_droit;
} Cadre;
Cadre c = {{0, 0}, {10, 5}};
On peut créer des tableaux de structures :
Point points[3] = {{0, 0}, {1, 2}, {2, 4}};
for (int i = 0; i < 3; i++) {
printf("(%d, %d)\n", points[i].x, points[i].y);
}
Gestion mémoire / Allocation dynamique¶
L’allocation de mémoire dynamique permet de demander au système de la mémoire à l’exécution, dont on ne connaît généralement pas la taille à la compilation. Grâce à l’allocation dynamique, nous pouvons adapter la quantité de mémoire au moment de l’exécution en fonction des besoins. Par exemple, le nombre de livres d’une bibliothèque n’est pas connu au moment de la compilation du programme de gestion de l’inventaire et pourra varier au cours du temps. En allouant dynamiquement la mémoire, on peut se rapprocher au plus près des besoins exacts de l’utilisateur.
On utilise la bibliothèque <stdlib.h> qui propose ces fonctions :
malloc: alloue un bloc de mémoire non initialisé ;void *malloc(size_t size);
size: nombre d’octets à demander au système.
mallocretourne un pointeur vers la mémoire allouée dynamiquement sur le tas (heap). En cas d’erreur, retourneNULL(sous POSIX,errnocontient alors le code d’erreur).int *tab = malloc(5 * sizeof(*tab)); // ou : // int *tab = malloc(5 * sizeof(int)); // on demande alors un espace mémoire de taille 5 * la taille d'un int // (ou de la taille d'un élément pointé par tab, donc un int) // si malloc renvoie NULL, alors l'allocation ne s'est pas bien passée if (tab == NULL) { // ou if (!tab) // dans ce cas malloc écrit la raison de l'erreur dans errno (sous POSIX) // et nous pouvons l'afficher avec perror puis quitter avec exit // (voir la section Gestion des erreurs) perror("Erreur malloc"); exit(EXIT_FAILURE); } // on peut ensuite accéder aux éléments comme avec un tableau classique for (int i = 0; i < 5; i++) { tab[i] = i * 10; } // lorsque l'on n'a plus besoin de la mémoire allouée // on l'indique au système qui va la libérer // avec free free(tab); // tab pointe alors sur une partie de la mémoire qui ne lui appartient plus // il est préférable de le faire pointer sur NULL // pour éviter d'avoir un "dangling pointer" // (pointeur qui référence une zone invalide) tab = NULL;
mallocdemande la taille de la mémoire à allouer et retourne un pointeur vers l’adresse du début de cette mémoire. La taille peut être passée en indiquant le type,taille * sizeof(int): on demande alorstailleblocs de 4 octets (taille d’unintsur les plateformes utilisées dans le cours). Sitaille = 5, on demande5 * 4 = 20 octetsconsécutifs en mémoire.mallocn’initialise pas la mémoire (son contenu est indéterminé) : il faut donc initialiser les valeurs avant de les utiliser.Les appels à
perroretexitsont détaillés dans la section Gestion des erreurs.calloc: alloue et initialise à zéro ;void *calloc(size_t nmemb, size_t size);
nmemb: nombre d’éléments dans le tableau ;size: taille des éléments en octets (taille d’un seul élément).
callocretourne un pointeur vers le tableau alloué dynamiquement sur le tas (heap) et dont les valeurs sont initialisées à zéro. En cas d’erreur, retourneNULL(sous POSIX,errnocontient alors le code d’erreur).int *zeros = calloc(5, sizeof(*zeros)); // 5 entiers à 0
callocdemande le nombre d’éléments puis la taille d’un élément et retourne un pointeur vers un bloc mémoire où tous les bits sont à zéro.realloc: réalloue un bloc (agrandir/réduire) ;void *realloc(void *ptr, size_t size);
ptr: zone de la mémoire à modifier ;size: nouvelle taille de la zone pointée parptr.
reallocretourne un pointeur vers la mémoire modifiée (peut-être déplacée en mémoire :realloccopie alors lui-même les données). En cas d’erreur, retourneNULL(sous POSIX,errnocontient alors le code d’erreur).int *tab = malloc(5 * sizeof(*tab)); // ... on travaille avec tab et nous avons besoin de plus de ressources // on crée un autre pointeur qui va uniquement servir à vérifier que // tout se passe bien lors de l'allocation mémoire // realloc demande l'adresse de la mémoire à modifier (diminuer ou agrandir) // et la taille que doit faire le bloc à la fin de l'opération int *tmp = realloc(tab, 10 * sizeof(*tab)); // si la réallocation a échoué, tmp contient NULL if (tmp == NULL) { // mais tab possède toujours la mémoire allouée de 5 int // il faut donc la libérer avant de terminer le programme free(tab); tab = NULL; exit(EXIT_FAILURE); } // si tmp != NULL alors la réallocation s'est bien passée // on peut alors faire pointer tab vers tmp tab = tmp; // pour éviter un futur dangling pointer tmp = NULL;
Dans le cas où on veut agrandir l’espace mémoire, la réallocation peut :
échouer et retourner
NULL(plus assez de place en mémoire, par exemple) ;réussir à agrandir l’espace précédent en gardant la mémoire contiguë, sans avoir à le déplacer ;
ne pas avoir la place d’agrandir l’espace déjà alloué en gardant un bloc de mémoire contigu, et devoir donc déplacer les données vers une zone qui peut tout contenir de manière contiguë. Dans ce cas, l’opération coûte plus cher en temps.
free: libère une zone précédemment allouée.void free(void *ptr);
ptr: zone de la mémoire à libérer.
Libère la zone mémoire pointée par
ptrqui doit avoir été allouée avecmalloc,callocourealloc. Sifree(ptr)a déjà été appelé sur la mémoire, le comportement est indéfini.Important : toujours libérer avec
free()quand on n’a plus besoin de la mémoire. Attention cependant à ne pas la libérer deux fois (comportement indéfini).Une fuite mémoire se produit lorsqu’un bloc alloué n’est pas libéré avec
free(). Cela peut épuiser la mémoire à long terme.Mettre ensuite le pointeur à
NULLest une bonne pratique pour bien indiquer que le pointeur ne pointe plus sur rien.Voir l’utilisation de valgrind ou AddressSanitizer pour la détection de fuites ou d’erreurs de manipulation de la mémoire.
free(tab); tab = NULL;
Quand on alloue dynamiquement, on reçoit un pointeur vers le bloc alloué.
Jusqu’ici, l’allocation que nous avons utilisée pour déclarer des variables dans le main ou les fonctions, par exemple avec int mon_int = 4;, est une allocation automatique sur la pile (stack), dont la durée de vie est limitée à celle du bloc où la variable est déclarée.
Avec malloc, nous avons accès à l’allocation dynamique, qui permet de stocker des données sur le tas (heap).
La durée de vie de la zone s’étend alors de l’appel à malloc jusqu’au free.
Le pointeur lui-même reste une variable locale comme les autres : il vit sur la pile, qu’il désigne une zone de la pile ou du tas. C’est seulement la zone pointée qui est allouée sur le tas.
Avertissement
Une fonction ne doit jamais retourner l’adresse d’une de ses variables locales : la variable est détruite à la sortie de la fonction et le pointeur renvoyé désigne une zone invalide (dangling pointer).
// À ne jamais faire !
int *mauvais(void) {
int x = 42; // vit sur la pile de la fonction
return &x; // x n'existe plus après le retour
}
// Correct : la zone allouée sur le tas survit à la fonction
int *init_tab_dynamique(size_t n) {
int *tab = malloc(n * sizeof(*tab));
if (!tab) {
return NULL;
}
for (size_t i = 0; i < n; ++i) {
tab[i] = (int)i;
}
return tab; // le free sera à faire par la fonction appelante
}
Propriété de la mémoire¶
Toute allocation a un propriétaire unique, responsable du free (on parle aussi d”ownership).
Une fonction qui renvoie de la mémoire allouée transfère cette responsabilité à l’appelant : il faut le documenter.
Une fonction qui reçoit un pointeur pour l’utiliser ou le modifier ne le libère pas.
Pour une structure allouée dynamiquement, on écrit souvent une fonction qui alloue et initialise (une fabrique) et une fonction qui libère :
typedef struct {
int x;
int y;
} Point;
// alloue et initialise : la propriété est transférée à l'appelant
Point *point_creer(int x, int y) {
Point *p = malloc(sizeof(*p));
if (!p) {
return NULL;
}
p->x = x;
p->y = y;
return p;
}
// utilise le point sans en prendre la propriété
void point_afficher(const Point *p) {
printf("(%d, %d)\n", p->x, p->y);
}
// libère le point et met le pointeur de l'appelant à NULL
void point_detruire(Point **pp) {
if (pp && *pp) {
free(*pp);
*pp = NULL;
}
}
int main(void) {
Point *p = point_creer(3, 4);
if (!p) {
perror("point_creer");
return EXIT_FAILURE;
}
point_afficher(p); // (3, 4)
p->y += 10;
point_afficher(p); // (3, 14)
// on donne l'adresse du pointeur pour permettre la mise à NULL
point_detruire(&p);
return EXIT_SUCCESS;
}
Nous pouvons aussi allouer des tableaux de structures :
#include <stdint.h> // SIZE_MAX
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int hauteur;
int largeur;
} Rectangle;
static void affiche_rectangle(const Rectangle *r) {
int perimetre = 2 * (r->hauteur + r->largeur);
int aire = r->hauteur * r->largeur;
printf("Rectangle %dx%d (périmètre : %d, aire : %d)\n",
r->hauteur,
r->largeur,
perimetre,
aire);
}
int main(void) {
size_t taille;
printf("Combien de rectangles voulez-vous ? ");
// force l'affichage immédiat du texte, qui ne se termine pas par un \n
fflush(stdout);
if (scanf("%zu", &taille) != 1) {
fprintf(stderr, "Entrée invalide.\n");
return EXIT_FAILURE;
}
if (taille == 0) {
// puts affiche un texte suivi d'un retour à la ligne dans stdout
puts("Aucun rectangle à traiter.");
return EXIT_SUCCESS;
}
// Protection contre le dépassement de capacité de taille * sizeof(Rectangle)
if (taille > SIZE_MAX / sizeof(Rectangle)) {
fprintf(stderr, "Taille trop grande.\n");
return EXIT_FAILURE;
}
Rectangle *rectangles = malloc(taille * sizeof(*rectangles));
if (!rectangles) {
perror("malloc");
return EXIT_FAILURE;
}
printf("Initialisation des rectangles :\n");
for (size_t i = 0; i < taille; ++i) {
printf("\t%2zu. Taille du rectangle (hauteur largeur) : ", i + 1);
fflush(stdout);
if (scanf("%d %d", &rectangles[i].hauteur, &rectangles[i].largeur) !=
2) {
fprintf(stderr, "Entrée invalide.\n");
free(rectangles);
return EXIT_FAILURE;
}
if (rectangles[i].hauteur <= 0 || rectangles[i].largeur <= 0) {
fprintf(stderr, "Dimensions positives requises.\n");
free(rectangles);
return EXIT_FAILURE;
}
}
puts("\nVoici vos rectangles :");
for (size_t i = 0; i < taille; ++i) {
printf("\t%2zu. ", i + 1);
affiche_rectangle(&rectangles[i]);
}
free(rectangles);
rectangles = NULL;
return EXIT_SUCCESS;
}
Cet exemple utilise fprintf(stderr, ...) et fflush(stdout) (voir la section Sorties), SIZE_MAX (valeur maximale d’un size_t, définie dans <stdint.h>) et le mot-clé static devant une fonction, qui la rend visible uniquement dans ce fichier (voir Autres mots-clés).
Dans le cas de tableaux dynamiques à deux dimensions (ou plus), deux choix s’offrent à nous :
avoir un tableau de pointeurs vers des tableaux qui ne sont pas forcément contigus en mémoire ;
avoir un pointeur sur un espace mémoire que l’on traite comme un tableau à plusieurs dimensions.
Dans le cas 1, on va donc allouer un premier pointeur sur pointeur sur notre type (int dans l’exemple) qui va nous donner les lignes du tableau, puis pour chaque pointeur sur le début de la ligne, allouer les colonnes :
#include <stdio.h>
#include <stdlib.h>
int main(void) {
// le nombre de lignes et colonnes du tableau
size_t nb_row = 5;
size_t nb_col = 10;
// tab2d va contenir les pointeurs vers les tableaux alloués dynamiquement
// tab2d[i] pointera vers les colonnes de la ligne i
int **tab2d = malloc(nb_row * sizeof(*tab2d));
if (!tab2d) {
perror("Erreur malloc row");
exit(EXIT_FAILURE);
}
for (size_t r = 0; r < nb_row; ++r) {
tab2d[r] = malloc(nb_col * sizeof(*tab2d[r]));
if (!tab2d[r]) {
perror("Erreur malloc col");
// l'allocation n'a pas pu se faire
// dans ce cas il faut libérer toute la mémoire déjà allouée
for (size_t i = 0; i < r; ++i) {
free(tab2d[i]);
}
free(tab2d);
exit(EXIT_FAILURE);
}
}
// ensuite nous pouvons initialiser le tableau et travailler avec
for (size_t r = 0; r < nb_row; ++r) {
for (size_t c = 0; c < nb_col; ++c) {
tab2d[r][c] = c + r * nb_col;
}
}
printf("Matrice :\n");
printf("Colonne | ");
for (size_t c = 0; c < nb_col; ++c) {
printf("%2zu ", c);
}
printf("\n");
for (size_t c = 0; c < nb_col * 3 + 11; ++c) {
printf("_");
}
printf("\n");
for (size_t r = 0; r < nb_row; ++r) {
printf("Ligne %2zu | ", r);
for (size_t c = 0; c < nb_col; ++c) {
printf("%2d ", tab2d[r][c]);
}
printf("\n");
}
// pour finir, nous devons libérer chaque zone mémoire allouée
for (size_t r = 0; r < nb_row; ++r) {
free(tab2d[r]);
}
// puis libérer le tableau de pointeurs sur les lignes
free(tab2d);
tab2d = NULL;
return EXIT_SUCCESS;
}
Sortie :
Matrice :
Colonne | 0 1 2 3 4 5 6 7 8 9
_________________________________________
Ligne 0 | 0 1 2 3 4 5 6 7 8 9
Ligne 1 | 10 11 12 13 14 15 16 17 18 19
Ligne 2 | 20 21 22 23 24 25 26 27 28 29
Ligne 3 | 30 31 32 33 34 35 36 37 38 39
Ligne 4 | 40 41 42 43 44 45 46 47 48 49
Nous nous retrouvons avec cette configuration, avec un premier tableau de pointeurs où chaque élément pointe vers un tableau d”int :
Dans le second cas, nous utilisons une matrice plate : on alloue un seul bloc mémoire et on le parcourt en utilisant le nombre de colonnes (longueur d’une ligne) :
#include <stdio.h>
#include <stdlib.h>
int main(void) {
// le nombre de lignes et colonnes du tableau
size_t nb_row = 5;
size_t nb_col = 10;
// flat_matrix va contenir la matrice aplatie
int *flat_matrix = malloc(nb_col * nb_row * sizeof (*flat_matrix));
if (!flat_matrix) {
perror("Erreur malloc");
exit(EXIT_FAILURE);
}
// ensuite nous pouvons initialiser le tableau et travailler avec
for (size_t r = 0; r < nb_row; ++r) {
for (size_t c = 0; c < nb_col; ++c) {
flat_matrix[c + r * nb_col] = c + r * nb_col;
}
}
printf("Matrice :\n");
printf("Colonne | ");
for (size_t c = 0; c < nb_col; ++c) {
printf("%2zu ", c);
}
printf("\n");
for (size_t c = 0; c < nb_col * 3 + 11; ++c) {
printf("_");
}
printf("\n");
for (size_t r = 0; r < nb_row; ++r) {
printf("Ligne %2zu | ", r);
for (size_t c = 0; c < nb_col; ++c) {
printf("%2d ", flat_matrix[c + r * nb_col]);
}
printf("\n");
}
// pour finir, on libère le tableau
free(flat_matrix);
flat_matrix = NULL;
return EXIT_SUCCESS;
}
Sortie :
Matrice :
Colonne | 0 1 2 3 4 5 6 7 8 9
_________________________________________
Ligne 0 | 0 1 2 3 4 5 6 7 8 9
Ligne 1 | 10 11 12 13 14 15 16 17 18 19
Ligne 2 | 20 21 22 23 24 25 26 27 28 29
Ligne 3 | 30 31 32 33 34 35 36 37 38 39
Ligne 4 | 40 41 42 43 44 45 46 47 48 49
Nous nous retrouvons avec cette configuration, où tout le tableau est contigu en mémoire :
Dans le cas de la matrice plate, l’équivalent de tab2d[r][c] est flat_matrix[r * nb_col + c].
On se décale de r fois le nombre de colonnes : quand r = 3, on se décale de 3 × 10 = 30, ce qui place au début de la ligne 3 ; on ajoute ensuite l’indice de colonne (quand c = 7, on avance de 7 à partir de l’indice 30 et on trouve 37).
Durée de vie et portée des variables¶
En C, chaque variable possède une portée (scope) et une durée de vie (lifetime) qui déterminent où elle peut être utilisée et combien de temps elle existe en mémoire.
Portée (scope)¶
La portée d’une variable définit la région du code où elle est accessible.
Variables locales, ou de bloc (déclarées sur la pile, allocation automatique) :
Déclarées à l’intérieur d’un bloc (corps d’une fonction, boucle, if, etc.), elles ne sont accessibles que dans ce bloc.
void ma_fonction(void) {
int x = 5; // x n'existe que dans ma_fonction
printf("%d\n", x);
}
int main(void) {
ma_fonction();
// printf("%d\n", x); // Erreur : x n'existe pas ici
return EXIT_SUCCESS;
}
Depuis C99, on peut déclarer des variables n’importe où dans un bloc, y compris dans l’en-tête d’une boucle for : leur portée est alors limitée à ce bloc.
for (int i = 0; i < 5; i++) {
int temp = i * 2; // temp n'existe que dans cette itération
printf("%d ", temp);
}
// i et temp n'existent plus ici
Variables globales (stockées dans le segment data si elles sont initialisées à une valeur non nulle, BSS sinon ; allocation statique) :
Déclarées en dehors de toute fonction, elles sont accessibles partout dans le fichier (et dans d’autres fichiers avec extern).
int compteur = 0; // variable globale (initialisée à 0 : segment BSS)
void incrementer(void) {
compteur++; // accessible ici
}
int main(void) {
compteur = 10; // et ici aussi
return EXIT_SUCCESS;
}
Avertissement
Les variables globales peuvent rendre le code difficile à maintenir. Privilégiez les variables locales quand c’est possible.
Durée de vie (lifetime)¶
La durée de vie définit combien de temps une variable existe en mémoire.
Durée de vie automatique (pile/stack) :
Par défaut, les variables locales ont une durée de vie automatique : elles sont créées à l’entrée du bloc et détruites à sa sortie.
void fonction(void) {
int a = 42; // a est créée ici
printf("%d\n", a);
} // a est détruite ici
Durée de vie statique :
Avec le mot-clé static, une variable locale conserve sa valeur entre les appels de fonction. Elle est initialisée une seule fois.
void compteur(void) {
static int count = 0; // initialisée une seule fois
count++;
printf("Appel n°%d\n", count);
}
int main(void) {
compteur(); // affiche : Appel n°1
compteur(); // affiche : Appel n°2
compteur(); // affiche : Appel n°3
return EXIT_SUCCESS;
}
Les variables globales ont aussi une durée de vie statique : elles existent pendant toute l’exécution du programme.
Durée de vie dynamique (tas/heap) :
Les zones allouées avec malloc existent jusqu’à ce qu’elles soient libérées avec free.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *p = malloc(sizeof(*p)); // création sur le tas
if (!p) {
perror("malloc");
return EXIT_FAILURE;
}
*p = 42;
// p est une variable locale (sur la pile)
// la zone pointée par p existe jusqu'à free(p)
free(p); // libération explicite
return EXIT_SUCCESS;
}
Type |
Portée |
Durée de vie |
Stockage |
|---|---|---|---|
Variable locale |
Bloc |
Automatique |
Pile (stack) |
Variable locale |
Bloc |
Statique (programme) |
Segment data/BSS |
Variable globale |
Fichier (ou programme avec |
Statique (programme) |
Segment data/BSS |
Variable dynamique |
Via pointeur |
Contrôlée ( |
Tas (heap) |
Préprocesseur¶
Le préprocesseur C agit avant la compilation proprement dite. Il traite toutes les directives commençant par #.
On y retrouve :
#include: insère le contenu d’un fichier, que ce soit un en-tête d’une bibliothèque (standard ou externe) ou l’un de vos fichiers d’en-tête (.h) ;#define: définit des constantes ou des macros ;#ifdef,#ifndef,#if,#else,#endif: compilation conditionnelle, par exemple pour éviter les inclusions multiples d’un fichier (la directive#pragma oncepermet d’éviter les inclusions multiples en une ligne et est acceptée par la majorité des compilateurs).
my_super_functions.h :
1// soit utiliser pragma once :
2// #pragma once
3// soit utiliser ifndef (pas les deux) :
4#ifndef MY_SUPER_FUNCTIONS_H
5#define MY_SUPER_FUNCTIONS_H
6
7#define MY_MAGIC_NUMBER 42
8#define SQUARE(x) ((x) * (x))
9
10/**
11* @brief Calcule la somme des carrés de deux nombres
12*
13* @param a premier nombre
14* @param b second nombre
15* @return int a^2 + b^2
16*/
17int my_formula(int a, int b);
18
19/**
20* @brief Calcule la somme des carrés de deux nombres et y ajoute
21* une pincée de nombre magique
22*
23* @param a premier nombre
24* @param b second nombre
25* @return int a^2 + b^2 + MY_MAGIC_NUMBER
26*/
27int my_magic_formula(int a, int b);
28
29#endif // MY_SUPER_FUNCTIONS_H
my_super_functions.c :
1#include "my_super_functions.h"
2
3int my_formula(int a, int b) {
4 return SQUARE(a) + SQUARE(b);
5}
6
7int my_magic_formula(int a, int b) {
8 return SQUARE(a) + SQUARE(b) + MY_MAGIC_NUMBER;
9}
main.c :
1#include <stdio.h>
2#include <stdlib.h>
3
4#include "my_super_functions.h"
5
6int main(void) {
7 int a = 5;
8 int b = 8;
9 int result1 = my_magic_formula(a, b);
10 int result2 = my_magic_formula(result1, b);
11
12 printf("result = %d\n", result2);
13
14 return EXIT_SUCCESS;
15}
On compile en donnant tous les fichiers .c (les .h sont inclus par le préprocesseur) :
$ gcc -std=c2x -Wall -Wextra -pedantic main.c my_super_functions.c -o main
$ ./main
result = 17267
Gestion des erreurs¶
En C, il n’existe pas de mécanisme intégré comme les exceptions en C++, Java, TypeScript, etc. La gestion des erreurs repose sur plusieurs principes :
Valeurs de retour
La plupart des fonctions de la libc retournent une valeur spéciale en cas d’erreur (souvent
-1ouNULL). Exemple avecfopen, qui ouvre un fichier et renvoie unFILE *(les fichiers sont détaillés dans la partie Fichiers) :1FILE *f = fopen("fichier.txt", "r"); 2if (f == NULL) { 3 perror("fopen"); 4 exit(EXIT_FAILURE); 5}
Ici,
fopenretourneNULLsi le fichier n’existe pas.perroraffiche un message d’erreur basé surerrno, etexittermine le programme avec le code donné.errno
errno(défini dans<errno.h>) contient le code de la dernière erreur, positionné par les fonctions système et de nombreuses fonctions de la libc en cas d’erreur. Il se manipule comme une variable globale, mais c’est en pratique une macro qui désigne une valeur propre à chaque thread. Exemple :1#include <stdio.h> 2#include <stdlib.h> 3#include <errno.h> 4#include <string.h> 5 6int main(void) { 7 FILE *f = fopen("inexistant.txt", "r"); 8 if (f == NULL) { 9 fprintf(stderr, "Erreur %d : %s\n", errno, strerror(errno)); 10 // proche de perror("Erreur"), qui affiche "Erreur: message" 11 return EXIT_FAILURE; 12 } 13 fclose(f); // ferme le fichier 14 return EXIT_SUCCESS; 15}
Fonctions utiles :
perror("msg"): affiche surstderrmsgsuivi de la description de l’erreur courante ;strerror(errno): retourne une chaîne décrivant l’erreur.
Toute fonction appelée (y compris
printfoufprintf) peut modifiererrno, même quand elle réussit : si on veut utiliser la valeur plus tard, il faut la sauvegarder immédiatement (int err = errno;).Codes retour du programme
Un programme en C retourne un entier à sa fin via
returndansmainouexit:EXIT_SUCCESS(0) → exécution correcte ;EXIT_FAILURE(1) → échec.
Exemple :
if (ptr == NULL) { perror("Erreur malloc"); exit(EXIT_FAILURE); }
Bonnes pratiques :
Toujours vérifier la valeur de retour des fonctions critiques (
malloc,fopen,fork, etc.) ;Utiliser
perroroustrerrorpour des messages d’erreur explicites ;Écrire les erreurs sur
stderrplutôt questdout(fait automatiquement avecperror) ;Libérer les ressources (fichiers, mémoire) avant de quitter sur erreur.
Aléatoire¶
La fonction rand() (standard C, <stdlib.h>, man 3 rand) génère un nombre pseudo-aléatoire.
Sa qualité est limitée, mais elle est disponible partout où le langage C est supporté.
int rand(void);
Retourne un entier pseudo-aléatoire compris entre 0 et RAND_MAX (au minimum 32767, valeur dépendante de l’implémentation).
Avant utilisation, il est conseillé d’initialiser le générateur avec srand() :
void srand(unsigned int seed);
seed: graine pour une nouvelle séquence du générateur pseudo-aléatoire.
Avec une même seed donnée à srand, rand donnera la même séquence de nombres pseudo-aléatoires (sur une même libc : la séquence dépend de l’implémentation).
Pour toujours avoir le même résultat à l’exécution :
srand(42);
Pour avoir des résultats différents à chaque exécution, vous pouvez utiliser
time(NULL)(<time.h>,man 2 time), qui renvoie le nombre de secondes écoulées depuis 1970-01-01 00:00:00 +0000 (UTC).
Exemple :
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void) {
unsigned int seed = (unsigned int)time(NULL);
srand(seed);
// Nombre pseudo-aléatoire entre 0 et 99
int r = rand() % 100;
printf("%d\n", r);
return EXIT_SUCCESS;
}
Note
L’utilisation directe de l’opérateur modulo (rand() % n) introduit un biais si RAND_MAX + 1 n’est pas un multiple de n : certaines valeurs sortent un peu plus souvent que d’autres.
Normaliser ((double)rand() / ((double)RAND_MAX + 1) * n) évite de ne garder que les bits de poids faible, mais répartit le biais autrement sans le supprimer.
int rand_0_99(void) {
return (int)((double)rand() / ((double)RAND_MAX + 1) * 100);
}
Pour une distribution uniforme, on tire à nouveau tant que la valeur tombe dans la zone « en trop » (méthode du rejet).
Pour les exercices du cours, le biais de rand() % n est négligeable.
Autres mots-clés¶
En plus des mots-clés comme if, while, etc., le langage C propose des mots-clés spécifiques qui influencent la durée de vie, la portée ou le comportement mémoire des variables.
static :
Utilisé dans une fonction, une variable
staticconserve sa valeur entre les appels ;void compteur(void) { static int i = 0; i++; printf("Appel %d\n", i); }
Utilisé hors d’une fonction (devant une variable globale ou une fonction),
staticlimite la portée à un fichier source.
extern :
Permet de déclarer une variable ou une fonction définie dans un autre fichier. Il est souvent utilisé pour partager des variables globales entre fichiers.
// dans global.h
extern int compteur_global;
// dans main.c
#include "global.h"
int compteur_global = 0;
// dans autre.c
#include "global.h"
void incrementer(void) {
compteur_global++;
}
volatile :
Le mot-clé volatile signale au compilateur qu’une variable peut changer à tout moment, sans que le code courant ne l’indique explicitement. Cela désactive certaines optimisations (ex. conservation de la valeur dans un registre). Il est utilisé pour les interactions avec le matériel (registres de périphériques) ou pour les variables modifiées par un gestionnaire de signal (volatile sig_atomic_t, voir la partie sur les processus).
volatile ne suffit pas à synchroniser des threads (il ne garantit ni l’atomicité ni l’ordre des accès mémoire) : il faut pour cela utiliser _Atomic ou des mutex (voir la partie sur les threads).
volatile sig_atomic_t flag = 0;
void gestionnaire(int sig) {
(void)sig;
flag = 1; // modifié par un gestionnaire de signal
}
restrict :
restrict est utilisé avec des pointeurs pour indiquer que les zones qu’ils désignent ne se chevauchent pas, ce qui permet plus d’optimisations par le compilateur.
C’est une promesse faite au compilateur : si elle n’est pas tenue, le comportement est indéfini.
void addition(int *restrict a, int *restrict b, int *restrict c) {
for (int i = 0; i < 1000; i++) {
c[i] = a[i] + b[i];
}
}
Ici, le programmeur promet avec restrict que a, b et c pointent vers des zones de mémoire distinctes.
const :
Déjà abordé dans la section sur les variables, const signifie que la valeur ne peut pas être modifiée.
const améliore la sécurité et la clarté du code.
const int a = 5; // variable constante
const int *p = &a; // pointeur vers une valeur constante
int b = 5;
int *const q = &b; // pointeur constant vers une valeur (modifiable si non const)
void addition(const int *const restrict a,
const int *const restrict b,
int *const restrict c) {
for (int i = 0; i < 1000; i++) {
c[i] = a[i] + b[i];
}
}
Dans addition, les pointeurs a et b ne peuvent être modifiés, ni les valeurs qu’ils désignent.
Pour c, seules les valeurs pointées peuvent l’être.
Avec les restrict, le compilateur a l’assurance que a, b et c pointent vers des zones mémoire différentes.
inline :
Permet au compilateur d’insérer le code d’une fonction à l’endroit où elle est appelée, au lieu de faire un appel réel. Cela réduit le coût d’appel mais augmente la taille du binaire.
static inline int carre(int x) {
return x * x;
}
Avertissement
Sans static, une fonction inline définie dans un fichier ne fournit pas de définition « externe » : si le compilateur choisit de ne pas l’insérer (par exemple sans optimisation, -O0), l’édition de liens échoue (« référence indéfinie vers carre »).
Écrivez donc static inline, en particulier dans les fichiers .h.