Translate

الجمعة، 29 نوفمبر 2013

Arbre binaire

                                                         

1
VI- Arbre binaire


Par définition un arbre binaire est un arbre avec une racine, et où chacun des noeuds
possède :
· soit aucun successeur,
· soit un successeur, à gauche ou à droite,
· soit deux successeurs.
Voici un exemple d’arbre binaire, dessiné avec sa racine en haut et ses branches en
descente, à l’inverse d’un arbre dans la nature :
On peut aussi dessiner cet arbre (à droite ci-dessus) en considérant que chaque noeud
est muni de deux branches pendantes auxquelles viennent s’accrocher, ou pas, d’autres
noeuds.
Un arbre binaire peut aussi être réduit à l’arbre vide. Pour comprendre l’importance de
l’arbre vide, voici comment on peut donner une définition récursive tout à fait cohérente
d’un arbre binaire : il s’agit soit de l’arbre vide, soit d’un noeud racine auquel sont
accrochés un arbre à gauche et un arbre à droite aussi. Si l’on oubliait le cas de l’arbre
vide, la définition perdrait toute signification : notamment l’arbre ne pourrait avoir
aucune feuille, c’est-à-dire un noeud auxquels sont accrochés deux arbres vides.
L’arbre avec ses sous-arbres, notamment ses arbres
vides V
On définit aussi ce que l’on appelle un arbre binaire plein. Outre l’arbre vide, il
s’agit d’un arbre avec une racine, et tel que chaque noeud possède soit aucun, soit deux
successeurs, l’un à gauche et l’autre à droite. On distingue alors deux types de noeuds :
les noeuds internes, qui ont deux successeurs, et les noeuds terminaux, sans successeurs.
On vérifie qu’il y a toujours un noeud terminal de plus que de noeuds internes 1.
1 Démontrons cette propriété. Appelons n le nombre de noeuds internes. Lorsque l’arbre est
tel que n=1, il y a deux noeuds terminaux, et la propriété est vraie pour n=1. Supposons que la
propriété soit vraie pour un certain nombre n de noeuds internes. Et montrons qu’elle le reste
pour n+1 noeuds, à savoir qu’il y a un noeud terminal de plus. En effet le passage de n noeuds
internes à n+1 noeuds internes se fait en transformant un noeud terminal en noeud interne avec ses
deux noeuds terminaux, ce qui maintient la différence de 1 entre le nombre des noeuds terminaux
et celui des noeuds internes.

2
Un arbre binaire rempli avec 11 noeuds, dont 5 internes et 6
terminaux
Il existe un lien étroit entre un arbre binaire et un arbre binaire plein. On peut en
effet associer à tout arbre binaire, avec ses n noeuds, un arbre binaire rempli avec les
mêmes n noeuds qui deviennent maintenant des noeuds internes, et n+1 noeuds terminaux
ajoutés au-dessous d’eux. Ce processus est réversible: il suffit d’enlever les noeuds
terminaux ou de les rajouter pour passer d’un type d’arbre à l’autre.
Un arbre binaire et l’arbre binaire plein correspondant
Nombre d’arbres binaires à n noeuds
Appelons c(n) le nombre d’arbres binaires à n noeuds (ou encore avec n noeuds
internes et n+1 noeuds terminaux, en prenant les arbres pleins). A cause de l’arbre vide,
on a c(0)=1. Donnons-nous n>0. Un noeud est la racine. Il reste à placer les n-1 noeuds
restants dans le sous-arbre gauche et dans le sous-arbre droit, de toutes les façons
possibles. Soit on ne place aucun noeud à gauche (sous-arbre vide), ce qui fait c(0)=1
façon, et n-1 noeuds dans le sous-arbre droit, avec c(n-1) possibilités, ce qui fait au total
c(0) c(n-1) cas. Soit on place un noeud à gauche, et n-2 noeuds à droite, ce qui fait c(1)
c(n-2) cas. Plus généralement, on place k noeuds à gauche, d’où c(k) sous-arbres
gauches, et n-1-k noeuds à droite, d’où c(n-1-k) sous-arbres droits, ce qui fait au total
c(k) c(n-1-k) arbres de la sorte. Cela vaut pour toutes les valeurs de k de 0 à n-1.
Finalement le nombre d’arbres ayant n noeuds est :
c(n) = c(0) c(n-1) + c(1) c(n-2) + … + c(k) c(n-1-k) + … + c(n-1) c(0).
Cette formule de récurrence, jointe à la condition initiale c(0)=1, donne les nombres
c(n) de proche en proche pour tout n. On appelle les nombres c(n) nombres de Catalan.
A partir de c(0) on trouve la succession 1, 1, 2, 5, 14, 42, 132, …

[N+1] vide au départ.
C[0]=1 ; afficher
for(n=1 ; n<=N ; n++)
{ cumul=0 ;
for(k=0 ;k<n ; k++) cumul += C[k]*C[n-1-k] ;
C[n]=cumul ; afficher
}




Le tri par arbre binaire ou tri des bijoutiers


Il s’agit là d’un exemple où l’arbre binaire doit être fabriqué noeud après noeud, par
le programme lui-même. Au départ il s’agit d’un banal problème de tri, plus précisément
le tri des bijoutiers, appelé aussi tri rapide, ou quick sort en anglais. Pour trier un tas de
diamants, le bijoutier utilise un tamis qui permet de séparer le tas initial en deux tas. Et
il recommence avec chacun des deux tas. La bonne qualité du tri dépend alors des tamis
utilisés à chaque étape, de façon que chaque tas soit séparé en deux parties à peu près
égales. Si les mailles des tamis sont trop grandes ou trop petites, le tri n’aura rien de
rapide. En termes informatiques, cela se traduit de la façon suivante. On part d’une liste
de nombres à trier. On prend le premier nombre, qui constitue la racine de l’arbre que
l’on va créer. Ce noeud, comme tous ceux qui vont suivre, est dessiné avec deux
branches pendantes. Puis on prend le deuxième nombre. Selon qu’il est plus petit ou
plus grand que le premier, on l’accroche sous la racine à gauche ou à droite. Et l’on
continue de la même façon. Chaque nombre est descendu dans l’arbre, en allant à
gauche s’il est plus petit que le nombre du noeud où il passe, et à droite s’il est plus
grand. Il prend finalement la première place vide sur laquelle il tombe. Par exemple :
________________________

Il s’agit bien du tri des bijoutiers puisqu’à gauche de chaque noeud de l’arbre se
trouvent les nombres plus petits, et à sa droite les nombres plus grands. L’arbre
Sous réserve que l’arbre soit dessiné comme ci-dessus (en diminuant l’angle entre
les branches pendantes lors de la descente) on constate qu’n projetant l’arbre sur une
ligne horizontale, comme si on l’écrasait sur le sol, les nombres sont dans l’ordre
croissant : 1 3 4 5 7 8 9. Mais comme cette opération de projection n’est pas simple à
réaliser sur ordinateur, on va chercher une lecture des nombres en ordre croissant par un
certain parcours de l’arbre.


Les trois parcours d’un arbre


Pour parcourir un arbre de la racine jusqu’au retour à la racine, on dispose du
chemin indiqué ci-dessous :

On constate que ce chemin passe trois fois le long de chaque noeud, soit en descente,
soit au-dessous, soit lors de la remontée. Si l’on note les nombres situés dans les noeuds
lors de la descente, on obtient le parcours dit préfixé : 7 3 1 5 4 9 8 avec la racine en
premier, puis la racine du sous-arbre gauche, etc. Lorsqu’on note les nombres quand on
passe en-dessous des noeuds, on a le parcours dit infixé : 1 3 4 5 7 8 9 avec la racine 7 au
centre, ayant à sa gauche le sous-arbre gauche et à droite le sous-arbre droit. C’est ce
parcours qui donne les nombres en ordre croissant. Enfin si l’on note les nombres lors de
la remontée on a le parcours dit postfixé : 1 4 5 3 8 9 7 avec la racine en dernier. Nous
verrons que la programmation de ces parcours est simple.

Comment fabriquer l’arbre ?

Cela se fait en deux étapes. D’abord on met en place la racine. Puis on insère les
autres cellules une à une, en utilisant un crochet que l’on fait descendre dans l’arbre, en
allant à gauche ou à droite selon que le nombre à placer est plus petit ou plus grand que
les nombres déjà placés dans les cellules. Quand le crochet a son extrémité basse qui
tombe sur une place vide, c’est là que va être placée la nouvelle cellule avec son
nombre. D’où le programme :
/* les nombres à trier sont supposés placés dans un tableau a[N] */
/* déclaration de la cellule de base */
struct cell { int n ; struct cell * g ; struct cell * d ;} ;
/* mise en place de la racine */
racine= (struct cell *) malloc (sizeof (struct cell)) ; racine->n=a[0]; racine->g=NULL;
racine->d=NULL;
/* insertion progressive des nombres dans l’arbre */
for(i=1 ;i<N ;i++)
{ e1= NULL; e2=racine; /* mise en place du crochet (e1 e2) en haut de l’arbre */
/* la nouvelle cellule */

ptr==(struct cell*)malloc (sizeof (struct cell)) ; ptr->n= a[i]; ptr->g=NULL; ptr-
>d=NULL;
while (e2!=NULL) /* on descend le crochet dans l’arbre */
{ e1=e2; if (a[i]<e2->n) e2=e2->g; else e2=e2->d; }
if (a[i]<e1->n) e1->g = ptr; else e1->d = ptr; /* on accroche la cellule pour de bon */
}

