Comment les membres de la classe C ++ sont-ils initialisés si je ne le fais pas explicitement?


158

Supposons que j'ai une classe avec memebers privé ptr, name, pname, rname, crnameet age. Que se passe-t-il si je ne les initialise pas moi-même? Voici un exemple:

class Example {
    private:
        int *ptr;
        string name;
        string *pname;
        string &rname;
        const string &crname;
        int age;

    public:
        Example() {}
};

Et puis je fais:

int main() {
    Example ex;
}

Comment les membres sont-ils initialisés en ex? Que se passe-t-il avec les pointeurs? Faites stringet intobtenez 0-initialisé avec les constructeurs par défaut string()et int()? Et le membre de référence? Et qu'en est-il des références const?

Que dois-je savoir d'autre?

Quelqu'un connaît-il un tutoriel qui couvre ces cas? Peut-être dans certains livres? J'ai accès dans la bibliothèque de l'université à de nombreux livres C ++.

J'aimerais l'apprendre pour pouvoir écrire de meilleurs programmes (sans bogues). Tout commentaire aiderait!


3
Pour des recommandations de livres, voir stackoverflow.com/questions/388242/…
Mike Seymour

Mike, euh, je veux dire un chapitre d'un livre qui l'explique. Pas un livre entier! :)
bodacydo

Ce serait probablement une bonne idée de lire un livre entier sur une langue dans laquelle vous avez l'intention de programmer, cependant. Et si vous en avez déjà lu un et qu'il ne l'explique pas, alors ce n'est pas un très bon livre.
Tyler McHenry

2
Scott Meyers (un ancien gourou du conseil en C ++) déclare dans Effective C ++ , "les règles sont compliquées - trop compliquées pour être mémorisées, à mon avis .... assurez-vous que tous les constructeurs initialisent tout dans l'objet." Ainsi , à son avis, la meilleure façon de (tentative de) écriture code « bug » est de ne pas essayer de mémoriser les règles (et en fait , il ne pas les jeter dans le livre), mais à tout initialize explicitement. Notez, cependant, que même si vous adoptez cette approche dans votre propre code, vous pourriez travailler sur des projets écrits par des personnes qui ne le font pas, donc les règles peuvent toujours être utiles.
Kyle Strand

2
@TylerMcHenry Quels livres sur C ++ considérez-vous comme "bons"? J'ai lu plusieurs livres sur C ++, mais aucun d'entre eux ne l'explique complètement. Comme indiqué dans mon commentaire précédent, Scott Meyers refuse explicitement de fournir les règles complètes dans Effective C ++ . J'ai également lu le C ++ moderne efficace de Meyers , le C ++ Common Knowledge de Dewhurst et A Tour of C ++ de Stroustrup . À ma mémoire, aucun d'entre eux n'a expliqué les règles complètes. Évidemment, j'aurais pu lire la norme, mais je ne considérerais guère cela comme un "bon livre"! : D Et je pense que Stroustrup l'explique probablement dans le langage de programmation C ++ .
Kyle Strand

Réponses:


207

Au lieu d'une initialisation explicite, l'initialisation des membres dans les classes fonctionne de la même manière que l'initialisation des variables locales dans les fonctions.

Pour les objets , leur constructeur par défaut est appelé. Par exemple, pour std::string, le constructeur par défaut le définit sur une chaîne vide. Si la classe de l'objet n'a pas de constructeur par défaut, ce sera une erreur de compilation si vous ne l'initialisez pas explicitement.

Pour les types primitifs (pointeurs, entiers, etc.), ils ne sont pas initialisés - ils contiennent tout ce qui se trouvait auparavant à cet emplacement mémoire.

Pour les références (par exemple std::string&), il est illégal de ne pas les initialiser, et votre compilateur se plaindra et refusera de compiler un tel code. Les références doivent toujours être initialisées.

