C++内存管理和向量

5
我对与向量相关的内存管理非常困惑,希望能够解释一些基本概念。
我有一个使用大型向量的程序。 我使用new运算符创建向量,并在程序结束时使用delete释放它们以获得内存。
我的问题是,如果程序崩溃或因任何原因中止,则会错过delete语句,是否有办法即使在这种情况下恢复内存。
我还有一些其他的大型向量,我没有使用new关键字进行分配。 我读到说这些向量将在堆上创建,但不需要以任何方式进行释放,因为内存管理是“在幕后”处理的。 但是我不确定这是否正确,因为每次运行程序时我都会丢失RAM。
所以我的第二个问题是,不使用new关键字创建的向量是否真的可以被留给自己的设备,并且即使代码在流程中中止,也可以信任它们自己清理。
我想到了第三个问题,如果向量自动在堆上创建,为什么要使用new关键字呢? 谢谢阅读, 本

3
“每次运行程序我都会丢失内存”是指“我的可用内存会变小直到我退出程序”,还是指“即使在我退出程序后,我的可用内存也会变得更小,并且在下一次运行时会变得更小,直到有一天我完全没有剩余的内存”? - Max Lybbert
我支持Max的问题。我相信Windows实际上不会卸载已终止的程序,除非需要这样做。这样它们在第一次启动后会更快地启动。 - T.E.D.
我想到了第三个问题,如果向量自动在堆上创建,为什么还需要使用new关键字呢?只有在需要将向量传递到当前范围之外的点时才需要这样做。在实践中,这相对较少见。 - rlbond
8个回答

17

我猜您的问题是关于std::vector< T >(而不是数组T[])的。

  1. 当您的应用程序因任何原因崩溃或中止时,操作系统会回收内存。除非您使用的是非常罕见的操作系统并发现了一个错误。
  2. 您需要区分向量本身使用的内存和其包含对象的内存。向量可以在堆上或堆栈上创建,如您所述,它分配给其包含元素的内存始终在堆上(除非提供自己的分配器执行其他操作)。由向量分配的内存由向量实现管理,如果向量被销毁(对于堆栈上的向量,它超出范围或者因为您删除了堆上的向量),其析构函数将确保释放所有内存。

4
许多嵌入式操作系统和实时操作系统在进程崩溃时不会清理资源。 - T.E.D.

11

不要使用new来创建向量。只需将它们放在堆栈上即可。

向量的析构函数会自动调用向量中每个元素的析构函数,因此您不必担心自己删除对象。但是,如果您有一个指向指针的向量,指针所指向的对象将不会被清理。以下是一些示例代码。为了清晰起见,我省略了大部分细节:

class HeapInt
{
    public:
        HeapInt(int i) {ptr = new int(i);}
        ~HeapInt() {delete ptr;}
        int& get() {return *ptr;}
    private:
        int* ptr;
};

int main()
{
    // this code DOES NOT leak memory
    std::vector<HeapInt> vec;
    for (int i = 0; i < 10; ++i)
    {
       HeapInt h(i);
       vec.push_back(h);
    }
    return 0;
}

即使main()抛出异常,也不会丢失任何内存。但是,这段代码确实存在内存泄漏:does

int main()
{
    // this code though, DOES leak memory
    std::vector<int*> vec;
    for (int i = 0; i < 10; ++i)
    {
       int* ptr = new int(i);
       vec.push_back(ptr);
    }
    // memory leak: we manually invoked new but did not manually invoke delete
    return 0;
}

5
是的,但是提问的人显然不知道向量通常应该放在堆栈上。 - rlbond
第二个示例会如何泄漏内存?程序结束后,操作系统会回收内存,或者我在内存管理方面错过了什么非常重要的东西? - Cole Tobin
3
是的,当任何程序结束时,操作系统会回收内存。但如果这是程序多次调用的函数,则会导致内存泄漏。 - rlbond

5

是的,您可以相信向量会在完成任务后自我清理。

然而,您不能相信向量所持有的内容能够自我清理。需要清理的可能是应用程序之外的持久化数据。如果需要清理的是内存,则不必担心。但如果需要确保 XML 标记都已关闭,则操作系统将无法帮助您。