Insertion de la cellule 5 dans l’arbre en construction
Il reste maintenant à afficher le résultat, à savoir les nombres triés. Pour cela on
utilise une fonction de parcours de l’arbre, que l’on lance à partir de la racine, soit
parcours(racine). Cette fonction se rappelle sur chacun des noeuds r de l’arbre :
void parcours( struct cell * r)
{ if (r !=NULL) { parcours (r->g); printf(“%d”, r->n); parcours(r->d); }
}
Les noeuds sont affichés entre les parcours des sous-arbres gauche et droite. Il s’agit
du parcours dit infixé de l’arbre, qui donne les nombres triés du plus petit au plus grand.
Remarquons que si l’on plaçait l’affichage non plus entre les deux parcours des sousarbres
gauche et droit, mais avant, on aurait le parcours préfixé, et si on le plaçait à la fin
on aurait le parcours postfixé.

Dessin de l’arbre


On peut aussi afficher l’arbre sur l’écran en gérant les coordonnées de chaque
cellule. A chaque étape du parcours de l’arbre, lorsque se produit l’affichage du nombre
r->n, il suffit d’augmenter l’abscisse d’une quantité constante (notée pasx). Cela
correspond à la projection de l’arbre sur le sol comme nous l’avions dit auparavant. Et à
chaque descente dans l’arbre lors de sa fabrication, l’ordonnée augmente aussi d’une
quantité constante (pasy). Il convient aussi de conserver le prédécesseur de chaque noeud
afin de pouvoir tracer les branches de jonction.

La cellule de base correspondant à un noeud contient maintenant des composantes
supplémentaires: ses coordonnées abs et ord, ainsi qu’un pointeur pred vers son
predecesseur.

struct cell { int n; struct cell * g; struct cell * d;int abs; int ord; struct cell * pred;};
main()

{ x=10;

randomize(); for(i=0;i<N;i++) a[i]=random(100);/* tableau de nombres pris au hasard*/

for(i=0;i<N;i++)
{ ptr=(struct cell*) malloc(sizeof(struct cell)); ptr->n=a[i]; ptr->g=NULL; ptr->d=NULL;

if (i==0) {racine=ptr;racine->pred=NULL; racine->ord=10;}
else { e1=NULL; e2=racine; y=10;
while (e2!=NULL) 
{ e1=e2;
 if (a[i]<e2->n) e2=e2->g; else e2=e2->d; y+=pasy; }
if (a[i]<e1->n) e1->g=ptr; else e1->d=ptr;
ptr->pred=e1; ptr->ord=y;
 }
   }
calculabs(racine); parcours(racine);
  }

void parcours(struct cell * r)
{ if (r!=NULL)
{ parcours (r->g);
afficher r->n en (r->abs, r->ord); if (r!=racine)  line(r->abs,r->ord r->pred->abs,r->pred->ord);

parcours (r->d);
}
}

void calculabs(struct cell * r)
{ if (r!=NULL) { calculabs(r->g); x+=pasx;r->abs=x; calculabs(r->d); }}
Notons que nous avons fait deux parcours de l’arbre, dont le premier appelé
calculabs détermine seulement les abscisses des noeuds.En effet si l’on se contenant du
seul parcours de l’arbre en y intégrant le calcul des abscisses, tout en dessinant le lienentre le noeud concerné et son prédécesseur, on n’aurait pas forcément à sa disposition
l’abscisse du prédécesseur, car celle-ci est parfois calculée plus tard dans la fonction
parcours.



Exercice : Insertion, recherche, et suppression dans un
arbre binaire



A partir de nombres que l’on se donne, on va construire l’arbre binaire
correspondant, comme auparavant, mais en mettant en place les fonctions classiques
associées, celles qui permettent d’insérer un nombre dans l’arbre, de supprimer un
nombre en cas de besoin, et de chercher si un nombre est présent ou non dans l’arbre.
Dans un contexte plus réaliste, on serait amené à entrer les nombres au coup par coup
dans l’arbre. Ici, on va simplement se donner un tableau a[N] de N nombres, puis insérer
dans l’arbre les nombres successifs de ce tableau.



1) Construire une liste de N nombres pris au hasard, et tous différents


Pour remplir la case numéro i du tableau a[N], on tire un nombre h au hasard, et on
recommence le tirage tant que ce nombre h est déjà placé dans la partie du tableau
précédemment remplie (de la case 0 à la case i – 1).
void creationtableau(void)
{ int i,j,h,dejafait;
srand(time(NULL));
for(i=0;i<N; i++)
{ do
{ dejafait=NON; h=rand()%(3*N); /* un nombre entre 0 et 3N – 1 */
for(j=0;j<i;j++) if(a[j]==h) {dejafait=OUI; break;}
}
while( dejafait==OUI);
a[i]=h;
}
for(i=0;i<N;i++) printf("%d ",a[i]);}


2) Fabriquer la fonction permettant d’insérer un nombre k dans l’arbre



Il existe deux cas, soit l’arbre est vide et l’on crée la racine de l’arbre pour y placer le
nombre k, soit l’arbre n’est pas vide, et l’on descend dans l’arbre : en passant d’un noeud
à son fils gauche si le nombre k est inférieur au nombre situé dans le noeud ) ou sinon à
droite, jusqu’à trouver une place libre pour la cellule contenant le nombre k. On reprend
pour cela le programme donné au début de ce chapitre (comment fabriquer l’arbre), en
utilisant une sorte de crochet d’extrémités haute et basse e1 et e2, descendant dans
l’arbre. Quand le pointeur e2 tombe sur une place vide (NULL), on accroche la cellule
contenant k au bout de la branche pendante sous la cellule où pointe e1, celle de droite
ou celle de gauche selon la valeur de k.
void inserer(int k)
{
if(racine==NULL) {racine=( struct cell * )malloc(sizeof(struct cell));
racine->n=k; racine->g=NULL; racine ->d=NULL;}
else


{ e1=NULL; e2=racine;
newcell=( struct cell * )malloc(sizeof(struct cell));
newcell->n=k; newcell->g=NULL; newcell->d=NULL;
while(e2!=NULL) { e1=e2; if (k<e2->n) e2=e2->g; else e2=e2->d; }
if (k<e1->n) e1->g=newcell; else e1->d=newcell;
}
}


3) Fabriquer une fonction de recherche d’un élément donné k à l’intérieur de
l’arbre


Quand l’arbre est construit, chaque cellule des noeuds contient un nombre n, et ce
nombre est tel que tous les nombres qui sont dans le sous-arbre gauche du noeud sont
inférieur à n, et que ceux situés dans son sous-arbre droit sont supérieurs. La recherche
de la présence ou non d’un nombre donné k peut se faire de façon dichotomique, et
récursive. Si le nombre k est inférieur à celui de la racine, on réduit la recherche en la
relançant à partir du fils gauche, et sinon à partir du fils droit. Le nombre k étant donné,
la fonction chercher(k, r) est appelée sur la racine, soit chercher (k, racine) dans le
programme principal, puis elle se rappelle sur les noeuds r en descente.
void chercher(int k, struct cell * r)
{ if (r==NULL) printf("\n%d n'est pas present",k);
else if (r->n==k) printf("\n%d est present", k);
else
{ if (k<r->n) chercher(k,r->g);
else if (k>r->n) chercher(k,r->d);
}
}


4) Construire la fonction de suppression d’un élément k dans l’arbre



On suppose que le nombre k est bien situé dans l’arbre, quitte à lancer auparavant la
fonction de recherche pour le savoir. La cellule contenant le nombre k est trouvée en
faisant descendre le crochet e1 e2, jusqu’à ce que e2 pointe sur cette cellule. Plusieurs
cas se présentent :
· La cellule de k est une feuille, c’est-à-dire qu’elle possède deux branches
pendantes, pointant vers NULL. Si elle est à gauche au-dessous de e1, on met le fils
gauche de e1 à NULL, et sinon le fils droit. La cellule de k est ainsi décrochée de
l’arbre, et l’on peut ensuite libérer la place en mémoire jusque là occupée par cette
cellule (free(e2)).
· La cellule contenant k possède une et une seule branche vide, celle de droite.
Par exemple, si c’est la branche droite qui pointe sur NULL, et pas la gauche, on
commence par regarder si la cellule de k est à gauche ou à droite de son « père » e1.
Si elle est à droite, comme sur le dessin ci-dessous, on accroche au fils droit de e1 le
fils gauche de e2, ce qui met le sous-arbre gauche de e2 comme sous arbre droit de e1,
tous ses éléments étant inférieurs à k mais supérieurs au nombre de la cellule sur lequel
pointe e1.
Si elle est à gauche, on accroche le sous-arbre gauche de e2 au fils gauche de e1.

