C动态内存管理

简介: C动态内存管理

现在有一个需求:先输入一个整数n,再输入以空格分隔的n个整数,然后求出这n个整数中最大的数。

#include<stdio.h>
int main() {
  int n;
  int arr[20];
  // 输入n的值
  scanf("%d", &n);
  // 循环n次,输入n个数据
  for (int i = 0; i < n; i++)
  {
    scanf("%d", &arr[i]);
  }
  // 暂时认为第一个元素为最大的
  int max = arr[0];
  // max与各个元素比较,把较大的放入max
  for (int i = 0; i < n; i++)
  {
    if (arr[i] > max)
      max = arr[i];
  }
  printf("%d\n", max);
}

输入:

10
8 6 4 1 2 5 7 9 3 0

输出:

9

这里的特殊性在于,数据的数量n不确定,由用户输入决定。对于不同的输入,会出现以下3种情况。

  1. 如果n小于20,那么仅使用数组arr中的n个元素,后续的20 - n个元素闲置。
  2. 如果n等于20,数组arr中所有元素均被使用到。
  3. 如果n大于20,数组arr无法容纳多于20个元素的数据。

第一种情况会造成有空的元素闲置,而第三种情况数组无法容纳所有需要输入的数据。那么,能否待用户输入n后,再确定数组的元素个数呢?

变长数组

int n;
scanf("%d", &n);
int arr[n];
printf("sizeof of arr %d\n", sizeof(arr));

变长数组已经从C语言标准中移除了。编译器不一定会支持变长数组的特性。也就是说,这段代码可能在编译器中无法通过编译

申请内存空间

更通用的方法是,使用头文件stdlib.h中的malloc函数,从内存中申请一段连续的内存空间。

函数 malloc 的声明如下:

void* malloc(size_t size);

参数size为需要申请的内存空间大小。

返回值为void *类型的指针。若申请成功,返回值为成功申请的内存的首地址。若申请失败,返回值为NULL

通过malloc函数成功申请内存空间后,我们可以按照需要,将返回的指针转为任意类型的指针使用。只要通过指针访问内存时,不要超过这段内存空间的大小即可。

#include <stdio.h>
#include <stdlib.h>
int main()
{
  int* pInt = NULL;
  pInt = malloc(sizeof(int));
  double* pDouble = NULL;
  pDouble = malloc(sizeof(double));
  *pInt = 123;
  *pDouble = 3.1415926;
  printf("%d %f", *pInt, *pDouble);
  return 0;
}

malloc(sizeof(int))申请了4字节的内存空间,若申请成功,它将返回一个void *类型的指针,其数值为成功申请的内存空间的首地址。我们可以把这4字节的内存空间用于装int类型的数据。只要将void *通过赋值转换为int *,接着对int *类型的指针取值再赋值即可。

同样的,malloc(sizeof(double))申请了8字节的内存空间,若申请成功,它将返回一个void *类型的指针,其数值为成功申请的内存空间的首地址。我们可以把这8字节的内存空间用于装double类型的数据。只要将void *通过赋值转换为double *,接着对double *类型的指针取值再赋值即可。

CC++的语法差异

在C语言中,**void ***可以通过赋值转换为其他类型的指针。

int* pInt = NULL;
pInt = malloc(sizeof(int));
double* pDouble = NULL;
pDouble = malloc(sizeof(double));

但是,在C++中,必须先把void *指针强制转换后,才能赋值给其他类型的指针。建议无论是写C语言代码还是**C++**代码,都做强制类型转换,这样有利于代码的可移植性。

通过强制转换将void *转换为其他类型的指针后,再赋值:

int *pInt = NULL;
pInt = (int *)malloc(sizeof(int));
double *pDouble = NULL;
pDouble = (double *)malloc(sizeof(double));

要严格保证使用指针访问成功申请的内存空间时,不要超过申请时预定的空间大小。

double* pDouble = NULL;
pDouble = (double*)malloc(sizeof(int));
*pDouble = 3.1415926;

上面的代码中,申请了sizeof(int),即4字节大小的空间。若申请成功,它将返回一个void*类型的指针,其数值为成功申请的内存空间的首地址。接着,把它转换为double*类型的指针,并赋值给pDouble。但是,若对pDouble指针使用取值运算符*。将访问从首地址开始的8个内存空间,超出了申请时预定的4个字节空间,这种做法可能导致程序崩溃。

动态创建数组

若需要动态创建一个有10int元素的数组,那么需要申请sizeof(int) * 10字节的内存空间,或者写成sizeof(int[10])也行。

#include <stdio.h>
#include <stdlib.h>
int main()
{
  int* pArr = NULL;
  // 申请sizeof(int) * n,转换为int *使用
  pArr = (int*)malloc(sizeof(int) * 10);
  // 给数组元素赋值
  for (int i = 0; i < 10; i++)
    pArr[i] = i;
  // 打印数组元素
  for (int i = 0; i < 10; i++)
    printf("%d ", pArr[i]);
}

