Séance 1 — Syntaxe C complète (fondamentaux)

Types, variables, opérateurs, contrôle, fonctions, tableaux, entrées-sorties, compilation

Dr. El Hadji Bassirou TOURÉ

Département de Mathématiques et Informatique, Faculté des Sciences et Techniques, Université Cheikh Anta Diop de Dakar

Objectifs de la séance

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.

  • Donner la taille et l’intervalle des types de base (x86-64, LP64), et prévoir les conversions implicites d’une expression.
  • Distinguer déclaration, définition et initialisation ; dire la portée et la durée de vie d’une variable.
  • Évaluer une expression : priorités, associativité, court-circuit, pré- et post-incrémentation.
  • Suivre l’exécution d’un if, d’un switch et d’une boucle, ligne par ligne, dans une table de trace.
  • Suivre des appels de fonctions (passage par valeur, récursion) et lire un tableau et une chaîne en mémoire.
  • Compiler avec gcc -std=c17 -Wall -Wextra, lire les messages, et reconnaître un comportement indéfini.

Types, variables et conversions

Les types de base et leurs tailles

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.

Le type char et son signe

char c = 200;
unsigned char uc = 200;
signed char sc = -56;
int somme = c + uc;
printf("%d %d %d %d\n", c, uc, sc, somme);

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.

Conversions implicites, pas à pas

Valeurs relevées sous gdb à chaque ligne ; ·· : valeur indéterminée (variable pas encore initialisée).

Sortie \(\rightarrow\)

d >= u
40000 -25536 13333.00

Lecture. a est un short (2 octets) : 30000 est dans l’intervalle \([-32768, 32767]\).

Lecture. b \(=\) 10000 ; la somme \(40000\) dépasse SHRT_MAX.

Lecture. Promotion entière : a et b deviennent des int ; 40000 tient dans un int.

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\), un int signé.

Lecture. u \(=1\), un unsigned int.

Lecture. d est converti en unsigned int : \(-1\) devient 4294967295, le test est faux (-Wsign-compare).

Lecture. La branche else s’exécute : d >= u, contre l’intuition.

Lecture. s / 3 : division entière (13333), puis conversion en double.

Lecture. Le type des opérandes fixe le calcul ; le type de la variable reçoit le résultat.

Déclaration, définition, initialisation

int total;
int main(void)
{
    int stock;
    int prix = 1250;
    static int ventes;
    printf("%d %d %d\n", total, ventes, prix);
    printf("%d\n", stock);
    return 0;
}

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.

Portée et masquage, pas à 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.

Durée de vie et constantes

void compter(void)
{
    static int appels = 0;
    int local = 0;
    appels++;
    local++;
    printf("appel %d, local %d\n", appels, local);
}
int main(void)
{
    compter();
    compter();
    compter();
}
#define TVA 18
#define CARRE(x) x * x
#define CARRE_SUR(x) ((x) * (x))
int main(void)
{
    const int plafond = 50000;
    int a = CARRE(2 + 3);
    int b = CARRE_SUR(2 + 3);
    printf("%d %d %d %d\n", TVA, plafond, a, b);
}

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.

Exercice : types et conversions

unsigned char octet = 250;
char lettre = 'A' + 2;
int n = 7;
size_t t = sizeof(n++);
octet = octet + 10;
int moyenne = (12 + 15 + 14) / 3;
double vraie = (12 + 15 + 14) / 3.0;
printf("%d %c %d %zu\n", octet, lettre, n, t);
printf("%d %.2f\n", moyenne, vraie);
printf("%d\n", -3 < sizeof n);

Énoncé : ce qu’affiche ce fragment

  1. la valeur de octet ;
  2. lettre, n et t ;
  3. moyenne et vraie ;
  4. la dernière ligne.

Correction

  1. \(250+10\) est calculé en int : 260 ; converti en unsigned char, modulo 256 (défini) : 4.
  2. \(65+2=67\) : 'C' ; sizeof n’évalue pas n++ : n reste 7, t \(=\) 4.
  3. \(41/3\) : division entière, 13 ; \(41/3.0\) : double, 13.67.
  4. -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

Opérateurs et évaluation

Division entière et modulo

int duree = 135;
int ecart = -135;
printf("%d h %d min\n", duree / 60, duree % 60);
printf("%d h %d min\n", ecart / 60, ecart % 60);
printf("%d %d\n", duree / -60, duree % -60);
double h1 = duree / 60;
double h2 = duree / 60.0;
printf("%.2f %.2f\n", h1, h2);

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.

