新的int [size] vs std :: vector

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

任何人都可以给我哪些更好的建议? 我从算法和ICPC的角度来看待?


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

当你手动分配时,你必须保证释放代码被执行,并且作为成员,你必须编写正确的拷贝分配和拷贝构造函数,在异常情况下做正确的事情而不会泄漏。

手册:

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);
}

对于具有复制语义的类来说实现它是一种恐怖。 复制可能抛出,分配可能抛出,并且我们希望事务语义理想。 一个例子会爆发这个答案。


好的在这里。

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

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

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]) {}
  • 问:如果f_的初始化失败会发生什么?
  • 答:所有已建成的Frobs都被销毁。 想象一下,20 Frob被构建,第21将失败。 在LIFO命令中,前20个Frob将被正确销毁。
  • 而已。 意思是:你现在有64个僵尸BarsFoos对象本身永远不会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
        }
    }
    

    仿形

    请记住我们的复制定义:

    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必须在引擎盖下复制沉重的资源。 它可能会失败,它会。
  • 答:在例外的时间点,到目前为止复制的所有对象都将保持如此。
  • 这意味着我们的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
        }
    }
    

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

    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 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_);
    }
    

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

    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_;
    };
    

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


    几乎在任何情况下, 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[]> ; 它有一个析构函数可以防止内存泄漏,并且与原始数组相比,没有运行时间开销。


    我不认为有new int[size]是最好的情况。 你有时会看到它在预先标准的代码中,但即使如此,我认为它不是一个好的解决方案; 在标准日之前,如果你的工具包中没有与std::vector等价的东西,那么你写了一个。 您可能想要使用new int[size]的唯一原因是实现了预标准的向量类。 (我自己分开的分配和初始化,就像标准库中的容器一样,但这对于一个非常简单的向量类来说可能是矫枉过正的。)

    链接地址: http://www.djcxy.com/p/40659.html

    上一篇: new int[size] vs std::vector

    下一篇: Memory leak with std::string when using std::list<std::string>