Langage C++/Version imprimable
Une version à jour et éditable de ce livre est disponible sur la Wikiversité,
une bibliothèque de livres pédagogiques, à l'URL :
http://fr.wikiversity.org/wiki/Langage_C%2B%2B
Introduction
Préambule :
modifierCe cours est pour le moment destiné à donner les clefs permettant l'apprentissage des bases du langage C++. Ce cours n'a pour le moment pas vocation à faire de vous des développeurs de haut vol mais simplement vous apprendre à utiliser le langage C++ de manière efficace à des fins d'aide de travail avec l'outil informatique.
Je m'efforcerai dans ce cours d’être précis, concis et pédagogique. Si d'aventure vous ne compreniez pas un point du cours, je m'efforcerai de répondre à vos questions.
Avant de commencer vous pouvez, si vous le désirez, consulter ce lien qui vous permettra d’en apprendre un peu plus sur l'histoire du C++
Introduction :
modifierDes Chiffres et des BITs
modifierDans un ordinateur les données sont représentées par des ensembles de cases mémoires. Ces cases mémoires, appelées "BIT" (de l'anglais Binary-digIT), ne peuvent prendre que 2 valeurs. Soit la valeur 0, soit la valeur 1. Pour représenter des nombres plus grands que 0 ou 1 nous devons rajouter des rangs de bits.
Voici, pour exemple, la représentation binaire des nombres décimaux de 0 à 9 :
Nombre décimal | Représentation binaire |
---|---|
0 | 0000 |
1 | 0001 |
2 | 0010 |
3 | 0011 |
4 | 0100 |
5 | 0101 |
6 | 0110 |
7 | 0111 |
8 | 1000 |
9 | 1001 |
Il existe une formule mathématique qui permet de connaître le nombre de valeurs possibles qu'un ensemble de n bit(s) peut prendre :
nombre_de_valeurs_possibles =
Il existe une formule mathématique permettant de connaître le nombre maximal que l’on peut représenter avec un ensemble de n bit(s) :
nombre_maximal_representable =
Il existe une formule qui permet de convertir n’importe quel nombre binaire en nombre décimal :
Nombre_décimal = où est le bit de rang commençant à zéro en partant de la droite et en lisant vers la gauche.
Si nous appliquons ces formules à notre exemple ci-dessus cela donne :
Pour un ensemble de 4 bits, aussi appelé quartet, nous avons :
- nous pouvons représenter valeurs,
- le plus grand nombre que l’on peut représenter avec cet ensemble sera .
Pour le nombre binaire 1010 nous avons :
Conversion Binaire → Décimal |
---|
1 0 1 0 |
8 + 0 + 2 + 0 |
10 |
Vous pourriez vous demander d'où vient cet écart de 1 entre le nombre de valeurs et la valeur maximale accessible, cela vient du fait que le 0 (en binaire sur 1 quartet : 0000) est comptabilisé dans le nombre de valeurs.
En informatique on appelle le bit le plus à droite d’un nombre binaire, le bit de poids faible car il représente la valeur . Par opposition au bit de poids faible, on appelle le bit le plus à gauche du nombre binaire le bit de poids fort car il représente la valeur la plus forte du nombre, soit pour notre quartet
→ Consultez ce lien pour plus d'informations à propos du système binaire
En regroupant des bits en quartets on s'aperçoit que l’on peut représenter toutes les valeurs par un seul chiffre hexadécimal.
Voici un tableau de conversions binaire <→ hexadécimal:
Binaire | Hexadécimal |
---|---|
0000 | 0 |
0001 | 1 |
0010 | 2 |
0011 | 3 |
0100 | 4 |
0101 | 5 |
0110 | 6 |
0111 | 7 |
1000 | 8 |
1001 | 9 |
1010 | A(10) |
1011 | B(11) |
1100 | C(12) |
1101 | D(13) |
1110 | E(14) |
1111 | F(15) |
→ Consultez ce lien pour plus d'informations à propos du système hexadécimal
À partir de maintenant et dans un souci de clarté, nous représenterons les valeurs exprimées dans le système hexadécimal en les préfixant par "0x" (les lettres seront en majuscules). Je n'utiliserai plus le système binaire qu’à des fins de démonstration en le préfixant par "0b".
ex:
Nombre décimal | Nombre hexadécimal | Nombre binaire |
---|---|---|
26 | 0x1A | 0b00011010 |
Les Octets :
modifierEn informatique il est courant (surtout dans les architectures PC et Mac) que les bits soient regroupés en ensembles de 8 bits (soit 2 quartets) que l’on appelle octets. Ce regroupement apporte 2 avantages :
- Toutes ses valeurs peuvent être représentées par seulement 2 chiffres hexadécimaux (de 0x00 à 0xFF).
- Il a permis la création d’un jeu de caractères ASCII étendu.
Des Octets et des Lettres :
modifierPuisque l'informatique n'est capable de gérer que des 0 et/ou des 1, comment faire en sorte d'obtenir des lettres ? Tout simplement en donnant une valeur numérique à chaque lettre. Ainsi dans le jeu de caractère ASCII étendu, nous avons une représentation numérique de chaque caractère avec une différenciation minuscule/majuscule. En fait pour un ordinateur une lettre n'est ni plus ni moins qu'un simple nombre.
→ Consultez ce lien pour plus d'informations à propos du jeu de caractères ASCII
Entiers, Relatifs, Naturels :
modifierNous avons vu que les octets peuvent représenter les nombres de 0 à 255 soit 256 valeurs. Mais comment l'ordinateur peut-il représenter des valeurs négatives ? En fait l'ordinateur ne sait pas faire la différence entre un nombre négatif et un nombre positif si on ne lui dit pas explicitement de la faire en lui indiquant qu’il doit travailler en mode signé.
Comment l'ordinateur représente-t-il des nombres entiers signés en nombres binaires ?
En mode signé, le bit de poids fort est interprété comme le signe du nombre (0 pour +, 1 pour -) on parle alors de bit de signe. Prenons l'exemple de l'octet :
Modes\ Valeurs | 0b00000000 | 0b00000001 | ... | 0b01111110 | 0b01111111 | 0b10000000 | 0b1000000 | 0b10000010 | ... | 0b11111110 | 0b11111111 |
---|---|---|---|---|---|---|---|---|---|---|---|
Interprétation Non-Signé | 0 | 1 | ... | 126 | 127 | 128 | 129 | 130 | ... | 254 | 255 |
Interprétation Signé | 0 | 1 | ... | 126 | 127 | -128 | -127 | -126 | ... | -2 | -1 |
On peut remarquer que les valeurs absolues sont dans un ordre inverse pour les nombres négatifs. Ceci est du à l’utilisation du complément à deux pour leur construction. Cet encodage, simple à mettre en œuvre, facilite ensuite énormément les opérations arithmétiques. (cf. lien)
Réels Entiers :
modifierNous avons vu comment un ordinateur pouvait représenter des entiers, signés ou non. Comment alors, à partir de nombres entiers relatifs composés de bits, représenter des nombres réels ?
En fait, l'organisme IEEE (Institute of Electrical and Electronics Engineers), a défini une norme permettant de coder en binaire, sur 2 octets et plus, un nombre réel. Cette norme a été reprise par les fondeurs de processeurs et a donc été admise comme standard pour la représentation de nombres dits « à virgule flottante ».
Voici une schématisation de la norme :
Signe du nombre | Valeur exposant signé | Valeur mantisse |
---|---|---|
1 bit | Taille variable selon encodage | Taille variable selon encodage |
La partie qui représente les données du nombre, la mantisse, est entière. On est donc obligé de déplacer la virgule jusqu'à atteindre la bonne valeur, ce qui est fait grâce à l'exposant. La virgule se déplaçant, grâce à l'exposant, aléatoirement d’un nombre à l'autre on dit qu'elle flotte, d'où l’expression « nombre à virgule flottante ».
→ Consultez ce lien pour plus d'informations à propos des nombres à virgules flottantes
→ Consultez ce lien pour plus d'informations à propos de l’IEEE
Opérations Logiques Binaire :
modifierPour pouvoir faire des opérations sur des valeurs logiques on doit savoir utiliser les opérateurs booléens de base.
Voici les différents opérateurs logiques booleens :
Opérateur YES (OUI) :
modifierL'opérateur YES effectue une comparaison sur une valeur logique. Si la valeur est vraie alors la sortie est vraie, sinon elle est fausse.
Voici la table de vérité de l'opérateur YES pour l'opération (YES x):
YES | x = Faux | x = Vrai |
---|---|---|
Résultat | Faux | Vrai |
Opérateur NOT (NON) :
modifierL'opérateur NOT effectue une comparaison sur une valeur logique. Si la valeur est vraie alors la sortie est fausse, sinon elle est vraie.
Voici la table de vérité de l'opérateur NOT pour l'opération (NOT x):
NOT | x = Faux | x = Vrai |
---|---|---|
Résultat | Vrai | Faux |
Opérateur AND (ET) :
modifierL'opérateur AND effectue une comparaison entre 2 valeurs logiques. Si les 2 valeurs sont vraies alors la sortie est vraie, sinon elle est fausse.
Voici la table de vérité de l'opérateur AND pour l'opération (x AND y):
AND | y = Faux | y = Vrai |
---|---|---|
x = Faux | Faux | Faux |
x = Vrai | Faux | Vrai |
Opérateur NAND (NON-ET) :
modifierL'opérateur NAND effectue une comparaison entre 2 valeurs logiques. Si les 2 valeurs sont vrai alors la sortie est fausse, sinon elle est vrai.
Voici la table de vérité de l'opérateur NAND pour l'opération (x NAND y):
NAND | y = Faux | y = Vrai |
---|---|---|
x = Faux | Vrai | Vrai |
x = Vrai | Vrai | Faux |
Opérateur OR (OU Inclusif) :
modifierL'opérateur OR effectue une comparaison entre 2 valeurs logiques. Si au moins 1 des 2 valeurs est vraie alors la sortie est vraie, sinon elle est fausse.
Voici la table de vérité de l'opérateur OR pour l'opération (x OR y):
OR | y = Faux | y = Vrai |
---|---|---|
x = Faux | Faux | Vrai |
x = Vrai | Vrai | Vrai |
Opérateur NOR (NON-OU) :
modifierL'opérateur NOR effectue une comparaison entre 2 valeurs logiques. Si les 2 valeurs sont fausses alors la sortie est vraie, sinon elle est fausse.
Voici la table de vérité de l'opérateur NOR pour l'opération (x NOR y):
NOR | y = Faux | y = Vrai |
---|---|---|
x = Faux | Vrai | Faux |
x = Vrai | Faux | Faux |
Opérateur XOR (OU Exclusif) :
modifierL'opérateur XOR effectue une comparaison entre 2 valeurs logiques. Si exactement 1 des 2 valeurs est vraie alors la sortie est vraie, sinon elle est fausse.
Voici la table de vérité de l'opérateur XOR pour l'opération (x XOR y):
XOR | y = Faux | y = Vrai |
---|---|---|
x = Faux | Faux | Vrai |
x = Vrai | Vrai | Faux |
En fait l'opérateur XOR peut être construit à partir d'autres opérateurs, mais, pour des raisons pratiques, il a directement été mis à disposition du langage.
XOR = ( ( (NOT x) AND (y) ) OR ( (x) AND (NOT y) ) )
À partir de tous ces opérateurs on peut réaliser n’importe quel modèle logique.
Mots clés
Les Mots Clés
modifierEn C++, comme en C d'ailleurs, il existe une série de mots qui ne peuvent et ne doivent être, en aucun cas, utilisés autrement que pour ce à quoi ils sont destinés. Ces mots sont dit "mots clés" et, dans certains cas, appelés "instructions", ou encore opérateurs, ou mots réservés et ont une signification particulière pour le compilateur.
Voici une liste des mots clés/mot réservés du C++ :
*
modifierMots clés | Utilisation |
---|---|
+, -, /, *, =, ., <, <=, ==, >=, > &&, ||, &, |, !, ~, != , ()(parenthèses), [] (Crochets)… (Triples Points), , (Virgule) | Opérateurs |
&, * | Opérateur de référencement/déréferencement (référence/pointeur) |
# | Préfixe de directives préprocesseur |
0x (Zéro-x) | Préfixe de nombre hexadécimal |
0 (Chiffre zéro) | Préfixe de nombre octal |
{} (Accolades) | Délimitation de portée |
:: (double deux points) | Opérateur de déréférencement de portée |
A
modifierMot Clé | Utilisation |
---|---|
asm | Déclarateur de code assembleur ! |
auto | Déclarateur de variable à désallocation automatique "Pile"(stack). |
B
modifierMot Clé | Utilisation |
---|---|
break | Instruction de branchement dans une boucle ou un traitement de cas. |
bool | Type de donnée logique dit "booléen". Prend la valeur vrai (true) ou faux (false). |
C
modifierMot Clé | Utilisation |
---|---|
case | Déclarateur de cas dans une instruction switch. |
catch | Récupérateur d'erreur. |
char | Type de donnée entier dit "caractère". En programmation structurée ce type est déconseillé à l’utilisation mais il permet de rendre certains services. |
class | Déclarateur de définition de classe. |
const | Déclarateur de constantes. |
continue | Instruction de branchement dans une boucle imbriquée. |
D
modifierMot Clé | Utilisation |
---|---|
default | Déclarateur de cas par défaut dans une instruction switch. |
delete | Désallocateur de mémoire dynamique "Tas"(heap). |
do | Déclarateur de boucle. Ne peut être utilisé qu'en association avec while. |
double | Type de donnée nombre flottant à double précision. |
E
modifierMot Clé | Utilisation |
---|---|
else | Déclarateur de cas par défaut dans une instruction if. |
enum | Structure de donnée énuméré. |
extern | Déclarateur d’une variable déclarée dans un autre fichier. |
explicit | Interdit les constructeurs pour casts implicites. |
F
modifierMot Clé | Utilisation |
---|---|
false | Valeur logique (Faux) |
float | Type de donnée nombre flottant à simple précision. |
for | Déclarateur de boucle paramétrée. |
friend | Déclarateur de classe ou de fonction ayant accès aux données privées. |
G
modifierMot Clé | Utilisation |
---|---|
goto | Instruction de branchement, en développement structuré son utilisation est interdite car elle rend la compréhension du code plus difficile. |
I
modifierMot Clé | Utilisation |
---|---|
if | Déclarateur de traitement conditionnel. |
inline | Déclarateur de MACRO |
int | Type de donnée entier. En programmation structurée ce type est déconseillé à l’utilisation mais il permet de rendre certains services. |
L
modifierMot Clé | Utilisation |
---|---|
long | Modificateur de longueur de type. |
M
modifierMot Clé | Utilisation |
---|---|
main | Méthode point d'entrée du programme. |
mutable | Rend une partie d’un objet constant modifiable. |
N
modifierMot Clé | Utilisation |
---|---|
new | Allocateur de mémoire dynamique "Tas"(heap). |
O
modifierMot Clé | Utilisation |
---|---|
operator | Déclarateur de surcharge d'opérateur. |
P
modifierMot Clé | Utilisation |
---|---|
private | Déclarateur de membre privé. |
protected | Déclarateur de membre protégé. |
public | Déclarateur de membre public. |
R
modifierMot Clé | Utilisation |
---|---|
register | Déclarateur de variable registre. |
return | Instruction de branchement. |
S
modifierMot Clé | Utilisation |
---|---|
short | Modificateur de longueur de type. |
signed | Modificateur d'interprétation de signe de type entier. |
sizeof | Opérateur spécial permettant de renvoyer la taille d’une variable stockée en pile(stack). |
static | Déclarateur de variable statique dite "de classe". En programmation structurée, il est déconseillé à l’utilisation mais permet de rendre certains services. |
struct | Déclarateur de structure. En programmation structurée il est déconseillé à l’utilisation il est préférable d’utiliser des classes à la place. |
switch | Déclarateur de traitement conditionnel cardinal. |
T
modifierMot Clé | Utilisation |
---|---|
template | Declarateur de paramétrage. |
this | Pointeur spécial désignant l'instance en cours de l'objet. En programmation structurée il est systématiquement et presque obligatoirement utilisé car il améliore la lisibilité. |
throw | Déclencheur d'exceptions. |
try | Déclarateur de section à déclenchement d'exceptions. |
true | Valeur logique (Vrai) |
typedef | Déclarateur de type. |
U
modifierMot Clé | Utilisation |
---|---|
unsigned | Modificateur d'interprétation de signe de type entier. |
union | Déclarateur d'union. En programmation structurée, il est très fortement déconseillé à l’utilisation il n'est utilisé que dans des cas très rares et pour des applications très spécifiques. |
V
modifierMot Clé | Utilisation |
---|---|
virtual | Déclarateur de méthode virtuelle. |
void | Indicateur d’une absence de type (là où un type serait attendu). |
volatile | Déclarateur de membre critique nécessitant un traitement d'actualisation particulier notamment lors de l’utilisation de threads. |
W
modifierMot Clé | Utilisation |
---|---|
while | Déclarateur de boucle conditionnelle. |
Instructions du langage C
modifierMot Clé | Utilisation |
---|---|
include, define, ifdef, ifndef, pragma, error | Directives préprocesseur |
malloc, realloc, calloc, free | Opérateurs C d'allocation/désallocation de mémoire dynamique. |
Types
Les Types
modifierEn C++ il existe plusieurs types organisés dans des catégories. Ces types ont différentes longueurs et différentes interprétations possibles dépendant de la plage de valeurs sur laquelle on veut travailler : signés ou non, à virgule flottante ou non.
Selon l'architecture de votre ordinateur les tailles des types peuvent varier ! Ce cours se base sur une architecture PC. D'autres machines ont potentiellement des types de longueurs différentes de celles exprimées ici. |
Types Intrinsèques :
modifierIl existe plusieurs catégories de types en C++ dont certaines sont incompatibles avec le langage C. Pour représenter les nombres non-signés on utilise le mot clé "unsigned". Pour représenter les nombres signés on utilise le mot clé "signed". Cela permet à l'ordinateur de savoir comment il doit interpréter les valeurs. Pour des raisons de simplification du codage, les types représentent par défaut des nombres signés, ce qui rend l’utilisation du mot clé "signed" facultative.
Catégories de types
modifierLe Type Vide
modifierCette catégorie, compatible avec le langage C, représente l'absence de type.
Le type vide est surtout utilisé pour l'écriture de méthodes (les méthodes ne renvoyant aucune valeur ont un type de retour vide), mais il arrive que l’on s'en serve pour accéder à une adresse mémoire sans essayer d’avoir une interprétation de la valeur se trouvant à cette adresse.
Type de Base | Taille du type en octets | Nombres de valeurs possibles | Plage de valeurs possibles |
---|---|---|---|
void | (inaccessible) | (inaccessible) | (inaccessible) |
Le Type Char, un type lettré !
modifierLe type char est un type un peu particulier : il se comporte comme un entier mais il est destiné à mémoriser des caractères (ou parfois des morceaux de caractères). Dans certains système d'exploitation il existe une table de 256 dessins représentant graphiquement, entre autres, les 26 lettres minuscules et majuscules (52 au total) ainsi que les 10 chiffres (0 à 9). Le reste des 256 codes permet de représenter d'autres caractères tels que le caractère null, l'espace, la tabulation, la ponctuation ou des caractères propres à des langues spéciales, parfois même des smileys. Le dessin de ces caractères ainsi que leur codage numérique sont définis par une norme de codage de caractères. Par exemple, la norme ASCII est une norme relativement ancienne, alors que la norme Unicode est plus universelle. L'ordinateur lui ne manipulera que les valeurs numérique de ce type, par contre les logiciels qui affichent la valeur de ce type à l'écran le font sous forme graphique, c'est-à-dire en affichant le dessin du caractère correspondant à la valeur affectée.
Commun aux processeurs 32 et 64 bit :
Mode | Type de Base | Taille du type en octets | Nombres de valeurs possibles | Plage de valeurs possible dans le mode |
---|---|---|---|---|
non-signé | unsigned char | 1 | 256 | [0 ; 255] |
signé | (signed) char | 1 | 256 | [ -128 ; +127 ] |
caractère (non-signé) | (signed|unsigned) char | 1 | 256 | Caractères de la table ASCII |
Les Entiers
modifierCette catégorie, compatible avec le langage C, regroupe les types représentant les nombres entiers. À partir du type short, tous les types entiers se réfèrent au type int. Là encore pour simplifier le codage, l'écriture du mot clé "int" est facultative excepté pour le type int signé lui-même.
Commun aux processeurs 32 et 64 bit :
Mode | Type de Base | Taille du type en octets | Nombres de valeurs possibles | Plage de valeurs possible dans le mode |
---|---|---|---|---|
non-signé | unsigned char | 1 | 256 | [ 0 ; 255 ] |
signé | (signed) char | 1 | 256 | [ -128 ; +127 ] |
non-signé | unsigned short (int) | 2 | 65 536 | [ 0 ; 65 535 ] |
signé | (signed) short (int) | 2 | 65 536 | [ −32 768 ; 32 767 ] |
non-signé | unsigned (int) | 4 | 4 294 967 296 | [0; 4 294 967 295 ] |
signé | (signed) int | 4 | 4 294 967 296 | [ −2 147 483 648 ; 2 147 483 647 ] |
Spécifiques aux processeurs 32 bits :
Mode | Type de Base | Taille du type en octets | Nombres de valeurs possibles | Plage de valeurs possible dans le mode |
---|---|---|---|---|
non-signé | unsigned long (int) | 4 | 4 294 967 296 | [0; 4 294 967 295 ] |
signé | signed long (int) | 4 | 4 294 967 296 | [ −2 147 489 648 ; 2 147 489 647 ] |
non-signé | unsigned long long (int) | 8 | 18 446 744 073 509 552 000 | [0; 18 446 744 073 509 552 000 ] |
signé | (signed) long long (int) | 8 | 18 446 744 073 509 552 000 | [ −9 223 372 036 854 776 000 ; 9 223 372 036 854 776 000 ] |
Spécifiques aux processeurs 64 bits :
Mode | Type de Base | Taille du type en octets | Nombres de valeurs possibles | Plage de valeurs possible dans le mode |
---|---|---|---|---|
non-signé | unsigned long (int) | 8 | 18 446 744 073 509 552 000 | [ 0 ; 18 446 744 073 509 552 000 ] |
signé | (signed) long (int) | 8 | 18 446 744 073 509 552 000 | [ −9 223 372 036 854 776 000 ; 9 223 372 036 854 776 000 ] |
En effet, selon que l’on soit en présence d’un processeur 32 ou 64 bits, certains types ont des tailles et des plages de valeurs variables. C’est pour cette raison que normalement on doit utiliser les types dit "Entiers Fixes" dont nous parlerons plus loin.
Une règle détermine la validité des tailles des types entiers :
[signed|unsigned], char <= short <= int <= long [<= long long]
Les Flottants
modifierCette catégorie, compatible avec le langage C, regroupe les types représentant les nombres à virgule flottante.
Commun aux processeurs 32 et 64 bit :
Type de Base | Taille du type en octets | Taille de l'exposant | Taille de la mantisse | Nombres de valeurs possibles | Plage de valeurs possible |
---|---|---|---|---|---|
float | 4 | 8 bits | 23 bits | 4 294 967 296 | (indisponible actuellement) |
double | 8 | 11 bits | 52 bits | 18 446 744 073 509 552 000 | (indisponible actuellement) |
long double | 12 | 15 bits | 64 bits | 79 008 162 513 705 380 000 000 000 000 | (indisponible actuellement) |
Le Type Logique
modifierCe type est un apport par rapport au langage C, il n'est donc pas compatible avec celui ci.
Le type bool est un type un peu particulier. Il est codé sur 4 octets et représente les données logiques vrai et faux par 2 mots clés respectivement "true" et "false". Ce type gère sa valeur de manière assez singulière :
true vaut 1, false vaut 0, mais d’une manière générale tout ce qui n’est pas false (égal à 0) vaut true (différent de 0). En effet comme ce type est codé sur 4 octets il possède la même plage de valeurs qu'un signed int et nous pouvons lui affecter n’importe quelle valeur de cette plage.
Commun aux processeurs 32 et 64 bit :
Type de Base | Taille du type en octets | Nombres de valeurs possibles | Plage de valeurs possible | Valeur = 0 | Valeur = [ −2 147 489 648 ; -1 ] U [ +1 ; 2 147 489 647 ] |
---|---|---|---|---|---|
bool | 4 | 4 294 967 296 | [ −2 147 489 648 ; 2 147 489 647 ] | false | true |
Les Méthodes
modifierNous parlerons en détail de ce type dans le chapitre Méthodes.
Les Pointeurs, Tableaux et Références
modifierLe type pointeur n'est qu'un raccourci vers la mémoire. Le type pointeur est en fait une adresse mémoire soit un entier non signé de la taille du bus d'adresse dans le contexte logiciel. Sa taille est donc variable suivant le matériel et le système d'exploitation utilisé.
Sa valeur représente l'adresse mémoire vers laquelle il pointe. Son type est le type dans lequel on lui demande de traduire la valeur existant à l'adresse où il pointe.
Il peut s'agir dans des cas peu fréquents de pointeurs sur méthodes mais nous verrons cela dans le chapitre Pointeur, Tableaux et Références.
Les Énumerations
modifierNous parlerons en détail de ce type dans le chapitre Énumérations.
Les Structures
modifierNous parlerons en détail de ce type dans le chapitre Structures, unions et champs de bits.
Les Champs de Bits
modifierNous parlerons en détail de ce type dans le chapitre Structures, unions et champs de bits.
Les Classes
modifierLes classes sont le summum de l'architecture logicielle. Elles permettent la programmation-objet et la construction d'applications au code robuste réutilisable et relativement facile à debugger. Nous en reparlerons plus loin.
Structures vs Classes
modifier- La visibilité d’une classe est par défaut privée, celle d’une structure, publique.
- L’héritage d’une classe est par défaut privé, celle d’une structure, publique.
- Template de classe, impossible en structure.
Types Étendus
modifierLes types étendus sont des types définis par les utilisateurs et sont le plus souvent des agglomérations de types intrinsèques et/ou types étendus. Ainsi il est possible de créer des types étendus agglomérant n’importe quel type intrinsèque en n’importe quelle quantité (dans les limites de la mémoire disponible) et dans n’importe quel ordre.
Le langage nous permet de définir de nouveaux types étendus grâce au mot clé "typedef".
La syntaxe de cette instruction est :
typedef NomDuTypeEtendu NomDuTypeDeBase;
Nous verrons des exemples concrets de cette syntaxe dans les chapitres suivants.
Opérateurs
Les Opérateurs C++
modifierLes opérateurs sont des symboles permettant d'effectuer des opérations sur des valeurs. Ils sont classés dans trois catégories.
Opérateurs d'affectation
modifierL'opérateur d'affectation simple "="
modifierL'opérateur d'affectation simple est "=" il signifie "affecter à".
Syntaxe :
<Destination> = <Source>; Où <Source> et <Destination> doivent être de même nature et où <Source> est une expression et <Destination> une variable ;
L'opérateur d'affectation additionneur "+="
modifierL'opérateur d'affectation additionneur est "+=" il signifie "affecter en additionnant à".
Syntaxe :
<Destination> += <Source>; Où <Source> et <Destination> doivent être de même nature et où <Source> est une expression et <Destination> une variable ;
L'opérateur d'affectation soustracteur "-="
modifierL'opérateur d'affectation soustracteur est "-=" il signifie "affecter en soustrayant à".
Syntaxe :
<Destination> -= <Source>; Où <Source> et <Destination> doivent être de même nature et où <Source> est une expression et <Destination> une variable ;
L'opérateur d'affectation multiplicateur "*="
modifierL'opérateur d'affectation multiplicateur est "*=" il signifie "affecter en multipliant à".
Syntaxe :
<Destination> *= <Source>; Où <Destination> et le résultat de l'opération doivent être de même nature et où <Source> est une expression et <Destination> une variable ;
L'opérateur d'affectation diviseur "/="
modifierL'opérateur d'affectation diviseur est "/=" il signifie "affecter en divisant à".
Syntaxe :
<Destination> /= <Source>; Où <Destination> et le résultat de l'opération doivent être de même nature et où <Source> est une expression et <Destination> une variable ;
L'opérateur d'affectation modulo "%="
modifierL'opérateur d'affectation modulo est "%=" il signifie "affecter le modulo de, à".
Syntaxe :
<Destination> %= <Source>; Où <Destination> et le résultat de l'opération doivent être de même nature et où <Source> est une expression et <Destination> une variable ;
Les opérateurs mathématiques
modifierOpérateur d'addition "+"
modifierL'opérateur permettant l'addition est "+" il permet d'additionner un nombre ou le résultat numérique d’une expression à un autre.
Opérateur de soustraction "-"
modifierL'opérateur permettant la soustraction est "-" il permet de soustraire un nombre ou le résultat numérique d’une expression à un autre.
Opérateur de multiplication "*"
modifierL'opérateur permettant la multiplication est "*" il permet de multiplier un nombre ou le résultat numérique d’une expression à un autre.
Opérateur de division "/"
modifierL'opérateur permettant la division est "/" il permet de diviser un nombre ou le résultat numérique d’une expression à un autre.
int x, y, z;
x = 5;
y = 3;
z = x / y; //z = 1
float r, o, p;
r = 5.0;
o = 3.0;
p = r / o; // p ~= 1.666666…
En c++ la division est entière si les deux opérandes sont entiers. Si l'un des opérandes est codé sur un format réel alors le résultat sera réel.
Opérateur de modulo "%"
modifierL'opérateur permettant de récupérer le reste de la division entière d’un nombre ou du résultat numérique d’une expression par un autre, est "%".
Les opérateurs comparatifs
modifierIls servent essentiellement à effectuer des tests logiques entre plusieurs valeurs numériques.
Comparateur d'égalité "=="
modifierLe comparateur C++ d'égalité == permet de vérifier si la valeur de gauche est strictement égale à la valeur de droite.
Cela revient à faire une inversion de l'opérateur logique XOR (soit NOT XOR). Voici la table de vérité du comparateur == pour l'opération (x == y) :
== (NOT XOR) | y = false | y = true |
---|---|---|
x = false | true | false |
x = true | false | true |
Comparateur "!=" (différent de)
modifierLe comparateur C++ différentiel "!=" permet de vérifier si la valeur de gauche est strictement différente de la valeur de droite. Cela revient à appliquer l'opérateur logique XOR.
Comparateur "<" (strictement inférieur à)
modifierLe comparateur "<" (strictement inférieur à) permet de vérifier si la valeur numérique de gauche est strictement inférieure à la valeur numérique de droite. Cet opérateur renvoie true uniquement si la valeur numérique de gauche est strictement inférieure à la valeur numérique de droite.
Comparateur "<=" (inférieur à)
modifierLe comparateur "<=" (inférieurs à) permet de vérifier si la valeur numérique de gauche est inférieure ou égale à la valeur numérique de droite. Cet opérateur renvoie true uniquement si la valeur numérique de gauche est inférieure ou égale à la valeur numérique de droite.
Comparateur ">=" (supérieur à)
modifierLe comparateur ">=" (supérieur à) permet de vérifier si la valeur numérique de gauche est supérieure ou égale à la valeur numérique de droite. Cet opérateur renvoie true uniquement si la valeur numérique de gauche est supérieure ou égale à la valeur numérique de droite.
Comparateur ">" (strictement supérieur à)
modifierLe comparateur ">" (strictement supérieur à) permet de vérifier si la valeur numérique de gauche est strictement supérieure à la valeur numérique de droite. Cet opérateur renvoie true uniquement si la valeur numérique de gauche est strictement supérieure à la valeur numérique de droite.
Opérateurs logiques
modifierLes opérateurs logiques représentent les manipulations et comparaisons possibles que l’on peut faire sur des données de types booléen et/ou binaire.
Attention : les opérateurs &, | et ^ sont des opérateurs bit à bit ! Les opérateurs logiques correspondant au & et au | sont respectivement && et ||. |
Les opérateurs logiques
modifierCette sous-catégorie représente les opérateurs logiques que nous avons vu en introduction en leur spécifiant une syntaxe. Il est à noter que l'opérateur logique YES n'a pas de représentation en C++. En effet le C++ utilise le postulat que les opérations doivent retourner true pour être vraies. Partant de ce principe, il n'est plus nécessaire d’avoir un opérateur YES.
Opérateur "!" (NOT)
modifierL'opérateur logique inverseur NOT a pour syntaxe en C++ le symbole "!" (point d'exclamation) ou l'expression "not". Pour inverser une valeur on écrira donc en C++
bool x, y;
x = true;
y = ! x; // y = false
// ou alors
y = not x; // mais cette syntaxe n'est plus utilisée
Opérateur "&&" (AND)
modifierLe comparateur C++ AND, représenté par "&&" (double "et commercial"), permet de vérifier si les 2 valeurs soumises sont vraies. Ce comparateur est souvent utilisé dans les tests car il permet une optimisation du code. Contrairement à l'opérateur logique "&" le comparateur "&&" teste la première valeur et si elle est false il renvoie directement false sans tester la deuxième valeur. Ce comportement permet un gain de temps sur les procédures de comparaison et permet d’éviter l'exécution d’un code qui serait invalide si le test de la première valeur renvoyait false.
bool x = true, y = true, z = false;
bool a, b, c;
a = x && y; // a = true
b = x && z; // b = false
c = x && !z; // c = true
//ou
a = x and y; // a = true //mais cette syntaxe n'est plus utilisée.
Opérateur "||" (OR)
modifierLe comparateur C++ OR "||" permet de vérifier si au moins une des valeurs soumises est vraie. Ce comparateur est souvent utilisé dans les tests car il permet une optimisation du code. Contrairement à l'opérateur logique "|", le comparateur "||" teste la première valeur, et si elle est à vrai, il renvoie directement true sans tester la deuxième valeur. Ce comportement permet un gain de temps sur les procédures de comparaison et permet d’éviter l'exécution d’un code qui serait invalide si le test de la première valeur renvoyait vrai.
Opérateurs bit à bit
modifierLes opérateurs bit à bit travaillent sur les variables en s'appliquant bit à bit[1].
Opérateur "&" (AND)
modifierL'opérateur AND a pour syntaxe en C++ le symbole "&" (l’esperluette ou et commercial). Pour effectuer un AND sur des valeurs on écrira donc en C++
Opérateur "|" (OR)
modifierL'opérateur OR a pour syntaxe en C++ le symbole "|" (la barre verticale). Pour effectuer un OR sur des valeurs on écrira donc en C++ :
Opérateur "^" (XOR)
modifierL'opérateur XOR a pour syntaxe en C++ le symbole "^" (Accent circonflexe). Pour effectuer un XOR sur des valeurs on écrira donc en C++ :
int a = 2, b = 6, c = 12, d, e, f;
d = a ^ b ; // d = 4
e = a ^ a ; // e = 0
f = a ^ c ; // f = 14
Opérateur "~" (NOT)
modifierL'opérateur NOT a pour syntaxe en C++ le symbole "~" (tilde). Pour effectuer un NOT sur des valeurs on écrira donc en C++ :
Opérateurs unaires
modifierEn C++, il existe des opérateurs qui ne nécessitent qu'un opérande. Ils sont appelés opérateurs unaires.
Opérateurs "++" (incrémentation)
modifierL'opérateur "++" est un peu particulier car il possède 2 formes et des effets indésirables. Je n'en parle que pour vous permettre d’éviter les cas problématiques.
Forme préfixée
modifierDans sa forme préfixée la théorie veut que l'opérande associé soit d’abord incrémenté puis renvoyé.
Où operande est la valeur à incrémenter puis à renvoyer.
Forme suffixée
modifierDans sa forme suffixée la théorie veut que l'opérande associé soit d’abord renvoyé puis incrémenté.
Où operande est la valeur à renvoyer puis à incrémenter.
Opérateurs "--" (décrémentation)
modifierL'opérateur "--" est un peu particulier car il possède 2 formes et des effets indésirables. Je n'en parle que pour vous permettre d’éviter les cas problématiques.
Forme préfixée
modifierDans sa forme préfixée la théorie veut que l'opérande associé soit d’abord décrémenté puis renvoyé.
Où operande est la valeur à décrémenter puis à renvoyer.
Forme suffixée
modifierDans sa forme suffixée la théorie veut que l'opérande associé soit d’abord renvoyé puis décrémenté.
Où operande est la valeur à renvoyer puis à décrémenter.
Effets secondaires
modifierEn C++ il existe un effet secondaire avec certains opérateurs. Si l’on utilise plusieurs opérateurs unaires ou d'affectation dans la même ligne de commande, le résultat est indéterminé par les spécifications du langage[2]. Ce qui veut dire que le compilateur fait ce qu’il veut. En fait ce problème survient car l'opérateur lit et modifie la valeur de ses opérandes.
Dans ce cas, "a" peut valoir 1 ou 2 selon comment le compilateur a traité les opérateurs. Autre exemple
Dans ce cas, "a" peut valoir 0, 1 ou 3 selon comment le compilateur a traité les opérateurs.
autre exemple :
Ici l’affichage peut avoir bien des valeurs.
Il est donc recommandé d’utiliser ces opérateurs avec précaution et d’éviter toute expression trop complexe(comme ci-dessus). D'ailleurs, de telles expressions ont plutôt tendance à rendre le code moins lisible - ce qui est l'inverse du but initial - et cache souvent une mauvaise conception.
Néanmoins, une utilisation raisonnable de ces opérateurs permet de rendre un code plus concis, sans perdre en lisibilité.
Le code ci-dessous filtre dans tab les entiers superieurs à 5 et les place dans filtered_tab.
int tab[10];
/*
initialisation de tab
*/
int filtered_tab[10];
int filtered_count = 0;
for(int i = 0; i < 10; ++i)
{
if(array[i] > 5)
filtered_array[filtered_count++] = array[i];
/* au lieu de
if(array[i] > 5
{
filtered_array[filtered_count] = array[i];
filtered_count++;
}
*/
}
\n, signifie une le line-feed.
\t, signifie la tabulation horizontale.
Et leurs origines viennent des ordinateurs qui existaient avant la démocratisation des écrans. Et ils étaient destinés aux imprimantes.
Le \n est un caractère spéciale que les imprimantes interprétaient comme instruction de faire avancer le papier d’une ligne verticalement.
Le \t est un caractère spéciale qui donnait l’instruction d’avancer la tête d’impression de 1 à 8 espaces horizontalement. Soit la position divisible par huit sans reste la plus près de sa position actuelle.
Références
modifier- ↑ https://www.miniwebtool.com/bitwise-calculator/?data_type=10&number1=6&number2=12&operator=OR
- ↑ Programmer en langage C++, Claude Delannoy, 2011
Voir aussi
modifier- Priorités des opérateurs sur Wikilivres
Syntaxe C++
Syntaxe C++
modifierAvant de passer à la suite il est temps de voir comment on structure les instructions en C++.
Sens de Lecture
modifierEn C++ les instructions se lisent de droite à gauche, (non ce n'est ni de l'arabe, ni de l'hébreu, ni du japonais). Cela est dû au fait que les langages de programmation sont issus de l'anglais. Les opérateurs transfèrent le plus souvent les valeurs situées à leur droite vers les valeurs situées à leur gauche et beaucoup d'opérateurs spécifient l'état des identificateurs situés à leur gauche.
Voici un exemple concret que vous verrez rapidement. Nous allons parler de la méthode principale de tout programme C++ aussi appelé « point d'entrée » du programme, il s'agit de la méthode « main
» et plus particulièrement de ses paramètres.
Attention : Pour ceux qui connaissent des langages comme le Pascal et qui s'attendraient à ce que je parle de fonction, en C++ il n'existe pas de distinctions clairement établie entre procédure et fonction comme en Pascal. Une méthode est un compromis entre la fonction et la procédure. La seule chose qui différencie la fonction de la procédure en C++ est le type de retour qui est 'void' pour une procédure et différent de 'void' pour les fonctions. On ne peut donc pas faire de différence entre une procédure et une fonction en C++. On regroupe donc les deux formes sous l'appellation de méthodes. |
Dans ce morceau de code nous voyons donc sur la première ligne :
La syntaxe :
signifie que nous avons affaire à une méthode.
Dans les parenthèses nous trouvons, séparés par une virgule, les paramètres (aussi appelés arguments) suivants :
Nous pouvons facilement comprendre le premier qui est un entier comptant les paramètres.
Le second est plus délicat car c’est un piège qui fait chuter plus d'un débutant.
Un débutant qui a des notions de syntaxe lira
puis
et en déduira qu’il s'agit d'un pointeur (*)
, appelé argv
, de tableau de char. En gros, une (seule) chaîne de caractères ce qui est totalement faux.
Ne vous alarmez pas si vous ne comprenez pas le concept de tableau il sera expliqué en temps et en heure. Essayez de simplement comprendre le sens de lecture.
En fait il faut le lire dans l'autre sens :
puis
déclare un tableau, appelé argv
, contenant des pointeurs (*)
de char
. Ce qui correspond à plusieurs pointeurs sur des caractères regroupés dans un tableau. Vous pourriez me dire que cela revient au même puisque l’on a un tableau de pointeurs sur des caractères. C’est vrai que l’on peut le voir sous cet angle, cependant un pointeur de caractère peut avoir 2 significations :
- un pointeur sur un et un seul caractère,
- un pointeur sur un caractère qui correspond au premier caractère d'une chaine de caractères à zéro terminal.
Il faut donc interpréter
comme un tableau de pointeurs sur des chaînes de caractères, soit un tableau contenant plusieurs chaînes de caractères. Cela vous paraîtra plus logique lorsque nous étudierons en détail le paramétrage d’applications en ligne de commande.
Une instruction se termine par un point-virgule
modifierEn C++ la grande majorité des instructions (hormis les définitions, les inclusions, les méthodes, les structures et certaines directives) se terminent par un point-virgule « ; »
Constantes et Variables
Constantes et Variables
modifierEn C++ nous avons besoin, pour pouvoir travailler, d'enregistrer des valeurs en mémoire.
La Mémoire
modifierLa mémoire dans un ordinateur est une succession d'octets (soit 8 bits), organisés les uns à la suite des autres et directement accessibles par une adresse. Ces adresses sont linéaires, et commencent à l'adresse zéro. Certaines de ces adresses sont affectées au matériel (carte graphique, disques durs, BIOS, etc.), les autres servent à enregistrer des données.
Organisation de la mémoire en C++
modifierEn C++ la mémoire est organisée en deux catégories, la pile (de l'anglais Stack) et le Tas (de l'anglais Heap). Cette formalisation vient de l'architecture même des microprocesseurs compatibles i80x86.
La Pile
modifierLa pile est un espace mémoire réservé au stockage des variables désallouées automatiquement en mémoire. Sa taille est limitée mais on peut choisir sa taille de manière à ce que l’on ne la remplisse jamais. Les compilateurs C++ gèrent ce paramétrage et nous n'avons plus à le configurer mais il est important de savoir où sont stockées les variables en mémoire et comment ce processus fonctionne.
La pile est bâtie sur le modèle LIFO (Last In First Out) ce qui signifie littéralement "Dernier Entré Premier Sorti" (DEPS). Il faut voir cet espace mémoire comme une pile d'assiettes où on a le droit d'empiler/dépiler qu'une seule assiette à la fois. Par contre on a le droit d'empiler des assiettes de taille différente. Lorsque l’on ajoute des assiettes on les empile par le haut, les unes au dessus des autres. Quand on les "dépile" on le fait en commençant aussi par le haut, soit par la dernière posée. Lorsqu'une valeur est dépilée elle est effacée de la mémoire.
Description | Adresse | no Élément | Type |
---|---|---|---|
Espace vide de la pile | (vide) | (vide) | (vide) |
Pointeur de pile → | Pile Base + x | n | char |
... | ... | ... | |
Pile Base + 7 | 4 | int (LSB2) | |
Pile Base + 6 | 4 | int (LSB1) | |
Pile Base + 5 | 4 | int (MSB2) | |
Pile Base + 4 | 4 | int (MSB1) | |
Pile Base + 3 | 3 | short(LSB*) | |
Pile Base + 2 | 3 | short(MSB**) | |
Pile Base + 1 | 2 | char | |
Base de la pile → | Pile Base | 1 | char |
* LSB = Least Significant Byte (Octet de Poids Faible)
** MSB = Most Significant Byte (Octet de Poids Fort)
Le Tas
modifierLe tas est l'espace mémoire qui n'est ni alloué au système d'exploitation, ni alloué à la pile du programme. Il est lui aussi limité mais il est aussi beaucoup plus grand que la pile. On lui alloue des valeurs que l’on veut pouvoir transmettre hors des portée des méthodes où elles sont définies. Ne fonctionnant pas comme une pile, la mémoire ainsi allouée doit être désallouée explicitement.
Allocation en Pile et en Tas
modifierAllocation en Pile
modifierUne allocation de valeur en pile se fait grâce au mot clé "auto".
//création d’une variable c de type char désallouée automatiquement à laquelle on affecte la valeur 'A'
auto char c = 'A';
Depuis la normalisation ANSI, il n'est plus nécessaire de préciser le mot clé "auto". Le compilateur génère automatiquement des variables à désallocation automatique.
Ce code est donc équivalent au précédent
Allocation en Tas
modifierPour allouer en tas en C++, il faut utiliser le mot clé "new" qui retourne un pointeur (nous verrons les pointeurs dans un autre chapitre). Après allocation il faut impérativement désallouer le tas sous peine de créer une fuite de mémoire. Pour désallouer une variable en tas, il faut utiliser le mot clé "delete" qui attend comme paramètre un pointeur.
//allocation d’un caractère en tas
char* a = new char;
//affectation de la valeur 'A' à la valeur pointée
*a = 'A';
//désallocation du caractère en tas
delete a;
Attention : L'allocation en tas ne permet pas la désallocation automatique. Chaque allocation de variable créée avec "new" doit impérativement être détruite avec "delete" sous peine de créer une fuite de mémoire. La fuite de mémoire est une zone mémoire qui a été allouée en tas par un programme qui a omis de la désallouer avant de se terminer. Cela rend la zone inaccessible à toute application (y compris le système d'exploitation) jusqu'au redémarrage du système. Si ce phénomène se produit trop fréquemment la mémoire se remplit de fuites et le système finit par tomber faute de mémoire. |
Variables et Constantes
modifierEn C++ il existe deux catégories de valeurs : Les Variables et les Constantes. On peut donc créer en mémoire soit un espace qui contient une valeur variable (ou modifiable), soit un espace qui contient une valeur constante (ou non-modifiable).
Dans le premier cas nous appelons ces espaces mémoire des "variables", dans le second cas nous les appelons des "constantes".
Les Variables
modifierCes espaces mémoire permettent d'enregistrer des valeurs modifiables à volonté. Il n'existe pas de mot clé spécialisé dans la création des variables.
Attention : Une variable non initialisée contient une valeur indéterminée, il est impératif d'initialiser une variable avant toute utilisation afin d’éviter toute erreur de logique. Si vous parvenez à en faire un réflexe vous gagnerez beaucoup de temps sur le débogage. |
Où <Type> correspond au type (de base ou étendu) qui contiendra la valeur constante ou variable <Valeur> et <AutreValeur>, désignée par le nom <NomVariable>. <Type> et <Valeur> doivent être de même nature. <NomVariable> est un identifiant unique répondant aux mêmes critères que les identifiants de constantes.
//exemple de variable du type caractère
char c = 'A';
//exemple de variable du type entier
int i = 2007;
Attention : Certains identifiants sont interdits. Ils sont utilisés par l'environnement de développement et les utiliser provoquera au mieux des erreurs de logique et au pire un plantage général de l'application, pouvant entraîner des instabilités du système. Non seulement la liste est très longue mais elle change en fonction de l'environnement de développement utilisé. Heureusement la plupart des compilateurs avertissent de la mauvaise utilisation de ces identifiants. Si, d'aventure, ils ne le faisaient pas et que vous constatiez des erreurs inexpliquées à l’utilisation d’un identifiant, cela viendrait peut-être d’un problème de conflit de noms d'identifiant. |
Le tableau ci-dessous montre les différentes possibilités pour un nom d'identifiant
Noms d'identifiant | Validité |
---|---|
a | Valide |
R2d2_c6PO_Starwars | Valide |
_a | Valide |
_2 | Valide |
_ | Valide (à éviter) |
-a | Non valide (le "-" est considéré comme l'opérateur mathématique moins) |
248sp542 | Non valide (commence par un chiffre) |
R 2 d 2 | Non valide (il ne doit pas y avoir d'espace) |
Métronome | Non valide (une lettre accentuée n’est pas un caractère autorisé dans un nom d'identifiant) |
Les Constantes
modifierContrairement aux variables les constantes ne peuvent pas être initialisées après leur déclaration et leur valeur ne peut pas être modifiée après initialisation.
Comme cité précédemment, ces espaces mémoire ne peuvent plus être modifiés une fois qu’ils ont été initialisés. Ces constantes doivent être déclarées avec le mot clé "const" et obligatoirement initialisées sur la même ligne.
Où <type> correspond au type (de base ou étendu) qui contiendra la valeur constante <valeur>, désignée par le nom <NomConstante>. <type> et <valeur> doivent être de même nature. <NomConstante> est un identifiant unique composé de caractères alphanumériques sans espace, sans accents et ne commençant pas par un chiffre.
//exemple de constante caractère
const char c = 'A';
//exemple de constante entière
const int i = 2007;
Attention : A l'instar des variables, les constantes ne peuvent pas porter n’importe quel nom d'identifiant. |
Les Constantes Non Nommées
modifierUne constante non nommée est en C++, une catégorie de constante qui n'a pas de nom. En pratique, ce sont des valeurs brutes non modifiables qui ne peuvent pas être représentées autrement.
//exemple de constante caractère non nommée
'A'
//exemple de constante chaîne non nommée
"Une chaine."
//exemple de constante entière non nommée
27
//exemple de constante réelle non nommée
2.6
Exemples :
modifierCes exemples sont très bien pour appréhender le concept de "constante" mais n'ont pas d'autre intérêt fonctionnel.
/*
Création d’une constante nommée "A_MinusculeAccentCirconflexeNumerique", de type "char",
initialisé avec la valeur constante "-125".
*/
const char A_MinusculeAccentCirconflexeNumerique = -125;
/*
Création d’une constante nommée "A_MinusculeAccentCirconflexeAlphanumerique", de type "char",
initialisé avec le caractère constant "â".
*/
const char A_MinusculeAccentCirconflexeAlphanumerique = 'â';
/*
Création d’une constante nommée "E_MajusculeAccentAiguNumerique", de type "unsigned char",
initialisé avec la valeur constante "144".
*/
const unsigned char E_MajusculeAccentAiguNumerique = 144;
/*
Création d’une constante nommée "E_MajusculeAccentAiguAlphanumerique", de type "unsigned char",
initialisé avec le caractère constant "â".
*/
const unsigned char E_MajusculeAccentAiguAlphanumerique = 'É';
/*
Création d’une constante nommée "MaxShort", de type "short",
initialisé avec la valeur constante "32768".
*/
const short MaxShort = 32768;
/*
Création d’une constante nommée "MinShort", de type "short",
initialisé avec la valeur constante "-32767".
*/
const short MinShort = -32767;
/*
Création d’une constante nommée "MaxUnsignedShort", de type "unsigned short",
initialisé avec la valeur constante "65535".
*/
const unsigned short MaxUnsignedShort = 65535;
/*
Création d’une constante nommée "MinUnsignedShort", de type "unsigned short",
initialisé avec la valeur constante "0".
*/
const unsigned short MinUnsignedShort = 0;
/*
Création d’une constante nommée "MaxInt", de type "int",
initialisé avec la valeur constante "2147483648".
*/
const int MaxInt = 2147483648;
/*
Création d’une constante nommée "MinInt", de type "int",
initialisé avec la valeur constante "-2147483647".
*/
const int MinInt = -2147483647;
/*
Création d’une constante nommée "MaxUnsignedInt", de type "unsigned int",
initialisé avec la valeur constante "4294967295".
*/
const unsigned int MaxUnsignedInt = 4294967295;
/*
Création d’une constante nommée "MinUnsignedInt", de type "unsigned int",
initialisé avec la valeur constante "0".
*/
const unsigned int MinUnsignedInt = 0;
Ces exemples sont très bien pour appréhender le concept de "variable" mais n'ont pas d'autre intérêt fonctionnel.
/*
Création d’une variable nommée "A_MinusculeAccentCirconflexeNumerique", de type "char",
initialisé avec la valeur constante numérique "-125".
*/
char A_MinusculeAccentCirconflexeNumerique = -125;
/*
Création d’une variable nommée "A_MinusculeAccentCirconflexeAlphanumerique", de type "char",
non-initialisé.
*/
char A_MinusculeAccentCirconflexeAlphanumerique;
//Initialisation de la variable "A_MinusculeAccentCirconflexeAlphanumerique" avec le caractère constant "â".
A_MinusculeAccentCirconflexeAlphanumerique = 'â';
/*
Création d’une variable nommée "E_MajusculeAccentAiguNumerique", de type "unsigned char",
non-initialisé.
*/
unsigned char E_MajusculeAccentNumerique;
//Initialisation de la variable "E_MajusculeAccentAiguNumerique" avec la valeur constante numérique "144".
E_MajusculeAccentAiguNumerique = 144;
/*
Création d’une variable nommée "A_E_MajusculeAccentAiguAlphanumerique", de type "unsigned char",
initialisé avec le caractère constant "É".
*/
unsigned char E_MajusculeAccentAiguAlphanumerique = 'É';
/*
Création d’une variable nommée "MaxShort", de type "short",
non-initialisé.
*/
short MaxShort;
//Initialisation de la variable "E_MajusculeAccentAiguNumerique" avec la valeur constante numérique "32768".
MaxShort = 32768;
/*
Création d’une variable nommée "MinShort", de type "short",
initialisé avec la valeur constante numérique "-32767".
*/
short MinShort = -32767;
/*
Création d’une variable nommée "MaxUnsignedShort", de type "unsigned short",
initialisé avec la valeur constante numérique "65535".
*/
unsigned short MaxUnsignedShort = 65535;
/*
Création d’une variable nommée "MaxUnsignedShort", de type "unsigned short",
mon-initialisé.
*/
unsigned short MinUnsignedShort;
//Initialisation de la variable "MinUnsignedShort" avec la valeur constante numérique "0".
MinUnsignedShort = 0;
/*
Création d’une variable nommée "MaxInt", de type "int",
mon-initialisé.
*/
int MaxInt;
//Initialisation de la variable "MaxInt" avec la valeur constante numérique "2147483648".
MaxInt = 2147483648;
/*
Création d’une variable nommée "MinInt", de type "int",
initialisé avec la valeur constante numérique "-2147483647".
*/
int MinInt = -2147483647;
/*
Création d’une variable nommée "MaxUnsignedInt", de type "unsigned int",
initialisé avec la valeur constante numérique "4294967295".
*/
unsigned int MaxUnsignedInt = 4294967295;
/*
Création d’une variable nommée "MinUnsignedInt", de type "unsigned int",
initialisé avec la valeur constante numérique "0".
*/
unsigned int MinUnsignedInt = 0;
Boucles & Structures Conditionnelles
Boucles & Structures Conditionnelles
modifierEn C++ on a très fréquemment besoin de contrôler les chemins d'exécutions, ce afin de réagir à certaines conditions ou pour répéter plusieurs fois un même morceau de code.
Structures conditionnelles
modifierLes structures conditionnelles servent à effectuer des vérifications avant d'exécuter du code spécifique à la condition.
IF, ELSE
modifierIl faut se représenter l'instruction "if" comme un test logique binaire : "si cela est vrai, alors je fais ceci, sinon, je fais cela."
Syntaxe :
if (<Conditions>)
{
<ActionsSiConditionsVrai>
}
else
{
<ActionsSiConditionsFausses>
}
Où <Conditions> sont les conditions à remplir pour que le bloc de <ActionsSiConditionsVrai> de la clause "if"(Si) soit exécuté, et fausses pour que <ActionsSiConditionsFausses> de la clause "else" (Sinon) soit exécuté. Il est à noter que la clause "else" est facultative.
Dans un souci d'optimisation de code et de compréhension les conditions doivent décrire le cas unique. La clause "else" traitant tous les autres cas.
Le fait d'écrire true == vBascule n’est pas une erreur de conception :
- Cela améliore la lisibilité du test.
- Cela permet d'assurer le plantage à la compilation du test si on fait l'erreur de ne mettre qu'un seul égal =.
- Cela permet de signaler aux développeur qui reliront le code que l’on veut bien la valeur true (dans le cas contraire on aurait mis false)
Attention: Il est dangereux et très fortement déconseillé d'enlever les accolades des clauses "if" et "else". Tout comme d’utiliser l'implémentation "else if" pour imbriquer des tests conditionnels. |
Attention:Exemples à ne pas faire :
Exemple
int a;
int b;
//...
if (a > b)
FaitASuperieurB();
else if (a == b)
FaitAEgalB();
else if ((a < b) && (a != 5))
FaitAInferieurB();
else if (a == 5)
FaisAInferieurBEtAEgalCinq();
FaitAutreChose();
|
Ceci est à prohiber pour 3 raisons :
- D'abord, c’est débile "a = b" et "a = 5" auraient été mieux placés dans les premiers "if"
- Ensuite, c’est illisible et seul un initié sait comment va être exécuté "FaitAutreChose"
- Enfin, c’est dangereux car on ne voit pas l'organisation des chemins d'exécutions.
Attention: Quant au groupement "else if" cela revient à faire :
soit : et donc : Ce qui est plus lisible. |
Le code de l'exemple à ne pas faire avec les accolades donnerait :
if (a > b)
{
FaitASuperieurB();
}
else
{
if (a == b)
{
FaitAEgalB();
}
else
{
if ((a < b) && (a != 5))
{
FaitAInferieurB();
}
else
{
if (a == 5)
{
FaisAInferieurBEtAEgalCinq();
}
}
}
}
FaitAutreChose();
Ce qui permet tout de suite de voir que "FaitAutreChose" ne fait pas partie des tests. En effet sans accolades les clauses "if" et "else" n'exécutent que l'instruction située juste derrière elles. De plus on voit les chemins de codes et les optimisations possibles.
Voici donc le même code optimisé
if (a == b)
{
FaitAEgalB();
}
else
{
if (a < b)
{
if (a == 5)
{
FaisAInferieurBEtAEgalCinq();
}
else
{
FaitAInferieurB();
}
}
else
{
FaitASuperieurB();
}
}
FaitAutreChose();
On peut voir ici que le nouveau code fait non seulement la même chose que le code original mais en plus on effectue un test de moins grâce à l'optimisation.
Malheureusement il existe encore beaucoup de code industriel écrit de manière compacte (sans accolades) et possédant des "else if" imbriqués.
Switch, case, default
modifierIl faut se représenter l'instruction "switch" comme un test logique multi-états : "Selon la variable, si elle est à telle valeur alors faire ceci, si elle est à telle autre valeur faire cela, si elle est à encore une autre valeur faire autre chose, ..., sinon : faire le traitement par défaut.".
Attention: Cette structure de test conditionnel ne permet de tester que des valeurs entières ou des chars, pas des chaines ni des flottants ! |
Syntaxe :
switch(<Variable>)
{
case <Valeur1>:
<ActionsSiValeur1>;
break;
case <Valeur2>:
{
<ActionsSiValeur2>;
}
break;
case <Valeur3>:
case <Valeur4>:
{
<ActionsSiValeur3Ou4>;
}
break;
case <Valeur5>:
{
<ActionsSiValeur5>;
}
break;
///...
case <ValeurN>:
{
<ActionsSiValeurN>;
}
break;
default:
{
<TraitementParDéfaut>
}
break;
}
Où <Variable> est la variable à tester <Valeur1> à <ValeurN> sont les différentes valeurs que <Variable> peut prendre et qui nécessitent un traitement particulier, <ActionsSiValeur1> à <ActionsSiValeurN> sont les différents traitements pour chaque valeur nécessitant leur propre traitement, <ActionsSiValeur3Ou4> est un cas où plusieurs valeurs (ici <Valeur3> et <Valeur4>) nécessitent le même traitement (remarquez l'absence de break; entre les deux cas, autrement obligatoire), <TraitementParDéfaut> signalé par la clause default est le traitement réservé à toute autre valeur que celles renseignées par les clauses case. Il est à noter que la clause default n’est pas obligatoire. Par contre il est impératif de mettre une clause break après chaque clause case suivie de code.
Dans un souci d'optimisation de code et de compréhension les valeurs renseignées dans les clauses case doivent décrire les cas uniques nécessitant des traitements personnalisés. La clause default traitant tous les autres cas.
#include <iostream>
using namespace std;
int main (int argc, char* argv[])
{
int vInteger = argc;
switch (vInteger)
{
case 0:
{
cout << "vInteger = 0" << endl;
}
break;
case 1:
case 2:
{
cout << "vInteger = 1 ou 2" << endl;
}
break;
default:
{
cout << "vInteger != (0, 1 et 2)" << endl;
}
break;
}
return 0;
}
Attention: Il est fortement conseillé de mettre entre accolades les traitements des clauses case, en particulier si les traitements en question sont longs ou utilisent des boucles ou des structures conditionnelles. |
Boucles
modifierEn C++ on peut demander à un programme de répéter plusieurs fois l'exécution du même code. Cela s’appelle itérer le code, ou plus simplement faire une boucle.
Boucles FOR
modifierLa boucle "for" est une itération bornée elle est généralement associée à une variable de contrôle et à une condition afin de ne pas exécuter le code indéfiniment. En pratique, on ne l'utilise que si l’on sait ou s'il y a moyen de savoir combien de fois on doit itérer.
Où <Initialisation> est la séquence d'initialisation de la boucle qui est en général utilisée pour initialiser le compteur de contrôle, <Condition> est la condition à réaliser pour que la boucle for continue d'itérer le code <ItérationCode>, et <ItérationContrôle> est la séquence d'itération du contrôle de la boucle.
Concrètement :
#include <iostream> // nécessaire pour utiliser "std:cout"
int vLimite = 5; // limite de l'itération.
for(int vIndex = 0; vIndex < vLimite; vIndex++)
{
std:cout << (vIndex + 1) << " / " << vLimite << "\n"; // Code itéré
}
La boucle "for" est à traduire littéralement par : "pour". Par analogie avec d'autres langages comme le Pascal on peut la comprendre ainsi : "Pour, De, À, Faire, Puis", soit en ce qui concerne notre exemple :
"Pour" l'entier 'vIndex', "De" la valeur '0', "À" la valeur 'vLimite', "Faire" : 'std:cout << (vIndex + 1) << " / " << vLimite;' "Puis" 'vIndex++'.
Attention: Il est à noter que si la condition est fausse à l'entrée de la boucle (ex: 1 < 0) alors la boucle n'effectue aucune itération. De même si la condition est toujours vraie (ex: 0 > 1) alors on ne sort jamais de la boucle. Ces deux comportements sont la plupart du temps les conséquences d'erreurs de conception. |
Boucles WHILE
modifierLa boucle "while" est une forme d'itération non bornée qui permet de répéter un code tant qu'une condition est vraie. La plupart du temps cette boucle est utilisée si l’on ne connait pas l'état initial de la variable qui est utilisée dans la condition, ou que l’on ne connait pas et que l’on ne peut pas calculer le nombre d'itérations nécessaire pour que la variable atteigne son état final.
Où <Condition> est la condition pour que la boucle fasse une nouvelle itération et <ItérationCode> le code à itérer.
#include <iostream> // nécessaire pour utiliser "std::cout"
bool vContinuer = true; // limite de l'itération.
unsigned int vFactorielle = 50;
int vResultat = 0;
while(true == vContinuer)
{
if(0 == vFactorielle)
{
vResultat = vResultat + 1;
vContinuer = false;
}
else
{
vResultat += vFactorielle; // Ajoute factorielle au résultat.
vFactorielle--; // décrémente factorielle.
}
}
std::cout << "Factorielle " << vFactorielle << " = " << vResultat << "\n";
Boucles DO WHILE
modifierLa différence entre une boucle while et une boucle do while est que dans la boucle "do while" le code inclus dans la boucle est exécuté au moins une fois.
Voici un exemple d'implémentation qui montre que les instructions de la boucle "do while" sont bien executé au moins 1 fois.
#include <iostream> // nécessaire pour utiliser "std::cout"
bool vContinuer = false; // limite de l'itération.
int vExecutionBoucle = 0;
do
{
vExecutionBoucle++;
std::cout << Boucle executée << vExecutionBoucle << " fois" << std::endl;
}
while(true == vContinuer);
std:cout << "Factorielle " << vFactorielle << " = " << vResultat << "\n";
Méthodes
Les Méthodes :
modifierLa méthode en C++ est l'un des éléments les plus basiques mais essentiels du langage. De nos jours, certains programmes informatiques tels les systèmes d'exploitation contiennent plusieurs millions de méthodes. Comme nous l'avons dit précédemment, la méthode permet de définir une suite d'opérations à exécuter dans un ordre séquentiel donné.
<TypeRetour> [<Portee>::]<NomMethode>([<TypeParametre> <NomParametre>[=<ValeurParDefaut>][,<...>]])
{
[<Instructions>;]
}
Où <TypeRetour> est le type de retour de la méthode <NomMethode>, <Portee> est le nom de la portée à laquelle est rattachée la méthode, s'il y a lieu, <TypeParametre> est le type du paramètre, <NomParametre> est le nom du paramètre, <ValeurParDefaut> est la valeur éventuellement souhaitée pour le paramètre, <...> sont des paramètres additionnels et <Instructions> sont les instructions contenues dans la méthode.
En C++ l’application la plus basique en C++ est le point d'entrée programme qui n'est autre que la méthode "main".
Ceci est le minimum de code requis pour la création d'une application en C++.
Décortiquons un peu cela :
int main(int argc, char* argv[])
Nous avons donc ici le point d'entrée du programme. C'est une méthode qui a comme type de retour une valeur entière. Cette valeur permet au système de savoir dans quelle circonstance s'est terminé le programme. Si le programme s'est fermé normalement, la valeur de retour sera égale à zéro. Par contre si une erreur s'est produite, alors la valeur de retour sera différente de zéro. En général, en cas d'erreur, main retourne -1, mais il peut y avoir d'autres valeurs retournées dans certains cas particuliers.
Le paramètre argc est en fait un compteur sur le nombre de chaines de caractères contenus dans le tableau argv[].
Premières applications :
modifierIci nous allons développer ce que nous avons appris de manière théorique dans les chapitres précédents.
Voici un programme d'exemple qui affiche à l'écran une lettre choisie par le programmeur.
#include <iostream> // nécessaire pour utiliser cout.
int main(int argc, char* argv[])
{
// Définit une constante de type unsigned char, dénommée MonCaractere et ayant pour valeur la lettre 'â'.
const unsigned char MonCaractere = 'â';
// Affiche la valeur de la constante MonCaractere sous forme de caractère à l'écran
std::cout << MonCaractere << std::endl;
// Renvoie 0 au système (traitement sans erreur)
return 0;
}
Autre exemple :
#include <iostream> // nécessaire pour utiliser cout.
int main(int argc, char* argv[])
{
//Définit une constante de type char, dénommée MonCaractere et ayant pour valeur la lettre 'â'.
const char MonCaractere = 'â';
//Affiche la valeur de la constante MonCaractere sous forme de caractère à l'écran
std::cout << MonCaractere << std::endl;
//Renvoie 0 au système (traitement sans erreur)
return 0;
}
Les 2 applications produisent le même résultat mais n'ont pas le même code. Cela confirme le fait que la représentation graphique des caractères se fait par le biais de la représentation non signée du codage du caractère.
Bonjour Monde !
modifierVoici l'une des applications les plus basiques que l’on peut faire en C++. Le célèbre "Bonjour Monde !" qui n'a d'autres but que d'afficher "Bonjour Monde !" à l'écran.
#include <iostream> // nécessaire pour utiliser cout
using namespace std;
int main(int argc, char* argv[])
{
cout << "Bonjour Monde !" << endl;
return 0;
}
Ce programme est statique c'est-à-dire qu’il fera toujours la même chose si on veut qu’il fasse autre chose il faut le reprogrammer. Ce genre de programme n’est pas toujours des plus utiles.
Bonjour M. X !
modifierL'étape suivante, pour comprendre l'utilité des paramètres des méthodes (et surtout ceux de la méthode main), est d’utiliser le programme suivant.
#include <iostream> // nécessaire pour utiliser cout
using namespace std;
int main(int argc, char* argv[])
{
cout << "Bonjour M. " << argv[argc - 1] << " !" << endl;
return 0;
}
Voyons ce qui se passe si on exécute la commande DOS: "[PATH]\BonjourMX.exe X". Le programme affiche "Bonjour M. X !".
Si nous exécutons "[PATH]\BonjourMX.exe Dupont", le programme affiche "Bonjour M. Dupont !".
Si nous exécutons "[PATH]\BonjourMX.exe Dupont Dupond" le programme affiche "Bonjour M. Dupond !".
Maintenant que se passerait-il si nous exécutions "[PATH]\BonjourMX.exe" ?
En fait, le programme est prévu pour afficher le dernier paramètre de la chaîne d'appel de la ligne de commande. Comme le système d'exploitation passe au programme le chemin de ce dernier comme premier paramètre, la commande "[PATH]\BonjourMX.exe" affichera "Bonjour M. [PATH]\BonjourMX.exe !".
Voici donc comment récupérer le nom de l’application ainsi que le chemin à partir duquel elle est lancée. La valeur de "argv[0]" correspond toujours au chemin du programme.
Appels de Méthodes :
modifierL'appel de méthodes permet l’utilisation et la réutilisation d'un morceau de code par l’ensemble du programme.
Voici une petite application qui permet de se familiariser avec l'utilité des méthodes. Imaginons que nous voulons rendre le calclul : y = a . x + b, accessible à tout le programme sans avoir à le réécrire partout dans le code. Nous devrons alors écrire la méthode "CalculeAffine" suivante :
#include <iostream> // nécessaire pour utiliser cout
using namespace std;
double mCalculeAffine(double pA, double pX, double pB) // Le petit "m" en préfixe correspond à "method" et le petit "p" à "parameter"(très utile avec les IDE et la complétion de code ([CTRL]+[Espace]))
{
return pA * pX + pB;
}
int main(int argc, char* argv[])
{
double vY; // Le petit "v" en préfixe correspond à "variable" (très utile avec les IDE et la complétion de code ([CTRL]+[Espace]))
double vA = 5;
double vX = 3;
double vB = 2;
cout << "y = " << vA << " . " << vX << " + " << vB << endl; // Affiche "y = 5 . 3 + 2"
vY = mCalculeAffine(vA, vX, vB); // Calcule la fonction mathématique au travers de la méthode;
cout << "y = " << vY << endl; // Affiche "y = 17"
vA = 9;
vX = 3;
vB = 3;
cout << "y = " << vA << " . " << vX << " + " << vB << endl; // Affiche "y = 9 . 3 + 3"
vY = mCalculeAffine(vA, vX, vB); // Calcule la fonction mathématique au travers de la méthode;
cout << "y = " << vY << endl; // Affiche "y = 30"
vA = 4;
vX = 8;
vB = 9;
cout << "y = " << vA << " . " << vX << " + " << vB << endl; // Affiche "y = 4 . 8 + 9"
vY = mCalculeAffine(vA, vX, vB); // Calcule la fonction mathématique au travers de la méthode;
cout << "y = " << vY << endl; // Affiche "y = 41"
vA = 7;
vX = 5;
vB = 2;
cout << "y = " << vA << " . " << vX << " + " << vB << endl; // Affiche "y = 7 . 5 + 2"
vY = mCalculeAffine(vA, vX, vB); // Calcule la fonction mathématique au travers de la méthode;
cout << "y = " << vY << endl; // Affiche "y = 37"
vA = 8;
vX = 9;
vB = 6;
cout << "y = " << vA << " . " << vX << " + " << vB << endl; // Affiche "y = 8 . 9 + 6"
vY = mCalculeAffine(vA, vX, vB); // Calcule la fonction mathématique au travers de la méthode;
cout << "y = " << vY << endl; // Affiche "y = 78"
return 0;
}
Ce code n'est pas trop mal, il fonctionne bien. Seulement, il a un problème, il comporte des doublons de code.
Un vrai développeur n'aurait jamais fait ce programme ainsi. Le copier/coller du code tel que je vous l'ai présenté est consommateur en lignes de code. De plus, il ne facilite pas la lecture du code et peut même introduire des erreurs. Il n'est jamais conseillé de copier/coller du code. Dans un cas sur deux, cela génère des erreurs. |
Voyons comment on pourrait arranger cela de manière plus lisible et moins gourmande en code :
#include <iostream> // nécessaire pour utiliser cout
using namespace std;
double mCalculeAffine(double pA, double pX, double pB)
{
return pA * pX + pB;
}
void mAfficheCalculsAffine(double pA, double pX, double pB)
{
int vY;
cout << "y = " << pA << " . " << pX << " + " << pB << endl; // Affiche "y = pA . pX + pB"
vY = mCalculeAffine(pA, pX, pB); // Calcule la fonction mathématique au travers de la méthode;
cout << "y = " << vY << endl; // Affiche "y = mCalculeAffine(vA, vX, vB)"
}
int main(int argc, char* argv[])
{
double vA = 5;
double vX = 3;
double vB = 2;
mAfficheCalculsAffine(vA, vX, vB); // Affiche "y = 5 . 3 + 2" puis "y = 17"
vA = 9;
vX = 3;
vB = 3;
mAfficheCalculsAffine(vA, vX, vB); // Affiche "y = 9 . 3 + 3" puis "y = 30"
vA = 4;
vX = 8;
vB = 9;
mAfficheCalculsAffine(vA, vX, vB); // Affiche "y = 4 . 8 + 9" puis "y = 41"
vA = 7;
vX = 5;
vB = 2;
mAfficheCalculsAffine(vA, vX, vB); // Affiche "y = 7 . 5 + 2" puis "y = 37"
vA = 8;
vX = 9;
vB = 6;
mAfficheCalculsAffine(vA, vX, vB); // Affiche "y = 8 . 9 + 6" puis "y = 78"
return 0;
}
On a ainsi réussi à économiser des répétitions de code en factorisant les doublons dans des méthodes
Récursivité :
modifier
Récursivité Directe :
modifierLa récursivité directe est la capacité qu'a un algorithme inclus dans une méthode à s'appeler lui-même au travers de la méthode qui le contient.
#include <iostream> // nécessaire pour utiliser cout
using namespace std;
unsigned long mFactorielle(unsigned long pNombre)
{
// Si le parametre "pNombre" vaut 0 alors
if(0 == pNombre) // <- Le test d'arrêt est très important, sans lui la récursion ne j'arrêterais jamais.
{
// Retourner 1 ("0! = 1", Factorielle de 0 est égal à 1)
return 1;
}
else // Sinon
{
// Retourner "pNombre * (pNombre - 1)!", (Factorielle de n = n * Factorielle de (n-1))
return pNombre * mFactorielle(pNombre - 1);
}
}
int main(int argc, char* argv[])
{
unsigned long vNombre = 12; // Valeur entière maximale calculable pour une factorielle sur un unsigned long
unsigned long vFactorielle; // Résultat de la factorielle du nombre.
cout << "n = " << vNombre << endl; // Affiche "n = 12"
vFactorielle = mFactorielle(vNombre); // Calcule la fonction mathématique au travers de la méthode;
cout << "n! = " << vFactorielle << endl; // Affiche "n! = 479001600"
return 0; // Sort du programme.
}
Récursivité Indirecte :
modifierLa récursivité indirecte est la capacité qu'a un algorithme inclus dans une méthode à s'appeler lui-même au travers d'une autre méthode que celle qui le contient.
#include <iostream> // nécessaire pour utiliser cout
using namespace std;
bool mNombreImpair(unsigned long pNombre); // nécessaire pour utiliser mNombreImpair dans mNombrePair (déclaration avancée)
// Vérifie si un nombre est pair
bool mNombrePair(unsigned long pNombre)
{
// Si le nombre est égal à 0
if (pNombre == 0)
{
// Retourner vrai
return true;
}
else // Sinon
{
// Retourner la vérification que le nombre "pNombre - 1" est impair
return mNombreImpair(pNombre - 1);
}
}
// Vérifie si un nombre est impair
bool mNombreImpair(unsigned long pNombre)
{
// Si le nombre est égal à 0
if ( pNombre == 0)
{
// Retourner faux
return false;
}
else // Sinon
{
// Retourner la vérification que le nombre "pNombre - 1" est pair
return mNombrePair(pNombre - 1);
}
}
int main(int argc, char* argv[])
{
unsigned int vNombre = 0;
unsigned int vLimite = 15; // Valeur arbitraire
// Tant que vNombre est supérieur ou égal à 0
while(vNombre <= vLimite)
{
cout << "Le nombre \"" << vNombre << "\" est "; // Affiche "Le nombre "(vNombre)" est "
// Si vNombre est pair
if(mNombrePair(vNombre))
{
cout << "pair."; // Affiche "pair."
}
else // Sinon
{
cout << "impair."; // Affiche "impair."
}
cout << endl; // Affiche un retour à la ligne
vNombre++; // Incrémente vNombre
}
return 0; // Sort du programme.
}
Il est à noter que la récursivité a un poids important sur la gestion de la pile. En effet, à chaque appel d'une méthode le programme change de contexte, ce qui l'oblige à sauvegarder les registres du processeur en pile. Comme la récursivité appelle plusieurs fois la même méthode, la pile est remplie à vitesse grand V, ce qui peut poser problème dans les systèmes ne disposent pas de grosses taille de pile.
Alternative à la Récursivité :
modifierEn C++, il est toujours possible d'écrire un algorithme qui effectue les tache de la récursion de manière itérative.
Pour la factorielle :
#include <iostream> // nécessaire pour utiliser cout
using namespace std;
unsigned long mFactorielleIterative(unsigned long pNombre)
{
unsigned long vResultat = 1;
unsigned long vControle = 1;
while (vControle <= pNombre)
{
vResultat = vResultat * vControle;
vControle++;
}
return vResultat;
}
int main(int argc, char* argv[])
{
unsigned long vNombre = 12; // Valeur entière maximale calculable pour une factorielle sur un unsigned long
unsigned long vFactorielle; // Résultat de la factorielle du nombre.
cout << "n = " << vNombre << endl; // Affiche "n = 12"
vFactorielle = mFactorielleIterative(vNombre); // Calcule la fonction mathématique au travers de la méthode;
cout << "n! = " << vFactorielle << endl; // Affiche "n! = 479001600"
return 0; // Sort du programme.
}
Pour le pair/impair :
#include <iostream> // nécessaire pour utiliser cout
using namespace std;
bool mNombreImpair(unsigned long pNombre);
// Vérifie si un nombre est pair
bool mNombrePairIteratif(unsigned long pNombre)
{
while (true)
{
// Si le nombre est égal à 0
if (pNombre == 0)
{
// Retourner vrai
return true;
}
else // Sinon
{
pNombre--;
if (pNombre == 0)
{
// Retourner la vérification que le nombre "pNombre - 1" est impair
return false;
}
else
{
pNombre--;
}
}
}
}
int main(int argc, char* argv[])
{
unsigned int vNombre = 0;
unsigned int vLimite = 15; // Valeur arbitraire
// Tant que vNombre est supérieur ou égal à 0
while(vNombre <= vLimite)
{
cout << "Le nombre \"" << vNombre << "\" est "; // Affiche "Le nombre "(vNombre)" est "
// Si vNombre est pair
if(mNombrePairIteratif(vNombre))
{
cout << "pair."; // Affiche "pair."
}
else // Sinon
{
cout << "impair."; // Affiche "impair."
}
cout << endl; // Affiche un retour à la ligne
vNombre++; // Incrémente vNombre
}
return 0; // Sort du programme.
}
La récursivité est préférable, dans la grande majorité des cas, à l'itération car elle permet, en général, une meilleure compréhension d'un algorithme. Cependant, il ne faut pas oublier qu'elle est consommatrice en pile et que cela peut poser problème dans certains systèmes ou pour certaines applications. Heureusement, la récursivité n'est applicable que dans un certain nombre limité de cas.
Polymorphisme de Méthode
modifierLe polymorphisme de méthode est la faculté qu'ont les méthodes de même nom mais de signatures différentes à pouvoir sélectionner la bonne méthode pour la bonne signature.
#include <stdio.h>
void mToString(char* pString, int pValue)
{
sprintf(pString, "%i", pValue);
}
void mToString(char* pString, unsigned int pValue)
{
sprintf(pString, "%u", pValue);
}
void mToString(char* pString, long pValue)
{
sprintf(pString, "%ld", pValue);
}
void mToString(char* pString, unsigned long pValue)
{
sprintf(pString, "%lu", pValue);
}
int main(int pArgumentsCount, char* pArgumentsValues[])
{
int vInteger = -1123456789;
unsigned int vUnsignedInteger = 2123456789;
long vLong = -3123456789;
unsigned long vUnsignedLong = 4123456789;
char* vString = new char[15];
mToString(vString, vInteger);
printf(vString);
mToString(vString, vUnsignedInteger);
printf(vString);
mToString(vString, vLong);
printf(vString);
mToString(vString, vUnsignedLong);
printf(vString);
delete [] vString;
return 0;
}
Pointeur, Tableaux et références
Les Pointeurs
modifierLes pointeurs servent à accéder aux données en mémoire par le biais de leur adresse. Le type pointeur est un entier non signé dont la taille dépend du type de processeur et du mode en cours. En gros, et pour simplifier, il utilise le format du "unsigned long int" actuellement en cours, où il enregistre l'adresse mémoire de la donnée à pointer.
Adresse | Valeur | Nom de variable |
---|---|---|
... | ... | ... |
0x635 | 'P' | x |
... | ... | ... |
0x7B8 | 0x00000635 | p |
... | ... | ... |
Voici la syntaxe :
Où <Type> est le type de la donnée en mémoire, "*" signifie que l’on a affaire à un pointeur, <NomPointeur> est l'identifiant du pointeur et <Valeur> l'adresse mémoire (unsigned long int) où le pointeur doit aller chercher la donnée.
Affectation d'adresse
modifierEn C++, il est utile de récupérer l'adresse d'une variable ou d'une constante en utilisant l'opérateur "&" qui signifie "adresse de" et se place à gauche de l'identifiant.
char x = 'A'; // création de la variable char en pile
char* p = &x; // création de la variable pointeur sur char en pile et affectation de l'adresse de la variable en pile.
Comprendre : création d'une variable nommée "p" de type pointeur sur char "char*" prenant pour valeur "=" l'adresse de "&" la variable "x"
Mécanique d'Adressage
modifierEn C++, il peut être utile de copier l'adresse d'une variable pointée par un pointeur sur un autre pointeur. Cela se fait ainsi :
char x = 'A'; // création de la variable char en pile
char* p1 = &x; // création de la variable pointeur sur char en pile et affectation de l'adresse.
char* p2 = p1; // copie la valeur de l'adresse pointée par p1 dans le pointeur p2. (p2 == p1), (p2 == &x)
On n'utilise pas l'opérateur "adresse de" dans ce cas là car "p1" renvoie l'adresse de "x"
Déréférencement
modifierEn C++, il peut être utile de récupérer la valeur d'une variable pointée par un pointeur. Cela se fait ainsi :
char x = 'A'; // création de la variable char en pile
char* p = &x; // création de la variable pointeur sur char en pile et affectation de l'adresse.
char y = *p; // Déréférencement du pointeur "p" pour obtenir la valeur pointée (y == 'A')
Arithmétique des pointeurs
modifierEn C++, les pointeurs ont leur propre algorithmique. L'incrémentation et la décrémentation est particulière pour ce type. En fait, l'incrémentation (ainsi que la décrémentation) se fait en multipliant la taille du type du pointeur (obtenue avec sizeof()) par la valeur que l’on veut lui ajouter/retrancher.
L'arithmétique de pointeur est un concept particulier qu’il est possible d’éviter avec les notations liées au tableau. En particulier en C++, il est intéressant de s'interdire de recourir à l’arithmétique de pointeurs.
int a; // création de la variable int en pile (sizeof(int) == 4)
int* p = &a; // création de la variable pointeur sur int et affectation de l'adresse de a.
int* x = p++; // création de la variable pointeur sur int x (x == &a)
int* y = p; // création de la variable pointeur sur int y (y == &a + 4 octets)
int* z = ++p; // création de la variable pointeur sur int z (z == &a + 8 octets)
x = p--; // (x == &a + 8 octets)
y = p; // (y == &a + 4 octets)
z = --p; // (z == &a)
// plus explicitement
x = p; // (x == &a)
y = p + 1; // (y == &a + 4 octets)
z = p + 1; // (z == &a + 8 octets)
x = p; // (x == &a + 8 octets)
y = p - 1; // (y == &a + 4 octets)
z = p - 1; // (z == &a)
Ce comportement permet la création et l’utilisation des tableaux. Nous verrons cela un peu plus bas.
Lorsque les pointeurs sont créés, ils prennent la valeur d'adresse qu’il y avait à l'emplacement qu’ils occupent. C'est pour cela qu’il faut impérativement les initialiser avant de s'en servir. Dans le cas contraire on peut assister, parfois, à des comportements des plus étranges.
Le pire des cas est celui du pointeur non initialisé à qui l’on affecte une valeur. Dans certains cas rares, l’application fonctionne sur l'ordinateur de développement car la donnée que le pointeur va prendre comme adresse, correspond à une adresse valide et donc le programme se déroule normalement.
Mais après installation sur le poste client, l’application plante car la donnée en mémoire n'est plus la même et ne correspond plus à une adresse valide.
Il est inacceptable d’utiliser la forme calculée des pointeurs pour accéder à des tableaux. Seule la forme entre crochets est valide. Le jour où un développeur instanciera un tableau dynamique sur un pointeur utilisé sous forme calculée, le programme plantera car le compilateur sera incapable de faire les déréférencements. |
Préférez donc la forme entre crochets
à la forme calculée
|
Les Tableaux
modifierEn C++, les tableaux permettent de mémoriser des ensembles de données de même type. Il en existe 2 sortes, les tableaux statiques et les tableaux dynamiques.
La différence entre tableaux statiques et dynamiques est que les tableaux statiques sont dans tous les cas des groupes d'octets contigus monolithiques dans la pile alors que les tableaux dynamiques multidimensionnels sont, le plus souvent, des groupes d'octets contigus disséminés en plusieurs paquets dans le tas.
Tableaux "Statiques"
modifierLe fait qu’ils soient enregistrés en pile fait que leur taille doit être connue dès la compilation et donc être constante. En effet, pour le processeur, il serait mal vu de pouvoir redimensionner le tableau après la création d'autres variables en pile. Dans le cas de l'agrandissement, on écraserait les nouvelles variables et dans le cas de la diminution, on ferait des "TROUS" dans la pile, qui alors s'effondrerait.(Rappelez vous qu’il faut considérer la pile comme une pile d'assiettes)
On peut calculer le volume que le tableau statique occupe en mémoire, en multipliant la taille du type utilisé par le nombre d'éléments.
Ainsi un tableau tridimensionnel de 4 * 3 * 2 valeurs int, fera :
4(grilles) * 3(lignes) * 2(colonnes) * 4(Octets du type int) = 24(éléments)* 4(Octets du type int) = 96 Octets contigus en pile.
Voici la syntaxe :
<Type> <NomTableau> [<Constante1>](...)[<ConstanteN>];
ou
<Type> <NomTableau> [<Constante1>](...)[<ConstanteN>] = {{<Variable11,...,Variable1M>},(...),{<VariableN1,...,VariableNM>}};
ou
<Type> <NomTableau> [](...)[] = {{<Variable11,...,Variable1M>},(...),{<VariableN1,...,VariableNM>}};
Où <Type> est le type des éléments du tableau, <NomTableau> est le nom de la variable tableau, [](...)[] indique que c’est un tableau à N dimensions, <ConstanteN> est le nombre d'éléments, constant, de la dimension N du tableau (facultatif si on a défini une liste de valeurs), = assigne des valeurs au tableau (facultatif si <ConstanteN> est présent), {{},(...),{}} est une liste de valeurs des dimensions (il y en a autant que <ConstanteN>, facultatif si <ConstanteN> est présent), et <VariableN1,...VariableNM> sont les différentes valeurs que le tableau doit affecter aux éléments de la ligne N (il doit y en avoir autant que <ConstanteN>, facultatif si <ConstanteN> est présent).
Concrètement, voyons ce que ça donne si on reprend notre exemple :
// Valeurs constantes
const int Grilles = 4;
const int Lignes = 3;
const int Colonnes = 2;
// Valeur variables
int deux = 2;
// Différentes syntaxes pour la création des tableaux statiques
int t1 [Grilles][Lignes][Colonnes]; // création d'un tableau d'entiers de 4 * 3 * 2 = 24 éléments non initialisés.
// Création d'un tableau d'entiers de 24 éléments initialisés.
int t2 [Grilles][Lignes][Colonnes] = {
{
{1, deux},
{3, 4},
{5, 6}
},
{
{7, 8},
{9, 10},
{11, 12}
},
{
{13, 14},
{15, 16},
{17, 18}
},
{
{19, 20},
{21, 22},
{23, 24}
}
};
// Création d'un tableau d'entiers de 24 éléments initialisés.
int t3 [][3][2] = {
{
{1, deux},
{3, 4},
{5, 6}
},
{
{7, 8},
{9, 10},
{11, 12}
},
{
{13, 14},
{15, 16},
{17, 18}
},
{
{19, 20},
{21, 22},
{23, 24}
}
};
Les tableaux statiques, sont organisés par lignes. Cela signifie que lorsque l’on incrémente l'indice du tableau, on change de colonne plus rapidement que ce que l’on change de ligne.
Pour reprendre notre exemple, cela donnerait (en supposant que l'adresse de base du tableau t1 soit 0x02C0) :
Adresse | Index | no Élément | Grille | Ligne | Colonne | Valeur |
---|---|---|---|---|---|---|
0x02C0 | t1[0][0][0] | 0 | 0 | 0 | 0 | 1 |
0x02C4 | t1[0][0][1] | 1 | 0 | 0 | 1 | 2 |
0x02C8 | t1[0][1][0] | 2 | 0 | 1 | 0 | 3 |
0x02CC | t1[0][1][1] | 3 | 0 | 1 | 1 | 4 |
0x02D0 | t1[0][2][0] | 4 | 0 | 2 | 0 | 5 |
0x02D4 | t1[0][2][1] | 5 | 0 | 2 | 1 | 6 |
0x02D8 | t1[1][0][0] | 6 | 1 | 0 | 0 | 7 |
... | ... | ... | ... | ... | ... | ... |
0x03A8 | t1[2][2][1] | 17 | 2 | 2 | 1 | 18 |
0x03AC | t1[3][0][0] | 18 | 3 | 0 | 0 | 19 |
0x03B0 | t1[3][0][1] | 19 | 3 | 0 | 1 | 20 |
0x03B4 | t1[3][1][0] | 20 | 3 | 1 | 0 | 21 |
0x03B8 | t1[3][1][1] | 21 | 3 | 1 | 1 | 22 |
0x03BC | t1[3][2][0] | 22 | 3 | 2 | 0 | 23 |
0x03C0 | t1[3][2][1] | 23 | 3 | 2 | 1 | 24 |
Pointeurs et Tableaux Statiques
modifierNous avons pu voir tout à l’heure que l'arithmétique des pointeurs permet de travailler sur plus d'une valeur voyons ce que cela donne :
// Valeur variable
int trois = 3;
// Différentes syntaxes pour la création des tableaux statiques
int t[4] = {1, 2, trois, 4}; // Création d'un tableau d'entier de 4 éléments initialisés.
trois = 0; // trois = 0
int* pInt1 = t; // Création d'un pointeur sur entiers auquel on affecte t
int* pInt2 = &t[0]; // Création d'un pointeur sur entiers auquel on affecte l'adresse du premier élément du tableau t (pInt1 == pInt2)
int a = *(pInt1+2); // a == 3
int b = *(t+2); // b == 3
int c = pInt1[2]; // c == 3
Tableaux "Dynamiques"
modifierPour un tableau dynamique, il existe deux méthodes.
Tableaux Compacts
modifierOn peut utiliser le format des tableaux statiques multidimensionnels que l’on reproduit en tas.
Voici la syntaxe de déclaration :
<Type>* <NomVariable>[<Constante1>](...)[<ConstanteN>] = new <Type>[<Variable>][<Constante1>](...)[<ConstanteN>];
Où <Type> est le type du tableau, <NomVariable> est le nom identifiant du tableau, <Variable> est une variable entière non signée définissant la première dimension du tableau, <ConstanteN> sont des constantes entières non signées définissant les autres dimensions du tableau.
Aucun tableau dynamique n'est assignable à la déclaration. On ne peut pas en C++ assigner des valeurs à un tableau dynamique lors de sa création.
// Valeurs constantes
const int vColonnes = 2;
const int vLignes = 3;
// Valeur variables
int vGrilles = 4;
int* vTableau[vLignes][vColonnes] = new int[vGrilles][vLignes][vColonnes]; // Création d'un tableau d'entiers de 24 éléments.
//...
delete [] vTableau;
Cela n’est pas très judicieux pour trois raisons :
- La première c’est que les tailles des dimensions du tableau sont fixées définitivement (à moins de recréer le tableau complet).
- La deuxième c’est que les différents éléments ne sont pas désolidarisables du tableau.
- La troisième pour redimensionner le tableau il faut le recopier intégralement.
Techniquement, on peut faire mieux.
La seule raison valable qui peut amener à utiliser ces types de tableaux dynamiques est leur forme compacte. Ceci dit ils ne font réellement gagner de l'espace que sur les tableaux dont on connaît à l'avance la longueur. En effet, la lourdeur du traitement pour ajouter ou soustraire des éléments est souvent rédhibitoire.
Il ne reste donc qu'une solution techniquement valable en environnement PC : L'indirection de pointeurs multiples.
Indirection de Pointeurs
modifierLe but du jeu est de créer un tableau de pointeurs où chaque pointeur pointe vers un autre tableau de pointeurs... (ce récursivement pour chaque dimension)... où chaque pointeur pointe vers un élément.
Où <Type> est le type du tableau, <NomTableau> est le nom du tableau et <Variable> une variable entière non signée.
Voyons comment cela est représenté en mémoire :
Voici un exemple concret qui montre comment créer/détruire et manipuler un tableau utilisant l'indirection de pointeurs.
#include <iostream>
using namespace std;
// Création d'un tableau de pointeurs triples d'entier.
int**** mCreerTableauDynamiquePointeursTriplesEntiers(int pGrilles, int pLignes, int pColonnes)
{
int**** vTableau = new int***[pGrilles];
// Pour chaque pointeur triple du tableau
for(int vIndexGrille = 0; vIndexGrille < pGrilles; vIndexGrille ++)
{
// Créer un tableau de pointeurs doubles dans vTableau[vIndexGrille].
vTableau[vIndexGrille] = new int** [pLignes];
// Pour chaque pointeur double du tableau vTableau[vIndexGrille].
for(int vIndexLigne = 0; vIndexLigne < pLignes; vIndexLigne++)
{
// Créer un tableau de pointeurs dans vTableau[vIndexGrille][vIndexLigne].
vTableau[vIndexGrille][vIndexLigne] = new int* [pColonnes];
// Pour chaque pointeur du tableau vTableau[vIndexGrille][vIndexLigne]
for(int vIndexColonne = 0; vIndexColonne < pColonnes; vIndexColonne++)
{
// Créer un entier dans vTableau[vIndexGrille][vIndexLigne][vIndexColonne].
vTableau[vIndexGrille][vIndexLigne][vIndexColonne] = new int;
// Remplit l'entier avec son numéro d'élément
*(vTableau[vIndexGrille][vIndexLigne][vIndexColonne]) = (vIndexGrille * pLignes * pColonnes) + (vIndexLigne * pColonnes) + vIndexColonne + 1;
}
}
}
return vTableau;
}
void mDetruireTableauDynamiquePointeursTriplesEntiers(int**** pTableau, int pGrilles, int pLignes, int pColonnes)
{
// Pour chaque pointeur triple du tableau pTableau
for(int vIndexGrille = 0; vIndexGrille < pGrilles; vIndexGrille++)
{
// Pour chaque pointeur double du tableau pTableau[vIndexGrille]
for(int vIndexLigne = 0; vIndexLigne < pLignes; vIndexLigne++)
{
// Pour chaque pointeur du tableau pTableau[vIndexGrille][vIndexLigne]
for(int vIndexColonne = 0; vIndexColonne < pColonnes; vIndexColonne++)
{
// Détruire l'entier dans pTableau[vIndexGrille][vIndexLigne][vIndexColonne].
delete pTableau[vIndexGrille][vIndexLigne][vIndexColonne];
}
// Détruire le tableau de pointeurs dans pTableau[vIndexGrille][vIndexLigne].
delete [] pTableau[vIndexGrille][vIndexLigne];
}
// Détruire le tableau de pointeurs doubles dans pTableau[vIndexGrille].
delete [] pTableau[vIndexGrille];
}
// Destruction du tableau de pointeurs triples d'entiers.
delete [] pTableau;
}
void mAfficherTableau(int**** pTableau, int pGrilles, int pLignes, int pColonnes)
{
for(int vIndexGrille = 0; vIndexGrille < pGrilles; vIndexGrille++)
{
cout << "Grille : " << vIndexGrille + 1 << endl;
for(int vIndexLigne = 0; vIndexLigne < pLignes; vIndexLigne++)
{
cout << " Ligne : " << vIndexLigne + 1 << endl;
for(int vIndexColonne = 0; vIndexColonne < pColonnes; vIndexColonne++)
{
cout << " Colonne : " << vIndexColonne + 1 << " = " << *pTableau[vIndexGrille][vIndexLigne][vIndexColonne] << endl;
}
}
}
cout << endl << "Appuyez sur [Entree] pour continuer..." << endl;
getchar();
}
void mInverserColonnes(int**** pTableau, int pGrilles, int pLignes, int pColonnes)
{
for(int vIndexGrille = 0; vIndexGrille < pGrilles; vIndexGrille++)
{
for(int vIndexLigne = 0; vIndexLigne < pLignes; vIndexLigne++)
{
for(int vIndexColonne = 0; vIndexColonne < (pColonnes / 2); vIndexColonne++)
{
int* vTemporaire = pTableau[vIndexGrille][vIndexLigne][vIndexColonne];
pTableau[vIndexGrille][vIndexLigne][vIndexColonne] = pTableau[vIndexGrille][vIndexLigne][(pColonnes - 1) - vIndexColonne];
pTableau[vIndexGrille][vIndexLigne][(pColonnes - 1) - vIndexColonne] = vTemporaire;
}
}
}
}
void mInverserLignes(int**** pTableau, int pGrilles, int pLignes)
{
for(int vIndexGrille = 0; vIndexGrille < pGrilles; vIndexGrille++)
{
for(int vIndexLigne = 0; vIndexLigne < (pLignes / 2); vIndexLigne++)
{
int** vTemporaire = pTableau[vIndexGrille][vIndexLigne];
pTableau[vIndexGrille][vIndexLigne] = pTableau[vIndexGrille][(pLignes - 1) - vIndexLigne];
pTableau[vIndexGrille][(pLignes - 1) - vIndexLigne] = vTemporaire;
}
}
}
void mInverserGrilles(int**** pTableau, int pGrilles)
{
for(int vIndexGrille = 0; vIndexGrille < (pGrilles / 2); vIndexGrille++)
{
int*** vTemporaire = pTableau[vIndexGrille];
pTableau[vIndexGrille] = pTableau[(pGrilles - 1) - vIndexGrille];
pTableau[(pGrilles - 1) - vIndexGrille] = vTemporaire;
}
}
int main(int argc, char* argv[])
{
// Valeur variables
int vColonnes = 2;
int vLignes = 3;
int vGrilles = 4;
int**** vTableau = mCreerTableauDynamiquePointeursTriplesEntiers(vGrilles, vLignes, vColonnes);
cout << "Tableau a l'etat initial" << endl << endl;
// Affiche le tableau
mAfficherTableau(vTableau, vGrilles, vLignes, vColonnes);
// Le tableau est trié du plus petit au plus grand.
cout << "Inversion des Colonnes" << endl << endl;
// Intervertit les colonnes
mInverserColonnes(vTableau, vGrilles, vLignes, vColonnes);
// Affiche le résultat
mAfficherTableau(vTableau, vGrilles, vLignes, vColonnes);
cout << "Invertion des Lignes" << endl << endl;
// Intervertit les lignes
mInverserLignes(vTableau, vGrilles, vLignes);
// Affiche le résultat
mAfficherTableau(vTableau, vGrilles, vLignes, vColonnes);
cout << "Inversion des Grilles" << endl << endl;
// Intervertit les Grilles
mInverserGrilles(vTableau, vGrilles);
// Affiche le résultat
mAfficherTableau(vTableau, vGrilles, vLignes, vColonnes);
// Le tableau est trié du plus grand au plus petit.
mDetruireTableauDynamiquePointeursTriplesEntiers(vTableau, vGrilles, vLignes, vColonnes);
return 0;
}
Pointeurs sur Méthodes
modifierEn C++, il est possible de créer des pointeurs sur des méthodes. Cela est surtout utile pour lier les fonctions d'une bibliothèque de liens dynamiques ou, plus intéressant, pour appeler des méthodes que l’on ne connaît pas à l'avance mais qui auront un prototype que l’on a défini. Cela permet, par exemple, de créer une liste de tâches à accomplir suite à un évènement donné.
Où "<TypeRetour>" est le type de retour de la méthode à pointer, "<NomPointeur>" est le nom attribué au pointeur, "<TypeParametre>" et "<NomParametres>" sont respectivement les types et noms des paramètres de la méthode
// Méthode
bool MaMethode(int MonParametre)
{
return true;
}
//Pointeur correspondant à la méthode
bool (*LaMethode)(int Params);
LaMethode = MaMethode;
// Appel de la méthode via le pointeur
bool a = LaMethode(8);
Remarquez bien les parenthèses autour de "*LaMethode". Si on ne les avait pas mis comme dans cet exemple :
// Méthode
bool MaMethode(int MonParametre)
{
return true;
}
//Pointeur correspondant à la méthode
bool *LaMethode(int Params);
LaMethode = MaMethode;
// Appel de la méthode via le pointeur
bool a = LaMethode(8);
Le compilateur aurait compris "LaMethode" une méthode qui accepte un paramètre "Params" de type "int" et retourne un pointeur de type "bool";
Pointeurs sur Méthodes Encapsulées
modifierIl est possible de créer des pointeurs sur des méthodes qui sont encapsulées dans des classes à deux conditions :
- la méthode doit être publique,
- la méthode doit être statique.
//Classe
class MaClasse
{
public:
// Méthode
static bool MaMethode(int MonParametre)
{
return true;
}
}
//Pointeur correspondant à la méthode
bool (*LaMethode)(int Params) = MaClasse::MaMethode;
// Appel de la méthode via le pointeur
bool a = LaMethode(8);
Les Références
modifierDevant la difficulté qu'avaient la plupart des programmeurs de l'époque, à gérer convenablement la mémoire avec les pointeurs, le C++ a introduit la notion de référence. Une référence est un ALIAS, un autre nom, pour une variable DÉJÀ EXISTANTE. À l'inverse du pointeur, une référence n'occupe pas de place en mémoire. Le compilateur la remplace par le nom de la variable actuellement assignée dans le code après avoir déroulé les traitements mais avant de traduire ce code en binaire.
Où "<Type>" est le type de la référence "<NomRéférence>" est l'identifiant de la référence et <NomVariable> est l'identifiant de la variable que la référence doit désigner. Contrairement aux pointeurs, la référence doit impérativement être assignée dès la déclaration et il n’est pas possible de réassigner la référence à une autre variable après initialisation.
Ou plus concrètement :
//Variable à référencer
int MaVariable = 5;
//Référence correspondant à la variable;
int& LaVariable = MaVariable;
// Modification de la variable via la référence.
LaVariable = 8;
Références sur Méthodes
modifierÀ l'instar du pointeur, il est possible de référencer une méthode.
Où "<Type>" est le type de retour de la méthode à référencer, "<NomRéférence>" est l'identifiant de la référence, <TypeParam> et <NomParam> sont respectivement le type et le nom de l'image des paramètres de la méthode à référencer et <NomMéthode> est l'identifiant de la méthode que la référence doit désigner. Contrairement aux pointeurs, la référence doit impérativement être assignée dès la déclaration et il n’est pas possible de réassigner la référence à une autre méthode après initialisation.
bool MaMethode(int MonParametre)
{
return true;
}
//Référence correspondant à la méthode
bool (&LaMethode)(int Params) = MaMethode;
// Appel de la méthode via la référence.
bool a = LaMethode(8);
Le Casting :
modifierL'une des fonctionnalités les plus appréciées dans la manipulation des variables est l'opérateur de casting, ou de refonte, que sont les parenthèses "()".
Syntaxe: |
---|
(<Type>)[<Constante>|<Variable>] |
Où <Type> est le type dans lequel la valeur <Constante> ou <Variable> doit être refondue.
En fait, on peut refondre des valeurs de types différents de manière sûre si au moins l'une de ces conditions est remplie :
- La valeur source peut être transcodée dans le type de destination.(ex. : (char)'A'; est équivalent à (char)41; "41" est un const int)
- Le type de la valeur source peut être approximée ou être un sous ensemble du type de la variable de destination (numérique, ex. : (double)int ou (int)double).
- Le type de la valeur source hérite du type de la variable de destination (upcasting).
- Le type de la valeur source est parente du type de la variable de destination (downcasting).
Attention : L'utilisation de l'opérateur de refonte est à utiliser avec précautions. Le compilateur ne vous avertira pas si un dépassement de capacité a lieu ou si vous faites des downcasting (refontes descendantes) avec des types de nature différente |
Exemples :
modifierExemple de cast pour une valeur constante
// Création d'une variable "c" de type "char" non initialisée.
char c;
// cast de la valeur constante 0x43 (0x43 = 'C') en char.
c = (char)0x43;
Autre exemple pour une variable :
// Création d'une variable "c" de type "char" non initialisée.
char c;
// Création d'une variable "i" de type "int" non initialisée.
int i;
// Affectation du caractère "A" à la variable c.
c = 'A';
// cast de la valeur de la variable ('A' = 0x41 = 65) en int.
i = (int)c;
Portée du code
Portée du code
modifierEn C++, la notion de "portée du code" est très importante car elle permet de décrire ce qui se passe, dans quel cadre, et dans une certaine mesure ce qui existe et ce qui n'existe pas encore au travers de contextes d'exécutions.
Portée Globale
modifierLa portée globale fournit un espace de partage pour toute l'application.
// Début de portée "Globale"
// Déclarations Globale d'inclusion d'entête
#include <iostream> // nécessaire pour utiliser std::cout
//Déclare l'indirection de la portée nommée "std" vers la portée
//globale (décompartimentation)(voir portée nommée plus bas)
using namespace std; // nécessaire pour utiliser directement cout
//Déclaration de constante Globale
const int gMaConstanteGlobale = 1;
//Déclaration de variable Globale
int gMaVariableGlobale = 1;
//Déclaration de la méthode globale "main"
int main(int argc, char* argv[])
{
return 0;
}
// Fin de portée "Globale"
La portée Globale englobe tout le programme.
Bien qu’il soit facile de s'en servir de fourre-tout, il est déconseillé de le faire. En fait, si on le faisait, on se retrouverait avec une myriade de types et de noms de variables que l’on ne pourrait pas distinguer des types et variables que l’on crée soit même[pas clair].
C'est pour cela qu’il existe les autres portées et notamment la portée nommée.
Portée Nommée "Espace de nom"
modifierLa portée nommée (aussi appelée "Espace de nom" du fait du mot clé qui les déclare "namespace") est la portée la plus simple à la disposition du développeur. Elle permet de fournir des noms pour encapsuler des constantes, des variables des types et des méthodes ceci afin de restreindre le champ d'application de ces objets ou de fournir une architecture pour leur utilisation. Ce type de portée est surtout utilisé dans le but de diviser pour mieux régner et délivre toute sa puissance dans l'implémentation des énumérations au sein d'un IDE graphique.
// Début de portée "Globale"
// Déclarations Globale d'inclusion d'entête
#include <iostream> // nécessaire pour utiliser std::cout
//Déclare le déréférencement de la portée nommée "std" vers la portée
//globale (décompartimentation)(voir portée nommée plus bas)
using namespace std; // nécessaire pour utiliser directement cout
//Déclaration de constante Globale
const int gMaConstanteGlobale = 1;
//Déclaration de variable Globale
int gMaVariableGlobale;
namespace NVariablesProgramme
{
//Déclaration d'un nouveau type "NVInteger" basé sur le type int
typedef int NInteger;
//Déclaration de constante de portée nommée
const int nvMaConstanteDePorteeNommee = 2;
//Déclaration de variable de portée nommée
int nvMaVariableDePorteeNommee;
}
//Déclaration de la méthode globale "main"
int main(int argc, char* argv[])
{
// Déréférencement manuel de la portée NVariablesProgramme obligatoire ici
// car on a pas utilisé using avec cet espace de nom.
NVariablesProgramme::nvMaVariableDePorteeNommee = gMaConstanteGlobale;
gMaVariableGlobale = NVariablesProgramme::nvMaConstanteDePorteeNommee;
return 0;
}
// Fin de portée "Globale"
Il peut être tentant de fournir des noms identiques à des types/variables/méthodes pour des d'espaces de noms différents. Mais cela est à éviter pour trois raisons :
- Cela porte à confusion,
- Ne sachant pas comment les utilisateurs vont les utiliser, il vaut mieux éviter les homonymes (risque d'inversion de variables),
- Cela provoque des erreurs de compilations si l'utilisateur met des using sur les deux namespaces simultanément et l'obligera à taper le nom du namespace à chaque fois.
Portée Locale
modifierLa portée locale et la plus petite portée qui existe. C'est la portée disponible dans les méthodes, les boucles et les structures de tests conditionnels. Elle est généralement déterminée par les accolades. La portée locale est la portée pour laquelle les variables déclarées dans cette même portée sont accessibles. C'est pour cette raison que les portées locales doivent être clairement identifiées et identifiables. Pour cela, il est impératif de les écrire toutes explicitement et de bonne pratique de tabuler chaque niveau de portées afin que l’on puisse facilement les dissocier. La tabulation standard des éditeurs de textes des IDE du marché tourne entre 4 et 6 espaces. Dans le projet LINUX, les recommandations de tabulation sont de 8 espaces. La plupart des développeurs modernes vous maudiront si vous tabulez en dessous de 4 espaces.
#include <iostream> // nécessaire pour utiliser std::cout
using namespace std; // nécessaire pour utiliser directement cout
const int gMaConstanteGlobale = 1;
int gMaVariableGlobale;
namespace NVariablesProgramme
{
typedef int NInteger;
const int nvMaConstanteDePorteeNommee = 2;
int nvMaVariableDePorteeNommee;
}
//Déclaration de la méthode globale "main"
int main(int argc, char* argv[])
{
// Portée locale de la méthode.
int j = 2; // "int j" est créé dans cette portée
if(1 < argc)
{
// Portée locale du test
int i = 1; // "int i" existe ici !
j = i; // ici j existe car l’on se trouve dans une portée sous-jacente à celle de sa déclaration.
cout << argc << endl;
}
// ici "int i" n'existe plus car on est sorti de sa portée de définition
NVariablesProgramme::nvMaVariableDePorteeNommee = gMaConstanteGlobale;
gMaVariableGlobale = NVariablesProgramme::nvMaConstanteDePorteeNommee;
return 0;
}
Portée "STATIQUE"
modifierLa portée statique est la portée des membres statiques du programme. Elle se distingue des autres types de portées par le fait qu'une variable statique existe au chargement du programme dans la mémoire mais n'est accessible que dans la portée locale de sa déclaration.
#include <iostream> // nécessaire pour utiliser std::cout
using namespace std; // nécessaire pour utiliser directement cout
static int vInteger = 0; // Cette variable n'existe qu'en 1 seul exemplaire et est créée
// au chargement du programme. Elle est accessible partout car
// elle est déclarée dans la portée globale.
int TestStatic()
{
static int vInteger2 = 0; // Cette variable n'existe qu'en 1 seul exemplaire et est créée
// au chargement du programme. Elle n'est accessible qu’à l'intérieur
// de la méthode car elle n'a pas été déclarée dans la portée globale.
// Par contre, comme elle est statique et qu'elle a été créée au
// chargement du programme, elle conserve sa valeur même en dehors de
// la méthode.
// Cette instruction n'est par conséquent exécutée qu'une seule fois.
return vInteger2++; // Comme vous pourrez le constater, vInteger2 est incrémenté à chaque
// appel, ce qui prouve que la déclaration n'est exécutée qu'une seule
// fois et que la variable garde sa valeur en dehors de la méthode
// contrairement à une variable non statique.
}
//Déclaration de la méthode globale "main"
int main(int argc, char* argv[])
{
int vControle = 0;
while (vControle < 10)
{
vControle = TestStatic();
cout << vControle << endl;
}
return 0;
}
Énumérations
Énumérations
modifierEn C++ les énumérations servent à créer des listes de constantes entières. Cela permet de regrouper des constantes d'un même contexte dans un même type. Pour des raisons de clarté il est préférable de définir chaque énumération dans un fichier d'en-tête propre. Pour des raisons de conception objet il est nécessaire de typer l'énumération à l'aide du mot clé "typedef". Enfin pour des raisons d'interférences il est conseillé de cloisonner l'énumération à un namespace.
namespace <NomNamespaceEnum>
{
typedef enum
{
<NomConstante0> [[= <ValeurConstante0>] [,
[...]
<NomConstanteN-1> [= <ValeurConstanteN-1>]]]
}<NomTypeEnum>;
}
Où <NomNamespaceEnum> est le nom du namespace qui encapsulera la portée des constantes énumérées, <NomConstante0> et <NomConstanteN-1> sont les identifiants des constantes, <ValeurConstante0> et <ValeurConstanteN-1> sont les valeurs respectivement de <NomConstante0> et <NomConstanteN-1> et <NomTypeEnum> est le type de l'énumération.
Il est à noter que les valeurs des constantes sont facultatives. Sans aucune précision, la valeur d'une constante est la valeur de la constante précédente augmenté de 1, la première valeur étant par défaut "0". Il faut aussi savoir qu’il ne peut y avoir qu'un identifiant pour une valeur donnée.
namespace NECouleursCartes
{
typedef enum
{
Pique,
Coeur,
Trefle,
Carreau
}ECouleursCartes;
}
int main(int argc, char* argv[])
{
NECouleursCartes::ECouleursCartes vMaCouleurCarte;
vMaCouleurCarte = NECouleursCartes::ECouleursCartes::Coeur;
//ou plus simplement :
vMaCouleurCarte = NECouleursCartes::Coeur;
return 0;
}
Structures, unions et champs de bits
Structures, unions et champs de bits
modifierLes structures existent en C++ uniquement pour la compatibilité ascendante et ne servent plus qu’à quelques cas spécifique comme les unions, les champs de bits et quelques applications matérielles mais même dans ces circonstances, il est possible de trouver des alternatives plus "Objet".
Le but principal de la structure était de regrouper et de mieux gérer les données qui avaient des affinités communes dans un langage qui ne disposait pas d'autres moyens (hormis l'énumération) pour organiser les données d'un programme.
Usuellement, les structures sont utilisées conjointement avec le mot clé "typedef". Cette écriture permet d’utiliser le casting et l'auto-référencement en est facilité.
En C++ la structure a été remplacée avantageusement par la classe mais nous verrons cela un peu plus tard.
Il existe trois catégories de structures :
- Les structures simples,
- Les unions,
- Les champs de bits.
Les Structures Simples
modifierEn C++ la structure simple a évolué pour suivre un modèle plus "orienté objet", désormais, il est possible et conseillé d'inclure les méthodes qui manipulent les données de la structure dans la structure elle-même. Chose qui était très lourd à faire en C. On pouvait bien créer un pointeur sur méthode mais cela n'était pas aussi trivial que ce que permet le C++.
typedef struct [<NomStructure>][: <StructureParente>]
{
[[<Visibilite>:] <TypeChamp1> <NomChamp1>;]
[...]
[[<Visibilite>:] <TypeChampN> <NomChampN>;]
[[<Visibilite>:] <TypeMethod1> <NomMethod1>(<ParametresMethod1>);]
[...]
[[<Visibilite>:] <TypeMethodN> <NomMethodN>(<ParametresMethodN>);]
}<NomTypeStructure>;
Où <NomStructure> (optionnel) est le nom interne de la structure, <StructureParente> (optionel) est le nom de la structure parente, <Visibilite> (optionel) déclare la visibilité des membres (parmi private, protected et public, par défaut : public), <TypeChamp1> est le type du premier champ ayant pour nom <NomChamp1>, <TypeChampN> est le type du dernier champ ayant pour nom <NomChampN>, <TypeMethod1> est le type de la première méthode <NomMethod1> ayant pour paramétrés <ParametresMethod1>, <TypeMethodN> est le type de la dernières méthode <NomMethodN> ayant pour paramètres <ParametresMethodN> et <NomTypeStructure> est le type de la structure
Dans "TestStruct.h"
#ifndef TESTSTRUCT_H
#define TESTSTRUCT_H
typedef struct TestStruct; // Nécessaire uniquement pour l'auto-pointeur.
typedef struct TestStruct // Nom de structure local nécessaire uniquement pour les méthodes non inlinées
{
private:
TestStruct* aSuivant; // auto-pointeur
int aValeurTest; // Valeur de test
public:
TestStruct(); // constructeur par défaut
TestStruct(TestStruct* pSuivant); // Constructeur paramétré
TestStruct(TestStruct& pCopie); // Constructeur par copie
virtual ~TestStruct(); // Destructeur
TestStruct* mSuivant(); // Accesseur en lecture pour aSuivant
int mValeurTest(); // Accesseur en lecture pour aValeurTest
void mValeurTest(int pNouvelleValeur); // Accesseur en écriture pour aValeurTest
}TestStruct; // Nom du type
#endif // TESTSTRUCT_H
Dans "TestStruct.cpp"
#include "TestStruct.h"
TestStruct::TestStruct():aSuivant(0),aValeurTest(0)
{
//ctor
}
TestStruct::TestStruct(TestStruct* pSuivant):aSuivant(pSuivant),aValeurTest(0)
{
//ctor
}
TestStruct::TestStruct(TestStruct& pCopie):aSuivant(pCopie.mSuivant()),aValeurTest(pCopie.mValeurTest())
{
//ctor
}
TestStruct::~TestStruct()
{
//dtor
}
TestStruct* TestStruct::mSuivant()
{
return this->aSuivant; // retour de aSuivant
}
int TestStruct::mValeurTest()
{
return this->aValeurTest; // retour de la valeur enregistrée
}
void TestStruct::mValeurTest(int pNouvelleValeur)
{
if(this->aValeurTest != pNouvelleValeur) // Si la nouvelle valeur est bien différente de celle déjà enregistrée
{
this->aValeurTest = pNouvelleValeur; // Affectation de la nouvelle valeur.
}
}
Dans "main.cpp"
#include "TestStruct.h"
int main(int argc, char* argv[])
{
TestStruct* vA = new TestStruct(); // Construction d'une structure en tas
vA->mValeurTest(9); // Attribution de la valeur 9 dans la variable test de vA
TestStruct vB(vA); // Construction d'une structure en pile et assignation de vA comme suivant
vB.mValeurTest(vB.aSuivant()->mValeurTest() - 1); // Affectation de 8 (= (9 de vA) - 1) dans la variable test de vB
TestStruct vC(vB); // clonage de vB dans vC
cout << "vA->" << vA << ":Suivant=" << vA->mSuivant() << ":ValeurTest=" << vA->mValeurTest();
cout << "vB." << &vB << ":Suivant=" << vB->mSuivant() << ":ValeurTest=" << vB->mValeurTest();
cout << "vC." << &vC << ":Suivant=" << vC->mSuivant() << ":ValeurTest=" << vC->mValeurTest();
delete vA; // Destruction de vA
}
Alternative à la structure
modifierEn fait, je l'ai déjà dit, la structure a été avantageusement remplacée par la classe, déclarée par le mot clé class.
Les Unions
modifierLes unions sont une forme de typage faiblement typé. Les unions ont été conservées du C pour compatibilité et ne correspondent pas à la philosophie d'un langage orienté objet.
Les unions permettent de créer des espaces mémoire où l’on peut interpréter une même donnée de différentes manières ou de diviser une même donnée en sous ensembles. En fait, une union déclare la disposition de tous ses membres en partant de la même adresse contrairement à la structure qui dispose ses membres les uns à la suite des autres.
Les unions ne sont pas à proprement parler des structures. Utilisées telles quelles, elles ne permettent pas l'héritage mais il existe un moyen de contourner cet obstacle. Il est possible de créer des unions non nommées, cela permet de déléguer la manipulation des membres directement à la portée supérieure. Sachant cela il suffit d'encapsuler une union dans une structure typée pour rendre l'héritage possible. Cela implique cependant qu’il sera impossible de créer un constructeur ou des méthodes non inlinées au niveau de l'union. Pour des raisons de lecture il sera donc préférable de déléguer la gestion des méthodes de l'union à la portée supérieure (soit la structure dans notre cas). Cela exige aussi que tous les attributs de l'union soient déclarés comme public.
Syntaxe:
typedef struct [<NomStructure>]
{
private:
union
{
public:
[<TypeChamp1> <NomChamp1>;]
[...]
[<TypeChampN> <NomChampN>;]
};
[...]
}<NomTypeStructure>;
Où <NomStructure>(optionnel) est le nom interne de la structure d'accueil, <TypeChamp1> et <TypeChampN> sont les types des champs respectivement <NomChamp1> et <TypeChampN>, <NomTypeStructure> est le nom du type de la structure.
typedef struct TestUnion
{
private:
union // Ici l'union permet de lire le long ou d’utiliser sa valeur comme adresse d'une chaine.
{ // Ce genre d'union n’est pas très intéressante car il suffit de caster le long en char*
// pour obtenir le même résultat et l'avantage sera la clarté du casting comparé à l'opacité
// de l'union.
public:
long Valeur;
char* Chaine;
};
//....
}TestUnion;
typedef struct TestStruct
{
short PoidFort;
short PoidFaible;
}TestStruct;
typedef struct TestUnion
{
private:
union // Ici l'union permet de lire l'int ou de lire que sa partie haute (TestUnion.Decoupe.PoidFort) ou
{ // sa partie basse(TestUnion.Decoupe.PoidFaible).
// Ce genre d'union est plus intéressante mais il est possible de faire mieux avec des classes
// pour obtenir le même résultat et l'avantage sera la clarté des méthodes de classes encapsulant
// les castings comparé à l'opacité de l'union.
public:
int Valeur;
TestStruct Decoupe;
};
//....
}TestUnion;
Alternative à l'union
modifierComme je l'ai dit plus haut, une classe et ses méthodes qui encapsulent les castings sera plus claire et propre que de manipuler la même variable avec plusieurs noms différents.
Champs de bits
modifierLes champs de bits (ou "Drapeaux" de l'anglais "Flags"), qui ont leur principale application en industrie, sont des structures qui ont la possibilité de regrouper au plus juste, dans un nombre d'octets moindre, plusieurs valeurs. Cela vient directement du monde de l'électronique. Il existe nombres d'exemples de ce fonctionnement dans l’industrie.
Cela dit, les champs de bits sont très fortement déconseillés car aucun standard C, C++ ne fixe les règles d'implémentations.
La norme ISO C++ 14 (ISO/IEC JTC1 SC22 WG21 N 3690) énonce ceci :
"Allocation of bit-fields within a class object is implementation-defined. Alignment of bit-fields is implementation-defined."
Cela signifie qu'aucune garantie n'est fournie en ce qui concerne l'alignement mémoire. certains compilateurs peuvent générer un exécutable demandant l'accès à deux cases mémoire simultanément. Si cela ne pose que des problèmes de performances sur Intel, d'autres processeurs peuvent générer une erreur d'accès non alignée à la mémoire (Cas des processeur Spark par exemple).
Cela signifie également que chaque compilateur peut implémenter le champ de bit à sa manière dans une classe (et donc par extension dans une structure). Deux compilateurs peuvent alors générer deux exécutables aux comportement différents à partir du même code source.
Cela est vrai surtout en cas de cast ou d'union permettant de d'accéder à un champ de bit comme un seul entier ou champ par champ. Dans ce cas, rien ne garantit l'ordre dans lequel seront accédés les champs de bit.
La tentation est trop forte pour ne pas faire une petite parenthèse sur le standard C (ISO/IEC 9899:201x) qui est encore plus explicitement permissif :
"An implementation may allocate any addressable storage unit large enough to hold a bit- field. If enough space remains, a bit-field that immediately follows another bit-field in a structure shall be packed into adjacent bits of the same unit. If insufficient space remains, whether a bit-field that does not fit is put into the next unit or overlaps adjacent units is implementation-defined. The order of allocation of bit-fields within a unit (high-order to low-order or low-order to high-order) is implementation-defined. The alignment of the addressable storage unit is unspecified."
Cela veut dire la même chose mais sans sous entendus.
Bref, voici un champ de bit :
typedef struct [<NomChampsBits>]
{
<TypeChamp> [<NomChamp1>] : <NombresBits1>;
[...]
[<TypeChamp> [<NomChampN>] : <NombresBitsN>;]
}<NomTypeChampsBits>;
Où <NomChampsBits> est le nom interne des champs de bits(facultatif), <TypeChamp> est le type (ou taille totale) du champs de bit, [<NomChamp1>] et [<NomChampN>] sont les nom des sous-champs(facultatif), <NombresBits1> et <NombresBitsN> sont les taille des sous-champs, respectivement, [<NomChamp1>] et [<NomChampN>], <NomTypeChampsBits> est le nom du type donné au champs de bits
typedef struct
{
unsigned short Jours : 5; // 0-31 soit de 1 à 31
unsigned short Mois : 4; // 0-15 soit de 1 à 12
unsigned short Annee : 7; // 0-127 soit de 0 à 99, (sans les siècles)
}Date;
Alternative aux champs de bits
modifierÀ l'instar de l'union et de la structure, une classe et ses méthodes qui encapsulent les castings seront plus claires et propres que de manipuler plusieurs sous-noms différents d'une même variable.
Objet
Programmation orientée objet
modifierHistorique
modifierPréhistoire : la programmation monolithique séquentielle
modifierAu temps des prémices de l'informatique, à l'époque des dinosaures électromécaniques qui faisaient le volume d'un bâtiment de quatre étages, où les bandes magnétiques n'existaient pas encore, où la programmation se faisait en connectant des câbles et des trous ou, pour les plus sophistiqués, par cartes perforées, où le moindre calcul scientifique prenait plus de six mois à la main (et à la règle à calcul) et seulement une semaine une fois informatisé, la programmation était monolithique. C'est-à-dire que les développeurs de l'époque faisaient des programmes (la plupart du temps en binaire) de manière séquentielle et s'adressaient directement au système physique de traitement qui sera nommé plus tard et après miniaturisation : processeur. De fait le programme en question était le seul programme que la machine connaissait. Il n'y avait donc pas de traitements parallèles ni de problèmes d'accès concurrentiel aux ressources. Le programme était purement séquentiel et on recréait un nouveau programme pour chaque nouvelle application (qui à l'époque n'étaient que des calculs). Les programmes étaient courts (de quelques centaines à quelques milliers d'ordres machines simples). Le terme de "BUG" (insecte en anglais) est introduit à cause des cafards qui venaient se réchauffer un peu trop près de l'ancêtre des transistors, des lampes à tubes électrostatiques, et finissaient par griller entre les pâtes du composant, ce qui entrainait des pannes longues et coûteuses à réparer.
Antiquité : la programmation procédurale
modifierÀ cette époque le transistor en silicium a déjà été intégré aux ordinateurs qui n'occupent plus qu'une salle de réunion entière. Les bandes magnétiques sont devenues monnaie courante et les programmeurs développent maintenant sur un clavier et un écran. Les premiers compilateurs pour langages extensibles lui permettent grâce au concept de procédures et de fonctions d'enrichir le langage de base dont il dispose en composant des suites d'instructions qu’il peut réutiliser. On développe alors les programmes de manière fonctionnelle afin de pouvoir décomposer un traitement en sous-traitements de plus en plus simples.
Moyen Âge : La programmation orientée donnée
modifierÀ ce moment le processeur vient de voir le jour. Les ordinateurs occupent le volume d'une armoire. Les premiers systèmes d'exploitations mono-tâches voient le jour. Le programmeur définit toutes les variables dont il aura besoin et les regroupe par fonctionnalités dans des fichiers distincts avec les procédures et fonctions qui les traitent.
Renaissance : La multiprogrammation
modifierÀ ce stade les processeurs/micro-contrôleurs sont courants. Les ordinateurs ont la taille de valises. Les premiers systèmes d'exploitation en ligne de commandes permettent une utilisation facilitée de l'ordinateur qui intègre désormais un disque dur et un lecteur de disquettes. Les programmeurs de l'époque commencent à utiliser le PIC (Programmable Interrupt Controller) au maximum de ses ressources. Les programmes résidents voient le jour. Le programme ne travaille plus seul, il doit cohabiter avec les autres programmes résidents dans la machine. Les concepts de processus et de threads voient le jour et les problèmes de concurrences d'accès aux ressources aussi. Les premiers réseaux d'entreprises voient le jour.
Temps Modernes : La programmation orienté objet
modifierÀ l'époque, les industries du logiciel se heurtent de plus en plus à la difficulté de développer des projet de taille toujours croissante. Pire : plus il y a de monde sur le projet, moins le projet devient gérable avec les outils de l'époque, et ce, même pour des projets simples. La programmation orienté objet est un concept récupéré de l'ingénierie du bâtiment qui permet de décrire les différents aspects du logiciel de manière formelle et ainsi maitriser les couts, les délais, les risques et assurer la qualité du logiciel développé.
Pourquoi la programmation orienté objet
modifierEn 1995, le Standish Group édita un rapport édifiant intitulé "CHAOS" sur l'état des échecs de projets informatiques dans plus de 360 sociétés américaines. Ce rapport fait référence et fait état que les entreprises américaines et organismes gouvernementaux perdent des centaines de milliards de dollars par ans, de ne pas appliquer les mêmes règles de conception et de gestion du risque que dans les domaines de génie civil. En effet, ce rapport dénonce une renonciation des cadres techniques à appliquer les mêmes méthodes que leurs homologues du bâtiment, pour la plupart du temps pour des raisons politiques de rond de jambes et, pour les autre rares cas, d'incompétences de capture des besoins ou d'omission de diagnostic techniques.
- 16% de projets conformes aux prévisions initiales en temps et couts (mais souvent diminués en fonctionnalités),
- 53% de projets dépassements en coûts et délais d'un facteur 2 à 3,
- 31% de projets abandonnés.
Depuis, le Standish Group édite des mises à jours annuelle de ces chiffres, et malheureusement les chiffres restent très alarmants et surtout très réels.
En effet le logiciel étant un produit qui n'a de consistance qu'au travers de l'ordinateur qui l'exécute, les ingénieurs de l'époque n'arrivent pas à concevoir qu’il est aussi difficile d'abattre un mur porteur dans un immeuble que de redévelopper un ensemble de méthodes imbriquées les unes aux autres dans du code métier.
Qu'est-ce que la programmation orienté objet
modifierLe paradigme de la programmation orientée-objet repose entièrement sur la "classe" à laquelle on délègue la gestion des données qui lui sont encapsulées et sont accessibles via l'interface représenté par ses méthodes appelées aussi accesseurs/mutateurs/propriétés. Les concepts d'abstraction, de généralisation, d'héritage, et de polymorphisme sont aussi cruciaux.
De nos jours : La programmation évènementielle
modifierGrâce aux avancées technologiques en graphisme, l'entreprise Xerox développe une interface graphique pour ses imprimantes qui sera ensuite reprise par Steeve Jobs pour le compte du nouvel ordinateur d'Apple le Maccintosh. Bill Gates, voyant une menace en ce concurrent pour son MS DOS, débauche des ingénieurs d'Apple et crée son Windows 3.1x. La technologie basée sur les Interruptions du PIC permet de recevoir, de propager et de traiter en temps réel les évènements utilisateur. Les jeux informatiques voient le jour, les interfaces graphiques facilitent grandement l'accès à l'ordinateur qui se démocratise. Un enfant est capable d’utiliser un ordinateur. Internet voit le jour et se répand. Les "Design patterns" du GoF et du GRASP rendent possibles des réutilisations de développements en exposant des problèmes récurrents de programmation et leurs solutions-types.
Demain : La programmation orienté agent
modifierLa programmation orienté agent est une évolution de la programmation orienté objet. Au lieux d’utiliser des objets classiques uniquement réactifs, on utilise des objets composites comme les automates et/ou les réseaux neuronaux afin de rendre les objets, du moins en partie, pro-actifs en fonction de leurs états internes, de l'état de leur environnement et des taches qu’ils ont à réaliser. Le but du jeu est de rendre les agents autonomes et indépendants de l'interface de leurs voisins. Ainsi cela permet une plus grande souplesse de dialogue entre les agents. Cela pose toutefois l'éternel problème de la construction des moteurs de tels agents. En effet, plus un comportement logiciel est sophistiqué, plus le développement est difficile, couteux et instable. De nombreux frameworks voient le jour et comme tous les frameworks standards actuels ils sont incompatibles entre eux, un comble pour une architecture qui aspire à une compatibilité massive. Peut-être qu'un jour les interfaces s'uniformiseront mais, en attendant, vu la complexité de la mise en œuvre de ces agents, il me semble (et cela n'engage que moi) prématuré de les utiliser dans des solutions industrielles de grande envergure hormis éventuellement pour le domaine de la robotique et des domaines associés qui font encore partie de la recherche et qui ne sont pas encore entrés dans le grand public.
L'Objet
modifierDans le domaine de l'informatique, un objet est quelque-chose de concret au sens mathématique du terme que l’on peut manipuler ou dont on peut récupérer des informations.
- Un cercle de rayon 8 cm,
- Un triangle équilatéral de 3 cm de côté ou encore
- Un cube de 5 cm d'arêtes.
Les moules ou modèles qui ont permis d'obtenir ces figures (respectivement)
- (pi*(R*R)),
- (a*a)=(b*b)+(c*c)-(2*B*C*Cos(A)) où A=B=C=60°, et
- a*a*a
correspondent en C++ à ce que l’on appelle des Classes.
En développement on parle plus souvent d'instance de classe pour désigner la représentation informatisée d'un objet.
Pour faire une analogie avec le moulage, on pourrait dire que l'instance correspond à la figurine de plâtre peinte et vernie, et que la classe correspond au moule en caoutchouc qui lui a donné forme.
Classe
La classe
modifierEn programmation orientée objet, tout est basé sur le concept de la classe. La classe est une entité autonome capable de maintenir, une fois instanciée (définie), la cohérence des données qu'elle est chargée d'entretenir. (nous mettrons en évidence la syntaxe dans la partie implémentation)
Les visibilités : niveaux de visibilité dans une classe
modifierIl existe trois niveaux de visibilité dans une classe :
- Privé (Niveau par défaut) : Permet de masquer complètement les données et méthodes à des classes tierces et même aux classes dérivées. On parle d'encapsulation (peut être utilisé par les méthodes mais est surtout destiné aux variables d'une classe).
- Protégé : permet de partager des données uniquement aux classes dérivées. (Normalement uniquement utilisé pour les méthodes)
- Public : permet de partager les données avec toutes les classes tierces. (Normalement uniquement utilisé pour les méthodes)
Les attributs : variable de classes
modifierLa classe est une entité qui peut être composée de variables internes que l’on nomme attributs membres.
La théorie veut que les attributs d'une classe ne soient accessibles directement qu’à cette classe. Bien qu'en C++ il soit possible de définir des attributs membres de visibilité publique ou protégée, il est normalement indispensable de rendre les attributs membres privés (je n'ai jamais eu à rendre publiques ou même protégés des attributs).
En effet le paradigme-objet étant de rendre la classe responsable de la gestion de ses attributs pour assurer la gestion correcte et la cohésion des données, il serait mal vu qu'une autre classe accède malencontreusement à ces attributs et vienne bouleverser l'organisation que la classe permet de maintenir entre eux. Quand une classe privatise un attribut on dit qu'elle l'encapsule.
Les méthodes : méthodes de classes
modifierNous avons déjà vu les méthodes auparavant.
Les méthodes déterminent le comportement qu'une classe est capable de réaliser. La classe possède au moins trois méthodes spéciales :
- Un constructeur sans paramètres appelé aussi constructeur par défaut ou, selon le cas, un constructeur paramétré qui oblige à fournir des paramètres pour créer la classe.
- Un constructeur par copie qui prend en paramètre une référence sur la même classe (dans la majorité des cas elle est fournie par défaut par le compilateur mais il peut être fréquemment nécessaire de la définir manuellement).
- Un destructeur qui est chargé d'appliquer un traitement pour éventuellement nettoyer la mémoire. Bien qu’il soit souvent vide, l'implémenter explicitement et systématiquement permet de faire fonctionner le mécanisme d'héritage polymorphe.
Les méthodes donnant accès aux attributs sont appelés suivant le sens d'accès :
- Des accesseurs pour les méthodes accédant en lecture seule.
- Des mutateurs pour les méthodes accédant en écriture seule.
- Des propriétés pour les méthodes accédant en écriture et en lecture.
La théorie veux que tout attribut qui aurait besoin d’être transmis ou modifié, le soit par l'une de ces méthodes d'accès. (Je n'ai jamais eu à faire autrement.) Les méthodes définissent aussi l'interface d'une classe.
Généralisation de classes
modifierEn programmation orientée objet, on peut structurer une hiérarchie de classes en arborescence. Les classes du sommet de l'arbre sont les classes les plus abstraites et générales. Les classes les plus profondes sont les plus concrètes et les plus spécialisés.
Pour illustrer ces propos supposons les figures suivantes :
- Carré
- Rectangle
- Parallélogramme
- Losange
- Quadrilatère
- Cercle
- Ovale
Imaginons maintenant que nous devions réaliser des classes basées sur ces figures. Supposons que chaque classe doit être capable de dessiner. Supposons aussi que nous devions gérer chacune de ces classes avec le même (et unique) pointeur et appeler la même méthode pour le dessin de chaque classe.
Dans les figures imposées on peut voir que "Carré" est un cas particulier de (ou "une sorte de") "Rectangle" qui est lui-même un cas particulier du "Parallélogramme" qui est lui-même un "Quadrilatère". Quant à "Losange" c’est un "Quadrilatère".
On peut voir aussi que "Cercle" est un cas particulier de "Ovale".
Cependant rien ne lie Ovale et Quadrilatère.
Nous allons donc devoir généraliser Ovale et Quadrilatère en "Figure"
Ainsi Quadrilatère et Ovale sont des Figures, ainsi un pointeur sur Figure est capable de manipuler n’importe quelle classe sous-jacente, puisque, après tout, un carré est une figure tout comme un cercle.
Traitons maintenant le problème de la méthode unique. En fait depuis que l’on a créé la classe Figure ce n'est plus un problème.
En effet il suffit de définir la méthode "Dessine()" dans la classe Figure pour que toutes les autres classes en héritent.
Abstraction de classes
modifierLors de généralisations successives il est courant de se rendre compte qu'une classe est trop "abstraite" pour avoir suffisamment de données exploitables pour pouvoir en générer une instance. C'est le cas pour notre classe "Figure". En effet elle est trop générique et rien d'intéressant ne peut en sortir, cependant elle fournit une interface que toutes les autres classes devront reproduire fidèlement. La méthode "Dessiner" ne représente pas grand chose pour une Figure. Nous ne pouvons pas décrire de comportement pour cette méthode dans cette classe. Nous ne pouvons donc pas instancier la classe car cela n'aurait pas de sens. Nous allons donc devoir rendre cette classe abstraite. Cela signifie que la classe n’est pas instanciable en l'état mais qu'une classe héritée non abstraite peut être interprétée comme cette classe (via l'utilisations de pointeurs ou de références).
Héritage de classes
modifierL'héritage est la faculté qu’à une classe de pouvoir transmettre ses attributs et méthodes à ses classes dérivées et, sous certaines conditions, permettre à ses classes dérivées de redéfinir ses méthodes pour pouvoir les améliorer et les spécialiser.
Tout d’abord il faut savoir qu'en C++ une classe dérivée reçoit toujours une copie de l'intégralité des attributs et des méthodes de sa classe ancêtre. En C++, bien que l’on puisse choisir la façon dont sont copiés les membres de la classe ancêtre, la théorie-objet veux que l’on hérite toujours des classes ancêtres de manière publique afin que tous les membres publics de la classe ancêtre soient aussi disponibles de manière publique dans la classe dérivée. L'héritage permet de ne pas réécrire éternellement les mêmes codes, de réutiliser les objets, de pouvoir les spécialiser et mettre en œuvre le polymorphisme de classe.
Polymorphisme de classes
modifierLe polymorphisme en informatique se traduit par la capacité qu'a un pointeur de classe ancêtre présentant une interface donnée, à appeler la méthode de l'instance de la classe dérivée correspondant à la méthode de la classe ancêtre. En C++ la mise en œuvre du polymorphisme se fait à l'aide du mot clé "virtual". Dans la pratique et pour reprendre l'exemple vu précédemment :
Si l’on créé un pointeur sur Figure que l’on lui assigne l'adresse de l'instance d'un carré et que l’on demande au pointeur-figure de se dessiner alors le pointeur va appeler la méthode virtuelle et par le biais de l'héritage virtuel appeler la méthode implémentée dans la classe Carré.
Implémentation
modifierLes classes sont donc la représentation logique du concept d'objet. Voici en C++ comment implémenter ces classes conformément à la théorie de l'objet.
class <NomNouvelleClasse> : [public: <ClasseAncêtre>[, <AutreClasseAncêtre>][, ...]]
{
[ [<Visibilitée>:] <TypeAttribut1> <NomAttribut1>;]
[<Visibilitée>:] <TypeAttributN> <NomAttributN>;] ]
[<Visibilitée>:] <TypeMethode1> <NomMethode1>(<TypeParamettre> <NomParamettre>[ = <ValeurParDefaut>][, ...]);
[ [<Visibilitée>:] <TypeMethodeN> <NomMethodeN>(<TypeParamettre> <NomParamettre>[ = <ValeurParDefaut>][, ...]); ]
};
Où <NomNouvelleClasse> est le nom de la classe, <ClasseAncêtre> est une classe ancêtre tout comme <AutreClasseAncêtre> et sont facultatives si la classe n'a pas à avoir d'ancêtre, <TypeAttribut1> et <TypeAttributN> sont les types des attributs, <NomAttribut1> et <NomAttributN> sont les noms des attributs, les attributs sont facultatifs, <TypeParamettre> et <NomParamettre> sont les paramètres des méthodes de la classe, <TypeMethode1> et <TypeMethodeN> sont les types de retours des méthodes de la classe, <NomMethode1> et <NomMethodeN> sont les noms des méthodes de la classe, , <Visibilitée> peut prendre trois valeurs: public, protected ou private (par défaut). Un membre déclaré public peut être manipulé par n’importe quelle classe, un membre protected ne peut être manipulé que par la classe et ses dérivées tandis qu'un membre private ne peut être manipulé que par les méthodes de la classe.
Les méthodes sont soit définies directement dans la déclaration de classe (auquel cas ce sont des Macros), soit définies en dehors de la déclaration dans un fichier source séparé de la manière suivante :
<TypeRetour> [<NomClasse>::]<NomMethode>([<TypeParametre> <NomParametre>[,<...>]])
{
[<Instructions>;]
}
Voici un exemple de la classe la plus simple à réaliser. Il faut dire aussi qu'elle ne fait strictement rien.
dans le ".h"
#ifndef FAINEANTE_H
#define FAINEANTE_H
class Faineante;
{
public:
// Constructeur par défaut
Faineante();
// Constructeur par copie
Faineante(Faineante& pCopie);
// Destructeur
virtual ~Faineante(); // "virtual" active le polymorphisme
};
#endif //FAINEANTE_H
Dans le ".cpp"
Faineante::Faineante()
{
}
Faineante::Faineante(Faineante& pCopie)
{
}
Faineante::~Faineante()
{
}
Exemples:
modifierMaintenant reprenons nos exemples de figures de tout à l’heure.
dans "Carre.h"
#ifndef Carre_H
#define Carre_H
#include "Rectangle.h"
// Un carré est un rectangle dont toutes les arrêtes sont égales.
class Carre : public Rectangle
{
public:
// Constructeur paramétré
Carre(double pArrete);
// Destructeur
virtual ~Carre();
virtual void mDessine();
};
dans "Carre.cpp"
#include "Carre.h"
// Constructeur paramétré
// Appelle le constructeur de la classe ancêtre en le paramétrant correctement
Carre::Carre(double pArrete):Rectangle(pArrete, pArrete)
{
}
// Destructeur
Carre::~Carre()
{
}
void Carre::mDessine()
{
//Dessine ici le Carre (vrais code de dessin non pertinent car trop volumineux pour le gain obtenu sur l’intérêt de la leçon).
cout << "Dessine Carre :\n" << endl;
Rectangle::mDessine();
}
dans "Rectangle.h"
#ifndef Rectangle_H
#define Rectangle_H
#include "Parallelogramme.h"
//Un Rectangle est un Prarallelogramme avec tous ses angles droits.
class Rectangle : public Parallelogramme
{
public:
Rectangle(double pArreteAB, double pArreteBC);
virtual ~Rectangle();
virtual void mDessine();
};
dans "Rectangle.cpp"
#include "Rectangle.h"
// Appelle le constructeur de la classe ancêtre en le paramétrant correctement
Rectangle::Rectangle(double pArreteAB, double pArreteBC):Parallelogramme(pArreteAB, pArreteBC, 90.0)
{
}
// Destructeur
Rectangle::~Rectangle()
{
}
void Rectangle::mDessine()
{
//Dessine ici le Rectangle (vrais code de dessin non pertinent car trop volumineux pour le gain obtenu sur l’intérêt de la leçon).
cout << "Dessine Rectangle :\n" << endl;
Parallelogramme::mDessine();
}
dans "Parallelogramme.h"
#ifndef Parallelogramme_H
#define Parallelogramme_H
#include "Quadrilatere.h"
// Un Parallélogramme est un Quadrilatère dont les cotés parallèles sont égaux.
class Parallelogramme: public Quadrilatere
{
private:
double& mCorrectionAngle(double& pAngle);
protected:
virtual void mAngleA(double& pAngle);
virtual void mAngleB(double& pAngle);
virtual void mAngleC(double& pAngle);
virtual void mAngleD(double& pAngle);
public:
// Constructeur parametré
Parallelogramme(double pArreteAB, double pArreteBC, double pAngleA);
// Destructeur
virtual ~Parallelogramme();
// Méthode de dessin des parallélogrammes.
virtual void mDessine();
};
dans "Parallelogramme.cpp"
#include <iostream>
#include "Parallelogramme.h"
using namespace std;
double& mCorrectionAngle(double& pAngle)
{
// Corriger les dépassements
double vAngleMaximal = 180.0;
pAngle %= vAngleMaximal;
// Si l'angle est
if(pAngle < 0)
{
// Corriger le signe.
pAngle *= -1;
}
return pAngle;
}
virtual void mAngleA(double& pAngle)
{
Quadrilatere::mAngleA(pAngle);
}
virtual void mAngleB(double& pAngle)
{
Quadrilatere::mAngleB(pAngle);
}
virtual void mAngleC(double& pAngle)
{
Quadrilatere::mAngleC(pAngle);
}
virtual void mAngleD(double& pAngle)
{
Quadrilatere::mAngleD(pAngle);
}
// Constructeur paramétré
Parallelogramme::Parallelogramme(double pArreteAB, double pArreteBC, double pAngleA):Quadrilatère(pArreteAB, pArreteBC, pArreteAB, pArreteBC, 0.0, 0.0, 0.0, 0.0)
{
// Dans un Parallélogramme les arrêtes :
// AB = CD et BC = AD.
// Les angles :
// A = C, B = D et A + B + C + D = 360°.
// Ici nous passons par les méthodes de Parallélogramme pour garantir que les angles seront inférieur ou égal à 180.
double vAngleMax = 180.0;
this->mAngleA(pAngleA);
this->mAngleB(vAngleMax - this->mCorrectionAngle(pAngleA));
this->mAngleC(pAngleA);
this->mAngleD(vAngleMax - this->mCorrectionAngle(pAngleA));
}
// Destructeur
virtual Parallelogramme::~Parallelogramme()
{
}
// Méthode de dessin des parallélogrammes.
virtual void Parallelogramme::mDessine()
{
//Dessine ici le Parallelogramme (vrais code de dessin non pertinent car trop volumineux pour le gain obtenu sur l’intérêt de la leçon).
cout << "Dessine Parallelogramme :\n" << endl;
Quadrilatere::mDessine();
}
dans "Losange.h"
#ifndef Losange_H
#define Losange_H
#include "Quadrilatere.h"
class Losange : public Quadrilatere
{
private:
double& mCorrectionAngle(double& pAngle);
protected:
virtual void mAngleA(double& pAngle);
virtual void mAngleB(double& pAngle);
virtual void mAngleC(double& pAngle);
virtual void mAngleD(double& pAngle);
public:
Losange(double pArreteAB, double pArreteBC, double pAngleA);
virtual ~Losange();
virtual void mDessine();
};
dans "Losange.cpp"
#include "Losange.h"
// L'angle maximal d'un angle de losange est : 180°
double& Losange::mCorrectionAngle(double& pAngle)
{
// Corriger les dépassements
double vAngleMaximal = 180.0;
pAngle %= vAngleMaximal;
// Si l'angle est
if(pAngle < 0)
{
// Corriger le signe.
pAngle *= -1;
}
return pAngle;
}
void Losange::mAngleA(double& pAngle)
{
// Mettre à jour l'angle.
Quadrilatere::mAngleA(mCorrectionAngle(pAngle));
}
void Losange::mAngleB(double& pAngle)
{
// Mettre à jour l'angle.
Quadrilatere::mAngleB(mCorrectionAngle(pAngle));
}
void Losange::mAngleC(double& pAngle)
{
// Mettre à jour l'angle.
Quadrilatere::mAngleC(mCorrectionAngle(pAngle));
}
void Losange::mAngleD(double& pAngle)
{
// Mettre à jour l'angle.
Quadrilatere::mAngleD(mCorrectionAngle(pAngle));
}
Losange::Losange(double pArreteAB, double pArreteBC, double pAngleA):Quadrilatere(pArreteAB, pArreteBC, pArreteBC, pArreteAB, 0.0, 0.0, 0.0, 0.0)
{
// Dans un losange les arrêtes :
// AB = DA et BC = CD.
// Les angles :
// A = C, B = D et A + B + C + D = 360°.
// Ici nous passons par les méthodes de Losange pour garantir que les angles seront inférieur ou égal à 180.
double vAngleMax = 180.0;
this->mAngleA(pAngleA);
this->mAngleB(vAngleMax - this->mCorrectionAngle(pAngleA));
this->mAngleC(pAngleA);
this->mAngleD(vAngleMax - this->mCorrectionAngle(pAngleA));
}
~Losange::Losange()
{
}
void Losange::mDessine()
{
//Dessine ici le Losange (vrais code de dessin non pertinent car trop volumineux pour le gain obtenu sur l’intérêt de la leçon).
cout << "Dessine Losange :\n" << endl;
Quadrilatere::mDessine();
}
dans "Quadrilatere.h"
#ifndef Quadrilatere_H
#define Quadrilatere_H
#include "Figure.h"
class Quadrilatere : public Figure
{
private:
double aArreteAB;
double aArreteBC;
double aArreteCD;
double aArreteDA;
double aAngleA;
double aAngleB;
double aAngleC;
double aAngleD;
double& mCorrectionAngle(double& pAngle);
double& mCorrectionArrete(double& pArrete);
protected:
void mArreteAB(double& pArrete);
void mArreteBC(double& pArrete);
void mArreteCD(double& pArrete);
void mArreteDA(double& pArrete);
virtual void mAngleA(double& pAngle);
virtual void mAngleB(double& pAngle);
virtual void mAngleC(double& pAngle);
virtual void mAngleD(double& pAngle);
public:
Quadrilatere(double& pArreteAB, double& pArreteBC, double& pArreteCD, double& pArreteDA, double& pAngleA, double& pAngleB, double& pAngleC, double& pAngleD);
virtual ~Quadrilatere();
virtual void mDessine();
double mArreteAB();
double mArreteBC();
double mArreteCD();
double mArreteDA();
double mAngleA();
double mAngleB();
double mAngleC();
double mAngleD();
};
dans "Quadrilatere.cpp"
#include <math.h>
#include "Quadrilatere.h"
// L'angle maximal d'un angle de quadrilatère est : 360°
double& Quadrilatere::mCorrectionAngle(double& pAngle)
{
// Corriger les dépassements
double vAngleMaximal = 360.0;
pAngle %= vAngleMaximal;
// Si l'angle est
if(pAngle < 0)
{
// Corriger le signe.
pAngle *= -1;
}
return pAngle;
}
// Propriété AngleA de Quadrilatere. Remarquez comment la propriété contraint l'intégrité des données de la classe via la méthode mCorrectionAngle.
void Quadrilatere::mAngleA(double& pAngle): Figure()
{
// Mettre à jour l'angle.
this->aAngleA = mCorrectionAngle(pAngle);
}
void Quadrilatere::mAngleB(double& pAngle)
{
// Mettre à jour l'angle.
this->aAngleB = mCorrectionAngle(pAngle);
}
void Quadrilatere::mAngleC(double& pAngle)
{
// Mettre à jour l'angle.
this->aAngleC = mCorrectionAngle(pAngle);
}
void Quadrilatere::mAngleD(double& pAngle)
{
// Mettre à jour l'angle.
this->aAngleD = mCorrectionAngle(pAngle);
}
// Une arrête n’est pas négative
double& Quadrilatere::mCorrectionArrete(double& pArrete)
{
if(pArrete < 0)
{
pArrete *= -1;
}
return pArrete;
}
void Quadrilatere::mArreteAB(double& pArrete)
{
this->aArreteAB = mCorrigeArrette(pArrete);
}
void Quadrilatere::mArreteBC(double& pArrete)
{
this->aArreteBC = mCorrigeArrette(pArrete);
}
void Quadrilatere::mArreteCD(double& pArrete)
{
this->aArreteCD = mCorrigeArrette(pArrete);
}
void Quadrilatere::mArreteDA(double& pArrete)
{
this->aArreteCD = mCorrigeArrette(pArrete);
}
Quadrilatere::Quadrilatere(double& pArreteAB, double& pArreteBC, double& pArreteCD, double& pArreteDA, double& pAngleA, double& pAngleB, double& pAngleC, double& pAngleD)
{
// Assure l'intégrité de la classe;
this->mArreteAB(pArreteAB);
this->mArreteBC(pArreteBC);
this->mArreteCD(pArreteCD);
this->mArreteDA(pArreteAD);
this->mAngleA(pAngleA);
this->mAngleB(pAngleB);
this->mAngleC(pAngleC);
this->mAngleD(pAngleD);
}
Quadrilatere::~Quadrilatere()
{
// rien à détruire.
}
void Quadrilatere::mDessine()
{
//Dessine ici le parallélogramme (vrais code de dessin non pertinent car trop volumineux pour le gain obtenu sur l’intérêt de la leçon).
cout << "Dessine Quadrilatere :\n" <<
"\tAB = " << this->mArreteAB() << ",\n" <<
"\tBC = " << this->mArreteBC() << ",\n" <<
"\tCD = " << this->mArreteCD() << ",\n" <<
"\tDA = " << this->mArreteDA() << ",\n" <<
"\tAngle A = " << this->mAngleA() << ",\n" <<
"\tAngle B = " << this->mAngleB() << ",\n" <<
"\tAngle C = " << this->mAngleC() << ",\n" <<
"\tAngle D = " << this->mAngleD() << endl;
}
double Quadrilatere::mArreteAB()
{
return this->aArreteAB;
}
double Quadrilatere::mArreteBC()
{
return this->aArreteBC;
}
double Quadrilatere::mArreteCD()
{
return this->aArreteCD;
}
double Quadrilatere::mArreteDA()
{
return this->aArreteDAB;
}
double Quadrilatere::mAngleA()
{
return this->aAngleA;
}
double Quadrilatere::mAngleB()
{
return this->aAngleB;
}
double Quadrilatere::mAngleC()
{
return this->aAngleC;
}
double Quadrilatere::mAngleD()
{
return this->aAngleD;
}
dans "Figure.h"
#ifndef Figure_H
#define Figure_H
class Figure
{
public:
Figure();
virtual ~Figure();
virtual void mDessine() = 0; // "virtual void mDessine() = 0;" est une méthode dite "virtuelle pure" (à cause du "= 0").
// Elle n'a pas de corps et rend donc de fait la classe "Figure" abstraite. Cela signifie
// que l’on ne peux pas instancier cette classe directement. Pour pouvoir instancier cette
// classe il faut la dériver et implémenter la méthode (lui donner un corps).
};
dans "Figure.cpp"
#include "Figure.h"
Figure::Figure()
{
}
Figure::~Figure()
{
}
// Le corps de mDessine n'apparait pas car c’est une méthode virtuelle pure
GFDL | Vous avez la permission de copier, distribuer et/ou modifier ce document selon les termes de la licence de documentation libre GNU, version 1.2 ou plus récente publiée par la Free Software Foundation ; sans sections inaltérables, sans texte de première page de couverture et sans texte de dernière page de couverture. |