INITIATION
A LA PROGRAMMATION STRUCTURÉE
AVEC LE LANGAGE C++

Denis Bureau et Michel Couprie.

© Groupe E.S.I.E.E. 2002
Composé avec LATEX
( version 2001 )
( version 2000 )
( version 1999 )

Table des matières

1  Les ``briques de base'' de la construction de programmes
    1.1  Un programme très simple
    1.2  Commentaires
    1.3  Identificateurs
    1.4  Affichage de messages
    1.5  Notion de type de données
    1.6  Constantes
    1.7  Variables
    1.8  Place des déclarations
    1.9  Affichage de valeurs
    1.10  Acquisition de valeurs numériques
2  Types de données
    2.1  Type int
        2.1.1  Représentation des valeurs
        2.1.2  Opérateurs arithmétiques
        2.1.3  Opérateurs relationnels
        2.1.4  Fonctions mathématiques
    2.2  Type double
        2.2.1  Représentation
        2.2.2  Opérateurs arithmétiques
        2.2.3  Opérateurs relationnels
        2.2.4  Fonctions mathématiques
    2.3  Type char
        2.3.1  Représentation des valeurs
        2.3.2  Opérateurs arithmétiques
        2.3.3  Opérateurs relationnels
        2.3.4  Fonctions usuelles
    2.4  Type bool
        2.4.1  Opérateurs logiques
    2.5  Conversions de type
    2.6  Représentation interne
3  Expressions arithmétiques et logiques
4  Instructions, Structure séquentielle
    4.1  Affectation
    4.2  Instruction composée

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

Avertissement

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.

Introduction

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é).

1  Les ``briques de base'' de la construction de programmes

1.1  Un programme très simple

Exemple 1  

#include <iostream.h>

int main()
  {
  cout << "bienvenue a l'ESIEE" << endl;
  return 0;
  }

Ce premier exemple est l'un des plus courts que l'on puisse concevoir, sa seule fonction étant d'afficher un message (bienvenue à l'ESIEE ) sur l'écran de l'ordinateur.

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.

1.2  Commentaires

Exemple 2  

/* Ce programme resoud les systemes lineaires
   par la methode de Gauss */
#include <iostream.h>
int main() 
  {
  ...

Sur la première ligne de l'exemple ci-dessus, on trouve un texte délimité par les signes ``/*'' au début et ``*/'' à la fin. C'est un commentaire . Facultatifs, les commentaires servent à introduire en marge d'un programme des explications favorisant la lisibilité, mais qui n'interviennent pas dans le programme lui-même. Ils peuvent être introduits à n'importe quel endroit dans le programme (sauf à l'intérieur d'un mot ou d'un symbole), pour éclairer le sens de définitions, de modules, d'instructions, etc.

1.3  Identificateurs

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;
  }

Un identificateur se compose de caractères alphanumériques (lettres et chiffres). Le signe souligné ``_'' (underscore) peut également être utilisé dans un identificateur. Un identificateur débute obligatoirement par une lettre ou par un ``_'' .

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.

1.4  Affichage de messages

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

Pour introduire un saut de ligne, on peut inclure dans la chaîne de caractères la séquence \n . Ainsi, l'instruction d'affichage :

   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)

1.5  Notion de type de données

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.

1.6  Constantes

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 cet exemple, les mots : double et int définissent le type de la constante.

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;

Autre exemple :

Exemple 6  

const int COTE_CARRE = 5;
const int SURFACE_CARRE = COTE_CARRE * COTE_CARRE;
const int PERIMETRE_CARRE = COTE_CARRE * 4;

Dans cette exemple, il suffit de changer la valeur de la constante COTE_CARRE pour que les valeurs des deux autres constantes soient recalculées en conséquence.

1.7  Variables

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;

1.8  Place des déclarations

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 ).

1.9  Affichage de valeurs

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;

1.10  Acquisition de valeurs numériques

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;

