TP C2 2324 (obligatoirement dans un Terminal Linux, ou MacOS)

Objectifs :
- apprendre à manipuler les pointeurs
- comprendre les paramètres de sortie, les chaînes de caractères, et l'équivalence tableau/pointeur

Attention à tout lire et à bien suivre chaque étape l'une après l'autre ;
conserver tous les programmes C écrits au fur et à mesure.

I. Les deux modes de passage de paramètres

En java, le seul mode de passage de paramètre est dit "par valeur" ou "par recopie", car on recopie la valeur du paramètre effectif (de la fonction appelante) dans le paramètre formel (de la fonction appelée). Tous les paramètres sont donc des paramètres d'entrée car les valeurs entrent dans la fonction appelée (et n'en sortent pas !).
Exemple en java :
void m1() { int vE = 12;  System.out.println( m2( vE ) + " : " + vE ); }

int m2( int pE ) { pE = pE+1;  return pE*2; }

L'appel de m1() affichera 26 : 12 car le paramètre effectif vE (qui vaut 12) est d'abord recopié dans le paramètre formel pE (qui maintenant vaut aussi 12), puis pE est incrémenté (et vaut donc 13), puis m2 retourne pE*2 (donc 26), résultat qui est affiché par m1 suivi d'un deux-points suivi de vE (qui vaut toujours 12). Donc la procédure m2 n'a pas pu modifier le contenu de la variable vE.

