本文主要介绍了C语言中常用的内存函数和数组的动态内存分配,并稍微提了一嘴柔性数组。动态内存分配是C语言中十分重要的一环,其中对二维数组的动态内存分配是个难点,需要多思考。
目录
- 1. 为什么存在动态内存分配
- 2. 动态内存函数
- malloc
- free
- calloc
- realloc
- 3. 动态内存分配数组
- 3.1 一维数组的动态分配和使用
- 3.2 二维数组的动态内存分配和使用
- 4. 柔性数组
1. 为什么存在动态内存分配
要开辟一个数组,我们可以很简单做到:
int arr[10];char str[20];struct S s[10];
我们可以很容易地声明整型数组、字符数组、结构体数组…
但是,这样声明出来的数组有很明显的缺点:
- 数组大小固定
- 当数组固定大小很大时,可能会栈溢出(Stack overflow)
但实际上,很多情况下我们并不知道自己要开辟多大的空间,这些很多时候是只有在程序跑起来才知道的,所以这就引出了动态内存分配。
动态内存分配出来的空间是在堆区开辟的,这是它与通过定义数组分配出来的空间最本质的区别。
2. 动态内存函数
动态内存分配是需要调用动态内存函数实现的,下面介绍四种内存函数,点击超链接即可转到官方解释。
malloc 是我们见得最多的动态内存函数。
它会向内存申请一块连续可用的空间,空间大小是 size 个字节,并返回指向这块空间的指针。
它返回的是个指针,所以在使用它时要用指针接收:
char* ch = malloc(sizeof(char) * size1);int* arr = malloc(sizeof(int) * size2);struct S* s = malloc(sizeof(stuctt S) * size3);
但这样并不严谨,因为 malloc 返回类型是空指针,所以在接受它的返回值时最好再对它进行强制类型转换:
char* ch = (char*)malloc(sizeof(char) * size1);int* arr = (int*)malloc(sizeof(int) * size2);struct S* s = (struct S*)malloc(sizeof(stuctt S) * size3);
但是,当动态内存开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查:
char* ch = (char*)malloc(sizeof(char) * size);if(ch == NULL){perror("fun_name");return;}
free
有动态内存分配的地方就一定会有 free 函数。
它的参数是一个指针,指针指向的空间是动态内存分配出来的一块空间。它会释放掉这块空间,将这块空间还给操作系统。
它释放的空间一定是要动态内存分配出来的,这块空间一定是在堆区的,否则会引发异常:
用 free 去释放栈区的空间,这是万万不能够的!!
此外,如果传过去的指针是个 NULL 空指针,free 就会纯纯摆烂,啥也不干。
释放空间之后,原指针就没有任何意义,但 free 不会自动给它置成空指针,此时他就成为一个野指针,所以我们要即及时将其置成空指针:
int* ptr = (int*)malloc(sizeof(int) * size);if(ptr == NULL){perror("fun_name");return;}...free(ptr);ptr = NULL;
它的返回类型和 malloc 一样,参数部分则有不同:num :元素个数
size :每个元素的大小(byte)
num :元素个数
size :每个元素的大小(byte)
所以它的作用就是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为 0。
所以它的使用和 malloc 基本一致:
int *p = (int*)calloc(10, sizeof(int));if(NULL == p){perror("fun_name");return;}...free(p);p = NULL;
相比 malloc ,它只是把分配的空间全部初始化为 0:
虽然我们能通过 malloc 或 calloc 函数动态内存分配一块空间,但这块空间分配完成后大小也是固定的,如果空间满了需要扩容或空间多了需要缩减,这时 realloc 函数就登场了。
realloc 的作用是对已经动态分配的一块空间再次分配。
它有两个参数:
- memblock:要调整的内存地址,这块内存是动态内存分配得到的
- size:以字节为单位的新大小
它会返回调整之后的内存起始位置。
关于调整之后的内存起始位置会出现以下两种情况:
- 与原来内存的起始位置相同
- 与原来内存的起始位置不同
- 当要缩小原有的内存时,原来的内存空间已经足够,此时它的返回值就是原来内存的起始位置。
- 当要扩大原有的内存时,又有两种情况:
原有空间之后有足够大的内存时,直接在原内存的基础上再开辟后边几个连续的空间,此时它的返回值是原来内存的起始位置;
原有空间之后没有足够大的内存进行扩容时,此时会在堆空间上另找一个合适大小的连续空间来使用,这样函数返回的是一个新的内存地址,与原来内存的起始位置不同。这时会拷贝原来空间的内容到新的空间的相应位置,原空间就被释放掉了。
所以,在使用 realloc 函数时就要注意用一个临时指针接收,当内存调整成功后再将临时指针赋给原指针:
int main(){int* ptr = (int*)malloc(100);if (ptr == NULL){preeor("main");return;}...//定义一个临时指针接收新地址int* tmp = NULL;tmp = (int*)realloc(ptr, 1000);if (tmp != NULL){ptr = tmp;}...free(ptr);ptr = NULL;return 0;}
3. 动态内存分配数组
对于动态内存分配我们主要用于动态开辟一维和二维数组。
下面就看看开辟数组的方法和正确使用这块空间并释放。
3.1 一维数组的动态分配和使用
动态开辟一维数组还是比较简单的:
int main(){int size = 10;//给整形数组动态分配int* arr = (int*)malloc(sizeof(int) * size);if (arr == NULL){perror("main");return;}//给数组赋值for (int i = 0; i < size; i++)arr[i] = i;free(arr);arr = NULL;return 0;}
在上面这段代码中,对动态内存分配的一块空间我们直接用给数组赋值的方式对其赋值,其可行性是源于数组的指针式访问和下标式访问:
*(p + i) <==> p[i]*(*(arr + i ) + j) <==> arr[i][j]
不过一定要记得使用完这块空间要及时释放。
3.2 二维数组的动态内存分配和使用
对于二维数组的动态开辟,其方法是不唯一的,下面给出三种方法。
第一种
由于二维数组在内存中是连续存放的,所以我们可以开辟一大块空间将二维数组当成一维数组存放起来。这样数组元素的存储在内存中是连续的。
第二种
动态开辟一个二维数组,可以开辟一个指针数组,每个元素存放的是一个指针,每个指针都指向一个数组,再分别对每个一维数组分配空间。
但是,这种方式开辟出来的数组不是连续存储的
:
第三种
动态开辟一个二维数组,还可以借用数组指针,然后通过对数组指针访问到数组元素,此时开辟出来的数组是连续存储的
。其实这种方法和第一种方法有同工异曲之妙,只是这种方式访问数组元素更简单:
下面是每种方法的代码实现:
//方法1#define ROW 4#define COL 4int main(){//动态开辟int* arr = (int*)malloc(sizeof(int) * ROW * COL);if (arr == NULL){perror("main");return;}//访问数组并赋值int count = 1;for (int i = 0; i < ROW; i++)for (int j = 0; j < COL; j++)arr[i * COL + j] = count++;//释放内存free(arr);arr = NULL;return 0;}
//方法2#define ROW 4#define COL 4int main(){//用二级指针动态申请二维数组int** arr = (int**)malloc(sizeof(int*) * ROW);//这样只开辟了ROW个存放整型指针的空间if (arr == NULL){perror("main");return;}//对每个一维数组开辟空间for (int i = 0; i < ROW; i++){arr[i] = (int*)malloc(sizeof(int) * COL);//给每个一级指针arr[i]分配COL个整型空间if (arr[i] == NULL){perror("main");return;}}//访问数组元素并赋值int count = 1;for (int i = 0; i < ROW; i++)for (int j = 0; j < COL; j++)arr[i][j] = count++;//释放二维数组的每个一维数组for (int i = 0; i < ROW; i++){free(arr[i]);arr[i] = NULL;}//释放二级指针申请的数组free(arr);arr = NULL;return 0;}
//方法3#define ROW 4#define COL 4int main(){//用数组指针形式申请一个ROW行COL列的二维数组int(*arr)[COL] = (int(*)[COL])malloc(sizeof(int) * ROW * COL);if (arr == NULL){perror("main");return;}//访问数组成员并对其赋值int count = 1;for (int i = 0; i < ROW; i++)for (int j = 0; j < COL; j++)arr[i][j] = count++;//释放内存free(arr);arr = NULL;return 0;}
无论是哪种方式,刚用起来肯定生疏,但用多了就熟悉了。
而且,一定要记得释放内存,特别是第二种方式!
4. 柔性数组
柔性数组(flexible array)是一种不完整类型,而 C99 的标准,就支持了这种类型。
标准规定:结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员。
typedef struct S{ int i; int a[0];//柔性数组成员}s;
对于有些编译器可能报错,a[0] 改成 a[ ] 即可。
柔性数组在结构中声明,柔性数组成员前面必须有至少一个其他成员。
sizeof 返回的这种结构大小不包括柔性数组的内存。
包含柔性数组成员的结构用 malloc() 函数进行内存的动态分配,并且
分配的内存应该大于结构的大小
,以适应柔性数组的预期大小。
//对结构体进行动态内存分配s* p = (s*)malloc(sizeof(s) + 100 * sizeof(int));if (p == NULL){perror("main");return;}...free(p);p = NULL;