En exécutant ces deux instructions, le programme s'interrompt après avoir affiché le message ``donnez la valeur du rayon :'' . Il attend que l'utilisateur tape un nombre au clavier (par exemple : 2.45) et qu'il valide l'opération grâce à la touche ``return''. La valeur saisie est alors stockée dans la variable Rayon .

Remarque : le sens de circulation des données permet de se souvenir s'il faut employer << ou >>  .

2  Types de données

É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;
  }

Uu seul type de données apparaît ici, c'est le type double (nombre à virgule flottante en double précision). Les deux variables définies Rayon et Surface sont de ce type.

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 :


1.234567 = 123.4567E-2 = 0.001234567E3

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


2.1  Type int

Le type
int

(de l'anglais INTeger, entier) correspond à un entier relatif stocké sur un nombre de bits qui dépend du système (machine et compilateur) utilisé. En turbo-C (v 2.0) sur un compatible PC par exemple, un int était stocké sur 16 bits (nombre à 16 chiffres en numération binaire), et était compris entre -215( = -32768) et +215-1( = 32767) . Sur la plupart des stations de travail, un int est stocké sur 32 bits, et donc compris entre -231 et +231-1 .

2.1.1  Représentation des valeurs

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

2.1.2  Opérateurs arithmétiques

+ 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

Note : le même opérateur / sert également pour la division de nombres de type double. Il s'agira d'une division entière seulement si les deux opérandes de la division sont de type entier.

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

Le lecteur averti aura noté que l'opération ``modulo 2'' permet de tester la parité d'un entier, et que l'opération ``modulo 10'' permet d'extraire le chiffre des unités pour la notation décimale d'un entier.

2.1.3  Opérateurs relationnels

== égal
!= différent
< inférieur strict
<= inférieur ou égal
> supérieur strict
>= supérieur ou égal

Opérateurs relationnels

2.1.4  Fonctions mathématiques

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++).

2.2  Type double

Le type
double

correspond aux nombres à virgule flottante en double précision, stockés usuellement sur 64 bits.

Il existe également un type float donnant une moins grande précision. Usuellement les variable de type float sont stockées sur 32 bits.

2.2.1  Représentation

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

2.2.2  Opérateurs arithmétiques

+ additition
- soustraction
* multiplication
/ division

Opérateurs arithmétiques pour les nombres de type double

2.2.3  Opérateurs relationnels

Ce sont les mêmes que pour les entiers.

2.2.4  Fonctions mathématiques

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;
  }

2.3  Type char

Le type
char

représente l'ensemble de tous les caractères imprimables et non imprimables ordonnés selon le code ASCII (chaque caractère est représenté par un entier entre 0 et 127) :

... < '0' < '1' < ... < '9' < ...'A' < 'B' < ...'Z' < ... < 'a' < 'b' < ... < 'z' < ...

2.3.1  Représentation des valeurs

Les valeurs des caractères imprimables sont représentées entre apostrophes.

Exemple 7  

    char LettreMajuscule;

    LettreMajuscule = 'A';

Pour certains caractères ``spéciaux'' souvent utilisés (la tabulation, le retour à la ligne¼) il existe une notation mnémonique :

    '\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)N

pour 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)C

dé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;
  }

2.3.2  Opérateurs arithmétiques

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';

2.3.3  Opérateurs relationnels

Ce sont les mêmes que pour les entiers.

2.3.4  Fonctions usuelles

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>

2.4  Type bool

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.

2.4.1  Opérateurs logiques

&& 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;
  }

2.5  Conversions de type

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)NomObjet

comme 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 */

2.6  Représentation interne

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

3  Expressions arithmétiques et logiques

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.

En cas d'ambiguïté, l'évaluation a lieu selon l'ordre de priorité suivant :

  1. la parenthèse ( ) la plus interne
  2. les fonctions
  3. ! signe -
  4. * / %
  5. + -
  6. < <= > >=
  7. == !=
  8. &&
  9. ||

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 )
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 ?

4  Instructions, Structure séquentielle

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;
  }