Donc, dans votre cas spécifique, s'ils ne sont pas explicitement initialisés:

    int *ptr;  // Contains junk
    string name;  // Empty string
    string *pname;  // Contains junk
    string &rname;  // Compile error
    const string &crname;  // Compile error
    int age;  // Contains junk

4
+1. Il convient de noter que, selon la définition standard stricte, les instances de types primitifs ainsi qu'une variété d'autres choses (toute région de stockage) sont toutes considérées comme des objets .
stinky472

7
"Si la classe de l'objet n'a pas de constructeur par défaut, ce sera une erreur de compilation si vous ne l'initialisez pas explicitement" c'est faux ! Si une classe n'a pas de constructeur par défaut, elle reçoit un constructeur par défaut par défaut qui est vide.
Wizard

19
@wiz Je pense qu'il voulait dire littéralement «si l'objet n'a pas de constructeur par défaut» comme dans aucun constructeur non plus généré, ce qui serait le cas si la classe définit explicitement des constructeurs autres que celui par défaut (aucun ctor par défaut ne sera généré). Si nous devenons trop pédanatiques, nous confondrons probablement plus que l'aide et Tyler fait un bon point à ce sujet dans sa réponse à moi avant.
stinky472

8
@ wiz-loz Je dirais que foocela a un constructeur, c'est juste implicite. Mais c'est vraiment un argument de sémantique.
Tyler McHenry

4
J'interprète "constructeur par défaut" comme un constructeur qui peut être appelé sans arguments. Ce serait soit celui que vous définissez vous-même, soit généré implicitement par le compilateur. Donc le manque de celui-ci, signifie ni défini par vous-même ni généré. Ou c'est comme ça que je le vois.
5

28

Tout d'abord, laissez-moi vous expliquer ce qu'est une liste d'initialisation de mémoire . Un mem-initializer-list est une liste de mem-initializer séparés par des virgules , où chaque mem-initializer est un nom de membre suivi de (, suivi d'une liste d'expressions , suivi d'un ). La liste d'expressions est la façon dont le membre est construit. Par exemple, dans

static const char s_str[] = "bodacydo";
class Example
{
private:
    int *ptr;
    string name;
    string *pname;
    string &rname;
    const string &crname;
    int age;

public:
    Example()
        : name(s_str, s_str + 8), rname(name), crname(name), age(-4)
    {
    }
};

la liste-initialiseur-mem du constructeur sans arguments fourni par l'utilisateur est name(s_str, s_str + 8), rname(name), crname(name), age(-4). Cette mem-initializer-list signifie que le namemembre est initialisé par le std::stringconstructeur qui prend deux itérateurs d'entrée , le rnamemembre est initialisé avec une référence à name, le crnamemembre est initialisé avec une référence const à name, et le agemembre est initialisé avec la valeur -4.

Chaque constructeur a sa propre liste d'initialisation de mémoire et les membres ne peuvent être initialisés que dans un ordre prescrit (essentiellement l'ordre dans lequel les membres sont déclarés dans la classe). Ainsi, les membres de Examplene peuvent être initialisés dans l'ordre: ptr, name, pname, rname, crnameet age.

Lorsque vous ne spécifiez pas de mem-initializer d'un membre, le standard C ++ dit:

Si l'entité est un membre de données non statique ... de type classe ..., l'entité est initialisée par défaut (8.5). ... Sinon, l'entité n'est pas initialisée.

Ici, étant donné qu'il names'agit d'un membre de données non statique de type classe, il est initialisé par défaut si aucun initialiseur pour namen'a été spécifié dans la liste mem-initializer . Tous les autres membres de Examplen'ont pas de type de classe, ils ne sont donc pas initialisés.

Lorsque le standard dit qu'ils ne sont pas initialisés, cela signifie qu'ils peuvent avoir n'importe quelle valeur. Ainsi, comme le code ci-dessus ne s'est pas initialisé pname, cela pourrait être n'importe quoi.

