Big Tuto : Apprenez le C++

Chapitre 21 : Objets, pointeurs et références

 

Tutoriel présenté par : Robert Gillard (Gondulzac)
Date de publication : 13 mai 2017
Date de révision : -

 

Retrouvez les projets complets de ce chapitre :

  

 

 

   Préliminaires

 

   Les trois chapitres précédents vous auront permis de vous familiariser avec la surcharge des opérateurs. angel

   A la fin de ce chapitre, lorsque vous découvrirez le corrigé de notre petit challenge du chapitre 20, vous comprendrez toute l'importance que peut représenter la surcharge d'opérateurs lors d'opérations sur des objets. wink

   En attendant nous allons achever, dans ce chapitre, de nous pencher sur quelques autres fonctionnalités concernant les objets, pointeurs et références dans une classe simple.

 


   1 – Transtypage

 

   Pour rappel, le transtypage est la conversion d'un objet d'un certain type en un objet d'un autre type. cool

   Les trois derniers chapitres nous ont permis de faire des opérations arithmétiques sur des objets avec des entiers et d'afficher leurs résultats par l'intermédiaire de la surcharge des opérateurs.

   Considérons le cas où nous voudrions affecter un nombre d'un type quelconque défini par le langage, à un objet de notre classe ''Nombre'' par exemple, et ceci, sans devoir utiliser la surcharge d'opérateurs. Et bien, cela est impossible ; En effet, nous ne pouvons dans ce cas qu'utiliser des éléments de types semblables.

   Par contre nous pouvons convertir un type défini par le langage en un objet de notre classe Nombrewink

   Voyons ceci avec un simple exemple :

 

      1.1 – Conversion d'un type ''unsigned'' en un objet d'une classe ''Nombre''.

 

   Exemple : Projet 125Transtypage

   Fichier Transtypage.h
 

//Projet 125Transtypage
//Conversion d'une valeur de type unsigned en objet de la classe Nombre
//Fichier Transtypage.h
 
#ifndef DEF_NOMBRE
#define DEF_NOMBRE
 
#include<iostream>
 
class Nombre
{
public:
 
// Constructeurs
Nombre() { _nb = 0; }
Nombre(size_t val) { _nb = val; }
 
// Destructeur
~Nombre() {}
 
//Méthodes membres d'acquisition et d'assignation
size_t get_Valeur() const { return _nb; }
void set_Valeur(size_t val) { _nb = val; }
 
private:
//Donnée membre privée
size_t _nb;
 
};
 
#endif 

 

   Ce fichier header est pratiquement identique au fichier Chap18Ex_2.h du chapitre précédent, les fonctions de surcharge d'opérateurs en moins.

   Retenons pour l'instant toute l'utilité dans ce projet du constructeur surchargé prenant un type défini par le langage en paramètre. Nous allons voir pourquoi à la suite du fichier .cpp. wink


   Fichier Transtypage_MainFile.cpp 

 

//projet 125Transtypage
//Conversion d'une valeur de type unsigned en objet de la classe Nombre
//Fichier : Transtypage_MainFile.cpp
 
#include <iostream>
#include "Transtypage.h"
#include <conio.h>
 
using namespace std;
using uint = unsigned;
 
int main()
{
//Déclaration d'une variable de type unsigned
uint maVariable = 15;
 
//Le constructeur par défaut initialise un objet à 0.
Nombre nombreUn;
//Le constructeur surchargé initialise un second objet à 10
Nombre nombreDeux(10);
 
//On affiche la valeur de maVariable
cout << endl;
cout << "Valeur de la variable 'maVariable de type unsigned = " << maVariable;
cout << endl;
//On affiche les valeurs des objets à l'aide de la méthode
//d'accès publique get_Valeur()
cout << "Valeur de l'objet 'nombreUn' = " << nombreUn.get_Valeur() << endl;
cout << "Valeur de l'objet 'nombreDeux' = " << nombreDeux.get_Valeur() << endl;
 
//Transtypage
cout << endl;
cout << "Maintenant on donne la valeur de maVariable a chaque objet" << endl;
cout << "par un transtypage d'un type unsigned vers chaque objet : " << endl << endl;
nombreUn = maVariable;
nombreDeux = maVariable;
 
//On affiche les valeurs modifiées des objets à l'aide de la méthode
//d'accès publique get_Valeur()
cout << "Valeur de l'objet 'nombreUn' = " << nombreUn.get_Valeur() << endl;
cout << "Valeur de l'objet 'nombreDeux' = " << nombreDeux.get_Valeur() << endl;
 
_getch();
return 0;
}  

 

   Dans la fonction main() on initialise une variable de type unsigned int ainsi que deux objets crées par leur constructeur respectif et on affiche ces valeurs.

   A la suite du commentaire ''//Transtypage'', on affecte les valeurs de la variable ''maVariable'' aux deux objets nombreUn et nombreDeux et ensuite on affiche ces derniers. On compile le projet et... frown ça marche ! laugh Il n'y a pas d'erreur, silence du compilateur. blush

   Vous allez me dire bien sûr que cela marche, on peut bien affecter une valeur à un objet non ? surprise

   Bien entendu, mais pourquoi cela marche-t-il dans ce cas précis ? wink

   Allez, faites un petit essai. Mettez le constructeur surchargé en commentaire dans le fichier Transtypage.h ainsi que toutes les lignes concernant l'objet nombreDeux dans la fonction main() et recompilez le tout. cheeky

   Voilà, le compilateur vous dit maintenant qu'il ne peut pas mélanger des pommes avec des poires. angry

   En effet, ''maVariable'' est de type unsigned et non un objet de type Nombre. ''maVariable'' doit donc préalablement être convertie en objet de type Nombre avant d'être affectée à un autre objet de même type. wink

   Et c'est bien sûr le constructeur surchargé à qui l'on passe un argument de type défini par le langage (ici size_t mais nous aurions pu mettre des unsigned partout dans le fichier header) qui va indiquer au compilateur de réaliser un transtypage d'un type unsigned vers un objet de type Nombre. Et c'est tout ce qu'il y a à dire sur ce sujet. wink

   Ce qui donne comme résultat dans la console :

 

 

      Exercice 1

   Refaites le projet ci-dessus en écrivant les lignes d'affichage des objets nombreUn et nombreDeux sans implémenter des fonctions membres d'accès publique. 

