深入C语言:动态内存管理魔法

简介: 深入C语言:动态内存管理魔法

一、 为什么存在动态内存分配

目前我们已经掌握了以下两种开辟内存的方式:

// 在栈上开辟4个字节
int val = 20;
 
// 在栈空间上开辟10个字节的连续空间
char arr[10] = {0};


上述开辟空间的方式有两个特点:

  • 空间开辟的大小是固定的。
  • 数组在声明时必须指定数组的长度,在编译时会开辟并分配其所需要的内存空间。


为了解决静态内存开辟的内存空间固定的问题,C语言引⼊了动态内存开辟,让程序员⾃⼰可以申请和释放空间,就⽐较灵活了。


所谓动态内存分配(Dynamic Memory Allocation) 就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不象数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。


为什么会存在动态内存开辟?

有时我们需要的空间大小在程序运行的时候才能知道,这时在数组编译时开辟空间的方式就不能满足了,这时我们就需要动态内存开辟来解决问题。


二、动态内存函数

2.1 malloc函数

void *malloc(size_t size)


  1. 头文件#include <stdlib.h>
  2. 声明:void* malloc (size_t size);
  1. size – 内存块的大小,以字节为单位
  2. 如果参数 size 为0,malloc的⾏为是标准是未定义的,取决于编译器。
  1. 作用:向内存申请⼀块连续可⽤的空间,并返回指向这块空间的指针
  2. 返回值:返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使⽤的时候使⽤者⾃⼰来决定。
  1. 如果开辟成功,则返回⼀个指向开辟好空间的指针。
  2. 如果开辟失败,则返回⼀个 NULL 指针,因此malloc的返回值⼀定要做检查。

示例如下:

int main()
{
  int* arr = (int*)malloc(sizeof(int) * 10);
  //开辟十个大小为整型的空间
  //返回类型强转为int*
  if (arr == NULL)//如果开辟失败
  {
    perror("malloc fail: ");//打印错误信息
                 return 1;//直接返回
  }
  int i = 0;
  for (i = 0; i < 10; i++)//存入数据
  {
    arr[i] = i;
  }
  for (i = 0; i < 10; i++)//打印数据
  {
    printf("%d ", arr[i]);
  }
  return 0;
}

2.2 calloc函数

void *calloc(size_t num, size_t size)

calloc 函数的功能实为 num 个大小为 size 的元素开辟一块空间,并把空间的每个字节初始化为 0,返回一个指向它的指针。


与malloc函数做对比:

  • malloc 只有一个参数,而 calloc 有两个参数,分别为元素的个数和元素的大小。
  • 与函数 malloc 的区别在于 calloc 会在返回地址前把申请的空间的每个字节初始化为 0  


代码示例如下:

#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    //malloc
    int* p1 = (int*)malloc(40);  //开辟40个空间
    //calloc
    int* p2 = (int*)calloc(10, sizeof(int));  //开辟10个大小为int的空间,40
   
    if (p1 == NULL)
        return 1;
    if (p2 == NULL)
        return 1;
   
    int i = 0;
    for (i = 0; i < 10; i++)
        printf("%d ", *(p1 + i));
    printf("\n");
    for (i = 0; i < 10; i++)
        printf("%d ", *(p2 + i));
   
    free(p1);
    p1 = NULL;
    free(p2);
    p2 = NULL;
    
    return 0;
}


运行结果:10个随机值

0 0 0 0 0 0 0 0 0 0

说明 calloc 会对内存进行初始化,把空间的每个字节初始化为 0 。如果我们对于申请的内存空间的内容,要求其初始化,我们就可以使用 calloc 函数来轻松实现。


2.3 reallco函数

void *realloc(void *ptr, size_t size)


realloc 函数,让动态内存管理更加灵活。用于重新调整之前调用 malloc 或 calloc 所分配的 ptr 所指向的内存块的大小,可以对动态开辟的内存进行大小的调整。具体介绍如下:


  1. ptr 为指针要调整的内存地址。
  2. size 为调整之后的新大小。
  3. 返回值为调整之后的内存起始位置,请求失败则返回空指针。
  4. realloc 函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。


realloc 函数在调整内存空间时存在的三种情况:


  1. 当原有空间之后没有足够大的空间时,直接在原有内存之后直接追加空间,原来空间的数组不发生变化。
  2. 当原有空间之后没有足够大的空间时,会在堆空间上另找一个合适大小的连续的空间来使用。函数的返回值将是一个新的内存地址。
  3. 如果找不到合适的空间,就会返回一个空指针。


用法演示:如果 realloc 找不到合适的空间,就会返回空指针,所以realloc在开辟空间时存在返回空指针的危险,我们不要拿指针直接接收 realloc,可以使用临时指针判断一下realloc是否为空

