当调用 delete[] X 命令时,到底发生了什么以及谁负责



我试图找出谁是组件或模块(可能属于操作系统?),它们在应用程序或进程运行时实际执行这些操作,并专门运行命令delete[] X

的问题是在我阅读delete[] X后提出的,我知道编译器负责(根据其实现)知道要删除多少个X对象。但是,编译器在运行时并不"活跃"!我的意思是,在编译时,编译器不知道用户在新命令中需要多少内存,因此在删除时也不知道,那么当程序实际运行时在运行时实际发生了什么?

我读到的答案之一是所谓的运行时系统,它是什么? 它是否连接到 CPU - 因为 CPU 最终会执行命令......或者也许是操作系统?

我看到的另一个答案是它"由系统的分配器完成"(delete[] 如何知道要删除多少内存?) - 再次这个组件(操作系统、CPU)在哪里?

编译器负责生成在需要时delete的代码。它不需要在发生时运行。生成的代码可能是对例程的函数调用,该例程执行以下操作:

void delete_arr(object *ptr)
{
    size_t *actual_start = ((size_t *)ptr) - 1;
    int count = *actual_start;
    for (int i = count-1; i >= 0; i--)
        destruct(ptr[i]);
    free(actual_start);
}

当调用new[]时,它实际上保存了分配的内存旁边的元素数量。当您调用delete[]时,它会查找数字计数,然后删除该数量的元素。

提供这些功能的库称为C++标准库或C++运行时环境。该标准没有说明运行时的构成,因此定义可能会有所不同,但要点是它是支持运行C++代码所需的内容。

运行时

C++间接地)使用操作系统原语来更改运行程序的进程的虚拟地址空间。

阅读有关计算机体系结构、CPU 模式、操作系统、操作系统内核、系统调用、指令集、机器代码、目标代码、链接器、重定位、名称重整、编译器、虚拟内存的更多信息。

在我的 Linux 系统上,new(由C++标准库提供)通常构建在 malloc(3)(由 C 标准库提供)之上,它可以调用 mmap(2) 系统调用(在内核内实现),从而更改虚拟地址空间(通过处理 MMU)。delete(来自C++标准库)通常构建在 free(3) 之上,它可以调用 munmap(2) 系统调用来改变虚拟地址空间。

