web-dev-qa-db-fra.com

Membres de la classe qui sont des objets - Pointeurs ou pas? C++

Si je crée une classe MyClass et qu'un membre privé dit MyOtherClass, est-il préférable de faire de MyOtherClass un pointeur ou non? Qu'est-ce que cela signifie aussi de l'avoir comme pointeur non plus en ce qui concerne l'endroit où il est stocké en mémoire? L'objet sera-t-il créé lors de la création de la classe?

J'ai remarqué que les exemples de QT déclarent généralement les membres de la classe comme des pointeurs lorsqu'ils sont des classes.

Cordialement

Marque

35
Mark

Si je crée une classe MyClass et qu'un membre privé dit MyOtherClass, est-il préférable de faire de MyOtherClass un pointeur ou non?

vous devriez généralement le déclarer comme une valeur dans votre classe. ce sera local, il y aura moins de risque d'erreurs, moins d'allocations - en fin de compte, moins de choses qui pourraient mal tourner, et le compilateur peut toujours savoir qu'il est là à un décalage spécifié donc ... cela aide l'optimisation et la réduction binaire à un quelques niveaux. il y aura quelques cas où vous saurez que vous devrez gérer un pointeur (c'est-à-dire polymorphe, partagé, nécessite une réaffectation), il est généralement préférable d'utiliser un pointeur uniquement lorsque cela est nécessaire - en particulier lorsqu'il est privé/encapsulé.

Qu'est-ce que cela signifie aussi de l'avoir comme pointeur non plus en ce qui concerne l'endroit où il est stocké en mémoire?

son adresse sera proche de (ou égale à) this - gcc (par exemple) a quelques options avancées pour vider les données de classe (tailles, vtables, décalages)

L'objet sera-t-il créé lors de la création de la classe?

oui - la taille de MyClass augmentera en fonction de sizeof (MyOtherClass), ou davantage si le compilateur le réaligne (par exemple, sur son alignement naturel)

29
justin

Où votre membre est-il stocké en mémoire?

Jetez un oeil à cet exemple:

struct Foo { int m; };
struct A {
  Foo foo;
};
struct B {
  Foo *foo;
  B() : foo(new Foo()) { } // ctor: allocate Foo on heap
  ~B() { delete foo; } // dtor: Don't forget this!
};

void bar() {
  A a_stack; // a_stack is on stack
             // a_stack.foo is on stack too
  A* a_heap = new A(); // a_heap is on stack (it's a pointer)
                       // *a_heap (the pointee) is on heap
                       // a_heap->foo is on heap
  B b_stack; // b_stack is on stack
             // b_stack.foo is on stack
             // *b_stack.foo is on heap
  B* b_heap = new B(); // b_heap is on stack
                       // *b_heap is on heap
                       // b_heap->foo is on heap
                       // *(b_heap->foo is on heap
  delete a_heap;
  delete b_heap;
  // B::~B() will delete b_heap->foo!
} 

Nous définissons deux classes A et B. A stocke un membre public foo de type Foo. B a un membre foo de type pointer to Foo

Quelle est la situation pour A:

  • Si vous créez une variable a_stack de type A sur stack, l'objet (évidemment) et ses membres se trouvent également sur stack.
  • Si vous créez un pointeur sur A comme a_heap dans l'exemple ci-dessus, seule la variable de pointeur se trouve sur le stack; tout le reste (l'objet et ses membres) est sur le heap.

À quoi ressemble la situation dans le cas de B:

  • vous créez B sur stack: alors l'objet et son membre foo se trouvent sur stack, mais l'objet sur lequel foo pointe (la pointee) se trouve sur heap. En bref: b_stack.foo (le pointeur) est sur la pile, mais *b_stack.foo le (pointee) est sur le tas.
  • vous créez un pointeur sur B nommé b_heap: b_heap (le pointeur) est sur la pile, *b_heap (la pointee) est sur le heap, ainsi que le membre b_heap->foo et *b_heap->foo.

