Big Tuto : Apprenez le C++

Chapitre 13 : Types de données complexes (1)

 

Tutoriel présenté par : Robert Gillard (Gondulzac)
Date de publication : 3 juillet 2016
Date de révision : -

 

     Préliminaires

 

   Nous conviendrons pour ce chapitre et le suivant, de nommer ''Types de données complexes'', les types spéciaux que nous reprendrons respectivement sous les termes de ''Structures'', ''Unions'', ''Champs de bits'' et ''Constantes énumérées''.

   Ces types spéciaux, contrairement aux autres types définis par le langage, peuvent produire d'autres types, ainsi que des objets plus ou moins sophistiqués créés par le programmeur. wink

   En ce qui concerne les structures, nous allons voir dans le prochain chapitre que celles-ci, contrairement au langage C, pourront accueillir des fonctions membres, appelées aussi méthodes et que ces fonctions membres seront directement adaptées à l'utilisation des structures.

   Utiliser des fonctions membres dans une structure va permettre l'accès aux éléments de cette structure sans avoir besoin d'utiliser du code n'y appartenant pas. Ce procédé d'intégrer des données et des méthodes dans une structure s'appelle l'encapsulation et donne à la structure un aspect ''boîte noire'', rendant le programme plus modulable, plus facile à gérer. En effet, contrairement aux fonctions ordinaires, les méthodes d'une structure n'existent pas ''en dehors'' de celle à laquelle elles appartiennent.

   Cette manière d'appréhender les structures par l'encapsulation des données et des fonctions membres sera déjà une bonne introduction à notre future (re)découverte des classes C++. cool

   Nous verrons bientôt que les classes de C++ offrent un degré de protection encore bien supérieur à celui des structures mais laissons pour l'instant de côté toutes ces considérations et lançons-nous sans tarder à la rencontre des structures du C++. angel

 

Retrouvez les projets complets de ce chapitre :

  

 

   1 – Structures
      1.1 – Utilisation des structures

 

   Une utilisation intéressante des structures est par exemple celui de la gestion des entrées que certains d'entre-vous auront pu voir dans les tutoriels ''Big tuto SDL 2 Expert'' ou ''Big tuto SFML Action Rpg'' de Jay. wink

   Dans le tutoriel SDL 2, le type est structuré selon une approche ''C'' soit :

 

/* Structure pour gérer l'input (clavier puis joystick) */

typedef struct Input

{

int left, right, up, down, jump, attack, enter, erase, pause;

} Input;

 

   Soit une structure de type C++ dans le tutoriel Action Rpg :

 

//Structures

struct Button { bool left, right, up, down, attack, run, enter, magie; };

 

   Nous voyons que le style C++ se passe du mot-clé ''typedef'' et nous allons aussi découvrir quelques autres différences de syntaxe.

   Une autre utilisation intéressante des structures, que nous allons développer dans ce chapitre et dans le suivant, est la création de bases de données. Et c'est justement pour la création de bases de données que nous introduirons l'encapsulation des variables (appelées aussi attributs) et fonctions membres (méthodes) dans les structures. wink
 

 

      1.2 – Création d'une structure simple

 

   Nos exemples et exercices concernant les structures continueront pour l'instant à être implémentés entièrement dans un seul fichier .cpp. Dans quelque temps, lors de notre découverte des classes C++, nous lierons chaque fois un fichier header (.h) correspondant à un projet .cpp. wink

   Nous commencerons par la création d'une structure simple :

 

//projet 083Struct_1

//Création d'une structure simple

#include<iostream>

#include<string>

#include<conio.h>

 

using namespace std;

using uint = unsigned int;

 

struct arc_long

{

string nom;

uint prix_achat;

uint portee;

};

 

int main()

{

//Création d'un objet monArc

arc_long monArc;

 

monArc.nom = "Arc long du guerrier des plaines du sud'";

monArc.prix_achat = 150;

monArc.portee = 55;

 

cout << endl;

 

cout << "Mon arc se nomme '" << monArc.nom << endl;

cout << "Son prix est de " << monArc.prix_achat << " PO" << endl;

cout << "Sa portee est de " << monArc.portee << " metres" << endl;

 

_getch();

return 0;

}

 

   Remarquons la forme de cette structure. Nous déclarons en premier le mot-clé struct suivi du nom que nous voulons donner à la structure, ici ''arc_long''. Les variables déclarées à l'intérieur de la structure sont comprises entre une accolade ouvrante et une accolade fermante suivie d'un point-virgule.

   Nous venons ainsi de définir un nouveau type, le type ''arc_long''. En effet, dans la fonction main(), nous déclarons un objet monArc de type arc_long (pour ce qui est des structures et plus tard des classes, nous utiliserons le terme ''objet'' pour un élément créé par un type de données complexe).

   La définition des variables ''nom'', ''prix_achat'' et ''portee'' de notre structure se fait alors par l'intermédiaire du nom de l'objet créé suivi d'un point et du nom de la variable.  

monArc.nom = "Arc long du guerrier des plaines du sud'";

monArc.prix_achat = 150;

monArc.portee = 55;

 

   Celles-ci sont alors affichées à la suite de leur définition. Ce qui donne dans la console :       

 

 

   Nous allons maintenant montrer une seconde forme d'implémentation d'une structure. Il s'agit de ce qu'on appelle une ''structure anonyme'' qui est une autre manière de créer des objets. Le nom de l'objet à créer est placé immédiatement après l'accolade fermante et avant le point-virgule.

   Par exemple :

