Instanciation d'objets C ++

113

Je suis un programmeur C essayant de comprendre C ++. De nombreux didacticiels illustrent l'instanciation d'objet à l'aide d'un extrait de code tel que:

Dog* sparky = new Dog();

ce qui implique que plus tard, vous ferez:

delete sparky;

ce qui a du sens. Maintenant, dans le cas où l'allocation de mémoire dynamique n'est pas nécessaire, y a-t-il une raison d'utiliser ce qui précède au lieu de

Dog sparky;

et que le destructeur soit appelé une fois que Sparky est hors de portée?

Merci!

el_champo
la source

Réponses:

166

Au contraire, vous devriez toujours préférer les allocations de pile, dans la mesure où, en règle générale, vous ne devriez jamais avoir de nouveau / supprimer dans votre code utilisateur.

Comme vous le dites, lorsque la variable est déclarée sur la pile, son destructeur est automatiquement appelé lorsqu'elle sort du champ d'application, qui est votre principal outil pour suivre la durée de vie des ressources et éviter les fuites.

Donc, en général, chaque fois que vous avez besoin d'allouer une ressource, que ce soit de la mémoire (en appelant new), des descripteurs de fichiers, des sockets ou autre, enveloppez-la dans une classe où le constructeur acquiert la ressource et le destructeur la libère. Ensuite, vous pouvez créer un objet de ce type sur la pile et vous êtes assuré que votre ressource est libérée lorsqu'elle est hors de portée. De cette façon, vous n'avez pas à suivre vos paires nouvelles / supprimées partout pour vous assurer d'éviter les fuites de mémoire.

Le nom le plus courant de cet idiome est RAII

Examinez également les classes de pointeurs intelligents qui sont utilisées pour encapsuler les pointeurs résultants dans les rares cas où vous devez allouer quelque chose avec du nouveau en dehors d'un objet RAII dédié. Vous passez plutôt le pointeur à un pointeur intelligent, qui suit ensuite sa durée de vie, par exemple par le comptage des références, et appelle le destructeur lorsque la dernière référence sort de la portée. La bibliothèque standard a std::unique_ptrune gestion simple basée sur l'étendue et std::shared_ptrqui effectue le comptage des références pour implémenter la propriété partagée.

De nombreux didacticiels illustrent l'instanciation d'objets à l'aide d'un extrait de code tel que ...

Donc, ce que vous avez découvert, c'est que la plupart des tutoriels sont nulles. ;) La plupart des didacticiels vous enseignent de mauvaises pratiques C ++, y compris l'appel de new / delete pour créer des variables lorsque ce n'est pas nécessaire, et vous donnant une durée de vie difficile à suivre de vos allocations.

jalf
la source
Les pointeurs bruts sont utiles lorsque vous voulez une sémantique de type auto_ptr (transfert de propriété), mais conservent l'opération de swap non lancée et ne voulez pas la surcharge du comptage des références. Cas de bord peut-être, mais utile.
Greg Rogers
2
C'est une bonne réponse, mais la raison pour laquelle je ne prendrais jamais l'habitude de créer des objets sur la pile est parce que la taille de cet objet n'est jamais complètement évidente. Vous demandez simplement une exception de dépassement de capacité.
dviljoen
3
Greg: Certainement. Mais comme vous le dites, un cas de pointe. En général, il vaut mieux éviter les pointeurs. Mais ils sont dans la langue pour une raison, indéniablement. :) dviljoen: Si l'objet est gros, vous l'enveloppez dans un objet RAII, qui peut être alloué sur la pile, et contient un pointeur vers les données allouées au tas.
jalf
3
@dviljoen: Non, je ne le suis pas. Les compilateurs C ++ ne gonflent pas inutilement les objets. Le pire que vous verrez est généralement qu'il est arrondi au multiple de quatre octets le plus proche. Habituellement, une classe contenant un pointeur prendra autant d'espace que le pointeur lui-même, donc cela ne vous coûte rien en utilisation de la pile.
jalf
20

Bien qu'avoir des choses sur la pile puisse être un avantage en termes d'allocation et de libération automatique, cela présente certains inconvénients.

  1. Vous ne souhaiterez peut-être pas allouer d'énormes objets sur la pile.

  2. Envoi dynamique! Considérez ce code:

#include <iostream>

class A {
public:
  virtual void f();
  virtual ~A() {}
};

class B : public A {
public:
  virtual void f();
};

