L'objectif d'une variable constante est de déclarer une valeur immuable qui ne peut pas être
modifiée après son initialisation, afin d'assurer l'intégrité des données et de prévenir les erreurs
potentielles.
/* Une variable constante
*/
#include <stdio.h>
intmain() { const intannee = 2017; // une variable constante annee = 2019; // tentative de modification d'une variable constante printf("C'est l'annee %d", annee); return 0; }
3.2. Les variables constantes
Erreur pendant la compilation
Les variables constantes sont des valeurs en lecture seule, et toute tentative de modification
de
leur valeur après leur initialisation entraîne une erreur de compilation pour préserver leur
immutabilité.
$ gcc bonjour.c
const.c: In function ‘main’:
const.c:5:8: error: assignment of read-only variable ‘annee’
annee = 2019;
3.2. Les variables constantes
#define, const et constexpr (C23)
#define TAILLE 100 /* texte remplacé, aucun type */
const int taille = 100; /* typé, mais pas une constante :
int t[taille]; est un VLA */
constexpr int MAX = 100; /* C23 : vraie constante typée */
int t[MAX];
#define : traité par le préprocesseur, aucun type, aucune portée. Utile pour la
compilation conditionnelle.
const signifie « je ne modifierai pas cet objet », pas « valeur
connue à la compilation ».
constexpr (C23) : constante typée, vérifiée, utilisable comme taille de tableau.
const char *p : le caractère est constant. char *const p : c'est le
pointeur. Lisez de droite à gauche.
3.3. La portée des variables
Une variable globale
Les variables globales sont visibles et modifiables depuis toutes les fonctions du programme.
Il
est généralement recommandé de limiter l'utilisation de variables globales et de les utiliser
avec précaution pour éviter des effets secondaires indésirables dans un programme complexe.
/* affiche un message à l'écran en utilisant une variable globale
*/
#include <stdio.h> intannee = 2017; // une variable globale
Dans la fonction main(), l'affichage de la valeur de annee est tenté avant sa déclaration et son initialisation, ce qui
génère
une erreur de compilation. Le compilateur ne sait pas ce qui est tenté d'être affiché avant que la
variable annee ne soit définie.
3.3. La portée des variables
Erreur pendant la compilation
$ gcc bonjour.c
bonjour.c: In function ‘main’:
bonjour.c:5:30: error: ‘annee’ undeclared (first use in this function)
5 | printf("C'est l'annee %d", annee);
| ^~~~
bonjour.c:5:30: note: each undeclared identifier is reported only once for each function it appears in
L'erreur de compilation indique que la variable annee n'a pas été
déclarée avant son utilisation dans la fonction main().
3.3. La portée des variables
Une variable locale
/* affiche un message à l'écran en utilisant une variable locale */ #include <stdio.h> intannee = 2017; // une variable globale intmain()
{ intannee = 2018; // une variable locale printf("C'est l'annee %d", annee); //affiche 2018 return 0; }
La variable annee déclarée en dehors de main() est
globale : visible depuis toutes les fonctions.
Celle déclarée à l'intérieur est locale : visible seulement dans
main().
Dans une portée, la variable locale a la priorité sur la variable globale du même
nom.
3.3. La portée des variables
Une variable locale
/* affiche un message à l'écran en utilisant une variable locale */ #include <stdio.h> intannee = 2017; // une variable globale
Il est important de noter que cette fonction effectue un passage par valeur, ce qui signifie
que
les valeurs originales de a et b
ne
seront pas modifiées en dehors de la fonction.
Après l'exécution de cette fonction, les valeurs pointées par *a et
*b ont été échangées de manière efficace grâce à l'utilisation de
pointeurs, et ces modifications sont visibles en dehors de la fonction, car elle utilise le
passage
par référence.
3.4. Le passage de paramètres
2.2. Passage par référence: un tableau
Le passage par référence d'un tableau est une manière de transmettre un tableau à une fonction
en
lui fournissant directement une référence (ou un pointeur) vers le tableau d'origine,
plutôt que de faire une copie du tableau.
Lorsqu'un tableau est passé en tant qu'argument à une fonction, il est automatiquement converti en
un
pointeur vers son premier élément. Cela signifie que charmessage[] est équivalent à char*message dans le contexte de cette fonction.
Remarque : L'erreur lors de la compilation est due à la déclaration
d'une
fonction avec un tableau multidimensionnel sans spécifier la taille de la deuxième dimension.
$ gcc tableau.c
tableau.c:3:21: error: array type has incomplete element type ‘int[]’
3 | void affichage( int tableau[][] ) {
| ^~~~~~~
tableau.c:3:21: note: declaration of ‘tableau’ as multidimensional array must have bounds for all dimensions except the first
tableau.c: In function ‘main’:
tableau.c:15:13: error: type of formal parameter 1 is incomplete
3.4. Le passage de paramètres
2.2. Passage par référence: un tableau
Pour corriger l'erreur, nous avons précisé la taille de la deuxième dimension.
intmain()
{ int**tableau, lignes = 2, colonnes = 10; tableau = calloc(lignes, sizeof(int *)); for ( inti = 0;
i < lignes; i++) { tableau[i] = calloc(colonnes, sizeof(int));
} for ( inti = 0;
i < lignes; i++) { for ( intj = 0;
j < colonnes; j++) { tableau[i][j] = i+j;
}
}
...
3.4. Le passage de paramètres
Passage par référence: un tableau
... affichage(tableau,
lignes,
colonnes); for ( inti = 0;
i < lignes; i++) { free(tableau[i]);
} free(tableau); return 0; }
Remarque : Le tableau tableau a été
passé
par référence à la fonction affichage, de sorte que toute modification apportée à tableau à
l'intérieur
de cette fonction serait également répercutée sur le tableau d'origine.
3.5. Préprocesseur
cercle.c
Le préprocesseur est la première étape de la compilation : il traite les directives et
transforme le code source avant que le compilateur ne commence son travail.
#define PI 3.14159
floataire(
floatrayon) { return(PI * rayon
* rayon);
}
#define définit la constante PI. Le préprocesseur fait une
substitution de texte : chaque occurrence de PI est remplacée par 3.14159.
Remarque : l'option gcc -E
montre le code après prétraitement.
3.5. Préprocesseur
Prototype (defs.h)
#define PI 3.1415926535
#include insère le contenu du fichier
"defs.h" dans le code source : ses définitions et ses macros
deviennent utilisables.
Ce fichier ne fait que définirPI. La page suivante montre ce qui se
passe quand cercle.c l'inclut et redéfinit PI de son
côté.
3.5. Préprocesseur
cercle.c
#include"defs.h" #ifndef PI // Si PI n'est pas défini #define PI 3.14159 #endif floataire(
floatrayon) { return(PI * rayon
* rayon);
}
Comme PI est déjà défini dans "defs.h", la valeur
3.14159 n'est pas utilisée. La raison n'est pas qu'une valeur soit plus précise que l'autre :
#ifndef PI est simplement faux, donc le
#define qui suit est ignoré. La première définition
rencontrée l'emporte, quelle que soit sa valeur.
3.5. Préprocesseur
Prototype (defs.h)
Erreur: PI est une macro et non une variable, et il n'est pas
modifiable.
La tentative de modification de la valeur de PI générera une erreur de compilation, car les macros
ne
peuvent pas être réassignées.
#include"defs.h" floataire(
floatrayon) {
PI = 3.14; return(PI * rayon
* rayon);
}
Erreur (compilation)
Error: lvalue required as left operand of assignment
PI = 3.14;
^
3.5. Préprocesseur
operators.h
int
somme(
int,
int); intnum = 20;
L'utilisation
#include"operators.h" // en-têtes(headers) #include"operators.h" // pas d'erreurs
Lorsqu'un fichier d'en-tête contient uniquement des déclarations, il n'y a pas d'erreur si le
fichier d'en-tête est inclus plusieurs fois. Les déclarations n'introduisent pas de données réelles
en
mémoire, elles spécifient simplement le type des fonctions ou des variables.
$ gcc operator.c
note: previous definition of ‘num’ was here
int num=20;
Lorsque le fichier d'en-tête "operators.h" est inclus deux fois, cela entraîne une double inclusion
de la
définition de
la variable num, ce qui n'est pas autorisé en C.
#include"operators.h" // en-têtes(headers) #include"operators.h" // 2eme fois, mais pas d'erreurs
À la deuxième inclusion, #ifndef
OPERATORS_H est faux : le contenu n'est pas inclus une seconde fois.
Le garde ne protège que dans un fichier .c. Un en-tête qui définitint num = 20; et qui est inclus par deux .c donne
multiple definition of `num' à l'édition de liens.
La règle : l'en-tête déclare (extern int num;), un seul .c
définit.
Pas de nom en double tiret bas : __OPERATORS_H__ est réservé à
l'implémentation.
L'affichage de ce programme dépend de la manière dont les membres de la structure sont alignés
en
mémoire par le compilateur. Les règles d'alignement peuvent varier en fonction de l'architecture
matérielle et du compilateur utilisé.
En général, les compilateurs tentent d'aligner les membres de la structure de manière à
optimiser
l'accès à la mémoire, en particulier pour les types de données couramment utilisés.
Cela signifie que les membres peuvent être décalés pour obtenir un meilleur alignement.
couleur1couleur2
3.6. Alignement en mémoire
Alignement de 4 octets
La taille totale d'une structure est déterminée par la somme des tailles de ses membres, mais en
tenant compte de l'alignement.
couleur1 = 8 octets : les trois
unsigned char occupent 3 octets, puis 1 octet de remplissage amène
compteur sur un multiple de 4.
couleur2 = 12 octets : rouge (1 octet),
3 octets de remplissage, compteur (4 octets), vert et
bleu (2 octets), puis 2 octets de remplissage final pour que la taille soit un
multiple de l'alignement (4).
Conclusion : à membres identiques, l'ordre de déclaration
change la taille. Déclarez du plus grand au plus petit pour limiter le remplissage.
L'utilisation des directives #pragma pack(push)
et
#pragma pack(1) dans le code vous permet de spécifier un
alignement
d'un octet pour la structure couleur3. Cela signifie que les
membres de
la structure ne seront pas alignés de manière traditionnelle, mais plutôt sur un seul octet, ce qui
minimise l'utilisation de la mémoire. Remarque: L'affichage de ce programme: 7
L'alignement en mémoire est important pour optimiser les performances, en particulier sur
certaines
architectures matérielles.
Pour aligner les structures sur un nombre spécifique d'octets (dans ce cas, 4 octets), des
octets de
remplissage, appelés "paddings," sont souvent ajoutés pour s'assurer que chaque membre est
correctement aligné.
Remarque: Utilisez l'option gcc
-Wpadded
pour savoir si une structure nécessite du padding pour être alignée.
alignof donne l'alignement exigé par un type, comme sizeof en donne la
taille.
alignas impose un alignement plus strict : ligne de cache (64 octets), instructions
SIMD, tampons partagés avec du matériel.
aligned_alloc (C11) : la taille doit être un multiple de l'alignement ; se libère avec
free.
3.6. Alignement en mémoire
static_assert: vérifier à la compilation
static_assert(sizeof(struct entete) == 14,
"en-tête BMP mal aligné");
static_assert vérifie une condition à la compilation : si elle est fausse, le
programme ne compile pas.
C'est la façon correcte de garantir qu'une structure décrivant un format de fichier ou un
paquet réseau a bien la taille attendue, plutôt que de le découvrir à l'exécution.
alignas, alignof et static_assert sont des
mots-clés depuis C23 ; en C11 il fallait inclure <stdalign.h> et
<assert.h>.
scptr->bleu = 0x01
modifie
également le membre bleu, mais à travers le pointeur scptr. La notation scptr->bleu
est exactement équivalente à (*scptr).bleu : les deux
accèdent au membre bleu de la structure pointée par
scptr. La deuxième affectation écrit
0x22 au même endroit.
L'opérateur -> combine les opérations de déréférencement et d'accès au membre
en
une seule étape. Cela rend le code plus lisible et réduit les risques d'erreurs de déréférencement.
Les
deux notations sont interchangeables.
Un membre d'une structure est modifié en utilisant une fonction qui prend un
pointeur
vers la structure en argument. Cette approche permet de manipuler directement la structure à
l'intérieur
de la fonction sans avoir besoin de renvoyer la structure modifiée, car les pointeurs permettent de
travailler avec la même instance de la structure.
La fonction nochange est censée modifier le membre
bleu
de la structure c pour lui attribuer la valeur 0x03. Cependant, la fonction est déclarée pour prendre la structure
c en tant que copie (par valeur) au lieu d'un pointeur. Par
conséquent,
lorsqu'elle est appelée dans la fonction main, une copie de la structure c1 est passée à la fonction nochange. Toute modification apportée à
cette
copie n'affecte pas la structure d'origine.
3.7. Les structures et les pointeurs
Une liste de couleurs simplement chaînée
Une liste simplement chaînée est une structure de données linéaire composée de nœuds, où chaque nœud
contient une valeur et une référence (pointeur) vers le nœud suivant. Elle est utilisée pour stocker
des
éléments de manière séquentielle, offrant une manipulation efficace des données en insérant ou
supprimant des éléments en temps constant, mais avec un accès moins efficace aux éléments au milieu
de
la liste.
3.7. Les structures et les pointeurs
Une liste de couleurs simplement chaînée
Chaque nœud de la liste contient également un pointeur vers le nœud suivant de la
liste, permettant de stocker et de naviguer à travers une séquence de couleurs. Cette structure est
souvent utilisée pour représenter une séquence de couleurs dans des applications graphiques ou de
traitement d'images.
Un pointeur cptr est utilisé pour parcourir la liste, et à chaque étape, la valeur
du
composant "bleu" de la couleur actuelle est affichée à l'aide de printf. La boucle continue jusqu'à
ce
que le pointeur atteigne la fin de la liste, ce qui permet de parcourir et d'afficher les composants
"bleu" de toutes les couleurs de la liste.
3.7. Les structures et les pointeurs
Une liste d'entiers simplement chaînée
3.7. Les structures et les pointeurs
Une liste d'entiers simplement chaînée
Chaque élément de la liste est représenté par la structure element, qui contient un numéro entier
(numero) et un pointeur vers l'élément suivant (suivant). Deux fonctions sont fournies : insertion
pour
ajouter un nouvel élément à la liste et parcours pour parcourir et afficher les éléments de la
liste.
Cela permet de construire et de manipuler une liste d'entiers simplement chaînée.
struct element{ unsigned int numero; struct element *suivant;
};
// insertion d'un élement dans une liste voidinsertion(structelement*, int);
// parcours de la liste voidparcours(struct element *);
Insertion d'un élément dans une liste simplement chaînée
L'objectif de cette fonction est d'insérer un nouvel élément dans une liste chaînée en créant un
nouvel
élément, en assignant une valeur à ce nouvel élément, et en ajustant les pointeurs pour l'insérer
correctement dans la liste existante, ce qui permet de modifier la structure de la liste chaînée.
voidparcours(structelement *premier) { structelement *elem = premier->suivant; // premier est une sentinelle : // son numero n'est jamais initialisé while(elem != NULL) { printf("%u\n", elem->numero); elem = elem->suivant;
}
}
3.7. Les structures et les pointeurs
Une liste de couleurs doublement chaînée
Une liste doublement chaînée a pour objectif de permettre la navigation dans une structure de données
linéaire de manière bidirectionnelle, offrant un accès à la fois vers l'élément précédent et
l'élément
suivant, ce qui facilite l'insertion, la suppression et la recherche efficace des éléments.
L'objectif de cette structure de données est de créer une liste doublement chaînée de couleurs, où
chaque
élément conserve des informations sur la couleur, un compteur, ainsi que des pointeurs vers
l'élément
précédent et l'élément suivant. Cela permet une navigation bidirectionnelle efficace et des
opérations
telles que l'insertion et la suppression d'éléments au sein de la liste chaînée.
while (1) { charstrnum[50]; if (fgets(strnum, sizeof(strnum), stdin) == NULL) break; if(strcmp(strnum, "FIN\n") == 0) { break;
}
// alloué après le test : sinon fuite structelement *elem = malloc(sizeof(*elem)); if (elem == NULL) break; if (sscanf(strnum, "%d", &elem->num) != 1)
{ free(elem); continue; } insertion_fin(&liste, elem);
}
parcourir_debut(&liste); parcourir_fin(&liste);
}
3.7. Les fonctions et les pointeurs
Les fonctions somme et soustraction effectuent des opérations d'addition et de soustraction
respectivement.
int
somme(
inta,
intb
) { returna
+ b;
}
int
soustraction(
inta,
intb
) { returna
- b;
}
3.7. Les fonctions et les pointeurs
Le pointeur de fonction "func" est utilisé pour sélectionner dynamiquement l'une de ces fonctions en
fonction de la valeur de l'opérateur, puis appelle la fonction appropriée pour effectuer le calcul.
Dans
cet exemple, op vaut '-' : c'est donc
soustraction qui est appelée, et le programme affiche
value: -10. Remplacez '-' par '+' et le même appel
func(20, 30) affiche value: 50 — sans qu'une seule ligne d'appel
ne change.
intmain() { int (*func)(int, int); //pointeur de function charop = '-'; intnum1 = 20, num2 = 30; if (op == '+') { func = somme;
} else { func = soustraction;
} printf("value: %d\n",func(20, 30));
}
3.8. Les erreurs: perror
/* Fichier: stats.c * la taille d'un fichier
* auteur: John Samuel */
intmain(intargc, char ** argv)
{ struct statsf; intstatus; if (argc < 2) { fprintf(stderr, "Usage: stats fichier\n"); return(EXIT_FAILURE);
} status = stat (argv[1], &sf); if (status == -1) { perror("Stats"); return(EXIT_FAILURE);
} printf("Taille: %jd octets\n", (intmax_t) sf.st_size); return 0; }
3.8. Les erreurs: perror
Le programme utilise la fonction stat pour obtenir des informations sur le fichier spécifié par
l'utilisateur. Les informations sont stockées dans la structure struct stat sf. Si la fonction stat
échoue, elle renvoie -1, et le programme affiche un message d'erreur à l'aide de perror et se
termine
avec le code de sortie "EXIT_FAILURE".
La compilation
$ gcc -o stats stats.c
L'exécution
$./stats stats.c
Taille: ... octets
$ echo $?
0
3.8. Les erreurs: perror
L'exécution
$ ./stats nostats
Stats: No such file or directory
$ echo $?
1
Le code se termine ensuite avec le code de sortie "1", qui indique généralement qu'une erreur s'est
produite pendant l'exécution du programme. Dans ce cas, l'erreur provient du fait que le fichier
spécifié en argument n'a pas été trouvé, d'où le message d'erreur "No such file or directory".
L'objectif de la fonction perror dans cet exemple est d'afficher un message d'erreur explicite à
l'utilisateur en cas d'échec lors de l'exécution de la fonction stat, en indiquant la nature de
l'erreur, comme "No such file or directory" dans le cas d'un fichier introuvable.
DIR *dirp = opendir(argv[1]); if (dirp == NULL) { perror("opendir"); return(EXIT_FAILURE);
}
3.10. Répertoire (dossier)
struct dirent * ent; while(1) { ent = readdir(dirp); if (ent == NULL) { break;
} printf("%s\n", ent->d_name);
}
closedir(dirp); return(0);
}
Ce programme a pour objectif de lister les fichiers et répertoires d'un répertoire spécifié par
l'utilisateur. Le programme utilise une boucle pour parcourir le contenu du répertoire. Il lit
chaque
entrée (fichiers ou sous-répertoires) du répertoire à l'aide de la fonction readdir. Si la fin du
répertoire est atteinte, la boucle s'arrête.
3.10. Programmation modulaire
La programmation modulaire est une approche de développement logiciel qui consiste à diviser un
programme
en modules indépendants, également appelés unités ou composants, chacun responsable d'une
fonctionnalité
ou d'une tâche spécifique.
Modules indépendants : Chaque module est conçu pour accomplir une tâche spécifique et est
généralement indépendant des autres modules. Il peut s'agir de fonctions, de fichiers source, de
bibliothèques ou de classes, selon la structure du programme.
Encapsulation : Les détails d'implémentation d'un module sont masqués aux autres parties
du
programme. Seules les interfaces publiques sont accessibles, ce qui permet de cacher la
complexité
interne et de prévenir les interférences non souhaitées.
Communication entre modules : Les modules communiquent généralement entre eux via des
interfaces bien définies, telles que des fonctions ou des structures de données partagées. Cela
permet une interaction contrôlée et prévisible entre les parties du programme.
3.10. Programmation modulaire
Réutilisation de code : Les modules autonomes peuvent être réutilisés dans d'autres
projets
ou parties du programme, ce qui accélère le développement et améliore la cohérence du code.
Lisibilité et maintenabilité : En divisant le programme en modules, il devient plus
facile de
comprendre, de déboguer et de maintenir le code. Chaque module ayant une responsabilité claire,
il
est plus simple d'identifier et de résoudre les problèmes.
Testabilité : Les modules autonomes peuvent être testés individuellement, ce qui facilite
la
création de tests unitaires pour chaque fonctionnalité du programme.
Éviter les redondances : La programmation modulaire permet d'éviter la duplication de
code en
regroupant des fonctionnalités similaires dans un seul module réutilisable.
3.11. Réseau: Architecture client-serveur
3.11. Réseau: Architecture client-serveur
Client
L'objectif est de créer un socket client pour établir une connexion avec un serveur distant via le
protocole TCP/IP en utilisant le port 8089. Le code configure l'adresse du serveur, crée un socket,
et
tente de se connecter au serveur distant. En cas d'échec, il affiche un message d'erreur.
3.11. Réseau: Architecture client-serveur
Client
#define PORT 8089 int main() { int socketfd; int bind_status; struct sockaddr_in server_addr, client_addr; /* Création d'un socket
* AF_INET: Protocoles Internet IPv4
* SOCK_STREAM: flux d'octets bidirectionnels, basés sur la connexion
*/
socketfd = socket(AF_INET, SOCK_STREAM, 0); if ( socketfd < 0 ) {
perror("Impossible d'ouvrir un socket"); return -1;
}
3.11. Réseau: Architecture client-serveur
Client
// détails de l'adresse du serveur
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT); // l'adresse à joindre, pas INADDR_ANY
inet_pton(AF_INET, "127.0.0.1",
&server_addr.sin_addr);
//Se connecter au serveur int connect_status = connect(socketfd, (struct sockaddr *)
&server_addr, sizeof(server_addr)); if ( connect_status < 0 ) {
perror("Impossible de se connecter au serveur"); return -1;
}
3.11. Réseau: Architecture client-serveur
Serveur
L'objectif est de créer un serveur socket en utilisant le protocole TCP/IP (IPv4), de configurer le
serveur pour accepter les connexions entrantes sur un port donné, d'écouter les demandes de
connexion
des clients et d'accepter ces connexions lorsqu'elles sont établies. Le serveur est configuré pour
être
capable d'accepter plusieurs connexions de clients en utilisant la fonction accept.
3.11. Réseau: Architecture client-serveur
Serveur
int main() { int socketfd; int bind_status; struct sockaddr_in server_addr, client_addr;
/* Création d'un socket
* AF_INET: Protocoles Internet IPv4
* SOCK_STREAM: flux d'octets bidirectionnels, basés sur la connexion
*/
socketfd = socket(AF_INET, SOCK_STREAM, 0); if ( socketfd < 0 ) {
perror("Impossible d'ouvrir un socket"); return -1;
}
3.11. Réseau: Architecture client-serveur
Serveur
int option = 1;
setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR,
&option, sizeof(option)); // détails de l'adresse du serveur
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = INADDR_ANY;
bind_status = bind(socketfd, (struct sockaddr *)
&server_addr, sizeof(server_addr)); if (bind_status < 0 ) {
perror("Impossible de se lier à un socket"); return -1;
}
3.11. Réseau: Architecture client-serveur
// Commencez à écouter le socket
listen(socketfd, 10); char data[1024];
socklen_t client_addr_len = sizeof(client_addr); int client_socket_fd = accept(socketfd,
(struct sockaddr *) &client_addr, &client_addr_len); if(client_socket_fd < 0 ) {
perror("Impossible d'accepter les demandes des clients"); return -1;
}
3.12. La chaine de compilation
Les étapes de la chaîne de compilation
Préprocesseur → langage intermédiaire → optimisation → génération de code natif
→ assemblage (code objet) → édition de liens. Chaque étape rejette ce qu'elle ne
sait pas traiter.
3.12. La chaine de compilation
1. Préprocesseur
Cette étape consiste à traiter les directives de préprocesseur contenues dans le code source, telles
que
les directives d'inclusion (#include) et de remplacement de macros (#define). Le résultat est
généralement un fichier source modifié avec ces directives résolues.
cercle.c
#ifndef PI #define PI 3.14159 #endif
floataire(
floatrayon) { return(PI * rayon
* rayon);
}
float aire(float rayon) {
return(3.14159 * rayon * rayon);
}
Le résultat affiche le code source après le traitement des directives de préprocesseur. Cela peut
inclure
des inclusions de fichiers, la résolution de macros et d'autres transformations avant la compilation
proprement dite.
3.12. La chaine de compilation
2. Langage intermédiaire
Ces étapes montrent comment le code source est transformé en diverses représentations intermédiaires
avant d'atteindre le code machine final. Chacune de ces étapes permet une analyse et une
optimisation du
code à différents niveaux.
$ gcc -v bonjour.c # les étapes importantes
$ gcc -save-temps -v bonjour.c # les fichiers *.i, *.s
$ gcc -fdump-tree-all bonjour.c
$ gcc -fdump-rtl-all bonjour.c
3.12. La chaine de compilation
2. Langage intermédiaire
Ce code généré montre la représentation GIMPLE de GCC : une forme intermédiaire à trois
adresses, produite après l'arbre de syntaxe abstraite (AST). Chaque expression y est
décomposée en opérations élémentaires (_1, _2...).
-O1 : optimisations de base, tout en gardant une bonne expérience de débogage.
-O2 : optimisations plus avancées ; meilleure performance, compilation plus
longue.
-O3 : plus agressif que -O2 ; peut augmenter la taille de l'exécutable.
-Os : réduit la taille du binaire, utile sous contrainte de stockage.
-Ofast : déconseillé (et déprécié depuis GCC 14). Il active
-ffast-math, qui autorise le compilateur à violer IEEE 754 : les résultats en
virgule flottante changent.
-Og : optimise en conservant des informations de débogage utiles.
3.12. La chaine de compilation
4. Génération de code natif
L'option -S indique au compilateur de produire le code assembleur au lieu de générer un exécutable.
Le
résultat de cette étape est un fichier avec l'extension .s, qui contient le code assembleur généré.
$ gcc -S bonjour.c
$ cat bonjour.s
3.12. La chaine de compilation
4. Génération de code natif et l'optimisation de code
intmain() { intnum = 2 + 3; return(0);
}
Compilation (Pas d'optimisations)
$ gcc -O0 -S somme.c
L'absence d'optimisation (-O0) signifie que le compilateur n'a pas appliqué
d'optimisations particulières, ce qui permet d'obtenir un code assembleur directement équivalent au
code
C source.
main:
.LFB0:
.cfi_startproc
xorl %eax, %eax
ret
.cfi_endproc
Compilation (optimisation: 2) :
L'optimisation de niveau 2 a permis au compilateur de reconnaître que l'addition 2 + 3 est constante
et
que la valeur de retour de la fonction est toujours 0. Par conséquent, le calcul inutile est
éliminé, ce
qui donne un code assembleur très efficace et court. Cette optimisation est typique des
optimisations de
constantes que le compilateur peut effectuer pour améliorer les performances du code.
Cela signifie que le programme compilé est conçu pour être exécuté sur une machine d'architecture 64
bits, spécifiquement une architecture x86-64.
3.12. La chaine de compilation
4. Génération de code
sur une machine d'architecture 64 bits
Cette commande indique au compilateur GCC de générer un exécutable 32 bits en utilisant
l'architecture
i686 (Intel 80386). L'option -m32 spécifie que l'exécutable doit être compilé en tant qu'application
32
bits.
$ gcc -march=i686 -m32 bonjour.c
Cette commande est similaire à la précédente, mais elle génère également le code assembleur (-S) pour
le
programme.
Cette commande compile le fichier source "client.c" en un fichier objet "client.o". L'option -c
indique
au compilateur de générer uniquement le fichier objet sans produire d'exécutable. Cela permet de
diviser
le processus de compilation en deux étapes.
$ gcc -c client.c
$ gcc -c color.c
Cette commande prend les fichiers objets "color.o" et "client.o" et les compile en un exécutable
nommé
"color". Le compilateur lie les fichiers objets pour créer l'exécutable final.
$ gcc -o color color.o client.o
Cette approche est couramment utilisée pour séparer la compilation en plusieurs étapes, ce qui peut
être
utile dans des projets plus importants où différentes parties du programme sont gérées séparément
avant
d'être liées ensemble pour former l'exécutable final.
3.12. La chaine de compilation
5. Code objet (Modifications et recompilation)
$ gcc -c client.c color.c
$ gcc -o client client.o color.o
$ vim client.c # Modification du fichier
$ gcc -c client.c
$ vim client.c # Modification du fichier
$ gcc -c client.c
$ gcc -o client client.o color.o
L'objectif de cette séquence de commandes est de compiler des fichiers source en fichiers objet, puis
de
recompiler le programme avec des modifications apportées au fichier source "client.c". Cela permet
de
mettre à jour le programme en reflétant les changements apportés au code source sans avoir à
recompiler
entièrement toutes les sources. Cela peut accélérer le processus de développement en évitant de
recompiler inutilement les parties du programme qui n'ont pas changé.
3.13. Optimisation du code
Mesurer avant d'optimiser
$ gcc -std=c17 -O2 -o prog prog.c # production
$ gcc -std=c17 -Og -g -o prog prog.c # déboguable
$ time ./prog
$ perf stat ./prog
$ perf record ./prog && perf report
-O0 aucune optimisation, -O2 le compromis habituel, -O3 plus agressif,
-Os la taille, -Og le débogage. -flto optimise entre fichiers.
Une intuition sur les performances est fausse plus souvent que juste : mesurez d'abord,
avec les options de production.
Une fonction qui représente 2 % du temps ne peut pas faire gagner plus de
2 %.
3.13. Optimisation du code
L'ordre des priorités
L'algorithme et la complexité. Passer de O(n²) à O(n log n) gagne davantage que toutes
les optimisations de bas niveau réunies. C'est presque toujours ici que se trouve le
problème.
Les accès mémoire. Un défaut de cache coûte des centaines de cycles, une addition en
coûte un. Parcourir un tableau contigu est bien plus rapide que suivre une liste chaînée
dispersée en mémoire, même avec le même nombre d'opérations.
Le travail évitable. Allocations répétées, entrées-sorties non tamponnées, appels
système dans une boucle, calculs invariants recalculés à chaque tour.
Les détails d'instructions. En dernier, et seulement si la mesure le justifie.
Remarque : parcourir un tableau à deux dimensions ligne par ligne
(l'ordre de rangement en C) au lieu de colonne par colonne peut changer le temps d'exécution d'un
facteur important, sans modifier une seule opération.
À -O2 et -O3, le compilateur déroule les boucles, élimine le code mort et
vectorise lui-même, c'est-à-dire produit des instructions SIMD.
-fopt-info-vec-missed dit ce qu'il n'a pas pu vectoriser, et pourquoi.
const et restrict ne sont pas décoratifs : ils débloquent des optimisations
impossibles autrement.
Écrire du SSE ou de l'AVX à la main est un dernier recours : non portable, difficile à
maintenir, souvent battu par le compilateur.
-march=native optimise pour la machine qui compile : le binaire peut ne
pas démarrer ailleurs.
3.13. Optimisation du code
Optimisation et comportement indéfini
int lire(struct s *p) {
int x = p->a; /* déréférencement */
if (p == NULL) /* ce test peut être supprimé */
return -1;
return x;
}
L'optimiseur suppose que le programme ne contient aucun comportement indéfini. Ici, le
déréférencement prouve que p n'est pas nul : le test devient inutile.
Conséquence : un programme avec un comportement indéfini peut
fonctionner en -O0 et échouer en -O2.
La bonne réponse n'est jamais de baisser le niveau d'optimisation, mais de supprimer le
comportement indéfini.
3.14. Sécurité
Le comportement indéfini est la cause racine
Un comportement indéfini (UB) est une situation pour laquelle la norme n'impose rien : le
compilateur a le droit de produire n'importe quoi.
Accès en dehors des limites d'un tableau ou d'un bloc alloué
Utilisation de mémoire après free, ou double libération
Lecture d'une variable ou d'un tampon non initialisé
Déréférencement d'un pointeur nul ou invalide
Débordement d'un entier signé, décalage d'un nombre de bits invalide
Accès concurrent non synchronisé à la même donnée
Le point essentiel :la sécurité en C consiste d'abord à
éliminer les comportements indéfinis.
3.14. Sécurité
Vulnérabilités classiques (1/2): la mémoire
Débordement de tampon en lecture ou en écriture : la taille n'est pas vérifiée, ou
l'indice n'est pas borné. C'est la vulnérabilité historique du C.
Utilisation après libération et double libération : le pointeur survit à la
mémoire qu'il désigne. Souvent exploitable.
Lecture de mémoire non initialisée : peut divulguer le contenu d'une allocation
précédente.
Débordement d'entier avant une allocation : la taille calculée devient petite,
l'allocation réussit, l'écriture sort du bloc.
Ces quatre familles se détectent presque toutes avec AddressSanitizer et
valgrind, à condition d'exécuter le code sur les bons cas.
3.14. Sécurité
Vulnérabilités classiques (2/2): les entrées
Chaîne de format contrôlée par l'utilisateur : printf(saisie) permet de
lire et d'écrire en mémoire.
Données non validées venant du réseau, d'un fichier ou de l'utilisateur : longueurs,
indices et tailles annoncés doivent être vérifiés avant d'être utilisés.
Valeurs de retour ignorées : malloc, fopen,
read et snprintf peuvent échouer ou tronquer.
Code généré automatiquement : un assistant d'IA produit du code qui compile mais qui
oublie de vérifier les tailles, les valeurs de retour ou la durée de vie des données.
Règle générale : toute donnée venant de l'extérieur est hostile
jusqu'à preuve du contraire.
3.14. Sécurité
Chaînes de caractères: quoi utiliser
À éviter
Pourquoi
À utiliser
gets
aucune borne ; retiré du langage en C11
fgets
strcpy, strcat, sprintf
aucune borne
snprintf
scanf("%s", t)
aucune largeur maximale
fgets, ou "%9s"
strncpy
ne termine pas la chaîne
snprintf
Correction importante :strncpy n'est pas une version sûre de
strcpy — voir la page suivante.
3.14. Sécurité
Copier une chaîne correctement
char dst[8];
/* faux : dst n'est pas terminé par '\0' */
strncpy(dst, "Bonjour tout le monde", sizeof dst);
/* correct : toujours terminé, troncature détectable */
int n = snprintf(dst, sizeof dst, "%s", source);
if (n < 0 || (size_t) n >= sizeof dst)
; /* la chaîne a été tronquée */
Si la source est trop longue, strncpyn'écrit pas le '\0' ;
si elle est plus courte, il remplit le reste de zéros.
snprintf termine toujours et renvoie la longueur qu'il aurait fallu :
comparez-la à la taille du tampon.
strlcpy tronque et termine, mais n'existe que sous BSD et glibc ≥ 2.38.
3.14. Sécurité
Entiers: débordements et conversions
for (size_t i = n - 1; i >= 0; i--) /* ne se termine jamais */
... /* size_t est non signé */
Le débordement d'un entier signé est un comportement indéfini ; celui d'un entier
non signé boucle silencieusement. Les deux produisent des failles.
Une comparaison entre int et size_t convertit le int en
non signé : une valeur négative devient énorme.
Validez les longueurs avant de calculer avec, jamais après : le calcul qui déborde ne
laisse aucune trace.
_FORTIFY_SOURCE remplace memcpy, strcpy,
sprintf... par des versions vérifiées. Nécessite -O1 au
minimum.
-fstack-protector-strong produit le message
*** stack smashing detected *** rencontré au chapitre 2.
-fPIE -pie permettent le chargement à une adresse aléatoire (ASLR).
Ces protections limitent l'impact d'un bogue, elles ne le corrigent
pas.
Vérification systématique : compilez avec des avertissements stricts, testez les cas
limites et utilisez AddressSanitizer, UndefinedBehaviorSanitizer, Valgrind ou une analyse
statique quand ils sont disponibles.
3.15. Règles de codage
Les règles de codage sont des directives et des conventions qui définissent la
manière dont le code doit être écrit pour assurer une lisibilité, une maintenabilité et une
sécurité maximale.
Indentation et espacement :
Utilisez une indentation cohérente (généralement 4 espaces) et des espaces plutôt que
des tabulations.
Alignez le code de manière appropriée pour améliorer la lisibilité.
Nommage des identificateurs :
Utilisez des noms significatifs, ni trop courts ni trop longs.
Suivez une convention cohérente, telle que camelCase ou snake_case.
3.15. Règles de codage
Commentaires :
Commentez la logique complexe ou les parties délicates.
Utilisez des commentaires clairs et concis ; évitez ceux qui répètent ce que fait le
code.
Fonctions :
Divisez le code en fonctions modulaires ; limitez leur longueur pour qu'elles fassent
une seule chose.
Utilisez des prototypes et déclarez les fonctions avant de les utiliser.
Constantes :
Utilisez des constantes nommées ; évitez les valeurs littérales magiques.
3.15. Règles de codage
Gestion de la mémoire :
Libérez avec free toute mémoire allouée dynamiquement dès qu'elle n'est
plus nécessaire.
Chaque allocation doit être associée à une libération correspondante, sur
tous les chemins d'exécution.
Évitez les fonctions dangereuses :
Évitez gets(), strcpy(),
strcat() et sprintf(), qui ne vérifient aucune
borne.
Préférez fgets() et snprintf().
Attention :strncpy() n'est
pas une version sûre de strcpy() : elle ne termine pas la chaîne
si la source est trop longue.
Gestion des erreurs :
Gérez les erreurs et fournissez une information utile ; ne les ignorez jamais.
3.15. Règles de codage
Utilisation de la bibliothèque standard :
Privilégiez les fonctions de la bibliothèque standard plutôt que de réinventer la
roue.
Portabilité :
Écrivez un code qui fonctionne sur différentes plates-formes et différents
compilateurs.
Évitez de dépendre de comportements spécifiques à une plate-forme.
Modularité :
Décomposez le code en modules logiques réutilisables.
Évitez la dépendance excessive entre les modules.
3.15. Règles de codage
Convention d'écriture de code :
Suivez une convention de style établie pour le projet ou l'organisation.
Respectez la cohérence du style au sein du projet.
Utilisation responsable de l'IA :
Relisez le code généré comme s'il provenait d'une source non fiable.
Vérifiez les appels de fonctions, les allocations, les tailles de tampons, les
conversions de types et la gestion des erreurs.
Conservez seulement le code que vous pouvez expliquer, tester et corriger.
Indentation, espaces et longueur des lignes se règlent une fois dans un fichier
.clang-format versionné : ce n'est plus un sujet de relecture.
Ce qu'aucun outil ne peut vérifier, et qui est le vrai travail de relecture :
qui est propriétaire de chaque allocation, et qui la libère ;
ce que fait chaque fonction en cas d'erreur ;
la durée de vie de chaque pointeur et les préconditions.
Écrire ces contrats dans le fichier d'en-tête est plus utile que n'importe quelle règle de
style.
3.16. Makefile
Makefile
Makefile et make créent un exécutable à partir du code source. La dernière heure de modification des
fichiers décide de l'exécution ou non des commandes.
$ cat Makefile client: client.c color.c # création de l'exécutable client <TAB>gcc -o client client.c color.c
La règle client dépend des fichiers source client.c et color.c. Le texte
après # est un commentaire.
La ligne suivante commence par une tabulation (notée ici
<TAB>) : c'est l'action à effectuer pour construire la cible.
3.16. Makefile
Makefile
$ make
gcc -o client client.c color.c
Le programme "make" lit le fichier Makefile et trouve la règle "client." Il vérifie si les
dépendances
("client.c" et "color.c") ont été modifiées depuis la dernière exécution. Si elles l'ont été, ou si
l'exécutable "client" n'existe pas, alors "make" exécute l'action spécifiée."make" compile le
programme
en utilisant GCC, ce qui est indiqué dans le Makefile, et génère l'exécutable "client" en utilisant
les
fichiers source "client.c" et "color.c."
Attention : cette règle-ci recompile tous les fichiers
source dès que l'un d'eux change, parce qu'elle passe directement des .c à l'exécutable. Pour ne
recompiler que ce qui a changé, il faut des règles intermédiaires produisant des .o —
c'est l'objet de la page suivante.
3.16. Makefile
Makefile
$ cat Makefile client: client.o color.o # création de l'exécutable client
gcc -o client client.o color.o
L'objectif du Makefile est d'automatiser le processus de compilation d'un programme appelé "client."
Il
définit des règles pour créer l'exécutable "client" en compilant les fichiers source "client.c" et
"color.c," ainsi que leurs dépendances "client.h" et "color.h" si nécessaire.
3.16. Makefile
Makefile
L'objectif est de définir des variables pour le compilateur (CC) et les options de compilation
(CFLAGS).
Il définit également les fichiers objets (COBJS et SOBJS) pour le programme client et serveur, ainsi
que
les cibles "server" et "client" pour la compilation de ces programmes. Le Makefile permet ainsi de
compiler ces programmes en utilisant les règles et les variables prédéfinies.
all: $(SERVER) $(CLIENT) #création des exécutables
$(SERVER): $(SOBJS) # création de l'exécutable server
$(CC) -o $(SERVER) $(SOBJS)
$(CLIENT): $(COBJS) # création de l'exécutable client
$(CC) -o $(CLIENT) $(COBJS)
.c.o: # Compilation de tous les fichiers
$(CC) -c $*.c
Le Makefile a pour objectif de compiler deux programmes, "server" et "client," en utilisant des
fichiers
source correspondants. La règle "all" indique que les cibles "server" et "client" doivent être
construites. Pour construire ces cibles, le Makefile utilise les fichiers objets "SOBJS" et "COBJS"
respectivement. Enfin, la règle ".c.o" définit comment compiler tous les fichiers source en fichiers
objets en utilisant le compilateur "CC."
3.16. Makefile
Exécution d'un Makefile
$ vim server.c # Modification du fichier
$ vim client.c # Modification du fichier
$ vim color.c # Modification du fichier
$ make # Compilation de tous les fichiers et création d'un exécutable.
$ vim color.c # Modification du fichier
$ make # Compilation d'un seul fichier (color.c) et création d'un exécutable.
$ make # Ni compilation ni création d'un nouveau exécutable car aucun fichier n'a été modifié
Remarque : Lorsque vous exécutez make une troisième fois, il constate
que
aucun fichier n'a été modifié depuis la dernière compilation, donc aucune compilation n'est
effectuée,
et aucun nouvel exécutable n'est créé. Cette étape est importante pour économiser du temps et des
ressources lors du développement de logiciels, car seuls les fichiers pertinents sont recompilés.
Le problème : si un .h n'est pas listé comme
dépendance, le modifier ne déclenche aucune recompilation et l'exécutable mélange des versions
incompatibles.
-MMD fait produire par gcc un fichier .d listant tous les en-têtes
réellement inclus ; -include les fait lire par make.
Symptôme d'une dépendance manquante : un bogue qui disparaît après
make clean.
make compile, make check recompile avec les sanitizers et exécute,
make format normalise le style, make clean repart de zéro.
3.17. L'environnement de développement
Les outils autour du compilateur
$ bear -- make # produit compile_commands.json
$ clang-format -i src/*.c # met en forme
$ clang-tidy src/*.c # analyse statique
$ git diff # ce qui a changé depuis hier
clangd est un serveur de langage (LSP) : diagnostics en temps réel, complétion et saut à
la définition, dans VS Code, Vim, Emacs ou presque tout éditeur.
Il a besoin de compile_commands.json : bear -- make le produit depuis un
Makefile, CMake avec -DCMAKE_EXPORT_COMPILE_COMMANDS=ON.
git : comparer la version qui marchait et celle qui ne marche plus est la méthode de
débogage la plus rapide qui existe.
À chaque envoi, la machine recompile avec deux compilateurs, les avertissements en erreurs
et les sanitizers activés. La règle est appliquée par l'outil, pas par la bonne volonté du
relecteur.
3.17. L'environnement de développement
Un environnement reproductible
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y \
gcc clang make cmake gdb valgrind \
clang-tidy clang-format cppcheck bear
La version du compilateur détermine la norme par défaut, les avertissements et le texte
des messages : deux machines donnent deux résultats.
Solutions : WSL2 sous Windows, une machine virtuelle, ou une image Docker avec
une chaîne d'outils figée.
Notez la version des outils dans le README.md du TP.
3.18. Programmer en C avec un assistant d'IA
De quoi parle-t-on ?
Trois formes d'outils, aux usages différents :
un assistant conversationnel auquel on décrit un besoin ;
une complétion dans l'éditeur, qui propose la suite pendant la frappe ;
un agent qui modifie plusieurs fichiers, compile et exécute lui-même.
Ces modèles ont été entraînés sur de grandes quantités de code public, dont beaucoup de C
ancien, non portable ou incorrect. Ils reproduisent aussi bien les bonnes pratiques que
les mauvaises.
Ils produisent du C plausible, rapidement, dans n'importe quel style. Plausible ne veut
pas dire correct.
La question de ce chapitre n'est pas « faut-il les utiliser »
: ils font partie de l'environnement professionnel. La question est : que doit
garantir le développeur, et comment ?
3.18. Programmer en C avec un assistant d'IA
Pourquoi le C est un cas particulier
Ce que ces outils font bien : la syntaxe, les idiomes de la bibliothèque standard, le code
répétitif, l'explication et la traduction de code existant.
Ce qu'ils font mal — et c'est précisément la difficulté du C :
le comportement indéfini, qui ne produit ni erreur de compilation ni exception ;
la propriété de la mémoire : qui alloue, qui libère, une seule fois ;
la durée de vie des objets et la validité des pointeurs ;
les chemins d'erreur, rarement testés et rarement corrects ;
la disponibilité réelle d'une fonction : norme C, POSIX, GNU ou BSD ;
les hypothèses de taille, d'alignement et de boutisme.
3.18. Programmer en C avec un assistant d'IA
Pas de filet de sécurité
Pas de ramasse-miettes, pas de vérification des indices, pas de vérificateur d'emprunts, pas
de contrôle de type à l'exécution.
Le compilateur accepte un programme qui contient un comportement indéfini : ce n'est pas
son rôle de le refuser.
Les seules vérifications qui existent sont celles que vous ajoutez : options
d'avertissement, analyse statique, sanitizers, tests, relecture.
Conséquence : dans un langage sûr, un
modèle qui se trompe produit une erreur visible. En C, il produit une faille. C'est exactement
là que le développeur C reste nécessaire — et c'est ce que ce cours vous apprend à
faire.
3.18. Programmer en C avec un assistant d'IA
Erreurs fréquentes dans le code généré (1/2)
Valeur de retour de malloc, fopen ou readnon
vérifiée.
Fuite sur le chemin d'erreur : libéré à la fin, pas avant les return
anticipés.
sizeof(pointeur) utilisé comme si c'était la taille du tableau.
Retour de l'adresse d'une variable locale.
Décalage d'un rang : <= au lieu de <, ou pas de place pour le
'\0'.
Pointeur libéré deux fois quand deux chemins d'exécution se rejoignent.
3.18. Programmer en C avec un assistant d'IA
Erreurs fréquentes dans le code généré (2/2)
strncpy présenté comme la version sûre de strcpy : l'erreur la
plus reproduite.
scanf("%s", tampon) sans largeur maximale.
n * sizeof(x) passé à malloc sans vérifier le débordement.
Format non conforme : %d pour un size_t.
Fonctions inventées, ou propres à la glibc, à BSD, ou à une version récente.
Code vérifié pour Linux 64 bits seulement, alors que l'énoncé demandait du code portable.
3.18. Programmer en C avec un assistant d'IA
Un exemple à relire
Proposition d'un assistant : « écris une fonction qui lit une ligne et en renvoie une
copie ». Ce code compile sans erreur.
Exécuter instrumenté :-fsanitize=address,undefined, sur les cas limites
autant que sur les données réelles.
Écrire le test qui échoue avant la correction et réussit après.
Lire ligne à ligne : pour chaque pointeur, d'où vient-il, qui le libère, peut-il être
nul ?
Vérifier chaque fonction dans man 3 : arguments, valeur de retour,
conditions d'erreur.
La formule à retenir : le compilateur et les sanitizers sont vos
relecteurs. L'assistant est un collègue rapide qui n'exécute jamais son code.
3.18. Programmer en C avec un assistant d'IA
Formuler une demande précise
Trop vague :
« écris une fonction qui copie une chaîne »
Utilisable :
« en C17, une fonction qui renvoie une copie allouée
d'une chaîne ; l'appelant libère ; renvoie NULL si
l'allocation échoue ; aucun avertissement avec
-Wall -Wextra -pedantic ; cible Linux/glibc. »
Doivent être dits : la norme, les options, le contrat de propriété, la
convention d'erreur, la plateforme, les contraintes de l'énoncé.
Si vous n'êtes pas capable d'écrire la demande, vous ne serez pas capable de juger la
réponse.
3.18. Programmer en C avec un assistant d'IA
Responsabilité, licence, confidentialité
Vous signez le code. « L'assistant l'a écrit » n'est pas une explication
recevable.
Licences : du code généré peut reproduire des fragments issus de l'entraînement.
Renseignez-vous sur la politique de votre entreprise.
Confidentialité : ne transmettez jamais à un service externe du code appartenant à un
client ou à un employeur, ni un sujet d'examen.
Traçabilité : dans les domaines réglementés (médical, avionique, automobile, défense),
l'origine et la revue du code sont des exigences formelles.
En évaluation : n'utilisez que ce qui est autorisé, et soyez capable d'expliquer tout ce
que vous rendez.
3.18. Programmer en C avec un assistant d'IA
Le métier de développeur C
Où le C est encore incontournable : noyaux et pilotes, systèmes embarqués et temps
réel, réseaux, bases de données, calcul haute performance, et la couche basse de presque tous
les autres langages.
Le contexte change : la pression pour la sûreté mémoire est forte — recommandations
publiques sur les langages à mémoire sûre, arrivée de Rust dans le noyau Linux, réécriture de
composants critiques. Le C ne disparaît pas ; il est entouré, et les exigences sur le code C
augmentent.
Ce qui se déplace : la valeur passe de « écrire les lignes » à
garantir que les lignes sont correctes — lire, relire, tester, comprendre la machine
et la norme.
La compétence durable : savoir ce que la norme garantit et ce
qu'elle ne garantit pas. C'est précisément ce qu'aucun modèle ne peut garantir à votre place.
3.18. Programmer en C avec un assistant d'IA
Ce qui est évalué
En TP : utilisez les outils que vous voulez, assistants compris. Ils font partie de
l'environnement professionnel.
Au DS (théorie, 60 %) : sur machine, documents autorisés, sans internet.
À l'examen écrit de TP (pratique, 40 %) : sur papier, seul, sans documents ni
machine. Ce qui est évalué est ce que vous avez compris.
Les questions de l'examen de TP sont du même type que le travail fait en séance :
dire ce qu'affiche un extrait de code ;
dessiner l'état de la mémoire à un instant donné ;
trouver une erreur et proposer la correction ;
interpréter un message du compilateur, d'un sanitizer ou de gdb ;
relire une proposition d'assistant et la justifier ou la corriger.