struct

{

string nom;

uint prix_achat;

uint portee;

} monArc2;

 

   L'initialisation de l'objet monArc2 se faisant alors de la même façon dans la fonction main() :

monArc2.nom = "Arc leger d'Elfe des bois";

 

   Cette seconde forme d'implémentation d'une structure est à éviter au moins pour deux raisons : d'une part, le type de l'objet créé est indéfini, ce qui pourrait causer des problèmes lors de certaines utilisations mais surtout, parce que la première forme de déclaration est plus adaptée à l'étude des classes C++ que nous verrons bientôt. wink

 

Remarque : Dans les structures, contrairement aux classes, les données et fonctions membres sont ''public'' par défaut, ce qui permet d'y avoir accès en dehors des structures par l'intermédiaire des objets créés. Ceci offre, bien entendu une moins bonne protection des données, rendant l'aspect ''boîte noire'' des structures moins efficace que celui apporté par les classes en C++.

 

 

   1.3 – Saisir des éléments d'une structure au clavier

 

   Et bien oui, cette fois nous allons remplir notre structure manuellement après l'avoir déclarée. Imaginez que vous deviez saisir l'une ou l'autre base de données telle un carnet d'adresses, les caractéristiques de différents types de héros que vous voudriez choisir pour créer votre rpg favori ou tout autre liste quelconque se rapportant à votre génie créatif, il vous faudra sans doute passer par la saisie de toutes ces données pour les intégrer dans des structures adéquates. cheeky

   A partir de maintenant nous n'utiliserons plus (dans la plus grande majorité des cas) que le type ''string'' en ce qui concerne les chaînes de caractères. Donc si vous vous souvenez de ce que nous avons vu dans le chapitre précédent, nous allons utiliser l'instruction getline(cin, chaine) pour leur saisie. Nous voyons ceci dans un second exemple wink :

 

//projet 084Struct_2

//Saisie d'éléments dans une structure simple

#include<iostream>

#include<string>

#include<conio.h>

 

using namespace std;

using uint = unsigned int;

 

struct arc_court

{

string nom;

uint prix_achat;

uint portee;

};

 

 

 

int main()

{

 

arc_court monArc;

 

cout << endl;

cout << "Saisissez les champs suivants pour remplir la structure 'arc_court'" << endl << endl;

cout << "Nom de l'arme : ";

getline (cin, monArc.nom);

cout << "Prix d'achat : ";

cin >> monArc.prix_achat;

 

cout << "Portee : ";

cin >> monArc.portee;

cout << endl;

 

cout << "Lecture des éléments de la structure" << endl << endl;

cout << monArc.nom << endl;

cout << monArc.prix_achat << endl;

cout << monArc.portee;

cout << endl;

 

_getch();

return 0;

}

 

   Cette structure comprend un enregistrement. Cet enregistrement est l'ensemble des éléments composant l'intérieur de la structure et chaque élément de la structure est appelé un champ.

   Hormis ces définitions, il n'y a rien de difficile dans cet exemple que nous n'ayons déjà vu. Une remarque d'importance s'invite, cependant :

   Si dans la saisie des différents champs de la structure nous avions placé le champ ''prix_achat'' avant le champ ''nom'', nous aurions eu un problème. En effet, comme nous l'avons vu dans le chapitre précédent, il aurait fallu ajouter l'instruction cin.ignore() avant l'acquisition de la chaîne et ce, pour vider le buffer (tampon) d'entrée dû à la frappe de la touche ''return''.

   Donc chaque fois qu'un champ de type numérique devrait être saisi avant un champ de type string, vous placerez l'instruction cin.ignore() avant getline(cin, chaine) pour la saisie de cette chaîne. wink

 

   Et le résultat dans la console : 

 

 

   Voilà, nous venons de créer une structure comportant un enregistrement dont nous avons saisi les différents champs. Mais si vous voulez que, par exemple, le marchand Ethiolas comptabilise 10 ou 15 arcs de type ''arc composite'' dans son stock, vous n'entrerez bien entendu pas 10 ou 15 enregistrements au clavier. laugh Vous créerez plutôt ce que l'on appelle un tableau d'objets ; voyons comment. wink

 

 

   1.4 – Création d'un tableau d'objets

 

//projet 085Struct_3

 

//Création d'un tableau d'objets de type 'arc_composite'

#include<iostream>

#include<string>

#include<conio.h>

 

using namespace std;

using uint = unsigned int;

 

struct arc_composite

{

string nom;

uint prix_achat;

uint prix_revente;

uint portee;

};

 

 

 

int main()

