小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc


本文重点

  • 1. 动态内存函数的介绍
    • 1.1 malloc和free
      • 1.1.1 malloc
      • 1.1.2 搭配使用的free
    • 1.2 calloc
    • 1.3 realloc
  • 2. 常见动态内存错误
    • 2.1 对动态开辟的空间越界访问
    • 2.2 对NULL指针解引用
    • 2.3 对非动态开辟的内存用free释放
    • 2.4 使用free释放动态开辟内存的一部分
    • 2.5 对同一块内存多次释放
    • 2.6 动态开辟的内存忘记释放(内存泄漏)
  • 3. 经典笔试题
    • 3.1 笔试题1
    • 3.2 笔试题2
    • 3.3 笔试题3
    • 3.4 笔试题4
  • 4. C/C++程序的内存开辟

引:为什么存在动态内存分配?
我们已经掌握的内存开辟方式有:
int a = 0; //4byte int arr[10] = { 0 }; //40byte

这样的开辟方式—
  1. 开辟空间大小是固定的,想大不能大,想小不能小;
  2. 定义数组时,必须给定大小,然而有时我们需要的空间大小在编译时才会知道。
小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc
文章图片

由此引入动态内存开辟,这就要学习动态内存函数。
正文开始@边通书
1. 动态内存函数的介绍 1.1 malloc和free 1.1.1 malloc
C语言提供了一个动态内存开辟的函数:(头文件:#include
void* malloc (size_t size);

  • void*:这块内存是为谁申请的也不知道,返回什么类型也不合适,那就返回通用类型。
  • size:要申请的字节数。
作为malloc函数的使用者,我很清楚我申请的内存空间要来做什么,在使用时要做强制类型转换:
int* ptr = (int*)malloc(10 * sizeof(int));

功能:在堆区上申请size个字节的空间,并返回堆区上的起始地址。
  • 若开辟成功,返回一个指向开辟好空间的指针;
  • 若开辟失败,则返回空指针NULL
??因此,malloc的返回值一定一定要做检查!!!
1.1.2 搭配使用的free
C语言提供了另外一个函数用于做动态内存释放和回收的:
(头文件:#include)
void free(void* ptr);

功能:把ptr所指向的空间还给操作系统
注:
??1. 若参数ptr指向的空间不是动态开辟的,则free函数的行为是标准未定义的。
??2. 若参数ptrNULL空指针,即free(NULL); okay,只不过什么都不干。
上代码感受它们的使用:
思考:free(ptr); 后的ptr == NULL; 是否有必要?
#include #includeint main() { //申请空间 int* p = (int*)malloc(10 * sizeof(int)); if (p == NULL) {return -1; } //使用 int i = 0; for (i = 0; i < 10; i++) {*(p + i) = i; } //释放ptr指向的这段内存 free(p); //p = NULL; //是否有必要? return 0; }

调试起来:
小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc
文章图片

可以看到ptr指向的这段空间虽然释放了,但ptr依然指向这段空间(也就是说free根本就不会使ptr发生改变,也不会将ptr主动置空)。
小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc
文章图片

此时ptr是野指针,很危险,就一定要ptr == NULL; ,让ptr永远也找不到这段空间。
??3. 释放ptr指向的内存空间后free(ptr); ,一定要将ptr置空ptr==NULL
1.2 calloc C语言还提供了一个calloc函数,也可用来动态内存分配:
(头文件:#include)
void* calloc (size_t num, size_t size);

  • void*:空类型,为了适应各种指针类型,以便正确解引用
  • num:要申请的元素个数
  • size:元素大小
功能:为大小为sizenum个元素开辟一块空间,并将每一个字节都初始化为0.
  • 若开辟成功,则返回开辟好空间的起始地址
  • 若开辟失败,则返回空指针NULL
??因此,与malloc一样,对于calloc的返回值也一定一定要做检查!!!
上代码感受:
#include #include #include #includeint main() { //申请10个int的空间 int* p = (int*)calloc(10, sizeof(int)); if (p == NULL) {printf("%s", strerror(errno)); //错误码翻译出错误信息 return -1; } //申请成功 int i = 0; for (i = 0; i < 10; i++) {printf("%d ", *(p + i)); //打印 } //释放 free(p); p = NULL; return 0; }

运行+调试结果:
小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc
文章图片

演示申请空间失败的情况,申请空间过大:
小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc
文章图片

对比malloc和calloc:
??malloc:只负责在堆区申请空间,并返回起始地址,不会初始化空间
??calloc:在堆区申请空间,初始化为0,并返回起始地址
以后也很简单,我要初始化我就用calloc,不想初始化我就用malloc.
1.3 realloc 有时我们发现过去申请的内存太小/过大了,为了合理使用内存就一定要对内存大小进行调整。
realloc函数的出现,让动态内存管理更加灵活.(头文件:#include)
void* realloc (void* ptr, size_t size);

  • ptr:要调整的内存地址
  • size:调整后的新大小
  • void*:调整后的内存起始地址
??realloc在调整内存空间时存在两种情况:
情况1:原空间后有足够大的空间
小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc
文章图片

情况2:原空间后没有足够大的空间
小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc
文章图片

这个函数在调整原内存空间大小的同时,还会将原内存中的数据移动到新的空间中去。
上代码感受:
#include #include #include #includeint main() { int* p= (int*)malloc(10 * sizeof(int)); if (p == NULL) {printf("%s\n", strerror(errno)); return -1; } //申请成功 //使用 //... //空间不够,增加空间至20int int* ptr = (int*)realloc(p, 20 * sizeof(int)); //用新指针来接收 if (ptr == NULL) {return -1; } else {p = ptr; } int i = 0; for (i = 0; i < 20; i++) {*(p + i) = i; } //打印 for (i = 0; i < 20; i++) {printf("%d ", *(p + i)); } //释放 free(p); p = NULL; return 0; }

??注: 为什么这里一定要用新指针来接收:
//空间不够,增加空间至20int int* ptr = (int*)realloc(p, 20 * sizeof(int)); //用新指针来接收

//若仍用旧指针p来接收 int* p = (int*)realloc(p, 20 * sizeof(int));

不可,因为,若内存申请失败返回NULL,则原来p指向的空间也找不到了。
2. 常见动态内存错误 下面再次强化常见动态内存分配的错误点,以后都要避免奥。
2.1 对动态开辟的空间越界访问
#include #includevoid test() { int* p = (int*)malloc(10 * sizeof(int)); if (p == NULL) {return ; } //使用 int i = 0; for (i = 0; i <= 10; i++) {*(p + i) = i; } //释放 free(p); p = NULL; } int main() { test(); return 0; }

运行结果:挂了
小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc
文章图片

2.2 对NULL指针解引用
void test() {int *p = (int *)malloc(INT_MAX/4); *p = 20; free(p); }

直接这样写代码是有风险的,若malloc申请内存空间失败,则返回NULL*p = 20; 则是对空指针解引用。
这就要求我们在申请空间后一定要进行判断,先判断,再使用。
?更正— 在申请空间后一定要进行判断
void test() {int *p = (int *)malloc(INT_MAX/4); if(p == NULL) {return; } *p = 20; free(p); }

2.3 对非动态开辟的内存用free释放
void test() {int a = 10; //栈 int *p = &a; free(p); //ok? }

运行结果 — 又挂了
小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc
文章图片

2.4 使用free释放动态开辟内存的一部分 上代码:
#include #includeint main() { int* p = (int*)malloc(10 * sizeof(int)); if (p == NULL) {return -1; } //使用 int i = 0; for (i = 0; i < 5; i++) {*p++ = i; } //释放 free(p); p = NULL; return 0; }

运行结果—又挂了
小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc
文章图片

小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc
文章图片

?传给free的,必须是动态开辟内存的起始位置。
2.5 对同一块内存多次释放
#include #includeint main() { int* p = (int*)malloc(10 * sizeof(int)); free(p); free(p); //重复释放 return 0; }

运行结果—又挂了
小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc
文章图片

?更正:在free释放p指向的空间后,记得将p置空p = NULL;
#include #includeint main() { int* p = (int*)malloc(10 * sizeof(int)); free(p); p = NULL; free(p); //p为NULL,okay,只不过什么也不做 p = NULL; return 0; }

2.6 动态开辟的内存忘记释放(内存泄漏) 在堆区上申请的动态内存空间有两种回收方式:
  1. 主动free
  2. 当程序退出时,申请的空间也会回收
思考:
#include #includeint main() { int* p = (int*)malloc(10 * sizeof(int)); if (p == NULL) {return -1; } //使用 //忘记释放了 getchar(); return 0; }

运行起来,光标闪烁,getchar(); 等待接收字符,程序不结束,如果这段空间不被释放,造成了内存泄漏。
忘记释放不再使用的动态开辟的空间会造成内存泄漏—这块空间你又不用你又不释放,别人也无法使用,如果服务器7*24小时的在跑,那非常可怕,内存会被一点一点耗干。
?动态开辟的内存空间,一定要释放,且要正确释放。
3. 经典笔试题 3.1 笔试题1 上代码思考:
#include #include #includevoid GetMemory(char *p) { p = (char *)malloc(100); } void Test(void) { char *str = NULL; GetMemory(str); strcpy(str, "hello world"); printf(str); //运行结果? } int main() { Test(); return 0; }

运行结果—挂了
小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc
文章图片

解析:
小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc
文章图片

1°程序崩溃
str传给p是值传递,pstr的一份临时拷贝,所以当malloc在堆上开辟的空间起始地址放在p中(即p指向了这段空间时),不会影响strstr仍为NULL
②当str为空时,strcpy想把"hello world"拷贝到str所指向空间时,程序崩溃,因为NULL指向的空间是不能直接访问的(还记得strcpy模拟实现的时候进去就要断言吗)。
2°内存泄漏
更可怕的是想释放都没办法释放,函数调用完,p随之销毁(即malloc开辟出来的空间的起始地址丢失),这段空间无法找到,无从释放。
? 更正: 传址调用
void GetMemory(char** p) { *p = (char *)malloc(100); } void Test(void) { char *str = NULL; GetMemory(&str); strcpy(str, "hello world"); printf(str); //运行结果? //释放 free(str); str = NULL; }

运行结果:
小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc
文章图片

??注:
这里大家可能有疑惑在printf(str);
char* p = "hello world"; printf("hello world"); printf(p); //okay!

3.2 笔试题2 上代码思考:
#include #includechar* GetMemory(void) { char p[] = "hello world"; return p; } void Test(void) { char *str = NULL; str = GetMemory(); printf(str); //运行结果? } int main() { Test(); return 0; }

运行结果 — 打印随机值
小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc
文章图片

解析 — 事实上,这都属于返回栈空间地址引发的问题
小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc
文章图片

像这样返回栈空间地址就是典型的造成野指针的原因之一。
【小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc】与之相似的错误代码演示:
#includeint* test() { int n = 10; return &n; }int main() { int* p = test(); printf("%d\n", *p); return 0; }

预测运行结果:可能打印随机值。
运行结果—虽然报了一个警报,但还是打印了10
小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc
文章图片

这可能会让你震惊,这里打印10也并非巧合,如果还没调用其他函数,销毁的空间未被覆盖掉,确实可能访问到10.
我当然也可以让它不是10,这里随便打印一个"hehe":
int main() { int* p = test(); printf("hehe\n"); printf("%d\n", *p); //运行结果? return 0; }

再来运行,运行结果— 随机值
小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc
文章图片

解析—画图简述函数栈桢创建与销毁:
小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc
文章图片

这些讨论,都是为了说明返回栈空间地址时的这种错误。
3.3 笔试题3
#include #include #includevoid GetMemory(char **p, int num) { *p = (char *)malloc(num); } void Test(void) { char *str = NULL; GetMemory(&str, 100); strcpy(str, "hello"); printf(str); } int main() { Test(); return 0; }

小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc
文章图片

问题:内存泄漏!!
?要记得释放空间:
free(str); str = NULL;

3.4 笔试题4
#include #include #includevoid Test(void) { char *str = (char *)malloc(100); strcpy(str, "hello"); free(str); if (str != NULL) {strcpy(str, "world"); printf(str); //运行结果? } }int main() { Test(); return 0; }

有结果也不对,要明白编译器是不能查出所有错误的,不然要你程序猿干嘛。
小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc
文章图片

解析
小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc
文章图片

因此,还是要记得free后,将str置空。
free(str); str = NULL;

4. C/C++程序的内存开辟 小边的强势总结C语言篇|动态内存函数+经典笔试题@动态内存管理---malloc +free + calloc + realloc
文章图片

C/C++程序内存分配的几个区域:
  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行 结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限,因此会有栈溢出的情况。
    栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、 返回地址等。
  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS(操作系统)回收 。分配方式类似于链表。
  3. 数据段/静态区(static):存放全局变量、静态数据。程序结束后由系统释放。
  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
有了这幅图,我们就可以更好的理解static关键字修饰局部变量的例子了。
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁,所以生命周期变长。
下篇文章将介绍柔型数组。
未完待续@边通书

    推荐阅读