c++ - utiliser - utilité des pointeurs




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

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 ceci:

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?


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 interne, votre objet myObject sera détruit une fois que cette fonction sera retournée. Donc c'est 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;

alors l'instance de classe Object pointée par myObject ne sera pas détruite une fois la fonction terminée, et l'allocation est sur heap.

Maintenant, si vous êtes programmeur Java, le second exemple est plus proche de la façon dont l'allocation d'objets 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.


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 cela a à voir avec l'efficacité et la vitesse puisque nous avons un accès direct à l'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. delete appelle ce destructeur pour vous.

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

Cela n'a rien à voir avec la 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 std::string une std::string place, std::string 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 secondaire : Une erreur que beaucoup de débutants font est l'utilisation de tableaux de longueur variable. C'est une extension GNU et aussi une extension de Clang car elles 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 indexés comme des tableaux et parce que les tableaux se décomposent en pointeurs au niveau supérieur dans une déclaration de fonction. Cependant, une fois qu'un tableau se réduit à un pointeur, le pointeur perd sa sizeof information. Donc sizeof(pointer) donnera la taille du pointeur en octets, qui est habituellement 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 fonctions qui vous permettent de traiter les objets comme un autre, par exemple en utilisant le mot-clé as . Donc, si quelqu'un voulait traiter un objet Entity comme un objet Player , 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 le mot-clé as . Pour être clair, si un casting échoue, il renvoie un pointeur invalide. Donc !test est essentiellement un raccourci pour vérifier si le test est NULL ou un pointeur invalide, 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 une erreur. 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 # commencent par utiliser des pointeurs sans le comprendre! Cela part littéralement du mauvais pied. Vous devez abandonner tout ce que vous savez, car 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 que de nombreux compilateurs font 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 transférer la propriété plutôt que de copier les gros objets pour les déplacer . Cela a conduit à la création de sémantiques de déplacement et de pointeurs intelligents .

Si vous utilisez des pointeurs, (R) RVO ne se produit pas . 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. Une fuite d'erreur peut se produire si l'appelant d'une fonction est responsable de la delete un objet alloué dynamiquement et ainsi de suite. 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.


En C ++, les objets alloués sur la pile (en utilisant l' Object object; instruction dans un bloc) ne vivront que dans la portée dans laquelle 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 heap, en utilisant Object* obj = new Object() , ils continuent à vivre en tas jusqu'à ce que vous 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é.


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 devez 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. Lorsque vous faites du new Object() , l'objet a une durée de stockage dynamique, ce qui signifie qu'il reste actif jusqu'à ce que vous le delete explicitement. Vous ne devez utiliser la durée de stockage dynamique que lorsque vous en avez besoin. Autrement dit, vous devriez toujours préférer 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 , ce 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. Jetez un oeil à 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 la 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 précédemment, préférez toujours 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 autorisant la transmission d'un nullptr 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 les standards C ++ précédents, 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 l' idiome de Pimpl .

  5. Vous devez vous connecter à 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 sa fonction get member. 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.


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.

Point de départ: la construction Object * object = new Object() est en réalité ce qui se rapproche le plus de la sémantique typique de Java (ou C # d'ailleurs).


Techniquement, c'est un problème d'allocation de mémoire, mais voici deux aspects plus pratiques de cela. Cela a à voir avec deux choses: 1) Scope, quand vous définissez un objet sans pointeur vous ne pourrez plus y accéder après le bloc de code dans lequel il est défini, alors que si vous définissez un pointeur avec "nouveau" alors vous can access it from anywhere you have a pointer to this memory until you call "delete" on the same pointer. 2) If you want to pass arguments to a function you want to pass a pointer or a reference in order to be more efficient. When you pass an Object then the object is copied, if this is an object that uses a lot of memory this might be CPU consuming (eg you copy a vector full of data). When you pass a pointer all you pass is one int (depending of implementation but most of them are one int).

Other than that you need to understand that "new" allocates memory on the heap that needs to be freed at some point. When you don't have to use "new" I suggest you use a regular object definition "on the stack".


"Necessity is the mother of invention." The most of important difference that I would like to point out is the outcome of my own experience of coding. Sometimes you need to pass objects to functions . In that case if your object is of a very big class then passing it as an object will copy its state (which you might not want ..AND CAN BE BIG OVERHEAD) thus resulting in overhead of copying object .while pointer is fixed 4 byte size (assuming 32 bit).Other reasons are already mentioned above...


