====== TD : Premiers pas avec C++ ====== ===== Première Partie :Installation d'un environnement C++ ===== ==== Etape 1 : Installation de l'environnement ==== En vous référant à la [[..:outils|documentation disponible]] sous ce lien et en n'hésitant pas à demander à l'enseignant de vous accompagner, installer un environnement de développement sur votre ordinateur. ==== Etape 2 : Compilation d'un exemple simple de classe C++ === En fonction de l'environnement, essayer de créer un projet "console" et ensuite ajouter le code suivant dans ce projet. * Fichier ''rational.hpp''\\ #ifndef rationalHPP #define rationalHPP #include #include #include class rational { private: long m_numerator; long m_denominator; template friend inline std::basic_ostream& operator << ( std::basic_ostream& aStream, const rational& aRational); public: rational() : m_numerator(0), m_denominator(1) {} rational(long theNumerator) : m_numerator(theNumerator), m_denominator(1) {} rational(long theNumerator, long theDenominator) { if (theDenominator == 0) throw std::invalid_argument("denominator is 0"); if (theDenominator < 0) { theNumerator *= -1; theDenominator *= -1; } m_numerator = theNumerator; m_denominator = theDenominator; } rational(const rational& aRational): m_numerator(aRational.m_numerator), m_denominator(aRational.m_denominator) {} operator double() const { return (double)m_numerator / (double)m_denominator; } rational operator + (const rational& anotherRational) const { return rational(m_numerator * anotherRational.m_denominator + m_denominator * anotherRational.m_numerator, m_denominator * m_numerator); } rational operator - (const rational& anotherRational) const { return rational(m_numerator * anotherRational.m_denominator - m_denominator * anotherRational.m_numerator, m_denominator * m_numerator); } rational operator * (const rational& anotherRational) const { return rational(m_numerator * anotherRational.m_numerator, m_denominator * m_numerator); } rational operator / (const rational& anotherRational) const { return rational(m_numerator * anotherRational.m_denominator - m_denominator * anotherRational.m_numerator, m_denominator * m_numerator); } }; template inline std::basic_ostream& operator << ( std::basic_ostream& aStream, const rational& aRational) { aStream << aRational.m_numerator; if (aRational.m_denominator != 1) aStream << "/" << aRational.m_denominator; return aStream; } #endif * Fichier ''main.cpp''\\ #include"rational.hpp" #include int main() { try { rational valueA = { 1, 0 }; } catch (std::exception& e) { std::cout << e.what() << std::endl; } rational valueA = { 4, 3 }; rational valueB = { 3, 4 }; std::cout << "A + B = " << (valueA + valueB) << std::endl; std::cout << "A - B = " << (valueA - valueB) << std::endl; std::cout << "A * B = " << (valueA * valueB) << std::endl; std::cout << "A / B = " << (valueA / valueB) << std::endl; std::cout << "4 * B = " << (4 * valueB) << std::endl; return 0; } Assurez vous que le projet compile et s'exécute correctement. Il suffit de vérifier que le programme compile et s'exécute correctement. ===== Seconde partie : Création d'une classe "Compteur" en C++ ===== ==== Question 2.1 ==== Créer un projet "console" vide dans votre environnement de développement. Cela dépend de votre environnement. ==== Question 2.2 ==== Ajoutez un fichier ''Counter.h'' ou ''Counter.hpp'' ou ''Counter.hh'' dans votre fichier et introduisez la structure de la classe ''Counter'' dans ce fichier. class MyCounter { public: unsigned counter; unsigned max; }; A nouveau, il suffit de créer les deux fichiers. Par contre, il est important de s'assure que le contenu d'un fichier entête (ie. .h, .hh ou .hpp) ne puissent pas être compilé deux fois même s'il est chargé plus d'une fois. Pour ce faire, il est de bonne pratique d'utiliser la structure suivante : #ifndef nameHPP #define nameHPP // contenu de votre fichier. #endif où ''name'' correspond au nom de votre fichier, "HPP" pour l'extension '.hpp', "H" pour l'extension '.h', ainsi de suite. La première fois que le fichier est chargé par le compilateur, l'identifiant ''nameHPP'' n'est pas défini, on analyse le code situé après la directive ''#ifndef''. La première ligne suivante : ''#define nameHPP'' définit l'identifiant ''nameHPP'' et puis ensuite le contenu du fichier est analysé. Si on charge une seconde fois le fichier, l'identifiant ''nameHPP'' est défini, donc le contenu situé entre la directive ''#ifndef nameHPP'' et la directive ''#endif'' est ignoré. Cela empêche d'avoir des erreurs liées au fait que l'on charge le fichier d'entête plus d'une fois. === Question 2.3 === Ajoutez les fonctions membres à votre compteur : class MyCounter { public: ... void increment() { counter ++; if(counter > max) counter = 0; } void reset() { counter = 0; } }; Expliquer ce que font ces fonctions membres. La première fonction augmente le compteur de un en un tant que la valeur maximale n'est pas atteinte. Si la valeur maximale est atteinte, il recommance à compter à partir de la valeur 0. La seconde fonction remet le compteur à 0. === Question 2.4 === Testez le code avec le programme ''main.cpp'' suivant : #include #include "counter.hpp" void testCounter() { MyCounter counterA; counterA.counter = 0; counterA.max = 2; MyCounter counterB; counterB.counter = 0; counterB.max = 4; for(int i = 0; i < 4; i++) { std::cout << "compteur A: (" << counterA.counter << ", " << counterA.max << ")" << std::endl; std::cout << "compteur B: (" << counterB.counter << ", " << counterB.max << ")" << std::endl; counterA.increment(); counterB.increment(); } } int main() testCounter(); return 0; } Expliquer le comportement des compteurs. Le ''counterA'' a été créé avec ''count'' à 0 et ''max'' à 2. Le ''counterB'' a été créé avec ''count'' à 0 et ''max'' à 4. Incrémenter le compteur ''counterA'' va générer la séquence : 0, 1, 2, 0, ... Incrémenter le compteur ''counterB'' va générer la séquence : 0, 1, 2, 3, ... ===== Troisième partie : Les constructeurs ===== Nous voyons dans le code précédent que nous créons un ''compteur'' et que nous l'initialisation en mettant à jour les valeurs ''counter'' et ''max''. Cependant, ceci n'est pas optimal pour plusieurs raisons: * nous avons un objet qui n'a pas été initialisé du tout. * nous avons un objet qui peut-être partiellement initialisé, seul le champ ''max'' ou le champ ''counter'' a été initialisé, * nous avons un objet qui est mal initialisé, par exemple la valeur du chmap ''max'' est inférieur à la valeur du ''counter'', ce qui ne doit jamais se produire. ==== Question 3.1 ==== Vérifier le problème de la mauvaise initialisation d'un objet en exécutant le code suivant. void createArrayOfCounters() { MyCounter counters[4]; for(int i = 0, i < 4; i ++) std::cout << i << " : " << counters[i].counter << "/" << counters[i].max << std::endl; } void main() { createArrayOfCounters(); return 0; } Vous constatez que les valeurs des champs ''count'' et ''max'' ont des valeurs qui sortent de nul part. ==== Question 3.2 ==== Proposer des constructeurs pour les compteurs pour s'assurer que le compteur est bien initialisé. On proposera au moins deux constructeurs : * un constructeur qui met ''counter'' à 0 et ''max'' à une valeur maximale passée en paramètre et strictement supérieure à 0, * un constructeur qui met ''counter'' et ''max'' à des valeurs passées en paramètre. Cependant si ''counter'' est supérieur à ''max'' on met ''counter'' à 0. === Question 3.2.1 === Ajouter ces constructeurs à votre classe. Nous ajoutons les deux constructeurs comme suit: class MyCounter { public: int counter; int max; explicit MyCounter(int theMaxValue): count(0), max(theMaxValue) {} MyCounter(int theCounter, int theMaxValue): count(theCounter), max(theMaxValue) {} void increment() { counter ++; if(counter > max) counter = 0; } void reset() { counter = 0; } }; Comme nous ne souhaitons pas une conversion automatique d'un entier en compteur, nous devons mettre ''explicit'' pour empêcher que le compilateur utiliser le constructeur ''MyCounter(int)'' comme constructeur de conversion lui permettant de convertir un entier en un objet de type ''MyCounter''. === Question 3.2.2 === Modifier la fonction ''testCounter'' pour utiliser ces constructeurs afin de créer et d'initialiser les objets ''counterA'' et ''counterB''. Le code est simplifier, puisqu'il suffit de passer au constructeur la valeur maximale du compteur, supprimant les initialisations des champs ''counter'' et ''max'' qui ne sont désormais plus nécessaires et souhaitables. void testCounter() { MyCounter counterA(2); MyCounter counterB(4); for(int i = 0; i < 4; i++) { std::cout << "compteur A: (" << counterA.counter << ", " << counterA.max << ")" << std::endl; std::cout << "compteur B: (" << counterB.counter << ", " << counterB.max << ")" << std::endl; counterA.increment(); counterB.increment(); } } ===== Quatrième partie : Rendre publique/privée des champs et fonctions ===== Nous voyons bien que les champs ''counter'', ''max'' sont des champs qui ne devraient pas pouvoir être modifié accidentellement puisque cela modifie le comportement du compteur. Par exemple, le code suivant conduit à un comportement erratique : int main() { MyCounter counterA; counterA.counter = 0; counterA.max = 10; for(int i = 0; i < 4; i++) { std::cout << "compteur A: (" << counterA.counter << ", " << counterA.max << ")" << std::endl; counterA.increment(); counterA.max -= 2; } return 0; Dans ce cas, il faut rendre les champs uniquement accessible en lecture et non pas en lecture et écrire. Pour ce faire, il suffit de : * rendre les champs privés (on doit transformer la déclaration de l'objet ''MyCounter'' de ''struct'' en ''class''. * définir dans cette classe au moins une section ''public'' et une section ''private'':\\ class MyCounter { private: ... // section privée public: ... // section publique }; * mettre les champs privés et définir des fonctions pour accéder aux valeurs. Nous appellons ces fonctions des accesseurs. Souvent en lecture elle commence par ''get'' suivit du nom de la variable, en écriture, par ''set'' et le nom de la variable. Ici, nous proposons de définir les fonctions ''getCounter'' et ''getMax'' qui elles seront publiques. ==== Question 4.1 ==== Modifier votre classe en rendant les champs ''max'' et ''counter'' privés et en déclarant des méthodes d'accès ''getCounter'' et ''getMax''. Il suffit de déplacer les deux champs ''counter'' et ''max'' dans la section privée ''private'' de la classe et d'ajouter les méthodes ''int getCounter() const'' et ''int getMax() const'' qui retourne une copie du champ ''counter'' et du champ ''max''. On ajoute le mot clé ''const'' pour indiquer que la méthode ne modifie aucun champ de l'objet et donc ne modifie pas l'objet. class MyCounter { private: int counter; int max; public: explicit MyCounter(int theMaxValue): count(0), max(theMaxValue) {} MyCounter(int theCounter, int theMaxValue): count(theCounter), max(theMaxValue) {} int getCounter() const { return counter; } int getMax() const { return max; } void increment() { counter ++; if(counter > max) counter = 0; } void reset() { counter = 0; } }; ==== Question 4.2 ==== Vérifier que la compilation de la fonction ''testCounter'' génère des erreurs. Le compilateur indique qu'il n'est pas possible d'accéder aux champs ''counter'' et ''max''. ==== Question 4.3 ==== Modifier la fonction ''testCounter'' pour désormais appeller les méthodes d'accès. Vérifier le bon fonctionnement de la fonction ''testCounter''. Il suffit de remplacer les accès aux champs ''max'' et ''counter'' par des appels aux méthodes ''getMax()'' et ''getCounter()''. void testCounter() { MyCounter counterA(2); MyCounter counterB(4); for(int i = 0; i < 4; i++) { std::cout << "compteur A: (" << counterA.getCounter() << ", " << counterA.getMax() << ")" << std::endl; std::cout << "compteur B: (" << counterB.getCounter() << ", " << counterB.getMax() << ")" << std::endl; counterA.increment(); counterB.increment(); } }