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
¶
// 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
// 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¶
NB : le bloc d'activation de la fonction main
réserve un espace pour
- les 4 variables locales
x
,pt0
,p
etq
- 0 paramètre
Soit une taille totale de $4+2 \times 4+8+8=28$ octets (approx). Cf CTutor
// 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){...}
// 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 deb
on met la valeur dea
// 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¶
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
- directement :
on peut bien sûr modifier la valeur des pointeurs (et les faire pointer ailleurs)
Exemple 1 : Que fait le code suivant ?¶
// 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;
}
//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
Exemple 2 : Que fait le code suivant ?¶
//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;
}
//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
Exemple 3 : Que fait le code suivant ?¶
//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;
}
//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
Attention :¶
on ne déréférence (
*p
) que les pointeurs validesdé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)
//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
//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)
// 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 typeint
- sous-entendu
pour le commun des mortels (MP2I/MPI) :
int* p = &x
- sous-entendu
p
est de typeint*
- sous-entendu
pour les indécis :
int * p = &x
A retenir : on ne définit qu'un pointeur à la fois (pourquoi ??)
// 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
// 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
- ex :
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 exemplesizeof(int*)
- avec
malloc
:p = (T*) malloc(...)
(cf plus tard)
- ex :
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
// 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;
}
// 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
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
// 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 !
// 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
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¶
// 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 lecturela 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>
// 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 ...
// 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 ...
// 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
// 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
// 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
// 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
// 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
// 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
// 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 peutpenser à 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"
// 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
// 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
// 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 pourprintf
)(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¶
// 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
// 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 ??
// 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 tasfree(..)
: 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
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
NB :
tab
pointe sur le premier élément du tableau- donc
*tab
est le premier élement du tableau - donc
*tab
ettab[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)
// 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
malloc/free
: plusieurs usages¶
- des fonctions qui créent et renvoient un tableau
// 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;
}
malloc/free
: plusieurs usages¶
- créer un "objet" qui peut passer de fonction en fonction
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
oufree
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. Donctab
est aussi de typeint*
*tab
correspond àtab[0]
// 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é ?
// 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.
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 ...
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
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)