1. Compléments
sur la fonction scanf.
On a déjà vu le fonctionnement détaillé de la fonction scanf
avec les spécificateurs de format %c, %d et %f,
dans le paragraphe 8 de la séance 2 de travaux dirigés.
On a également vu, dans le paragraphe 7 de la séance 4 de travaux dirigés,
que la fonction scanf permet de lire une
chaîne de caractères à l'aide du spécificateur de format %s.
Voici la description détaillée de la lecture d'une chaîne de caractères :
- Tant que le caractère le plus ancien du buffer d'entrée
est un séparateur, ce caractère est vidé du buffer
d'entrée.
- Ensuite, tant qu'on ne rencontre pas un caractère séparateur,
les caractères du buffer d'entrée sont stockés à
l'adresse indiquée et vidés du buffer d'entrée.
- Enfin, un caractère \0 est rajouté en fin de chaîne.
Exemple :
scanf("%d",&i);
scanf("%s",ch);
printf("Entier : %d ; chaîne : %s\n",i,ch);
Si l'utilisateur tape <retour-chariot>10abc<espace>5<retour-chariot>,
alors on aura l'affichage suivant :
et il restera les caractères <espace>5<retour-chariot>
dans le buffer d'entrée.
En revanche, si on intervertit les deux instructions scanf
dans la séquence précédente, et si l'utilisateur tape
les mêmes caractères que précédemment, alors
on aura l'affichage suivant :
et il restera seulement le caractère <retour-chariot>
dans le buffer d'entrée.
2. Compléments sur la fonction printf.
On a déjà vu le fonctionnement de la fonction printf
avec les spécificateurs de format %c, %d et %f,
dans le paragraphe 7 de la première séance de travaux dirigés,
ainsi que dans divers exemples.
La fonction printf permet également d'afficher
une expression de type "chaîne de caractères", à l'aide du spécificateur de format %s,
et ce de la manière suivante :
où expr a pour valeur l'adresse de la zone mémoire
contenant la chaîne à afficher.
Remarques :
- L'affichage d'une chaîne de caractères, contrairement
à l'affichage d'un caractère seul, d'un entier ou d'un réel,
se fait en passant à la fonction printf
un paramètre de type "adresse".
- L'instruction printf("%s",expr); fonctionne
correctement seulement si la chaîne située à l'adresse
expr est une vraie chaîne de caractères, c'est-à-dire
si elle se termine bien par un caractère \0
et, dans ce cas, tous les caractères précédant le
caractère \0 seront affichés, y compris les
caractères séparateurs, mais pas le caractère \0
dont le rôle est d'indiquer, en mémoire, la fin de la chaîne
de caractères.
Pour récapituler, on peut faire le tableau suivant, dans lequel
les différentes syntaxes d'appels aux fonctions
scanf et printf sont rappelées, avec les quatre spécificateurs de format
les plus courants %c, %d, %f et %s :
Lecture
|
Affichage
|
scanf("%c",&car);
|
printf("%c",car);
|
scanf("%d",&i);
|
printf("%d",i);
|
scanf("%f",&r);
|
printf("%f",r);
|
scanf("%s",ch);
|
printf("%s",ch);
|
3. Gestion dynamique de la mémoire.
Une gestion "dynamique" de la mémoire est possible,
grâce aux fonctions suivantes :
- void *malloc(size_t taille) : cette fonction
alloue "dynamiquement", en mémoire, un espace de taille octets,
et renvoie l'adresse de cet espace mémoire
(ou NULL si l'allocation ne peut être réalisée). Le type
size_t équivaut au type unsigned
int ("entier non signé"). Dans l'exemple suivant :
char *p;
...
p=malloc(20);
on effectue la "réservation dynamique" d'un espace mémoire
de 20 octets, qui permettra de stocker une chaîne
d'au plus 19 caractères utiles (il faut prévoir
la place du caractère \0).
- void free(void *pointeur) :
cette fonction libère
la zone mémoire accessible par un pointeur passé
en paramètre, dans le cas où cette zone mémoire a
été allouée dynamiquement par un appel à
la fonction malloc.
Attention :
- L'exécution d'un programme n'est normale que si chaque zone mémoire allouée dynamiquement,
à l'aide d'un appel à la fonction malloc, est désallouée à l'aide d'un appel à la
fonction free, à un moment où cette zone n'est plus utile.
- Ne jamais libérer une zone mémoire
déjà libérée. Il pourrait
alors se produire des erreurs de nature imprévisible.
- Pour pouvoir utiliser les fonctions malloc et free,
il est nécessaire de rajouter, en début de programme, la
"directive d'inclusion de fichier" suivante :
4. Structures.
Une structure est un ensemble de variables (de types éventuellement
différents) adapté à une gestion spécifique
des données. Par exemple, pour gérer les coordonnées
d'un point (abscisse et ordonnée), on pourra définir le type
suivant :
struct point
{
- int x;
- int y;
};
Le point-virgule, après l'accolade fermante
de la déclaration de la structure, est nécessaire. La définition d'une structure, comme d'ailleurs
la définition d'un type quelconque, à l'aide du mot-clé typedef (cf. la
troisième séance de travaux dirigés), peut être faite à l'intérieur d'une fonction ou en dehors
de toute fonction, auquel cas on dit qu'il s'agit d'une définition globale. Contrairement à ce qui
a été dit pour les variables, la définition globale d'une structure n'est pas déconseillée,
bien au contraire.
Exemple de déclaration d'une variable
de type structure :
Remarques :
- On peut également définir le type struct point en écrivant :
struct point
{
- int x,y;
};
mais cette deuxième écriture est moins lisible que la précédente.
- On aurait également pu déclarer la variable pt, de type struct
point, au moment où on définissait le type struct point, en utilisant la syntaxe
suivante :
struct point
{
- int x;
- int y;
} pt;
5. Initialisation d'un tableau à la déclaration.
Un tableau peut être initialisé au moment de sa déclaration
en indiquant la liste de ses valeurs.
Exemples :
- int tab[5]={1,2,3};
Les trois premiers éléments du tableau tab
reçoivent respectivement les valeurs 1, 2
et 3. Les deux derniers éléments du tableau
ont des valeurs inconnues a priori.
- char ch1[5]={'a','b','c','\0'}; est équivalent à char ch1[5]="abc";
La dernière case du tableau a une valeur inconnue a priori.
Rappel :
Dans les deux exemples précédents, tab
et ch1 ne sont pas des variables, mais des adresses.
On ne peut donc pas écrire des instructions telles que :
En revanche, si on fait la déclaration suivante :
alors ch2 est bien une variable.
Attention :
- Si on initialise un tableau sans prévoir la place du caractère
\0 (par exemple, si on initialise ch1 par
: char ch1[5]="abcde";), alors il risque de se
produire des erreurs surprenantes à l'exécution.
La déclaration char ch1[5]="abcde"; ne provoque aucun message d'erreur
à la compilation, contrairement à la déclaration char ch1[5]={'a','b','c','d','e','\0'};
qui provoque un message d'avertissement à la compilation.
- Il est fortement déconseillé, bien que cela soit correct en général,
d'initialiser ch2 de la façon suivante :
char *ch2="abcd"; /* Écriture à éviter absolument ! */
Pour s'en convaincre, il suffit de constater que l'instruction suivante :
provoquerait alors une erreur à l'exécution du type Segmentation fault.
La raison en est la suivante : l'initialisation char *ch2="abcd";
nécessite, en plus de l'allocation statique du pointeur ch2, l'allocation dynamique
de 5 octets, dans une zone
de la mémoire (ou "segment") réservée aux "constantes chaînes de caractères", dans laquelle aucune
modification de valeur n'est autorisée, d'où le message un peu mystérieux Segmentation fault
si l'on tente d'opérer une modification dans ce "segment".
Rappelons, pour fixer les idées, que l'initialisation suivante :
ne correspond pas à une allocation dynamique, mais à une allocation statique de
5 octets, dans une zone de la mémoire où les modifications de valeurs
sont autorisées.
6. Initialisation d'une structure.
Pour initialiser la variable pt de type struct
point déclarée précédemment, on peut
utiliser la syntaxe suivante :
Il est également possible d'initialiser une structure au moment
de sa déclaration, en donnant la liste des valeurs de ses champs,
de la même manière que pour un tableau. En reprenant l'exemple
précédent, au moment de déclarer la variable pt,
on peut écrire :
Attention :
Les initialisations à la déclaration
d'un tableau ou d'une structure ne peuvent pas être utilisées
ailleurs qu'au moment des déclarations. Par exemple, l'écriture
suivante est incorrecte :
struct point pt;
...
pt={20,30}; /* Écriture incorrecte. */
7. Pointeurs et structures.
7.1. Priorité des opérateurs.
L'opérateur . est prioritaire sur l'opérateur
*
Exemple :
struct point pt,*pp;
pp=&pt;
On a alors les identités suivantes :
(*pp).x == pt.x
et
(*pp).y == pt.y
Par contre, l'identité *pp.x == pt.x est illégale
puisqu'elle est équivalente, d'après la priorité
qui vient d'être évoquée, à *(pp.x) == pt.x.
Or pp.x n'a pas de sens.
On peut également utiliser l'opérateur -> (qui est de même priorité que l'opérateur
.) :
Dans le cas de l'exemple précédent, on a donc l'identité
pp->x == (*pp).x. Conformément à l'exemple ci-dessus, pt.x
est une notation correcte, mais pas pt->x, puisque pt
n'est pas un pointeur vers une variable de type struct point.
Remarque :
Il est déconseillé de retenir par coeur les règles de priorité entre
opérateurs. En revanche, il est toujours conseillé de parenthéser les expressions,
de façon à éviter les ambiguïtés. On conseille donc
de ne pas écrire une expression comme *pp.x qui comporte
deux opérateurs et n'est pas parenthésée.
7.2. Listes chaînées.
Le plus souvent, on manipule une liste chaînée à l'aide d'éléments
de type "structure", dont le dernier champ est un pointeur (la valeur de ce pointeur sera, à l'exception du
dernier élément de la liste, l'adresse de l'élément suivant). Par exemple,
on peut choisir de représenter une courbe par une liste chaînée
de points :
struct courbe
{
- struct point pt;
- struct courbe *suivant;
};
8. Exercice 1.
Soit trois points de coordonnées respectives (10,20),
(30,20) et (20,10). Écrire
l'initialisation du triangle construit à partir de ces trois points, en initialisant
trois variables, respectivement c1, c2
et c3 (cf. le schéma du paragraphe suivant) du type
struct courbe défini précédemment.
Questions complémentaires :
1) À partir de c1, et en utilisant les pointeurs,
donner la valeur de l'abscisse de p3.
2) Que valent :
a) ((*(*c1.suivant).suivant).pt).x
b) (((c3.suivant)->suivant).pt).x
c) (((*c2.suivant).suivant)->pt).x
9. Compléments sur la représentation en mémoire.
On peut compléter les règles déjà énoncées dans la séance 3 de travaux
dirigés, concernant la représentation graphique de la mémoire centrale
d'un ordinateur lors de l'exécution d'un programme, par les règles suivantes, relatives aux
structures :
- Une structure est représentée sous la forme de cases
connexes, où chaque case correspond à un champ. Chaque case comporte deux colonnes :
la première colonne contient le nom du champ, la deuxième colonne contient sa valeur,
si elle est définie.
- Les structures indépendantes sont dissociées
spatialement sur le graphique.
- Les identificateurs de structures (lorsqu'ils existent) apparaissent
à proximité de la représentation de celles-ci.
Exemple :
Le triangle de l'exercice 1 correspond à la représentation
en mémoire suivante :
10. Exercice 2.
Soit le programme suivant :
#include <stdio.h>
struct s1
{
char ch1[4];
char ch2[4];
};
int main(void)
{
struct s1 st1={"abc","def"};
struct s2
{
char ch3[4];
struct s1 ss1;
} st2={"ghi",{"jkl","mno"}};
printf("%c %c\n",st1.ch1[0],*st1.ch2);
printf("%s %s\n",st1.ch1,st1.ch2);
printf("%s %s\n",st2.ch3,st2.ss1.ch2);
printf("%s %s\n",st2.ch3+1,st2.ss1.ch2+1);
return(0);
}
|
Donner la représentation des données dans la mémoire
centrale et les résultats affichés à l'écran.
Ces pages ont été réalisées
par A. Crouzil, J.D. Durou et Ph. Joly.
Pour tout commentaire, envoyer un mail à
crouzil@irit.fr, à durou@irit.fr ou à
Philippe.Joly@irit.fr.