{

const uint nb_arcs(15);

arc_composite monArc[nb_arcs];

 

for (int i = 0; i != nb_arcs; ++i)

{

monArc[i].nom = "Arc composite du Gardien des Tours Sacrees";

monArc[i].prix_achat = 180;

monArc[i].prix_revente = 20;

monArc[i].portee = 65;

}

 

cout << endl;

cout << "Mon arc se nomme '" << monArc[0].nom << endl;

cout << "Son prix d'achat est de " << monArc[0].prix_achat << " PO" << endl;

cout << "Le prix de revente est de : " << monArc[0].prix_revente << " PO" << endl;

cout << "Sa portee est de " << monArc[0].portee << " metres" << endl;

 

_getch();

return 0;

}

 

   Nous créons en premier une valeur const uint égale à 15 qui représentera le stock d'arcs de notre marchand Ethiolas. A la ligne suivante, nous créons un tableau de 15 objets ''monArc'' de type ''arc_composite'' que l'on remplit à l'aide d'une boucle ''for''. Remarquez que si nous avons pu utiliser une boucle sous cette forme c'est parce que les valeurs des éléments représentant un même indice sont identiques (et c'est normal, il s'agit des mêmes arcs). cheeky

   Vous aurez en outre remarqué que nous avons ajouté un champ dans la structure, il s'agit du champ ''prix_revente'', qui sera le prix généreusement offert par Ethiolas si nous lui revendons l'arc que nous venons de lui acheter. Mais je suppose que cela ne doit pas vous étonner, vous qui connaissez bien les rpg's ; vous aviez déjà compris que les marchands sont tous des voleurs... cheeky laugh

   Et nous ne placerons pas les résultat de la console ici, ceux-ci étant pratiquement les mêmes que ceux du premier exemple de ce chapitre. wink

 

 

   1.5 – Création de tableaux d'objets dans le "tas"

 

   Dans le chapitre 9, Allocation dynamique de mémoire (partie 1), nous avons vu que nous pouvions créer des variables dans la partie de la mémoire que l'on appelle le ''tas''.

   Et bien nous pouvons faire la même chose avec des objets de n'importe quel type de structure. cool


   Si nous reprenons la création de 15 objets ''monArc'' de type ''arc_composite'' que nous venons de voir dans l'exemple précédent, nous pourrions donc, comme nous l'avions fait dans le chapitre 9 sur des variables, réserver de la mémoire dans le tas pour ces 15 objets à l'aide d'un pointeur, soit d'une référence sur le tableau. Ce qui nous donnerait : 

 

arc_composite *monArc = new arc_composite [15];

dans le cas où new renvoie un pointeur sur le début du tableau

ou

arc_composite &monArc = *new arc_composite [15];

dans le cas où new renvoie une référence sur le début du tableau

 

   (Voir chapitre 9 - §1.1, 1.2 et 1.3 pour rappel)

   Bien, si nous utilisons la première forme, nous devrons toujours faire appel aux indices dans la boucle et le corps du programme sera identique sauf que nous n'oublierons pas de désallouer la mémoire utilisée pour le tableau en fin de programme avec l'instruction :

delete [15] monArc;

 

   Remarque : Lors de la dé-allocation d'un tableau d'objets, vous devez inclure le nombre d'objets créés entre les crochets ! wink

 

   En utilisant une référence sur le début du tableau nous pouvons tout simplement nous passer de la boucle for. Voyons cela dans un exemple :

 

//projet 086truct_4

//Réservation dans le tas d'un tableau d'objets de type 'arc_composite'

#include<iostream>

#include<string>

#include<conio.h>

 

using namespace std;

using uint = unsigned int;

 

struct arc_composite

{

string nom;

uint prix_achat;

uint prix_revente;

uint portee;

};

 

 

 

int main()

{

const uint nb_arcs(15);

//Création d'un tableau d'objet dans le tas dans le cas où

//new renvoie une référence sur le début du tableau

arc_composite &monArc = *new arc_composite[nb_arcs];

 

monArc.nom = "Arc composite du Gardien des Tours Sacrees";

 

monArc.prix_achat = 180;

monArc.prix_revente = 20;

 

monArc.portee = 65;

 

cout << endl;

cout << "Mon arc se nomme '" << monArc.nom << endl;

cout << "Son prix d'achat est de " << monArc.prix_achat << " PO" << endl;

cout << "Le prix de revente est de : " << monArc.prix_revente << " PO" << endl;

cout << "Sa portee est de " << monArc.portee << " metres" << endl;

//On libère la mémoire du tableau

delete[15](arc_composite*)(&monArc);

 

_getch();

return 0;

}

 

   Et n'oubliez pas que dans ce cas, en ce qui concerne la dé-allocation mémoire du tableau d'objets, delete a besoin de connaître l'adresse du premier byte de la zone à effacer, ce qui veut dire que nous devons forcer l'objet à être un pointeur sur cette première zone, soit par l'instruction :

delete[15](arc_composite*)(&monArc);

 

   Ce qui donne comme résultat dans la console :   

 

 

   Et maintenant on pourrait peut-être penser à faire notre premier exercice, non ? cheeky

 

   Exercice 1

 

   Créez une structure que vous nommerez : arme_de traits_Arc.
   A l'aide du type de cette structure créez trois objets en utilisant l'allocation dynamique de mémoire par laquelle ''new'' renvoie une référence sur un objet créé. Ces 3 objets seront respectivement dénommés arc_court, arc_long et arc_composite.

   Pour chaque objet, vous entrerez les champs de la structure au clavier ainsi que leurs valeurs correspondantes en vous basant sur les exemples cités plus haut.

   Vous créerez également un champ ''prix_revente'' pour chaque objet auquel vous attribuerez respectivement les valeurs 12, 16 et 20.

   Pour terminer, affichez les valeurs saisies pour les champs de chaque objet.

 

 

   1.6 – Utilisation d'éléments statiques dans les structures

 

   Si nous relisons l'énoncé de l'exercice 1, on constate que nous avons une seule structure, donc un seul type dénommé ''arme_de traits_Arc'' et qu'à partir de ce type nous allons déclarer 3 objets.
   Les noms des champs de ces objets sont identiques mais chaque objet possède ses propres valeurs.

   Ces 3 objets étant différents, les modifications que nous pourrions faire dans l'un n'aurait aucune incidence sur les deux autres. Il se pourrait cependant, que dans une telle structure, nous ayons besoin d'un champ possédant une valeur commune à ces différents objets.

   Reprenant les objets arc_court, arc_long et arc_composite, nous pourrions, par exemple, dire que ces armes sont faites de la même matière, avec le même bois.

   Pour ce faire nous avons besoin d'un élément supplémentaire, déclaré ''static''.

 

        Initialisation d'éléments ''static'' dans une structure

 

   L'initialisation d'éléments ''static'' communs à plusieurs objets peut se faire de deux manières différentes :

   Première manière : dans la structure elle-même. Si par exemple nous reprenons la structure de l'exercice 1, nous la déclarerions comme ceci : 

 

