Méthodes de programmation C++

éléements de cours

Xavier Serpaggi

La programmation orientée objet est née dans les années 1960 avec le langage Simula 67 dont un des objectifs était de faire de la simulation. Ce n'était que les prémices et il est admis que le premier langage objet (incluant les concepts fondamentaux) est Smalltalk, développé par Xerox dans les années 1970.

Le pic d'intérêt pour les langages à objets s'est vraiment produit dans les années 1980 avec Objective C, C++, Eiffel,...

Les langages de programmation modernes sont le plus souvent objet (Java, C#, .NET, ...) et ces concepts sont même adoptés par certains langages de script comme Ruby ou Python.

Une intéressante fresque sur l'histoire des langages de programmation est disponible à l'adresse suivante : http://www.levenez.com/lang/.

Le cours de Méthodes de programmation/C++ a été mis en place pour vous apporter quelques connaissances en langage C++ et vous familiariser avec les concepts de la programmation orientée objet. Le but est que vous puissiez être opérationnel rapidement une fois confronté à du code écrit dans ce langage ou même dans un autre langage à objets.

Le cours méthodes de programmation présente une vision théorique des structures de données et des algorithmes informatiques de base.

Ce document, destiné à vous accompagner dans vos enseignements, propose un bref rappel des structures de données fondamentales ainsi qu'un survol des notions clés de la programmation orientée objet.

Table des matières [+]

  1. Rappels
    1. Ordinateur
    2. Représentation des données
    3. Structures de données
  2. Principes de la programmation orientée objet
    1. Classe
    2. Héritage
    3. Polymorphisme
    4. Encapsulation
    5. Conclusion
  3. Programmer en C++ — principes de base
    1. Types, variables et constantes
    2. Procédures
    3. Structures de contrôle de base
    4. Programmation C++

Rappels

Rappels de base sur la représentation des données dans un ordinateur, sur les structures de données et les structures de contrôles appliqués au C++.

Ordinateur

La définition d'un ordinateur, telle que prise sur Wikipedia, est la suivantes :

Un ordinateur est une machine dotée d'une unité de traitement lui permettant d'exécuter des programmes enregistrés. Cette machine permet de traiter automatiquement les données, ou informations, selon des séquences d'instructions prédéfinies appelées aussi programmes.

Elle interagit avec l'environnement grâce à des périphériques comme le moniteur, le clavier, la souris, l'imprimante, le modem, le lecteur de CD (liste non-exhaustive). Les ordinateurs peuvent être classés selon plusieurs critères (domaine d'application, taille ou architecture).

La technologie actuelle d'ordinateurs date du milieu du XXe siècle. Dans cette technologie, un ordinateur est un ensemble de circuits électroniques qui manipulent des données sous forme binaire.

Les parties misent en évidence sont celles qui ont le plus de sens pour nous : dans le cadre de ce cours nous allons écrire des programmes (dans un langage de programmation particulier) pour traiter des données de façon automatique.

Il faudra sans cesse garder à l'esprit que les données que nous manipulons sont représentées sous forme binaire et que cela impose certaines limitations, comme nous allons le voir dans le parties suivantes.

Représentation des données

Les ordinateurs que nous utilisons tous les jours, de la simple machine à calculer au plus complexe des super-calculateurs en passant par les téléphones portables et les téléviseurs, ont tous en commun la représentation des données qu'ils manipulent. Tous ne savent manipuler que des données binaires, c'est à dire des séquences de 0 et de 1. Les calculs se font donc en base 2.

Un ordinateur va interpréter différemment ces données en fonction du contexte, mais dans tous les cas, l'ensemble de ce qui est représentable à l'aide de ces bits (ces 0 et ces 1) est fini du fait qu'un ordinateur a besoin d'espace de stockage pour se souvenir de ces données et que cet espace est lui même fini.

La manipulation des données par un ordinateur se fait par blocs de 0 et de 1, en général des blocs de 8 bits, que l'on nomme des octets. C'est l'unité de base avec laquelle il est possible de représenter 28 = 256 valeurs (de 0 à 255).

Pour résoudre certains problèmes, des formats particuliers ont été créés, permettant de représenter de manière efficace telle ou telle donnée (un caractère, un nombre, etc.)

Les sections suivantes exposent certaines de ces représentation en fonction de leur domaine d'exploitation.

Caractères

Bien qu'utilisant toujours des nombres, les façons de représenter des caractères au sein d'un ordinateur sont nombreuses et variées. Ceci s'explique en général par certaines contraintes techniques liées au domaine d'utilisation.

Une représentation cependant, s'est imposée et reste encore très utilisée de nos jours : le codage ASCII, qui dans ses dernières versions est capable de représenter 256 caractères (un octet est utilisé) dont 95 sont effectivement affichables (les autres étant des caractères de contrôle ou des caractères dits semi-graphiques).

Ce codage est parfait pour l'anglais, mais n'offre pas la possibilité de représenter tous les caractères de toutes les autres langues. Et de toutes façons, le fait de ne pouvoir représenter que 256 symboles impose d'utiliser un autre codage pour des langues telles que le chinois qui possède plusieurs milliers d'idéogrammes. La norme Unicode a été établie pour palier ces manques, mais dans le cadre de ce cours nous ne manipulerons principalement que du codage ASCII.

Nombres

La représentation des nombres est également délicate. Depuis que nous sommes tout petits nous avons appris à compter et à faire la différence entre les différents groupes de nombres.

Le vocabulaire mathématique est très riche dans ce domaine et nous disposons des entiers naturels, des entiers relatifs, des rationnels, des réels et même des nombres complexes.

Chacun de ces groupes contient une infinité de nombres et c'est un des problèmes : tous ne seront pas représentés au sein d'un ordinateur.

Les groupes d'entiers vont être représentables de manière exacte au sein d'un ordinateur (mais n'oublions pas, toujours en nombre limité).

Les nombres qui ne sont pas des entiers soulèvent un autre problème en plus de la limite du nombre d'éléments représentables : la précision de la représentation est également limitée.

Entiers

Les entiers sont représentés simplement par des groupes pairs de deux à huit octets (en grande majorité).

  • 2 octets, soit 16 bits, soit 216 valeurs représentables,
    • [0 ; 216-1][0 ; 65 535]
    • ou [-215 ; 215-1] → [-32 768 ; 32 767]
  • 4 octets soit 32 bits, soit 232 valeurs représentables,
    • [0 ; 232-1][0 ; 4 294 967 295]
    • ou [-231 ; 231-1] → [-2 147 483 648 ; 2 147 483 647]
  • 8 octets soit 64 bits, soit 264 valeurs représentables,
    • [0 ; 264-1][0 ; 18 446 744 073 709 551 615]
    • ou [-263 ; 263-1][-9 223 372 036 854 775 808 ; 9 223 372 036 854 775 807]

La représentation d'un entier au sein d'un ordinateur correspond donc exactement à l'entier mathématique que l'on imagine représenter, tant que ce dernier est compris dans l'intervalle des valeurs représentables pour le nombre d'octets utilisés.

Flottants

Pour ce qui est de la représentation des nombres autres que les entiers, c'est plus délicat puisque l'on doit représenter un nombre très important de valeurs avec un seul et même format. La représentation à virgule flottante tente de résoudre ce problème en représentant un nombre réel avec une notation scientifique.

L'IEEE a proposé un standard de représentation sous la norme IEEE 754 dont le principe est exposé ici.

Chaque nombre flottant est représenté par un triplet (signe,mantisse,exposant) et utilise un certain nombre d'octets (4 ou 8) pour être stocké.

Le nombre résultant peut alors s'écrire sous la forme R = (-1)signe × (1+mantisse) × 2exposant

Selon la norme IEEE 754, il a y principalement deux représentations : simple précision et double précision. Le tableau ci-dessous donne les tailles en bits de chacun des éléments du triplet.

Nombre de bits utilisés pour représenter chacun des éléments d'un flottant IEEE 754
PrécisionSigneExposantMantisse
Simple1823
Double11152

Pour pouvoir représenter les exposants négatifs et éviter la notation en complément à deux qui pouvait poser problème, le choix a été fait de rajouter une constante (un biais) à l'exposant.

C'est à dire que, dans le cas de la représentation en simple précision, la valeur qui est stockée dans les 8 bits de l'exposant est égale à exposant + 127. Les valeurs possibles pour l'exposant varient donc de -126 à +127.

Dans le cas de la double précision, le biais est de 1023, ce qui permet d'avoir des exposants variant de -1022 à +1023.

Chaque bit de la mantisse représente un coefficient de la somme pondérée de toutes les puissances négatives de la base (ici 2). En nommant les bits de celui de poids le plus fort à celui de poids le plus faible bi (i ∈ [1;23]), la valeur de la mantisse est : b1 × 2-1 + b2 × 2-2 + ... + b23 × 2-23 pour une représentation en simple précision.

Pour illustrer tout cela, prenons le simple nombre -1,5. Sa représentation en simple précision est 1 01111111 10000000000000000000000.

  • Le bit de signe vaut 1, ce qui signifie que le nombre est négatif. signe = 1
  • L'exposant se calcule en retirant 127 à la valeur stockée. 01111111(base 2) = 127(base 10), donc 127 - 127 = 0. exposant = 0
  • La mantisse commence par un 1 et ne contient ensuite que des 0, donc sa valeur se calcule simplement comme étant 1 × 2-1 = 0.5. mantisse = 0.5

Au final, notre nombre s'exprime comme (-1)1 × (1 + 0,5) × 20 = -1,5

Le lecteur pourra faire l'exercice consistant à trouver le nombre en base 10 correspondant à cette représentation : 0 01111111 10011001100110011001101.

Ce qui est important de retenir de cette présentation des nombres au sein des ordinateurs, c'est que la conversion d'un nombre décimal n'est pas forcément exacte. Il y a de fait une approximation qui est imposée et les calculs résultants sont parfois mathématiquement faux.

Un exemple de ces approximation est la représentation en IEEE 754 simple précision de la valeur 0,1. C'est un nombre rationnel on ne peut plus simple et la conversion donne le mot de 4 octets suivant : 0 01111011 10011001100110011001101. Mais en opérant la transformation inverse pour obtenir à nouveau le nombre décimal, on obtient la valeur 0.10000000149011611938477.

Structures de données de base

Tout programme informatique manipule des données. Il arrive rapidement un moment où l'on se rend compte que l'organisation des ces données est importante et peut influer de manière significative sur les performances du programme.

Le but de ce rappel n'est pas de se substituer à un cours d'algorithmique, mais de redonner une rapide description des structures courantes permettant d'organiser les données.

Le point commun à ces structures est que toutes sont stockées dans la mémoire vive de l'ordinateur au moment où le programme s'exécute. Mémoire que l'on peut imaginer comme un grand espace segmenté en cases ayant toutes la même taille (un octet ou 8 bits) ; chacune des cases étant repérée par son adresse.

Tableau

Le tableau est la structure la plus simple. Elle permet de stocker un nombre prédéfini d'éléments ayant tous le même type. Certains langages de programmation autorisent l'utilisation de tableaux dynamiques dont la taille n'est pas connue a priori et/ou peut varier au cours de l'exécution : c'est le cas du C++.

D'un point de vue de l'organisation de la mémoire, tous les éléments d'un tableau sont stockés les uns à la suite des autres. Ainsi, si l'on connaît l'adresse de la case mémoire représentant le premier élément du tableau et la taille unitaire des données qu'il stocke, il est possible de connaître l'adresse de n'importe lequel de ses éléments.

L'avantage est un accès direct et rapide à n'importe quel élément. Mais une lourdeur apparaît si l'on désire, par exemple, insérer ou retirer un élément au milieu du tableau : il va falloir procéder à des décalages pour faire la place au nouvel arrivant ou pour éviter les trous.

Les tableaux sont en général à une dimension (un vecteur). Cependant les langages de programmation proposent souvent des raccourcis syntaxiques permettant au programmeur d'avoir l'impression qu'il manipule des tableaux à plusieurs dimensions.

Liste chaînée

La liste est une structure complexe, définie comme une suite de cellules qui ont toutes la même organisation interne et qui permettent de stocker un ensemble de données éventuellement hétérogènes. La gestion d'une liste est à la fois plus souple et plus délicate que celle d'un tableau.

Cette souplesse est obtenue via l'adoption d'une organisation mémoire plus complexe que celle des tableaux. En effet, contrairement à ces derniers, toutes les cellules d'une liste ne sont pas forcément contiguës en mémoire : l'adresse de la deuxième cellule n'est plus égale à l'adresse de la première cellule à laquelle on ajoute l'occupation mémoire d'une cellule.

Ceci nécessite d'adopter un mécanisme de chaînage des cellules qui va, pour chacune, préciser quel est la suivante. Chaque cellule va donc contenir une référence (une adresse mémoire) à la cellule suivante. C'est ce point qui est délicat dans la gestion des listes. Une mauvaise gestion des références peut faire perdre une partie de la liste et gaspiller de la mémoire (on parle de fuite mémoire).

Cette organisation empêche donc d'accéder rapidement et directement à une quelconque cellule de la liste : il faut toujours partir de la première et avancer jusqu'à trouver la bonne.

Par contre, l'ajout ou la suppression d'une cellule est bien plus simple puisque cette fois, il n'y a plus besoin de procéder à des décalages, que ce soit dans un sens ou dans l'autre, il suffit de créer une nouvelle cellule en mémoire et de redéfinir quelques références pour la relier logiquement aux autres.

Différents types de listes existent comme les listes doublement chaînées (référence vers la cellule suivante et vers la cellule précédente) ou encore les anneaux (la liste ne se termine jamais, la dernière cellule a une cellule suivante qui est la première cellule.

Arbre

L'arbre est une structure de données que l'on peut imaginer comme une généralisation de la liste chaînée (en fait, il est plus raisonnable d'imaginer la liste comme un cas particulier de l'arbre). Le principe est le même que la liste puisque la structure permet de stocker des données hétérogènes au sein de cellules. Mais cette fois, chaque cellule contient des références à plusieurs cellules que l'on nomme cellules filles. La cellule qui n'a pas de père est appelée cellule racine et une cellule qui n'a pas de fille est appelée feuille.

Les méthodes de traitement particulières aux listes ne s'appliquent pas exactement aux arbres ; des méthodes différentes existent.

L'utilisation des arbres en informatique est très répandue dans le domaine de la recherche d'information (arbre binaire de recherche, arbre de localisation, arbre lexicographique, ...) ou dans la description de parcours logiques (les différents coups à jouer sur un échiquier par exemple).

Graphe

Un graphe est une structure de données organisées en sommets et en arrêtes. Un sommet est éventuellement relié à un autre via une ou plusieurs arrêtes. Les arrêtes peuvent être orientées ou non, pondérées ou non.

Contrairement aux trois structures précédentes, il est très rare, d'un point de vue informatique, de représenter un graphe tel quel. En effet, la gestion de sommets ayant un nombre non fixe de relations, orientées ou non, pondérées ou non, s'avère ne pas être optimale. Pour contourner ce problème des représentation à base de tableaux (matrices d'adjacences) ou de listes (listes d'incidences) ont été imaginées.

D'un point de vue informatique, ce qui est intéressant avec les graphes c'est non pas le fait qu'il puisse stocker des données, mais plutôt qu'il soit très adapté à la modélisation de nombreux problèmes et qu'il existe de nombreux algorithmes permettant de les traiter. Par exemple, un graphe peut représenter un réseau routier et l'algorithme dit du plus court chemin permet de trouver la moyen le plus rapide de se rendre d'un endroit à un autre.

Principes de la programmation orientée objets

Nous présentons ici les principes généraux de la programmation orientée objet. Nous donnerons des exemples en langage C++ en nous prendrons comme sujet d'étude les véhicules.

Classe

Une classe est une entité permettant de représenter un concept. Cette entité n'a pas d'existence tant que la classe en question n'est pas instanciée.

Un classe vehicule peut par exemple représenter le concept de véhicule.

Objet

Le terme d'objet est parfois utilisé pour désigner l'instance d'une classe. La classe reste dans le domaine du conceptuel alors que l'objet lui, est ce qui est manipulé lors de l'exécution du programme.

Attributs

Une classe peut posséder des attributs (ou encore des champs), qui représentent des valeurs, éventuellement de différents types, en nombre choisi par le programmeur.

Notre classe vehicule peut par exemple contenir des attributs comme le nombre de places disponibles, la quantité de carburant dans le réservoir, la vitesse maximale et la consommation horaire.

Méthodes

Une classe propose des méthodes qui permettent de lui faire passer des messages et de nous renvoyer des réponses. C'est par exemple au travers de méthodes que l'on peut accéder à la valeur d'un attribut ou même, qu'il est possible de la modifier. Mais une méthode peut aussi simplement décrire un comportement de notre classe. Par exemple, nous pouvons imaginer avoir une méthode qui permette de mettre un véhicule en route, ou une méthode qui donnerait la quantité de carburant disponible dans le réservoir.

Constructeur

Le constructeur est une méthode particulière qui est automatiquement appelée lors de la création d'un objet. Elle a pour but de procéder à diverses initialisations qu'il n'est souvent pas possible de faire ailleurs.

Destructeur

Le destructeur est le pendant du constructeur au moment de la destruction d'un objet. Il est là pour éventuellement faire le ménage avant de partir et laisser la mémoire de l'ordinateur dans un état cohérent.

Il existe beaucoup de cas où il n'est pas nécessaire de détailler ce que fait le destructeur. En effet, certaines classe ne font pas d'allocation dynamique de mémoire et le fait de détruire une instance de la classe va effectivement libérer toute la mémoire que cette dernière utilisait.

Il est cependant tout à fait possible d'écrire un destructeur vide uniquement pour préciser que l'on sait qu'il n'y a pas besoin de faire autre chose que ce que le langage fait par défaut.

Structure

Une structure peut être vue rapidement comme une classe qui ne possède que des attributs et pas de méthodes. Nous pouvons imaginer que c'est un espace de stockage de données éventuellement hétérogènes mais que l'on désire manipuler comme une seule entité.

Héritage

L'héritage est l'un des concepts fondateurs de la programmation orientée objets. C'est la possibilité de définir de nouvelles classes à partir de classes parentes. Ces nouvelles classes vont hériter des attributs et des méthodes de leur super classe.

Pour rester dans le domaine des véhicules, nous pouvons imaginer créer une classe spécifique aux avions qui hériterai de véhicule. Un avion est un véhicule et à ce titre possède les mêmes propriétés qu'un véhicule générique. Cependant il a des spécificités comme par exemple, son altitude de vol recommandée ou encore le nombre de moteurs dont il est équipé ainsi que de leur type ou bien encore ses capacités en terme d'avionique.

Une classe qui hérite d'une autre classe possède donc toutes les caractéristiques de la classe parente plus éventuellement certaines que le programmeur décide de rajouter. Ceci est vrai tant pour les attributs que pour les méthodes.

Héritage multiple

Parfois, lorsque l'on défini une nouvelle classe et que l'on décide de la faire hériter d'une autre, peut se poser le problème de déterminer de quelle classe il est le plus approprié de partir. Par exemple, la classe hydravion héritera-t-elle de la classe bateau ou de la classe avion ?

Pour répondre à ce problème, certains langages permettent de faire de l'héritage multiple. Notre classe hydravion pourrait donc hériter à la fois des propriétés des avions et des bateaux.

Tous les langages n'autorisent pas cette possibilité. Par exemple, l'héritage multiple est possible en C++ mais n'existe pas en Java.

Polymorphisme

Le dernier concept important de la programmation orientée objets est le polymorphisme. Il est directement dépendant du concept d'héritage et n'a pas de sens hors de ce contexte.

Grâce à ce mécanisme, une classe peut redéfinir certaines des méthodes ou certains des attributs d'une classe dont elle hérite. Par exemple, pour une classe avion qui hériterai de la classe vehicule il serait possible de redéfinir la méthode avancer() pour que la vitesse soit donnée en nœuds (kt) et qu'il y ait une prise en compte de l'altitude et de la température extérieure.

Jusque là ce principe est déjà très intéressant. Mais il l'est encore plus quand on considère qu'un avion est toujours un véhicule, au même titre par exemple qu'un autobus. Nous pouvons donc imaginer une parc de véhicules (qui serait représenté par un tableau de vehicule) et à chaque fois que l'on fait appel à la méthode avancer() sur l'un de ces véhicules, la méthode correspondant au type le plus bas dans l'arbre d'héritage est utilisée : c'est bien un avion ou un autobus que l'on fait avancer, plus un simple véhicule.

La souplesse apportée par le polymorphisme permet au programmeur plus de liberté tout en gardant un cadre strict de vérification.

Encapsulation

Un autre concept important de la programmation orientée objets est l'encapsulation. C'est à dire, la possibilité d'embarquer dans une classe, des objets d'autres classes et éventuellement, masquer leur présence en n'en permettant l'accès que par des méthodes particulières et maîtrisées.

La classe vehicule que nous présentons depuis le début peut proposer une forme simple d'encapsulation via des méthodes qui retourneraient le quantité de carburant maximale ou disponible, ou encore le nombre de personnes que l'on peut transporter. Au lieu d'accéder directement aux champs correspondants pour en lire ou en modifier la valeur, la classe propose une interface par l'utilisation de ces méthodes. Cela permet à son concepteur de maîtriser l'accès aux attributs. Et si, pour continuer sur notre exemple, le concepteur désirait stocker la consommation en litres par secondes et la présenter à l'utilisateur de la classe en gallon par heure, il lui suffirait de modifier la méthode permettant d'obtenir la valeur de la consommation, sans pour autant toucher à la mécanique interne de la classe.

Protection

Dans l'exemple précédent sur la consommation, nous pouvons légitimement nous demander pourquoi l'utilisateur d'une classe irait faire appel à des méthodes (avec un surcoût en terme de temps d'exécution) plutôt que de lire ou écrire directement les valeurs des attributs, surtout s'il sait à quoi correspondent les valeurs, quelles en sont les unités, etc.

La réponse est que, le concepteur de la classe d'origine aura pris soin de protéger certains attributs et certaines méthodes pour que tout programmeur utilisant cette classe ne puisse pas y accéder directement. Ceci lui permet de proposer une sorte de boîte noire avec quelques boutons en façade dont on sait précisément quel sera le comportement en fonction de l'action que l'on mène dessus. C'est l'interface de la classe.

Pour arriver à cet objectif, il existe différents niveaux de protection pour les méthodes et les attributs :

  • private : seule la classe a le droit d'accéder aux attributs et méthodes définis ainsi ;
  • protected : seule la classe et ses descendantes ont le droit d'accéder aux attributs et méthodes définis ainsi ;
  • public : tout le monde a le droit d'accéder aux attributs et méthodes définis ainsi (en C++ c'est le niveau par défaut lorsque rien n'est précisé).

Conclusion

Ce que nous avons présentés ci-dessus n'a pas pour ambition d'aller plus loin que de décrire rapidement les concepts fondamentaux de la programmation orientée objets.

Ces concepts se retrouveront dans tous les langages orientés objets et même si certaines spécificités existent de l'un à l'autre, vous ne devriez pas être déroutés par les concepts.

Même si les exemples ont étés donnés en C++, ils sont loin d'être complets et sont, en l'état actuel des choses non fonctionnels. Cependant, vous trouverez ci-dessous une version de la classe vehicule qui reprend un bon nombre de concepts abordés dans ce chapitre.

class vehicule {
private:
	int tPlaces;		// places disponibles conducteur compris
	int qPlaces;		// places disponibles a un moment donne

	int tCarburant;		// un code representant le carburant utilise
	float qCarburant;	// quantite de carburant disponible
	float conso;		// conso. instant. en l/h a la vitesse vc

	float vMax;		// vitesse maximale (m/s)
	float vc;		// vitesse a laquelle est relevee la conso.

public:
	vehicule(int tP, int tC, float c, float v, float vconso)
	:tPlaces(tP),qPlaces(tP),tCarburant(tC),
	 qCarburant(0),conso(c),vMax(v),vc(vconso)
	{ }

	~vehicule()
	{ }

	int getCarburant()
	{
		return qCarburant;
	}

	int getPlaces()
	{
		return qPlaces;
	}

	float getConso()
	{
		return conso;
	}

	// ajoute q litres de carburant
	void ajouteCarburant(float q)
	{
		qCarburant = qCarburant + q;
	}

	// fait parcourir au vehicule d metres a la vitesse v
	// Cette methode semble pouvoir etre dependante du type de vehicule,
	// elle est donc declaree virtuelle pour pouvoir etre redefinie dans
	// les classes filles.
	virtual bool avancer(float d, float v) ;
} ;

Programmer en C++ — principes de base

Un programme C++ est composé d'une suite d'instructions dont le but est d'accomplir une tâche précise. Ce programme, écrit dans un langage de haut niveau comme le C++, n'a pas vocation a être exécuté par l'ordinateur. En fait, ce dernier ne comprends pas ce langage de haut niveau et une phase de traduction/transformation est nécessaire : c'est la compilation effectuée par un programme spécial nommé compilateur.

Types, variables et constantes

Le C++ est un langage typé, c'est à dire qu'il manipule des données dont on connaît à chaque instant le type (un entier, un flottant, une chaîne de caractères, ...). Ces données sont stockées dans des variables et du type de ces variables va dépendre l'information qu'il est possible d'y stocker.

Types

Les types de base du langage sont les suivants :

char,
short int, unsigned short int,
int, unsigned int,
long int, unsigned long int,
bool,
float,
double,
long double

Il existe également un type nommé void qui signifie l'absence de type. Donc, s'il n'y a pas de type, il n'y a pas de variable ; pourtant, c'est un type fort utile et nous en reparlerons plus loin.

Variables

Une variable est un espace de stockage, une zone de la mémoire de l'ordinateur, qu'il nous est possible de manipuler. Une variable a un type qui est défini au moment de sa création et il n'est pas possible qu'elle en change pendant sa durée de vie.

Le contenu d'une variable peut changer pendant l'exécution d'un programme. C'est d'ailleurs en général le but des variables : stocker une information donnée à un moment donné.

L'exemple suivant nous montre comment déclarer une variable de type double dont le nom est var et comment lui donner une valeur :

double var ;
var = 1.618 ;

Vous remarquerez au passage, que chaque instruction se termine obligatoirement par un pont-virgule et cela pour que le compilateur sâche où elles se terminent.

Bien entendu, il est possible de donner une valeur à une variable au moment de sa création :

double var = 1.618 ;

Les variables ont une durée de vie, c'est à dire qu'elles n'existent pas forcément partout et tout le temps dans notre programme. La règle pour déterminer la durée de vie d'une variable est extrêmement simple :

Une variable a la durée de vie du bloc dans lequel elle est définie.

Reste à définir un bloc pour que nous sachions exactement de quoi nous parlons :

Un bloc est un ensemble d'instructions englobées dans une paire d'accolades.

Nous verrons où et comment utiliser ces blocs. Il suffit de retenir que chaque variable n'existe que dans le bloc dans lequel elle est défini.

Constantes

Il est également possible de définir des constantes. Contrairement aux variables, la valeur d'une constante est censée rester la même du début à la fin du programme.

Les constantes sont de deux types : celles qui sont définies (et non typées) et celles qui sont déclarées (et typées).

La façon de créer une constante déclarée ressemble beaucoup à la façon de créer une variable. Seul l'ajout du mot clé const nous permet de faire la différence. De plus, comme la valeur de cette constante ne peux pas changer, il est nécessaire de l'initialiser au moment de sa création, comme dans l'exemple ci-dessous :

const int nCols = 5 ;
const char separateur = ':' ;

Une constante définie se crée non pas à l'aide d'une variable typée, mais par le biais de la directive du préprocesseur define (une directive du préprocesseur est toujours introduite par le caractère # en tout début de ligne) :

#define VAL_MAX 20

Ici, notre constante a pour nom VAL_MAX et pour "valeur" 20.

Cette façon de définir des constantes s'apparente, pour le préprocesseur, à faire du rechercher/remplacer. Partout où il va trouver VAL_MAX dans le code source, il va le remplacer par 20, sans vérification que ce soit sur la cohérence de l'expression résultante. Cette vérification est laissée à la tâche du compilateur qui fera de son mieux...

Procédures

Un programme C++ est généralement découpé en plusieurs procédures (ou fonctions) dont chacune effectue une tâche précise. Ces procédures peuvent communiquer entre elles à l'aide de paramètres et de valeurs renvoyées : une procédure admet des paramètres et renvoie (retourne) un résultat. Par exemple, la procédure ajout qui ferait la somme de deux entiers pourrait s'écrire ainsi :

int ajout(int a, int b)
{
	return a + b ;
}

Il faut distinguer plusieurs parties dans cet extrait de code. Tout d'abord la déclaration de la procédure : int ajout(int a, int b) où l'on trouve le nom de la procédure : ajout ; les types et les noms de ses paramètres entre parenthèses : (int a, int b) ; et le type de ce qu'elle doit renvoyer qui est précisé au tout début : int. Ensuite arrive le bloc de définition de cette procédure, qui est compris entre une accolade ouvrante et l'accolade fermante appariée. Dans le corps de cette procédure (à l'intérieur du bloc de définition donc) nous trouvons la suite d'instructions qui permet à notre procédure de faire ce que l'on attend d'elle. Ici, elle calcule la somme de ses arguments et renvoie immédiatement le résultat à la procédure appelante.

Bien entendu, des procédures plus complexes peuvent exister, mais aussi des procédures plus simples. Imaginons une procédure qui affiche simplement la valeur qui lui est passée en paramètre. Par définition, cette procédure ne renvoie rien à la procédure appelante, elle se contente d'afficher. Voici un exemple d'une telle procédure :

void affiche(string s)
{
	cout << s << endl ;
}

Nous remarquons la structure identique à toutes les procédures : son nom (affiche), ses paramètres (string s) et le type de ce qu'elle renvoie qui ici est le type particulier utilisé pour désigner le fait qu'il n'y a justement pas de type : void.

Structures de contrôle de base

Les structures de contrôle permettent de prendre des décisions en fonction de certaines conditions.

Il est possible d'en voir deux types : les tests et les boucles.

Les tests

Le but ici est de tester une condition. Par exemple, " Est-ce que cet entier est positif ? " ou " Ce mot est il le même que cet autre ? "

Les tests peuvent être plus complexes et présenter une forme de dépendance les uns des autres : " Si le réel b est égal à zéro alors afficher qu'il n'est pas possible de diviser par b, sinon afficher le résultat de la division de a par b. ". Ou encore " Si l'entier est négatif alors ajouter la valeur i à cet entier sinon si l'entier est positif alors retirer la valeurs j à cet entier, sinon ne rien faire ".

Voici un exemple de tests écrit en C++ :

int recentrage(int e, int i, int j)
{
	if ( e < 0 ) {
		e += i ;
		cout << "Ajout de la valeur " << i << endl ;
	} else if ( e > 0 ) {
		e -= j ;
		cout << "Retrait de la valeur " << j << endl ;
	} else {
		cout << "On ne touche plus à rien !" << endl ;
	}
}

Il existe d'autres formes de tests et parmi celles là, la construction switch ... case. En première approche elle peut être considérée comme un écriture différente du classique if ... else if ... else qui prend moins de place. C'est le cas, mais son mode de fonctionnement est en fait très différent et elle permet d'autres souplesses.

la syntaxe de cette construction est illustrée dans le morceau de code suivant :

void testeCaractere(char c)
{
	switch (c) {
		case 'a' :
			// code à exécuter quand c vaut 'a'
			break ;
		case 'b' :
			// code à exécuter quand c vaut 'b'
		case 'c' :
			// code à exécuter quand c vaut 'c'
			// Attention : il n'y a pas d'instruction
			//    break pour le cas 'b'. Cette partie
			//    sera donc également exécutée si
			//    c vaut 'b'
			break ;
		default :
			// code à exécuter quand c ne vaut
			// ni 'a', ni 'b', ni 'c'
	}
}

L'idée ici est de comparer le contenu de la variable présente dans les parenthèses du switch avec les constantes numériques décrites par chacune des étiquettes case. Le fait que ces case soient des étiquettes nous force à utiliser l'instruction d'arrêt break pour montrer l'endroit où le traitement pour ce cas particulier prend fin. Si cette instruction n'est pas présente, alors le code pour ce cas sera exécute, puis le code pour le cas suivant et ainsi de suite jusqu'à ce qu'un break soit rencontré ou que l'on arrive à la fin du bloc du switch.

Il existe un cas particulier optionnel pour désigner l'action par défaut (l'action à envisager si aucun des cas au dessus n'est satisfait). Il est décrit par le mot-clé default.

Les boucles

Les boucles permettent de répéter la même instruction plusieurs fois. Le nombre de fois peut dépendre d'une valeur prédéfinie (" ajouter 1 aux 10 éléments du tableau ") ou dépendre d'une action extérieure (" Tant que l'utilisateur n'a pas relâché la touche, faire avancer mon personnage ").

Voici trois exemples de boucle en C++. Ces trois exemples proposent trois façons de faire la même chose avec les trois structures de boucle existantes en C++.

La boucle for

void afficheListeEntiers(int d, int f)
{
	for (int compteur = d ; compteur <= f ; compteur ++)
	cout << compteur << endl ;
}

La boucle while

void afficheListeEntiers(int d, int f)
{
	int compteur ;

	compteur = d ;
	while (compteur <= f) {
		cout << compteur << endl ;
		compteur ++ ;
	}
}

La boucle do ... while

void afficheListeEntiers(int d, int f)
{
	int compteur ;

	compteur = d ;
	do {
		cout << compteur << endl ;
		compteur ++ ;
	} while (compteur < f) ;
}

Programmation C++

Ce que vous venez de lire ci-dessus s'apparente beaucoup à du C ou tout autre langage de programmation pas forcément orienté objet. C'est de la programmation impérative classique.

L'approche Objet de C++ sera expliquée au travers d'exemples pendant les travaux pratiques.


  1. [↑] http://fr.wikipedia.org/wiki/Objective_C
  2. [↑] http://fr.wikipedia.org/wiki/C++
  3. [↑] http://fr.wikipedia.org/wiki/Eiffel_(langage)
  4. [↑] le préprocesseur est un programme qui analyse le fichier avant la phase de compilation pour traiter les directives qui lui sont propre (en C++ ces directives sont celles qui commencent par le caractère # comme par exemple #include ou #define). De nos jours, les préprocesseurs sont généralement inclus dans les compilateurs et le processus est transparent pour le programmeur.