5 Structures alternatives
5.1 Choix simple
5.2 Choix multiple
5.2.1 Exercices
6 Structures répétitives
6.1 Structure ``Tant que'' (while )
6.2 Structure ``Faire ... Tant que'' (do ... while )
6.3 Structure ``Pour'' (for )
6.4 Boucles imbriquées
7 Sous-programmes (fonctions et procédures)
7.1 Exécution d'un sous-programme
7.2 Définition de fonctions
7.3 Appel d'une fonction
7.4 Procédures
7.5 Paramètres
7.6 Définition de procédure
7.7 Appel d'une procédure
7.8 Règles de visibilité
7.9 Notion de ``prototype''
7.10 Quand créer des sous-programmes ?
7.11 Recommandations
8 Définition de nouveaux types de données
8.1 Type défini par synonymie
8.2 Type défini par énumération
8.2.1 Valeur des éléments
8.2.2 Opérations sur une variable de type énuméré
8.2.3 Vérifications de type à la compilation
9 Types structurés : tableaux
9.1 Définition
9.2 Type des composantes
9.3 Tableau à 2 dimensions
9.4 Tableau multidimensionels
9.5 Tableaux et paramètres
9.6 Référence à un élément du tableau
9.7 Tableaux de caractères
9.8 Lecture / écriture de tableaux
9.8.1 Tableaux numériques
9.8.2 Tableaux de caractères
9.9 Dangers des tableaux
9.10 Exercices
9.10.1 Tableaux de dimension 1
9.10.2 Tableaux à plusieurs dimensions
10 Types structurés : structures hétérogènes (struct)
10.1 Définition
10.2 Accès aux composantes
10.3 Passage d'une structure en paramètre
11 Pointeurs
11.1 Définition d'un type pointeur
11.2 Variables de type pointeur
11.3 Valeurs de pointeurs - l'opérateur &
11.4 Accès à l'objet ``pointé'' - l'opérateur *
11.5 Pointeur NULL
11.6 Allocation de mémoire - l'opérateur new
11.7 Libération de mémoire - l'opérateur delete
11.8 Pointeurs et tableaux
11.9 Pointeurs et structures
11.10 Listes chaî nées
12 Fichiers de texte
12.1 Inclusion
12.2 Déclaration (et ouverture)
12.3 Lecture
12.4 Écriture
12.5 Fermeture
12.6 Passage de paramètres
12.7 Conclusion
13 Du C++ au C-ANSI
13.1 Entête
13.2 Affichage
13.3 Saisie
13.4 Type de données logiques
13.5 Allocation dynamique
13.6 Passage de paramètre par adresse
13.7 Fichiers de texte
13.7.1 Inclusion
13.7.2 Déclaration (et ouverture)
13.7.3 Lecture
13.7.4 Écriture
13.7.5 Fermeture
14 Notions de logique booléenne
14.1 Valeurs possibles
14.2 Opérateurs
14.3 Tables de vérité
14.4 Règles de simplification
14.5 Évaluation en C++
15 Bibliographie
Le but de ce document est de servir de support à un cours d'initiation à la programmation structurée .
L'enseignement de la programmation ne peut se concevoir sans une importante part de travaux pratiques, durant laquelle les étudiants conçoivent et mettent au point leurs propres programmes, et tirent les conséquences de leurs erreurs.
Le langage choisi pour les travaux pratiques est un sous-ensemble du C++ qui concilie la simplicité et la modularité du Pascal à l'avantage de se rapprocher des standards de programmation du moment. Ce compromis doit avoir pour effet de faciliter le passage soit vers le C-ANSI1, soit vers le C++ au sens orienté objet (voire vers Java), éventuellement en auto-formation.
Le présent document n'est donc pas un cours de langage C++. Nous ne présentons ici que ce qui est strictement indispensable à la progression pédagogique. En particulier, l'aspect orienté objet n'est pas abordé dans ce cours.
L'étudiant désirant aller plus loin dans la connaissance de C++ trouvera facilement, dans les manuels de référence et les nombreux ouvrages qui lui sont consacrés, des présentations plus exhaustives.
Cependant, il est préférable à notre avis de maîtriser parfaitement le contenu du présent cours avant d'explorer les autres possibilités du langage.
Un programme informatique est constitué d'ordres simples, bien définis et dont l'effet est parfaitement connu. Mais un grand programme, qui peut comporter des millions de ces ordres élémentaires, est un objet très complexe, susceptible de réponses ou de comportements surprenants, et parfois même pouvant sembler ``intelligents''.
Cette complexité est la principale difficuté que doit vaincre le concepteur de programmes informatiques. Autant il est facile de concevoir sans méthode un programme de quelques dizaines d'instructions, autant il est nécessaire de s'imposer des règles de conception et de construction pour maîtriser la complexité des grands programmes.
Les notions clés qui permettent cette maîtrise de la complexité sont : modularité et structuration.
La modularité, c'est l'application de l'adage : diviser pour vaincre. On résoud plus facilement un problème complexe en le décomposant en plusieurs sous-problèmes de difficulté moindre. Encore faut-il que ces sous-problèmes soient relativement indépendants.
La structuration vient naturellement lorsqu'on décompose une tâche en sous-tâches, qui elles-mêmes se décomposent à leur tour en sous-sous-tâches, etc. Le résultat obtenu prend alors une forme - une structure - hiérarchique.
Les principaux langages informatiques utilisés aujourd'hui (C, C++, Ada, Pascal, Java, etc.) permettent l'expression de programmes sous forme structurée, et comportent des outils (les procédures, les fonctions¼) pour construire des modules.
Ce cours a pour but, non d'enseigner seulement un langage, mais d'introduire à travers un sous-ensemble du langage C++ ces notions très générales qui servent de base à toute conception méthodique de programmes (quel que soit le langage utilisé).
Exemple 1
#include <iostream.h>
int main()
{
cout << "bienvenue a l'ESIEE" << endl;
return 0;
}
Sa première ligne :
#include <iostream.h>sert à introduire des informations utiles au compilateur, concernant les fonctions standard d'entrée/sortie (Input/Output stream = flux d'entrée/sortie) comme celles permettant d'afficher des caractères à l'écran, de saisir des valeurs au clavier, etc. Nous retrouverons donc cette entête dans tous les programmes réalisant des entrées/sorties.
Les mots-clés int main (main signifie : principal, et nous reviendrons plus tard sur le sens de int) introduisent le corps du programme principal. Celui-ci se compose d'un bloc d'instructions, délimité par les symboles accolades { et }. Sa dénomination vient du fait qu'il peut faire appel à des programmes auxiliaires, ou sous-programmes.
La seule instruction de ce programme d'exemple (cout ...) affiche le message de bienvenue à l'écran. Notez les guillemets ("), qui servent à délimiter le message, et le symbole endl (end of line), qui provoque un passage du curseur à la ligne suivante, après l'affichage du message. Notez également l'opérateur << qui doit précéder, dans une instruction cout, chaque partie de message. Le point-virgule (;) termine l'instruction. Notez enfin la dernière instruction qui permet de retourner un code d'erreur au système d'exploitation (ici, 0 signifie qu'il n'y a pas d'erreur); cette syntaxe sera expliquée au chapitre 7.2 sur les fonctions
En dehors des zones entre guillemets, les blancs ou espaces, retours à la ligne et tabulations peuvent être utilisés pour améliorer la présentation des programmes. Le compilateur les traite comme des séparateurs de symboles, et ignore ceux qui sont superflus.
Exemple 2
/* Ce programme resoud les systemes lineaires
par la methode de Gauss */
#include <iostream.h>
int main()
{
...
Les identificateurs sont les noms des objets qui apparaissent dans vos programmes. Dans l'exemple suivant, il y a trois identificateurs : les noms Dividende, Diviseur et Quotient qui désignent des variables servant à stocker des entiers relatifs. Des identificateurs sont également utilisés pour définir des constantes , des types de données , des fonctions (nous examinerons ces notions dans la suite du cours).
Exemple 3
#include <iostream.h>
int main()
{
int Dividende, Diviseur, Quotient;
Dividende = 24;
Diviseur = 3;
Quotient = Dividende / Diviseur;
cout << Quotient << endl;
return 0;
}
Le nombre de caractères que peut comporter un identificateur dépend de votre compilateur (par exemple pour Turbo-C: nombre de caractères quelconque, mais seuls les 32 premiers caractères sont significatifs).
Les majuscules et les minuscules sont des caractères différents : ainsi les identificateurs NAME, Name et name sont différents les uns des autres.
Attention cependant : les mots de la liste suivante sont réservés et ne peuvent être réutilisés :
asm | auto | break | catch | case | char |
class | const | continue | default | delete | do |
double | else | enum | extern | float | for |
friend | goto | if | inline | int | long |
new | operator | overload | private | protected | public |
register | return | short | signed | sizeof | static |
struct | switch | this | template | try | typedef |
union | unsigned | virtual | void | volatile | while |
Mots réservés du C++
Il est possible d'éviter de tomber accidentellement sur un de ces mots en commençant systématiquement ses identificateurs par une majuscule.
Dans l'exemple 1.1 , la seule instruction exécutable est une instruction d'affichage annoncée par l'ordre cout (de l'anglais Channel OUTput , canal de sortie. Prononcez : "cé-a-outte"). Les ordres d'affichage :
cout << element; cout << element1 << element2 << element3;
provoquent l'affichage des éléments annoncés par le signe << . Si l'élément est entre guillemets (") alors le texte est imprimé littéralement.
Pour inclure un guillemet dans un texte à afficher littéralement, il faut le faire précéder d'une barre oblique ( \ ), pour éviter que celui-ci soit interprété comme la fin du texte :
instruction : | cout << "imprime \" et le reste" |
effet à l'écran : | imprime " et le reste |
cout << "Les sanglots longs\n des violons\n de l'automne" << endl;
donne l'effet suivant à l'écran :
Les sanglots longs des violons de l'automne
En fin d'affichage il est préférable pour passer à la ligne suivante d'utiliser le symbole endl, qui a également pour effet de forcer un affichage immédiat à l'écran.
Exercice 1
Écrire le programme qui fait afficher le texte suivant :
"... diviser chacune des difficultes que j'exprimerais
en autant de parties qu'il se pourrait et qu'il serait requis
pour les mieux resoudre."
Rene DESCARTES
Le Discours de la Methode (1637)
En mathématique, nous distinguons les nombres entiers naturels, les entiers relatifs, les rationnels, etc.
En informatique, nous distinguons de même différents types de nombres, notamment des nombres de type int (en anglais integer = entier) et double (nombres à virgule flottante en double précision). int et double sont deux types de données élémentaires .
Leur but est de modéliser en machine les éléments de ces ensembles mathématiques idéaux, sans vraiment y parvenir, car la place en mémoire que l'on peut consacrer au stockage d'un nombre est forcément limitée.
Dans la pratique, la place qu'occupe un nombre de type int est, par exemple, 32 bits, ce qui limite l'intervalle des entiers représentables à [-231, +231-1] . Nous reviendrons plus en détail dans la suite sur cette notion de type de données.
Il est conseillé, pour la lisibilité et la maintenabilité des programmes, de substituer aux valeurs numériques brutes des symboles les représentant : ce sont les constantes. Par exemple :
Exemple 4
const double PI = 3.1415;
const double ACCPESANTEUR = 9.81;
const int NBREMAX = 30;
Dans la suite du programme, on pourra faire référence à ces valeurs par leur nom, par exemple :
Exemple 5
cout << "perimetre du disque de rayon 1 = ";
cout << 2 * PI;
cout << endl;
Exemple 6
const int COTE_CARRE = 5;
const int SURFACE_CARRE = COTE_CARRE * COTE_CARRE;
const int PERIMETRE_CARRE = COTE_CARRE * 4;
Une variable est une unité de stockage qui permet de mémoriser, durant l'exécution du programme, une information d'un type donné. Les variables doivent être définies avant leur utilisation :
Exemple 7
/* declaration de variables */
int NombreDeDisques;
double Rayon, Surface;
La place dans un programme des déclarations de constantes et de variables est importante. C'est cette place qui, dans un programme complexe, détermine la zone où les objets déclarés pourront être utilisés (portée d'une déclaration).
Dans un programme simple ne comportant qu'un seul bloc d'instructions (délimité par les accolades { et }), la place des déclarations est au début du bloc : juste après l'accolade ouvrante {, et avant la première instruction exécutable (voir l'exemple 1.3 ).
Nous avons vu comment afficher un message sous forme littérale : si l'élément à afficher est entre guillemets (") alors le texte sera imprimé littéralement.
Sinon, c'est qu'il s'agit d'une variable, d'une constante ou du résultat d'un calcul dont la valeur sera affichée :
Exemple 8
cout << (ACCPESANTEUR * (1 - 0.01));
cout << Surface;
La plupart des programmes nécessitent une interaction avec un utilisateur. Nous venons de voir comment le programme peut communiquer des informations à l'utilisateur en affichant des messages et des données à l'écran. L'utilisateur doit pouvoir, lui aussi, communiquer des informations au programme. C'est le but de l'ordre d'acquisition cin (de l'anglais Channel INput , canal d'entrée. Prononcez : "cé-inne").
Exemple 9
cout << "donnez la valeur du rayon : "
cin >> Rayon;
Remarque : le sens de circulation des données permet de se souvenir s'il faut employer << ou >> .
Étudiez l'exemple suivant :
Exemple 1
/* calcul de la surface d'un disque de rayon donne */
#include <iostream.h>
int main()
{
const double PI = 3.14159;
double Rayon, Surface;
/* acquisition de la donnee */
cout << "rayon en m du disque "
<< "dont vous voulez connaitre la surface ? ";
cin >> Rayon;
/* traitement */
Surface = PI * Rayon * Rayon;
/* affichage du resultat */
cout << "la surface du disque est de "
<< Surface
<< " metres carres." << endl;
return 0;
}
Dans certains langages (comme Pascal) on nomme ce type ``réel''. On parle indifféremment de ``flottants'' ou nombres à ``virgule flottante'' (floating point) par référence à la notation mantisse-exposant, qui permet de placer la virgule ou le point décimal à n'importe quel endroit :
|
Dans les exemples précédents, nous avons également rencontré des variables de type int (entier relatif).
Plus généralement, un type est un ensemble de données de même nature régies par des règles communes.
Nous aborderons dans un premier temps les types simples prédéfinis, et nous verrons par la suite comment le programmeur peut définir de nouveaux types de données.
Les 4 types simples prédéfinis que nous utiliserons dans ce cours correspondent respectivement aux entiers relatifs, aux nombres à virgule flottante en double précision, aux caractères, et aux valeurs logiques. Ce sont :
int double char bool |
Le type
int |
Une valeur entière en notation décimale s'exprime comme une succession de chiffres décimaux (entre 0 et 9) précédés ou non d'un signe.
Exemple 2
27500
-123
+418
+ | additition |
- | soustraction |
* | multiplication |
/ | division |
% | modulo |
Opérateurs arithmétiques pour les entiers
L'opérateur / sert à calculer le quotient de la division entière.
Exemple 3
13 / 5 vaut 2
1998 / 10 vaut 199
L'opérateur % (modulo) sert à calculer le le reste de la division entière.
Exemple 4
13 % 5 vaut 3
1998 % 10 vaut 8
7 % 2 vaut 1
8 % 2 vaut 0
== | égal |
!= | différent |
< | inférieur strict |
<= | inférieur ou égal |
> | supérieur strict |
>= | supérieur ou égal |
Opérateurs relationnels
nom | type param | type retour | rôle |
abs | int | int | valeur absolue d'un int |
Pour utiliser cette fonction dans votre programme, vous devrez placer en en-tête la directive :
#include <math.h>Nous verrons au chapitre 7.2 p.pageref comment définir de nouvelles fonctions (non prévues dans le standard C++).
Le type
double |
Il existe également un type float donnant une moins grande précision. Usuellement les variable de type float sont stockées sur 32 bits.
La forme générale des valeurs de type double est :
[ ± ] chiffres[.chiffres ] [ E [±] chiffres ]
ou bien :
[ ± ] .chiffres [ E [±] chiffres ]
(les éléments entre crochets [ ] sont optionnels).
Exemple 5
2.3
-12E6
+123.456E-20
.123
+ | additition |
- | soustraction |
* | multiplication |
/ | division |
Opérateurs arithmétiques pour les nombres de type double
Ce sont les mêmes que pour les entiers.
Dans les programmes devant effectuer des calculs, il est souvent nécessaire d'avoir recours à des fonctions de la bibliothèque mathématique . Voici celles qui sont le plus souvent utilisées :
nom | type param | type retour | rôle |
acos | double | double | arc cosinus |
asin | double | double | arc sinus |
atan | double | double | arc tangente |
ceil | double | double | arrondi à la valeur supérieure |
cos | double | double | cosinus |
exp | double | double | exponentielle |
fabs | double | double | valeur absolue d'un double |
floor | double | double | arrondi à la valeur inférieure |
log | double | double | logarithme néperien |
sin | double | double | sinus |
sqrt | double | double | racine carrée |
Fonctions usuelles de la bibliothèque mathématique
Pour utiliser ces fonctions dans votre programme, vous devrez placer en en-tête la directive :
#include <math.h>
Voici un exemple d'utilisation de la fonction sqrt (racine carrée = square root ) :
Exemple 6
#include <iostream.h>
#include <math.h>
int main()
{
cout << "Le Nombre d'Or vaut : " << (sqrt(5) + 1) / 2 << endl;
return 0;
}
Le type
char |
... < '0' < '1' < ... < '9' < ...'A' < 'B' < ...'Z' < ... < 'a' < 'b' < ... < 'z' < ...
Les valeurs des caractères imprimables sont représentées entre apostrophes.
Exemple 7
char LettreMajuscule;
LettreMajuscule = 'A';
'\t' tab (tabulation) '\n' newline (retour a la ligne) '\\' \
Pour les autres, il suffit de connaître leur code ASCII. On utilisera la forme :
(char)Npour désigner la valeur du caractère dont le code ASCII est N.
De façon analogue, si C est une variable de type char,
(int)Cdésigne le code ASCII du caractère C (entier compris entre 0 et 127).
Exemple 8
#include <iostream.h>
int main()
{
const char BIP = (char)7; /* code ascii 7 = bip sonore */
char carac;
cin >> carac; /* saisie d'un caractere au clavier */
if (carac == 'A') /* si c'est un A, alors */
cout << BIP; /* emet un bip sonore */
return 0;
}
Les caractères étant codés par des entiers, on peut leur appliquer les mêmes opérateurs, notamment l'addition et la soustraction. A charge pour le programmeur de vérifier que de telles opérations ont un sens.
Exemple 9
/* conversion d'une majuscule en minuscule */
carac = carac + 'a' - 'A';
Ce sont les mêmes que pour les entiers.
Ces fonctions servent, soit à tester si un caractère donné appartient à un certain ensemble (par exemple les lettres minuscules), soit à transformer un caractère (par exemple, mise en majuscule d'une lettre).
nom | type param | type retour | rôle |
isdigit | char | int | est-ce un chiffre ? |
isalpha | char | int | est-ce une lettre ? |
isalnum | char | int | est-ce un chiffre ou une lettre ? |
iscntrl | char | int | est-ce un caractère non imprimable ? |
islower | char | int | est-ce une lettre minuscule ? |
isupper | char | int | est-ce une lettre majuscule ? |
tolower | char | char | transforme en minuscule |
toupper | char | char | transforme en majuscule |
Fonctions usuelles de traitement d'un caractère
Pour utiliser ces fonctions dans votre programme, vous devrez placer en en-tête la directive :
#include <ctype.h>
La logique dite ``booléenne'' s'intéresse aux fonctions dont les espaces de départ et d'arrivée sont réduits à 2 éléments :
faux vrai
Malgré son apparence modeste, cet ensemble est très utile pour construire des expressions logiques complexes, mémoriser des conditions de test, etc. Pour plus de détails, se reporter au chapitre 14 .
En C, il n'existe pas de type ``booléen'' particulier. On représente la valeur ``faux'' par l'entier 0, et la valeur ``vrai'' par toute autre valeur entière (généralement 1).
En C++, il existe le type ``bool'', ainsi que les deux constantes
true et false ,
correspondant respectivement aux valeurs vrai et faux.
Attention !
A l'affichage, ces deux valeurs apparaissent respectivement comme
1 et 0.
&& | et |
|| | ou |
! | non |
Opérateurs logiques
Exemple 10
#include <iostream.h>
int main()
{
bool b;
b = (true || false) && ! false;
cout << "b = " << b << endl;
/* quelle sera la valeur imprimee ? */
return 0;
}
Il est souvent nécessaire de convertir une valeur d'un type à un autre pour le besoin d'un calcul, ou de faire participer à une même opération des objets de types différents (par exemple double et int).
Certaines conversions sont implicites, mais d'une façon générale il est préférable de les rendre explicites en préfixant l'objet à convertir par le type que l'on veut lui attribuer, entre parenthèses :
(NomType)NomObjetcomme nous l'avons vu avec les conversions entier-caractère (section 2.3 ). Cette opération est connue dans la ``littérature'' anglophone sous le nom de cast .
Attention : la conversion de type peut occasionner une perte d'information, comme dans le cas d'une conversion double ® int. Dans ce cas en effet, c'est la partie entière de la valeur qui est conservée.
Exemple 11
double D;
int I;
/* ... */
I = (int)D; /* I recoit la partie entiere de D */
I = D; /* meme effet, mais provoque de la part */
/* du compilateur un message d'avertissement */
L'ANSI-C et le C++ permettent de représenter les nombres différemment suivant les plate-formes matérielles et logicielles. Les exemples qui suivent ne s'appliquent qu'au logiciel Borland C++, mais ils illustrent bien la diversité des types numériques.
Sur une architecture 32 bits : | |||
Type | Taille | Intervalle | Applications |
(en octets) | |||
char | 1 | -128 à +127 | très petits nombres et caractères ASCII |
unsigned char | 1 | 0 à 255 | petits nombres et caractères |
short int | 2 | -32.768 à +32.767 | compteurs de boucles et nombres pas trop grands |
unsigned int | 4 | 0 à 4.294.967.295 | nombres en général |
int | 4 | -2.147.483.648 à +2.147.283.647 | grands nombres |
long | 4 | -2.147.483.648 à +2.147.283.647 | grands nombres |
unsigned long | 4 | 0 à 4.294.967.295 | distances astronomiques |
float | 4 | ±3.4 × 10-38 à ±3.4 × 10+38 | calcul scientifique avec 7 chiffres significatifs |
double | 8 | ±1.7 × 10-308 à ±1.7 × 10+308 | calcul scientifique avec 15 chiffres significatifs |
long double | 10 | ±3.4 × 10-4932 à ±1.1 × 10+4932 | calcul financier avec 18 chiffres significatifs |
Sur une architecture 16 bits : | |||
Type | Taille | Intervalle | Applications |
(en octets) | |||
char | 1 | -128 à +127 | très petits nombres et caractères ASCII |
unsigned char | 1 | 0 à 255 | petits nombres et caractères |
short int | 2 | -32.768 à +32.767 | compteurs de boucles et nombres pas trop grands |
int | 2 | -32.768 à +32.767 | compteurs de boucles et nombres pas trop grands |
unsigned int | 2 | 0 à 65.535 | compteurs de boucles et nombres en général |
long | 4 | -2.147.483.648 à +2.147.283.647 | grands nombres |
unsigned long | 4 | 0 à 4.294.967.295 | distances astronomiques |
float | 4 | ±3.4 × 10-38 à ±3.4 × 10+38 | calcul scientifique avec 7 chiffres significatifs |
double | 8 | ±1.7 × 10-308 à ±1.7 × 10+308 | calcul scientifique avec 15 chiffres significatifs |
long double | 10 | ±3.4 × 10-4932 à ±1.1 × 10+4932 | calcul financier avec 18 chiffres significatifs |
Une expression est une combinaison cohérente, éventuellement parenthésée, d'opérandes et d'opérateurs qui sont évalués pour donner une valeur.
Opérande |
Opérateurs |
En cas d'ambiguïté, l'évaluation a lieu selon l'ordre de priorité suivant :
A priorité égale, évaluation de gauche à droite.
Pour les expressions booléennes de type A && B (A et B), l'opérande droit (B) n'est évalué que si nécessaire. En effet, si l'évaluation de A donne la valeur logique ``faux'', la valeur de l'expression est ``faux'' quelle que soit la valeur de B, il est donc inutile d'évaluer B. Il en est de même pour les expressions de la forme A || B (A ou B) , lorsque l'évaluation de l'opérande gauche (A) donne ``vrai'' (voir également le chapitre 14 p.pageref).
Il est préférable, en général, d'utiliser les parenthèses pour rendre explicite l'ordre dans lequel les opérations doivent s'effectuer. Non seulement cela évite d'avoir à connaître la table des priorités sur le bout des doigts, mais cela facilite beaucoup la lecture du programme.
Attention !
Les opérateurs == et != ne doivent pas être utilisés
avec des données de type double à cause de problèmes de
précision, surtout quand ces données sont issues d'un calcul.
Il faut donc remplacer if (X == Y) par
if ( fabs( X - Y ) < PRECISION )
où PRECISION est une constante préalablement définie
(1E-9 par exemple).
C'est également pour cette raison que les programmes manipulant des
données comptables n'utilisent pas de nombres réels, car les
résultats doivent toujours tomber juste au centime près.
Exercice 1
Que valent ces expressions ?
Exemple 1
#include <iostream.h>
#include <math.h>
int main()
{
double X, Y; /* declarations */
cin >> X; /* acquisition d'une valeur */
Y = sin(X); /* calcul et memorisation du sinus de X */
cout << Y << endl; /* affichage du resultat */
return 0;
}
cin >> X;
est une instruction d'entrée (input), une valeur est attendue du clavier puis stockée dans la variable X. La seconde :
Y = sin(X);
est une instruction d'affectation , le sinus de la valeur stockée dans X est calculé puis stocké dans la variable Y. La troisième :
cout << Y << endl;
est une instruction de sortie (output), la valeur stockée dans la variable Y est affichée à l'écran.
Ces trois instructions sont regroupées en une instruction composée (ou bloc d'instructions) délimitée par les accolades { et } . Nous connaissons donc trois formes d'instruction :
Affectation - instruction composée - entrée / sortie |
La forme générale d'une instruction d'affectation est :
VARIABLE = EXPRESSION; |
L'expression est d'abord évaluée, puis le résultat de cette évaluation est stocké dans la variable.
Un cas particulier d'affectation est particulièrement fréquent et possède une syntaxe simplifiée. Il s'agit de l'opération d'incrément qui consiste à augmenter d'une unité le contenu d'une variable. Sous sa forme standard, l'opération d'incrément peut s'écrire :
VARIABLE = VARIABLE + 1;Sous la forme condensée, on peut écrire l'instruction logiquement équivalente :
VARIABLE++; |
L'opération inverse, nommée décrément , possède également une forme condensée, ainsi
VARIABLE = VARIABLE - 1;est logiquement équivalent à :
VARIABLE- -; |
La forme générale d'une instruction composée est :
|
Remarque : chaque instruction composée ou bloc peut comporter au début une ou plusieurs déclarations locales. Les objets définis localement ne sont utilisables (``visibles'') que dans le bloc dans lequel ils sont définis.
Note importante : une instruction simple se termine par un point-virgule. Une instruction composée se termine par une accolade fermante.
Exercice 1
En choisissant des noms de variables adéquats
et en définissant toutes les constantes nécessaires,
écrire les déclarations et instructions C++
qui correspondent aux formules ci-dessous :
où g est la constante gravitationelle (9,81 m/s2)
t = 2p
æ
ú
Ö
l
g
où p est le demi-périmètre du
triangle.
s =
_____________
Öp(p-a)(p-b)(p-c)
Lorsqu'un traitement doit être subordonné à une condition, on utilise la structure :
|
Exemple 1
/* resolution d'une equ. du second degre a coeff. non nuls */
#include<iostream.h>
#include<math.h>
int main()
{
double A, B, C, Delta;
/* acquisition */
cout << "entrez les valeurs des coefficients A, B, et C non nuls";
cout << endl;
cin >> A >> B >> C;
/* traitement et affichage des resultats */
Delta = B * B - 4 * A * C;
if (Delta == 0) cout << "X1 = X2 = " << -B / (2 * A) << endl;
if (Delta > 0)
{
cout << "X1 = " << (-B + sqrt(Delta)) / (2 * A) << endl;
cout << "X2 = " << (-B - sqrt(Delta)) / (2 * A) << endl;
}
if (Delta < 0) cout << "pas de racines reelles" << endl;
return 0;
}
|
Note : si INSTRUCTION1 est une instruction simple (non composée), elle doit obligatoirement se terminer par un point-virgule, même si la présence du mot-clé else indique que l'instruction if n'est pas terminée.
Exemple 2
/* affiche le plus grand de deux nombres lus */
#include<iostream.h>
int main()
{
double X, Y, Max;
/* acquisition */
cout << "entrez deux nombres separes par un espace" << endl;
cin >> X >> Y;
/* traitement */
if (X > Y)
Max = X;
else
Max = Y;
/* affichage du resultat */
cout << Max << " est le plus grand de " << X << " et de " << Y << endl;
return 0;
}
Lorsque le choix entre divers traitements dépend de la valeur d'une seule expression, on peut utiliser la structure :
|
Remarques :
Si une même instruction doit être exécutée pour différentes valeurs de constantes, on peut utiliser dans le switch la forme :
|
/* determination de jour ouvre */ #include<iostream.h> int main() { int Jour; char Boulot; /* acquisition */ cout << "entrez un jour de la semaine" << endl << "(lundi: 1, mardi: 2,..., dimanche: 7)" << endl; cin >> Jour; /* traitement */ switch (Jour) { case 1: case 2: case 3: case 4: case 5: Boulot = 't'; break; case 6: case 7: Boulot = 'r'; break; default: Boulot = 'e'; } /* fin switch */ /* affichage du resultat */ switch (Boulot) { case 'e': cout << "erreur" << endl; break; case 'r': cout << "repos" << endl; break; case 't': cout << "travail" << endl; break; /* facultatif */ } /* fin switch */ return 0; }
Exercice 1
Ecrire un programme qui saisit trois nombres et affiche le plus petit.
Lorsque l'on doit répéter un traitement tant qu' une condition reste vérifiée, on utilise la structure :
|
Exemple 1
/* demande a l'utilisateur d'entrer une etoile (*)
et insiste jusqu'a obtenir satisfaction */
#include <iostream.h>
int main()
{
const char ETOILE = '*';
char Carac;
/* initialisations */
Carac = ETOILE + 1;
/* acquisition d'un caractere et traitement */
while (Carac != ETOILE)
{
cout << "entrez une " << ETOILE << " !!!" << endl;
cin >> Carac;
} /* fin while */
cout << "merci." << endl;
return 0;
}
Lorsque le traitement doit être effectué au moins une fois, on utilise cette variante de la structure précédente :
|
Exemple 2
/* demande a l'utilisateur d'entrer une etoile (*)
et insiste jusqu'a obtenir satisfaction */
#include <iostream.h>
int main()
{
const char ETOILE = '*';
char Carac;
/* acquisition d'un caractere et traitement */
do
{
cout << "entrez une " << ETOILE << " !!!" << endl;
cin >> Carac;
} while (Carac != ETOILE);
cout << "merci." << endl;
return 0;
}
Exemple 3
Ecrire un programme qui saisit une valeur réelle positive A et calcule sa racine
carrée avec une précision telle que l'écart entre A et le carré de sa
racine carrée ainsi estimée est inférieur à 10-6.
/*
Compte le nombre de $ et de caracteres differents de $ entres par
l'utilisateur. Celui-ci termine par le caractere * .
*/
#include <iostream.h>
int main()
{
const char CARSPECIAL = '$';
const char CARFIN = '*';
int CompteSpecial, CompteNonSpecial; /* compteurs */
char Carac;
/* initialisations */
CompteSpecial = 0;
CompteNonSpecial = 0;
Carac = CARFIN + 1;
cout << "entrez des caracteres, terminez par *" << endl;
/* acquisition d'un caractere et traitement */
while (Carac != CARFIN)
{
cin >> Carac;
if (Carac == CARSPECIAL)
CompteSpecial++;
else
CompteNonSpecial++;
} /* fin while */
CompteNonSpecial--; /* on ne compte pas l'etoile */
/* affichage des resultats */
cout << endl << CompteSpecial << " " << CARSPECIAL << " et ";
cout << CompteNonSpecial << " caracteres differents de ";
cout << CARSPECIAL << endl;
return 0;
}
Lorsque l'on désire répéter un traitement un nombre de fois déterminé, on peut utiliser la structure :
|
L'expression booléenne TEST est évaluée avant chaque répétition
éventuelle de l'INSTRUCTION_FOR.
Si elle est fausse, on quitte la structure et on passe à la
suite du programme.
Si elle est vraie, on exécute l'INSTRUCTION_FOR (qui
peut bien sûr être une instruction composée), puis l'instruction
INCREMENT, et enfin on retourne à l'évaluation de TEST.
L'instruction INCREMENT est exécutée après chaque exécution d'INSTRUCTION_FOR. Typiquement, elle sert à incrémenter un compteur.
Exemple 4
#include <iostream.h>
int main()
{
const int MAX = 5;
int I;
for (I = 1; I <= MAX; I++)
cout << " " << I << " fois" << endl;
return 0;
}
execution:
1 fois
2 fois
3 fois
4 fois
5 fois
Exemple 5
/* affichage des codes ASCII des caracteres entre Z et A */
#include <iostream.h>
int main()
{
char Carac;
for (Carac = 'Z'; Carac >= 'A'; Carac--)
{
cout << " caractere : " << Carac;
cout << " code ASCII : " << (int)Carac;
cout << endl;
} /* fin for */
return 0;
}
Exemple 6
/* calcul de factorielle a l'aide d'une variable de controle */
#include <iostream.h>
int main()
{
int I, N;
double Produit;
/* entree des donnees */
cout << "factorielle de ? ";
cin >> N;
/* traitement */
Produit = 1;
for (I = 2; I <= N; I++)
Produit = Produit * I;
/* affichage du resultat */
cout << N << "! vaut " << Produit << endl;
return 0;
}
Il est courant d'avoir à imbriquer des boucles, particulièrement lorsque l'on traite des objets à plusieurs dimensions. Imaginez que vous ayez à examiner un damier : il vous faut examiner chacune des dix lignes, et pour chaque ligne, il faut examiner chacune des dix cases qui la constitue. Cela donne l'algorithme suivant :
pour LIGNE allant de 1 a 10 faire debut pour COLONNE allant de 1 a 10 faire EXAMINER_CASE(LIGNE, COLONNE) fin
Exemple 7
Écrire un programme qui saisit deux entiers au clavier, et qui
affiche à l'écran un rectangle composé de caractères '*',
dont les dimensions correspondent aux entiers saisis.
/*
programme affichant un rectangle - exemple d'execution :
entrez 2 entiers : 3 8
********
********
********
*/
#include <iostream.h>
int main()
{
int I, J, NbLigne, NbColonne;
cout << "entrez 2 entiers : ";
cin >> NbLigne >> NbColonne;
for (I = 0; I < NbLigne; I++)
{
for (J = 0; J < NbColonne; J++)
{
cout << "*";
} /* fin for J */
cout << endl;
} /* fin for I */
return 0;
}
*....
.*...
..*..
...*.
....*
.....
.....
.....
Les sous-programmes (fonctions et procédures) offrent au programmeur le moyen de réaliser des modules de programmation relativement indépendants, réalisant chacun une tâche parfaitement définie, et pouvant être développés, vérifiés et testés séparément.
Une fois écrit et mis au point, un sous-programme pourra être vu et utilisé comme une ``boîte noire'', dont on n'aura à connaître que le nom (symbolisant le calcul réalisé), et les paramètres d'entrée et de sortie.
Prenons comme exemple une fonction que nous connaissons déjà, la fonction sin de la bibliothèque mathématique. Il se peut qu'un calcul compliqué soit réalisé pour trouver le sinus d'un nombre flottant, mais une fois la fonction sin définie, tout ce qu'il nous faut en savoir se résume au diagramme suivant :
L'exécution d'une fonction, ou plus généralement d'un sous-programme, est provoquée par son appel qui figure dans le programme principal (ou dans un autre sous-programme). L'appel provoque un débranchement temporaire de l'exécution du programme principal (ou du sous-programme) appelant , qui est interrompu pour laisser s'exécuter le sous-programme appelé . Après la fin de l'exécution du sous-programme appelé, l'exécution du programme appelant reprend là où elle avait été interrompue.
Exemple 1
Contenu du programme principal :
Contenu (définition) du sous-programme B :
Exécution :
debut programme
SEQUENCE 1
APPEL SOUS-PROGRAMME B
SEQUENCE 2
APPEL SOUS-PROGRAMME A
SEQUENCE 3
fin programme
debut sous-programme
SEQUENCE 4
APPEL SOUS-PROGRAMME A
SEQUENCE 5
fin sous-programme
debut programme
SEQUENCE 1
debut sous-programme B
SEQUENCE 4
debut sous-programme A
...
fin sous-programme A
SEQUENCE 5
fin sous-programme B
SEQUENCE 2
debut sous-programme A
...
fin sous-programme A
SEQUENCE 3
fin programme
Nous connaissons une fonction standard prédéfinie : la fonction sin . Nous allons voir maintenant comment le programmeur peut définir, puis utiliser ses propres fonctions.
La notion de fonction, en programmation, modélise la notion mathématique de fonction : à une valeur du paramètre x choisie dans un ensemble de départ (le domaine de définition de la fonction), on fait correspondre (on calcule) une valeur f(x) appartenant à l'ensemble d'arrivée. Il est également possible de définir des fonctions de plusieurs variables (f(x,y,z)) mais la valeur retournée est toujours unique.
Voici tout d'abord un exemple de définition de fonction.
Exemple 2
/* fonction a valeur entiere et parametre entier
calculant le cube de l'argument
*/
int Cube (const int I)
{
return I * I * I;
}
TypeRésultat
NomFonction |
{ | |
instructions | corps |
de la | |
return expression; | fonction |
} |
Remarques :
Voici un programme définissant et utilisant une fonction à deux paramètres, l'un entier, l'autre flottant, et retournant une valeur flottante :
Exemple 3
/* exemple d'utilisation d'une fonction */
#include<iostream.h>
/* =============================================== */
/* FONCTION: Puissance(R, I) */
/* calcule R a la puissance I (I positif ou nul) */
double Puissance (const double R, const int I)
{
int Compteur; /* compte le nombre de multiplications */
double Produit; /* stocke les produits partiels */
Produit = 1;
for (Compteur = 1; Compteur <= I; Compteur++)
Produit = Produit * R;
return Produit;
} /* fin fonction Puissance */
/* =============================================== */
/* PROGRAMME PRINCIPAL -- TEST DE : Puissance */
int main()
{
int N;
double X, Y;
cout << "entrez la valeur reelle: "; /* saisie */
cin >> X;
cout << "entrez l'exposant entier non negatif: ";
cin >> N;
Y = Puissance(X, N); /* traitement */
cout << Y << endl; /* affichage */
return 0;
}
Un appel de fonction peut également faire partie d'une expression, ou être utilisée comme paramètre effectif :
Exemple 4
Incorporer SinH dans un programme adéquat.
Incorporer Factorielle dans un programme adéquat.
#include <math.h>
/* exemple de fonction a un parametre d'entree : tangente */
double Tan(const double x)
{
return sin(x) / cos(x);
}
/* exemples d'appel : */
x = Tan(2.3);
if (Tan(Theta) < 1E-4) ...
cout << 1 + ( exp(Tan(Alpha + Beta)) / 2) );
sinh x =
ex-e-x
2
ì
ï
í
ï
î
0!
= 1
1!
= 1
"n > 1
n!
= 2 ×3 ×¼×n
Nous avons vu qu'une fonction retourne en résultat une valeur unique à chaque exécution. Que faire lorsque nous avons besoin de produire, dans un même sous-programme, plusieurs résultats différents ? Ou pas de résultat du tout ?
Les procédures répondent à ce besoin, en autorisant plusieurs genres de paramètres. Par contre, une procédure ne renvoie pas de valeur : tout se passe au niveau des paramètres. Une procédure se déclare grâce au mot-clé void (néant), qui précède le nom de la procédure lors de sa définition :
Exemple 5
void Affiche_N_LignesBlanches(const int N)
{
int I;
for ( I=0; I<N; I++ )
cout << endl;
}
En fait, C++ distingue seulement deux modes de transmission de paramètres :
Note : Le mode de passage de paramètre par référence est spécifique au langage C++, et n'existe pas en C (sauf implicitement pour les données de type tableau, comme nous le verrons plus loin). Pour ``simuler'' en C un passage de paramètre par référence, il faudra utiliser les pointeurs , notion qui sera abordée au chapitre 11 .
Pour les fonctions, nous n'avons envisagé que des paramètres d'entrée, passés par valeur. Cette convention est adoptée par la plupart des programmeurs.
Pour indiquer qu'un paramètre doit être passé par référence, il faut faire précéder son nom du caractère & et ne pas mettre le mot const . Par exemple :
Exemple 6
#include <iostream.h>
/* exemple de procedure avec un parametre mixte :
increment de N+1 unites
*/
void Incr(int & A, const int N)
{
A = A + N + 1;
}
/* exemple d'appel : */
int main()
{
int B;
B = 6;
Incr(B, 4);
cout << B << endl; /* que vaut B maintenant ? */
return 0;
}
Exemple 7
/* avec la definition de l'exemple precedent: */
Incr(4, 1); /* INTERDIT: 4 est une constante, pas une variable */
Incr(B+1, 3); /* INTERDIT: B+1 est une expression, pas une variable */
La forme générale d'une définition de procédure est la suivante :
void | NomProcédure (déclaration de paramètres formels) |
{ | |
Corps de la procédure | |
} |
Un appel de procédure est de la forme :
NomProcédure (liste de paramètres effectifs);
Une procédure peut ne pas posséder de paramètre :
Exemple 8
et qui possède trois paramètres de sortie : X1 et X2 de type
double,
représentant les racines réelles de l'équation si elles existent, et
SolutionDansR, un booléen vrai s'il existe des racines réelles
distinctes ou non, et faux sinon.
Définir un environnement de test pour cette procédure (programme
principal, éventuellement procédure auxiliaire pour le test),
ainsi qu'un jeu de valeurs permettant de tester tous les cas différents
pouvant se présenter.
#include <iostream.h>
/* exemple de procedure sans parametre : provoque un "beep" sonore */
void Beep()
{
cout << (char) 7;
}
/* appel : */
int main()
{
Beep();
return 0;
}
a x2 + b x + c = 0
La première règle de visibilité est : un objet (constante, variable, type, etc) ne peut être utilisé que dans le bloc (programme ou sous-programme) dans lequel il est déclaré.
Exemple 9
#include<iostream.h>
int F1()
{
int I; /* Variable LOCALE */
I = 1; /* I est VISIBLE dans le bloc */
return I; /* d'instructions de F1 */
}
void P1()
{
cout << I; /* ERREUR : I n'est pas visible HORS du bloc de F1 */
}
Exemple 10
#include<iostream.h>
const double PI = 3.14159;
double Perimetre (const double Rayon)
{
return 2 * PI * Rayon;
/* la constante PI est VISIBLE dans le bloc de Perimetre */
}
/* ... */
Les définitions globales de constantes peuvent se révéler indispensables lorsque des paramètres de sous-programmes utilisent ces définitions. L'exemple suivant utilise une constante dont le caractère global est impératif.
Exemple 11
#include<iostream.h>
const double PI = 3.14159265358979;
double perimetre( const double r )
{
return 2 * PI * r;
}
double surface( const double r )
{
return PI * r * r;
}
int main()
{
double rayon;
cout << "Quel rayon ? ";
cin >> rayon;
cout << "perimetre=" << perimetre( rayon ) << endl;
cout << "surface=" << surface( rayon ) << endl;
return 0;
}
Dans le cas de variables, il est fortement déconseillé -sauf cas très particuliers, voire exceptionnels- de profiter de cet effet de visibilité. Les variables globales doivent dans la mesure du possible être bannies.
Remarque : il n'est pas possible (contrairement à des langages comme Pascal ou Ada) d'imbriquer les sous-programmes, c'est-à-dire de définir un sous-programme localement à un autre sous-programme.
Il s'agit d'une déclaration se limitant à l'entête d'un sous-programme. Indispensables dans le cas d'utilisation de sous-programmes définis dans des fichiers compilés séparément, ainsi que pour des sous-programmes s'appelant mutuellement, les prototypes peuvent également être utilisés pour améliorer la lisibilité des programmes.
En introduisant au début les prototypes de toutes les fonctions et procédures définies par la suite, on donne en effet au lecteur une vue synthétique d'un ensemble de définitions de sous-programmes.
De plus, lorsque des prototypes ont été introduits, les définitions effectives peuvent se faire dans un ordre quelconque.
Exemple 12
#include<iostream.h>
/* DECLARATION DE FONCTION (PROTOTYPE) : */
double Puissance(const double x, const int n); /* x a la puissance n */
/* PROGRAMME PRINCIPAL : */
int main()
{ cout << Puissance(2.0, 10) << endl; return 0; }
/* DEFINITION DE FONCTION : */
double Puissance(const double x, const int n)
{
int i;
double r;
r = 1;
for(i = 0; i < n; i++) r = r * x;
return r;
}
Un sous-programme doit obéir aux règles suivantes:
Nous avons jusqu'à présent utilisé uniquement des données de type standard : int, double, char . Ces types de données modélisent des ensembles bien connus de nombres ou de symboles. Mais si une résistance électrique peut être modélisée par un double, ou la taille d'une mémoire par un int, comment modéliser dans un programme des objets plus complexes, comme une liste de noms, un ensemble d'ensembles, une matrice, un fichier de bulletins de paie ?
Pour cela, le programmeur doit définir de nouveaux types de données, simples ou structurés. Nous allons d'abord nous intéresser à la définition de types simples définis par synonymie ou énumération, et dans les chapitres suivants nous étudierons deux types structurés particulièrement importants : les tableaux et les structures hétérogènes.
Pour plus de clarté, il est possible de donner un nouveau nom à un type existant.
Exemple 1
Après avoir écrit typedef double Reel;
il est possible d'utiliser partout le type
Reel à la place du type double, moins explicite.
Exemple 2
Pour définir la notion d'entier naturel, il suffit décrire :
typedef unsigned int Naturel;
Supposez que vous deviez écrire un programme pour un distributeur automatique de jus de fruits. L'ensemble des fruits n'est pas un type prédéfini de C, comment faire ?
Une solution serait de décider d'un codage arbitraire des fruits par des entiers : orange = 1, citron = 2, etc. L'inconvénient est de contraindre le programmeur à rappeler par des commentaires la signification de chacun de ces codes, faute de quoi le programme deviendrait illisible.
De plus, le compilateur ne pourra faire aucune vérification quant à l'emploi de ces valeurs. Il sera donc possible d'exécuter, sans que cela soit sanctionné au niveau de la compilation, des instructions absurdes, comme l'addition d'un fruit et d'une somme d'argent.
C (ainsi que C++, Pascal, ADA, etc., mais pas Java) autorise la définition du type ``fruits'', par simple énumération des fruits qui nous intéressent :
Exemple 3
typedef enum {Orange, Citron, Pamplemousse, Tomate} Fruit;
/* definition de variables de type Fruit : */
Fruit F1, F2;
typedef enum { ENUMERATION DES ELEMENTS } NOMTYPE;
Les éléments d'un type énuméré sont représentés, en machine, par des entiers. Par défaut, le premier élément de la liste est codé par l'entier 0, le second par l'entier 1, etc.
Exemple 4
/* si le type bool n'existait pas (utile en C) */
typedef enum {false, true} bool; /* false = 0 true = 1 */
Exemple 5
typedef enum {ARRIERE = -1, ARRET = 0, AVANT = 1} Mouvement;
L'affectation d'une variable de type énuméré se fait selon les règles habituelles. Il est également possible de comparer des valeurs de type énuméré, d'incrémenter, de décrémenter une variable de type énuméré. Ces opérations portent en fait sur la représentation interne des symboles énumérés.
Si l'on affiche une valeur définie par énumération, c'est l'entier correspondant (la représentation interne du symbole) qui sera affichée.
Exemple 6
#include <iostream.h>
int main()
{
typedef enum {Orange, Citron, Pamplemousse, Tomate} Fruit;
Fruit Intrus;
int I;
Intrus = Tomate;
cout << " Les agrumes sont : " << endl;
for (I = (int)Orange; I <= (int)Pamplemousse; I++)
cout << (Fruit)I << endl; /* meme effet avec cout << I << endl; */
cout << " L'intrus est : " << endl;
cout << Intrus << endl;
return 0;
}
#include <iostream.h>
typedef enum
{
Cadillac, Rolls_Royce, Maserati, Toyota
} Marque;
void Appreciation(const Marque Auto)
{
switch (Auto)
{
case Cadillac: cout << "voyant" << endl; break;
case Rolls_Royce: cout << "confortable" << endl; break;
case Maserati: cout << "esthetique" << endl; break;
case Toyota: cout << "pas cher" << endl; break;
} /* switch */
}
Exemple 8
typedef enum {
Orange
, Citron, Pamplemousse, Tomate} Fruit;
typedef enum {Bleu, Vert, Rouge,
Orange
} Couleur; /* INTERDIT !!! */
Un des rôles du compilateur, et non des moindres, est de détecter autant que possible les erreurs, et d'aider le programmeur en les lui signalant. Ainsi, dans toute expression, affectation ou appel de sous-programme, il vérifie la cohérence entre les types des objets employés.
Définir des types différents pour modéliser des classes d'objets différentes, c'est donner au compilateur l'information nécessaire pour ce travail de détection d'erreurs.
Exemple 9
#include <iostream.h>
int main()
{
typedef enum Surface {Carre, Rectangle, Triangle, Disque};
typedef enum Volume {Cube, Parallelepipede, Pyramide, Sphere};
Volume Vol;
Surface Sur;
Sur = Disque; /* cette affectation est correcte */
Vol = Disque; /* ERREUR de type detectable par le compilateur */
return 0;
}
Un tableau est une collection ordonnée2 d'objets, en nombre fini et de même nature . On peut ainsi concevoir des tableaux d'entiers, de flottants, de caractères, mais aussi des tableaux de tableaux (tableaux à plusieurs dimensions) ou des tableaux contenant d'autres objets définis par le programmeur.
Chaque élément d'un tableau est repéré par sa position ou indice (qui est un nombre entier). Il est possible grâce à cet indice d'aller modifier ou consulter n'importe quel élément d'un tableau.
En C et en C++, l'indice du premier élément d'un tableau est toujours 0.
La forme générale d'une définition de type tableau est la suivante :
typedef TYPE_DES_COMPOSANTES NOM_DU_TYPE [NOMBRE_D_ELEMENTS];
Exemple 1
#include <iostream.h>
int main()
{
const int NbElements = 10;
typedef int TypeTableau[NbElements];
/* definit le type: tableau de 10 entiers */
int I;
TypeTableau UnTableau;
/* definit une variable de type TypeTableau */
for (I = 0; I < NbElements; I++) UnTableau[I] = 0;
/* range la valeur 0 dans toutes les "cases" du tableau */
for (I = 0; I < NbElements; I++)
if (UnTableau[I] != 0)
cout << "il y a un bug!" << endl;
/* teste si tous les elements sont bien a 0 */
return 0;
}
Les composantes d'un tableau peuvent être de n'importe quel type (standard ou défini par le programmeur).
Exemple 2
/* definit le type Mot: tableau de 20 caracteres */
typedef char Mot[20];
/* definit le type ListeFruits: tableau de 10 Fruits */
const int NombreFruits = 10;
typedef enum {Orange, Citron, Pamplemousse, Tomate} Fruit;
typedef Fruit ListeFruits[NombreFruits];
L'exemple 9.1 montrait un type de tableau à une dimension (un vecteur). Un tableau à deux dimensions peut se concevoir comme un tableau d'objets de type vecteur :
Exemple 3
const int NbLignes = 3;
const int NbColonnes = 3;
typedef double VecteurLigne[NbColonnes];
typedef VecteurLigne Matrice[NbLignes];
Exemple 4
const int NbLignes = 3;
const int NbColonnes = 3;
typedef double Matrice[NbLignes][NbColonnes];
Selon le même principe, on peut concevoir des tableaux à N dimensions. L'exemple suivant définit et commente les déclarations de types et de variables nécessaires pour stocker en mémoire une séquence de 25 images, chaque image étant composée de trois plans correspondant aux composantes rouge, verte et bleue, et chaque plan comportant 600 lignes de 800 pixels :
Exemple 5
OU (beaucoup mieux) :
typedef int Sequence[25][3][600][800];
Sequence s1, s2;
const int NBCOLONNES = 800;
const int NBLIGNES = 600;
const int NBCOULEURS = 3;
const int NBIMAGES = 25;
typedef int Pixel;
typedef Pixel Ligne[NBCOLONNES];
typedef Ligne Plan[NBLIGNES];
typedef Plan Image[NBCOULEURS];
typedef Image Sequence[NBIMAGES];
Sequence s1, s2;
C++ type Commentaire s1 Sequence Séquence s1 s1[24] Image 25ème image de s1 s1[24][0] Plan plan Rouge de la 25ème image de s1 s1[24][0][300] Ligne 301ème ligne du plan Rouge de la ¼ s1[24][0][300][10] Pixel 11ème pixel de la 301ème ligne du ¼
En C comme en C++, le passage d'un tableau en paramètre de sous-programme est toujours effectué par adresse, jamais par valeur.
En résumé :
valeur (entrée) | référence (sortie ou mixte) | |
int (idem double, char ¼) | const int x | int & x |
tableau | const TypTab t | TypTab t |
Remarque : Nous verrons au chapitre 11.8 p.pageref que le passage d'un tableau en entrée ne se fait pas vraiment par valeur puisque toutes les valeurs du tableau ne sont pas recopiées à cette occasion.
Soit une variable Tab de type TypeTableau définie par :
const int NbElements = 100; typedef int TypeTableau[NbElements]; TypeTableau Tab;Pour faire référence, par exemple, au 13ème élément de TAB, on écrit :
Tab[12]car la numérotation des éléments d'un tableau commence à 0. Le premier élément d'un tableau est donc toujours Tab[0] .
La syntaxe reste la même, qu'il s'agisse de modifier ou de lire le contenu de l'élément.
Il est possible d'employer une expression quelconque pour désigner l'indice de l'élément à accéder :
Exemple 6
const int NbElem = 8;
typedef double Vecteur[NbElem];
void ExempleAcces(Vecteur V)
{
int J;
J = NbElem - 1; /* indice du dernier element */
V[ J ] = 0.0;
V[ J - 1 ] = V[ J ];
V[ (J + 1) / 2 ] = V[ J - 1 ];
/* . . . */
}
Exemple 7
const int NbLignes = 3;
const int NbColonnes = 3;
typedef double Matrice[NbLignes][NbColonnes];
void ExempleAccesMatrice(Matrice M)
{
int I, J;
I = 0;
J = 0;
M[ I ][ J ] = 1;
/* . . . */
}
Le type ``tableau de caractères'' est très utilisé pour représenter les textes, mots et chaînes de caractères manipulées dans les programmes.
Dans ce type d'application, il est courant de réserver un tableau de taille suffisante, pour pouvoir y ranger lors de l'exécution des chaînes de caractères de longueurs diverses.
Pour indiquer la fin d'une chaîne de caractères, on emploie le caractère nul, celui dont le code ASCII est 0, et que l'on désigne par '\0' .
Note : pour les chaînes de caractères constantes (entre guillemets), le caractère nul final est rajouté automatiquement par le compilateur.
Une bibliothèque nommée string contient des fonctions et procédures usuelles de manipulation de chaînes de caractères. Pour les utiliser dans un programme, il faut placer dans l'entête la directive :
#include <string.h>
On peut par exemple utiliser le sous-programme strcpy (string copy ) pour initialiser une variable de type chaîne de caractères :
Exemple 8
#include <iostream.h>
#include <string.h>
int main()
{
const int TAILLE = 10;
typedef char Chaine[TAILLE];
Chaine Mot;
strcpy(Mot, "ok");
cout << Mot << endl; /* resultat: ok */
return 0;
}
Il faut effectuer un ordre de lecture ou d'écriture pour chaque composante du tableau :
Exemple 9
#include <iostream.h>
const int TAILLE = 3;
typedef double Matrice[TAILLE][TAILLE];
void AcquisMatrice(Matrice M)
{
int I, J;
/* acquisition des coefficients d'une matrice 3 x 3 */
for (I = 0; I < 3; I++)
{
for (J = 0; J < 3; J++)
{
cout << "M[" << I << "," << J << "] ?";
cin >> M[I][J];
}
}
/* . . . */
}
Le cas des tableaux de caractères est particulier, il est possible de lire ou d'afficher leur contenu par un seul ordre :
Exemple 10
#include <iostream.h>
const int TailleChaine = 128;
typedef char Chaine[TailleChaine];
void SaisieMot(Chaine Mot)
{
Mot[0] = '\0';
while (Mot[0] != 'q')
{
cin.getline(Mot, TailleChaine);
cout << Mot << endl;
}
}
Si l'utilisateur du programme tape au clavier (strictement) moins de TailleChaine caractères avant le caractère de validation (\n), ceux-ci seront rangés dans le tableau Mot et un caractère nul (\0) sera introduit dans le tableau à la suite du dernier caractère saisi.
Si l'utilisateur du programme tape au clavier plus de TailleChaine - 1 caractères avant le caractère de validation (\n), les caractères en ``trop'' seront conservés dans le tampon d'entrée jusqu'au prochain appel à cin.
Si l'utilisateur du programme tape au clavier exactement TailleChaine - 1 caractères, le programme ne s'arrêtera pas sur la prochaine instruction getline car il considèrera le caractère \n de la première saisie comme une chaîne vide. Pour éviter cet inconvénient, il faut ajouter l'instruction cin.sync(); après chaque getline, ce qui vide le buffer avant la prochaine saisie.
Les langages C et C++ sont très permissifs et, hélas, ne contrôlent pas que toute instruction accède bien à une case autorisée dans un tableau.
Par exemple, a = t[-1]; ou a = t[10]; pour un tableau t de 10 cases sont deux instructions autorisées, alors qu'elles accèdent à la case précédant (respectivement suivant) les 10 cases réservées pour le tableau t.
Ces instructions ont apparemment peu de conséquences. En fait, la plupart du temps, le programme donne certains résultats faux, ce qui est d'autant plus dangereux qu'il est possible de ne pas s'en apercevoir.
Par contre, une instruction du type t[-1] = n; fait presque toujours ``planter'' le programme car elle peut écraser n'importe qu'elle donnée, mais aussi des instructions, ce qui donne un comportement du programme complètement imprévisible avant le ``plantage''.
Ceci est la cause d'erreur la plus longue et la plus difficile à découvrir, notamment car le plantage du programme ne survient pas à l'endroit où se situe l'erreur, mais plus tard, selon ce qui a été écrasé. D'autre part, le simple fait d'ajouter un affichage pour déboguer, modifie le comportement du programme, et celui-ci se plante à un autre endroit, voire ne se plante plus !
Il est donc impératif de bien vérifier tous les indices (notamment dans les boucles), ainsi que les tailles de tableau (notamment dans les entrées/sorties de chaînes de caractères).
Après avoir passé une journée entière à essayer de trouver
où se situe le dépassement d'indice, il peut être utile de
lire la page :
http://www.esiee.fr/~bureaud/fi/unites../debugtab.html .
Y est décrit une méthode qui, moyennant la modification de toutes les déclarations de tableaux, permet généralement de trouver la cause de l'erreur.
Une fois l'erreur corrigée, il est ensuite possible, soit de désactiver l'ensemble du système de détection d'erreur par un simple #define, soit de re-modifier toutes les déclarations pour qu'elles retrouvent leur forme habituelle.
Cette méthode peut paraître lourde, mais elle ne prend que quelques minutes, alors que la recherche de telles erreurs dans un programme de plusieurs centaines de lignes peut prendre plusieurs jours ...
Exercice 1
Écrire un programme qui lit huit entiers et qui les affiche dans l'ordre
inverse de l'ordre d'entrée. On utilisera une boucle pour la lecture et une
autre boucle pour l'écriture.
Écrire un sous-programme qui vérifie si un mot ou une phrase
est un palindrome.
Le principe de la méthode est de ``marquer'' successivement
les multiples de 2, 3, ¼ qui ne sont donc pas premiers.
A la fin (comment la determiner ?),
les nombres non marqués sont premiers.
Exemple, avec décisions à chaque étape
(V pour ``est premier'', F pour ``n'est pas premier'') :
ARA
KAYAK
QSDFGHHGFDSQ
ELU PAR CETTE CRAPULE
ESOPE RESTE ICI ET SE REPOSE
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 ...
v f f f f f f f f f f f
v f f f f f f f
v f f f f
v f f
Exercice 9
Écrire un programme qui affiche les dix premières lignes du
triangle de Pascal. (Rappel : le triangle de Pascal regroupe
les coefficients binomiaux Cnp, et se construit facilement
grâce à la relation : Cnp = Cn-1p-1 + Cn-1p)
Nous avons étudié dans le paragraphe précédent le type tableau . Dans un tableau, tous les éléments sont de même type.
La structure hétérogène (struct) permet de regrouper des éléments hétérogènes, c'est-à-dire de types différents.
Cette structure permet en outre de nommer chacune de ses composantes, contrairement au tableau dans lequel chaque élément est repéré par un indice .
Considérons par exemple un logiciel de gestion de stock pour une épicerie. Pour chaque article, on tient à jour les informations suivantes :
Manifestement, toutes ces informations ne sont pas de type simple. La désignation (le nom de l'article) est une chaîne de caractères, et nécessite donc une définition du type :
const int MaxNom = 80; typedef char TypNom[MaxNom];
De même, la date de péremption de l'article peut être représentée par une chaîne de la forme ``01/12/92'', et donc être définie par :
typedef char TypDate1 [9]; /* 9 = 8 pour la date + 1 pour le \0 */mais cette représentation rend difficile la comparaison de dates. On peut donc lui préférer la représentation suivante, sous forme de structure, en utilisant le constructeur struct :
typedef struct { int Jour; int Mois; int Annee; } TypDate;
Nous pouvons désormais définir la structure de donnée ``Article'', elle aussi construite à partir de types standard ou définis préalablement, à l'aide du constructeur struct.
typedef struct { TypNom Nom; double PrixUnitaire; int Disponible; TypDate DatePeremption; int DelaiReapp; } Article;
Une composante d'une structure est aussi appelée un champ .
La forme générale d'une définition de type struct est la suivante :
typedef struct | ||
{ | ||
TypeChamp1 | NomChamp1; | |
TypeChamp2 | NomChamp2; | |
¼ | ||
TypeChampN | NomChampN; | |
} NOM_DU_TYPE; |
Grâce aux définitions ci-dessus, définissons une variable de type ``Article'' :
Article A1;
Pour faire référence (en lecture comme en écriture) au prix unitaire de cet article, on écrira :
A1.PrixUnitaire
La forme générale d'un accès à une composante de structure est :
Structure.NomComposante
Cette notation s'étend naturellement au cas des structures emboîtées. Ainsi pour désigner le mois de péremption d'un article a1, on écrira :
A1.DatePeremption.Mois
Voici, pour illustrer la notion d'enregistrement, la définition d'une fonction permettant de comparer deux dates (de type TypDate) :
Exemple 1
/* cette fonction renvoie true si la date d1 <= d2, false sinon */
bool Precede(const TypDate D1, const TypDate D2)
{
bool res;
if (D1.Annee < D2.Annee) res = true;
else if (D1.Annee > D2.Annee) res = false;
else /* d1.Annee == d2.Annee */
if (D1.Mois < D2.Mois) res = true;
else if (D1.Mois > D2.Mois) res = false;
else /* D1.Mois == D2.Mois */
if (D1.Jour <= D2.Jour) res = true;
else /* d1.Jour > d2.Jour */ res = false;
return res;
}
Les règles sont les mêmes que pour les types de base (passage par valeur avec const, ou par adresse avec &).
Exercice 1
Écrire en C++ les déclarations nécessaires pour représenter une
personne dans un agenda personnel, avec notamment son nom, son prénom,
son adresse (rue, code postal, ville), son numéro de téléphone, etc.
Nous avons vu que les objets manipulés dans les programmes sous forme de variables, qu'ils soient de type simple (int, double ...) ou composé (tableau, structure) correspondent à des emplacements réservés dans la mémoire de l'ordinateur.
La mémoire centrale des ordinateurs que nous utilisons (stations de travail, PC¼) est découpée logiquement en octets (mots de 8 bits). Chacun de ces octets est numéroté, autrement dit, il possède une adresse unique, un nombre entier qui sert à l'identifier.
Chaque octet de mémoire peut stocker une donnée de type caractère (char). Pour d'autres types de données comme les entiers, plusieurs octets consécutifs doivent être utilisés (les entiers de type int, dans notre environnement de programmation, sont stockés sur quatre octets). L'adresse de la zone de mémoire correspondant à un objet de type entier est par convention l'adresse du premier octet, dans l'ordre des adresses croissantes, de cette zone.
Pour manipuler les adresses, nous avons besoin d'un nouveau type de données : le type pointeur . En fait, un pointeur est simplement une adresse, donc un entier ; mais pour des raisons de sécurité, on prend soin de distinguer pointeurs et entiers (un pointeur n'est donc pas un int), ainsi que les différents types de pointeurs entre eux. Ainsi, un pointeur de flottant en double précision (adresse d'un objet de type double) et un pointeur de caractère (adresse d'un objet de type char) sont de types différents.
Voici, en C++, la définition du type ``pointeur de flottant'', autrement dit, adresse d'un objet de type double :
Exemple 1
typedef double *PtrDouble; /* declaration de type */
Puisque nous connaissons un nouveau type de données, il est naturel de pouvoir déclarer des variables de ce type, c'est-à-dire des variables pour stocker des pointeurs (ou adresses) :
Exemple 2
PtrDouble P1, P2; /* declaration de variables */
Vous trouverez dans de nombreux programmes des définitions de variables de type pointeur non précédées de la définition du type pointeur correspondant. Cela peut alléger l'écriture dans le cas de pointeurs sur des objets de type simple (int, char, double, ¼) mais cette pratique est déconseillée s'il s'agit d'objets structurés.
Exemple 3
double *P1, *P2; /* declaration de variables */
Soit V une variable (par exemple de type entier), nous pouvons récupérer son adresse grâce à l'opérateur & ; l'expression &V désigne en effet cette adresse.
Exemple 4
int main()
{
typedef int *PtrEntier;
PtrEntier Pe
int Entier;
Pe = &Entier;
return 0;
}
La connaissance de l'adresse d'un objet nous permet d'accéder au contenu (à la valeur) de cet objet, par l'usage de l'opérateur * :
Exemple 5
#include <iostream.h>
int main()
{
typedef int * PtrEntier;
PtrEntier Pe;
int Entier;
Entier = 17;
Pe = &Entier; /* Pe contient l'adresse de la variable Entier */
cout << "Entier contient: " << *Pe << endl; /* affiche 17 */
*Pe = *Pe + 1; /* incremente la valeur contenue dans Entier */
cout << "Entier contient: " << Entier << endl; /* affiche 18 */
return 0;
}
Attention : Une erreur courante consiste à utiliser *Pe avant que la variable Pe n'ait été initialisée. La variable Pe peut alors contenir une adresse quelconque ; le comportement du programme n'est donc pas prévisible (plantage, résultats incohérents, etc).
Le pointeur NULL est une adresse fictive ne correspondant à aucune zone de mémoire accessible. Dans la plupart des systèmes, il vaut 0. C'est une constante définie notamment dans le fichier iostream.h .
Il est utilisé pour signaler une erreur lors de l'emploi d'une fonction devant retourner un pointeur, ou pour symboliser un ``objet vide'' dans le cas d'objets complexes chaînés au moyen de pointeurs (voir section 11.10 ).
Il me paraît indispensable de faire ici quelques remarques importantes, en ce sens qu'elles vous permettront (peut-être) d'éviter des erreurs douloureuses, ou du moins de les corriger plus vite :
A quoi s'expose-t-on dans le cas contraire ?
A une erreur lors de l'exécution du programme provoquant son arrêt complet et l'affichage d'un message du type bus error-core dumped ou segmentation fault.
Comment allouer de la mémoire ?
C'est ce que nous allons voir tout de suite.
La première façon d'allouer de la place mémoire pour y stocker des objets vous est déjà connue : c'est la déclaration de variables. Les adresses de ces objets peuvent alors être récupérées grâce à l'opérateur & déjà présenté.
La seconde façon, dite allocation dynamique, consiste à faire exécuter dans le programme une opération d'allocation nommée new qui retourne comme résultat :
L'opérateur new prend pour opérande droit le type de l'objet à allouer, ce qui définit la taille de la zone de mémoire à réserver pour stocker cet objet.
Exemple 6
#include <iostream.h>
int main()
{
typedef int * PtrEntier;
PtrEntier Pe;
Pe = new int;
if (Pe == NULL)
cout << "allocation d'un entier ratee" << endl;
else
{
cout << "allocation d'un entier reussie" << endl;
/* REMARQUE : a ce niveau, la place memoire reservee
a un contenu indetermine. Pour y stocker une valeur
il faut executer, par exemple: */
*Pe = 13;
}
return 0;
}
Pour déclarer une variable de type tableau, on a vu que la dimension du tableau devait être spécifiée par une constante. La seule façon d'allouer un tableau dont on ne connaît pas à l'avance la dimension est donc l'allocation dynamique.
L'allocation dynamique de tableau peut se faire simplement par l'opération :
pointeur_type_objet = new nom_type_objet[taille_tableau];La section 11.8 précise les rapports existant entre pointeurs et tableaux.
Lorsqu'une zone mémoire allouée dynamiquement (par new) n'est plus utilisée par le programme, il est possible, et même souvent souhaitable, de la libérer, c'est-à-dire de la rendre à nouveau disponible pour d'autres utilisations, grâce à l'opérateur delete :
Exemple 7
int main()
{
typedef int * PtrEntier;
PtrEntier Pe;
Pe = new int;
/* ... */
delete Pe;
return 0;
}
La libération de la mémoire occupée par un tableau alloué dynamiquement s'effectue par un seul delete :
Exemple 8
int main()
{
int taille;
typedef int * PtrEntier;
PtrEntier Pe;
cout << "Quelle taille ? ";
cin >> taille;
Pe = new int[taille]; // taille est une variable !
/* ... */
delete Pe; /* libere en une fois le tableau */
return 0;
}
Les noms de variables de type tableau ont un statut particulier en C++ : ils représentent l'adresse du premier élément du tableau, et ce sans qu'il soit besoin d'avoir recours à l'opérateur & .
Remarque : on peut déduire de ce qui précède que les expressions Mot (dans l'exemple ci-dessous) et &(Mot[0]) sont équivalentes.
Il est donc possible d'utiliser le nom d'une variable de type tableau exactement comme une constante de type pointeur , comme le montre l'exemple suivant :
Exemple 9
const int TailleChaineCar = 80;
typedef char ChaineCar[TailleChaineCar];
void Change_aA(ChaineCar Mot)
{
typedef char *PtrChar;
PtrChar P;
P = Mot; /* utilise l'equivalence tableau - pointeur */
while (*P != '\0')
{
if (*P == 'a')
*P = 'A'; /* change les a minuscules en A majuscules */
P++;
}
}
Par exemple, incrémenter d'une unité une variable de type pointeur de flottant conduit à rajouter à l'adresse contenue dans la variable le nombre d'octets composant un flottant (huit pour le type double dans notre environnement). Cette opération peut donc être vue logiquement comme le ``passage au flottant suivant''.
Autre exemple : si le pointeur P est un pointeur de flottant et contient l'adresse 197568, après l'exécution de l'instruction P = P - 2; P contiendra l'adresse 197552.
Note : dans l'exemple précédent, le paramètre Mot ne doit pas, logiquement, être passé par référence, car il représente l'adresse du premier élément du tableau ; or cette adresse ne doit pas être modifiée par la fonction.
Il est possible, avec les opérateurs et les constructions que nous venons d'étudier, de définir et de manipuler des pointeurs de type structure (struct), en allouant les structures soit dynamiquement, soit sous forme de variables.
Voici un exemple simple :
Exemple 10
typedef struct
{
double Reel;
double Imag;
} Complexe;
typedef Complexe *PtrComplexe;
PtrComplexe AlloueComplexeNul()
{
PtrComplexe P;
P = new Complexe;
(*P).Reel = 0.0;
(*P).Imag = 0.0;
return P;
}
(*P).Imag est équivalent à : P->Imag
(*((*p).ptr1)).ptr2 est équivalent à : p->ptr1->ptr2
etc.
Une utilisation particulièrement intéressante des pointeurs associés aux structures est la définition de structures dites chaînées , dont l'exemple le plus simple est la liste chaînée. On pourra généraliser cet exemple aux arbres et aux graphes.
L'idée est de réserver un champ dans chaque cellule de liste pour pointer sur la cellule suivante de la liste.
Une liste peut ainsi être représentée uniquement par le pointeur sur sa première cellule.
La liste vide, ou la fin de liste, sera représentée par le pointeur NULL.
Voici les déclarations de type nécessaires à la définition d'une structure de liste de flottants en double précision (double) :
Exemple 11
typedef struct TmpCellule
{
double Element;
TmpCellule *Suivant;
} Cellule;
typedef Cellule *PtrCellule;
typedef PtrCellule Liste;
Noter également que, comme le type Cellule n'a pas à cet endroit reçu de définition complète, il faut dans la définition du champ Suivant employer la syntaxe :
nom_provisoire * nom_champ;
Il est également possible d'écrire : struct nom_provisoire * nom_champ;
Le lecteur attentif aura certainement relevé la redondance des types PtrCellule et Liste, qui sont en effet strictement équivalents. Cette redondance est volontaire, elle permet d'améliorer la lisibilité en distinguant les pointeurs de cellule (utilisés pour pointer une cellule isolée ou une cellule particulière, par exemple lors d'une construction ou d'un parcours de liste) et les pointeurs de liste, qui, pointant effectivement sur la première cellule de la liste, permettent indirectement l'accès à la liste entière.
Voici pour terminer un exemple simplifié de construction et d'affichage d'une liste composée de deux cellules. Il ne sera pas tenu compte de la possibilité d'erreur lors de l'allocation (voir exemple 11.6 ) ni de la libération de mémoire (voir section 11.7 ).
Exemple 12
#include <iostream.h>
typedef struct TmpCellule
{
double Element;
TmpCellule *Suivant;
} Cellule;
typedef Cellule *PtrCellule;
typedef PtrCellule Liste;
Liste CreeListeExemple()
{
Liste L;
PtrCellule Temp;
L = NULL; /* liste vide */
/* ajout future 2eme cellule */
Temp = new Cellule;
Temp->Element = 2.0;
Temp->Suivant = L;
L = Temp;
/* ajout 1ere cellule en debut de liste */
Temp = new Cellule;
Temp->Element = 1.0;
Temp->Suivant = L;
L = Temp;
return L;
}
void AfficheListe(Liste L)
{
PtrCellule Temp;
for (Temp = L; Temp != NULL; Temp = Temp->Suivant)
cout << Temp->Element << endl;
}
int main()
{
AfficheListe(CreeListeExemple());
return 0;
}
Ce type de diagramme est très utilisé dans la littérature informatique pour visualiser les structures chaînées.
Exercice 2
Écrire des fonctions informatives sur une liste qui répondent à des
questions telles que :
Ce chapitre a pour but d'introduire les notions de base pour pouvoir lire et écrire des données simples dans des fichiers.
Ces fichiers sont appelés ``fichiers de texte'' car ils sont éditables à l'aide d'un simple éditeur de texte.
D'autre part, l'opération de lecture dans un fichier de texte est équivalente à celle de lecture au clavier ; de même, celle d'écriture dans un fichier de texte est équivalente à celle d'affichage à l'écran.
Cela signifie que, sous réserve d'avoir au préalable déclaré une ``variable fichier'', il suffira de remplacer cin ou cout par cette variable.
Pour utiliser les fichiers en C++, il est nécessaire d'inclure le fichier <fstream.h> .
Il est à noter qu'une telle déclaration provoque également l'ouverture du fichier, et qu'une instruction du type open() n'est donc pas nécessaire.
Il est par contre possible de tester si l'ouverture s'est effectuée
correctement par l'instruction suivante :
if ( Var_fich.fail() ) cout << "Je n'ai pas pu ouvrir ..." << endl;
Attention !
Pour obtenir le comportement décrit ci-dessus pour ifstream
avec le compilateur MicroSoft Visual C++, il faut écrire :
ifstream Var_fich( Nom_fich, ios::nocreate );
Exemple 1
Soit la déclaration : ifstream F_entree( "entiers.dat" );
en supposant que le fichier ëntiers.dat" existe bien.
Exemple 2
Soit la déclaration : ofstream F_sortie( "entiers.dat" );
Si le fichier ëntiers.dat" existe déjà, il est écrasé, sinon il est
créé.
F_sortie << Tab;
F_sortie << Tab << endl;
(Þ prochaine écriture à la ligne suivante)
La fermeture d'un fichier s'effectue automatiquement à la sortie du bloc d'instructions { } dans lequel la ``variable fichier'' est déclarée.
Toutefois, si pour verrouiller le fichier le moins longtemps possible vis à vis des autres utilisateurs il est souhaité une fermeture plus tôt, il est possible de l'effectuer par l'instruction : F_entree.close(); ou F_sortie.close(); .
Les paramètres de type ifstream ou ofstream doivent obligatoirement être passés par référence.
Exemple 3
#include <iostream.h>
#include <fstream.h>
const int MAX=80;
typedef char Chaine[MAX];
void affiche( ifstream & );
int main()
{
Chaine nom;
cout << "Quel fichier ? ";
cin >> nom;
ifstream fe( nom, ios::nocreate );
affiche( fe );
return 0;
} // main()
void affiche( ifstream & f )
{
Chaine tmp;
while ( ! f.eof() )
{
f.getline( tmp, MAX );
cout << tmp << endl;
}
} // affiche()
Ce chapitre n'a fait qu'effleurer les possibilités de traitement de fichiers, mais l'indispensable est là.
De nombreuses autres fonctions sont disponibles pour accéder aux fichiers de texte, et d'autres sortes de fichiers (non visualisables par un éditeur de texte) existent également.
Ce chapitre a pour but de vous montrer, au travers d'exemples simples, comment transformer en C-ANSI un programme écrit dans le sous-ensemble du C++ présenté dans ce document.
Remplacer #include <iostream.h> par #include <stdio.h>
int : | remplacer cout << I << ',' << J << endl; |
par printf( "%d,%d\n", I, J ); | |
double : | remplacer cout << D << endl; par printf( "%lf\n", D ); |
char[ ] : | remplacer cout << mot << endl; par printf( "%s\n", mot ); |
int : | remplacer cin >> I >> J; par scanf( "%d %d", &I, &J ); |
double : | remplacer cin >> D; par scanf( "%lf", &D ); |
char[ ] : | remplacer cin >> mot; par scanf( "%s", mot ); |
char[ ] : | remplacer cin.getline( ligne, taille ); |
par fgets( ligne, taille, stdin ); mais le '\n' est conservé ! |
Le type bool n'existe pas. Mais on peut le définir comme au paragraphe 8.2.1 .
(voir section 7.5 ).
Prenons l'exemple d'un programme C++ échangeant le contenu de deux variables :
Exemple 1
#include <iostream.h>
void Echange( int &A, int &B )
{
int T;
T = A;
A = B;
B = T;
}
int main()
{
int X, Y;
X = 4;
Y = 128;
Echange( X, Y );
cout << "X=" << X << endl; /* affiche 128 */
cout << "Y=" << Y << endl; /* affiche 4 */
return 0;
}
Exemple 2
#include <stdio.h>
void Echange( int *A, int *B ) /* des adresses d'entier seront
passees explicitement en parametre */
{
int T;
T = *A; /* il faut acceder aux objets */
*A = *B; /* dont les adresses sont dans */
*B = T; /* les parametres A et B */
}
int main()
{
int X, Y;
X = 4;
Y = 128;
Echange( &X, &Y ); /* adresses des variables X et Y explicitement
passees en parametres */
printf( "X=%d\n", X ); /* affiche 128 */
printf( "Y=%d\n", Y ); /* affiche 4 */
return 0;
}
Les exemples ci-dessous font référence au chapitre 12 .
Il n'y a pas d'équivalent à <fstream.h> ; le fichier <stdio.h> suffit.
Il est à noter d'une part, qu'il est possible de déclarer Var_fich au début du programme, puis d'effectuer l'ouverture plus tard ; d'autre part, que le mot-clé FILE est une exception puisqu'il doit obligatoirement être écrit en majuscules.
Soit la déclaration FILE * F_entree; et l'ouverture
F_entree = fopen( "entiers.dat", "r" ); .
Soit la déclaration FILE * F_sortie; et l'ouverture
F_sortie = fopen( "entiers.dat", "w" ); .
Elle n'est pas automatique. Remplacer F_entree.close(); et F_sortie.close(); par fclose( F_entree ); et fclose( F_sortie ); .
Les deux seules valeurs possibles sont Vrai et Faux (ou bien V et F dans les tables de vérité, true et false en C++ et en Java, 1 et 0 en C, T et F en Fortran, etc...).
Les opérateurs booléens habituels sont : NON, ET, OU. Les opérateurs de comparaison fournissent des valeurs booléennes.
Exemple d'expression booléenne : NON ( 'a' <= carac ET carac <= 'z' )
|
|
|
Voir notation section 2.4 .
En C++, l'évaluation d'une expression booléenne s'effectue de gauche à droite, et s'arrête dès que la valeur de l'expression globale peut être déterminée.
Exemples :
Exercice 1
L'opérateur XOR n'existe pas en C++, mais il est utilisé en électronique.
Il se traduit par OU exclusif et signifie ``soit l'un, soit l'autre, mais pas les deux'', par
opposition au OU (inclusif) qui signifie ``soit l'un, soit l'autre, mais éventuellement
les deux à la fois''.
Écrivez la table de vérité de l'opérateur XOR.
Il peut être créé par une combinaison d'opérateurs NON, ET, OU. Laquelle ?
Il est très facile à créer puisque A NAND B = NON( A ET B ).
Écrivez la table de vérité de l'opérateur NAND.
Tous les opérateurs logiques peuvent être recrées à l'aide uniquement de
combinaisons d'opérateurs NAND.
1American National Standard Institute
2l'ordre porte sur le
rang des objets, non sur leur valeur