Notez que vous devez toujours suivre d'autres règles, telles que la règle selon laquelle les références doivent toujours être initialisées. Le fait de ne pas initialiser les références est une erreur du compilateur.


C'est la meilleure façon d'initialiser les membres lorsque vous souhaitez séparer strictement la déclaration (in .h) et la définition (in .cpp) sans afficher trop d'éléments internes.
Matthieu

12

Vous pouvez également initialiser les membres de données au moment où vous les déclarez:

class another_example{
public:
    another_example();
    ~another_example();
private:
    int m_iInteger=10;
    double m_dDouble=10.765;
};

J'utilise ce formulaire à peu près exclusivement, même si j'ai lu que certaines personnes le considèrent comme une `` mauvaise forme '', peut-être parce qu'il n'a été introduit que récemment - je pense en C ++ 11. Pour moi, c'est plus logique.

Une autre facette utile des nouvelles règles est de savoir comment initialiser les membres de données qui sont eux-mêmes des classes. Par exemple, supposons que ce CDynamicStringsoit une classe qui encapsule la gestion des chaînes. Il a un constructeur qui vous permet de spécifier sa valeur initiale CDynamicString(wchat_t* pstrInitialString). Vous pourriez très bien utiliser cette classe comme membre de données dans une autre classe - disons une classe qui encapsule une valeur de registre Windows qui, dans ce cas, stocke une adresse postale. Pour `` coder en dur '' le nom de la clé de registre sur laquelle cela écrit, utilisez des accolades:

class Registry_Entry{
public:
    Registry_Entry();
    ~Registry_Entry();
    Commit();//Writes data to registry.
    Retrieve();//Reads data from registry;
private:
    CDynamicString m_cKeyName{L"Postal Address"};
    CDynamicString m_cAddress;
};

Notez que la deuxième classe de chaîne qui contient l'adresse postale réelle n'a pas d'initialiseur, donc son constructeur par défaut sera appelé lors de la création - peut-être en le définissant automatiquement sur une chaîne vide.


9

Si votre exemple de classe est instancié sur la pile, le contenu des membres scalaires non initialisés est aléatoire et non défini.

Pour une instance globale, les membres scalaires non initialisés seront mis à zéro.

Pour les membres qui sont eux-mêmes des instances de classes, leurs constructeurs par défaut seront appelés, donc votre objet chaîne sera initialisé.

  • int *ptr; // pointeur non initialisé (ou mis à zéro si global)
  • string name; // constructeur appelé, initialisé avec une chaîne vide
  • string *pname; // pointeur non initialisé (ou mis à zéro si global)
  • string &rname; // erreur de compilation si vous ne parvenez pas à l'initialiser
  • const string &crname; // erreur de compilation si vous ne parvenez pas à l'initialiser
  • int age; // valeur scalaire, non initialisée et aléatoire (ou remise à zéro si globale)

J'ai expérimenté et il semble que ce string namesoit vide après l'initialisation de la classe sur la pile. Etes-vous absolument sûr de votre réponse?
bodacydo

1
string aura un constructeur qui a fourni une chaîne vide par défaut - je vais clarifier ma réponse
Paul Dixon

@bodacydo: Paul a raison, mais si vous vous souciez de ce comportement, cela ne fait jamais de mal d'être explicite. Jetez-le dans la liste des initialiseurs.
Stephen

Merci d'avoir clarifié et expliqué!
bodacydo

2
Ce n'est pas aléatoire! Aléatoire est un mot trop gros pour ça! Si les membres scalaires étaient aléatoires, nous n'aurions pas besoin d'autres générateurs de nombres aléatoires. Imaginez un programme qui analyse les données "restantes" - comme des fichiers non supprimés en mémoire - les données sont loin d'être aléatoires. Ce n'est même pas indéfini! C'est généralement difficile à définir, car nous ne savons généralement pas ce que fait notre machine. Si ces "données aléatoires" que vous venez de supprimer sont la seule image de votre père, votre mère peut même trouver cela offensant si vous dites que c'est aléatoire ...
slyy2048

