Obligement - L'Amiga au maximum

Samedi 20 avril 2024 - 09:56  

Translate

En De Nl Nl
Es Pt It Nl


Rubriques

Actualité (récente)
Actualité (archive)
Comparatifs
Dossiers
Entrevues
Matériel (tests)
Matériel (bidouilles)
Points de vue
En pratique
Programmation
Reportages
Quizz
Tests de jeux
Tests de logiciels
Tests de compilations
Trucs et astuces
Articles divers

Articles in english


Réseaux sociaux

Suivez-nous sur X




Liste des jeux Amiga

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z,
ALL


Trucs et astuces

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z


Glossaire

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z


Galeries

Menu des galeries

BD d'Amiga Spécial
Caricatures Dudai
Caricatures Jet d'ail
Diagrammes de Jay Miner
Images insolites
Fin de jeux (de A à E)
Fin de Jeux (de F à O)
Fin de jeux (de P à Z)
Galerie de Mike Dafunk
Logos d'Obligement
Pubs pour matériels
Systèmes d'exploitation
Trombinoscope Alchimie 7
Vidéos


Téléchargement

Documents
Jeux
Logiciels
Magazines
Divers


Liens

Associations
Jeux
Logiciels
Matériel
Magazines et médias
Pages personnelles
Réparateurs
Revendeurs
Scène démo
Sites de téléchargement
Divers


Partenaires

Annuaire Amiga

Amedia Computer

Relec


A Propos

A propos d'Obligement

A Propos


Contact

David Brunet

Courriel

 


