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 !).
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, 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 ! |
![]() |
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".
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 :
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