[C++] Héritage et vectors

Collection <vector> d'un type de classe de base contenant des objets dérivés

Le problème exposé dans ce sujet a été résolu.

Bonjour à toutes et à tous.

Voilà, j’ai (encore ?) un léger problème concernant la POO.

Je vous explique :

Tout d’abord on a une classe qui va gérer plusieurs autres objets. Cette classe je vais l’appeller Gestionnaire. Ensuite, j’ai une classe de base que je vais appeller simplement Base. Puis de Base, j’ai deux autres classes dérivées, Fille 1 et Fille 2.

Gestionnaire possède un membre private: qui est un vector d’objets Base. Base possède deux membres qui sont m_base1 et m_base2. Fille 1 et Fille 2 possèdent chacune un membre (du même type std::string) qui sont les deux m_fille dont seule leur valeure diffère.

Ce que je souhaite faire :

Je souhaite que dans Gestionnaire ont puisse afficher la valeur de m_base1 et m_base2 mais aussi la valeur de m_fille peut importe le type d’objet sachant que le vector d’objets Base peut contenir des Fille 1 et des Fille 2.

Ce que ça donne :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Gestionnaire.h

class Gestionnaire
{
    private:

        std::vector<Base::Base> m_base;

    public:

        Base();
        ~Base();

                void show();
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Gestionnaire.cpp

// Headers C++

#include <iostream>
#include <vector>

// Classes

#include "gestionnaire.h"

Gestionnaire::Gestionnaire() // Constructeur
{
    m_base = {};
}
Gestionnaire::~Gestionnaire() // Destructeur
{

}

void Gestionnaire::show()
{
    std::cout << "Bases : " << std::endl;
    std::cout << std::endl;

    for(int i {0} ; i < (int)m_base.size() ; ++i)
    {
        std::cout << "m_base1 : " << m_base[i].Base1() << std::endl; // Getter classe Base
        std::cout << "m_base2 : " << m_base[i].Base2() << std::endl; // Getter classe Base
        std::cout << "m_fille : " << m_base[i].Fille() << std::endl; // Problème ici
        std::cout << std::endl;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// Base.h

#ifndef BASE_H
#define BASE_H

class Base
{
    private:

        std::string m_base1;
        char m_base2;

    public:

        Base(std::string base1, char base2);
        ~Base();

        std::string Base1() { return m_base1; }; // Getter
            char Base2() { return m_base2; }; // Getter


};

class Fille1 : public Base
{
    private:

        std::string m_fille;

    public:

        Fille1(Base const& b, std::string fille);
        ~Fille1();

        std::string Fille() { return m_fille; }; // Getter
};

class Fille2 : public Base
{
    private:

        std::string m_fille;

    public:

        Fille2(Base const& b, std::string fille);
        ~Fille2();

        std::string Fille() { return m_fille; }; // Getter
};

#endif
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// Base.cpp

#include <iostream>
#include <string>

#include "base.h"

// Classe Base

Base::Base(std::string base1, char base2) : m_base1(base1), m_base2(base2)
{

}

Base::~Base()
{

}

// Classe Fille1

Fille1::Fille1(Base const& b, std::string fille) : Base(b), m_fille(fille)
{

}

Fille1::~Fille1()
{

}

// Classe Fille2

Fille2::Fille2(Base const& b, std::string fille) : Base(b), m_fille(fille)
{

}

Fille2::~Fille2()
{

}

J’espère que je n’ai embrouillé personne :(

Donc au final je souhaite pouvoir créer des objets Fille1 ou Fille2, les stocker dans un vector de type Base et pouvoir utiliser les fonctions membres de la classe mère et des classes filles. Sauf que quand CLang arrive sur std::cout << "m_fille : " << m_base[i].Fille() << std::endl; dans gestionnaire.cpp, il m’insulte : error: no member named 'Fille' in 'Base'

Et il a raison car effectivement il n’y a pas de fonction membre Fille dans la classe Base car le vector est de type composé Base. Mais vu qu’il contient des Fille1 et des Fille2 je devrait pouvoir afficher le retour de Fille(), non ?

C’est comme ça que j’ai interprété ce qu’il me dit mais impossible de savoir comment résoudre ce problème.

Merci d’avance !

Ah d’accord, en fait je suis dans les derniers chapîtres du C++ Primer 5th, et je n’ai pas abordé le polymorphisme du tout. Pour comprendre j’ai besoin de pratiquer sinon je ne retiens rien donc je fais en parallèle de mon apprentissage, le Javaquarium proposé sur Zds, en C++.

L’exercice 1.1 est atteint sans problèmes mais j’avoue qu’il ne me manquait plus que ça pour terminer le 1.2, ou on doit si j’ai bien compris l’exercice utiliser l’héritage pour les poissons et donc du coup bah le polymorphisme.

Le polymorphisme dynamique est unique ou il y a un polymorphisme simple aussi ?


EDIT : Je viens de lire le sujet sur le cours de @gbdivers et dans le Primer et j’avoue que j’ai du mal à comprendre le principe et surtout à l’utiliser…


EDIT 2 : Est-ce que la fonction membre de la classe fille doit aussi être dans la classe mère ? (En utilisant virtual). Je doute de ça car ce serait nul en fait si on ne pouvait pas avoir une classe dérivée avec des fonctions membres qui n’existe pas dans la classe mère.

+0 -0

En effet, ça sera difficile de faire le Javaquarium sans polymorphisme dynamique. Le Javaquarium est un exercice sur l’orienté objet, et le propos fondamental de l’OO est de paramétrer dynamiquement le comportement des objets, ce qui se fait via polymorphisme en C++.

Qu’est-ce que tu n’as pas compris exactement par rapport au polymorphisme ? Si j’essaie de tout expliquer ça risque d’être long.

Pour les différents types de polymorphisme, il y a polymorphisme à partir du moment où la fonction référencée par un nom donné dépend du contexte de l’appel. Il y a le polymorphisme statique, qui comprend la surcharge de fonction par types de paramètres et les templates, et le polymorphisme dynamique qui comprend les pointeurs sur fonction et les fonctions virtuelles dans une hiérarchie de classe. La différence entre les deux se fait sur le moment de la résolution : à la compilation pour le statique et à l’exécution pour le dynamique.

Pour ton edit 2, j’attends de savoir ce que tu as déjà compris pour répondre.

+0 -0

De ce que j’ai compris :

  • Le mot clé virtual permet au compilateur de savoir quelle fonction membre utiliser, celle de la classe mère ou celle de la classe fille selon de quel type d’objet il s’agit.
  • Dès qu’il y a héritage, la classe mère doit avoir son destructeur marqué virtual car sinon au moment de la destruction de l’objet, celle des classes filles ne se feront pas (Donc pas de destructeur dans les classes filles)
  • Du coup, effectivement, le polymorphisme dont j’ai besoin là, c’est le dynamique car dans le constructeur des classes filles, je passe par référence un objet de type classe mère et que les deux fonctions membres (Qui sont des getters inlinés) sont marquées virtual car je les utilisent aussi dans les classes filles

En revanche, ce que je ne comprends pas, c’est pourquoi une fonction membre dans la classe fille qui n’existe pas dans la classe mère ne peut pas être utilisée, (Enfin je pense que je n’arrive pas à implémenter ça pour le coup) et que du coup même en passant par référence un objet de la classe mère à la classe fille pour la construction de cette dernière, le programme "voit" mes objets filles comme des objets mères.

La fonction membre en question est aussi un getter inliné qui retourne la valeur de la variable membre de la classe fille mais rien de tout ça n’est existant dans la classe mère.

J’espère que c’est ce que tu attendais, camarade :euh: .

+0 -0

Le mot clé virtual permet au compilateur de savoir quelle fonction membre utiliser, celle de la classe mère ou celle de la classe fille selon de quel type d’objet il s’agit.

En effet, mais il faut en plus que les instances de la classe fille soient accédées à travers une référence (ou un pointeur) sur la classe mère. Sinon, on a du slicing et ça ne marche pas (la fonction de la classe mère est appelée).

Dès qu’il y a héritage, la classe mère doit avoir son destructeur marqué virtual car sinon au moment de la destruction de l’objet, celle des classes filles ne se feront pas (Donc pas de destructeur dans les classes filles)

C’est l’idée, mais seulement dans les hiérarchies polymorphiques (où des instances de classes filles peuvent être accédées via des références sur la classe mère). Il y a une règle quelque part qui dit qu’un destructeur doit être public et virtuel dans une hiérarchie polymorphique, et protégé et non-virtuel dans une hiérarchie non-polymorphique. Si ça n’est pas clair, tu peux t’en tenir à héritage => destructeur virtuel.

Pour le problème que tu as, ce n’est pas possible comme ça. Le principe derrière le polymorphisme est celui de substituabilité : remplacer une instance d’une classe mère par une instance d’une classe fille ne doit pas casser le programme (la classe fille ne peut pas renforcer les préconditions ou affaiblir les postconditions). Le corollaire, c’est que les instances des différentes classes filles doivent également être substituables entre elles, ce qui fait qu’on doit s’en tenir à l’interface de la classe mère (car rien ne dit au compilateur que toutes les classes filles de Mere implémentent une fonction Fille (enfin si, il peut s’en rendre compte, mais ça ne serait pas confortable d’en faire une règle du langage, car des erreurs pourraient apparaître sur une interface selon la disposition d’une autre interface)).

La solution est donc d’ajouter la fonction Fille à l’interface de Mere, soit en fournissant une implémentation, soit en en faisant une fonction virtuelle pure.

Et du coup pour

Est-ce que la fonction membre de la classe fille doit aussi être dans la classe mère ? (En utilisant virtual).

Oui, si tu as besoin de l’utiliser polymorphiquement depuis une référence sur la classe mère. Sinon, ça n’est pas obligé.

+1 -0

Bonjour et merci pour ta réponse qui m’a permis de comprendre un peu plus de choses. Du coup j’ai utilisé une fonction virtuelle pure. Mais j’ai une autre question :

Ma classe mère du coup est devenue une classe abstraite (Encore un truc nouveau là :pirate: ) Si j’ai bien compris, on ne peut pas instancier un objet d’une type abstrait, il faut donc instancier directement un objet de la classe dérivée. D’accord, mais alors du coup est-ce que je dois ré-implenter toute la classe mère dans la classe fille ? (Les mêmes variables membres, fonctions, etc…) ou juste implanter celles de la classe fille ? J’ai un doute mais j’ai l’impression que c’est la première solution si je regarde dans le Primer.


EDIT : En re-lisant ton post, je viens de saisir comment fonctionne le polymorphisme et ce par rapport à un héritage simple. Seulement c’est que je n’arrive pas à implémenter le tout.


EDIT 2 : Bon j’ai re-lu le Primer et un peu avancé et j’ai compris d’autres choses, du coup j’ai modifié mon code. (J’aime pas le travail mâché mais à ce qu’il parait on peut jamais s’en sortir tout le temps tout seul dans la vie sauf si on s’apelle Mike Horn lol)

Ce que j’ai fait, j’ai supprimé tous mes getters qui finalement foutent plus le boxon qu’autre chose et j’ai implanté dans la classe mère une fonction membre virtuelle void .afficher() qui claque à l’écran les membres de la classe mère et ensuite dans chaque classe fille on a également la même fonction membre sauf que dans l’implémentation, celles des classes filles font appel à la fonction membre de la classe mère avec son scope et ensuite j’affiche le membre de la classe fille.

Aucun problème de compilation, je crois avoir compris que le polymorphisme fonctionne comme ça :D … Sauf que dans mon vector j’ai toujours des objets mères car quand le programme déroule, il m’affiche bien les membres de la classe mère mais pas celui de la fille donc j’en conclu que même en faisant un .push_back() d’objets filles, le vector se torche avec.

Cependant j’ai lu dans le primer que pour avoir plusieurs types d’objets dans une collection (Array, Vector), il fallait en fait déclarer un vector de pointeur d’objets mère. D’accord, très bien mais moi je souhaite utiliser les références à la place et ne pas du coup utiliser les pointeurs ainsi que l’allocation dynamique new et delete (Préférer les capsules RAII mais ça je ne sais pas faire encore).

Une idée ?

+0 -0

Si j’ai bien compris, on ne peut pas instancier un objet d’une type abstrait, il faut donc instancier directement un objet de la classe dérivée.

p4radox

Exactement.

D’accord, mais alors du coup est-ce que je dois ré-implenter toute la classe mère dans la classe fille ? (Les mêmes variables membres, fonctions, etc…) ou juste implanter celles de la classe fille ?

p4radox

Dans ta classe fille, il suffit de définir les fonctions abstraites (virtuelles pures) de ta classe mère.

Voici un exemple d’implémentation tout simple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// notre classe abstraite qu'on ne peut pas instancier
struct Animal
{
    // et notre fonction abstraite
    virtual std::string cri() = 0;
};

// une classe concrète, fille de notre classe abstraite
struct Chien : Animal
{
    // on redéfinit (override) la fonction abstraite
    std::string cri() override
    {
        return "ouaf";
    };
};

// une autre classe fille
struct Chat : Animal
{
    std::string cri() override
    {
        return "miaou";
    };
};

Avec un exemple d’utilisation :

1
2
3
4
5
6
7
8
9
// on crée une liste d'animaux abstraits
std::vector<std::unique_ptr<Animal>> animaux;
// on y ajoute des animaux concrets
animaux.push_back(std::make_unique<Chien>());
animaux.push_back(std::make_unique<Chat>());

// on affiche leur cri
for(auto & animal : animaux)
    std::cout << animal->cri() << std::endl;

Ce qui va afficher :

1
2
ouaf
miaou

Je comprends mieux, en effet.

Par contre, est-ce que dans une classe abstraite on peux mettre des membres que la classe concrète peut ré-utiliser ou alors une classe abstraite n’est qu’un concept abstrait justement et toute l’implémentation se fait dans les classes concrètes ?

En revanche, unique_ptr je n’ai jamais vu ce truc, qu’est-ce que c’est ?

Une classe abstraite n’a pas de constructeur logiquement alors, si ?

+0 -0

Par contre, est-ce que dans une classe abstraite on peux mettre des membres que la classe concrète peut ré-utiliser ou alors une classe abstraite n’est qu’un concept abstrait justement et toute l’implémentation se fait dans les classes concrètes ?

[…]

Une classe abstraite n’a pas de constructeur logiquement alors, si ?

p4radox

Non, une classe abstraite peut parfaitement avoir un constructeur, des variables membres et des fonctions définies. Une classe est dite abstraite dès le moment qu’au moins une de ses fonctions membres est abstraite, ce qui rend l’instanciation impossible.

Java (qui est un langage qui se base énormément sur la POO) utilise également la notion d’interface pour une classe qui est non seulement abstraite, mais qui contient uniquement des fonctions abstraites1 et aucune variable.

Il faut également savoir que l’on peut dériver d’une classe qui n’est pas abstraite. Pour notre exemple, on aurait donc très bien pu ajouter un constructeur et des fonctions définies.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct Animal
{
    Animal(float taille)
    {
        m_taille = taille;
    }

    // avec 'virtual', on indique ce que cette fonction peut être redéfinie dans une sous-classe
    virtual std::string cri()
    {
        return "cri par défaut";
    }

    private:
        float m_taille;
};

Avec ce code, il est donc possible d’instancier un objet de type Animal, et les sous-classes ne sont plus obligées de redéfinir la fonction cri.

En revanche, comme Animal possède désormais un constructeur avec un paramètre (et pas de constructeur par défaut), les sous-classes sont obligées d’appeler explicitement ce constructeur, soit en définissant un constructeur semblable :

1
2
3
4
5
6
struct Chien : Animal
{
    Chien(float taille) : Animal(taille)
    {
    }
};

Soit en définissant un constructeur par défaut :

1
2
3
4
5
6
struct Chien : Animal
{
    Chien() : Animal(1.5)
    {
    }
};

En revanche, unique_ptr je n’ai jamais vu ce truc, qu’est-ce que c’est ?

p4radox

Il s’agit simplement d’un objet qui représente un pointeur en suivant le principe RAII (en gros, la durée de vie de la resource pointée est la même que celle du pointeur). Cela permet notamment d’éviter les fuites mémoire qui pourraient survenir avec des pointeurs nus :

1
2
3
std::vector<Animal*> animaux;
animaux.push_back(new Chien());
animaux.push_back(new Chat());

Ici, à la moindre exception, les pointeurs sont détruits mais les resources reste en vie : fuite mémoire.


  1. Même si depuis Java 8 il y a les "implémentations par défaut". 

J’ai essayé d’implanter ça comme tu le décris dans cet exemple, en passant aux constructeurs filles un objet par référence de type de la classe mère.

Tout fonctionne sans erreurs sauf que dans mon vector, ce sont des objets mère et non des objets filles. Comment faire comprendre au compilateur que je veux dans un vector des objets filles de type différents sachant que normalement un vector ne contient qu’un seul type d’objet dans sa collection ?

En gros, un vector de type classe mère composé d’objets fille1 ou fille2 ou du coup je peux appeller une fonction membre sur les elements et grâce au mot clé virtual, le compilateur comprendrais de quelle classe il s’agit.

Je dirais bien les templates mais j’ai peur de dire une connerie. C’était une connerie


EDIT : Je vais vous montrer ce que j’ai fait, comme ça vous perdrez moins de temps

poisson.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#ifndef POISSON_H
#define POISSON_H

class Poisson
{
    private:

        std::string m_nom;
        char m_sexe;
        std::string m_regime;

    protected:

        std::string Nom() { return m_nom; };
        char Sexe() { return m_sexe; };
        std::string Regime() { return m_regime; };


    public:

        Poisson(std::string nom = "Poisson", char sexe = 'A', std::string regime = "Inconnu");
        virtual ~Poisson() {};

        virtual void afficher() = 0;
        virtual void nourrir() = 0;


};

class Carnivore : public Poisson
{

    public:

        Carnivore(std::string nom, char sexe, std::string regime);
        virtual ~Carnivore() {};

        void afficher();
        void nourrir();
};

class Herbivore : public Poisson
{

    public:

        Herbivore(std::string nom, char sexe, std::string regime);
        virtual ~Herbivore() {};

        void afficher();
        void nourrir();

};

#endif

poisson.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <string>

#include "algue.h"
#include "poisson.h"

// Classe Poisson

Poisson::Poisson(std::string nom, char sexe, std::string regime) : m_nom(nom), m_sexe(sexe), m_regime(regime)
{

}

// Classe Carnivore

Carnivore::Carnivore(std::string nom, char sexe, std::string regime) : Poisson(nom, sexe, regime)
{

}

void Carnivore::afficher()
{
    std::cout << "Nom : " << Nom() << std::endl;
    std::cout << "Sexe : " << Sexe() << std::endl;
    std::cout << "Régime : " << Regime() << std::endl;
} 

// Classe Herbivore

Herbivore::Herbivore(std::string nom, char sexe, std::string regime) : Poisson(nom, sexe, regime)
{

}

void Herbivore::afficher()
{
    std::cout << "Nom : " << Nom() << std::endl;
    std::cout << "Sexe : " << Sexe() << std::endl;
    std::cout << "Régime : " << Regime() << std::endl;
}

aquarium.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#ifndef AQUARIUM_H
#define AQUARIUM_H

#include <vector>
#include <memory>

#include "algue.h"
#include "poisson.h"

class Aquarium
{
    private:

        int m_cycles;
        std::vector<std::unique_ptr<Poisson>> m_poissons;
        std::vector<Algue::Algue> m_algues;

    public:

        Aquarium();
        ~Aquarium();

        void ajouterPoisson();
        void ajouterAlgue();
        void nouveauCycle();
};

#endif

aquarium.cpp

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// Headers C++

#include <iostream>
#include <string>

// Classes

#include "aquarium.h"

// Corps de la classe Aquarium

Aquarium::Aquarium() // Constructeur
{
    m_cycles = 0;
    m_algues = {};
}
Aquarium::~Aquarium() // Destructeur
{

}

void Aquarium::ajouterPoisson()
{
    char fin_ajout {1};

    do
    {
        std::cout << "Entrez le nom du poisson : ";

        std::string nom {"Poisson"};
        std::cin >> nom;

        std::cout << "Entrez le sexe du poisson (M/F) : ";

        std::cin.get();

        char sexe {'I'};
        std::cin >> sexe;

        std::cout << "Entrez le régime alimentaire du poisson : ";

        std::string regime {""};
        std::cin >> regime;

        if(regime == "Carnivore")
        {
            m_poissons.push_back(std::make_unique<Carnivore>(nom, sexe, regime));
        }
        else if(regime == "Herbivore")
        {
            m_poissons.push_back(std::make_unique<Herbivore>(nom, sexe, regime));
        }

        std::cout << std::endl;

        char choix {'O'};

        std::cout << "Voulez-vous ajouter un autre poisson ?(O/N) : ";
        std::cin >> choix;
        std::cout << std::endl;

        if (choix == 'N')
            fin_ajout = 0;

    }while(fin_ajout);

}

void Aquarium::ajouterAlgue()
{
    char fin_ajout {1};

    do
    {
        std::cout << "Entrez le nombre d\'algues à ajouter : ";

        int nb_algues {0};
        std::cin >> nb_algues;

        std::cout << std::endl;

        for(int i {0} ; i < nb_algues ; ++i)
        {
            Algue::Algue a("Algue");
            m_algues.push_back(a);
        }

        char choix {'O'};

        std::cout << "Voulez-vous ajouter d\'autres algues ?(O/N) : ";
        std::cin >> choix;
        std::cout << std::endl;

        if (choix == 'N')
            fin_ajout = 0;

    }while(fin_ajout);
}

void Aquarium::nouveauCycle()
{
    m_cycles++;

    std::cout << "CYCLE #" << m_cycles << std::endl;
    std::cout << "----------" << std::endl;
    std::cout << std::endl;

    std::cout << "Poissons : " << std::endl;
    std::cout << std::endl;

    for(int i {0} ; i < (int)m_poissons.size() ; ++i)
    {
        m_poissons[i]->afficher();
        std::cout << std::endl;
    }

    std::cout << "Algues : " << m_algues.size() << std::endl;
}

EDIT 2 : Bon, en parcourant le net, j’ai appris deux trois choses en plus et je pense commencer piger le truc.

  • En utilisant un pointeur intelligent pour créer une collection d’objets dérivés (unique_ptr ou shared_ptr), on respecte le principe RAII donc aucun risque de fuite mémoire en comparaison à l’utilisation des pointeurs Raw (Les pointeurs du C quoi).
  • J’ai entendu parler de collection hétérogène, c’est exactement ce que je cherche à faire, grâce aux pointeurs intelligents sachant que mes deux classes filles sont identiques donc au niveau de la conception c’est correct.
  • Le polymorphisme est le fait de faire changer de forme (Morphing quoi) mais ce changement va s’opérer plusieures fois. Donc comme ma classe Poisson peut devenir une classe Carnivore ou Herbivore, on arle bien de polymorphisme. Sinon, c’est juste un héritage.
  • Cependant, mon vector d’objets est déclaré dans le constructeur de la classe Aquarium (Bah oui, les poissons (Carnivores ou Herbivores ne s’auto-contiennent pas, c’est l’aquarium qui les contient, d’ou ma collection car je ne sais pas combien de poissons je vais ajouter). Mais que dois-je mettre dans l’implémentation du constructeur explicite (Soit dans aquarium.ccp) afin d’initialiser ce vector ? A moins que je fasse fausse route sur ça.std::move à l’air d’être la solution à ce problème mais je n’ai pas compris un traitre mot de son utilisation

Je n’ai qu’une erreur à la compilation : référence indéfinie vers vtable Et ce pour chaque classe fille.

Merci pour vos réponses et d’avoir pris le temps de m’aider déjà, comprendre ce language en autodidacte n’est pas une chose aisée mais c’est important pour moi.

+0 -0

Bonjour à tous, La nuit porte conseil apparement, du coup je pense que c’est plutôt un gros problème de conception que j’ai et je suis en train de tout reprendre à zéro. J’ai aussi l’impression d’avoir encore une partie du cerveau ancrée dans le language C et je n’arrive pas à vraiment penser en OO.

Dès que j’aurais tout ré-implanté, je vous posterez mon travail.

Bonne journée à tous.

@p4radox : Tu n’as pas besoin de stocker le régime dans le poisson puisque c’est son type final qui va le définir. Là en plus, ta classe m’autorise à créer un poisson carnivore en lui transmettant comme régime "herbivore" ou "aeiflaeruhmih".

1
for(int i {0} ; i < (int)m_poissons.size() ; ++i)
p4radox

Première chose : n’utilise pas les casts façon langage C, seconde chose, plutôt que faire ce cast pas propre du tout, utilise le bon type pour la variable i : std::size_t. Quand ton destructeur ne fait rien, contente toi de ne pas le mettre quand il n’est pas virtuel, et quand il est virtuel, tu peux utiliser le = default.

D’accord pour le cast, c’est vrai que la taille d’un vector est de type std::size_t donc pourquoi ne pas déclarer i du même type dans le for, c’est pas faux, merci.

J’ai recommencé tout le programme, en essayant de faire plus propre. J’ai donc maintenant une classe mère abstraite Animal dont tout va dériver (C’était stupide de créer une classe Herbivore ou Carnivore, y’a pas que les poissons qui on ce régime dans la vie. En gros je me suis inspiré du réel pour penser en OO)

Par contre, là ou je galère mais à mort, c’est dans l’utilisation d’une collection hétérogène.

En fait dans ma classe Aquarium j’ai un vector suivant : std::vector<std::unique_ptr<Animal> > animaux; qui est un membre de cette classe Aquarium, et donc j’ai compris que si l’on veut stocker des poissons et des moules dans la collection hétérogène animale, il fallait une classe abstraite de base (Donc la classe Animal qui a au moins une fonction membre virtuelle pure).

Mais ce que je ne comprends pas c’est comment travailler avec cette collection (Je sais que l’on ne peut pas la copier car on ne peut pas copier des std::unique_ptr). Coment initialiser une collection hétérogène dans un constructeur ? (dans le constructeur de la classe Aquarium)

Bonsoir, après avoir perdu la raison et fini à l’asile, j’ai enfin compris comment implémenter tout ça et surtout j’ai appris beaucoup de choses :D (Grâce à vous les gars !)

En re-lisant mes posts, je me rend-compte maintenant de la stupidité de certaines déclarations &gt;_&lt;

J’ai compris comment utiliser ces satanés collections hétérogènes, enfin ! Et j’ai amélioré la conception de mon programme au passage, car la POO c’est cool mais penser en OO quand on vient du C, bah ’C’ la galère, HAHAHahaha… Pardon.

Le truc qui m’a rendu fou en fait c’est que je cherchais à initialiser un truc qui ne peut pas l’être Bah oui, un std::vector de pointeur unique std::unique_ptr est non copiable (Comparé à un vector de pointeurs partagé std::shared_ptr). Donc le constructeur n’a rien à faire là-dedans, j’ai simplement déclaré un std::vector de type std::unique_ptr en tant que membre private: de la classe Aquarium et quand j’en ai eu besoin dans ma fonction membre pour ajouter un animal de la classe abstraite Animal (Dont dérive une classe Poisson) bah j’utilise .push_back(std::make_unique<Poisson>());, tout simplement !

Olybri m’avait mis la réponse sous les yeux, mais le temps que je comprenne le pricipe…

Donc du coup je vous met le programme ici. Il y a sûrement des choses à améliorer comme la vérification de saisie dans la fonction membre voir() de la classe Aquarium Parce que si l’on rentre autre chose que un int, la boucle est infinie :euh:

Et au lieu d’avoir un vieux switch() et de devoir faire des .push_back() dans le constructeur de la classe Aquarium pour définir les valeur possibles, je pourrai mettre ça dans un fichier et initialiser les std::vector contenant les seules possibilités offertes avec ce qu’il y a dans le fichier, comme ça on ne touchera pas au code pour pouvoir ajouter des races d’animaux par la suite. Mais c’était juste pour voir si j’avais bien compris le tout et que j’implémentai ça correctement.

aquarium.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#ifndef AQUARIUM_H
#define AQUARIUM_H

#include <vector>
#include <memory>
#include <string>

#include "animal.h"

class Aquarium
{
    private:

        // Membres privés

        int m_cycle; // Cycle en cours
        std::vector<std::string> m_liste_possible_animaux;
        std::vector<std::string> m_liste_possible_vegetaux;
        std::vector<char> m_liste_possible_sexes_animaux;
        std::vector<std::string> m_liste_possible_regimes_animaux;

        std::vector<std::unique_ptr<anx::Animal::Animal> > m_animaux; // Collection d'animaux (Poissons, Crustacés, etc...)
        //std::vector<std::unique_ptr<Vegetal> > m_vegetaux; // Collection de végétaux (Algues, coraux, etc...)

    public:

        // Ensemble Constructeur/Destructeur

        Aquarium();

        // Fonctions membres

        void ajouterAnimal();
        void ajouterVegetal();
        void voir();
        void nouveauCycle();
};

#endif

aquarium.cpp

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
// Headers C++

#include <iostream>
#include <string>

// Header de la classe

#include "aquarium.h"

// Corps de la classe

Aquarium::Aquarium()
{
    m_cycle = 0;
    m_liste_possible_animaux.push_back("Poisson");
    m_liste_possible_regimes_animaux.push_back("Carnivore");
    m_liste_possible_regimes_animaux.push_back("Herbivore");
    m_liste_possible_sexes_animaux.push_back('M');
    m_liste_possible_sexes_animaux.push_back('F');
}

void Aquarium::ajouterAnimal()
{
    // Choix du type d'animal

    std::cout << "Choisissez le type d'animal à ajouter dans l'aquarium (Entrez le numéro) : " << std::endl;

    for(std::size_t i {0} ; i < m_liste_possible_animaux.size() ; ++i)
    {
        std::cout << i << " : " << m_liste_possible_animaux[i] << std::endl;
    }

    std::cout << ">> ";

    int choix {0}, n_race {0};

    while(!(std::cin >> choix) && choix > static_cast<int>(m_liste_possible_animaux.size()))
    {
        std::cerr << "La saisie n\'est pas valide !" << std::endl;
    }

    std::string race {m_liste_possible_animaux[choix]};
    n_race = choix;

    // Saisie de l'espèce

    std::cout << "Entrez l\'espèce de l\'animal : " << std::endl;

    std::cout << ">> ";

    std::string entree {""};

    while(!(std::cin >> entree))
    {
        std::cerr << "La saisie n\'est pas valide !" << std::endl;
    }

    std::string espece {entree};

    // Choix du type de régime alimentaire

    std::cout << "Choisissez le régime alimentaire de l\'animal (Entrez le numéro) : " << std::endl;

    for(std::size_t i {0} ; i < m_liste_possible_regimes_animaux.size() ; ++i)
    {
        std::cout << i << " : " << m_liste_possible_regimes_animaux[i] << std::endl;
    }

    std::cout << ">> ";

    choix = 0;

    while(!(std::cin >> choix) && choix > static_cast<int>(m_liste_possible_regimes_animaux.size()))
    {
        std::cerr << "La saisie n\'est pas valide !" << std::endl;
    }

    std::string regime {m_liste_possible_regimes_animaux[choix]};

    // Choix du sexe

    std::cout << "Choisissez le sexe de l\'animal (Entrez le numéro) : " << std::endl;

    for(std::size_t i {0} ; i < m_liste_possible_sexes_animaux.size() ; ++i)
    {
        std::cout << i << " : " << m_liste_possible_sexes_animaux[i] << std::endl;
    }

    std::cout << ">> ";

    choix = 0;

    while(!(std::cin >> choix) && choix > static_cast<int>(m_liste_possible_sexes_animaux.size()))
    {
        std::cerr << "La saisie n\'est pas valide !" << std::endl;
    }

    char sexe {m_liste_possible_sexes_animaux[choix]};

    // Saisie du nom

    std::cout << "Entrez le nom de l\'animal : " << std::endl;

    std::cout << ">> ";

    entree = "";

    while(!(std::cin >> entree))
    {
        std::cerr << "La saisie n\'est pas valide !" << std::endl;
    }

    std::string nom {entree};

    // Saisie de l'âge

    std::cout << "Entrez l\'âge de l\'animal (0 pour un nouveau-né) : " << std::endl;

    std::cout << ">> ";

    choix = 0;

    while(!(std::cin >> choix))
    {
        std::cerr << "La saisie n\'est pas valide !" << std::endl;
    }

    int age {choix};

    switch(n_race)
    {
        case 0:

            m_animaux.push_back(std::make_unique<anx::Poisson::Poisson>(sexe, age, race, regime, nom, espece));
            break;

        default:
            break;
    }

}

void Aquarium::ajouterVegetal()
{

}

void Aquarium::voir()
{
    for(auto & animal : m_animaux)
        animal->voir();
}

void Aquarium::nouveauCycle()
{

}

animal.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#ifndef ANIMAL_H
#define ANIMAL_H

#include <string>

namespace anx
{
    class Animal
    {
        private:

            // Membres

            char m_sexe;
            int m_age;
            std::string m_race;
            std::string m_regime;
            std::string m_nom;

        protected:

            // Getters

            char Sexe() { return m_sexe; };
            int Age() { return m_age; };
            std::string Race() { return m_race; };
            std::string Regime() { return m_regime; };
            std::string Nom() { return m_nom; };

        public:

            // Ensemble Constructeur/Destructeur

            Animal(char sexe = 'U', int age = 0, std::string race = "Inconnue", std::string regime = "Inconnu", std::string nom = "Inconnu") 
            : m_sexe(sexe), m_age(age), m_race(race), m_regime(regime), m_nom(nom) {} ;

            virtual ~Animal() = default;

            // Fonctions membres

            virtual void voir() = 0;
            virtual void seNourrir() = 0;
    };

    class Poisson : public Animal
    {
        private:

            // Membres

            std::string m_espece;

        protected:

            // Getters

            std::string Espece() { return m_espece; };

        public:

            // Constructeur

            Poisson(char sexe = 'U', int age = 0, std::string race = "Inconnue", std::string regime = "Inconnu", std::string nom = "Inconnu", 
                std::string espece = "Inconnue") : Animal(sexe, age, race, regime, nom), m_espece(espece) {} ;

            // Fonctions membres

            virtual void voir() override;
            virtual void seNourrir() override;
    };
}

#endif

animal.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Headers C++

#include <iostream>

// Header de la classe

#include "animal.h"

// Corps de la classe

void anx::Poisson::voir()
{
    std::cout << "Race : " << Race() << std::endl;
    std::cout << "Espèce : " << Espece() << std::endl;
    std::cout << "Régime alimentaire : " << Regime() << std::endl;
    std::cout << "Sexe : " << Sexe() << std::endl;
    std::cout << "Nom : " << Nom() << std::endl;
    std::cout << "Âge : " << Age() << std::endl;
    std::cout << std::endl;
}

void anx::Poisson::seNourrir()
{

}

En tout cas, merci à tous pour votre aide, je vais pouvoir continuer l’exercice du javaquarium tout en apprenant, car il n’y a qu’en pratiquant et en découvrant au fur et à mesure que je peux comprendre et retenir les choses, si je lisait le Primer de A à Z direct, j’aurai rien retenu, alors que là en posant des questions, en relisant le cours et en cherchant (beaucoup) de mon côté, j’arrive à mes fins.

Merci encore !


PS: Je reviendrai sûrement vous embêter encore si y’a encore des montagnes après celle-là :lol:

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte