Les pointeurs, même plus peur... (Partie 1 sur 2) - Perfectionnement > Le C et l'ASM - Articles et téléchargements
Les pointeurs, même plus peur... (Partie 1 sur 2) - Perfectionnement > Le C et l'ASM - Articles et téléchargements
Pseudo Pass se souvenir de moi     Créer un compte
ARTICLES et TELECHARGEMENTS ~ FORUMS ~ LIENS  
 
             
 
proposer
 
   
             
 
Catégories
 
   
             
 
Recherche
 
   
 
Articles-et-telechargements > Perfectionnement > Le C et l'ASM > Les pointeurs, même plus peur... (Partie 1 sur 2)

Les pointeurs, même plus peur... (Partie 1 sur 2)

Publié par maverick59 le 10/03/2013 (4979 lectures)
Première édition le 10/03/2013
L'utilisation des pointeurs en C est incontournable pour qui veut coder proprement et efficacement. Nous proposons ici une approche simple pour les découvrir, par des exemples et la comparaison au codage sans pointeur. Il n'est pas question ici de faire un cours magistral sur les pointeurs mais seulement d'en comprendre l’intérêt et les principes de base, sans se perdre dans les détails.

Une fois cet article assimilé, vous pourrez découvrir la suite : Les pointeurs, approfondissement (Partie 2 sur 2)

Cet article est articulé en quelques points principaux :
  • Une courte intro
  • Un petit rappel sur les adresses et la déclaration de variables
  • La découverte des pointeurs et de leur déclaration
  • Le passage de pointeurs à une fonction
  • Un résumé

Intro

Cet article utilise certaines notions considérées comme acquises, telles que la déclaration et l’initialisation d’une variable, l’appel d’une fonction, le passage de valeurs à une fonction. Il en sera fait un rapide rappel, si vous ne vous sentez pas à l’aise avec ces notions, commencez par lire d’autres articles afin de vous familiariser avec les bases du C.

Une histoire d’adresse

La mémoire d’un micro-contrôleur, tout comme celle d’un ordinateur peut être représentée sous forme d’un tableau de 2 colonnes et d’un grand nombre de lignes (surtout pour un ordinateur), voici un exemple totalement arbitraire :
AdressesContenu
0255
13
2123
342
412
......
10000
......
(Très grand nombre)42
Au démarrage, les valeurs contenues dans la RAM (mémoire volatile) sont totalement aléatoires tant qu’elles ne sont pas initialisées dans le programme. Ce n’est pas le cas de la FLASH (mémoire non volatile, contenant le programme).

Dans votre programme, contenant la célèbre fonction « main », vous pouvez trouver :
void main(void)
{
     
unsigned char ma_variable 10;
}
Dans cet exemple, il est demandé au compilateur d’allouer un espace dans la mémoire dont il dispose et de lui donner la valeur "10". Cette déclaration étant faite à l’intérieur d’une fonction, la variable est dite "Locale", contrairement aux variables déclarées hors des fonctions qui sont dites "Globales".

Admettons que le compilateur décide de placer cette variable à l’adresse 0, voici à quoi pourrai ressembler le début de la mémoire de notre micro-contrôleur : (Dans le cas, ou l’utilisateur n’a pas imposé de mapping particulier au compilateur).
AdressesContenu
010
13
2123
342
412
......
10000
......
(Très grand nombre)42
Les données sont disponibles tant que le programme se trouve à l’intérieur de la fonction ayant demandé l’allocation de la mémoire. Par contre, une fois sorti de cette fonction, le compilateur va considérer qu’elles n’ont plus lieu d’être utilisées, et pourra utiliser cet espace mémoire à d’autres fins.

Pour mettre en avant cette notion, voici concrètement un exemple :
Dans cet exemple qui nous servira de fil rouge durant cet article, nous allons écrire un petit programme simple ayant pour but de convertir des minutes en heures et minutes (Exemple : 90 minutes, correspondent à 1 heure et 30 minutes).

