Partage
  • Partager sur Facebook
  • Partager sur Twitter

Namespaces

Apprentissage des namespaces

Sujet résolu
    24 septembre 2022 à 10:09:38

    Bonjour !

    Je suis actuellement en train d'apprendre à créer des namespaces, donc je faisais quelques tests, mais je me suis heurté à un problème : il y a une erreur dans mon code mais je n'arrive pas à la trouver car le compilateur me retourne simplement : "error: ld returned 1 exit status" sans aucune ligne où est arrivé l'erreur.

    Après quelque recherches, j'ai appris que cette erreur était survenue lors de l'édition des liens, mais je ne parviens tout de même pas à comprendre où est mon erreur. J'envois mon code.

    Main.cpp :

    #include <iostream>
    #include "Test.hpp"
    
    using namespace std;
    
    int main(){
        int var(test::somme());
        cout << var;
        cin >> var;
    
        return 0;
    }

    Test.hpp :

    #ifndef NAMESPACE_TEST_DEFINITION
    #define NAMESPACE_TEST_DEFINITION
    
    namespace test{
        int var1 = 1;
        int var2 = 1;
    
        int somme();
    }
    
    #endif // NAMESPACE_TEST_DEFINITION


    Test.cpp :

    #include "Test.hpp"
    
    namespace test{
        int somme(){
            return var1 + var2;
        }
    }

    Et j'ai une dernière question : entre les accolades du namespace, dans le "Test.cpp", quand je tente de modifier les variables var1 et var2 avec "<nom variable> = <nouvelle valeur>", le compilateur me retourne l'erreur "<nom variable> does not name a type", ça signifie qu'on ne peut pas modifier les variables d'un namespace à l'intérieur de lui-même ???

    Merci par avance.

    P.S. : J'ai tenté de redémarrer mon ordinateur, cela ne change rien.

    -
    Edité par Yixraie 24 septembre 2022 à 10:13:25

    • Partager sur Facebook
    • Partager sur Twitter
      24 septembre 2022 à 10:41:14

      Bonjour,

      L'erreur que tu as est une erreur lors de l'édition des liens. Il doit y avoir la raison avant "error: ld returned 1 exit status".

      L'erreur est vraisemblablement une redéfinition des variables test::var1 et test::var2. Elle sont définies dans un fichier d'entête. Il ne faut pas, car test.hpp est inclus 2 fois et donc va créer 2 fois ces variables.

      Quant à l'erreur dans  "<nom variable> = <nouvelle valeur>;", tu as dû l'écrire dans le namespace. Ça n'a pas de sens, on ne peut mettre du code que dans une fonction.

      -
      Edité par Dalfab 24 septembre 2022 à 10:41:41

      • Partager sur Facebook
      • Partager sur Twitter

      En recherche d'emploi.

        24 septembre 2022 à 11:48:03

        Bonjour !

        Si j'ai bien compris, je ne dois pas initialiser mes variables dans le hpp ? Je les initialise où du coup ??

        Merci pour la réponse !

        • Partager sur Facebook
        • Partager sur Twitter
          24 septembre 2022 à 12:14:56

          Ton problème est un problème de variables globales. Pas de namespace.

          C'est la doc/cours au sujet des variables globales qu'il faut que tu regardes (hint: extern à la déclaration; définition unique dans le .cpp).

          PS: les variables globales, c'est mal.

          • Partager sur Facebook
          • Partager sur Twitter
          C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.
            24 septembre 2022 à 12:35:17

            Re-bonjour !

            ça signifie que si je déclare une variable dans un fichier, je ne peux pas la redéclarer dans un autre fichier ? ça me parais normal que ce ne soit pas possible, mais pourtant on définit bien une fonction dans un hpp sans le mot-clé extern avant de la redéclarer dans le cpp après... non ?

            De plus je ne comprend pas bien : mes variables sont déclarées dans mon namespace, alors comment se fait-il qu'elles soient globales ?

            -
            Edité par Yixraie 24 septembre 2022 à 12:47:16

            • Partager sur Facebook
            • Partager sur Twitter
              24 septembre 2022 à 13:08:30

              Ce n'est pas une déclaration d'existence mais une déclaration définissante. Pour juste déclarer "elle existe", il faut mettre extern devant, et la définir ensuite dans un et un seul cpp.

              Non on ne définit pas de fonction dans les fichiers inclus, on ne fait que les déclarer. Dans les fichiers inclus on ne définit que les types qui seront ensuite utilisés (d'une façon qui requiert de connaitre leur structure/taille/fonctions contenues) -- après pour définir une fonction dans un fichier inclus, il fait la déclarer inline, ou la "cacher" (espaces de nom anonyme, etc), et on peut aussi juste déclarer un type (on parle de déclaration antiticipée/forward declaration)

              > De plus je ne comprend pas bien : mes variables sont déclarées dans mon namespace, alors comment se fait-il qu'elles soient globales ?

              Hum... Il y a 2 globalités, une de portée dans le nom (c'est là qu'interviennent les espaces de noms et les classes), et une liée à la résidence relativement à sa durée de vie, on parle aussi de portée (trop simple sinon...). Ce sont des données à "static duration storage". Dans tous les autres langages on va aussi parler de variable globale.

              Bref, ta variable existe avant même que le main() ne démarre. Elle existe encore après. Donc, c'est une variable globale.

              -
              Edité par lmghs 24 septembre 2022 à 13:10:57

              • Partager sur Facebook
              • Partager sur Twitter
              C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.
                24 septembre 2022 à 13:22:28

                lmghs a écrit:

                 > Bref, ta variable existe avant même que le main() ne démarre. Elle existe encore après. Donc, c'est une variable globale.

                -
                Edité par lmghs il y a 8 minutes

                Et comment je fait pour créer une variable dans mon espace de nom et qui n'est pas globale ?
                • Partager sur Facebook
                • Partager sur Twitter
                  24 septembre 2022 à 13:31:30

                  Une telle créature n'existe pas. Toute variable déclarée dans un NS sera globale -- elle commencera à vivre avant le main(), mourra après, et pourra être accessible depuis n'importe où, au détail du nom à rallonge.

                  Il faudrait peut-être que tu reviennes en arrière et étudie et intègre ce que fait le préprocesseur, ce que compile véritablement le compilateur (hint: unité de traduction), etc.

                  • Partager sur Facebook
                  • Partager sur Twitter
                  C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.
                    24 septembre 2022 à 14:07:08

                    Salut,

                    Yixraie a écrit:

                    Bonjour !

                    Si j'ai bien compris, je ne dois pas initialiser mes variables dans le hpp ? Je les initialise où du coup ??

                    Merci pour la réponse !

                    Ben, tout simplement, là où ton code est censé produire des instructions, c'est à dire, dans les fichiers *.cpp.

                    Par contre, tu vas avoir un problème, mais pour te permettre de comprendre le problème -- ce qui devrait t'aider à comprendre la solution -- je vais devoir digresser un peu.

                    La situation actuelle fait que le compilateur va "lire" ton code un peu à la manière dont tu lirais un bon roman: toi tu lis ton roman de la première page à la dernière, le compilateur lit le code d'un fichier *.cpp de la première ligne à la dernière.

                    Or, quand tu as lit la dixième page de ton roman, tu sais ce qui s'est passé ce qui s'est passé durant les neuf premières, mais tu n'a aucune idée de ce qui va se passer à la onzième, et tu es souvent en mesure d'attendre que "quelque chose que tu ignore" arrive ... plus tard.

                    Le compilateur fonctionne somme toutes de la même manière: quand il "lit" la dixième ligne, il "sait" ce qui s'est passé durant les neuf premières et il n'a aucune idée de ce qui se passe à la onzième.  La grosse différence avec toi, c'est qu'il "na pas la patience" d'attendre la onzième ligne pour voir ce qui "viendra plus tard": s'il rencontre "quelque chose" dont il n'a jamais entendu parler (à la dixième ligne) il va te dire "je ne connais pas ce truc, le code est faux", et "basta", il va arrêter le travail.

                    En plus, il faut savoir que, alors que tu as "de grandes chances" de te souvenir (au moins de détails) d'un livre qui t'a plu, le compilateur va "oublier" tout ce qu'il a pu faire dés qu'il a fini de travailler sur un fichier *.cpp

                    C'est à dire que s'il doit traiter les fichiers A.cpp, B.cpp et C.cpp l'un à la suite de l'autre, il va traiter A.cpp, "oublier" tout ce qu'il a fait, traiter B.cpp et à nouveau ... oublier tout ce qu'il a fait avant de traiter C.cpp et... encore oublier tout ce qu'il a fait.

                    Il faut donc s'assurer en permanence pour que le compilateur "connaisse" en permanence ... tout ce que l'on utilise.  Et pour cela il y a deux solutions:

                    La déclaration qui va se "contenter" de dire au compilateur que "quelque chose existe", et qui va se contenter d'inciter le compilateur à générer un "identifiant unique" (on parle de "symbole") pour ce quelque chose, sans lui faire générer l'instruction qui permettrait au processeur de l'utiliser.

                    Il est très simple de générer une déclaration, vu que cela se réduit généralement à donner le code qui permet de nommer la chose, et de le faire suivre directement par le symbole "end of statement" : le point-virgule ';'. On peut donc le faire très facilement

                    • pour les classes et les structures (car c'est sensiblement pareil) avec un code du genre de class MaClass; ou struct MaStructure; qui indique qu'il existe quelque part une classe nommée MaClasse ou une sturcture nommée MaStructure, même si on ne sait encore absolument pas de quoi elle est composée
                    • pour les fonctions avec un code proche de double maFonction(int, double); qui indique au compilateur qu'il existe "quelque part" une fonction nommée maFonction, qui prend un entier et un double comme paramètres et qui ne renvoie une valeur de type double, même si on n'a "aucune idée" des instructions qu'elle va exécuter
                    • ou même pour les espace de noms, avec un code proche de namespace MonEspaceDeNoms; qui indiquera au compilateur que l'espace de nom MonEspaceDeNoms existe, même si on n'en connait pas encore le contenu

                    Pour ce qui est des données (qu'il s'agisse de variables ou de constantes), les choses sont un peu plus difficiles, car, a priori, le compilateur va systématiquement essayer de réserver un espace mémoire suffisant que pour être en mesure de représenter entièrement la donnée en question.

                    La règle de base est donc que l'on va définir une donnée.

                    Et donc, la deuxième solution que l'on a de faire savoir que "quelque chose" existe au compilateur est la définition (on peut également parler d'implémentation pour les fonctions).  C'est le processus qui va tenter de "donner du corps" à l'élément déclaré.

                    Ce sera fait en placant ce code entre une paire d'accolade '{' et '}'

                    Pour les classes et les structures, ce sera fait en donnant ... le contenu qui les composent, par exemple

                    // on "donne du corps" à la classe MaClass
                    class MaClasse{
                    public:
                        // déclaration des fonctions publiques
                        void foo();
                        void bar();
                    private:
                        // déclaration des données privées
                        int data;
                    };
                    
                    //ou
                    // on "donne du corps à la structure MaStruct
                    struct MaStruct{
                        int i;
                        float f;
                        double d;
                    };

                    Pour les fonctions, ce sera fait en indiquant toutes les instructions qui doivent être exécutée pour obtenir le résultat souhaité, par exemple

                    double maFonction(int i, double d){
                        // tout ce qui doit être fait pour
                        // obtenir le résultat souhaité et 
                        // renvoyer une valeur de type double
                    }

                    pour les espaces de noms, ce sera fait en fournissant soit des déclarations, soit des définitions, par exemple

                    namespace MonEspaceDeNoms {
                        // ceci est la déclaration d'une fonction
                        // mais sa définition pourrait également prendre
                        // place ici
                        double maFonciton(int i, double d);
                        // ceci est la définition d'une classe
                        class MaClasse{
                        public:
                            // déclaration des fonctions publiques
                            void foo();
                            void bar();
                        private:
                            // déclaration des données privées
                            int data;
                        };
                        // nous pourrions aussi déclarer un espace de noms
                        // "interne" à celui dans lequel nous nous trouvons
                        namespace Details;
                    }

                     Ce qu'il est intéressant de savoir enfin, c'est qu'une définition a toujours un effet de déclaration, alors qu'une déclaration n'a jamais un effet de définition.

                    Enfin, comme on travaille souvent avec plusieurs fichiers qui seront "mis ensembles" par un outils appelé "linker" (ou éditeur de liens) pour générer l'exécutable final, il y a une règle essentielle en programmation qui s'appelle ODR (pour One Definition Rule ou règle de la définition unique, si tu préfères en français) qui s'applique: chaque définition (de fonction ou de donnée) ne peut apparaitre qu'une seule et unique fois dans l'ensemble des fichiers qui seront regroupés par l'éditeur de liens. (*)

                    Si, à un moment quelconque, l'éditeur de liens découvre qu'un symbole est défini plusieurs fois (que ce soit dans un seul fichier ou dans plusieurs), il ne pourra pas choisir vers lequel de ces symboles pointer, et il va donc t'engueuler parce qu'il est dans l'incapacité de faire son taf, avant ... d'arrêter purement et simplement de travailler.

                    Le truc, c'est que, comme le compilateur oublie systématiquement tout ce qu'il a pu faire lors du traitement d'un fichier particulier, ben, il n'a aucun moyen de garantir que cette règle soit respectée.

                    Or, rappelle toi, je t'ai dit que le code qui te permet de créer une donnée est systématiquement considéré par le compilateur comme une définition.

                    Cela implique que, si tu as un fichier d'en-tête qui prend la forme de

                    #ifndef NAMESPACE_TEST_DEFINITION
                    #define NAMESPACE_TEST_DEFINITION
                     
                    namespace test{
                        int var1 = 1; // ceci est une définition
                        int var2 = 1; // ceci est aussi une définition
                     
                        int somme();  // par contre, ceci est une déclaration
                    }
                     
                    #endif // NAMESPACE_TEST_DEFINITION

                    et que tu l'inclus dans deux fichiers (ou plus), que le compilateur va générer le code qui permet ... de définir les variables var1 et var2 dans ... tous les fichiers dans lesquels le fichier d'en-tête aura été inclus.  Ce qui va -- bien sur -- faire râler l'éditeur de liens.

                    (*) En fait, les choses sont même un peu plus compliquées, car il existe également la notion de contexte, qui implique que la même chose ne peut pas être définie deux fois dans le même contexte, alors que l'éditeur de liens pourrait encore sous certaines conditions se satisfaire de retrouver le même symbole défini dans deux contextes différents.

                    Cela ne change malgré tout rien aux explications ;)

                    C'est donc à nous de nous assurer que les données ne seront jamais déclarées qu'une seule et unique fois dans chaque contexte envisagé.

                    La bonne question est : veux tu vraiment que var1 et var2 (qui appartiennent à l'espace de nom Test ) soient connues dans ton fichier main.cpp alors que tu ne les utilises absolument jamais dans main.cpp?

                    Nous pourrions dire que, en l'état actuel des choses, tu n'en a pas vraiment besoin, vu que tu ne les utilises pas.

                    Tu déclare (d'une manière que l'on ne devrait plus utiliser) une donnée var qui prend la valeur renvoyée par la fonction Test::somme() (qui utilise effectivement les valeurs de Test::var1 et de Test::var2), mais, tout ce que tu as besoin de connaitre, dans main.cpp au sujet de l'espace de noms Test, c'est... l'existence de la fonction, vu qu'elle va agir comme une sorte de "boite noire": tu sais pas ce qu'elle contient, mais tu t'attends à obtenir "en sortie" une valeur de type int.

                    Au point où l'on en est, ton fichier Test.hpp pourrait donc se limiter à quelque chose comme

                    #ifndef NAMESPACE_TEST_DEFINITION
                    #define NAMESPACE_TEST_DEFINITION
                     
                    namespace test{
                        // Le compilateur n'a même pas besoin de connaitre
                        // l'existence des données var1 et var2 dans main.cpp
                        // autant ne pas les déclarer ici (ce qui nous
                        // arrange)
                        int somme();
                    }
                     
                    #endif // NAMESPACE_TEST_DEFINITION

                    Par contre, le compilateur doit connaitre ces deux données lorsqu'il va traiter le fichier Test.cpp, et il faudra donc le corriger pour lui donner la forme de

                    #include "Test.hpp"
                     
                    namespace test{
                        // je change les valeurs, pour préparer la suite 
                        int var1 = 2; 
                        int var2 = 3;
                        int somme(){
                            return var1 + var2;
                        }
                    }

                    Et ton programme devrait pouvoir fonctionner avec ces quelques modifications

                    Par contre, si nous décidions de modifier un tout petit peu ton fichier main afin de faire en sorte que la connaissance de var1 et var2 (dans main.cpp) devienne nécessaire, par exemple, en lui donnant la forme de

                    #include <iostream>
                    #include "Test.hpp"
                     
                    /* je suis contre l'utilisation de la directive 
                     * using namespace std; mais c'est hors sujet
                     */
                     
                    int main(){
                        /* Le meilleur moyen de rendre la connaissance
                         * de "quelque chose" indispensable est encore
                         * d'essayer de l'utiliser
                         */
                        std::cout<<"Test::var1 = "<<Test::var1<<"\n"; //pour var1
                        std::cout<<"Test::var2 = "<<Test::var2<<"\n"; //pour var2
                        /* il n'y a pas forcément besoin de passer par
                         * une donnée temporaire pour l'appel de la 
                         * fonction: nous pouvons directement afficher
                         * la valeur qu'elle nous renvoie
                         */
                        std::cout<<"Test::somme() = "<<Test::somme()<<"\n";
                        return 0;
                    }

                    Ben, en fait, les modifications que nous venons d'apporter à Test.hpp et à Test.cpp ne suffiront plus, car, en l'état des choses, le compilateur ne saura pas lorsqu'il traite main.cpp que les variables Test::var1 et Test::var2 existent.

                    Et rappelle toi, on ne peut pas se contenter de rajouter un int var1; et un int var2; dans Test.hpp, car le compilateur va considérer cela comme une définition, malgré le fait qu'on ne leur donne aucune valeur.

                    Ce que nous devons donc faire, c'est écrire un code qui revienne, en gros, à dire au compilateur quelque chose comme "je te garanti que ces variables existent bel et bien "quelque part" et que tu peux y faire appel "à ma guise", par contre je veux donc que tu considère cette instruction pour ce qu'elle est: une déclaration".

                    Et ca, c'est le rôle du mot clé extern.  Nous pouvons donc modifier "une dernière fois" notre fichier Test.hpp (vu que Test.cpp contient déjà la définition de var1 et de var2) pour lui donner la forme de

                    #ifndef NAMESPACE_TEST_DEFINITION
                    #define NAMESPACE_TEST_DEFINITION
                     
                    namespace test{
                        extern int var1; // c'est une déclaration, 
                        extern int var2; // aucun besoin de donner de 
                                         // valeur particulière
                        int somme();
                    }
                     
                    #endif // NAMESPACE_TEST_DEFINITION

                    Ce qui aura pour résultat final que, lorsqu'il traitera Test.cpp, le compilateur croisera les déclarations de var1 et de var2 et n'en fera rien, grâce à l'inclusion de Test.hpp, puis, plus loin, croisera les définitions de ces deux variables, et fera "ce qu'il faut" pour qu'elle trouve un endroit en mémoire pour exister.

                    De même, lorsqu'il traitera le fichier Main.cpp, il croisera les déclarations de Test::var2 et Test::var2, et saura donc qu'elles existent lorsqu'on essayera d'y accéder pour les afficher.  Et tout le monde sera content ;)

                    Par contre, je ne peux décemment pas faire toute une intervention qui a trait à des données constantes sans m'insurger sur le fait que

                    LES VARIABLES GLOBALES, C'EST MAL

                    Quand il s'agit de constantes (donc, de données dont la valeur ne changera jamais), cela peut se justifier d'en faire des globales, mais les variables (les données dont la valeur est susceptible d'être modifiée "n'importe quand"), cela peut poser de très (mais vraiment) très sérieux problèmes ;)

                    • 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 septembre 2022 à 15:06:31

                      Waouh, merci BEAUCOUP pour toutes ce explications !

                      Le problème, c'est que du coup je me pose de nouvelles questions:D

                      Tout d'abord, quand je déclare une variable statique dans une classe, je ne met pas le mot-clé externe et pourtant il n'y a pas d'erreurs, est-ce que "static" fait aussi le travail de "extern" ?

                      Ensuite, tu dis que (je cite :) "Les variables globales, c'est mal". Je l'ai bien compris car on me l'a déjà dit mais je ne sais pas quelles sont les autres solutions ?

                      Après, est-ce que tu peux m'en dire plus sur "Tu déclare (d'un manière que l'on ne devrait plus utiliser) une donnée var" ? Je ne vois pas trop de quoi tu parles.

                      Et enfin, si j'ai bien tout compris, quand je défini ma variable dans mon espace de nom dans le Test.hpp, normalement rien ne devrait m'empêcher de la modifier dans le cpp si ?

                      P.S. : J'ai peur de m'égarer sur un autre sujet avec cette question, mais j'ai l'impression que le type "auto" ne peut pas être déclaré sans être défini en même temps... et ça me pose problème.

                      -
                      Edité par Yixraie 24 septembre 2022 à 15:24:47

                      • Partager sur Facebook
                      • Partager sur Twitter
                        24 septembre 2022 à 17:51:32

                        Yixraie a écrit:

                        quand je déclare une variable statique dans une classe, je ne met pas le mot-clé externe et pourtant il n'y a pas d'erreurs, est-ce que "static" fait aussi le travail de "extern" ?

                        Disons plutôt que le mot clé static a "sensiblement le même effet" que le mot clé extern, mais pas pour les mêmes raisons.

                        En effet, le mot clé externdit au compilateur quelque chose comme

                        Cette variable existe "quelque part", je veux que tu le saches, je veux pas que tu décides de la faire "vivre en mémoire"

                        Par contre, avant de te parler du mot clé static, je me dois de préciser un "petit détail oublié" concernant la définition des classes et des structures: c'est que son contenu n'est en fait considéré que comme des déclarations.

                        Cela peut paraître bizard car on peut très bien avoir un code proche de

                        class MaClasse{
                        public:
                            /* Ceci n'est clairement que des déclarations */
                            MaClasse(int a, int b)
                            void foo();
                            /* alors que ceci est clairement une définition */
                            int somme() const{
                                return a + b;
                            }
                        private:
                            /* et ceci ne sont encore que des déclarations
                            int a; 
                            int b;
                        };

                        Dont le but est -- clairement -- de dire au compilateur "voici ce que contient ma classe" (c'est donc une définition de classe), ou, plus précisément, de faire savoir que, à l'intérieur de la classe en question, il doit savoir qu'il existe:

                        • une fonction particulière (le constructeur) prenant deux paramètres en entrées
                        • une fonction foo qui ne prend aucun paramètre et qui ne renvoie rien (on sait pas ce qu'elle fait, mais c'est pas grave)
                        • une fonction somme qui s'engage à ne pas modifier l'état de la variable à partir de laquelle elle sera appelée qui ne prend aucun paramètre et qui renvoie un entier (on sait déjà ce qu'elle fait, mais en fait ... on s'en fout... tout ce qu'on veut, c'est qu'il sache qu'elle existe)
                        • deux entiers nommés respectivement a et b, qui sont dans une accessibilité protégées.

                        et tout cela, ce sont bel et bien (relis les termes utilisés) ... des déclarations.

                        En effet, les données a et b ne pourront exister "quelque part en mémoire" que ... lorsque l'on aura effectivement créé une instance (je devrais écrire "défini une instance") de MaClasse.

                        Et, de même, nous ne pourrons appeler les fonctions foo et somme qu'à partir ... d'une instance existante de la classe MaClasse.

                        Autrement dit, tant que l'on n'a pas défini une instance de la classe MaClasse, on peut partir du principe qu'il n'y a ... absolument rien qui est fait. (ce n'est pas tout à fait vrai, dans le sens où il existe forcément une définition du constructeur et de la fonction foo "quelque part" dans ton code qui aura provoqué la génération des instructions adéquates par le compilateur, mais bon, c'est un détail).

                        Mais, revenons donc à nos moutons et remettons nous à parler du mot clé static.

                        Lorsque l'on déclare une variable ou une fonction membre comme étant "statique", ce que l'on fait au final, c'est dire quelque chose comme

                        Cette donnée (ou fonction) clairement partie de la classe (autrement dit: si nous voulons y accéder, nous devrons indiquer que nous cherchons bel et bien à accéder à la donnée ou à la fonction qui fait partie de cette classe bien précise, à l'exception de toute autre donnée ou fonction qui pourrait exister "en dehors de la classe") mais elle ne dépend d'aucune instance particulière de cette classe.

                        Et là, ben, je te renvoie quelque lignes plus haut afin d'éviter d'avoir à me répéter. Cela me permettra de te dire que, dans l'état dans lequel se trouve ma classe MaClasse, je ne peux pas écrire un code proche de

                        int main(){
                            MaClasse::foo(); 
                            /* ou */
                            int result = MaClasse::somme();
                        }

                        Parce que j'ai besoin d'une donnée de type MaClasse pour pouvoir faire appel à ces fonctions.

                        Par contre, si je modifie ma classe pour lui donner une forme proche de

                        class MaClasse{
                        public:
                            /* Ceci n'est clairement que des déclarations */
                            MaClasse(int a, int b)
                            void foo();
                            /* alors que ceci est clairement une définition */
                            int somme() const{
                                return a + b;
                            }
                            /* voici la déclaration d'une donnée(constante) 
                             * qui ne dépend d'aucune instance particulière
                             * de la classe
                             * NOTA: depuis peu, nous pouvons même donner 
                             *       directement une valeur dans le cas
                             *       présent 
                             */
                            static const int max = 255;
                        private:
                            /* et ceci ne sont encore que des déclarations
                            int a; 
                            int b;
                        };

                        la donnée max fait -- clairement -- partie de la classe MaClasse, par contre, elle n'a absolument pas besoin qu'une instance de la classe existe pour exister.

                        Plus précisément, la donnée MaClasse::max doit exister (et être accessible) ... même si aucune instance de la classe n'a été créée (et peut-être même si aucune instance de la classe ne sera jamaiscréée), ce qui me permet tout à fait d'avoir un code proche de

                        #include <iostream>
                        int main(){
                            /* NOTA: il n'y a aucune instance de MaClasse qui a été
                             *       créée nulle part, et il n'y en aura pas
                             */
                            std::cout<<"MaClasse::max = "<<MaClasse::max<<"\n";
                        }

                        Tu remarqueras -- au risque de digresser "encore un peu", que j'ai choisi de rendre la donnée max constante.  C'est parce que cette donnée va agir "comme une variable globale", malgré le fait qu'elle appartienne clairement à la classe MaClasse ;)

                        Yixraie a écrit:

                        Ensuite, tu dis que (je cite :) "Les variables globales, c'est mal". Je l'ai bien compris car on me l'a déjà dit mais je ne sais pas quelles sont les autres solutions ?

                        Ah, ben, ca, c'est une excellente question, je te remercie de l'avoir posée :D

                        Car il n'y a pas de "solution miracle" et la "bonne solution" dépendra souvent des circonstances.

                        Le plus simple est souvent de créer les données dont tu as besoin ... dans la fonction qui les utilise, par exempe (pour reprendre ton code de départ)

                        Dans le fichier d'en-tête

                        namespace Test{
                            // déclaration de la fonction somme
                            int somme();
                        }
                        

                        dans le fichier d'implémentation de la fonction

                        namespace Test{
                            int somme(){
                                int a = 3;
                                int b = 5;
                                return a + b;
                            }
                        }

                        l'appel de la fonction

                        int main(){
                            std::cou<<Test::somme()<<"\n";
                        }

                        Simple et efficace. Si cela marche comme cela et que tu n'as pas d'autres impératifs à respecter, c'est la solution qu'il te faut ;)

                        Une autre solution consiste à utiliser les paramètres et la valeur de retour à ton aventage si la fonction a besoin de donnée externes dont elle ne peut pas définir la valeur par elle même, par exemple:

                        #include <iostream>
                        namespace Test{
                           /* on peut vouloir faire jouer les deux valeurs
                            * utilisées pour calculer la somme, donnons les
                            * en paramètres
                            */
                            int somme(int gauche, int droite){
                                return gauche  + droite;
                            }
                        }
                        int main(){
                            /* ca fonctionne avec des données */
                            int a = 3;
                            int b = 5;
                            std::cout<<"somme(a, b) = "<<Test::somme(a,b)<<"\n";
                            /* parfois (c'est le cas ici) cela fonctionne
                             * avec des "valeurs magiques" (des valeurs
                             * arbitraires sorties d'on ne sait ou)
                             */
                            std::cout<<"somme(10, 15)= "<<Test::somme(10,15)<<"\n";
                        }

                        C'est presque tout aussi simple et aussi effeicace ;)

                        Notes que l'on peut mélanger les deux solutions abordées jusqu'à présent, par exemple

                        #include <iostream>
                        namespace Test{
                           int multiplieSomeParTrois(int gauche, int droite){
                               int terme = 3;
                               return (gauche + droite) * terme;
                           }
                        }
                        
                        int main(){
                            int a = 5;
                            int b = 4;
                            std::cout<<"resultat = "<<multiplieSomeParTrois(a,b)<<"\n";
                        }

                        Quand plusieurs données vont "tellement bien ensembles" qu'elles en viennent à "perdre tout leur sens" si elles sont utilisées seules, nous pouvons définir des types définis par l'utilisateur (classe ou structures) pour les regrouper. Et nous pouvons les utiliser aussi bien comme "donnée locale", comme paramètre ou comme valeur de retour.

                        Il faut savoir qu'il n'y a rien que nous puissions faire avec une classe que nous ne puissions pas faire avec une structure.  Nous pouvons même -- si nous le voulons -- fournir des fonctions membres à une structure, si nous le jugeons utile.

                        La seule différence qui existe entre le mot clé class et le mot clé struct, c'est l'accessibilité utilisée "par défaut" (comprends : tant que l'on n'a pas donné d'instruction contraire au compilateur) qui sera private pour les classes et public pour les structures.

                        Et il y a encore plein d'autres solutions, auxquelles je ne penserai pas forcément ici, mais qui sont "tellement cool" :D

                        Yixraie a écrit:

                        Après, est-ce que tu peux m'en dire plus sur "Tu déclare (d'un manière que l'on ne devrait plus utiliser) une donnée var" ? Je ne vois pas trop de quoi tu parles.

                        Il faut savoir que le C++ a subit une première très grosse évolution en 2011, qui a -- entre autres -- essayé d'homogénéiser la manière de définir les valeurs pour "a peu près tout".

                        Depuis cette évolution -- qui date quand même de onze ans!!!, et qui a été suivie par de nouvelles évolutions  en 2014, 2017, 2020 et une en préparation prévue pour 2023 -- nous préférons utiliser l'initialisation "homogène", qui utilise les accolades au lieu des parenthèses:

                        int main(){
                            /* avant */
                            int var(Test::somme());
                            /* maintenant */
                            int autreVar{Test::somme()};
                            /* ou */
                            int derniere{5};
                        
                        }

                        Yixraie a écrit:

                        Et enfin, si j'ai bien tout compris, quand je défini ma variable dans mon espace de nom dans le Test.hpp, normalement rien ne devrait m'empêcher de la modifier dans le cpp si ?

                        Non, tu dois déclarer ta variable dans Test.hpp, et la définir dans Test.cpp.

                        Je te reprends sur ce point parce qu'il est vraiment important d'utiliser le bon terme pour la bonne chose. La preuve, c'est que, si tu ne dis pas au compilateur que ce qui se trouve dans Test.hpp est une déclaration avec le mot clé extern, il va la considérer comme une définition, et que cela foutra le bordel partout ;)

                        Par contre, une fois ta variable déclarée dans Test.hpp, tu peux effectivement la modifier dans n'importe quel fichier d'implémentation (*.cpp) dans lequel le fichier d'en-tête (Test.hpp) a été inclu.

                        Pas seulement dans Test.cpp, mais aussi dans Main.cpp ou, si tu crée le fichier BlaBla.cpp qui inclut Test.hpp, dans BlaBla.cpp aussi.

                        Et si Test.hpp est inclut dans un fichier d'en-tête A.hpp, et que ce fichier d'en-tête est inclut dans A.cpp, tu pourra également modifier la valeur de cette variable dans A.cpp.

                        Peut-être commence tu à voir où se situe le problème avec les variables locales:  tout le monde peut "y chipoter à sa guise" depuis "n'importe où". Si bien que l'on ne peut jamais dire quelle sera sa valeur à un instant bien particulier parce qu'elle aura pu changer trente six fois de trente-six manière différentes pour trente-six raisons différentes entre le début du programme (moment où sa valeur d'origine aura été donnée) et le moment qui nous intéresse.

                        Yixraie a écrit:

                        mais j'ai l'impression que le type "auto" ne peut pas être déclaré sans être défini en même temps... et ça me pose problème.

                        Alors, ca, ca va dépendre énormément de la norme envisagée, car l'utilisation du mot clé auto est apparue en 2011, et chacune des évolutions ultérieures a ajouté une petite précision sur les possibilités de ce mot clé

                        Et le truc, c'est que c'est une fonctionnalité très simple (dans son principe) mais tellement géniale (dans les possibilités qu'elle offre) qu'il y a énormément à dire dessus.  Je te conseillerais donc effectivement de poser une question séparée sur le sujet (à moins que quelqu'un n'ait entamé une partie de réponse avant que je n'aie rédigé celle-ci) ;)

                        • 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 septembre 2022 à 21:45:44

                          Salut !

                          Quelques petites questions sur ce que tu viens de m'apprendre :

                          1 - L'utilisation des accolades au lieu des parenthèses pour initialiser une variable, à quoi ça sert ? Comment ça marche ?

                          2 - Je ne parviens toujours pas à saisir entièrement les notions de déclarations et de définitions, car quand je déclare ma variable dans mon header avec le mot clé extern, je peux la définir dans un fichier .cpp plus tard, aucun problème.

                          Par contre, quand je défini ma variable dans le header, pourquoi je ne pourrais pas la modifier dans le .cpp ? Pas la redéfinir hein, vraiment la modifier comme on le fait avec n'importe quelle variable avec l'utilisation de <variable> = <valeur>.

                          Merci d'avance !

                          • Partager sur Facebook
                          • Partager sur Twitter
                            25 septembre 2022 à 3:20:44

                            Bonjour,
                            Je ne prétend pas concurrencer mes prédécesseurs qui sont des experts en C++.
                            Mais, comme on dit, j'ai déjà "entendu parler" des éditeurs de liens.
                            (Ça s'appelait des "loader" dans l'antiquité ..., ld ça vient de loader)
                            >  2 - Je ne parviens toujours pas à saisir entièrement les notions de déclarations et de définitions, car quand je déclare ma variable dans mon header

                            > avec le mot clé extern, je peux la définir dans un fichier .cpp plus tard, aucun problème.
                            Je vais utiliser une comparaison.
                            Les fichiers *.cpp compilés sont des sortes de "boîtes noires" dont on ne sait rien, si ce n'est des noms et/ou adresses des fonctions qui s'y trouvent.
                            Mais on peut placer également sur "l'étiquette" de la boîte noire le nom d'une variable et donc son adresse.
                            C'est ce que fait extern
                            De la même façon que tu peux appeler une fonction de la boîte noire, tu peux modifier les variables de la boîte noire qui sont connues de l'extérieur (extern).
                            > Par contre, quand je défini ma variable dans le header, pourquoi je ne pourrais pas la modifier dans le .cpp ? Pas la redéfinir hein, vraiment la modifier
                            > comme on le fait avec n'importe quelle variable avec l'utilisation de <variable> = <valeur>.
                            Tu peux la modifier dans le "même" fichier *.cpp car, comme on te l'a dit, le compilateur la connait.
                            Mais ça reste une variable "globale", mais globale pour ce fichier *.cpp "seulement".

                            -
                            Edité par PierrotLeFou 25 septembre 2022 à 3:36:56

                            • Partager sur Facebook
                            • Partager sur Twitter

                            Le Tout est souvent plus grand que la somme de ses parties.

                              25 septembre 2022 à 9:15:21

                              PierrotLeFou a écrit:

                              Tu peux la modifier dans le "même" fichier *.cpp car, comme on te l'a dit, le compilateur la connait.

                              Pourtant, après avoir modifié ce qui n'allait pas, j'arrive également à modifier cette variable dans le main.cpp... Comment c'est possible, si le compilateur oublie ce qu'il a fait dans les fichiers précédents ?

                              -
                              Edité par Yixraie 25 septembre 2022 à 9:16:59

                              • Partager sur Facebook
                              • Partager sur Twitter
                                25 septembre 2022 à 10:45:27

                                Yixraie a écrit:

                                Pourtant, après avoir modifié ce qui n'allait pas, j'arrive également à modifier cette variable dans le main.cpp... Comment c'est possible, si le compilateur oublie ce qu'il a fait dans les fichiers précédents ?

                                Il oublie. Mais ce qui a été fabriqué reste. Si une unité de compilation a défini les variables et que l'autre les déclare externe, elle pourra accéder à ces variables.

                                • Partager sur Facebook
                                • Partager sur Twitter

                                En recherche d'emploi.

                                  25 septembre 2022 à 13:18:40

                                  Merci beaucoup pour toutes ces réponses ! Je passe le sujet en résolu !
                                  • Partager sur Facebook
                                  • Partager sur Twitter
                                    25 septembre 2022 à 14:11:05

                                    Yixraie a écrit:

                                    1 - L'utilisation des accolades au lieu des parenthèses pour initialiser une variable, à quoi ça sert ? Comment ça marche ?

                                    Il faut comprendre que, avant, l'initialisation des variables était un "joyeux bordel" car on avait une quantité assez importante de possibilités qui n'étaient utilisable que... une situation particulière.

                                    Par exemple:

                                    1. initialiser une donnée de "type primitif" (faisons simple: de type char, short, int, long, float ou double) se faisait en utiliisant l'opérateur = : int i = 5;
                                    2. initialiser un tableau de taille fixe se faisait avec l'opérateur = et des accolade : int tab[3]={1,2,3};
                                    3. sauf pour les chaines de caractères (qui sont des tableaux de char, à la base), qui utilisaient l'opérateur = : char maString[12]="hello world";
                                    4. initialiser une structure (ou une classe) lorsque le constructeur nécessite des paramètres se faisait avec des parenthèse: Position mapos(3,15);
                                    5. mais cette manière de faire n'allait pas pour les structures et les classes dont le constructeur ne prenait pas d'argument, car cela correspondait à une déclaration de fonction: MaClass objet(); n'était pas bon (c'est la déclaration d'une fonction appelée objet, qui ne prend aucun argument et qui renvoie une donnée de type MaClasse), et il fallait donc écrire MaClasse objet;
                                    6. Et quelques variations possibles qui ont finalement peu d'intérêt ici ;)

                                     Voilà qui fait beaucoup de possibilités et, comme ce sont des possibilités qui ne s'appliquent qu'à une situation bien particulière, beaucoup de possibilités de se tromper.

                                    Il fallait donc "homogénéiser" un peu tout cela, en ajoutant une nouvelle possibilité (les autres n'ont pas été supprimées pour que le code écrit avant continue à fonctionner) qui puisse fonctionner à la place de toutes les autres, pour que les développeurs puissent décider d'utiliser cette nouvelle possibilité "sans réfléchir" à tous les coups.

                                    En fait, cette nouvelle possibilité existait déjà, vu qu'ils ont choisi l'initialisation avec les accolades. Ils ont juste décidé de faire en sorte qu'elle puisse fonctionner ... dans toutes les situations.

                                    Désormais, nous pouvons donc initialiser n'importe quelle donnée de la même manière:

                                    • il n'y a plus besoin de l'opérateur égal pour les types primitifs : int i{5};
                                    • il n'y en a plus besoin pour les tableaux de taille fix: int tab[3]{1,2,3};
                                    • et cela fonctionne aussi pour les chaines de caractères : char maString[12]{"Hello world"};
                                    • on remplace les parenthèse d'appel au constructeur des structure et des classe par les accolade: Position mapos{3,15};
                                    • Et s'il n'y a pas de paramètres, cela fonctionne tout aussi bien: MaClasse objet{};
                                    • Et bien sur, il en va de même pour toutes les variations que je n'ai pas citées
                                    • mais en plus, cela nous rajoute quelques possibilités que nous n'avions pas "à l'époque".

                                    C'est ... Aussi simple que cela : tu veux initialiser une variable lors de sa définition, tu rajoute des accolades et "c'est parti" :D

                                    Bien sur, j'ai simplifié et il reste quelques détails à envisager.  Mais bon, en l'état actuel, tu sais tout ce qu'il te faut savoir pour arriver à comprendre.

                                    Yixraie a écrit:

                                    2 - Je ne parviens toujours pas à saisir entièrement les notions de déclarations et de définitions, car quand je déclare ma variable dans mon header avec le mot clé extern, je peux la définir dans un fichier .cpp plus tard, aucun problème.

                                    Ne t'inquiète pas du mot clé extern dans un premier temps, ce n'est qu'un "détail de syntaxe" du langage ;)

                                    Tout ce que tu dois comprendre, c'est qu'une déclaration se contente de dire au compilateur que "ce truc existe (mais je ne souhaite pas actuellement que tu me le crée)".

                                    Par contre, la déclaration ne contient absolument aucune information autre que ... le fait que "le truc dont on parle" existe bel et bien.

                                    Il faut pouvoir dire au compilateur que "quelque chose existe" pour différentes raisons:

                                    La première, c'est parce qu'il ne sera pas content s'il croise un terme qu'il ne connaît pas dans le code.

                                    La deuxième, c'est que le fait de savoir que "quelque chose" existe va lui permettre de faire certaines vérifications qu'il ne pourrait -- forcément -- pas faire s'il ignorait l'existence de ce "quelque chose" et pour lesquelles il n'a ... pas forcément besoin d'en savoir plus, en fait.

                                    Enfin -- et surtout-- le fait de dire que "quelque chose existe" va nous permettre de résoudre dans le code pas mal de problème qui risquent de survenir soit à cause d'une règle qui "vient foutre le bordel" (comme l'ODR) et que nous pourrons résoudre à l'aide du mot clé extern (relis la description que j'en ai faite plus tôt), soit parce que la situation va faire qu'autrement, nous allons nous trouver dans une situation du "serpent qui se bouffe la queue".

                                    Seulement, pour cette deuxième sorte de problème (les situations dans lesquelles le serpent se bouffe la queue), il faut revenir sur la notion de définition, je reviendrai là dessus "plus loin" ;)

                                    La notion de définition va -- bien sur -- dire au compilateur que "quelque chose existe", mais en plus, ca va "lui donner un corps", ca va obliger le compilateur à "faire quelque chose de plus" qu'à simplement prendre note du fait que le truc existe.

                                    Pour les types de données (les classes et les structures), cela va l'obliger à ... calculer la taille dont ce type de donnée a besoin pour pouvoir être représenté en entier en mémoire.  Ainsi, lorsque tu définis une structure sous la forme de (je fais simple)

                                    struct Position{
                                        int x;
                                        int y;
                                    };

                                    cela oblige le compilateur à calculer que, lorsque je voudrai créer une instance (une variable, une donnée) de type Position, il devra prévoir la place en mémoire pour pouvoir représenter ... deux entiers (le premier étant appelé x et le deuxième étant appelé y ).

                                    De même, pour les fonctions, cela va obliger le compilateur à ... générer des instructions compréhensibles par le processeurs qui correspondent aux différentes instructions de la fonction.  Ainsi, lorsque tu défini une fonction sous la forme (je fais toujours aussi simple ... quoi que)

                                    /* voici une fonction appelée foo, qui ne prend aucun 
                                     * paramètre et qui ne renvoie aucune information
                                     */
                                    void foo(){
                                        int essais{1};
                                        bool reussi{false};
                                        while(essai<4 && ! resussi){
                                            std::cout<<"combien font 3*2?";
                                            int result;
                                            cin >>result;
                                            reussi = result == 6;
                                            if(!resussi && essais<4){
                                                essais++;
                                                std::cout<<"C'est faux, allez, on réssaie\n"
                                                                "essais numero "<<essais<<"n";
                                            }
                                        }
                                        if(reussi)
                                            std::cout<<"bravo!, vous avez gagné un malabar\n";
                                        else
                                            std::cout<<"C'est faux\n"
                                                     <<"il faudra être plus attentif en classe\n";
                                    }

                                    Le compilateur n'aura pas d'autre choix que de traduire toutes ces instructions de manière à ce que cette fonction... existe dans le programme final.

                                    Enfin, pour les données (les "variables" sauf que certaines ne varient pas forcément), la définition va obliger le compilateur à ... prévoir un espace mémoire suffisant pour représenter entièrement la donnée en question. Ainsi, lorsque tu définis une donnée sous la forme de (on reste dans le simplisime)

                                    Position laPos{3,15};

                                    tu peux te dire que le compilateur va mettre en place "toute une série" d'instruction qui permettront non seulement l'espace nécessaire pour représenter les entiers nommés laPos.x et laPos.y, mais aussi pour faire en sorte que la valeur de x soit "de base" égale à 3 et que celle de y soit "de base" égale à 15.

                                    Pour faire simple, nous pourrions dire que la définition est une déclaration qui ... demande "plus de travail" de la part du compilateur ;)

                                    J'espère que c'est désormais plus clair, car, sinon, ce sera un véritable défi que d'essayer de t'expliquer les choses autrement :p:-°

                                    D'autant plus, que je te l'ai promis, je reviens avec mon histoire de "serpent qui se bouffe la queue".  Si tu veux boire un café, fumer une cigarette ou vider ta vessie, c'est peut être le moment rêvé, car les explications seront longues.  Et accroche toi, car "ca va faire mal" :D

                                    Mettons que j'aie une structure que je nommerai A et à laquelle je donne directement "un corps" dans le fichier A.hpp sous la forme de

                                    A.hpp

                                    ifndef A_HPP
                                    #define A_HPP
                                    #include <Position.hpp>
                                    struct A{
                                        int nbreVies{4};
                                        Position position;
                                    }
                                    #endif A_HPP

                                    Jusque là, y a rien de particulier: je donne une définition de ma structure A, ce qui oblige le compilateur à calculer la taille de l'espace mémoire qui sera nécessaire pour la représenter, à savoir: la taille d'un entier nommé nbreVies + la taille d'une donnée de type Position (qui correspond à la taille de deux entiers).  Soit... la taille nécessaire à la représentation de trois entiers au total.

                                    De la même manière, je peux désormais définir une structure B (dans un fichier B.hpp) qui contient -- entre autres -- une donnée de type A sous la forme de

                                    #ifndef B_HPP
                                    #define B_HPP
                                    #include <A.hpp>
                                    #include <string>
                                    struct B{
                                        std::string nom;
                                        A leA;
                                    };
                                    #endif // B_HPP

                                    Encore une foi, il n'y a pas de problème jusqu'ici, car c'est toujours le même principe qui est mis en oeuvre: nous donnons une définition de la classe B qui va obliger le compilateur à en calculer la taille qui sera égale à celle d'une std::string + celle d'une donnée de type A.

                                    Je passe sur les calculs, mais ca marche, je peux te l'assurer ;)

                                    Mettons maintenant que je veuille, à partir de la donnée leA que l'on trouve dans B, savoir à quel B leA appartient. Je ne peux pas me contenter de modifier le fichier A.hpp pour lui donner la forme de

                                    A.hpp

                                    ifndef A_HPP
                                    #define A_HPP
                                    #include <Position.hpp>
                                    struct A{
                                        int nbreVies{4};
                                        Position position;
                                        B proprietaire;
                                    }
                                    #endif A_HPP

                                    Il faut bien comprendre que c'est toute une donnée de type B qui devrait être stockée dans A, et voilà Ouroboros qui apparait, car, pour calculer la taille de A, il faudrait calculer la taille de B.  Sauf que pour calculer la taille de B, il faut connaitre la taille de A.

                                    Humm... Ca va clairement pas le faire...

                                    Ce n'est pas grave: l'absence de solution indique simplement qu'il n'y a pas de problème. Il y a donc forcément une solution pour résoudre "notre problème" :D Et la solution est d'utiliser ... un pointeur sur la structure B dans notre strucutre A.

                                    En effet, un pointeur n'est jamais qu'une valeur numérique entière (généralement non signée) dont la taille est parfaitement connue du compilateur (comme pour tous les types permettant de représenter des valeurs entières) et suffisante pour permettre de représenter n'importe quelle adresse mémoire accessible sur le système utilisé.

                                    Peu importe qu'il s'agisse d'un pointeur sur A, sur B, sur Truc ou sur MachinBrol, la taille d'un pointeur sera toujours la même, du moins, tant que l'on reste sur le même système.

                                    Si l'on modifie donc encore une fois notre fichier A.hpp pour lui donner la forme de

                                    ifndef A_HPP
                                    #define A_HPP
                                    #include <Position.hpp>
                                    struct A{
                                        int nbreVies{4};
                                        Position position;
                                        B * proprietaire;
                                    }
                                    #endif A_HPP

                                    nous "brisons le cercle", car, le compilateur arrivera à calculer la taille de A sans avoir à calculer la taille de B (qui contient un A, rappelons le), vu qu'elle se "limite" à. un entier, une donnée de type Position et un pointeur. Adieu, Ouroboros :D.

                                    Sauf que, il nous reste un autre problème à résoudre. 

                                    Tu te souviens, je t'ai expliqué dans ma première intervention la manière dont le compilateur lisait le code (n'hésite pas à la relire si c'est "un peu flou" ;) ).

                                    En effet, quand le compilateur va lire le code de A.hpp, il va tomber sur le terme ... B, qu'il ne connait absolument pas à ce moment là.  Et ca va gueuler, parce qu'il ne sera pas content.

                                    Nous pourrions nous dire qu'il suffirait de rajouter une directive préprocesseur (hint: toute ligne qui commence par un # est traitée par un outil particulier nommé préprocesseur AVANT que le compilateur ne se mette au travail) #include <B.hpp>, et que cela devrait résoudre le problème.

                                    Hé ben non.  Je te passe les détails, mais si A.hpp inclut B.hpp qui inclut lui-même A.hpp, c'est ... Ouroboros qui revient à grand pas...

                                    En outre, je ne peux pas mettre toute la définition de B dans A.hpp, vu que le compilateur devrait alors connaitre la taille de A... qu'il ne saurait même pas qu'il existe et qu'il n'a, de plus, pas encore calculée.

                                    Enfin, il faut reconnaitre qu'il n'y aurait pas beaucoup de sens à s'être "cassé la tête" à créer un fichier B.hpp si c'est -- au final -- pour devoir en recopier intégralement le contenu dans A.hpp.

                                    Par chance, si une définition est -- effectivement -- une déclaration qui "en demande plus" au compilateur, la déclaration, elle ne fait que dire au compilateur que "quelque chose existe".

                                    Et c'est génial, car notre principal problème à l'heure actuelle est de... faire savoir dans A.hpp qu'il existe effectivement une structure B.

                                    En effet, à partir du moment où l'on a pu dire au compilateur "ah, au fait, il existe une structure B", le compilateur s'en foutera parfaitement de ne pas savoir quelle est la taille de B, vu que, pour représenter la donnée nommée proprietaire dans A, il doit utiliser la taille d'un pointeur.

                                    "Tout ce qu'il nous reste" donc à faire, c'est de fournir une déclaration -- que l'on qualifiera pour la cause de "anticipée" -- de la structure B dans notre fichier A.hpp, qui prendra donc au final la forme de

                                    ifndef A_HPP
                                    #define A_HPP
                                    #include <Position.hpp>
                                    /* voici une déclaration (anticipée) de la structure B
                                     * qui redonnera le sourir au compilateur
                                     */
                                    struct B;
                                    /* et voici notre strucutre A
                                     */
                                    struct A{
                                        int nbreVies{4};
                                        Position position;
                                        B * proprietaire;
                                    }
                                    #endif A_HPP

                                    Yixraie a écrit:

                                    Par contre, quand je défini ma variable dans le header, pourquoi je ne pourrais pas la modifier dans le .cpp ? Pas la redéfinir hein, vraiment la modifier comme on le fait avec n'importe quelle variable avec l'utilisation de <variable> = <valeur>.

                                    Le problème n'est pas de ne pas pouvoir la modifier dans le .cpp, le problème est que ton header va être inclut dans différent fichiers .cpp.

                                    Et comme le compilateur "oublie" ce qu'il a fait une fois qu'il a fini de traiter un fichier .cpp, cela implique que ta variable sera définie dans différents fichiers résultant du traitement du compilateur. Et ca, c'est quelque chose que l'ODR ne nous autorise pas.

                                    Tu peux donc modifier une variable dans n'importe quel .cpp au sein duquel la variable est connue (donc déclarée), mais par contre, tu dois impérativement veiller à ce que cette variable ne soit définie que dans un seul fichier .cpp, autrement, tu va te faire engueuler.

                                    Et c'est pour cela que tu ne peux pas définirune variable dans un header.

                                    Yixraie a écrit:

                                    Pourtant, après avoir modifié ce qui n'allait pas, j'arrive également à modifier cette variable dans le main.cpp... Comment c'est possible, si le compilateur oublie ce qu'il a fait dans les fichiers précédents ?

                                    Ca, c'est "toute la beauté" du système de compilation du C++.

                                    Ce qu'il faut comprendre, c'est que ce que l'on appelle (souvent) "la compilation" est en fait un abus de langage pour désigner un processus "en trois temps" bien plus complexe, à savoir:

                                    • Pour chaque fichier .cpp, on fait d'abord travailler le préprocesseur.  Je te passe les détails qui n'ont pas vraiment d'intérêt dans cette discussion ;)
                                    • vient ensuite la compilation proprement dite: le compilateur utilise le résultat du taf du préprocesseur pour traduire ton code en une série d'instructions compréhensibles par le processur.  Il écrit alors son résultat dans ce que l'on appelle "un fichier objet"
                                    • Une fois que tous tes fichiers .cpp ont été compilés -- comprend: une fois qu'un fichier objet a été généré pour chacun des fichiers .cpp de ton projet -- l'éditeur de liens "prend le relais" et "regroupe" tous ces fichiers en un fichier unique qui sera ... le programme complet.

                                    Une fois que tu as compris cela, les choses sont claires: ce n'est pas parce que le compilateur "oublie" ce qu'il a pu faire après avoir traité un fichier que son travail est perdu, vu qu'il a pris la précaution d'en sauvegarder le résultat dans un fichier objet ;)

                                    Le seul truc, c'est qu'il n'a aucn moyen de se dire que "tiens, on me demande de définir telle variable, mais bon, il me semble que je l'ai déjà fait dans un fichier précédent".

                                    Et comme c'est un "brave petit soldat", qui saute quand on lui dit de sauter sans demander ni à quelle hauteur ni à quelle distance, si il croise une instruction qui lui dit "tu dois définir telle variable" dans un fichier .cpp (ou, plus précisément, dans le résultat du traitement du préprocesseur), hé bien, il définira la variable  qu'on lui a demandée.  Et ce sera forcément sauvegardé dans le fichier objet qu'il va générer.

                                    Bien sur, tout cela est simplifié au maximum, et il existe quelques subtilités sympas. Mais comme elles n'apportent pas grand chose à la compréhension, je te laisserai le plaisir de les découvrir plus tard (si elles t'intéressent).

                                    • 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
                                      25 septembre 2022 à 14:48:34

                                      Merci ! Tout ceci était parfaitement claire (même si je me rend compte que j'avait mal formulé mon message car en fait j'avait déjà compris la différence entre déclaration et définition grâce à tes messages précédents (de même que les étapes de compilations, je connaissais déjà :D)). Tu m'a appris énormément de choses (y compris sur la définition des tableaux statiques que je ne connaissais pas :-°) et je t'en remercie !

                                      il existe quelques subtilités sympas. Mais comme elles n'apportent pas grand chose à la compréhension, je te laisserai le plaisir de les découvrir plus tard (si elles t'intéressent).

                                      Bien sûr que ça m'intéresse ! J'aimerais bien apprendre et comprendre ce que je ne connais pas encore, tu aurais des liens à m'envoyer pour ces "subtilités" ?

                                      Encore un énorme merci pour toutes ces explications !

                                      • Partager sur Facebook
                                      • Partager sur Twitter
                                        25 septembre 2022 à 16:19:29

                                        Heu... non, je n'ai malheureusement pas de lien à te donner.

                                        D'autant plus que les subtilités vont souvent dépendre du compilateur que tu emploies, par exemple:

                                        Si tu n'as qu'un seul fichier à compiler, et que tu utilises une commande proche de

                                        CC nom_du_fichier.cpp 
                                        # voire
                                        CC nom_du_fichier -o nom_executable

                                        (où CC correspond au compilateur que tu utilise)

                                        Il se peut (en fonction du compilateur utilisé) que le compilateur ne génère pas le fichier objet et transmette directement le résultat de son traitement à l'éditeur de liens pour qu'il génère l'exécutable ... ou non...

                                        Dans un autre ordre d'idée, il se peut qu'une variable globale (ou plutot une variable statique) ne soit pas initialisée correctement en fonction de l'ordre dans lequel tu auras donné les différents fichiers objets à l'éditeur de liens ... ou non, encore une fois...

                                        Bref, un tas de "trucs", comme cela, qui peuvent arriver ... ou non... en fonction de pleins  de circonstances différentes, dont tout le monde serait totalement incapable de dresser une liste exhaustive, et qui t'amèneront à te poser de sérieuses questions... jusqu'à ce qu'une recherche spécifique au phénomène t'apporte la réponse ;)

                                        • 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
                                          25 septembre 2022 à 19:29:04

                                          Ok ! Merci encore pour tout ce que tu m'a appris !
                                          • Partager sur Facebook
                                          • Partager sur Twitter

                                          Namespaces

                                          × 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