c - 为什么C 程序员要尽可能减少使用'new' ?

  显示原文与译文双语对照的内容

我偶然发现内存泄漏 std::string 当使用 std::list <std::string>, 一个评论说:

停止使用 new 。 我看不出你在任何地方使用新的。 你可以在 C++ 中通过值创建对象,这是使用语言的巨大优势之一。 你不必在堆栈上分配所有内容。 停止像Java程序员一样思考。

我不确定他是什么意思。 有人能澄清为什么对象应该尽可能在 C++ 创造的价值,和内部会发生什么样的事呢? 如果我误解了答案,可以自由地解释。

时间:

有两种widely-used内存分配技术: 自动分配和动态分配。 通常,每一个都有对应的内存区域: 堆栈和堆。

堆栈

堆栈总是以顺序方式分配内存。 它可以这样做,因为它要求你以相反的顺序释放内存( First-In,Last-Out: 这是许多编程语言中的局部变量的内存分配技术。 它非常快,因为它需要最小的簿记,而下一个要分配的地址是隐式的。

在 C++ 中,这被称为自动存储,因为存储在作用域结束时被自动声明。 一旦执行完当前代码块( 使用 {} 分隔),就会自动收集该块中所有变量的内存。 这也是调用析构函数来清理资源的时刻。

堆允许更灵活的内存分配模式。 簿记更加复杂,分配速度较慢。 由于没有隐式释放点,你必须使用 delete 或者 delete[] ( free 中的) 手动释放内存。 然而,缺少一个隐式的释放点是堆灵活性的关键。

使用动态分配的原因

即使使用堆速度较慢,可能导致内存泄漏或者内存碎片,动态分配也有完美的使用情况,因为它的限制很小。

使用动态分配的两个主要原因:

  • 你不知道在编译时需要多少内存。 例如在将文本文件读入字符串时,通常不知道文件的大小,所以在运行程序之前,你不能决定要分配多少内存。

  • 你想要分配将在离开当前块后保留的内存。 例如你可能想要编写一个函数 string readfile(string path) 来返回文件的内容。 在这种情况下,即使堆栈可以保存整个文件内容,你也不能从函数返回,并保留分配的内存块。

为什么动态分配常常是不必要

在 C++ 中,有一个很整齐的构造称为函数 。 这里机制允许你通过将资源的生命周期与变量的生命周期进行对齐来管理资源。 它通常用于"换行"资源到另一个对象。 std::string 是一个完美的例子。 这里 Fragment:


int main ( int argc, char* argv[] )
{
 std::string program(argv[0]);
}

实际上分配一个可变的内存量。 std::string 对象使用堆分配内存并在它的析构函数中释放它。 在这种情况下,你没有仍然需要手动管理任何资源和动态内存分配的好处。

特别是,它意味着在这个Fragment中:


int main ( int argc, char* argv[] )
{
 std::string * program = new std::string(argv[0]);
 delete program;
}

不需要的动态内存分配。 程序需要更多的输入( ),并引入忘记释放内存的风险。 它没有明显的好处。

为什么要尽可能多地使用自动存储

基本上,最后一段总结了它。 尽可能多地使用自动存储使你的程序:

  • 类型更快速;
  • 运行时速度更快;
  • 不容易出现内存/资源泄漏。

加分点数

在引用的问题中,还有其他问题。 特别是以下类:


class Line {
public:
 Line();
 ~Line();
 std::string* mString;
};

Line::Line() {
 mString = new std::string("foo_bar");
}

Line::~Line() {
//mString->clear();//should not be neccessary
 delete mString;
}

实际使用的风险比下面的要大得多:


class Line {
public:
 Line();
 std::string mString;
};

Line::Line() {
 mString ="foo_bar";
//note: there is a cleaner way to write this.
}

原因是 std::string 正确定义了一个复制构造函数。 请考虑以下程序:


int main ()
{
 Line l1;
 Line l2 = l1;
}

使用原始版本,这个程序可能会崩溃,因为它在同一个字符串上使用了 delete 两次。 使用修改后的版本,每个 Line 实例都拥有自己的字符串实例,每个实例都有自己的内存,并且在程序结束时都会释放。

其他笔记

由于上原因,广泛使用 newport web被认为是 C++的最佳实践。 然而,还有一个额外的好处不是立即明显。 基本上,它比它的部分的总和好。 整个组成机制。 它的比例。

如果使用 Line 类作为构建块:


 class Table
 {
 Line borders[4];
 };

然后


 int main ()
 {
 Table table;
 }

分配四个 std::string 实例,四个 Line 实例,一个 Table 实例,所有的内容字符串和一切都是自动释放。

它很复杂。

首先,C++ 不是垃圾收集。 因此,对于每个新的,必须有相应的删除。 如果你无法放入这里删除,那么你就有内存泄漏。 现在,对于这样一个简单的例子:


std::string *someString = new std::string(...);
//Do stuff
delete someString;

这很简单,但是如果"做东西"抛出异常会发生什么? 糟糕:内存泄漏。如果"做东西"问题早于 return,会发生什么? 糟糕:内存泄漏。

这是最简单的 。 如果你碰巧把那个字符串返回给某人,现在他们必须删除它。 如果他们作为一个参数传递它,接收它的人是否需要删除它? 他们应该什么时候删除它?

或者,你可以这样做:


std::string someString(...);
//Do stuff

没有 delete 。对象是在"堆栈"上创建的,它将在超出范围后被销毁。 你甚至可以返回对象,从而将它的内容传递给调用函数。 你可以将对象传递给函数( 通常作为引用或者 const-reference: void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis) 等等。

没有 newdelete 。 没有问题是谁拥有内存或者谁负责删除它。 如果你:


std::string someString(...);
std::string otherString;
otherString = someString;

据悉, otherStringsomeString 数据的一个副本。 它不是指针;它是一个单独的对象。 它们可能具有相同的内容,但你可以更改其中一个而不影响其他内容:


someString +="More text.";
if(otherString == someString) {/*Will never get here */}

看看这个想法?

new 创建的对象最终必须为 delete,以免它们泄漏。 析构函数将不会被调用,内存将不会被释放,整个位。 因为 C++ 没有垃圾收集,所以它是一个问题。

由值创建的对象( 。 在堆栈溢出时自动终止。 编译器插入析构函数调用,函数返回时内存是 auto-freed 。

auto_ptrshared_ptr 之类的智能指针解决了悬空引用问题,但是它们需要编码规程并有其他问题。

此外,在多线程场景中,new 是线程之间的争用点;可能会对过度使用 new 造成性能影响。 堆栈对象创建是根据定义 thread-local,因为每个线程都有自己的堆栈。

值对象的缺点是一旦宿主函数返回,它们就会消亡- 你不能将引用传递给调用方,只通过复制或者返回值。

在很大程度上,有人将他们自己的弱点提升到一个通用规则。 本身没有错使用 new 操作符创建对象。 有一些理由是什么,你必须这样做一些纪律: 如果你创建一个对象,你需要确保它将被破坏。

最简单的方法是创建自动存储的对象,所以 C++ 知道摧毁它当它超出范围:


 {
 File foo = File("foo.dat");

//do things

 }

现在,当你观察块end-brace后脱落, foo 范围。 C++ 将自动为你调用 dtor 。 Java不同,你不需要等待GC找到它。

你写了


 {
 File * foo = new File("foo.dat");

你将希望与它显式匹配


 delete foo;
 }

或者更好,把你的File * 分配为一个"智能指针"。 如果你不小心,它可能会导致泄漏。

答案本身是错误的假设,如果你不使用 new 你不分配在堆上;事实上,在 C++ 你不知道。 最多,你知道一小部分的内存,一个指针,当然是在堆栈上分配。 但是,如果文件的实现类似于


 class File {
 private:
 FileImpl * fd;
 public:
 File(String fn){ fd = new FileImpl(fn);}

然后 FileImpl 仍然会在堆栈上分配。

是的,你最好确保


 ~File(){ delete fd ; }

在班上,没有它,你会从堆中内存泄漏,即使你没有显然分配在堆上。

当你使用new时,对象被分配给堆。 当你预计扩展时,通常使用它。 当你声明一个对象,例如

 
Class var;

 

它放在堆栈上。

你将总是调用你放置在堆上的新对象。 这将打开内存泄漏的可能性。 放置在堆栈上的对象不容易出现内存泄漏 !

new() 不应该用作 。 应尽可能小心地使用 。 并且应该尽可能多地使用,根据实用主义的要求。

在堆栈上的对象分配依赖于它们的隐式破坏,是一个简单的模型。 如果对象所需的作用域符合该模型,那么就不需要使用 new(),使用关联的delete() 和检查空指针。 如果你有大量短期对象在堆栈上分配,那么应该减少堆碎片的问题。

但是,如果对象的生命周期需要扩展到当前范围之外,那么 new() 就是正确的答案。 请确保你注意到什么时候以及如何调用 delete() 以及如何使用已经删除的对象以及使用指针的所有其他陷阱的可能性。

我觉得海报应该说 You do not have to allocate everything on theheap 而不是 stack

主要对象是在堆栈上分配( 如果对象大小允许,当然) 因为stack-allocation的廉价的成本,而不是heap-based分配器分配涉及相当一些工作,然后添加冗长,因为你必须管理数据分配在堆上。

我倾向于使用新"太多"的思想。 尽管最初使用系统类的海报的使用有点荒谬。 ( int *i; i = new int[9999]; ?真的? int i[9999]; 更清晰)。我认为就是评论者的山羊。

当你使用系统对象时,你需要对完全相同的对象有多个引用,这是非常的。 只要值是一样的,那就是所有的事情。 系统对象通常在内存中占用的空间不多。 ( 每个字符一个字节,在一个字符串中) 。 如果他们这么做了,那么这些库应该被设计为将内存管理考虑到( 如果写得好的话) 。 在这些情况下,( 除了他的代码中的一个或者两个新闻) 实际上是无意义的,仅用于引入混淆和潜在的Bug 。

当你使用自己的类/对象时( 例如。 原始的海报类,那么你必须开始考虑内存足迹,数据持久性,等等 本身的问题。 在这一点上,允许多个引用相同的价值是无价的,它允许结构如链表、字典、和图表,在多个变量不仅需要有相同的值,但参考相同对象在内存中。 但是,行类没有任何这些要求。 所以,原来的海报代码实际上完全不需要 new

...