L'objet sera-t-il créé automatiquement?

  • Dans le cas de A: Oui, foo sera automatiquement créé en appelant le constructeur implicite par défaut de Foo. Cela créera une integer mais pas l'initialisera (il aura un nombre aléatoire)!
  • Dans le cas de B: Si vous omettez nos ctor et dtor, alors foo (le pointeur) sera également créé et initialisé avec un nombre aléatoire, ce qui signifie qu'il pointe vers un emplacement random sur le tas. Mais notez que le pointeur existe! Notez également que le constructeur implicite par défaut n'allouera rien pour foo pour vous, vous devez le faire explicitement. C'est pourquoi vous avez généralement besoin d'un constructeur explicit et d'un destructor associé pour allouer et supprimer la pointe de votre pointeur de membre. N'oubliez pas la sémantique copy: qu'advient-il de la pointee si vous copiez l'objet (via la construction de copie ou l'affectation)?

Quel est le but de tout cela?

Il existe plusieurs cas d'utilisation d'un pointeur sur un membre:

  • Pour pointer sur un objet que vous ne possédez pas. Supposons que votre classe ait besoin d'accéder à une énorme structure de données très coûteuse à copier. Ensuite, vous pouvez simplement enregistrer un pointeur sur cette structure de données. Sachez que, dans ce cas, creation et delete de la structure de données sortent du cadre de votre classe. Quelqu'un d'autre doit en prendre soin.
  • Augmenter le temps de compilation, car il n'est pas nécessaire de définir la pointee dans votre fichier d'en-tête. 
  • Un peu plus avancé; Lorsque votre classe a un pointeur sur une autre classe qui stocke tous les membres privés, le "idiome Pimpl": http://c2.com/cgi/wiki?PimplIdiom , regardez aussi Sutter, H. (2000). ): C++ exceptionnel, p. 99-119
  • Et quelques autres, regardez les autres réponses

Conseil

Faites très attention si vos membres sont des pointeurs et que vous les possédez. Vous devez écrire les constructeurs et les destructeurs appropriés et penser aux constructeurs de copie et aux opérateurs d'affectation. Qu'advient-il de la pointee si vous copiez l'objet? Habituellement, vous devrez également construire la pointee!

21
WolfgangP

En C++, les pointeurs sont des objets à part entière. Ils ne sont pas vraiment liés à ce qu'ils désignent, et il n'y a pas d'interaction particulière entre un pointeur et sa pointee (est-ce un mot?)

Si vous créez un pointeur, vous créez un pointeur et rien d’autre . Vous ne créez pas l'objet sur lequel il peut ou non pointer. Et lorsqu'un pointeur sort du cadre, l'objet pointé n'est pas affecté. Un pointeur n'affecte en aucun cas la durée de vie de tout ce qu'il pointe.

Donc, en général, vous devriez pas utiliser les pointeurs par défaut. Si votre classe contient un autre objet, cet autre objet ne doit pas être un pointeur.

Cependant, si votre classe est au courant de d'un autre objet, un pointeur peut être un bon moyen de le représenter (car plusieurs instances de votre classe peuvent alors pointer sur la même instance, sans en prendre possession, et sans contrôler sa durée de vie)

17
jalf

La sagesse courante en C++ consiste à éviter autant que possible d'utiliser des pointeurs (nus). Des pointeurs particulièrement simples pointant vers la mémoire allouée dynamiquement.

La raison en est que les pointeurs rendent plus difficile l’écriture de classes robustes, en particulier lorsque vous devez également envisager la possibilité de lever des exceptions.

Quelques avantages du membre de pointeur:

  • L'objet enfant (MyOtherClass) peut avoir une durée de vie différente de celle de son parent (MyClass).
  • L'objet peut éventuellement être partagé entre plusieurs objets MyClass (ou autres).
  • Lors de la compilation du fichier d'en-tête pour MyClass, le compilateur ne doit pas nécessairement connaître la définition de MyOtherClass. Vous n'avez pas à inclure son en-tête, ce qui diminue les temps de compilation.
  • Réduit la taille de MyClass. Cela peut être important pour les performances si votre code copie beaucoup d'objets MyClass. Vous pouvez simplement copier le pointeur MyOtherClass et mettre en place une sorte de système de comptage de références.

Avantages d'avoir le membre comme objet:

  • Il n'est pas nécessaire d'écrire explicitement du code pour créer et détruire l'objet. C'est plus facile et moins sujet aux erreurs.
  • Rend la gestion de la mémoire plus efficace car un seul bloc de mémoire doit être alloué au lieu de deux.
  • L'implémentation d'opérateurs d'assignation, de constructeur de copie/déplacement, etc. est beaucoup plus simple. 
  • Plus intuitif
3
Timo

Cette question pourrait être délibérée à l'infini, mais l'essentiel est:

Si MyOtherClass n'est pas un pointeur:

  • La création et la destruction de MyOtherClass est automatique, ce qui peut réduire les bugs. 
  • La mémoire utilisée par MyOtherClass est locale à MyClassInstance, ce qui pourrait améliorer les performances.

Si MyOtherClass est un pointeur:

  • La création et la destruction de MyOtherClass relèvent de votre responsabilité.
  • MyOtherClass peut être NULL, ce qui pourrait avoir une signification dans votre contexte et économiser de la mémoire.
  • Deux instances de MyClass pourraient partager le même MyOtherClass
3
Drew Dormann

Je suis la règle suivante: si l’objet membre vit et meurt avec l’objet encapsulant, n’utilisez pas de pointeur. Vous aurez besoin d'un pointeur si l'objet membre doit survivre à l'objet encapsulant pour une raison quelconque. Cela dépend de la tâche à accomplir.

Habituellement, vous utilisez un pointeur si l'objet membre vous est donné et n'est pas créé par vous. Ensuite, vous n'avez généralement pas à le détruire non plus.

3
chvor

Si vous créez l'objet MyOtherClass en tant que membre de votre MyClass:

size of MyClass = size of MyClass + size of MyOtherClass 

Si vous créez l'objet MyOtherClass en tant que membre du pointeur de votre MyClass:

size of MyClass = size of MyClass + size of any pointer on your system 

Vous voudrez peut-être conserver MyOtherClass en tant que membre de pointeur car cela vous donne la possibilité de le diriger vers toute autre classe dérivée de celle-ci. Fondamentalement, vous aide à implémenter le polymorphisme dynamice.

1
Alok Save

Ça dépend... :-)