例如,如果您有一个包含一些奇怪锁定对象的向量,如下所示:

  class CLock
  {
  public:
      CLock() {}
      ~CLock() {}

      void Lock(...) {...}

      void Unlock(...) {...}
  };

  std::vector<CLock> myLockVec;

当向量完成时,您的 CLock 向量如何知道解锁所有内容?向量不具备了解锁的能力。

这与拥有指针向量的情况基本相同:

 std::vector<int*> myIntVec;

向量如何知道哪些指针已被删除和设置为NULL,哪些仍然存在?也许有些已经被删除并设置为您的特殊值0xdeadbeef,表示已删除。
重点是向量没有办法知道这一点,也不知道它的元素是指针、锁或其他什么。它们只需要是具有默认构造函数、可复制并满足向量对其元素的其他要求的东西。
解决方案是确保无论向量持有什么都需要负责清理。这被称为RAII——资源分配即初始化,更重要的是,在这里,资源销毁就是释放。对于上面的我们的CLock示例,答案很明显,确保在完成后解锁!
 class CLock
 {  
      ...
      ~Clock()
      {
          if (locked)
          {
              Unlock();
          }
      }
 } 

但是指针并不那么显而易见。解决方法是将指针包装在smart_ptr类中。其中最常用的就是boost智能指针家族

class CSmartPointer<T>
{
      CSmartPointer( T* rawPtr)
      {
         m_ptr = rawPtr;
      }

      ~CSmartPointer()
      {
         delete m_ptr;
      }
}

指针等功能带来了额外的特性,例如引用计数,但上面的示例应该能让您了解问题的本质及其通常解决方法。


4

我猜你说的是std::vector而不是语言数组。

  1. 当程序崩溃时,操作系统会恢复其内存。
  2. std::vector会释放它分配的内存。如果你存储指针,它们将不会被删除。
  3. 向量与其他变量一样创建,它们之所以不在堆中,不仅仅因为它们是向量。

3
任何由你的程序创建的内存都将在退出时释放。这是操作系统的一个特性,与你使用的编程语言无关。
“每次运行程序我都会失去RAM”的原因必须是其他影响 - 你是如何测量的?
至于为什么要使用“new” - 有两个原因:
- 你想控制它们何时被释放。 - 你想让它们在当前函数退出后持久存在。

1
和 Tobias 收到的相同评论。大多数嵌入式操作系统和许多实时操作系统在进程崩溃时不会清理资源。 - T.E.D.
+1 为指出许多人没有意识到的事情。虚拟内存管理器通常被视为理所当然的奢侈品。 - Void

2

我们中的其中一个有点困惑。

如果您使用std::vector,您不需要手动为其元素分配内存。每当执行push_back()时,会自动分配额外的空间。如果由于某种原因需要预先分配所有空间,则可以调用reserve()。无论哪种方式,当向量被销毁时,内存都会自动释放。

如果您正在使用new std::vector,则会得到指向向量的指针。这与在任何其他类上调用new没有区别。您创建了指向该类对象的指针,并且在调用delete时它将被析构。如果您不喜欢这种行为,请尝试在堆栈上创建向量。


1
关于“丢失的内存”,@RichieHindie所说的。
至于第二个问题:
可以不使用NEW关键字创建向量,真的可以让它们自行处理并信任它们在代码中断流时清除自己吗?
虽然正常程序终止(包括异常终止)确保析构函数执行(关于静态数据的析构函数有一些争议——理论上也应该运行,但实际上可能会偶尔出现问题),但进程的崩溃足够严重时无法保证任何行为——例如,kill -9保证尽快终止您的程序,而不给它执行任何析构函数或其他任何东西的机会。

未处理的异常(导致运行时最终调用terminate())可能不会导致析构函数执行。在这种情况下,堆栈展开行为是实现定义的。 - ASk

1

在编程中,另一个使用 "new" 的情况是当向量是类的成员变量时。NULL 可以用作附加信号量,例如在按需创建期间;此外,如果向量在您的类上稀疏地填充,则除非真正需要,否则不创建一个将为您节省内存,但会增加所有实例的额外 4 字节惩罚以及指针间接运行时惩罚。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接