【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释放,这会造成严重的内存泄漏。导致这部分内存无法再被其他程序使用。可能会导致程序的内存消耗不断增加,最终导致程序崩溃或者系统变得不稳定。

总结

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


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


目录
相关文章
|
5月前
|
存储 程序员 编译器
玩转C++内存管理:从新手到高手的必备指南
C++中的内存管理是编写高效、可靠程序的关键所在。C++不仅继承了C语言的内存管理方式,还增加了面向对象的内存分配机制,使得内存管理既有灵活性,也更加复杂。学习内存管理不仅有助于提升程序效率,还有助于理解计算机的工作原理和资源分配策略。
|
1月前
|
C语言 C++
c与c++的内存管理
再比如还有这样的分组: 这种分组是最正确的给出内存四个分区名字:栈区、堆区、全局区(俗话也叫静态变量区)、代码区(也叫代码段)(代码段又分很多种,比如常量区)当然也会看到别的定义如:两者都正确,记那个都选,我选择的是第一个。再比如还有这样的分组: 这种分组是最正确的答案分别是 C C C A A A A A D A B。
26 1
|
4月前
|
存储 Linux C语言
C++/C的内存管理
本文主要讲解C++/C中的程序区域划分与内存管理方式。首先介绍程序区域,包括栈(存储局部变量等,向下增长)、堆(动态内存分配,向上分配)、数据段(存储静态和全局变量)及代码段(存放可执行代码)。接着探讨C++内存管理,new/delete操作符相比C语言的malloc/free更强大,支持对象构造与析构。还深入解析了new/delete的实现原理、定位new表达式以及二者与malloc/free的区别。最后附上一句鸡汤激励大家行动缓解焦虑。
|
7月前
|
存储 缓存 编译器
【硬核】C++11并发:内存模型和原子类型
本文从C++11并发编程中的关键概念——内存模型与原子类型入手,结合详尽的代码示例,抽丝剥茧地介绍了如何实现无锁化并发的性能优化。
308 68
|
5月前
|
安全 C语言 C++
彻底摘明白 C++ 的动态内存分配原理
大家好,我是V哥。C++的动态内存分配允许程序在运行时请求和释放内存,主要通过`new`/`delete`(用于对象)及`malloc`/`calloc`/`realloc`/`free`(继承自C语言)实现。`new`分配并初始化对象内存,`delete`释放并调用析构函数;而`malloc`等函数仅处理裸内存,不涉及构造与析构。掌握这些可有效管理内存,避免泄漏和悬空指针问题。智能指针如`std::unique_ptr`和`std::shared_ptr`能自动管理内存,确保异常安全。关注威哥爱编程,了解更多全栈开发技巧。 先赞再看后评论,腰缠万贯财进门。
258 0
|
7月前
|
存储 编译器 程序员
【C语言】内存布局大揭秘 ! -《堆、栈和你从未听说过的内存角落》
在C语言中,内存布局是程序运行时非常重要的概念。内存布局直接影响程序的性能、稳定性和安全性。理解C程序的内存布局,有助于编写更高效和可靠的代码。本文将详细介绍C程序的内存布局,包括代码段、数据段、堆、栈等部分,并提供相关的示例和应用。
214 5
【C语言】内存布局大揭秘 ! -《堆、栈和你从未听说过的内存角落》
|
6月前
|
存储 程序员 编译器
什么是内存泄漏?C++中如何检测和解决?
大家好,我是V哥。内存泄露是编程中的常见问题,可能导致程序崩溃。特别是在金三银四跳槽季,面试官常问此问题。本文将探讨内存泄露的定义、危害、检测方法及解决策略,帮助你掌握这一关键知识点。通过学习如何正确管理内存、使用智能指针和RAII原则,避免内存泄露,提升代码健壮性。同时,了解常见的内存泄露场景,如忘记释放内存、异常处理不当等,确保在面试中不被秒杀。最后,预祝大家新的一年工作顺利,涨薪多多!关注威哥爱编程,一起成为更好的程序员。
176 0
|
7月前
|
存储 缓存 算法
【C语言】内存管理函数详细讲解
在C语言编程中,内存管理是至关重要的。动态内存分配函数允许程序在运行时请求和释放内存,这对于处理不确定大小的数据结构至关重要。以下是C语言内存管理函数的详细讲解,包括每个函数的功能、标准格式、示例代码、代码解释及其输出。
213 6
|
8月前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
196 1
|
7月前
|
算法 编译器 C语言
【C语言】C++ 和 C 的优缺点是什么?
C 和 C++ 是两种强大的编程语言,各有其优缺点。C 语言以其高效性、底层控制和简洁性广泛应用于系统编程和嵌入式系统。C++ 在 C 语言的基础上引入了面向对象编程、模板编程和丰富的标准库,使其适合开发大型、复杂的软件系统。 在选择使用 C 还是 C++ 时,开发者需要根据项目的需求、语言的特性以及团队的技术栈来做出决策。无论是 C 语言还是 C++,了解其优缺点和适用场景能够帮助开发者在实际开发中做出更明智的选择,从而更好地应对挑战,实现项目目标。
282 0