c语言 进阶 动态内存管理

动态内存管理

    • 1. 为什么存在动态内存分配
    • 2. 动态内存函数的介绍​
      • 2.1 malloc 和 free
        • malloc 函数
        • free 函数
      • 2.2内存泄漏
      • 2.3 calloc
      • 2.4 realloc
    • 3. 常见的动态内存错误
      • 3.1 对NULL指针的解引用操作
      • 3.2 对动态开辟空间的越界访问
      • 3.3 对非动态开辟内存使用free释放
        • 3.4 使用free释放一块动态开辟内存的一部分
      • 3.5 对同一块动态内存多次释放
      • 3.6 动态开辟内存忘记释放(内存泄漏)
    • 4. 几个经典的笔试题
        • 4.1 题分析
        • 4.2 题分析
        • 4.3 题分析
        • 4.4 题分析
    • 5. 柔性数组(Flexible Array)
        • 5.1 柔性数组的特点
        • 5.2 柔性数组的使用
        • 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(正常开辟):
// 申请可以存储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(开辟失败):​
// 申请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);专门用于释放动态开辟的内存,将内存归还给系统。

  • 示例:
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(忘记释放):
void test() {int* p = (int*)malloc(100);// 使用p后没有调用free(p),函数结束后p被销毁,再也无法找到这块内存,导致内存泄漏
}
int main() {test();// 程序运行期间,test函数中申请的100字节内存一直未被释放return 0;
}
  • 示例 2(指针被修改导致无法释放):
 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对比):
// 使用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会在堆区重新找一块大小合适的空间,将原来空间中的数据复制到新空间,然后释放原来的空间,返回新空间的指针。
  • 示例(正确使用):
// 先申请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字节内存无法释放,造成内存泄漏

  • 错误示例(用原指针接收返回值):
 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指针不指向任何有效的内存空间,对其进行解引用操作(如赋值、取值)会导致程序崩溃。
  • 示例(错误):
int* p = (int*)malloc(1000000000); // 申请过大空间,可能失败返回NULL
*p = 10; // 对NULL指针解引用,程序会崩溃

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

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

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

  • 错误原因:访问动态开辟的内存空间时,超出了申请的范围,就像数组越界访问一样,会导致不可预期的错误,可能修改其他内存的数据,也可能导致程序崩溃。
  • 示例:
// 申请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会导致程序行为未定义,通常会引发程序崩溃。
  • 示例(错误):
int a = 10; // 栈上的局部变量
int* p = &a;
free(p); // 错误,释放非动态开辟的内存,程序可能崩溃
p = NULL;

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

3.4 使用free释放一块动态开辟内存的一部分
  • 错误原因:free函数释放动态内存时,要求指针必须指向动态开辟内存的起始地址,因为内存管理系统需要通过起始地址来回收整个内存块。如果指针指向的是动态开辟内存的中间位置,free无法正确回收内存,会破坏堆区的内存管理结构,导致程序出错。
  • 示例(错误):
int* p = (int*)malloc(4 * sizeof(int)); // p指向动态开辟内存的起始地址
if (p == NULL) {perror("malloc failed");return 1;
}
p++; // p现在指向动态开辟内存的第二个int的位置,不再是起始地址
free(p); // 错误,释放的是内存的一部分,程序可能崩溃
  • 避免方法:在释放动态内存之前,确保指针指向动态开辟内存的起始地址。如果在操作过程中移动了指针,需要先保存起始地址。​
  • 示例(正确):​
	 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多次,会导致堆区内存管理结构被破坏,因为第一次释放后,该内存已经归还给系统,再次释放时,系统无法识别该内存块的状态,从而引发程序崩溃。
  • 示例(错误):
int* p = (int*)malloc(100);
free(p);
free(p); // 错误,对同一块内存多次释放,程序可能崩溃
  • 避免方法:释放内存后,立即将指针置为 NULL,因为 free 函数对 NULL 指针什么也不做,这样即使不小心再次释放,也不会出现错误。​
  • 示例(正确):
 int* p = (int*)malloc(100);
free(p);
p = NULL; // 释放后将指针置为NULL
free(p); // 安全,free对NULL指针无操作

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

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

4. 几个经典的笔试题

4.1 题分析
  • 代码实现
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 题分析
  • 代码实现
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 题分析
  • 代码实现
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 题分析
  • 代码实现
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),因此也被称为“可变长数组成员”。
  • 定义示例及编译器兼容性:
// 方式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. 结构成员的位置约束
    • 柔性数组成员必须是结构体的最后一个成员,不能有其他成员跟在其后。
    • 错误示例(柔性数组后有其他成员):
