本文主要介绍了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];

我们可以很容易地声明整型数组、字符数组、结构体数组…

但是,这样声明出来的数组有很明显的缺点:

  1. 数组大小固定
  2. 当数组固定大小很大时,可能会栈溢出(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 的元素开辟一块空间,并且把空间的每个字节初始化为 0

所以它的使用和 malloc 基本一致:

int *p = (int*)calloc(10, sizeof(int));if(NULL == p){perror("fun_name");return;}...free(p);p = NULL;

相比 malloc ,它只是把分配的空间全部初始化为 0


虽然我们能通过 malloccalloc 函数动态内存分配一块空间,但这块空间分配完成后大小也是固定的,如果空间满了需要扩容或空间多了需要缩减,这时 realloc 函数就登场了。

realloc 的作用是对已经动态分配的一块空间再次分配。

它有两个参数:

  1. memblock:要调整的内存地址,这块内存是动态内存分配得到的
  2. size:以字节为单位的新大小

它会返回调整之后的内存起始位置。

关于调整之后的内存起始位置会出现以下两种情况:

  1. 与原来内存的起始位置相同
  2. 与原来内存的起始位置不同
  • 当要缩小原有的内存时,原来的内存空间已经足够,此时它的返回值就是原来内存的起始位置。
  • 当要扩大原有的内存时,又有两种情况:
  1. 原有空间之后有足够大的内存时,直接在原内存的基础上再开辟后边几个连续的空间,此时它的返回值是原来内存的起始位置;
  2. 原有空间之后没有足够大的内存进行扩容时,此时会在堆空间上另找一个合适大小的连续空间来使用,这样函数返回的是一个新的内存地址,与原来内存的起始位置不同。这时会拷贝原来空间的内容到新的空间的相应位置,原空间就被释放掉了。

所以,在使用 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. 第一种
    由于二维数组在内存中是连续存放的,所以我们可以开辟一大块空间将二维数组当成一维数组存放起来。这样数组元素的存储在内存中是连续的。

  2. 第二种
    动态开辟一个二维数组,可以开辟一个指针数组,每个元素存放的是一个指针,每个指针都指向一个数组,再分别对每个一维数组分配空间。
    但是,这种方式开辟出来的数组不是连续存储的

  3. 第三种
    动态开辟一个二维数组,还可以借用数组指针,然后通过对数组指针访问到数组元素,此时开辟出来的数组是连续存储的。其实这种方法和第一种方法有同工异曲之妙,只是这种方式访问数组元素更简单:

下面是每种方法的代码实现:

//方法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[ ] 即可。

  1. 柔性数组在结构中声明,柔性数组成员前面必须有至少一个其他成员。

  2. sizeof 返回的这种结构大小不包括柔性数组的内存。

  3. 包含柔性数组成员的结构用 malloc() 函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

//对结构体进行动态内存分配s* p = (s*)malloc(sizeof(s) + 100 * sizeof(int));if (p == NULL){perror("main");return;}...free(p);p = NULL;