Partage
  • Partager sur Facebook
  • Partager sur Twitter

Les types de données de classe

Sujet résolu
    21 février 2018 à 21:45:45

    Bonjour :)

    Depuis quelque temps, une question me taraude... Je n'ai pas encore posé la posée, parce qu'elle me semblait stupide. Mais tant pis :

    Je voudrais savoir en quel type de données était transcrite les ... données personnalisées via les classes. Par exemple, si on déclare une classe "Légumes", et que l'on ajoute des champs et des méthodes : Couleur, Type, Bon, Pas Bon, ... En tant que quel type de donné sera-t-elle enregistrée ? Ou est-ce simplement une représentation structurée qui ne signifie rien pour le processeur ? (Enfin, vous me direz qu'un int, char, double, ... ne signifient rien non plus, mais c'est pour l'exemple).

    Par exemple, ceci :

    #include <iostream>
    #include <string>
    
    class Vegetable{
        public:
            bool IsVegetable(std::string str){
                if (str == "Navet")
                    return true;
                else if (str == "Carotte")
                    return true;
            }
    };
    
    int main(){
        Vegetable Legume;
        if (Legume.IsVegetable("Carotte"))
            std::cout << "Beark!" << std::endl;
        else if (Legume.IsVegetable("Navet"))
            std::cout << "Miam!" << std::endl;
        else
            std::cout << "Heuu... ?" << std::endl;
        return 0;
    }

    Si on colle ce code dans le champ prévu sur ce site (que je trouve génial) avec le langage C++ et comme compilateur, le x86-64 Gcc 7.3, cela ne parlera peut-être pas à tout le monde, mais on dirait que le code assembleur produit est "pensé" pour retranscrire les classes, en simples labels et fonctions. D'ailleurs, si on décompile l'exécutable produit, on se rend compte que les classes n'ont pas été gardées.

    Je voudrais, si possible bien sûr, quelques explications à ce sujet :)

    Je vous souhait avant tout, une bonne soirée !

    -
    Edité par Geralt de Riv 21 février 2018 à 21:49:10

    • Partager sur Facebook
    • Partager sur Twitter
    Le doute est le commencement de la sagesse
      21 février 2018 à 22:38:06

      La notion de classe n'existe pas en assembleur, donc ce n'est pas étonnant. Comme beaucoup d'autres concepts "haut niveau", tel que les boucles for/while/etc (l'assembleur utilise des sauts conditionnels) ou les variables.

      Même le concept de type n'existe pas réellement. Tu as des bits dans un registre. Si tu appelles l'instruction assembleur "additionner des entiers", tes valeurs seront considérées comme des entiers, si tu appelles l'instruction "additionner des réels", les valeurs seront des réels.

      Une classe est considéré comme un agregat de donneées en memoire. Par exemple :

      struct A {
          int i;
          int j;
      };

      sera deux entiers à la suite dans la mémoire. Si tu écris :

      A a;
      a.i = 123;
      a.j = 456;

      tu as :

      mov DWORD PTR [rbp-8], 123
      mov DWORD PTR [rbp-4], 456

      - l'adresse de la variable a = l'adresse de a.i. (Le registre rbp contient l'adresse memoire de la stack)

      - l'adresse de a.j = adresse de a.i + sizeof(int)

      Je te conseille de lire un cours sur le fonctionnement des compilateurs (et architecture des ordis et architecture des systèmes) si tu es interessé par tout ca.

      ATTENTION : on parle ici du fonctionnement simple, sans optimisation, sans utilisation de fonctionnalités spécifique du CPU. Mais le compilateur peut faire beaucoup de choses avec l'assembleur généré. Si tu écris du code qui manipule directement des adresses memoire, il faut etre sur de ce qu'on fait.

      -
      Edité par gbdivers 21 février 2018 à 22:41:30

      • Partager sur Facebook
      • Partager sur Twitter
        22 février 2018 à 9:44:10

        Salut,

        Comme l'a dit gbdivers, au niveau du code binaire, il  n'y a plus:

        • qu'une adresse mémoire "de début" et
        • un décalage pour accéder à la donnée membre

        le décalage pour accéder à une donnée membre étant calculé (par le compilateur) sur base de la taille des données membres qui précède + un "padding" éventuel pour permettre d'obtenir une adresses dont la valeur est un multiple spécifique en base 2 (1, 2, 4, 8, 16n ...) , basée sur la taille de la plus grande donnée membre.

        Et on peut aussi noter que l'adresse de la première donnée membre correspond forcément à l'adresse de la structure elle-même ;)

        Donc, si tu as une structure (struct et class, c'est pareil à la visibilité par défaut près ;) ) proche de

        struct MaStruct{
            char c; // considérons qu'il fait 1 bytes
            int i; // considérons qu'il fait 4 bytes
            short s; // considérons qu'il fait 2 bytes
        };

        et que tu demande la taille de MaStruct, tu risque d'obtenir un résultat égal à 12, car sa plus grande donnée membre est un int de taille 4, et que l'on aura alors, en mémoire, quelque chose comme

        adresse Start: adresse de la structure calculée en bytes
        
        Start + 0 -- Start + 0    : c
        Start + 1 -- Start + 3    : padding
        Start + 4 -- Start + 7    : i
        Start + 8 -- Start + 9    : s
        Start + 10 -- Start + 11  : padding

        Alors, bien sur, il y a quelques astuces dont le compilateur peut faire preuve, par exemple, si on déplace c pour qu'il suive le short sous la forme de

        struct MaStruct{
            int i; // considérons qu'il fait 4 bytes
            short s; // considérons qu'il fait 2 bytes
            char c; // considérons qu'il fait 1 bytes
        };

        Il se peut que tu obtiennes une taille égale à ... 8, qui sera représentée en mémoire sous la forme de

        adresse Start: adresse de la structure calculée en bytes
        
        Start + 0 -- Start + 3  : i
        Start + 4 -- Start + 5  : s
        Start + 6 -- Start + 6  : c
        Start + 7 -- Start + 7  : padding

        Et je ne vais pas te parler de ce qui se passe si l'on a affaire à des array ;)

        On pourrait juste ajouter le fait que, a priori, les différentes données seront placées en mémoire dans l'ordre dans lequel elles sont déclarées dans ta structure, quelle que soit l'accessibilité dans laquelle ils sont déclarés (la notion d'accessibilité n'existe que durant l'étape de compilation, et est totalement inconnue une fois le code binaire généré)

        -
        Edité par koala01 22 février 2018 à 9:45:43

        • Partager sur Facebook
        • Partager sur Twitter
        Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
          22 février 2018 à 17:11:06

          Merci beaucoup pour vos réponses :)

          C'est vraiment intéressant tout ça, mais on se rend vite compte que c'est assez compliqué à implémenter (pour faire un compilateur, je veux dire).

          Je pense effectivement lire un cours sur le fonctionnement des compilateurs, étant en train d'en créer un maintenant, je pense que ça me serait plus utile. D'ailleurs, en connaissez-vous de bons ?

          Je passe déjà le sujet [résolu], merci beaucoup :p

          • Partager sur Facebook
          • Partager sur Twitter
          Le doute est le commencement de la sagesse
            22 février 2018 à 18:23:52

            Et on n'a parlé que des choses simples. Pas des optimisations que le compilo peut faire. Par du cas de l'héritage, des fonctions virtuelles, etc.

            Tu as quelques tutos interessants sur ZdS : https://zestedesavoir.com/bibliotheque/?subcategory=systemes-dexploitation  https://zestedesavoir.com/tutoriels/427/les-optimisations-des-compilateurs/  https://www.tutorialspoint.com/compiler_design/index.htm 

            Si tu vas sur les sites des 3 compilos principaux, tu as pas mal d'articles et explications. Tu as aussi la LLVM conference.

            -
            Edité par gbdivers 22 février 2018 à 18:31:35

            • Partager sur Facebook
            • Partager sur Twitter
              22 février 2018 à 19:39:47

              gbdivers a écrit:

              Et on n'a parlé que des choses simples. Pas des optimisations que le compilo peut faire. Par du cas de l'héritage, des fonctions virtuelles, etc.

              J'ai parlé en général ^^

              Les 3 compilateurs principaux, tu sous-entends eux ? :

              • Compilateurs linux, (GCC, ...);
              • Compilateurs Visual, de Microsoft (VC++, VC, VB, ...);
              • Compilateurs LLVM (clang, ...).

              Sinon, tant qu'on y est et puisque tu en parle : c'est quoi exactement les fonctions virtuelles ? Je n'ai jamais bien compris leurs principes, et m'y suis jamais intéressés, parce que les fonctions non-virtuelles m'ont toujours convenues.

              -
              Edité par Geralt de Riv 22 février 2018 à 19:40:36

              • Partager sur Facebook
              • Partager sur Twitter
              Le doute est le commencement de la sagesse
                23 février 2018 à 2:07:36

                Bon, les fonctions virtuelles...

                Quand tu es dans une logique d'héritage, tu peux vouloir faire en sorte que ta classe de base expose un comportement dont le nom sera le même pour toutes les classes dérivées, mais dont le fonctionnement interne sera différent en fonction du type réel envisagé.

                Par exemple, si tu veux modéliser la notion de "forme géométrique", tu créeras sans doute une classe Shape, dont l'un des comportement permettra de... tracer la forme en question, et ce, quelle que puisse être la forme réelle que tu veux tracer.

                Seulement, il ne faut pas être grand clerc pour savoir qu'un cercle, ça ne se trace pas comme un triangle ou comme un rectangle. Et mieux encore, si on a "une forme" sans autre précision, il est impossible de la tracer "telle quelle", car on ne saurait pas ... quelle forme lui donner.

                On va donc prévoir dans la classe de base (Shape, selon mon exemple) un comportement (draw(), toujours selon mon exemple) dont on va dire au compilateur que ce comportement peut (doit, en l'occurrence) être (re)défini dans les classes dérivée.  Et cela, ca se fait en déclarant la fonction comme étant virutelle (en utilisant le mot clé virtual).

                Nous disposons également de la possibilité d'indiquer au compilateur que l'on n'est pas en mesure de fournir un comportement cohérent pour une fonction virtuelle, comme c'est le cas pour la fonction draw de la classe Shape :

                Tu pourrais décider tout à fait arbitrairement de tracer un cercle de X de rayon, mais tu aurais toujours des gens qui te demanderaient pourquoi ne pas avoir choisi de tracer un triangle ou un rectangle, ou pourquoi avoir choisi de le faire de X de rayon au lieu de le faire de Y de rayon...

                Et le fait est qu'ils auraient raison de te poser la question, car, dés que l'arbitraire entre en jeu, il n'y a aucune possibilité qui soit réellement meilleure que les autres!

                Du coup, la seule chose cohérente à faire pour cette fonction (au niveau de la classe Shape) est d'indiquer au compilateur que "l'on n'est pas en mesure de fournir un comportement cohérent" en faisant suivre la déclaration de la fonction d'un =0 (juste avant le ; final).

                En faisant cela, on va créer ce que l'on appelle une fonction virtuelle pure (il va de soi que l'on ne peut de toutes manières le faire qu'avec des fonctions virtuelles ;) ), et, comme le compilateur a horreur du vide, et qu'il se retrouve donc avec un fonction dont il sait qu'il n'y a pas d'implémentation, il va littéralement interdire la création de la classe de base, ainsi que de toute classes dérivée pour laquelle la fonction ne serait pas correctement implémentée.  Nous obtenons donc -- de facto -- ce que l'on appelle une classe abstraite.

                Pourquoi abstraite ? Simplement parce qu'une classe dont on peut créer une instance, c'est une classe concrète, et que abstrait est le contraire de concret :D

                De plus, depuis C++11, on peut indiquer au compilateur, au niveau des classes dérivées, que la fonction dont on (re)défini le comportement est bel et bien une fonction qui est déclarée dans la classe de base au moyen du mot clé override.

                Et, de même, nous pouvons également indiquer au compilateur que nous ne voulons pas que le comportement soit redéfini "une fois de plus" dans les éventuelles classes qui pourraient hériter de notre classe dérivée au moyen du mot clé final.

                Ces deux mots clés ont principalement pour but de permettre au compilateur de s'assurer que le contrat passé entre la classe de base et ses classes dérivées est bel et bien respecté ;)

                Au final, nous pourrions donc avoir un code ressemblant à

                class Shape{
                public:
                    /* quand le destructeur est virtuel, cela nous permet
                     * de détruire une forme (quelle qu'elle soit) alors que
                     * nous la connaissons comme étant de type Shape
                     */
                    virtual ~Shape() = default; /* = defaut indique que le
                                                 * "comportement classique,
                                                 * fourni "par défaut" par le compilateur
                                                 * doit être utilisé
                                                 */
                    /* la fonction draw :
                     * - on veut pouvoir en (re)définir le comportement dans les classes
                     *   dérivées
                     * - mais on est dans l'incapacité de fournir un comportement
                     *   cohérent quand on parle d'une forme, sans autre précision
                     * !!! C'est donc une fonction virtuelle pure !!!
                     * !!! et la classe Shape devient une classe ABSTRAITE !!!
                     */
                    virtual void draw() const = 0;
                    /* ... */
                };
                /* Les formes concrètes: */
                class Square : public Shape{
                public:
                    /* on indique au compilateur que c'est une fonction issue
                     * de la classe de base (Shape), mais que nous avons la
                     * certitude que son comportement ne sera plus redéfini
                     * dans les classes qui pourraient hériter de Square
                     */
                    void draw() const final override;
                    /* ... */
                };
                class Rectangle : public Shape{
                public:
                    /* IDEM */
                    void draw() const final override;
                    /* ... */
                };
                class Triangle: public Shape{
                public:
                    /* IDEM */
                    void draw() const final override;
                    /* ... */
                };
                class Circle : public Shape{
                public:
                    /* IDEM */
                    void draw() const final override;
                    /* ... */
                };

                (et, bien sur, tu auras une implémentation cohérente de void Square::draw() const, de void Triangle::draw() const, de void Rectangle::draw() const, de void Circle::draw() const et de toutes les autres redéfinitions du comportement de draw dans les différents fichiers d'implémentation ;) )

                Ainsi s'achève la "partie simple", qui est destinée au "commun des mortels", en ce qui concerne les fonctions virtuelles.

                Comme mon intervention est déjà importante, je vais m'arrêter là...  Mais, si tu veux vraiment savoir comment cela se passe au niveau du compilateur, dis le, et "quelqu'un" se fera sans doute un plaisir de "dégrossir" l'histoire ;)

                • Partager sur Facebook
                • Partager sur Twitter
                Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
                  23 février 2018 à 17:10:34

                  Je te remercie vraiment pour toutes tes explications :'(.

                  Comme l'a dit lui-même Bjarne Stroustrup, le C++ est devenu un langage plus complexe encore que ce qu'il n'aurait pu l'imaginer...

                  C'est vraiment génial tous ça, et bien sûr, si quelqu'un à des explications supplémentaires à donner sur les fonctions virtuelles (comme l'a donc proposé koala01), ou sur tout autre chose : Qu'il n’hésite pas !

                  • Partager sur Facebook
                  • Partager sur Twitter
                  Le doute est le commencement de la sagesse
                    24 février 2018 à 1:01:10

                    Pour faire "simple": comme tout se réduit, finalement à une adresse en mémoire (y compris les fonctions, dont l'adresse mémoire de leur première instruction est bien connue), le compilateur va créer un tableau qui fera le lien entre les fonctions virtuelles de la classe de base et l'implémentation de ces fonctions pour chacune des classes dérivées.  On appelle ce tableau vtable ou VMT.

                    De cette manière, lorsque l'on aura affaire à un objet que l'on ne connait que comme étant du type de base (bien sur, ce doit être une référence ou un pointeur ;) ), le compilateur ira chercher le "nom" de la fonction appelée pour le type de base, et fera en sorte de faire appel à l'implémentation spécifique au type réel de l'objet ;)

                    Bien sur, il s'agit ici d'une explication simplifiée à l'extrême et dont de nombreux détails ont finis "planqués sous le tapis", mais le principe reste malgré tout "relativement correct" ;)

                    • Partager sur Facebook
                    • Partager sur Twitter
                    Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
                      24 février 2018 à 10:36:01

                      Ok, merci pour tout :)
                      • Partager sur Facebook
                      • Partager sur Twitter
                      Le doute est le commencement de la sagesse

                      Les types de données de classe

                      × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
                      × Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
                      • Editeur
                      • Markdown