With pointers ,

  • can directly talk to the memory.

  • can prevent lot of memory leaks of a program by manipulating pointers.


I will include one important use case of pointer. When you are storing some object in the base class, but it could be polymorphic.

Class Base1 {
};

Class Derived1 : public Base1 {
};


Class Base2 {
  Base *bObj;
  virtual void createMemerObects() = 0;
};

Class Derived2 {
  virtual void createMemerObects() {
    bObj = new Derived1();
  }
};

So in this case you can't declare bObj as an direct object, you have to have pointer.


In areas where memory utilization is at its premium , pointers comes handy. For example consider a minimax algorithm, where thousands of nodes will be generated using recursive routine, and later use them to evaluate the next best move in game, ability to deallocate or reset (as in smart pointers) significantly reduces memory consumption. Whereas the non-pointer variable continues to occupy space till it's recursive call returns a value.


One reason for using pointers is to interface with C functions. Another reason is to save memory; for example: instead of passing an object which contains a lot of data and has a processor-intensive copy-constructor to a function, just pass a pointer to the object, saving memory and speed especially if you're in a loop, however a reference would be better in that case, unless you're using an C-style array.


There are many benefits of using pointers to object -

  1. Efficiency (as you already pointed out). Passing objects to functions mean creating new copies of object.
  2. Working with objects from third party libraries. If your object belongs to a third party code and the authors intend the usage of their objects through pointers only (no copy constructors etc) the only way you can pass around this object is using pointers. Passing by value may cause issues. (Deep copy / shallow copy issues).
  3. if the object owns a resource and you want that the ownership should not be sahred with other objects.

This is has been discussed at length, but in Java everything is a pointer. It makes no distinction between stack and heap allocations (all objects are allocated on the heap), so you don't realize you're using pointers. In C++, you can mix the two, depending on your memory requirements. Performance and memory usage is more deterministic in C++ (duh).


Well the main question is Why should I use a pointer rather than the object itself? And my answer, you should (almost) never use pointer instead of object, because C++ has references , it is safer then pointers and guarantees the same performance as pointers.

Another thing you mentioned in your question:

Object *myObject = new Object;

Comment ça marche? It creates pointer of Object type, allocates memory to fit one object and calls default constructor, sounds good, right? But actually it isn't so good, if you dynamically allocated memory (used keyword new ), you also have to free memory manually, that means in code you should have:

delete myObject;

This calls destructor and frees memory, looks easy, however in big projects may be difficult to detect if one thread freed memory or not, but for that purpose you can try shared pointers , these slightly decreases performance, but it is much easier to work with them.

And now some introduction is over and go back to question.

You can use pointers instead of objects to get better performance while transferring data between function.

Take a look, you have std::string (it is also object) and it contains really much data, for example big XML, now you need to parse it, but for that you have function void foo(...) which can be declarated in different ways:

  1. void foo(std::string xml); In this case you will copy all data from your variable to function stack, it takes some time, so your performance will be low.
  2. void foo(std::string* xml); In this case you will pass pointer to object, same speed as passing size_t variable, however this declaration has error prone, because you can pass NULL pointer or invalid pointer. Pointers usually used in C because it doesn't have references.
  3. void foo(std::string& xml); Here you pass reference, basically it is the same as passing pointer, but compiler does some stuff and you cannot pass invalid reference (actually it is possible to create situation with invalid reference, but it is tricking compiler).
  4. void foo(const std::string* xml); Here is the same as second, just pointer value cannot be changed.
  5. void foo(const std::string& xml); Here is the same as third, but object value cannot be changed.

What more I want to mention, you can use these 5 ways to pass data no matter which allocation way you have chosen (with new or regular ).

Another thing to mention, when you create object in regular way, you allocate memory in stack, but while you create it with new you allocate heap. It is much faster to allocate stack, but it is kind a small for really big arrays of data, so if you need big object you should use heap, because you may get , but usually this issue is solved using STL containers and remember std::string is also container, some guys forgot it :)


Object *myObject = new Object;

Doing this will create a reference to an Object (on the heap) which has to be deleted explicitly to avoid memory leak .

Object myObject;

Doing this will create an object(myObject) of the automatic type (on the stack) that will be automatically deleted when the object(myObject) goes out of scope.





c++11