与c++中的对象数组混淆

Confused with object arrays in C++

本文关键字:数组 对象 c++      更新时间:2023-10-16

所以我首先学习Java,现在我正试图切换到c++。我有一点困难,使数组正确工作。

现在我只是想创建一个对象"Player"的数组,并填充它。但是我得到一个错误。

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

错误提示:操作数"="匹配这些操作数。操作数类型为:Player = Player *

我不明白为什么这不起作用?

错误的意思是您试图将错误类型的值分配给变量。当错误显示为Player = Player *时,这意味着左边的变量是Player,右边的值是Player *

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

问题类似于如果你要这样做:

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

左边和右边的类型不匹配,没有自然的转换,所以你会得到一个错误。


第一个问题是,你来自Java背景,Java使用指针很多,但对你隐藏它们。c++根本不隐藏它们。其结果是c++在显式处理指针时具有不同的语法。Java去掉了所有这些,主要使用c++中的常规非指针语法来处理指针。

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;

理解使用指针和直接使用对象之间的区别是非常重要的:

Java:                                  C++:
Player a = new Player();               Player *a = new Player();
Player b = a;                          Player *b = a;
b.foo();                               b->foo();

在这段代码中只有一个对象,你可以通过ab访问它,这没有什么区别,ab都是指向同一个对象的指针。

C++:
Player c = Player();
Player d = c;
d.foo();

在此代码中有两个对象。它们是不同的,对d做一些事情不会影响c

如果在Java中你学过像int这样的"原始"类型和像String这样的对象类型之间的区别,那么一种思考方式就是在c++中所有的对象都是原始的。如果我们回头看看你的代码,并使用这个"c++对象就像Java原语"的规则,你可能会更好地看到哪里出了问题:

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

这应该清楚地表明,赋值的右侧应该只是一个Player值,而不是一个新播放器对象的动态分配。对于java中的int,它看起来像players[0] = 100;。由于Java中的对象类型是不同的,Java没有一种方法来编写Object ,而您可以编写int值。但是c++可以;players[0] = Player(playerWidth, playerHeight, 20, 1);


第二个问题是C中的数组很奇怪,c++继承了它。

C和c++中的

指针允许'指针运算'。如果你有一个指向一个对象的指针,你可以对它做加法或减法,然后得到一个指向另一个对象的指针。Java没有类似的东西。

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

此外,数组索引操作符[]也可用于指针。x[5]相当于*(x+5)。这意味着指针可以用作数组,这在C和c++中是惯用的和预期的。事实上,它甚至融入了c++。

在c++中,当你使用new动态分配一个对象时,例如new Player,你通常会得到一个指向你指定类型的指针。在本例中,您得到Player *。但是当你动态分配一个数组时,例如new Player[5],情况就不同了。返回的不是指向5个Players的数组的指针,而是指向第一个元素的指针。这就像任何其他Player *:

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

这个指针的唯一不同之处在于,当你对它进行指针算术运算时,你得到的是指向有效Player对象的指针:

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

newdelete在没有保护的情况下很难正确使用。为了证明这一点:

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

这段代码容易出错,很可能是错误的。具体来说,如果foo()抛出异常,则x泄漏。

在c++中,当你获得一个职责时,比如当你调用new时,你获得了稍后调用delete的职责,你应该记住

R.A.I.I.
责任*获取是初始化

*人们更常说"资源获取是初始化",但资源只是一种责任。我是在Jon Kalb的一次异常安全c++演讲中被说服使用后一个术语的。

R.A.I.I.意味着无论何时你获得一个职责,它应该看起来像你在初始化一个对象;具体来说,你是在初始化一个特殊的对象,它的目的是为你管理那个职责。这种类型的一个例子是std::unique_ptr<int>,它将管理由new分配的指向int的指针:

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

要管理你的Player数组,你可以像这样使用std::unqiue_ptr:

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

现在unique_ptr将为您处理分配,您不需要自己调用delete。(注意:当你分配一个数组时,你应该给unique_ptr一个数组类型;std::unique_ptr<Player[]>,当你分配其他任何东西时,你使用一个非数组类型,std::unique_ptr<Player>)

当然,c++有一个更专门的用于管理数组的R.A.I.I.类型,std::vector,你应该更喜欢使用std::unique_ptr:

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

