引言:上一篇文章主要介绍了动态内存的函数和基本用法。https://blog.csdn.net/2301_81479880/article/details/166902312?fromshare=blogdetail&sharetype=blogdetail&sharerId=166902312&sharerefer=PC&sharesource=2301_81479880&sharefrom=from_link
本文主要是运用动态内存开辟的一些注意事项,典型错误,经典例题和柔性数组。学好动态内存管理对于后续的学习数据结构是很有帮助的。
目录
1.常见的动态内存错误
1.1对NULL指针的解引用操作
1.2对动态内存空间的越界访问
1.3对非动态开辟的内存使用free
1.4使用free释放一块动态内存开辟的一部分
1.5对同一块动态内存多次释放
1.6动态开辟内存忘记释放(内存泄漏)
2.动态内存管理经典笔试题
2.1第一题
2.2第二题
2.3第三题
3.柔性数组
3.1柔性数组的特点
3.2柔性数组的使用
3.3柔性数组的优势
1.常见的动态内存错误
1.1对NULL指针的解引用操作
void test() { int* p = (int*)malloc(INT_MAX / 4); *p = 20;//如果pd的值是NULL,就会出问题 free(p); }使用malloc开辟内存失败时,返回值为NULL,如果不加判读直接使用,程序就会崩溃,所以在使用完malloc后及时判断,如果p不为空,程序才能继续使用。
1.2对动态内存空间的越界访问
void test() { int i = 0; int* p = (int*)malloc(10 * sizeof(int)); if (NULL == p) { return 1; } for (i = 0; i <= 10; i++) { *(p + i) = i;//当i为10时,越界访问 } free(p); }动态开辟的空间也是有自己使用范围的,超出这个范围就会越界访问,程序错误。
1.3对非动态开辟的内存使用free
void test() { int a = 10;///局部变量创建在栈区上 int* p = &a; free(p); }如果我们不信邪头铁尝试一下,程序直接就崩溃了,我们上一篇文章讲了,free函数用来释放动态开辟的内存,但是局部变量int是创建在栈区,强行使用free释放程序就会崩溃。
1.4使用free释放一块动态内存开辟的一部分
int main() { int* p = (int*)calloc(5, sizeof(int)); if (p == NULL)//开辟失败时返回NULL时 { perror("use malloc");//打印错误信息 return 1; } //使用空间 int i = 0; for (i = 0; i < 5; i++) { *p = i + 1; p++; } //释放内存 free(p);//这里的p不再是起始位置 p = NULL; return 0; }程序执行完后p指向的是下标4,或者是指向中间位置,这个时候使用free5个字节,程序也会崩溃,所以释放空间时,一定要让p指向起始位置。
1.5对同一块动态内存多次释放
void test() { int* p = (int*)malloc(100); free(p); //解决办法: p = NULL把p置为空 free(p);//重复释放 }同一块空间只能释放一次吗,重复释放程序就会报错,如果我们养好良好的代码习惯,每次释放完空间后,把p置NULL,后面的free就不会生效了。
1.6动态开辟内存忘记释放(内存泄漏)
void test() { int* p = (int*)malloc(100);//开辟了100字节的空间 if (NULL != p) { *p = 20; } } int main() { test(); while (1);//死循环 return 0; }当动态开辟的空间忘记释放时,别人是无法使用这块空间的,这就造成了内存泄露
注意:动态开辟的空间一定要以正确的方法释放,如果不释放也要交代清楚,你在代码中进行了动态内存分配,记得释放。
小结:
1.动态申请的内存空间,可以使用free直接释放。
2.如果没有使用free释放,在程序结束时,这个内存也会被操作系统回收。
2.动态内存管理经典笔试题
2.1第一题
#include <stdio.h> #include <stdlib.h> #include <string.h> void 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; }这个代码能运行吗?
我们可以看到,程序的返回值不是0,说明程序在运行中就崩溃了。
解析:
C 语言只有值传递。调用
GetMemory(str)时,传给形参p的是str的一份拷贝,p和str是两个不同的变量。所以
strcpy(NULL, "hello world")就是往地址 0 写数据,属于未定义行为,通常表现为段错误。顺带一提,那块 100 字节的内存在函数返回后就没人持有它的地址了,同时造成内存泄漏。
(注意:
"hello world"连结尾\0共 12 字节,100 字节本身够用,这里不是越界问题,而是指针根本没被赋值。)
2.2第二题
char* GetMemory(void) { char p[] = "hello world"; return p; } void Test(void) { char* str = NULL; str = GetMemory(); printf(str); } int main() { Test(); return 0; }这个代码正确吗,运行结果是什么?
解析:
创建char*类型的str并初始化为NULL,GetMemory函数中返回了p的地址(假设为0x12ff48),由str接收,现在str中存放的就是0x12ff48,可以通过地址打印数组hello world。
看起来没有问题,但是数组p是局部变量,它的生命周期只在GetMemory函数中,出了函数它就被释放掉了,所以str接收的其实是一个已经释放掉的空间,此时它就成为了野指针,强行打印就是非法访问,结果是一堆乱码。
2.3第三题
#include <stdio.h> #include <stdlib.h> #include <string.h> void 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; }解析:
经典错误free了之后再次使用动态开辟的空间,
ree(str)释放的是那块堆内存,str这个变量本身不受任何影响,它依然保存着原来的地址,所以if (str != NULL)永远为真,这个判断毫无意义,反而给了人一种"已经检查过、很安全"的错觉。随后往已经回收的空间内写数据,如果这块空间已经分配出去了,会破坏别人的数据,但也有可能还没有分配出去,程序正常跑完。
当我们free完成后,将str置为NULL,程序就没有问题了。
3.柔性数组
在c99中,结构体的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员。
struct st_type { int i; int a[]; //int a[];有些编译器不支持这种写法 }3.1柔性数组的特点
1.结构中的柔性数组成员前面必须至少⼀个其他成员。
2.sizeof返回的这种结构大小不包括柔性数组的内存。
3. 包含柔性数组成员的结构⽤malloc()函数进性内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
3.2柔性数组的使用
struct S { int n; int arr[];//柔性数组成员,我希望arr开始的时候能存放5个整行,后期arr的空间大小可调整 }; int main() { struct S* ps = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));//24 //创建一个结构体指针来接收 if (ps == NULL) { perror("use malloc"); return 1; } ps->n = 100; int i = 0; for (i = 0; i < 5; i++) { ps->arr[i] = i+1; } //扩容 struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 10 * sizeof(int)); if (ptr != NULL) { //扩容失败 perror("use realloc"); } else { //扩容成功 ps = ptr; ptr = NULL; for (i = 5; i < 10; i++) { ps->arr[i] = i + 1; } } //释放 free(ps); ps = NULL; return 0; }当我们的空间大小需要调整时,通过柔性数组就可以做到。
3.3柔性数组的优势
struct S { int n; int *arr; }; int main() { struct S* ps = (struct S*)malloc(sizeof(struct S)); if (ps == NULL) { perror("use malloc"); return 1; } ps->n = 100; int* ptr = (int*)malloc(5 * sizeof(int)); if (ptr != NULL) { ps->arr = ptr; } else { perror("use malloc"); return 1; } int i = 0; for (i = 0; i < 5; i++) { ps->arr[i] = i+1; } //扩容 struct S* ptr2 = (struct S*)realloc(ps->arr, 10 * sizeof(int)); if (ptr2 != NULL) { //扩容失败 perror("use realloc"); } else { //扩容成功 ps->arr = ptr2; ptr2 = NULL; for (i = 5; i < 10; i++) { ps->arr[i] = i + 1; } } //释放 free(ps->arr); ps->arr = NULL; free(ps); ps = NULL; return 0; }这里使用结构体包含指针的方式也能完成,但是柔性数组有两个优势。
1.方便内存释放:
如果我们的代码是在一个给别人用的函数中,你进行了二次分配内存,并且把整个结构体返还给用户,用户可以通过free释放结构体,但是如果用户不知道结构体内部的成员也要free,就会造成内存泄漏。所以我们把结构体的内存和成员变量要的内存一次性分配好,然后返回一个结构体指针,这样只要做一次free就可以把所有的内存释放。
2.有利于提高访问速度
连续的内存有益于提⾼访问速度,也有益于减少内存碎⽚。