Effectivement, c'est ce qui a été dit précédemment: * et & sont indépendants.
Si on reprend les bases:
1 2
|
void Methode(Type*parametre); |
Ici, on dit que le paramètre passé à Methode est une adresse. L'appel devra donc fournir une adresse et se fait :
1 2 3
|
Type valeur;
Methode(&valeur); |
Ici, l'opérateur& permet de récupérer l'adresse de la variable valeur.
Pour un tableau :
1 2
|
Type tableau[NbrElts]; |
L'adresse d'un tableau peut s'obtenir de 2 façons :
- Soit directement en utilisant le nom de la variable tableau. Celle-ci correspond en effet à l'adresse du premier élément du tableau:
1 2 3
|
Type tableau[NbrElts];
Methode(tableau); |
- Soit en récupérent l'adresse d'un élément du tableau. Par l'opérateur [], on déréférence le tableau sur une cellule donnée et ensuite on utilise l'opérateur & pour récupérer l'adresse de cette cellule:
1 2 3 4 5
|
Type tableau[NbrElts];
Methode(&tableau[i]);
// Ecriture équivalente (lié à l'arithmétique des pointeurs):
Methode(tableau+i) |
Tu peux trouver un tutoriel sur les pointeurs. Il a été écrit pour le C mais reste valide pour le C++.
Pourquoi passer un paramètre par pointeur ?
- Modifier la valeur du paramètre dans la méthode,
- Eviter la copie de l'objet sur la pile surtout s'il s'agit d'un objet important en mémoire (adjoint alors du qualificatif const si besoin),
- Passer un tableau (adjoint alors du qualificatif const si besoin).
En C++, pour atteindre les objectifs 1 et 2, on n'utilise que rarement les pointeurs et on privilégie les références. Par parenthèse, l'objectif 3 est souvent contourné par l'utilisation des conteneurs en lieu et place des tableaux en brut.
Maintenant examinons dans la méthode les deux utilisations possibles du paramètre :
1 2 3 4 5 6 7
|
void Methode(Type *parametre)
{
if(parametre!=NULL){ // Utilisation 1
*parametre = VALEUR; // Utilisation 2
}
} |
Dans la première utilisation, on travaille sur l'adresse donnée en paramètre. Dans la seconde utilisation, on travaille sur la valeur. Quelles sont les différences? L'utilisation 1 permet de connaître l'adresse du paramètre, c'est à dire l'emplacement mémoire où se trouve la variable qui a été fournie en paramètre au moment de l'appel. L'opérateur de déréfencement * dans la seconde utilisation permet de dire que l'on ne veut pas travailler sur cette adresse mais sur le contenu qui se trouve dans cette adresse. Donc *parametre permet de lire et de modifier ce contenu.
Enfin, dernier point, une adresse est une valeur particulière qui désigne un emplacement mémoire. Les lignes suivantes sont valides :
1 2 3 4
|
Type *adresse;
adresse = NULL;
adresse = 42; |
Le premier cas est bien connu, puisqu'on utilise NULL pour signifier que l'adresse n'est pas valide.
Le second cas est aussi correct du point de vue du langage, mais risque de planter à l'exécution car 42 n'est pas forcément une adresse valide. Il peut exister des sytèmes dans lesquels cette adresse est valide. C'est pourquoi, on peut très bien écrire des valeurs en dur pour des adresses. Mais, ce sont des cas très particuliers sur des systèmes bien maîtrisés.
Donc, venons-en aux référence. Le caractère & peut aussi servir à déclarer une référence. Il s'agit d'une variable particulière qui ne contient pas de valeur en propre mais qu'on lie à une autre variable. Cela peut se concevoir comme un synonyme :
1 2 3
|
Type variable;
Type &reference = variable; |
reference est un synonyme de variable. Utiliser variable ou reference est strictement identique. Elles ont les mêmes valeurs et les mêmes adresses :
1 2 3
|
ASSERT(variable==reference);
ASSERT(&variable==&reference); |
Note que le mot-clé & a deux significations complètement différentes entre les deux bouts de code précédent. Lorsqu'on écrit Type&reference, le mot-clé & sert à dire que le type de reference n'est pas Type mais une référence sur Type. Alors, que lorsqu'on écrit &variable ou &reference, le mot-clé & agit comme un opérateur qui sert à récupérer l'adresse de la variable. Il faut comprendre: même signe mais rôle différent !
Deuxième remarque, une référence est forcément initialisée à sa déclaration et ne peut être modifiée ensuite :
1 2 3 4 5 6 7 8 9
|
Type variable;
Type &reference = variable;
Type &reference2; // Interdit -> erreur à la compilation
Type variable2;
reference = variable2; // reference est toujours synonyme de variable, seul la valeur a été modifiée:
ASSERT(reference==variable);
ASSERT(&reference==&variable);
ASSERT(&reference!=&variable2); |
Enfin, contrairement aux adresses, les références n'ont pas de valeur en elle-même : elle ne désigne pas un emplacement mémoire. On ne peut pas écrire des choses comme suit :
1 2 3
|
Type &reference = NULL; // Erreur de compilation.
Type &reference = 42; // Erreur de compilation. |
On peut utiliser une référence comme paramètre d'une méthode :
void Methode(Type& parametre);
L'appel est alors :
1 2 3
|
Type valeur;
Methode(valeur); |
Et l'utilisation est :
1 2 3 4 5
|
void Methode(Type& parametre)
{
parametre = VALEUR;
} |
Pourquoi utilises-t-on une référence : ce sont les éléments 1 et 2 présentés ci-dessus : pour permettre de modifier la valeur de l'argument et/ou pour éviter de construire une copie de l'argument sur la pile.
Rien n'empêche de mélanger les deux comme tu l'as fait :
void Classe1::GestionTAB2(float*& tab)
Cela signifie que tab est une référence sur une adresse du type float.
Donc la ligne de code suivante :
1 2 3
|
float monTableau[N];
Classe1::GestionTAB2(tableau); |
Le paramètre tab est donc un synonyme sur tableau, les deux étant un pointeur sur float.
Venons-en à la question: pourquoi le code suivant ne fonctionne pas :
1 2 3
|
float monTableau[N];
Classe1::GestionTAB2(&tableau[i]); |
&tableau[i] permet d'obtenir l'adresse tableau + i. Or comme on ne peut pas initialiser une référence avec une valeur, la ligne ne compile pas.
Ce qu'il faut comprendre c'est qu'en écrivant GestionTAB2(tableau), tab devient synonyme de tableau. Alors qu'en écrivant GestionTAB2(&tableau[i]), on fait d'abord une opération &tableau[i] qui nous renvoie une adresse. Et, on ne peut lier une référence avec une donnée brut (l'adresse).
La question devient alors : mais pourquoi la ligne suivante permet de résoudre le problème :
1 2 3 4
|
void GestionTAB2(float*const& tab);
// Puis
Classe1::GestionTAB2(&tableau[i]); |
En fait, en adjoignant const, on permet au compilateur de créer une variable temporaire de type float*, de l'initialiser avec l'adresse &tableau[i] et on lie alors tab à cette variable temporaire. const est important ici en disant qu'on ne va pas modifier l'adresse, on permet la création de cette variable temporaire.
Bon, je ne sais pas si c'est suffisamment clair, mais pour approfondir, le mieux est encore de consulter une tutoriel ou un ouvrage sur les références et le C++
Partager