Si vous utilisez des pointeurs pour indiquer un class A, vous devez créer un objet de type A, par exemple. dans le constructeur de votre classe 

 m_pA = new A();

De plus, n'oubliez pas de détruire l'objet dans le destructeur ou vous avez une fuite de mémoire:

delete m_pA; 
m_pA = NULL;

Au lieu de cela, avoir un objet de type A agrégé dans votre classe est plus facile, vous ne pouvez pas oublier de le détruire, car cela se fait automatiquement à la fin de la vie de votre objet. 

D'autre part, avoir un pointeur présente les avantages suivants: 

  • Si votre objet est alloué sur la pile Et que le type A utilise beaucoup de mémoire , Il ne sera pas alloué à partir de la pile Mais à partir du tas.

  • Vous pouvez construire votre objet A plus tard (par exemple, dans une méthode Create) ou le détruire plus tôt (dans la méthode Close)

1
ur.

Un avantage du fait que la classe parent conserve la relation avec un objet membre en tant que pointeur (std :: auto_ptr) sur l'objet membre est que vous pouvez transférer la déclaration de l'objet plutôt que de devoir inclure le fichier d'en-tête de l'objet.

Cela permet de découpler les classes au moment de la construction, ce qui permet de modifier la classe d'en-tête de l'objet membre sans provoquer la recompilation de tous les clients de votre classe parent, même s'ils n'ont probablement pas accès aux fonctions de l'objet membre.

Lorsque vous utilisez un auto_ptr, vous devez uniquement vous occuper de la construction, ce que vous pouvez généralement faire dans la liste des initialiseurs. La destruction avec l'objet parent est garantie par auto_ptr.

1
andreas buykx

La chose simple à faire est de déclarer vos membres en tant qu'objets. De cette façon, vous n'avez pas à vous soucier de la construction, de la destruction et de l'affectation des copies. Tout cela est pris en charge automatiquement.

Cependant, il y a encore des cas où vous voulez des pointeurs. Après tout, les langages gérés (tels que C # ou Java) contiennent en réalité des objets membres par des pointeurs.

Le cas le plus évident est celui où l'objet à conserver est polymorphe. Comme vous l'avez fait remarquer, dans Qt, la plupart des objets appartiennent à une énorme hiérarchie de classes polymorphes et il est obligatoire de les tenir par des pointeurs, car vous ne savez pas à l'avance quelle taille aura l'objet membre.

Méfiez-vous des pièges courants dans ce cas, en particulier lorsque vous utilisez des classes génériques. La sécurité des exceptions est une préoccupation majeure:

struct Foo
{
    Foo() 
    {
        bar_ = new Bar();
        baz_ = new Baz(); // If this line throw, bar_ is never reclaimed
                          // See copy constructor for a workaround
    }

    Foo(Foo const& x)
    {
        bar_ = x.bar_.clone();
        try { baz_ = x.baz_.clone(); }
        catch (...) { delete bar_; throw; }
    }

    // Copy and swap idiom is perfect for this.
    // It yields exception safe operator= if the copy constructor
    // is exception safe.
    void swap(Foo& x) throw()
    { std::swap(bar_, x.bar_); std::swap(baz_, x.baz_); }

    Foo& operator=(Foo x) { x.swap(*this); return *this; }

private:
    Bar* bar_;
    Baz* baz_;
};

Comme vous le voyez, il est assez fastidieux d’avoir des constructeurs protégés contre les exceptions en présence de pointeurs. Vous devriez regarder RAII et les pointeurs intelligents (il existe de nombreuses ressources ici et ailleurs sur le Web).

0
Alexandre C.