文章目录
1. 动态分配内存是什么?
是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。通常我们的变量都是预先分配的,系统自动给分配和回收的。
a. 动态内存分配【重点 难点】
传统数组的缺点:
(1) 数组长度必须事先制定,且只能是常整数,不能是变量
例子:
int a[5]; //ok
int len =5; int a[len]; //error 不能是变量
(2) 传统形式的定义的数组,该数组的内存程序员无法手动释放它,是由系统自动释放的
在一个函数运行期间,系统为该函数中数组所分配的存储空间会一直存在,
直到该函数运行完毕时,数组的空间才会被系统释放。
(3)数组的长度一旦定义,其长度就不能再更改 a[5] 其中5就表示数组长度
数组的长度不能在函数运行的过程总动态的扩充或缩小
(4) A函数定义的数组,在A函数运行期间可以被其他函数使用,但A函数运行完毕之后,
A函数中的数组将无法再被其他函数使用
传统方式定义的数组,不能跨函数使用
【总结】为什么需要动态分配内存
动态数组很好的解决了传统数组的四个缺陷
传统数组也叫静态数组
b. 静态内存和动态内存的区别比较:
静态内存和动态内存的区别比较:
静态内存是由系统自动分配,由系统自动释放
静态内存是在栈中分配的
动态内存是由程序员手动分配,手动释放,如果忘了释放,就会出现内存泄漏,内存越用越少,最后就会死机。
动态内存是在堆分配的 堆就是堆排序
2. 动态分配内存怎么用?
C99可以使用变量作为数组定义的大小,在C99之前只能使用动态分配内存实现。
int arr[n];
近似于:
区别:
(1)指针最好提前声明:int* arr =NULL;
(2)强制类型转化:int* arr = (int*)malloc(n*sizeof(int));
(3)需要释放空间: free(arr);
(4)malloc申请大小的时候可以多申请+1;
int* arr = (int*)malloc(n*sizeof(int));
// 操作arr如同操作int arr[n]
free(arr);
example1:输入字符数组,并打印出来;
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
int main()
{
int i, n;
scanf("%d",&n);
int arr[n];
for(i = 0; i < n; ++i)
{
scanf("%d",arr+i);
}
for(i = 0; i < n; ++i)
{
printf("%d ",arr[i]);
}
return 0;
}
等价于:
#include <stdio.h>
#include <stdlib.h>
int main(){
int n;
scanf("%d",&n);
// int arr[n];
int* arr = (int*)malloc(n*sizeof(int));//动态的构造了一个数组,因为是整型指针变量,一个元素占4个字节,而n是元素个数
for(int i=0;i<n;++i){
scanf("%d",arr+i); //等价于scanf("%d",&arr[i]); //对动态一维数组进行赋值
}
for(int i=0;i<n;++i){
printf("%d ",arr[i]); // //对动态一维数组进行循环输出
}
printf("\n");
free(arr);
}
解析:
动态数组在使用上是跟静态数组的使用方式是一样的,只是构造的时候不一样而已。
因为arr是整型指针变量占4个字节,所以动态内存分配后,指向的是内存分配的n*4个字节中的前4个字节,如果arr+1 则表示
指向的是第二个4个字节(即第5个字节到第8个字节),而不是在前面4字节的基础上+1成为5个字节。因为每4个字节为一部分。
由几个字节为一部分,需要由pArr的指针变量类型来决定。
arr = (int *)malloc(4 * n);等同于int arr[0]、int arr[1]、int arr[2]、int arr[3]、int arr[4]
即int arr[5]
动态的构造了一个一维数组,该数组的数组名是arr,数组长度是len,单个元素如上所示。
上面静态的arr[5]有静态的20字节的内存
下面arr动态申请的有20个字节的内存
由上可知:
arr[0]是第一个元素,占用了20个字节的前4个字节
那么:arr[0]同样也是占用了动态分配的20字节的前4个字节
又arr[0]等同于 *(arr+0)
所以例如:arr[2]就等同于arr[2]
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/e0b104ba83eb4b6e8919395ff14b1a72.png)
2.1 申请动态分配内存malloc()
stdlib.h
中定义函数void* malloc(size_t size)
,向系统申请大小为size
的内存空间。返回结果是void*
,使用时转换成需要的指针类型。如果申请失败,返回NULL
。
example1:
#include <stdio.h>
#include <malloc.h> //不能省
int main(void)
{
int i = 5; //静态分配了 4个字节 //5行
int *p = (int *)malloc(4); //最终分配了8个字节,P变量占了4个字节,p所指向的内存也占了4个字节
//本身所占 的4个内存是静态分配的,p 所指向的 4个内存是动态分配的
/*
1.要使用malloc函数,必须添加malloc.h的头文件 或者stdlib.h
2.malloc函数只有一个形参,并且形参是整型;
3.形参 (4) 表示请求系统为本程序分配4个字节;也可以写成:int* arr = (int*)malloc(n*sizeof(int));
4.malloc函数只能返回第一个字节的地址
*/
*p = 5; //*p 代表的就是一个int变量,只不过*p这个整型变量的内存分配方式和5行的分配方式不同
free(p); //表示把p所指向的内存给释放掉,p本身的内存是静态的,不能由程序员手动释放,
//p本身的内存只能在p变量所在的函数运行终止时由系统自动释放
printf("hello!\n");
return 0;
}
解析:
(int *)是将malloc请求的字节强制转换为整型变量,按整型变量所占字节数来划分地址。
(第一个字节地址只有值,而没有类型,只有强制转换后才会给加上所属转换的类型。)
malloc后面的括号中必须是一个值,而这个值必须为整数,无论请求的字节数是多少,比如 malloc(200)
但是最终返回的都是第一个字节的地址,如果只知道第一个字节地址,但是并不知道第一个字节地址指向的变量
最终会占用几个字节,所以需要在前面进行强制的变量类型转换,告诉程序最后按什么标准来划分地址。以便明
白第一个字节地址到底指向的是一个什么类型的变量。最终表示请求来的字节数按第一个字节地址所指向的变量
类型所占的字节数来划分,比如(int *)是4个字节的整型变量类型,那么最后就会按4个字节来进行划分。
如果按 malloc(200) 来看:
char * 占1字节 200个变量
int * 占4字节 50个变量
double * 占8字节 25个变量
int *p = (int *)malloc(4)
完了之后,请求来的4个字节的首地址就会给p 而通过强制类型转换,也知道了转换后的类型。
example2:
#include <stdio.h>
#include <malloc.h>
void f(int *q)
{
*q = 10;
}
//void g(int **p)
//{
//}
int main(void)
{
int *p = (int *)malloc(4); //动态请求4字节 p指向的是(int*)
printf("*p = %d\n",*p); //输出的是垃圾数字
f(p);
//g(&p); //p是int*,&p是int**
printf("*p = %d\n",*p);
return 0;
}
程序运行的结果为:*p = 10
example3:
#include <stdio.h>
#include <malloc.h>
void f(int **q)
{
**q = **q + 30;
}
int main(void)
{
int i;
int *p;
printf("请输入一个数字:\n");
scanf("%d",&i);
p = (int *)malloc(4); //动态请求4个字节的内存 //第13行
p = &i;
f(&p);
printf("i = %d\n",i);
// free(p); error,此处不能添加,否则会有内存报错。
return 0;
}
程序运行的结果为:i = 输入的任何数+30后的结果
如果将free(p)取消,程序依然可以运行,只是却变成了由系统自行分配的静态内存。
跨函数使用内存的问题:
静态变量不能跨函数使用内存:
example4:
#include <stdio.h>
void f(int **q)
{
int i = 5;
//**q等价于*p *q等价于p q和**q都不等价于p
//*q = i; error,因为*q = i; 等价于 p = i 这样写是错误的,而p中存放的是地址不是变量
*q = &i; // p = &i
}
int main(void)
{
int *p;
f(&p); //第12行
printf("%d\n",*p); //本句语法没有问题,但逻辑上有问题。
//不能读取不属于自己的内存,在规则上讲 //第13行
return 0;
}
此程序在语法上是没有问题的,但是在现实中逻辑上是有问题的,不能这样去写程序。
程序在运行到第12行的时候,实际上f函数的静态内存已经由系统自动释放掉了,
所以这个时候第13行的p指向的i变量就不存在了,换句话说就是无法访问了。
由此可见静态变量当被终止的时候就不能够被其他函数使用了。
动态内存可以跨函数使用:
example5
#include <stdio.h>
#include <malloc.h>
void f(int **q)
{
*q = (int *)malloc(sizeof(int)); //sizeof(数据类型) 返回值是该数据类型所占的字节数
// 等价于 p = (int *)malloc(sizeof(int));
// q = 5; error
// *q = 5; //p=5; error
**q = 5; //*p = 5;
}
int main(void)
{
int i = 10;
int *p;
p = &i;
f(&p);
printf("%d\n",*p); // 第15行
printf("%d\n",i); // 第16行
return 0;
}
运行结果为:5 10
解析:
因为是动态分配的内存 而p指向的是malloc(sizeof(int))中的int的4个字节的内存,而最终此程序并没有手动释放内存的
free(q),且动态内存是在堆里面进行分配的不存在出栈,所以在最后第15行,仍然可以访问之前动态分配的内存。
但是i的地址没有发生改变,所以还是10
example6:获取系统最大可申请的空间
(一直在开辟空间)
#include <stdio.h>
#include <stdlib.h>
int main(){
int cnt = 0;
void* p = NULL ;
while( p = (void*)malloc(100*1024*1024))
++cnt;
printf("%d00MB\n",cnt);
}
2.2 释放动态分配内存free()
free()
归还申请的内存。
free()
的"坑":
(1) 试一下,能否释放空指针?
- 可以,没有问题;
free(NULL)
(2) 忘记
free()
,内存泄露。
(3) 修改了申请地址,然后free()
。//无效的指针;一些ide不报错;
int* arr = (int*)malloc(n*sizeof(int));
free(arr+1);
错误:无效的指针;一些ide不报错;
(4) 多次
free()
:提示多次释放,不知名错误。
int* arr = (int*)malloc(n*sizeof(int));
free(arr);
free(arr);
错误:多次释放(>2次,不知名错误)
(5).
free()
非malloc
内存。会进行内存警告; 警告:不在堆上的对象
int n = 0;
free(&n);
int a[n];
free(a);
警告:不在堆上的对象
(6)指针悬空/野指针:访问野指针会内存出错,程序崩溃;
指针p被free或者delete之后,只是把指针所指的内存释放掉了,没有改变指针的值,此时,p沦落为野指针
这就好比你的亲戚搬家了,你手上还留着他的旧地址
修正主义:(标准)①释放内存;②地址清空
free ( p);
p = NULL;
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
int main()
{
int n;
scanf("%d",&n);
int* p = NULL;
p = (int*)malloc(n*sizeof(int));
free(p);
p[0] = 10; //已经释放掉,仍然在使用
printf("p[0] = %d\n",p[0]);
return 0;
}
2.3 初始化动态分配内存
试一下,申请的动态内存内的值是多少?
(1)malloc----随机值
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
int main()
{
int n, i ;
scanf("%d",&n);
int* p = NULL;
p = (int*)malloc(n*sizeof(int));
for(i = 0; i < n; ++i)
{
printf("p[%d]=%d\n",i,p[i]);
}
free(p);
return 0;
}
(2.1)calloc(num,sizeof(type))----0
调用calloc()函数为n个type的数组分配存储空间,且保证所有type数初始化为0:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
int main()
{
int n, i ;
scanf("%d",&n);
int* p = NULL;
p = (int*)calloc(num,sizeof(int));
for(i = 0; i < n; ++i)
{
printf("p[%d]=%d\n",i,p[i]);
}
free(p);
p = NULL;
return 0;
}
(2.2)近似于
int arr[n] = {0};
(2.3)申请到的内存不总是空的,通常需要初始化,可以使用如下方式。
注意:按照字节进行初始化,一个int:4个字节,每个字节01,所以是 01010101
int* arr = (int*)malloc(n*sizeof(int));
memset(arr,0,n*sizeof(int));
free(arr);
代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
int main()
{
int n, i ;
scanf("%d",&n);
int* p = NULL;
p = (int*)malloc(n*sizeof(int));
memset(p,0,n*sizeof(int));
for(i = 0; i < n; ++i)
{
printf("p[%d]=%d\n",i,p[i]);
}
free(p);
p = NULL;
return 0;
}
2.4 重新调整内存大小
void* realloc (void* ptr, size_t size);
那么如何动态的增加数组的长度呢:
可以使用realloc函数:realloc(arr,100) 括号里面,前面表示数组名,后面表示要扩充的字节数容量上限。
1. 如果arr当初指向的是50个字节的内存,那么运行的时候就会扩充到100.前50个字节的数据会保留那么就直接扩展这段内存,realloc()
返回原来的首地址;
2. 如果arr当初指向的是150个字节的内存,那么运行的时候就缩小到100,后50个字节的数据会丢失。那么系统会重新向内存树申请一段合适的空间,并将原来空间里的数据块释放掉,而且realloc()
会返回重新申请的堆空间的首地址;
3. 如果创建失败,返回NULL
, 此时原来的指针依然有效;
2.5 内存分配函数小结
No. | 函数 | 作用 |
---|---|---|
1 | malloc() | 分配内存块,不初始化 |
2 | calloc() | 分配内存块,初始化为0 |
3 | realloc | 调整先前分配的内存块大小 |
4 | free | 释放分配内存块 |
2.6 标准库中相关函数
头文件:string.h
No. | 函数 | 作用 | 用法 |
---|---|---|---|
1 | memset() | 填充内存 | memset(arr,0,size); |
2 | memcpy() | 内存拷贝 | memcpy(目的,源,size); |
3 | memmove() | 内存移动,类似于覆盖,可以重叠 | memmove(目的,源,size); |
4 | memcmp() | 内存比较 ;如果返回值 < 0,则表示 str1 小于 str2。如果返回值 > 0,则表示 str2 小于 str1。如果返回值 = 0,则表示 str1 等于 str2。 | memcmp(str1,str2,size); |
5 | memchr() | 查找内存中第一个出现指定字符的位置 |
这些函数通常要加上头文件stdlib.h
或者string.h
。
memcpy
(1)size类似于malloc,字节;
(2)只针对对应的地址进行覆盖,其他保持不变;类似于赋值;
最后一个参数是需要拷贝的字节的数目!一个int类型占据4个字节!这样的话,本题5字节,实际上只能移动2个数字(往大的去)。如果要想达到将a地址开始的5个元素拷贝到a+3地址处,需要这么写:
memcpy(a + 3, a, 5*sizeof(int));
- 练习
(1)两个有序的数组的合并;
#include <stdio.h>
#include <string.h>
int main(){
int n;
scanf("%d",&n);
int a[n];
for(int i=0;i<n;++i){
scanf("%d",a+i);
}
int m;
scanf("%d",&m);
int b[m];
for(int i=0;i<m;++i){
scanf("%d",b+i);
}
int res[n+m];
int i=0,j=0,k=0;
while(i<n && j<m){
res[k++] = a[i]<b[j]?a[i++]:b[j++];
/*
if(a[i] < b[j]){
res[k] = a[i];
++k;
++i;
}else{
res[k] = b[j];
++k;
++j;
}
*/
}
if(i<n)
memcpy(res+k,a+i,sizeof(int)*(n-i));
/*如果a数组中还有剩余的元素没有被处理,那么我们就需要将这些元素复制到结果数组的末尾。
这是通过memcpy(res+k, a+i, sizeof(int)*(n-i))实现的,
其中res+k是结果数组中下一个要被写入的位置,a+i是a数组中下一个要被复制的位置,sizeof(int)*(n-i)是要复制的字节数。
同理,如果b数组中还有剩余的元素没有被处理,我们也需要将这些元素复制到结果数组的末尾。
这是通过memcpy(res+k, b+j, sizeof(int)*(m-j))实现的。
*/
if(j<m)
memcpy(res+k,b+j,sizeof(int)*(m-j));
for(int i=0;i<n+m;++i){
printf("%d\n",res[i]);
}
}
(2) 从终端输入未知数量的数字,按键Ctrl+D作为结束,逆序输出输入的数字;
注意:(1)阻塞式只在按下回车后才检测之前是否有Ctrl + Z 是否按下(Windows一般采用阻塞式,Ctrl + Z表示EOF)。
(2)非阻塞式 是指按下 Ctrl 的组合键 之后立即响应的方式(Unix / Linux 一般采用非阻塞式, Ctrl + D 表示EOF)。
***方案1:①用一个数组进行赋值(t),另一个数组进行拷贝( p)②空间里面有记录个数的内容,不打印出来,一般不会出现,按照方案3进行; ***
#include <stdio.h>
#include <stdlib.h>
void PrintMem(void* p,int n){
char* q = (char*)p;
for(int i=0;i<n;++i){
printf("%p %hhd\n",p+i,q[i]);
}
}
int main(){
int num;
int* p = NULL;
int count = 0;
while(scanf("%d",&num)!=EOF){
// printf("%d\n",num);
// realloc
int* t = malloc((count+1)*sizeof(int));
printf("t=%p\n",t);
PrintMem(t-2,(count+4)*sizeof(int));
if(NULL != p){
// 原来数据的拷贝
for(int i=0;i<count;++i){
t[i] = p[i];
printf("t%d = %p\n",i,t+i);
//printf("p%d = %p\n",i,p+i);
}
// memcpy
t[count] = num;//
printf("p=%p\n",p);
free(p);// 释放原来空间
p = t;
}else{
p = t;
p[count] = num;
}
++count;
}
for(int i=count-1;i>=0;--i){
printf("%d ",p[i]);
}
printf("\n");
//printf("Over\n");
free(p);
p = NULL;
}
方案2:①用一个数组进行赋值(t),另一个数组进行拷贝( p);②自带的函数
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(){
int num;
int* p = NULL;
int count = 0;
while(scanf("%d",&num)!=EOF){
// printf("%d\n",num);
// realloc
int* t = malloc((count+1)*sizeof(int));
if(NULL != p){
// 原来数据的拷贝
/*for(int i=0;i<count;++i){
t[i] = p[i];
}*/
memcpy(t,p,count*sizeof(int));
t[count] = num;//
free(p);// 释放原来空间
p = t;
}else{
p = t;
p[count] = num;
}
++count;
}
for(int i=count-1;i>=0;--i){
printf("%d ",p[i]);
}
printf("\n");
free(p);
p = NULL;
//printf("Over\n");
}
方案3:①malloc在原空间基础上进行扩充即可;②数组元素仍用-1的数据
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(){
int num;
int* p = NULL;
int count = 0;
while(scanf("%d",&num)!=EOF){
++count;
p = realloc(p, count*sizeof(int));
p[count-1] = num;
}
for(int i=count-1;i>=0;--i){
printf("%d ",p[i]);
}
printf("\n");
free(p);
p=NULL;
//printf("Over\n");
}
3. 动态分配内存面试题
分别运行下面的Test,会出现什么情况:
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test1(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
/*运行上一句时程序崩溃,因为GetMemory(char *p)不能传递动态内存,原因是GetMemory(str)的参数是值传递,不会改变str的值*/
printf(str);
}
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test2(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
/*乱码,*GetMemory()返回的是栈指针,在函数结束时已经栈退解,内容未知*/
}
void GetMemory2(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test3(void)
{
char *str = NULL;
GetMemory2(&str, 100);
strcpy(str, "hello");
/*能正确输出"hello",但有一个问题,内存泄露,malloc的内存没有free*/
printf(str);
free(str);
}
void Test4(void)
{
char *str = (char *)malloc(100);
strcpy(str, "hello");
free(str);
if (str == NULL)
{
strcpy(str, "world");
printf(str);
}
/*可以输出"world",因为free之后str变为了野指针,但是并没有置为NULL,因此还会继续执行下面的内容*/
}