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

Durée : 4h (ou 2 x 2h)

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 (appelée référence en Java). Cela nous permettra donc non seulement d'accéder à la valeur, mais également de la modifier depuis 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 (dont on doit passer l'adresse à scanf).
Attention ! scanf n'affiche rien, il ne faut donc pas mettre un message ou un \n dans le format, il faudra avoir prévu un printf avant.

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.
Que se passe-t-il si le nombre est négatif ? Améliorez avec un message d'erreur.

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   Le main doit transmettre à saisie l'adresse de vE : (&vE).
Dans saisie, le paramètre (pointeur d'entier) pVar contiendra donc l'adresse de la variable vE du main.
Et ainsi, le contenu de la variable vE pourra aussi être désigné par *pVar.
Donc en résumé, les deux notations pVar et &vE pointent vers un contenu indifféremment désigné par vE ou *pVar.
Attention ! Seules les notations en vert/bleu sont utilisables dans la fonction en, respectivement, vert/bleu.
 


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 (au lieu de toujours afficher le même), il faudra 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' (c'est bien un seul caractère !) 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 un nouveau programme 4C.c comportant :
- une fonction longueur (à écrire comme un exercice, sans utiliser string.h !) qui calcule et retourne la longueur de la chaîne passée en paramètre (quelle que soit sa longueur),
- le main qui :
1. déclare une variable entière vAvant, initialisée à 99
2. déclare un tableau de caractères permettant à l'utilisateur de saisir au clavier une chaîne de maximum 13 caractères utiles (donc qui doit pouvoir contenir, par exemple, le mot Programmation),
3. déclare une variable entière vApres, initialisée à 99
Il affichera ensuite la valeur de vAvant entre 2 parenthèses, puis le résultat du calcul de la longueur de la chaîne, puis la valeur de vApres entre 2 parenthèses.
Les 2 variables entières sont placées là comme témoins d'un éventuel débordement de tableau lorsqu'on saisira la chaîne de caractères (au 4D).
Si on saisit, par exemple, Bonjour, cela devra afficher  (99) 7 (99) . Compiler, tester.
Attention ! Si la chaîne saisie 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 le mot depassementdun .
Pour mieux comprendre ce qui se passe, vous pouvez afficher l'adresse des variables grace au format %p (comme pointeur). Si vous ne comprenez toujours pas, appelez l'intervenant.
Retester maintenant le programme précédent en saisissant une suite d'au moins une cinquantaine de caractères.
Appelez l'intervenant si vous ne comprenez pas ce qui se passe.
Attention ! Sur certaines installations Linux, rien d'anormal ne se passe. si c'est le cas, appelez un intervenant pour vérifier que vous aviez bien fait ce qu'il fallait.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

2ème partie

V. Les procédures d'échange

V.A Écrire le programme 5A.c dont le main :
1. saisit (à l'aide de scanf) deux variables entières,
2. appelle une procédure echangeE qui échangera (sans faire d'opérations + - ^ ...) les valeurs des 2 variables,
3. affiche les 2 variables.
Compiler, tester.
Attention ! Contrairement au langage Python, ni Java ni C ne permettent une double affectation simultanée, et en cas d'erreur, rappelez-vous de l'inconvénient du passage de paramètre par recopie !

V.B Écrire le programme 5B.c dont le main fera la même chose que le programme précédent, mais pour deux variables caractères, et une procédure echangeC.
Compiler, tester.

V.C Écrire maintenant le programme 5C.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 < 25)
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 et sans utiliser string.h)
Remarque : Une première version naïve peu performante peut calculer la longueur de chaque mot avant de commencer les échanges.
Lorsque cette v1 fonctionne, programmer une v2 qui ne recopie aucun caractère inutile.
4. affiche les deux mots.
Compiler, tester.

V.D Modifier le programme du V.C dans 5D.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.D.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 le réé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