struct arme_de_traits_Arc

{

string nom;

uint prix_achat;

uint prix_revente;

uint portee;

static string type_arme;

 

}; string arme_de_traits_Arc::type_arme = "Arme Elfe - Bois de la foret du Marais Noir";

 

   A l'intérieur de la structure on déclare une variable ''static'' de type string que l'on initialise après le point-virgule terminant la structure.

   Seconde manière : 

 

struct arme_de_traits_Arc

{

string nom;

uint prix_achat;

uint prix_revente;

uint portee;

static string type_arme;

 

};

 

   On déclare de même une variable de type ''string'' à l'intérieur de la structure mais elle pourra être initialisée n'importe où dans le programme avant la fonction main(); par exemple à la suite de déclarations de fonctions. wink

   Finalement, dans la fonction main(), à n'importe quel endroit de la fonction, si vous saisissez les lignes suivantes :

 

cout << "Arc court : " << arc_court.type_arme << endl;

cout << "Arc long : " << arc_long.type_arme << endl;

cout << "Arc composite : " << arc_composite.type_arme << endl;

 

   Vous aurez un résultat commun aux trois objets, soit :

 

Arme Elfe - Bois de la foret du Marais Noir

 

  Lors de l''initialisation de la chaîne commune, nous voyons que nous utilisons l'opérateur de résolution de portée ''::'', comme si la variable ''type_arme'' était une variable globale, ce qui bien entendu, n'en est rien. En effet, son domaine de visibilité se limite à sa propre structure ''arme_de_traits_Arc'', ce qui explique sa nécessité. wink

   Et voilà pour ce qui est des éléments ''static'' dans une structure. Pour cet exemple j'ai utilisé un élément de type ''string'' mais il est bien entendu que vous pouvez prendre n'importe quel type d'élément de structure (ou même plusieurs éléments de types différents) pour le déclarer commun à plusieurs objets si vous en avez besoin. smiley

 

 

   2 – Structures et fonctions

        2.1 – Passage d'un objet dans une fonction

 

   Nous pourrions avoir besoin de passer un ou plusieurs éléments d'un ou plusieurs objets à une fonction et ce, pour diverses raisons. Supposons par exemple que nous désirions comptabiliser la valeur du stock d'épées courtes de notre marchand Ethiolas qui en possède 22 et qui vient, il fallait s'y attendre, faire passer leur prix de 145 à 147,68 PO. Diable !? surprise

   Nous allons donc créer une fonction que nous nommerons Valeur_stock(). Il nous faudra cependant changer le type uint en ''double'' comme type des champs ''prix_achat' et ''prix_revente''.  

 

//projet 087Struct_5

//Passage d'éléments de structure à une fonction

#include<iostream>

#include<string>

#include<conio.h>

 

using namespace std;

using uint = unsigned int;

 

struct arme_tranchante_Epee

{

string nom;

double prix_achat;

double prix_revente;

uint quantite_en_stock;

};

 

double Valeur_stock(arme_tranchante_Epee epee_courte);

 

int main()

{

//Création d'un objet dans le tas renvoyant une référence sur l'objet

arme_tranchante_Epee &epee_courte = *new arme_tranchante_Epee;

 

//Remplissage des champs de la structure

epee_courte.prix_achat = 147.68;

epee_courte.prix_revente = 15.0;

epee_courte.quantite_en_stock = 22;

 

//Lecture des champs de la structure

cout << endl;

cout << "Lecture des champs de l'objet epee_courte" << endl << endl;

cout << "Prix d'achat d'une epee courte : " << epee_courte.prix_achat << " PO" << endl;

cout << "Prix de revente d'une epee courte : " << epee_courte.prix_revente << " PO" << endl;

 

cout << "Quantite en stock : " << epee_courte.quantite_en_stock << " pieces" << endl;

 

cout << endl;

Valeur_stock(epee_courte);

 

//Libération mémoire

delete (arme_tranchante_Epee*)(&epee_courte);

 

_getch();

return 0;

}

 

 

double Valeur_stock(arme_tranchante_Epee epee_courte)

