为什么C++程序员应该尽量减少"新"的使用?

bit*_*den 833 c++ heap memory-management c++-faq new-operator

在使用std :: list <std :: string>时偶然发现Stack Overflow问题内存泄漏与std :: string,其中一条评论说:

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

我不太确定他的意思是什么.为什么要尽可能经常地用C++中的值创建对象,它在内部有什么区别?我误解了答案吗?

And*_*ron 989

有两种广泛使用的内存分配技术:自动分配和动态分配.通常,每个都有一个相应的内存区域:堆栈和堆.

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

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

堆允许更灵活的内存分配模式.簿记更复杂,分配更慢.由于没有隐式释放点,因此必须使用deletedelete[](free在C中)手动释放内存.但是,缺少隐式释放点是堆灵活性的关键.

使用动态分配的原因

即使使用堆较慢并且可能导致内存泄漏或内存碎片,动态分配也有很好的用例,因为它的限制较少.

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

  • 您不知道在编译时需要多少内存.例如,在将文本文件读入字符串时,通常不知道文件的大小,因此在运行程序之前无法确定要分配的内存量.

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

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

在C++中,有一个称为析构函数的简洁结构.此机制允许您通过将资源的生命周期与变量的生命周期对齐来管理资源.这种技术称为RAII,是C++的一个显着特点.它将资源"包装"到对象中. std::string是一个很好的例子.这个片段:

int main ( int argc, char* argv[] )
{
    std::string program(argv[0]);
}
Run Code Online (Sandbox Code Playgroud)

实际上分配了可变数量的内存.该std::string对象使用堆分配内存并在其析构函数中释放它.在这种情况下,你是不是需要手动管理的任何资源,还是把动态内存分配的好处.

特别是,它暗示在这个片段中:

int main ( int argc, char* argv[] )
{
    std::string * program = new std::string(argv[0]);  // Bad!
    delete program;
}
Run Code Online (Sandbox Code Playgroud)

有不必要的动态内存分配.该程序需要更多的输入(!)并引入忘记释放内存的风险.这样做没有明显的好处.

为什么要尽可能经常使用自动存储

基本上,最后一段总结了它.尽可能经常使用自动存储使您的程序:

  • 更快打字;
  • 跑步时更快;
  • 不太容易出现内存/资源泄漏.

奖励积分

在引用的问题中,还有其他问题.特别是以下课程:

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

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

Line::~Line() {
    delete mString;
}
Run Code Online (Sandbox Code Playgroud)

实际上使用风险比以下风险更大:

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

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

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

int main ()
{
    Line l1;
    Line l2 = l1;
}
Run Code Online (Sandbox Code Playgroud)

使用原始版本,该程序可能会崩溃,因为它使用delete相同的字符串两次.使用修改后的版本,每个Line实例都将拥有自己的字符串实例,每个实例都有自己的内存,两者都将在程序结束时释放.

其他说明

由于上述所有原因,广泛使用RAII被认为是C++中的最佳实践.但是,还有一个额外的好处并不是很明显.基本上,它比它的各个部分的总和更好.整个机制组成.它可以扩展.

如果您将该Line类用作构建块:

 class Table
 {
      Line borders[4];
 };
Run Code Online (Sandbox Code Playgroud)

然后

 int main ()
 {
     Table table;
 }
