C1 : INTRODUCTION Sommaire : 1 Préliminaire : - Se positionner dans l’ordinateur 2 Programmation : rapide tour d’horizon : - Compilateur - Variables, fonctions - Environnement de développement - Fonction « main » 3 Qu’est ce qu’une variable ? Plongée en mémoire : - Mémoire adresse - Adresse, « mots » - « Nombre-octets » (notation étendue, troncature) - Codage des nombres négatifs (à titre indicatif) - Des « types » de variables - Variables par unité ou par ensembles. - Se représenter les variables dans la mémoire Références bibliographiques 1 2 Introduction : Nous situer dans l’ordinateur et ses différents niveaux d’intervention. L’ordinateur : une hiérarchie d’abstractions, des niveaux ou encore machines virtuelles NIVEAU - ABSTRACTION 6 Programmes applicatifs 5 Langage de programmation 4 Langage assembleur 3 Noyau du système d’exploitation 2 Langage machine 1 Microprogramme 0 Logique numérique <-Nous sommes ici 0 : Logique numérique, circuits électroniques de l’ordinateur, portes logiques (ET, OU) univers binaire 0/1 1 : Microprogramme, premier niveau de langage, tous les ordinateurs ne le possèdent pas (des séquences d’étapes utilisées par le niveau 2 du langage machine) 2 : Langage machine : à ce niveau, ajouter 2 nombres, déplacer des données d’un emplacement vers un autre, déterminer si un nombre est égal à 0 sont des instructions élémentaires suffisantes pour exécuter n’importe quel programme ou application des niveaux plus élevés ! 3 : Noyau système exploitation : ordonnancer et allouer les ressources d’un ordinateur aux différents programmes s’exécutant sur la machine. Il peut être programmé dans un langage de programmation de haut niveau qui a été traduit en langage machine, c'est-à-dire compilé. Rappelons que Le langge C a été créé initialement pour écrire le système d’exploitation UNIX. En plus du noyau le système d’exploitation complet comprend des programmes applicatifs (niveau 6), le plus souvent un rôle d’interface (gestion des fichiers, gestion des fenêtres etc.) 4 : Le langage d’assemblage est une représentation symbolique des instructions rencontrées aux niveaux inférieurs. Un programme en langage d’assemblage est converti en instructions de niveau inférieur par un traducteur appelé un assembleur 5 : Les langages de hauts niveaux grâce auxquels les applications peuvent être écrites plus facilement qu’en langage assembleur. Il existe des milliers de langages à ce niveau (les plus connus, Basic, C, pascal, Cobol, Fortan, Lisp…) Ils font eux-mêmes l’objet d’une classification en strates C, C++, JAVA, JAVASCRIPT… par exemple) 6 : Les applications ou collections de programmes dans des domaines multiples et variés. 3 1 Compilateur, variables, fonctions, environnement, fonction « main » Compilateur Le compilateur est un programme qui lit un programme écrit dans un premier langage _ le langage source _ et le traduit dans un programme équivalent écrit dans un autre langage _ le langage cible _ Eventuellement il peut signaler des erreurs dans le programme source. Si l’on écrit un programme en C, le langage C est le source et lorsque l’on fabrique un exécutable, la compilation réalise sa traduction en langage machine. En général le compilateur est accompagné d’une interface de type traitement de texte qui permet l’écriture des programmes en langages source. Variables, fonctions Au niveau le plus basique de l’écriture de programmes on a des variables et des fonctions. Ecrire un programme c’est définir des variables et concevoir des fonctions qui correspondent aux traitements opérés sur les variables. Les fonctions sont simplement des ensembles d’instructions qui modifie les valeurs des variables, selon les traitements que l’on souhaite opérer sur ces variables, les données. La notion d’algorithme correspond à la construction des fonctions ainsi qu’à l’organisation des fonctions entre elles. Un algorithme est une suite finie d’instructions en vue de l’accomplissement d’une tâche. Environnement Il est impossible aux programmeurs de réinventer la roue à chaque nouveau projet ! En général tout projet s’appuie sur un environnement qui offre, outre le compilateur, des librairies de fonctions prêtes à l’emploi. Librairies standards Le langage C est accompagné d’un certain nombre de librairies dites « standards ». Elles comprennent des fonctions de base dans différents domaines. Par exemple <math.h>, <string.h>, <stdlib.h>, <stdio.h>, <time.h>, etc. printf(), scanf(), rand() Dans les exemples qui suivent, nous utiliserons les fonctions de la librairie <stdio.h> printf() et scanf() et de la librairie <stdlib.h> la fonction rand(). Nous détaillerons plus tard ces fonctions, principalement : - La fonction printf() permet d‘afficher une chaîne de caractères dans une fenêtre console. Elle utilise un fichier nommé stdout qui est automatiquement et invisiblement créé au lancement du programme. - La fonction scanf() permet de récupérer des entrée clavier Elle utilise un fichier stdin qui est créé en même temps que stdout. - La fonction rand() renvoie une valeur comprise entre 0 et RAND_MAX. D’autres librairies libres de droits ou pas dans tous les domaines D’autres librairies dans des domaines spécialisées peuvent être ajoutées et utilisées, par exemple la très bonne librairie « allegro » (A Low Level Game Routine) pour la création de jeux vidéos. 4 Fonction main() Pour le système d’exploitation, un exécutable se traduit par une pile d’instruction avec une entrée, en quelque sorte la « tête » du programme, pour le programmeur c’est la fonction main(). Ainsi tout programme commence par un main(). Selon le système d’exploitation ou l’environnement de développement, le main() peut avoir des caractéristiques spécifiques. Mais le main() standard a l’aspect suivant : Exemple de programme qui affiche dans une fenêtre console « bonjour » : int main() // tête ou entrée du programme, { // ouverture bloc d’instructions // appel de la fonction printf() qui affiche la chaîne de caractères passée // en paramètre printf(« bonjour ») ; // arrêter le programme pour avoir le temps de lire le résultat system(« PAUSE ») ; // valeur de retour de la fonction qui indique un bon déroulement. return 0 ; } // fermeture bloc d’instructions Le signe // dans un programme indique que tout ce qui suit n’est plus considéré comme du code opérationnel mais est un commentaire sur le code ou du code mis en commentaire. 2 Plongée en mémoire : adresse, « mots », nombre-octets, variables et types Mémoire, adresse, mots Selon les explications données par Alfred Aho et Jeffrey Ullman, la mémoire repose sur des « puces mémoires » ou « puces RAM ». Une puce est un circuit intégré, d’environ un centimètre carré qui rassemble un grand nombre de conducteurs et composants électriques. C’est là où sont casés les fameux « bits » en grande quantité. Leur nombre est toujours une puissance paire de deux ; c’est-à-dire 22i avec i un entier. Tous les bits ont une adresse et ils sont en quelque sorte alignés du premier, d’adresse « 0 », au dernier dont l’adresse correspond au nombre de bits sur la puce. Du fait de cette adresse il est possible de lire et d’écrire sur chaque bit de la puce mémoire [AHO, 1993, p. 171]. Mais la mémoire principale est construite avec pour unité de base l’octet : c’est la plus petite quantité de stockage. Pour avoir des octets, huit puces sont placées en parallèle. Huit puces sont alignées sur la même adresse. Chaque puce fournit alors un bit de l’octet et ils sont lus ou écrits en même temps. Lire un octet prend alors le même temps que de lire un bit. Ainsi La mémoire d’un ordinateur est une série d’emplacement numérotés contenant chacun un nombre. Le numéro d’un emplacement est appelé une adresse, un emplacement est un ensemble de 8 bits. Le microprocesseur pourra effectuer 2 opérations sur les octets de la mémoire : 1) Modifier le contenu d’un emplacement, la valeur précédente sera écrasée par cette opération 5 2) consulter le contenu d’un emplacement, cette opération n’altère pas la valeur courante de l’emplacement [MERCIER, 1989, p 25]. Remarque : Le micro processeur, cœur de l’ordinateur exécute un nombre limité d’instructions simples : - déplacer des données en mémoire - inversion de bits (NOT) - opérations logiques (ET, OU) - addition de bits - déplacement et rotation de bits [MERCIER, 1989, p. 28]. Adresse, mots Vient ensuite la nécessité de lire et d’écrire un « mot », c’est-à-dire de disposer de plusieurs octets consécutifs qui conservent possible l’accès à chacun d’entre eux. Le plus souvent la mémoire est construite sur la base de quatre octets consécutifs ; 32 bits qui ne remettent pas en cause la définition de l’octet comme plus petite unité de mémoire ayant sa propre adresse. Pour ce faire deux niveaux d’adresses sont dégagés. Celui du mot, de quatre octets en quatre octets, et celui, interne au mot, des octets qui le constitue. Si « a » est l’adresse d’un mot, « a+0 », « a+1 », « a+2 », « a+3 » sont les adresses de ses octets ; et « a » progresse de quatre en quatre, a est toujours divisible par quatre. Ce principe permet de conserver l’octet comme plus petite unité de stockage mais également, en 32 bits avec 32 puces parallèles, de bénéficier d’un quasi parallélisme sur quatre octets [AHO, 1993, p. 172]. Nombre octets (notation étendue – binaire – décimal -tronquature), La mémoire de l’ordinateur correspond à une réalité physique corrélée avec une réalité Idéelle humaine qui touche aux mathématiques : le nombre. Ce concept n’existe pas dans la nature. C'est-à-dire qu’au niveau même de la simple variable on a cette dualité entre réalité physique de l’ordinateur et imaginaire humain. Comment se relient la réalité physique de l’ordinateur et le concept humain de nombre ? Notation étendue Rappelons qu’un nombre décimal comme « 8542 » peut s’écrire de la façon suivante [LIPSCHUTZ, 1983, p. 2 à 5] : 8542 = 8*103 + 5*102 + 4*101 + 2*100 = 8*1000 + 5*100 + 4*10 + 2*1 = 8000 + 500 + 40 + 2 . Et cette décomposition constitue ce qui est nommée la notation étendue de l’entier De même un nombre binaire peut-être représenté en notation étendue. Si l’on prend par exemple le chiffre binaire 110101 on obtient : 110101 = 1*25 + 1*24 + 0*23 + 1*22 + 0*21 + 1*20 et il n’y a plus qu’à effectuer le calcul : 110101 = 32 + 16 + 0 + 4 + 0 + 1 = 53 De l’octet binaire aux nombres décimaux Les bits sont disposés de droite à gauche de la position 0 à la position 7 comprises. La puissance correspond à la position du bit (« pos » sur la figure 9.2) et la valeur à additionner résulte de la multiplication de 2pos par la valeur du bit. Le nombre 110101 de l’exemple ci-dessus donne l’octet suivant : 6 bit 8 bit 7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 pos 7 pos 6 pos 5 pos 4 pos 3 pos 2 pos 1 pos 0 27 26 25 24 23 22 21 20 0 0 1 1 0 1 0 1 0 +32 +0 +16 +0 +4 +0 +1 On constate que le nombre décimal le plus grand codé sur un octet est 255. Ce serait : 27 + 26 + 25 + 24 + 23 + 22 + 21 + 20 = 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = 255 Et comme la valeur zéro compte pour un nombre, sur un octet on a 256 nombres possibles, soit 28 cas. Le principe de cette addition est le même pour 16 bits avec des positions de 0 à 15 et avec cette fois 216 cas. Idem pour 24 bits avec 224 cas et 32 bits avec 232 cas. En fait pour n bits il y a n positions de 0 à n -1 ce qui donne un total de 2n cas. Dépassement de capacité Si le nombre à coder dépasse la possibilité de codage qu’offre l’espace mémoire, il y a un dépassement de capacité. Par exemple un seul octet ne peut pas restituer des nombres supérieurs à 255. Pour ces nombres il se comporte comme si un modulo 28 était effectué, et la partie qui dépasse est tronquée. Affecter la valeur 257 à un octet donne 257 modulo 256 ce qui est égal à 1 : 257 1 0 0 0 0 0 0 0 1 1 Codage des nombre négatifs (quelques précisions à titre indicatif) Entier signé ou non-signé Dans le langage C, tout nombre est implicitement dit « signé » ce qui veut dire qu’il peut prendre des valeurs négatives ou positives. Cette propriété est attribuée par défaut à tous les types d’entiers. Mais il est possible d’avoir des nombres « non signés » qui excluent la propriété d’être positif ou négatif et dont la grandeur sera calculée sans système de signe. Le mot clé utilisé pour définir un entier « non signé », est « unsigned ». L’éventualité pour un nombre entier d’être négatif nécessite le codage de cette propriété du nombre avec le nombre lui-même. Prenons par exemple un nombre codé sur un octet, c’est le type « char » que nous présentons plus loin. Pour qu’il soit signé on peut par exemple utiliser la dernière position, le huitième bit, comme bit de signe avec 0 pour positif et 1 pour négatif. Tous les autres bits sont alors réservés à la grandeur que peut prendre ce nombre. Dans ce cas on obtient pour un octet des valeurs de 127 à –127. Mais il y a plusieurs problèmes. D’abord il y aura deux 0, un positif et un négatif. Et aussi les opérations d’addition ou de soustraction entre nombres positifs et négatifs seront compliquées. Si x et y ont des signes différents le 7 signe de x+y est celui du plus grand en valeur absolue. Il faut donc commencer par connaître lequel des deux est le plus grand pour connaître le signe du résultat, puis soustraire du plus grand le plus petit. Il y a plusieurs méthodes possibles pour le codage des nombres négatifs. Nous allons évoquer la méthode du complément à deux. Cette méthode a l’avantage de permettre la transformation d’une soustraction en addition. Rappelons que parmi le nombre limité d’instructions simples que le microprocesseur effectue, il y a l’addition de bits. C’est une solution rapide. Principe du complément en binaire Le complément à 2 d’un nombre binaire c’est son inverse +1, par exemple : Nombre binaire inverse Complément à 2 11010011 00101100 00101101 11001100 00110011 00110100 Complément à deux dans un système de signes La notation en complément à deux départage les nombres positifs des nombres négatifs selon la valeur du bit de tête (le plus à gauche). S’il est égal à 0 le nombre est positif. Son interprétation est faite comme s’il s’agissait d’une grandeur non signée. Pour n bits, il sera compris dans la fourchette 2n – 1 – 1 à 0. Sur un octet les valeurs possibles seront comprises entre 127 et 0, c’est-à-dire de 27 – 1 à 0. En revanche si le bit de tête est égal à 1, il est interprété comme un nombre négatif. Puisque le bit de tête est égal à 1, les grandeurs s’échelonnent entre 2n – 1 et 2n – 1, c’est-à-dire pour un octet entre 128 et 255. Ces valeurs sont interprétées comme étant celles des compléments à deux des valeurs positives et, pour n bits, par rapport à 2n. Pour obtenir les valeurs négatives qu’elles désignent il suffit de soustraire 2n du complément à deux ce qui donne : CA – 2n = – A Exemple avec une taille des nombres limités à 4 octets Par exemple prenons n = 4. Nous pouvons visualiser la situation avec un tableau qui récapitule ce principe de codage du signe (figure 1) : Pour n = 4, il y a 16 nombres possibles qui vont de 0 à 15. Les nombres positifs vont de 0 à 7. 0 n’a pas de correspondant négatif car le complément à deux de 0 est 2 4 ramené à 0 par le modulo 24. Les nombres dont les valeurs littérales vont de 8 à 15 commencent par 1 et sont considérés comme négatifs. Ils sont interprétés comme des compléments à deux auxquels est soustrait chaque fois 24. On utilise le principe de la notation étendue et de l’addition des puissances de deux et l’on soustrait 24 ce qui produit la valeur négative correspondante. Notons que le complément à deux de cette valeur négative revient à la valeur positive (Le complément à deux du complément à deux donne la valeur de départ). Les deux nombres, positif et négatif, sont reliés par cette relation de complémentarité à deux. Si l’on additionne par exemple 7 et – 7 ça fait 0111 + 1001 = 10000, la retenue provenant du bit de tête, 24, est éliminée ce qui donne 0. 8 NOMBRES POSITIFS Valeur Valeur binaire Complément décimale correspondante à deux « CA » 0 0000 1 0001 1111 2 0010 1110 3 0011 1101 4 0100 1100 5 0101 1011 6 0110 1010 7 0111 1001 1000 Valeur décimale littérale 15 14 13 12 11 10 9 8 NOMBRES NEGATIFS Interprétation en nombre négatif du complément à deux avec la méthode : CA – 2n –1 –2 –3 –4 –5 –6 –7 –8 = 1*23 + 1*22 + 1*21 + 1*20 = 1*23 + 1*22 + 1*21 = 1*23 + 1*22 + 1*20 = 1*23 + 1*22 = 1*23 + 1*21 + 1*20 = 1*23 + 1*21 = 1*23 + 1*20 = 1*23 – 24 – 24 – 24 – 24 – 24 – 24 – 24 – 24 Fig. 1 : Système de signe, méthode du complément à deux. ( Pour mémoire rappelons que 20 = 1, 21 = 2, 22 = 4, 23 = 8, 24 = 16 ) Dépassement de capacité Le problème du dépassement de capacité peut se poser. L’addition de deux entiers positifs, pour être juste ne doit pas être supérieure à la valeur positive maximum qui peut être codée dans le système signé. Par exemple 7+7 = 14 en grandeur absolue soit 1110 en binaire. Dans notre système signé et sur 4 bits ça donne le nombre – 2. De même pour l’addition de nombres négatifs. -7 + - 7 = 1001 + 1001 = 10010 soit 0010 après suppression de la retenue c.à.d le nombre 2. En revanche le résultat est juste quelles que soient les opérations s’il reste dans les limites définies par le nombre n de bits du codage ( de 0 à 7 et de -1 à -8 dans notre exemple sur quatre bits) : -3+-4 = 1101 + 1100 =11001 soit 1001 après suppression de la retenue c.à.d le nombre -7 qui est le juste résultat. Nous constatons également que la soustraction est opérable par une addition, ce qui est une propriété des compléments à deux. Est additionné un nombre interprété négatif : 3 – 7 = 0011 + 1001 = 1100 ce qui représente le résultat – 4 . Types (char, short, int, long, float, double, pointeur) Le nombre de bits qui servent au codage des nombres définit la fourchette des valeurs possibles que pourront prendre ces nombres. Chacun des types correspond à une taille en octets : 1. « char » pour caractère, codé sur un octet, s’il est signé sa valeur va de -27 à 27 -1, soit de -128 à +127. S’il n’est pas signé sa valeur est comprise entre 0 et 28 -1, soit 255. 2. « short int » pour entier court codé sur deux octets est généralement abrégé en « short » . S’il est signé sa valeur va de -215 à 215 -1 c’est-à-dire de –32768 à 32767. S’il n’est pas signé sa valeur est comprise entre 0 et 216 -1, soit 65535. 3. « long int » pour entier long, codé sur quatre octets est abrégé en « long ». S’il est signé sa valeur va de -231 à 231-1 c’est-à-dire de –2147483648 à 2147483647. S’il n’est pas signé il prend des valeurs entre 0 et 232 –1, soit 4294967295. 9 4. En fonction de l’environnement de programmation, le « int » tout court est soit analogue au short soit analogue au long : « chaque compilateur est libre de choisir des tailles d’entiers adaptés à la machine sur laquelle il tourne, mais il doit respecter des tailles minimales de 16 bits pour les types short et int, et de 32 bits pour le type long. De plus les shorts ne doivent pas être plus longs que les ints, qui ne doivent pas être plus longs que les longs. » [KERNIGHAN, 1995, p. 36]. Dans l’environnement sur lequel nous travaillons, PC-Windows et compilateur Visual C++ 6, le « int » est sur quatre octets, analogue au long. 5. float et double correspondent aux nombres à virgule. Notons que par défaut ils sont signés (c'est-à-dire qu’ils acceptent des valeurs positives et négatives) sinon ils sont préfixés par le mot-clé « unsigned ». 6. le pointeur est un type particulier de variable : une variable qui peut contenir une adresse mémoire ( en général un nombre hexadécimal) et rien d’autre. C’est une variable dont le rôle est de permette d’accéder à des adresses mémoire. Variables par unité ou par ensembles : Les variables peuvent être appréhendées par unités ou par ensemble selon deux optiques : - le Tableau est un ensemble d’objets de même type. Par exemple un tableau de 10 chars se déclare de la façon suivante dans un programme : char tab[10] ; // 1*10=10 octets un tableau de 34 entiers : int toto[10] ; //4*10= 40 octets Un tableau de 12 pointeurs : Char* str[15] ; // 4*15= 60 octets Taille de tableaux En C la taille du tableau doit être explicite, taille de l’objet*taille du tableau donne la taille totale réservée en mémoire. Accès aux éléments d’un tableau L’accès aux différents éléments se fait avec l’opérateur crochet [ ] et tous les éléments sont indicés de 0 à Taille du tableau : toto[0] toto[1] toto[2] toto[3] toto[4] ... toto[9] // éléments numérotés de 0 compris à 9 compris - la structure est un ensemble d’objets de types différents Par exemple une structure pour stocker les caractéristiques d’un personnage pourra être déclarée comme suit dans un programme : struct ennemi{ char* name; // nom de l’ennemi char injure[80] ; // injure potentielle int x,y ; // la position à l’écran int pasx, pasy ; // la vitesse du déplacement int style ; // style d’ennemi 10 int dtmps ; int maxt ; int tmps ; // heure entrée // temps maximum d’apparition // temps courant // etc. }; Taille d’une structure La structure a une taille fixe. La taille d’une structure en mémoire est légèrement supérieure à la somme des tailles de ses éléments à cause de l’alignement des adresses mémoire (voir « Se représenter les variables en mémoire » plus bas). Accès aux éléments d’une structure L’accès aux différents champs de la structure se fait avec l’opérateur . (point). Soit une structure dans le programme : struct ennemi ee ; ee.name= « arthur » ; strcpy(ee.injure, « gros chnouf ») ; ee.x=rand()%TX ; ee.y=rand()%TY ; // nom de l’ennemi // vocabulaire de l’ennemi // position horizontale aléatoire dans l’écran // position verticale aléatoire dans l’écran Remarque : il peut y avoir des tableaux dans une structure et il peut y avoir des tableaux de structures. Par exemple dans un programme la déclaration : struct ennemi tab[50] ; Cette déclaration réserve un bloc de mémoire consécutive de la taille de 50 structures « ennemi ». Se représenter les variables dans la mémoire Lorsque dans le programme des variables de différents types sont déclarées, les octets qui leur sont nécessaires sont alloués de façon consécutive dans la mémoire principale de l’ordinateur à partir d’une adresse donnée. Toutefois, à part pour le type char (un octet) les variables ont besoin de commencer sur une frontière de mot, c’est-à-dire à une adresse divisible par quatre, ce que nous avons vu un peu plus haut. Ainsi, soit la déclaration suivante : struct{ char toto ; int titi ; }; Cette structure va probablement demander huit octets et non pas cinq en mémoire du fait d’un besoin d’alignement de l’entier « titi » sur une frontière de mot. Cette nécessité d’alignement peut engendrer « des trous » non référencés dans la structure [KERNIGHAN, 1995, p.136]. Admettons maintenant la déclaration suivante : 11 int i, j ; char tab[6] ; struct bob{ int titi ; char toto[5] ; }; int calc[5] ; Et prenons arbitrairement 100 comme première adresse pour la variable « i » ; « j » est à 104, (le type int représente quatre octets ) « tab » commence ainsi à 108 jusque 114 (le char fait un octet). « bob » commence sur une frontière à 116 donc il n’y a rien de 114 à 116. « bob.titi » prend quatre octets jusque 120 et « toto » cinq octets, jusque 125. A nouveau rien de 125 à 128 et « calc » commence sur une frontière à 128 jusque 148. Cette situation est illustrée par la figure ci-dessous. 100 104 108 114 116 116 i 120 j tab[0] . . . tab[5] 125 128 148 bob.titi bob.toto[0] ... bob.toto[4] struct bob calc[0] . . . calc[4] Se représenter des variables en mémoire Références bibliographiques AHO Alfred, ULLMAN Jeffrey, Concepts fondamentaux de l’informatique, Dunod, Paris 1993. BRAQUELAIRE Jean-Pierre, Méthodologie de la programmation en C, Masson, Paris 1998. KERNIGHAN Brian, RITCHIE Dennis, Le langage C, Masson, Paris 1995, première édition 1978. LIPSCHUTZ Seymour, Mathématiques pour informaticiens, McGraw-Hill International, Angleterre 1983. MERCIER Philippe, Assembleur, une découverte pas à pas, Marabout Informatique, Alleur (Belgique) 1989. 12