typedef struct wrong_st {int a;int flex[];  // 柔性数组int b;       // 错误:柔性数组后不能有其他成员
} wrong_type;  // 编译器会报错
  1. sizeof运算符的计算规则:​
    • sizeof计算包含柔性数组的结构体大小时,仅计算柔性数组前面所有成员的总大小,完全忽略柔性数组的存在。​
    • 示例(基于type_a):
	// 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元素的空间):
	// 计算总大小: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 柔性数组的使用
  • 基本使用流程:动态分配内存→初始化成员→访问柔性数组→释放内存。
    • 完整示例:
#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 个):
// 原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):
// 分配:一次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)
 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)而导致的内存泄漏。​
    • 示例(函数返回场景):
// 返回柔性数组结构体,用户只需一次释放
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:减少内存碎片​
    • 内存碎片指系统中存在大量零散的、无法被有效利用的小内存块。柔性数组通过一次内存分配获取所有所需空间,相比两次分配(结构体 + 指针)能减少内存碎片的产生,尤其在频繁创建和销毁动态数组时,效果更明显。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/915453.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/915453.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Redis的五大基本数据类型

一、Redis基本知识与Redis键&#xff08;key&#xff09;常用操作命令。redis的默认端口6379。mysql默认端口号3306。 默认16个数据库&#xff0c;类似数组的下标从0开始&#xff0c;初始默认使用0号库。可以使用select index来切换数据库&#xff0c;如&#xff1a;select 1&a…

达梦数据库JSON_TABLE使用说明

在达梦数据库&#xff08;DM Database&#xff09;中&#xff0c;将 JSON 数据转换为表格形式可以使用内置的 JSON_TABLE 函数。以下是详细步骤和示例&#xff1a;1. 核心函数&#xff1a;JSON_TABLE JSON_TABLE 用于将 JSON 数据解析为关系表结构&#xff0c;支持从 JSON 对象…

A316-1926-V1 USB多路高清音频解码器模组技术解析

随着数字音频技术的不断发展&#xff0c;高品质音频解决方案的需求日益增长。本文将介绍一款基于XMOS技术的高性能USB音频解码器模组——A316-1926-V1&#xff0c;这是一款专为高清音频应用设计的专业模组。核心技术与特性A316-1926-V1是一款集成了多项先进技术的USB多路高清音…

.NET 8 中的 KeyedService

.NET 8 中的 KeyedService&#xff1a;新特性解析与使用示例 一、引言 在 .NET 8 的 Preview 7 版本中&#xff0c;引入了 KeyedService 支持。这一特性为开发者提供了按名称&#xff08;name&#xff09;获取服务的便利&#xff0c;在某些场景下&#xff0c;开发者无需再自行…

Paimon对比基于消息队列(如Kafka)的传统实时数仓方案的优势

弊端&#xff1a;数据重复 -> 优势&#xff1a;Paimon 主键表原生去重原方案弊端 (Kafka)问题: 消息队列&#xff08;Kafka&#xff09;是仅支持追加&#xff08;Append-Only&#xff09;的日志流。当 Flink 作业发生故障恢复&#xff08;Failover&#xff09;或业务逻辑迭代…

Linux Shell 命令 + 项目场景

shell 命令1. 基础文件操作命令1.1 ls - 列出目录内容1.2 find - 文件搜索2. 版本控制命令2.1 git - 版本控制系统2.2 高级 Git 操作3. 文本搜索命令3.1 grep - 文本搜索3.2 高级搜索技巧4. Android 构建系统命令4.1 source - 加载环境变量4.2 lunch - 选择构建目标4.3 m - And…

A316-Mini-V1:超小尺寸USB高清音频解码器模组技术探析

引言 随着便携式音频设备的普及&#xff0c;对小型化、高性能音频解决方案的需求日益增长。本文将介绍一款极致小型化的高性能USB音频解码器模组——A316-Mini-V1&#xff0c;这是一款基于XMOS XU316芯片的微型音频处理模组。产品概述 A316-Mini-V1是一款专为小尺寸产品设计的M…

低代码平台买saas好还是私有化好

选择低代码平台采用SaaS还是私有化部署&#xff0c;应根据企业具体情况考虑安全性、成本控制、维护难度、扩展需求等因素。 其中&#xff0c;安全性是决定企业选择的重要因素之一。私有化部署意味着企业能够完全掌控数据和系统的安全管理&#xff0c;更适合对数据安全要求极高的…

基于SkyWalking的微服务APM监控实战指南