· La cellule a comme branche vide unique celle située à sa gauche. On fait
comme dans le cas précédent.
· La cellule n’a aucune branche vide. Dans ce cas, on commence par chercher la
cellule contenant le plus petit nombre supérieur à k. Pour cela il faut d’abord emprunter
le branche droite de la cellule de k, puis ensuite descendre toujours à gauche jusqu’à
tomber sur une branche gauche pendante (vide). On utilise un nouveau crochet ee1 ee2,
placé au départ avec ee1 sur e2 et ee2 sur le fils droit de e1. Puis on le descend, toujours
à gauche, jusqu’à ce que ee2 pointe sur la cellule voulue ( ee2->g == NULL), et ee1 sur
son père. Dans l’exemple ci-dessous, à partir de la cellule contenant 10 à supprimer, on
tombe sur la cellule contenant 11. C’est ce nombre qui va remplacer le 10 dans la
cellule où pointe e2, et celle-ci conserve ces deux branches à droite et à gauche. Mais
cela ne suffit pas. Il faut aussi supprimer la cellule contenant 11 (où pointe ee2) puisque
la cellule sur laquelle pointe e2 la remplace, tout en conservant sa branche de droite (ee2
->d), que l’on accroche maintenant à la branche gauche de son père ee1.


Mais il y a un cas particulier. On a vu que pour arriver à la position finale de ee2, on
faisait d’abord un démarrage à droite, suivi de descentes à gauche. S’il y a au moins une
descente à gauche, ce qui correspond au dessin ci-dessus, ce que l’on vient de faire est
valable. Mais il peut arriver qu’il n’y ait aucune descente à gauche, auquel cas le
crochet ee1 ee2 reste dans sa position initiale, avec ee2 à droite sous ee1 (qui est aussi
e2). Dans ce cas il faut accrocher le sous-arbre droit de ee2 au sous-arbre droit de e2 (ou
ee1), et non pas à celui de gauche comme auparavant.
La fonction de suppression s’en déduit :
void supprimer(int k)
{ struct cell *ee1,*ee2;
e1=NULL; e2=racine; /* recherché de la cellule contenant k */
while(e2->n!=k)
{ e1=e2;
if (k<e2->n) e2=e2->g; else e2=e2->d;
}
if (e2->g==NULL && e2->d==NULL) /* la cellule est une feuille */
{ if (e1->g==e2) e1->g=NULL; else e1->d =NULL;
free(e2);
}
else if ( e2->g==NULL) /* la cellule a sa branche gauche vide, mais pas l’autre */
{ if(e1->g==e2) e1->g=e2->d; else e1->d=e2->d; free(e2);}
else if ( e2->d==NULL) /* la cellule a sa branche droite vide, mais pas l’autre */
{ if(e1->g==e2) e1->g=e2->d; else e1->d=e2->d; free(e2);}
else if ( e2->d==NULL) /* la cellule a sa branche droite vide, mais pas l’autre */
{ if(e1->g==e2) e1->g=e2->g; else e1->d=e2->g; free(e2);}
else /* la cellule a deux sous-arbres non vides accrochés à gauche et à droite */
{ ee2=e2->d; ee1=e2;
while(ee2->g!=NULL) /* ee2 va pointer sur le nombre qui remplace k dans sa cellule */
{ ee1=ee2; ee2=ee1->g; }
if(ee2==ee1->g) ee1->g= ee2->d; /* ee2 est à gauche de son père */
else ee1->d=ee2->d; /* cas particulier: pas de descente à gauche pour avoir ee2 */
e2->n=ee2->n;
free(ee2);
}

5) Faire le programme principal utilisant les fonctions précédentes



Aux fonctions précédentes, on doit ajouter la fonction de parcours de l’arbre (voir
plus haut) qui donne les N nombres une fois triés, ce qui permet de vérifier le bon
fonctionnement des fonctions. Il reste le programme principal et les déclarations
préliminaires :
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <time.h>
#define N 20
#define OUI 1
#define NON 0
int a[N];
struct cell { int n; struct cell * g; struct cell *d;};
struct cell * racine=NULL; struct cell * newcell, *e1,*e2;
void creationtableau(void);
void inserer(int k);
void parcours(struct cell * r);
void chercher(int k, struct cell * r);

void supprimer(int k);
int main()
{ int i,j,hh,s;
creationtableau();
for(i=0;i<N;i++) inserer(a[i]);
printf("\n\n"); parcours(racine);
printf("\n"); chercher(20,racine); /* 20 est-il présent? */
hh=rand()%N; s=a[hh]; printf("\ns= %d",s); /* on prend un élément au hasard à supprimer */
supprimer(s);
printf("\n\n"); parcours(racine); /* pour constater la suppression de l’élément */
getch();return 0;
}
                                                              

                                                       fin.                                

Cours Archietcture des ordinateurs

                           Cours Archietcture des ordinateurs

                                Chapitre 3 : le microprocesseur
                                                   2 LMD

1- Définition

Un microprocesseur est un circuit intégré complexe caractérisé par une très grande intégration et doté des facultés d'interprétation et d'exécution des instructions d'un programme. Il est chargé d’organiser les tâches précisées par le programme et d’assurer leur exécution. Il doit aussi prendre en compte les informations extérieures au système et assurer leur traitement. C’est le cerveau du système.
A l’heure actuelle, un microprocesseur regroupe sur quelques millimètres carrés des fonctionnalités toujours plus complexes. Leur puissance continue de s’accroître et leur encombrement diminue régulièrement respectant toujours, pour le moment, la fameuse loi de Moore (1).

2- Architecture de base d’un microprocesseur

Un microprocesseur est construit autour de deux éléments principaux : Une unité de commande et une unité de traitement, associés à des registres chargées de stocker les différentes informations à traiter. Ces trois éléments sont reliés entre eux par des bus interne permettant les échanges d’informations.

Remarques :

Il existe deux types de registres :
• les registres d'usage général permettent à l'unité de traitement de manipuler des données à vitesse élevée. Ils sont connectés au bus données interne au microprocesseur.
• les registres d'adresses (pointeurs) connectés sur le bus adresses.

2.1 L’unité de commande

Elle permet de séquencer le déroulement des instructions. Elle effectue la recherche en mémoire de l'instruction. Comme chaque instruction est codée sous forme binaire, elle en assure le décodage pour enfin réaliser son exécution puis effectue la préparation de l'instruction suivante. Pour cela, elle est composée par :

- le compteur de programme constitué par un registre dont le contenu est initialisé avec l'adresse de la première instruction du programme. Il contient toujours l’adresse de l’instruction à exécuter.

- le registre d'instruction et le décodeur d'instruction : chacune des instructions à exécuter est rangée dans le registre instruction puis est décodée par le décodeur d’instruction.

- Séquenceur : Il organise l'exécution des instructions au rythme d’une horloge. Il élabore tous les signaux de synchronisation internes ou externes (bus de commande) du microprocesseur en fonction des divers signaux de commande provenant du décodeur d’instruction ou du registre d’état par exemple. Il s'agit d'un automate réalisé soit de façon câblée (obsolète), soit de façon micro-programmée.

2.2 L’unité de traitement

C’est le coeur du microprocesseur. Elle regroupe les circuits qui assurent les traitements nécessaires à l'exécution des instructions :

- L’Unité Arithmétique et Logique (UAL) est un circuit complexe qui assure les fonctions logiques (ET, OU, Comparaison, Décalage , etc…) ou arithmétique (Addition, soustraction).

- Le registre d'état (PSW) est généralement composé de 8 bits à considérer individuellement. Chacun de ces bits est un indicateur dont l'état dépend du résultat de la dernière opération effectuée par l’UAL. On les appelle indicateur d’état ou flag ou drapeaux. Dans un programme le résultat du test de leur état conditionne souvent le déroulement de la suite du programme. On peut citer par exemple les indicateurs de :
bit de retenue (carry : C)



bit de retenue intermédiaire (Auxiliary-Carry : AC)
bit de signe (Sign : S)
bit de débordement (overflow : OV ou V)
bit de zéro (Z)
bit de parité (Parity : P)
- Les accumulateurs sont des registres de travail qui servent à stocker un opérande au début d'une opération arithmétique et le résultat à la fin de l'opération.
2.3 Schéma fonctionnel d’un processeur à 1 seul accumulateur
3- Cycle d’exécution d’une instruction
Le microprocesseur ne comprend qu’un certain nombre d’instructions qui sont codées en binaire. Le traitement d’une instruction peut être décomposé en trois phases.
Phase 1: Recherche de l'instruction à traiter
1. Le PC contient l'adresse de l'instruction suivante du programme. Cette valeur est placée sur le bus d'adresses par l'unité de commande qui émet un ordre de lecture ;
2. Au bout d'un certain temps (temps d'accès à la mémoire), le contenu de la case mémoire sélectionnée est disponible sur le bus des données.