Incrémentation, pas à pas

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.

Évaluation en court-circuit, pas à pas

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.

Opérateurs bit à bit et décalages

unsigned int d = 0x5;
unsigned int w = 0x2;
printf("%x %x %x\n", d | w, d & 0x4, d ^ 0x1);
printf("%x\n", ~d);
printf("%u %u\n", d << 3, 0xb4u >> 2);
printf("%u\n", 1u << 31);
int neg = -20;
printf("%d\n", neg >> 2);

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).

Priorités et associativité

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\)
int masque = 6;
int r1 = masque & 4 == 4;
int r2 = (masque & 4) == 4;
int r3 = 1 << 2 + 1;
int r4 = 10 - 4 - 3;
printf("%d %d %d %d\n", r1, r2, r3, r4);

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.

Exercice : état des variables ligne par ligne

Énoncé. Donner a, b et c après chacune des lignes 4 à 8, en nommant la règle appliquée.

int a = 4;
int b = 0;
int c = 9;
a += b++ * 2;
c = a > 3 && ++b > 1;
b = c || a++;
a = a << 1 | 1;
c = a % 4 == 1 ? a / 2 : -1;
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

  1. L4 : b++ vaut 0, puis b \(=1\) ; a += 0.
  2. L5 : a > 3 vrai, donc ++b est évalué (\(b=2\), \(2>1\)) ; c \(=1\).
  3. L6 : c vrai : || s’arrête, a++ n’est pas exécuté ; b \(=1\).
  4. L7 : << avant | : \((4 << 1)\,|\,1 = 9\).
  5. L8 : % avant == avant ?: : \(9~\% 4 = 1\), vrai ; c \(= 9/2 = 4\).

Structures de contrôle

Conditions et boucles

int age = 17;
int inscrit = 1;
if (age >= 18)
    if (inscrit)
        printf("vote\n");
else
    printf("mineur\n");
printf("fin\n");

Compilateur \(\rightarrow\)

s4_ifelse.c:6:8: warning: suggest explicit braces to avoid ambiguous 'else' [-Wdangling-else]

Résultat \(\rightarrow\)

fin
int n = 0;
while (n > 0) {
    printf("while %d\n", n);
    n--;
}
do {
    printf("do-while %d\n", n);
    n--;
} while (n > 0);
for (int k = 3; k > 0; k--)
    printf("for %d\n", k);

Résultat \(\rightarrow\)

do-while 0
for 3
for 2
for 1

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).

Un switch sans break, pas à pas

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 boucle for avec continue et 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.

Boucles imbriquées et goto

for (int i = 1; i <= 3; i++) {
    for (int j = 1; j <= 3; j++) {
        if (j == 2)
            break;
        printf("(%d,%d) ", i, j);
    }
}
printf("\n");
int i;
int j;
for (i = 1; i <= 9; i++)
    for (j = i; j <= 9; j++)
        if (i * j == 24)
            goto trouve;
printf("aucun\n");
return 0;
trouve:
printf("%d x %d = 24\n", i, j);

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\).

Exercice : switch et boucles

for (int j = 0; j < 4; j++) {
    switch (j % 3) {
    case 0:
        printf("D");
    case 1:
        printf("K");
        break;
    default:
        printf("-");
    }
}
printf("\n");
int tours = 0;
for (int i = 10; i > 0; i -= 3)
    tours++;
int m = 5;
do
    m -= 2;
while (m > 10);
printf("%d %d\n", tours, m);

Énoncé

  1. ce qu’affiche la première boucle ;
  2. les valeurs de i et le nombre de tours ;
  3. la valeur finale de m.

Correction

  1. j \(=0\) : case 0, sans break : « DK » ; j \(=1\) : « K » ; j \(=2\) : default, « - » ; j \(=3\) : \(3\%3=0\), « DK » : DKK-DK.
  2. i : 10, 7, 4, 1, puis \(-2\) arrête : 4 tours.
  3. do-while : un tour, m \(=3\), puis \(3 > 10\) faux : 3.

Résultat \(\rightarrow\)

DKK-DK
4 3

Fonctions

Prototype et définition

