引言
C/C 作为广泛使用的系统级编程语言,提供了直接操控内存的能力,这也意味着开发者需要对内存管理有深刻的理解。本文旨在深入浅出地讲解C/C 内存管理机制,包括内存分布、动态内存分配与释放、以及内存管理的最佳实践。
内存分布图解
- 栈又叫堆栈–非静态局部变量/函数参数/返回值等等,栈是向下增长的。
- 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。
- 堆用于程序运行时动态内存分配,堆是可以上增长的。
- 数据段–存储全局数据和静态数据。
- 代码段–可执行的代码/只读常量。
C语言中动态内存管理方式
- malloc:
void* malloc(size_t size);
- 功能:malloc函数用于在堆上分配一块连续的内存空间。它接受一个参数,即所需内存的大小(以字节为单位),并返回指向这块内存的指针。
- 初始化:malloc不会对分配的内存进行初始化,内存中的内容是未定义的,可能是之前的值或者全零,具体取决于操作系统。
- 使用场景:当不需要初始化内存或者特定初始化时使用。
- calloc:
void* calloc(size_t num, size_t size);
- 功能:calloc也用于在堆上分配内存,但它接受两个参数,分别是要分配的元素数量和每个元素的大小(以字节为单位)。calloc会确保分配的内存区域中的每个字节都被初始化为零。
- 初始化:与malloc不同,calloc会将分配的内存全部初始化为零,这使得它适合用于数组或结构体等需要初始化为默认值的情况。
- 使用场景:当需要一个清零的内存块时使用,比如初始化数组。
- realloc:
void* realloc(void* ptr, size_t size);
- 功能:realloc用于调整先前通过malloc、calloc或realloc分配的内存块的大小。它接受两个参数,第一个是之前分配的内存的指针,第二个是新的大小(可以比原来大也可以比原来小)。
- 初始化:realloc不涉及初始化新分配的内存部分,如果扩大了内存块,新增的部分通常也是未定义的值。
- 使用场景:当原先分配的内存大小不再满足需求,需要扩大或减小内存空间时使用。需要注意的是,如果减小内存空间,超出新大小的部分数据会被截断。
C 内存管理方式
对于C语言内存管理方式上的一些无法解决的地方和不方便使用的地方,C 进行优化,形成C 的内存管理机制。
new/delete操作内置类型
使用方式:
代码语言:javascript复制// 动态申请一个int类型的空间
int* ptr4 = new int;
// 动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);
// 动态申请10个int类型的空间
int* ptr6 = new int[10];
delete ptr4;
delete ptr5;
delete[] ptr6;
图示规则:
注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用
new[]
和delete[]
。
- 尽量匹配使用
new和delete操作自定义类型
- new和delete相比叫malloc和free在操作自定义类型上最大的区别就是对于自定义类型除了开辟空间,还会调用构造函数和析构函数。
- 对于内置类型几乎操作一样
operator new与operator delete函数
对operator new与operator delete的源码剖析可以发现:
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间
- operator new在全局函数中实际是通过:malloc申请空间
- operator delete在全局函数中实际是通过:free销毁空间
new和delete的实现原理
内置类型
申请的是内置类型的空间,new和malloc,delete和free基本类似 ,不同的地方是:new在申请空间失败时会抛异常,malloc会返回NULL。
自定义类型
- new的原理
- new会首先会调用operator new函数来申请空间(malloc)
- 然后再调用自定义类型的构造函数,在开辟的空间上执行构造函数,完成对象的构造
- delete的原理
- delete会先执行析构函数,将当前对象中的资源进行=清理
- 后调用operaor delete函数进行释放对象创建时开辟的空间(free)
- new T[N]的原理
- 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
- 在申请的空间上执行N次构造函数
- delete[]的原理
- 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
- 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间
定位new表达式(placement-new)
定位new表达式(Placement New Expression),或简称placement new,是C 中一种特殊的内存分配方式,它允许你在已经分配好的内存区域内构造对象。与标准的new操作符不同,定位new不负责内存的分配,而是直接在你指定的内存地址上调用对象的构造函数。这对于实现内存池、重复利用已分配的内存块、在特定内存位置(如共享内存)创建对象等场景非常有用
使用格式
- new (place_address) type或者
- new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表
示例
代码语言:javascript复制我们现在开辟一块与A类相同大小的空间
A* p1 = (A*)malloc(sizeof(A));
代码语言:javascript复制使用定位new对已有的空间p1调用A的构造函数进行初始化
new(p1)A;
注意事项
- 内存管理:使用定位new后,对象的生命周期管理完全由程序员负责。这意味着你不能使用普通的delete来释放这个对象,因为那会试图释放由malloc分配的内存,导致未定义行为。你应该直接调用对象的析构函数,并手动归还内存(如果适用):
A->~A(); // 手动调用析构函数
std::free(p1); // 释放内存
- 内存对齐:确保提供的内存地址是正确对齐的,以便能够容纳特定类型的对象。如果不对齐,可能导致未定义行为。
- 安全性:使用定位new时,你需要确保所指定的内存区域足够大,以容纳完整的对象实例,包括可能的内部对齐填充。否则,可能会覆盖周边内存,引发严重错误。
- 标准库支持:C 标准库提供了一个全局的operator new(void*, std::size_t)重载,它不执行任何实际的内存分配,专门用于定位new表达式。这个重载是固定的,不能被用户自定义版本替代。
常见面试题
malloc/free和new/delete的区别
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。 不同的地方是:
- malloc和free是函数,new和delete是操作符
- malloc申请的空间不会初始化,new可以初始化
- malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可, 如果是多个对象,[]中指定对象个数即可
- malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
- malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
- 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间
后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理