TP C2 2627v1 (obligatoirement dans un Terminal Linux, ou MacOS)
Durée : 4h (ou 2 x 2h)
|
Objectifs :
|
|
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 !).
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 !
|
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 ); où 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, n'ajouter aucun autre caractère que des formats % ; il faudra donc avoir prévu un printf avant. |
II.B Écrire
un programme 2B.c
avec la seule fonction
main
(vue au TP C1.1) 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 variable d'environnment
esiee
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 formats
% 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 ! |
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.
![]() |
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 (respectivement bleu) sont utilisables dans la fonction en vert (respectivement bleu). |
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".
▸ 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). |
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 déclare (dans l'ordre) :
1. une variable entière
vAvant, initialisée à 99
2. 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. 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.
Comprenez-vous que passer une variable par adresse revient à passer
l'adresse de cette variable par recopie ?
Ce qui explique qu'on peut modifier la valeur de la variable, mais pas son adresse !
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 réelles, et une procédure
echangeR.
(Très peu de choses à modifier ;-)
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)
4. affiche les deux mots.
Compiler, tester.
Avez-vous dû faire 24 ou 25 échanges ?
Donc, combien de recopies de caractères ?
- Lorsque cette v1 fonctionne, une v2 peu performante peut calculer la longueur
de chaque mot avant de commencer les échanges pour essayer d'en faire moins.
Compiler, tester.
Combien d'échanges avez-vous dû faire ?
Donc, combien de recopies de caractères ?
C'est mieux, mais combien de caractères parcourus/examinés avant de commencer ?
- Lorsque cette v2 fonctionne, programmer une v3 qui non seulement ne calcule plus
les longueurs, mais ne recopie aucun caractère inutile
lorsque les 2 mots n'ont pas la même longueur
(3 boucles peuvent être utiles).
Compiler, tester.
Combien d'échanges avez-vous dû faire ?
Et combien de recopies de caractères ?
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.
Combien d'échanges avez-vous dû faire ?
Donc, combien de recopies ?
Et combien de caractères parcourus/examinés ?
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
(donc très peu de choses à modifier ;-),
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 déjà vue en Java 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
si possible avant le TD C2, sinon avant le cours C2.