谢谢观看!希望以下内容帮助到了你,对你起到作用的话,可以一键三连加关注!你们的支持是我更新地动力。
因作者水平有限,有错误还请指出,多多包涵,谢谢!
目录
- 一、 为什么要有动态内存分配
- 二、`malloc`和`free`
- 2.1`malloc`
- 2.2`free`
- 三、`calloc`和`realloc`
- 3.1`calloc`
- 3.2`realloc`
- 四、 常见的动态内存的错误
- 4.1对NULL指针的解引用操作
- 4.2对动态开辟空间的越界访问
- 4.3对非动态开辟内存使用`free`释放
- 4.4 使用`free`释放一块动态开辟内存的一部分
- 4.5对同一块动态内存多次释放
- 4.6 动态开辟内存忘记释放(内存泄漏)
- 五、动态内存经典笔试题分析
- 题目1
- 题目2
- 题目3
- 题目4
- 六、柔性数组
- 6.1 柔性数组的特点:
- 6.2 柔性数组的使用
- 6.3 柔性数组的优势
- 七、总结C/C++中程序内存区域划分
一、 为什么要有动态内存分配
我们已经掌握的内存开辟方式有:
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
但是上述的开辟空间的方式有两个特点:
- 空间开辟大小是固定的。
- 数组在申明的时候,必须指定数组的长度,数组空间一旦确定了大小不能调整但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小会根据实际情况发生变化,如果使用数组的话,可能开辟的空间太大,也可能开辟的空间不够,那数组的编译时开辟空间的方式就不能满足了。
所以在C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活了。这就需要使用到库函数来进行申请内存空间了。
二、malloc
和free
2.1malloc
C语言提供了一个动态内存开辟的函数:
void* malloc (size_t size);
//size表示需要申请的一块空间大小是多少,单位是字节
//void*表示返回那一块空间的起始位置的地址
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
注意事项:
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个
NULL
指针,因此malloc
的返回值⼀定要做检查。 - 返回值的类型是 void* ,所以
malloc
函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。 - 如果参数
size
为0
,malloc
的行为是标准是未定义的,取决于编译器 - 使用函数
malloc
之前需要加上头文件#include<stdlib.h>
具体使用方式:
//代码一
int* p = (int*)malloc(10*sizeof(int));
char* p = (char*)malloc(10*sizeof(char));
//代码二
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(10*sizeof(int));
if(p == NULL)
{
//空间开辟失败
perror("malloc");//perror函数会打印出错误信息
return 1;//1表示异常返回,在主函数main()中
}
//可以使用40个字节的空间了
int i = 0;
for(i = 0;i < 10;i++)
{
scanf("%d",(p+i));//scanf需要的参数是空间的地址
}
for(i = 0;i < 10;i++)
{
printf("%d ",*(p+i));
}
return 0;
}
那么malloc
申请的空间和数组的空间有什么区别呢?
- 动态内存的大小是可以调整的
- 开辟空间的位置不一样,
malloc
开辟的空间在堆区,数组arr
开辟的空间在栈区。
2.2free
C语言提供了另外一个函数free
,专门是用来做动态内存的释放和回收的,函数原型如下::
void free (void* ptr);
//ptr表示形参是指针,传递地址给它
//void *表示传递任何类型的指针都可以
free函数用来释放动态开辟的内存。
注意事项:
- 如果参数
ptr
指向的空间不是动态开辟的,那free
函数的行为是未定义的。 - 如果参数
ptr
是NULL
指针,则函数什么事都不做。
具体使用方式:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(10*sizeof(int));
if(p == NULL)
{
//空间开辟失败
perror("malloc");//perror函数会打印出错误信息
return 1;//1表示异常返回,在主函数main()中
}
//可以使用40个字节的空间了
int i = 0;
for(i = 0;i < 10;i++)
{
scanf("%d",(p+i));//scanf需要的参数是空间的地址
}
for(i = 0;i < 10;i++)
{
printf("%d ",*(p+i));
}
free(p);//动态开辟的空间使用后要进行释放
//释放的是指针p指向的空间,使空间归还给操作系统
//但是p依然指向着那块空间,还保留着地址,此时p就是野指针了,所以还得对p指针赋值NULL
p = NULL;//将NULL赋值给p,表示p是空指针
return 0;
}
总结:malloc
和free
函数最好成对使用
三、calloc
和realloc
3.1calloc
C语言还提供了一个函数叫 calloc
, calloc
函数也用来动态内存分配。原型如下:
void* calloc (size_t num, size_t size);
//num表示开辟空间的个数
//size表示一个空间的大小是多少,单位是字节
//void* 表示返回所开辟空间的起始地址
注意事项:
- 函数的功能是为
num
个大小为size
的元素开辟一块空间,并且把空间的每个字节初始化为0
。 - 与函数
malloc
的区别只在于calloc
会在返回地址之前把申请的空间的每个字节初始化为全0
,其他使用方法和malloc
函数一模一样。
具体使用方式:
#include<stdio.h>
#include<stdlib.h>
int main()
{
//申请10个整型的空间
//int* p = (int*)malloc(10*sizeof(int));
int* p = (int*)calloc(10,sizeof(int));
if(p == NULL)
{
//空间开辟失败
perror("calloc");//perror函数会打印出错误信息
return 1;//1表示异常返回,在主函数main()中
}
//可以使用40个字节的空间了
int i = 0;
for(i = 0;i < 10;i++)
{
printf("%d ",*(p+i));//结果为:0 0 0 0 0 0 0 0 0 0
}
free(p);//动态开辟的空间使用后要进行释放
//释放的是指针p指向的空间,使空间归还给操作系统
//但是p依然指向着那块空间,还保留着地址,此时p就是野指针了,所以还得对p指针赋值NULL
p = NULL;//将NULL赋值给p,表示p是空指针
return 0;
}
3.2realloc
函数原型如下:
void* realloc (void* ptr, size_t size);
//ptr表示要调整的内存地址
//size表示调整之后新大小
//返回值为调整之后的内存起始位置
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
具体使用方式:
int main()
{
//申请10个整型的空间
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
perror("calloc");
return 1;
}
//使用空间
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);//*(p+i)
}
//调整空间-希望变成20个整型空间
int* p = (int*)realloc(p, 20 * sizeof(int));
//这里对realloc函数返回空间的起始地址传递给指针p是否正确?
if (ptr != NULL)
{
p = ptr;
}
//使用
//...
//释放
free(p);
p = NULL;
return 0;
}
//用法二:realloc不仅可以调整空间,可以开辟空间
int * p = (int *)realloc(NULL,40);
//等价于 int * p = (int*)malloc(10*sizeof(int))
在上面的代码中的这一部分,我们怎么判断是否正确呢?这就需要了解一下,realloc
函数调整内存空间的细节了
//调整空间-希望变成20个整型空间
int* p = (int*)realloc(p, 20 * sizeof(int));
//这里对realloc函数返回空间的起始地址传递给指针p是否正确?
realloc
在调整内存空间的是存在两种情况:
- 情况1:原有空间之后有足够大的空间
- 情况2:原有空间之后没有足够大的空间
注意:情况1
和情况2
都是正常可以分配空间的情况,如果未分配成功的话,会返回NULL
空指针。
所以通过上面的细节,我们知道了假如realloc
函数分配空间失败后返回NULL
,然后我们将指针p
去接受,会导致我们连原来已经分配好的空间都找不到了,但是我们希望就算分配失败了也不应该将原来的空间都找不到。所以我们可以使用另一个指针ptr
去接收它的返回值,然后再进行判断这个指针是否为NULL,如果不是NULL,则再将prt
的值赋值给p
。
int main()
{
//申请10个整型的空间
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
perror("calloc");
return 1;
}
//使用空间
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);//*(p+i)
}
//调整空间-希望变成20个整型空间
//int* p = (int*)realloc(p, 20 * sizeof(int));不使用这种方式,有危险
int* ptr = (int*)realloc(p, 20 * sizeof(int));
//ptr不为NULL时,再赋值给p
if (ptr != NULL)
{
p = ptr;
}
//使用
//...
//释放
free(p);
p = NULL;
return 0;
}
四、 常见的动态内存的错误
4.1对NULL指针的解引用操作
//错误写法
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
//因为malloc函数可能会分配空间失败返回NULL,然而当对NULL空指针解引用操作*会出错
free(p);
p=NULL;
}
//正确写法
void test()
{
int *p = (int *)malloc(INT_MAX/4);
if(p == NULL)
{
perror("malloc");
return 1;
}
*p = 20;
free(p);
p=NULL;
}
4.2对动态开辟空间的越界访问
//错误写法
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
exit(EXIT_FAILURE);
}
for(i=0; i<40; i++)
{
*(p+i) = i;//当i是10的时候越界访问
}
free(p);
}
//正确写法
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的时候越界访问
}
free(p);
}
4.3对非动态开辟内存使用free
释放
//错误写法
void test()
{
int a = 10;
int *p = &a;
free(p);//ok?
//free只能对动态内存处理
}
4.4 使用free
释放一块动态开辟内存的一部分
//错误写法
void test()
{
int *p = (int *)malloc(100);
p++;//此时的p指针并不是指向了动态内存分配的起始地址,而是跳过了4个字节的空间
free(p);//p不再指向动态内存的起始位置
}
4.5对同一块动态内存多次释放
//错误写法
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放,相当于对p野指针进行了释放,这是不允许的
}
//正确写法
void test()
{
int *p = (int *)malloc(100);
//...
free(p);
p=NULL;
//...
free(p);
p=NULL;
}
4.6 动态开辟内存忘记释放(内存泄漏)
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
五、动态内存经典笔试题分析
题目1
//错误写法
void GetMemory(char *p)//p是str的临时拷贝
{
p = (char *)malloc(100);//后面没有free释放,导致内存泄漏
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");//str还是空指针,导致了对NULL指针解引用操作,程序崩溃
//要知道strcpy的实现
printf(str);//这样的写法没有错,是对的,类型于printf("hehe\n"),"hehe\n"是常量字符串,值是首字符的地址
}
//正确写法
//写法一
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;
}
//写法二
void GetMemory()
{
char * p = (char *)malloc(100);
return p;
}
void Test(void)
{
char *str = NULL;
str =GetMemory();
strcpy(str, "hello world");
printf(str);
free(str)
str = NULL;
}
题目2
char *GetMemory(void)
{
char p[] = "hello world";//当出了该函数,数组是局部变量,有作用域,空间中的数据会被销毁
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
//虽然str指针接收了字符数组的首元素地址,指向了原来的字符数组空间,但是那空间已经归还操作系统了,不在属于这个程序了
//str是野指针了
printf(str);
}
注意:题目2就是典型的错误,返回栈空间地址的问题。
题目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);//结果可以打印出hello,但是不使用后,要将动态申请的内存空间给释放掉,
//所以导致内存泄漏
}
//正确写法
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
free(str);
str = NULL;
}
题目4
//错误写法
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);//此时str是野指针,但是str还是指向原来的空间
if(str != NULL)
{
strcpy(str, "world");//strcpy函数中年会对野指针str进行了解引用操作,非法访问
printf(str);
}
}
//正确写法
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
str = NULL;
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
六、柔性数组
也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。C99
中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
struct S
{
int n;
char c;
double d;
int arr[];//未知大小的数组 - arr就是柔性数组的成员
//也可以写成int arr[0]
};
6.1 柔性数组的特点:
- 结构中的柔性数组成员前面必须至少一个其他成员。
sizeof
返回的这种结构大小不包括柔性数组的内存。- 包含柔性数组成员的结构用
malloc ()
函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
struct S
{
int n;
int arr[0];//柔性数组成员
};
int main()
{
printf("%d\n", sizeof(struct S));//输出的是4
return 0;
}
6.2 柔性数组的使用
struct S
{
int n;
int arr[];//柔性数组成员
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S) + 20*sizeof(int));
//20*sizeof(int)是为arr数组动态分配的,表示数组arr有20个整型
if(ps == NULL)
{
perror("malloc()");
return 1;
}
//使用空间
ps->n=100;
int i = 0;
for(i = 0; i < 20 ; i++)
{
ps->arr[i] = i + 1;
}
//调整ps指向空间的大小
struct S* ptr = (struct S*)realloc(ps,sizeof(struct S) + 40 * sizeof(int));
if(ps != NULL)
{
ps = ptr;
ptr = NULL;//防止ptr为野指针
}
else
{
return 1;
}
//使用空间
for(i = 0 ; i < 40 ; i++)
{
printf("%d ",ps->arr[i]);
}
//释放空间
free(ps);
ps = NULL;
return 0;
}
6.3 柔性数组的优势
//代码一
struct S
{
int n;
int* arr;
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S));
if (ps == NULL)
{
perror("malloc");
return 1;
}
int*tmp = (int*)malloc(20*sizeof(int));
if (tmp != NULL)
{
ps->arr = tmp;
}
else
{
return 1;
}
ps->n = 100;
int i = 0;
//给arr中的20个元素赋值为1~20
for (i = 0; i < 20; i++)
{
ps->arr[i] = i + 1;
}
//调整空间
tmp = (int*)realloc(ps->arr, 40*sizeof(int));
if (tmp != NULL)
{
ps->arr = tmp;
}
else
{
perror("realloc");
return 1;
}
//
for (i = 0; i < 40; i++)
{
printf("%d ", ps->arr[i]);
}
//释放
free(ps->arr);//先释放第二次申请的空间
ps->arr = NULL;
free(ps);//再释放第一次申请的空间
ps = NULL;
return 0;
}
//代码二
struct S
{
int n;//4
int arr[];
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S) + 20*sizeof(int));
if(ps == NULL)
{
perror("malloc()");
return 1;
}
//使用这些空间
ps->n = 100;
int i = 0;
for (i = 0; i < 20; i++)
{
ps->arr[i] = i + 1;
}
//调整ps指向空间的大小
struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 40 * sizeof(int));
if (ptr != NULL)
{
ps = ptr;
ptr = NULL;
}
else
{
return 1;
}
//使用
for (i = 0; i < 40; i++)
{
printf("%d ", ps->arr[i]);
}
//释放空间
free(ps);
ps = NULL;
return 0;
}
上述 代码一
和 代码二
可以完成同样的功能,但是 代码二
的实现有两个好处:
七、总结C/C++中程序内存区域划分