web-dev-qa-db-fra.com

Confondu avec les tableaux d'objets en C++

J'ai donc d'abord appris Java et maintenant j'essaie de passer au C++. J'ai un peu de difficulté à faire fonctionner correctement les tableaux.

En ce moment, je tente simplement de créer un tableau d'objet "Player" et de le remplir avec un. Mais je reçois une erreur.

Player* players = new Player[1];
players[0] = new Player(playerWidth, playerHeight, 20, 1);

L'erreur dit: L'opérande "=" correspond à ces opérandes. Les types d'opérandes sont: Player = Player *

Je ne comprends pas pourquoi ça ne marche pas?

13
Eric Smith

Ce que l'erreur dit, c'est que vous essayez d'affecter une valeur d'un type incorrect à la variable. Lorsque l'erreur indique Player = Player *, cela signifie que la variable du côté gauche est un Player et que la valeur du côté droit est un Player *.

players[0] = new Player(playerWidth, playerHeight, 20, 1);

Le problème est similaire à si vous deviez le faire:

int x;
x = "Hello, World!";

Les types gauche et droit ne correspondent pas, et il n'y a pas de conversion naturelle, donc vous obtenez une erreur.


Le premier problème est que vous venez d'un contexte Java et que Java utilise beaucoup de pointeurs, mais vous les cache. C++ ne les cache pas du tout. La conséquence est que C++ a une syntaxe différente pour traiter explicitement les pointeurs. Java s'est débarrassé de tout cela et a principalement utilisé la syntaxe standard sans pointeur de C++ pour traiter les pointeurs.

Java:                                  C++:

Player player = new Player();          Player *player = new Player();

Player player2;                        Player *player2 = nullptr;

** no equivalent in Java **            Player player3;

player.foo();                          player->foo();

** no equivalent in Java **            player3.foo();

** no equivalent in Java **            *player;

** no equivalent in Java **            &player2;

Il est très important de comprendre la différence entre travailler avec des pointeurs et travailler directement avec un objet:

Java:                                  C++:

Player a = new Player();               Player *a = new Player();
Player b = a;                          Player *b = a;
b.foo();                               b->foo();

Dans ce code, il n'y a qu'un seul objet, auquel vous pouvez accéder via a ou b et cela ne change rien, a et b sont tous deux des pointeurs. au même objet.

C++:

Player c = Player();
Player d = c;
d.foo();

Dans ce code, il y a deux objets. Ils sont distincts et faire quelque chose pour d n’affecte pas c.

Si, dans Java, vous avez appris la distinction entre les types "primitifs" tels que int et les types Object tels que String, il est alors possible de penser que tous les objets sont primitifs en C++. Si nous examinons votre code et utilisons cette règle 'Les objets C++ ressemblent à la règle Java primitives', vous pourrez peut-être mieux voir ce qui ne va pas:

Java:
int[] players = new int[1];
players[0] = new int(playerWidth); // huh???

Cela devrait indiquer clairement que le membre de droite de l'affectation devrait simplement être une valeur de joueur plutôt qu'une allocation dynamique d'un nouvel objet de joueur. Pour un int dans Java, cela ressemble à players[0] = 100;. Comme les types d'objets dans Java sont différents Java n'a pas le moyen d'écrire les valeurs Object comme vous pouvez écrire int valeurs. Mais C++ le fait; players[0] = Player(playerWidth, playerHeight, 20, 1);


Le deuxième problème est que les tableaux en C sont bizarres et que C++ en a hérité.

Les pointeurs en C et C++ autorisent l'arithmétique de pointeur. Si vous avez un pointeur sur un objet, vous pouvez ajouter ou soustraire de celui-ci et obtenir un pointeur sur un autre objet. Java n'a rien de tel.

int x[2]; // create an array of two ints, the ints are 'adjacent' to one another
// if you take the address for the first one and 'increment' it
// then you'll have a pointer to the second one.

int *i = &x[0]; // i is a pointer to the first element
int *j = &x[1]; // j is a pointer to the second element

// i + 1 equals j
// i equals j - 1

De plus, l'opérateur d'index de tableau [] fonctionne sur les pointeurs. x[5] est équivalent à *(x+5). Cela signifie que les pointeurs peuvent être utilisés en tant que tableaux, ce qui est idiomatique et attendu en C et C++. En fait, il est même intégré au C++.

En C++, lorsque vous utilisez new pour allouer de manière dynamique un objet, par exemple. new Player, vous obtenez normalement un pointeur sur le type que vous avez spécifié. Dans cet exemple, vous obtenez Player *. Mais lorsque vous allouez dynamiquement un tableau, par exemple, new Player[5], c'est différent. Au lieu de récupérer un pointeur sur un tableau de cinq Players, vous récupérez en fait un pointeur sur le premier élément. C'est comme n'importe quel autre Player *:

