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:减少内存碎片
    • 内存碎片指系统中存在大量零散的、无法被有效利用的小内存块。柔性数组通过一次内存分配获取所有所需空间,相比两次分配(结构体 + 指针)能减少内存碎片的产生,尤其在频繁创建和销毁动态数组时,效果更明显。
相关推荐
眠修2 小时前
部署 Zabbix 企业级分布式监控
笔记·分布式·zabbix
O执O3 小时前
JavaWeb笔记四
java·hive·hadoop·笔记·web
kfepiza4 小时前
Debian-10,用dpkg, *.deb包,安装Mysql-5.7.42 笔记250717
linux·笔记·mysql·debian
404未精通的狗4 小时前
(数据结构)线性表(中):SLIst单链表
c语言·数据结构·链表
艾莉丝努力练剑4 小时前
【LeetCode&数据结构】栈的应用——有效的括号问题详解
c语言·开发语言·数据结构·学习·链表
爱装代码的小瓶子5 小时前
数据结构之栈
c语言·开发语言·数据结构·算法
ouliten5 小时前
cuda编程笔记(9)--使用 Shared Memory 实现 tiled GEMM
笔记·cuda
笑衬人心。5 小时前
RPC 与 Feign 的区别笔记
笔记·网络协议·rpc
Gu_shiwww5 小时前
C语言基础7——两种简单排序算法和二维数组
c语言·数据结构·算法·小白初步
秋说5 小时前
【PTA数据结构 | C语言版】列出连通集
c语言·数据结构·算法·深度优先·图论