【问题标题】:How to allocate an object with a complex constructor?如何分配具有复杂构造函数的对象?
【发布时间】:2012-06-25 12:42:54
【问题描述】:

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

假设我有一个简单的类point

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

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

现在让我们假设一个更复杂的类battlefield,我想在类game中使用它:

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。这导致了下一个问题。

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

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

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

  • 我为 battlefield 创建一个简单的构造函数,它不会初始化对象。但是这种方法的问题是我有一个半初始化的对象,也就是一个违反 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 构造函数的堆上分配 battlefield。但我必须小心构造函数中的异常,我必须注意析构函数会删除 battlefield

    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++ 项目的最佳实践是什么?哪些对象在堆上分配,哪些在栈上分配?为什么?

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

【问题讨论】:

  • 顺便说一下,您在这里使用的术语“堆栈”具有误导性;当您创建一个对象作为另一个类的成员时,您不是在堆栈上分配它,而是在分配父对象的任何地方。如果battlefield 分配在堆上(C++ 术语中的“freestore”),那么它的所有子对象都将在堆上。
  • “因为在 64 位机器上 sizeof(point) == sizeof(void*), if a am not wrong” - 你错了; int 保证为至少 32 位类型,但它完全由实现定义。它也可以是 64 位的。此外,“堆栈”和“堆”在 C++ 标准中没有定义;您想提到“自动”和“动态”存储,因为堆栈和堆是实现细节。
  • "int 保证至少为 32 位" - 真的吗?

标签: c++ memory-management raii


【解决方案1】:

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

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

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

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

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

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

【讨论】:

    【解决方案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)) {
        // ...
    }
    

    【讨论】:

    • 所以基本上你说,如果我有一个特别复杂的构造函数,我应该尝试重构构造函数,如果这不可能,我应该编写一个工厂函数。当我假设battlefield 中有一个明确定义的移动构造函数时,我是否正确,您的工厂函数不会创建任何不必要的battlefield 副本?
    • 在前者的情况下,行为良好的移动构造函数不会受到影响。顺便说一句,你说你需要很多东西来创造你的战场。如果这些是并发活动,原则上,您可以将设置对象的创建视为强制连接点,然后再继续进行战场构建。在后者的情况下,您应该将您的构造函数设为私有,在这种情况下,您不能有明确定义的移动构造函数。将移动语义实现为工厂方法。
    猜你喜欢
    • 1970-01-01
    • 2020-03-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多