或者c++ 11中的

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

你的类型不匹配。毫无疑问,您试图将Player*存储到已经分配的Player中!

Player* players = new Player[1];

这将创建一个长度为1的数组,包含实例化的Player,并将整个内容存储到Player*中。players[0]的类型将是Player

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

这将尝试创建一个新的Player*并将其存储在数组中。但是该数组包含Player对象。你应该直接写

players[0] = Player(...)

或者,我猜这对你来说更合适,你应该完全停止使用new,而使用std::vector

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

不仅是更容易使用,但你也不必记得以后delete它。当std::vector超出作用域时,它将自动销毁。另外,不像你的数组,std::vector可以包含任意数量的对象,所以你可以随意添加新的球员或删除现有的球员。

也有其他数据结构可能更适合您,这取决于您的确切用途,但std::vector是一个很好的起点。

原因是,变量的类型

players[0]

是Player (object)。然而,操作符"new" (new Player)返回一个指针(Player*)

如果你想只有一个对象,正确的做法是:

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

不要忘记在c++中你需要自己清理这些乱七八糟的东西——在末尾某处调用

delete player;

为您创建的每个对象。c++没有垃圾收集器——这意味着所有手工创建的(通过"new")对象都会保留,直到你手动删除它们。

在Java中,当你使用关键字"new"时,你实际上得到了一个指向对象的指针。这是在Java中实例化对象类型的唯一方法。因此,当你在Java中说你有一个"对象数组"时,更正确的说法是你有一个指向对象的指针数组。

c++并不隐藏对象是指针的事实。可以用变量来引用对象,也可以用变量来引用指向对象的指针。

在您的示例中,您需要显式地将其声明为指向对象的指针数组。

Players **players = new (Player*)[1];                         // Create an array of player pointers
players[0] = new Player(playerWidth, playerHeight, 20, 1);    // Create a single player
虽然c++允许您使用关键字new显式地创建对象,但您必须确保在完成后清除对象,否则它们将永远不会被释放(称为内存泄漏)。

这是c++和Java的主要区别之一;Java的对象是垃圾收集的,程序员不必担心管理对象的生命周期。

一旦你完成了,你将需要清理你分配的单个播放器,以及数组。好的经验法则是,每次调用new应该对应于调用delete

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

然而,值得注意的是,与Java不同的是,对象是在堆上分配的,在c++中,您可以在堆栈上创建对象,就好像它们是基本类型(如int, float, char)一样。这允许您拥有局部作用域的对象,以及在内存中连续对齐的对象。在Java中没有办法做到这一点。

如果以这种方式分配对象数组,则为数组中的每个对象调用默认构造函数。

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:正如其他人提到的,在您的情况下,使用std::vector而不是数组可能更容易(无需担心内存分配),并且与数组具有相同的性能级别;然而,我认为在c++中熟悉指针的概念是非常重要的,因为它们可以帮助你理解内存是如何组织的。

下面是创建Player指针向量的语法。

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

创建实际玩家对象实例向量的语法(这是最受欢迎的解决方案):

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.

在Java中,您执行Foo f = new Foo();,为您提供一个动态分配的对象,其生命周期由垃圾收集器管理。

现在,在c++中,Foo* f = new Foo;看起来很相似,也给了你一个动态分配的对象(你可以通过指针f访问它),但是c++没有内置的垃圾收集器。在大多数情况下,函数式c++的等效函数是Foo f;,它为您提供了一个局部对象,当您离开当前函数(通过return或throw)时,该对象将被销毁。

如果你需要动态分配,使用"智能指针",它实际上是行为类似指针的类。在c++ 98中,只有std::auto_ptr,人们经常使用boost::shared_ptr来补充它。在较新的c++ 11中,std::unique_ptrstd::shared_ptr实现了相同的功能。

我希望这能给你一些你需要阅读的指示,但总的来说,Juanchopanza给出了一个很好的建议:除非你真的需要,否则不要使用new。好运!

这里你已经分配了一些内存来存储一个播放器的数组(不是很有用,但这是第一步)。

你的变量"players"现在存储了这个数组中第一个(也是唯一一个)插槽的地址。然后通过使用players[0]访问第一个Player,您可以直接在其内存中写入/读取,而不需要更多的分配。

对于索引0的值设置问题,我提出如下解决方案:

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