Diaporama n°9¶

Le langage C - Pointeurs¶

I. Les pointeurs¶

I.1 Définition et manipulation de pointeurs¶

I.2 Passage d'un pointeur en paramètre à une fonction¶

I.3 La fonction scanf¶

II. Allocation mémoire¶

III. Retour sur les tableaux¶

IV. Quelques dilemnes ...¶

I. Les pointeurs¶

I.1 Définition et manipulation de pointeurs¶

Un pointeur est l'adresse d'un emplacement mémoire.

Grâce à cette adresse, on va pouvoir aller lire ou modifier la valeur à cet emplacement.

NB :

  • chaque variable correspond à un emplacement mémoire
  • chaque case d'un tableau correspond à un emplacement mémoire

L'emplacement mémoire associé à la variable v est donné par : &v¶

In [2]:
// Illustration des adresses
#include <stdio.h>

int main(){
    int x = 0,y = 0;
    float tab[2];
    printf("adresse de x:%p\n", &x); // %p : adresse mémoire hexadécimale
    printf("adresse de y:%p\n", &y);
    printf("adresse de tab[0]:%p\n", &(tab[0]));
    printf("adresse de tab[1]:%p\n", &(tab[1]));
    return 0;
}
adresse de x:0x7ffec010f1a8
adresse de y:0x7ffec010f1ac
adresse de tab[0]:0x7ffec010f1b0
adresse de tab[1]:0x7ffec010f1b4

NB¶

En fait, l'adresse précise importe peu¶

Cf la suite : on manipule l'adresse de x, l'adresse de y ... ces adresses sont susceptibles de changer à chaque exécution, ou sur des machines différentes.

Tous les langages de programmation manipulent de la mémoire¶

Sur les langages "bas-niveau", comme C, le programmeur peut manipuler explicitement la mémoire. C'est souvent source d'erreurs : manipulations à faire de manière très rigoureuse.

Sur des langages "haut-niveau", comme Python ou OCaml, le programmeur manipule des objets sans avoir à manipuler explicitement leurs adresses, de manière transparente.

Variable de type pointeur¶