{

cout << "Valeur du stock d'epees courtes du marchand Ethiolas" << endl;

cout << epee_courte.quantite_en_stock << " * " << epee_courte.prix_achat << " = ";

cout << static_cast<double>(epee_courte.quantite_en_stock * epee_courte.prix_achat) << " PO" << endl;

 

return 0;

}

 

   Jetons un coup d'oeil à notre structure ''arme_tranchante_Epee''. Nous y avons ajouté un champ ''quantite_en_stock'' de type unsigned int. Ce champ va servir à calculer la valeur du stock d'épées courtes du marchand et pourrait être utilisé pour le calcul éventuel du nombre restant de ce genre d'arme après achat.

   Ce programme passe une copie de l'objet ''epee_courte'' à la fonction Valeur_stock(). C'est pourquoi nous ne pouvons qu'utiliser telles quelles les valeurs des champs ''quantite_en_stock'' et ''prix_achat'' dans la définition de la fonction, ce qui est tout-à-fait suffisant pour le calcul de la valeur du stock.

  Les champs utilisés dans la fonction Valeur_stock() contiennent donc des valeurs locales dont le domaine de définition ne s'étend qu'à cette fonction. Toute modification d'une de celles-ci ne serait pas répercuté au retour de la fonction appelante dans la fonction main().

   Donc si nous avions voulu créer une fonction qui comptabilise le nombre d'épées ou autres items achetés, nous aurions dû passer un pointeur ou une référence lors de la définition de cette fonction. wink

   Avec comme résultat dans la console :

 

 

   Avant de passer à un exercice sur ce sujet de modification de champs dans une fonction, nous voudrions donner un exemple d'accès aux champs d'une structure dans le cas où on déclarerait un objet réservé dans le tas et sur lequel on renvoie un pointeur.

 

 

        2.2 – Accès aux champs d'une structure à l'aide de l'opérateur "flèche"

 

   Nous avons vu jusqu'à présent que l'on accédait aux éléments d'une structure à l'aide de l'opérateur point (''.''), soit dans le cas de création d'un objet sur la pile, soit dans le cas de création d'un objet dans le tas renvoyant une référence sur cet objet.

   C'est-à-dire que si par exemple nous avons une structure dénommée ''arc_long'' et que nous voulons créer un objet ''monArc'', nous avons :

   Dans le premier cas :

arc_long monArc;        Pour la déclaration de l'objet monArc sur la pile
monArc.nom;                Pour accéder au champ ''nom'' de la structure

 

    Dans le second cas :


arc_long &monArc = *new arc_long;       Pour la déclaration de l'objet monArc dans le tas

                                                                           renvoyant une référence sur l'objet.

monArc.nom;                                                 Pour accéder au champ ''nom'' de la structure.

 

   Nous avons utilisé ces deux cas jusqu'à présent. Si maintenant nous voulons déclarer l'objet dans le tas renvoyant un pointeur sur l'objet, nous avons vu que la réservation mémoire devait se faire comme ceci :

arc_long *monArc = new arc_long; 

 

   mais que pour accéder au champ ''nom'' de cette structure, nous écrirons :

(*monArc).nom avec les parenthèses autour de l'indirection du pointeur et en utilisant également l'opérateur ''point''. Ce genre d'expression se révélant quelque peu compliquée, C++ a introduit l'opérateur ''flèche'', soit ''->'' pour réaliser la même opération. Cet opérateur est constitué du symbole ''tiret'' suivi par le symbole ''plus grand que''.


   En résumé nous dirons que dans ce cas, l'expression (*monArc).nom est équivalente à l'expression monArc->nom

   Et c'est cette dernière expression que nous utiliserons dorénavant dans le cas de réservation dans le tas avec renvoi d'un pointeur sur l'objet créé.

   Voilà, nous pouvons vous en montrer un exemple en réécrivant le projet 083Struct_1 de ce chapitre. wink
 

//projet 088Struct_6

//Réservation d'un objet dans le tas avec renvoi d'un pointeur sur l'objet.

#include<iostream>

#include<string>

#include<conio.h>

 

using namespace std;

using uint = unsigned int;

 

struct arc_long

{

string nom;

uint prix_achat;

uint portee;

};

 

int main()

{

//Création d'un objet monArc dans le tas avec renvoi d'un pointeur sur l'objet.

arc_long *monArc = new arc_long;

 

//On accède aux différents champs à l'aide de l'opérateur flèche.

monArc->nom = "Arc long du guerrier des plaines du sud'";

monArc->prix_achat = 150;

monArc->portee = 55;

 

cout << endl;

 

cout << "Mon arc se nomme '" << monArc->nom << endl;

cout << "Son prix est de " << monArc->prix_achat << " PO" << endl;

 

cout << "Sa portee est de " << monArc->portee << " metres" << endl;

 

_getch();

return 0;

}

 

   Nous avons déjà tout dit, on accède aux champs de la structure à l'aide de l'opérateur flèche. Et le résultat dans la console est identique à celui du projet 083Struct_1.

   Ceci étant acquis, nous allons passer à notre petit challenge habituel qui va vous permettre la modification de champs d'objets dans une fonction. wink

 

 

        2.3 – Passage d'un pointeur ou d'une référence sur un objet dans une fonction

 

   Dans le § 2.1 nous avons dit que pour procéder à toute modification des éléments d'une structure dans une fonction, nous devons passer un pointeur ou une référence lors de l'appel de cette fonction. C'est ce que nous allons faire ici dans un exercice. wink

 

    Un petit challenge (Exercice 2)

 

   Je vous donne deux structures à créer, celles-ci représentent des éléments de votre inventaire et de l'inventaire du marchand Ethiolas : 

 