Le programme ci-dessus comporte, dans sa partie exécutable, une séquence de trois instructions. Cela signifie qu'elles seront exécutées l'une après l'autre , dans l'ordre de leur apparition dans le programme. La première :

    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

4.1  Affectation

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- -;

4.2  Instruction composée

La forme générale d'une instruction composée est :



{
Declarations Locales (facultatives)

INSTRUCTION1
INSTRUCTION2
...
INSTRUCTIONn
}



Les instructions formant la séquence sont exécutées l'une après l'autre, dans l'ordre où elles sont écrites.

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 :

Exercice 2 Ecrire un programme qui effectue la conversion en centimètre s d'une longueur exprimée en mètres et en centimètres.

Exercice 3 Ecrire un programme qui saisit trois nombres et les affiche dans l'ordre inverse de l'ordre de saisie.

Exercice 4 Un nombre rationnel s'exprime sous la forme [P/Q] où P et Q sont des entiers. Ecrire un programme qui saisit deux entiers correspondants à P et Q et affiche le nombre réel correspondant en notation décimale.

Exercice 5 Ecrire un programme qui saisit un entier positif n et affiche la somme des n premiers entiers (sans faire une boucle !).

Exercice 6 Ecrire un programme qui saisit les coordonnées des sommets d'un triangle et qui affiche le périmètre et la surface de ce triangle.

5  Structures alternatives

5.1  Choix simple

Lorsqu'un traitement doit être subordonné à une condition, on utilise la structure :



if (EXPRESSION_BOOLEENNE)
INSTRUCTION



A l'exécution, l'EXPRESSION BOOLÉENNE est évaluée. Ce peut être une comparaison, une variable de type int dont la valeur sera interprétée comme un booléen, ou une quelconque expression logique. Si l'évaluation de cette expression donne la valeur logique ``vrai'' (toute valeur différente de 0), l'INSTRUCTION est exécutée, sinon elle ne l'est pas et l'on passe à la suite du programme. Notez bien que l'INSTRUCTION peut être une instruction composée (un bloc délimité par { et } ).

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;
  }

Lorsque le choix entre deux traitements est subordonné à une condition, on utilise la structure :



if (EXPRESSION_BOOLEENNE)
INSTRUCTION1
else
INSTRUCTION2



A l'exécution, l'EXPRESSION BOOLÉENNE est évaluée ; si l'évaluation de cette expression donne la valeur logique ``vrai'' (toute valeur différente de 0), l'INSTRUCTION1 est exécutée, sinon c'est l'INSTRUCTION2 qui est exécutée. INSTRUCTION1 et INSTRUCTION2 peuvent être des instructions simples ou composées.

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;
  }

5.2  Choix multiple

Lorsque le choix entre divers traitements dépend de la valeur d'une seule expression, on peut utiliser la structure :



switch (EXPRESSION)
{
case CONSTANTE1 : LISTE_INSTRUCTIONS1 break;
case CONSTANTE2 : LISTE_INSTRUCTIONS2 break;
...
case CONSTANTEn : LISTE_INSTRUCTIONSn break;
default: LISTE_INSTRUCTIONSn+1;
}



L'EXPRESSION est tout d'abord évaluée. Le fonctionnement est le suivant :
Si la valeur de l'EXPRESSION est égale à la CONSTANTE1, alors la LISTE_INSTRUCTIONS1 est exécutée.
Sinon, si la valeur de l'EXPRESSION est égale à la CONSTANTE2, alors la LISTE_INSTRUCTIONS2 est exécutée, etc.
Si la valeur de l'EXPRESSION n'est égale à aucune des constantes, alors la LISTE_INSTRUCTIONSn+1 est exécutée.

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 :



case CONSTANTE1 :
case CONSTANTE2 :
case CONSTANTE3 : LISTE_INSTRUCTIONS break;
...



Exemple 3   Le programme suivant détermine si le jour de la semaine donné en entrée est un jour ouvré ou un jour chômé.

