Types, variables, opérateurs, contrôle, fonctions, tableaux, entrées-sorties, compilation
Département de Mathématiques et Informatique, Faculté des Sciences et Techniques, Université Cheikh Anta Diop de Dakar
Ce que la séance construit
La syntaxe du C, instruction par instruction, avec ce que chaque ligne fait des variables : types et tailles sur la machine du cours, conversions implicites, opérateurs et priorités, contrôle, fonctions et passage par valeur, tableaux et chaînes, printf et scanf ; puis la chaîne de compilation, et les comportements que la norme ne définit pas.
if, d’un switch et d’une boucle, ligne par ligne, dans une table de trace.gcc -std=c17 -Wall -Wextra, lire les messages, et reconnaître un comportement indéfini.D’où l’on vient, où l’on va
Amont. Algorithmique : variables, affectation, conditions, boucles, sous-programmes ; un premier contact avec le C.
Cette séance. Le langage C tel que gcc le compile sur la machine du cours : ce que chaque instruction fait des variables, ce que la norme garantit et ce qu’elle laisse indéfini.
Aval. Séance 2 : la représentation des entiers et des flottants, qui explique les conversions et les débordements vus ici ; Séance 3 : adresses, pointeurs, tableaux et chaînes en mémoire.
printf("char %zu %d %d\n", sizeof(char), CHAR_MIN, CHAR_MAX);
printf("short %zu %d %d\n", sizeof(short), SHRT_MIN, SHRT_MAX);
printf("int %zu %d %d\n", sizeof(int), INT_MIN, INT_MAX);
printf("unsig. %zu 0 %u\n", sizeof(unsigned), UINT_MAX);
printf("long %zu\n", sizeof(long));
printf("l.long %zu\n", sizeof(long long));
printf("float %zu\n", sizeof(float));
printf("double %zu\n", sizeof(double));
printf("l.dbl %zu\n", sizeof(long double));
printf("size_t %zu\n", sizeof(size_t));Résultat \(\rightarrow\)
char 1 -128 127
short 2 -32768 32767
int 4 -2147483648 2147483647
unsig. 4 0 4294967295
long 8
l.long 8
float 4
double 8
l.dbl 16
size_t 8
La norme ne fixe que des minimums et l’ordre char \(\le\) short \(\le\) int \(\le\) long \(\le\) long long ; sous Windows (LLP64), long mesure 4 octets.
Lecture. Chaque valeur a un type, qui fixe sa taille et son intervalle. sizeof est un opérateur, évalué à la compilation (sauf pour un VLA), de type size_t (%zu) ; sizeof(char) vaut 1. Ici Linux x86-64, modèle LP64 : int 4 octets, long et size_t 8.
Résultat \(\rightarrow\)
-56 200 -56 144
Résultat, avec -funsigned-char \(\rightarrow\)
200 200 -56 400
gcc ne signale rien avec les options du cours.
Lecture. La norme laisse à l’implémentation le choix du signe de char : signé sur x86-64 (gcc), non signé sur ARM sous Linux. 200 ne tient pas dans un char signé (\([-128, 127]\)) : la conversion est définie par l’implémentation, gcc réduit modulo 256 : \(200-256=-56\). Pour des octets, écrire unsigned char.
Valeurs relevées sous gdb à chaque ligne ; ·· : valeur indéterminée (variable pas encore initialisée).
Sortie \(\rightarrow\)
d >= u
40000 -25536 13333.00
Compilateur \(\rightarrow\)
s2_init.c:9:5: warning: 'stock' is used uninitialized [-Wuninitialized]
Résultat \(\rightarrow\)
0 0 1250
32767
Trois mots
Une déclaration annonce un nom et son type ; une définition réserve aussi la mémoire (int stock;) ; extern int total; n’est qu’une déclaration. Une initialisation donne la première valeur.
Variable automatique non initialisée
Sa valeur est indéterminée : la lire est un comportement indéfini. Ici 32767, ce qui restait dans la pile ; une autre exécution ou une autre option peut donner autre chose.
Lecture. total (portée de fichier) et ventes (static) sont des variables statiques : initialisées à 0 avant le début du programme. stock, automatique, ne l’est pas.
Sortie \(\rightarrow\)
5
10
18
Lecture. Appel : le taux local, non initialisé, masque le taux global.
Lecture. taux de afficher \(=10\) ; le global (18) n’est pas touché.
Lecture. Le bloc ouvre une portée : un troisième taux masque les deux autres.
Lecture. Le nom est cherché du bloc intérieur vers l’extérieur : 5.
Lecture. Fin du bloc : son taux disparaît, celui de afficher redevient visible.
Lecture. Fin de afficher : sa variable locale disparaît.
Lecture. Dans main, seul le taux de portée de fichier est visible.
Résultat de s2_macro \(\rightarrow\)
18 50000 11 25
Résultat de s2_static \(\rightarrow\)
appel 1, local 1
appel 2, local 1
appel 3, local 1
Lecture. appels (static) : initialisée une fois, elle dure tout le programme ; local est recréée à chaque appel. #define substitue du texte : CARRE(2 + 3) devient 2 + 3 * 2 + 3, 11.
Énoncé : ce qu’affiche ce fragment
octet ;lettre, n et t ;moyenne et vraie ;Correction
int : 260 ; converti en unsigned char, modulo 256 (défini) : 4.'C' ; sizeof n’évalue pas n++ : n reste 7, t \(=\) 4.double, 13.67.-3 est converti en size_t : 18446744073709551613 ; le test est faux : 0 (-Wsign-compare).Résultat \(\rightarrow\)
4 C 7 4
13 13.67
0
Résultat \(\rightarrow\)
2 h 15 min
-2 h -15 min
-2 15
2.00 2.25
Règle (C99 et suivantes)
Entre deux entiers, / tronque vers zéro, et (a / b) * b + a % b == a : le reste a le signe du dividende. % n’existe que pour les entiers.
Lecture. Une durée en minutes devient des heures et des minutes par / et %. duree / 60 vaut 2 avant d’être converti en double : le type des opérandes, pas celui de la variable, décide du calcul ; 60.0 rend la division flottante.
Valeurs relevées sous gdb après chaque ligne.
Sortie \(\rightarrow\)
16 6 6 7
Lecture. n \(=5\).
Lecture. ++n : n est incrémenté avant l’usage ; a reçoit la nouvelle valeur, 6.
Lecture. n++ : la valeur avant incrémentation (6) va dans b ; n passe à 7.
Lecture. n-- : c reçoit 7 et n redescend à 6.
Lecture. n += 10 équivaut à n = n + 10, n évalué une fois : 16.
Lecture. Une seule modification de n par expression : chaque ligne reste lisible et définie.
En orange : les opérandes réellement évalués.
Sortie \(\rightarrow\)
rupture
servis = 1
Lecture. stock \(=0\).
Lecture. demande \(=12\).
Lecture. servis \(=0\).
Lecture. stock != 0 est faux : && s’arrête là ; la division par zéro n’est jamais évaluée.
Lecture. stock == 0 est vrai : || est déjà vrai, ++servis n’est pas exécuté (servis reste 0).
Lecture. Le test vaut 1 : affichage.
Lecture. demande > 10 est vrai : && évalue l’opérande droit, ++servis s’exécute.
Lecture. Ordre garanti : de gauche à droite, et l’opérande droit seulement si nécessaire.
Résultat \(\rightarrow\)
7 4 4
fffffffa
40 45
2147483648
-5
Décalage d’un entier signé
E1 << E2 est indéfini si \(E1 \times 2^{E2}\) ne tient pas dans le type (C17 6.5.7 §4) : 1 << 31 l’est, 1u << 31 vaut 2147483648. >> d’un négatif est défini par l’implémentation (gcc : \(-5\)).
Lecture. Droits sur trois bits (lecture 0x4, écriture 0x2, exécution 0x1) : | ajoute, & teste, ^ inverse un bit, ~ inverse les 32 bits. << 3 multiplie par 8, >> 2 divise par 4. Les bits se manipulent sur des types non signés (Séance 2).
| opérateurs (priorité décroissante) | assoc. |
|---|---|
| () [] -> . x++ x– | \(\rightarrow\) |
| ! ++x –x +x -x (type) * & sizeof | \(\leftarrow\) |
| * / % | \(\rightarrow\) |
| + - | \(\rightarrow\) |
| << >> | \(\rightarrow\) |
| < <= > >= | \(\rightarrow\) |
| == != | \(\rightarrow\) |
| & | \(\rightarrow\) |
| ^ | \(\rightarrow\) |
| && | \(\rightarrow\) |
| ?: | \(\leftarrow\) |
| = += -= *= /= %= <<= >>= &= ^= | = |
| , | \(\rightarrow\) |
Compilateur \(\rightarrow\)
s3_priorites.c:5:21: warning: suggest parentheses around comparison in operand of '&' [-Wparentheses]
s3_priorites.c:7:16: warning: suggest parentheses around '+' inside '<<' [-Wparentheses]
Résultat \(\rightarrow\)
0 1 8 3
Lecture. & est moins prioritaire que == : masque & 4 == 4 se lit masque & (4 == 4), soit \(6\,\&\,1=0\) ; << est moins prioritaire que + : \(1 << 3 = 8\). - associe à gauche (\((10-4)-3\)), = à droite. Des parenthèses explicites lèvent l’ambiguïté et l’avertissement.
Énoncé. Donner a, b et c après chacune des lignes 4 à 8, en nommant la règle appliquée.
| ligne | a |
b |
c |
|---|---|---|---|
| début | 4 |
0 |
9 |
| 4 | 4 |
1 |
9 |
| 5 | 4 |
2 |
1 |
| 6 | 4 |
1 |
1 |
| 7 | 9 |
1 |
1 |
| 8 | 9 |
1 |
4 |
Correction
b++ vaut 0, puis b \(=1\) ; a += 0.a > 3 vrai, donc ++b est évalué (\(b=2\), \(2>1\)) ; c \(=1\).c vrai : || s’arrête, a++ n’est pas exécuté ; b \(=1\).<< avant | : \((4 << 1)\,|\,1 = 9\).% avant == avant ?: : \(9~\% 4 = 1\), vrai ; c \(= 9/2 = 4\).Compilateur \(\rightarrow\)
s4_ifelse.c:6:8: warning: suggest explicit braces to avoid ambiguous 'else' [-Wdangling-else]
Résultat \(\rightarrow\)
fin
Lecture. else se rattache au if sans else le plus proche, malgré l’indentation : rien ne s’affiche avant « fin ». while teste avant le corps (0 tour ici), do-while après (au moins un tour).
Valeurs relevées sous gdb ; gcc signale les deux chutes (lignes 8 et 10).
Sortie \(\rightarrow\)
tarif = 2500
Lecture. zone \(=2\).
Lecture. tarif \(=0\).
Lecture. switch évalue zone une fois et saute à l’étiquette case 2: ; le case 1 n’est pas exécuté.
Lecture. tarif \(=1000\) ; pas de break : l’exécution continue à la ligne suivante.
Lecture. Fall-through : le code du case 3 s’exécute aussi ; tarif \(=2500\) (-Wimplicit-fallthrough).
Lecture. break sort du switch ; default est sauté.
Lecture. Les étiquettes case sont des points d’entrée, pas des blocs : chaque cas finit par break.
Une rangée par ligne exécutée (les huit dernières) ; valeurs relevées sous gdb.
Sortie \(\rightarrow\)
k = 4, somme = 3
Lecture. somme \(=0\) ; k n’a pas encore de valeur.
Lecture. Première fois : k = 1, puis le test ; ensuite k++ puis le test, à chaque tour.
Lecture. k % 3 == 0 : faux.
Lecture. k * k > 10 : \(1 > 10\), faux.
Lecture. somme += k : somme \(=1\).
Lecture. k++ : k \(=2\) ; test k <= 10 : vrai.
Lecture. k % 3 == 0 : faux.
Lecture. k * k > 10 : \(4 > 10\), faux.
Lecture. somme += k : somme \(=3\).
Lecture. k++ : k \(=3\) ; test k <= 10 : vrai.
Lecture. k % 3 == 0 : vrai ; continue va suivre.
Lecture. continue : fin du tour, retour à l’incrémentation k++ ; la ligne 11 est sautée.
Lecture. k++ : k \(=4\) ; test k <= 10 : vrai.
Lecture. k % 3 == 0 : faux.
Lecture. k * k > 10 : \(16 > 10\), vrai ; break va suivre.
Lecture. break : sortie de la boucle, sans k++ ; k vaut 4.
Lecture. Après la boucle, k garde la valeur du tour interrompu.
Résultat \(\rightarrow\)
(1,1) (2,1) (3,1)
3 x 8 = 24
goto
Saut vers une étiquette de la même fonction. Deux usages admis : sortir de boucles imbriquées, et rejoindre un code de nettoyage commun en cas d’erreur. Pas de boucle par goto, pas de saut dans un bloc (Dijkstra, 1968).
Lecture. break ne quitte que la boucle sur j ; goto trouve quitte les deux boucles d’un coup : \(3\times8=24\).
Énoncé
i et le nombre de tours ;m.Correction
j \(=0\) : case 0, sans break : « DK » ; j \(=1\) : « K » ; j \(=2\) : default, « - » ; j \(=3\) : \(3\%3=0\), « DK » : DKK-DK.i : 10, 7, 4, 1, puis \(-2\) arrête : 4 tours.do-while : un tour, m \(=3\), puis \(3 > 10\) faux : 3.Résultat \(\rightarrow\)
DKK-DK
4 3
Résultat \(\rightarrow\)
5900
7
Compilateur, ancien(1, 2, 3) remplacé par moderne(1, 2, 3) \(\rightarrow\)
s5_prototype_err.c:8:20: error: too many arguments to function 'moderne'
Prototype
Déclaration d’une fonction avec les types de ses paramètres : le compilateur vérifie chaque appel qui la suit. La définition donne le corps.
Lecture. int ancien(); (sans prototype) : l’appel à trois arguments passe sans message, mais le nombre d’arguments diffère de celui de la définition : comportement indéfini (C17 6.5.2.2 §6) ; ici 7. Avec (void), le même appel est refusé.
Quoi
À l’appel, chaque argument est évalué et sa valeur est copiée dans le paramètre correspondant, une variable locale de la fonction appelée. En C, tous les arguments sont passés ainsi ; la fonction rend au plus une valeur, par return.
Pourquoi
La fonction travaille sur ses copies : elle ne peut pas modifier par erreur les variables de l’appelant, et chaque appel, récursif compris, a ses propres variables.
Quand ça échoue
Une fonction qui modifie son paramètre pour « rendre » un résultat ne change rien chez l’appelant ; il faut rendre la valeur, ou passer une adresse (Séance 3). Un tableau, lui, n’est pas copié : la fonction reçoit l’adresse de son premier élément.
Sous gdb, par exemple, compte en …ebec et solde en …ebcc : deux emplacements, 32 octets d’écart.
Sortie \(\rightarrow\)
dans crediter : 10000
apres crediter : 7500
Lecture. compte \(=7500\) dans main.
Lecture. Appel : les valeurs des arguments sont copiées dans les paramètres ; solde n’est pas compte.
Lecture. Seule la copie change : solde \(=10000\), compte reste 7500.
Lecture. crediter affiche sa copie.
Lecture. Fin de l’appel : solde et montant disparaissent, avec la modification.
Lecture. compte vaut toujours 7500 ; un résultat se transmet par return (ou une adresse, Séance 3).
Compilateur \(\rightarrow\)
s5_retour.c:12:1: warning: control reaches end of non-void function [-Wreturn-type]
Résultat \(\rightarrow\)
10000 1 -1
11
Fin sans return
Atteindre l’accolade finale d’une fonction non void, puis utiliser la valeur, est un comportement indéfini (C17 6.9.1 §12) ; exception : main, dont la fin vaut return 0; (C17 5.1.2.2.3). Ici signe(0) affiche 11, valeur rendue par le printf précédent (11 caractères), restée dans le registre du résultat.
Lecture. return termine la fonction et rend une valeur à l’appelant : credite rend 10000. Pour x == 0, aucun return n’est atteint : gcc le signale (-Wreturn-type).
Appel le plus récent en haut, comme la commande backtrace de gdb.
Sortie \(\rightarrow\)
25
Lecture. puissance(5, 2) : un appel, ses propres base et n ; main attend.
Lecture. n == 0 : faux.
Lecture. Appel récursif puissance(5, 1) : un deuxième n, au-dessus du premier.
Lecture. n == 0 : faux.
Lecture. Troisième appel, n \(=0\) : trois appels en cours.
Lecture. n == 0 : vrai, cas de base.
Lecture. return 1 : l’appel le plus récent se termine le premier.
Lecture. Retour dans l’appel 2 : \(5\times1=5\), rendu à l’appel 1.
Lecture. Retour dans l’appel 1 : \(5\times5=25\), rendu à main.
Lecture. r \(=25\) ; il ne reste que main.
Énoncé. (a) v et w après l’appel de f ; (b) pour chaque appel de g, n et la valeur rendue ; (c) le nombre maximal d’appels de g en cours.
Correction
x reçoit une copie de 5 ; x \(=10\) dans f seulement : v \(=\) 5, w \(=\) 11.n \(=1\), puis se terminent dans l’ordre inverse : 1, \(1+2=3\), \(3+3=6\), \(6+4=10\).g en cours au plus, chacun avec son n ; sortie : 5 11 10.| appel | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
n |
4 |
3 |
2 |
1 |
| rend | 10 |
6 |
3 |
1 |
Valeurs relevées sous gdb ; la disposition des octets en mémoire est l’objet de la Séance 3.
Sortie \(\rightarrow\)
16 4 100
Lecture. Quatre int contigus, sacs[0] à sacs[3] : l’indice part de 0.
Lecture. sacs[1] (85) est lu, \(85+15=100\) est écrit dans sacs[3].
Lecture. sizeof sacs \(=16\), sizeof sacs[0] \(=4\) : 4 éléments, calculé à la compilation.
Lecture. Indices valides : 0 à 3 ; sacs[4] serait hors du tableau (comportement indéfini).
Compilateur \(\rightarrow\)
s5_parametre.c:7:41: warning: 'sizeof' on array function parameter 't' will return size of 'int *' [-Wsizeof-array-argument]
Résultat \(\rightarrow\)
dans main : 20
dans somme : 8
total : 92
Lecture. Dans main, sizeof recolte \(=5\times4=20\). Passé à une fonction, un tableau n’est pas copié : t reçoit l’adresse du premier élément, et sizeof t donne la taille d’une adresse, 8. La taille se passe donc en paramètre (n). Le lien entre tableaux et adresses est l’objet de la Séance 3.
Contenu du tableau relevé sous gdb ; sous chaque case, le code du caractère en hexadécimal.
Sortie \(\rightarrow\)
Diourbel 8 10
Diou 4
Lecture. 8 caractères ('D' \(=\) 0x44), puis '\0' ; la dernière case est aussi mise à 0.
Lecture. strlen compte les caractères jusqu’au '\0', exclu : 8.
Lecture. %s affiche jusqu’au '\0' ; sizeof ville \(=10\), la taille du tableau.
Lecture. ville[4] = '\0' : le 'r' (0x72) devient 00.
Lecture. La chaîne s’arrête au premier '\0' : « Diou », longueur 4 ; « bel » reste dans le tableau.
int qte = 42;
long habitants = 18000000L;
double prix = 1250.5;
printf("[%d] [%5d] [%-5d] [%05d]\n", qte, qte, qte, qte);
printf("[%x] [%X] [%o] [%u]\n", qte, 255, 8, 3000000000u);
printf("[%ld] [%zu]\n", habitants, sizeof habitants);
printf("[%f] [%.2f] [%9.1f] [%e]\n", prix, prix, prix, prix);
printf("[%c] [%s] [%%]\n", 'J', "Joal");
printf("[%d]\n", prix);Résultat \(\rightarrow\)
[42] [ 42] [42 ] [00042]
[2a] [FF] [10] [3000000000]
[18000000] [8]
[1250.500000] [1250.50] [ 1250.5] [1.250500e+03]
[J] [Joal] [%]
[-136292928]
Compilateur \(\rightarrow\)
s5_printf.c:12:15: warning: format '%d' expects argument of type 'int', but argument 2 has type 'double' [-Wformat=]
Lecture. Largeur, cadrage, zéros, bases, précision. Chaque format attend un type (%ld : long, %zu : size_t). Un double lu par %d : comportement indéfini, signalé par -Wformat.
Lecture. scanf reçoit l’adresse de chaque variable (&sacs), pour y écrire ; %lf pour un double. « abc » n’est pas un entier : rien n’est lu, sacs garde 25.
| Addr | +0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 |
|---|---|---|---|---|---|---|---|---|
0x00 |
0e0e |
0000 |
0000 |
0000 |
1f0909 |
0000 |
0000 |
0000 |
0x08 |
1111 |
0000 |
0000 |
0000 |
ff |
7f |
4d4d |
6565 |
0x10 |
6464 |
006969 |
6e6e |
6161 |
0000 |
0000 |
0000 |
0000 |
Dump avant la ligne 3 (adresses abrégées), little-endian. ASCII : M 4d, e 65, d 64, i 69, n 6e, a 61.
Énoncé
pluie[0], pluie[1], pluie[2] ?quartier ; sa longueur, sa taille ?Correction
pluie[i] en \(4i\) : 0e 00 00 00 se lit 0x0000000e \(=14\), 09 00 00 00 \(=9\), 11 00 00 00 \(=17\) ; 0x0c, 0x0d : hors des variables.0x0e : 4d 65 64 69 6e 61 00, « Medina » ; strlen \(=6\), sizeof \(=10\).quartier[3] en \(\texttt{0x0e}+3=\texttt{0x11}\) : 69 devient 00 ; pluie[1] en 0x04 : \(14+17=31\), 1f.00.| phase | option | produit |
|---|---|---|
| préprocesseur | -E |
.i, sans # |
| compilation | -S |
.s, assembleur |
| assemblage | -c |
.o, objet |
| édition de liens | — | exécutable |
Terminal \(\rightarrow\)
$ gcc -std=c17 -E s6_phases.c -o s6_phases.i
$ wc -l s6_phases.i
550 s6_phases.i
$ gcc -std=c17 -S s6_phases.c
$ grep call s6_phases.s
call printf@PLT
$ gcc -std=c17 -c s6_phases.c
$ nm s6_phases.o
0000000000000000 T main
0000000000000000 D paquets
U printf
$ gcc s6_phases.o -o s6_phases && ./s6_phases
3 : 1062
Lecture. Le .i compte 550 lignes, presque toutes venues de <stdio.h>, sans directive (marqueurs de ligne seuls) ; le .s appelle printf. nm : T (code), D (donnée initialisée), U (non défini ici : printf, trouvé dans la bibliothèque C à l’édition de liens).
taxe.h
taxe.c
facture.c
facture2.c : plus int taux_tva = 20;
Terminal \(\rightarrow\)
$ gcc -std=c17 -Wall -c taxe.c facture.c
$ gcc taxe.o facture.o -o facture && ./facture
18 7080
$ gcc facture.o -o facture
facture.c:(.text+0xe): undefined reference to `ttc'
...
collect2: error: ld returned 1 exit status
$ gcc -std=c17 -Wall -c facture2.c
$ gcc taxe.o facture2.o -o facture2
/usr/bin/ld: facture2.o:(.data+0x0): multiple definition of `taux_tva'; taxe.o:(.data+0x0): first defined here
collect2: error: ld returned 1 exit status
Lecture. extern déclare sans définir. Sans taxe.o : undefined reference ; deux définitions : multiple definition.
Quoi
Un comportement indéfini est une construction pour laquelle la norme n’impose rien : débordement d’un entier signé, variable non initialisée lue, division par zéro, accès hors d’un tableau, double modification sans point de séquence (C17 3.4.3, annexe J.2). À distinguer du défini par l’implémentation (signe de char), documenté par le compilateur, et du non spécifié (ordre d’évaluation).
Pourquoi
Le compilateur produit un code rapide sur chaque machine en supposant que le programme n’exécute jamais une telle construction.
Quand ça échoue
Le programme peut sembler fonctionner, s’arrêter sur un signal, ou changer de résultat avec les options d’optimisation ; aucun résultat n’est garanti. -Wall -Wextra, -fsanitize=undefined et valgrind en détectent une partie.
Résultat \(\rightarrow\)
-2147483648 0
Résultat avec -fsanitize=undefined \(\rightarrow\)
-2147483648 1
Sortie d’erreur avec -fsanitize=undefined \(\rightarrow\)
s7_debord.c:10:9: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
s7_debord.c:5:14: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
Lecture. INT_MAX + 1 ne tient pas dans un int : comportement indéfini. Ici suivant vaut \(-2147483648\) (la Séance 2 dira pourquoi), et deborde rend 0 : gcc a remplacé x + 1 < x par « faux », puisqu’un débordement n’est pas censé arriver. Avec -fsanitize=undefined, le calcul est fait et signalé : 1. Sur unsigned, le calcul est modulo \(2^{32}\), défini.
Compilateur \(\rightarrow\)
s7_noninit.c:5:8: warning: 'quota' is used uninitialized [-Wuninitialized]
Résultat \(\rightarrow\)
grand quota
32767
Sortie d’erreur sous valgrind \(\rightarrow\)
Conditional jump or move depends on uninitialised value(s)
at 0x109179: main (s7_noninit.c:5)
Résultat avec -fsanitize=undefined \(\rightarrow\)
debut
Sortie d’erreur avec -fsanitize=undefined \(\rightarrow\)
s7_zero.c:8:25: runtime error: division by zero
Fin du processus : signal SIGFPE (Floating point exception)
Lecture. quota vaut 32767 ici ; valgrind signale le test sur une valeur non initialisée. Division entière par zéro : ici SIGFPE ; fflush fait sortir « debut » avant l’arrêt.
Compilateur \(\rightarrow\)
s7_ordre.c:11:7: warning: operation on 'i' may be undefined [-Wsequence-point]
Résultat \(\rightarrow\)
appel 1
appel 2
3 6
Points de séquence
Fin d’une expression complète (;, condition d’un if ou d’une boucle), &&, ||, ?:, virgule opérateur, appel de fonction (après ses arguments) : les effets qui précèdent sont achevés. Entre deux, une variable n’est modifiée qu’une fois.
Lecture. trace(1) + trace(2) : l’ordre des deux appels est non spécifié (ici 1 puis 2), le résultat 3 ne change pas. i = i++ + 1 modifie i deux fois sans ordre : comportement indéfini (-Wsequence-point) ; ici 6, sans garantie.
Énoncé
Pour chaque fragment : défini, défini par l’implémentation, ou indéfini ? Donner la valeur quand elle existe.
Correction
u \(=\) 4294967295.int.q \(=\) \(-4\), m \(=\) \(-1\).-Woverflow).k est lu avant d’avoir une valeur.i modifié deux fois sans point de séquence.int (1u << 31 vaut 2147483648).Résultat de (1), (3) et (4) \(\rightarrow\)
4294967295 -4 -1 44
À retenir
char 1, short 2, int 4, long, size_t et double 8 octets ; le signe de char dépend de l’implémentation.int ; entre int et unsigned, l’int est converti (-1 devient 4294967295) ; le type des opérandes décide du calcul (7 / 2 vaut 3).& et | sont moins prioritaires que == ; << l’est moins que + ; && et || évaluent de gauche à droite, en court-circuit.case sans break continue dans le suivant ; break et continue agissent sur la boucle la plus intérieure.'\0'.-std=c17 -Wall -Wextra : lire chaque avertissement ; un comportement indéfini n’a aucun résultat garanti.Séance 2 : représentation numérique
Les octets derrière les valeurs : binaire et hexadécimal, entiers non signés et complément à deux, conversions entre signé et non signé, extension de signe, débordements, et flottants IEEE 754. Les résultats de cette séance y trouvent leur explication : \(-56\) pour (char)200, \(-25536\) pour (short)40000, 4294967295 pour (unsigned)-1, \(-2147483648\) pour INT_MAX + 1.
static, préprocesseur), § 7.2 et 7.4 (printf, scanf).Mesures de la séance : x86-64, Linux 6.18, gcc 13.3.0 (-std=c17 -Wall -Wextra -O0 -g), glibc 2.39, gdb 15.1, valgrind 3.22.0 ; adresses rendues déterministes par setarch -R.
Programmation C avancée — Séance 1 Dr. El Hadji Bassirou TOURÉ • DMI/FST/UCAD
Lecture.
aest unshort(2 octets) : 30000 est dans l’intervalle \([-32768, 32767]\).Lecture.
b\(=\) 10000 ; la somme \(40000\) dépasseSHRT_MAX.Lecture. Promotion entière :
aetbdeviennent desint; 40000 tient dans unint.Lecture. Rangée dans un
short, la somme est hors intervalle : conversion définie par l’implémentation, ici \(40000-65536=-25536\).Lecture.
d\(=-1\), unintsigné.Lecture.
u\(=1\), ununsigned int.Lecture.
dest converti enunsigned int: \(-1\) devient 4294967295, le test est faux (-Wsign-compare).Lecture. La branche
elses’exécute :d >= u, contre l’intuition.Lecture.
s / 3: division entière (13333), puis conversion endouble.Lecture. Le type des opérandes fixe le calcul ; le type de la variable reçoit le résultat.