cout << "Valeur de l'objet 'nombreUn' = " << nombreUn << endl;

cout << "Valeur de l'objet 'nombreDeux' = " << nombreDeux << endl;

 

 

   2 – Retour sur le pointeur ''this''

 

   Nous avons parlé du pointeur ''this'' dans le chapitre 18, § 2.4. Les débutants en C++ étant souvent déroutés par la manipulation de ce pointeur, j'en rappellerai ici la notion principale. wink

   Chaque classe possède en effet un pointeur dénommé ''this'' et ce pointeur pointe sur un objet créé par la classe.

   Une fois déréférencé, ce pointeur pointant vers un objet, devient alors l'objet lui-même qui peut-être renvoyé (Voir chapitre 18, § 2.5 – ''Renvoi du pointeur ''this'' déréférencé'').

   Notons également qu'il est automatiquement ajouté à toutes les fonctions membres d'une classe et peut dès lors être utilisé de façon explicite dans une fonction membre. C'est ce que nous allons voir dans le projet suivant. smiley

   Et pour ce faire, nous nous baserons sur le projet 096Class_3 du chapitre 15 mais dans lequel nous n'utiliserons qu'un seul constructeur.

 

      Exemple : Projet 126This

      Fichier This.h  

 

//projet 126This
//Fichier : This.h
//Utilisation explicite du pointeur this dans une méthode membre
#ifndef DEF_126THIS
#define DEF_126THIS
 
#include <iostream>
#include <string>
 
class Player
{
public:
 
//Constructeur par défaut
Player() {}
 
//Destructeur
~Player() {}
 
//Fonctions membres d'accès publique
//Assignation de données
void set_Name(std::string nom) { this->_nom = nom; }
void set_Race(std::string race) { this->_race = race; }
void set_Sex(std::string sexe) { this->_sexe = sexe; }
 
//Lecture des données
void read_Name() const { std::cout << this->_nom; }
void read_Race() const { std::cout << this->_race; }
void read_Sex() const { std::cout << this->_sexe; }
 
private:
//Données membres privées
std::string _nom;
std::string _race;
std::string _sexe;
 
};
#endif 

 

   Dans ce fichier nous utilisons explicitement le pointeur ''this'' afin d'accéder à chaque membre d'un objet Player.

   Utilisé tel quel, le pointeur ''this'' n'a pas beaucoup d'intérêt et on peut parfaitement s'en passer dans les fonctions membres. Le but est d'attirer l'attention du lecteur sur le fait que chaque classe possède un pointeur ''this'' qui pointe sur chaque objet créé par une classe et qu'il est automatiquement passé à chaque fonction membre. En tant que pointeur, il mémorise également l'adresse d'un objet.

 

     Fichier This_MainFile.cpp    

 

//projet 126This
//Fichier : This_MainFile.cpp
//Utilisation explicite du pointeur this dans une méthode membre
 
#include "This.h"
#include <string>
#include <conio.h>
 
using namespace std;
 
int main()
{
//Création d'un objet de type Player
//Utilisation du constructeur par défaut
Player heros;
 
//On assigne les membres de l'objet heros
heros.set_Name("Aria");
heros.set_Race("Elfe");
heros.set_Sex("Feminin");
 
cout << endl;
cout << "Utilisation explicite du pointeur this dans une methode membre" << endl;
 
cout << "On affiche les membres de l'objet 'heros' :" << endl << endl;
 
cout << "Nom : ";
heros.read_Name();
cout << endl;
cout << "Race : ";
heros.read_Race();
cout << endl;
cout << "Sexe : ";
heros.read_Sex();
cout << endl;
 
_getch();
return 0;
} 

 

   Le fichier This_MainFile.cpp se contente quant-à-lui d'afficher les membres de l'objet heros.

   Ce qui donne dans la console :

 

 

 

   3 – Pointeurs sur objets constants et non constants

 

   Pour modifier un objet créé avec un pointeur réservé dans le tas, il faut que le pointeur pointe sur un objet non constant.

   Supposons maintenant que nous désirions créer un pointeur sur un objet constant. Qu'es-ce que cela veut dire ? frown Et bien, si nous reprenons le projet précédent, Aria restera toujours Aria, sa race sera toujours Elfe et son sexe sera toujours féminin, à moins que... frown Mais là alors, je n'ai jamais encore programmé ce genre d'opération ! surprise

   Voyons d'abord ceci dans un nouvel exemple en créant un objet non const :

 

      Exemple : Projet 127ObjetNonConst

      Fichier ObjetNonConst.h  

 

//projet 127ObjetNonConst
//Fichier : ObjetNonConst.h
//Création d'un objet non constant de type Player
#ifndef DEF_127OBJETCONST
#define DEF_127OBJETCONST
 
#include <iostream>
#include <string>
 
 
class Player
{
 
public:
//Constructeur par défaut
Player(std::string nom, std::string race, std::string sexe) : _nom(nom), _race(race), _sexe(sexe) {}
 
//Destructeur
~Player() {}
 
//Fonctions membres d'accès publique
//Méthode d'assignation des données
void set_Name(std::string nom) { _nom = nom; }
void set_Race(std::string race) { _race = race; }
void set_Sex(std::string sexe) { _sexe = sexe; }
 
//Lecture des données
std::string get_Name() const { return _nom; }
std::string get_Race() const { return _race; }
std::string get_Sex() const { return _sexe; }
 
private:
//Données membres privées
std::string _nom;
std::string _race;
std::string _sexe;
 
 
};
#endif 

 

   Par rapport à l'exemple précédent, les membres de l'objet créé sont initialisés dans le constructeur par défaut. Et puisque cela n'est pas nécessaire, nous ne faisons plus explicitement appel au pointeur ''this''.

 

      Fichier ObjetNonConst_MainFile.cpp
 