#include <stdio.h>
#include <stdlib.h>
 
int main() 
{
    int* p = (int*)calloc(10, sizeof(int));
    if (p == NULL) {
        perror("main");
        return 1;
    }
    //使用
    int i = 0;
    for (i = 0; i < 10; i++) {
        *(p + i)  = 5;
    }
    
    //此时,这里需要 p 指向的空间更大,需要 20 个int的空间
    //realloc 调整空间
    int* ptmp = (int*)realloc(p, 20*sizeof(int));
    
    //如果ptmp不等于空指针,再把p交付给它
    if (ptmp != NULL) {
        p = ptmp;
    }
 
    //释放
    free(p);
    p = NULL;
}


2.4 free函数

动态内存开辟的空间并不像静态开辟内存的空间会随着一段程序的结束而回收,这时就需要我们手动回收,否则就会造成内存泄漏。


  1. 声明:void free(void *ptr)
  1. ptr – 指针指向一个要释放内存的内存块,该内存块之前是通过调用 malloc、calloc 或 realloc 进行分配内存的。如果传递的参数是一个空指针,则不会执行任何动作。
  1. 作用:释放之前调用 calloc、malloc 或 realloc 所分配的内存空间。
  2. 返回值:该函数不返回任何值。


代码示例如下:

#include <stdio.h>
#include <stdlib.h>
 
int main(void) 
{
    // 假设开辟10个整型空间
    int arr[10]; // 在栈区上开辟
 
    // 动态内存开辟
    int* p = (int*)malloc(10*sizeof(int)); // 开辟10个大小为int的空间
 
    // 使用时先判断是否开辟成功
    if (p == NULL) {
        perror("main"); // main: 错误信息
        return 0;
    }
    
    // 使用
    int i = 0;
    for (i = 0; i < 10; i++) {
        *(p + i) = i;
    }
    for (i = 0; i < 10; i++) {
        printf("%d ", p[i]);
    }
 
    // 回收空间
    free(p);
    p = NULL; // 需要手动置为空指针
 
    return 0;
}


运行结果为 0 1 2 3 4 5 6 7 8 9


注意:


  • 如果参数 ptr 指向的空间不是动态开辟的,那么 free 函数的行为是未定义的。
  • 如果参数 ptr 是 NULL 指针,那么 free 将不会执行任何动作。
  • 使用完之后一定要记得使用 free 函数释放所开辟的内存空间。
  • 使用指针指向动态开辟的内存,使用完并 free 之后一定要记得将其置为NULL空指针。


为什么 free 之后,一定要把 p 置为空指针?

因为 free 之后那块开辟的内存空间已经不在了,它的功能只是把开辟的空间回收掉,但是 p 仍然还指向那块内存空间的起始位置,为了防止后续再对这块空间执行操作,我们需要及时使用 p = NULL 把他置成空指针。


三、常见的动态内存错误

3.1 对NULL空指针的解引用操作

错误代码示例:

#include <stdlib.h>
#include <stdio.h>
 
int main()
{
    int* p = (int*)malloc(9999999999);
    int i = 0;
    for (i = 0; i < 10; i++) {
        *(p + i) = i; // 对空指针进行解引用操作,非法访问内存
    }
 
    return 0;
}


  1. 当malloc申请的空间太大时存在失败的情况,失败返回NULL指针。
  2. 而系统无法访问NULL指针指向的地址,这时编译器会报一个警告:

解决方案:对 malloc 函数的返回值做判空处理

#include <stdlib.h>
#include <stdio.h>
 
int main()
{
    int* p = (int*)malloc(9999999999);
    // 对malloc函数的返回值做判空处理
    if (p == NULL) {
        perror("main");
        return 1;
    }
    int i = 0;
    for (i = 0; i < 10; i++) {
        *(p + i) = i; // 对空指针进行解引用操作,非法访问内存
    }
 
    return 0;
}


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

void test()
{
  int i = 0;
  int* p = (int*)malloc(10 * sizeof(int));
  if (NULL == p)
  {
    perror("malloc fail: ");//打印错误信息
    return 1;//直接返回
  }
  for (i = 0; i <= 10; i++)
  {
    *(p + i) = i; //当i是10的时候越界访问
  }
  free(p);
         p=NULL;
}


  1. malloc只申请了十个整型大小的空间。
  2. for循环循环了十一次,越界访问,错误信息如下:

改正方法:

