Table des matières
Mémoire et malloc
C, on a eu l'occasion de le dire, est proche de la machine et permet un contrôle fin de l'utilisation des capacités de la machine. Cela passe par une suppression de certains automatismes qui facilitent l'utilisation d'autres langages.
processus
Petit rappel terminologique : le programme est le fichier texte dans lequel on écrit en langage C ou autre. Quand le programme est exécuté, on parle de processus. Puisque nous allons parler de l'utilisation mémoire lors de l'exécution du programme, il faudra parler de processus.
Exemple
Pour bien comprendre voyons un exemple : supposons que l'on veuille une fonction qui fabrique un tableau 4, 8, 9, 15, 45.
Python
exemple en python
def creer_tableau():
tableau = [4, 8, 9, 15, 45]
return tableau
t = creer_tableau()
print(t[1])
Ce code fonctionne sans difficulté et on n'imagine pas qu'il puisse créer un problème car on a l'habitude de certains automatismes. Le C met à jour ces automatismes et va nous montrer que ce n'est pas si simple.
C
exemple c
#include <stdio.h>
#include <stdlib.h>
int* creer_tableau(){
int tableau[5] = {4, 8, 9, 15, 45};
return tableau;
}
int main()
{
int *t = creer_tableau();
printf("%d\n", t[1]);
return 0;
}
La compilation ne passe pas. Pourquoi ?
Variable locale
La variable tableau créée dans creer_tableau est locale, c'est à dire qu'elle ne devrait vivre que le temps de l'exécution de la fonction creer_tableau. Sitôt la fonction terminée, l'espace mémoire réservé pour le contenu du tableau doit être libéré.
Je rappelle que la variable tableau contient l'adresse mémoire de la première case du tableau. C'est un pointeur (d'où la déclaration int * ligne 4).
Alors:
- La fonction réserve des cases mémoires commençant à l'adresse
tableau, - La fonction renvoie l'adresse
tableau - En même temps qu'elle renvoie l'adresse, la fonction détruit le contenu en mémoire, si bien que l'adresse ne pointe plus sur rien !
On dit plutôt libérer une case mémoire plutôt que détruire une case mémoire.
Ça marche en Python
Python utilise des mécanismes automatiques qui dans ce genre de cas va empêcher la libération des cases mémoires réservées dans la fonction quand on en a encore besoin après la fin de la fonction.
Cela signifie que Python décide automatiquement quelles mémoires il doit conserver et quelles mémoires il doit libérer. Cela fonctionne très bien en général mais n'est pas forcément optimum.
Le heap et le stack
heap = tas, stack = pile
Décrivons ici comment son gérées ces mémoires.
- Le système d'exploitation alloue au processus un espace mémoire. Le processus ne peut pas accéder n'importe comment à la mémoire physique de l'ordinateur sinon rien n'empêcherait un processus malicieux d'aller perturber les zones mémoires des autres processus.
- Le processus gère donc sa zone mémoire, allouée par l'OS. Il va l'organiser en deux parties, la pile et le tas.
- La pile correspond aux zones mémoires qui correspondent aux fonctions. Quand une fonction débute son exécution, ses variables locales sont placées dans la pile. Et lorsque la fonction se termine, ses variables locales sont dépilées (donc libérées)
- Le tas est une zone qui sera accessible différemment et dans laquelle on pourra créer des mémoires sans qu'elles soit dépilées.
Mais attention ! Quand on crée une mémoire dans le tas, cette mémoire ne se libérera pas toute seule.
Toute la mémoire est néanmoins libérée à la fin du processus !
Cela veut dire que si on crée beaucoup de variable dont la mémoire est dans le tas, on risque de faire gonfler la consommation mémoire du processus. Il faut donc veiller à libérer les mémoires ainsi créées.
Réserver la mémoire où on l'utilise
Une première solution à ce problème est réserver l'espace nécessaire à l'endroit où on en aura besoin.
Si j'ai besoin du tableau dans main, je réserve la mémoire dans le main, pas dans la fonction creer_tableau.
- On réserve donc le tableau
tligne 14, - Ligne 15, on transmet
tà la fonctioncreer_tableaupour qu'elle écrive le contenu du tableau.
exemple c
int main()
{
int t[5];
creer_tableau(t);
printf("%d\n", t[1]);
return 0;
}
Dans ce cas il faut adapter la fonction creer_tableau qui n'a plus à renvoyer un tableau, puisque le tableau lui est fourni, la fonction n'a qu'à le remplir.
exemple c
void creer_tableau(int *tableau){
tableau[0] = 4;
tableau[1] = 8;
tableau[2] = 9;
tableau[3] = 15;
tableau[4] = 45;
}
L'avantage de cette méthode est que l'on n'a pas à se soucier du temps de vie de l'espace mémoire réservé. C libérera la variable t suivant un mécanisme normal.
Le malloc
Toutes les déclarations vues jusqu'à présent, comme int i = 12; créait les variables dans la pile. Quand on veut créer une variable dans le tas, il faut utiliser malloc.
Comprendre memory allocation. On qualifie cette allocation de dynamique.
malloc renvoie un pointeur et il faut préciser la taille nécessaire.
Cas du tableau
- nous voulions créer
tableau = {4, 8, 9, 15, 45}, c'est donc un tableau de 5 entiers. On a donc besoin d'une zone mémoire dont la taille est de 5int. - Mais quelle est la taille en mémoire d'un
int? On sait qu'unint, en C, occupe 2 octets. Mais le mieux est d'utilisersizeof(int)qui répond directement à la question. Notre tableau occupe donc5*sizeof(int). - L'allocation dynamique (dans le tas) se fera donc par
malloc(5*sizeof(int))(ligne 5 ci-dessous)
mallocne sera pas compris par un compilateur C++. Si vous utilisez C playground, pensez à configurer la compilation en C.
allouer avec malloc
#include <stdio.h>
#include <stdlib.h>
int* creer_tableau(){
int *tableau = malloc(5*sizeof(int));
if (tableau == NUll){
exit(1); // interrompt l'exécution avec erreur
}
tableau[0] = 4;
tableau[1] = 8;
tableau[2] = 9;
tableau[3] = 15;
tableau[4] = 45;
return tableau;
}
int main()
{
int *t = creer_tableau();
printf("%d\n", t[1]);
return 0;
}
- À présent,
tableaun'est plus dans la pile, l'espace mémoire n'est plus libéré avec la fin decreer_tableau.t(ligne 19) pointe donc sur une zone mémoire valide. - Même si le processus dure encore longtemps,et même si on ne se sert plus du tableau, la zone mémoire reste réservée.
- Quand la fonction
main()se termine, le processus prend fin et toute la mémoire est libérée. - Remarquez l'ajout d'un test, lignes 6 à 8, qui interrompt le processus s'il n'a pas été possible de réserver l'espace mémoire.
Libérer une mémoire du tas
Puisque la libération de la mémoire ne se fait plus automatiquement, il peut devenir utile de demander explicitement la libération, pour limiter la consommation mémoire du processus.
Il faut utiliser la fonction free.
Libérer avec free
int main()
{
int *t = creer_tableau();
printf("%d\n", t[1]);
free(t);
return 0;
}
Nous avons ajouter la ligne 18.
