Plan cours La notion de pointeur et d’adresse mémoire. Le passage de paramètres par adresse (ou référence). Pointeur sur un tableau. La notion d’allocation dynamique. malloc, realloc, calloc free Les erreurs classiques de l’utilisation d’adresses.
La notion de pointeur et d’adresse Chaque variable définie se trouve à une adresse numérique en mémoire. Par abus de langage, en C, un pointeur est une variable dont le contenu est une adresse mémoire. Cette adresse mémoire contient une donnée d’un certain type alors nous parlons toujours de pointeur sur un type (ex: pointeur sur un entier, pointeur sur un double, pointeur sur un pointeur, …) En C On définit un pointeur en mettant une * lors de la définition à droite du type sur lequel le pointeur pointera (ex: int* p; p est un pointeur sur un entier) Un pointeur n’est pas initialisé lors de sa définition. On peut obtenir l’adresse mémoire d’une variable à l’aide de &. Donc, &p donne l’adresse où se trouve p en mémoire. Et oui, un pointeur a une adresse mémoire. On peut consulter ou modifier le contenu mémoire à l’adresse contenue dans une variable-pointeur.
La notion de pointeur et d’adresse Imaginons qu’une variable x est définie à l’adresse mémoire 1000. Imaginons qu’une variable pointeur est définie et affectée à la valeur de l’adresse de x. int x = 5; int* ptr_x = //obtenir l’adresse de x ptr_x (1000) x On peut modifier ou consulter le contenu de x via la variable ptr_x en faisant *ptr_x. &x; 1000 5
Le passage de paramètre par adresse (ou référence) Il n’est pas possible par défaut de modifier un paramètre effectif via un paramètre formel puisque la valeur est passée par copie. La première utilisation que nous faisons des pointeurs est pour permettre la modification d’un paramètre effectif par un sous-programme via son paramètre formel. Technique : Nous envoyons l’adresse du paramètre effectif plutôt que son contenu. Il sera alors possible de modifier le paramètre effectif via le pointeur.
Le passage de paramètre par adresse (ou référence) Exemple : void echanger_valeur(int* ptr_v1, int* ptr_v2) { int tmp = *ptr_v1; *ptr_v1 = *ptr_v2; *ptr_v2 = tmp; } //exemple d’appel int x = 5; int y = 7; echanger_valeur(&x, &y); //x vaudra 7 et y vaudra 5 après l’exécution **Pensez à scanf
Pointeur sur un tableau La deuxième utilisation que nous faisons des pointeurs est pour obtenir l’adresse d’un tableau. Le nom de la variable d’un tableau statique est un pointeur sur la première case du tableau. C’est automatique, nous n’avons rien de spécial à écrire sinon donner le nombre de cases avec [] lors de la définition de la variable. On sait qu’on peut obtenir le contenu d’une case en utilisant les [] mais on peut aussi à l’aide de l’arithmétique des pointeurs où on peu ajouter ou soustraire une valeur d’une adresse. L’adresse obtenue dépend du nombre d’octets nécessaires selon le type pointé.
Pointeur sur un tableau Illustration: int tab[MAX]; // tab est le pointeur sur la première case tab On accède à une case par tab[indice] ou *(tab+indice) où indice est le nombre de cases * sizeof(int) à partir de l’adresse contenue dans tab. Exemple : tab[2] est équivalent à *(tab+2) et correspond à l’adresse du début de la troisième case soit tab + 2 * sizeof(int). ***Habituellement, on utilise [].
La notion d’allocation dynamique L’allocation dynamique est la possibilité de requérir de l’espace mémoire (et d’en obtenir l’adresse) en cours d’exécution du programme. Il y a trois fonctions qui permettent cela : void * malloc(size_t size); Alloue size * le nombre d’octets et retourne l’adresse void * calloc ( size_t num, size_t size ); Alloue num * size octets et retourne l’adresse en plus d’initialiser chaque case à 0.
La notion d’allocation dynamique void * realloc ( void * ptr, size_t size ); Alloue size octets d’espace mémoire et copie les size premiers octets de ptr à l’adresse qui sera retournée. Il arrive que l’adresse retournée soit ptr s’il y avait assez de d’espace au bout du tableau pour l’allonger. Si ptr est null, la fonction se comporte comme malloc. À l’utilisation, il faut convertir le type void* en celui du pointeur qui recevra l’adresse. int* tab_entier = (int*) malloc(NB * sizeof(int)); double* tab_notes = (double*) calloc(NB,sizeof(double));
La notion d’allocation dynamique La mémoire allouée par une de ces fonctions l’est à un endroit différent d’une variable locale. Ce qui fait qu’elle reste allouée même si l’allocation est réalisée dans un sous-programme. Après l’exécution d’une des fonctions d’allocation, la mémoire alloué est disponible pour utilisation et accessible via le pointeur qui a reçu l’adresse.
La notion d’allocation dynamique Allouer un tableau à deux dimensions dynamiquement (int**). Il faut allouer un tableau de la taille du nombre de lignes Il faut allouer un autre tableau de la taille du nombre de colonnes pour chaque ligne (dans une boucle). int ligne; int** tab2D = (int**) malloc(NB_LIGNES * sizeof(int*)); for(ligne =0; ligne < NB_LIGNES; ligne++) tab[ligne] = (int*) malloc(NB_COLONNES * sizeof(int)); ***On peut utiliser la notation [ ][ ] par la suite.
La notion d’allocation dynamique Un des avantages de l’allocation dynamique est de pouvoir libérer l’espace lorsqu’il n’est plus utile. On libère l’espace mémoire à l’aide de free ou de realloc avec une taille de 0*. Exemple : - free(tab) //libère la mémoire dont l’adresse est //contenue dans tab) realloc(tab,0); //Libère l’espace de tab et retourne null. *La libération de la mémoire inutilisée est obligatoire dans ce cours *Prenez l’habitude de mettre le pointeur à NULL après un free ( tab = NULL)
Les erreurs classiques de l’utilisation d’adresses. Il y a plusieurs erreurs lorsqu’on débute avec la notion d’adresse. En voici quelques-unes. Confondre l’adresse de la variable pointeur de l’adresse contenu dans la variable pointeur. En effet, une variable pointeur a une adresse en mémoire comme toutes les variables. int* ptr = &x; //ptr == &x mais &ptr != ptr
Les erreurs classiques de l’utilisation d’adresses. Illustration: On désire utiliser scanf dans un sous-programme pour saisir une valeur d’un paramètre effectif dont nous avons reçu l’adresse . void saisir(int* valeur_a_saisir) { scanf (« %d », valeur_a_saisir); } *Ici, on remarque qu’il n’y a pas de & à l’appel de scanf puisque ce serait l’adresse du paramètre formel et non celui du paramètre effectif.
Les pièges de l’utilisation d’adresses. 2. L’adresse d’une variable locale d’un sous-programme n’existe plus lorsque le sous-programme est terminé. Un piège est de retourner l’adresse d’une variable locale et de penser qu’on retourne une adresse valide. int* créer_tableau() { int tab[MAX]; return tab; } *L’adresse retournée sera celle d’une mémoire non allouée dynamiquement.
Les pièges de l’utilisation d’adresses. 3. Définir un tableau 2D avec ** et ne pas allouer l’espace de la deuxième dimension (voir acétate 11). 4. Tenter d’utiliser le passage de paramètres par référence pour allouer l’espace d’un tableau dans un sous-programme et le retourner via les paramètres formels. Le piège est de ne pas utiliser l’adresse de la variable tableau. void alloue(int* tab[], int nb) { *tab = (int*) malloc (nb*sizeof(int)); }