Dans un premier essai, admettons que l’on ait déclaré deux variables de façon globale :
unsigned char Heures ;
unsigned char Minutes 90 ;
Et que l’on utilise une fonction comme celle-ci pour convertir des données :
void conversion(unsigned void)
{
    
Heures Heures Minutes 60 ;     //1 heure = 60 minutes
    
Minutes Minutes 60 ;        //minutes = reste de minutes / 60
}
Dans le "main", on appelle alors cette fonction :
void main(void)
{
    
conversion() ;
}
Nous aurons bien, Heures = 1, et Minutes = 30.
Le problème est que si plusieurs conversions sont nécessaires, les contenus de "heures" et "minutes" (variables globales) seront écrasés à chaque conversion. Et que seules ces variables peuvent être utilisées pour faire cette conversion.

Pour éviter cela nous pouvons passer des valeurs à la fonction de conversion, afin qu’elle n’utilise pas de variables globales, ce qui donnera plus de portabilité à la fonction.

Voici la fonction qui sera chargée de faire cette conversion :
void conversion(unsigned char heuresunsigned char minutes)
{
    
heures heures minutes 60 ;     //1 heure = 60 minutes
    
minutes minutes 60 ;        //minutes = reste de minutes / 60
}
Et la fonction main, qui initialise le programme, et appelle la conversion :
void main(void)
{
unsigned char heures 
unsigned char minutes 90 ;

conversion(heuresminutes) ;
}
Lignes par lignes, le programme commence dans le "main", il va demander au compilateur de lui allouer deux espaces en mémoire ("heures" et "minutes") et de leurs donner les valeurs 0 et 90.

Puis il appelle la fonction "conversion" en lui passant ces deux valeurs.
Dans cette fonction les nombres d’heures et de minutes passés en paramètre vont être convertis comme demandé, puis la fonction se termine et le programme retourne dans le "main".

Si à ce moment là on décide d’afficher le nombre d’heures et de minutes qui ont été calculées précédemment, voici ce que donnerai le résultat :
heures = 0 ;
minutes = 90 ;
Et là je vous vois faire de grands yeux !!! Juste avant, ces valeurs ont été modifié dans la fonction "conversion" non ?!

Réponse : hé bien non ! Voici le pourquoi…

A la fonction "conversion", on passe les valeurs de "heures" et "minutes" qui valent bien respectivement 0 et 90. Hors dans cette fonction, sont déclarées deux autres variables ayant pour noms "heures" et "minutes".

Mais où sont elles déclarées, me direz-vous !
A cette ligne, je vous répondrais :
void conversion(unsigned char heuresunsigned char minutes)
De ce fait, le compilateur va allouer deux autres espaces mémoires, qui prendront pour valeurs celles que l’on passera à la fonction lors de son appel, ici 0 et 90. Puis il utilisera ces deux nouvelles variables pour faire ses calculs, mais sans toucher à celles crées dans le "main"! Elles peuvent avoir le même nom, mais pour le programme, elles n’ont absolument rien à voir !

Pourquoi utiliser des pointeurs ?

Pour faire rapide, l’intérêt d’une fonction est de regrouper dans un même "bloc" une suite d’instructions qui vont être régulièrement utilisées, et de les appeler au besoin sans avoir à tout réécrire.

La possibilité de leur passer des valeurs permet de rendre ce système encore plus pratique et lui donner une capacité d’adaptation.

Comment faire pour récupérer le résultat qu’une fonction a calculé ?

Autant il est facile de faire renvoyer une seule valeur à une fonction :
unsigned char test(unsigned char entree)
{
    
entree entree ;
    return 
entree ;
}
Cette fonction va recevoir en paramètre une valeur "entree", qu’elle va multiplier par trois avant de retourner le résultat.

Comment faire pour qu’une fonction renvoie plusieurs valeurs ?

Vous pouvez retourner le problème dans tous les sens, il est impossible en C de faire retourner plus d’une seule valeur à une fonction.
Dans la suite de cet article, nous verront comment il est possible de contourner ce problème grâce aux pointeurs.

On ne montre pas du pointeur !

Comme le dit le titre, un pointeur ça montre, ou plutôt ça pointe.

