1. 为什么要有动态内存分配
动态内存分配为我们提供了很大的便利,如果我们想要一块自定的内村大小,可以通过动态内存管理来实现,从而提升了代码的灵活性,之前我们学习的空间开辟一共两种,
int a = 1;
int arr[] = {1,2,3};
这两种方式并不能根据我们自己的需求来修改需要的内存,这两种内存已经是固定的,无法进行修改了的,数组大小是在一开始就申明好的,无法进行修改数组大小的操作。
2. malloc和free
c语言提供了一个开辟内存的函数叫malloc,具体怎么用呢看一段代码,
void* malloc (size_t size);
这是malloc的使用,参数是一个size_t类型的,返回类型是void*,参数传入要开辟空间的大小,接下来使用一下malloc
int*a = (int*)malloc(4*sizeof(int));
这段代码是使用malloc,首先创建一个int*a的指针来接收,因为malloc返回的开辟空间的地址值,是一个void*型,我们要用int*来强转,然后再创建一个int*的指针来接收,a指向的便是开辟的空间,类似于数组,但是这种内存空间是可控的.
如果开辟开辟失败则返回空指针NULL,如果参数 size 为0,malloc的⾏为是标准是未定义的,取决于编译器。
2.2 free
c语言提供了另外一个函数free,是专门用来回收内存空间的 ,
void free (void* ptr);
free如果参数不是动态空间,那free的行为是未定义的,如果参数是空指针,那free是不进行任何操作的,malloc和free都在<stdlib.h>里的,举个例子
#include <stdio.h>
#include <stdlib.h>
int main()
{
int num = 0;
scanf("%d", &num);
int arr[num] = {0};
int* ptr = NULL;
ptr = (int*)malloc(num*sizeof(int));
if(NULL != ptr)//判断ptr指针是否为空
{
int i = 0;
for(i=0; i<num; i++)
{
*(ptr+i) = 0;
}
}
free(ptr);//释放ptr所指向的动态内存
ptr = NULL;//是否有必要?
return 0;
}
图中的ptr=NULL是否有必要呢,仔细想来,ptr指向的空间被free回收了,但是ptr依然是指向那个空间,因此ptr就成为了一个野指针,野指针是很危险的,所以把ptr置为空指针是很有必要的。
3. calloc和realloc
3.1 calloc
c语言还提供了一个函数叫calloc,和malloc类似的。
void* calloc (size_t num, size_t size);
calloc两个参数,第一个参数是开辟个数,第二个是开辟类型,所以和calloc类似,但是不同的是,malloc会把所有开辟空间的值都设置为0 ,举个例子
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p = (int*)calloc(10, sizeof(int));
if(NULL != p)
{
int i = 0;
for(i=0; i<10; i++)
{
printf("%d ", *(p+i));
}
}
free(p);
p = NULL;
return 0;
}
输出结果:0 0 0 0 0 0 0 0 0 0
所以如果我们对申请的内存空间的内容要求初始化,那么可以很⽅便的使⽤calloc函数来完成任务。
3.2 realloc
realloc从名字上面理解一下,re是又的意思,所以relloc是又开辟一块新的空间,并且把原先的值赋值到新的空间里面去,• 有时会我们发现过去申请的空间太⼩了,有时候我们⼜会觉得申请的空间过⼤了,那为了合理的时
候内存,我们⼀定会对内存的⼤⼩做灵活的调整。那 realloc 函数就可以做到对动态开辟内存⼤⼩的调整。
函数原型如下:
void* realloc (void* ptr, size_t size);
ptr是需要修改的地址,size为修改后的内存
realloc分为两种情况,
第一种是原先有足够大的空间地址,会在后面继续开辟剩下的地址
第二种是原先没有足够大的空间,会重新申请一块新的空间来存放,并且把原先的数据都复制到这个空间内。
4. 常⻅的动态内存的错误
4.1 对NULL指针的解引⽤操作
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
INT_MAX
是一个非常大的值,通常是2^31 - 1
(在32位系统中),即 2147483647。INT_MAX / 4
的值大约是 536870911。malloc
函数尝试分配约 2 GB 的内存,这在大多数系统上是不合理的,因为大多数用户程序无法分配这么多内存。这个分配很可能会失败,返回一个空指针(NULL
)。
所以我们要进行检测,p是否为空指针。
4.2 对动态开辟空间的越界访问
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释放
free是专门用来释放动态内存的,如果对非动态内存来进行释放,同样程序会崩溃掉的。
void test()
{
int a = 10;
int *p = &a;
free(p);//ok?
}
4.4 使⽤free释放⼀块动态开辟内存的⼀部分
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
如果p指向的不是动态内存的起始地址,同样,程序会直接崩溃的。
4.5 对同⼀块动态内存多次释放
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
对动态空间的重复释放,也会导致程序的崩溃。
4.6 动态开辟内存忘记释放(内存泄漏)
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
在主函数中,调用完test函数后,函数创建的动态内存没有被释放掉,而程序也没有停下的意思,就造成了内存泄漏。
总计一下,动态内存空间的释放一共就两种方式,第一种就是使用free函数,第二种就是等待整个程序的结束吗,整个程序结束后,动态内存的空间就被释放掉了,在一些服务器中,需要24小时不停止的运行,因此动态空间忘记释放之后,并不能通过程序结束来释放,因此在使用完动态空间之后,一定要进行空间的释放,并且把指向动态空间的指针置为空指针,防止出现野指针。
5. 动态内存经典笔试题分析
5.1 题⽬1:
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
分析一下,程序能打印出来Hello world吗。
答案是不能的,因为在传入str中,p只是一个副本,p指向的空间也只是一个副本,离开了这个函数并不会对str有什么影响。
5.2 题⽬2:
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
还是不会输出hello world,虽然用str接收了返回值,但是数组p在函数运行之后,内存就已经释放掉了。
5.3 题⽬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 world的,因为传入的是str的地址,修改的也是str值指向的内容
5.4 题⽬4:
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
这个也是不行的,str在释放之后,指向的内存空间已经不复存在,但是str依然指向一块内存,所以str已经成为了一个野指针,再次拷贝字符串,程序会崩溃。
6. 柔性数组
柔性数组根据名字我们可以大概来猜一下,这个数组应该是可变数组。
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
typedef struct st_type
{
int i;
int a[];//柔性数组成员
}type_a;
a可以写0也可以不写,但是有些编译器会报错,此时就可以把0删除即可。
6.1 柔性数组的特点:
柔性数组是结构体最后一个成员,并且他的前面一定有其他成员变量,sizeof计算结构体大小时是不会计算柔性数组大小的,包含柔性数组成员的结构⽤malloc()函数进⾏内存的动态分配,并且分配的内存应该⼤于结构的⼤⼩,以适应柔性数组的预期⼤⼩。
例如:
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
int main()
{
printf("%d\n", sizeof(type_a));//输出的是4
return 0;
}
6.2 柔性数组的使⽤
struct S
{
int i;
int a[];
}a;
int main()
{
struct S*a = (struct S*)malloc(4 + 40);
a->i = 0;
}
因此a[]就有了40个字节大小的空间。
6.3 柔性数组的优势
上述的代码通过这个代码也可以实现
struct S
{
int i;
int* a;
}a;
int main()
{
a.i = 0;
int* p = (int*)malloc(40);
if (p != NULL)
{
a.a = p;
}
}
两种代码是不同风格的代码,第一种是直接在结构体里开辟40个字节的空间,而第二个是在结构体里创建一个指针,指针指向一个40字节的空间,二者各有利弊。
第⼀个好处是:⽅便内存释放
如果我们的代码是在⼀个给别⼈⽤的函数中,你在⾥⾯做了⼆次内存分配,并把整个结构体返回给⽤⼾。⽤⼾调⽤free可以释放结构体,但是⽤⼾并不知道这个结构体内的成员也需要free,所以你不能指望⽤⼾来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返回给⽤⼾⼀个结构体指针,⽤⼾做⼀次free就可以把所有的内存也给释放掉。
第⼆个好处是:这样有利于访问速度.
连续的内存有益于提⾼访问速度,也有益于减少内存碎⽚。(其实,我个⼈觉得也没多⾼了,反正你跑不了要⽤做偏移量的加法来寻址)
7. 总结C/C++中程序内存区域划分
C/C++程序内存分配的⼏个区域:
1. 栈区(stack):在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时
这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内存容量有限。栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。
2. 堆区(heap):⼀般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配⽅
式类似于链表。
3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体(类成员函数和全局函数)的⼆进制代码。