pArr = (int*)malloc(sizeof(int) * 10);中的sizeof(int) * 10写成sizeof(int[10])也是可以的。

之前讨论的都是malloc函数成功申请到内存的情况,作为一个稳健的程序应当也考虑到失败的情况。

返回值判断是否申请成功

malloc函数申请内存空间失败,它将返回NULL。对NULL指针取值将导致程序崩溃。建议每次通过malloc函数申请内存空间都对返回值进行判断。

#include <stdio.h>
#include <stdlib.h>
int main()
{
  int* pInt = NULL;
  pInt = (int*)malloc(sizeof(int));
  // 判断malloc函数是否成功申请内存空间
  if (pInt != NULL)
  {
    // 若不为NULL,再使用这个指针
    *pInt = 123;
    printf("%d", *pInt);
  }
}

释放内存空间

通过malloc函数申请内存空间,并使用完成后,要记得使用free函数把这段内存空间释放。

函数free的声明如下:

void free (void* ptr);

通过 malloc 申请内存空间后,系统内记录了这段内存空间的首地址和空间大小,保存到已分配的内存空间列表中,并保证这段空间不会再分配给别的地方。需要释放这段内存空间时,将首地址传入free函数。free函数将查找这个首地址是否在已分配的内存空间列表中,若存在,则根据列表中记录的首地址和空间大小,释放这段内存空间。释放后,这段内存空间可以再次分配给别的地方。

#include <stdio.h>
#include <stdlib.h>
int main()
{
  int* pInt = NULL;
  pInt = malloc(sizeof(int));
  if (pInt != NULL)
  {
    *pInt = 123;
    printf("%d", *pInt);
    // 释放内存空间
    free(pInt);
  }
}

上面的代码中,申请了sizeof(int)个内存空间当做int类型来使用。使用完毕后,使用free函数将其释放。free函数的参数是void *类型的指针,而void *类型的指针可以接收任何类型的指针。所以,可以直接将pInt传递给free函数而无需转换。

不能释放偏移后的指针

若将pInt偏移后,再传递给free函数。已分配的内存空间列表中并没有记录这个首地址,这样做并不能释放之前malloc(sizeof(int))分配的内存空间,并且有可能导致程序崩溃。

// pInt偏移后,再传递给free函数
free(pInt + 1);

若只调用malloc申请内存空间,而不调用free函数释放内存空间,成功申请内存空间将一直保留直到程序结束。这期间程序所占用的内存空间将会越来越大,直到没有可分配的空间,无法再成功申请内存空间为止。

#include <stdio.h>
#include <stdlib.h>
int main()
{
  while (1)
  {
    void* p = malloc(1024 * 1024);
    printf("%d\n", p);
  }
  return 0;
}

这种情况往往是申请了内存空间,但是忘记释放导致的。对于不再使用的内存空间,应当及时释放。

如上面示例代码的情况,申请的内存空间首地址存放到指针p中,而下一次新申请的内存空间首地址会覆盖掉上一次的首地址。由于没有保存内存空间的首地址,程序中将无法再通过任何方式使用或释放这些内存空间。这种现象被称作内存泄露,具有内存泄露问题的代码若长时间运行,会导致程序所占用的内存空间将会越来越大,直到没有可分配的空间,无法再成功申请内存空间为止。

从函数中返回指针

由于通过malloc函数申请的内存空间直到调用free函数释放或程序结束前都是有效的。因此,将指向malloc函数申请的内存空间的指针从函数中返回是合法的。

#include <stdio.h>
#include <stdlib.h>
int* func()
{
  int* pInt = NULL;
  pInt = malloc(sizeof(int));
  if (pInt != NULL)
  {
    *pInt = 123;
  }
  return pInt;
}
int main()
{
  int* p = func();
  if (p != NULL)
  {
    printf("%d", *p);
    // 使用完记得释放
    free(p);
  }
  return 0;
}

正确输出

123

函数func中,申请了sizeof(int)字节内存空间,若申请成功,将这段内存空间存放整型数据123。并将指向这段内存空间的指针pInt作为返回值返回。

函数main中,调用函数func获得返回的int *类型的指针p。由于不能保证func函数返回的指针一定有效,这里也要做指针判空。若指针不为空,才可以使用它。使用完毕后,记得使用free函数释放内存空间。

若在函数中申请一段内存空间作为数组使用,将数组首元素指针从函数中返回。在被调函数结束后,主调函数依然可以通过数组首元素指针偏移访问数组的所有元素。但是,必须要注意偏移时,不要访问超过内存空间预定大小的位置。并且使用完毕后,记得释放内存空间。