int ancien();
int moderne(void);
int prix_ttc(int ht);
int main(void)
{
    printf("%d\n", prix_ttc(5000));
    printf("%d\n", ancien(1, 2, 3));
}
int prix_ttc(int ht)
{
    return ht + ht * 18 / 100;
}
int ancien()
{
    return 7;
}

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, pourquoi, quand ça échoue : passage par valeur

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.

Passage par valeur, pas à pas

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).

Valeur de retour : return

int credite(int solde, int montant)
{
    return solde + montant;
}
int signe(int x)
{
    if (x > 0)
        return 1;
    if (x < 0)
        return -1;
}
int main(void)
{
    int compte = credite(7500, 2500);
    printf("%d %d %d\n", compte, signe(9), signe(-4));
    printf("%d\n", signe(0));
}

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).

Une fonction récursive, pas à pas

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.

Exercice : appels et pile d’appels

É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.

int f(int x)
{
    x = x * 2;
    return x + 1;
}
int g(int n)
{
    if (n <= 1)
        return n;
    return g(n - 1) + n;
}
/* dans main */
int v = 5;
int w = f(v);
printf("%d %d %d\n", v, w, g(4));

Correction

  1. x reçoit une copie de 5 ; x \(=10\) dans f seulement : v \(=\) 5, w \(=\) 11.
  2. Les appels s’empilent jusqu’à n \(=1\), puis se terminent dans l’ordre inverse : 1, \(1+2=3\), \(3+3=6\), \(6+4=10\).
  3. 4 appels de 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

Tableaux, chaînes et entrées-sorties

Un tableau, pas à pas

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).

Un tableau en paramètre

int somme(int t[], int n)
{
    int s = 0;
    for (int i = 0; i < n; i++)
        s += t[i];
    printf("dans somme : %zu\n", sizeof t);
    return s;
}
int main(void)
{
    int recolte[5] = {12, 30, 7, 18, 25};
    printf("dans main : %zu\n", sizeof recolte);
    printf("total : %d\n", somme(recolte, 5));
    return 0;
}

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.

Une chaîne de caractères, pas à pas

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.

Affichage formaté : printf

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 formatée : scanf

int sacs = 0;
double prix = 0;
int lus = scanf("%d %lf", &sacs, &prix);
printf("lus = %d : %d, %.1f\n", lus, sacs, prix);
lus = scanf("%d", &sacs);
printf("lus = %d : %d\n", lus, sacs);

Entrée standard \(\rightarrow\)

25 3500.5
abc

Résultat \(\rightarrow\)

lus = 2 : 25, 3500.5
lus = 0 : 25

Valeur de retour

scanf rend le nombre de valeurs lues, 0 si l’entrée ne convient pas, EOF (\(-1\) avec la glibc) en fin de fichier : la tester avant d’utiliser les variables.

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.

Exercice : un tableau et une chaîne dans un dump

int pluie[3] = {14, 9, 17};
char quartier[10] = "Medina";
quartier[3] = '\0';
pluie[1] = pluie[0] + pluie[2];
printf("%s %d\n", quartier, pluie[1]);
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é

  1. où sont pluie[0], pluie[1], pluie[2] ?
  2. où est quartier ; sa longueur, sa taille ?
  3. les cases modifiées par les lignes 3 et 4 ;
  4. ce qu’affiche la ligne 5.

Correction

  1. 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.
  2. En 0x0e : 4d 65 64 69 6e 61 00, « Medina » ; strlen \(=6\), sizeof \(=10\).
  3. quartier[3] en \(\texttt{0x0e}+3=\texttt{0x11}\) : 69 devient 00 ; pluie[1] en 0x04 : \(14+17=31\), 1f.
  4. Med 31 : la chaîne s’arrête au premier 00.

Compilation et comportement indéfini

Les quatre phases de la compilation

#define PORT 300
#define TTC(x) ((x) + (x) * 18 / 100)
int paquets = 3;
int main(void)
{
    int total = paquets * TTC(PORT);
    printf("%d : %d\n", paquets, total);
}
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).

Compilation séparée et édition de liens

taxe.h

extern int taux_tva;
int ttc(int ht);

taxe.c

#include "taxe.h"
int taux_tva = 18;
int ttc(int ht)
{
    return ht + ht * taux_tva / 100;
}

facture.c

#include <stdio.h>
#include "taxe.h"
int main(void)
{
    printf("%d %d\n", taux_tva, ttc(6000));
}

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, pourquoi, quand ça échoue : comportement indéfini

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.

Débordement d’un entier signé