void test()
{
  int i = 0;
  int* p = (int*)malloc(10 * sizeof(int));
  if (NULL == p)
  {
    perror("malloc fail: ");//打印错误信息
    return 1;//直接返回
  }
  for (i = 0; i < 10; i++)
  {
    *(p + i) = i; //当i是10的时候越界访问
  }
  free(p);
  p = NULL;
}


3.3 对非动态开辟的空间使用free释放

void test()
{
  int a = 10;
  int* p = &a;
  free(p);
         p=NULL;//ok?
}


  1. free()只能释放有动态内存开辟在堆上的空间。
  2. p指向的空间是静态内存开辟的,无法释放,释放就会出错:

改正方法:

void test()
{
  int a = 10;
  int* p = &a;
}


  • 静态内存开辟的空间并不需要释放。

3.4 使用free释放一块动态开辟内存的一部分

void test()
{
  int* p = (int*)malloc(100);
  p++;
  free(p); //p不再指向动态内存的起始位置
  p = NULL;
}


  1. p++跳过一个整型大小的空间。
  2. free()释放p只会释放当前位置开始之后的空间,有一个整型大小的空间未被释放,造成内存泄漏。

改正方法:

void test()
{
  int* p = (int*)malloc(100);
  free(p); 
  p = NULL;
}


  • 不能随意改变p指向的位置,开辟多少内存就释放多少内存

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

#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    int* p = malloc(10*sizeof(int));
    if (p == NULL) {
        return 1;
    }
    int i = 0;
    for (i = 0; i < 10; i++) {
        p[i] = i;
    }
 
    //释放
    free(p);
    //再释放
    free(p);
  
    return 0;
}
  1. p已经被释放归还给操作系统,但是此时p还指向该内存,是一个野指针。
  2. 再次释放p就会出现内存出错问题。

解决方案:在第一次释放后紧接着将 p 置为空指针,这样再次free空指针就不会进行任何操作。

// 释放
free(p);
p = NULL;
 
free(p); // 此时p为空,free什么也不做


3.6 动态开辟内存忘记释放导致内存泄露

void test()
{
  int* p = (int*)malloc(100);
  if (NULL != p)
  {
    *p = 20;
  }//内存泄漏
}
 
int main()
{
  test();
}


动态开辟的内存空间有两种回收方式:  1. 主动释放(free)      2. 程序结束

malloc 这一系列函数 和 free 一定要成对使用,记得及时释放。

void test()
{
  int* p = (int*)malloc(100);
  if (NULL != p)
  {
    *p = 20;
  }
  
  free(p);
  p = NULL;
}


四、柔性数组

4.1 柔性数组的定义

C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

1. struct S {
2. int n;
3. int arr[0];   //柔性数组成员
4. };


部分编译器可能会报错,

1. struct S {
2. int n;
3. int arr[];   //柔性数组成员
4. };


特点:

  • 结构中的柔性数组成员前面必须至少一个其他成员。
  • sizeof 返回的这种结构大小不包括柔性数组的内存。
  • 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小


4.2 柔性数组的大小

依靠我们结构体学过得内存对齐的原则,我们可以计算结构体的大小。

代码示例:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
 
struct S {
    int n;
    int arr[0];  //前面至少有一个成员
};
 
int main() {
 
    printf("%d", sizeof(struct S));   //大小为4,不包含柔性数组
    //后面+的大小就是给柔性数组准备的
    struct S* ps = (struct S*)malloc(sizeof(struct S) + sizeof(int));
 
    return 0;
}


输出结果:4

从上述可知柔性数组成员是不计入结构体大小的。

4.3 柔性数组的使用

柔性数组的使用与结构体使用十分类似,具体使用如下:

#include <stdio.h>
#include <stdlib.h>
 
struct S {
    int n;
    int arr[0];
};
 
int main() {
    //期望arr的大小是10个整型
    struct S* ps = (struct S*)malloc(sizeof(struct S) + sizeof(int));
    ps->n = 10;
 
    //使用
    int i = 0;
    for (i = 0; i < 10; i++) {
        ps->arr[i];
    }
 
    //增容
    struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 20*sizeof(int));
    if (ptr != NULL) {
        ps = ptr;
    }
 
    // 再次使用
    ………………
    …………
    ……
 
    // 释放
    free(ps);
    ps = NULL;
 
    return 0;
}


4.4 柔性数组的优势

对比下面两组代码:

1. 使用柔性数组

#include <stdio.h>
#include <stdlib.h>
 
struct S {
    int n;
    int arr[0];
};
 
