Question Pourquoi devrais-je utiliser un pointeur plutôt que l'objet lui-même?


Je viens d'un environnement Java et j'ai commencé à travailler avec des objets en C ++. Mais une chose qui m'est venue à l'esprit est que les gens utilisent souvent des pointeurs sur des objets plutôt que sur les objets eux-mêmes, par exemple cette déclaration:

Object *myObject = new Object;

plutôt que:

Object myObject;

Ou au lieu d'utiliser une fonction, disons testFunc(), comme ça:

myObject.testFunc();

nous devons écrire:

myObject->testFunc();

Mais je ne peux pas comprendre pourquoi devrions-nous le faire de cette façon. Je suppose que cela a à voir avec l'efficacité et la vitesse puisque nous avons un accès direct à l'adresse mémoire. Ai-je raison?


1333
2018-03-03 11:54


origine


Réponses:


Il est très regrettable que vous voyiez si souvent une allocation dynamique. Cela montre juste combien de mauvais programmeurs C ++ il y a.

Dans un sens, vous avez deux questions regroupées en une seule. La première est quand devrions-nous utiliser l'allocation dynamique (en utilisant new)? La seconde est quand devrions-nous utiliser des pointeurs?

Le message important à retenir est que vous devriez toujours utiliser l'outil approprié pour le travail. Dans presque toutes les situations, il y a quelque chose de plus approprié et plus sûr que d'effectuer une allocation dynamique manuelle et / ou d'utiliser des pointeurs bruts.

Allocation dynamique

Dans votre question, vous avez démontré deux façons de créer un objet. La principale différence est la durée de stockage de l'objet. En faisant Object myObject; Au sein d'un bloc, l'objet est créé avec une durée de stockage automatique, ce qui signifie qu'il sera détruit automatiquement lorsqu'il sortira de sa portée. Quand tu fais new Object(), l'objet a une durée de stockage dynamique, ce qui signifie qu'il reste actif jusqu'à ce que vous explicitement delete il. Vous ne devez utiliser la durée de stockage dynamique que lorsque vous en avez besoin. C'est, vous devriez toujours préfèrent créer des objets avec une durée de stockage automatique lorsque vous le pouvez.

Les deux principales situations dans lesquelles vous pourriez avoir besoin d'allocation dynamique:

  1. Vous avez besoin de l'objet pour survivre à la portée actuelle - cet objet spécifique à cet emplacement de mémoire spécifique, pas une copie de celui-ci. Si vous êtes d'accord pour copier / déplacer l'objet (la plupart du temps vous devriez l'être), vous devriez préférer un objet automatique.
  2. Vous devez allouer beaucoup de mémoire, qui peut facilement remplir la pile. Ce serait bien si nous n'avions pas à nous préoccuper de cela (la plupart du temps, vous ne devriez pas avoir à le faire), car c'est vraiment en dehors du C ++, mais malheureusement nous devons faire face à la réalité des systèmes que nous Je développe pour.

Lorsque vous avez absolument besoin d'une allocation dynamique, vous devez l'encapsuler dans un pointeur intelligent ou un autre type qui effectue RAII (comme les conteneurs standard). Les pointeurs intelligents fournissent la sémantique de propriété des objets alloués dynamiquement. Jeter un coup d'œil à std::unique_ptr et std::shared_ptr, par exemple. Si vous les utilisez de manière appropriée, vous pouvez presque entièrement éviter d'effectuer votre propre gestion de la mémoire (voir Règle de Zéro).

Pointeurs

