【问题标题】:How to best handle C++ object initialization: empty constructors or pointers?如何最好地处理 C++ 对象初始化:空构造函数或指针?
【发布时间】:2015-06-03 21:54:57
【问题描述】:

我想知道对于必须具有相对较大范围/较长生命周期的对象而言,进行对象初始化和存储的最佳方法是什么。假设我们有一个GameEngine 类,它需要初始化并保存对Window 的引用以进行渲染。在程序的整个生命周期中都需要引用,并且窗口至少需要知道它的尺寸。

在 Java 中,我会这样做:

// Declaration:
Window window;
// Initialization:
window = new Window(width, height);

我知道在 C++ 中,第一个已经调用了 Window 类的默认构造函数,因此是 declarationinitialization。因此,拥有window = Window(width, height); 将是assignment,丢弃已经存在的对象。

我能找到的第一个解决方案是使用指针

// GameEngine.hpp
class GameEngine {
    Window *window;
};

// Somewhere in GameEngine.cpp:
window = new Window(width, height);

但话又说回来,我经常读到一个应该尽可能偏爱普通对象而不是指针,事实上,我很快就让自己陷入了一堆乱七八糟的指针,所以我正在寻找另一种方法。

另一种解决方案似乎将您的对象设计为具有不带参数的构造函数并稍后设置对象

// GameEngine.hpp
class GameEngine {
    Window window;
};

// Somewhere in GameEngine.cpp
window.setWidth(width);
window.setHeight(height);

这可行,但有一个严重的缺点:对象(至少在这种情况下)可能处于不一致的状态,因为在不设置宽度/高度的情况下尝试显示窗口会导致错误或崩溃。它对某些对象有效,但对大多数对象无效。

避免这种情况的一种方法是使用默认值。例如,Window 类的构造函数可能如下所示

Window::Window(int width = 800, int height = 600) {}

甚至是这样

Window::Window() : width(DEFAULT_WIDTH), height(DEFAULT_HEIGHT) {}

但在很多情况下,默认值很难确定。另外,他们应该从哪里来? Window 类是否应该定义DEFAULT_WIDTHDEFAULT_HEIGHT或者我应该这样做吗?

// GameEngine.hpp
class GameEngine {
    static const int DEFAULT_WIDTH = 800;
    static const int DEFAULT_HEIGHT = 600;
    Window window(800,600);
};

但这似乎很糟糕,因为我已经读过您不应该在标头中进行任何初始化,只能在声明中进行,因此此时实际上不应该知道 DEFAULT_WIDTHDEFAULT_HEIGHT 的值(并且只能在 .cpp 中初始化,对吗?)。

我错过了一个选项吗? 或者在 C++ 中是否普遍假设程序员应该知道自己在做什么,并在使用对象之前确保对象处于一致状态? 何时使用哪种方法?

【问题讨论】:

  • @close 投票 - 对我来说,这看起来不像是基于意见的问题。虽然措辞有点糟糕(可能是由于缺乏理解),但问题实际上是关于 C++ 语言的技术问题。
  • 认为它是在询问如何进行两阶段初始化
  • 我觉得我错过了什么;为什么不能同时创建 Window 对象并对其进行初始化是有原因的吗?你需要声明和初始化在不同的地方吗?因为如果你不这样做,那么只需在声明语句中包含初始化就没有问题。
  • @MattMcNabb 呵呵。我认为问题是关于如何避免进行两阶段初始化。
  • @domdom 两阶段初始化意味着你在构建的时候初始化一些东西,然后再初始化一些东西。您正在描述初始化然后分配,这是实现两阶段的一种方式;但不是唯一的方法。我的回答显示了另一种不涉及分配的方式。这个问题没有单一的正确答案,这是一个通常基于经验的设计决定!所以我建议只是做一些事情,然后你会通过看看你的项目结果来了解它的利弊。

标签: c++ pointers object initialization declaration


【解决方案1】:

如果你只想构造一次并且可以在类的初始化中完成,那么你不需要指针。您可以将其声明为成员并在构造函数中对其进行初始化,如下所示:

HPP

class Game
{
    private:
        Window window_;

    public:
        Game(int, int);
}

CPP

Game::Game(int width, int height) : window_(width, height)
{
}

这将在您构造 Game 对象时构造窗口对象,并将持续存在直到 Game 对象被销毁。如果您希望以后能够构建它或随时重构它,请使用 std::unique_ptr ,如下所示:

HPP

class Game
{
    private:
       std::unique_ptr<Window> window_;

    public:
        Game(int, int);
        void SomeMethod(int, int);

}

CPP

Game::Game(int width, int height)
{
    window_ = std::make_unique<Window>(width, height);
}

Game::SomeMethod(int width, int height)
{
    window_ = std::make_unique<Window>(width, height);
}

这将在 Game 对象被销毁时自动删除窗口,并在每次调用 std::make_unique 构建新窗口时自动删除窗口。这是关于 unique_ptr 的一些文档: http://en.cppreference.com/w/cpp/memory/unique_ptr