Run Code Online (Sandbox Code Playgroud)

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

  • 最后提到RAII的+1,但应该有一些关于异常和堆栈展开的东西. (50认同)
  • 提及堆栈分配的_downside_(至少在C++ 1x之前)是一个很好的补充 - 如果你不小心,你经常需要不必要地复制东西.例如,一个"怪物"在它死亡时向"世界"吐出一个"宝藏".在它的'Die()`方法中,它为世界增添了宝藏.它必须在其他地方使用`world-> Add(new Treasure(/*...*/))`来保存宝藏.替代品是`shared_ptr`(可能是矫枉过正),`auto_ptr`(所有权转移的语义差),价值转移(浪费)和`move` +`unique_ptr`(尚未广泛实施). (15认同)
  • @Tobu:是的,但这个帖子已经很长了,我想让它更专注于OP的问题.我最终会写一篇博文或其他内容,并从这里链接到它. (7认同)
  • 你所说的堆栈分配的局部变量可能有点误导."堆栈"指的是调用堆栈,它存储*堆栈帧*.这些堆栈帧以LIFO方式存储.特定帧的局部变量被分配,就好像它们是结构的成员一样. (7认同)
  • @someguy:的确,解释并不完美.实施在其分配政策中具有自由.但是,变量需要以LIFO方式初始化和销毁​​,因此类比成立.我认为这不会使答案复杂化. (7认同)
  • @ kizzx2:这个问题已经解决了.如果分配的对象必须转义当前块的生命周期,则必须使用动态分配(即使它是间接的,例如将副本存储在向量中). (4认同)
  • @Tony,Omnifarious:和Tobu一样,静态分配有点超出了这个回复的范围.我想把这篇文章集中在OP的问题上,这个问题归结为"To`new`或者不是'new`." (2认同)
  • 也许在"其他笔记"部分之前添加关于[三个规则](http://en.wikipedia.org/wiki/Rule_of_three_(C%2B%2B_programming))的注释?不知道这是否会污染这个答案 (2认同)
  • 使用动态堆内存还有另一个重要的可能原因——即使知道所需内存的确切大小。一个对象可能只是太大了,以致于将其放在堆栈中时会产生问题。理想情况下,一些代码分析工具可能会在将大对象放入堆栈内存时发出警告,但程序员也应该记住这个问题。 (2认同)

Dig*_*oss 167

因为堆栈快速而且万无一失

在C++中,只需要一条指令就可以为给定函数中的每个局部作用域对象分配空间(在堆栈上),并且不可能泄漏任何内存.该评论意图(或应该有意)说出"使用堆栈而不是堆"之类的东西.

  • *cough*`int x; return&x;` (49认同)
  • @Charlie是对的.**自动变量快速且万无一失**会更准确. (31认同)
  • @Charlie:班级内部需要以任何一种方式建立.正在进行比较以分配所需的空间. (28认同)
  • "只需要一条指令来分配空间" - 哦,胡说八道.当然只需要一条指令就可以添加到堆栈指针中,但是如果类有任何有趣的内部结构,那么除了添加到堆栈指针之外还会有很多.同样有效的是,在Java中它不需要指令来分配空间,因为编译器将在编译时管理引用. (18认同)
  • 快是的.但肯定不是万无一失的.没有什么是万无一失的.你可以得到一个StackOverflow :) (15认同)
  • @rxantos没有什么是万无一失的,因为无论你多么努力地做出万无一失的事情,他们都会不断创造出更好更好的傻瓜,无论如何都会找到一种方法来打击它:P;) (9认同)
  • 除非它不是.请参阅下面我写的示例 - 您无法在堆栈中的类中分配类状态; 在那里犯错,堆栈不会帮助你. (4认同)
  • @Oli:给出一个简单的问题,我担心"自动变量"不会在OP的脑海中响起任何铃声(或者那些稍后会偶然发现这个问题的人).我们说堆栈是用作口语吗? (2认同)
  • @Charlie,请记住确切的原始问题.不知何故,OP似乎知道他错过了关于分配速度和建议的根本原因的关键见解.简单的问题.简单回答.显然,我们可以更全面地使用C++来覆盖C++,并且使用严格准确的*("自动变量")*术语,但这在某种程度上是不可能的,部分是无益的. (2认同)
  • @yuqli:对声明*“堆栈是万无一失的”*的抨击 - 从函数返回局部变量的地址是一个并不少见的错误(尽管它经常是伪装发生的,不像我发布的那么明显)。*使用*这样的指针会调用UB。 (2认同)
  • @yuqli /sf/ask/450885291/ (2认同)