Cependant, il existe d'autres utilisations plus générales pour les pointeurs bruts au-delà de l'allocation dynamique, mais la plupart ont des alternatives que vous devriez préférer. Comme avant, toujours préférer les alternatives sauf si vous avez vraiment besoin de pointeurs.

  1. Vous avez besoin de sémantique de référence. Parfois, vous voulez passer un objet en utilisant un pointeur (indépendamment de la façon dont il a été alloué) parce que vous voulez que la fonction à laquelle vous le passez ait accès à cet objet spécifique (pas une copie de celui-ci). Cependant, dans la plupart des situations, vous devriez préférer les types de référence aux pointeurs, car c'est précisément ce pour quoi ils sont conçus. Notez qu'il ne s'agit pas nécessairement d'étendre la durée de vie de l'objet au-delà de la portée actuelle, comme dans la situation 1 ci-dessus. Comme précédemment, si vous acceptez de transmettre une copie de l'objet, vous n'avez pas besoin de sémantique de référence.

  2. Vous avez besoin de polymorphisme. Vous ne pouvez appeler des fonctions que de manière polymorphique (c'est-à-dire selon le type dynamique d'un objet) via un pointeur ou une référence à l'objet. Si c'est le comportement dont vous avez besoin, vous devez utiliser des pointeurs ou des références. Encore une fois, les références devraient être préférées.

  3. Vous voulez représenter qu'un objet est facultatif en permettant un nullptr être passé lorsque l'objet est omis. Si c'est un argument, vous devriez utiliser des arguments par défaut ou des surcharges de fonctions. Sinon, vous devriez utiliser un type qui encapsule ce comportement, tel que std::optional (introduit en C ++ 17 - avec des normes C ++ antérieures, utilisez boost::optional).

  4. Vous voulez découpler les unités de compilation pour améliorer le temps de compilation. La propriété utile d'un pointeur est que vous n'avez besoin que d'une déclaration forward du type pointé vers (pour utiliser réellement l'objet, vous aurez besoin d'une définition). Cela vous permet de découpler des parties de votre processus de compilation, ce qui peut améliorer considérablement le temps de compilation. Voir le Pimpl idiome.

  5. Vous devez vous interfacer avec une bibliothèque C ou une bibliothèque de style C. À ce stade, vous êtes obligé d'utiliser des pointeurs bruts. La meilleure chose que vous pouvez faire est de vous assurer que vous laissez seulement vos pointeurs bruts au dernier moment possible. Vous pouvez obtenir un pointeur brut à partir d'un pointeur intelligent, par exemple, en utilisant son get fonction de membre. Si une bibliothèque effectue une allocation pour vous et qu'elle s'attend à ce que vous la libériez via un descripteur, vous pouvez souvent envelopper la poignée dans un pointeur intelligent avec un suppresseur personnalisé qui libérera l'objet de manière appropriée.


1346
2018-03-03 12:06



Il existe de nombreux cas d'utilisation pour les pointeurs.

Comportement polymorphique. Pour les types polymorphes, les pointeurs (ou références) sont utilisés pour éviter le découpage:

class Base { ... };
class Derived : public Base { ... };

void fun(Base b) { ... }
void gun(Base* b) { ... }
void hun(Base& b) { ... }

Derived d;
fun(d);    // oops, all Derived parts silently "sliced" off
gun(&d);   // OK, a Derived object IS-A Base object
hun(d);    // also OK, reference also doesn't slice

Sémantique de référence et éviter la copie. Pour les types non polymorphes, un pointeur (ou une référence) évitera de copier un objet potentiellement coûteux

Base b;
fun(b);  // copies b, potentially expensive 
gun(&b); // takes a pointer to b, no copying
hun(b);  // regular syntax, behaves as a pointer

Notez que C ++ 11 a une sémantique de déplacement qui peut éviter de nombreuses copies d'objets coûteux dans l'argument de la fonction et comme valeurs de retour. Mais l'utilisation d'un pointeur va certainement les éviter et permettra plusieurs pointeurs sur le même objet (alors qu'un objet ne peut être déplacé que d'une seule fois).

Acquisition de ressources. Création d'un pointeur vers une ressource à l'aide du new l'opérateur est un anti-motif en C ++ moderne. Utilisez une classe de ressources spéciale (un des conteneurs Standard) ou une pointeur intelligent (std::unique_ptr<> ou std::shared_ptr<>). Considérer:

{
    auto b = new Base;
    ...       // oops, if an exception is thrown, destructor not called!
    delete b;
}

contre.

{
    auto b = std::make_unique<Base>();
    ...       // OK, now exception safe
}

Un pointeur brut ne doit être utilisé que comme une «vue» et ne pas être impliqué dans la propriété, que ce soit par création directe ou implicitement à travers des valeurs de retour. Voir également cette Q & A de la FAQ C ++.

Contrôle de vie plus précis Chaque fois qu'un pointeur partagé est copié (par exemple en tant qu'argument de fonction), la ressource vers laquelle il pointe est conservée en vie. Objets réguliers (non créés par new, soit directement par vous ou à l'intérieur d'une classe de ressources) sont détruits lors de la sortie de la portée.


