首页 文章

C程序员为什么要尽量减少'new'的使用?

提问于
浏览
774

我偶然发现Stack Overflow问题Memory leak with std::string when using std::liststd::stringone of the comments这样说:

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

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

17 回答

  • 3

    两个原因:

    • It 's unnecessary in this case. You'让你的代码变得更加复杂 .

    • 它在堆上分配空间,这意味着你必须在以后记住 delete ,否则会导致内存泄漏 .

  • 1

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

    堆栈

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

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

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

    使用动态分配的原因

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

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

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

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

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

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

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

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

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

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

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

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

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

    • 更快打字;
      运行时

    • 更快;

    • 不太容易出现内存/资源泄漏 .

    奖励积分

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

    class Line {
    public:
        Line();
        ~Line();
        std::string* mString;
    };
    
    Line::Line() {
        mString = new std::string("foo_bar");
    }
    
    Line::~Line() {
        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 实例将拥有自己的字符串实例,每个实例都有自己的内存,两者都将在程序结束时释放 .

    其他说明

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

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

    class Table
     {
          Line borders[4];
     };
    

    然后

    int main ()
     {
         Table table;
     }
    

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

  • 28

    new 在堆上分配对象 . 否则,在堆栈上分配对象 . 查找the difference between the two .

  • 1

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

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

  • 10
    • C本身不使用任何内存管理器 . 其他语言如C#,Java都有垃圾收集器来处理内存

    • C使用操作系统例程来分配内存,过多的新/删除可能会破坏可用内存

    • 对于任何应用程序,如果经常使用内存,建议预先分配它并在不需要时释放 .

    • 不正确的内存管理可能导致内存泄漏,而且很难跟踪 . 因此,在函数范围内使用堆栈对象是一种经过验证的技术

    • 使用堆栈对象的缺点是,它在返回时会创建多个对象副本,传递给函数等 . 但是,智能编译器非常了解这些情况并且它们已经针对性能进行了优化

    • 如果在两个不同的地方分配和释放内存,那么在C中真的很乏味 . 发布的责任总是一个问题,主要是我们依赖一些常用的指针,堆栈对象(最大可能)和auto_ptr(RAII对象)等技术

    • 最好的是,你可以控制内存,最糟糕的是,如果我们对应用程序采用不适当的内存管理,你将无法控制内存 . 由于内存损坏导致的崩溃是最糟糕且难以追踪的 .

  • 16

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

    Operator new具有不确定的执行时间

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

    Operator new是一个隐式线程同步

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

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

  • 14

    因为堆栈快速且万无一失

    在C中,只需要一条指令就可以为给定函数中的每个局部范围对象分配空间 - 在堆栈上 - 并且不可能泄漏任何内存 . 该评论旨在(或应该有意)说出像"use the stack and not the heap"这样的内容 .

  • 158

    前C 17:

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

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

    foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));
    

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

    解决方案:使用 make_shared .

    后C 17:

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

    Barry in another answer提供了C17引入的新评估订单的更详细说明 .

  • 67

    new 创建的对象必须最终 delete 以免泄漏 . 析构函数赢了,全部都被释放了 . 由于C没有垃圾收集,这是一个问题 .

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

    auto_ptrshared_ptr 这样的智能指针解决了悬空引用问题,但是它们需要编码规则并且还有其他问题(可复制性,引用循环等) .

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

    值对象的缺点是它们在主机函数返回后会死亡 - 您无法通过复制或按值返回来将引用传递回调用者 .

  • 16

    情况很复杂 .

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

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

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

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

    或者,您可以这样做:

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

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

    全部没有 newdelete . 有's no question of who owns the memory or who'负责删除它 . 如果你这样做:

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

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

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

    看到这个想法?

  • -3

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

    Class var;
    

    它放在堆栈上 .

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

  • 14

    new 是新的 goto .

    回想一下为什么 goto 如此受到谴责:虽然它是一个强大的,低级别的流量控制工具,人们经常以不必要的复杂方式使用它,这使得代码难以理解 . 此外,最有用和最容易阅读的模式是在结构化编程语句中编码的(例如 forwhile );最终的效果是 goto 适当的代码是相当罕见的,如果你想写 goto ,你可能做得很糟糕(除非你真的知道你在做什么) .

    new 类似 - 它经常用于使事情变得不必要地复杂和难以阅读,并且可以编码的最有用的使用模式已被编码到各种类中 . 此外,如果您需要使用任何尚未使用标准类的新使用模式,您可以编写自己的编码类的类!

    我甚至认为 newgoto 更糟糕,因为需要配对 newdelete 语句 .

    就像 goto 一样,如果你认为你需要使用 new ,你可能会做得很糟糕 - 特别是如果你在一个类的实现之外这样做,其生命的目的是封装你需要做的任何动态分配 .

  • 13

    在很大程度上,使用 new 运算符创建对象本身没有任何错误 . 有一些争论的原因是你必须遵守一些规则:如果你创建一个对象,你需要确保它将被销毁 .

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

    {
        File foo = File("foo.dat");
    
        // do things
    
     }
    

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

    如果你写的

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

    你想要明确地匹配它

    delete foo;
      }
    

    甚至更好,将 File * 分配为"smart pointer" . 如果你不小心它可能会导致泄漏 .

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

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

    那么 FileImpl 仍将在堆栈上分配 .

    是的,你最好确定

    ~File(){ delete fd ; }
    

    在课堂上也是如此;没有它,你显然会在堆上分配 .

  • 95

    核心原因是堆上的对象总是难以使用和管理而不是简单的 Value 观 . 编写易于阅读和维护的代码始终是任何认真程序员的首要任务 .

    另一种情况是我们使用的库提供了值语义并且不需要动态分配 . Std::string 就是一个很好的例子 .

    但是对于面向对象的代码,使用指针 - 这意味着使用 new 预先创建它 - 是必须的 . 为了简化资源管理的复杂性,我们提供了许多工具来使其尽可能简单,例如智能指针 . 基于对象的范式或通用范例假设了 Value 语义,并且需要更少或没有 new ,就像其他地方所说的海报一样 .

    传统的设计模式,尤其是GoF书中提到的那些,使用 new 很多,因为它们是典型的OO代码 .

  • 10

    避免过度使用堆的一个值得注意的原因是性能 - 特别是涉及C使用的默认内存管理机制的性能 . 虽然在简单的情况下分配可以非常快,但是在没有严格顺序的情况下对非统一大小的对象执行大量 newdelete 不仅导致内存碎片,而且还使分配算法复杂化并且在某些情况下绝对会破坏性能 .

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

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

  • 10

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

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

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

  • 924

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

    当你非常罕见的时候,你很重要 . 并且系统对象不能很好地写好 . 在这些情况下,(除了他的代码中的一两个新闻),new几乎毫无意义,只会引起混乱和潜在的错误 .

    当你使用Line类时,你必须开始考虑内存占用,数据持久性等问题 . 此时,允许多次引用相同的值是非常宝贵的 - 它允许构造链接列表,字典和图形,其中多个变量不仅需要具有相同的值,而且还需要在内存中引用完全相同的对象 . 但是,Line类没有't have any of those requirements. So the original poster'的代码实际上完全没有 new 的需要 .

相关问题