Partie 1 - Bases de C¶
Support présentationCette partie a pour objectif de rafraîchir vos connaissances en C et de commencer à aborder les points que nous verrons dans la suite du cours ou de ce que vous avez pu commencer à voir dans d’autres cours (allocation de la mémoire sur le stack ou la heap, représentation de variables en binaire,…). Ce n’est pas 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 algorithmie et en programmation (peu importe le langage) et que vous savez utiliser un minimum le 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.
Malgré ses ≈50 ans, il reste très utilisé dans de nombreuses applications grâce à sa vitesse d’exécution et sa portabilité, et le restera très probablement pour de nombreuses années (voir Zig ou Rust qui se présentent comme des alternatives modernes au C, Rust étant le 3ème langage à rejoindre le développement du noyau Linux après l’assembleur et le C).
Il est notamment employé, entre autres, pour le développement de systèmes d’exploitation, de pilotes ou de systèmes embarqués, mais pas seulement.
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 fixes (<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 (2023)
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 un compilateur, qui génère d’abord un code objet (binaire intermédiaire), avant de l’assembler et de faire l’édition de liens pour produire 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, …)
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 à des comportements indéterminés
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)
2#include <stdio.h>
3#include <stdlib.h>
4
5int main(void) {
6
7 // booléen
8 bool b = true;
9 printf("bool b = '%b'\t(taille: %zu octet(s))\n", b, sizeof(b));
10
11 // caractère pouvant être signé ou non signé avec unsigned
12 // (attention : utiliser des apostrophes simples 'A' et non "A")
13 char c = 'A';
14 printf("char c = '%c'\t(taille: %zu octet(s))\n", c, sizeof(c));
15
16 // entier signé
17 // ou non signé avec unsigned
18 int i = 42;
19 printf("int i = %d\t(taille: %zu octet(s))\n", i, sizeof(i));
20
21 // entier non signé pour les tailles et index
22 // ou signé avec ssize_t
23 // (spécifique aux fonctions système et mémoire)
24 size_t s = sizeof(int);
25 printf("size_t s = %zu\t(taille: %zu octet(s))\n", s, sizeof(s));
26
27 // nombre à virgule flottante
28 // float et double
29 double d = 3.1415926535;
30 printf("double d = %1.2lf\t(taille: %zu octet(s))\n", d, sizeof(d));
31
32 // pointeur vers entier
33 int *p = &i;
34 printf("int* p = %p\t(pointe vers i = %d)\n", (void *)p, *p);
35
36 // pointeur générique (void*)
37 void *vp = &c; // peut pointer vers n'importe quoi
38 printf("void* vp = %p\t(pointe vers char c)\n", vp);
39
40 return EXIT_SUCCESS;
41}
L’opérateur sizeof retourne la taille en mémoire (en octets, ou bytes en anglais, un octet = 8 bits dont les valeurs sont 0 ou 1) de la variable ou du type donné.
Résultat (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'
L’output suivant montre les adresses et valeurs en mémoire pour ces variables :
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 x (1+mantisse) x 2^(exp-bias) (hors cas spéciaux).
L’alignement explique les frontières d’adresses « propres » (x4 pour int/float, x8 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 00le sens plus « naturel » pour un humain pour lire la valeur.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 ou réordonner légèrement pour respecter ces contraintes. Comme pour le
float fqui est placé à la suite duint32_t i32et avant leint64_t i64qui est pourtant déclaré ensuite.
Entrées - sorties¶
Sorties¶
Comme vu avec le programme « Hello World! » ou les variables et leurs types, 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, ... );
Elle affiche du texte formaté à l’aide d’un template (le texte donné comme premier argument) pouvant contenir des spécificateurs de format (%d par exemple) suivi de valeurs ou variables.
Voici quelques spécificateurs de format courants :
%c:char%d:int%f:float/double%lf:double(pourscanf)%s:char *%zu:size_t%p:void *(il faut caster le pointeur en(void *))
Il est possible d’ajouter un padding comme %04d pour ajouter autant de 0 qu’il faut pour que l’affichage prenne 4 caractères, ou %4d pour des espaces, voir exemple de la doc.
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, ... );
Elle récupère l’entrée utilisateur grâce au format demandé et à l’adresse de la variable où saisir la valeur.
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.
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.
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.
Les opérateurs¶
Les opérateurs en C sont les suivants :
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)
Comme en mathématiques, les opérateurs ont des priorités.
Les parenthèses peuvent être utilisées pour forcer l’ordre d’évaluation. En l’absence d’ordre clair entre opérateurs de même priorité, le choix dépend du compilateur et de ses optimisations ou, dans certains cas, l’ordre des opérandes est non spécifié et certaines combinaisons provoquent un comportement indéfini. Ce comportement peut entraîner des décalages importants dans les résultats avec les nombres flottants en fonction du compilateur et du niveau d’optimisation choisis.
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 un 8");
}
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 le mot-clé break, toutes les instructions sous le case sont exécutées.
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 ou passée :
break: sort immédiatement de la bouclefor (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 préciser le void quand c’est nécessaire.
On place souvent les prototypes de fonction en haut du fichier .c ou dans un fichier .h.
#include <stdio.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 0;
}
// Définition
int carre(int n) {
return n * n;
}
La première fonction appelée à l’exécution d’un programme en C est la fonction spéciale main.
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) contenant chacun des arguments passés au programme.
#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 doit toujours retourner un entier (int), généralement :
return 0;oureturn EXIT_SUCCESS;indique que le programme s’est terminé normalement.Un autre entier (souvent
return 1;oureturn EXIT_FAILURE;) peut signaler une erreur.
Les tableaux¶
Un tableau en C est une structure permettant de stocker plusieurs valeurs de même type en mémoire contiguë.
Déclaration d’un tableau d’entiers :
// tableau de 5 entiers
int tableau[5];
// avec initialisation
int notes[5] = {10, 12, 15, 9, 14};
int zéros[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 valeurs[] = {1, 2, 3, 4}; // taille = 4
La taille du tableau doit être connue à la compilation, elle doit donc être définie avec #define TAILLE 5 ou directement donnée dans la taille du tableau.
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 le stack (pile) avec une taille inconnue à la compilation (avec une variable, une entrée utilisateur, lecture dans un fichier, …). Ils peuvent donc causer des stack overflow (dépassement de la taille du stack, voir gestion mémoire). Ils sont peu portables et ne peuvent pas être utilisés dans des structures.
Si vous voulez un tableau de taille inconnue à la compilation, préférez l’allocation dynamique sur la heap (tas) 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 tableau[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)
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 :
Il ignore les espaces lors de la lecture de chaînes (
%ss’arrête au premier espace).Il peut laisser des caractères non lus dans le tampon d’entrée (
stdin), 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);
Elle lit une ligne depuis le flux (
stdinici) et la stocke dansbuffer.Lit jusqu’à
taille - 1caractères ou jusqu’à\n.Ajoute toujours
\0à la fin.Si le retour à la ligne est présent, il est conservé (
\n).
char nom[100];
printf("Entrez votre nom : ");
fgets(nom, sizeof(nom), stdin);
printf("Bonjour %s!", nom); // Attention, le \n est conservé !
Pour supprimer le retour à la ligne :
nom[strcspn(nom, "\n")] = '\0';
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 donner l’adresse d’une variable on utilise &, mon_pointeur = &ma_variable donne à mon_pointeur l’adresse de ma_variable.
Puis, on peut modifier la valeur à l’adresse pointée avec l’opérateur de déréférencement *, *p = 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);
Résultat :
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
Les pointeurs permettent 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 0;
}
Le nom d’un tableau décroît en pointeur vers son premier élément dans la plupart des expressions :
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
Le *(p + 1) pratique ce que l’on appelle l’arithmétique de pointeurs, on demande au compilateur d’avancer dans la mémoire d’une case 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, le tableau est à l’adresse 0x...9c soit ...156 en décimal, tout comme le pointeur p.
L’adresse à tab[1] ou p + 1 est à 0x...a0 soit ...160 en décimal, donc 4 octets plus loin (la taille d’un int étant de 4).
L’adresse à tab[2] ou p + 2 est à 0x...a4 soit ...164 en décimal, donc 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);
}
Affiche :
0x...9c 1
0x...a0 2
0x...a4 3
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 rien pointer : il vaut alors NULL (ou nullptr à partir de C23) :
int *p = NULL;
int *p = nullptr; // à partir de C23
Il est prudent de tester qu’un pointeur n’est pas NULL avant de l’utiliser :
if (p != NULL) {
printf("%d\n", *p);
}
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 0;
}
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 0;
}
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 0;
}
void * est 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 juste après :
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, la fonction retourne unvoid *qui peut gérer tous les typesfree, de la même manière, prend en entrée un pointeur vers n’importe quel type pour le libérer
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 que l’on peut accéder avec ..
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);
}
Il vaut donc mieux passer les structures par adresse avec l’aide d’un pointeur pour éviter une surconsommation de mémoire et de perte de temps de calcul :
// passage par adresse, seul le pointeur est copié
void afficher_point(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;
}
Dans le cas d’un pointeur sur struct, il ne faut plus utiliser le . pour accéder à la valeur d’un champ mais -> (qui 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}; // plus besoin d'écrire 'struct Point'
Une structure peut contenir d’autres structures :
typedef struct {
Point coin_haut_gauche;
Point coin_bas_droit;
} Rectangle;
Rectangle r = {{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¶
La mémoire dynamique permet d’allouer de la mémoire à l’exécution. 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é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 la variable globale errno // et nous pouvons l'afficher avec perror // voir la section gestion des erreurs pour plus d'informations 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 à initialiser et retourne un pointeur vers l’adresse du début de cette mémoire. La taille peut être passée en indicant le type,taille * sizeof(int)où on va demandertailleblocs de 4 octets (taille d’un int), sitaille = 5, on demande alors5 * 4 = 20 octetsconsécutifs en mémoire.mallocne modifie pas la mémoire allouée, il faut donc bien initialiser les valeurs avant de les utiliser.calloc: Alloue et initialise à zéroint *zeroes = calloc(5, sizeof(*zeroes)); // 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)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 pour agrandir l’espace déjà alloué en gardant un bloc de mémoire contiguë et devoir donc déplacer les données précédentes 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éeImportant : 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 sur plus 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é pour déclarer des variables dans le main ou les fonctions, par exemple avec int mon_int = 4;, est une allocation automatique sur le stack (ou pile en français) dont la durée de vie est limitée à celle du bloc où elle est déclarée.
Avec l’aide de malloc, nous avons accès à l’allocation dynamique qui permet de stocker des variables sur la heap (ou tas en français), grâce à ceci, la durée de vie de la variable est entre l’appel à malloc et sa libération avec free.
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 ? ");
// flush assure que l'affichage se fasse bien vu qu'il n'y a pas de retour à
// la ligne dans le printf
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 0;
}
// Garde contre overflow : 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 0;
}
Dans le cas de tableaux dynamiques à deux dimensions (ou plus), deux choix s’offrent à nous :
avoir un tableau de pointeur 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 pointeur sur les lignes
free(tab2d);
tab2d = NULL;
return 0;
}
Output :
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
Dans le second cas, nous utilisons une matrice plate, on déclare un seul espace mémoire d’un bloc et on utilise la taille (en particulier la taille d’une colonne) de la matrice pour le parcourir :
#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 0;
}
Output :
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
Dans le cas de la matrice plate, l’équivalent de matrix[r][c] est flat_matrix[r * nb_col + c ].
On se décale r fois du nombre de colonnes (quand r = 3, on se décale de 3 * 10 = 30 on se retrouve sur la ligne 3) puis on ajoute l’indice de la colonne (quand c = 7 partant de l’indice 30, on avance de 7 et on trouve le 37).
Préprocesseur¶
Le préprocesseur C agit avant la compilation proprement dite. Il traite toutes les directives commençant par #.
On peut retrouver les #include qui insèrent le contenu d’un fichier que ça soit d’une bibliothèque standard, externe ou l’un de vos fichiers d’en-tête (.h).
Les #define pour définir des constantes ou macros.
Les #ifdef, #ifndef, #if, #else, #endif pour donner des directives conditionnelles de compilation, par exemple pour éviter les inclusions multiples d’un fichier (la directive #pragma once permet en une ligne d’éviter les inclusions multiples et est acceptée par la majorité des compilateurs).
my_super_functions.h :
1// soit utiliser pragma once
2#pragma once
3// ou 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 My formula that compute the sum of the square of two numbers
12*
13* @param a first number
14* @param b second number
15* @return int a^2 + b^2
16*/
17int my_formula(int a, int b);
18
19/**
20* @brief My formula that compute the sum of the square of two numbers and add
21* some sprinkles of magic number
22*
23* @param a first number
24* @param b second number
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
3
4int my_formula(int a, int b) {
5 return SQUARE(a) + SQUARE(b);
6}
7
8int my_magic_formula(int a, int b) {
9 return SQUARE(a) + SQUARE(b) + MY_MAGIC_NUMBER;
10}
main.c :
1#include <stdio.h>
2
3#include "my_super_functions.h"
4
5int main(void) {
6 int a = 5;
7 int b = 8;
8 int result1 = my_magic_formula(a, b);
9 int result2 = my_magic_formula(result1, b);
10
11 printf("result = %d\n", result2);
12
13 return 0;
14}
Gestion des erreurs¶
En C, il n’existe pas de mécanisme intégré comme les exceptions en C++/Java/TypeScript/… 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: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é sur la variable globaleerrno.errno
errno(défini dans<errno.h>) est une variable globale modifiée par les fonctions système en cas d’erreur. 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 printf("Erreur %d : %s\n", errno, strerror(errno)); 10 return EXIT_FAILURE; 11 } 12 fclose(f); 13 return EXIT_SUCCESS; 14}
Fonctions utiles : -
perror("msg"): affichemsgsuivi de la description de l’erreur courante. -strerror(errno): retourne une chaîne décrivant l’erreur.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) { fprintf(stderr, "Erreur malloc\n"); 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 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);
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)(man 2 time) qui renvoie le nombre de secondes écoulées depuis 1970-01-01 00:00:00 +0000 (UTC).srand((unsigned) time(NULL));
Exemple :
#include <stdlib.h>
#include <time.h>
#include <stdio.h>
int main(void) {
srand((unsigned) time(NULL));
// Nombre pseudo-aléatoire entre 0 et 99
int r = rand() % 100;
printf("%d\n", r);
return 0;
}
Note
L’utilisation directe de l’opérateur modulo (rand() % n) introduit un biais si RAND_MAX+1 n’est pas multiple de n.
Pour une distribution plus uniforme, on peut normaliser :
int rand_0_99(void) {
return (int)((double)rand() / ((double)RAND_MAX + 1) * 100);
}
Autres mots-clés¶
En plus des mots clés comme if, while, … 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,
staticlimite la portée à un fichier source.
extern :
Permet de déclarer une variable ou une fonction définie dans un autre fichier. Elle est souvent utilisée 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. mise en cache). Utilisé pour les interactions avec le matériel ou les threads.
volatile int flag = 0;
void interruption(void) {
flag = 1; // modifié par une interruption matérielle
}
restrict :
restrict est utilisé avec des pointeurs pour indiquer qu’ils ne se chevauchent pas. Cela permet plus d’optimisations par le compilateur.
void addition(int * restrict a, int * restrict b, int * restrict c) {
for (int i = 0; i < 1000; i++) {
c[i] = a[i] + b[i];
}
}
restrict garantit ici 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 vers a et b ne peuvent pas être modifiés, ni les valeurs pointées, pour c seules les valeurs pointées peuvent être modifiées. Avec les restrict le compilateur a l’assurance que a, b et c pointent vers des zones mémoires 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.
inline int carre(int x) {
return x * x;
}