3. L'instruction est stockée dans le registre instruction du processeur.

Phase 2: Décodage de l’instruction et recherche de l'opérande

Le registre d'instruction contient maintenant le premier mot de l'instruction qui peut être codée sur plusieurs mots. Ce premier mot contient le code opératoire qui définit la nature de l'opération à effectuer (addition, rotation,...) et le nombre de mots de l'instruction.
1. L'unité de commande transforme l'instruction en une suite de commandes élémentaires nécessaires au traitement de l'instruction.
2. Si l'instruction nécessite une donnée en provenance de la mémoire, l'unité de commande récupère sa valeur sur le bus de données.

3. L’opérande est stockée dans un registre.

4. L'unité de commande positionne le PC pour l'instruction suivante.
Phase 3: Exécution de l'instruction

1. Le micro-programme réalisant l'instruction est exécuté.

2. Les drapeaux sont positionnés (registre d'état)


4- Jeu d’instructions
La première étape de la conception d’un microprocesseur est la définition de son jeu d’instructions. Le jeu d’instructions décrit l’ensemble des opérations élémentaires que le microprocesseur pourra exécuter. Il va donc en partie déterminer l’architecture du microprocesseur à réaliser et notamment celle du séquenceur. A un même jeu d’instructions peut correspondre un grand nombre d’implémentations différentes du microprocesseur.
4.1 Type d’instructions
Les instructions que l’on retrouve dans chaque microprocesseur peuvent être classées en 4groupes :
• Transfert de données pour charger ou sauvegarder en mémoire centrale, effectuer des transferts de registre à registre (load et store pour les processeurs à 1 accumulateur, Move pour les processeurs à plusieurs registres et push et pop pour les processeurs à pile)
• Opérations arithmétiques : addition ADD, soustraction SUB, division DIV, multiplication MUL.
• Opérations logiques : ET, OU, NON, NAND, etc.
• Contrôle de séquence : branchement inconditionnel JMP, branchement conditionnel JE, JZ, etc, comparaison CMP, etc
• Opérations d’E/S : comme lecture via le clavier ou afficher sur ecran etc.
4.2 Format d’instruction
Les instructions et leurs opérandes (paramètres) sont stockés en mémoire principale. La taille totale d’une instruction (nombre de bits nécessaires pour la représenter en mémoire) dépend du type d’instruction et aussi du type d’opérande. Chaque instruction est toujours codée sur un nombre entier d’octets afin de faciliter son décodage par le processeur. Une instruction est composée de deux champs :
• le champ code instruction, qui indique au processeur quelle opération réaliser
• le champ (ou les champs opérandes) qui contient la donnée, ou l’adresse d’une donnée en mémoire.
Il y a des opérations qui ne nécessitent pas des opérandes tel que fin de programme, comme il y a des instructions qui nécessitent un seul opérande tel que les instrcutions de branchement. Les instructions de transfert necéssitent deux opérandes source et destination. Par contre les instrcutions arithmétique et logique necéssitent 3 opérandes, le troisième opérande correspond au résultat de l’opération.
Le nombre de registres de travail (accumulateurs) se différent d’un processeur à un autre, il y a des processeurs qui contiennent soit :

- un seul accumulateur, ce type de processeur appelé processeur à 1 adresse,
- plusieurs registres (jusque 4 registres), ce type de processeur appelé processeur à 2 adresses,

- plusieurs registres (jusqu’à 32 registres), ce type de processeur appelé processeur à 3 adresse

- une pile. ce type de processeur appelé processeur à 0 adresse.
Selon le type de processeur on distingue 4 formats des instructions : (voir les opérations arithmétique et logique)

- Instruction à 1 adresse




4.3 Mode d’adressage
Un champ adresse peut permettre de référencer un registre ou un mot en mémoire. Il peut contenir le numéro du registre ou l'adresse effective du mot mais ce ne sont pas les seules manières d'identifier un opérande. Pour faciliter la programmation il existe de nombreux modes d'adressage. Le mode est défini soit par le code opération lorsque celui-ci impose un type déterminé, soit par un code faisant partie du champ adresse.
Les exemples utilisés ci-dessous pour expliquer les différents modes d’adressage sont empruntés aux instructions de l'assembleur PDP-11.

- Adressage implicite
Le code opération identifie automatiquement l'opérande, l'instruction ne peut porter que sur un registre particulier (par exemple test sur le signe du résultat d'une opération arithmétique : concerne le registre d'état PSW). Aucun champ adresse n'est nécessaire.
- Adressage immédiat
La valeur de l'opérande est contenue dans le champ adresse si le nombre de bits dans ce champ est suffisant, sinon dans le mot suivant l'instruction.
MOV #100, R1 Après cette instruction le registre R1 contient la valeur 100.
- Adressage registre
Le champ adresse contient le numéro du registre opérande.
CLR R3 Après cette instruction le contenu de R3 est nul.
- Adressage direct
Le champ adresse de l'instruction contient l'adresse effective de l'opérande.
100 : 250
MOV 100, R2 Après cette instruction le registre R2 contient le mot qui se situe à l'adresse 100 en mémoire, c'est-à-dire 250.
- Adressage indirect
Le champ adresse contient l'adresse d'un pointeur : mot en mémoire qui contient l'adresse effective de l'opérande.
MOV (R1), R4 Après cette instruction R4 contient la valeur du mot dont l'adresse est contenue dans R1. Comme R1 vaut 100 on trouve 250 dans R4.
- Adressage indexé
Ce mode d'adressage est très utile lorsqu'on travaille, par exemple, sur des tableaux. Considérons un bloc de n mots consécutifs débutant à l'adresse A. Le kième mot se trouve à l'adresse A + (k - 1). Pour référencer ce mot il est possible d'utiliser un registre d'index. L'adresse effective est calculée en additionnant le contenu de ce registre d'index à l'adresse qui se trouve dans le champ adresse de l'instruction. Sur certaines machines tous les registres généraux peuvent être utilisés comme registres d'index. La présence d'un registre d'index s'accompagne généralement de la possibilité d'incrémentation et décrémentation automatiques.
MOV R4, 100(R3)+
CLR 100(R3) Avant la première opération R3 est nul, donc le contenu de R4 est transféré à l'adresse 100. Après le registre R3 est incrémenté. L'instruction suivante permet de mettre à zéro le contenu du mot à l'adresse suivante.
- Adressage basé
L'adressage basé est comparable à l'adressage indexé mais cette fois l'adresse effective est obtenue en additionnant le contenu du registre de base au contenu du champ adresse de l'instruction. Ce mode d'adressage est utilisé par exemple en cas d'allocation dynamique de la mémoire : la position du programme en mémoire peut changer en fonction de la charge du système et il n'occupe pas toujours un espace contigu. Cette technique permet également de réduire le nombre de bits dans le champ adresse : le registre de base contient la première adresse d'un bloc de 2k mots et l'adresse (sur k bits) contenue dans l'instruction représente le déplacement à l'intérieur du bloc.
- Adressage relatif
L'adresse effective est obtenue en additionnant le contenu du compteur ordinal au contenu du champ adresse de l'instruction. Ce type d'adressage est utilisé par exemple dans des instructions de branchement. N'oublions pas que lecalcul de l'adresse effective peut nécessiter quelques opérations (addition par exemple). L'utilisation de certains modes d'adressage sophistiqués (le 68020 de Motorola dispose par exemple d'une cinquantaine de modes d'adressage) peut donc augmenter le temps de traitement d'une instruction.

4.4 Temps d’exécution

Chaque instruction nécessite un certain nombre de cycles d’horloges pour s’effectuer. Le nombre de cycles dépend de la complexité de l’instruction et aussi du mode d’adressage. Il est plus long d’accéder à la mémoire principale qu’à un registre du processeur. La durée d’un cycle dépend de la fréquence d’horloge du séquenceur.

5- Langage de programmation

Le langage machine est le langage compris par le microprocesseur. Ce langage est difficile à maîtriser puisque chaque instruction est codée par une séquence propre de bits. Afin de faciliter la tâche du programmeur, on a créé différents langages plus ou moins évolués.
Le langage assembleur est le langage le plus « proche » du langage machine. Il est composé par des instructions en général assez rudimentaires que l’on appelle des mnémoniques. Ce sont essentiellement des opérations de transfert de données entre les registres et l'extérieur du microprocesseur (mémoire ou périphérique), ou des opérations arithmétiques ou logiques. Chaque instruction représente un code machine différent. Chaque microprocesseur peut posséder un assembleur différent. La difficulté de mise en oeuvre de ce type de langage, et leur forte dépendance avec la machine a nécessité la conception de langages de haut niveau, plus adaptés à l'homme, et aux applications qu'il cherchait à développer. Faisant abstraction de toute architecture de machine, ces langages permettent l'expression d'algorithmes sous une forme plus facile à apprendre, età dominer (C, Pascal, Java, etc…). Chaque instruction en langage de haut niveau correspondra à une succession d’instructions en langage assembleur. Une fois développé, le programme en langage de haut niveau n’est donc pas compréhensible par le microprocesseur. Il faut le compiler pour le traduire en assembleur puis l’assembler pour le convertir en code machine compréhensible par le microprocesseur. Ces opérations sont réalisées à partir de logiciels spécialisés appelés compilateur et assembleur.


6- Performances d’un microprocesseur
On peut caractériser la puissance d’un microprocesseur par le nombre d’instructions qu’il est capable de traiter par seconde. Pour cela, on définit :
• le CPI (Cycle Par Instruction) qui représente le nombre moyen de cycles d’horloge nécessaire pour l’exécution d’une instruction pour un microprocesseur donné.
• le MIPS (Millions d'Instructions Par Seconde) qui représente la puissance de traitement du microprocesseur.

Code machine ( 68HC11 ) Assembleur ( 68HC11 ) Langage C
Pour augmenter les performances d’un microprocesseur, on peut donc soit augmenter la fréquence d'horloge (limitation matérielle), soit diminuer le CPI (choix d'un jeu d'instruction adapté).
7- Notion d’architecture RISC et CISC
Actuellement l’architecture des microprocesseurs se composent de deux grandes familles :
• L’ architecture CISC (Complex Instruction Set Computer)
• L’architecture RISC (Reduced Instruction Set Computer)
6.1 L’architecture CISC
Pourquoi
Par le passé la conception de machines CISC était la seule envisageable. En effet, vue que la mémoire travaillait très lentement par rapport au processeur, on pensait qu’il était plus intéressant de soumettre au microprocesseur des instructions complexes. Ainsi, plutôt que de coder une opération complexe par plusieurs instructions plus petites (qui demanderaient autant d’accès mémoire très lent), il semblait préférable d’ajouter au jeu d’instructions du microprocesseur une instruction complexe qui se chargerait de réaliser cette opération. De plus, le développement des langages de haut niveau posa de nombreux problèmes quant à la conception de compilateurs. On a donc eu tendance à incorporer au niveau processeur des instructions plus proches de la structure de ces langages.
Comment

C’est donc une architecture avec un grand nombre d’instructions où le microprocesseur doit exécuter des tâches complexes par instruction unique. Pour une tâche donnée, une machine CISC exécute ainsi un petit nombre d’instructions mais chacune nécessite un plus grand nombre de cycles d’horloge. Le code machine de ces instructions varie d’une instruction à l’autre et nécessite donc un décodeur complexe (micro-code).
6.2 L’architecture RISC
Pourquoi
Des études statistiques menées au cours des années 70 ont clairement montré que les programmes générés par les compilateurs se contentaient le plus souvent d'affectations, d'additions et de multiplications par des constantes. Ainsi, 80% des traitements des langages de haut niveau faisaient appel à seulement 20% des instructions du microprocesseur. D’où l’idée de réduire le jeu d’instructions à celles le plus couramment utilisées et d’en améliorer la vitesse de traitement.
Comment
C’est donc une architecture dans laquelle les instructions sont en nombre réduit (chargement, branchement, appel sous-programme). Les architectures RISC peuvent donc être réalisées à partir de séquenceur câblé. Leur réalisation libère de la surface permettant d’augmenter le nombres de registres ou d’unités de traitement par exemple. Chacune de ces instructions s’exécutent ainsi en un cycle d’horloge. Bien souvent, ces instructions ne disposent que d’un seul mode d’adressage. Les accès à la mémoire s’effectue seulement à partir de deux instructions (Load et Store). Par contre, les instructions complexes doivent être réalisées à partir de séquences basées sur les instructions élémentaires, ce qui nécessite un compilateur très évolué dans le cas de programmation en langage de haut niveau.

Comparaison
Le choix dépendra des applications visées. En effet, si on diminue le nombre d'instructions, on crée des instructions complexes (CISC) qui nécessitent plus de cycles pour être décodées et si on diminue le nombre de cycles par instruction, on crée des instructions simples (RISC) mais on augmente alors le nombre d'instructions nécessaires pour réaliser le même traitement.



                                                                             FIN.

السبت، 23 نوفمبر 2013

IE Internet Explorer

Navigateur Internet de Microsoft actuellement dans sa sixième version, ce logiciel qui permet d'afficher des pages Web, est le plus utilisé dans le monde. Ce grand nombre d'utilisateurs s'explique par le fait que Microsoft fournit ce logiciel avec tous ses systèmes d'exploitation depuis l'apparition de Windows 98. Cette pratique, un peu abusive vis à vis des autres éditeurs de navigateurs comme Netscape, a valu à Microsoft un long procès pour abus de position dominante et concurrence déloyale.

الأربعاء، 20 نوفمبر 2013

Dèfintion CSCO

Cisco Systems (NASDAQ : CSCO) est une entreprise informatique américaine qui vendait, à l'origine, uniquement du matériel réseau (routeur et switch ethernet).

Historique

Fondé en 1984 par Leonard Bosack et Sandra Lerner, un couple qui travaillait au service informatique de l'université de Stanford, Cisco n'a pas été la première société à créer et vendre des routeurs mais Cisco créa le premier routeur multi-protocoles permettant d'interconnecter des réseaux utilisant des protocoles de communication différents.
Cisco, dont le siège social se trouve à San José en Californie, tire son nom et son logo de laville où elle a été fondée, San Francisco et son fameux Golden Gate.
La société est entrée en bourse (NASDAQ) en 1990. Après de légères pertes en 2001 (- 1milliard de $), elle a régulièrement accru ses bénéfices de 2002 (1.8 milliard) à 2005 (5,7 milliard). Son CEO actuel (2005) est John Chambers.
La spécificité de la gamme des produits Cisco est l'uniformité de son système d'exploitation. En effet, la majorité des produits Cisco utilise un système d'exploitation propriétaire nomméIOS.
Entré sur le marché de la VoIP en 1999, Cisco systems est aujourd’hui le premier fournisseur des entreprises sur le marché de la téléphonie sur réseaux IP (ToIP), incluant l’Internet Protocol (IP) et les systèmes de circuits traditionnels avec six millions de téléphones IP vendus de 1999 à 2005. Le 8 millionième téléphone IP a été livré à la Deutsche Bank en Mai 2006.

Offre produits

Cisco propose aujourd'hui une gamme importante de solutions telles que :
  • voix sur réseau IP, IPTV
  • Technologies sans fil wifi
  • solutions de sécurité:
    • Cisco PIX Firewall nouvellement appelé ASA pour Adaptive Security Appliance (après le rachat de "Network Translation Inc")
    • Cisco IOS routers
    • Cisco VPN3000
    • solutions de VPN
  • solutions de stockage en réseau (SAN)
  • switch ATM, MPLS (gamme Catalyst)
  • logiciels (Cisco Unified Communications Manager)
Avec la vidéo sur Internet et IPTV, Cisco estime que, à compter de 2006, les débits des réseaux vont devoir être augmentés de 300 à 500 % par an. L'accroissement futur ne sera pas seulement généré par la télévision HD mais également par des émissions qui pourront fournir jusqu'à plusieurs dizaines d'angles de vue possibles d'un événement, la vidéoconférence, etc. C'est cette évolution qui a entraîné le rachat, en 2005, de ScientificAtlanta pour 6,9 milliards de dollars et Kiss Technology par Linksys, sa filiale. Les deux compagnies sont des spécialistes de la convergence réseau/vidéo.
En 2005, Cisco Systems adopte le nouveau standard baptisé IMS (IP Multimedia Subsystem) qui permettra aux opérateurs télécoms de proposer sur des mobiles 3G, des services fonctionnant aujourd’hui sur réseaux filaires.

Certifications

Cisco a mis en place 10 000 "Cisco Networking Academies" dans 160 pays qui forment approximativement 420 000 étudiants chaque annèe aptes à installer et maintenir des réseaux informatiques...
Cisco propose également des certifications pour les professionnels du monde des réseaux. Elles sont classées sous différents niveaux de qualifications :
Niveau 1 - Associate
CCDA (Cisco Certified Design Associate)
CCNA (Cisco Certified Network Associate)
Niveau 2 - Professional
CCNP (Cisco Certified Network Professional)
CCDP (Cisco Certified Design Professional)
CCIP (Cisco Certified Internetwork Professional)
CCSP (Cisco Certified Security Professional)
Niveau 3 - Expert
CCIE (Cisco Certified Internetwork Expert)
Qualified specialist
CCVP (Cisco Certified Voice Professional)


Langage C++

                                 Historique de C++


Très tôt, les concepts de la programmation orientée objet (en abrégé P.O.O.) ont donné naissance

à de nouveaux langages dits « orientés objets » tels que Smalltalk, Simula, Eiffel ou,

plus récemment, Java. Le langage C++, quant à lui, a été conçu suivant une démarche

hybride. En effet, Bjarne Stroustrup, son créateur, a cherché à adjoindre à un langage structuré
existant (le C), un certain nombre de spécificités lui permettant d’appliquer les concepts

de P.O.O. Dans une certaine mesure, il a permis à des programmeurs C d’effectuer une transition

en douceur de la programmation structurée vers la P.O.O. De sa conception jusqu’à sa
normalisation, le langage C++ a quelque peu évolué. Initialement, un certain nombre de

publications de AT&T ont servi de référence du langage. Les dernières en date sont : la version
2.0 en 1989, les versions 2.1 et 3 en 1991. C’est cette dernière qui a servi de base au travail

du comité ANSI qui, sans la remettre en cause, l’a enrichie de quelques extensions et

surtout de composants standard originaux se présentant sous forme de fonctions et de classes

génériques qu’on désigne souvent par le sigle S.T.L (Standard Template Library). La norme

définitive de C++ a été publiée par l’ANSI en juillet 1998.



                             1 Présentation par l’exemple de quelques

                              instructions du langage C++

                            1.1 Un exemple de programme en langage C++




Voici un exemple de programme en langage C++, accompagné d’un exemple d’exécution.

Avant de lire les explications qui suivent, essayez d’en percevoir plus ou moins le fonctionnement.

#include <iostream>

#include <cmath>

using namespace std ;

main()


{ int i ;

float x ;


float racx ;

const int NFOIS = 5 ;

cout << "Bonjour\n" ;

cout << "Je vais vous calculer " << NFOIS << " racines carrees\n" ;

for (i=0 ; i<NFOIS ; i++)

{ cout << "Donnez un nombre : " ;

cin >> x ;

if (x < 0.0)

cout << "Le nombre " << x << "ne possede pas de racine carree\n " ;

else

{ racx = sqrt (x) ;

cout << "Le nombre " << x << " a pour racine carree : " << racx << "\n" ;
}
}
cout << "Travail termine - au revoir " ;
}
Bonjour

Je vais vous calculer 5 racines carrees

Donnez un nombre : 8
Le nombre 8 a pour racine carree : 2.82843

Donnez un nombre : 4
Le nombre 4 a pour racine carree : 2

Donnez un nombre : 0.25
Le nombre 0.25 a pour racine carree : 0.5

Donnez un nombre : 3.4
Le nombre 3.4 a pour racine carree : 1.84391

Donnez un nombre : 2
Le nombre 2 a pour racine carree : 1.41421
Travail termine - au revoir




                      1.3 Déclarations

                     Les quatre instructions :



int i ;

float x ;

float racx ;

const int NFOIS = 5 ;


sont des « déclarations ».

La première précise que la variable nommée i est de type int, c’est-à-dire qu’elle est destinée


à contenir des nombres entiers (relatifs). Nous verrons qu’en C++ il existe plusieurs types

d’entiers.

Les deux autres déclarations précisent que les variables x et racx sont de type float, c’est-àdire

qu’elles sont destinées à contenir des nombres flottants (approximation de nombres

réels). Là encore, nous verrons qu’en C++ il existe plusieurs types flottants.

Enfin, la quatrième déclaration indique que NFOIS est une constante de type entier, ayant la

valeur 5. Contrairement à une variable, la valeur d’une constante ne peut pas être modifiée.

En C++, comme dans la plupart des langages actuels, les déclarations des types des variables

sont obligatoires. Elles doivent apparaître avant d’être effectivement utilisées. Ici, nous les

avons regroupées au début du programme (on devrait plutôt dire : au début de la fonction

main). Il en ira de même pour toutes les variables définies dans une fonction ; on les appelle

« variables locales » (en toute rigueur, les variables définies dans notre exemple sont des

variables locales de la fonction main). Nous verrons également (dans le chapitre consacré

aux fonctions) qu’on peut définir des variables en dehors de toute fonction : on parlera alors

de variables globales.




                          1.9 L’instruction using



La norme de C++ a introduit la notion d’« espaces de noms » (namespace). Elle permet de

restreindre la « portée » des symboles à une certaine partie d’un programme et donc, en particulier,


de règler les problèmes qui peuvent se poser quand plusieurs bibliothèques utilisent

les mêmes noms. Cette notion d’espace de noms sera étudiée par la suite. Pour l’instant, retenez


que les symboles déclarés dans le fichier iostream appartiennent à l’espace de noms std.

L’instruction using sert précisément à indiquer que l’on se place « dans cet espace de noms


std » (attention, si vous placez l’instruction using avant l’incorporation des fichiers en-tête,

vous obtiendrez une erreur car vous ferez référence à un espace de noms qui n’a pas encore

été défini !).




                      1.10 Exemple de programme utilisant le type caractère


Voici un second exemple de programme, accompagné de deux exemples d’exécution, destiné

à vous montrer l’utilisation du type « caractère ». Il demande à l’utilisateur de choisir une

opération parmi l’addition ou la multiplication, puis de fournir deux nombres entiers ; il affiche

alors le résultat correspondant.

#include <iostream>

using namespace std ;

main()

{ char op ;

int n1, n2 ;

cout << "opération souhaitée (+ ou *) ? " ;

cin >> op ;

cout << "donnez 2 nombres entiers : " ;

cin >> n1 >> n2 ;

if (op == ’+’) cout << "leur somme est : " << n1+n2 << "\n" ;

else cout << "leur produit est : " << n1*n2 << "\n" ;

}
opération souhaitée (+ ou *) ? +

donnez 2 nombres entiers : 25 13





                               2.4 Le format libre


Comme tous les langages récents, le C++ autorise une mise en page parfaitement libre. En

particulier, une instruction peut s’étendre sur un nombre quelconque de lignes, et une même

ligne peut comporter autant d’instructions que vous le souhaitez. Les fins de ligne ne jouent

pas de rôle particulier, si ce n’est celui de séparateur, au même titre qu’un espace, sauf dans

les « constantes chaînes » où elles sont interdites ; de telles constantes doivent impérativement

être écrites à l’intérieur d’une seule ligne. Un identificateur ne peut être coupé en deux

par une fin de ligne, ce qui semble évident.

Bien entendu, cette liberté de mise en page possède des contreparties. Notamment, le risque

existe, si l’on n’y prend garde, d’aboutir à des programmes peu lisibles.

À titre d’exemple, voyez comment pourrait être (mal) présenté notre programme précédent :




                            1 La notion de type



La mémoire centrale est un ensemble de positions binaires nommées bits. Les bits sont regroupés


en octets (8 bits), et chaque octet est repéré par ce qu’on nomme son adresse.

L’ordinateur, compte tenu de sa technologie actuelle, ne sait représenter et traiter que des

informations exprimées sous forme binaire. Toute information, quelle que soit sa nature,

devra être codée sous cette forme. Dans ces conditions, on voit qu’il ne suffit pas de connaître

le contenu d’un emplacement de la mémoire (d’un ou de plusieurs octets) pour être en

mesure de lui attribuer une signification. Par exemple, si vous savez qu’un octet contient le

« motif binaire » suivant :

01001101


vous pouvez considérer que cela représente le nombre entier 77 (puisque le motif ci-dessus

correspond à la représentation en base 2 de ce nombre). Mais pourquoi cela représenterait-il

un nombre ? En effet, toutes les informations (nombres entiers, nombres réels, nombres complexes,

caractères, instructions de programme en langage machine, graphiques, images, sons,

vidéos...) devront, au bout du compte, être codées en binaire.

Dans ces conditions, les huit bits ci-dessus peuvent peut-être représenter un caractère ; dans

ce cas, si nous connaissons la convention employée sur la machine concernée pour représenter


les caractères, nous pouvons lui faire correspondre un caractère donné (par exemple M,

dans le cas du code ASCII). Ils peuvent également représenter une partie d’une instruction

machine ou d’un nombre entier codé sur 2 octets, ou d’un nombre réel codé sur 4 octets, ou...

On comprend donc qu’il n’est pas possible d’attribuer une signification à une information

binaire tant que l’on ne connaît pas la manière dont elle a été codée. Qui plus est, en général,

il ne sera même pas possible de « traiter » cette information. Par exemple, pour additionner

deux informations, il faudra savoir quel codage a été employé afin de pouvoir mettre en

oeuvre les bonnes instructions (en langage machine). Par exemple, on ne fait pas appel aux

mêmes circuits électroniques pour additionner deux nombres codés sous forme « entière » et

deux nombres codés sous forme « flottante ».

D’une manière générale, la notion de type, telle qu’elle existe dans les langages évolués, sert


à régler (entre autres choses) les problèmes que nous venons d’évoquer.

Les types de base du langage C++ se répartissent en quatre catégories en fonction de la

nature des informations qu’ils permettent de représenter :

• nombres entiers (mot-clé int) ;

• nombres flottants (mot-clé float ou double) ;

• caractères (mot-clé char) ;

• valeurs booléennes, c’est-à-dire dont la valeur est soit vrai, soit faux (mot-clé bool).





                          2 Les types entiers

                     2.1 Les différents types usuels d’entiers prévus par C++



C++ prévoit que, sur une machine donnée, on puisse trouver jusqu’à trois tailles différentes

d’entiers, désignées par les mots-clés suivants :

• short int (qu’on peut abréger en short) ;

• int (c’est celui que nous avons rencontré dans le chapitre précédent) ;

• long int (qu’on peut abréger en long).

Chaque taille impose naturellement ses limites. Toutefois, ces dernières dépendent non seulement

du mot-clé considéré, mais également de la machine utilisée : tous les int n’ont pas la

même taille sur toutes les machines ! Fréquemment, deux des trois mots-clés correspondent à

une même taille1.





                       2.2 Leur représentation en mémoire





Pour fixer les idées, nous raisonnerons ici sur des nombres entiers représentés sur 16 bits ,

mais il sera facile de généraliser notre propos à une taille quelconque.

Quelle que soit la machine (et donc, a fortiori, le langage !), les entiers sont codés en utilisant


un bit pour représenter le signe (0 pour positif et 1 pour négatif).

a) Lorsqu’il s’agit d’un nombre positif (ou nul), sa valeur absolue est écrite en base 2, à la


suite du bit de signe. Voici quelques exemples de codages de nombres (à gauche, le nombre

en décimal, au centre, le codage binaire correspondant, à droite, le même codage exprimé en

hexadécimal) :

1 0000000000000001 0001
2 0000000000000010 0002
3 0000000000000011 0003
16 0000000000010000 0010
127 0000000001111111 007F
255 0000000011111111 00FF

b) Lorsqu’il s’agit d’un nombre négatif, sa valeur absolue est codée généralement suivant ce