/* 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;
}

5.2.1  Exercices

Exercice 1 Ecrire un programme qui saisit trois nombres et affiche le plus petit.

Exercice 2 Ecrire un programme qui

Exercice 3 Ecrire un programme qui saisit les coordonnées de trois points et qui indique si ceux-ci sont alignés.

Exercice 4 Ecrire un programme qui saisit un entier et qui, sans faire de calcul, affiche soit : ËRREUR" s'il n'est pas compris entre 1 et 10, soit : "P" s'il est premier, soit : "NP" s'il ne l'est pas.

6  Structures répétitives

6.1  Structure ``Tant que'' (while )

Lorsque l'on doit répéter un traitement tant qu' une condition reste vérifiée, on utilise la structure :



while (EXPRESSION_BOOLEENNE)
INSTRUCTION



Lors de l'exécution de cette structure, l'EXPRESSION BOOLÉENNE est évaluée. Si elle est fausse, on quitte la structure et on passe à la suite du programme. Si elle est vraie, on exécute l'INSTRUCTION (qui peut bien sûr être une instruction composée), puis on retourne au début de la structure, c'est-à-dire au test de l'EXPRESSION BOOLÉENNE.

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;
  }

6.2  Structure ``Faire ¼ Tant que'' (do ¼ while )

Lorsque le traitement doit être effectué au moins une fois, on utilise cette variante de la structure précédente :



do
INSTRUCTION
while (EXPRESSION_BOOLEENNE)



Lors de l'exécution de cette structure, l'INSTRUCTION (qui peut bien sûr être une instruction composée) est exécutée une première fois, puis l'EXPRESSION BOOLÉENNE est évaluée. Si elle est fausse, on quitte la structure et on passe à la suite du programme. Si elle est vraie, on retourne au début de la structure, c'est-à-dire au début de l'INSTRUCTION.

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;
  }

Question : pourquoi n'est il plus nécessaire d'initialiser la variable Carac, comme dans l'exemple 6.1 ?

Exemple 3  

/*
    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;
  }

Exercice 1 Ecrire un programme qui saisit des caractères, séparés par des RETURN, jusqu'à trouver le caractère 'q' ou 'Q', et qui affiche pour chaque caractère saisi le code ASCII correspondant.

Exercice 2 Si xn est une valeur approchée de ÖA, alors xn+1 = [1/2] ( xn + [A/(xn)] ) est une meilleure valeur approchée de ÖA.

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.

6.3  Structure ``Pour'' (for )

Lorsque l'on désire répéter un traitement un nombre de fois déterminé, on peut utiliser la structure :



for (INITIALISATION; TEST; INCREMENT)
INSTRUCTION_FOR



L'instruction INITIALISATION est exécutée une fois et une seule au début de l'exécution du for.

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

Remarque : la variable servant de "compteur" n'est pas forcément un entier.

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;
  }

La variable servant de "compteur" (dite aussi variable de contrôle) est souvent utilisée à l'intérieur de la boucle pour des tests ou des calculs :

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;
  }

6.4  Boucles imbriquées

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;
  }

Exercice 3 Ecrire un programme qui réalise le "dessin" suivant à l'aide de boucles imbriquées.

*....
.*...
..*..
...*.
....*
.....
.....
.....

7  Sous-programmes (fonctions et procédures)

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 :

sinus.gif

7.1  Exécution d'un sous-programme

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 :

debut programme
  SEQUENCE 1
  APPEL SOUS-PROGRAMME B
  SEQUENCE 2
  APPEL SOUS-PROGRAMME A
  SEQUENCE 3
fin programme

Contenu (définition) du sous-programme B :

debut sous-programme
  SEQUENCE 4
  APPEL SOUS-PROGRAMME A
  SEQUENCE 5
fin sous-programme

Exécution :

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

7.2  Définition de fonctions

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;
  }

La forme générale d'une définition de fonction est la suivante :

TypeRésultat
NomFonction

(déclarations de paramètres formels)
{
instructions corps
de la
return expression; fonction
}

Remarques :

7.3  Appel d'une fonction

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;
  }

Remarques :

Un appel de fonction peut également faire partie d'une expression, ou être utilisée comme paramètre effectif :

Exemple 4  

#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) );

Exercice 1 Définir une fonction SinH qui admet un argument double et renvoie une valeur double. Le sinus hyperbolique est défini par la formule


sinh  x = ex-e-x
2

Incorporer SinH dans un programme adéquat.

Exercice 2 Définir une fonction Factorielle qui admet un argument entier positif et renvoie une valeur entière. La fonction Factorielle associe à tout entier naturel n, un entier naturel usuellement noté n! défini par :


ì
ï
í
ï
î
0!
= 1
1!
= 1
"n > 1
n!
= 2 ×3 ×¼×n

Incorporer Factorielle dans un programme adéquat.

7.4  Procédures

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;
  }

7.5  Paramètres

Nous pouvons logiquement distinguer trois genres de paramètres :

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;
  }

Mais attention : si une procédure possède un paramètre passé par référence, il est obligatoire d'utiliser une variable pour ce paramètre lors de l'appel :

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 */

