3

我想我对 C++ 相当了解,并且我正在考虑实现比“玩具”程序更大的东西。我知道堆栈内存和堆内存与 RAII 成语之间的区别。

假设我有一个简单的类

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

我总是在堆栈上分配点,因为对象很小。因为在 64 位机器上sizeof(point) == sizeof(void*),如果没有错,我会走得更远,默认情况下按值传递分数。

现在让我们假设一个更复杂的职业战场,我想在职业游戏中使用它:

class battlefield {
public:
    battlefield(int w, int h, int start_x, int start_y, istream &in) {
        // Complex generation of a battlefield from a file/network stream/whatever.
    }
};

因为我真的很喜欢 RAII 和对象离开范围时的自动清理,所以我很想在堆栈上分配战场。

game::game(const settings &s) :
        battlefield(s.read("w"), s.read("h"), gen_random_int(), gen_random_int(), gen_istream(s.read("level_number"))) {
    // ...
}

但是我现在有几个问题:

  • 由于这个类没有零参数构造函数,我必须在我使用战场的类的初始化列表中对其进行初始化。这很麻烦,因为我需要来自某个地方的 istream。这导致了下一个问题。

  • 复杂的构造函数有时会“滚雪球”。当我在游戏类中使用战场并在初始化列表中初始化游戏的构造函数时,游戏的构造函数也会变得相当复杂,游戏本身的初始化也可能变得繁琐。(当我决定将istream作为游戏构造函数的参数时)

  • 我需要辅助函数来填写复杂的参数。

我看到这个问题的两种解决方案:

  • 要么我为不初始化对象的战场创建一个简单的构造函数。但是这种方法的问题是我有一个半初始化的对象,也就是一个违反 RAII 习惯用法的对象。在这样的对象上调用方法时可能会发生奇怪的事情。

    game::game(const settings &s) {
        random_gen r;
        int x = r.random_int();
        int y = r.random_int();
        ifstream in(s.read("level_number"));
        in.open();
        this->battlefield.init(s.read("w"), s.read("h"), x, y, in);
        // ...
    } 
    
  • 或者我在游戏构造函数的堆上分配战场。但是我必须小心构造函数中的异常,并且我必须注意析构函数会删除战场

    game::game(const settings &s) {
        random_gen r;
        int x = r.random_int();
        int y = r.random_int();
        ifstream in(s.read("level_number"));
        in.open();
        this->battlefield = new battlefield(s.read("w"), s.read("h"), x, y, in);
        // ...
    } 
    

我希望你能看到我在想的问题。我遇到的一些问题是:

  • 我不知道这种情况是否有设计模式?

  • 大型 C++ 项目的最佳实践是什么?哪些对象在堆上分配,哪些在栈上分配?为什么?

  • 关于构造函数的复杂性的一般建议是什么?对于构造函数来说,从文件中读取太多了吗?(因为这个问题主要来自复杂的构造函数。)

4

2 回答 2

2

但是这种方法的问题是我有一个半初始化的对象,也就是一个违反 RAII 习惯用法的对象。

那不是RAII。这个概念是您使用对象来管理资源。当您获取堆内存、信号量、文件句柄等资源时,您必须将所有权转移给资源管理类。这就是 C++ 中智能指针的用途。unique_ptr如果您想拥有对象的唯一所有权,则必须使用或者如果shared_ptr您希望多个指针拥有所有权,则必须使用 a。

或者我在游戏构造函数的堆上分配战场。但是我必须小心构造函数中的异常,并且我必须注意析构函数会删除战场。

如果你的构造函数抛出一个异常,那么对象的析构函数就不会被调用,你最终可能会得到一个半熟的对象。在这种情况下,您必须记住在抛出异常之前在构造函数中进行了哪些分配,并解除所有这些分配。再次,智能指针将有助于自动清理资源。查看此常见问题解答

哪些对象在堆上分配,哪些在栈上分配?为什么?

尽可能在堆栈中分配对象。然后,您的对象仅在该块的范围内具有生命。如果您遇到不可能进行堆分配的情况 - 例如:您只知道运行时的大小,对象的大小太大而不能放在堆栈上。

于 2012-06-23T12:36:16.307 回答
2

你可以让你的战场从设置中构建:

explicit battlefield(const settings& s);

或者,为什么不为您创建一个工厂函数battlefield

例如

battlefield CreateBattlefield(const settings& s)
{
    int w = s.read("w");
    int h = s.read("w");
    std::istream& in = s.genistream();
    return battlefield(w, h, gen_random_int(), gen_random_int(), in);
}

game::game(const settings &s) :
    battlefield(CreateBattlefield(s)) {
    // ...
}
于 2012-06-23T12:45:28.953 回答