今天首先很讨厌自己学了也有一阵C++了但是感觉如果让我说的话还是支支吾吾不知从何说起。感觉就是所有关键词知识点都在抢占大脑输出通道,没有逻辑章法。

第二个讨厌的就是malloc失败了假装自己没事,原因就是只返回一个void*类型,还得我们自己做一次检查。就像买了房子只给了钥匙,然后发现这钥匙是NULL用不了。

先倒出来脑子里有的,再一点点整理。

new/delete和malloc/free都可以进行内存分配和释放,区别在于new/delete是C++里的一个运算符,而malloc则是C库里的一个函数。这里先点一下题,这个性质也就决定了,new既然作为内置实现的一个运算符,任何new过程中出现的失败都会使得new这个操作失败并抛出异常,默认会抛出 std::bad_alloc 异常。而malloc作为函数,它只会返回一个void*指针,不会抛出异常操作。至于这块地址能不能用,用户还得自己检查一遍。

malloc和free:

malloc和free是C语言内存管理的基石。我在本科的时候写C最讨厌的就是这两位,因为抄起来相对较长,malloc这个名字看起来又不是什么正经英语。malloc,全名:memory allocation(这下看起来正经了)和free来自C语言,C++也就继承了这两个函数,这两位隶属于C标准库。一会再整理new和delete,那两位是靠malloc来实现的。

为什么不叫memaloc?之前一直以为是ma+loc,今天定睛一看嘿原来是m+alloc

当调用malloc(size_t size)时,操作系统和运行时库会进行一系列复杂的操作。具体如下:malloc首先会从进程的堆中寻找足够大的连续空闲内存块。如果找到了合适的,它就会把这个内存块标记为已分配,然后,返回指向内存块起始地址的指针。

但是,作为C++程序员,以及那群面试官中老登们,成功固然令人欣喜,失败则意味着能有更多考察我的知识点()。malloc什么时候会失败?

第一个原因很简单,就是内存不足。进程请求的内存总量,超过了系统能提供的物理内存和虚拟内存之和时,分配就会失效。

第二个也很好想到,就是连续的内存不够,内存太碎片了,这时候也会失效。

第三个是关于地址空间的。进程的虚拟地址空间如果全占完了,即使物理内存还剩很多也没用。(32位系统进程是4GB的虚拟地址空间)。

第四个是内存分配器损坏,由于程序错误比如缓冲区溢出,重复释放等(这个有疑问)。

当空间不够时,malloc会返回NULL来表示分配失败。我们必须检查这个返回值。对空指针解引用是未定义行为(段错误)。

与malloc相对应的就是free。free(void* ptr)负责释放之前分配的内存。由内存分配器将被释放的内存块标记为空闲。

free也会失败。首先free不返回任何值,这里的失败主要指的是程序的行为出错。最经典的就是重复释放,对一块内存多次调用free也会导致未定义行为。内存分配器会认为这块内存已经被释放,再次释放时可能破坏内存管理数据结构。

什么是破坏内存管理的数据结构:

许多分配器在分配的内存块前面存储元数据(大小、状态、链接指针等)。

当程序第一次调用free释放内存时,内存分配器会在这块内存区域的头部或尾部找到它自己之前记录的管理数据。这些数据至少包括这块内存的大小,以及一个表示该内存是否已被分配的状态标志。分配器会将这个状态标志改为“空闲”。之后,分配器通常会把这整块内存(包括用户数据区域和管理数据区域)视为一个空闲块,并将其添加到一个用于跟踪所有空闲内存的数据结构中。这个数据结构常见的形式是链表或树。例如,如果是链表,分配器会在这个空闲块的管理数据区写入指向链表中下一个空闲块和上一个空闲块的指针。

这里会出现几种破坏情况:

第一种情况是,在两次free之间,没有其他代码改动过这块内存。此时管理数据中的状态可能已经是“空闲”。当分配器试图再次将其标记为“空闲”时,这个操作本身可能无意义,但也可能破坏其他用于内部记账的标志。更重要的是,分配器会再次执行“将空闲块插入数据结构”的操作。这会导致同一个内存块在空闲链表或树中出现两次。当后续进行内存分配时,分配器可能会把这个块分配出去,但它在链表中还有一个副本。之后对这个副本的任何操作,比如尝试从链表中移除它,都会访问到可能已被用户数据覆盖的内存区域(现在它是已分配状态),从而导致读取或写入错误的内存地址。

第二种更常见且更复杂的情况是,在两次free之间,这块内存可能已经被重新分配出去,用于其他用途。新的数据可能已经写入了原来的用户区域,甚至可能覆盖了部分原始的管理数据(如果新的使用方式允许写入足够多的数据)。当第二次free执行时,分配器读取的管理数据是无效的或被篡改过的。例如,它读到一个被用户数据篡改过的“内存块大小”值。分配器会根据这个错误的大小值去计算哪里是这块内存的尾部,以及哪些是相邻的内存块,进而去修改那些它认为是“相邻块”头部的管理数据。这直接破坏了其他正在使用的内存中的数据,而这些数据可能属于程序的变量、对象或其他关键结构。

第二种释放失败的原因我称之为多管闲事。比如试图释放栈上的变量、全局变量或通过 new 分配的内存。

第三种就是完全想当然地去释放。如果我释放的不是原始分配给我的指针,而是我对这个指针做了点运算,比如,我就想释放我申请内存的后半部分。这么想想好像非常符合情理。但最后释放可能就会出错。因为内存分配器依赖这个原始指针精确值来定位内存块的管理信息。任何指针运算都会破坏这个前提,导致分配器操作错误的内存地址,进而损坏内存管理数据或用户数据。

关于返回值以及void*

malloc 返回一个 void* 类型的指针,void* 字面意思是”指向 void 的指针”,但更准确的理解是”通用指针”或”无类型指针”。

内存本身只是字节序列,没有内在的类型信息。编译器需要知道如何解释这些字节。例如,同样的 4 个字节,可以被解释为整数、浮点数、4 个字符,或者只是一个内存地址。void* 表示”这里有一块内存,但不知道它应该被解释为什么类型”。

这时候理解了当时为什么觉得抄malloc函数很麻烦:比如

进行 int* ptr = (int*)malloc(sizeof(int)); 时,发生了两件事:

  1. malloc 分配了一块适当大小的原始内存,返回 void*
  2. 还要通过类型转换告诉编译器这块内存当作整数来使用

一句里出现了三个int,让本科时候的我抄得一头雾水。

关于new和delete,我后面专门开一篇来学习整理,因为那个感觉和自定义类型相关性较高,重点在对象的创建和释放上。底层还是malloc这些。