【C语言进阶】那些你必须掌握的C/C++要点——动态内存管理(1)

简介: 【C语言进阶】那些你必须掌握的C/C++要点——动态内存管理(1)

前言

  • 其实如果你想把这部分内容学好,掌握以下四个函数的使用方法就行
  • 下面我们来依次介绍这几个函数

一.为什么要动态内存分配

  • 在之前我们已经学会了这种开辟内存的方法:
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间

但是上述的开辟空间的方式有两个不那么好的地方:

1. 空间开辟大小是固定的。

2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

但是对于空间的需求,。有时候我们需要的空间大小在程序运行的时候才能知道,

  • 这样就经常会导致我们在栈空间上开辟的空间太大了或者太小了,显然这种开辟空间的方式不太能满足我们的需求

二. malloc与free

  • 我们要知道的是,当你开辟了一块空间不再使用时,就必须把它free释放掉还给操作系统,因此,这一块我们合并到一起来讲

1.malloc

  • C语言提供了一个动态内存开辟的函数:
void* malloc (size_t size);
  • 这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
    如果开辟成功,则返回一个指向开辟好空间的指针。
    如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  • 如果参数 size 为0,也就是开辟一块空间大小为0的空间,malloc的这种行为是标准是未定义的,取决于编译器。

2.free

  • C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:
void free (void* ptr);
  • free函数用来释放动态开辟的内存。
    如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
    如果参数 ptr 是NULL指针,则函数什么事都不做。
  • malloc和free都声明在 stdlib.h 头文件中。
  • 好了,我们来结合具体例子实际来看下效果
#include <stdio.h>
#include <stdlib.h>
 int main()
{
      //int arr[10];
      int* p = (int*)malloc(40);//开辟40个字节的整形空间,把返回的开辟好空间的起始地址保存在p中
      if (p == NULL)//判断malloc开辟内存是否成功
      {
        perror("malloc");//如果没成功,通过perror来报错
        return 1;
      }
      //开辟成功
      int i = 0;
      for (i = 0; i < 10; i++)
      {
        printf("%d\n", *(p + i));//打印一下此时p中空间存储的内容
      }
      free(p);//用完后释放空间,把开辟的空间返回给操作系统
      p = NULL;//将p置空
      return 0;
 }

除了注释中提到的点,还有以下几个值得注意的问题:

  • 1.与之后讲的calloc不同,malloc申请到空间后直接返回这片空间的起始地址,不会初始化空间的内容,所以结果打印的随机值是正常现象

2.关于用free释放,如果你每次开辟空间在使用完后都不释放,这是典型的内存泄露。操作系统的内存空间是有限的,早晚你的操作系统的空间就会被这些已经无用的内容给占满,因此一定记得开辟完空间后在不用时使用free释放

3.关于把p置为空指针这点,有些初学者可能会质疑这部分的必要性,其实这是非常有必要的,当我们用free把开辟的空间释放后,这片空间已经不属于你的p了,如果你的p依然指向这块空间,毫无疑问此时的p已经变成了野指针,是非常危险的,因此非常有必要!

三.calloc

C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。原型如下:

void* calloc (size_t num, size_t size);
  • 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
  • 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
    举个例子:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p = (int*)calloc(10, sizeof(int));
if(NULL != p)
{
//使用空间
}
free(p);
p = NULL;
return 0;
}

  • 通过calloc的特点我们不难看出,calloc在某些需要我们对申请的内存空间的内容要求初始化时能发挥极大的作用

四.realloc

  • realloc函数的出现让动态内存管理更加灵活。

有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们需要对内存的大小做灵活的调整。那realloc 函数就可以做到对动态开辟内存大小的调整。

  • 函数原型如下:
void* realloc (void* ptr, size_t size);
  • ptr 是要调整的内存地址
  • size 调整之后新大小
  • 返回值为调整之后的内存起始位置。
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
  • realloc在调整内存空间的是存在以下两种情况:

1.原地扩容

  • 这种情况适用于原有空间之后有足够大的空间
  • 此时我们只需在原有空间的后面扩容,返回扩容后的空间即可

扩展内存直接在原有内存之后直接追加空间,原来空间的数据不发生变化。

2.异地扩容

  • 当我们原有空间后面的空间不够时,realloc就会进行异地扩容
  • 扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址

  • 由于realloc扩容时存在两种情况,因此我们要注意以下错误
#include <stdio.h>
int main()
{
    int* ptr = (int*)malloc(100);
    if (ptr != NULL)
    {
        //业务处理
    }
    else
    {
        exit(EXIT_FAILURE);
    }
    //扩展容量
    //代码1
    ptr = (int*)realloc(ptr, 1000);//这样可以吗?(如果申请失败会如何?)
    //代码2
    int* p = NULL;
    p = realloc(ptr, 1000);
    if (p != NULL)
    {
        ptr = p;
    }
    //业务处理
    free(ptr);
    return 0;
}
  • 我们来对比一下以上的两段代码
  • 我们发现在代码1中如果直接把realloc的值赋给我们的ptr,如果是异地开辟的话,我们的ptr就会指向新的内存的起始地址,当此时realloc开辟失败的话,由于ptr指向发生了变化,我们就找不到之前的内存空间了,因此在使用realloc时,我们需要像代码2一样先判断一下开辟的新空间是否成功,当开辟成功时,再把realloc赋给我们的ptr。