En C, les paramètres que nous avons utilisé au TP C1 fonctionnent de la même manière, mais il en existe une deuxième sorte : les paramètres de sortie (ou d'entrée/sortie, car Qui peut le plus peut le moins !) qui utilisent un passage de paramètre dit "par adresse". Cela veut dire qu'on ne passe plus la valeur d'une variable, mais son adresse. Cela nous permettra donc non seulement d'accéder à la valeur, mais également de la modifier dans la fonction appelante !
Cette possibilité est très utilisée en C, notamment pour saisir des valeurs.



II. La saisie de valeurs

II.A. Pour saisir une valeur en C, il faut utiliser la fonction de bibliothèque scanf qui est déclarée dans stdio.h et qui s'utilise de la façon suivante :
scanf( format, lieu );
format est une chaîne de caractères spécifiant le type de donnée attendue (comme dans printf) et lieu est l'adresse de la variable dans laquelle on souhaite stocker la valeur saisie par l'utilisateur.
En C, pour calculer l'adresse d'une variable var, on écrit simplement & var . Par exemple,
int vE; scanf( "%d", &vE );
lit la valeur entière tapée au clavier et la stocke dans la variable vE.
Attention ! scanf n'affiche rien, il n'est donc pas possible de mettre un message dans le format.

II.B Écrire un programme 2B.c avec la seule fonction main qui demande à l'utilisateur de saisir une valeur entière au clavier et qui en affiche le triple sous la forme suivante : 12 * 3 = 36 (si la valeur saisie est 12).
Compiler, tester. (voir le tpC1 II.D si la commande mycc n'est plus disponible)
Si vous avez l'impression que rien ne se passe quand vous lancez le programme, ajoutez l'affichage d'un message pour prévenir l'utilisateur de ce qu'on attend de lui ... Recompiler, tester.

II.C Écrire maintenant le programme 2C.c avec la seule fonction main qui demande à l'utilisateur de saisir une valeur réelle (quel sera le format ?) au clavier et qui en affiche la racine carrée (où trouver cette fonction mathématique ?). Compiler, tester.

II.D Comme printf, scanf accepte plusieurs % si chacun correspond à une variable qu'on lui fournit.
Modifier le programme du II.B dans 2D.c pour que l'utilisateur puisse saisir 2 valeurs entières séparées par un espace et en affiche la somme. Compiler, tester.



III. Mon premier pointeur

III.A On désire maintenant écrire une procédure saisie qui affiche un message avant la saisie. Cette procédure aura donc pour paramètre l'adresse de la variable dans laquelle on veut stocker la valeur. Mais comment indiquer que le paramètre est une adresse ?
- Lors de l'appel, nous avons vu qu'il suffit d'écrire &var pour passer l'adresse de la variable var
- Mais lors de la définition, il nous faut un moyen de noter le type "adresse d'entier" ; cela se note avec une * après le type :
void saisie( int * pVar )
Une première façon de lire cette déclaration est de rapprocher l'* du int pour dire que le paramètre pVar est du type int*, c'est-à-dire "adresse d'entier" appelé en C "pointeur d'entier".
Le corps de la procédure saisie va ressembler à :
{
  printf( "Veuillez saisir un nombre entier : " );
  scanf( "%d", pVar );
} // saisie(.)
Bien remarquer qu'il n'y a pas & avant pVar dans le scanf car pVar est déjà une adresse d'entier !

Modifier le programme du II.B dans un nouveau programme 3A.c pour que le main affiche le triple de la valeur qui aura été saisie (par la nouvelle procédure du même nom !). Compiler, tester. vE <- pVar


III.B
On désire maintenant que la procédure saisie transforme toute valeur négative saisie en valeur positive. On aimerait donc pouvoir écrire, si la valeur était dans une variable entière vX :
if ( vX < 0 )  vX = -vX;
Mais dans notre cas, comment mettre la valeur saisie dans vX ?
Écrire int vX = pVar; provoquerait une erreur de compilation puisque pVar n'est pas un entier mais l'adresse d'un entier.
Si on regarde différemment la déclaration de la procédure saisie en rapprochant l'* de pVar, on obtient int *pVar, ce qui veut dire que *pVar est un entier ; nous tenons donc notre solution : *pVar se lit "l'entier qui est à l'adresse pVar", d'où la possibilité de remplacer vX par *pVar partout dans l'instruction if ci-dessus.
Modifier le programme du III.A dans 3B.c pour ajouter ce test, compiler, tester.


III.C
Modifier le programme du III.B dans 3C.c pour que l'utilisateur saisisse une valeur réelle (dans la procédure saisie) et en affiche la racine carrée (dans le main). Compiler, tester.
Remarque : Si on souhaite conserver la procédure de saisie d'un nombre réel tout en spécifiant le message que l'on souhaite afficher juste avant la saisie, il faut pouvoir passer à la procédure un paramètre supplémentaire de type chaîne de caractères. Mais le type String n'existe pas en C ...


IV. Les chaînes de caractères

IV.A En C, une chaîne de caractères est un simple tableau de caractères dont chaque case contient un caractère de la chaîne (avec une condition qui sera détaillée au IV.B). On utilise alors le type char qui occupe un seul octet pour stocker le code ASCII du caractère.
Pour utiliser les chaînes plus agréablement, de nombreuses fonctions ont été définies dans string.h, comme par exemple strcpy (qui signifie string copy) qui recopie la chaîne passée en second paramètre dans la chaîne passée en premier paramètre :
char vChaine[10];  strcpy( vChaine, "Bonjour" );
où le tableau vChaine est déclaré comme un tableau de 10 char dont on ne connaît a priori pas les valeurs (qui peuvent être vues comme des caractères aléatoires).
Après le strcpy, vChaine[0] vaut 'B' jusqu'à vChaine[6]qui vaut 'r'. Les simple quotes (ou apostrophes) sont utilisées pour désigner un seul caractère (c'est-à-dire une constante du type char) à la différence des double quotes (ou guillemets) qui désigneraient une chaîne de caractères (c'est-à-dire un tableau de char).




IV.B.1 La procédure saisie prendra donc un tableau de char en premier paramètre, contenant le message à afficher. Le prototype de la fonction (l'équivalent de la signature en java) peut donc s'écrire :
void saisie( char pMessage[], double * pVar )
mais comme le nom d'un tableau en C (par exemple vTab) représente l'adresse de son premier élément (donc vTab == &vTab[0]), on peut aussi écrire :
void saisie( char * pMessage, double * pVar )
C'est ce qu'on appelle l'équivalence tableau <--> pointeur. Il faut donc comprendre ici que passer un tableau en paramètre, c'est passer son adresse (ou plus exactement l'adresse de son premier élément), et donc c'est permettre la modification de ses éléments.

Attention ! Cette équivalence tableau <--> pointeur ne doit pas masquer une différence essentielle lors de la déclaration :
char vChaine[10]; déclare un tableau de 10 octets, alors que
char * vPtr; déclare seulement un pointeur de caractère (de 4 octets sur une machine 32 bits).
Ensuite, si on stocke dans vPtr l'adresse du premier élément de vChaine par l'instruction vPtr = &vChaine[0]; ou plus simplement vPtr = vChaine; l'équivalence tableau <--> pointeur jouera à plein et on pourra indifféremment utiliser vPtr ou vChainepour accéder au tableau en écrivant vPtr[6] (qui vaudra 'r' dans l'exemple du IV.A) ou *vChaine (qui vaudra 'B').

IV.B.2 Comme nous voulons afficher le message passé en paramètre, il nous faut connaître le format qui permet à printf d'afficher une chaîne de caractères : il s'agit de "%s".
Mais au fait, si on ne passe à printf que l'adresse du premier élément du tableau de caractères, comment fait-elle pour savoir quand s'arrêter d'afficher des caractères du tableau ? Le tableau a été déclaré avec 10 cases, mais s'il ne contient que le mot "Bonjour", il ne faut afficher que 7 caractères !
En fait, toutes les fonctions de la bibliothèque standard du C comme printf ou celles de string.h utilisent la même convention : elles s'arrêtent dès qu'elles trouvent le caractère nul, c'est-à-dire le caractère de code ASCII 0. Même le compilateur ajoute automatiquement ce caractère à la fin des chaînes de caractères littérales comme "Bonjour" : il faut donc un tableau d'au moins 8 caractères pour contenir une chaîne d'apparemment 7 caractères !

C'est une des premières causes de plantage des programmes C (lorsqu'on recopie une chaîne de N caractères dans un tableau de N char ..., vous voyez le problème ?)
Le caractère nul se note '\0' qui veut dire caractère de code ASCII 0.
A noter également que le contenu des cases 8 et 9 du tableau est aléatoire (comme toute variable non initialisée).


Donc, modifier le programme du III.C dans 4B.c pour qu'il passe un message (tel que "Veuillez saisir le 1er réel :") en paramètre à la procédure saisie qui l'affichera avant de saisir la valeur (nul besoin de strcpy dans cet exercice). Compiler, tester.

IV.C Écrire le programme 4C.c pour que, désormais, la procédure saisie permette à l'utilisateur de saisir au clavier une chaîne de maximum 13 caractères utiles (donc qui doit pouvoir contenir le mot Programmation). Le main appellera ensuite une fonction longueur (à écrire) qui calcule et retourne la longueur de n'importe quelle chaîne passée en paramètre, puis enfin affichera le résultat de cette fonction (si on saisit Bonjour, cela devra afficher 7). Compiler, tester.
Attention ! Si la chaîne comporte un espace, seuls les caractères situés avant seront pris en compte par le premier scanf.

IV.D Retester maintenant le programme précédent en saisissant une suite de plusieurs dizaines de caractères.
Appelez l'intervenant si vous ne comprenez pas ce qui se passe.


V. Les procédures d'échange

V.A Écrire le programme 5A.c pour que l'utilisateur saisisse dans le main (à l'aide de scanf) deux variables entières, puis qui appelle une procédure echangeE qui échangera les valeurs des 2 variables, puis qui affichera les 2 variables. Compiler, tester.
Attention ! Contrairement au langage Python, ni Java ni C ne permettent une double affectation simultanée, et rappelez-vous de l'inconvénient du passage de paramètre par recopie !

V.B Écrire maintenant le programme 5B.c dont le main :
1. déclare deux tableaux de 25 caractères
2. permet à l'utilisateur de saisir deux mots (qui peuvent être de longueurs différentes)
3. appelle une procédure echangeM (à écrire) qui échange les deux mots (en recopiant les caractères, mais sans déclarer de 3ème tableau)
Remarque : Une première version peu performante peut calculer la longueur de chaque mot avant de commencer les échanges.
4. affiche les deux mots.
Compiler, tester.

V.C Modifier le programme du V.B dans 5C.c pour que le main :
1. déclare en plus des deux tableaux, deux pointeurs de caractères pointant chacun sur un des deux tableaux.
   Ces 2 variables supplémentaires sont utiles pour convertir chaque char[] en  char* : la différence, c'est que char[] est équivalent à un pointeur constant alors que char* est un pointeur variable.
   (Vous pouvez visualiser le problème en essayant d'interpréter le message d'erreur obtenu quand on appelle echangeE du V.A avec les paramètres 5 et 10, par exemple)
2. permette à l'utilisateur de saisir deux mots
3. appelle une procédure echangeM plus efficace qui échange les deux mots sans recopier les caractères ! (Lire l'aide ci-dessous)
4. affiche les deux mots.
Compiler, tester.

Aide pour le V.C.3 :
Pour arriver à écrire ça malgré une syntaxe peu intuitive, il vaut mieux repartir de la procédure echangeE du 5A et tout simplement remplacer les int par des PtrChar, après avoir défini ce nouveau type 'pointeur de caractère'. Une fois que votre programme fonctionne, essayez de l'écrire sans définir le type PtrChar, juste pour essayer de comprendre cette écriture quelque peu absconse.


VI. Équivalence fonction / procédure

Les deux exercices ci-dessous permettent uniquement de montrer qu'on peut choisir fonction ou procédure quel que soit le problème posé, même si à chaque fois, l'une est plus pratique que l'autre.

VI.A En C, une fonction à N paramètres peut s'écrire sous forme d'une procédure à N+1 paramètres, le dernier étant un paramètre de sortie fournissant le résultat habituellement retourné par la fonction.
Modifier le programme du III.A dans 6A.c pour que la procédure saisie soit désormais une fonction. Que faut-il changer dans le main ? Compiler, tester.

VI.B L'inverse étant également vrai, modifier le programme du IV.C dans 6B.c pour que la fonction longueur soit désormais une procédure. Que faut-il changer dans le main ? Compiler, tester.

Les modifications que vous avez dû effectuer dans le main mettent en évidence une différence fondamentale qui demeure entre fonction et procédure : l'appel d'une fonction est une expression alors que l'appel d'une procédure est une instruction.


VII. Terminer ce TP en travail personnel avant le cours C2


S'il vous reste du temps dans ce TP ou pour réviser :

+I. L'adresse et le pointeur

Lors du TP C2, vous avez appris à calculer l'adresse d'une variable et à la stocker dans un pointeur, ainsi qu'à accéder à la valeur se trouvant à une certaine adresse.
+I.a Dans le main, déclarer une variable réelle vX ainsi qu'un pointeur de double vPtr contenant l'adresse de vX.
+I.b Écrire un scanf pour remplir la variable vX en utilisant vPtr, et un printf pour l'afficher en utilisant vX.
+I.c Écrire un scanf pour remplir la variable vX en utilisant vX, et un printf pour l'afficher en utilisant vPtr.

+II. Les passages de paramètres

Lors du TP C2, vous avez appris à passer les paramètres soit par valeur (en entrée seulement comme en Java ou en Python), soit par adresse (en entrée/sortie). Il vous faudra donc bien choisir entre ces 2 possibilités pour chaque paramètre lorsque vous écrirez une procédure par la suite.
+II.a Écrire une procédure modifV à 2 paramètres qui permet de mettre la valeur du second paramètre dans le réel qu'on lui passe en premier paramètre.
Appeler modifV avec vPtr dans le main pour que vX reçoive la valeur 12.345 et afficher vX pour le vérifier.
+II.b Écrire une procédure modifP à 2 paramètres qui modifie son premier paramètre pointeur de double de telle sorte qu'il pointe à l'adresse passée en second paramètre.
Dans le main, déclarer une deuxième variable réelle vY initialisée à 0.0 puis appeler modifP pour que vPtr pointe désormais sur vY.
Appeler modifV avec vPtr pour que vY reçoive la valeur -3.21, puis afficher vX et vY pour vérifier que les 2 valeurs sont correctes.

+III. La définition de type

Lors de la séquence C1, vous avez appris à déclarer de nouveaux types.
Transformer le programme +II.b pour qu'il utilise partout le type PtrReel au lieu de double *.

+IV. Échanges et tableaux

Lors du TP C2, vous avez appris à écrire une procédure d'échange de 2 variables.
+IV.a Écrire une procédure echangeE qui permettra dans le main d'échanger les valeurs de 2 variables entières.
+IV.b Sur le même modèle, écrire une procédure echangeP qui permettra dans le main d'échanger les valeurs de 2 chaînes de caractères. Pour cela :
- Déclarer une constante MAXCHAR valant 9, puis dans toute la suite du main, ne plus utiliser la valeur mais toujours MAXCHAR, de telle sorte que sa valeur soit aisément modifiable à un seul endroit.
- Déclarer un nouveau type Chaine9 permettant de stocker des chaînes d'au plus 9 caractères utiles.
Attention ! En C, les [] se mettent après le nouvel identificateur et non après le type ...
- Déclarer 2 variables vMot1 et vMot2 de ce type.
- Initialiser vMot1 avec "Mathematiques" et vMot2 avec "Informatique".
Attention ! Pour éviter tout problème, il ne faut pas recopier plus de 9 caractères utiles ! strncpy nous permet cela, car elle accepte un 3ème paramètre pour préciser ce nombre maximal. L'inconvénient par rapport à strcpy, c'est qu'il ne faut pas oublier de positionner le caractère nul dans la dernière case ...
- Très peu de changements sont nécessaires pour passer d'echangeE à echangeP ; par contre, dans le main, on ne peut pas utiliser directement vMot1 et vMot2, il faut d'abord les recopier dans des pointeurs de caractères (=> nouveau type PtrChar) : pourquoi ?
- Afficher les 2 mots (en utilisant les 2 pointeurs) avant et après l'appel à echangeP.