一、动态内存分配
1.1 为什么会出现动态内存分配
我们已知的内存开辟方式,大多是以 int a或者 int arr[10] 这种。
int a; 表示在栈空间中申请4个字节用来放一个整型。
int arr[10],在栈空间中申请连续的40个字节,用来存放10个int类型。
这两种方式的特点就是:
(1)空间开辟大小是固定的,无法变化
(2)数组在申明的时候,必须指明数组的长度,他所需要的内存存在编译时分配
但是有时候我们需要的空间大小在程序运行的时候才能知道,是有变大变小的需求的,这种时候就需要动态内存分配了
1.2 动态内存分配函数
下面四个函数头文件都为 <stdlib.h>
malloc
- void* malloc (size_t size); 向内存申请一块连续可用的空间,并返回指向这块空间的指针
- 创建size个字节的空间,创建成功返回指向这个空间的指针,创建失败则返回NULL
- 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
- 注意如果创建失败了,但是访问了,是非法访问。因为空指针是不能访问的,所以malloc创建空间后,要进行一次是否为空指针的判断
- 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
free
- void free (void* ptr); 动态内存的释放和回收
- 使用场景:在动态内存开辟之后,如果要把这个内存主动释放掉,就需要free函数,如果没有主动释放,程序结束之后,会自动销毁。但在程序结束之前,有【闲置】的隐患
- free释放空间是指,用户没有了对这块区域的使用权限,但是本身的值是没有改变的,为了避免误用指向释放空间的指针,在free使用后,需要赋值NULL
- 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
- 如果参数 ptr 是NULL指针,则函数什么事都不做。
calloc
- void* calloc (size_t num, size_t size);
- 为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0
- 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0
- 相比malloc,效率更低(多了初始化的操作)
realloc
- void* realloc (void* ptr, size_t size); 重新分配空间
- 使用场景:我想要这个空间的大小改变,变大或变小。
- ptr指的是要重新分配空间的那个空间的地址,size指的是重新分配的空间的新大小,返回值为调整之后的内存起始位置
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
- realloc在调整内存空间的是存在两种情况:
- 情况1:原有空间之后有足够大的空间
- 要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化(返回旧的起始地址)
- 情况2:原有空间之后没有足够大的空间
- 原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用,把旧空间的数据拷贝到新空间,并且把旧的空间释放掉,这样函数返回的是一个新的内存地址
- 情况1:原有空间之后有足够大的空间
- (int*)realloc(NULL,40); == malloc(40);
1.3 常见的动态内存分配错误
对NULL指针的解引用操作
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
解析:
INT_MAX是个极大的数字,除以4之后依旧很大,堆区里无法开辟如此大的空间,所以开辟失败,返回空指针。解引用空指针,是非法操作
对动态开辟空间的越界访问
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;
}
free(p);
}
解析:
malloc里面开辟的是10个int类型的字节大小,即40字节大小。但是只会for循环访问却是共访问了11个int类型,当i=10的时候,会发生越界访问
对非动态内存开辟使用free释放
void test()
{
int a = 10;
int *p = &a;
free(p);
}
解析:
如果free形参指向的空间不是动态开辟的,那free函数的行为是未定义的
使用free释放一块动态开辟内存的一部分
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);
}
解析:
p原本指向新开辟的内存空间,但是后置++后,p指的空间就变成了新开辟空间的第四个字节的位置,因为p指向的不是动态内存的起始位置,所以free只释放了动态开辟内存的一部分,是非法的
对同一块动态内存多次释放
void test()
{
int *p = (int *)malloc(100);
free(p); //但是如果在第一次释放之后,赋值为空,可以过。【空指针可以重复释放】
free(p);//重复释放
}
动态开辟内存忘记释放(内存泄漏)
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
解析
free的作用就是断开联系,将这块内存还给操作系统。这里指针p忘记释放了,同时因为p是局部变量,出了函数就要被销毁,p用不上这个空间了,别不知道这个空间的地址,也用不了,没人记得这块空间,所以也再也找不到这个空间了。
返回栈空间地址的问题
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
解析:
p是str的临时拷贝,p所指向的空间和str的空间是分开的,所以下面有两个错误:
(1)str仍然是空指针,空指针是不能被访问的
(2)p是局部变量,会被销毁,也没有主动释放,无法再找到申请的这100个字节空间,会造成内存泄漏
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
解析:
打印的结果是烫烫烫烫
str里面存的是p的地址,p里面存的是常量字符串首元素的地址,即’h’的地址,但是因为p是局部变量,出函数范围会被销毁,所以到printf函数的时候,str里面存的东西已经还回去了,再打印访问就是非法访问
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
解析:
能正常打印,但是没有主动释放,存在内存泄漏的问题
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
解析:
打印的结果是world,但是存在问题(非法访问)
free会释放空间,即【对于这块空间,没有权限访问】,但是里面的值未动,str依旧指的是’h’的地址,而后通过strcpy复制,所以打印的是world
访问没有权限的空间,是非法访问
二、C/C++程序的内存开辟
C/C++程序内存分配的几个区域:
- 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
- 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
- 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放
- 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
三、柔性数组
3.1 什么是柔性数组
概念:
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员
使用场景:
数组的创建内存和数据一般是固定的,如int arr[10];但是如果我想要这个数组里面的空间变化,就需要柔性数组来实现空间的变大或变小
结构形式
typedef struct st_type
{
int i;
int a[0];//柔性数组成员 数组的大小是未知的
}type_a;
typedef struct st_type
{
int i;
int a[];//柔性数组成员 数组的大小是未知的
}type_a;
特点
- 结构中的柔性数组成员前面必须至少一个其他成员(如果一个成员都没有,没办法确定内存大小,从而开辟空间)
- sizeof 返回的这种结构大小不包括柔性数组的内存
- 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
3.3 柔性数组的使用
int i = 0;
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
p->i = 100;
for(i=0; i<100; i++)
{
p->a[i] = i;
}
free(p); //这样柔性数组成员a,相当于获得了100个整型元素的连续空间
下面的代码可以实现同样的效果
typedef struct st_type
{
int i;
int *p_a;
}type_a;
type_a *p = (type_a *)malloc(sizeof(type_a));
p->i = 100;
p->p_a = (int *)malloc(p->i*sizeof(int));
for(i=0; i<100; i++)
{
p->p_a[i] = i;
}
free(p->p_a);
p->p_a = NULL;
free(p);
p = NULL;
区别:
方法1相比于方法2有两个好处:
方便释放内存
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给
用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你
不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好
了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
有利于访问速度
连续的内存有益于提高访问速度,也有益于减少内存碎片。(虽然都需要用偏移量的加减来找位置,并没有省很多)