【讨论】:

  • 我认为这实际上让我意识到我的概念错误:我认为拥有class GameEngine { Window window; } 已经调用了Window 的ctor,因为Window window; 会这样做,如果它在某些函数中发生这样的情况,对吧?但是这里没有调用ctor,我可以在GameEngine的成员初始化列表中初始化对象,这对于大多数情况来说确实是一个令人满意的解决方案。感谢您帮助解决初学者的困惑!
  • 正确,将某些东西声明为类的成员不会构造它。它会在构造类的时候被构造出来。
  • 还有一个问题;如果我Game 的ctor 中初始化window 会发生什么?它仍然是未初始化的还是编译器会调用Window的默认ctor?
  • 如果在类的初始化中没有为成员显式调用构造函数,那么它将尝试使用默认的无参数构造函数,如果不存在,则会出现编译错误。因此,如果 window 有一个无参数构造函数,那么它将被调用,否则它将不会编译
  • 啊,明白了。你帮了我很多,乔希。谢谢!
【解决方案2】:

您显然误解了 C++。您永远不会像标题中那样拥有Window window;。这定义了一个 Window 对象,每次包含标题时

您可能拥有class GameEngine { Window window; .... },但这实际上并没有创建一个窗口。每个 GameEngine 构造函数都有一个初始化列表,您可以在那里初始化window。有道理:游戏引擎会创建它需要的窗口。

【讨论】:

  • 抱歉,我显然试图使代码摘录简短。当然,Window window; 将在类定义中。我会相应地进行编辑。那么这将是正确的,对吧?
【解决方案3】:

如果你在谈论类成员,那么声明不是构造函数被调用的同一点。这些成员的初始化正是初始化列表(您似乎知道)的用途!

class Window {
   int x;
   int y;
 public:
   Window(int x, int y);
 };

class Game {
 Window window;
public:
 Game();
};

然后你可以像这样从游戏构造函数中调用窗口类的构造函数:

Game::Game() : window(DEFAULT_HEIGHT, DEFAULT_WIDTH) {}

如果您在谈论全局对象:如果您真的需要一个全局对象(尽管您可能不想要),您可以(并且应该!)在标题中声明具有外部链接的对象(这只会使名称可用,但不调用任何构造函数)并在实现中进行定义:

声明:

extern Window window;

实施:

Window window(DEFAULT_WIDTH, DEFAULT_HEIGHT);

【讨论】:

  • 谢谢。您的第一句话几乎概括了让我感到困惑的地方;我认为类成员声明会触发他们的构造函数。如果可以的话,我也会将您的答案标记为已接受,但我必须选择一个。 :\
【解决方案4】:

理想情况下,您应该设计您的类,以便您需要的所有初始化都可以在构造函数中进行。 Example here.

但这并不总是可能的(例如,如果您想要一个在游戏期间发生某些特定事件之前不会创建的窗口);或者,作为一个新程序员,你可能很难理解。


一种方法是使用指针 - 但使用智能指针而不是原始指针。

如果您的类需要包含一些对象句柄,但您还没有准备好创建对象,那么您可以拥有一个类成员:

std::unique_ptr<Window> p_window;

那么当你准备好创建窗口时,就可以执行代码了:

p_window.reset( new Window(bla bla bla) );

智能指针负责在其包含对象被销毁时调用delete,如果您不小心尝试执行“浅拷贝”,它会给出编译错误。

要在指针指向某个地方时使用它,您可以写p_window-&gt;bla...,并检查它是否已被分配,但您可以使用if ( p_window )

【讨论】:

  • 也谢谢你。我想我现在明白我的误解是什么了。对不起我初学者的困惑。再次感谢您的意见。 :)
【解决方案5】:

您缺少的选项是在创建 Window 对象时对其进行初始化。在知道如何初始化它们之前,不要在函数中声明 Window 对象。如果您有一个具有Window 成员的对象,请让该对象的构造函数初始化Window 成员。

如果创建对象的时间确实不确定,或者您确实需要在准备创建有效对象之前为其声明一个变量,那么指针是正确的做法。

您提到的建议的重点不是改变您设计对象的方式,而是您需要重新考虑使用对象的方式:您需要忘掉从所有事情中养成的习惯-is-a-pointer 编程环境,如 Java。

(尽管您应该使用智能指针,如 unique_ptrshared_ptr 视情况而定)

(另外,如果您认为一个类几乎总是需要与指针一起使用,那么在指针周围创建一个包装类是很有用的,它就像一个“普通对象”,即使它内部是用一个指针实现的)

【讨论】:

  • 是的,我认为来自 Java 的人在尝试使用 C++ 时确实会导致一些混乱。我比我想象的更挣扎。不过,它正在慢慢融合。感谢您的输入。 :)
猜你喜欢
  • 2023-03-14
  • 2015-11-05
  • 1970-01-01
  • 2012-03-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-07-03
  • 1970-01-01
相关资源
最近更新 更多