On peut stocker cette adresse dans une variable de type pointeur (que l'on appelle couramment pointeur).

Pour v une variable de type T,

T* p = &v : définit une variable p de type pointeur vers T initialisée avec l'adresse de v

On dit que p est un pointeur vers v

NB : T* p = &v

p est une variable comme les autres

  • elle a un type : T* (pointeur vers T).
  • elle occupe un espace mémoire : les pointeurs ont en général une taille de 64 bits (sur les ordinateurs 64 bits)
  • elle a pour valeur une adresse mémoire (en fait, l'adresse précise importe peu)
  • elle peut ne pas être initialisée
  • on peut changer sa valeur
In [3]:
// Définition de pointeurs
#include <stdio.h>

struct Point2D{
    float x,y;
};

typedef struct Point2D point2D;

int main(){
    int x = 0;
    point2D pt0 = {.x=4.3,.y=7.1}; // initialiseur struct
    int* p = &x; // pointeur vers x
    point2D* q = &pt0; //pointeur vers pt0
    return 0;
}
/tmp/tmp1w5n1vjn.c: In function ‘main’:
/tmp/tmp1w5n1vjn.c:14:14: warning: unused variable ‘q’ [-Wunused-variable]
   14 |     point2D* q = &pt0; //pointeur vers pt0
      |              ^
/tmp/tmp1w5n1vjn.c:13:10: warning: unused variable ‘p’ [-Wunused-variable]
   13 |     int* p = &x; // pointeur vers x
      |          ^

Visualisation¶

image.png

NB : le bloc d'activation de la fonction main réserve un espace pour

  • les 4 variables locales x, pt0, p et q
  • 0 paramètre

Soit une taille totale de $4+2 \times 4+8+8=28$ octets (approx). Cf CTutor

In [4]:
// NB : taille d'un type pointeur
#include <stdio.h>

struct Point2D{
    float x,y;
};

typedef struct Point2D point2D;

int main(){
    printf("%d ", sizeof(int*));
    printf("%d ", sizeof(char*));
    printf("%d ", sizeof(point2D*));
    return 0;
}
8 8 8 

NB : tous les pointeurs sont de même taille (8 octets = 64 bits) sur la plupart des machines

Une variable et un type comme les autres¶

  • On peut tester l'égalité de deux pointeurs, l'inégalité de deux pointeurs (on compare les adresses)

  • (HP et vivement déconseillé) arithmétique sur les pointeurs (ex : q = p+1)

  • NULL : valeur spéciale qui désigne une adresse invalide (0)

int* p = NULL;

  • Une fonction peut prendre en paramètre un pointeur, renvoyer un pointeur (cf plus tard)

int* mafonction(int* a, int* b){...}

In [1]:
// Egalité, inégalité de pointeurs, pointeur NULL
#include <stdio.h>

int main(){
    int x = 0, y = 0;
    int* p = &x; // pointeur vers x
    int* q = &x; // pointeur vers x
    int* r = &y; // pointeur vers y 
    int* s = NULL; // pointeur invalide
    if (p==q) {
        printf("p et q : adresses identiques\n");
    }
    if (p!=r) {
        printf("p et r : adresses différentes\n");
    }
    if (s == NULL){
        printf("s est le pointeur NULL\n");
    }
    if (r != NULL){
        printf("r n'est pas le pointeur NULL");
    }
    return 0;
}
p et q : adresses identiques
p et r : adresses différentes
s est le pointeur NULL
r n'est pas le pointeur NULL

Déréférencement¶

Pour récupérer ou modifier la valeur pointée par p, on utilise la notation *p

*p : donne la valeur pointée par p (de type T)

*p = w : on remplace la valeur pointée par w (qui doit être de type T)

Left-value / Right-value¶

à gauche du signe = : *p désigne l'emplacement pointé par p

à droite du signe = : *p désigne la valeur pointée par p

NB : comme pour les noms de variables en général

  • b = a : à l'emplacement de b on met la valeur de a
In [5]:
// Déréférencement
#include <stdio.h>
struct Point2D{
    float x,y;
};
typedef struct Point2D point2D;

int main(){
    int x = 0;
    point2D pt0 = {.x=4.3,.y=7.1};
    int* p = &x; // pointeur vers x
    point2D* q = &pt0; //pointeur vers pt0
    
    printf("valeur pointée par p : %d\n", *p); // right-value
    printf("champs de la valeur pointée par q : %f %f\n", (*q).x, (*q).y); // right-value
    
    *p = 1; // on modifie l'emplacement pointé par p (left-value)
    (*q).y = 4.6; // on modifie le champ y de l'emplacement pointé par q (left-value)
    
    printf("x=%d pt0=(%f,%f)", x, pt0.x, pt0.y);
    return 0;
}
valeur pointée par p : 0
champs de la valeur pointée par q : 4.300000 7.100000
x=1 pt0=(4.300000,4.600000)

Visualisation¶

image.png

CTutor

NB : plusieurs façons de faire la même chose¶

  • on peut modifier la valeur de x de deux façons :

    • directement : x = 3
    • via le pointeur p : *p = 3
  • on peut bien sûr modifier la valeur des pointeurs (et les faire pointer ailleurs)

Exemple 1 : Que fait le code suivant ?¶

In [ ]:
// Exemple 1
#include <stdio.h>

int main(){
    int x = 3, y = 5;
    int* p = &x;
    int* q = &y;
    *p = 2;
    *q = *p;
    printf("x=%d y=%d", x, y);
    return 0;
}
In [6]:
//Exemple 1
#include <stdio.h>

int main(){
    int x = 3, y = 5;
    int* p = &x;  // p pointe vers x
    int* q = &y;  // q pointe vers y
    *p = 2;   // on remplace la valeur pointée par p par 2
    *q = *p;  // on remplace la valeur pointée par q par la valeur pointée par q
    printf("x=%d y=%d", x, y);
    return 0;
}
x=2 y=2

CTutor

Exemple 2 : Que fait le code suivant ?¶

In [ ]:
//Exemple 2
#include <stdio.h>

int main(){
    int x = 3, y = 5;
    int* p = &x;
    *p += 1;
    x = y;
    p = &y;
    *p = x+1;
    printf("x=%d y=%d", x, y);
    return 0;
}
In [7]:
//Exemple 2
#include <stdio.h>

int main(){
    int x = 3, y = 5;
    int* p = &x; // p pointe vers x
    *p += 1;     // on incrémente la valeur pointée par x de 1
    x = y;       // affectation
    p = &y;      // p pointe vers y
    *p = x+1;    // on modifie la valeur pointée par p
    printf("x=%d y=%d", x, y);
    return 0;
}
x=5 y=6

C Tutor

Exemple 3 : Que fait le code suivant ?¶

In [ ]:
//Exemple 3
#include <stdio.h>

int main(){
    int tab[2] = {14,36};
    int* p = &tab[0];
    int* q = &tab[1];
    int x = *p;
    *p = *q;
    *q = x;
    printf("tab : %d %d", tab[0], tab[1]);
    return 0;
}
In [8]:
//Exemple 3
#include <stdio.h>

int main(){
    int tab[2] = {14,36};
    int* p = &tab[0];
    int* q = &tab[1];
    int x = *p;
    *p = *q;
    *q = x;
    printf("tab : %d %d", tab[0], tab[1]);
    return 0;
}
tab : 36 14

CTutor

Attention :¶

  • on ne déréférence (*p) que les pointeurs valides

  • déréférencer autre chose qu'un pointeur provoque une erreur de type à la compilation (en général fatale)

  • déréférencer le pointeur NULL fait planter le programme (pas d'erreur de type : l'exécution est lancée)

In [9]:
//ERREUR de déréférencement
#include <stdio.h>

int main(){
    int x = 3;
    printf("%d", *x); 
    return 0;
}
/tmp/tmp93ltv8hg.c: In function ‘main’:
/tmp/tmp93ltv8hg.c:6:18: error: invalid type argument of unary ‘*’ (have ‘int’)
    6 |     printf("%d", *x);
      |                  ^~
[C kernel] GCC exited with code 1, the executable will not be executed

Erreur : x n'est pas un pointeur

In [10]:
//ERREUR déréférencement de NULL
#include <stdio.h>

int main(){
    int* x = NULL;
    printf("Hello\n");
    printf("%d", *x);
    return 0;
}
Hello
[C kernel] Executable exited with code -11

Typage OK : on déréférence un type pointeur

Exécution NOK (Not OK) : on accède à une adresse invalide (0)

A retenir : déréférencement pour struct¶

Pour les pointeurs sur type struct, la notation (*p).champ peut être remplacée par p->champ (notation usuelle, à retenir)

In [36]:
// Notation pointeur sur struct : ->
#include <stdio.h>

struct Point2D{
    float x,y;
};

typedef struct Point2D point2D;

int main(){
    point2D pt0 = {.x=4.3,.y=7.1};
    point2D* q = &pt0; //pointeur vers pt0
    
    q->x = q->y; // le champ x de la valeur pointée par q
    q->y += 1;
    printf("pt0=(%f,%f)", pt0.x, pt0.y);
    return 0;
}
pt0=(7.100000,8.100000)

(HP) : plusieurs notations pour les pointeurs¶

Il existe plusieurs notations :

  • pour les puristes du C : int *p = &x

    • sous-entendu *p est de type int
  • pour le commun des mortels (MP2I/MPI) : int* p = &x

    • sous-entendu p est de type int*
  • pour les indécis : int * p = &x

A retenir : on ne définit qu'un pointeur à la fois (pourquoi ??)

In [37]:
// Notation à éviter
#include <stdio.h>

int main(){
    int x=0, y=1;
    int* p, q; // à éviter, car trompeur
    p = &x;
    q = &y;
    *p += 1;
    *q += 1;
    return 0;
}
/tmp/tmp2_6btvow.c: In function ‘main’:
/tmp/tmp2_6btvow.c:8:7: warning: assignment to ‘int’ from ‘int *’ makes integer from pointer without a cast [-Wint-conversion]
    8 |     q = &y;
      |       ^
/tmp/tmp2_6btvow.c:10:5: error: invalid type argument of unary ‘*’ (have ‘int’)
   10 |     *q += 1;
      |     ^~
[C kernel] GCC exited with code 1, the executable will not be executed

int* p, q;

Dans cet exemple, q est de type int, et non int* ...

cf notation des puristes du C : int *p, q : p pointeur, q de type int

A retenir : on ne définit qu'un pointeur à la fois

In [ ]:
// Notation à utiliser : définition d'un seul pointeur à la fois
#include <stdio.h>

int main(){
    int x=0, y=1;
    int* p; int* q; // OK
    p = &x;
    q = &y;
    *p += 1;
    *q += 1;
    return 0;
}

NB : attention aux confusions avec *¶

  • opérateur binaire de multiplication
    • ex : a = b*c; x *= 2;
    • jamais en left-value

NB : attention aux confusions avec *¶

  • type pointeur (modificateur)
    • ex : int* p = &v; l'étoile est en général attachée au type, mais pas forcément
    • pour la déclaration du type : d'une variable, d'un paramètre, d'une valeur de retour
    • dans l'expression sizeof(type) : par exemple sizeof(int*)
    • avec malloc : p = (T*) malloc(...) (cf plus tard)

NB : attention aux confusions avec *¶

  • opérateur unaire de déréférencement
    • *p l'étoile est en général attachée à la variable, mais pas forcément
    • en left-value ou en right-value.

Lire ce code.¶

Quelle est la signification de chaque * ?

NB : le déréférencement est prioritaire sur la multiplication

In [ ]:
// Signification des * : Ecriture à éviter : trop peu lisible
#include <stdio.h>

int main(){
    int x = 2, y = 1;
    int * p = & x; 
    int * q = p;
    * p = * p * * q;
    int * r = & y;
    * r = sizeof(int *) * * p + 1;
    * q *= 2;
    printf("%d %d", x, y);
    return 0;
}
In [11]:
// Signification des * : Ecriture à utiliser
#include <stdio.h>

int main(){
    int x = 2, y = 1;
    int* p = &x;  //type
    int* q = p;   //type
    *p = *p * *q; // deref, deref, mult, deref
    int* r = &y;  // type
    *r = sizeof(int*) * *p + 1; // deref, type, mult, deref
    *q *= 2; // deref, mult
    printf("%d %d", x, y);
    return 0;
}
8 33

CTutor

I.2 Passage d'un pointeur en paramètre à une fonction¶

Rappel :

  • le passage des paramètres lors d'un appel de fonction se fait par valeur

    • sauf pour les tableaux (? cf la suite)
  • les valeurs des paramètres sont passés (copiés) sur la pile

In [39]:
// ERREUR / RAPPEL : Ceci ne modifie pas x
#include <stdio.h>

void incr(int a){
    a = a + 1;
}

int main(){
    int x = 3;
    incr(x);
    printf("%d", x);
    return 0;
}
3

Dans la fonction main, la valeur de x n'est pas modifiée

C'est a, la copie de x, qui est modifiée. CTutor

Avec des pointeurs¶

Si on passe l'adresse d'un emplacement à une fonction,

même si cette adresse est une copie,

la fonction peut y écrire !

In [41]:
// OK : ici, on modifie x
#include <stdio.h>

void incr(int* a){
    *a = *a + 1;
}

int main(){
    int x = 3;
    int* p = &x;
    incr(p);
    printf("%d", x);
    return 0;
}
4

CTutor

NB : c'est toujours un passage par valeur : la valeur du pointeur (l'adresse) est copiée sur la pile

version un peu plus succincte¶

In [40]:
// Ou plus directement

#include <stdio.h>

void incr(int* a){
    *a = *a + 1;
}

int main(){
    int x = 3;
    incr(&x);
    printf("%d", x);
    return 0;
}
4

On passe directement l'adresse de x, sans utiliser de variable intermédiaire

Fonctions renvoyant "plusieurs" valeurs¶

Une fonction C ne renvoie qu'une seule valeur

NB :

  • en Python aussi, mais on peut facilement renvoyer un couple de valeurs
  • en C, c'est faisable en définissant un struct, mais peu courant

En C, grâce aux pointeurs, on peut passer à la fonction un/des pointeurs lui indiquant où stocker un/des résultats supplémentaires.

On simule ainsi plusieurs valeurs de retour.

Exemple : la fonction scanf

I.3 La fonction scanf (enfin!)¶

NB l'équivalent du input de Python

La fonction scanf permet de lire des informations depuis l'entrée standard

utilisation : int scanf(chaine_de_formatage, arg1, arg2, ...)

utilisation : int scanf(chaine_de_formatage, arg1, arg2, ...)

  • scanf est une fonction à nombre variable d'arguments.

  • arg1, arg2 ... sont des pointeurs vers les emplacements qui contiendront les résultats de la lecture

  • la chaîne de formatage est le premier argument passé à la fonction. C'est une chaîne de caractères contenant des séquences %x qui désignent les informations à lire depuis le flux d'entrée standard (dans l'ordre).

  • renvoie le nombre de valeurs correctement lues

  • il faut tenir compte des types (comme avec printf) :

    • %d : lecture décimal d'un entier
    • %f : lecture décimal d'un flottant
    • %c : lecture d'un caractère unique
    • %s : lecture d'une chaîne de caractères sans espace (ni retour à la ligne ou tabluation)
    • ...

NB : ne pas oublier #include <stdio.h>

In [4]:
// Exemple scanf (à éviter) : lecture d'un entier

#include <stdio.h> 

int main(){
    int x;
    printf("saisissez un nombre entier :");
    scanf("%d", &x); // on place les résultats dans x
    printf("valeur de x : %d", x);
    return 0;
}
saisissez un nombre entier :15
valeur de x : 15

Apparemment, tout va bien, mais ...

In [3]:
// Exemple scanf (à éviter) : saisie incorrecte

#include <stdio.h> 

int main(){
    int x;
    printf("saisissez un nombre entier :");
    scanf("%d", &x); // on place les résultats dans x
    printf("valeur de x : %d", x);
    return 0;
}
saisissez un nombre entier :ab17
valeur de x : 32764

entrée incorrecte : la valeur x n'est pas initialisée ...

In [5]:
// Exemple scanf (OK) : programmation défensive

#include <stdio.h> 

int main(){
    int x;
    printf("saisissez un nombre entier :");
    if (scanf("%d", &x) == 1) { // si une valeur est lue
        printf("valeur de x : %d", x);        
    } else {
        printf("saisie incorrecte");
    }
    return 0;
}
saisissez un nombre entier :ab17
saisie incorrecte
In [6]:
// Exemple scanf (OK) : programmation défensive

#include <stdio.h> 

int main(){
    int x;
    printf("saisissez un nombre entier :");
    if (scanf("%d", &x) == 1) { // si une valeur est lue
        printf("valeur de x : %d", x);        
    } else {
        printf("saisie incorrecte");
    }
    return 0;
}
saisissez un nombre entier :17
valeur de x : 17
In [7]:
// Exemple scanf : lecture d'un float

#include <stdio.h> 

int main(){
    float x;
    printf("saisissez un nombre à virgule :");
    if (scanf("%f", &x) == 1) { // on place les résultats dans x
        printf("valeur de x : %f", x);
    } else {
        printf("saisie incorrecte");
    }
    return 0;
}
saisissez un nombre à virgule :12.5
valeur de x : 12.500000
In [8]:
// Exemple scanf : lecture d'un mot (une chaîne de caractères sans espace)

#include <stdio.h> 

int main(){
    char name[50]; // prévoir suffisamment de place
    printf("saisissez votre :");
    if (scanf("%s", &name) == 1) { // on place les résultats dans name
        printf("valeur de name : %s", name);
    } else {
        printf("saisie incorrecte");
    }
    return 0;
}
saisissez votre :Thomas
valeur de name : Thomas
In [7]:
// Exemple scanf : lecture d'un mot (une chaîne de caractères sans espace)

#include <stdio.h> 

int main(){
    char name[50]; // prévoir suffisamment de place
    printf("saisissez votre :");
    if (scanf("%s", &name) == 1) { // on place les résultats dans name
        printf("valeur de name : %s", name);
    } else {
        printf("saisie incorrecte");
    }
    return 0;
}
saisissez votre :qsfjm//.l jqdslkfj
valeur de name : qsfjm//.l

Seuls les caractères avant l'espace sont lus

In [8]:
// Exemple scanf : lecture de plusieurs valeurs
#include <stdio.h> 

int main(){
    int x,y;
    printf("saisissez un quotient a/b:");
    if (scanf("%d/%d", &x, &y) == 2){ // on place les résultats dans x et y
        printf("numérateur : %d  dénominateur : %d", x, y);
    } else {
        printf("saisie incorrecte");
    }
    return 0;
}
saisissez un quotient a/b:45/11
numérateur : 45  dénominateur : 11
In [11]:
// Exemple scanf : lecture de plusieurs valeurs
#include <stdio.h> 

int main(){
    char lettre;
    float x,y;
    printf("saisissez le nom et les coordonnées d'un point:");
    if (scanf("%c(%f;%f)", &lettre, &x, &y) == 3){
        printf("lettre : %c, abscisse : %f  ordonnée : %f", lettre, x, y);
    } else {
        printf("saisie incorrecte");
    }
    return 0;
}
saisissez le nom et les coordonnées d'un point:A(17.2;-3.4)
lettre : A, abscisse : 17.200001  ordonnée : -3.400000

NB scanf¶

  • scanf lit le flux d'entrée tant qu'il le peut

  • penser à vérifier la valeur de retour (nombre d'informations reconnues)

  • en cas d'échec, le flux n'a pas été absorbé

  • ce qui peut provoquer quelques "bizarreries"

In [14]:
// Incorrect : Bizarrerie scanf : lecture partielle

#include <stdio.h> 

int main(){
    float x;
    printf("saisissez un nombre à virgule :");
    if (scanf("%f", &x) == 1) {
        printf("valeur de x : %f", x);
    } else {
        printf("saisie invalide : retentez votre chance\n");
        if (scanf("%f", &x) == 1) {
            printf("valeur de x : %f", x);
        } else {
            printf("vous êtes vraiment nul ..");
        }
    }
    return 0;
}
saisissez un nombre à virgule :7qdsf
valeur de x : 7.000000
In [1]:
// Incorrect : Bizarrerie scanf : tentative de re-lecture d'un float

#include <stdio.h> 

int main(){
    float x;
    printf("saisissez un nombre à virgule :");
    if (scanf("%f", &x) == 1) {
        printf("valeur de x : %f", x);
    } else {
        printf("saisie invalide : retentez votre chance\n");
        if (scanf("%f", &x) == 1) {
            printf("valeur de x : %f", x);
        } else {
            printf("vous êtes vraiment nul ..");
        }
    }
    return 0;
}
saisissez un nombre à virgule :qsdf
saisie invalide : retentez votre chance
vous êtes vraiment nul ..

Pas de nouvelle saisie : il reste des caractères dans le flux d'entrée

In [15]:
// Correct : Bizarerie scanf : re-lecture d'un float
#include <stdio.h> 

int main(){
    float x;
    printf("saisissez un nombre à virgule :");
    if (scanf("%f", &x) == 1) {
        printf("valeur de x : %f", x);
    } else {
        printf("saisie invalide : retentez votre chance\n");
        while (getchar()!= '\n'){ // on lit les caractères du flux d'entrée jusqu'au dernier
        }
        if (scanf("%f", &x) == 1) {
            printf("valeur de x : %f", x);
        } else {
            printf("vous êtes vraiment nul ..");
        }
    }
    return 0;
}
saisissez un nombre à virgule :qsdkf
saisie invalide : retentez votre chance
12.45
valeur de x : 12.450000

Pour bien faire, il faudrait une boucle while : tant que la saisie n'est pas correcte, redemander

NB¶

  • (HP) les possibilités offertes par scanf sont très nombreuses (comme pour printf)

  • (HP) scanf et %s à l'origine de nombreuses failles de sécurité

    • si la chaîne saisie au clavier est plus longue que l'espace mémoire prévu pour récupérer la chaîne ...

scanf en MP2I/MPI :

  • on ne traitera que les entrées sont correctement saisies
  • on vérifiera systématiquement la valeur renvoyée par scanf (programmation défensive)
  • en cas d'erreur de saisie : on affiche un message d'erreur, puis on met fin au programme (exit(0))
  • on considèrera que les mots saisis ne dépasseront pas la taille mémoire prévue

On pourra éventuellement rattraper une erreur de saisie (cf exemple précédent), en utilisant :

  • char getchar() qui lit un caractère sur le flux d'entrée
  • jusqu'à ce que soit lu '\n' qui termine une saisie au clavier

Utilisation scanf en MP2I/MPI¶

In [1]:
// Scanf, version simple 
#include <stdio.h> 

int main(){
    float x;
    printf("saisissez un nombre à virgule :");
    if (scanf("%f", &x) == 1) {
        printf("valeur de x : %f", x);
    } else {
        printf("saisie invalide \n");
        exit(0);
    }
    printf("FIN");
    return 0;
}
saisissez un nombre à virgule :aeaez
saisie invalide 
In [2]:
// Scanf, version avec rattrapage
#include <stdio.h> 

int main(){
    float x;
    printf("saisissez un nombre à virgule :");
    while (scanf("%f", &x) != 1) {
        printf("saisie invalide, retentez votre chance \n");
        while (getchar()!= '\n'); // on vide le flux d'entrée
    }
    printf("valeur de x : %f", x);
    return 0;
}
saisissez un nombre à virgule :asdfq
saisie invalide, retentez votre chance 
qsdf
saisie invalide, retentez votre chance 
qsdf
saisie invalide, retentez votre chance 
7.3
valeur de x : 7.300000

II. Allocation(s) mémoire¶

Pour l'instant :

  • les variables globales sont allouées dans un espace mémoire à part (allocation statique)

    • durée de vie : toute la durée de l'exécution du programme
  • les variables locales et paramètres sont alloués sur la pile (allocation automatique)

    • durée de vie : uniquement la durée de l'appel de fonction
    • espace créé automatiquement en début de l'appel, et détruit automatiquement en fin d'appel

2 Problèmes :

  • les espaces réservés doivent avoir une taille connue à la compilation (déjà vu)

    • et si on veut un tableau de taille définie par une entrée utilisateur ??
  • un espace alloué sur la pile n'est plus accessible après la fin de l'appel de fonction (subtil)

    • et si on veut une fonction qui renvoie un tableau ??
In [5]:
// Code FAUX
#include <stdio.h>

int* test(){
    int x = 17;
    return &x; // pointeur vers x, mais invalide après la fin de l'appel
}

int main(){
    int* z = test();
    printf("essayons quand même ...\n");
    printf("%d", *z);
    return 0;
}
/tmp/tmpothje9i0.c: In function ‘test’:
/tmp/tmpothje9i0.c:5:12: warning: function returns address of local variable [-Wreturn-local-addr]
    5 |     return &x; // pointeur vers x, mais invalide après la fin de l'appel
      |            ^~
essayons quand même ...
[C kernel] Executable exited with code -11

1 Solution :¶

l'allocation dynamique de mémoire, dans un espace appelé le tas

  • malloc(...) : ("memory allocation") pour allouer de la mémoire dans le tas

  • free(..) : pour libérer cette mémoire allouée

malloc/free¶

void* malloc(taille) :

  • alloue une zone mémoire de taille octets dans le tas, utilisée pour stocker un tableau homogène
  • renvoie un pointeur sur le début de la zone mémoire
  • void* signifie que la fonction renvoie un pointeur de type indéterminé

void free(void* ptr):

  • libère la mémoire allouée (qui devient à nouveau disponible pour d'autres appels à malloc)
  • il faut passer en paramètre le pointeur/l'adresse obtenu/e par l'appel à malloc (NB : pas la taille)

Pour allouer un tableau de n éléments de type T, on a besoin de n*sizeof(T) octets

  • T* tab = (T*) malloc(n*sizeof(T)); NB : l'espace alloué n'est pas extensible

(T*) indique une conversion explicite de type ("transtypage"). On force le pointeur de type void* à prendre le type T*

Pour désallouer ce tableau

  • free(tab); NB : la désallocation n'est pas automatique

CTutor

Pour accéder aux éléments de ce tableau, on utilise la même syntaxe que pour ... les tableaux (!!)

tab[i] : le i-ème emplacement/élément du tableau

CTutor

NB :

  • tab pointe sur le premier élément du tableau
  • donc *tab est le premier élement du tableau
  • donc *tab et tab[0], c'est pareil
  • MP2I/MPI on privilégie la notation tableau

Pointeur ou tableau ???¶

  • T* tab = (T*) malloc(n*sizeof(T)); : pointeur ??

  • tab[i] : tableau ??

En fait, un pointeur et un tableau, c'est (presque) pareil ... (cf fin du cours)

malloc/free : plusieurs usages¶

  • créer un tableau dont la taille n'est pas connue à la compilation (impossible de faire un tableau statique)
In [24]:
// Exemple malloc / free
#include <stdio.h>

int main(){
    int taille;
    printf("Taille du tableau ?\n");
    scanf("%d", &taille);
    int* tab = (int*) malloc(taille * sizeof(int));
    for (int i=0; i<taille; i+=1){
        tab[i] = i*i;
    }
    for (int i=0; i<taille; i+=1){
        printf("%d ", tab[i]);
    }
    free(tab);
    return 0;
}
Taille du tableau ?
20
0 1 4 9 16 25 36 49 64 81 100 121 144 169 196 225 256 289 324 361 

CTutor

malloc/free : plusieurs usages¶

  • des fonctions qui créent et renvoient un tableau
In [18]:
// Exemple malloc / free
#include <stdio.h>
#include <stdlib.h>

int* creeTableauAleatoire(int n){
    int* p = (int*) malloc(n*sizeof(int));
    for (int i = 0; i < 10; i++){
        p[i] = rand()%10;
    }
    return p;
}

int main(){
    int n = 10;
    int* tab = creeTableauAleatoire(n);
    /* ... */
    free(tab);
    return 0;
}

CTutor

malloc/free : plusieurs usages¶

  • créer un "objet" qui peut passer de fonction en fonction
In [ ]:
T* creeTruc(){
    // alloue un Truc dans le tas avec malloc
    T* truc = (T*) malloc(sizeof(T));
    return truc;
}

detruitTruc(T* truc){
    // libère la mémoire avec free
    free(truc);
}

int calculeAvecUnTruc(T* truc){ ... }

int main(){
    T* t = creeTruc();
    /* on fait des trucs avec t ... */
    detruitTruc(t);
}

Plein de problèmes potentiels¶

C'est au programmeur de gérer rigoureusement cette mémoire dynamique (pas de gestion automatique)

Erreurs fréquentes :

  • accéder à la zone avant qu'elle ait été allouée
  • sortir de la zone allouée
  • oublier de libérer une zone allouée (fuite mémoire)
  • libérer deux fois une zone allouée
  • libérer une zone qui n'a pas été allouée

NB : le programmeur ne fait pas tout.¶

  • un allocateur (libc ou OS) gère le tas :

    • décide de l'emplacement exact de la zone mémoire demandée,
    • de recycler les zones mémoires libérées,
    • se souvient de la taille des zones allouées
  • et donc : les appels à malloc ou free ont un coût plus important qu'une allocation/désallocation en pile.

III. et les tableaux alors ...¶

un tableau et un pointeur, c'est (presque) pareil

le nom d'un tableau statique est un pointeur (constant)

int tab[20]; :

  • l'espace réservé est de 20 int.
  • tab est l'adresse du premier élément. Donc tab est aussi de type int*
  • *tab correspond à tab[0]
In [37]:
// Illustration : tableau et pointeur
#include <stdio.h>

int main(){
    int tab[3]={7,8,9};
    int* p = tab; // pas d'erreur de type : tab est un int*
    *p = 4; // p vu comme un pointeur sur le premier élement
    p[2] = 11; // p vu comme un tableau
    printf("%d %d %d", tab[0], tab[1], tab[2]);
    return 0;
}
4 8 11

Notation pour les paramètres de fonctions¶

Pour les paramètres de fonctions, ces deux notations sont équivalentes :

  • mafonction(int t[], ...){ ... }

  • mafonction(int* t, ...){ ... }

car ... un tableau et un pointeur, c'est (presque) pareil

On privilégiera la première forme, plus lisible.

La signature de la seconde forme est ambiguë : le paramètre t est-il un tableau ? ou un pointeur sur un entier isolé ?

In [36]:
// Illustration : tous les cas possibles

#include <stdio.h>

// paramètre tab "version tableau"
int somme1(int tab[], int taille){
    int s = 0;
    for (int i =0; i< taille; i+=1){
        s += tab[i];
    }
    return s;
}

// paramètre tab "version pointeur"
int somme2(int* tab, int taille){
    int s = 0;
    for (int i =0; i< taille; i+=1){
        s += tab[i];
    }
    return s;
}

int main(){
    int tab1[3]={1,2,3}; // tableau statique
    int* tab2 = (int*) malloc(3 * sizeof(int)); // tableau dans le tas
    tab2[0]=1; tab2[1]=2; tab2[2]=3;
    printf("%d %d %d %d", somme1(tab1, 3), somme1(tab2, 3), somme2(tab1, 3), somme2(tab2, 3));
    free(tab2);
    return 0;
}
6 6 6 6

Passage de tous les paramètres par valeur¶

Rappel du "cas particulier" : lorsqu'on passe un tableau en paramètre à une fonction, le tableau est modifiable.

C Tutor

Ce n'est pas le tableau qui est passé en paramètre, mais un pointeur

et on peut donc modifier les données pointées (le contenu du tableau)

Ce cas particulier n'en est pas un :

Lors d'un appel de fonction, tous les paramètres sont passés par valeur. (Certains de ces paramètres sont des pointeurs)

IV. Quelques dilemnes ...¶

Les pointeurs apportent une grande souplesse de programmation.

En C, il y a souvent plusieurs façons de programmer la même chose.

Laquelle choisir ??

NB : il n'y a pas de réponse "définitive", mais il faut comprendre le fonctionnement de l'allocation mémoire en C

Dilemne 1 : allouer dans la pile ou allouer dans le tas ?¶

  • on peut toujours allouer dans le tas, mais c'est (un peu) coûteux, et un peu fastidieux (appels malloc free)
  • allouer dans la pile rend le code plus rapide, et plus concis (pas d'appel malloc/free), mais peut faire grossir la pile (par exemple : allouer un grand tableau + appels récursifs = risque de "stack overflow")

Dilemne 2 : passer un paramètre par pointeur ou pas ?¶

on peut toujours passer par un pointeur ...

In [ ]:
int add(int* a, int* b){ // à éviter !
    return *a + *b;
}

Code correct, mais il est complètemement inutile ici d'utiliser des pointeurs

De plus, la taille occupée par les paramètres sur la pile est 2 * 8 = 16 octets

On écrira bien sûr

In [ ]:
int add(int a, int b){
    return a + b;
}

la taille occupée par les paramètres sur la pile est 2 * 4 = 8 octets

On passe un paramètre par pointeur :

  • s'il a vocation à être modifié (ex : jouer le rôle de valeur de retour)
  • s'il s'agit d'un pointeur vers le tas
  • s'il est jugé trop gros pour être passé sur la pile (c'est le cas des paramètres de type tableau)

Ouf ... le plus dur est fait !!¶

A suivre ... retour sur des détails laissés en chemin¶