//projet 127ObjetNonConst
//Fichier : ObjetNonConst_MainFile.cpp
//Création d'un pointeur sur un objet non constant de type Player
 
#include "ObjetConst.h"
#include <string>
#include <conio.h>
 
using namespace std;
 
int main()
{
//Création dans le tas d'un pointeur sur un objet de type Player
cout << endl;
cout << "Creation dans le tas d'un pointeur sur un objet de type Player" << endl;
Player *heros = new Player("Aria", "Elfe", "Female");
 
//On lit les membres de l'objet heros
cout << "Nom : " << heros->get_Name() << endl;
cout << "Race : " << heros->get_Race() << endl;
cout << "Sexe : " << heros->get_Sex() << endl;
 
cout << endl;
 
//On modifie le nom de notre héroïne à l'aide du pointeur heros
//Pas de problème, objet non constant.
heros->set_Name("Thiellawen");
 
//Lecture du nom modifié
cout << "Un membre de l'objet peut etre modifie car l'objet est non constant !";
cout << endl;
cout << "Nom : " << heros->get_Name() << endl;
 
delete heros;
 
_getch();
return 0;
} 

 

    Les commentaires nécessaires se trouvent dans le fichier, et cela est facilement compréhensible. Vous voyez que nous avons pu modifier les membres d'un objet non const en les faisant pointer par un pointeur d'un objet non const.

   Oui mais tu as utilisé un pointeur non const avec lequel tu as pu modifier le nom de l'héroïne et moi je voudrais créer un objet const ne pouvant plus être modifié... Alors ? surprise

   Bonne question, et ceci va faire l'objet de votre second exercice. wink

 

   Exercice 2

   Modifiez le projet précédent en créant un pointeur dans le tas sur un objet non const et un autre sur un objet const. Essayez de modifier un élément des deux objets. wink

 


   4 – Créer une référence sur un objet d'une classe

 

   Dans le chapitre 4 de notre tutoriel C++, nous avons défini une référence de cette façon :

''Contrairement aux pointeurs qui sont considérés comme des objets, les références représentent simplement un autre nom d'un objet déjà existant ou ''alias'' de cet objet. On les appelle également variables d'adresses''.

   Nous avions ensuite montré comment initialiser une référence (voir chapitre 4.2 – Les Références).

   Il en va de même en ce qui concerne les objets issus d'une classe, ceux-ci peuvent être référencés.

   La référence à un objet sera utilisée comme l'objet lui-même et toute modification d'un ou de plusieurs membres d'un objet sera répercutée sur le ou les membres de la référence à cet objet. wink

   Nous allons montrer ceci dans un nouvel exemple :


       Exemple : Projet 128RefObjet

      Fichier RefObjet.h  

 

//projet 128RefObjet
//Fichier : RefObjet.h
//Création d'une référence sur un objet
#ifndef DEF_128REFOBJET
 
#define DEF_128REFOBJET
 
#include <iostream>
#include <string>
 
typedef unsigned uint;
 
class Arc_long
{
public:
//Constructeur par défaut
Arc_long(std::string nom, uint prix, uint portee) : _nom(nom), _prix(prix), _portee(portee) {}
 
//Destructeur
~Arc_long() {}
 
//Fonctions membres d'accès publique
//Méthode d'assignation des données
void set_Name(std::string nom) { _nom = nom; }
void set_Prix(uint prix) { _prix = prix; }
void set_Portee(uint portee) { _portee = portee; }
 
//Lecture des données
std::string get_Name() const { return _nom; }
uint get_Prix() const { return _prix; }
uint get_Portee() const { return _portee; }
 
private:
//Données membres privées
std::string _nom;
uint _prix;
uint _portee;
 
};
#endif 

 

   Le constructeur par défaut prend les nom, prix et portee de l'objet en paramètres. On définit ensuite les fonctions membres d'assignation et d'acquisition des données.


      Fichier RefObjet_MainFile.cpp

  

//projet 127ObjetConst
//Fichier : ObjetConst_MainFile.cpp
//Création d'une référence sur un objet
 
#include "RefObjet.h"
#include <string>
 
#include <conio.h>
 