void A::f() {cout << "A";}
void B::f() {cout << "B";}

int main(void) {
  A *a = new B();
  a->f();
  delete a;
  return 0;
}

Cela imprimera "B". Voyons maintenant ce qui se passe lors de l'utilisation de Stack:

int main(void) {
  A a = B();
  a.f();
  return 0;
}

Cela affichera "A", ce qui pourrait ne pas être intuitif pour ceux qui sont familiers avec Java ou d'autres langages orientés objet. La raison en est que vous n'avez plus de pointeur vers une instance de B. Au lieu de cela, une instance de Best créée et copiée dans une avariable de typeA .

Certaines choses peuvent se produire de manière non intuitive, en particulier lorsque vous êtes nouveau dans C ++. En C, vous avez vos pointeurs et c'est tout. Vous savez comment les utiliser et ils font TOUJOURS la même chose. En C ++ ce n'est pas le cas. Imaginez simplement ce qui se passe, lorsque vous utilisez a dans cet exemple comme argument pour une méthode - les choses deviennent plus compliquées et cela fait une énorme différence s'il aest de type Aou A*ou même A&(appel par référence). De nombreuses combinaisons sont possibles et elles se comportent toutes différemment.

Univers
la source
2
-1: les personnes incapables de comprendre la sémantique des valeurs et le simple fait que le polymorphisme a besoin d'une référence / pointeur (pour éviter le découpage d'objets) ne constituent aucun problème avec le langage. La puissance de c ++ ne doit pas être considérée comme un inconvénient simplement parce que certains ne peuvent pas apprendre ses règles de base.
underscore_d
Merci pour l'information. J'ai eu un problème similaire avec la méthode prenant un objet au lieu d'un pointeur ou d'une référence, et quel gâchis c'était. L'objet a des pointeurs à l'intérieur et ils ont été supprimés trop tôt à cause de cette adaptation.
BoBoDev
@underscore_d Je suis d'accord; le dernier paragraphe de cette réponse devrait être supprimé. Ne prenez pas le fait que j'ai édité cette réponse pour signifier que je suis d'accord avec elle; Je ne voulais simplement pas que les erreurs y soient lues.
TamaMcGlinn
@TamaMcGlinn était d'accord. Merci pour la bonne modification. J'ai supprimé la partie opinion.
UniversE
13

Eh bien, la raison d'utiliser le pointeur serait exactement la même que la raison d'utiliser des pointeurs en C alloués avec malloc: si vous voulez que votre objet vive plus longtemps que votre variable!

Il est même fortement recommandé de NE PAS utiliser le nouvel opérateur si vous pouvez l'éviter. Surtout si vous utilisez des exceptions. En général, il est beaucoup plus sûr de laisser le compilateur libérer vos objets.

PierreBdR
la source
13

J'ai vu cet anti-pattern de la part de personnes qui ne comprennent pas tout à fait l'opérateur & address-of. S'ils ont besoin d'appeler une fonction avec un pointeur, ils alloueront toujours sur le tas afin qu'ils obtiennent un pointeur.

void FeedTheDog(Dog* hungryDog);

Dog* badDog = new Dog;
FeedTheDog(badDog);
delete badDog;

Dog goodDog;
FeedTheDog(&goodDog);
Mark Ransom
la source
Alternativement: void FeedTheDog (Dog & hungryDog); Chien chien; FeedTheDog (chien);
Scott Langham
1
@ScottLangham vrai, mais ce n'était pas le point que j'essayais de faire valoir. Parfois, vous appelez une fonction qui prend un pointeur NULL facultatif par exemple, ou peut-être est-ce une API existante qui ne peut pas être modifiée.
Mark Ransom
7

Traitez le tas comme un bien immobilier très important et utilisez-le de manière très judicieuse. La règle empirique de base est d'utiliser la pile chaque fois que possible et d'utiliser le tas chaque fois qu'il n'y a pas d'autre moyen. En allouant les objets sur la pile, vous pouvez obtenir de nombreux avantages tels que:

(1). Vous n'avez pas à vous soucier du déroulement de la pile en cas d'exceptions

(2). Vous n'avez pas à vous soucier de la fragmentation de la mémoire causée par l'allocation de plus d'espace que nécessaire par votre gestionnaire de tas.