int main() {
    //期望arr的大小是10个整型
    struct S* ps = (struct S*)malloc(sizeof(struct S) + sizeof(int));
    ps->n = 10;
 
    //使用
    int i = 0;
    for (i = 0; i < 10; i++) {
        ps->arr[i];
    }
 
    //增容
    struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 20*sizeof(int));
    if (ptr != NULL) {
        ps = ptr;
    }
 
    // 再次使用
    ………………
    …………
    ……
 
    // 释放
    free(ps);
    ps = NULL;
 
    return 0;
}


2. 使用指针

#include <stdio.h>
#include <stdlib.h>
 
struct S {
    int n;
    int* arr;
};
 
int main() {
    struct S* ps = (struct S*)malloc(sizeof(struct S));
    if (ps == NULL)
        return 1;
    ps->n = 10;
    ps->arr = (int*)malloc(10 * sizeof(int));
    if (ps->arr == NULL)
        return 1;
 
    // 使用
    int i = 0;
    for (i = 0; i < 10; i++) {
        ps->arr[i];
    }
 
    // 增容
    int* ptr = (struct S*)realloc(ps->arr, 20 * sizeof(int));
    if (ptr != NULL) {
        ps->arr = ptr;
    }
 
    // 再次使用 
    ………………
    …………
    ……
 
    // 释放
    free(ps->arr); // 先free第二块空间
    ps->arr = NULL;
    free(ps);
    ps = NULL;
 
    return 0;
}


上述 代码1 代码2 可以完成同样的功能,但是 方法1 的实现有两个好处:

(1)第一个好处:有利于内存释放

代码1只需要释放一次内存,代码2需要释放两次,防止了内存释放不干净的疏忽


(2) 第二个好处:有利于访问速度

连续内存多多少少有益于提高访问速度,还能减少内存碎片。

malloc 的次数越多,产生的内存碎片就越多,这些内存碎片不大不小,再次被利用的可能性很低。内存碎片越多,内存的利用率就会降低,频繁的开辟空间效率会变低,碎片也会增加

相关文章
|
2天前
|
存储 缓存 关系型数据库
MySQL事务日志-Redo Log工作原理分析
事务的隔离性和原子性分别通过锁和事务日志实现,而持久性则依赖于事务日志中的`Redo Log`。在MySQL中,`Redo Log`确保已提交事务的数据能持久保存,即使系统崩溃也能通过重做日志恢复数据。其工作原理是记录数据在内存中的更改,待事务提交时写入磁盘。此外,`Redo Log`采用简单的物理日志格式和高效的顺序IO,确保快速提交。通过不同的落盘策略,可在性能和安全性之间做出权衡。
1519 4
|
29天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
5天前
|
人工智能 Rust Java
10月更文挑战赛火热启动,坚持热爱坚持创作!
开发者社区10月更文挑战,寻找热爱技术内容创作的你,欢迎来创作!
503 19
|
2天前
|
存储 SQL 关系型数据库
彻底搞懂InnoDB的MVCC多版本并发控制
本文详细介绍了InnoDB存储引擎中的两种并发控制方法:MVCC(多版本并发控制)和LBCC(基于锁的并发控制)。MVCC通过记录版本信息和使用快照读取机制,实现了高并发下的读写操作,而LBCC则通过加锁机制控制并发访问。文章深入探讨了MVCC的工作原理,包括插入、删除、修改流程及查询过程中的快照读取机制。通过多个案例演示了不同隔离级别下MVCC的具体表现,并解释了事务ID的分配和管理方式。最后,对比了四种隔离级别的性能特点,帮助读者理解如何根据具体需求选择合适的隔离级别以优化数据库性能。
179 1
|
8天前
|
JSON 自然语言处理 数据管理
阿里云百炼产品月刊【2024年9月】
阿里云百炼产品月刊【2024年9月】,涵盖本月产品和功能发布、活动,应用实践等内容,帮助您快速了解阿里云百炼产品的最新动态。
阿里云百炼产品月刊【2024年9月】
|
21天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
9天前
|
Linux 虚拟化 开发者
一键将CentOs的yum源更换为国内阿里yum源
一键将CentOs的yum源更换为国内阿里yum源
457 5
|
7天前
|
存储 人工智能 搜索推荐
数据治理,是时候打破刻板印象了
瓴羊智能数据建设与治理产品Datapin全面升级,可演进扩展的数据架构体系为企业数据治理预留发展空间,推出敏捷版用以解决企业数据量不大但需构建数据的场景问题,基于大模型打造的DataAgent更是为企业用好数据资产提供了便利。
314 2
|
23天前
|
人工智能 IDE 程序员
期盼已久!通义灵码 AI 程序员开启邀测,全流程开发仅用几分钟
在云栖大会上,阿里云云原生应用平台负责人丁宇宣布,「通义灵码」完成全面升级,并正式发布 AI 程序员。
|
25天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2608 22
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析