新的 int\[size\] 与 std::vector

新手上路,请多包涵

为了分配动态内存,我一直在 C++ 中使用向量。但是最近,在阅读一些源代码时,我发现了“new int[size]”的用法,并在一些研究中发现它也分配了动态内存。

谁能给我建议哪个更好?我从算法和 ICPC 的角度来看?

原文由 varagrawal 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 400
2 个回答

总是 喜欢 标准容器。它们具有明确定义的复制语义,异常安全,并且可以正确释放。

当你手动分配时,你必须保证发布代码被执行,并且作为成员,你必须编写正确的复制赋值和复制构造函数,在发生异常的情况下做正确的事情而不泄漏。

手动的:

 int *i = 0, *y = 0;
try {
    i = new int [64];
    y = new int [64];
} catch (...) {
    delete [] y;
    delete [] i;
}

如果我们希望我们的变量只有它们真正需要的范围,它就会变得很臭:

 int *i = 0, *y = 0;
try {
    i = new int [64];
    y = new int [64];
    // code that uses i and y
    int *p;
    try {
        p = new int [64];
        // code that uses p, i, y
    } catch(...) {}
    delete [] p;
} catch (...) {}
delete [] y;
delete [] i;

要不就:

 std::vector<int> i(64), y(64);
{
    std::vector<int> p(64);
}

对于具有复制语义的类来说,实现它是一件很可怕的事情。复制可能会抛出,分配可能会抛出,理想情况下,我们需要事务语义。一个例子会打破这个答案。


好的。

可复制类 - 手动资源管理与容器

我们有这个看起来很无辜的班级。事实证明,这是非常邪恶的。我想起了美国麦基的爱丽丝:

 class Foo {
public:
    Foo() : b_(new Bar[64]), f_(new Frob[64]) {}
private:
    Bar  *b_;
    Frob *f_;
};

泄漏。大多数初学者 C++ 程序员都认识到缺少删除。添加它们:

 class Foo {
public:
    Foo() : b_(new Bar[64]), f_(new Frob[64]) {}
    ~Foo() { delete f_; delete b_; }
private:
    Bar  *b_;
    Frob *f_;
};

未定义的行为。中级 C++ 程序员认识到使用了错误的删除操作符。解决这个问题:

 class Foo {
public:
    Foo() : b_(new Bar[64]), f_(new Frob[64]) {}
    ~Foo() { delete [] f_; delete [] b_; }
private:
    Bar  *b_;
    Frob *f_;
};

如果类被复制,糟糕的设计、泄漏和双重删除就会潜伏在那里。复制本身很好,编译器干净地为我们复制了指针。但是编译器不会发出代码来创建 数组 的副本。

稍微有经验的 C++ 程序员认识到三法则没有得到尊重,这意味着如果您明确编写了析构函数、复制赋值或复制构造函数中的任何一个,您可能还需要写出其他的,或者在没有实现的情况下将它们设为私有:

 class Foo {
public:
    Foo() : b_(new Bar[64]), f_(new Frob[64]) {}
    ~Foo() { delete [] f_; delete [] b_; }

    Foo (Foo const &f) : b_(new Bar[64]), f_(new Frob[64])
    {
        *this = f;
    }
    Foo& operator= (Foo const& rhs) {
        std::copy (rhs.b_, rhs.b_+64, b_);
        std::copy (rhs.f_, rhs.f_+64, f_);
        return *this;
    }
private:
    Bar  *b_;
    Frob *f_;
};

正确的。 … 前提是您可以保证永远不会耗尽内存,并且 Bar 和 Frob 都不会在复制时失败。乐趣从下一节开始。

编写异常安全代码的仙境。

建造

Foo() : b_(new Bar[64]), f_(new Frob[64]) {}

  • Q:如果 f_ 初始化失败怎么办?
  • A:所有已构建的 Frobs 都被销毁。想象一下 20 Frob 被构建了,第 21 个将失败。然后,按照 LIFO 顺序,前 20 个 Frob 将被正确销毁。

而已。意思是:你现在有 64 个僵尸 BarsFoos 对象本身永远不会复活,因此不会调用它的析构函数。

如何使这个异常安全?

构造函数应该总是 完全成功或完全 _失败_。它不应该是半生半死的。解决方案:

 Foo() : b_(0), f_(0)
{
    try {
        b_ = new Bar[64];
        f_ = new Frob[64];
    } catch (std::exception &e) {
        delete [] f_; // Note: it is safe to delete null-pointers -> nothing happens
        delete [] b_;
        throw; // don't forget to abort this object, do not let it come to life
    }
}

复印

记住我们对复制的定义:

 Foo (Foo const &f) : b_(new Bar[64]), f_(new Frob[64]) {
    *this = f;
}
Foo& operator= (Foo const& rhs) {
    std::copy (rhs.b_, rhs.b_+64, b_);
    std::copy (rhs.f_, rhs.f_+64, f_);
    return *this;
}

  • 问:如果任何副本失败会怎样?也许 Bar 将不得不在后台复制大量资源。它可能会失败,它会。
  • A:在异常的时间点,到目前为止复制的所有对象都将保持原样。

这意味着我们的 Foo 现在处于不一致和不可预测的状态。为了赋予它事务语义,我们需要完全或不完全建立新状态,然后使用无法抛出的操作将新状态植入我们的 Foo 中。最后,我们需要清理临时状态。