que l’on nomme la « technique du complément à deux »2. Pour ce faire, cette valeur est d’abord

exprimée en base 2 puis tous les bits sont inversés (1 devient 0 et 0 devient 1) et, enfin, on

ajoute une unité au résultat. Voici quelques exemples (avec la même présentation que

précédemment) :

-1 1111111111111111 FFFF
-2 1111111111111110 FFFE
-3 1111111111111101 FFFD
-4 1111111111111100 FFFC
-16 1111111111110000 FFF0
-256 1111111100000000 FF00

1. Dans une implémentation donnée, on peut connaître les caractéristiques des différents types entiers grâce à des

constantes (telles que INT_MAX, INT_MIN) définies dans le fichier en-tête climits.

2. Bien que non imposée totalement par la norme, cette technique tend à devenir universelle. Dans les

 (anciennes)*


mineures (deux représentations du zéro : +0 et -0, différence d’une unité sur la plage des valeurs couvertes pour une




                                               Remarques



1 Le nombre 0 est codé d’une seule manière (0000000000000000).

2 Si l’on ajoute 1 au plus grand nombre positif (ici 0111111111111111, soit 7FFF en

hexadécimal ou 32768 en décimal) et que l’on ne tient pas compte de la dernière retenue



(ou, ce qui revient au même, si l’on ne considère que les 16 derniers bits du résultat),

on obtient... le plus petit nombre négatif possible (ici 1000000000000000, soit

8000 en hexadécimal ou -32768 en décimal). Nous verrons qu’en C++, la situation dite

« de dépassement de capacité » (correspondant au cas où un résultat d’opération

s’avère trop grand pour le type prévu) sera traité ainsi, en ignorant un bit de retenue...

taille d’entier donnée).





                     2.3 Les types entiers non signés


