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

不要用那么多new。我看不出你为什么在任何地方都用新的。你可以在c++中通过值创建对象,这是使用该语言的一个巨大优势。您不必分配堆上的所有内容。不要像Java程序员那样思考。

我不太确定他这么说是什么意思。

为什么在c++中应该尽可能多地根据值创建对象,这在内部有什么不同?我误解了答案吗?


当前回答

New在堆上分配对象。否则,对象分配在堆栈上。查一下两者的区别。

其他回答

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

Class var;

它被放置在堆栈上。

你总是需要对你用new放在堆上的对象调用destroy。这就有可能导致内存泄漏。放在堆栈上的对象不容易发生内存泄漏!

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

堆栈

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

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

Heap

堆支持更灵活的内存分配模式。记账更复杂,分配更慢。因为没有隐式释放点,你必须手动释放内存,使用delete或delete[] (C中的free)。然而,没有隐式释放点是堆灵活性的关键。

使用动态分配的原因

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

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

You don't know how much memory you need at compile time. For instance, when reading a text file into a string, you usually don't know what size the file has, so you can't decide how much memory to allocate until you run the program. You want to allocate memory which will persist after leaving the current block. For instance, you may want to write a function string readfile(string path) that returns the contents of a file. In this case, even if the stack could hold the entire file contents, you could not return from a function and keep the allocated memory block.

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

在c++中,有一个简洁的构造叫做析构函数。这种机制允许您通过将资源的生命周期与变量的生命周期对齐来管理资源。这种技术被称为RAII,是c++的特点。它将资源“包装”到对象中。Std::string就是一个很好的例子。这个代码片段:

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

实际上分配的内存是可变的。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];
 };

Then

 int main ()
 {
     Table table;
 }

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

我发现有几个重要的原因让我们忽略了尽量少做新事情:

Operator new的执行时间不确定

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

操作符new是一个隐式线程同步

是的,你听到了。你的操作系统需要确保你的页表是一致的,因此调用new会导致你的线程获得一个隐式互斥锁。如果你一直从许多线程调用new,你实际上是在序列化你的线程(我用32个cpu做过这个,每个cpu都调用new来获得几百个字节,哎呦!那是一个需要调试的皇家p.i.t.a.。)

其余的,比如速度慢、碎片化、容易出错等,其他答案已经提到了。

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

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

像unique_ptr、shared_ptr这样的智能指针解决了悬空引用问题,但它们需要编码规则,并有其他潜在的问题(可复制性、引用循环等)。

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

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

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

这就是创建内存池要解决的问题,可以减轻传统堆实现的固有缺点,同时仍然允许您在必要时使用堆。

不过,最好还是完全避免这个问题。如果可以将它放到堆栈中,那么就这样做。