三.常见的内存错误

1.对NULL指针的解引用操作

void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
  • 如果我们malloc开辟内存失败的话,我们的p中存放的就是NULL的地址,在C语言中对NULL解引用是一种标准未定义行为,会直接导致程序报错!

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

void test()
{
    int i = 0;
    int* p = (int*)malloc(10 * sizeof(int));
    if (NULL == p)
    {
        exit(EXIT_FAILURE);
    }
    for (i = 0; i <= 10; i++)
    {
        *(p + i) = i;//当i是10的时候越界访问
    }
    free(p);
}
  • 非常常见的错误,我们一共就开辟了10个int型的空间,程序却走向了第11个,明显的错误。

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

void test()
{
int a = 10;
int *p = &a;
free(p);//ok?
}
  • 我们的p都不是动态开辟的,两个不是一个概念,不需要free。

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

void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
  • 注意我们的free是不能一块一块的释放动态开辟的空间的,要释放就要释放全部动态开辟的内存。如果你想释放一部分内存,你需要重新分配一个新的内存块,并将需要保留的数据复制到新的内存块中,然后再使用函数释放原始的内存块。如下代码
#include <stdlib.h>
#include <string.h>
int main() {
    // 分配动态内存
    char* ptr = malloc(10);
    // 检查内存是否成功分配
    if (ptr == NULL) {
        // 处理内存分配失败的情况
        return 1;
    }
    // 复制数据到新的内存块
    char* newPtr = malloc(5);
    memcpy(newPtr, ptr, 5);
    // 释放原始的内存块
    free(ptr);
    // 使用新的内存块进行操作
    // 释放新的内存块
    free(newPtr);
    return 0;
}

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

void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
  • 对已经释放的空间重复释放是毫无意义的,注意不要多写

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

void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
  • 在使用完动态开辟的空间后忘记使用free释放,这会造成严重的内存泄漏。导致这部分内存无法再被其他程序使用。可能会导致程序的内存消耗不断增加,最终导致程序崩溃或者系统变得不稳定。

总结

今天的内容到这里就结束了,动态内存管理的基本知识点都在这里了,如果你能把上面的内容都学会的话,那么你就掌握了动态内存管理中的大部分内容,之后我们在为大家讲解几个有关的面试题加深大家的理解并且在介绍一下有关柔性数组的知识。


好了,如果你有任何疑问欢迎在评论区或者私信我提出,大家下次再见啦!


目录
相关文章
TU^
|
2天前
|
C语言
C语言内存函数和字符串函数模拟实现
C语言内存函数和字符串函数模拟实现
TU^
8 0
|
3天前
|
存储 程序员 C++
C++堆内存分配
C++堆内存分配
10 2
|
1天前
|
存储 安全 程序员
C++语言中的内存管理技术
C++语言中的内存管理技术
|
1天前
|
存储 程序员 编译器
C语言变量声明内存分配(转载)
C语言变量声明内存分配(转载)
7 0
|
1天前
|
存储 安全 编译器
【C语言】动态内存管理 -- -- 深入了解malloc、calloc、realloc、free、柔性数组(万字深入了解)
【C语言】动态内存管理 -- -- 深入了解malloc、calloc、realloc、free、柔性数组(万字深入了解)
9 0
【C语言】动态内存管理 -- -- 深入了解malloc、calloc、realloc、free、柔性数组(万字深入了解)
|
1天前
|
存储 缓存 程序员
C++内存管理:避免内存泄漏与性能优化的策略
C++内存管理涉及程序稳定性、可靠性和性能。理解堆和栈的区别至关重要,其中堆内存需手动分配和释放。避免内存泄漏的策略包括及时释放内存、使用智能指针和避免野指针。性能优化策略则包括减少内存分配、选用合适数据结构、避免深拷贝及缓存常用数据。通过这些最佳实践,可提升C++程序的效率和质量。
TU^
|
2天前
|
存储 C语言
C语言浮点数在内存中的存储
在C语言中,浮点数类型用float和double表示。float类型使用4个字节(32位),而double类型使用8个字节(64位)。浮点数表示的范围:float.h中定义
TU^
7 0
|
2天前
|
C语言
c语言内存函数
c语言内存函数
10 0
|
3天前
|
C语言
c语言:字符串和内存函数介绍-2
c语言:字符串和内存函数介绍
6 0
|
3天前
|
C语言
c语言:字符串和内存函数介绍-1
c语言:字符串和内存函数介绍
10 0