Diaporama n°8¶

Le langage C - Définir de nouveaux types¶

Synthèse partielle : C¶

Rappel :

  • les types doivent être indiqués par le programmeur

  • chaque variable, chaque expression, est typée à la compilation

Le typage à la compilation présente 3 intérêts :

  • (1) : prévention des erreurs avant l'exécution (ce ne sont souvent que des warning, qui n'empêchent pas l'exécution du code : typage statique faible).
  • (2) : absence de vérification des types pendant l'exécution, ce qui rend l'exécution du code très rapide
  • (3) : on connaît la quantité d'espace mémoire allouée à chaque variable

Corollaire de (3) : Pour chaque fonction :

  • on connaît d'avance la taille de l'espace mémoire occupée par les paramètres et les variables locales

  • on peut prédéfinir un emplacement précis pour chaque variable dans cet espace, ce qui permet de rendre l'exécution encore plus efficace

Ces informations permettent de définir le bloc d'activation d'une fonction, qui se place sur la pile d'exécution à chaque appel de fonction.

Exemple : C Tutor vs. Python Tutor.

En Python, l'interpréteur découvre ces informations à l'exécution (c'est plus lent)

En C : à chaque appel de fonction, les variables locales et les paramètres d'appel

  • sont alloués dans la pile (comme en Python),

  • à un endroit précis (pas comme en Python)

  • ont pour durée de vie la durée de l'appel de la fonction (comme en Python)

(HP) : Vérification dans le code assembleur généré par le compilateur¶

  • gcc -S test.c permet de générer test.s, le code assemleur pour le fichier text.c

  • c'est la version "lisible" du code machine

image.png

  • %rsp %rbp (registres du processeur) correspondent à des emplacements (adresses) dans la pile

  • les emplacements de chaque variable sont de la forme -4(%rbp), -8(%rbp) ... le processeur sait exactement où aller chercher les valeurs des variables image.png

Les types structurés ou "types construits"¶

c'est-à-dire

construits à partir des types de base¶

I. les tableaux "statiques"¶

II. les chaines de caractères¶

III. typedef et struct¶

I. les tableaux "statiques" en C¶

  • statique : leur taille est connue à la compilation (avant l'exécution)

  • NB : si la taille d'un tableau n'est pas connue à l'avance, on peut créer un tableau "dynamique" avec malloc (cf pointeurs et gestion mémoire)

  • Attention : les tableaux (statique ou dynamique) en C ne sont pas "extensibles" ($\neq$ listes Python)

I.1 Définir une variable de type tableau :¶

  • T tab[N];

  • représente N éléments de même type, placés consécutivement en mémoire, et a priori non intialisés

  • l'espace mémoire occupé par le tableau est N * sizeof(T)

  • N doit être un entier littéral (écrit avec des chiffres). Par exemple, int valeurs[20] définit un tableau de 20 entiers

I.2 Initialiser un tableau¶

Initialisation avec un "initialiseur" :¶

  • T tab[3] = {11,-4,103}; : chaque élément est initialisé

  • T tab[3] = {11}; : le premier élément est initialisé à 11, les autres sont initialisés à 0

  • T tab[] = {11,4,7}; : dans ce cas, la taille du tableau est déduite de l'initaliseur (ici 3)

Initialisation sans "initialiseur"¶

  • T tab[3]; : 3 valeurs quelconques ; on écrira à l'emplacement de chaque élément (écriture directe, boucle for ...)

I.3 Accéder au i-ème élément du tableau :¶

  • tab[i] : valeur de type T

  • en lecture / écriture (sauf si const)

  • les indices "valides" vont de 0 à N-1

  • Attention : En C, aucun contrôle de la validité d'un accès

In [11]:
// Code incorrect : accès à tableau non initialisé

#include <stdio.h>

int main(){
    int valeurs[3];
    printf("valeurs : %d %d %d", valeurs[0], valeurs[1], valeurs[2]);
    return 0;
}
In file included from /tmp/tmpo7x9mtxd.c:3:
/tmp/tmpo7x9mtxd.c: In function ‘main’:
/home/julien/jupyter-c-kernel/jupyter_c_kernel/resources/stdio_wrap.h:290:16: warning: ‘valeurs[2]’ is used uninitialized in this function [-Wuninitialized]
  290 | #define printf printf_wrap
      |                ^~~~~~~~~~~
/home/julien/jupyter-c-kernel/jupyter_c_kernel/resources/stdio_wrap.h:290:16: warning: ‘valeurs[1]’ is used uninitialized in this function [-Wuninitialized]
/home/julien/jupyter-c-kernel/jupyter_c_kernel/resources/stdio_wrap.h:290:16: warning: ‘valeurs[0]’ is used uninitialized in this function [-Wuninitialized]
valeurs : -1623975619 466316880 32765

Le tableau contient a priori n'importe quoi¶

In [7]:
// Exemple tableau avec initialiseur (complet)

#include <stdio.h>

int main(){
    int valeurs[3] = {34,45,11};
    printf("valeurs : %d %d %d", valeurs[0], valeurs[1], valeurs[2]);
    return 0;
}
valeurs : 34 45 11
In [10]:
// Exemple tableau sans initialiseur : on initialise ensuite

#include <stdio.h>

int main(){
    int valeurs[3];
    for (int i=0; i < 3; i++) {
        valeurs[i] = 10*i;
    }
    printf("valeurs : %d %d %d", valeurs[0], valeurs[1], valeurs[2]);
    return 0;
}
valeurs : 0 10 20
In [12]:
// Code incorrect : trop de valeurs dans l'initialiseur

#include <stdio.h>

int main(){
    int valeurs[3] = {34,45,11,18};
    printf("valeurs : %d %d %d", valeurs[0], valeurs[1], valeurs[2]);
    return 0;
}
/tmp/tmpm805jiu0.c: In function ‘main’:
/tmp/tmpm805jiu0.c:6:32: warning: excess elements in array initializer
    6 |     int valeurs[3] = {34,45,11,18};
      |                                ^~
/tmp/tmpm805jiu0.c:6:32: note: (near initialization for ‘valeurs’)
valeurs : 34 45 11
In [8]:
// Exemple tableau avec initialiseur incomplet : complété avec des 0

#include <stdio.h>

int main(){
    int valeurs[3] = {177};
    printf("valeurs : %d %d %d", valeurs[0], valeurs[1], valeurs[2]);
    return 0;
}
valeurs : 177 0 0
In [15]:
// Exemple tableau sans taille déclarée, mais avec initialiseur
// la taille allouée est celle de l'initialiseur

#include <stdio.h>

int main(){
    int valeurs[] = {177,144,37};
    printf("valeurs : %d %d %d", valeurs[0], valeurs[1], valeurs[2]);
    return 0;
}
valeurs : 177 144 37
In [19]:
// Code incorrect : tentative de modification d'un tableau const

#include <stdio.h>

int main(){
    const int valeurs[3] = {177,144,37};
    valeurs[0] = 45;
    printf("%d", valeurs[0]);
    return 0;
}
/tmp/tmp4f8wv3ax.c: In function ‘main’:
/tmp/tmp4f8wv3ax.c:7:16: error: assignment of read-only location ‘valeurs[0]’
    7 |     valeurs[0] = 45;
      |                ^
[C kernel] GCC exited with code 1, the executable will not be executed
In [17]:
// Code incorrect : accès illicite
// NB : aucun warning, aucune erreur ...

#include <stdio.h>

int main(){
    int valeurs[3] = {177,144,37};
    printf("valeurs : %d %d %d", valeurs[4], valeurs[5], valeurs[6]);
    return 0;
}
valeurs : 1676675723 278219232 32767

image.png

Remarques sur les tableaux en C¶

  • en C, les tableaux sont homogènes et ne sont pas extensibles

  • pas d'indices négatifs, pas d'extractions de sous-tableau, pas d'énumération des éléments ...

  • pas de append (la taille est fixée)

  • pas de len (la taille est fixée, donc connue par le programmeur !)

image.png

! Attention !¶

En C, il n'est pas possible, étant donné un tableau, d'obtenir sa longueur :

  • Il est parfois nécessaire d'utiliser une variable pour stocker cette longueur

  • Pour les fonctions qui prennent un tableau en paramètre, il est souvent indispendable de passer la longueur en paramètre (cf exemple à venir)

(longue) Remarque sur la taille des tableaux statiques¶

Le programme officiel MP2I/MPI préconise de donner la taille d'un tableau statique de manière "littérale" (= un nombre écrit avec des chiffres) :

  • int valeurs[20];

Toutefois, laisser apparaître une valeur numérique dans le code est une mauvaise pratique en programmation ...

La bonne pratique consisterait à définir une variable constante pour ce nombre

const int N = 20; en début de programme

int valeurs[N];

Malheureusement, cette façon de procéder fonctionne, mais cache un mécanisme complexe, contraire à l'esprit du langage C, et est hors programme.

En C, la taille d'un tableau STATIQUE ne doit pas être définie par une variable (ou par le résultat d'un calcul). Si besoin, on utilise un tableau DYNAMIQUE (cf la suite)

Un programmeur C procède de la façon suivante (hors programme en MP2I/MPI) :

#define N 20 en début de programme

int valeurs[N];

NB : #define N 20 est une directive du préprocesseur qui modifie le code de la façon suivante : avant la compilation, tous les N sont remplacés par 20

NB : le préprocesseur procède par copier-coller : N n'est donc pas une variable

NB : Une variable nommée Nb ne sera pas remplacée par 20b.

Remarque sur déclaration de la taille des tableaux statiques¶

En pratique:

  • pour les programmes courts, on suivra les consignes du programme MP2I / MPI : int valeurs[20];

  • pour les programmes longs, pour éviter la répétition de constantes numériques dans le code, on utilisera #define N 20 puis int valeurs[N]; pour définir les tailles de tableau statique

  • dans tous les cas, on utilisera const pour les constantes qui ne sont pas des tailles de tableau statique.

    • Par exemple : const int NB_CARTES = 32;
In [1]:
// Exemple d'utilisation d'une constante définie avec #define

#define N 10 // N taille du tableau statique

int main(){
    int tab[N];
    for (int i = 0; i < N; i++){
        tab[i] = N - i;
    }
    for (int i = 0; i < N; i++){
        printf("%d ", tab[i]);
    }
    return 0;
}
10 9 8 7 6 5 4 3 2 1 
In [3]:
// NB : Après préprocesseur, et avant la compilation : les N sont remplacés par 10
int main(){
    int tab[10];
    for (int i = 0; i < 10; i++){
        tab[i] = 10 - i;
    }
    for (int i = 0; i < 10; i++){
        printf("%d ", tab[i]);
    }
    return 0;
}
10 9 8 7 6 5 4 3 2 1 

I.4 Fonction : paramètre de type tableau¶

Lors d'un appel de fonction, la règle générale est le passage des paramètres par valeur :

  • les valeurs des paramètres sont calculées puis passées sur la pile (via le bloc d'activation)

  • si une variable est passée en paramètre, sa valeur est recopiée sur la pile. Modifier la valeur du paramètre ne modifie pas la valeur de la variable initiale

In [5]:
// Exemple - tentative de modification de paramètre
#include <stdio.h>

void modif(int a){
    a += 1; // modification du paramètre (copié) !
}

int main(){
    int z = 0;
    modif(z);
    printf("z=%d", z); // z non modifié
    return 0;
}
z=0

En détails : C Tutor

Cas particulier :¶

les tableaux sont passés par référence.

  • La valeur recopiée sur la pile est l'adresse du tableau, c'est-à-dire une référence ou un pointeur vers ce tableau

  • NB : les concepteurs du C ont fait une exception afin de ne pas avoir à recopier les éléments du tableau sur la pile (ce qui serait très coûteux si le tableau est de grande taille)

Conséquences :

  • on peut modifier les éléments d'un tableau passé en paramètre à une fonction (NB : comme pour les listes en Python)

  • la taille du tableau passé en paramètre est a priori sans importance pour le compilateur. Il faut juste connaître le type de ses éléments (la taille des éléments du tableau)

  • T mafonction(int tab[]){ ... } : passage d'un paramètre de type 'tableau d'entiers'

In [6]:
// Exemple - modification de paramètre de type tableau
#include <stdio.h>

void modif(int a[]){
    a[0] += 1; // modification du tableau
}

int main(){
    int z[2] = {17,24};
    modif(z);
    printf("z[0]=%d", z[0]); // z[0] modifié !
    return 0;
}
z[0]=18

En détails : C Tutor

Variantes:¶

  • (Très fréquent) pour une fonction qui prend en paramètre un tableau de taille quelconque, on doit passer la taille du tableau en paramètre

    • T mafonction(int tab[], int taille){ ... }
  • (Fréquent) il est possible de passer le tableau sous forme de pointeur (cf la suite). Le mécanisme reste identique

    • T mafonction(int* tab, int taille){ ... } (cf pointeurs)
  • (Rare) il est possible d'indiquer la taille du tableau :

    • T mafonction(int tab[5]){ ... }
In [1]:
// Exemple - paramètre de type tableau et taille
#include <stdio.h>

int somme(int t[], int n){
    /* calcule la somme des éléments du tableau t de longueur n */
    int s = 0;
    for (int i=0; i<n; i++){
        s += t[i];
    }
    return s;
}

int main(){
    int tab1[5] = {10,20,5,30,5};
    int tab2[3] = {100,10,7};
    int s1 = somme(tab1,5);
    int s2 = somme(tab2,3); 
    printf("s1=%d s2=%d", s1, s2);
    return 0;
}
s1=70 s2=117

II. Les chaines de caractères¶

En C, une chaîne de caractères est un tableau de caractères avec un caractère final nul

image.png

NB : ce caractère final est appelé sentinelle. Il permet de savoir où s'arrête une chaîne de caractère de longueur inconnue.

cf. plus tard ...

III. typedef et struct¶

En C, très peu de types sont prédéfinis : pas de couples, pas de listes, pas de dictionnaires, ...

Il est donc fréquent de définir ses propres types.

Au programme MP2I/MPI :

  • struct : accoler plusieurs variables de différents types

Définition¶

struct nom_s { T1 c1; T2 c2; ... };¶

  • définit un nouveau type : struct nom_s

  • composé de plusieurs champs : c1 de type T1, c2 de type T2, ...

Définition d'une variable¶

  • struct nom_s ma_variable;

Accès aux champs (en lecture / écriture)¶

  • ma_variable.c1
  • ma_variable.c2

Initialiseur¶

  • struct nom_s ma_variable = {.c1 = v1, .c2 = v2, ...} // dans n'importe quel ordre

Exemple¶

Par exemple, le type stuct Point2D constitué de deux champs de type float

In [1]:
// Exemple struct : définition d'un nouveau type

struct Point2D {  // nouveau type : struct Point2D
    float x;
    float y;
};
In [13]:
// Exemple struct : définition d'une variable, et accès aux champs

#include <stdio.h>

struct Point2D {  // nouveau type : struct Point2D
    float x;
    float y;
};

int main() {
    struct Point2D pt;  // variable pt de type : struct Point2D
    pt.x = 1.5;
    pt.y = -4.0;
    printf("%f %f", pt.x, pt.y);
    return 0;
}
1.500000 -4.000000
In [1]:
// Exemple struct : en paramètre d'une fonction

#include <stdio.h>

struct Point2D {
    float x;
    float y;
};

void affichePoint(struct Point2D p){
    printf("point : (%f,%f)", p.x, p.y);
}

int main() {
    struct Point2D pt;
    pt.x = 1.5;
    pt.y = -4.0;
    affichePoint(pt);
    return 0;
}
point : (1.500000,-4.000000)

Renommer un type avec typedef¶

Les types écrits en plusieurs mots rendent le code assez indigeste ...

Pour donner un nouveau nom à un type existant (alias) :

typedef ancien_type nouveau_type;¶

Par exemple :

  • typedef unsigned int uint; // définit le nouveau nom de type uint
  • typedef struct Point2D point2D; // définit le nouveau nom de type point2D
In [19]:
// Utilisation classique (au programme) de struct : 
#include <stdio.h>

struct Point2D {  // définition 
    float x,y;
};

typedef struct Point2D point2D; // nouveau type

void affichePoint(point2D p){
    printf("point : (%d,%d)", p.x, p.y);
}

int main() {
    point2D pt;
    pt.x = 1.5;
    pt.y = -4.0;
    printf("%f %f", pt.x, pt.y);
    return 0;
}
1.500000 -4.000000

NB : on peut définir et renommer un type struct simultanément¶

pratique courante pour un programmeur C

In [20]:
// Utilisation classique de struct :
#include <stdio.h>

typedef struct Point2D {  // définition et renommage
    float x,y;
} point2D;

void affichePoint(point2D p){
    printf("point : (%d,%d)", p.x, p.y);
}

int main() {
    point2D pt;
    pt.x = 1.5;
    pt.y = -4.0;
    printf("%f %f", pt.x, pt.y);
    return 0;
}
1.500000 -4.000000

On peut construire des types plus complexes¶

In [ ]:
// Exemple : un struct qui contient un tableau de 3 struct

struct Point2D {
    float x;
    float y;
};

typedef struct Point2D point2D;

struct Triangle {
    point2D sommets[3];  // tableau de 3 points2D
};

typedef struct Triangle triangle;

NB : variable de type struct ...¶

Une variable de ce type se comporte comme n'importe quel autre type :

  • variable locale / globale

  • passage par valeur (et donc copie sur pile) lors d'un appel de fonction

  • peut être renvoyé par une fonction

La taille de ce type est (en première approximation) la somme des tailles de ses champs.

In [31]:
// Taille mémoire occupée par une variable de type struct

struct Point2D {
    float x;
    float y;
}; // taille 2*4 = 8 octets

typedef struct Point2D point2D;

struct Triangle {
    point2D sommets[3];  // taille 3*8 = 24 octets
};

typedef struct Triangle triangle;

printf("%d", sizeof(triangle));
24

A suivre ...¶