Player *p   = new Player;    // not an array
Player *arr = new Player[5]; // an array

La seule chose qui rend ce pointeur différent est que lorsque vous faites un calcul arithmétique de pointeur dessus, vous obtenez des pointeurs sur des objets Player valides:

Player *x = p + 1;   // not pointing at a valid Player
Player *y = arr + 3; // pointing at the fourth array element

new et delete sont difficiles à utiliser correctement si vous les utilisez sans protection. Pour démontrer ceci:

int *x = new int;
foo();
delete x;

Ce code est sujet aux erreurs et probablement faux. Plus précisément, si foo() lève une exception, alors x a une fuite.

En C++, chaque fois que vous acquérez une responsabilité, par exemple lorsque vous appelez new vous acquérez la responsabilité d'appeler delete ultérieurement, vous devez vous souvenir

R.A.I.I.
Responsability * Acquisition Is Initialization

* Plus souvent, les gens disent que "l'acquisition de ressources est une initialisation", mais les ressources ne sont qu'un type de responsabilité. Jon Kalb m'a persuadé d'utiliser ce dernier terme dans l'une de ses Exception Safe C++ discussions.

R.A.I.I. signifie que chaque fois que vous acquérez une responsabilité, il devrait ressembler à l'initialisation d'un objet; Plus précisément, vous initialisez un objet spécial dont le but est de gérer cette responsabilité pour vous. Un exemple d'un tel type est std::unique_ptr<int> qui gérera les pointeurs vers ints alloué avec new:

C++:

std::unique_ptr<int> x(new int);
foo();
// no 'delete x;'

Pour gérer votre tableau Player, vous utiliseriez std::unqiue_ptr comme ceci:

std::unique_ptr<Player[]> players(new Player[1]);
players[0] = Player(playerWidth, playerHeight, 20, 1);

Maintenant, le unique_ptr gérera cette allocation pour vous et vous n'avez pas besoin d'appeler vous-même delete. (NB: lorsque vous allouez un tableau, vous devez donner unique_ptr un type de tableau; std::unique_ptr<Player[]>, et lorsque vous allouez quoi que ce soit, vous utilisez un type de type non-tableau, std::unique_ptr<Player>.)

Bien sûr, C++ a un R.A.I.I. encore plus spécialisé. tapez pour la gestion des tableaux, std::vector, et préférez-le plutôt que std::unique_ptr:

std::vector<Player> players(1);
players[0] = Player(playerWidth, playerHeight, 20, 1);

Ou en C++ 11:

std::vector<Player> players { Player(playerWidth, playerHeight, 20, 1) };
36
bames53

Vos types ne correspondent pas. Et ce n’est pas étonnant, vous essayez de stocker un Player* dans une Player déjà allouée!

Player* players = new Player[1];

Cela crée un tableau de longueur 1, contenant une Player instanciée, et stocke le tout dans un Player*. Le type de players[0] va être Player.

players[0] = new Player(...)

Cela tente de créer un nouveau Player* et de le stocker dans le tableau. Mais le tableau contient des objets Player. Tu devrais juste dire

players[0] = Player(...)

Sinon, et je suppose que c'est plus approprié pour vous, vous devriez cesser d'utiliser new entièrement et utiliser un std::vector.

std::vector<Player> players;
players.Push_back(Player(playerWidth, playerHeight, 20, 1));
// or players.emplace_back(playerWidth, playerHeight, 20, 1);

Non seulement cela est beaucoup plus facile à utiliser, mais vous n’avez pas non plus à vous souvenir de delete plus tard. Lorsque le std::vector sort de la portée, il se détruit automatiquement. De plus, contrairement à votre tableau, std::vector peut contenir un nombre quelconque d’objets, vous pouvez donc ajouter de nouveaux joueurs ou supprimer des joueurs existants à volonté.

Il existe également d'autres structures de données qui pourraient vous convenir davantage, en fonction de votre utilisation exacte, mais std::vector est un bon point de départ.

8
Lily Ballard

La raison en est, le type de votre variable 

players[0]

est Player (objet). Cependant, l'opérateur "new" (new Player) renvoie un pointeur (Player *)

Si vous voulez avoir un seul objet, la manière correcte de le faire sera:

Player* player = new Player(playerWidth, playerHeight, 20, 1);

Et n'oubliez pas qu'en C++, vous devez nettoyer le désordre après vous - quelque part dans l'appel final

delete player;

pour chaque objet que vous avez créé. C++ n'a pas Garbage Collector, ce qui signifie que tous les objets créés manuellement (par "nouveaux") restent jusqu'à ce que vous les supprimiez manuellement.

4
DarkWanderer

