1.为什么要动态内存分配
2.malloc和free
3.calloc和realloc
4.常见的动态内存的错误
5.动态内存经典笔试题分析
6.柔性数组
7.总结C/C++程序内存区域划分
1.为什么要有动态内存分配
我们掌握的内存开辟方式有:
int val=20;//在栈区上开辟四个空间
char arr[10]={0};//在栈区上开辟10个字节的连续空间
以上的开辟空间方式有俩个特点:
1.开辟空间大小是固定的。
2.在VS环境中,数组的声明必须指定数组的长度,数组空间大小确定了就不能改变,
但是不是任何时候都能精准把握需要开辟多少空间,所有C语言引入了动态内存开辟,让程序员自己申请和释放空间,有灵活性。
2.malloc和free
C语言提供了一个动态内存开辟的函数:
void* malloc(size_t size);//需要一个无符号整型的参数
这个函数会向内存申请一块连续可用的空间,并返回一个指针(类型是void*,指向开辟的空间)。
1.如果开辟失败,会返回一个NULL指针,因此malloc的返回值需要检查。
2.返回值是void*类型的,malloc并不知道开辟空间的类型,具体类型需要使用者自己定义(用到强制转换)。
3.如果参数size为0,malloc的行为是标准未定义的(错误行为),取决于编译器。
free
C语言提供了另外一个函数free,专门用来做动态内存释放和回收。
void free(void* ptr);
1.参数需要一个地址(指向动态开辟的空间,如果不是会报错)。
2.如果ptr指向NULL指针,则函数无事发生。
malloc和free都声明在stdlib.h头文件中。
例子:
图中可知数组大小必须一开始确定好(一个确定的数)。
开辟4个整型大小的空间(int* ptr指向),但是这些都是未初始化的(随机值),开辟完后要用free函数释放掉开辟的空间,在把ptr指向的地方置NULL,不然会造成野指针(指向一块没有权限的地方)。
3.calloc和realloc
calloc
C语言还提供了一个函数叫calloc,这个函数也用来动态内存分配。
void* calloc(size_t num,size_t size);
1.函数的功能是为num个大小为size的元素开辟空间,并把空间的每个字节初始化为0.
2.与malloc的区别在于会不会把每个字节初始话以及参数不同。
如果想对申请的内存空间的内容要求初始化,可以使用calloc来实现。
3.realloc
1.realloc函数的出现是动态内存管理更加灵活。
2.又是开辟太少或者太多便可以使用realloc函数来解决。
void* realloc(void* ptr,size_t size);
1.ptr是要进行调整的内存地址(用malloc或者realloc开辟的空间会返回一个地址)
2.size是调整后的大小
3.返回值为内存起始位置
4.realloc在调整内存空间是存在俩种情况:
情况1:原有的空间后面有足够的空间开辟(连续存放的下)
情况2:原因的空间后面没有足够大的空间连续存放
对应情况1:在原有内存空间后面直接追加空间,返回的地址不变。
对应情况2:原有空间后面不够,则在堆区空间上找到另外一个合适大小的连续空间来使用(动态内存开辟是在堆区上开辟的)。则返回值就是一个新的内存地址。(之前的会自动被释放掉)
写程序的时候不知道原有空间后面也没有足够的空间,所以使用realloc函数就要注意一些。(申请失败就会返回NULL)
这个是不安全的使用方法:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = NULL;
if (p != NULL)
{
//
}
else
{
return 1;
}
p = (int*)realloc(p, 100);//如果扩展失败了怎么办?
return 0;
}
如果扩展失败会返回NULL空指针,不仅没多,而却原本的也没有了(指针不在指向之前开辟的空间),竹篮打水一场空。
下面是安全的realloc使用方法:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = NULL;
if (p != NULL)
{
//
}
else
{
return 1;
}
int* pp = NULL;
pp = (int*)realloc(p, 100);
if (pp != NULL)
{
p = pp;//把开辟好的空间返回给p(指针赋值就是把指向的地方告诉被赋值的指针,让它也可以指向这个地方)
}
free(p);
p = NULL;
pp=NULL;
return 0;
}
先让pp也指向开辟的空间,让pp去检验是否为空,不为NULL,则说明了扩展成功了,再把pp指向的地址赋给p。
另外realloc也可以当成malloc使用:
realloc(NULL,20)=malloc(20);
4.常见的动态内存错误
1.对NULL指针的解引用操作
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(INT_MAX / 4);//【C/C++】中常量 INT_MAX 和 INT_MIN 分别表示最大、最小整数 。 INT_MAX , INT_MIN 数值大小因为int占4字节32位,
//根据二进制编码的规则,INT_MAX = 2^31-1,INT_MIN= -2^31。C/C++中,所有超过该限值的数,都会出现溢出,出现 warning,但是并不会出现error。
// 如果想表示的整数超过了该限值,可以使用长整型 long long 占8字节64位。
*p = 20;
free(p);
return 0;
}
因为malloc开辟的空间都是未初始化的(既有可能是NULL),但*这个解引号是不能解引用空指针。
2.对动态开辟空间的越界访问
#include <stdio.h>
#include <stdlib.h>
int main()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
exit(-1);//结束程序作用
}
for (i = 0; i <= 10; i++)
{
*(p + i) = i;
}
free(p);//不free(pp)是因为pp和p指向的是同一个地方,
//把p释放了就不用多写一个了,只需要把pp置成NULL,避免野指针出现
p = NULL;
pp=NULL;
return 0;
}
只开辟了10个整型大小的空间,但是在for循环里面访问了第11个空间。
3.对非动态开辟内存使用free释放
4.使用free释放一块动态开辟内存的一部分
5.对同一块内存空间多次释放
6.动态内存空间忘记释放(内存泄漏)
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = NULL;
p = (int*)malloc(4 * sizeof(int));
return 0;
}
这个虽然不会报错,但是要保持一个好的习惯(开辟空间就要记得去释放)。
开辟空间未以正确释放,导致无法回收之前分配出去的内存,则无法被使用,导致内存浪费。
程序一直工作时,若开辟的空间不释放会越积越多直到挂掉,重启程序。
malloc/calloc/realloc申请的内存,如果不使用时可以用free,也可以让程序结束,操作系统会回收掉开辟的空间。
这种状况可能为造成为释放,但if条件成立,不会之执行下面的过程。
void test()
{
int* p = (int*)malloc(100);
//使用
int n = 3;
if (n > 0)
return;
free(p);
p = NULL;
}
int main()
{
test();
while (1);
}
下面也是容易疏忽的地方,在调用函数中申请空间,要在函数中free,不能在main函数释放,因为int* p是在栈区上申请的变量(局部变量),指向开辟的空间,但出了test()函数就会被销毁掉,从而找不到指向动态开辟的空间。
void test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while (1);
}
5.动态内存经典笔试题分析
题目1:
从整体来看没有free函数的影子,没有释放开辟空间,其次在调用GetMemory函数时传的时值,不是地址,这样是给形参p开辟一块空间,跟str没关系,str指向的还是NULL,而后面运用函数strcpy时,会有解引用过程,但str是空指针是不能被解引用的,所以程序会挂掉,不会打印任何东西。
只有通过传地址过去,用二级指针接受&str,这样在解引时是指针刚好对应malloc返回一个地址,是str指向开辟的空间,最后在释放str并置空。
第二种改法:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include<string.h>
char* GetMemory()
{
char* p = (char*)malloc(100);
return p;
}
void test()
{
char* str = GetMemory();
strcpy(str, "hello world");
printf("%s",str);
free(str);
str = NULL;
}
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返回是会被销毁的。指向空间被销毁了,未被覆盖的情况还是能打印出来,若被覆盖则不能打印。
题目3:
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
str通过调用函数指向一块开辟的空间,但并没有用free函数释放空间。
题目4:
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
需要注意一点,free(str),但指针str还是指向开辟的空间(为置空的情况下),只是指向的空间是没有权限的(非法访问),所以if条件还是能执行(只是free掉了str但str还是能指向开辟的空间,但非法访问无权限了),然而在无权限的地方是不能操作的(strcpy函数是无权限执行的,错误的), 所以这个问题是为把释放的指针置空,这是主要问题。
6.柔性数组
例如:
typedef struct st_typr
{
int i;
int a[];//柔性数组成员
}type_a;//重命名
柔性数组的特点:
1.结构体中的柔性数组成员前面必须至少有一个其他成员。
2.sizeof返回的这种结构大小不包括柔性数组的内存。(所以前面至少有一个成员,不然结构体的大小就为0,不合理)
3.包含柔性数组成员的结构用malloc函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性的预期大小。
例子:
#include<stdio.h>
#include<stdlib.h>
typedef struct rx
{
int i;
int a[];
}rx;
int main()
{
int i = 0;
rx* p = (rx*)malloc(sizeof(rx) + 100 * sizeof(int));//第一个是结构体的大小,后一个是柔性数组的要开辟的大小
p->i = 100;
for (i = 0; i < 100; i++)
{
p->a[i] = i;
}
for (i = 0; i < 10; i++)
{
printf("%d", p->a[i]);
}
free(p);
p = NULL;
return 0;
}
柔性数组a,开辟了100个整型元素的连续空间。
第二种方式:
#include<stdio.h>
#include<stdlib.h>
typedef struct rx
{
int i;
int* a;
}rx;
int main()
{
int i = 0;
rx* p = (rx*)malloc(sizeof(rx));//第一个是结构体的大小,后一个是柔性数组的要开辟的大小
p->i = 100;
p->a = (int*)malloc(p->i * sizeof(int));//因为a的类型是int* 可以接受malloc开辟空间返回的地址
for (i = 0; i < 100; i++)
{
p->a[i] = i;//a[i]=*(a+i)
}
for (i = 0; i < 10; i++)
{
printf("%d", p->a[i]);
}
free(p->a);//要先freep->a因为先free(p)会找不到p->a
free(p);
p = NULL;
return 0;
}
上述代码都可以完成相同的功能,但是方法1实现有俩个好处:
7.总结C/C++程序内存区域划分
上面图片能清晰的了解变量属于哪一个地方。