De façon quelque peu atypique, C++ vous autorise à définir trois autres types voisins des précédents


en utilisant le qualificatif unsigned. Dans ce cas, on ne représente plus que des nombres

positifs pour lesquels aucun bit de signe n’est nécessaire. Cela permet théoriquement de

doubler la taille des nombres représentables ; par exemple, avec 16 bits, on passe de l’intervalle

[-32768; 32767] à l’intervalle [0 ; 65535]. Mais cet avantage est bien dérisoire, par rapport

aux risques que comporte l’utilisation de ces types (songez qu’une simple expression

telle que n-p va poser problème dès que la valeur de p sera supérieure à celle de n !).

En pratique, ces types non signés seront réservés à la manipulation directe d’un « motif

binaire » (tel un « mot d’état ») et non pas pour faire des calculs. Nous verrons d’ailleurs

qu’il existe des opérateurs spécialisés dits « de manipulation de bits ». Comme nous aurons

l’occasion de le rappeler, il est conseillé d’éviter de méler des entiers signés et des entiers





                           2.4 Notation des constantes entières


La façon la plus naturelle d’introduire une constante entière dans un programme est de

l’écrire simplement sous forme décimale, avec ou sans signe, comme dans ces exemples :

+533 48 -273

Vous pouvez également utiliser une notation octale (base 8) ou hexadécimale (base 16). La


