两者之间有一个重要的区别。
未分配的所有内容都与new
C# 中的值类型非常相似(人们经常说这些对象是在堆栈上分配的,这可能是最常见/最明显的情况,但并非总是如此)。更准确地说,未使用分配的对象new
具有自动存储持续时间
分配的所有内容都new
在堆上分配,并返回指向它的指针,就像 C# 中的引用类型一样。
在堆栈上分配的任何东西都必须有一个常量大小,在编译时确定(编译器必须正确设置堆栈指针,或者如果对象是另一个类的成员,它必须调整另一个类的大小) . 这就是 C# 中的数组是引用类型的原因。它们必须是,因为使用引用类型,我们可以在运行时决定请求多少内存。这同样适用于这里。只有具有恒定大小(可以在编译时确定的大小)的数组才能分配自动存储持续时间(在堆栈上)。必须在堆上分配动态大小的数组,方法是调用new
.
(这就是与 C# 的任何相似性停止的地方)
现在,在堆栈上分配的任何东西都有“自动”存储持续时间(您实际上可以将变量声明auto
为来自)
自动存储持续时间意味着它听起来像,变量的持续时间是自动处理的。相比之下,在堆上分配的任何东西都必须由您手动删除。这是一个例子:
void foo() {
bar b;
bar* b2 = new bar();
}
此函数创建三个值得考虑的值:
在第 1 行,它在堆栈上声明了一个b
类型变量(自动持续时间)。bar
在第 2 行,它在堆栈上声明一个bar
指针b2
(自动持续时间),并调用 new,在堆上分配一个bar
对象。(动态持续时间)
当函数返回时,会发生以下情况:首先,b2
超出范围(破坏顺序总是与构造顺序相反)。但是b2
只是一个指针,所以什么也没有发生,它占用的内存被简单地释放了。重要的是,它指向的内存(bar
堆上的实例)没有被触及。只有指针被释放,因为只有指针具有自动持续时间。其次,b
超出范围,因此由于它具有自动持续时间,因此调用其析构函数并释放内存。
堆上的bar
实例呢?它可能还在那里。没有人愿意删除它,所以我们泄露了内存。
从这个例子中,我们可以看到任何具有自动持续时间的东西都保证在超出范围时调用其析构函数。这很有用。但是在堆上分配的任何东西只要我们需要它就可以持续,并且可以动态调整大小,就像数组一样。这也很有用。我们可以使用它来管理我们的内存分配。如果 Foo 类在其构造函数中在堆上分配一些内存,并在其析构函数中删除该内存会怎样。然后我们可以两全其美,保证再次释放的安全内存分配,但没有强制所有内容都在堆栈上的限制。
这几乎就是大多数 C++ 代码的工作方式。以标准库std::vector
为例。这通常在堆栈上分配,但可以动态调整大小和调整大小。它通过在必要时在堆上内部分配内存来做到这一点。该类的用户永远不会看到这一点,因此不会泄漏内存或忘记清理您分配的内容。
这个原理叫做RAII(Resource Acquisition is Initialization),它可以扩展到任何必须被获取和释放的资源。(网络套接字、文件、数据库连接、同步锁)。它们都可以在构造函数中获取,并在析构函数中释放,因此您可以保证您获得的所有资源都会再次被释放。
作为一般规则,永远不要直接从您的高级代码中使用 new/delete。始终将它包装在一个可以为您管理内存的类中,这将确保它再次被释放。(是的,这条规则可能有例外。特别是,智能指针要求您new
直接调用,并将指针传递给其构造函数,然后由构造函数接管并确保delete
正确调用。但这仍然是一个非常重要的经验法则)