using namespace std;
 
 
int main()
{
//Création d'un objet de type unArc
cout << endl;
cout << "Creation de l'objet unArc" << endl;
Arc_long unArc("Arc long de l'Elfe du Marais Noir", 230, 55);
 
//On lit les membres de l'objet unArc
cout << "Nom : " << unArc.get_Name() << endl;
cout << "Prix : " << unArc.get_Prix() << endl;
cout << "Portee : " << unArc.get_Portee() << endl;
cout << endl;
 
cout << "On cree une reference de unArc" << endl;
Arc_long &ref_unArc = unArc;
 
//On lit les membres de la référence
cout << "Lecture des membres de la reference :" << endl;
cout << "Nom : " << ref_unArc.get_Name() << endl;
cout << "Prix : " << ref_unArc.get_Prix() << endl;
cout << "Portee : " << ref_unArc.get_Portee() << endl;
cout << endl;
 
//On modifie le prix de l'objet unArc
unArc.set_Prix(250);
cout << "On modifie le prix de l'objet unArc." << endl;
cout << "Celui-ci est egalement modifie dans la reference :" << endl;
cout << "UnArc Prix = " << unArc.get_Prix() << endl;
cout << "ref_unArc Prix = " << ref_unArc.get_Prix() << endl;
 
_getch();
return 0;
} 

 

     Ce qui donne comme résultat dans la console :

 

 

 

   5 – Passage d'objets par référence

 

   Lors de notre étude sur les fonctions nous avons donné des exemples de passage de variables par valeurs ou par références (voir chapitres 7 à 10). Nous avions vu à ce moment que le passage d'une référence sur un tableau ou un vector se faisait bien plus rapidement (donc plus efficacement) que le passage du tableau ou du vector eux-mêmes.

   Il en va de même avec le passage d'objets d'une classe vers une fonction. Imaginez un vector comportant des centaines d'objets de plusieurs membres à passer à l'une ou l'autre fonction. Il est certain que dans ce cas la manière la plus efficace de passer ce vector est de le faire par référence.

   Il faut en outre se rappeler que le passage par valeur d'un objet à une fonction créera d'office une copie de cet objet et qu'à l'appel de cette fonction, le renvoi de cet objet va également impliquer la création d'une autre copie. surprise

   Rappelons-nous également ce que nous avions expliqué au sujet du constructeur de copie dans le chapitre 16 (§ 4.1) Le constructeur de copie va copier un objet à l'identique d'un objet créé par un constructeur par défaut et que le compilateur va se charger lui-même de créer une copie des variables membres d'un objet vers un objet créé à partir du premier.

   Tout ceci est évidemment très lourd pour le compilateur, surtout si nous avons un grand nombre d'objets à passer. indecision

   En recréant la classe Arc_long de notre dernier exemple, nous allons montrer ci-dessous un exemple dans lequel nous créerons une fonction en dehors de la classe. Nous passerons un objet en référence à cette fonction, celle-ci ayant pour but la lecture des membres de l'objet unArc.

 

      Exemple : Projet 129ObjetParReference

      Fichier ObjetParReference.h  

 

//projet 129ObjetParReference
//Fichier : ObjetParReference.h
//Passage d'un objet par référence
 
#ifndef DEF_129OBJET_PAR_REFERENCE
#define DEF_129OBJET_PAR_REFERENCE
 
#include <iostream>
#include <string>
 
typedef unsigned uint;
 
class Arc_long
{
public:
//Constructeur par défaut
Arc_long(std::string nom, uint prix, uint portee) : _nom(nom), _prix(prix),
_portee(portee) {}
 
//Constructeur de copie
Arc_long(Arc_long &) {};
 
//Destructeur
~Arc_long() {}
 
//Fonctions membres d'accès publique
//Méthode d'assignation des données
void set_Name(std::string nom) { _nom = nom; }
void set_Prix(uint prix) { _prix = prix; }
void set_Portee(uint portee) { _portee = portee; }
 
//Lecture des données
std::string get_Name() const { return _nom; }
uint get_Prix() const { return _prix; }
uint get_Portee() const { return _portee; }
 
 
private:
//Données membres privées
std::string _nom;
uint _prix;
uint _portee;
 
};
 
 
//Fonction à laquelle on passe une référence (ici un pointeur) sur un objet
inline
Arc_long *Read_members(Arc_long *myArc)
{
//Lecture des membres de l'objet myArc
std::cout << "Nom : " << myArc->get_Name() << std::endl;
std::cout << "Prix : " << myArc->get_Prix() << std::endl;
std::cout << "Portee : " << myArc->get_Portee() << std::endl;
 
return myArc;
}
 
 
#endif 

 

     A l'intérieur de la classe Arc_long nous déclarons les constructeurs et le destructeur (notez qu'il n'était pas obligé d'ajouter le constructeur de copie car celui-ci ne devant faire qu'une copie de surface, le compilateur se serait chargé d'en créer un lui-même cheeky).

   Suivent nos méthodes d'accès publique en assignation et lecture de données. Nous définissons ensuite une fonction en dehors de la classe, Read_members() qui comme son nom l'indique, va permettre de relire les valeurs des membres d'un objet Arc_long. Vous voyez que nous passons un pointeur sur un objet Arc_long, ceci empêchera le compilateur de créer une copie sur la pile. wink Pour terminer, la fonction retourne également un pointeur sur l'objet, ce qui évite d'appeler le constructeur et le destructeur (Je vous rappelle que lors d'un précédent chapitre, nous avions indiqué que le terme ''Passage par référence incluait aussi bien les pointeurs que les références elles-mêmes wink).


      Fichier ObjetParReference.cpp

 

//projet 129ObjetParReference
//Fichier : ObjetParReference.h
//Passage d'un objet par référence
 
#include "ObjetParReference.h"
#include <string>
#include <conio.h>
 
 
using namespace std;
 
int main()
{
//Création d'un objet de type Arc_long
cout << endl;
cout << "Creation de l'objet unArc" << endl;
Arc_long unArc("Arc long de l'Elfe du Marais Noir", 230, 55);
 
//Copie de unArc dans autreArc
cout << "Creation de l'objet autreArc par copie de unArc..." << endl;
Arc_long &autreArc(unArc);
 
cout << endl;
 
//Appel de Read_members
cout << "Appel de la fonction Read_members a laquelle on passe une reference sur l'objet" << endl;
cout << "Lecture des membres de l'objet autreArc..." << endl << endl;
Read_members(&autreArc);
cout << endl;
 
_getch();
return 0;
} 

 

   Dans la fonction main(), on crée un objet de type Arc_long ainsi que (pourquoi pas ? wink) une copie de cet objet.

   On appelle ensuite la fonction Read_members() qui affichera les valeurs des membres de l'objet passé en référence.

   C'est tout pour cet exemple mais nous devons tout de même faire une remarque sur celui-ci :

Dans la fonction Read_members() écrite telle quelle dans notre exemple, rien ne nous empêcherait en effet, par exemple, d'écrire comme première instruction :

