Programmation C
Gestion de la mémoire
|
|

|
|

|
Organisation
- Nécessité d'utiliser de la mémoire au cours du fonctionnement d'une application
- Pour le stockage des instructions exécutées
- Segment de texte (non traité ici)
- Pour le stockage des informations gérées/stockées
- Segments de données (variables globales et variables statiques, initialisées ou non)
- Pile
- Tas
- Pile (stack)
- Zone mémoire dédiée au stockage de toutes les variables "locales" existant au cours de la vie du programme
- Paramètres des fonctions
- Lors de tout appel de fonction, création d'une copie locale de chaque paramètre effectif endossée par le paramètre formel correspondant
- Allocation sur la pile de la mémoire nécessaire au stockage de ces copies locales
- Existence de ces copies locales durant l'exécution de la fonction
-> Dépilement et cessation d'existence de ces copies à la fin de la fonction
- Valeurs retour des fonctions
- Empilement de la valeur retour au cours de son retour
- Variables locales aux fonctions
- Lors de toute exécution d'une fonction, allocation sur la pile de la mémoire nécessaire au stockage des variables de portée locale à la fonction
- Dépilement à la fin de l'exécution de la fonction
-> Cessation d'existence de ces variables
- Variables locales aux blocs d'instructions
- Lors de tout exécution d'un bloc d'instructions (séquence d'instructions délimitée par un couple accolade ouvrante - accolade fermante), allocation sur la pile de la mémoire nécessaire au
stockage des variables de portée locale au bloc d'instructions
- Dépilement à la fin de l'exécution du bloc
-> Cessation d'existence de ces variables
- Conclusion : empilement de toutes les variables locales dans la pile au cours de leur existence, dépilement à la fin de leur existence
- Gestion automatique de ces empilements/dépilements au cours de la vie du programme
-> Pas de la responsabilité du développeur de le faire
- Problème pratique : taille de la pile
- Pile énormément utilisée -> utilisation aussi rapide que possible nécessaire
- Impossible d'agrandir ou de réduire la taille de la pile à chaque accès pour un empilement ou un dépilement
- Agrandissement possible et géré automatiquement (définition possible de la taille initiale à la compilation et/ou à l'exécution), mais pas de réduction
- Agrandissement limité par l'espace mémoire dont dispose l'application
- Si plus de place, plantage
- Se méfier de la quantité de mémoire nécessaire au stockage des paramètres de fonction et des variables locales de fonction (minimiser la place nécessaire quitte à sacrifier un peu de performance ?),
surtout si on utilise la récursivité et que celle-ci est de profondeur importante
- Attention : taille de la pile relativement limitée par défaut
- Tas (heap)
- Zone mémoire dédiée aux allocations dynamiques de mémoire (voir + loin)
- Taille du tas : toute la mémoire allouée à l'application après retrait du segment de texte et des segments de données
-> "Grande" taille
|
Allocation dynamique
- Définition : réservation temporaire exclusive d'une certaine quantité de mémoire en un bloc d'octets contigus
- Lors d'une allocation, quantité réservée déterminée au choix du développeur, seulement limitée par la possibilité de trouver une zone mémoire de taille suffisante
- Possibilité de faire autant d'allocations mémoires différentes simultanées que souhaité dans la limite de la mémoire disponible
- Fonctionnalité très importante des langages informatiques
- Possibilité d'implanter des structures de données dynamiques de stockage d'information
- liste
- vecteur
- ensemble
- collection
- pile
- file
- table de hachage
- arbre
- graphes
- ...
- Possibilité de développer des programmes qui vont pouvoir s'adapter à la taille des données qu'ils doivent traiter
- Quantité de mémoire utilisée ajustée strictement à ce qui est nécessaire (pas de surutilisation)
- ...
- Allocation dynamique utilisée pour réserver des tableaux mais aussi des enregistrements uniques
- Trois fonctions déclarées dans l'API standard stdlib.h (à inclure obligatoirement)
- Fonctions utilisées en association avec l'opérateur sizeof car il est nécessaire de calculer en octets les tailles d'allocation
|
Désallocation
- Contrairement à certains langages informatiques (Java, C#...), pas de ramasse-miettes (garbage collector, GC) automatique en C
-> Pas de processus en tâche fond permettant de libérer de façon automatique les allocations dynamiques de mémoire qui ne servent plus de façon que cette mémoire puisse resservir
-> Travail à la charge du développeur
-> Si travail pas fait ou mal fait, non restitution et donc augmentation progressive de la quantité de mémoire utilisée par un programme (et donc par le système d'exploitation) -> cessation
d'activité ou plantage inéluctable du programme
- Seule désallocation réalisée automatiquement : zone mémoire réallouée si un realloc aboutit
- Une seule fonction déclarée dans l'API standard stdlib.h
- free
Syntaxe
void free(void* ptr);
Désallocation de la zone mémoire pointée par ptr
- Exemples : Gestion de la désallocation pour les 4 programmes exemples d'allocation ci-dessus
#include <stdio.h>
#include <stdlib.h>
void main(void) {
int* ptr = (int*) malloc(sizeof(int));
if (ptr == NULL) {
printf("Allocation avortee\n");
}
else {
printf("Allocation reussie a l'adresse : %p\n", ptr);
}
if (ptr != NULL) {
printf("Desallocation a l'adresse : %p\n", ptr);
free(ptr);
ptr = NULL;
}
else {
printf("Attention, tentative de desallocation de NULL");
}
}
|
Malloc1Free.c - Malloc1Free.exe
|
Allocation reussie a l'adresse : 000001A0D8C5AA60 Desallocation a l'adresse : 000001A0D8C5AA60
|
|
#include <stdio.h>
#include <stdlib.h>
void desallocation(void* ptr) {
if (ptr != NULL) {
printf("Desallocation a l'adresse : %p\n", ptr);
free(ptr);
}
else {
printf("Attention, tentative de desallocation de NULL");
}
}
void main(void) {
size_t taille1 = 5;
printf("Allocation de %zu octets ", taille1 * sizeof(double));
printf("pour un tableau de %zu double\n", taille1);
double* ptr1 = (double*) malloc(taille1 * sizeof(double));
if (ptr1 == NULL) {
printf("Allocation avortee\n");
}
else {
printf("Allocation reussie a l'adresse : %p\n", ptr1);
}
size_t taille2 = 80 * (1024ULL * 1024ULL * 1024ULL);
double* ptr2 = (double*) malloc(taille2);
printf("Tentative d'allocation de %zu octets ", taille2);
printf("soit %zu Go\n", taille2 >> 30);
if (ptr2 == NULL) {
printf("Allocation avortee\n");
}
else {
printf("Allocation reussie a l'adresse : %p\n", ptr2);
}
printf("\n");
desallocation(ptr1);
ptr1 = NULL;
desallocation(ptr2);
ptr2 = NULL;
}
|
Malloc2Free.c - Malloc2Free.exe
|
Allocation de 40 octets pour un tableau de 5 double
Allocation reussie a l'adresse : 0000024F9ED1E290
Tentative d'allocation de 85899345920 octets soit 80 Go
Allocation avortee
Desallocation a l'adresse : 0000024F9ED1E290
Attention, tentative de desallocation de NULL
|
|
#include <stdio.h>
#include <stdlib.h>
void desallocation(void* ptr) {
if (ptr != NULL) {
printf("Desallocation a l'adresse : %p\n", ptr);
free(ptr);
}
else {
printf("Attention, tentative de desallocation de NULL");
}
}
void main(void) {
size_t taille = 500000;
int* ptr = (int*) calloc(taille, sizeof(int));
if (ptr == NULL) {
printf("Allocation avortee\n");
}
else {
printf("Allocation reussie a l'adresse : %p\n", ptr);
}
desallocation(ptr);
ptr = NULL;
}
|
Calloc1Free.c - Calloc1Free.exe
|
Allocation reussie a l'adresse : 0000016F60955040 Desallocation a l'adresse : 0000016F60955040
|
|
#include <stdio.h>
#include <stdlib.h>
void desallocation(void* ptr) {
if (ptr != NULL) {
printf("Desallocation a l'adresse : %p\n", ptr);
free(ptr);
}
else {
printf("Attention, tentative de desallocation de NULL");
}
}
int main(void) {
size_t taille1 = 3;
size_t taille2 = 5;
size_t taille3 = 2;
int* ptr = (int*) malloc(taille1 * sizeof(int));
int* tmp;
if (ptr == NULL) {
printf("Allocation avortee\n");
exit(1);
}
printf("Allocation reussie a l'adresse : %p\n", ptr);
for (int i = 0; i < taille1; i++) {
ptr[i] = i;
}
tmp = (int*) realloc(ptr, taille2 * sizeof(int));
if (tmp == NULL) {
printf("Reallocation avortee\n");
desallocation(ptr);
ptr = NULL;
exit(2);
}
ptr = tmp;
printf("Reallocation reussie a l'adresse : %p\n", ptr);
tmp = (int*) realloc(ptr, taille3 * sizeof(int));
if (tmp == NULL) {
printf("Reallocation avortee\n");
desallocation(ptr);
ptr = NULL;
exit(3);
}
ptr = tmp;
printf("Reallocation reussie a l'adresse : %p\n", ptr);
desallocation(ptr);
ptr = NULL;
return 0;
}
|
Realloc1Free.c - Realloc1Free.exe
|
Allocation reussie a l'adresse : 000001CDDAD6E560
Reallocation reussie a l'adresse : 000001CDDAD6E5A0
Reallocation reussie a l'adresse : 000001CDDAD6B1C0
Desallocation a l'adresse : 000001CDDAD6B1C0
|
|
|
Erreurs fréquemment commises par les développeurs
- Oublier de faire une allocation mémoire
-> Réalisation de lectures et d'écritures mémoire là où cela ne devrait pas être (pas de contrôle à l'exécution)
-> Plantage qui peut sembler aléatoire
- Se tromper dans le nombre d'octets à allouer en n'allouant pas assez de place
-> Réalisation de lectures et d'écritures mémoires là où cela ne devrait pas être (pas de contrôle à l'exécution)
-> Plantage qui peut sembler aléatoire
Exemple : Allouer n octets pour une chaînes de caractères de n caractères alors qu'il en faut n+1
- Ne pas désallouer certaines zones allouées
-> Saturation de la mémoire disponible
-> Plantage qui peut sembler aléatoire
Solution éventuelle : Implanter l'équivalent un ramasse miettes
- Désallouer deux fois la même zone mémoire
-> Plantage qui peut sembler aléatoire
Solution : Après désallocation, affecter systématiquement les pointeurs désalloués avec NULL, ne désallouer un pointeur que s'il est différent de NULL
- Pas d'outil standard pour détecter ce type de problèmes
- Quelques outils non standards mais couramment utilisés
- Préoccupation permanente du développeur : valider la gestion de la mémoire du point de vue des allocations et des désallocations
Pas toujours simple car il ne faut pas seulement prévoir la ou les situations où tout se passe bien, mais aussi toutes les situations où une allocation échoue
voire échoue alors que toutes les allocations précédentes ont abouti
|
Allocation dynamique et fonctions
- Possibilité d'utiliser l'allocation dynamique dans les fonctions
- Attention : pas de désallocation automatique en fin de fonction de ce qui a été alloué au cours de l'exécution de la fonction
- Possibilité d'utiliser l'allocation dynamique dans une fonction et de faire retourner le pointeur sur la zone allouée en retour de fonction
- Attention : obligation pour la fonction appelante de réaliser la désallocation réalisée dans la fonction appelée
- Exemple
#include <stdio.h>
#include <stdlib.h>
float* creerTableauFloat(size_t n) {
return (float*)calloc(n, sizeof(float));
}
int* creerEtInitialiserTableauInt(size_t n, int val) {
int* ti = (int*)calloc(n, sizeof(int));
if (ti != NULL) {
for (int i = 0; i < n; i++) {
ti[i] = val;
}
}
return ti;
}
void main(void) {
size_t taille1 = 3;
size_t taille2 = 5;
float* tf = creerTableauFloat(taille1);
if (tf == NULL) {
printf("Allocation du tableau de flotants avortee\n");
}
else {
printf("Allocation tableau de flotants reussie a l'adresse : %p\n", tf);
free(tf);
tf = NULL;
}
int* ti = creerEtInitialiserTableauInt(taille2, -1);
if (ti == NULL) {
printf("Allocation du tableau d'entiers avortee\n");
}
else {
printf("Allocation du tableau d'entiers reussie a l'adresse : %p\n", ti);
for (int i = 0; i < taille2; i++) {
printf("%3d", ti[i]);
}
printf("\n");
free(ti);
ti = NULL;
}
}
|
AllocationDynamiqueEtFonctions.c - AllocationDynamiqueEtFonctions.exe
|
|
|
Pointeur sur pointeur et allocation dynamique
- Possibilité de définir des variables de type pointeur sur pointeur sur type permettant ainsi de gérer des tableaux dynamiques à 2 dimensions (à deux indices)
- Exemple : int** t;
- Usuellement, premier indice = numéro de ligne, second indice = numéro de colonne
- Pointeur de pointeur = tableau unidimensionnel de taille n de pointeurs = tableau unidimensionnel de taille n de tableaux unidimensionnels de tailles constantes ou variables = matrice si tailles
constantes pour les lignes
- Premier niveau de pointeur : tableau de n pointeurs sur type alloué dynamiquement
- Second niveau de pointeur : n tableaux de type alloués dynamiquement avec la taille souhaitée pour chaque ligne définissant ainsi le nombre d'éléments stockables sur la ligne
- Valeurs de chaque ligne de la matrices stockées contiguement en mémoire (allocation de chaque ligne en une seule allocation)
- Lignes de valeurs non forcément stockées contiguement en mémoire (autant d'allocations que de lignes)
- Généralisable aux dimensions supérieures
- Exemple
#include <stdio.h>
#include <stdlib.h>
void main(void) {
size_t nbL = 5;
size_t nbC = 500000;
printf("Allocation de %zu x %zu int\n", nbL, nbC);
int** ti =(int**) calloc(nbL, sizeof(int*));
for (int i = 0; i < nbL; i++) {
ti[i] = (int*) calloc(nbC, sizeof(int));
}
for (int i = 0; i < nbL; i++) {
printf("%p\n", ti[i]);
}
printf("\n");
printf("Desallocation\n");
if (ti != NULL) {
for (int i = 0; i < nbL; i++) {
free(ti[i]);
ti[i] = NULL;
}
free(ti);
ti = NULL;
}
}
|
PointeurSurPointeur1.c - PointeurSurPointeur1.exe
|
Allocation de 5 x 500000 int
000001E6C7A1D040
000001E6C7C1F040
000001E6C7E15040
000001E6C8001040
000001E6C81FE040
Desallocation
|
|
#include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t nbL = 100;
size_t nbC = 500000000;
printf("Tentative d'allocation pour %zu x %zu entiers\n", nbL, nbC);
printf("soit %zu octets (environ %zu Go)\n",
nbL * nbC * sizeof(int),
(nbL * nbC * sizeof(int)) >> 30);
int** ti =(int**) calloc(nbL, sizeof(int*));
if (ti != NULL) {
int i = 0;
while ((i < nbL) && ((ti[i] = (int*) calloc(nbC, sizeof(int))) != NULL)) {
i++;
printf("+");
}
printf("\n");
if (ti[nbL - 1] == NULL) {
printf("Allocation impossible a la ligne %d/%zu\n", i + 1, nbL);
printf("Desallocation de ce qui avait ete alloue\n");
for (int j = 0; j < i; j++) {
free(ti[j]);
ti[j] = NULL;
printf("-");
}
printf("\n");
free(ti);
ti = NULL;
}
}
printf("\n");
if (ti != NULL) {
printf("Allocation reussie\n");
}
else {
printf("Allocation avortee\n");
}
if (ti != NULL) {
for (int i = 0; i < nbL; i++) {
free(ti[i]);
ti[i] = NULL;
}
free(ti);
ti = NULL;
printf("Desallocation terminee\n");
}
return 0;
}
|
PointeurSurPointeur2.c - PointeurSurPointeur2.exe
|
Tentative d'allocation pour 100 x 500000000 entiers
soit 200000000000 octets (environ 186 Go)
+++++++++++++++++++++++++++++++++++++++++++
Allocation impossible a la ligne 44/100
Desallocation de ce qui avait ete alloue
-------------------------------------------
Allocation avortee
|
|
#include <stdio.h>
#include <stdlib.h>
int** creerMatrice(size_t nbLignes, size_t nbColonnes) {
int** ti = (int**) calloc(nbLignes, sizeof(int*));
if (ti != NULL) {
int i = 0;
while ((i < nbLignes) &&
((ti[i] = (int*) calloc(nbColonnes, sizeof(int))) != NULL)) {
i++;
}
if (ti[nbLignes - 1] == NULL) {
for (int j = 0; j < i; j++) {
free(ti[j]);
ti[j] = NULL;
}
free(ti);
ti = NULL;
}
}
return ti;
}
void detruireMatrice(int** ti, size_t nbLignes, size_t nbColonnes) {
if (ti != NULL) {
for (int i = 0; i < nbLignes; i++) {
free(ti[i]);
ti[i] = NULL;
}
free(ti);
ti = NULL;
}
}
void test(size_t nbL, size_t nbC) {
printf("Tentative d'allocation pour %zu x %zu entiers\n", nbL, nbC);
printf("soit %zu octets (environ %.2lf Go)\n",
nbL * nbC * sizeof(int),
((nbL * nbC * sizeof(int)) >> 20)/1024.0);
int** ti = creerMatrice(nbL, nbC);
if (ti != NULL) {
printf("Allocation reussie\n");
detruireMatrice(ti, nbL, nbC);
ti = NULL;
printf("Desallocation terminee\n");
}
else {
printf("Allocation avortee\n");
}
}
void main(void) {
test(100, 500000000);
printf("\n");
test(1000, 500000);
}
|
PointeurSurPointeur3.c - PointeurSurPointeur3.exe
|
Tentative d'allocation pour 100 x 500000000 entiers
soit 200000000000 octets (environ 186.26 Go)
Allocation avortee
Tentative d'allocation pour 1000 x 500000 entiers
soit 2000000000 octets (environ 1.86 Go)
Allocation reussie
Desallocation terminee
|
|
|