int deborde(int x)
{
    return x + 1 < x;
}
int main(void)
{
    int max = INT_MAX;
    int suivant = max + 1;
    printf("%d %d\n", suivant, deborde(max));
    return 0;
}

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.

Variable non initialisée, division par zéro

int quota;
if (quota > 100)
    printf("grand quota\n");
printf("%d\n", quota);

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)
int lots = 250;
int familles = 0;
printf("debut\n");
fflush(stdout);
printf("%d\n", lots / familles);

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.

Ordre d’évaluation

int trace(int k)
{
    printf("appel %d\n", k);
    return k;
}
int main(void)
{
    int s = trace(1) + trace(2);
    int i = 5;
    i = i++ + 1;
    printf("%d %d\n", s, i);
    return 0;
}

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.

Exercice : défini ou indéfini ?

/* (1) */ unsigned u = 0;
          u = u - 1;
/* (2) */ int n = INT_MAX;
          n = n + 1;
/* (3) */ int q = -9 / 2;
          int m = -9 % 2;
/* (4) */ char c = 300;
/* (5) */ int k;
          k = k + 1;
/* (6) */ int i = 3;
          i = i++ * 2;
/* (7) */ int d = 1 << 31;

Énoncé

Pour chaque fragment : défini, défini par l’implémentation, ou indéfini ? Donner la valeur quand elle existe.

Correction

  1. Défini : arithmétique modulo \(2^{32}\), u \(=\) 4294967295.
  2. Indéfini : débordement d’un int.
  3. Défini : troncature vers zéro, q \(=\) \(-4\), m \(=\) \(-1\).
  4. Défini par l’implémentation : gcc réduit modulo 256, 44 (-Woverflow).
  5. Indéfini : k est lu avant d’avoir une valeur.
  6. Indéfini : i modifié deux fois sans point de séquence.
  7. Indéfini : \(2^{31}\) ne tient pas dans un int (1u << 31 vaut 2147483648).

Résultat de (1), (3) et (4) \(\rightarrow\)

4294967295 -4 -1 44

Synthèse

À retenir

À retenir

  1. Le type fixe la taille et l’intervalle : sur Linux x86-64, char 1, short 2, int 4, long, size_t et double 8 octets ; le signe de char dépend de l’implémentation.
  2. Dans une expression, les petits types sont promus en 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).
  3. Une variable automatique non initialisée est indéterminée ; une variable statique part de 0. La portée dit où un nom est visible, la durée de vie combien de temps l’objet existe.
  4. Priorités : & et | sont moins prioritaires que == ; << l’est moins que + ; && et || évaluent de gauche à droite, en court-circuit.
  5. Un case sans break continue dans le suivant ; break et continue agissent sur la boucle la plus intérieure.
  6. Les arguments sont passés par valeur ; un tableau passé à une fonction perd sa taille ; une chaîne finit par '\0'.
  7. -std=c17 -Wall -Wextra : lire chaque avertissement ; un comportement indéfini n’a aucun résultat garanti.

Prochaine séance

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.

Références

  • R. E. Bryant, D. R. O’Hallaron, Computer Systems: A Programmer’s Perspective, 3e éd., Pearson, 2016 : § 1.2 (système de compilation), § 2.1.2 (tailles des types), § 2.1.7 à 2.1.9 (opérateurs bit à bit, logiques, décalages), § 7.1 à 7.6 (édition de liens, symboles).
  • B. W. Kernighan, D. M. Ritchie, The C Programming Language, 2e éd., Prentice Hall, 1988 : chap. 2 (types, opérateurs, expressions, tableau des priorités § 2.12), chap. 3 (contrôle), chap. 4 (fonctions, portée, static, préprocesseur), § 7.2 et 7.4 (printf, scanf).
  • K. N. King, C Programming: A Modern Approach, 2e éd., Norton, 2008 : chap. 4 à 10 (expressions, contrôle, types de base, tableaux, fonctions, organisation d’un programme), chap. 13 (chaînes).
  • ISO/IEC 9899:2018 (C17) : 3.4.1 à 3.4.4 (comportements défini par l’implémentation, indéfini, non spécifié), 5.1.2.3 (exécution, points de séquence), 6.2.1 (portées), 6.2.4 (durées de vie), 6.3.1 (conversions arithmétiques), 6.5 (expressions), 6.8.4 et 6.8.5 (sélection, itération), 6.9.1 (définition de fonction).

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.