Date de première publication : 2013/10/14
Compléments sur la forme normale de Coplien
Le code suivant va nous servir à analyser le comportement du constructeur de copie et de l'opérateur d'affectation au travers de l'héritage et de la composition. Vous pouvez également réutiliser le code de la classe Bavarde
.
class M {
public:
M() {
std::cout << "M::M()" << std::endl;
}
~M() {
std::cout << "M::~M()" << std::endl;
}
M(const M&) {
std::cout << "M::M(const M&)" << std::endl;
}
};
class F : public M {
public:
F() {
std::cout << "F::F()" << std::endl;
}
~F() {
std::cout << "F::~F()" << std::endl;
}
/*
F(const F& f) {
std::cout << "F::F(const F&)" << std::endl;
}
*/
};
int main(int, char**) {
F f1;
F f2 = f1;
f1 = f2;
return 0;
}
Commençons par le constructeur de copie :
- Compiler, exécuter et analyser la trace générée par le programme
Le constructeur de copie par défaut de F
a appelé le constructeur de copie de M
.
- Décommenter le code de la classe
F
. Que remarquez-vous ?
Le constructeur de copie de M
n'est plus appelé, c'est le constructeur par défaut qui a été appelé.
- Comment instaurer, à votre avis, le comportement auquel on pourrait s'attendre ?
Il faut explicitement appeler le constructeur de copie de M
avec la liste d'initialisation.
Passons maintenant à l'opérateur d'affectation :
- Ajouter un opérateur d'affectation à la classe
M
- Compiler et exécuter. Que remarquez-vous ?
L'opérateur d'affectation par défaut de F
appelle cet opérateur.
- Ajouter un opérateur d'affectation à la classe
F
. Que remarquez-vous ?
L'opérateur d'affectation de M
n'est plus appelé !
- Comment appeler l'opérateur d'affectation de la classe
M
dans l'opération de la classeF
?
Si vous avez cliqué, c'est que vous avez oublié ce que l'on a vu en cours avec la méthode contient()
de Forme
, Cercle
et Rectangle
.
La syntaxe est la suivante : Nom_classe_mère::méthode(paramètres)
Finissons par l'agrégation...
- Copier la classe
M
et faites en une classeA
. La duplication de code est prohibée par le code de déontologie des développeurs et le châtiment est au-delà de ce que vous pourriez endurer mais faites-le ! - Ajouter un attribut à
A
de typeM
- Instancier un objet
a1
par défaut et créer un nouvel objeta2
par copie. Que remarquez-vous ? - Comment faire pour que la copie soit correctement faite ? On peut aussi faire de l'affectation entre attributs, ça marche mais c'est bien moins propre.
Fin d'un programme
Tester le programme ci-dessous. Il met en exergue les différents comportements en fonction des différentes manières de quitter le programme :
- fin naturelle (par défaut)
std::exit()
std::terminate()
std::unexpected()
(on ne devrait jamais l'appeler)
class Bavarde {
std::string nom;
public :
Bavarde(std::string n):nom(n) {
std::cout << "constructeur " << nom << std::endl;
}
~Bavarde() {
std::cout << "destructeur " << nom << std::endl;
}
};
Bavarde g("global");
int main(int, char **) {
Bavarde t("local");
static Bavarde s("statlocal");
//std::exit(1);
//std::terminate();
//std::unexpected(); // ne s'appelle pas normalement
return 0;
}
Exceptions
Ajouter le mécanisme des exceptions sur les classes du TP précédent, à savoir :
Les exceptions notoires que l'on peut avoir à gérer sont :
- Les problèmes d'allocation lors de l'initialisation ou de l'agrandissement (
std::bad_alloc
oustd::invalid_argument
). - Les problèmes sur des indices non corrects : vous pouvez créer votre propre exception (
OutOfRangeException
) ou alors utiliserstd::out_of_range
TEST_CASE("exceptions aux bornes") {
Chaine s(10);
REQUIRE_THROWS_AS( s[-1] == 0, Chaine::OutOfRangeException);
// OU
REQUIRE_THROWS_AS( s[-1] == 0, std::out_of_range);
REQUIRE_THROWS_AS( s[12] == 0, std::bad_alloc); // :-)
}
- Un message d'accès si le pointeur interne (
tab
) est nul avec une exception à redefinirnull_pointer
(comme classe fille delogic_error
)
TEST_CASE("exception sur pointeur null") {
Chaine s(0);
// verification que l'heritage est bien fait
std::logic_error * pe = new null_pointer;
delete pe;
REQUIRE_THROWS_AS( s[1] == 0, null_pointer);
}
Pour tester le problème de la mauvaise allocation, il faudra faire un essai avec un tableau initialisé avec une taille très grande, ou alors faire une LOOOOOONNNNGUE boucle.
La classe Pile
Nous allons continuer l'exploration des objets qui permettent d'en contenir d'autres et nous allons nous intéresser à une pile d'entiers.
Pour que ce ne soit pas trop difficile à coder, nous allons considérer une pile de taille fixe et donc utiliser un tableau en sous-main. La taille sera fixée à l'initialisation et tout dépassement de pile devra être signalé par l'envoi d'une exception.
Les méthodes qu'il faut écrire sont les suivantes :
empty()
renvoie un booléen sur l'état de la pilepush()
permet de placer un nouvel élément sur la pilepop()
permet d'enlever le dernier élément de la piletop()
permet de consulter le dernier élément de la pile- les constructeur et destructeur qui vont bien
Cette classe sera réutilisée pour le TP sur la généricité et est développée en respectant quelques tests (loin d'être exhaustifs).
La base de l'exercice est clonable ici :
git clone https://gitlab.com/kiux/CPP6.git
TEST_CASE("Constructeur par defaut") {
Pile p; // cela implique que par defaut la capacite de la pile n'est pas nulle => pas d exception
CHECK( p.empty() );
CHECK( 0 == p.size() );
}
// A faire quand on aura vu les exceptions
TEST_CASE("Exceptions de mauvaise construction") {
REQUIRE_THROWS_AS( Pile(-1).empty(), std::invalid_argument );
REQUIRE_THROWS_AS( Pile( 0).empty(), std::invalid_argument );
}
// A faire quand on aura vu les exceptions
TEST_CASE("Exception pile vide") {
REQUIRE_THROWS_AS( Pile().pop(), std::invalid_argument );
}
TEST_CASE("Live pile") {
Pile p(10);
CHECK( p.empty() );
CHECK( 0 == p.size() );
p.push(5);
CHECK( !p.empty() );
CHECK( 1 == p.size() );
CHECK( 5 == p.top() );
p.push(2);
p.push(1);
CHECK( 3 == p.size() );
CHECK( 1 == p.top() );
p.pop();
CHECK( 2 == p.size() );
CHECK( 2 == p.top() );
p.pop();
p.pop();
CHECK( 0 == p.size() );
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10