Pour reprendre l’exemple ci-dessus, nous avions dans le "main" :
unsigned char heures 
unsigned char minutes 90 ;
Disons que le compilateur décide de placer "heures" à l’adresse 0, et "minutes" à l’adresse 1 ; voilà à quoi ressemblerait notre mémoire :
AdressesContenu
0(heures)0
1(minutes)90
2123
342
412
......
10000
......
(Très grand nombre)42
Introduisons maintenant une nouvelle notion, le pointeur :
unsigned char *pointeurSurheures NULL;
Il s’agit là aussi d’un emplacement en mémoire qui va être alloué par le compilateur, sauf qu’au lieu de contenir une valeur, elle va contenir une adresse.
Les pointeurs DOIVENT être initialisés, avec la valeur NULL (en majuscules) à défaut d’avoir directement leur valeur utile.
Un pointeur se reconnait à sa déclaration grâce à l’* devant son nom.

Ecrivons maintenant ceci dans notre programme :
void main(void)
{
unsigned char heures 
unsigned char minutes 90 ;
unsigned char *pointeurSurheures NULL ;

pointeurSurheures  = &heures ;
}
Le symbole "&" signifie "adresse de".

En toutes lettres, nous demandons au compilateur d’allouer deux emplacements en mémoire de type "unsigned char", nommés "heures" et "minutes", ainsi qu’un troisième emplacement mémoire de type "unsigned char *", soit un pointeur sur un "unsigned char" nommé "pointeurSurheures".
Puis ce dernier reçoit la valeur "&heures", c'est-à-dire l’adresse de "heures", soit 0.

