使用对象而不是指针时双重释放或损坏

double free or corruption when using object instead of pointer

为什么第一个代码片段在调用析构函数时会导致双重释放或损坏错误,而第二个代码片段可以正常工作?

int main( int argc, char** argv )
{
vector<int> vec = *new vector<int>(10);
vec.at(3) = 6;
vec.~vector(); 
}

这个有效:

int main( int argc, char** argv )
{
vector<int> *vec = new vector<int>(10);
vec->at(3) = 6;
vec->~vector();
}

即使析构函数被调用两次:当对象超出范围时,为什么错误出现在倒数第二行(根据 gbd)而不是 }

两个版本都包含错误,但第一个版本非常糟糕,如果您决定使用矢量对象,它应该是这样的:

int main()
{
    std::vector<int> vec(10);
    vec.at(3) = 6;
}

这是使用指针的样子:

int main( int argc, char** argv )
{
    std::vector<int> *vec = new std::vector<int>(10);
    vec->at(3) = 6;
    delete vec;
}

甚至不需要最后一行,因为程序将终止并且 ram 在任何情况下都是空闲的。

这个vector<int> vec = *new vector<int>(10);实际上创建了两个向量。第一个是由 new vector<int>(10) 创建的。然后使用复制构造函数创建第二个 vec。 第一个永远不会被摧毁。第二个被销毁两次,通过手动调用析构函数并在超出范围时自动调用。

第一种情况:

vector<int> vec = *new vector<int>(10);

这里发生了三件事:

  1. 您动态分配一个向量。 new returns 一个指针。在这种情况下,它是一个临时变量,没有名称。
  2. 您取消引用此指针并获得一个 右值
  3. 你构建了另一个对象,vec,用上一步的右值初始化它。这有效地调用了一个复制构造函数 vector(const vector&).

因此,有两个向量。第一个在堆中的某个地方,你没有指向它的指针。这是 内存泄漏 。然后,有一个自动持续时间对象vec。两个向量具有相同的内容。

vec.~vector();

在这里你显式地调用了一个自动持续时间对象的析构函数。你几乎不需要这样做。这主要用于实现 placement new.

一旦离开作用域(例如离开函数体),析构函数就会自动再次调用。因此,您会遇到双重错误。

第二种情况:

vector<int> *vec = new vector<int>(10);
vec->~vector();

这里你销毁对象(即调用析构函数),但不释放它占用的内存。因此,您仍然存在内存泄漏。但是,由于当我们离开范围时动态对象不会自动销毁,因此不会发生双重释放错误。

您应该使用delete vec; 来销毁动态分配的向量。它将调用析构函数并释放内存。

让我们逐行检查代码。方案一:

vector<int> vec = *new vector<int>(10);

矢量 vec 定义在 = 的左侧。另一个未命名的向量是在堆上 = 的右侧创建的。请注意,这涉及两个免费存储分配:一个用于(小)向量对象本身,另一个用于其中的数据,10 个整数。 new 返回的地址不会保留在任何地方,而是立即取消引用,因此 = 右侧的表达式是一个矢量对象。它用于复制初始化 vec。这涉及到为 vec 的数据在空闲存储上分配内存并将所有匿名向量的元素复制到其中。请注意,vec 的数据与右侧向量的数据位于不同的位置。

vec.at(3) = 6;:与讨论无关。

vec.~vector();: 执行vec 的析构函数将释放vec 初始化时分配给空闲存储上的数据的内存。它不会尝试释放 vec 的内存(这很好,因为 vec 不在堆上而是在堆栈上,并且当堆栈被展开时会自动销毁,因为范围是剩余的)。

}:vec 的作用域结束,因此再次调用 vec 的析构函数(语言不保留销毁簿,例如对象中没有 "destroyed" 标志)。这是一件坏事,因为正如我们所知,~vector() 试图释放为其数据分配的内存。 (它是否也应该将数据指针设置为空值是值得商榷的,在这种情况下多次取消分配尝试不会是灾难性的。反驳的论点是这只会掩盖灾难性的编程错误。)

除了由于错误的显式析构函数调用导致 vec 数据明显双重取消分配之外,还很重要的是,用于 vec 初始化的空闲存储区中的向量永远不会被释放或销毁。 (它不能被释放,因为地址丢失了。)在功能齐全的运行时环境中,对于 PODs 的向量是可以的:POD 元素不需要销毁,而运行时 returns 是一个进程的堆当进程退出时到 OS。但是提示很明显:如果元素需要销毁怎么办(想想现在永远不会关闭的数据库连接);并且有独立的实现,其中内存可能不会返回到 OS(哪个 OS?),或者代码被用作 long-运行 服务器的一部分而没有重新考虑。

节目二:

std::vector<int> *vec = new std::vector<int>(10);

这一行在 = 的左侧定义了一个 pointer vec。右侧在自由存储上创建了一个整数向量。该未命名向量的地址用于初始化指针 vec。请注意,与第一个示例一样,对 new 的调用涉及空闲存储区上的两个分配:用于向量本身的(小)内存,以及用于向量数据的(单独的,大)内存"contains".

vec->at(3) = 6;与讨论无关

vec->~vector(); 显式调用 vec 的析构函数。这会释放向量的数据,但不会影响向量本身。后者是不好的,因为 vec 指向的矢量对象是在空闲存储上分配的,也应该被释放。同时执行这两项操作的正确方法是按照另一个答案的建议调用 delete 。 (但上面的讨论适用——在正常运行时,如果程序无论如何结束,int 向量并不重要)。

}:指针vec的范围结束,不会触发任何东西(特别是,它不会释放vec指向的内存,这是这里不好,它不调用向量的析构函数,这在这里很好)。请注意,智能指针的行为会有所不同,并且当它们的作用域结束时,可能会对它们内部持有的原始指针调用 delete。