解决方案是使用复制和交换习语( http://gotw.ca/gotw/059.htm )。

首先,我们改进我们的复制构造函数:

 Foo (Foo const &f) : f_(0), b_(0) {
    try {
        b_ = new Bar[64];
        f_ = new Frob[64];

        std::copy (rhs.b_, rhs.b_+64, b_); // if this throws, all commited copies will be thrown away
        std::copy (rhs.f_, rhs.f_+64, f_);
    } catch (std::exception &e) {
        delete [] f_; // Note: it is safe to delete null-pointers -> nothing happens
        delete [] b_;
        throw; // don't forget to abort this object, do not let it come to life
    }
}

然后,我们定义一个非抛出的交换函数

class Foo {
public:
    friend void swap (Foo &, Foo &);
};

void swap (Foo &lhs, Foo &rhs) {
    std::swap (lhs.f_, rhs.f_);
    std::swap (lhs.b_, rhs.b_);
}

现在我们可以使用我们新的异常安全复制构造函数和异常安全交换函数来编写异常安全复制赋值运算符:

 Foo& operator= (Foo const &rhs) {
    Foo tmp (rhs);     // if this throws, everything is released and exception is propagated
    swap (tmp, *this); // cannot throw
    return *this;      // cannot throw
} // Foo::~Foo() is executed

发生了什么?首先,我们建立新的存储并将 rhs’ 复制到其中。这可能会抛出,但如果发生了,我们的状态不会改变并且对象仍然有效。

然后,我们用临时的胆量交换我们的胆量。临时获得不再需要的东西,并在范围结束时释放这些东西。我们有效地将 tmp 用作垃圾箱,并正确选择 RAII 作为垃圾收集服务。

您可能想查看 http://gotw.ca/gotw/059.htm 或阅读 Exceptional C++ 以了解有关此技术和编写异常安全代码的更多详细信息。

把它放在一起

不能扔或不准扔的总结:

  • 复制原始类型 永远不会抛出
  • 不允许抛出析构函数(否则,异常安全代码根本不可能)
  • 交换 函数不应抛出**(C++ 程序员以及整个标准库都希望它不会抛出)

最后是我们精心制作的、异常安全的、更正后的 Foo 版本:

 class Foo {
public:
    Foo() : b_(0), f_(0)
    {
        try {
            b_ = new Bar[64];
            f_ = new Frob[64];
        } catch (std::exception &e) {
            delete [] f_; // Note: it is safe to delete null-pointers -> nothing happens
            delete [] b_;
            throw; // don't forget to abort this object, do not let it come to life
        }
    }

    Foo (Foo const &f) : f_(0), b_(0)
    {
        try {
            b_ = new Bar[64];
            f_ = new Frob[64];

            std::copy (rhs.b_, rhs.b_+64, b_);
            std::copy (rhs.f_, rhs.f_+64, f_);
        } catch (std::exception &e) {
            delete [] f_;
            delete [] b_;
            throw;
        }
    }

    ~Foo()
    {
        delete [] f_;
        delete [] b_;
    }

    Foo& operator= (Foo const &rhs)
    {
        Foo tmp (rhs);     // if this throws, everything is released and exception is propagated
        swap (tmp, *this); // cannot throw
        return *this;      // cannot throw
    }                      // Foo::~Foo() is executed

    friend void swap (Foo &, Foo &);

private:
    Bar  *b_;
    Frob *f_;
};

void swap (Foo &lhs, Foo &rhs) {
    std::swap (lhs.f_, rhs.f_);
    std::swap (lhs.b_, rhs.b_);
}

将其与我们最初的、看起来天真烂漫的代码进行比较:

 class Foo {
public:
    Foo() : b_(new Bar[64]), f_(new Frob[64]) {}
private:
    Bar  *b_;
    Frob *f_;
};

您最好不要向其中添加更多变量。迟早,你会忘记在某个地方添加正确的代码,你的整个班级都会生病。

或使其不可复制。

 class Foo {
public:
    Foo() : b_(new Bar[64]), f_(new Frob[64]) {}
    Foo (Foo const &) = delete;
    Foo& operator= (Foo const &) = delete;
private:
    Bar  *b_;
    Frob *f_;
};

对于某些类,这是有道理的(例如流;要共享流,用 std::shared_ptr 明确表示),但对于许多类来说,它没有。

真正的解决方案。

 class Foo {
public:
    Foo() : b_(64), f_(64) {}
private:
    std::vector<Bar>  b_;
    std::vector<Frob> f_;
};

此类具有干净的复制语义,是异常安全的(请记住:异常安全并不意味着不抛出,而是不泄漏并且可能具有事务语义),并且不会泄漏。

原文由 Sebastian Mach 发布,翻译遵循 CC BY-SA 4.0 许可协议

在几乎任何情况下, std::vector 都是可取的。它有一个析构函数来释放内存,而手动管理的内存必须在完成后显式删除。很容易引入内存泄漏,例如,如果某些东西在被删除之前抛出异常。例如:

 void leaky() {
    int * stuff = new int[10000000];
    do_something_with(stuff);
    delete [] stuff; // ONLY happens if the function returns
}

void noleak() {
    std::vector<int> stuff(10000000);
    do_something_with(stuff);
} // Destructor called whether the function returns or throws

如果您需要调整数组大小或复制数组,它也更方便。

偏爱原始数组的唯一原因是如果您有极端的性能或内存限制。 vector 是比指针更大的对象(包含大小和容量信息);它有时会对其对象进行值初始化,而原始数组将默认初始化它们(对于普通类型,这意味着它们未初始化)。

在这些问题可能很重要的极少数情况下,您应该考虑 std::unique_ptr<int[]> ;它有一个析构函数,可以防止内存泄漏,并且与原始数组相比没有运行时开销。

原文由 Mike Seymour 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题