Nic*_*las 103

情况很复杂.

首先,C++不是垃圾回收.因此,对于每个新的,必须有相应的删除.如果你没有把这个删除,那么你有内存泄漏.现在,对于这样一个简单的情况:

std::string *someString = new std::string(...);
//Do stuff
delete someString;
Run Code Online (Sandbox Code Playgroud)

这很简单.但是如果"Do stuff"抛出异常会发生什么?糟糕:内存泄漏.如果"做东西" return早发布会怎么样?糟糕:内存泄漏.

这是最简单的情况.如果您碰巧将该字符串返回给某人,现在他们必须将其删除.如果他们将其作为参数传递,接收它的人是否需要删除它?什么时候应该删除它?

或者,你可以这样做:

std::string someString(...);
//Do stuff
Run Code Online (Sandbox Code Playgroud)

delete.该对象是在"堆栈"上创建的,一旦超出范围,它将被销毁.您甚至可以返回对象,从而将其内容传输到调用函数.您可以将对象传递给函数(通常作为引用或const引用:void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis).等等.

所有没有newdelete.毫无疑问,谁拥有记忆或谁负责删除记忆.如果你这样做:

std::string someString(...);
std::string otherString;
otherString = someString;
Run Code Online (Sandbox Code Playgroud)

据了解,otherString有副本的数据someString.它不是指针; 它是一个单独的对象.它们可能恰好具有相同的内容,但您可以在不影响另一个的情况下更改一个:

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

看到这个想法?

  • @JustinTime您不必担心释放动态分配的对象的内存,这些对象将保留程序的生命周期.程序执行时,操作系统会为其创建物理内存或虚拟内存的图集.虚拟内存空间中的每个地址都映射到物理内存的地址,当程序退出时,映射到其虚拟内存的所有内容都将被释放.因此,只要程序完全退出,您就不必担心永远不会删除已分配的内存. (4认同)
  • 关于这一点...如果一个对象是在“main()”中动态分配的,并且在程序运行期间存在,由于这种情况而无法在堆栈上轻松创建,并且指向它的指针将传递给任何函数需要访问它,这会在程序崩溃的情况下导致泄漏吗?还是安全的?我会假设后者,因为操作系统释放程序的所有内存也应该在逻辑上释放它,但当涉及到“new”时,我不想假设任何事情。 (2认同)

Sev*_*yev 73

创建的对象new最终必须delete泄漏.析构函数不会被调用,内存不会被释放,整个位.由于C++没有垃圾收集,这是一个问题.

由值(即堆栈)创建的对象在超出范围时自动死亡.析构函数调用由编译器插入,并在函数返回时自动释放内存.

智能指针unique_ptr,shared_ptr解决悬空参考问题,但它们需要编码规则,并有其他问题(可复制性,参考循环等).

此外,在大量多线程场景中,new线程之间存在争用点; 过度使用会对性能产生影响new.根据定义,堆栈对象创建是线程本地的,因为每个线程都有自己的堆栈.