myArc->set_Name("Arc de Robin des Bois");

   Ce qui aurait pour effet de remplacer l'''arc long de l'Elfe du Marais Noir'' par celui de notre héros Saxon, ce que nous n'aurions pas prévu au départ. angry

   Nous allons voir comment remédier à cela dans le projet suivant angel :

 

 

   6 – Passage d'un pointeur sur un objet constant

 

      Exemple : Projet 130ObjetParReference_2

      Fichier ObjetParReference_2.h

 

   Vu que nous n'avons qu'une modification à faire dans le fichier ObjetParReference.h, nous ne réécrirons ici que cette modification. J'ai réécrit le projet entier sous le nom 130ObjetParReference_2 que vous pourrez télécharger si vous le désirez.

   Fonction modifiée :   

 

//Fonction à laquelle on passe une référence (ici un pointeur) sur un objet constant
inline
const Arc_long *Read_members(const Arc_long *myArc)
{
//Lecture des membres de l'objet myArc
std::cout << "Nom : " << myArc->get_Name() << std::endl;
std::cout << "Prix : " << myArc->get_Prix() << std::endl;
std::cout << "Portee : " << myArc->get_Portee() << std::endl;
 
return myArc;
} 

 

   Puisque nous voulons éviter toute modification dans la fonction, nous devons cette fois passer un pointeur sur un objet constant à la fonction et bien entendu celle ci doit renvoyer ce pointeur, d'où l'ajout des deux qualificateurs ''const''. smiley

 

 

   7 – Passage d'une référence sur un vector de strings

 

   Cette fois nous allons passer une référence sur un vecteur de chaînes. Nous utiliserons une référence car il est communément admis par les programmeurs C++ que les références sont moins ''lourdes'' et plus faciles à implémenter que les pointeurs. angel

   Pour cet exemple, nous allons créer une classe ''Arme_tranchante'' qui initialisera un vector de strings comportant deux membres au départ.

   Cependant nous implémenterons deux fonctions de lecture et d'écriture de données : deux fonctions membres de la classe ajouter_arme() et lire_arme() ainsi que deux fonctions inline extérieure à la classe, ajouter_arme() et lire_arme(). Nous allons voir les différences de chaque type de fonction. cool

 

      Exemple : Projet 131RefSurVectorStrings

      Fichier RefSurVectorStrings.h  

 

//projet 131RefSurVectorStrings
//Fichier : RefSurVectorStrings.h
//Passage d'une référence sur un vector de strings
 
#ifndef DEF_REFSURVECTORSTRINGS
#define DEF_REFSURVECTORSTRINGS
 
#include <iostream>
#include <string>
#include <vector>
 
class Arme_tranchante
{
public:
//Constructeur par défaut à qui l'on passe une référence sur un vector string
Arme_tranchante(std::vector<std::string>&noms)
{
noms.push_back("Claymore de fer");
noms.push_back("Epee longue en ebonite");
}
 
//Destructeur
~Arme_tranchante() {}
 
//FonctionS membres
void ajouter_arme(std::vector<std::string>&noms)
{
noms.push_back("Dague de plaie sanglante");
}
 
void lire_arme(std::vector<std::string>&noms) const
{
for (auto &i : noms)
std::cout << i << std::endl;
}
 
private:
 
};
 
 
 
//Fonction inline d'acquisition des données à laquelle on passe une référence sur un vector
//string
inline
Arme_tranchante &lire_arme(Arme_tranchante &monArme, std::vector<std::string>&noms)
{
for (auto &i : noms)
std::cout << i << std::endl;
return monArme;
}
 
 
//Fonction inline d'ajout de données à laquelle on passe une référence sur un vector string
inline
Arme_tranchante &ajouter_arme(Arme_tranchante &monArme, std::vector<std::string>&noms)
{
noms.push_back("Hache d'armes orque");
return monArme;
}
 
 
#endif 

 

   Nous commençons par implémenter le constructeur par défaut à qui l'on passe un vector de strings en paramètre et qui ajoute les noms de deux armes de type Arme_tranchante dans le vector.

   A la suite du destructeur, nous définissons deux fonctions membres en ajout et en lecture de données.

   L'implémentation du corps de la méthode membre lire_arme() nous est déjà connue, ce code ayant été apporté par le standard C++ 11 peut se traduire par :

{

Pour (chaque membre du vector noms, référencé par la variable i)

Afficher le nom de ce membre ;

}

   Nous implémentons ensuite deux fonctions inline en dehors de la classe. L'une pour afficher les données du vector et l'autre pour en acquérir.

   Comparons deux fonctions. Par exemple la fonction membre lire_arme() avec la fonction inline du même nom extérieure à la classe. Nous passons une référence sur un vector de strings à chacune d'entre-elles mais la fonction extérieure à la classe a besoin de connaître l'adresse de l'objet sur lequel elle doit agir, c'est pourquoi une référence à cet objet lui est également passée en paramètre. wink

   Cette fonction, bien que réalisant la même action que la fonction membre du même nom est donc plus lourde à implémenter et la fonction membre est donc celle à préférer dans ce cas d'exemple.

   Les fonctions qui doivent être impérativement définies en dehors de la classe sont par exemple des fonctions de surcharge de certains opérateurs dont les opérateurs d'entrées/sorties operator<< et operator>> que nous avons vus dans le chapitre 20. wink


      Fichier RefSurVectorStrings.cpp  

 

//projet 131RefSurVectorStrings
//Fichier : RefSurVectorStrings.h
//Passage d'une référence sur un vector de strings
 
#include "RefSurVectorStrings.h"
#include <conio.h>
 
using namespace std;
 
int main()
{
//Déclaration d'un vector vide
vector<string>armes;
 
//Création d'un objet de type Arme_tranchante
//à l'aide du constructeur par défaut à qui l'on passe un vector de strings
Arme_tranchante uneArme(armes);
 
cout << endl;
cout << "Passage de references sur un vector string de deux fonctions inline" <<
endl;
cout << "Le constructeur ajoute 2 armes dans le vector" << endl << endl;
 
//Appel de la fonction membre lire_arme()
cout << "Appel de la fonction membre lire_arme()" << endl;
cout << "Les noms lus dans le vector sont : " << endl;
uneArme.lire_arme(armes);
cout << endl;
 
//Appel de la fonction membre ajouter_arme() puis de la fonction inline
//ajouter_arme
cout << "Appel de la fonction membre ajouter_arme()" << endl;
cout << "puis de la fonction inline ajouter_arme() " << endl << endl;
uneArme.ajouter_arme(armes);
ajouter_arme(uneArme, armes);
 
//Appel de la fonction inline Lire_arme()
cout << "Appel de la fonction inline Lire_arme()" << endl;
cout << "Les noms lus dans le vector sont : " << endl;
lire_arme(uneArme, armes);
 
 
cin.get();
return 0;
} 

 

   Il n'y a pas grand-chose à dire ici, tout ce qui est nécessaire est commenté dans la fonction main().

   Remarquez simplement que l'appel aux fonction membres lire_arme() et ajouter_arme() se fait bien entendu à partir de l'objet uneArme, créé au début du main() par la classe Arme_tranchante.

   Ce qui donne comme résultat dans la console :

 

 

   Remarque :

   Il n'est pas interdit de mélanger pointeurs et références dans le prototype d'une fonction. Ainsi notre fonction inline lire_Arme() aurait pu s'écrire :

inline

Arme_tranchante *lire_arme(Arme_tranchante &monArme, std::vector<std::string>&noms)

{

for (auto &i : noms)

std::cout << i << std::endl;

return &monArme;

}

 

   ainsi que la fonction inline ajouter_arme() :

 

inline

Arme_tranchante *ajouter_arme(Arme_tranchante &monArme, std::vector<std::string>&noms)

{

noms.push_back("Hache d'armes orque");

return &monArme;

}

 

   Ce n'est bien entendu pas nécessaire d'ajouter de la complication (et risques d'erreurs supplémentaires) à ce type d'implémentation. Si néanmoins vous vous sentez une âme de voltigeur conscient des risques que vous prenez, je ne pourrai certes pas vous en blâmer. laugh

   Voila, je crois que l'on peut passer aux corrigés des exercices du chapitre 20.

 

 

   8 – Corrigés des exercices du chapitre 20


   Exercice 1 ( projet Chap20Exercice_1)

 

   Dans cet exercice on demandait de refaire l'exercice 2 du chapitre 19 qui demandait d'écrire un programme qui affiche le résultat de l'expression suivante :

resultat = 10 + objet1 + 20 + objet2 – 15

   Il fallait ajouter une fonction amie operator<<() pour l'affichage du résultat et une fonction amie operator>>() pour acquérir les valeurs des objets ''objet1'' et ''objet2'' au clavier.

 

   Le code :

      Fichier chap20Ex_1.h  

 

//Chap20Exercice_1
//Surcharge d'opérateurs
//Fichier Chap20Ex_1.h
 
#include <iostream>
 
class Nombre
{
public:
//Constructeurs
Nombre() { _valeur = 0; }
Nombre(int val) { _valeur = val; }
 
//Fonction Affiche()
void Affiche() const { std::cout << _valeur; }
 
//Methode membre d'acquisition
size_t get_Val() { return _valeur; }
 
//Opérateurs
Nombre Nombre::operator+ (const size_t val) //Utilisation : objet1 + 20
{
Nombre temp;
temp._valeur = _valeur + val;
return temp;
}
 
Nombre Nombre::operator+ (Nombre &rhs) //Utilisation : ... + objet2
{
Nombre temp;
temp._valeur = _valeur + rhs._valeur;
return temp;
}
 
Nombre Nombre::operator- (const size_t val) //Utilisation : objet2 - 15
{
Nombre temp;
temp._valeur = _valeur - val;
return temp;
}
 
Nombre Nombre::operator= (const Nombre rhs) //Utilisation : objet3 = ...
{
if (this == &rhs)
return *this;
else
{
_valeur = rhs._valeur;
return *this;
}
}
 
//Déclaration des fonctions amies
const friend Nombre operator+ (const size_t val, const Nombre &rhs); //Utilisation :
//10 + objet1
friend Nombre operator+ (const Nombre &rhs, const size_t val);
friend std::ostream &operator<< (std::ostream &out, Nombre &rhs);
friend std::istream &operator>> (std::istream &in, Nombre &rhs);
 
 
private:
size_t _valeur;
 
};
 
 
 
//Définition des fonctions amies
const Nombre operator+ (const size_t val, const Nombre &rhs)
{
Nombre temp;
temp._valeur = val + rhs._valeur;
return temp;
}
 
 
Nombre operator+ (const Nombre &rhs, const size_t val)
{
Nombre temp;
temp._valeur = val + rhs._valeur;
return temp;
}
 
 
std::ostream &operator<< (std::ostream &out, Nombre &rhs)
{
out << rhs.get_Val();
return out;
}
 
 
std::istream &operator>> (std::istream &in, Nombre &rhs)
{
in >> rhs._valeur;
return in;
} 
 

 

   Par rapport à l'exercice 2 du chapitre 19, nous ajoutons ici deux fonctions amies operator<<() et operator>>(). Nous avons étudié l'implémentation de ces fonctions dans le chapitre 20, § 1.2 et 1.3 de ce chapitre.

   Je vous rappellerai quand-même que les fonctions amies de surcharge des opérateurs d'entrées/sorties doivent être déclarées dans le corps de la classe et définies en dehors de celle-ci. wink


      Fichier chap20Ex_1MainFile.cpp  

 

 

//Chapitre20Exercice_1
//Surcharge d'opérateurs
//Fichier Chap20Ex_1MainFile.cpp
 
#include <conio.h>
#include "Chap20Ex_1.h"
 
using namespace std;
 
 
int main()
{
Nombre objet1;
Nombre objet2;
Nombre resultat;
 
cout << endl;
cout << "Creation des objets objet1, objet2 et resultat" << endl << endl;
cout << "Valeur de 'resultat' = " << resultat << endl;
cout << "Valeur de objet1 = " << objet1 << endl;
cout << "Valeur de objet2 = " << objet2 << endl << endl;
 
cout << "Entrez une nouvelle valeur de objet1 : ";
cin >> objet1;
cout << "Entrez une nouvelle valeur de objet2 : ";
cin >> objet2;
 
resultat = 10 + objet1 + 20 + objet2 - 15;
 
cout << endl;
cout << "resultat = 10 + objet1 + 20 + objet2 - 15" << endl;
cout << "resultat = " << resultat;
 
 
_getch();
return 0;
} 

 

   Dans le fichier Chap19Ex_2Mainfile.cpp nous étions obligés d'utiliser une fonction membre d'affichage du résultat, n'ayant pas encore parlé de la surchage des opérateurs ''<<'' et ''>>''.

  Ici, nous utilisons la surcharge en entrée par les instructions :

cin >> objet1 et cin >> objet2;

   et en sortie, par les instructions :

cout << "resultat = 10 + objet1 + 20 + objet2 - 15" << endl;
cout << "resultat = " << resultat;

 


      Exercice 2 (un petit challenge – Achat d'items à un marchand)

 

   Vous pouvez vous reporter à l'énoncé de cet exercice dans le chapitre 20 si vous le désirez mais pour plus de facilité nous écrirons néanmoins ici le résultat à atteindre.

   On doit donc additionner deux objets (items saisis au clavier) dont les noms doivent être identiques. Chaque item que nous supposons être une petite potion de vie comporte trois champs :

- Le nom
- La quantité achetée
- Le coût d'un item

   Un test sur le type (nom entré au clavier) sera réalisé. Si ces noms sont identiques, les calculs suivants vont se faire :

- Total des items achetés pour les deux transactions
- Le coût d'achat total
- Le prix unitaire moyen d'une potion (intéressant bien entendu si nous entrons un prix différent pour chaque achat)
- Pour terminer nous devrons afficher ces résultats.

   Si les noms d'items entrés sont différents, le programme le signalera et nous devrons quitter celui-ci.

 

   Le code :

      Fichier chap20Ex_2.h  

 

//Projet : Chap20Exercice_2
//Addition d'objets comportant plusieurs membres
//Fichier : Chap20Ex_2.h
 
#ifndef ACHAT_ITEM_H
#define ACHAT_ITEM_H
 
#include <iostream>
#include <string>
 
typedef unsigned uint;
 
class Achat_item
{
public:
 
//Constructeur par défaut
Achat_item() : _unites_Achetees(0), _valeur_Totale(0.0) {}
 
// Fonctions membres
double prix_moyen() const;
std::string isItem() const { return _type_item; }
 
//Fonction operator+=()
Achat_item &operator+=(const Achat_item&);
 
//fonctions amies
friend std::istream &operator>>(std::istream&, Achat_item&);
friend std::ostream &operator<<(std::ostream&, const Achat_item&);
friend bool operator==(const Achat_item&, const Achat_item&);
 
private:
std::string _type_item;
uint _unites_Achetees;
double _valeur_Totale;
double _prix_unitaire;
};
 
 
//Operateur d'égalité operator==
bool operator == (const Achat_item &lhs, const Achat_item &rhs)
{
return lhs.isItem() == rhs.isItem();
}
 
 
//Opérateur d'inégalité operator!=
bool operator != (const Achat_item &lhs, const Achat_item &rhs)
{
return !(lhs == rhs);
}
 
 
// Cette fonction suppose que les deux objets se réfèrent au même type
Achat_item& Achat_item::operator+=(const Achat_item& rhs)
{
_unites_Achetees += rhs._unites_Achetees;
_valeur_Totale += rhs._valeur_Totale;
return *this;
}
 
 
// opérateur binaire ''+'' non membre : doit déclarer un paramètre pour chaque opérande
// Cette fonction suppose que les deux objets se réfèrent au même type
Achat_item operator+ (const Achat_item& lhs, const Achat_item& rhs)
{
Achat_item local(lhs);
local += rhs;
return local;
}
 
 
//Operateur d'entrée operator>>
std::istream &operator>>(std::istream &in, Achat_item &it)
{
double prix_achat;
std::cout << "Type item : ";
in >> it._type_item;
std::cout << "Unites achetees : ";
in >> it._unites_Achetees;
std::cout << "Prix achat : ";
in >> prix_achat;
 
// Test sur la validité des entrées
if (in)
it._valeur_Totale = it._unites_Achetees * prix_achat;
else
it = Achat_item(); // Erreur sur les entrées, on réinitialise l'opération
return in;
}
 
 
//Opérateur de sortie operator<<
std::ostream &operator<<(std::ostream& out, const Achat_item& it)
{
out << "Nom item : " << it.isItem() << std::endl;
out << "Quantite achetee : " << it._unites_Achetees << std::endl;
out << "Cout total : " << it._valeur_Totale << std::endl;
out << "Prix unitaire moyen : " << it.prix_moyen() << std::endl;
 
return out;
}
 
 
//Fonction membre de calcul du prix moyen
double Achat_item::prix_moyen() const
{
if (_unites_Achetees)
return _valeur_Totale / _unites_Achetees;
else
return 0;
}
 
#endif 

 

    Bien, il y a pas mal de choses à dire sur ce fichier header mais nous allons le faire pas-à-pas. wink

   On peut maintenant commenter notre classe Achat_item.

   Les variables privées dont nous avons besoin sont :

- Le type de l'item : std::string _type_item;

- La quantité achetée : uint _unites_Achetees;

- Le coût total des achats : double _valeur_Totale;

- Le prix unitaire : double _prix_unitaire;

 

   Nous commençons par créer notre constructeur qui initialise le nombre d'unités achetées et la valeur totale à zéro.

   Une fonction membre prix_moyen() et une fonction isItem() sont ensuite déclarées. Cette dernière retournant le type de l'item saisi, servira à la comparaison du nom des items des deux transactions.

   Nous savons que nous allons effectuer des opérations arithmétiques sur des objets donc nous avons besoin d'opérateurs surchargés. Voyons ceux-ci :

- Déclaration de la fonction operator+=() nécessaire pour l'addition des unités achetées et la mise à jour de leur valeur totale. Cette fonction sera définie en dehors de la classe.

- Suivent les déclarations de fonctions amies de surcharge des opérateurs I/O ''>>'' et ''<<'' ainsi que de la déclaration de la fonction amie de l'opérateur de comparaison ''<'' et de la fonction amie de surcharge d'égalité ''= =''. Ces fonctions amies, nous l'avons vu, doivent être définies en dehors de la classe.

- La fonction bool operator == (const Achat_item &lhs, const Achat_item &rhs) retourne ''vrai'' s'il y a égalité parfaite entre les noms entrés. ''lhs'' (left-hand side) et ''rhs'' (right-hand side) représentant respectivement les membres de gauche et de droite de l'opérateur d'égalité ''= =''.

- La fonction bool operator != (const Achat_item &lhs, const Achat_item &rhs) quant-à-elle retournera également l'état des noms entrés si ceux-ci sont inégaux.

- Vient ensuite la définition de la fonction Achat_item& Achat_item::operator+=(const Achat_item& rhs) dont nous avons vu la déclaration un peu plus haut. L'opérateur+= devant mettre à jour la valeur de l'argument de gauche avec la valeur de l'argument de droite, ceci se fait en renvoyant l'objet courant pointé par this. Celui-ci étant un pointeur et non une valeur nous retournons donc le pointeur déréférencé *this.

- Et pour suivre nous voyons la définition de la fonction operator+() nécessaire à l'addition de deux objets : Achat_item operator+ (const Achat_item& lhs, const Achat_item& rhs). Dans l'implémentation de cette fonction, on initialise une variable locale à qui l'on passe la valeur du membre de gauche de l'opération, soit lhs. On affecte ensuite le membre de droite (rhs) à cette variable locale et la fonction retourne celle-ci avant qu'elle ne sorte de son domaine de définition.

- Viennent ensuite nos fonctions de surcharge des opérateurs I/O dont nous venons de parler dans le chapitre précédent.

- La fonction operator>>() permet la saisie des membres de l'objet à lécran et fait un test sur l'état du flux d'entrée istream. Si celui-ci est ''vrai'' on calcule la valeur totale des achats effectués, sinon l'opération est réinitialisée par appel au constructeur.

- La fonction operator<<(), quant-à-elle, affiche simplement les résultats effectués sur les deux transactions.

- Pour terminer, la fonction membre prix_moyen() retourne, comme son nom l'indique, la moyenne du prix d'un vial si nous avons entré un prix différent pour chaque transaction sinon le prix moyen restera bien entendu le prix saisi pour une transaction si les prix sont les mêmes pour les deux achats.

 

      Fichier chap20Ex_2.MainFile.cpp

 

//Projet : Chap20Exercice_2
//Addition d'items comportant plusieurs membres
//Fichier : Chap20Ex_2MainFile.cpp
 
#include <iostream>
#include <conio.h>
#include "Chap20Ex_2.h"
 
using namespace std;
 
int main()
{
Achat_item item1, item2;
cout << endl;
 
cout << "Entrez les caracteristiques de l'item (nom, quantite, prix)" << endl;
cout << "Un nom d'item comportant plusieurs mots devra etre relie par des" << endl;
 
cout << "caracteres de soulignement ! Ex : Petite_potion_de_regeneration" << endl << endl;
cout << "Realisation de deux transactions (Les noms d'item doivent etre identiques !)" << endl << endl;
 
//Ecriture de deux transactions
cout << "Entrez la premiere transaction :" << endl;
cin >> item1;
cout << endl;
 
cout << "Entrez la seconde transaction :" << endl;
 
cin >> item2;
cout << endl;
 
// Test sur l'égalité des deux noms d'item
if (item1.isItem() == item2.isItem())
{
cout << "Check item type : items identiques" << endl;
cout << "Calcul du total des achats et prix moyen d'un vial : " << endl;
cout << item1 + item2 << endl;
 
_getch();
 
return 0; //Opération réussie
}
else
{
cerr << "Check item type : items non identiques !..." << endl;
_getch();
 
return -1; //Erreur !
}
} 

 

   Dans la fonction main(), le constructeur crée deux objets item1 et item2. La fonction demande ensuite l'entrée des deux transactions.

   Un test sur l'égalité du nom des objets est fait. Si ceux-ci sont identiques, l'addition des objets peut être réalisée et leur affichage se faire à l'aide de l'opérateur surchargé ''<<''.

   Si les noms entrés sont différents, nous l'affichons sous la forme d'un message d'erreur.

   Et le résultat à obtenir dans la console vous avait été montré lors de l'écriture de l'énoncé dans le chapitre 20. wink

   Voila, c'est tout pour ce chapitre. cool

   Avant de passer à l'étude de la dérivation de classes (Héritage) et d'entrer ainsi plus en profondeur dans la découverte de la POO, j'ai pensé à introduire auparavant l'étude des conteneurs séquentiels de la STL (STANDARD TEMPLATE LIBRARY). Ceci vous permettra d'approfondir les notions déjà vues de vectors, strings et itérateurs et de découvrir d'autres types de conteneurs. 

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

      @ bientôt pour le chapitre 22 – Les conteneurs séquentiels (1).                                                                     

            Gondulzak.  angel
 
 
 

Connexion

CoalaWeb Traffic

Today104
Yesterday126
This week230
This month3904
Total1743111

23/04/24