7.6  Définition de procédure

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
}

7.7  Appel d'une 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  

#include <iostream.h>

/*  exemple de procedure sans parametre : provoque un "beep" sonore  */
void Beep()
  {
  cout << (char) 7;
  }

/*  appel :  */
int main()
{
  Beep();
  return 0;
}

Exercice 3 Définir une procédure NBeep qui admet un paramètre entier N, et qui provoque consécutivement N "beeps" sonores :

  1. avec une boucle "for" et un compteur local;
  2. avec une boucle "while" et en utilisant le paramètre passé par valeur comme compteur.

Exercice 4 Définir une procédure Equa2 qui prend comme paramètres d'entrée trois double a, b, c coefficients de l'équation du second degré :


a x2 + b x + c = 0

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.

7.8  Règles de visibilité

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 */
  }

Des déclarations peuvent également apparaître en dehors de tous les blocs d'instructions, on dit alors qu'elles sont globales . Les objets ainsi définis peuvent être utilisés partout après leur définition. On peut donc considérer qu'il existe un bloc ``global'' qui inclut tous les autres.

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 */
  }

/* ... */

Attention : dans le cas où un objet global et un objet local portent exactement le même nom, c'est la définition la plus locale qui prime.

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;
  }

Nous verrons au chapitre suivant qu'il est souvent nécessaire de déclarer de nouveaux types en global car ils servent à la fois dans le programme principal et dans les sous-programmes.

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.

7.9  Notion de ``prototype''

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;
  }

7.10  Quand créer des sous-programmes ?

7.11  Recommandations

Un sous-programme doit obéir aux règles suivantes:

[modularité:] un sous-programme réalise une tâche et une seule (par exemple, une fonction de calcul ne doit pas afficher de résultat) ;
[autonomie:] un sous-programme est autonome et ne doit pas lire ni modifier directement les variables du programme qui l'appelle, sans passer par des paramètres ;
[transparence:] un sous-programme doit être conçu de façon à ce que le programmeur qui y fait appel (non nécessairement son concepteur) n'ait pas à tenir compte des choix du concepteur ;
[convivialité:] l'appel (l'utilisation) du sous-programme doit être la plus évidente possible.

8  Définition de nouveaux types de données

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.

8.1  Type défini par synonymie

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.

D'autre part, il est également possible de définir des nouveaux types à partir des modificateurs permis par le langage.

Exemple 2  
Pour définir la notion d'entier naturel, il suffit décrire :
typedef unsigned int Naturel;

8.2  Type défini par énumération

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;

La forme générale d'une définition de type énuméré est la suivante :

typedef enum { ENUMERATION DES ELEMENTS } NOMTYPE; 

8.2.1  Valeur des éléments

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 */

