绪论
书接上回,本章来到动态内存管理,这章的知识相较于结构体来说来简单一点,但是有许多地方需要注意不能马虎,并且该章的知识也比较重要,通过名称可以知道动态的内存管理,这样就可以对内存有一个很方便的管理方法!
所以安全带系好,发车啦(建议电脑观看)。
附:红色,部分为重点部分;蓝颜色为需要记忆的部分(不是死记硬背哈,多敲);黑色加粗或者其余颜色为次重点;黑色为描述需要
思维导图:
要XMind思维导图的话可以私信哈
目录
1.动态内存分配存在的意义
为什么要有动态内存管理?
下面通过对比来解释:
在我们一般申请内存的时候都是通过创建变量类型来申请的空间
如:int(4byte)、char(1byte)、int [ ]、结构体、联合体、枚举......
而这些内存的空间当我们创建好后他就固定死了,就比较的局限(死板)。
现在C语言提供了一种方式就是动态内存管理(函数)当我们需要时就创建一定的空间,当空间不够后又可以再次的补充其空间,当然当空间过大也可以减小空间的大小
2.动态内存函数
2.1malloc
知识点:
对于malloc函数来说他的类型是void *malloc(size_t);
用法:申请一个连续可用的size字节大小的空间,开辟成功时返回指向这块空间的指针
头文件:#include<stdlib.h>
所以一般的用法是要将返回来的void * 强转成自己所需要的指针类型
如:
int main() { //当你要开辟多个以整型大小为基础的空间时 int * ptr = (int *)malloc(sizeof(int) * 10);//当然你也可以直接在()内写成“40”byte //对于上面因为你需要开辟的是整形个大小的空间所以最后将返回的指针强转成整形指针类型 //并且ptr也要是整形指针这是你所要用的,你甚至可以把它想像成开辟了一个大小为10的整形数组 return 0; }
为什么这样写我已经写上了注释
细节:
既然是申请那就有可能会失败,所以当申请失败的时候系统会返回一个空指针(NULL)
所以在我们写完后要加上一个判断,判断其是否申请成功
if (ptr == NULL) { printf("%s\n", strerror(errno));//打印错误 //perror("ptr");//打印错误 return 0; }
- 不能开辟0byte的空间
- 判断空间是否申请成功
- 用free释放申请好的空间(并且将其指针置为空)
练习
用动态内存分配的空间来打印打印1~10:
分析:因为最终传回来的是指针所以就可以通过指针的方法来进行内存的访问,并且所开辟的大小也是以整形大小一个个开辟的,所以直接+1即可
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<stdlib.h> int main() { //当你要开辟多个以整型大小为基础的空间时 int* ptr = (int*)malloc(40); if (ptr == NULL) { perror("ptr");//打印错误 return 0; } for(int i = 0; i<10 ; i++) { *(ptr+i) = i +1; printf("%d ",*(ptr + i)); } free(ptr);//释放向内存中借的空间 ptr = NULL; return 0; }
2.2free
知识点:
函数:void free (void* ptr);
当我们向操作系统借了的空间所以用完了后我们需要将其归还(若不归还的话对这空间再你没有结束程序的过程中都处于被借的状态也就浪费了这片没有用的空间)
用法:释放所传递过来的动态内存开辟的指针所指向的空间
细节:
对于free所释放的空间,虽然将其所存的内存归还给了操作系统,但ptr所指地址并不会被其改变,所以为了防止我们后面不小心还使用ptr这个地址指向的被已经释放的内存,所以需要再将ptr置为NULL
free(ptr); ptr = NULL;
- 当所传递过来的是NULL时,free什么都不做。
- 不能释放不是动态内存开辟的空间
2.3callloc
知识点:
void* calloc (size_t num, size_t size);
该函数的意义和malloc一样,都是用来开辟空间的。
但是其参数不相同,num表示的是元素个数,而size表示的是每个元素的大小,返回值一样
所以对于之前用malloc开辟的10个整形大小的空间(40byte)就还可以写成calloc形式的:
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
perror("p");
return 0;
}
//使用
free(p);
p = NULL;
return 0;
}
细节:
- 同样的calloc也需要判断是否申请成功
- 用free释放申请好的空间(并且将其指针置为空)
通过对比上面两段代码我们可知:
- malloc在申请往空间后不会对这部分空间有任何作业
- calloc在申请完空间后会对这部分全部空间初始化为0
- 所以在我们使用时可以通过他们不同的类型进行选择使用
上面的malloc、calloc都是开辟内存的空间的,而下面的realloc就是修改开辟的内存空间
2.4realloc
知识点:
void* realloc (void* ptr, size_t size);
第一个参数是要调整的内存地址(由malloc、calloc、realloc开辟的内存块),第二个参数是所要调整的新的空间的大小(size个字节),返回情况是一样的
具体使用如下
int main()
{
int* p = (int*)malloc(sizeof(int) * 5);
if (p == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i < 5; i++)
{
*(p + i) = i + 1;
printf("%d ", *(p + i));
}
int* ptr = (int *)realloc(p, sizeof(int) * 10);
if(ptr != NULL)//当等于空指针时就没有动作了不用管ptr
{
p = ptr;
ptr = NULL;
}
for (int i = 5; i < 10; i++)
{
*(p + i) = i + 1;
printf("%d ", *(p + i));
}
free(p);
p = NULL;
return 0;
}
细节:
在上述代码中我们可以发现在用realloc增加或减少动态内存的空间后 返回时 接收返回值的指针是一个新的指针(int * ptr)而不是所要改变的那片空间的地址(p),这是因为:realloc开辟空间时分着两种情况:
- 当realloc所要开辟的空间在原始空间的后面有足够的空间让其使用时那就直接在原始位置处开辟
- 当realloc所要开辟的空间在原始位置后面没有足够的空间让其使用,此时就会寻找一个放的下的空间,并且将原数据拷贝过去,并且释放掉原空间和返回新的地址
- 就是因为可能的两种情况,所以为了避免第二种释放原位置返回新地址,所以用一个新指针接收其返回的地址,如果直接写成 p = realloc(p,sizeof(int)*10)的话 假如开辟没有成功那就会返回NULL而原本还有的5个空间大小直接就没了(p = NULL)。
当第一个参数传的是NULL时其用法和malloc类型开辟申请一块新的空间:
realloc(NULL,40) == malloc(40) ;并且返回指向这块空间的地址
3.动态内存常见的错误
3.1对NUL指针的解应用操作
知识点:
在开辟一块空间时要加上判断是否开辟成功,否则假如没开辟成功的话就会对NULL地址进行解应用(NULL空指针不能进行访问,若访问就会报错:非法访问)
细节:
int main()
{
int* ptr = (int*)malloc(40);
if (ptr == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i < 10; i++)
{
*(ptr + i) = i + 1;
printf("%d ", *(ptr + i));
}
return 0;
}
不加(在vs环境下)时会报警告,所以malloc的返回值一定要判断
3.2对动态内存开辟的空间越界访问
知识点:
我们要注意在malloc、realloc中他所要开辟的大小都是以byte为单位的,不能看成你所要开辟的某类型的个数,否则就很可能因为这样而导致越界的问题
细节:
如下面这段错误代码,就是错认为开辟了100个int类型大小的空间
int main()
{
int* ptr = (int*)malloc(100);
for (int i = 0; i < 100; i++)
{
ptr[i] = 0;
}
return 0;
}
以为开辟了100int的实际上只开辟了25个int(100byte)的空间,所以对于后面的75个空间都是非法访问的
3.3对非动态开辟的内存进行free释放
知识点:
对于free来说只能用来释放堆区上的动态开辟的空间,不能对非动态开辟的内存进行释放(可不能杀疯了)
细节:
int main()
{
int a = 0;//正常开辟的变量存在栈区上
int *p = &a;
free(p);
p = NULL;
return 0;
}
此时因为对非动态内存开辟的空间进行释放,将会导致其程序崩溃。
3.4使用free释放动态内存开辟的一部分空间
知识点:
在用free释放空间时,我们不能只释放开辟的一部分空间,而是应该将所开辟的空间都释放掉,若释放一部分空间同样会报错
细节:
int main()
{
int* p = (int*)malloc(100);
if (p == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i < 25; i++)
{
*p = i;
printf("%d ", *p);
p++;
}
free(p);
p = NULL;
return 0;
}
此时因为后置++会有副作用(p = p + 1)会导致其p的地址发生改变,再对p地址进行释放(此时的p没有指到创建时的位置了)就会导致只对一部分动态内存开辟的空间进行释放而导致其程序崩溃。
3.5对同一块动态内存空间多次释放
知识点:
对于已经释放的空间其指向的指针已经变成了野指针若再次释放就会导致报错
细节:
int main()
{
int* p = (int*)malloc(100);
if (p == NULL)
{
perror("malloc");
return 1;
}
// 使用...
//释放
free(p);
// ...
free(p);
return 0;
}
对于以上这种情况,因为对一个野指针的空间进行释放所以会导致错误;
但是当我们养成良好的代码习惯:在free释放后将p置为NULL ,这样即使再次释放对于free来说当传进来的指针是NULL时就不会进行任何操作。
3.6动态内存空间的忘记释放
知识点:
当对使用完后空间忘记释放时,就会导致一个空间,即使没用了但仍然占着,就会导致空间的浪费。
对此又称:内存泄漏
细节:
void test()
{
int* p = (int*)malloc(100);
if(p == NULL)
{
perror("malloc");
return;
}
}
int main()
{
test();
return 0;
}
此时因为开辟了一片空间当是并没有对其进行释放,就会导致内存释放;
对此我们有两种解决方法:
1.直接在函数内部释放
void test()
{
int* p = (int*)malloc(100);
if(p == NULL)
{
perror("malloc");
return;
}
free(p);
p = NULL;
}
int main()
{
test();
return 0;
}
2.将开辟空间后所用的指针返回并进行接收在主函数内进行释放;
//该函数进行了malloc开辟空间,返回开辟空间的起始地址
//记得后面要释放
int * test()
{
int* p = (int*)malloc(100);
if (p == NULL)
{
perror("malloc");
return;
}
return p;
}
int main()
{
int * ptr = test();
free(ptr);
ptr = NULL;
return 0;
}
并且对于这种传递回开辟空间的地址的函数来说最后要进行注释一下,避免别人使用时忘记释放
内存泄漏的危害:
当你不进行内存释放的话,每当你用下该函数就会导致一部分空间被占用,以此往复就会导致内存被占满而导致程序挂掉。
4.动态内存常见问题、笔试题
- 问题有:非法访问、内存泄漏(具体已标注释)
void GetMemory(char* p)//p有自己的独立空间 { p = (char*)malloc(100); } void Test(void) { char* str = NULL; GetMemory(str);//此处为传值调用,就会导致p其实只是str的零时拷贝并不会改变str,所以str并没有开辟好空间,仍然为NULL strcpy(str, "hello world");//因为str仍然为NULL所以就会有非法访问问题(访问了NULL地址) printf(str); } //并且因为p开辟了一个空间且后面也并没有free,也会导致内存泄漏 int main() { Test(); return 0; }
- 问题:非法访问,当函数调用往后会将函数前所借用的内存空间归还给操作系统。(返回栈空间地址问题)
char *GetMemory(void) { char p[] = "hello world"; return p; } void Test(void) { char *str = NULL; str = GetMemory(); printf(str); //此处虽然是p处的地址,但是p指向的空间已经被收回了,所以就会导致访问到别收回的空间非法访问 }
5.C/C++程序在内存中的内存开辟
知识点:
一般来说,在内存中有着:内核区、栈区、堆区、静态区(数据段)、代码段、内存映射段
6.柔性数组
知识点
柔性数组是在c99标准下的
在结构体中的最后一个元素允许是未知大小的数组,这个数组就被称为柔性数组
struct s
{
int a;
char b;
char arr[];//柔性数组成员
//char arr[0];写0或者不写0是一样的,数组的大小是未知的,
};
细节点:
- 在柔性数组成员前至少有一个成员
- sizeof返回大小时不包含柔性数组的大小
- 包含柔性数组的结构应该用malloc来进行动态内存分配,并且分配适应的内存并且应该大于结构体的大小,目的是为了适应柔性数组预期的大小 。
struct s *ptr = (struct s*)malloc(sizeof(struct s) + sizeof(char) * 10);//后面开辟的10个char的空间是柔性数组所需的空间 //再通过ptr来访问结构体
并且还可以再通过realloc的方式来增容
柔性数组就好比一个结构体内指针成员,他们都是开辟一块空间,但是用指针成员的话,会相对来说比较麻烦即柔性数组相对于的好处:
- 不需要内存的单独释放
- 不需要开辟内存时单独开辟
- 访问速度较块(没有较多的内存碎片)
- 若用指针则反之
具体如下:
struct s
{
int a;
char b;
char arr[];//柔性数组成员
//char arr[0];写0或者不写0是一样的,数组的大小是未知的,
};
struct s1
{
int a;
char b;
char *p;
};
int main()
{
struct s *ptr = (struct s*)malloc(sizeof(struct s) + sizeof(char) * 10);
//后面开辟的10个char的空间是柔性数组所需的空间(直接一起开辟)
//再通过ptr来访问结构体
printf("%d\n", sizeof(struct s));
free(ptr);
ptr = NULL;
struct s1* ps = (struct s1*)malloc(sizeof(struct s1));
ps->a = 100;
ps->b = 'a';
ps->p = malloc(10 * sizeof(char));//单独开辟
if (ps->p == NULL)
{
perror("malloc");
return 1;
}
//使用 ....
//单独释放
free(ps->p);
ps->p = NULL;
return 0;
}
本章完。预知后事如何,暂听下回分解。
持续更新大量C语言细致内容,三连关注哈