#include <stdio.h>
#include <stdlib.h>
int* func(int n)
{
  int* pArr = NULL;
  pArr = malloc(sizeof(int) * n);
  if (pArr == NULL)
  {
    // 申请失败,直接返回NULL
    return pArr;
  }
  // 申请成功,给每个元素赋值
  for (int i = 0; i < n; i++)
    pArr[i] = i;
  return pArr;
}
int main()
{
  // 数组长度为n,n初始化为10
  int n = 10;
  // 获取数组首元素指针
  int* p = func(n);
  if (p != NULL)
  {
    // 通过首元素指针偏移访问所有数组元素
    for (int i = 0; i < n; i++)
      printf("%d ", p[i]);
    // 使用完记得释放
    free(p);
  }
  return 0;
}

函数func中,申请了sizeof(int) * n字节内存空间。若申请失败,此时pArrNULL,则直接返回pArr若申请成功,给每个元素赋值后,将数组首元素指针pArr返回。

函数main中,调用函数func获得返回的int *类型的指针p,它指向一个int类型数组的首元素。由于不能保证func函数返回的指针一定有效,这里也要做指针判空。若指针不为空,才可以使用它。通过首元素指针偏移可以访问所有数组元素。但是,首元素指针最多向后偏移9个元素。若继续向后偏移,将导致越界访问。最后,别忘了将首元素指针传入free函数释放内存空间。

目录
相关文章
|
人工智能 大数据
1+X大数据分析与应用中级备考经验
作为上海城建职业学院人工智能应用学院大数据技术专业的专任教师,在2020年12月份指导学生参与阿里巴巴《1+X大数据分析与应用中级》考试中,取得了优异的成绩。34位参加考证的同学,共有27个同学通过了考试,再次分享备考经验,以供参考。
2773 2
1+X大数据分析与应用中级备考经验
|
11月前
|
人工智能 算法 安全
探索人工智能在医疗诊断中的应用及挑战
本文深入探讨了人工智能在医疗诊断领域的现状、应用及其面临的伦理和技术挑战。通过分析AI技术如何辅助医生进行疾病诊断,提高诊断的准确性和效率,文章揭示了AI在医疗影像分析、基因检测、风险评估等方面的潜力。同时,指出了数据隐私、算法透明度、医患关系变化等挑战,并对未来AI与医疗健康的融合趋势进行了展望。
408 1
|
9月前
|
SQL 安全 Serverless
活动实践 | 基于EMR StarRocks实现游戏玩家画像和行为分析
基于阿里云EMR Serverless StarRocks,利用其物化视图和DLF读写Paimon等能力,构建游戏玩家画像和行为分析平台。通过收集、处理玩家行为日志,最终以报表形式展示分析结果,帮助业务人员决策。
|
10月前
|
监控 API 云计算
云计算成本优化:AWS Cost Explorer与预算管理的艺术
【10月更文挑战第27天】在云计算中,成本管理至关重要。本文介绍如何使用AWS Cost Explorer进行成本优化和预算管理。通过案例分析,展示如何创建自定义报告、发现成本动因、检测异常,并创建预算来监控和控制成本。此外,还提供了Python示例代码,帮助用户自动化预算创建过程。
220 4
|
11月前
|
Java
Java 中锁的主要类型
【10月更文挑战第10天】
267 59
|
10月前
|
消息中间件 NoSQL Redis
【赵渝强老师】Redis的消息发布与订阅
本文介绍了Redis实现消息队列的两种场景:发布者订阅者模式和生产者消费者模式。其中,发布者订阅者模式通过channel频道进行解耦,订阅者监听特定channel的消息,当发布者向该channel发送消息时,所有订阅者都能接收到消息。文章还提供了相关操作命令及示例代码,展示了如何使用Redis实现消息的发布与订阅。
288 0
|
DataWorks 关系型数据库 MySQL
DataWorks产品使用合集之mysql节点如何插入数据
DataWorks作为一站式的数据开发与治理平台,提供了从数据采集、清洗、开发、调度、服务化、质量监控到安全管理的全套解决方案,帮助企业构建高效、规范、安全的大数据处理体系。以下是对DataWorks产品使用合集的概述,涵盖数据处理的各个环节。
192 1
|
算法 安全 关系型数据库
深度|庖丁解InnoDB之Buffer Pool
聚焦在Buffer Pool的本职功能上,从其提供的接口、内存组织方式、Page获取、刷脏等方面进行介绍
105304 90
|
缓存 前端开发 Java
浅析JVM invokedynamic指令与Java Lambda语法
【8月更文挑战第27天】在Java的演进历程中,invokedynamic指令的引入和Lambda表达式的出现无疑是两大重要里程碑。它们不仅深刻改变了Java的开发模式和性能表现,还极大地推动了Java在函数式编程和动态语言支持方面的进步。本文将从技术角度浅析JVM中的invokedynamic指令及其与Java Lambda语法的紧密联系。
178 0
|
NoSQL 应用服务中间件 Linux
CentOS7搭建MySQL+Redis+MongoDB+FastDF
CentOS7搭建MySQL+Redis+MongoDB+FastDF
301 0