Il est possible de choisir soi-même sa représentation interne en spécifiant les valeurs entières associées (qui doivent être données en ordre strictement croissant) à chaque élément :

Exemple 5  

typedef enum {ARRIERE = -1, ARRET = 0, AVANT = 1} Mouvement;

8.2.2  Opérations sur une variable de type énuméré

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;
  }

Exemple 7  

#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 */
  }

Remarque : Avec ce type de déclaration (non Orientée Objet), on ne peut pas avoir le même élément dans 2 types énumérés différents.

Exemple 8  

typedef enum {
Orange

, Citron, Pamplemousse, Tomate} Fruit;
typedef enum {Bleu, Vert, Rouge,
Orange

} Couleur; /* INTERDIT !!! */

8.2.3  Vérifications de type à la compilation

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;
  }

9  Types structurés : tableaux

9.1  Définition

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;
  }

Remarque : le nombre d'éléments est défini à partir de constantes . On ne peut donc pas définir ainsi un type de tableau dont la dimension varie au cours de l'exécution du programme.

9.2  Type des composantes

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];

9.3  Tableau à 2 dimensions

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];

Mais d'autres formulations sont possibles, pour un résultat équivalent :

Exemple 4  

const int NbLignes   = 3;
const int NbColonnes = 3;
typedef double Matrice[NbLignes][NbColonnes];

9.4  Tableau multidimensionels

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

 

typedef int Sequence[25][3][600][800];
Sequence s1, s2;

OU (beaucoup mieux) :

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 ¼

9.5  Tableaux et paramètres

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.

9.6  Référence à un élément du tableau

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 ];
    /* .  .  . */
  }

Pour les tableaux à plusieurs dimensions, il faut spécifier autant de valeurs d'indice que le tableau comporte de dimensions. Ainsi, un élément de matrice est repéré par un indice de ligne et un indice de colonne (I et J dans l'exemple suivant) :

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;
  /* . . . */
  }

9.7  Tableaux de caractères

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;
  }

La bibliothèque string contient d'autres fonctions très utiles pour manipuler des chaînes de caractères : strlen (retourne la longueur d'une chaîne), strcmp (compare deux chaînes), strcat (ajoute une chaîne à la fin d'une autre chaîne), etc.

9.8  Lecture / écriture de tableaux

9.8.1  Tableaux numériques

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];
      }
    } 

  /* . . . */
  }

9.8.2  Tableaux de caractères

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;
      }
  }

Notez la syntaxe particulière cin.getline(...) qui offre l'avantage ici de préciser le nombre maximum de caractères à lire dans le flux d'entrée.

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.

9.9  Dangers des tableaux

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 ...

9.10  Exercices

9.10.1  Tableaux de dimension 1

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.

Exercice 2 Écrire un sous-programme qui incrémente d'une unité les éléments d'un tableau de huit entiers.

Exercice 3 Écrire un sous-programme qui inverse les huit éléments d'un tableau. On utilisera une seule boucle.

Exercice 4 Écrire un sous-programme qui calcule la somme des éléments d'un tableau de huit entiers.

Exercice 5 Écrire un sous-programme qui détermine la plus grande et la plus petite valeur d'un tableau de huit entiers. On utilisera une seule boucle.

Exercice 6 Écrire un programme qui compte les fréquences (le nombre d'occurences) des caractères entrés par l'utilisateur.

Exercice 7 Un palindrome est un mot ou une phrase qui se lit indifféremment de gauche à droite ou de droite à gauche ; par exemple, sont des palindromes :

ARA
KAYAK
QSDFGHHGFDSQ
ELU PAR CETTE CRAPULE
ESOPE RESTE ICI ET SE REPOSE

Écrire un sous-programme qui vérifie si un mot ou une phrase est un palindrome.

Exercice 8 Écrire un algorithme qui détermine, par la méthode du crible d'Ératosthène, les nombres premiers parmi les 10000 premiers entiers naturels.

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'') :

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

9.10.2  Tableaux à plusieurs dimensions

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)