En Java, lorsque vous utilisez le mot clé "new", vous récupérez un pointeur sur un objet. C'est le seul moyen d'instancier un type d'objet en Java. Ainsi, lorsque vous dites que vous avez un "tableau d'objets" en Java, il est plus correct de dire que vous avez un tableau de pointeurs sur les objets.

C++ ne cache pas le fait que les objets sont des pointeurs. Vous pouvez avoir une variable référençant un objet ou une variable référençant un pointeur sur un objet.

Dans votre exemple, vous devez explicitement le déclarer comme un tableau de pointeurs sur des objets.

Players **players = new (Player*)[1];                         // Create an array of player pointers
players[0] = new Player(playerWidth, playerHeight, 20, 1);    // Create a single player

Et tandis que C++ vous permet de créer explicitement des objets en utilisant le mot-clénew, vous devez vous assurer de nettoyer vos objets une fois que vous avez terminé, sinon ils ne seront jamais désalloués (fuite mémoire). 

C'est l'une des principales différences entre C++ et Java. Les objets Java sont ramassés et le programmeur n'a pas à s'inquiéter de la gestion de la durée de vie d'un objet.

Une fois que vous avez terminé, vous devrez nettoyer à la fois le joueur individuel que vous avez alloué, ainsi que le tableau. En règle générale, chaque appel ànewdoit correspondre à un appel àdelete.

delete players[0];  // delete the player pointed to by players[0]
delete[] players;   // syntax for deleting arrays

Cependant, il est intéressant de noter que contrairement à Java, où les objets sont alloués sur le tas, vous pouvez créer des objets sur la pile en C++ comme s'il s'agissait de types primitifs (tels que int, float, char). Cela vous permet d'avoir des objets localement étendus, ainsi que alignés de manière contiguë en mémoire. Il n'y a aucun moyen de faire cela en Java.

Si vous allouez un tableau d'objets de cette façon, le constructeur par défaut est appelé pour chaque objet du tableau.

Player p;                           // This calls the default constructor and returns a Player object

Players *players = new Player[5];   // Create an array of player objects
players[0].playerWidth = 8;         // valid because the object has already been constructed

delete[] players; // don't forget to cleanup the array.
                  // no need to cleanup individual player objects, as they are locally scoped.

EDIT: Comme d'autres l'ont déjà mentionné, utiliser std :: vector au lieu d'un tableau est probablement plus facile dans votre cas (inutile de vous soucier de l'allocation de mémoire) et se situe dans le même ordre de performances qu'un tableau; Cependant, je pense qu'il est extrêmement important de maîtriser la notion de pointeurs en C++, car ils vous aident à comprendre comment la mémoire est organisée.

Voici la syntaxe pour créer un vecteur de pointeurs de joueur.

std::vector<Player*> players(1); // Creates a vector of pointer to player with length 1
players[0] = new Player(playerWidth, playerHeight, 20, 1); // Create a new player object
delete players[0];                                         // delete the player

Et la syntaxe pour créer un vecteur d'instances d'objet Player réelles (celle-ci est la solution la plus recommandée):

std::vector<Player> players(5); // Creates a vector of five player objects
players[0].playerWidth = 8; //already constructed, so we can edit immediately
//no cleanup required for the vector _or_ the players.
3
Porkbutts

En Java, vous faites Foo f = new Foo();, ce qui vous donne un objet alloué dynamiquement dont la durée de vie est gérée par le garbage collector.

Désormais, en C++, Foo* f = new Foo; a un aspect similaire et vous donne également un objet alloué de manière dynamique (auquel vous pouvez accéder via le pointeur f), mais C++ n’a pas de récupérateur de déchets intégré. Dans la plupart des cas, l'équivalent C++ fonctionnel est Foo f;, ce qui vous donne un objet local qui est détruit lorsque vous quittez la fonction en cours (via return ou throw).

Si vous avez besoin d'une allocation dynamique, utilisez des "pointeurs intelligents", qui sont en fait des classes qui se comportent comme des pointeurs. En C++ 98, il n'y a que std::auto_ptr et les gens utilisent souvent boost::shared_ptr pour le compléter. Dans le nouveau C++ 11, il y a std::unique_ptr et std::shared_ptr qui obtiennent le même résultat.

J'espère que cela vous donne quelques indications dans les directions où vous devez lire un peu, mais dans l'ensemble, Juanchopanza a donné un bon conseil: n'utilisez pas new à moins que vous n'ayez vraiment besoin. Bonne chance!

2
Ulrich Eckhardt

Ici, vous avez alloué de la mémoire pour stocker un tableau d’un lecteur (ce n’est pas vraiment utile, mais c’est une première étape).

Votre variable "players" stocke maintenant l'adresse du premier (et unique) emplacement de ce tableau . Ensuite, en accédant au premier joueur avec les joueurs [0], vous êtes en mesure d'écrire/lire directement dans sa mémoire. plus d'allocation est nécessaire.

0
Jelly