事情在细节上要复杂得多:

  • new在分配了内存后调用构造函数malloc

  • delete在释放内存之前调用析构函数free

  • free通常会将释放的内存区域标记为可由将来的malloc重用(因此通常不会释放带有munmap的内存

  • 所以malloc通常会在从内核请求更多地址空间(使用 mmap )之前重用以前释放的内存区域

  • 对于数组new[]delete[],内存区域包含数组的大小,构造函数(new[])或析构函数(delete[])在循环中被调用

  • 从技术上讲,当您SomeClass*p = new SomeClass(12);编码时,首先使用 ::operator new(调用 malloc)分配内存,然后使用 12 作为参数调用 SomeClass 的构造函数

  • 当你delete p;编码时,SomeClass的析构函数被调用,然后使用::operator delete释放内存(它调用free

顺便说一句,Linux 系统是由自由软件组成的,所以我强烈建议您在机器上安装一些 Linux 发行版并使用它。因此,您可以研究内核的libstdc++(标准C++库,它是GCC编译器源代码的一部分,但由您的程序链接),libc(标准C库)的源代码。你也可以跟踪(1)你的C++程序和进程,以了解它正在做什么系统调用。

如果使用 GCC,您可以通过使用 g++ -Wall -O -fverbose-asm -S foo.cc 编译foo.cc C++源文件来获取生成的汇编程序代码,从而生成foo.s汇编程序文件。您还可以使用g++ -Wall -O -fdump-tree-gimple -c foo.cc获得编译器内部中间Gimple内部表示的一些文本视图(您将获得一些foo.cc.*.gimple,也许还有许多其他GCC转储文件)。您甚至可以使用 GCC MELT 工具在 Gimple 表示中搜索某些内容(我设计并实现了其中的大部分;使用 g++ -fplugin=melt -fplugin-arg-melt-mode=findgimple )。

标准C++库具有内部不变量和约定,C++编译器负责在发出汇编代码时遵循它们。因此,编译器及其标准C++库是密切合作共同设计和编写的(C++库实现中的一些肮脏技巧需要编译器支持,也许通过编译器内置等......这不是特定于C++:Ocaml 人员还共同设计和共同实现 Ocaml 语言及其标准库。

C++运行时系统在概念上有几个层:C++标准库libstdc++,C标准库libc,操作系统(以及底部的硬件,包括MMU)。所有这些都是实现细节,C++11语言标准并没有真正提及它们。

可能会

有所帮助

  • 对于每次调用全局 ::运算符 new(),它将采用传递的对象大小并添加额外数据的大小
  • 它将分配在上一步中推断出的大小的内存块
  • 它将偏移指向未被额外数据占用的块部分的指针,并将该偏移值返回给调用方

:运算符 delete() 将反向执行相同的操作 - 移动指针, 访问额外的数据,释放内存。

并且通常在删除分配给堆中的对象数组时使用 [] 使用。据我所知,新的 [] 还在分配的内存的开头添加额外的数据,其中存储有关删除 [] 运算符的数组大小的信息。它也可能是有用的:

换句话说,在一般情况下,new[] 分配的内存块在实际数据前面有两组额外的字节:以字节为单位的块大小(由 malloc 引入)和元素计数(由 new[] 引入)。第二个是可选的,如您的示例所示。第一个通常始终存在,因为它是由malloc无条件分配的。即,即使您只请求 20 个字节,您的 malloc 调用也会物理分配超过 20 个字节。malloc 将使用这些额外的字节来存储以字节为单位的块大小。 ...

new[] 从运算符 new[] 请求的"额外字节"并不用于"存储分配内存的大小",正如您似乎认为的那样。它们用于存储数组中的元素数量,以便 delete[] 知道要调用多少个析构函数。在您的示例中,析构函数是微不足道的。没有必要打电话给他们。因此,无需分配这些额外的字节并存储元素计数。

它分两个阶段工作。一个是编译器做看起来很神奇的事情,然后堆做看起来也很神奇的事情。

也就是说,直到你意识到诀窍。然后魔法就消失了。

但是拳头让我们回顾一下当你做new X[12]时会发生什么;

编译器在概念上编写的代码在概念上如下所示:

void* data = malloc(12 * sizeof(X))
for (int i=0; i != 12; ++i) {  
  X::Ctor(data);
  data += sizeof(X); 
}

其中Ctor(void* this_ptr)是一个秘密函数,用于设置this指针调用 X 的构造函数。在本例中为默认值。

所以在破坏时,我们可以撤消它,如果我们能把 12 个藏在容易找到的地方......

我猜你已经猜到了哪里了...

任何地方!真的!例如,它可以在对象开始之前存储。

第一行变为以下 3 行:

void* data = malloc((12 * sizeof(X)) +sizeof(int));
*((int*)data) = 12;
data += sizeof(int);

其余的保持不变。

当编译器看到delete [] addr时,它知道 4 个字节在addr之前可以找到对象计数。它还需要调用free(addr - sizeof(int));

顺便说一下,这本质上与mallocfree使用的技巧相同。至少在过去我们有简单的分配器的日子里是这样。

当您使用 new 关键字时,程序会从堆上的操作系统请求一个内存块来保存对象。 返回指向该内存空间的指针。 在不使用new的情况下,编译器将对象放在堆栈上,在编译期间,这些对象的内存在堆栈上对齐。 当不再需要 new 创建的任何对象时,都需要将其删除,因此重要的是指向堆块的原始指针不会丢失,以便您可以对其调用 delete。 当您使用delete[]时,它将释放数组中的所有块。 例如,如果您创建了 char* anarray = new char[128] delete[],则使用 delete[],如果您string *str = new string()创建了 delete[],如果您创建了 delete,则使用 delete,因为字符串称为对象,而 char* 是指向数组的指针。

编辑:某些对象重载 delete 运算符,以便您的对象可以支持正确释放动态内存,因此对象可以负责确定它的行为

相关内容

最新更新