值对象的缺点是,一旦主机函数返回它们就会死掉 - 你无法通过复制或按值返回来传递那些返回给调用者的引用.

  • +1.Re"由`new`创建的对象必须最终被`删除'以免泄漏." - 更糟糕的是,`new []`必须与`delete []`匹配,如果你``删除``new []`-ed memory或`delete []``new`-ed memory,你得到未定义的行为 - 很少有编译器警告过这个问题(像Cppcheck这样的工具可以做到). (9认同)
  • @TonyDelroy有些情况下编译器无法发出警告.如果函数返回指针,则可以在new(单个元素)或new []时创建它. (3认同)

sar*_*rat 29

  • C++本身不使用任何内存管理器.其他语言如C#,Java都有垃圾收集器来处理内存
  • 使用操作系统例程来分配内存的C++和过多的新/删除可能会破坏可用内存
  • 对于任何应用程序,如果经常使用内存,建议预先分配它并在不需要时释放.
  • 不正确的内存管理可能导致内存泄漏,而且很难跟踪.因此,在函数范围内使用堆栈对象是一种经过验证的技术
  • 使用堆栈对象的缺点是,它在返回时会创建多个对象副本,传递给函数等.但是,智能编译器非常了解这些情况,并且它们已经针对性能进行了优化
  • 如果在两个不同的地方分配和释放内存,那么在C++中真的很乏味.发布的责任总是一个问题,主要是我们依赖于一些常用的指针,堆栈对象(最大可能)和auto_ptr(RAII对象)等技术
  • 最好的是,你可以控制内存,最糟糕的是,如果我们对应用程序采用不正确的内存管理,你将无法控制内存.由于内存损坏导致的崩溃是最糟糕的,难以追踪.

  • 实际上,任何分配内存的语言都有一个内存管理器,包括c.大多数都很简单,即int*x = malloc(4); int*y = malloc(4); ...第一次调用将分配内存,也就是询问操作系统的内存,(通常是块1k/4k),这样第二次调用,实际上不会分配内存,而是给你一块它分配的最后一块.IMO,垃圾收集器不是内存管理器,因为它只处理内存的自动释放.要被称为内存管理器,它不仅应该处理解除分配而且还应该分配内存. (5认同)
  • C++ 不“使用操作系统例程”;这不是语言的一部分,它只是一个常见的实现。C++ 甚至可以在没有任何操作系统的情况下运行。 (2认同)

Emi*_* L. 23

我发现错过了尽可能少的新内容的几个重要原因:

运算符new具有不确定的执行时间

调用new可能会也可能不会导致操作系统为您的进程分配新的物理页面,如果您经常这样做,这可能会非常慢.或者它可能已经准备好了合适的内存位置,我们不知道.如果您的程序需要具有一致且可预测的执行时间(例如在实时系统或游戏/物理模拟中),则需要避免new在您的时间关键循环中.

运算符new是隐式线程同步

是的,你听说过我,你的操作系统需要确保你的页面表是一致的,因此这样的调用new将导致你的线程获得隐式的互斥锁.如果你一直在new从许多线程调用你实际上是在线程序列化(我已经用32个CPU完成了这个,每个都按下来new获得几百个字节,哎哟!这是调试的皇家皮塔)

其他答案已经提到了诸如缓慢,碎片,容易出错等其他问题.

  • 可以通过使用new / delete放置并事先分配内存来避免这两种情况。或者,您可以自己分配/释放内存,然后调用构造函数/析构函数。这就是std :: vector通常的工作方式。 (2认同)
  • @mikkorantalainen,这在技术上是正确的,但在内存不足的情况下,当您推送到磁盘时,所有的赌注都将失去性能,因此您无能为力。无论如何,在合理的情况下,避免新呼叫的建议不会失效。 (2认同)

Meh*_*dad 19

预C++ 17:

因为即使将结果包装在智能指针中,它也容易出现细微的泄漏.

考虑一个"谨慎"的用户,他记得在智能指针中包装对象:

foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));
Run Code Online (Sandbox Code Playgroud)

此代码是危险的,因为有没有保证,要么shared_ptr被构建之前无论是T1T2.因此,如果一个new T1()new T2()另一个成功后失败,那么第一个对象将被泄露,因为没有shared_ptr存在可以销毁和解除分配.

解决方案:使用make_shared.

后C++ 17:

这不再是一个问题:C++ 17对这些操作的顺序施加了约束,在这种情况下,确保每次调用new()必须紧跟在构造相应的智能指针之后,其间不进行其他操作.这意味着,在new()调用第二个对象时,保证第一个对象已经被包装在其智能指针中,从而防止在抛出异常时发生任何泄漏.

Barry 在另一个答案中提供了由C++ 17引入的新评估顺序的更详细解释.

  • 其他解决方案:永远不要每行动态分配多个对象. (5认同)
  • @Antimony:是的,当你已经分配了一个对象时,与你没有分配任何对象相比,分配多个对象会更加诱人. (3认同)
  • 即使在后 C++17 的情况下,如果 `new` 成功并且随后的 `shared_ptr` 构造失败,仍然会发生泄漏。`std::make_shared()` 也可以解决这个问题 (3认同)

Cha*_*tin 17

在很大程度上,这是有人将自己的弱点提升到一般规则.使用运算符创建对象本身没有任何错误new.有一些争论的原因是你必须用一些纪律来做这件事:如果你创建一个对象,你需要确保它将被销毁.

最简单的方法是在自动存储中创建对象,因此C++知道在超出范围时将其销毁:

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

    // do things

 }
Run Code Online (Sandbox Code Playgroud)

现在,观察一下,当你在结束后从那个块上掉下来时,foo超出了范围.C++会自动为你调用它的dtor.与Java不同,您无需等待GC找到它.

如果你写的

 {
     File * foo = new File("foo.dat");
Run Code Online (Sandbox Code Playgroud)

你想要明确地匹配它

     delete foo;
  }
Run Code Online (Sandbox Code Playgroud)

甚至更好,将你File *的"智能指针" 分配.如果你不小心它可能会导致泄漏.

答案本身就是错误的假设,即如果你不使用new你,就不要在堆上分配; 事实上,在C++中你不知道.至多,你知道一个小的内存,比如说一个指针,肯定是在堆栈上分配的.但是,请考虑File的实现是否类似

  class File {
    private:
      FileImpl * fd;
    public:
      File(String fn){ fd = new FileImpl(fn);}
Run Code Online (Sandbox Code Playgroud)

然后FileImpl仍然可以在栈上分配.

是的,你最好确定

     ~File(){ delete fd ; }
Run Code Online (Sandbox Code Playgroud)

在课堂上也是如此; 没有它,你会泄漏从堆内存,即使你没有明显地在堆中分配的.

  • @Charlie:评论确实*不*说你永远不应该使用`new`.它说如果您*在动态分配和自动存储之间进行选择,请使用自动存储. (9认同)
  • @Charlie:使用`new`没什么不对,但是如果你使用`delete`,那你做错了! (8认同)
  • 我同意使用'new`*本身*没有任何问题,但是如果你看一下评论所引用的原始代码,那么`new`就会被滥用.代码的编写方式与Java或C#类似,其中`new`几乎用于每个变量,当事物在堆栈上更有意义时. (7认同)
  • 有道理.但通常会强制执行一般规则以避免常见的陷阱.无论这是个人的弱点,内存管理都足够复杂,需要像这样的一般规则!:) (5认同)
  • 您应该查看引用问题中的代码.该代码肯定存在许多问题. (4认同)
  • @Charlie:确切地说:-)."然后FileImpl仍将在堆栈上分配." < - 我认为你的意思是放堆而不​​是堆栈. (3认同)
  • @Charlie好的,但我们不需要证明一条规则说"在XYZ情况下不要使用'new`"; 我们需要证明一个规则,即"在ABC情境中使用`new`",因为使用`new`比仅仅声明一个值更难. (2认同)

