Initiation
à la
programmation
Contact : gengis.scann@free.fr
Contenu
de cette
page :
Préface : les
langages de programmation
Définition
Programmer
: établir
à l'avance une suite d'opérations, les phases d'un
projet. En informatique, fournir à un ordinateur les
données et les instructions concernant un problème
à résoudre ou une tâche à exécuter
(Petit Larousse).
Donner des instructions à un ordinateur suppose la connaissance
d'un langage de programmation.
C'est un langage qui peut être
traduit en un code exécutable,
c'est
à dire un fichier compréhensible par votre ordinateur.
Par exemple ce que WINDOWS nomme application,
un fichier dont le nom a la forme toto.exe.
Le programme qui permet de tranformer un (des) fichier(s)
source(s) en un code exécutable s'appelle un compilateur, ou un interpréteur dans certains
cas.
Les langages
Les langages de programmation dits
"évolués" existent
depuis un demi-siècle. Les deux ancêtres FORTRAN (pour les scientifiques) et
COBOL (pour la gestion) ont
subi
quelques replâtrages, mais sont
toujours là.
Après quelques années de développements
anarchiques de logiciels (ils le sont toujours un peu d'ailleurs), il
devint
nécessaire
d'écrire des programmes plus maîtrisables et plus
fiables. En 1971
la NASA
perdit une sonde spatiale dans l'Atlantique (MARINER 8) car
le compilateur FORTRAN avait
laissé passer un bug
assez pitoyable. Le gouvernement américain
investit alors dans ce qui
aurait dû devenir un langage majeur : Ada. La programmation devint
structurée, modulaire,
abstraite, et actuellement "objet". Des langages et des dialectes
spécialisés sont apparus, certains ont disparu, d'autres
ont
survécu.
Citons rapidement d'autres survivants : PASCAL fut un langage
structuré très apprécié pour ses vertus
pédagogiques. Ce fut
le langage C qui
connut un succès universel un peu surprenant : il n'avait
été conçu au départ par Dennis RITCHIE non
pas comme un langage de programmation général, mais pour
programmer le système UNIX sur diverses machines. Au
début des années 80, Bjarne STROUSTRUP invente C++
, un langage objet, qui lui
aussi connait un gros succès. Puis apparait JAVA, un langage
interprété plus spécialisé dans les
interfaces graphiques, dans les bouts de programmes tournant sur le
net.
D'une certaine façon, ils se ressemblent tous.
Tous ont des
défauts et des
qualités. On choisit un langage en fonction du travail que l'on
doit faire, puis en fonction de la maîtrise qu'en a le
programmeur.
Contrairement à une idée reçue, la qualité
de la programmation dépend plus de la compétence du
programmeur/concepteur que du choix du langage. C'est le
programmeur qui choisit
de découper un logiciel en composants indépendants et
maîtrisables,
qui choisit les bons algorithmes, qui écrit ses programmes de
façon
lisible ou non. La "puissance du langage" vous permettra de gagner du
temps de développement,
en généralisant plus facilement certains
composants. Mais c'est comme pour la musique. Si vous donnez un piano
à
queue à un ringard, il jouera toujours aussi mal.
Pour ce cours,
on a
choisi un sous-ensemble de C++
On a choisi comme langage support de
ce cours le C++ pour les
raisons suivantes :
- Il fallait bien en choisir un. On a rejeté l'idée
d'un pseudo-code
neutre, car il vaut mieux pouvoir tester directement ce que l'on
programme. Et puis un compilateur est un outil extrêmement
précieux
pour détecter les fautes de frappe ou les erreurs de
programmation les
plus évidentes.
- C++ est le plus récent et contient quelques
améliorations par
rapport au C standard qui permettent de simplifier certains aspects de
la programmation.
- On s'est bien sûr
contenté d'utiliser les
instructions les plus simples et les plus standard, en
général communes aux autres langages.
- On fera néanmoins
référence à
d'autres langages comme C Ansi, Pascal ou FORTRAN 77.
- Vous pourrez aussi
bénéficier de la considération de vos ami(e)s en
leur
faisant croire que vous connaissez le C++, ou que vous "faites" du
C++ pour reprendre une expression répandue.
Le but de ce cours est de vous
apporter quelques principes
généraux pour
vous permettre d'apprendre le plus
facilement possible
n'importe quel nouveau langage et de vous adapter à tout
environnement de programmation : petit programme à la maison,
logiciel professionnel développé seul ou en
équipe.
Retour
au début
Aspect pratique
Les exemples qui
figurent dans ces pages (fichiers XXX.cpp, ".cpp" étant une des
extensions autorisées pour le C++) peuvent être
exécutés par
l'intermédiaire d'un compilateur C++. Ils ont été
testés au moyen de g++,
le compilateur c++ de GNU diffusé
gratuitement. g++ est
utilisable sur Linux.
Si vous êtes sous WINDOWS, vous
pouvez utiliser l'émulateur d'UNIX CygWin ( http://www.cygwin.com) : CygWin
crée une fenêtre alphanumérique sur votre
ordinateur, et vous pouvez travailler comme si vous étiez sur
UNIX.
On peut aussi utiliser Turbo C ou Visual C++, produit payant de
Microsoft. Le
choix et l'utilisation du compilateur ne font pas partie des objectifs
de ce cours. Même si nous avons une idée précise de
ce que sont les produits Microsoft.
Tous les exemples du cours peuvent
être
téléchargés.
En appuyant sur la touche "shift"
de votre clavier, cliquez avec le bouton gauche votre souris le nom du
fichier incriminé (par exemple hello.cpp
) .
Votre navigateur vous propose alors de copier le fichier quelque part
sur votre ordinateur. Si vous
cliquez
sans shifter c'est le contenu du fichier qui apparaît.
Une solution plus rapide consiste
à télécharger
tous les programmes en une seule fois au moyen du fichier
compressé antirouille.tgz
.
Si tous les exemples sont en C++, on
s'est permis de renommer certains
mots-clés et d'utiliser certaines fonctions, afin de faire
apparaître des concepts plus généraux, des
accessoires pratiques, et d'éliminer le côté
rébarbatif du langage. Tout ceci étant
défini dans le fichier definitions.h
On reconnaitra ces termes car ils commencent tous par une majuscule.
Tous les mots-clés ou les noms de fonctions en minuscules font
partie du langage C++.
Bonne route.
Retour au début
Un
premier programme
Vous pouver télécharger les fichiers sources : hello.cpp
et definitions.h
.
Le programme suivant écrit une phrase sur votre
écran.
- // C++ : Mon
premier programme
-
- #include "definitions.h"
-
- main ()
- {
/* Si vous comprenez l'espagnol */
- Print (" Ola Hombre");
- }
|
Si vous téléchargez ce
programme et le fichier
definitions.h,
que vous le compilez
avec un compilateur c++, et que
vous lancez le code exécutable, vous verrez s'afficher sur votre
écran :
Quelques explications :
- L'exécution d'un programme
commence par l'appel du programme
principal, qui en C/C++
est une
fonction nommée main.
(ligne 5). Les instructions (lignes 1 et 3) qui se trouvent avant
contiennent de
l'information qui complète le programme qui suit.
- Le corps
du programme,
c'est à dire les instructions de la
fonction "main", est délimité par les accollades ouvrantes et fermantes
(lignes 6 et 8). On aussi parle de bloc
d'instructions.
- Les instructions se terminent par un
point
virgule. Ici la seule instruction (ligne 7) est un appel à
la fonction Print.
- L'argument de la fonction Print est
une chaîne de caractères,
délimitée par des doubles
quotes.
- Avant le corps du programme (ligne
3), on trouve une directive
d'inclusion. Cette instruction
revient à copier le
contenu du fichier inclus à l'endroit indiqué. Ce fichier
(definitions.h) contient des
informations nécessaires au
programme. Comme par exemple lui indiquer que la fonction Print est
utilisable et possède un argument.
- Pour le moment on ne
détaillera pas le fichier definitions.h.
Cela fait
partie aussi de l'apprentissage de la
programmation : c'est un travail
d'équipe qui s'appuie sur des connaissances acquises, que
l'on
masque pour ne pas encombrer le
programme en cours d'écriture.
- En ligne 1, on trouve un commentaire délimité
par les 2 slashes : // et par
la fin de la ligne.
- En ligne 6, on trouve une autre
forme de commentaire délimité
par /* et par */. Un tel commentaire peut donc
tenir sur plusieurs lignes.
Retour au début
Les
déclarations
Déclaration de variables
Un programme est une liste
d'instructions permettant de manipuler
des variables. Une variable
est nommée au moyen d'un identifieur.
Un identifieur est une suite de lettres ou de chiffres ou du
caractère "_" (nommé underscore ou blanc
souligné).
Un identifieur doit commencer par une lettre. Dans les langages les
plus anciens, (Fortran et Pascal),
les majuscules et les minuscules sont équivalentes. En
C/C++/Java les mots-clés du langage sont en minuscules. On
se sert
du blanc souligné ou de l'alternance majuscule/minuscules pour
définir des noms composés. Les caractères
accentués sont interdits.
Identifieurs corrects
: n,
nombre_de_trucs,
NomDuTruc, a386, OM, PSG.
Identifieurs incorrects : 2be3 (commence par
un chiffre) ,
nombre_d'éléphants (à
cause des accents et de l'apostrophe)
Une variable possède un type. Ce
type est défini lors
de la déclaration.
Avant d'être utilisée, une
variable doit
préalablement être déclarée.
C'est lors de la déclaration que l'on nomme les variables qui
vont être utilisées, que l'on leur associe un type et éventuellement une
valeur initiale.
type identifieur = valeur initiale;
ou
type
identifieur ;
|
On va décrire deux types fondamentaux :
entiers et réels. Les autres types seront décrits au fur
et à mesure du cours.
Déclarations d'entiers
Pour les implémenter les variables entières, on utilisera
le type int standard
C/C++
ou Java.
int marignan = 1515;
int rayon
= 24;
int diametre
= 2*rayon;
|
Remarques générales
concernant les déclarations :
- Le compilateur oblige de
déclarer une variable avant son
utilisation. Il est recommandé de l'initialiser aussi. Par
exemple si la variable rayon n'est
pas
initialisée, l'initialisation de la variable diametre est incorrecte.
- On peut initialiser les variables au
moyen d'une expression (par
exemple
diametre=2*rayon). C++ et Java
l'autorisent. C Ansi n'autorise que les constantes numériques.
- On peut déclarer les
variables à n'importe quel
endroit du programme, pourvu que ce soit avant leur usage. En C, Pascal, et FORTRAN, les
déclarations doivent se trouver en début de programme.
- En
général, pour un
entier est implémenté sur un mot de 4 octets, ce qui
permet de stocker une valeur de -2147483648
à l2147483647. Il
existe d'autres variantes d'entiers en
C/C++/Java. Selon que
vous souhaitez stocker de plus grandes valeurs ou gagner de la place
(stocker sur 2 octets) :
short, long, unsigned long et même parfois long long. Les curieux
pourront consulter un manuel C. Pour ce cours, on se contentera
du type int.
Déclarations de réels
Il s'agit
d'implémenter des
nombre réels, (plus exactement un sous-ensemble des rationnels)
sous a forme classique (mantise, exposant). On utilisera le type
standard C float. Au niveau de
la constante
réelle, le
point sert à délimiter la partie décimale.
L'exposant (puissance de 10) se note au moyen de la lettre "e".
Ainsi 2e+3 correspond à deux dix puissance 3 et vaut 3000.
float pi
= 3.14159;
float kilo
= 1.0e+3 ;
float micro
= 1.0e-6;
|
Il
existe une autre
dénomination de reels en C/C++/Java : double qui occupe
plus de place et qui permet une précision plus grande.
Retour
au début
Les instructions de
base
Les affectations
Il s'agit de modifier le valeur d'une variable. Sa syntaxe est :
Exemples :
|
resultat =
(a+b) / (a-b);
delta = b*b - 4*a*c;
moyenne = (a+b+c)/3;
c1 = cos
(theta);
compteur = compteur + 1;
|
- L'expression est une succession
d'opérandes et d'opérateurs (+, _, *, /). Les
opérandes peuvent être des constantes, des variables, des
appels de fonction, ou des sous expressions entre parenthèses.
- Les variables utilisées dans la
partie droite de l'expression doivent
être initialisées. Une variable non déclarée
est immédiatement détectée par le compilateur. Ce
qui n'est pas le cas d'une variable non
initialisée. Le problème est qu'on risque une erreur
à l'exécution. Le programmeur doit donc être
vigilant.
- Les opérateurs multiplicatifs sont
plus prioritaires que les
opérateurs additifs. Ainsi dans
l'expression y = a+b*x; la multiplication est effectuée
avant l'addition, bien que l'opérateur + soit situé avant
le *.
- A priorité égale, c'est le premier opérateur
rencontré qui est effectué. Ainsi a/b*c vaut (a/b)*c et non pas a/(b*c).
- En cas de doute, ne pas hésiter
à utiliser des parenthèses.
- Il arrive que se produisent des dépassements de
capacité. En cas de valeur entière
excédant la taille de la variable (par exemple pour un entier
"ordinaire" implémenté sur 4 octets, le maximum est de
2147483647) ou de suite à
une division par zéro,
le
programme s'interrompt brutalement.
- La division (opérateur /) est
euclidienne
si les 2 opérandes sont entiers, réelle sinon.
- Voir le paragraphe consacré aux conversions entiers /réels
et aux dépassements de
capacité.
- En
FORTRAN on trouve
l'opérateur puissance noté **. Alors qu'en C/C++/Java, on
dispose à cet usage de la fonction pow (power). Dans certains
dialectes, on utilise le caractère ^.
Appels de fonctions et de procédures
Un appel de
fonction permet de lancer un traitement quelconque et
éventuellement d'en récupérer un résultat.
Cette instruction permet en une ligne d'effectuer un traitement qui
pourrait éventuellement avoir été
réalisé en une ou plusieurs
milliers de lignes. L'appel de fonction permet (devrait permettre) de
regrouper les instructions d'un logiciel en des programmes de taille
raisonnable qui devraient donc rester clairs et lisibles.
Un appel de fonction se reconnait à la
présence de parenthèses
qui suivent un
identifieur. Entre ces parenthèses figurent zéro ou
plusieurs arguments qui
constituent l'information transmise à la fonction par le
programme appelant.
reponse = ma_fonction (arg1, arg2, ....argn);
ou
ma_fonction (arg1, arg2, ....argn);
|
On a coutume
d'appeler procédure
une
fonction qui ne rend pas de valeur. On a vu la procédure Print dans le programme hello.cpp. Attention
(C/C++) :
même s'il n'y a pas d'argument, ne pas oublier les
parenthèses. Cette erreur n'est pas signalée par le
compilateur C et provoque seulement un avertissement en C++. Le
problème est que dans les
deux cas, l'appel de procédure n'est pas
exécuté.
Pour
l'anecdote, en Pascal une
procédure sans
argument n'a pas besoin de parenthèses. En FORTRAN une procédure se nomme subroutine. Elle est appelée
au moyen du mot-clé call.
Pour accéder à une fonction, le
compilateur doit
contrôler le
nombre et le type des arguments utilisés. En C et C++, une
fonction doit être déclarée.
On indique le type de valeur qu'elle rend ainsi que le type de
chacun de des arguments. On appelle cette déclaration l'entête
ou le prototype de
la
fonction. En général le prototype des fonctions
utilisées figure dans les includes
appelés au début de programme. Les langages les
plus récents, C++ et Java autorisent l'utilisation d'un
même nom de fonction associé à des arguments de
type différents. On peut donc appeler plusieurs variantes d'une
fonction ayant un même nom, mais une signature différente.
type_f ma_fonction (type1 arg1, type2 arg2, ..., typen argn);
|
La
présence des
entêtes des
fonctions utilisées n'est pas forcémént fiable :
on est toujours à la merci d'une fonction qui évolue et dont on oublie de modifier le prototype. Il existe
d'autres d'autres solutions que le prototypage des fonctions
utilisées. En Java ou en
FORTRAN 90, pour connaître le nombre et le type des
arguments utilisés on accède directement à de
l'information
issue de la compilation des fonctions utilisées. En FORTRAN 77,
on n'a pas de moyen de déclarer les arguments des fonctions
appelées car le compilateur ne le contrôle pas.
Exemples de prototypes des fonctions
utilisées dans les exemples
de cette page :
|
Procedure Print (Cstring message);
Procedure Print (float
valeur);
Procedure Print (int
valeur);
Procedure Display (float variable);
// Affiche une variable
Procedure Display (int
variable); // nom = valeur
Procedure Display (Cstring variable);
int ReadInteger
(); // Saisit un entier à la console
float ReadReal
(); // Saisit un réel à la console
int Modulo
(int i, int j); // renvoie i
modulo j
|
En C, C++, Java c'est le
mot-clé void qui en
fait est utilisé pour Procedure.
Blocs d'instructions
Un bloc
d'instructions est constitué de
plusieurs instructions regroupées entre deux accollades.
Elles seront utilisées dans les instructions qui suivent,
par exemple pour les itérations. Elles servent aussi à
définir des variables locales au bloc.
{
int val = 1;
instruction;
instruction;
instruction;
instruction;
}
|
Ici la variable val est valide à
l'intérieur du bloc.
Instructions conditionnelles
Le test complet :
if (condition)
instruction;
else
instruction;
|
Le test simple ne possède pas de branche
else :
if (condition)
instruction;
|
Les instructions peuvent être, et sont la plupart du temps des
blocs d'instructions.
if (a<b)
{
mini
= a;
mani = b;
}
else
{
mini
= b;
mani = a;
} |
Enchaînement de tests :
if (note>=15)
Print ("Bravo
Hélène");
else
if (note>=10)
Print ("Pas mal Nico");
else
Print ("Tu ne
t'arranges pas, Alexandre .."); |
Convention : Le "else"
se rapporte au dernier "if"
ouvert :
Exemple
if (c1)
if
(c2)
instruction_1;
else
instruction_2;
|
est équivalent à :
if (c1)
{
if (c2)
instruction_1;
else instruction_2;
}
|
et non pas à :
if (c1)
{
if (c2) instruction_1;
}
else
instruction_2;
|
Conclusion : en cas de doute, utilisez des accollades. Utilisez les
systématiquement d'ailleurs.
Ada et FORTRAN 77 ont levé
cette ambiguïté en "fermant le test" au moyen du
mot-clé endif. Ils ont aussi introduit le
mot-clé intermédiaire elseif
permettant de définir des cascades de tests.
L'expression
conditonnelle
L'expression conditionnelle peut
utiliser les opérateurs
relationnels suivants : <, <=, >, =>, ==
(égalité), != (différent).
Attention : une erreur
très fréquente en C/C++ consiste à confondre
l'opérateur d'égalité "==" avec l'opérateur
d'affectation "=". L'ennui, c'est que le compilateur ne détecte
pas cette erreur. En effet les multiples possibilités
syntaxiques du C donnent un sens à cette fformulation.
On peut aussi utiliser les opérateurs logiques et (And) , ou (Or), non (Not).
Exemple :
if (note1 > note_eliminatoire And note2>note_eliminatoire
And (note1+note2)>=20
)
{
Print ("Félicitations vous etes
recus");
moyenne =( note1+note2)/2;
}
|
Remarque : En C/C++/Java les opérateurs Or et And fonctionnent selon le principe
de "l'évaluation progressive" appelée aussi
"évaluation paresseuse" :
- Dans l'expression E = C1 And C2,
si C1 est fausse, E est forcément fausse, mais dans ce cas C2
n'est pas évaluée;
- Dans l'expression E = C1 Or
C2, si C1 est vraie, E est forcément
vraie, mais dans ce cas C2 n'est pas évaluée;
Instructions itératives
Boucle
"while"
Cette boucle effectue l'instruction tant que la condition est vraie :
while (condition)
corps_de_la_boucle;
|
Exemple (fichier facteurs.cpp) . Cet
algorithme affiche la liste des facteurs premiers d'un nombre :
//
C++ : Decomposition en facteurs premiers
#include "definitions.h"
main ()
{
int valeur = ReadInteger();
Display (valeur);
int reste = valeur;
int facteur = 2;
while (reste>facteur)
{
if (Modulo (reste,facteur)==0)
{
Display
(facteur);
reste =
reste /
facteur;
}
else
{
facteur
=
facteur + 1;
}
}
Display (reste);
} |
Boucle "for"
for (instruction_depart ; condition ; increment)
corps_de_la_boucle; |
Cette syntaxe est propre à C/C++/Java. Elle est une forme
condensée de :
instruction_depart;
while (condition )
{
corps_de_la_boucle;
increment;
}
|
Elle est utilisée
fréquemment pour faire varier un
indice. Par exemple :
for (int nro=0 ; nro<nombre ;
nro++)
corps_de_la_boucle; |
Notons a présence de l'opérateur
d'incrément ++. nro++
est une forme condensée de
nro=nro+1. Les formes
condensées spécifiques à C
ne font pas partie de ce cours, mais ce type de boucle est trop
systématiquement employé sous cette forme pour qu'on ne
fasse pas une exception. Notons aussi le zéro comme point de
départ et l'inégalité stricte par rapport au
nombre font aussi partie des habitudes issues de C. Voir aussi les
indices de
tableaux.
Boucle "do
...
while"
Comme la boucle while, sauf que le corps de la boucle est
exécuté au mois une fois.
do
instruction;
while
(condition);
|
Sélection
Il s'agit
d'appliquer un traitement sélectif
pondéré par une valeur de type entier ou assimilé,
comme les caractères ou les énumérations.
L'instruction a la forme suivante :
switch (expression_entiere)
{
case val1 :
instructions ;
break;
case
val2 :
instructions ;
break;
default :
instructions ;
} |
Remarques :
- On peut associer un même traitement à plusieurs
valeurs. Il suffit de placer plusieurs "case val :" cote à cote.
- Si on ne place pas de break, le traitement se poursuit au "case"
suivant. Sinon on sort du bloc "switch". Comme ce cas de figure
est relativement rare, penser à placer un commentaire
d'explication.
- Ne pas oublier un traitement en cas de valeur non prévue (default).
Exemple :
switch ( jour)
{
case 1 : Print ("Lundi");
break;
case
2 : Print ("Mardi");
break;
case
3 : Print ("Mercredi");
break;
case
4 : Print ("Jeudi");
break;
case
5 : Print ("Vendredi");
break;
case
6 :
case 7 : Print ("C'est week-end !!");
break;
default :
Print (" Erreur dans les
données");
} |
Retour
au début
Le
pré-processeur
Si le compilateur analyse le contenu
d'un programme, il existe un
mécanisme activé avant la compilation proprement dite.
Ce mécanisme permet de définir des symboles (directive #define) ou d'inclure
(directive #include) le
contenu de fichiers sources à un endroit donné. On peut
aussi tester la définition de symboles (#ifdef ou #ifndef ...#endif).
Exemple :
#include
"definitions.h"
#ifndef PI
#define PI
3.14159
#endif
#define Message "Vois
avez gagné"
#define Carre(a)
((a)*(a))
|
Le mécanisme de définition
fut pendant longtemps le seul moyen de définir des constantes en C. Il est d'ailleurs
utilisé asssez abusivement dans ce cours pour éliminer
certaines syntaxes C++ trop lourdes ou trop consommatrices
d'informations non essentielles. Attention cependant : le compilateur
ne "voit" pas les symboles, il ne voit que leur forme substituée.
On peut aussi définir des formules
par exemple Carre.
Notez les parenthèses. Si par exemple on avait écrit
#define
Carre(a) a*a, alors Carre(x-1) serait développé en x-1*x
- 1, c'est à dire -1, un comble pour un carré !
Le mécanisme d'inclusion est
très utile, voir indispensable en C/C++, Fortran 77,
Pascal,
pour partager de
l'information entre plusieurs programmes. Et aussi pour condenser en
une seule ligne une grande quantité d'informations. Par exemple
l'ensemble des déclarations (fonctions, constantes, ...)
système ou communes à un projet ou à un
thème donné.
Par exemple en C/C++, si vous utilisez
des fonctions de lecture/ écriture sur des fichiers texte, vous
devez inclure le fichier stdio.h.
Pour ce cours, qui n'est pas un cours de C, les fichiers
systèmes utiles sont déjà inclus dans le fichier definitions.h. Vous pouvez
donc ignorer ce genre de détail d'implémentation.
Attentions ce ne sont pas des
"instructions" du C, mais des
directives lues par un
programme qui travaille avant le compilateur. Il
n'y a donc pas lieu de les terminer par un point-virgule.
En
Java, il n'existe pas de
pre-processeur. Comme en FORTRAN 90, le compilateur
accède directement à des fichiers compilés pour
effectuer ses vérifications. C++ a décidé de
remplacer autant que faire se peut les instructions #define par des constantes ou des
fonctions en ligne.
En C/C++, il est possible que les
includes soient imbriqués. Aussi lorsqu'on crée un
include, il est indispensable de mettre en place un mécanisme de
"double inclusion" qui a la forme suivante (voir le fichier
definitions.h)
:
Exemple :
|
// Incl: Redefinitions generales
#ifndef _DEFINITIONS_H #define _DEFINITIONS_H
#include <stdio.h> #include <string.h>
. . . . . . . . . . .
#endif
|
Retour
au début
Autres
déclarations
Les Constantes
(const)
En fait il s'agit d'un attribut que
l'on peut accoller à tout
type de variable. Utiliser une constante permet avant tout de
dimensionner un tableau. Le compilateur dans certains cas peut
empêcher la modification d'une constante. On cherche à
améliorer la fiablilté des programmes.
La syntaxe est la suivante :
const float pi
= 3.14159;
const int revolution
= 1789;
|
Les Enumérations (enum)
Il s'agit de définir un type qui
associe à une variable un nombre fini de valeurs. La
syntaxe est la suivante :
enum nom_du_type
{VAL0, VAL1, VAL2, ..., VALn};
nom_du_type var1, var2;
|
Les valeurs VAL0, VAL1, ... sont équivalentes à des
constantes entières qui se succèdent en partant de
zéro.
Exemple :
enum m {UnPeu,
Beaucoup, Passionnement};
enum enum_cardinal
{ Nord, Est, Sud, Ouest };
enum_cardinal dir1, dir2;
|
Des variables de type enum_cardinal confèrent
plus de sens et
une plus grande lisibilité au
programme qui les utilise. On comprend mieux une instruction "if
(dir1==Sud)" que " if (dir1==2)"
La première ligne est équivalente à :
const int UnPeu
= 0;
const int
Beaucoup = 1;
const int
Passionnement = 2;
|
Ou bien :
#define
UnPeu
= 0;
#define
Beaucoup = 1;
#define
Passionnement = 2;
|
Les booléens
(bool)
Une variable booléenne a deux
états : vrai
ou faux. En C++ et en Java, le type se nomme bool, les deux constantes
booléennes sont true
et false.
bool ouvert
= true ;
bool xdepasse
= x > maxi;
bool ydedans
= y >= a And y
<= b;
|
En C Ansi, il n'y a pas de type
booléen. Il est assimilé au
type entier. Une extression entière est considérée
comme vraie si elle est différente de 0. Les constantes
booléennes sont en principe 1 ou 0. En fait, les (bons)
programmeurs redéfinissent un
type booléen au moyen d'une énumération ou
de symboles : enum bool {true,false}.
Les tableaux
Un tableau est un ensemble de
variables scalaire accessibles au moyen
d'un indice placé entre des crochets.
Lors de la déclaration on indique sa dimension au moyen d'une constante. Un tableau peut
être initialisé lors de sa déclaration au moyen
d'une liste d'éléments placés entre deux accolades.
On prendra dans ce cours la convention C/C++/Java, selon laquelle
l'indice d'un tableau commence
à zéro et
doit être strictement
inférieur à sa dimension. Attention, le
dépassement d'indice de tableau est une erreur très
fréquente.
En FORTRAN et en Pascal, , par
défaut l'indice varie de 1 à n inclus, mais on peut
redéfinir l'indice minimum.
Exemple :
const int dim_table =
10;
const int
nbr_coeff = 3;
int
carre[dim_table];
float
coeff [nbr_coeff] = {
1.1, 2.3, 3.5 };
for (int nro=0; nro<dim_table; nro++)
carre [nro]
= nro*nro;
|
Pour implémenter des tableaux
à plusieurs
dimensions (des matrices), en C/C++/Java on définit des tableaux de tableaux.
Exemple :
const int nbr_cols = 3;
const int
nbr_ligs = 2;
int matrix
[nbr_ligs] [nbr_cols] = { {11, 12, 13},
{21, 22, 23} };
|
Remarques :
- Si matrix[i][j] est un
entier, matrix[i] est un
tableau de nbr_cols=3
entiers, et matrix un
tableau de nbr_ligs=2
tableaux.
- Les
éléments désignés par le dernier indice
sont les éléments de plus bas niveau contigus.
- Les éléments matrix[i][j]
et matrix[i][j+1] sont contigus.
- On dit aussi que : " le dernier indice varie le plus vite".
- Finalement la disposition des valeurs d'une matrice [ligne]
[colonne] correspond à la fois à l'ordre traditionnel
mathématique (ligne puis colonne) et à la
disposition écrite : deux lignes de 3 nombres.
- En
Fortran, il existe des tableaux
à 2 (voire 3) indices. On écrit directement integer matrix (nbr_ligs, nbr_cols), Mais l'ordre des
éléments est différents : ce sont les
éléments repérés par le premier indice qui
sont contigus.
- Si vous avez un doute sur l'ordre ligne/colonne, faites un
programme test.
Les caractères (char)
Une constante de type caractère est
constituée d'un
caractère délimité par deux simples quotes. En
C
et C++ le type caractère est noté char.
char lettre
= 'a';
char
pourcent = '%';
char
quote = '\'';
char
antislash = '\\';
char
carnul = '\0';
|
Notons que pour utiliser la quote
entre quotes, il faut la faire précéder de l'antislash
"\". Et pour faire apparaître l'antislash, on double l'antislash.
En Fortran, on
double la quote.
En fait un caractère est
équivalent
à un
entier variant de 0 à 255 correspondant au numéro
(nommé code ascii) du
caractère. Par exemple le code ascii de 'a' (minuscule) vaut 97,
celui de 'A' (majuscule) vaut 65 et celui du caractère '0'
(zéro) vaut 20. Il est donc permis d'écrire :
int code_0
= '0';
Display (code_0);
|
Le
résultat du programme est :
Les chaînes
de caractères (Cstring)
On notera ce
type Cstring (notation propre
à
ce cours, définie dans le fichier definitions.h). On peut affecter
une Cstring au moyen d'une constante de
type chaîne de caractères. Une telle constante est
constituée d'un ensemble de
caractères délimitée par deux doubles quotes. Si une double quote ou un antislash figurent dans
la chaîne, on les fait précéder d'un antislash
("/").
.
On peut aussi affecter une chaîne de
caractères avec l'adresse nulle : NULL.
Cstring bateau
= "titanic";
Cstring message
= "Tiens, un iceberg !!";
Cstring
yacht
= bateau;
Cstring
hasa_quote = "My name is \"nobody\" ";
Cstring
canot
= NULL;
|
C/C++/Java
permettent de gérer les chaînes trop longues grâce
au mécanisme des "chaînes adjacentes". Si deux
chaînes ne sont séparées que par par des "espaces"
(blancs, tabulations ou sauts de ligne), elles sont fusionnées.
Cstring barre
=
"__________________________________________"
"__________________________________________"; |
En
C/C++, le contenu d'une chaîne de
caractères est équivalent à un tableau de
caractères qui se termine par le caractère nul. On peut donc aussi affecter une
Cstring au moyen d'un tableau de caractères. On peut aussi appliquer à ce tableau, en plus des
opérations classiques des tableaux, toutes les fonctions
dédiées aux chaînes de caractères. Réciproquement on peut accéder
au ième caractère d'une chaîne au moyen de
l'opérateur
"crochets". Notons
que l'on ne peut modifier le contenu d'une chaîne de
caractères définie par une constante.
Suite à
la convention qui
veut que la chaîne est délimitée par un
caractère nul supplémentaire, une chaîne de N
caractères doit tenir dans un tableau de taille N+1. Si ce
caractère nul est absent, manipuler une chaîne "mal
terminée" risque de provoquer un arrêt brutal du programme.
En Fortran
le délimiteur est la simple quote et il n'existe pas de
distinction entre caractère et chaîne de caractères.
Un caractère est une
chaîne de longueur 1.
const int
size_string = 31;
char
tabchar [size_string+1];
Cstring
chn = tabchar
|
Fonction
|
Rôle
|
| int strlen (Cstring chaine) |
Donne la longueur d'une
chaîne
(nombre de caractères avant le caractère nul) |
strcpy (Cstring dest, Cstring
orig)
|
Copie la chaine orig dans dest. |
| strcat (Cstring dest, Cstring
plus) |
Place la chaîne plus à la fin de la
chaîne dest. |
| bool Cestegal (Cstring c1,
Cstring c2) |
Egalité de chaînes
de caractères
|
| int strcmp (Cstring c1, Cstring
c2) |
Comparaison de chaînes
<0 si c1<c2, 0 si c1=c2, >0 sinon
|
| sprintf (Cstring dest, Cstring
format, values...) |
Print formatté sur
chaîne de caractères (Cf Fichiers) |
Précautions à prendre lors des manipulations
de chaînes de caractères :
- Les débordements de tableaux de
caractères (lors des copies et concaténations).
- L'écrasement du caractère
nul.
- L'interdiction de modifier une
chaîne constante.
Les structures (struct)
Une structure C/C++ est une
donnée constituée de la juxtaposition de données
appelées membres. Les
membres peuvent être des scalaires, des tableaux ou d'autres
structures. (En Pascal on parle de
record, en FORTRAN 77 on n'en parle pas).
struct str_complex {
float reel;
float imag;
};
// Ce type est utilisé de la
façon suivante :
str_complex un = { 1, 0};
str_complex i =
{ 0, 1};
str_complex z1, z2;
// On accède aux champs de la
façon suivante :
z1.reel = 36;
z1.imag = 15;
z2 =
z1; // Copie champ à champ.
|
L'affectation permet de copier une
structure sur l'autre. La copie se fait champ à champ.
La syntaxe C++
est allégée par rapport à C Ansi. Il fallait
écrire "struct st_reel z;". Et il n'y a pas de copie de
structure.
Retour
au début
Les
fichiers texte (Filetext)
On notera ce
type Filetext (notation
propre à
ce cours, définie dans le fichier definitions.h). Il existe des
fichiers non formattés ou binaires ou acccès direct qui
ne seront pas traités dans ce cours.
Un fichier est initialisé au moyen de
l'ouverture d'un fichier.
La fonction fopen est utilisée à cet usage. Un fichier C
est un pointeur. Il peut être initialisé avec la valeur
NULL pour indiquer que la
variable ne correspond à aucun fichier ouvert.
Filetext fic
= fopen ("monfichier.txt", "r");
|
Il existe deux fichers
prédéfinis (et déja
ouverts) : stdin pour la
lecture au clavier, stdout pour
l'écriture à l'écran.
Quelques fonctions pour gérer les fichiers :
Fonction
|
Rôle
|
| Filetext fopen (Cstring nom,
Cstring mode) |
Ouvre un fichier. mode vaut "r" pour un fichier
ouvert en lecture, "w" pour un fichier ouvert en écriture.
|
| fclose (Filetext fic) |
Ferme un fichier ouvert |
| fprintf (Filetext fic,
Cstring format, values, ....) |
Procédure d'impression
formattée sur un fichier |
| printf (Cstring format,
values, ....) |
Procédure d'impression
formattée sur la console |
Print (value)
|
Imprime la valeur
(ebtière, réelle, chaîne) et passe à la
ligne.
|
Display (expr)
|
Ecrit une ligne de la
forme : ... expr = valeur
|
Exemple d'utilisation de la procédure d'impression
formattée :
int nombre
= 0;
float value = 0;
Filetext ft = fopen ("result.log", "w");
........................................................;
fprintf (ft, "Nombre d'iterations : %d, le résultat
vaut
%g \n", nombre, value);
fclose (ft);
|
Le principe de l'écriture formattée (en C, Fortran) est
le suivant :
- Un format est une chaîne de caractères contenant
quelques caractères spéciaux.
- Le programme recopie la chaîne caractère par
caractère jusqu'à rencontrer un de ces caractères :
- Un % permet d'insérer une valeur figurant dans les
arguments de la fonction.
Voici les principaux métacaractères :
Metasymbole
|
Signification
|
| \n |
Saut de ligne. A placer
génératement en fin de format
|
\t
|
Caractère de tabulation
|
\"
|
Pour placer une quote |
| %d |
Ecriture d'un entier |
| %g |
Ecriture d'un réel |
| %s |
Ecriture d'une chaîne de
caractères |
%c
|
Ecriture d'un caractère
|
%%
|
Pour écrire la
caractère %
|
Retour
au début
Les fonctions et les
procédures
Généralités
On a vu comment appeler une fonction.
On va voir comment en définir une.
Il suffit d''associer à un entête de fonction un corps de
fonction délimité entre accollades. La valeur esr
retournée au programme principal au moyen du mot-clé return.
- // C++ : Un appel de fonction
- #include "definitions.h"
- // ====================================
Fonction Moyenne
- float moyenne (float
a, float b)
- {
- float
moy = (a+b)/2;
- return
moy;
- }
- // ==================================== Programme
principal
- main ()
- {
- float x
=10;
- float y
= 15;
- float
m = moyenne (x,y);
- Display (m);
- }
|
L'entête de la fonction,
contrairement au prototypene se termine
pas par un
point-virgule, mais est
immédiatement suivie du
corps
de la fonction.
Lors d'un appel de fonction
(ligne 14) la démarche est la suivante :
- Les arguments effectifs ( x est y) du programme principal
sont copiés, aprés une éventuelle
conversion, à la place des arguments formels
(a et b) de la fonction.
- On déroule les instructions de la fonction (ligne 4)
et la valeur calculée est retournée au programme appelant.
- Les différentes fonctions d'un logiciel peuvent
être dans des fichiers séparés. Il est
nécessaire qu'un programme appelant connaisse les entêtes
des fonctions utilisées. En général on les
regroupe dans des includes.
- Si la fonction est définie dans le même fichier
avant la fonction appelante, il n'est pas nécessaire d'utiliser
un prototype.
Arguments variables
En C/C++, les arguments sont copiés. On parle de
passage d'argument par valeur.
Aussi, si l'on modifie l'un d'entre eux, c'est leur
copie qui est modifiée.
Alors que les arguments effectifs du programme appelant sont
inchangés.Pour pouvoir modifier les arguments, on doit effectuer
un
passage d'argument par adresse
(ou par référence) au moyen de l'opérateur & :
- // C++ : Passage d'arguments par référence
- #include "definitions.h"
- // ==================================== permuter
- Procedure permuter
(float
&gauche, float &droite)
- {
- float
tampon = gauche;
- gauche = droite;
- droite = tampon;
- }
- // ==================================== Programme
principal
- main ()
- {
- float
x =10;
- float
y = 15;
- permuter (x,
y);
- Display (x);
- Display (y);
- }
|
Hormis l'opérateur & devant les arguments formels, rien ne
change. Le compilateur gère les adresses des variables dans
le programme appelant et le programme appelé. Contrainte : On ne
peut passer des constantes ou des expressions par adresse, et on ne
peut effectuer de conversion d'argument. Si un argument passé
par adresse est de type réel, on ne peut lui fournir une
variable entière. Exemples :
// C++ : Passage d'arguments par référence : erreurs
#include "definitions.h"
Procedure permuter (float
&gauche, float &droite);
main ()
{
float
x = 10;
const float
x = 12;
int
n = 15;
permuter (x, n);
// Incorrect, n est une variable entière
permuter (x, y);
// Incorrect, y est une constante
permuter (x, 3.14); // Idem
permuter (x, x+1);
// Incorrect, x+1 est une expression
} |
En Fortran 77, tous les passages
d'argument se font par adresse. en C Ansi, le passage par adresse se
fait au moyen de pointeurs, ce qui alourdit un peu les notations. En
Pascal, c'est le mot clé var qui
remplace l'opérateur &.
Tableaux passés en argument
En C/C++ (dans les autres langages aussi d'ailleurs) un tableau est
équivalent à l'adresse du premier élément
de ce tableau.
L'opérateur & n'est donc pas utile. L'instruction
appel_fonction(table) est
équivalente à
appel_fonction(&table[0]).
Il n'est pas non plus nécessaire de préciser sa
dimension.. En effet, si le
tableau est
déclaré
dans le programme appelant
et
dans la fonction, il n'est
défini
(associé à un emplacement mémoire)
que dans le programme
appelant. Exemple :
// C++ : Exemple de boucle
#include "definitions.h"
// ========================== main
int remplir (int nombre, int table[])
{
for (int ii=0; ii<nombre; ii++)
{
table[ii] = ii*ii;
}
}
// ========================== main
int main ()
{
const int size =
10;
int table
[size];
remplir (size, table);
Display (table[5]);
}
|
.
Variables rémanentes (static)
En général, lors d'un appel de fonction, les variables de
cette fonction sont réinitialisées. On dit qu'une
variable est rémanente si
sa valeur est mémorisée d'un appel à l'autre. On
s'en sert par exemple pour implémenter un compteur d'appels.
Exemples :
// C++ : Passage d'arguments par
référence : erreurs
#include "definitions.h"
// ================================ cumuler
float cumuler (float
ajout)
{
static float
bilan = 100;
static bool
premiere_fois = true;
if (premiere_fois)
{
premiere_fois = false;
printf ("Premier
appel : somme initiale = %g Euros\n", bilan)
}
bilan = bilan + ajout;
return bilan;
}
// ================================ main
main ()
{
bilan (30);
bilan (-20);
bilan (10);
} |
En Fortran 77, c'est l'instruction
save qui permet de préserver la valeur d'une variable d'un appel
à l'autre.
Exemple de fonctions : calcul du pgcd
Lorsqu'on appelle une fonction dans une même fonction, on
parle de fonction récursive.
(impossible en Fortran 77).
Exemple : (fichier pgcd.cpp)
// C++ : Exemple de fonction récursive
#include "definitions.h"
// ======================================== calculer_pgcd
int calculer_pgcd (int v1, int v2)
{
if (v1<=0 Or v2<=0)
return 0;
if
(v1==v2)
return v1;
int pgcd;
if (v1<v2) pgcd = calculer_pgcd
(v1, v2-v1);
else pgcd =
calculer_pgcd (v1-v2, v2);
return pgcd;
}
// ======================================== main
int main ()
{
int v1 =
7*5*11*13;
int v2 =
7*2*2*6*7;
Display (v1);
Display (v2);
float pgcd =
calculer_pgcd (v1, v2);
Display (pgcd);
}
|
Retour
au début
Portée des variables
Dans un même fichier
Considérons un fichier source contenant plusieurs fonctions. En
résumé :
- Chaque fonction dispose de ses propres variables et ne peut
accéder aux variables locales des autres fonctions que par
passage d'arguments.
- On peut déclarer des variables à l'intérieur
des blocs. On acccède
aux variables du bloc courant et des blocs
englobants.
- Il est possible cependant de définir des données globales à plusieurs
fonctions. On l'a vu lors des déclarations de types (structures ou
énumérations) ou de constantes
en tête de fichier. On peut aussi
déclarer des variables globales.
On leur attribue le qualificatif Global
(notation spécifique à ce cours). En C/C++ c'est l'attribut static qui
est utilisé.
|
//
C++ : Gestion d'une liste, variables globales
#include "definitions.h"
const int pas_trouve = -1;
enum enum_errr { OK, ERR, ERR_TROP, ERR_DEJALA};
const int max_eleves
= 20;
Global Cstring tab_noms [max_eleves];
Global float tab_notes [max_eleves];
Global int nbr_eleves
= 0;
// ============================================ trouver_eleve
int trouver_eleve (Cstring nom)
{
for (int nro=0; nro<nbr_eleves; nro++)
if (Cequals (nom,
tab_noms[nro]))
return nro;
return pas_trouve;
}
// ============================================ ajouter_eleve
int ajouter_eleve (Cstring nom, float note)
{
if (nbr_eleves>=max_eleves) return ERR_TROP;
int nro = trouver_eleve (nom);
if (nro>=0)
return ERR_DEJALA;
tab_noms [nbr_eleves] = NewString (nom);
tab_notes [nbr_eleves] = note;
nbr_eleves = nbr_eleves + 1;
return OK;
}
// ============================================ imprimer_liste
Procedure imprimer_liste ()
{
Print (".... Impression de la liste");
Display (nbr_eleves);
for (int nro=0; nro<nbr_eleves; nro++)
{
printf (" %s
: %g\n", tab_noms[nro], tab_notes[nro]);
}
}
// ============================================ saisir_notes
Procedure saisir_notes ()
{
const int size_rep = 63;
char reponse [size_rep+1];
while (true)
{
int lg =
ReadString
(reponse, "Entrez un nom : ", size_rep);
if (lg<=0) return;
float note =
ReadReal
("Sa note ? ");
ajouter_eleve
(reponse, note);
}
}
// ============================================ Main
int main ()
{
saisir_notes ();
imprimer_liste ();
}
|
Dans plusieurs fichiers (variables externes)
Il est possible d'accéder à la même variable
à partir de plusieurs fichiers source. On leurs associe
l'attribut extern. Si la
déclaration multiple de la même variable dans plusieurs
fichiers source ne pose pas de problème, il est cependant
nécessaire que sa définition,
c'est à dire son emplacement mémoire soit unique. Exemple, premier fichier :
- // C++ : Premier fichier
- #include "definitions.h"
-
- extern String son_nom;
- .....................................................................................
|
Second fichier :
- // C++ : Second fichier
- #include "definitions.h"
-
- extern String son_nom;
- String
son_nom = "Wolfgang
Amadeus";
// Valeur initiale
- ......................................................................................
|
- La variable son_nom est
déclarée à
la ligne 4 du 1er et du 2ème fichier.
- Elle est définie à
la ligne 4 du second fichier.
L'attribut Global utilisé
précédemment permet d'autoriser des variables homonymes
dans plusieurs fichiers sans qu'elles n'interfèrent. C'est une
protection.
Les variables externes ne sont pas
indispensables
Utiliser des variables externes
comporte quelques risques. En fait, une variable externe peut
être modifiée dans n'importe quel programme. Ce qui dans
un logiciel de 300 000 lignes peut effrayer certains chefs de projet
paranoïaques. Dont votre serviteur fait partie.
On peut toujours remplacer une variable externe par une variable globale déclarée dans un seul fichier
à laquelle on associe une fonction
d'interrogation (ligne 4), éventuellement une fonction de
modification (ligne 11).
Exemple, premier fichier :
- // C++ : Premier fichier
- #include "definitions.h"
-
- Global String son_nom
= get_his_name ();
- .....................................................................................
- set_his_name ("Ludwig Von");
|
Second fichier :
- // C++ : Second fichier
- #include "definitions.h"
-
- Global String son_nom
= "Wolfgang Amadeus"; //
Valeur initiale
- // ============================== Interrogation
- String
get_his_name ()
- {
- return
son_nom;
- }
- // ============================== Modification
- Procedure set_his_name
(String hisname)
- {
- son_nom =
hisname;
- }
- ......................................................................................
|
Retour
au début
Les pointeurs et
l'allocation programmée
Allouer une variable consite
à associer une adresse de variable
à un emplacement
mémoire fourni par le système
Lors des déclarations de variables (structures ou tableaux dont
on connait la taille), cette allocation est statique car la taille du tableau
est connue à la compilation. Or on peut souhaiter
dimensionner les
tableaux ou créer des variables au moment de
l'exécution. C'est le mécanisme
d'allocation programmée ou
d'allocation dynamique.
Mettre en place une allocation dynamique se fait en
plusieurs étapes :
- déclarer une adresse de variable, aussi
nommée pointeur,
- lui fournir un emplacement mémoire disponible, c'est
à dire définir ce pointeur,
- manipuler la variable référencée par de
pointeur.
Allocation
dynamique de structures
Voyons sur un exemple la démarche appliquée à une
structure :
- struct str_complexe
{
-
float reel;
-
float imag;
-
};
-
// Déclaration
d'un
pointeur d'un type donné
- str_complexe* adrz = NULL
;
-
// Allocation d'une variable
- adrz = new str_complexe;
-
// Utilisation d'une variable allouée
- *(adrz).reel =
1;
-
// notation
condensée : accès à un champ
-
// à partir d'un adresse de structure
- adrz->imag =
1;
-
// On libère la
mémoire :
- delete adrz;
- adrz = NULL;
// Pour éviter
d'accéder à un emplacement
mémoire illicite
|
Quelques explications :
- L'instruction 1 est une déclaration
de structure. Ce qui suit est néanmoins valable pour tout type
de donnée.
- L'instruction 6 est la déclaration
du pointeur
adrz. Un pointeur est typé. A tout pointeur, on peut associer la
valeur
particulière NULL qui signifie que l'adresse n'est pas
renseignée.
- L'instruction 8 associe à cette
adresse (opérateur new)
un emplacement mémoire disponible fourni par le système. En C c'est la fonction malloc() qui
remplit ce rôle.
- L'instruction 10 montre comment on
accède à la valeur dédignée par l'adresse.
- L'instruction 13 est équivalente
à l'instruction 10 sous une forme allégée.
- Enfin, une fois que la variable
allouée a rempli son rôle dans un logiciel, on la
libère (instruction 15) au moyen de l'opérateur delete. Le système
considère alors que la mémoire
désignée par l'adresse est disponible. En Java L'opérateur delete n'existe
pas. C'est le système qui libère automatiquement la
mémoire allouée devenue inaccessible. On appelle ce
mécanisme garbage collector ou ramasse miettes.
- Après une libération, le
pointeur adrz n'a plus de sens : la mémoire
désignée n'est plus disponible et peut être
attribuée lors du prochaine allocation. On peut donc associer
à adrz la valeur NULL.
Si la notation
étoilée vous pose des problémes, vous pouvez
toujours définir des symboles :
#define Pointerof(type)
type*
#define Valueof(addr)
*addr
|
Allocation dynamique de tableaux
Exemple pour
un tableau :
-
// Déclaration
d'un
pointeur
- int* tabdyn = NULL ;
-
// Saisie d'une valeur entière
- int taille
= ReadInteger ();
- if (taille < 2)
taille = 2;
-
// Allocation dynamique du tableau
- tabdyn= new
int [taille];
-
// Utilisation d'une variable
allouée.
- for (int nro=0; nro<taille; nro++)
-
tabdyn [nro] = 0;
-
// Libération
de la mémoire :
- delete
[] tabdyn;
|
Quelques commentaires :
- Instruction 2 : une adresse de tableau est équivalente
à l'adresse du premier élément de ce
tableau.
- Instruction 7 :par rapport à l'allocation simple, on
indique à l'opérateur new le nombre d'éléments
à allouer.
- Instruction 9 : il n'y a pas de différence entre la
manipulation d'un tableau alloué statiquement ou dynamiquement .
- Instruction 12 : Il est important
de rappeler les [] lors de la
libération de la mémoire.
Remarques :
- En C/C++, un tableau est
équivalent au pointeur désignant le premier
élément du
tableau.
- Le type Cstring,
utilisé dans le paragraphe des
chaînes de caractères est en fait équivalent
à char*
- En C les opérateurs
new/delete sont remplacés par deux fonctions : malloc et free.
Exemple
: gestion d'une liste chaînée
Ce programme (
liste_fifo.cpp) est une
application directe de l'allocation dynamique. Il
s'agit de créer une liste "fifo" ( First In, First Out, c'est
à dire qu'on place le dernier arrivé en fin de
liste).
- // C++ : Gestion d'une liste chaînée
- #include "definitions.h"
- struct str_maillon
- {
- Cstring nom;
- str_maillon* suivant;
- };
- // =========================================== creer_liste
- str_maillon* creer_liste ()
- {
- const
int size_rep = 63;
- char reponse
[size_rep+1];
- str_maillon* tete = NULL;
- str_maillon* queue = NULL;
- while (ReadString
(reponse, "Entrez un nom : ", size_rep)>0)
- {
-
// Creation
d'une copie de chaine
- int lg = strlen (reponse);
- Cstring name = new char [lg];
- strcpy
(name, reponse);
-
str_maillon*
elt = new str_maillon;
-
elt->nom = name;
-
elt->suivant = NULL;
- if (
tete==NULL)
tete
= elt;
-
else
queue->next = elt;
- queue =
elt;
- }
- return tete;
- }
- // ========================================== imprimer_liste
- Procedure imprimer_liste
(str_maillon* tete)
- {
- Print (".... Impression de la liste");
- for (str_maillon* elt=tete; elt!=NULL;
elt=elt->suivant)
- Print (elt-> nom);
- }
- // ========================================== detruire_liste
- Procedure detruire_liste
(str_maillon* tete)
- {
- Print ("....Destruction de la liste");
- str_maillon* next = NULL;
- for (str_maillon* elt=tete; elt!=NULL;
elt=next)
- {
- next = elt->suivant;
- delete elt-> nom;
- delete elt;
- }
- }
- // ========================================== Main
- int main ()
- {
- str_maillon* tete = creer_liste ();
- imprimer_liste (tete);
- detruire_liste (tete);
- }
|
Utilisation de pointeurs dans la passage d'arguments variables
On a vu que pour pouvoir modifier un argument d'une procédure,
il fallait indiquer dans l'en-tête de la procédure que
l'on effectuait un passage par adresse au moyen de l'opérateur
& (ligne 4).
- // C++ : Exemple de fonction récursive
- #include "definitions.h"
- // ======================================== motif_value
- Procedure motif_value (int &value)
- {
- value = 1515;
- }
- // ======================================== main
- int main ()
- {
- int v1 = 0;
- modif_value (v1);
- Display (v1);
- }
|
On peut obtenir un effet équivalent au moyen d'un pointeur.
- // C++ : Exemple de fonction récursive
- #include "definitions.h"
- // ======================================== motif_value
- Procedure motif_value (int* value)
- {
- *value = 1515;
- }
- // ======================================== main
- int main ()
- {
- int v1 = 0;
- modif_value (&v1);
- Display (v1);
- }
|
Quelques commentaires :
- En ligne 4 on déclare un pointeur sur le type
concerné.
- Ligne 6 : on modifie la variable référencée
par le pointeur;
- Ligne 12 : l'argument de la fonction étant uun pointeur,
on passe en argument l'adresse de la variable au moyen de
l'opérateur &.
Cela nécessite plus de manipulations que
la technique précédente. C'était cependant la seule
disponible en C.
Vous pouvez
toujours définir des symboles :
#define Pointerof(type)
type*
#define Valueof(addr)
*addr
#define Addrof(var)
&var
|
Retour
au début
Aspect numérique
Conversion
de
types (entiers et réels)
Quand des
opérandes de types différents interviennent
dans une expression, elles sont toutes converties en un seul type
suivant les principes suivants :
- Les types sont ordonnés : par
exemple le type réel est considéré comme plus
élevé que le type entier.
- La conversion d'un réel
en
entier provoque une troncature de la partie décimale du
réel. Il y a dégradation de l'information. Il est
possible que le compilateur place un avertissement sur la ligne
incriminée.
- Pour éviter que le compilateur ne
réagisse, utiliser la fonction de conversion n = Integer(x).
- Avant l'exécution d'une
opération, les deux opérandes sont converties dans le
type le plus élevé le résultat étant
calculé dans ce type.
- A la fin du calcul de l'expression, le
résultat est converti dans le type de la variable de gauche.
Exemples :
|
Resultat
|
Commentaire
|
| int valeur = 3.14; |
3
|
Le réel est
tronqué.
|
float valeur = 3;
|
3.0
|
L'entier est converti en
réel.
|
float a=6;
float b = 16;
float valeur = a/10 + b/10;
|
2.2
|
a/10 vaut 0.6, b/10 vaut 1.6. La somme vaut 2.2.
|
float a=6;
float b = 16;
int valeur = a/10 + b/10; |
2
|
La somme
précédente est tronquée à l'entier
inférieur. .
|
int
a = 6;
float b = 16;
float valeur = a/10 + b/10; |
1.6
|
a/10 est tronqué à 0 (division entière), b/10 vaut
1.6 ;
|
int
a = 6;
float b = 16;
int valeur = a/10 + b/10; |
1
|
La somme
précédente est tronquée à l'entier
inférieur le plus proche. |
int a = 6;
int b = 16;
int valeur = a/10 + b/10; |
1
|
a/10 est tronqué à0, b/10 est tronqué à1 ;
ce sont des divisions entières. |
Particularité
des réels
Le traitement spécifique des
nombre flottants occupe une grande
partie du cours d'Analyse numérique.
Si un dépassement de capacité ou
une division par zéro provoquent une erreur fatale pour les nombres
entiers, ce n'est pas le cas pour les nombres réels. Une norme
(IEEE) a
associé au type flottant certaines valeurs
particulières (+Infini, -Infini, Indeterminé (ou
Undefined ou Not a
number ou NaN)). On a défini une
arithmétique spécifique qui permet de poursuivre le
calcul. Par exemple Infini+infini = Infini, Infini-Infini=NaN, 1/Infini
= 0, .... Charge au
programmeur de vérifier si le résultat final est un
nombre réel ou l'une de ces 3 pseudo valeurs.
L'addition de réels peut provoquer des
troncatures. Soit y = a +
b. Si a est très grand par rapport à b, la valeur de b
est ignorée, et à l'arrivée y et a ont la
même valeur.
Les égalités de réels se
ramènent souvent
à un encadrement : x <= y + epsilon et x >= y - epsilon.
Retour
au début
Organisation des fonctions dans un
logiciel
On le redit : l'appel de fonction est l'instruction la plus importante.
Elle permet en une ligne d'effectuer un
traitement qui
pourrait éventuellement avoir été
réalisé en une ou plusieurs
milliers de lignes. L'appel de fonction permet (devrait permettre) de
regrouper les instructions d'un logiciel en des programmes de taille
raisonnable qui devraient donc rester clairs et lisibles.
Si l'on développe un logiciel au sein d'une équipe, ou si
l'on souhaite réutiliser certains développements, ou tout
simplement si l'on souhaite être capable de relire ce que l'on a
écrit avant son départ en vacances, il convient de
réfléchir à l'organisation des fonctions
Les différentes fonctions d'un logiciel sont le plus souvent
regroupées par catégorie dans des fichiers
séparés. Chaque composante concerne un
thème particulier et agit
sur des
données spécifiques. On parle de programmation
modulaire.
Chaque composante du logiciel agit sur une autre au moyen de fonctions.
Les prototypes de ces fonctions sont contenus dans un
include. Les
spécifications de ces fonctions constituent la partie
"visible" ou "publique" d'une composante. Les données internes
d'une composante sont "privées" et inaccessibles aux autres
composantes. On parle aussi d'
encapsulation
des données.
La programation dite "objet" n'est que le prolongement de la
programmation modulaire. On y ajoute le concept d'"héritage", et
on y regroupe fonctions et données dans des "classes".
C++ et Java permettent la mise en oeuvre des concepts objet. Dans ce
cours, même si on utilise des syntaxes C++, on n'abordera pas la
programmation dite "objet" Pour cela on pourra se référer
à l'(excellent) ouvrage de Claude Delanoy. "
Programmer en C++". (Editions
Eyrolles).
Néanmoins les principes de modularité et d'encapsulation
sonr réalisables dans n'importe quel langage. Y compris en
Fortran 77. Le but est d'écrire des programmes plus faciles
à maintenir, et de confier leur développement à
des spéialistes.
Retour
au début
Bibliographie
Ce cours n'est qu'une initiation à la programmation. Pour en
savoir plus sur les "vrais" langages, je vous renvoie à des
ouvrages spécialisés.
Le
Langage C ANSI.
Philippe Drix.
Editions Dunod.
Philippe Drix part de Pascal et arrive à C
Programmer
en langage C++. Claude Delanoy
Editions Eyrolles.
Claude Delanoy part de C et arrive
à C++
Programmer
en langage Java. Claude Delanoy
Editions Eyrolles.
Cette fois-ci le point de comparaison est
C++
Claude Delanoy a écrit d'autres ouvrages sur le C, Fortran
90. N'hésitez pas à les consulter.
Mais le plus important n'est pas tant de connaître totalement les
subtilités d'un langage. C'est même secondaire par rapport
à la connaissance des algorithmes les plus
fréquents.
Algorithmes
en langage C Robert Sedgewick
Editions Dunod .
Retour
au début