forme octale se note en faisant précéder le nombre écrit en base 8 du chiffre 0.

Par exemple :

014 correspond à la valeur décimale 12,

037 correspond à la valeur décimale 31.

La forme hexadécimale se note en faisant précéder le nombre écrit en hexadécimal (les dix

premiers chiffres se notent 0 à 9, A correspond à dix, B à onze... F à quinze) des deux caractères

0x (ou 0X). Par exemple :

0x1A correspond à la valeur décimale 26 (16+10)

non signés dans une même expression, même si cela est théoriquement autorisé par la norme.

Les deux dernières notations doivent cependant être réservées aux situations dans lesquelles

on s’intéresse plus au motif binaire qu’à la valeur numérique de la constante en question.

D’ailleurs, ces constantes sont de type non signé (alors que les constantes écrites en notation

décimale sont bien signées).







                      Informations complémentaires


Par défaut, une constante entière écrite en notation décimale est codée dans l’un des deux

types signé int ou long (on utilise le type le plus petit, suffisant pour la représenter). On

peut imposer à une constante décimale

- d’être non signée, en la suffixant par « u », comme dans : 1u ou -25u ;

- d’être du type long, en la suffixant par « l », comme dans 456l ;

- d’être du type unsigned long en la suffixant par « ul », comme dans 2649ul.

Là encore, ces



                       3 Les types flottants

                   3.1 Les différents types et leur représentation en mémoire

Les types flottants permettent de représenter, de manière approchée, une partie des nombres

réels. Pour ce faire, ils s’inspirent de la notation scientifique (ou exponentielle) bien connue

qui consiste à écrire un nombre sous la forme 1.5 1022 ou 0.472 10-8 ; dans une telle notation,

on nomme « mantisses » les quantités telles que 1.5 ou 0.472 et « exposants » les quantités

telles que 22 ou -8.

Plus précisément, un nombre réel sera représenté en flottant, en déterminant deux quantités

M (mantisse) et E (exposant) telles que la valeur  M . B E

représente une approximation de ce nombre. La base B est généralement unique pour une

machine donnée (il s’agit souvent de 2 ou de 16) et elle ne figure pas explicitement dans la

représentation machine du nombre.

C++ prévoit trois types de flottants correspondant à des tailles différentes : float, double et

long double.

La connaissance des caractéristiques exactes du système de codage n’est généralement pas

indispensable, sauf lorsque l’on doit faire une analyse fine des erreurs de calcul1. En revan-




che, il est important de noter que de telles représentations sont caractérisées par deux

éléments :

• La précision : lors du codage d’un nombre décimal quelconque dans un type flottant, il est

nécessaire de ne conserver qu’un nombre fini de bits. Or la plupart des nombres s’exprimant

avec un nombre limité de décimales ne peuvent pas s’exprimer de façon exacte dans un tel

codage. On est donc obligé de se limiter à une représentation approchée en faisant ce que

l’on nomme une erreur de troncature. Quelle que soit la machine utilisée, on est assuré que

cette erreur (relative) ne dépassera pas 10-6 pour le type float et 10-10 pour le type long double.

• Le domaine couvert, c’est-à-dire l’ensemble des nombres représentables à l’erreur de troncature

près. Là encore, quelle que soit la machine utilisée, on est assuré qu’il s’étendra au

moins de 10-37 à 10+37.





                     4 Les opérateurs relationnels


Comme tout langage, C++ permet de comparer des expressions à l’aide d’opérateurs classiques

de comparaison. En voici un exemple :

2 * a > b + 5

Le résultat de la comparaison est une valeur booléenne prenant l’une des deux valeurs true ou

false.

Les expressions comparées pourront être d’un type de base quelconque et elles seront soumises