5

Les membres non statiques non initialisés contiendront des données aléatoires. En fait, ils auront juste la valeur de l'emplacement mémoire auquel ils sont assignés.

Bien sûr, pour les paramètres d'objet (comme string), le constructeur de l'objet pourrait effectuer une initialisation par défaut.

Dans votre exemple:

int *ptr; // will point to a random memory location
string name; // empty string (due to string's default costructor)
string *pname; // will point to a random memory location
string &rname; // it would't compile
const string &crname; // it would't compile
int age; // random value

2

Les membres avec un constructeur verront leur constructeur par défaut appelé pour l'initialisation.

Vous ne pouvez pas dépendre du contenu des autres types.


0

S'il est sur la pile, le contenu des membres non initialisés qui n'ont pas leur propre constructeur sera aléatoire et indéfini. Même s'il est mondial, ce serait une mauvaise idée de compter sur leur remise à zéro. Qu'il soit sur la pile ou non, si un membre a son propre constructeur, il sera appelé pour l'initialiser.

Donc, si vous avez la chaîne * pname, le pointeur contiendra des indésirables aléatoires. mais pour le nom de la chaîne, le constructeur par défaut de string sera appelé, vous donnant une chaîne vide. Pour vos variables de type de référence, je ne suis pas sûr, mais ce sera probablement une référence à un morceau de mémoire aléatoire.


0

Cela dépend de la façon dont la classe est construite

Répondre à cette question consiste à comprendre une énorme déclaration de cas de commutation dans le standard du langage C ++, et qui est difficile à comprendre pour de simples mortels.

À titre d'exemple simple de la difficulté des choses:

main.cpp

#include <cassert>

int main() {
    struct C { int i; };

    // This syntax is called "default initialization"
    C a;
    // i undefined

    // This syntax is called "value initialization"
    C b{};
    assert(b.i == 0);
}

Dans l'initialisation par défaut, vous commenceriez à partir de: https://en.cppreference.com/w/cpp/language/default_initialization nous allons à la partie "Les effets de l'initialisation par défaut sont" et commençons l'instruction de cas:

  • "si T est un non-POD ": non (la définition de POD est en elle-même une énorme instruction switch)
  • "si T est un type de tableau": non
  • "sinon, rien n'est fait": il reste donc une valeur indéfinie

Ensuite, si quelqu'un décide de valoriser initialize, nous allons sur https://en.cppreference.com/w/cpp/language/value_initialization "Les effets de l'initialisation de la valeur sont" et commençons l'instruction case:

  • "si T est un type de classe sans constructeur par défaut ou avec un constructeur par défaut fourni par l'utilisateur ou supprimé": ce n'est pas le cas. Vous allez maintenant passer 20 minutes à googler ces termes:
    • nous avons un constructeur par défaut défini implicitement (en particulier parce qu'aucun autre constructeur n'a été défini)
    • il n'est pas fourni par l'utilisateur (défini implicitement)
    • il n'est pas supprimé ( = delete)
  • "si T est un type de classe avec un constructeur par défaut qui n'est ni fourni par l'utilisateur ni supprimé": oui
    • "l'objet est initialisé à zéro, puis il est initialisé par défaut s'il a un constructeur par défaut non trivial": pas de constructeur non trivial, juste une initialisation à zéro. La définition de "zero-initialize" au moins est simple et fait ce que vous attendez: https://en.cppreference.com/w/cpp/language/zero_initialization

C'est pourquoi je vous recommande fortement de ne jamais vous fier à une initialisation zéro "implicite". À moins qu'il n'y ait de bonnes raisons de performances, initialisez explicitement tout, soit sur le constructeur si vous en avez défini un, soit à l'aide de l'initialisation agrégée. Sinon, vous rendez les choses très risquées pour les futurs développeurs.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.