10  Types structurés : structures hétérogènes (struct)

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 .

10.1  Définition

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;

10.2  Accès aux composantes

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;
}

10.3  Passage d'une structure en paramètre

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.

Exercice 2 Écrire une fonction qui calcule la différence en heures, minutes, secondes, entre deux temps exprimés en heures, minutes, secondes.

11  Pointeurs

11.1  Définition d'un type pointeur

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 */

11.2  Variables de type pointeur

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 */

Dans cet exemple, P1 et P2 sont donc des variables susceptibles de recevoir des adresses de flottants (double). Par abus de langage, on appelle souvent pointeur une telle variable, alors qu'il vaudrait mieux réserver cette appellation à l'adresse contenue dans cette variable.

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 */

Attention : si vous déclarez simultanément plusieurs variables de type pointeur, comme dans l'exemple ci-dessus, le signe * doit précéder chaque nom de variable.

11.3  Valeurs de pointeurs - l'opérateur &

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;
  }

11.4  Accès à l'objet ``pointé'' - l'opérateur *

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;
  }

Remarques :

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).

11.5  Pointeur NULL

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 ).

11.6  Allocation de mémoire - l'opérateur new

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 :

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;
  }

L'avantage de l'allocation dynamique par rapport à la déclaration de variables est de permettre d'adapter, lors de l'exécution du programme, la consommation de mémoire à la taille effective des données traitées.

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.

11.7  Libération de mémoire - l'opérateur delete

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;
  }

Attention : pour utiliser l'opérateur delete, il faut être certain que l'opérande pointe effectivement sur un objet alloué grâce à l'opérateur new .

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;
  }

Exercice 1 Écrire une fonction qui calcule le plus grand nombre premier inférieur ou égal à un entier N donné, par la méthode suivante :

11.8  Pointeurs et tableaux

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++;
        }
  }

Il est important de préciser que l'incrémentation ou la décrémentation d'une variable de type pointeur (P++, P=P-2, etc) n'agit pas sur le pointeur en qualité de nombre entier (une adresse est en effet, somme toute, un nombre entier), mais bien en sa qualité d'adresse d'un objet possédant une taille précisée par son type .

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.

11.9  Pointeurs et structures

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;
  }

Une notation spécifique permet de simplifier l'écriture de l'accès aux champs d'une structure pointée, simplification appréciable surtout dans le cas de structures possédant des champs pointant sur d'autres structures ¼ :

(*P).Imag est équivalent à : P->Imag

(*((*p).ptr1)).ptr2 est équivalent à : p->ptr1->ptr2

etc.

11.10  Listes chaî nées

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 que pour désigner le type du champ Suivant, il est nécessaire de faire apparaître le nom ``provisoire'' de la structure (TmpCellule) au début de la définition, après le mot-clé struct.

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;
  }

Remarques :

  1. la liste créée dans cet exemple peut se représenter par le diagramme suivant :

    liste.gif

    Ce type de diagramme est très utilisé dans la littérature informatique pour visualiser les structures chaînées.

  2. Toutefois une représentation plus exacte serait :

    listee.gif
  3. Enfin, certains préfèrent utiliser une première cellule "bidon" pour simplifier les traitements (la liste n'est jamais réduite au pointeur NULL). D'autres considèrent cette solution comme intellectuellement inacceptable car une cellule par liste est toujours allouée pour rien.

Exercice 2 Écrire des fonctions informatives sur une liste qui répondent à des questions telles que :

  1. La liste est-elle vide ?
  2. Combien contient-elle de cellules ?
  3. Contient-elle une cellule avec telle valeur ?
  4. Combien de fois contient-elle une telle cellule ?

Exercice 3 Écrire des procédures d'insertion de cellule telles que :

  1. Ajouter au début,
  2. à la fin,
  3. avant
  4. ou après une certaine valeur,
  5. avant
  6. ou après la Nème cellule.

Exercice 4 Écrire des procédures de destruction de cellule telles que :

  1. Enlever la première,
  2. la dernière,
  3. ou la Nème cellule,
  4. enlever la première cellule qui contient telle valeur,
  5. ou toutes les cellules qui contiennent cette valeur,
  6. détruire toute la liste.

Exercice 5 Écrire des procédures de traitement global d'une liste telles que :

  1. Afficher la liste,
  2. trier la liste,
  3. ou inverser l'ordre des cellules.

Exercice 6 Écrire des procédures de manipulation de plusieurs listes telles que :

  1. Comparer deux listes,
  2. insérer une liste dans une autre,
  3. rechercher une liste dans une autre,
  4. enlever une liste d'une autre.

12  Fichiers de texte

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.

12.1  Inclusion

Pour utiliser les fichiers en C++, il est nécessaire d'inclure le fichier <fstream.h> .

12.2  Déclaration (et ouverture)

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 );

