Copy of https://perso.isima.fr/loic/cpp/tp06.fr.php
tete du loic

 Loïc YON [KIUX]

  • Enseignant-chercheur
  • Référent Formation Continue
  • Responsable des contrats pros ingénieur
  • Référent entrepreneuriat
  • Responsable de la filière F2 ingénieur
  • Secouriste Sauveteur du Travail
mail
loic.yon@isima.fr
phone
(+33 / 0) 4 73 40 50 42
location_on
Institut d'informatique ISIMA
  • twitter
  • linkedin
  • viadeo

[C++] TP 6

Read in English

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 :

Le constructeur de copie par défaut de F a appelé le constructeur de copie de M.

Le constructeur de copie de M n'est plus appelé, c'est le constructeur par défaut qui a été appelé.

Il faut explicitement appeler le constructeur de copie de M avec la liste d'initialisation.

Passons maintenant à l'opérateur d'affectation :

L'opérateur d'affectation par défaut de F appelle cet opérateur.

L'opérateur d'affectation de M n'est plus appelé !

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...

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 ou std::invalid_argument).
  • Les problèmes sur des indices non corrects : vous pouvez créer votre propre exception (OutOfRangeException) ou alors utiliser std::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 à redefinir null_pointer (comme classe fille de logic_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 pile
  • push() permet de placer un nouvel élément sur la pile
  • pop() permet d'enlever le dernier élément de la pile
  • top() 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() );

}