155
2018-03-06 18:40



Il y a beaucoup d'excellentes réponses à cette question, y compris les cas d'utilisation importants des déclarations anticipées, du polymorphisme etc. mais je pense qu'une partie de l'âme de votre question n'a pas de réponse - à savoir ce que signifient les différentes syntaxes en Java et C ++.

Examinons la situation en comparant les deux langues:

Java:

Object object1 = new Object(); //A new object is allocated by Java
Object object2 = new Object(); //Another new object is allocated by Java

object1 = object2; 
//object1 now points to the object originally allocated for object2
//The object originally allocated for object1 is now "dead" - nothing points to it, so it
//will be reclaimed by the Garbage Collector.
//If either object1 or object2 is changed, the change will be reflected to the other

L'équivalent le plus proche de ceci est:

C ++:

Object * object1 = new Object(); //A new object is allocated on the heap
Object * object2 = new Object(); //Another new object is allocated on the heap
delete object1;
//Since C++ does not have a garbage collector, if we don't do that, the next line would 
//cause a "memory leak", i.e. a piece of claimed memory that the app cannot use 
//and that we have no way to reclaim...

object1 = object2; //Same as Java, object1 points to object2.

Voyons l'alternative C ++:

Object object1; //A new object is allocated on the STACK
Object object2; //Another new object is allocated on the STACK
object1 = object2;//!!!! This is different! The CONTENTS of object2 are COPIED onto object1,
//using the "copy assignment operator", the definition of operator =.
//But, the two objects are still different. Change one, the other remains unchanged.
//Also, the objects get automatically destroyed once the function returns...

La meilleure façon d'y penser est que Java (implicitement) gère les pointeurs vers les objets, tandis que C ++ peut gérer les pointeurs vers les objets ou les objets eux-mêmes. Il y a des exceptions à cela - par exemple, si vous déclarez des types Java "primitifs", ce sont des valeurs réelles qui sont copiées, et non des pointeurs. Alors,

Java:

int object1; //An integer is allocated on the stack.
int object2; //Another integer is allocated on the stack.
object1 = object2; //The value of object2 is copied to object1.

Cela dit, l'utilisation de pointeurs n'est PAS nécessairement la bonne ou la mauvaise façon de gérer les choses; Cependant, d'autres réponses ont couvert cela de manière satisfaisante. L'idée générale est que, en C ++, vous avez beaucoup plus de contrôle sur la durée de vie des objets et sur leur lieu de vie.