Voici donc notre mémoire après cela :
AdressesContenu
0(heures)0
1(minutes)90
2(pointeurSurHeures)0(adresse de "heures"
342
412
......
10000
......
(Très grand nombre)42
NOTE : En fait, ce tableau n’est pas correct.
Sur un microcontrôleur 8bits (du moins chez Freescale, à vérifier pour d’autres fondeurs), les adresses des emplacements mémoire sont codés sur 16 bits, et vont de 0x0000 à 0xFFFF (0 à 65535 en base décimale, soit 65536 adresses).
Hors un pointeur est sensé contenir l’adresse de n’importe quel élément en mémoire, il doit dont contenir une valeur comprise entre 0x0000 et 0xFFFF, pour cela il lui faut utiliser deux octets en mémoire.
Sur un micro-contrôleur 32 bits, les adresses sont codées sur 32 bits, soit 4 octets, les pointeurs occupent dont 4 emplacements mémoire. (Même remarque, valable chez Freescale).

Ainsi, sur un micro-contrôleur 8 bits, le tableau de mémoire serait en réalité ceci :
AdressesContenu
0(heures)0
1(minutes)90
2(pointeurSurHeures)0(adresse haute de "heures"
3(pointeurSurHeures)0(adresse basse de "heures"
412
......
10000
......
(Très grand nombre)42
Sur un microcontrôleur 32 bits, le tableau de mémoire deviendrait :
AdressesContenu
0(heures)0
1(minutes)90
2(pointeurSurHeures)0(adresse haute de "heures"
3(pointeurSurHeures)0(adresse haute de "heures"
4(pointeurSurHeures)0(adresse basse de "heures"
5(pointeurSurHeures)0(adresse basse de "heures"
......
10000
......
(Très grand nombre)42
Cela dit, dans un souci de clarté et de simplicité, nous ne représenterons les pointeurs que sur un seul octet dans cet article ; le but étant d’illustrer le principe. Fin de l’apparté.

Comment utiliser cette adresse ?

Si on veut modifier la valeur de la variable « heures », on peut écrire ceci :
heures ;
Ce qui va mettre la valeur 3 dans la case mémoire correspondant à la variable "heures". Mais on peut aussi utiliser le fait que l'on connait désormais son adresse, contenue dans la variable "pointeurSurheures".
On peut donc écrire ceci :
*pointeurSurheures  ;
Ce qui signifie : mettre la valeur 3, à l’emplacement dont "pointeurSurheures" contient l'adresse, soit mettre 3 à l’adresse 0.

Abracadabra !!!

Nous avons la solution à notre problème du départ qui consistait à faire retourner deux valeurs à une fonction.

Si nous passons des valeurs à cette fonction, elle peut toujours modifier ses variables locales, cela n’a aucun impact sur les variables passées et déclarées à l’extérieur.
Mais si on passe les adreses des paramètres à la fonction, elle va alors pouvoir modifier directement le contenu des variables d’origines !

Un petit détail : lors de la déclaration d'un pointeur le type de données vers lequel pointe le pointeur doit figurer devant le pointeur.

Un pointeur vers une variable de type "unsigned char" :
unsigned char *pointeur NULL ;

Un pointeur vers une variable de type "float" :
float *pointeur NULL ;

Un pointeur vers une structure de type "mastructure" :
struct mastructure *pointeur NULL ;
etc…

Pouvez-vous me passer le pointeur, s’il vous plait ?

Il est aussi possible de passer un pointeur à une fonction.

Au lieu de déclarer une fonction ne renvoyant rien, et à qui l’on passe deux "unsigned char" ainsi :
void conversion(unsigned char heuresunsigned char minutes)
On va déclarer une fonction à qui on va passer deux pointeurs vers des variables de type "unsigned char" :
void conversion(unsigned char *heuresunsigned char *minutes)
Ainsi notre programme du début deviendra ceci :
void conversion(unsigned char *ptheuresunsigned char *ptminutes)
{
    *
ptheures = *ptheures + *ptminutes 60 ;     //1 heure = 60 minutes
    
*ptminutes = *ptminutes 60 ;        //minutes = reste de minutes / 60
}


void main(void)
{
unsigned char heures 
unsigned char minutes 90 ;

unsigned char *pointeurSurheures = &heures ;
unsigned char *pointeurSurminutes = &minutes ;

conversion(pointeurSurheurespointeurSurminutes) ;
}
Pareil que tout à l’heure, on déclare deux variables de type "unsigned char", nommées "heures" et "minutes", initialisées à 0 et 90.
Mais aussi des pointeurs vers des "unsigned char" nommés "pointeursVersheures" et "pointeursVersminutes", initialisés avec les adresses de "heures" et de "minutes".

Notre mémoire (simplifiée) ressemble alors à ceci :
AdressesContenu
0 (heures)0
1 (minutes)90
2 (pointeurSurHeures)0 (adresse de "heures"
3 (pointeurSurMinutes)1 (adresse de "minutes"
412
......
10000
......
(Très grand nombre)42
Ou encore pour le « main » :
void main(void)
{
unsigned char heures 
unsigned char minutes 90 ;

conversion(&heures , &minutes ) ;
}
Ainsi, on passe les adresses de "heures" et de "minutes" à la fonction "conversion", qui va modifier le contenu de la mémoire à ces adresses.

La ligne :
*ptheures = *ptheures + *ptminutes 60 ;
Signifie : le contenu de l’adresse "ptheures" vaut le contenu de l’adresse "ptheures" plus le contenu de l’adresse "ptminutes" divisé par 60.
Avec au départ "ptheures" = adresse de "heures", soit 0, et "ptminutes" = adresse de "minutes", soit 1.
Nous avons donc "ptminutes" = 1, et "*ptminutes" = 90.

Pour résumer

Lorsque l’on appelle une fonction déclarée ainsi :
void conversion(unsigned char heuresunsigned char minutes)
Deux variables dites locales vont être crées, et elles n’auront rien à voir avec d’autres variables crées à l’extérieur, même si elles ont le même nom.

La ligne :
unsigned char mavariable ;
Déclare une variable de type "unsigned char" et l’initialise avec la valeur 0.
unsigned char *pointeur NULL ;
Déclare un pointeur vers une variable de type "unsigned char".
pointeur = &mavariable ;
Affecte à "pointeur" l’adresse de la variable "mavariable"
*pointeur ;
Affecte la valeur 5 à l’adresse sur laquelle "pointeur" pointe ; ici "mavariable" prendra la valeur 5.

A savoir aussi que le passage par paramètres, c'est-à-dire sans utilisation de pointeurs, a deux autres inconvénients :
-Il y a copie des données en RAM, donc doublement de l’utilisation de celle-ci.
-Cette copie prend du temps au CPU.
Alors que avec le passage par référence (c'est le vrai nom du passage par pointeur) les données sont modifiées sur leur lieu d'origine ce qui induit ni surconsommation de RAM ni temps additionnel d'exécution.

N'hésitez pas à laisser des commentaires ci dessous si cet article vous a intéressé.

Tags: C   pointeurs  


Article précédent Article suivant
Article précédent Les pointeurs, approfondissement (Partie 2 sur 2) Le C et l'ASM, quelle différence ? Article suivant
Les commentaires appartiennent à leurs auteurs. Nous ne sommes pas responsables de leur contenu.
Auteur Commentaire en débat
Charly
Posté le: 11/03/2013 11:27  Mis à jour: 11/03/2013 11:28
Pilier de la communauté
Inscrit le: 23/10/2005
De: Aix les Bains (73)
Contributions: 1884
 Re: Les pointeurs, même plus peur... (Partie 1 sur 2)
Merci !

J'aime les exemples et la comparaison critique à d'autres méthodes.
Je crois que j'ai enfin pigé et je n'ai plus d'excuses pour ne pas m'y mettre
Tu m'as fait passer un GAP énoooorme !
Alban
Posté le: 11/03/2013 12:04  Mis à jour: 11/03/2013 12:10
Pilier de la communauté
Inscrit le: 09/10/2006
De: Cambridge, Angleterre & Glasgow, Ecosse
Contributions: 858
 Re: Les pointeurs, même plus peur... (Partie 1 sur 2)
Sympa, je pourrai envoyer des gens vers un article en francais maintenant...
Stephane
Posté le: 11/03/2013 17:00  Mis à jour: 11/03/2013 17:05
Pilier de la communauté
Inscrit le: 13/10/2005
De: haute-savoie (74)
Contributions: 1149
 Re: Les pointeurs, même plus peur... (Partie 1 sur 2)
pour la conclusion, j'utilise beaucoup désormais le passage par référence : systématiquement avec les structures, en fonction du cas pour les variables.

Mais il convient d'indiquer un point qui peut s'avérer un inconvénient si on n'y fait pas attention :
La passage par paramètre du fait de la copie permet de ne pas altérer la variable d'origine. On a une variable quelque part, on la copie et on joue avec la copie.
Dans le cas où cette variable est transmise à un fonction qui fait n'importe quoi, on ne perd pas sa donnée d'origine).
Le passage par référence comme indiqué dans la conclusion permet de modifier directement la variable à son emplacement d'origine. De ce fait, il n'y a pas de protection d'écriture sur la variable.
Dans le cas de variables sensibles, voire critique, on peut ajouter dans la fonction à laquelle on transmet cette variable un peu de code pour verrouiller l'écriture si la valeur à écrire n'est pas celle attendue.

Super tuto Maverick
ikoria
Posté le: 17/03/2013 14:21  Mis à jour: 17/03/2013 14:51
Accro
Inscrit le: 06/07/2007
De:
Contributions: 672
 Re: Les pointeurs, même plus peur... (Partie 1 sur 2)
il est également utile de s'attarder sur l'architecture pour le choix.
Si l'on prend par exemple le passage d'une variable 32 bit(ou plus), une structure etc. à une fonction sur un miro 8bit. L'utilisation des pointeurs permet de gagner beaucoup de ram et de temp cpu :)
ikoria
Posté le: 20/08/2015 09:06  Mis à jour: 20/08/2015 09:06
Accro
Inscrit le: 06/07/2007
De:
Contributions: 672
 Re: Les pointeurs, même plus peur... (Partie 1 sur 2)
héhé, je déterre le sujet :)

Pour les histoires de protection, pour l'article 2, il pourrait y avoir une partie expliquant la difference entre:
const U8 prt;
U8 * const prt;
const 
U8 * const prt


;)
Powered by XOOPS© The XOOPS Project
Contacter les administrateurs

Les pointeurs, même plus peur... (Partie 1 sur 2) - Perfectionnement > Le C et l'ASM - Articles et téléchargements