基于SkyWalking的微服务APM监控实战指南 1. 业务场景描述 随着微服务在生产环境中大规模应用&#xff0c;系统链路复杂、实例弹性伸缩、灰度发布等特点都给性能监控和问题诊断带来了新的挑战。传统的单机或轻量级监控方案已无法满足微服务环境下的全链路、分布式追踪和实时告警…

Python 进阶(五): Excel 基本操作

目录 1. 概述2. 写入 2.1 使用 xlwt2.2 使用 XlsxWriter 3. 读取4. 修改 1. 概述 在现实中&#xff0c;很多工作都需要与数据打交道&#xff0c;Excel 作为常用的数据处理工具&#xff0c;一直备受人们的青睐&#xff0c;而大部分人都是手动操作 Excel&#xff0c;如果数据量…

32、鸿蒙Harmony Next开发:使用动画-动画概述

​​​属性动画转场动画粒子动画组件动画动画曲线动画衔接动画效果帧动画&#xff08;ohos.animator&#xff09; UI&#xff08;用户界面&#xff09;中包含开发者与设备进行交互时所看到的各种组件&#xff08;如时间、壁纸等&#xff09;。属性作为接口&#xff0c;用于控制…

【STM32】485接口原理

485 通信实验 这篇文章是对 RS485通信 的原理、硬件连接、接口芯片&#xff08;SP3485&#xff09;、总线结构等都有详尽的说明。我们在此处进行清晰有条理的讲解整理&#xff0c;便于学习和实验操作。 在了解485接口通信原理之前&#xff0c;我们先复习一下串口&#xff1a;串…

亚马逊二审攻防全攻略:预防、应对与长效合规之道

当店铺收到二审通知&#xff0c;不少卖家会陷入焦虑与慌乱&#xff0c;只要掌握科学的预防策略与应对方法&#xff0c;不仅能降低二审风险&#xff0c;即便遭遇审核也能顺利突围。一、未雨绸缪&#xff1a;预防二审的四大核心策略夯实资料真实性根基资料的真实性与一致性是亚马…

添加状态信息

1首先在数据字典里加入可借阅和不可借阅状态2导入数据字典export default {name: "Book",dicts: [book_borrow_status],//导入数据字典data() {return {formData: {name: null,author: null,num: null,price: null,typeId: null,status:null//新加状态属性},3设置状态…

234、回文链表

题目&#xff1a;解答&#xff1a;对143稍作修改即可&#xff0c;判断两个指针指向的是否一直相等。终止条件为不等或者head2nullptrclass Solution { public:ListNode *rev(ListNode *head){ListNode *cur head;ListNode *pre nullptr;while(cur){ListNode * nxt cur->n…

第15次:商品搜索

实现用户在页面可自由搜索某个商品的功能。 第1步&#xff1a;准备搜索功能用到的库 pip install whoosh pip install jieba pip install django-haystackwhoosh是搜索引擎&#xff0c;对英文支持较好&#xff0c;但对中文效果不佳。jieba为中文分词库&#xff0c;弥补whoosh…

《使用Qt Quick从零构建AI螺丝瑕疵检测系统》——0. 博客系列大纲

目录【《使用Qt Quick从零构建AI螺丝瑕疵检测系统》系列简介】第一部分&#xff1a;基础入门与项目启航第二部分&#xff1a;核心视觉算法开发第三部分&#xff1a;模拟完整工业流程第四部分&#xff1a;软件打包与高级特性【《使用Qt Quick从零构建AI螺丝瑕疵检测系统》系列简…

【Python】Python中的循环语句

循环语句导读一、基本概念1.1 循环语句的执行流程1.2 循环语句的分类二、while语句三、for语句四、break与continue五、死循环六、循环中的else语句七、range()函数结语导读 大家好&#xff0c;很高兴又和大家见面啦&#xff01;&#xff01;&#xff01; 在上一篇内容中我们…

docker|Linux|以centos基础镜像为基础制作nmap专用镜像(镜像瘦身计划)

一、 最近由于某些场景下需要使用nmap&#xff0c;而nmap的rpm安装包在源目标机器上使用有软件冲突&#xff0c;因此&#xff0c;计划使用docker部署nmap 具体计划为 1、使用centos的基础镜像&#xff0c;在有网环境下&#xff0c;通过配置阿里云的yum仓库&#xff0c;在cen…

基于单片机公交车报站系统/报站器

传送门 &#x1f449;&#x1f449;&#x1f449;&#x1f449;其他作品题目速选一览表 &#x1f449;&#x1f449;&#x1f449;&#x1f449;其他作品题目功能速览​​​​​​​ 概述 公交车自动报站系统利用单片机作为核心控制器&#xff0c;结合GPS/北斗定位模块、语音存…