Prenez le point de départ - le Object * object = new Object() La construction est en fait ce qui est le plus proche de la sémantique typique de Java (ou C # d'ailleurs).


111
2018-03-03 14:34



Une autre bonne raison d'utiliser des pointeurs serait pour déclarations à terme. Dans un projet assez important, ils peuvent vraiment accélérer le temps de compilation.


73
2018-03-07 07:30



Préface

Java n'est rien comme C ++, contrairement à hype. La machine hype Java aimerait vous faire croire que parce que Java a une syntaxe similaire à C ++, que les langages sont similaires. Rien ne peut être plus éloigné de la vérité. Cette désinformation fait partie de la raison pour laquelle les programmeurs Java vont en C ++ et utilisent une syntaxe similaire à Java sans comprendre les implications de leur code.

En avant nous allons

Mais je ne peux pas comprendre pourquoi devrions-nous le faire de cette façon. Je suppose que   a à voir avec l'efficacité et la rapidité puisque nous avons un accès direct à la   adresse mémoire Ai-je raison?

Au contraire, en fait. Le tas est beaucoup plus lent que la pile, car la pile est très simple par rapport au tas. Les variables de stockage automatique (alias variables de la pile) ont leurs destructeurs appelés une fois qu'ils sortent de la portée. Par exemple:

{
    std::string s;
}
// s is destroyed here

D'un autre côté, si vous utilisez un pointeur dynamiquement alloué, son destructeur doit être appelé manuellement. deleteappelle ce destructeur pour vous.

{
    std::string* s = new std::string;
}
delete s; // destructor called

Cela n'a rien à voir avec le new syntaxe répandue en C # et Java. Ils sont utilisés à des fins complètement différentes.

Avantages de l'allocation dynamique

1. Vous n'avez pas besoin de connaître la taille du tableau à l'avance

L'un des premiers problèmes rencontrés par de nombreux programmeurs C ++ est que lorsqu'ils acceptent des entrées arbitraires des utilisateurs, vous ne pouvez allouer qu'une taille fixe à une variable de pile. Vous ne pouvez pas non plus modifier la taille des tableaux. Par exemple:

char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow

Bien sûr, si vous avez utilisé un std::string au lieu, std::string se redimensionne en interne, ce qui ne devrait pas poser de problème. Mais essentiellement, la solution à ce problème est l'allocation dynamique. Vous pouvez allouer de la mémoire dynamique en fonction de l'entrée de l'utilisateur, par exemple:

int * pointer;
std::cout << "How many items do you need?";
std::cin >> n;
pointer = new int[n];

Note latérale: Une erreur que beaucoup de débutants font est l'utilisation de   tableaux de longueur variable. C'est une extension GNU et aussi une à Clang   parce qu'ils reflètent de nombreuses extensions de GCC. Donc, le suivant    int arr[n] ne devrait pas être invoqué.

Parce que le tas est beaucoup plus grand que la pile, on peut allouer arbitrairement / réallouer autant de mémoire qu'il en a besoin, alors que la pile a une limitation.

2. Les tableaux ne sont pas des pointeurs

Comment est-ce un avantage que vous demandez? La réponse deviendra claire une fois que vous aurez compris la confusion / mythe derrière les tableaux et les pointeurs. On suppose généralement qu'ils sont identiques, mais ils ne le sont pas. Ce mythe vient du fait que les pointeurs peuvent être indicés comme des tableaux et parce que les tableaux se désintègrent en pointeurs au niveau supérieur dans une déclaration de fonction. Cependant, une fois qu'un tableau se désintègre en un pointeur, le pointeur perd son sizeof information. Alors sizeof(pointer) donnera la taille du pointeur en octets, qui est généralement de 8 octets sur un système 64 bits.

Vous ne pouvez pas assigner à des tableaux, seulement les initialiser. Par exemple:

int arr[5] = {1, 2, 3, 4, 5}; // initialization 
int arr[] = {1, 2, 3, 4, 5}; // The standard dictates that the size of the array
                             // be given by the amount of members in the initializer  
arr = { 1, 2, 3, 4, 5 }; // ERROR

D'un autre côté, vous pouvez faire ce que vous voulez avec des pointeurs. Malheureusement, parce que la distinction entre les pointeurs et les tableaux est agitée à la main en Java et en C #, les débutants ne comprennent pas la différence.

3. Polymorphisme

Java et C # ont des fonctionnalités qui vous permettent de traiter les objets comme un autre, par exemple en utilisant le as mot-clé. Donc, si quelqu'un voulait traiter un Entity objet en tant que Player objet, on pourrait faire Player player = Entity as Player; Ceci est très utile si vous avez l'intention d'appeler des fonctions sur un conteneur homogène qui ne devrait s'appliquer qu'à un type spécifique. La fonctionnalité peut être réalisée de la même manière ci-dessous:

std::vector<Base*> vector;
vector.push_back(&square);
vector.push_back(&triangle);
for (auto& e : vector)
{
     auto test = dynamic_cast<Triangle*>(e); // I only care about triangles
     if (!test) // not a triangle
        e.GenericFunction();
     else
        e.TriangleOnlyMagic();
}

Donc, si seulement Triangles avait une fonction Rotate, ce serait une erreur de compilation si vous essayiez de l'appeler sur tous les objets de la classe. En utilisant dynamic_cast, vous pouvez simuler as mot-clé. Pour être clair, si un casting échoue, il renvoie un pointeur invalide. Alors !test est essentiellement un raccourci pour vérifier si test est NULL ou un pointeur non valide, ce qui signifie que la distribution a échoué.

Avantages des variables automatiques

Après avoir vu toutes les grandes choses que l'allocation dynamique peut faire, vous vous demandez probablement pourquoi personne n'utiliserait l'allocation dynamique tout le temps. Je t'ai déjà dit une raison, le tas est lent. Et si vous n'avez pas besoin de tout ce souvenir, vous ne devriez pas en abuser. Donc, voici quelques inconvénients dans aucun ordre particulier:

  • C'est sujet aux erreurs. L'allocation manuelle de la mémoire est dangereuse et vous êtes sujet à des fuites. Si vous n'êtes pas compétent pour utiliser le débogueur ou valgrind (un outil de fuite de mémoire), vous pouvez tirer vos cheveux de votre tête. Heureusement, les idiomes de RAII et les pointeurs intelligents soulagent un peu, mais vous devez être familier avec des pratiques telles que la règle des trois et la règle des cinq. Il y a beaucoup d'informations à prendre, et les débutants qui ne savent pas ou ne s'en soucient pas tomberont dans ce piège.

  • Ce n'est pas nécessaire. Contrairement à Java et C # où il est idiomatique d'utiliser le new mot-clé partout, en C ++, vous ne devriez l'utiliser que si vous en avez besoin. La phrase commune va, tout ressemble à un clou si vous avez un marteau. Alors que les débutants qui commencent par C ++ ont peur des pointeurs et apprennent à utiliser les variables de pile par habitude, les programmeurs Java et C # début en utilisant des pointeurs sans le comprendre! Cela part littéralement du mauvais pied. Vous devez abandonner tout ce que vous savez parce que la syntaxe est une chose, l'apprentissage de la langue en est une autre.

1. (R) RVO - Aka, Optimisation de la valeur de retour (nommée)

Une optimisation de nombreux compilateurs sont des choses appelées élision et optimisation de la valeur de retour. Ces choses peuvent éviter les copys inutiles, ce qui est utile pour les objets qui sont très volumineux, comme un vecteur contenant beaucoup d'éléments. Normalement, la pratique courante consiste à utiliser des pointeurs pour transfert de propriété plutôt que de copier les gros objets à bouge toi les autour. Cela a conduit à la création de déplacer la sémantique et pointeurs intelligents.

Si vous utilisez des pointeurs, (R) RVO ne NE PAS se produire. Il est plus avantageux et moins sujet aux erreurs de tirer parti de (R) RVO plutôt que de renvoyer ou de passer des pointeurs si vous vous inquiétez de l'optimisation. Des erreurs peuvent survenir si l'appelant d'une fonction est responsable de deleteun objet alloué dynamiquement et tel. Il peut être difficile de suivre la propriété d'un objet si les pointeurs sont passés comme une patate chaude. Utilisez simplement les variables de pile car c'est plus simple et mieux.


62
2018-03-07 10:00



C ++ vous donne trois façons de passer un objet: par pointeur, par référence et par valeur. Java vous limite avec le dernier (la seule exception est les types primitifs comme int, boolean etc). Si vous voulez utiliser C ++ non pas comme un jouet bizarre, alors vous feriez mieux de connaître la différence entre ces trois façons.

Java prétend qu'il n'y a pas de problème comme "qui et quand devrait détruire cela?". La réponse est: Le garbage collector, super et horrible. Néanmoins, il ne peut pas fournir une protection à 100% contre les fuites de mémoire (oui, Java pouvez mémoire de fuite). En fait, GC vous donne un faux sentiment de sécurité. Plus votre VUS est gros, plus votre chemin vers l'évacuateur est long.

C ++ vous laisse face à la gestion du cycle de vie des objets. Eh bien, il y a des moyens pour y faire face (pointeurs intelligents famille, QObject dans Qt et ainsi de suite), mais aucun d'entre eux ne peut être utilisé de manière «feu et oublier» comme GC: vous devriez toujoursgardez à l'esprit la gestion de la mémoire. Non seulement vous devez vous soucier de détruire un objet, mais vous devez également éviter de détruire le même objet plus d'une fois.

Pas encore effrayé? Ok: références cycliques - maniez-les vous-même, humain. Et rappelez-vous: tuez chaque objet une seule fois, nous, les runtimes C ++, n'aimons pas ceux qui gâchent des cadavres, laissez les morts seuls.

Alors, revenons à votre question.

Lorsque vous passez votre objet par valeur, pas par pointeur ou par référence, vous copiez l'objet (tout l'objet, qu'il s'agisse de quelques octets ou d'un énorme vidage de base de données - vous êtes assez intelligent pour éviter ce dernier, aren ' t vous?) chaque fois que vous faites '='. Et pour accéder aux membres de l'objet, vous utilisez '.' (point).

