文章目录
一. 为什么存在动态内存管理
因为在之前我们学习的开辟空间,具有一定的局限性,空间的大小是固定的,在很多的时候达不到我们想要的目的。静态的开辟空间只能开辟多少就是多少,并没有动态开辟内存灵活,在动态开辟空间中,可以空间不够了,继续申请,空间开辟多了,可以释放,更加的灵活。动态内存管理开辟的是一段连续的空间。
二.动态内存的函数介绍
1.malloc
在这里我们可以看到malloc的参数是无符号整型,而它的返回类型是void*,返回的是它开辟空间的起始地址,为什么是void?因为它只知道开辟多少空间,而并不知道它要赋给什么类型,所以才用void
#include<stdlib.h>//malloc,free,calloc,realloc的头文件
下面我们就来看一下它是如何来使用的
#include<stdio.h>
#include<stdlib.h>
int main()
{
int arr[10] = {0};//我们之前学习的在栈上开辟空间
void* p = malloc(40);
//malloc就是void的类型,所以用p接收没问题
int* str = (int*)malloc(40);//开辟40字节的空间
return 0;
}
malloc也可能出现内存开辟失败的情况,然后其返回为空指针,这是一个危险的情况,所以我们每次在开辟内存之后,要验证一下开辟的空间是否成功。
int* str = (int*)malloc(INT_MAX);//开辟最大空间,肯定会报错
if (str == NULL)
{
perror(str);
return 1;
}
输出结果:
malloc的使用
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* str = (int*)malloc(40);//开辟40字节的空间
if (str == NULL)
{
perror(str);
return 1;
}
for (int i = 0; i < 10; i++)
{
*(str + i) = i;
}
for (int i = 0; i < 10; i++)
{
printf("%d",*(str+i));
}
free(str);
str=NULL;
return 0;
}
2.free
free的作用也就是对开辟的动态内存空间进行释放,动态开辟内存并不像函数那样,在栈区,运行结束就自动释放内存空间,动态内存空间开辟在堆区需要自己释放它的空间。
继续看上面的代码
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* str = (int*)malloc(40);//开辟40字节的空间
if (str == NULL)
{
perror(str);
return 1;
}
for (int i = 0; i < 10; i++)
{
*(str + i) = i;
}
for (int i = 0; i < 10; i++)
{
printf("%d",*(str+i));
}
free(str);
str=NULL;
return 0;
}
释放完空间要对指针变量置为NULL,否则或如果后面使用到该指针变量,就会出现野指针的错误,定义指针变量未初始化
3.calloc
calloc是和malloc差不多的,区别在于calloc在开辟空间的同时,对内存进行了初始化,还有就是参数上的差别。
也可以理解为 malloc+memset=calloc
下面看一下calloc是如何应用的
#include<stdio.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
perror("calloc");
}
for (int i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (int i = 0; i < 10; i++)
{
printf("%d", *(p + i));
}
free(p);
p = NULL;
return 0;
}
malloc和free,calloc和free都是两两成对出现的,如果没有进行free的话,就属于内存泄漏的问题,开辟了空间并没有用,别人也不能用。
但是它们两两成对也并不能代表就不会出现错误,来看下面的例子:
int test()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return 1;
}
if (1)//在还没有到free就提前返回了
{
return 2;
}
for (int i = 0; i < 10; i++)
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}//内存泄露
int main()
{
test();
return 0;
}
4.realloc
realloc函数的出现让动态内存管理更加灵活。
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,
那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。
那 realloc 函数就可以做到对动态开辟内存大小的调整
下面就来看一下它是怎么用的
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
perror("calloc");
return 1;
}
for (int i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (int i = 0; i < 10; i++)
{
printf("%d", *(p + i));
}
int* ptr = (int*)realloc(p, 80);//增加空间
//realloc开辟空间失败的话也是返回NULL
if (ptr != NULL)
{
p = ptr;
ptr = NULL;
}
free(p);
p = NULL;
return 0;
}
再增容的时候,也会有一些细节问题,接下来我们来看一下:
接下来我们在编译器中看一下扩容时改变地址的问题
在下面我们可以看到ptr和p的地址是不一样的,realloc开辟了18000的空间,也就说明了扩容太大的话可能会改变地址。
下面这个图片是realloc开辟80字节的空间,p的后面内容够用,所以就没有改变地址。
int main()
{
realloc(NULL, 40);//也就等于malloc(40)
return 0;
}
三.常见的动态内存错误
1.对NULL指针的解引用操作
在动态内存开辟空间的时候,开辟完之后要验证一下是否开辟成功,如果没有开辟成功,会返回NULL
所以也就会出现对NULL指针的解引用的错误。
int main()
{
int* p = (int *)malloc(40);
//*p = 5;//这样直接赋值是错误的,需要验证一下
if (p == NULL)
{
return 1;
}
else
{
*p = 5;
}
free(p);
p=NULL;
return 0;
}
2.对动态开辟空间的越界访问
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i <= 10; i++)
{
*(p + i) = i;//i=10时是越界访问
}
free(p);
p = NULL;
return 0;
}
3.对非动态开辟内存使用free释放
非动态内存当中并不需要用free来释放空间,它是出了函数就自动释放空间了
int main()
{
int a = 0;
int* p = &a;
free(p);
p = NULL;
return 0;
}
4.使用free释放一块动态开辟内存的一部分
意思也就是开辟空间之后,在使用过程中指向起始地址的指针,并没有指向起始地址,而最后释放这个没有指向起始地址指针,就会出现错误。所以我们在使用动态开辟内存时一定要记录起始位置。
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i <= 5; i++)
{
*p= i;
p++;
}
free(p);//释放时p已经不是起始位置了
p = NULL;
return 0;
}
5.对同一块动态内存多次释放
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 0;
}
free(p);
free(p);//重复释放
return 0;
}
6.动态开辟内存忘记释放(内存泄漏)
下面就属于内存泄漏,开辟了空间却忘记释放
int test()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
return 0;
}
int main()
{
test();
return 0;
}
四.经典笔试题
第一题:
void GetMemory(char* p)//p接收的为NULL,并非str地址
{
p = (char*)malloc(100);//开辟完空间并没有返回
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");//这里的str仍为NULL
printf(str);
//没有进行内存释放
}
int main()
{
Test();
return 0;
}
改正后的代码
void GetMemory(char** p)//传过来str地址
{
*p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);
free(str);//释放内存
str = NULL;
}
int main()
{
Test();
return 0;
}
也可以改为这样,不过下面这种方法,传参就没有任何意义了
char* GetMemory(char* p)
{
p = (char*)malloc(100);
return p;
}
void Test(void)
{
char* str = NULL;
str=GetMemory(str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
第二题:
char* GetMemory(void)
{
char p[] = "hello world";
return p;//当出了这个函数p的内容就销毁了
}
void Test(void)
{
char* str = NULL;
str = GetMemory();//str为野指针
printf(str);
}
int main()
{
Test();
return 0;
}
第三题
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
//开辟空间之后没有进行释放
}
int main()
{
Test();
return 0;
}
改正后的代码
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;
}
int main()
{
Test();
return 0;
}
第四题
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;
}
改正后的代码
void Test(void)
{
char* str = (char*)malloc(100);
if (str == NULL)
{
perror("malloc");
return ;
}
strcpy(str, "hello");
free(str);
str = NULL;
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
五.C/C++程序的内存开辟
C/C++程序内存分配的几个区域:
- 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结
束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是
分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返
回地址等。 - 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分
配方式类似于链表。 - 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
- 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序
结束才销毁,所以生命周期变长。
六.柔性数组
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
struct S
{
int a;
char b;
int arr[];//柔性数组成员
//有一些其他编译器的写法是
//int arr[0];两种方法都可以
};
1.柔性数组的特点
结构中的柔性数组成员前面必须至少一个其他成员。
sizeof 返回的这种结构大小不包括柔性数组的内存。
包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大
小,以适应柔性数组的预期大小
2.柔性数组的使用
方法一
struct S
{
int a;
char b;
int arr[];
};
int main()
{
struct S *ptr=(struct S *)malloc(sizeof(struct S) + 4 * sizeof(int));
if (ptr == NULL)
{
perror("malloc");
return 1;
}
ptr->a = 100;
ptr->b = 'a';
for (int i = 0; i < 4; i++)
{
ptr->arr[i] = i;
}
printf("%d %c\n",ptr->a,ptr->b);
for (int i = 0; i < 4; i++)
{
printf("%d",ptr->arr[i]);
}
struct S* ps = (struct S*)realloc(ptr,sizeof(struct S)+10*sizeof(int));
if (ps == NULL)
{
perror("realloc");
return 2;
}
free(ps);
ps = NULL;
return 0;
}
不用柔性数组,也可以达到同样的目的
方法二
struct S
{
int a;
char b;
int *arr;
};
int main()
{
struct S* ptr = (struct S*)malloc(sizeof(struct S));
if (ptr == NULL)
{
perror("malloc");
return 1;
}
ptr->a = 100;
ptr->b = 'a';
int* p = (int*)malloc(4 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
else
{
ptr->arr = p;
}
for (int i = 0; i < 4; i++)
{
ptr->arr[i] = i;
}
printf("%d %c\n", ptr->a, ptr->b);
for (int i = 0; i < 4; i++)
{
printf("%d", ptr->arr[i]);
}
int* ps= (int*)realloc(ptr->arr, sizeof(int) * 10);
if (ps == NULL)
{
perror("realloc");
return 2;
}
else
{
ptr->arr = ps;
}
free(ptr);
ptr = NULL;
free(ptr->arr);
ptr->arr = NULL;
return 0;
}
虽然两种方法都能实现同样的功能,但是还是方法一相对来说更好
第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给
用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你
不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好
了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度.
连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正
你跑不了要用做偏移量的加法来寻址)