Programmation : C - les pointeurs
(Article écrit par Batchman et extrait d'A-News (Amiga News) - septembre 1989)


Après la structure des programmes et les différents types de variables, voici aujourd'hui le dernier d'entre eux, les pointeurs. J'espère que vous êtes motivés, car ça n'est peut-être pas très simple pour ceux qui débutent ; mais il faut y passer car ce sont les pointeurs qui font toute la puissance du C. N'oubliez pas que, face aux doutes, seule la pratique vous aidera à les surmonter (ça n'est pas de Clint Eastwood, c'est de moi).

Qu'est-ce qu'un pointeur ?

Un pointeur est une variable qui contient une adresse. Sur Amiga, celle-ci est codée sur quatre octets mais vous n'avez pas à vous en préoccuper. Cette adresse est celle de variables de tous les types.

Elle peut être aussi une adresse absolue, mais c'est rarement utilisé sur notre machine dans la mesure où la plupart des structures de données n'ont pas une adresse fixe.

Vous pouvez enfin avoir l'adresse d'une fonction. Dans ce cas, le pointeur contient l'adresse du point d'entrée de la fonction (l'adresse de la première instruction) mais le compilateur fait le maximum pour vous le cacher. Ceci pour la bonne raison que vous ne devez pas pouvoir modifier un programme. Par exemple, vous pouvez changer la première instruction d'une fonction, puisque vous connaissez son adresse. Mais vous ne devez pas le faire et le compilateur ne vous facilitera pas la tâche parce que ce n'est pas comme cela que l'on programme chez des gens bien élevés et qui ont fait des études.

Pourquoi les pointeurs ?

Très bonne question, mon cher Batchman et je me remercie de me l'avoir posée. Effectivement, dans un langage structuré comme Pascal, assez comparable au C, on utilise relativement peu les pointeurs : dans deux cas principalement, l'allocation dynamique de variables et les structures chaînées. En C, on utilise les pointeurs bien sûr dans ces deux cas, mais aussi pour le passage de paramètres à des fonctions. En effet, il faut distinguer deux façons de passer les paramètres : par valeur ou par référence.

Le passage par valeur

Le passage par valeur consiste à allouer une variable dynamique sur la pile à l'entrée de la fonction appelée et à lui donner la même valeur que la variable passée en paramètre. Lorsque la fonction appelée est terminée, cette variable dynamique est détruite. C'est la solution à utiliser de préférence car vous avez dupliqué votre variable paramètre et vous pouvez lui faire subir tout ce que vous voulez : à la fin de la fonction, vous retrouverez votre variable initiale indemne. Mais ce n'est pas toujours ce que vous souhaitez : vous pouvez avoir envie que votre fonction travaille sur la variable même qui est spécifiée.

Exemple : voici une fonction qui incrémente un entier (je sais, c'est bête mais c'est un exemple).

C

Que fait-elle ? Lorsqu'elle est appelée, elle alloue un entier "x", lui donne la valeur de la variable passée en paramètre (ici "y", soit 10), incrémente cette valeur (x = 11). Enfin. la fonction se termine, la variable "x" est libérée et que croyez-vous que le printf affiche ? 10 puisque "y" n'a pas été modifié.

Un autre piège connu du même style est l'échange de deux variables :

C

En appliquant le même raisonnement que précédemment, vous remarquez que les valeurs de "a" et "b" sont bien échangées mais pas les deux variables passées en paramètres de "Echange".

Pour remédier à ce problème, on peut utiliser l'autre mode de passage des paramètres :

Le passage par référence

Ceci revient à passer non pas une valeur, mais l'adresse de la variable concernée. Évidemment, le traitement ultérieur est différent pour le programme : il ne doit pas travailler directement sur une variable mais sur une donnée dont il connaît l'adresse. Il doit faire une indirection.

Un autre avantage du passage par référence : supposez que vous appelez une fonction avec comme paramètre un grand tableau. Si vous le passez par valeur, il devra être alloué sur la pile et recopié. Ça peut occuper de la mémoire (éventuellement beaucoup si le tableau est très grand ou si la fonction est récursive) et ça prend du temps. Si vous passez par référence, seule l'adresse du tableau est allouée : vous gagnez du temps et de la place.

Ces deux modes existent en Pascal mais pas en C. En C, seul le passage par valeur est possible. Mais ça ne veut pas dire pour autant que le passage par référence est impossible. En effet, rien ne vous empêche de passer une adresse par valeur, cela revient au même. Résultat : vous pouvez donc simuler un passage par référence.

Et voilà, tout ça pour expliquer l'utilité des pointeurs, lorsque vous devrez faire un passage de paramètres par référence, vous devrez fournir à la fonction appelée l'adresse de la variable concernée. Et cette adresse est contenue dans un pointeur... C'est beau, non ?

Voici donc comment il faut écrire en C non relativiste la fonction "Echange" :

C

...et l'appeler par : "Echange(&y,&z);"

Les "&y" et "&z" dans l'appel de "Echange" signifient que l'on passe en paramètre de cette fonction, non pas les variables "y" et "z", mais leur adresse. Et les étoiles devant "a" et "b" à l'intérieur de la fonction représentent l'indirection :
  • "x" reçoit l'entier dont l'adresse est dans "a".
  • L'entier dont l'adresse est dans "a" reçoit l'entier dont l'adresse est dans "b".
  • L'entier dont l'adresse est dans "b" reçoit la valeur "x".
Vu ? Finalement, c'est plutôt simple. Voyons maintenant la déclaration de nos pointeurs : "int *a,*b;". Les deux étoiles signifient ici que l'on ne déclare pas deux entiers, mais deux pointeurs sur des entiers.

Si on avait la déclaration : "int a, *b;" on déclarerait respectivement un entier et un pointeur sur un entier. Nuance !

Résumons :

Pour déclarer un pointeur, on écrit :

type *identificateur;

Par exemple :

char *p; float *toto; struct TypeConstruit *tc;

Encore plus fort :

int *(tableau[10]);

...est un pointeur sur un tableau de dix entiers, alors que :

int *tableau[10];

...est un tableau de dix pointeurs d'entiers (car les crochets sont prioritaires par rapport à l'étoile).

Toujours plus fort :

int *fonction();

...est une fonction retournant un pointeur d'entier alors que :

int (*fonction)();

...est un pointeur sur une fonction retournant un entier (car les parenthèses sont aussi prioritaires par rapport à l'étoile).

Le nec plus ultra :

int **machin;

...est un pointeur sur un pointeur d'entier.

Pour utiliser un pointeur : si on travaille sur une adresse, on peut faire de nombreuses opérations (voir plus loin).

Si on veut obtenir une donnée dont on connaît l'adresse :

variable = *pointeur;

("pointeur" étant un pointeur sur une variable du même type que "variable").

Si on veut obtenir l'adresse d'une variable :

pointeur = &variable;

...ou pointeur = tableau;

En effet, l'opérateur "&" donne l'adresse d'une variable sauf dans le cas des tableaux où l'adresse est donnée par le nom du tableau lui-même ("tableau" est équivalent à "&tableau").

Si "Toto" désigne une structure, "&Toto" représente l'adresse de la structure et "&Toto.champs" l'adresse d'un des champs de celle-ci. Attention, rien ne vous garantit que l'adresse de la structure coïncide avec l'adresse de son premier champ. C'est peut-être le cas, mais vous ne devez pas compter là-dessus.

Pointeurs sur des structures

Voici quelque chose qui sert très souvent. On dispose souvent, en particulier avec les fonctions système, de pointeurs sur des structures. On peut accéder aux champs de ces enregistrements par la construction : "(*structure).champ". Mais cette construction est très souvent utilisée et son écriture pas très pratique. On lui préfère donc la suivante qui est strictement équivalente : "structure->champ".

En clair, ces deux écritures permettent d'accéder à un champ d'une structure dont on connaît seulement l'adresse. Et bien sûr, si ce champ contient l'adresse d'une structure dont vous désirez connaître un champ, vous pouvez enchaîner ces formules :

structure1->structure2->structure3->ainsi_de_suite->...

Pour bien comprendre ce mécanisme essentiel regardez des programmes d'exemple : on le retrouve souvent.

Opérations sur les pointeurs

Vous pouvez faire un certain nombre d'opérations sur des pointeurs, afin de comparer ou de calculer des adresses. Ceci ne sert quasiment que pour les tables. Mais il faut manier ces opérations avec précaution car il n'y a aucun contrôle sur les résultats. Si vous vous trompez, vous taterez du Guru !

Une règle importante : pour toutes ces opérations, l'unité utilisée est la taille des variables du type en question. Par exemple, les entiers longs sont codés sur Amiga sur quatre octets. Si vous incrémentez un pointeur (sur un entier long) contenant 0x1000, il pointera sur l'adresse 0x1004. Si c'était un short, il pointerait sur 0x1002.

La conséquence de ceci est que vous n'avez pas à gérer la taille de vos données.

Voici les différentes opérations possibles, avec les types des arguments et du résultat :

Opérateur / Argument 1 / Argument 2 / Résultat
Addition + / pointeur / entier / pointeur
Différences - / pointeur / entier / pointeur
Incrémentation ++ / pointeur / entier / pointeur
Décrémentation -- / pointeur / entier / pointeur

Petit exemple pour illustrer : si P1 et P2 sont deux pointeurs valant respectivement 0x1000 et 0x2000, la différence des deux vaudra :
  • 0x1000 si ce sont des pointeurs de caractères.
  • 0x400 si ce sont des pointeurs d'entiers longs.
  • 1 si ce sont des pointeurs sur des tableaux de 0x1000 (soit 4096) caractères.
Les chaînes de caractères

Ceux qui ont fait du BASIC, attention ! Les chaînes de caractères en C sont tout à fait différentes. Une chaîne de caractères en C est une suite de caractères d'une longueur quelconque, terminée par le caractère "Null" ayant pour code ASCII 0 (on le désigne en C par \0). Cette chaîne est donc définie par un pointeur de caractère qui désigne le début, et par un marqueur de fin.

On déclare une chaîne par :

char *toto = "Anews, le journal qu'il est bon";

Que fait le compilateur ? Il réserve de la place pour stocker la chaîne de caractères plus un octet pour "Null", le marqueur de fin. Et d'autre part, il réserve "toto", le pointeur de caractères qui reçoit l'adresse du premier caractère de la zone déjà réservée. Il ne faut surtout pas croire, et c'est là la différence avec le BASIC, que l'on va ranger la chaîne dans "toto". C'est juste son adresse.

Autre piège, si vous faites le test :

if ("toto" == "toto") then condition...

La condition ne sera jamais vérifiée. Parce que *toto" est différent de "toto" ! Non, il ne faut pas me faire enfermer, c'est tout simple :
  • Le compilateur trouve le premier "toto" : il réserve cinq octets pour ranger cette chaîne et il garde un pointeur sur son premier caractère.
  • Il trouve "toto" une deuxième fois : il réserve de nouveau cinq octets et garde un pointeur sur cette chaîne.
  • Il compare les deux pointeurs qui bien sûr sont différents, d'où le résultat...
Ceci implique que la manipulation des chaînes se fait avec des fonctions spéciales. Mais ces fonctions ne font pas partie du langage C, elles constituent les bibliothèques, écrites en C, et qui sont fournies avec chaque compilateur. Puisqu'elles ne font pas partie du langage C, nous n'en parlons pas ici. Le mieux est de consulter la documentation de votre compilateur. Mais nous les rencontrerons dans les programmes d'exemples.

Voilà, ce sera tout pour cette fois mais nous reparlerons des pointeurs quand nous verrons les conversions explicites de type. Au programme du mois prochain, les types énumérés et les classes de stockage.


[Retour en haut] / [Retour aux articles]