Naveen
la source
1
Il devrait y avoir une certaine considération quant à la taille de l'objet. La pile est de taille limitée, donc les objets très volumineux doivent être alloués au tas.
Adam
1
@Adam Je pense que Naveen a toujours raison ici. Si vous pouvez mettre un gros objet sur la pile, faites-le, car c'est mieux. Il y a de nombreuses raisons pour lesquelles vous ne pouvez probablement pas. Mais je pense qu'il a raison.
McKay
5

La seule raison pour laquelle je m'inquiéterais est que Dog est maintenant alloué sur la pile, plutôt que sur le tas. Donc, si Dog a une taille de mégaoctets, vous pouvez avoir un problème,

Si vous devez emprunter la nouvelle route / supprimer, méfiez-vous des exceptions. Et à cause de cela, vous devez utiliser auto_ptr ou l'un des types de pointeurs intelligents boost pour gérer la durée de vie de l'objet.

Roddy
la source
1

Il n'y a aucune raison de créer un nouveau (sur le tas) lorsque vous pouvez allouer sur la pile (sauf si pour une raison quelconque vous avez une petite pile et que vous souhaitez utiliser le tas.

Vous pouvez envisager d'utiliser un shared_ptr (ou l'une de ses variantes) de la bibliothèque standard si vous souhaitez allouer sur le tas. Cela gérera la suppression pour vous une fois que toutes les références à shared_ptr auront disparu.

Scott Langham
la source
Il y a beaucoup de raisons, pour un exemple, voyez-moi répondre.
dangerousdave
Je suppose que vous voudrez peut-être que l'objet survit à la portée de la fonction, mais l'OP ne semble pas suggérer que c'est ce qu'ils essayaient de faire.
Scott Langham
0

Il y a une raison supplémentaire, que personne d'autre n'a mentionnée, pour laquelle vous pourriez choisir de créer votre objet dynamiquement. Les objets dynamiques basés sur des tas vous permettent d'utiliser le polymorphisme .

dangereuxdave
la source
2
Vous pouvez également travailler avec des objets basés sur la pile de manière polymorphe, je ne vois aucune différence entre les objets alloués à la pile / et au tas à cet égard. Par exemple: void MyFunc () {Chien chien; Nourrir (chien); } void Feed (Animal & animal) {auto food = animal-> GetFavouriteFood (); PutFoodInBowl (nourriture); } // Dans cet exemple, GetFavouriteFood peut être une fonction virtuelle que chaque animal remplace avec sa propre implémentation. Cela fonctionnera de manière polymorphe sans aucun tas impliqué.
Scott Langham
-1: le polymorphisme ne nécessite qu'une référence ou un pointeur; il est totalement indépendant de la durée de stockage de l'instance sous-jacente.
underscore_d
@underscore_d "L'utilisation d'une classe de base universelle implique un coût: les objets doivent être alloués en tas pour être polymorphes;" - bjarne stroustrup stroustrup.com/bs_faq2.html#object
dangerousdave
LOL, je m'en fiche si Stroustrup lui-même l'a dit, mais c'est une citation incroyablement embarrassante pour lui, car il a tort. Ce n'est pas difficile de tester cela vous-même, vous savez: instancier certains DerivedA, DerivedB et DerivedC sur la pile; instanciez également un pointeur alloué par pile vers Base et confirmez que vous pouvez l'associer à A / B / C et les utiliser de manière polymorphe. Appliquer la pensée critique et la référence standard aux revendications, même si elles sont de l'auteur original de la langue. Ici: stackoverflow.com/q/5894775/2757035
underscore_d
En d'autres termes, j'ai un objet contenant 2 familles distinctes de près de 1000 objets polymorphes chacun, avec une durée de stockage automatique. J'instancie cet objet sur la pile, et en faisant référence à ces membres via des références ou des pointeurs, il atteint toutes les capacités du polymorphisme sur eux. Rien dans mon programme n'est alloué dynamiquement / par tas (à part les ressources appartenant à des classes allouées à la pile), et cela ne change pas les capacités de mon objet d'un iota.
underscore_d
-4

J'ai eu le même problème dans Visual Studio. Vous devez utiliser:

yourClass-> classMethod ();

plutôt que:

yourClass.classMethod ();

Hamed
la source
3
Cela ne répond pas à la question. Vous notez plutôt qu'il faut utiliser une syntaxe différente pour accéder à l'objet alloué sur le tas (via le pointeur vers l'objet) et à l'objet alloué sur la pile.
Alexey Ivanov