struct inventaire_marchand //structure pour le marchand

{

static string nom_marchand;

string nom_item;

uint prix_vente;

uint quantite_en_stock;

}; string inventaire_marchand::nom_marchand = "Ethiolas";

 

struct inventaire_heros //structure pour le héros

{

string nom_item;

uint prix_achat;

uint prix_revente;

 

uint quantite_en_stock;

};

 

   ainsi que deux objets à créer dans le tas (vous utiliserez le cas avec retour d'un pointeur sur chaque objet) :

 

//Création d'objets dans le tas renvoyant un pointeur sur les objets

inventaire_marchand *item_marchand = new inventaire_marchand;

inventaire_heros *item_heros = new inventaire_heros;

 

   Remarquez que vous aurions pu créer un tableau d'objets dans le cas où notre inventaire aurait dû accueillir de nombreux items (armes, potions, armures etc...) mais ce n'est pas nécessaire pour notre exemple. cheeky

   Allez, je vous remplis également les champs mais ne le dites à personne... blush

 

cout << "Au depart, inventaire marchand = 5 potions et inventaire heros = 0 potion" << endl << endl;

 

//Remplissage des champs de la structure inventaire_marchand

item_marchand->nom_item = "Petite potion de vie";

item_marchand->prix_vente = 80;

item_marchand->quantite_en_stock = 5;

 

//Remplissage des champs de la structure inventaire_heros

item_heros->nom_item = "Petite potion de vie";

item_heros->prix_achat = 80;

item_heros->prix_revente = 8;

 

item_heros->quantite_en_stock = 0;

 

   Bon, ça suffit maintenant, c'est vous qui allez continuer. angel

 

   A faire :

- Déclaration d'une fonction Transactions() avec passage d'une référence sur chaque objet.
- Dans la fonction main() : lecture des champs de la structure ''inventaire_heros''
- Appel de la fonction Transactions()
- Dans la fonction Transactions() :
       - Achetez deux potions de vie au marchand (c'est-à-dire qu'au niveau des inventaires, vous ferez les opérations de la transaction dans cette fonction) wink
- Dans la fonction main(), au retour de la fonction Transactions(), affichez les champs ''quantite_en_stock'' de chaque inventaire.

 

   Nous ne nous occuperons pas pour l'instant de la quantité d'or que possèdent les deux personnages, nous reverrons tout ceci plus en profondeur lors de l'étude des classes. wink

 

 

   3 – Corrigés des exercices du chapitre 12


    Corrigé de l'exercice 1 :

 

   Pour cet exercice il était demandé :

   En utilisant l'instruction cin.get(), écrivez une fonction qui va saisir 5 chaînes de caractères de type ''char'' et renverra un pointeur sur un tableau de 5 chaînes de type ''string'' à lire dans la fonction main().


   Voici le code :

 

//projet Chap12Exercice_1

//Saisie d'une chaine avec la forme surchargée de cin.get() comprenant deux paramètres;

#include<iostream>

#include<string>

#include<conio.h>

 

using namespace std;

using uint = unsigned long;

 

 

string Saisie(string *tableau, const size_t longTab);

 

 

int main()

{

const size_t longueurTableau = 5;

string tab[longueurTableau];

 

cout << endl;

Saisie(tab, longueurTableau);

cout << endl;

 

for (auto i(0); i != longueurTableau; ++i)

{

cout << *(tab + i);

cout << endl;

}

 

_getch();

return 0;

}

 

 

string Saisie(string *tableau, const size_t longTab)

{

 

const uint longueurChaine = 256;

char chaine[longueurChaine];

 

for (auto i(0); i != longTab; ++i)

{

cout << "Entrez une chaine : ";

cin.get(chaine, longueurChaine);

 

cin.ignore();

 

tableau[i] = chaine;

}

 

cout << endl;

 

return *tableau;

}

 

   Nous n'avons rien de bien compliqué dans cet exercice. Dans la fonction main() on déclare un tableau de 5 strings. Celui-ci est ensuite passé à la fonction Saisie(). Dans la définition de cette fonction, on déclare une chaîne de type ''char'' d'une longueur de 256 caractères. L'utilisateur procède ensuite à une saisie de 5 chaînes de type ''char'' à l'aide de la fonction cin.get(). Ces chaînes sont alors retournées dans les chaîne de type ''string'' et renvoyées dans la fonction main() où elles sont affichées. 

 

   Corrigé de l'exercice 2 :

 

   Pour cet exercice il était demandé :

   Modifiez le projet 077peek_putback. La chaîne de départ à entrer au clavier sera :
      Le_Sorcier_du!Temple!de_la!Lune!Rousse (cette fois les espaces sont remplacés par des caractères de soulignement)

 

   La chaîne à obtenir est :
      le sorcier du temple de la lune rousse (chaîne dans laquelle il faudra en plus remplacer les caractères majuscules par des caractères minuscules).

 

   Voici le code : 

 

//projet Chap12Exercice_2

//Modification de la saisie de caractères

//Utilisation des méthodes peek() et putback() de cin.

#include<iostream>

#include<cctype>

#include<conio.h>

 

using namespace std;

 

 

int main()

{

 

char caract;

 

cout << endl;

cout << "Ce programme donnera comme resultat la chaine suivante :" << endl;

cout << "le sorcier du temple de la lune rousse" << endl << endl;

cout << "entrez exactement la chaine suivante :" << endl;

cout << "Le_Sorcier_du!Temple!de_la!Lune!Rousse" << endl << endl;

 

int i = 0;

 

while ((caract = cin.get()) != EOF)

{

 

while (cin.peek() == '!')

{

cin.ignore(1, '!');

cin.putback(' ');

}

 

while (cin.peek() == '_')

{

cin.ignore(1, '_');

cin.putback(' ');

}

 

caract = tolower(caract);

cout << caract;

 

++i;

}

 

 

return 0;

}

 

   Bien, quelques explications quand-même. wink Nous avons trois boucles ''while'' dans le programme. La boucle extérieure teste chaque caractère entré au clavier jusqu'à EOF.

   La première boucle intérieure teste si un caractère est un point d'exclamation et si tel est le cas, le caractère est supprimé par la fonction ignore() et remplacé par un caractère ''espace'' à l'aide de la fonction putback(). Au sortir de cette boucle, notre chaîne est à ce moment :

      Le_Sorcier_du Temple de_la Lune Rousse

 

   La seconde boucle intérieure remplace les trois caractères de soulignement par un caractère ''espace''.

   Finalement, chaque caractère de la chaîne est converti en caractère minuscule (on ne s'occupe pas de ceux qui sont déjà en minuscules), ce qui nous donne la chaîne demandée :

      le sorcier du temple de la lune rousse

 

   Et vous devriez avoir un résultat de ce genre dans la console : 

 

 


   Corrigé de l'exercice 3_1 :

 

   Pour cet exercice il était demandé :

   Dans la fonction main(), saisissez un nom de fichier à l'écran (par exemple : Ennemis.txt) de type ''string''. Déclarez ensuite la chaîne suivante :

const string mesEnnemis[] = { "Gobelin", "Orc", "Squelette", "Liche", "Elfe noir" };

 

   En vous référant au chapitre 10 (§ 2.2), vous déclarerez ensuite une référence sur un tableau de 5 chaînes. Créez ensuite deux fonctions, EcrisFich() et LisFich(). Dans la première fonction vous passerez le nom du fichier à créer ainsi que la référence sur le tableau de 5 strings. La fonction LisFich() quant-à-elle sera appelée en lui passant le nom du fichier en paramètre. Cette fonction devra afficher le contenu du fichier à l'écran.

 

   Voici le code :

 

//projet Chap12Exercice_3.1

//Ecriture de chaînes de type string dans un fichier

//Implémentation d'une fonction pour l'écriture et d'une seconde pour la lecture

#include<fstream>

#include<iostream>

#include<string>

#include<conio.h>

 

using namespace std;

using uint = unsigned int;

//Déclararion de la fonction EcrisFich()

//Passage du nom de fichier et d'une référence sur un tableau de strings en paramètres

void EcrisFich(const string nomFichier, string (&mesEnnemis)[5]);

//Déclaration de la fonction LisFich - passage du nom du fichier en paramètre

void LisFich(const string fichierEnnemis);

 

int main()

{

const string nom_Fichier = "Ennemis.txt";

string mesEnnemis[] = { "Gobelin", "Orc", "Squelette", "Liche", "Elfe noir" };

 

//On initialise une référence sur le tableau de 5 strings

string(&Ennemis)[5] = mesEnnemis;

 

cout << endl;

cout << "Ecriture fichier...";

 

EcrisFich(nom_Fichier, mesEnnemis);

 

cout << endl;

cout << "***Le fichier a ete cree***" << endl;

 

LisFich(nom_Fichier);

 

_getch();

return 0;

}

 

 

void EcrisFich(const string nom_Fichier, string (&Ennemis)[5])

{

ofstream writeFile(nom_Fichier);

 

//Si le fichier a bien été crée...

if (writeFile)

{

for (uint i = 0; i != 5; ++i)

writeFile << (Ennemis[i]) << endl;

}

 

else

{

cout << "Erreur : le fichier n'a pu etre ouvert !" << endl;

}

 

//Fermeture du fichier

writeFile.close();

}

 

 

void LisFich(const string fichierEnnemis)

{

string ligne;

 

cout << endl;

cout << "Lecture du fichier " << fichierEnnemis << "..." << endl << endl;

ifstream readFile(fichierEnnemis);

 

if (readFile)

{

//On lit le fichier ligne par ligne

while (getline(readFile, ligne))

cout << ligne << endl;

}

 

else

cout << "Erreur : le fichier n'a pu etre ouvert !" << endl;

 

cout << endl;

cout << "***Fin de fichier***" << endl;

//Fermeture du fichier

readFile.close();

}

 

   Pour cet exercice nous initialisons donc une référence sur un tableau de dimension connue (voir chapitre 10, § 2.2 ainsi que le projet 067ReferenceSurTableau).

   Nous déclarons deux fonctions, EcrisFich() et LisFich(). A la fonction EcrisFich(), nous passons le nom du fichier ainsi qu'une référence sur le tableau ''mesEnnemis'' comportant 5 chaînes. Dans cette déclaration, la valeur de la longueur du tableau doit être un numérique, une constante serait signalée ''inconnue''. Dans le chapitre 10, ceci nous avait fait émettre quelques réserves quant à ce type de code et c'est pourquoi (nous allons le voir dans l'exercice 3.2 wink) l'initialisation d'un vector par un tableau, est bien plus efficace.

   La fonction LisFich() quant-à-elle n'a besoin que du nom du fichier en paramètre.

 

Attention : lors de l'appel à la fonction EcrisFich() dans la fonction main(), nous devons passer l'adresse du tableau et c'est pourquoi l'appel se fait par EcrisFich(nom_Fichier, mesEnnemis);


   La définition des fonctions EcrisFich() et LisFich() ne pose pas de problème, cela a été expliqué dans le chapitre 12.
 

 

   Corrigé de l'exercice 3_2 :

 

   Pour cet exercice il était demandé :

   En vous inspirant du projet 064VectorFunc_3 du chapitre 9, réalisez le même programme que l' exercice 3.1 en initialisant un vector avec un tableau. Les éléments du vector seront écrits dans la fonction EcrisFich() et affichés dans la fonction LisFich().

 

   Voici le code :                                                  

 

//projet Chap12Exercice_3.2

//Ecriture de chaînes de type string dans un fichier

//par l'initialisation d'un vector par un tableau

//Implémentation d'une fonction pour l'écriture et d'une seconde pour la lecture

#include<fstream>

#include<iostream>

#include<vector>

#include<string>

#include<conio.h>

 

using namespace std;

using uint = unsigned int;

//Déclararion de la fonction EcrisFich()

 

//Passage du nom de fichier et d'une référence sur un vector en paramètres

void EcrisFich(const string nomFichier, vector<string> &);

//Déclaration de la fonction LisFich - passage du nom du fichier en paramètre

void LisFich(const string fichierEnnemis);

 

int main()

{

const string nom_Fichier = "Ennemis.txt";

const string ennemis[] = { "Gobelin", "Orc", "Squelette", "Liche", "Elfe noir" };

 

//Initialisation d'un vector avec le tableau

vector<string>mesEnnemis(begin(ennemis), end(ennemis));

 

cout << endl;

cout << "Ecriture du fichier " << nom_Fichier;

 

EcrisFich(nom_Fichier, mesEnnemis);

 

cout << endl;

cout << "***Le fichier a ete cree***" << endl;

 

LisFich(nom_Fichier);

 

_getch();

return 0;

}

 

 

void EcrisFich(const string nom_Fichier, vector<string>(&mesEnnemis))

{

ofstream writeFile(nom_Fichier);

 

//Si le fichier a bien été crée...

if (writeFile)

{

for (auto it : mesEnnemis)

writeFile << it << endl;

}

 

else

{

cout << "Erreur : le fichier n'a pu etre ouvert !" << endl;

}

 

//Fermeture du fichier

writeFile.close();

}

 

 

void LisFich(const string fichierEnnemis)

{

//string nom_Fichier = "Ennemis.txt";

string ligne;

 

cout << endl;

cout << "Lecture du fichier " << fichierEnnemis << "..." << endl << endl;

ifstream readFile(fichierEnnemis);

 

if (readFile)

{

//On lit le fichier ligne par ligne

while (getline(readFile, ligne))

cout << ligne << endl;

}

 

else

cout << "Erreur : le fichier n'a pu etre ouvert !" << endl;

 

cout << endl;

cout << "***Fin de fichier***" << endl;

//Fermeture du fichier

readFile.close();

} 

 

   La modification importante de ce corrigé par rapport au précédent est la déclaration, et par conséquent la définition de la fonction EcrisFich(). En effet, cette fois nous passons une référence sur un vector string en paramètre.

   Dans la fonction main(), comme dans l'exercice 1, nous créons un tableau de 5 chaînes de caractères mais cette fois nous initialisons un vector avec les éléments du tableau en utilisant les fonctions begin() et end() de la classe string afin de définir la longueur initiale de ce vector. Cette initialisation du vector ainsi que sa longueur est représentée par :

vector<string>mesEnnemis(begin(ennemis), end(ennemis));

 

   Une référence à ce tableau est passée à la fonction qui va créer le fichier Ennemis.txt, la fonction LisFich() étant identique à la fonction de l'exercice précédent. wink

   Et les deux exercices donnent comme résultat dans la console :

 

 

   Ces deux exemples nous donnent à réfléchir sur l'intérêt d'utilisation de tableaux ou de vectors. Dans le premier exemple, le tableau mesEnnemis[] est limité au passage et à l'écriture de 5 chaînes de caractères. Dans le second exemple, bien que notre tableau de départ soit le même que celui de l'exemple 1, nous l'utilisons pour initialiser un vector, ce qui nous aurait permis d'ajouter des éléments dans ce vector à l'aide de la fonction push_back().

   Il n'y a bien entendu pas d'erreur à utiliser un tableau mais cela ne devrait se faire que dans un cadre strict où vous êtes certains que ce tableau ne devra jamais être amené à croître.

   Dans toute incertitude, préférez un vector.

 

   Et voilà, c'est tout en ce qui concerne ce chapitre.

   Les corrigés des exercices de ce chapitre seront présentés à la fin du chapitre 14

 

      @ bientôt pour le chapitre 14 – Types de données complexes (2), où nous verrons notamment l'implémentation de fonctions membres dans les structures.                                                                     

            Gondulzak.  angel
 
 
 

Connexion

CoalaWeb Traffic

Today69
Yesterday126
This week195
This month3869
Total1743076

23/04/24