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 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<exception>
#include<stdexcept>
#include<iostream>
class rational
{
private:
long m_numerator;
long m_denominator;
template<typename charT, typename charTraits>
friend inline std::basic_ostream<charT, charTraits>& operator << (
std::basic_ostream<charT, charTraits>& 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<typename charT, typename charTraits>
inline std::basic_ostream<charT, charTraits>& operator << (
std::basic_ostream<charT, charTraits>& 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<iostream>
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<iostream>
#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();
}
}
</hidden>
===== 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 :
<code cpp>
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()
.
<code cpp>
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();
}
}
in202/seance_1/td_1.txt · Last modified: 2022/11/18 10:46 (external edit)