Lorsque vous passez votre objet par pointeur, vous copiez seulement quelques octets (4 sur des systèmes 32 bits, 8 sur 64 bits), à savoir - l'adresse de cet objet. Et pour le montrer à tout le monde, vous utilisez cet opérateur '->' sophistiqué lorsque vous accédez aux membres. Ou vous pouvez utiliser la combinaison de '*' et '.'

Lorsque vous utilisez des références, vous obtenez le pointeur qui prétend être une valeur. C'est un pointeur, mais vous accédez aux membres via '.'.

Et, pour faire sauter votre esprit une fois de plus: quand vous déclarez plusieurs variables séparées par des virgules, alors (regardez les mains):

  • Le type est donné à tout le monde
  • Valeur / pointeur / modificateur de référence est individuel

Exemple:

struct MyStruct
{
    int* someIntPointer, someInt; //here comes the surprise
    MyStruct *somePointer;
    MyStruct &someReference;
};

MyStruct s1; //we allocated an object on stack, not in heap

s1.someInt = 1; //someInt is of type 'int', not 'int*' - value/pointer modifier is individual
s1.someIntPointer = &s1.someInt;
*s1.someIntPointer = 2; //now s1.someInt has value '2'
s1.somePointer = &s1;
s1.someReference = s1; //note there is no '&' operator: reference tries to look like value
s1.somePointer->someInt = 3; //now s1.someInt has value '3'
*(s1.somePointer).someInt = 3; //same as above line
*s1.somePointer->someIntPointer = 4; //now s1.someInt has value '4'