12.3  Lecture

Exemple 1

Soit la déclaration : ifstream F_entree( "entiers.dat" ); en supposant que le fichier ëntiers.dat" existe bien.

Lorsqu'on lit successivement des lignes dans un fichier, il faut déterminer à quel moment s'arrêter pour ne pas aller au-delà de la fin de fichier. La boucle typique est : while ( ! F_entree.eof() ) ...  (eof signifie end of file).

12.4  Écriture

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éé.

12.5  Fermeture

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(); .

12.6  Passage de paramètres

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()

12.7  Conclusion

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.

13  Du C++ au C-ANSI

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.

13.1  Entête

Remplacer #include <iostream.h> par #include <stdio.h>

13.2  Affichage

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 );

13.3  Saisie

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é !

13.4  Type de données logiques

Le type bool n'existe pas. Mais on peut le définir comme au paragraphe 8.2.1 .

13.5  Allocation dynamique

13.6  Passage de paramètre par adresse

(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;
  }

Et voici maintenant la version C de ce programme, utilisant des pointeurs pour produire rigoureusement le même effet que le programme précédent :

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;
  }

13.7  Fichiers de texte

Les exemples ci-dessous font référence au chapitre 12 .

13.7.1  Inclusion

Il n'y a pas d'équivalent à <fstream.h> ;  le fichier <stdio.h> suffit.

13.7.2  Déclaration (et ouverture)

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.

13.7.3  Lecture

Soit la déclaration FILE * F_entree; et l'ouverture
F_entree = fopen( "entiers.dat", "r" ); .

13.7.4  Écriture

Soit la déclaration FILE * F_sortie; et l'ouverture
F_sortie = fopen( "entiers.dat", "w" ); .

13.7.5  Fermeture

Elle n'est pas automatique. Remplacer F_entree.close(); et F_sortie.close(); par fclose( F_entree ); et fclose( F_sortie ); .

14  Notions de logique booléenne

14.1  Valeurs possibles

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...).

14.2  Opérateurs

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' )

14.3  Tables de vérité

A NON A
V F
F V

ET V F
V V F
F F F

OU V F
V V V
F V F

14.4  Règles de simplification

  1. NON( NON A ) ® A
  2. A ET A ® A
  3. A OU A ® A
  4. NON( A ET B ) ® (NON A) OU (NON B)
  5. NON( A OU B ) ® (NON A) ET (NON B)
  6. NON( x < = y ) ® ( x > y )
  7. NON( x < y ) ® ( x > = y )

14.5  Évaluation en C++

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 ?

Exercice 2 L'opérateur NAND n'existe pas en C++, mais il est très utilisé dans les composants électroniques.

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.

  1. A NAND A = ?
  2. (A NAND B) NAND (A NAND B) = ?
  3. (A NAND A) NAND (B NAND B) = ?
  4. (A NAND (B NAND B)) NAND ((A NAND A) NAND B) = ?

15  Bibliographie


Footnotes:

1American National Standard Institute

2l'ordre porte sur le rang des objets, non sur leur valeur


File translated from TEX by TTH, version 2.75.
On 13 Sep 2002, 15:42.