And*_*mbe 15

new()不应该尽可能地使用.应尽可能小心使用.并且应该根据实用主义的需要经常使用它.

堆栈上的对象依赖于它们的隐式破坏是一个简单的模型.如果对象的所需范围符合该模型,则无需使用new()关联delete()和检查NULL指针.在堆栈中有大量短期对象的情况下,应该减少堆碎片的问题.

但是,如果对象的生命周期需要超出当前范围,那么这new()是正确的答案.只要确保你注意你何时以及如何调用delete()以及NULL指针的可能性,使用删除的对象和使用指针所带来的所有其他陷阱.

  • "如果你的对象的生命周期需要超出当前范围,那么new()就是正确的答案"......为什么不优先按值返回或者通过非`constst?ref或指针接受调用者范围的变量. .? (8认同)
  • @Tony:是的,是的!我很高兴听到有人提倡参考.创建它们是为了防止出现此问题. (2认同)
  • @TonyD ...或组合它们:按值返回智能指针。这样调用者和在许多情况下(即在`make_shared/_unique` 可用的情况下)被调用者永远不需要`new` 或`delete`。这个答案忽略了真正的要点:(A) C++ 提供了诸如 RVO、移动语义和输出参数之类的东西——这通常意味着通过返回动态分配的内存来处理对象创建和生命周期扩展变得不必要和粗心。(B) 即使在需要动态分配的情况下,stdlib 也提供了 RAII 包装器,可以减轻用户丑陋的内部细节。 (2认同)

Tim*_*Tim 13

使用new时,会将对象分配给堆.它通常在您预期扩展时使用.声明一个对象时,

Class var;
Run Code Online (Sandbox Code Playgroud)

它放在堆栈上.

您将始终必须使用new调用您在堆上放置的对象上的destroy.这打开了内存泄漏的可能性.放在堆栈上的对象不容易出现内存泄漏!

  • +1"[heap]通常在你预期扩展时使用" - 比如附加到`std :: string`或`std :: map`,是的,敏锐的洞察力.我最初的反应是"但也非常普遍地将对象的生命周期与创建代码的范围分离",但实际上通过值返回或通过非`constst`引用或指针接受调用者范围的值更好,除非有"扩张"也参与其中.还有其他一些声音用途,比如工厂方法...... (2认同)

tyl*_*erl 11

避免过度使用堆的一个值得注意的原因是性能 - 特别是涉及C++使用的默认内存管理机制的性能.虽然分配可以在琐碎的情况下相当快的,做了很多new,并delete在没有严格的顺序不统一大小的对象不仅导致内存碎片,但它也变得复杂分配算法,可以完全摧毁在某些情况下的性能.

这就是创建要解决的内存池的问题,允许减轻传统堆实现的固有缺点,同时仍然允许您根据需要使用堆.

但是,更好的是,完全避免这个问题.如果你可以将它放在堆栈上,那么就这样做.


Kha*_*sar 10

我认为这张海报的意思是说You do not have to allocate everything on theheap而不是stack.

基本上对象是在堆栈上分配的(当然,如果对象大小允许),因为堆栈分配的成本低廉,而不是基于堆的分配,这涉及分配器的相当多的工作,并且增加了冗长,因为那时你必须管理堆上分配的数据.


小智 10

我倾向于不同意使用新"太多"的想法.虽然原始海报使用新系统类有点荒谬.(int *i; i = new int[9999];?真的?int i[9999];更清楚.)我认为就是评论者的山羊.

当您使用系统对象时,您需要多个引用完全相同的对象是非常罕见的.只要价值相同,那就重要了.并且系统对象通常不会在内存中占用太多空间.(每个字符一个字节,字符串).如果他们这样做,那么库应该被设计为考虑到内存管理(如果它们写得很好).在这些情况下,(除了他的代码中的一个或两个新闻),新的几乎毫无意义,只会引起混乱和潜在的错误.

但是,当您使用自己的类/对象时(例如原始海报的Line类),您必须开始考虑内存占用,数据持久性等问题.此时,允许多次引用相同的值是非常宝贵的 - 它允许构造链接列表,字典和图形,其中多个变量不仅需要具有相同的值,而且需要在内存中引用完全相同的对象.但是,Line类没有任何这些要求.所以原始海报的代码实际上完全没有必要new.


归档时间:

查看次数:

119800 次

最近记录:

6 年,1 月 前