c语言 进阶 动态内存管理

动态内存管理

    • [1. 为什么存在动态内存分配](#1. 为什么存在动态内存分配)
    • [2. 动态内存函数的介绍​](#2. 动态内存函数的介绍)
      • [2.1 malloc 和 free](#2.1 malloc 和 free)
        • [malloc 函数](#malloc 函数)
        • [free 函数](#free 函数)
      • 2.2内存泄漏
      • [2.3 calloc](#2.3 calloc)
      • [2.4 realloc](#2.4 realloc)
    • [3. 常见的动态内存错误](#3. 常见的动态内存错误)
      • [3.1 对NULL指针的解引用操作](#3.1 对NULL指针的解引用操作)
      • [3.2 对动态开辟空间的越界访问](#3.2 对动态开辟空间的越界访问)
      • [3.3 对非动态开辟内存使用free释放](#3.3 对非动态开辟内存使用free释放)
        • [3.4 使用free释放一块动态开辟内存的一部分](#3.4 使用free释放一块动态开辟内存的一部分)
      • [3.5 对同一块动态内存多次释放](#3.5 对同一块动态内存多次释放)
      • [3.6 动态开辟内存忘记释放(内存泄漏)](#3.6 动态开辟内存忘记释放(内存泄漏))
    • [4. 几个经典的笔试题](#4. 几个经典的笔试题)
        • [4.1 题分析](#4.1 题分析)
        • [4.2 题分析](#4.2 题分析)
        • [4.3 题分析](#4.3 题分析)
        • [4.4 题分析](#4.4 题分析)
    • [5. 柔性数组(Flexible Array)](#5. 柔性数组(Flexible Array))
        • [5.1 柔性数组的特点](#5.1 柔性数组的特点)
        • [5.2 柔性数组的使用](#5.2 柔性数组的使用)
        • [5.3 柔性数组的优势](#5.3 柔性数组的优势)

1. 为什么存在动态内存分配

  • 已掌握的内存开辟方式及局限:
    • 栈上开辟的空间,如int val = 20;是在栈上分配4个字节,char arr[10] = {0};是在栈上分配10个字节的连续空间。
    • 这些方式有明显局限:
      1. 空间大小固定,比如char arr[10]只能开辟10个字节,无法根据程序运行时的需求改变大小,而且栈空间通常有限,不能开辟过大的空间。
      2. 数组在声明时必须指定长度,像int n; scanf("%d", &n); char arr[n];这种在C99之前是不允许的,因为数组的长度需要在编译时确定,而程序运行时才能知道的长度无法通过这种方式开辟空间。
    • 实际开发中,很多场景下空间大小只有在程序运行时才能确定,例如根据用户输入的数字来决定需要存储多少个数据,这时候静态开辟空间的方式就无法满足需求,动态内存分配应运而生。

2. 动态内存函数的介绍​

2.1 malloc 和 free

malloc 函数
  • malloc函数
    • 函数原型void* malloc(size_t size);的作用是向内存的堆区申请一块连续可用的空间。
      • 如果开辟成功,则返回一个指向开辟好空间的指针。
      • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查
      • 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
      • size_t size
        表示要分配的内存块的大小(以字节为单位)。
      • 如果参数size0malloc的行为是标准是未定义的,取决于编译器
    • 示例1(正常开辟):
c 复制代码
// 申请可以存储5个int类型数据的空间,int占4字节,所以总共申请5*4=20字节
int* p = (int*)malloc(5 * sizeof(int));
// 必须检查开辟是否成功,因为当内存不足时,malloc会返回NULL
if (p == NULL) {
    // 打印错误信息,perror会在字符串后加上具体的错误原因
    perror("malloc failed");
    return 1; // 开辟失败,退出程序
}
// 成功开辟后使用空间,给每个元素赋值
for (int i = 0; i < 5; i++) {
    p[i] = i * 10;
}
  • 示例 2(开辟失败):
c 复制代码
// 申请1000000000个int类型的空间,可能因内存不足导致失败
int* p = (int*)malloc(1000000000 * sizeof(int));
if (p == NULL) {
    perror("malloc failed"); // 可能输出"malloc failed: Not enough space"
    return 1;
}
  • 特性总结:
    • 开辟成功返回指向该空间的指针,由于返回类型是void*,所以需要根据实际存储的数据类型进行强制类型转换,比如存储int类型就转为int*
    • 开辟失败返回 NULL 指针,所以使用前必须检查 返回值是否为 NULL
    • size0 时,C 语言标准没有定义其行为,不同的编译器可能有不同的处理方式,有的可能返回 NULL,有的可能返回一块很小的空间,实际开发中应避免这种情况。
free 函数

函数原型void free(void* ptr);专门用于释放动态开辟的内存,将内存归还给系统。

  • 示例:
c 复制代码
int* p = (int*)malloc(5 * sizeof(int));
if (p == NULL) {
    perror("malloc failed");
    return 1;
}
// 使用空间...
free(p); // 释放p指向的动态内存,此时这块内存归还给系统,不能再使用
p = NULL; // 释放后将指针置为NULL,避免成为野指针,野指针指向的内存已无效,使用会导致不可预期的错误
  • 特性总结:​

    • 只能释放动态开辟的内存,比如int a = 10; int* p = &a; free(p);这种释放栈上空间的行为是未定义的,可能导致程序崩溃。
    • ptr NULL 指针时,free 函数什么也不做,所以释放后将指针置为NULL是安全的。
  • malloc和free都声明在 stdlib.h 头文件中

2.2内存泄漏

定义: 动态开辟的内存没有通过 free 释放,并且指向该内存的指针也丢失了,导致系统无法回收这块内存,这就是内存泄漏。

  • 示例 1(忘记释放):
c 复制代码
void test() {
    int* p = (int*)malloc(100);
    // 使用p后没有调用free(p),函数结束后p被销毁,再也无法找到这块内存,导致内存泄漏
}
int main() {
    test();
    // 程序运行期间,test函数中申请的100字节内存一直未被释放
    return 0;
}
  • 示例 2(指针被修改导致无法释放):
c 复制代码
 int* p = (int*)malloc(100);
p++; // 指针指向了动态开辟空间的第二个字节,不再指向起始位置
free(p); // 错误,无法释放,因为free需要指向动态开辟空间的起始地址,同时原起始地址丢失,导致内存泄漏
  • 危害:内存泄漏不会导致程序立即崩溃,但如果程序长期运行(如服务器程序、嵌入式程序),随着时间的推移,泄漏的内存会越来越多,最终会耗尽系统内存,导致程序运行缓慢甚至崩溃。
  • 预防
    • 动态内存使用完毕后,及时调用 free 函数释放,并将指针置为 NULL
    • 在函数中申请的动态内存,要确保在函数返回前释放,或者将指针传递出去由外部释放。
    • 避免在释放内存前修改指针的指向,如果需要移动指针操作,先保存起始地址。

2.3 calloc

  • 函数原型void* calloc(size_t num, size_t size);,其功能是为num个大小为size的元素开辟一块空间,并且会将这块空间的每个字节都初始化为0。与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
  • 示例(与malloc对比):
c 复制代码
// 使用calloc申请3个int类型的空间
int* p1 = (int*)calloc(3, sizeof(int));
// 使用malloc申请3个int类型的空间
int* p2 = (int*)malloc(3 * sizeof(int));
if (p1 == NULL || p2 == NULL) {
    perror("malloc/calloc failed");
    return 1;
}
// 打印空间中的值
printf("calloc初始化后的值:");
for (int i = 0; i < 3; i++) {
    printf("%d ", p1[i]); // 输出0 0 0,因为calloc会初始化
}
printf("\nmalloc初始化后的值:");
for (int i = 0; i < 3; i++) {
    printf("%d ", p2[i]); // 输出随机值,因为malloc不会初始化
}
// 释放空间
free(p1);
p1 = NULL;
free(p2);
p2 = NULL;

输出结果

  • malloc 的区别:
    • 参数不同:calloc 需要两个参数,分别是元素的个数和每个元素的大小;malloc 只需要一个参数,即总共需要开辟的字节数。
    • 初始化不同:calloc 会将申请的空间每个字节都初始化为 0;malloc 不会初始化,空间中的值是随机的(取决于内存中之前存储的数据)。
  • 适用场景:当需要申请一块初始化为 0 的动态内存时,使用 calloc 更方便,避免了使用 malloc 后再调用 memset 进行初始化的步骤。

2.4 realloc

  • 函数原型void* realloc(void* ptr, size_t size);,用于调整已经动态开辟的内存空间的大小,ptr是指向原来动态开辟空间的指针,size是调整后的新大小(以字节为单位)。
  • 调整内存的两种情况:
    1. 原有空间之后有足够的空闲空间:这种情况下,realloc会直接在原有空间的后面追加空间,不会移动原有数据,返回原来的指针。
    2. 原有空间之后没有足够的空闲空间:这种情况下,realloc会在堆区重新找一块大小合适的空间,将原来空间中的数据复制到新空间,然后释放原来的空间,返回新空间的指针。
  • 示例(正确使用):
c 复制代码
// 先申请4个int的空间
int* p = (int*)malloc(4 * sizeof(int));
if (p == NULL) {
    perror("malloc failed");
    return 1;
}
// 给空间赋值
for (int i = 0; i < 4; i++) {
    p[i] = i;
}
// 现在需要将空间调整为8个int,用新指针接收realloc的返回值
int* new_p = (int*)realloc(p, 8 * sizeof(int));
if (new_p == NULL) {
    perror("realloc failed");
    // 如果realloc失败,原来的p仍然有效,需要释放,避免内存泄漏
    free(p);
    p = NULL;
    return 1;
}
// 调整成功,更新指针
p = new_p;
// 使用调整后的空间
for (int i = 4; i < 8; i++) {
    p[i] = i;
}
// 释放空间
free(p);
p = NULL;

注意 :最好别用要动态修改的指针来接受返回值因为若realloc失败返回NULL,会导致指针变为NULL,原来的100字节内存无法释放,造成内存泄漏

  • 错误示例(用原指针接收返回值):
c 复制代码
 int* p = (int*)malloc(100);
// 错误,若realloc失败返回NULL,会导致p变为NULL,原来的100字节内存无法释放,造成内存泄漏
p = (int*)realloc(p, 200);

注意事项:​

  • realloc 的第一个参数为 NULL 时,其功能相当于 malloc,即realloc(NULL, size)等价于malloc(size)
  • 调整后的空间大小可以比原来小,此时会截断原有数据,只保留前面部分数据。
  • 使用 realloc 后,原来的指针可能会失效(当需要移动数据时),所以必须使用 realloc 的返回值来访问调整后的空间。

3. 常见的动态内存错误

3.1 对NULL指针的解引用操作

  • 错误原因:当malloccallocrealloc函数开辟内存失败时,会返回NULL指针,而NULL指针不指向任何有效的内存空间,对其进行解引用操作(如赋值、取值)会导致程序崩溃。
  • 示例(错误):
c 复制代码
int* p = (int*)malloc(1000000000); // 申请过大空间,可能失败返回NULL
*p = 10; // 对NULL指针解引用,程序会崩溃

避免方法: 在使用动态内存函数返回的指针之前,必须检查该指针是否为 NULL。

  • 示例(正确):
c 复制代码
 int* p = (int*)malloc(1000000000);
if (p == NULL) {
    perror("malloc failed"); // 打印错误信息
    return 1; // 不继续使用指针,避免解引用NULL
}
*p = 10; // 指针非NULL,可安全使用

3.2 对动态开辟空间的越界访问

  • 错误原因:访问动态开辟的内存空间时,超出了申请的范围,就像数组越界访问一样,会导致不可预期的错误,可能修改其他内存的数据,也可能导致程序崩溃。
  • 示例:
c 复制代码
// 申请3个int的空间,共3*4=12字节,有效访问范围是p[0]到p[2]
int* p = (int*)malloc(3 * sizeof(int));
if (p == NULL) {
    perror("malloc failed");
    return 1;
}
// 循环访问到了p[3]和p[4],超出了申请的空间范围,属于越界访问
for (int i = 0; i < 5; i++) {
    p[i] = i; // i=3、4时越界
}
free(p);
p = NULL;
  • 危害:越界访问可能会修改其他动态开辟的内存数据,或者破坏堆区的管理信息,导致后续的内存操作(如 free)出现错误。
  • 避免方法:访问动态开辟的空间时,严格控制访问范围,确保不超过申请的大小。比如申请了 n 个 int 类型的空间,访问索引就只能在 0 到 n-1 之间。

3.3 对非动态开辟内存使用free释放

  • 错误原因:free函数的作用是释放动态开辟的内存(堆区的内存),而栈上的局部变量、全局变量等非动态开辟的内存,其生命周期由系统自动管理,不需要也不能用free释放,对这些内存使用free会导致程序行为未定义,通常会引发程序崩溃。
  • 示例(错误):
c 复制代码
int a = 10; // 栈上的局部变量
int* p = &a;
free(p); // 错误,释放非动态开辟的内存,程序可能崩溃
p = NULL;

避免方法:明确区分动态开辟的内存和非动态开辟的内存,只对通过malloccallocrealloc函数申请的内存使用 free 释放。

3.4 使用free释放一块动态开辟内存的一部分
  • 错误原因:free函数释放动态内存时,要求指针必须指向动态开辟内存的起始地址,因为内存管理系统需要通过起始地址来回收整个内存块。如果指针指向的是动态开辟内存的中间位置,free无法正确回收内存,会破坏堆区的内存管理结构,导致程序出错。
  • 示例(错误):
c 复制代码
int* p = (int*)malloc(4 * sizeof(int)); // p指向动态开辟内存的起始地址
if (p == NULL) {
    perror("malloc failed");
    return 1;
}
p++; // p现在指向动态开辟内存的第二个int的位置,不再是起始地址
free(p); // 错误,释放的是内存的一部分,程序可能崩溃
  • 避免方法:在释放动态内存之前,确保指针指向动态开辟内存的起始地址。如果在操作过程中移动了指针,需要先保存起始地址。
  • 示例(正确):
c 复制代码
	 int* p = (int*)malloc(4 * sizeof(int));
if (p == NULL) {
    perror("malloc failed");
    return 1;
}
int* q = p; // 保存起始地址
p++; // 移动指针进行操作
// ... 使用p进行操作
free(q); // 使用保存的起始地址释放内存
q = NULL;
p = NULL;

3.5 对同一块动态内存多次释放

  • 错误原因:同一块动态内存被free多次,会导致堆区内存管理结构被破坏,因为第一次释放后,该内存已经归还给系统,再次释放时,系统无法识别该内存块的状态,从而引发程序崩溃。
  • 示例(错误):
c 复制代码
int* p = (int*)malloc(100);
free(p);
free(p); // 错误,对同一块内存多次释放,程序可能崩溃
  • 避免方法:释放内存后,立即将指针置为 NULL,因为 free 函数对 NULL 指针什么也不做,这样即使不小心再次释放,也不会出现错误。
  • 示例(正确):
c 复制代码
 int* p = (int*)malloc(100);
free(p);
p = NULL; // 释放后将指针置为NULL
free(p); // 安全,free对NULL指针无操作

3.6 动态开辟内存忘记释放(内存泄漏)

  • 错误原因:动态开辟的内存需要手动通过free释放,如果使用完毕后没有释放,并且指向该内存的指针也丢失了(如指针超出作用域被销毁),系统就无法回收这块内存,导致内存泄漏。
  • 示例1(函数中忘记释放):
c 复制代码
void test() {
    int* p = (int*)malloc(100); // 在函数内部申请动态内存
    // 使用p进行操作,但没有释放
} // 函数结束,p被销毁,无法再释放申请的100字节内存,造成内存泄漏
int main() {
    test();
    // 程序运行期间,test函数申请的内存一直未被释放
    return 0;
}
  • 示例 2(指针被覆盖导致无法释放):
  • 危害:对于短期运行的程序,内存泄漏可能不会有明显影响,因为程序结束后操作系统会回收所有内存;但对于长期运行的程序(如服务器程序、后台服务),内存泄漏会导致可用内存越来越少,最终程序会因内存不足而崩溃。
  • 避免方法:
    • 动态内存使用完毕后,及时调用 free 释放,并将指针置为 NULL。
    • 在函数中申请的动态内存,如果需要在函数外部使用,要将指针返回给外部,由外部负责释放;如果不需要在外部使用,一定要在函数返回前释放。
    • 避免覆盖指向动态内存的指针,如果需要重新赋值,先释放原来的内存。

4. 几个经典的笔试题

4.1 题分析
  • 代码实现
c 复制代码
void GetMemory(char* p) {
    p = (char*)malloc(100);  // 为形参p分配内存
}
void Test(void) {
    char* str = NULL;
    GetMemory(str);          // 传递str的值(NULL)
    strcpy(str, "hello world");  // 操作NULL指针
    printf(str);
}
  • 运行结果:程序崩溃。
  • 原因详解:
    • 值传递的局限性:GetMemory函数的参数pstr的副本(值传递),p在函数内被赋值为malloc 返回的地址,但这不会改变str的值(str仍为 NULL)。
    • NULL 指针解引用:strcpy(str, ...)试图向NULL 指针指向的内存写入数据,这是未定义行为,会导致程序崩溃
    • 内存泄漏隐患:GetMemory malloc 分配的内存地址仅存于p,函数结束后p被销毁,该内存无法释放,造成内存泄漏。
4.2 题分析
  • 代码实现
c 复制代码
char* GetMemory(void) {
    char p[] = "hello world";  // 局部数组,存于栈区
    return p;  // 返回局部数组的地址
}
void Test(void) {
    char* str = NULL;
    str = GetMemory();  // 接收已销毁的局部数组地址
    printf(str);  // 访问无效内存
}
  • 运行结果:打印随机值或乱码(行为未定义)。
  • 原因详解:
    • 局部变量的生命周期:数组pGetMemory函数的局部变量,存储在栈区,函数执行结束后,栈区内存被释放,p的地址变为无效(野指针)。
    • 野指针访问:str接收的是无效地址,此时访问该地址的内存(printf(str)),读取到的是栈区残留的随机数据,结果不可预期。
    • 关键结论:不要返回局部变量的地址,其指向的内存会随函数结束而失效。
4.3 题分析
  • 代码实现
c 复制代码
void GetMemory(char**p, int num) {
    *p = (char*)malloc(num);  // 为二级指针指向的指针分配内存
}
void Test(void) {
    char* str = NULL;
    GetMemory(&str, 100);  // 传递str的地址(二级指针)
    strcpy(str, "hello");  // 向分配的内存写入数据
    printf(str);  // 打印"hello"
}
  • 运行结果:正常打印 "hello",但存在内存泄漏。
  • 原因详解:
    • 二级指针的作用:GetMemory的参数p&str(二级指针),*p就是str本身,因此*p = malloc(...)能正确为str分配内存(str指向堆区的 100 字节)。
    • 内存泄漏问题:str指向的堆区内存未通过 free 释放,程序结束前该内存一直被占用,造成内存泄漏(尤其在多次调用时)。
    • 改进方案:使用后添加free(str); str = NULL;释放内存。
4.4 题分析
  • 代码实现
c 复制代码
void Test(void) {
    char* str = (char*)malloc(100);  // 分配堆区内存
    strcpy(str, "hello");
    free(str);  // 释放str指向的内存
    if (str != NULL) {  // str仍指向已释放的内存(野指针)
        strcpy(str, "world");  // 向已释放的内存写入数据
        printf(str);  // 访问无效内存
    }
}
  • 运行结果:可能打印 "world",也可能崩溃或打印乱码(行为未定义)。
  • 原因详解:
    • free 后的指针状态:free(str)释放了内存,但str的值并未改变(仍指向原地址),此时str成为野指针。
    • 访问已释放内存:strcpy(str, "world")向已归还给系统的内存写入数据,这会破坏堆区管理结构,可能导致后续内存操作出错(如再次 malloc 时崩溃)。
    • 预防措施:释放内存后应立即将指针置为 NULL,即free(str); str = NULL;,此时if (str != NULL)条件不成立,避免无效操作。

5. 柔性数组(Flexible Array)

  • 柔性数组是C99标准引入的特殊数组形式,仅能作为结构体的最后一个成员存在,其大小在结构体定义时无需指定(或指定为0),因此也被称为"可变长数组成员"。
  • 定义示例及编译器兼容性:
c 复制代码
// 方式1:数组大小指定为0,早期C99支持此形式,部分编译器(如GCC)兼容
typedef struct st_type {
    int len;          // 用于记录柔性数组的实际长度
    int data[0];      // 柔性数组成员,必须位于结构体末尾
} type_a;

// 方式2:不指定数组大小(空数组形式),是C99推荐写法,兼容更多编译器(如MSVC)
typedef struct st_type {
    int len;
    int data[];       // 柔性数组成员,同样位于结构体末尾
} type_a;
  • 核心约束:柔性数组成员前面必须至少有一个其他类型的成员(如示例中的int len),且不能是结构体的唯一成员。这是因为柔性数组本身不占用结构体的固定内存,需要通过前面的成员确定其起始偏移量。
5.1 柔性数组的特点
  1. 结构成员的位置约束
    • 柔性数组成员必须是结构体的最后一个成员,不能有其他成员跟在其后。
    • 错误示例(柔性数组后有其他成员):
c 复制代码
typedef struct wrong_st {
    int a;
    int flex[];  // 柔性数组
    int b;       // 错误:柔性数组后不能有其他成员
} wrong_type;  // 编译器会报错
  1. sizeof运算符的计算规则
    • sizeof计算包含柔性数组的结构体大小时,仅计算柔性数组前面所有成员的总大小,完全忽略柔性数组的存在。
    • 示例(基于type_a):
c 复制代码
	// type_a中仅int len一个非柔性成员,占4字节
printf("sizeof(type_a) = %zu\n", sizeof(type_a));  // 输出4,不包含data[]的大小
  • 原理:柔性数组的大小在编译期未知,无法纳入结构体的固定大小计算,其内存需在运行时动态分配。
  1. 内存分配的强制性与计算方式
    • 包含柔性数组的结构体必须通过动态内存分配函数(malloc/calloc/realloc)创建实例,不能在栈上直接定义变量(如type_a obj;是错误的,因为无法确定柔性数组的大小)。
    • 分配内存时,总大小计算公式为:结构体固定大小(sizeof(type_a)) + 柔性数组实际所需字节数。
    • 示例(为柔性数组分配 10 个int元素的空间):
c 复制代码
	// 计算总大小:4(len) + 10*4(data)= 44字节
type_a* p = (type_a*)malloc(sizeof(type_a) + 10 * sizeof(int));
if (p == NULL) {
    perror("malloc failed");
    exit(EXIT_FAILURE);
}
p->len = 10;  // 记录柔性数组的实际长度,方便后续访问
5.2 柔性数组的使用
  • 基本使用流程 :动态分配内存→初始化成员→访问柔性数组→释放内存。
    • 完整示例:
c 复制代码
#include <stdio.h>
#include <stdlib.h>

typedef struct st_type {
    int len;   // 记录柔性数组元素个数
    int data[]; // 柔性数组成员
} type_a;

int main() {
    // 1. 分配内存:结构体固定大小(4字节) + 5个int(20字节)= 24字节
    type_a* p = (type_a*)malloc(sizeof(type_a) + 5 * sizeof(int));
    if (p == NULL) {
        perror("malloc failed");
        return 1;
    }

    // 2. 初始化:设置柔性数组长度并赋值
    p->len = 5;
    for (int i = 0; i < p->len; i++) {
        p->data[i] = i * 10;  // 直接通过结构体指针访问柔性数组
    }

    // 3. 访问柔性数组元素
    printf("柔性数组元素:");
    for (int i = 0; i < p->len; i++) {
        printf("%d ", p->data[i]);  // 输出:0 10 20 30 40
    }
    printf("\n");

    // 4. 释放内存(一次free即可)
    free(p);
    p = NULL;  // 避免野指针
    return 0;
}
  • 柔性数组的动态调整(体现 "柔性"):
    • 通过 realloc 函数可以随时调整柔性数组的大小,原数据会自动迁移到新空间(若空间地址改变)。
    • 示例(将上述示例中的柔性数组从 5 个int扩展到 8 个):
c 复制代码
// 原p指向24字节空间,扩展为:4 + 8*4 = 36字节
type_a* new_p = (type_a*)realloc(p, sizeof(type_a) + 8 * sizeof(int));
if (new_p == NULL) {
    perror("realloc failed");
    free(p);  // 若扩展失败,释放原有内存
    return 1;
}
p = new_p;
p->len = 8;  // 更新长度记录

// 为新增的3个元素赋值
for (int i = 5; i < p->len; i++) {
    p->data[i] = i * 10;
}

// 验证扩展后的数据
printf("扩展后元素:");
for (int i = 0; i < p->len; i++) {
    printf("%d ", p->data[i]);  // 输出:0 10 20 30 40 50 60 70
}

free(p);
p = NULL;
  • 注意:调整大小时,realloc的第二个参数必须重新计算(sizeof(type_a) + 新元素个数*元素大小),不能直接基于原有柔性数组的长度累加。
5.3 柔性数组的优势

以"存储一段动态长度的整数序列"为例,对比柔性数组与"结构体+指针"两种实现方式,凸显柔性数组的优势:

  • 实现方式对比
    • 柔性数组方式(type_a):
c 复制代码
// 分配:一次malloc完成所有内存申请
type_a* fa = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int));
fa->len = 100;

// 使用:直接通过fa->data[i]访问
for (int i = 0; i < fa->len; i++) {
    fa->data[i] = i;
}

// 释放:一次free即可
free(fa);
fa = NULL;
  • 结构体 + 指针方式(type_b)
c 复制代码
 typedef struct ptr_type {
    int len;
    int* data;  // 用指针指向动态数组
} type_b;

// 分配:需两次malloc,分别申请结构体和数组内存
type_b* pb = (type_b*)malloc(sizeof(type_b));
pb->len = 100;
pb->data = (int*)malloc(pb->len * sizeof(int));  // 二次分配

// 使用:通过pb->data[i]访问
for (int i = 0; i < pb->len; i++) {
    pb->data[i] = i;
}

// 释放:需两次free,且必须先释放数组,再释放结构体
free(pb->data);  // 若忘记释放,会导致内存泄漏
pb->data = NULL;
free(pb);
pb = NULL;:​
  • 优势 1:内存释放的简洁性与安全性
    • 柔性数组只需一次free操作,无需关注内部成员的内存管理,尤其在函数返回动态结构体时,能避免用户因忘记释放成员内存(如type_b中的data)而导致的内存泄漏。
    • 示例(函数返回场景):
c 复制代码
// 返回柔性数组结构体,用户只需一次释放
type_a* create_flex_array(int n) {
    type_a* p = (type_a*)malloc(sizeof(type_a) + n * sizeof(int));
    p->len = n;
    return p;
}

// 用户使用
type_a* arr = create_flex_array(50);
// ... 使用后
free(arr);  // 简单安全,无内存泄漏风险
  • 优势 2:内存连续性与访问效率
    • 柔性数组的所有内存(结构体固定部分 + 柔性数组部分)是连续的,存储在同一块内存区域中。这种连续性带来两个好处:
      • 减少 CPU 缓存失效:连续内存更可能被一次性加载到 CPU 缓存中,访问时无需频繁从内存中读取,速度更快。
      • 简化地址计算:访问fa->data[i]时,编译器只需通过fa的地址 +sizeof(int)(len的大小)即可定位到data的起始地址,再加上i*sizeof(int)得到目标元素地址,仅需一次地址计算。
    • 结构体 + 指针方式中,结构体与数组内存是离散的,访问pb->data[i]时,需先从pb中读取data指针的地址,再计算i对应的偏移量,涉及两次地址计算,且离散内存更难被 CPU 缓存优化。
  • 优势 3:减少内存碎片
    • 内存碎片指系统中存在大量零散的、无法被有效利用的小内存块。柔性数组通过一次内存分配获取所有所需空间,相比两次分配(结构体 + 指针)能减少内存碎片的产生,尤其在频繁创建和销毁动态数组时,效果更明显。
相关推荐
赵谨言16 小时前
基于python大数据的城市扬尘数宇化监控系统的设计与开发
大数据·开发语言·经验分享·python
Yurko1316 小时前
【C语言】程序控制结构
c语言·开发语言·学习
say_fall16 小时前
数据结构之顺序表:一款优秀的顺序存储结构
c语言·数据结构
Peace & Love48716 小时前
C++初阶 -- 模拟实现list
开发语言·c++·笔记
摇滚侠17 小时前
Spring Boot3零基础教程,云服务停机不收费,笔记71
java·spring boot·笔记
丰锋ff17 小时前
英一2013年真题学习笔记
笔记·学习
摇滚侠17 小时前
Spring Boot3零基础教程,监听 Kafka 消息,笔记78
spring boot·笔记·kafka
能不能别报错17 小时前
K8s学习笔记(二十二) 网络组件 Flannel与Calico
笔记·学习·kubernetes
yuuki23323317 小时前
【数据结构】顺序表的实现
c语言·数据结构·后端
GilgameshJSS18 小时前
STM32H743-ARM例程31-CAN
c语言·arm开发·stm32·单片机·嵌入式硬件