int a = 20; // 在栈空间上分配一个 int 类型的变量,开辟四个字节,并将其初始化为 20
char arr[10] = {0}; // 在占空间开辟10个字节的连续空间
上述方法特性:
动态分配内存意味着在程序运行时,根据需要分配一块内存空间,而不是在编译时静态地分配。这为程序提供了更大的灵活性,允许根据实际需求分配和释放内存
语法:
void* malloc(size_t size);
这里的 size 是要分配的内存块的字节数,函数返回一个指向新分配内存起始位置的指针。这个指针的类型是 void*,因此通常需要将其强制转换为所需的类型。
size_t 是一种用于表示内存块大小、数组索引和对象大小等的无符号整数类型。在C语言中,它是标准库 <stddef.h> 中定义的。通常,size_t 被用作与内存相关的函数的参数和返回类型,以确保能够表示所需的内存大小。
size_t 在malloc语法中表示要分配的内存块的字节数。由于内存的大小通常不能为负数,因此使用无符号类型 size_t 是合适的选择。这样可以确保能够表示大于等于零的整数值,以表示内存的大小。
你可以使用 %zu 格式说明符来打印 size_t 类型的值
calloc也用来动态内存分配,但 calloc 提供了一个额外的参数,用于指定要分配的元素的数量和每个元素的大小,而malloc只有分配的内存块的字节数。语法:
void* calloc (size_t num,size_t size);
calloc 的功能是在内存中分配 num * size 字节的空间,并将分配的每个字节初始化为零(与malloc的区别),返回的指针指向新分配的内存块的起始位置,或者如果分配失败,则返回 NULL。
所以如果我们对申请的内存空间的内容要求初始化,那么可以很?便的使?calloc函数来完成任务
有时会我们发现过去申请的空间太小了,有时候我们?会觉得申请的空间过大了,那为了合理的分配内存,我们?定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。用于更改之前由 malloc、calloc 或 realloc 分配的内存块的大小。它可以用于调整内存块的大小,以便适应更大或更小的需求。
语法:
void* realloc(void* ptr,size_t size);
realloc在调整内存空间的时候存在两种情况:
<1>原空间地址后边有足够的扩容空间
这种情况可以直接扩展内存,会在原有内存之后直接追加空间,原来的数据也不会发生变化(包括数据和数据地址),返回的还是原地址
<2>原空间地址后边没有足够的需要的空间
这种情况,会比第一种情况耗损更多(两者相比,但是效率还是较快的),会在堆空间上找一个合适大小的连续空间来使用,将原有的数据全部复制到新的更大的空间,并释放掉原空间,最后返回新的空间的内存地址(函数会自动处理释放原空间,不需要再去free)
需要注意的是,realloc 的返回值可能与之前的指针相同,也可能是一个新的指针。因此,在使用 realloc 后,最好将其返回值赋给原指针,以确保在调整大小时不会丢失原始指针。
free函数是专门用来做动态内存的释放和回收的,函数原型:
void free (void* ptr);
malloc和free都声明在 stdlib.h 头?件中。
注:在每次释放以后,将ptr指针置空,即
ptr = NULL;
,这一步可以预防悬垂指针的问题。悬垂指针是指在释放了内存块后,仍然保留对该内存块的指针,这样可能导致一些未定义行为或错误的操作。
当你使用 free 释放内存块后,该内存块不再属于你的程序,而是被系统回收。如果你保留对已经释放的内存的指针,并尝试在之后的代码中访问或修改它,可能会导致以下问题:
- 悬垂指针引发未定义行为: 尝试使用已经释放的内存块可能导致未定义行为,因为系统可以随时重新分配或修改该内存块。
- 难以调试和定位问题: 悬垂指针问题可能不会立即导致程序崩溃,但可能在后续的代码中引发难以预测的错误。这样的问题可能很难调试和定位。
- 通过将指针 ptr 设置为 NULL,你可以确保在释放内存后,任何对指针的操作都会导致空指针引用,而不是悬垂指针引用。这样可以避免一些潜在的问题,并使代码更加健壮。
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20; // 如果内存开辟失败,p的值是NULL,就会有问题
free(p);
}
内存分配失败的处理: 在调用 malloc 后,应该检查分配是否成功。如果内存分配失败,malloc 返回 NULL,而在代码中并没有检查 p 是否为 NULL。在分配大量内存时,可能会导致分配失败,因此最好在分配后进行检查。
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
exit(EXIT_FAILURE);
}
for(i=0; i<=10; i++)
{
*(p+i) = i; // 当i是10的时候越界访问 ,下标为10是第11个数,但是只开辟了10个int的空间大小
}
free(p);
}
循环条件 i <= 10 会导致在第11次循环时访问数组越界,这可能导致不可预测的结果,包括程序崩溃或数据损坏。改为 i < 10
void test() {
int a = 10;
int *p = &a;
free(p);
}
这段代码试图释放一个不是由 malloc、calloc 或 realloc 分配的内存块。在这里,变量 a 是在栈上分配的局部变量,不应该使用 free 函数进行释放。
free 函数的目的是释放由动态内存分配函数(如 malloc、calloc、realloc)分配的内存块。这些内存块的生命周期延伸到程序员通过 free 明确释放它们。对于栈上分配的局部变量,它们的生命周期由程序的执行上下文和作用域管理,而不需要显式地使用 free 进行释放。
变量 a 在栈上分配,而动态分配的内存空间通常位于堆上。
栈区和堆区的区别:
- 栈(Stack):
由编译器自动管理。
存储局部变量、函数参数和函数调用信息。
具有快速的分配和释放内存的特性,但生命周期受到函数的调用和返回的影响。
变量在栈上分配时,会在离开其作用域时自动释放。- 堆(Heap):
由程序员手动管理或通过垃圾回收器进行管理。
用于存储动态分配的内存,需要显式调用函数(如 malloc、calloc、realloc)来分配和释放内存。
具有更灵活的生命周期,内存可以在程序的任何地方分配和释放,生存周期不受函数的调用和返回的限制。
void test()
{
int *p = (int *)malloc(100);
p++;
free(p); // p不再指向动态内存的起始位置
}
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p); // 重复释放
}
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
在 test 函数中,通过 malloc 分配了一块内存,但没有在函数结束时调用 free 来释放这块内存。这意味着程序在运行时无法再访问该内存块的指针,导致内存泄漏。如果这个问题在一个循环或长时间运行的程序中出现,内存泄漏可能会导致程序占用越来越多的内存,最终耗尽系统的可用内存。