C++中对象间交换成员变量的正确方式

The right way of exchanging member variables between objects in C++

我被分配到我的大学工作。我的任务是编写一个贪吃蛇游戏,并从中获得乐趣并将其变成我自己的。除了必须用面向对象的方法编写之外,没有给我其他指示。我对 C++ 非常熟悉,但我缺乏一些实际经验。无论如何。我需要一个对象来访问其他对象的某些数据成员。例如,我有一个蛇对象,我需要它访问游戏中“水果”对象的坐标,以便蛇可以吃掉水果并生长。我知道有多种方法可以解决这个问题。我目前在我的代码中使用的一种方法是通过指向构造函数的指针传递成员。但是我不喜欢这种方法,因为代码很快就会变得混乱,而且过了一段时间后很难知道是什么。简单的解决方法是只使用全局变量,但是我不想再走那条路,因为我们正在处理一个大学项目,而且一般来说,据我所知,全局变量是不行的。将所有代码放在一个大 class 中也可以,但这会使代码更加混乱,并且首先否定使用 oop 的全部原因。我知道继承是一回事,但是据我了解,一旦您创建了从某个包含所有数据的基础 class 派生的不同对象,成员就不会受到任何影响,并且可以具有不同的值。我读过一些关于数据传输对象的内容,但显然它们也不是一个好主意。

好的,总结一下,我可以让我的代码工作,但我知道这可能不是最好的方法,我愿意学习如何更好或更正确地完成它。欢迎任何建议或想法。我想知道它在行业中是如何完成的;)。在此先感谢您的帮助。

您通常不会像那样公开 data 成员,而是提供对封装它们的对象的一些访问,以及允许读取信息的函数(并且,可能,写的)。

您会在面向对象编程的上下文中遇到“getter”和“setters”,但这些访问器不需要“瘦”:它们应该反映对象的逻辑属性class,它可能会也可能不会直接映射到单个数据成员。

所以:

class Fruit
{
public:
   Flavour GetFlavour() const;
   void SetFlavour(const Flavour newFlavour);

private:
   Flavour m_flavour;
};

int main()
{
   Fruit fruit;
   fruit.GetFlavour();
}

当然,现在我们仍然有同样的问题:如果我再添加一个 class,它如何才能“看到”水果以使用其闪亮的新访问器函数?

class Fruit
{
public:
   Flavour GetFlavour() const;
   void SetFlavour(const Flavour newFlavour);

private:
   Flavour m_flavour;
};

class Juicer
{
public:
   void UseFoodItem(const Fruit&);
};

int main()
{
   Fruit fruit;
   fruit.GetFlavour();
   
   Juicer juicer;
   juicer.useFoodItem(fruit);
}

看看我是如何在需要时将对水果的引用传递给榨汁机的?这很干净。这并不混乱,因为您不必担心谁拥有什么以及拥有多长时间。但是只能使用UseFoodItem里面的水果。如果您需要更持久的引用,以便其他函数也可以使用它怎么办?

事实证明,引用仍然是一个很好的方法。让我们将示例更贴近您的用例:

// Let's imagine you've defined this somewhere, and it contains
// both X and Y coordinates on a shared grid
class Location;

class Fruit
{
public:
   Location GetLocation() const;
   void MoveTo(const Location newLocation);

private:
   Location m_location;
};

class Snake
{
public:
   Snake(const Fruit& fruit) : m_fruit(fruit) {}
   void DoAThing();

private:
   const Fruit& m_fruit;
};

int main()
{
   Fruit fruit;
   fruit.MoveTo(Location{20, 15});
   
   Snake snake(fruit);
   snake.DoAThing();
}

(您是否注意到位置 setter 不仅仅是“SetLocation”?逻辑上,对水果执行的操作是 移动 它。事实上,这是通过改变它的“位置”来实现的,这是一个实现细节,所以我们把它隐藏在 class 的界面后面。)

在这里,DoAThing 是你的蛇在移动,或者你的游戏中发生的任何事情。关键是您可以从其中访问 fruit,也可以从您添加的任何其他函数访问。只要 main 中的 fruit 仍在范围内,这就是安全的。

使用智能指针可以使它更安全。 shared_ptr<Fruit> 可以在许多其他对象之间共享,而不会造成太多混乱:您会知道水果的存在是正确的,直到您的游戏中的其他任何东西都不再需要它为止。

综上所述,我不会让蛇知道水果的任何事情。我会把这个逻辑放在其他地方,在更高的层次上。我会让你的蛇被某个 function/class 控制,而你的水果由那个 class 拥有,并且在每一步它都应该检查蛇并检查水果,然后决定如果发生碰撞该怎么办被检测到。现在变得有点复杂了,但它看起来大概是这样的:

// Let's imagine you've defined this somewhere, and it contains
// both X and Y coordinates on a shared grid
class Location;

// Let's imagine you've defined this somewhere, and it represents
// an action a user can take. For example, "move the snake up" (which
// would likely be triggered by an "up arrow" keypress).

// Let's imagine you've defined this somewhere, and it blocks for
// user input then transforms that into an Action
Action AcceptUserInput();

class Fruit
{
public:
   Location GetLocation() const;
   void MoveTo(const Location newLocation);

private:
   Location m_location;
};

class Snake
{
public:
   Location GetLocation() const;
   void MoveTo(const Location newLocation);

private:
   Location m_location;
};

class Game
{
public:
   void SetSnakeInitialLocation(const Location snakeLocation)
   {
      m_snake.MoveTo(snakeLocation);
   }
   
   void AddFruit(const Location fruitLocation)
   {
      Fruit newFruit;
      newFruit.MoveTo(fruitLocation);
      
      m_fruits.push_back(newFruit);
   }
   
   void PerformAction(const Action action)
   {
      // If the action is to move the snake, do that by
      // calling things on m_snake, e.g. m_snake.MoveTo(newLocation)
      // Otherwise, do whatever needed.
      
      // Now, let's examine the state of the board. We have
      // all the information needed to do that, and don't have
      // to obtain it from anywhere else.
      for (Fruit& fruit : m_fruits)
      {
          if (m_snake.Intersects(fruit))
          {
             // Grow the snake; remove the fruit
             // 
             // (N.B. you can't use ranged-for like this if you're
             // going to erase from m_fruits, but that detail is out
             // of scope of this answer)
          }
      }
   }

private:
   std::vector<Fruit> m_fruits;
   Snake m_snake;
};

int main()
{
   Game game;
   game.AddFruit(Location{20, 15});
   game.SetSnakeInitialLocation(Location{0, 0});
   
   while (true)
   {
      Action nextAction = AcceptUserInput();
      game.PerformAction(nextAction);
   }
}

现在基本上根本没有共享状态:只有独立的对象和拥有大量对象的单个实体。你不希望“上帝对象”在你的程序中做和拥有绝对的一切,但你的游戏板确实拥有蛇和所有的水果,所以在这里将它们存储为它的成员是有意义的。

这不是最好的代码。它可能有一些拼写错误,对于游戏,您可能需要研究 访问者模式 这样的东西,这样 PerformAction 就不会成为 if 需要了解各种实体含义的知识。

此外,我很想保留 Game 中的位置,而不是将它们作为每个单独对象的属性。所以 Fruit 会有像它的味道这样的属性(如果你有不同种类的水果),但它的位置将完全由 Game 中的某个容器管理。您必须考虑如何表示风蛇的所有组成点。

但这些细节与我在这里试图展示的内容无关。