s1.someReference.someInt = 5; //now s1.someInt has value '5'
                              //although someReference is not value, it's members are accessed through '.'

MyStruct s2 = s1; //'NO WAY' the compiler will say. Go define your '=' operator and come back.

//OK, assume we have '=' defined in MyStruct

s2.someInt = 0; //s2.someInt == 0, but s1.someInt is still 5 - it's two completely different objects, not the references to the same one

21
2018-03-03 12:00



En C ++, les objets alloués sur la pile (en utilisant Object object; déclaration à l'intérieur d'un bloc) ne vivra que dans la portée où ils sont déclarés. Lorsque le bloc de code termine l'exécution, les objets déclarés sont détruits. Alors que si vous allouez de la mémoire sur le tas, en utilisant Object* obj = new Object(), ils continuent à vivre en tas jusqu'à ce que vous appeliez delete obj.

Je créerais un objet sur le tas quand j'aime utiliser l'objet non seulement dans le bloc de code qui l'a déclaré / alloué.


19
2018-03-03 12:19



Mais je ne peux pas comprendre pourquoi devrions-nous l'utiliser comme ça?

Je vais comparer comment cela fonctionne dans le corps de la fonction si vous utilisez:

Object myObject;

Dans la fonction, votre myObject sera détruit une fois que cette fonction sera rétablie. C'est donc utile si vous n'avez pas besoin de votre objet en dehors de votre fonction. Cet objet sera placé dans la pile de threads actuelle.

Si vous écrivez à l'intérieur du corps de la fonction:

 Object *myObject = new Object;

then Instance de classe d'objets pointée par myObject ne sera pas détruit une fois la fonction terminée, et l'allocation est sur le tas.

Maintenant, si vous êtes programmeur Java, le second exemple est plus proche de la façon dont l'allocation d'objet fonctionne sous Java. Cette ligne: Object *myObject = new Object; est équivalent à Java: Object myObject = new Object();. La différence est que sous java myObject il y aura du garbage collecté, alors que sous c ++ il ne sera pas libéré, vous devrez quelque part appeler explicitement `delete myObject; sinon vous allez introduire des fuites de mémoire.

Depuis c ++ 11, vous pouvez utiliser des méthodes sûres d'allocation dynamique: new Object, en stockant des valeurs dans shared_ptr / unique_ptr.

std::shared_ptr<std::string> safe_str = make_shared<std::string>("make_shared");

// since c++14
std::unique_ptr<std::string> safe_str = make_unique<std::string>("make_shared"); 

De plus, les objets sont très souvent stockés dans des conteneurs, comme des cartes ou des vecteurs, ils gèrent automatiquement la durée de vie de vos objets.


18
2018-03-03 12:05