动态内存的意义
在了解动态分配之前,回顾一下之前学的内存开辟方式。
int a=20;
这样就开辟了一块类型为int,存放数据为20的空间,并为其取名为a。
但是,这样的开辟方式,所开辟的空间大小是固定
的。
那么,当我们遇到了在编译前无法获知数据所需大小,只有运行时才知道
的情况时,就应当采取动态内存分配了。
摘录一段百度百科的解释:
在c/c++语言中,编写程序有时不能确定数组应该定义为多大,因此这时在程序运行时要根据需要从系统中
动态多地获得内存空间
。所谓动态内存分配,就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存
的方法。动态内存分配不像数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小
。
c/c++的内存开辟
在c/c++的程序中,内存会被分配在以下几个区域:
- 栈区(stack):
分配给进程的采用先进后出方式访问的内存区
。在执行函数时,内部变量都可在栈上创建,当函数执行完毕则自动销毁
。有着效率高,但分配容量有限的特点。栈区里主要存放局部变量,函数参数,返回数据与地址
等。普通的局部变量基本都在栈区进行分配空间,所以在栈区上的变量只要一出作用域就会被销毁
。 - 堆区(heap):一般由程序员分配释放,其方式类似于链表。
- 数据段(静态区):也就是熟知的
static
,存放全局变量与静态数据
,待程序结束
后才会由系统释放。这也是为何其生命周期同程序一同结束的原因。 - 代码段:存放函数体的
二进制代码
。
动态内存函数
C语言允许建立内存动态分配区域,以存放一些临时用的数据,这些数据不必在程序的声明部分定义,也不必等到函数结束时才释放,而是需要时随时开辟,不需要时随时释放。C语言中,内存的动态分配是通过系统提供的库函数来实现的,主要有malloc
、calloc
和 free
函数。
malloc函数
malloc与free都包含于stdlib.h
头文件中。
其声明为:
void* malloc(size_t size);
其作用为:向函数内存申请一块
连续可用
空间,并返回指向这块空间
的地址。
- 如果开辟失败,就会返回
null指针
。 - 开辟成功,就返回指向开辟空间的指针。
- size
不应该设置为0
,因为此时其行为未定义,取决于编译器。
使用示例:
int* p=NULL;
p=(int*)malloc(40);
申请了一块空间,大小为40个字节,由指针p来指向这块空间。
可能有人会有疑惑:
为何malloc前需要声明未曾提及的强制类型转换?
在编译器上不加强制类型转换
一般也不会报错或者发出警告。
在ANSI/ISO标准c下,反而不如直接使用malloc,使用malloc进行强转,有助于将程序更方便的移植到c++
里。
所以在这里使用强转大概是因为很多老师将c/c++混在一起教学,且包括我的学校等都在使用devc++这种老掉牙的编译器。
因为不知道是否申请内存成功,所以一定要检查返回值
if(p != NULL)
{
//操作
}
或者if(p==NULL),就打印申请失败等类似字面意思。
free函数
free函数通常会与malloc一起使用
。如果在使用完一份空间后不使用free进行销毁,会造成内存泄露
这样的严重情况。
在使用完一份空间后加上这样的代码块
free(p);
p=NULL;
这里在释放内存后一定要主动将p置为空
。
calloc函数
calloc函数也是用来进行内存分配的。
其声明为:
void* calloc(size_t num,size_t size);
功能为向num
个大小为size
的元素开辟一块空间,并且将空间的所有字节化为0
。
这点与malloc是有出入的。malloc并不会将每个字节都化为0。具体使用哪个看实际情况。
使用示例:
int* p=(int*)calloc(10,sizeof(int));
申请了一块空间,其大小为int*10。
也是注意在使用完后,一定要free掉。
realloc函数
如果发现之前使用malloc,calloc申请的空间太小或太大
了,就可以使用realloc来做一个灵活的调整。
其声明为:
void *realloc(void *ptr,size_t size);
- 其中的ptr是需要调整的
内存地址
。 - size是调整后的
新大小
。 - 函数返回值是调整后
内存的起始位置
。 - 函数在调整原内存空间大小基础上,会将原有内存中数据
移到新空间
。
realloc在调整空间时,会有两种情况出现。
若是原有空间后面有足够大小
的空间,那么扩展内存就是直接追加空间
,原空间数据不变化。
但是若空间不够,就需要在堆空间里另找
合适大小的连续空间,就会导致函数返回一个新的地址
。
在进行扩展时,尽量不要有这种写法。
ptr=(int*)realloc(ptr,1000);
如果申请失败,会把原有的数据也丢了。所以请定义一个新指针
来作为返回值,如果其不为空,再赋给ptr,这样更为稳妥。
动态内存常见的错误
越界访问动态开辟的空间
int *p=malloc(5*sizeof(int));
if(p==NULL)
printf("malloc failed\n");
for(int i=0;i<10;i++)
*(p+i)=i;
free(p);
很显然,只开辟5个int大小的空间,却往里面放入了10个int大小的数据,这是不可行的。
对空指针进行解引用的操作
int *p=malloc(5*sizeof(int));
*p=20;
free(p);
若申请空间失败,导致p的值仍然是NULL,就会出现问题。所以提倡对是否申请成功
一定要判断。
用free释放一块动态开辟内存空间
int *p=malloc(5*sizeof(int));
p++;
free(p);
此时的p已经不再指向动态内存的起始位置
。所以free的操作无法执行完全。
多次使用free释放
int *p=malloc(5*sizeof(int));
free(p);
//操作
free(p);
这种重复释放的情况也是不被允许的,但是在使用完动态开辟的内存空间之后,一定要记得free掉,不然会造成内存泄漏
等现象。
传参出错
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
上面这段代码希望将str的内存分配在函数部分完成,然后回到主函数将hello world拷贝到str里。但是事与愿违,其函数传递的参数是有误的,想要改变str里的值,就必须传递str的地址
。接收str的p不过是一份拷贝,p的改变不代表str的改变
。所以str仍然是NULL,在strcpy的阶段会因参数在0地址空间而发生访问内存出错
。
所以将gtememory里的参数替换为&str
,同时接收的参数换为char**p,以二级指针的形式来接收。
忽略栈帧的特性
char *GetMemory(void)
{
char p[] = "hello world";
return p; }
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
这串代码采用了返回一个指针来让str接收的方法,但却忽略了临时变量出了其作用域,就会被销毁
的特点。所以仍然会出错。在这一点想要深究可以尝试理解栈帧在程序里的作用与运作。
野指针
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
} }
在这里确实还可以正常输出,但需要注意的是,将str free掉以后,str虽然仍旧指向hello,但对其已经没有使用权限
!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);
}
忘记了free,这点非常重要。在使用完内存空间后一定要释放掉
!
柔性数组
柔性数组(flexible array),在c99中,结构中
最后
一个元素允许是未知大小
的数组。
struct a
{
int i;
int arr[];
};
其具备以下的特点:
- 在结构中的柔性数组成员
前
必须要有一个其他类型
的成员。 - sizeof返回的结构体大小并
不包含柔性数组的内存
。 - 如果结构体中包含了柔性数组,那么就应该使用
动态内存分配
的形式,其分配的内存也应该大于结构的大小。
如下为上面的结构体中的柔性数组成员赋100个整型元素的连续空间。
struct a *p=malloc(sizeof(struct a)+100*sizeof(int));
p.i=0;
for(int i=0;i<100;i++)
{
p.arr[i]=[i];
}
使用柔性数组可以利于提升访问速度
,因为其使用的是一段连续的内存空间。