精通c - 高频交易c++




为什么C++程序员应该尽量减少对'新'的使用? (12)

因为即使将结果包装到智能指针中,它也容易发生细微的泄漏。

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

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

此代码很危险,因为不能保证 T1T2 之前构建shared_ptr 。 因此,如果new T1()new T2()在另一个成功后失败,则第一个对象将被泄漏,因为不存在shared_ptr来销毁和释放它。

解决方案:使用make_shared

我偶然发现堆栈溢出问题使用std :: list <std :: string>时使用std :: string发生内存泄漏 , 其中一个注释说:

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

我不确定他的意思。 为什么应该尽可能经常通过C ++中的值创建对象,它在内部会产生什么差异? 我误解了答案吗?


因为堆叠速度快而且万无一失

在C ++中,只需一条指令即可为堆栈上的空间分配给定函数中的每个本地作用域对象,并且不可能泄漏任何内存。 该评论打算(或应该打算)说“使用堆栈而不是堆”。


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

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

auto_ptrshared_ptr这样的智能指针解决了悬挂参考问题,但它们需要编码规范并且有其他问题(可复制性,参考循环等)。

而且,在大量多线程场景中, new是线程之间的争用点; 可能会对过度使用new产生性能影响。 由于每个线程都有自己的堆栈,因此创建堆栈对象是按照线程定义的。

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


new在堆上分配对象。否则,对象将分配在堆栈上。查看两者的区别


两个原因:

  1. 在这种情况下没有必要。 你让代码不必要地变得更复杂。
  2. 它会在堆上分配空间,这意味着您必须记住稍后将其delete ,否则会导致内存泄漏。

其核心原因是堆上的对象总是比简单的值难以使用和管理。 编写易于阅读和维护的代码始终是任何严肃程序员的首要任务。

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

然而,对于面向对象的代码,使用指针 - 这意味着使用new来预先创建 - 是必须的。 为了简化资源管理的复杂性,我们有许多工具使其尽可能简单,例如智能指针。 基于对象的范式或泛型范式假设价值语义,并且需要较少或不需要new ,就像其他地方所述的海报一样。

传统的设计模式,特别是GoF书中提到的那些,使用了很多new的设计模式,因为它们是典型的面向对象代码。


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

Class var;

它被放置在堆栈上。

你将永远不得不打电话销毁你放在堆上的新物件。 这打开了内存泄漏的可能性。 放置在堆栈上的对象不容易泄漏内存!


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

当你使用系统对象时,你很少会需要多个对同一个对象的引用。 只要价值是一样的,那就是重要的。 系统对象通常不占用内存中的太多空间。 (每个字符一个字节,一个字符串)。 如果他们这样做,这些库应该被设计成考虑到内存管理(如果它们写得很好)。 在这些情况下(除了代码中的一两个新闻外),新增功能实际上毫无意义,只会引入混淆和潜在错误。

然而,当你使用自己的类/对象时(例如原来的海报的Line类),那么你必须开始考虑内存占用,数据持久性等问题。 在这一点上,允许多次引用相同的值是无价的 - 它允许链接列表,字典和图形等结构,其中多个变量不仅需要具有相同的值,还要引用内存中完全相同的对象 。 但是,Line类没有任何这些要求。 所以原始海报的代码实际上对于new绝对没有需要。


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

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


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

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

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

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

使用动态分配的原因

即使使用堆比较慢并且可能导致内存泄漏或内存碎片,但对于动态分配来说,有非常好的用例,因为它的限制较少。

使用动态分配的两个关键原因:

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

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

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

在C ++中有一个称为析构函数的整洁构造。 该机制允许您通过将资源的生命周期与变量的生命周期对齐来管理资源。 这种技术被称为RAII ,是C ++的区别点。 它将资源“包装”到对象中。 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实例和所有字符串的内容, 所有内容都被自动释放


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

这就是需要解决的内存池问题,它可以缓解传统堆实现的固有缺点,同时仍然允许您根据需要使用堆。

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


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




c++-faq