new int [size] vs std :: vector

var*_*wal 14 c++ memory vector dynamic

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

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

Seb*_*ach 33

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

手动分配时,必须保证释放代码的执行,并且作为成员,您必须编写正确的副本分配和复制构造函数,这样做是正确的,不会在发生异常时泄漏.

手册:

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

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

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;
Run Code Online (Sandbox Code Playgroud)

要不就:

std::vector<int> i(64), y(64);
{
    std::vector<int> p(64);
}
Run Code Online (Sandbox Code Playgroud)

对于具有复制语义的类来实现它是一种恐怖.复制可能会抛出,分配可能会抛出,理想情况下我们需要事务语义.一个例子会破解这个答案.


好的,这里.

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

我们有这个无辜的上课.事实证明,这是非常邪恶的.我想起了美国人麦吉的爱丽丝:

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

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

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

未定义的行为.中级C++程序员认识到使用了错误的delete-operator.解决这个问题:

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

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

稍微有经验的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_;
};
Run Code Online (Sandbox Code Playgroud)

正确....如果您可以保证永远不会耗尽内存,并且Bar和Frob都无法复制失败.乐趣从下一节开始.

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

施工

Foo() : b_(new Bar[64]), f_(new Frob[64]) {}
Run Code Online (Sandbox Code Playgroud)
  • 问:如果初始化f_失败会怎样?
  • 答:所有Frobs已建成的都被销毁了.想象20 Frob建造,21世纪将失败.在LIFO命令中,前20个Frob将被正确销毁.

而已.意思是:你Bars现在有64个僵尸.在Foos本身还没有出现,生活中的对象,它的析构函数将因此无法调用.

如何使这个例外安全?

构造函数应始终完全成功或完全失败.它不应该是半生或半死.解:

Foo() : b_(0), f_(0)
{
    try {    
        b_ = new Bar[64];
        f_ = new Foo[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
    }
}
Run Code Online (Sandbox Code Playgroud)

仿形

记住我们的复制定义:

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;
}
Run Code Online (Sandbox Code Playgroud)
  • 问:如果任何副本失败会怎样?也许Bar将不得不复制重要资源.它会失败,它会失败.
  • 答:在异常的时间点,到目前为止复制的所有对象都将保持不变.

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

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

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

Foo (Foo const &f) : f_(0), b_(0) { 
    try {    
        b_ = new Bar[64];
        f_ = new Foo[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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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_);
}
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

发生了什么?首先,我们建立新的存储并将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 Foo[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 Foo[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_);
}
Run Code Online (Sandbox Code Playgroud)

将它与我们最初的,无辜的代码相比较,这对于骨头来说是邪恶的:

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

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

或者使其不可复制.

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_;
};
Run Code Online (Sandbox Code Playgroud)

对于某些类来说,这是有意义的(对于一个实例来说是流;要共享流,要用std :: shared_ptr显式),但对于许多类而言,它不是.

真正的解决方案.

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

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


Mik*_*our 15

几乎在任何情况下,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
Run Code Online (Sandbox Code Playgroud)

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

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

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