[英]new 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);
}
对于具有复制语义的类来实现它是令人恐惧的。 理想情况下,复制可能会抛出,分配可能会抛出,并且我们需要事务语义。 一个例子将使这个答案破灭。
好啦
我们有这个无辜的外表。 事实证明,这是非常邪恶的。 我想起了美国麦基的爱丽丝:
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 ++程序员认识到使用了错误的delete-operator。 解决这个问题:
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个僵尸Bars
。 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++
以获取有关此技术和编写异常安全代码的更多详细信息。
不能抛出或不允许抛出什么的摘要:
最后是我们精心设计的,异常安全的,经过纠正的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_);
}
将其与我们最初的,看起来很无辜的代码相形见evil:
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]
的唯一原因是在实现标准前向量类时。 (我自己分开的分配和初始化,就像标准库中的容器一样,但是对于一个非常简单的向量类来说,这可能会显得过分杀手。)
尽管这两种方法都分配了动态内存,但一个方法是处理任意长度的数据( std::vector<T>
)的对象,而另一个方法只是指向大小为N
( int
s)的存储插槽的顺序行的指针。这个案例)。
除其他差异外
如果您尝试附加新值并且空间不足,则std::vector<T>
会自动为数据分配大小的内存。 int *
不会。
当向量超出范围时, std::vector<T>
将释放分配的内存,而int *
不会。
一个int *
几乎没有开销(与向量相比),尽管std::vector<T>
并不完全是新事物,并且通常已经非常优化。 您的瓶颈可能在其他地方。
但是std::vector<int>
总是比int *
消耗更多的内存,并且某些操作将总是花费更多的时间。
因此,如果存在内存/ CPU限制,并且希望减少每个周期,则可以使用int *
。
在某些情况下,一个绝对比另一个更可取!
当您需要“原始” /“真实”内存并对其进行完全控制时, operator new
是您的最佳选择。
例如,当您使用placement new
。
通过原始/真实内存,我的意思是某些没有通过包装容器管理的东西,例如std::vector<T>
。
当您正在寻找一个可以处理任意内容的容器并且不想重新发明有关内存管理的方法时; std::vector<T>
(或任何其他适当的STL容器)
谁能给我建议哪个更好?
向量更好
...
手动内存管理很容易并且经常导致内存泄漏,甚至更糟的是,行为不确定。
在内部,向量做的完全相同,也将注意内存释放。 因此,没有任何理由使用new运算符。 std :: vector是c ++的一部分,它是标准的,经过测试且安全的,如果您有一些标准的工作方式,请不要使用原始指针。
如果需要动态调整大小的对象序列,请使用向量。 如果需要分配原始内存并自己管理该内存,请分配内存。 您会发现有时矢量更有用,而在其他时候,平面内存缓冲区会更好。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.