aux règles de conversion présentées dans le paragraphe précédent. Cela signifie qu’au bout

du compte on ne sera amené à comparer que des expressions de type numérique, même si

dans les opérandes figurent des valeurs de type short, char ou bool (true>false sera vraie).

Voici la liste des opérateurs relationnels existant en C++ :



                      Opérateur Signification


< inférieur à

<= inférieur ou égal à

> supérieur à

>= supérieur ou égal à

== égal à

!= différent de


Remarquez bien la notation (==) de l’opérateur d’égalité, le signe = étant réservé aux affectations.

En ce qui concerne leur priorité, il faut savoir que les quatre premiers opérateurs (<, <=, > et

>=) sont de même priorité. Les deux derniers (== et !=) possèdent également la même priorité,

mais celle-ci est inférieure à celle des précédents. Ainsi, l’expression :

a < b == c < d

est interprétée comme :

( a < b) == (c < d)

ce qui, en C++, a effectivement une signification, étant donné que les expressions a<b et c<d

sont, finalement, des quantités entières. En fait, cette expression prendra la valeur 1 lorsque

les relations a < b et c < d auront toutes les deux la même valeur, c’est-à-dire soit lorsqu’elles

seront toutes les deux vraies, soit lorsqu’elles seront toutes les deux fausses. Elle prendra la

valeur 0 dans le cas contraire.

D’autre part, ces opérateurs relationnels sont moins prioritaires que les opérateurs arithmétiques.

Cela permet souvent d’éviter certaines parenthèses dans des expressions.



                                11 L’opérateur conditionnel

                               Considérons l’instruction suivante :



if ( a>b )


max = a ;

else

max = b ;

Elle attribue à la variable max la plus grande des deux valeurs de a et de b. La valeur de max

pourrait être définie par cette phrase :

Si a>b alors a sinon b

En C++, il est possible, grâce à l’aide de l’opérateur conditionnel, de traduire presque littéralement

la phrase ci-dessus de la manière suivante :

max = a>b ? a : b

L’expression figurant à droite de l’opérateur d’affectation est en fait constituée de trois

expressions (a>b, a et b) qui sont les trois opérandes de l’opérateur conditionnel, lequel se

matérialise par deux symboles séparés : ? et :.

D’une manière générale, cet opérateur évalue la première expression qui joue le rôle d’une

condition. Comme toujours en C++, celle-ci peut être en fait de n’importe quel type. Si sa

valeur est différente de zéro, il y a évaluation du second opérande, ce qui fournit le résultat ;

si sa valeur est nulle, en revanche, il y a évaluation du troisième opérande, ce qui fournit le

résultat.

Voici un autre exemple d’une expression calculant la valeur absolue de 3*a + 1 :

3*a+1 > 0 ? 3*a+1 : -3*a-1

L’opérateur conditionnel dispose d’une faible priorité (il arrive juste avant l’affectation), de

sorte qu’il est rarement nécessaire d’employer des parenthèses pour en délimiter les différents


opérandes (bien que cela puisse parfois améliorer la lisibilité du programme). Voici,

toutefois, un cas où les parenthèses sont indispensables :

z = (x=y) ? a : b

Le calcul de cette expression amène tout d’abord à affecter la valeur de y à x. Puis, si cette

valeur est non nulle, on affecte la valeur de a à z. Si, au contraire, cette valeur est nulle, on


affecte la valeur de b à z.

Il est clair que cette expression est différente de :

z = x = y ? a : b

laquelle serait évaluée comme :

z = x = ( y ? a : b )

Bien entendu, une expression conditionnelle peut, comme toute expression, apparaître à son

tour dans une expression plus complexe. Voici, par exemple, une instruction (notez qu’il

s’agit effectivement d’une instruction, car elle se termine par un point-virgule) affectant à z la

plus grande des valeurs de a et de b :

z = ( a>b ? a : b ) ;

De même, rien n’empêche que l’expression conditionnelle soit évaluée sans que sa valeur*


soit utilisée comme dans cette instruction :

a>b ? i++ : i-- ;

Ici, suivant que la condition a>b est vraie ou fausse, on incrémentera ou on décrémentera la

variable i.





                                     13 L’opérateur sizeof



L’opérateur sizeof, dont l’emploi ressemble à celui d’une fonction, fournit la taille en octets

(n’oubliez pas que l’octet est, en fait, la plus petite partie adressable de la mémoire). Par

exemple, dans une implémentation où le type int est représenté sur 2 octets et le type double

sur 8 octets, si l’on suppose que l’on a affaire à ces déclarations :


int n ;

double z ;

• l’expression sizeof(n) vaudra 2 ;

• l’expression sizeof(z) vaudra 8.

Cet opérateur peut également s’appliquer à un type de nom donné. Ainsi, dans l’implémentation

précédemment citée :


• sizeof(int) vaudra 2 ;

• sizeof(double) vaudra 8.

Quelle que soit l’implémentation, sizeof(char) vaudra toujours 1 (par définition, en quelque


sorte).

Cet opérateur offre un intérêt :

• lorsque l’on souhaite écrire des programmes portables dans lesquels il est nécessaire de connaître

la taille exacte de certains éléments ;

• pour éviter d’avoir à calculer soi-même la taille d’objets d’un type relativement complexe

pour lequel on n’est pas certain de la manière dont il sera implémenté par le compilateur. Ce

sera notamment le cas des structures ou des objets.




                        3 L’instruction switch

                        3.1 Exemples d’introduction de l’instruction switch

                        a) Premier exemple



Voyez ce premier exemple de programme accompagné de trois exemples d’exécution.

#include <iostream>

using namespace std ;

main()

{ int n ;
cout << "donnez un entier : " ;

cin >> n ;

switch (n)

{ case 0 : cout << "nul\n" ;

break ;

case 1 : cout << "un\n" ;

break ;

case 2 : cout << "deux\n" ;

break ;

}
cout << "au revoir\n" ;

}

donnez un entier : 0

nul

au revoir

donnez un entier : 2

deux

au revoir

donnez un entier : 5

au revoir






                            2.3 Imbrication des instructions if



Nous avons déjà mentionné que les instructions figurant dans chaque partie du choix d’une

instruction pouvaient être absolument quelconques. En particulier, elles peuvent, à leur tour,

renfermer d’autres instructions if. Or, compte tenu de ce que cette instruction peut comporter

ou ne pas comporter de else, il existe certaines situations où une ambiguïté apparaît. C’est le

cas dans cet exemple :


if (a<=b) if (b<=c) cout << "ordonné" ;

else cout << "non ordonné" ;

Est-il interprété comme le suggère cette présentation ?


if (a<=b) if (b<=c) cout << "ordonné" ;

else cout << "non ordonné" ;

ou bien comme le suggère celle-ci ?

if (a<=b) if (b<=c) cout << "ordonné" ;

else cout << "non ordonné" ;

La première interprétation conduirait à afficher "non ordonné" lorsque la condition a<=b est

fausse, tandis que la seconde n’afficherait rien dans ce cas. La règle adoptée par le langage

C++ pour lever une telle ambiguïté est la suivante :

Dans notre exemple, c’est la seconde présentation qui suggère le mieux ce qui se passe.

Voici un exemple d’utilisation de if imbriqués. Il s’agit d’un programme de facturation avec

remise. Il lit en donnée un simple prix hors taxes et calcule le prix TTC correspondant (avec

un taux de TVA constant de 19,6 %). Il établit ensuite une remise dont le taux dépend de la

valeur ainsi obtenue, à savoir :

• 0 % pour un montant inférieur à 1 000 euros ;

Un else se rapporte toujours au dernier if rencontré auquel un else n’a pas encore

été attribué.

Les instructions de contrôle

• 1 % pour un montant supérieur ou égal à 1 000 euros et inférieur à 2 000 euros ;
• 3 % pour un montant supérieur ou égal à 2 000 euros et inférieur à 5 000 euros ;
• 5 % pour un montant supérieur ou égal à 5 000 euros.

#include <iostream>

using namespace std ;

main()

{ const double TAUX_TVA = 19.6 ;

double ht, ttc, net, tauxr, remise ;

cout << "donnez le prix hors taxes : " ;

cin >> ht ;

ttc = ht * ( 1. + TAUX_TVA/100.) ;

if ( ttc < 1000.) tauxr = 0 ;

else if ( ttc < 2000 ) tauxr = 1. ;

else if ( ttc < 5000 ) tauxr = 3. ;

else tauxr = 5. ;

remise = ttc * tauxr / 10

0. ;

net = ttc - remise ;

cout << "prix ttc = " << ttc << "\n" ;

cout << "remise = " << remise << "\n" ;

cout << "net à payer = " << net << "\n" ;
}
donnez le prix hors taxes : 500

prix ttc = 598

remise = 0

net à payer = 598

donnez le prix hors taxes : 4000

prix ttc = 4784

remise = 143.52

net à payer = 4640.48