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érertest.s
, le code assemleur pour le fichiertext.c
c'est la version "lisible" du code machine
%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
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 à 0T 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, bouclefor
...)
I.3 Accéder au i-ème élément du tableau :¶
tab[i]
: valeur de type Ten 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
// 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¶
// 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
// 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
// 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
// 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
// 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
// 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
// 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
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 !)
! 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
puisint valeurs[N];
pour définir les tailles de tableau statiquedans 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;
- Par exemple :
// 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
// 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
// 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'
// 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]){ ... }
// 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
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 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
// Exemple struct : définition d'un nouveau type
struct Point2D { // nouveau type : struct Point2D
float x;
float y;
};
// 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
// 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)
Par exemple :
typedef unsigned int uint;
// définit le nouveau nom de typeuint
typedef struct Point2D point2D;
// définit le nouveau nom de typepoint2D
// 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
// 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¶
// 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.
// 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