动态开辟是一种按需求开辟,并且可以改变开辟空间大小的一种灵活的内存开辟方式。在开始这部分内容之前,先来看一下内存的大致分配,其中堆区主要是进行动态内存开辟的:
一、常用动态内存函数的介绍
头文件都是 <stdlib.h>,开辟失败为了方便的看到错误,会用到函数 perror ( ) ,它是 strerror( ) 的改良还记得吗?
1、malloc ( )
void *malloc( size_t size );
该函数表示向内存申请一块连续可用的空间,它的形参表示要开辟的内存大小,单位是字节。( malloc(0)是允许的,也会返回一个指针,只是没有空间所以不可使用而已)malloc的实参可以是常量或者常量表达式,也可以是变量。
返回值为开辟好的那块空间的地址的指针,指针类型是void*,若是开辟失败,就会返回一个NULL。( 由于返回值的类型是 void ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候通过强制转换指定。)*
//malloc的实参可以是常量或者常量表达式,也可以是变量,切记要进行强制类型转换
//实例,开辟10个整型的空间大小
int* p1 = NULL;
p1 = (int*)malloc(10 * sizeof(int));
//若开辟失败,则通过函数 perror() 报错,否则就可以使用了
if (p1 == NULL)
{
perror("malloc-error:");
}
else
{
int i = 0;
for (i = 0; i < 10; i++)
{
*(p1 + i) = i;
printf("%d ", p1[i]);
}
}
2、calloc ( )
void *calloc( size_t num, size_t size );
该函数表示为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
与函数 malloc 唯一的区别是会在返回地址之前把申请的空间的每个字节初始化为全0。因此若对申请的内存空间的内容要求初始化,就可以很方便的使用calloc函数。
//与malloc很相似,只是会把开辟的内存中每个字节都置零
int* p2 = NULL;
p2 = (int*)calloc(10, sizeof(int));
//若开辟失败,则通过函数 perror() 报错,否则就可以使用了
if (p2 == NULL)
{
perror("calloc-error:");
}
else
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", p2[i]);
}
}
3、realloc ( )
realloc函数的出现让动态内存的管理更加灵活,它可以做到对动态开辟内存大小的调整。
void *realloc( void *memblock, size_t size );
该函数表示将原来 memblock 指向的那块内存区域调整为 size 大小,返回调整之后的内存起始位置。调整失败返回指针 NULL ,此时,原来的指针仍然有效;调整成功分两种情况:若是调整后开辟的内存减少,realloc仅仅是改变指针;若是调整后开辟的内存增加, realloc 在调整内存空间时就存在两种情况:
- 原有空间之后有足够大的空间
就直接原有内存之后直接追加空间,原来空间的数据不发生变化,返回原来的地址。 - 原有空间之后没有足够大的空间
这时在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间,并将原来的数据块释放掉。
由于第二种情况的存在,在对调整后的空间命名的时候一定不要与原来的内存地址指针重名。若是重名之后开辟成功还好;若是开辟失败,函数返回值为NULL,就会赋给调整后的值,这就导致调整前后的地址都成了NULL,造成数据丢失以及内存泄漏!!!
//把上面malloc开辟的10个int大小的内存空间调整为20个int大小的内存空间
//这里的p3不要与上面的p1重名,否则可能导致内存泄露
int* p3 = NULL;
p3 = (int*)realloc(p1, 20 * sizeof(int));
//若开辟失败,则通过函数 perror() 报错,否则就可以使用了
if (p3 == NULL)
{
perror("realloc-error:");
}
else
{
int i = 0;
for (i = 0; i < 20; i++)
{
*(p3 + i) = i;
printf("%d ", p3[i]);
}
}
4、free ( )
既然开辟了这么多空间,就需要释放,否则就会造成内存泄露问题(即使进程结束,该段内存也会被占用。),这时就会用到这个函数。
void free( void *memblock );
这个函数表示将动态开辟的内存通过其指针释放掉。若 memblock 指向的空间不是动态开辟的,那free函数的行为是未定义的;若 memblock 是NULL指针,则函数什么事都不做。
另外,在释放内存的时候还要手动将地址置为空。因为在该函数内部无法实现这一步(传入的是地址,而不是指向该地址的指针。也就是说这个函数是传值调用而不是传址调用->关于函数调用方式,详情点击)
//释放掉上面开辟的p1
free(p1);
p1 = NULL;
5、实例
接下来,把所有用到的动态开辟函数整合起来,和上面的每部分实例还有有点区别,不过都是可以的。
#define CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
//完整动态开辟举例
int main()
{
//malloc使用
//malloc的实参可以是常量或者常量表达式,切记要进行强制类型转换
int* p1 = (int*)malloc(10 * sizeof(int));
if (p1 == NULL)
{
perror("malloc-error:");
}
else
{
printf("malloc's use:");
int i = 0;
for (i = 0; i < 10; i++)
{
*(p1 + i) = i;
printf("%d ", p1[i]);
}
printf("\n");
}
//calloc使用
//初始值会给每个字节赋0
int* p2 = (int*)calloc(10, sizeof(int));
if (p2 == NULL)
{
perror("calloc-error:");
}
else
{
printf("calloc's use:");
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", p2[i]);
}
printf("\n");
}
//realloc使用
//这里的p1和p3不要重名,否则可能导致内存泄露
int* p3 = NULL;
p3 = (int*)realloc(p1, 20 * sizeof(int));
if (p3 == NULL)
{
perror("calloc-error:");
}
//也可以调整之后再赋回给原来的p1,这时不要忘了把p3置空
//这样释放的时候也只要释放p1即可
else
{
p1 = p3;
p3 = NULL;
printf("realloc's use:");
int i = 0;
for (i = 0; i < 20; i++)
{
*(p1 + i) = i;
printf("%d ", p1[i]);
}
printf("\n");
}
//free使用
free(p1);
p1 = NULL;
free(p2);
p2 = NULL;
return 0;
}
运行结果:
二、常见错误
-
开辟空间后,没有对指针的有效性进行判断(开辟失败返回NULL指针),导致对空指针进行解引用。
-
开辟好后越界访问
-
使用 free() 函数释放非动态开辟空间,会直接终止进程报错,当然NULL指针除外(若释放NULL,就是什么也不释放,自然什么也不用做)。
-
有的时候开辟好之后,使用了,指针指向的已经不是开辟空间的起始地址,也就是说只释放了一部分,这样也是不对的。例如:
void test() { int *p = (int *)malloc(100); p++; free(p);//p不再指向动态内存的起始位置 }
三、柔性数组
结构体中的最后一个元素允许是未知大小的数组,这个数组就叫做该结构体的柔性数组成员。
结构体中的柔性数组成员前面必须至少一个其他成员,sizeof() 计算的结构体大小不包括柔性数组的内存。包含柔性数组成员的结构体分配内存时用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构体的大小。
-
柔性数组的形式有两种:
typedef struct s { int i; int a[0];//柔性数组成员 }a; typedef struct s { int i; int a[];//柔性数组成员 }a;
因为柔性数组用到了动态开辟,所以是需要释放的,释放时直接释放结构体即可。
-
下面看一下柔性数组的具体使用:
struct S { int i; char s[]; }; int main() { struct S* p = (struct S*)malloc(sizeof(p) + 6); if (p == NULL) { perror("malloc_error:"); return 0; } else { p->i = 6; int k = 0; for (k = 0; k < p->i; k++) { p->s[k] = 'a' + k; } for (k = 0; k < p->i; k++) { printf("%d:%c ", k + 1, p->s[k]); } free(p); p = NULL; } return 0; }
-
其实不用柔性数组也可以实现再结构体里做动态开辟,下面看一下代码:
struct S { int i; char* s; }; int main() { struct S* p = (struct S*)malloc(sizeof(struct S)); if (p == NULL) { perror("malloc_p:"); return 0; } else { p->i = 6; p->s = (char*)malloc(sizeof(char) * p->i); if (p->s == NULL) { perror("malloc_p->s:"); return 0; } else { int k = 0; for (k = 0; k < p->i; k++) { p->s[k] = 'a' + k; } for (k = 0; k < p->i; k++) { printf("%d:%c ", k + 1, p->s[k]); } free(p->s); p->s = NULL; free(p); p = NULL; } } return 0; }
下面通过图解来看一下两种不同实现方法:
上述两种方法都都使用到 malloc() 可以完成同样的功能,但是使用了柔性数组有两个好处:
1、方便内存释放:把结构体的内存以及其成员要的内存一次性分配好并返回一个结构体指针,一次就可以把所有的内存释放掉。
2、利于访问速度:连续的内存有益于提高访问速度,也有益于减少内存碎片。
动态开辟为内存的开辟大大提升了灵活性,也节省了内存,在实际编程也用到很多。好了,这部分内容就这些,欢迎指正!