【C】动态内存函数@动态内存管理 —— malloc | free | calloc | realloc

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 动态内存函数

@TOC

:key: 引:为什么存在动态内存分配?

我们已经掌握的内存开辟方式有:

int a = 0;//4byte
int arr[10] = { 0 };//40byte 

这样的开辟方式---

  1. 开辟空间大小是固定的,想大不能大,想小不能小;
  2. 定义数组时,必须给定大小,然而有时我们需要的空间大小在编译时才会知道。

在这里插入图片描述
由此引入动态内存开辟,这就要学习动态内存函数

正文开始@一个人的乐队

1. 动态内存函数的介绍

1.1 malloc和free

1.1.1 malloc

C语言提供了一个动态内存开辟的函数:(头文件:#include<stdlib.h>

void* malloc (size_t size);
  • void*:这块内存是为谁申请的也不知道,返回什么类型也不合适,那就返回通用类型
  • size:要申请的字节数

作为malloc函数的使用者,我很清楚我申请的内存空间要来做什么,在使用时要做强制类型转换:

int* ptr = (int*)malloc(10 * sizeof(int));

:yellow_heart:功能:在堆区上申请size个字节的空间,并返回堆区上的起始地址

  • 若开辟成功,返回一个指向开辟好空间的指针;
  • 若开辟失败,则返回空指针NULL

:snowflake:因此,malloc的返回值一定一定要做检查!!!

1.1.2 搭配使用的free

C语言提供了另外一个函数用于做动态内存释放和回收的:
(头文件:#include<stdlib.h>)

void free(void* ptr);

:yellow_heart:功能:把ptr所指向的空间还给操作系统

注:

:snowflake:1. 若参数 ptr指向的空间 不是动态开辟的,则 free函数的行为是 标准未定义的。
:snowflake:2. 若参数 ptrNULL空指针,即 free(NULL); okay,只不过 什么都不干

上代码感受它们的使用:
思考:free(ptr);后的ptr == NULL;是否有必要?

#include<stdio.h>
#include<stdlib.h>

int main()
{
    //申请空间
    int* p = (int*)malloc(10 * sizeof(int));
    if (p == NULL)
    {
        return -1;
    }
    //使用
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        *(p + i) = i;
    }
    //释放ptr指向的这段内存
    free(p);
    //p = NULL;//是否有必要?
    return 0;
}

调试起来:
在这里插入图片描述
可以看到ptr指向的这段空间虽然释放了,但ptr依然指向这段空间(也就是说free根本就不会使ptr发生改变,也不会将ptr主动置空)。
在这里插入图片描述

此时ptr是野指针,很危险,就一定要ptr == NULL;,让ptr永远也找不到这段空间。

:snowflake:3. 释放 ptr指向的内存空间后 free(ptr);,一定要 ptr置空 ptr==NULL

1.2 calloc

C语言还提供了一个calloc函数,也可用来动态内存分配:
(头文件:#include<stdlib.h>)

void* calloc (size_t num, size_t size);
  • void*:空类型,为了适应各种指针类型,以便正确解引用
  • num:要申请的元素个数

    • size:元素大小

:yellow_heart:功能:为大小为sizenum个元素开辟一块空间,并将每一个字节都初始化为0.

  • 若开辟成功,则返回开辟好空间的起始地址
  • 若开辟失败,则返回空指针NULL

:snowflake:因此,与malloc一样,对于calloc的返回值也一定一定要做检查!!!

上代码感受:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>

int main()
{
    //申请10个int的空间
    int* p = (int*)calloc(10, sizeof(int));
    if (p == NULL)
    {
        printf("%s", strerror(errno));//错误码翻译出错误信息
        return -1;
    }
    //申请成功
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        printf("%d ", *(p + i));//打印
    }
    //释放
    free(p);
    p = NULL;
    return 0;
}

运行+调试结果:
在这里插入图片描述
演示申请空间失败的情况,申请空间过大:
在这里插入图片描述

对比malloc和calloc:

:snowflake: malloc:只负责在堆区申请空间,并返回起始地址, 不会初始化空间
:snowflake: calloc:在堆区申请空间, 初始化为0,并返回起始地址

以后也很简单,我要初始化我就用calloc,不想初始化我就用malloc.

1.3 realloc

有时我们发现过去申请的内存太小/过大了,为了合理使用内存就一定要对内存大小进行调整。
realloc函数的出现,让动态内存管理更加灵活.(头文件:#include<stdio.h>)

void* realloc (void* ptr, size_t size);
  • ptr:要调整的内存地址
  • size:调整后的新大小
  • void*:调整后的内存起始地址

:snowflake:realloc在调整内存空间时存在两种情况:

:purple_heart:情况1:原空间后有足够大的空间
在这里插入图片描述
:purple_heart:情况2:原空间后没有足够大的空间
在这里插入图片描述

:yellow_heart:这个函数在调整原内存空间大小的同时,还会将原内存中的数据移动到新的空间中去。

上代码感受:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>

int main()
{
    int* p= (int*)malloc(10 * sizeof(int));
    if (p == NULL)
    {
        printf("%s\n", strerror(errno));
        return -1;
    }
    //申请成功
    //使用
    //...
    //空间不够,增加空间至20int
    int* ptr = (int*)realloc(p, 20 * sizeof(int));//用新指针来接收
    if (ptr == NULL)
    {
        return -1;
    }
    else
    {
        p = ptr;
    }
    
    int i = 0;
    for (i = 0; i < 20; i++)
    {
        *(p + i) = i;
    }
    //打印
    for (i = 0; i < 20; i++)
    {
        printf("%d ", *(p + i));
    }
    
    //释放
    free(p);
    p = NULL;
    return 0;
}

:heart:注: 为什么这里一定要用新指针来接收:

//空间不够,增加空间至20int
int* ptr = (int*)realloc(p, 20 * sizeof(int));//用新指针来接收
//若仍用旧指针p来接收
int* p = (int*)realloc(p, 20 * sizeof(int));

不可,因为,若内存申请失败返回NULL,则原来p指向的空间也找不到了。

2. 常见动态内存错误

下面再次强化常见动态内存分配的错误点,以后都要避免奥。

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

#include<stdio.h>
#include<stdlib.h>

void test()
{
    int* p = (int*)malloc(10 * sizeof(int));
    if (p == NULL)
    {
        return ;
    }
    //使用
    int i = 0;
    for (i = 0; i <= 10; i++)
    {
        *(p + i) = i;
    }
    //释放
    free(p);
    p = NULL;
}
int main()
{
    test();
    return 0;
}

运行结果:挂了:cold_sweat:
在这里插入图片描述

2.2 对NULL指针解引用

void test()
{
     int *p = (int *)malloc(INT_MAX/4);
     *p = 20;
     free(p);
}

直接这样写代码是有风险的,若malloc申请内存空间失败,则返回NULL*p = 20;则是对空指针解引用。
这就要求我们在申请空间后一定要进行判断,先判断,再使用。

:white_check_mark: 更正--- 在申请空间后 一定要进行判断
void test()
{
     int *p = (int *)malloc(INT_MAX/4);
     if(p == NULL)
     {
         return;
     }
     *p = 20;
     free(p);
}

2.3 对非动态开辟的内存用free释放

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

运行结果 --- 又挂了:cold_sweat:
在这里插入图片描述

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

上代码:

#include<stdio.h>
#include<stdlib.h>

int main()
{
    int* p = (int*)malloc(10 * sizeof(int));
    if (p == NULL)
    {
        return -1;
    }
    //使用
    int i = 0;
    for (i = 0; i < 5; i++)
    {
        *p++ = i;
    }
    //释放
    free(p);
    p = NULL;
    return 0;
}

运行结果---又挂了:cold_sweat:
在这里插入图片描述
在这里插入图片描述

:white_check_mark:传给 free的,必须是动态开辟 内存的起始位置

2.5 对同一块内存多次释放

#include<stdio.h>
#include<stdlib.h>

int main()
{
    int* p = (int*)malloc(10 * sizeof(int));
    free(p);
    free(p);//重复释放
    return 0;
}

运行结果---又挂了:cold_sweat:
在这里插入图片描述

:white_check_mark:更正:在 free释放 p指向的空间后,记得 p置空 p = NULL;
#include<stdio.h>
#include<stdlib.h>

int main()
{
    int* p = (int*)malloc(10 * sizeof(int));
    free(p);
    p = NULL;
    free(p);//p为NULL,okay,只不过什么也不做
    p = NULL;
    return 0;
}

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

在堆区上申请的动态内存空间有两种回收方式:

  1. 主动free
  2. 当程序退出时,申请的空间也会回收

思考:

#include<stdio.h>
#include<stdlib.h>

int main()
{
    int* p = (int*)malloc(10 * sizeof(int));
    if (p == NULL)
    {
        return -1;
    }
    //使用
    //忘记释放了
    getchar();
    return 0;
}

运行起来,光标闪烁,getchar();等待接收字符,程序不结束,如果这段空间不被释放,造成了内存泄漏。

忘记释放不再使用的动态开辟的空间会造成内存泄漏---这块空间你又不用你又不释放,别人也无法使用,如果服务器7*24小时的在跑,那非常可怕,内存会被一点一点耗干。

:white_check_mark:动态开辟的内存空间, 一定要释放,且要正确释放。

3. 经典笔试题

3.1 笔试题1

上代码思考:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

void GetMemory(char *p) 
{
    p = (char *)malloc(100);
}
void Test(void) 
{
    char *str = NULL;
    GetMemory(str);
    strcpy(str, "hello world");
    printf(str);//运行结果?
}
int main()
{
    Test();
    return 0;
}

运行结果---挂了:cold_sweat:
在这里插入图片描述

:key:解析:

在这里插入图片描述

:fish:1°程序崩溃

str传给p值传递pstr的一份临时拷贝,所以当malloc在堆上开辟的空间起始地址放在p中(即p指向了这段空间时),不会影响strstr仍为NULL
②当str为空时,strcpy想把"hello world"拷贝到str所指向空间时,程序崩溃,因为NULL指向的空间是不能直接访问的(还记得strcpy模拟实现的时候进去就要断言吗)。

:fish:2°内存泄漏

更可怕的是想释放都没办法释放,函数调用完,p随之销毁(即malloc开辟出来的空间的起始地址丢失),这段空间无法找到,无从释放。

:white_check_mark: 更正: 传址调用
void GetMemory(char** p) 
{
    *p = (char *)malloc(100);
}
void Test(void) 
{
    char *str = NULL;
    GetMemory(&str);
    strcpy(str, "hello world");
    printf(str);//运行结果?

    //释放
    free(str);
    str = NULL;
}

运行结果:
在这里插入图片描述
:heart:注:
这里大家可能有疑惑在printf(str);

char* p = "hello world";
printf("hello world");
printf(p);//okay!

3.2 笔试题2

上代码思考:

#include<stdio.h>
#include<stdlib.h>

char* GetMemory(void) 
{
    char p[] = "hello world";
    return p;
}
void Test(void) 
{
    char *str = NULL;
    str = GetMemory();
    printf(str);//运行结果?
}
int main()
{
    Test();
    return 0;
}

运行结果 --- 打印随机值:cold_sweat:
在这里插入图片描述

:key:解析 --- 事实上,这都属于 返回栈空间地址引发的问题

在这里插入图片描述
像这样返回栈空间地址就是典型的造成野指针的原因之一。

与之相似的错误代码演示:
#include<stdio.h>

int* test()
{
    int n = 10;
    return &n;
}

int main()
{
    int* p = test();
    printf("%d\n", *p);
    return 0;
}

预测运行结果:可能打印随机值。

运行结果---虽然报了一个警报,但还是打印了10
在这里插入图片描述
这可能会让你震惊,这里打印10也并非巧合,如果还没调用其他函数,销毁的空间未被覆盖掉,确实可能访问到10.

我当然也可以让它不是10,这里随便打印一个"hehe":

int main()
{
    int* p = test();
    printf("hehe\n");
    printf("%d\n", *p);//运行结果?
    return 0;
}

再来运行,运行结果--- 随机值
在这里插入图片描述
解析---画图简述函数栈桢创建与销毁:
在这里插入图片描述
这些讨论,都是为了说明返回栈空间地址时的这种错误。

3.3 笔试题3

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

void GetMemory(char **p, int num) 
{
    *p = (char *)malloc(num);
}
void Test(void) 
{
    char *str = NULL;
    GetMemory(&str, 100);
    strcpy(str, "hello");
    printf(str);
}
int main()
{
    Test();
    return 0;
}

在这里插入图片描述

问题:内存泄漏!!

:white_check_mark:要记得释放空间:
free(str);
str = NULL;

3.4 笔试题4

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

void Test(void) 
{
    char *str = (char *)malloc(100);
    strcpy(str, "hello");
    free(str);
    if (str != NULL)
    {
        strcpy(str, "world");
        printf(str);//运行结果?
    }
}

int main()
{
    Test();
    return 0;
}

有结果也不对,要明白编译器是不能查出所有错误的,不然要你程序猿干嘛。
在这里插入图片描述

:key:解析

在这里插入图片描述
因此,还是要记得free后,将str置空。

free(str);
str = NULL;

4. C/C++程序的内存开辟

在这里插入图片描述
C/C++程序内存分配的几个区域:

  1. ==栈区(stack)==:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行 结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限,因此会有栈溢出的情况。

栈区主要存放运行函数而分配的局部变量函数参数、返回数据、 返回地址等。

  1. ==堆区(heap)==:一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS(操作系统)回收 。分配方式类似于链表。
  2. ==数据段/静态区(static)==:存放全局变量静态数据程序结束后由系统释放
  3. ==代码段==:存放函数体(类成员函数和全局函数)的二进制代码。

有了这幅图,我们就可以更好的理解static关键字修饰局部变量的例子了。

实际上普通的局部变量是在栈区分配空间的, 栈区的特点是在上面创建的变量 出了作用域就销毁
但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁,所以 生命周期变长

下篇文章将介绍柔型数组。
未完待续

相关文章
|
3天前
|
监控 Java 大数据
【Java内存管理新突破】JDK 22:细粒度内存管理API,精准控制每一块内存!
【9月更文挑战第9天】虽然目前JDK 22的确切内容尚未公布,但我们可以根据Java语言的发展趋势和社区的需求,预测细粒度内存管理API可能成为未来Java内存管理领域的新突破。这套API将为开发者提供前所未有的内存控制能力,助力Java应用在更多领域发挥更大作用。我们期待JDK 22的发布,期待Java语言在内存管理领域的持续创新和发展。
|
2天前
|
存储 并行计算 算法
CUDA统一内存:简化GPU编程的内存管理
在GPU编程中,内存管理是关键挑战之一。NVIDIA CUDA 6.0引入了统一内存,简化了CPU与GPU之间的数据传输。统一内存允许在单个地址空间内分配可被两者访问的内存,自动迁移数据,从而简化内存管理、提高性能并增强代码可扩展性。本文将详细介绍统一内存的工作原理、优势及其使用方法,帮助开发者更高效地开发CUDA应用程序。
|
30天前
|
存储 Java 程序员
JVM自动内存管理之运行时内存区
这篇文章详细解释了JVM运行时数据区的各个组成部分及其作用,有助于理解Java程序运行时的内存布局和管理机制。
JVM自动内存管理之运行时内存区
|
21天前
|
程序员 C++
malloc与free的内存管理奥秘:技术分享
【8月更文挑战第22天】在软件开发过程中,内存管理是一个至关重要的环节。特别是在使用C或C++这类语言时,程序员需要手动管理内存的分配与释放。malloc和free函数是这一过程中的核心工具。本文将深入探讨malloc如何分配内存,以及free如何知道释放多少内存,帮助你在工作学习中更好地掌握这一技术干货。
44 4
|
19天前
|
Linux 测试技术 C++
内存管理优化:内存泄漏检测与预防。
内存管理优化:内存泄漏检测与预防。
30 2
|
1月前
|
存储 算法 安全
实现一个malloc内存分配器
需要注意的是,这个例子是一个非常简单的实现,它只是为了演示原理,对于一个强壮的、用于生产环境的内存分配器来说还远远不够。一个成熟的分配器将考虑到多线程的同步、更复杂的内存分配算法、碎片整理策略、错误处理,以及安全性问题,比如防止缓冲区溢出。在实际应用程序中,你应该使用标准库提供的或操作系统提供的内存分配器,除非你确实需要并且能够处理自己实现内存分配器所带来的复杂性。
28 3
|
2月前
|
开发者 Java
JVM内存问题之top命令的物理内存信息中,'used'和'free','avail Mem'分别表示什么
JVM内存问题之top命令的物理内存信息中,'used'和'free','avail Mem'分别表示什么
|
2月前
|
设计模式 SQL 安全
Java面试题:设计一个线程安全的内存管理器,使用观察者模式来通知所有线程内存使用情况的变化。如何确保在添加和移除内存块时的线程安全?如何确保任务的顺序执行和调度器的线程安全?
Java面试题:设计一个线程安全的内存管理器,使用观察者模式来通知所有线程内存使用情况的变化。如何确保在添加和移除内存块时的线程安全?如何确保任务的顺序执行和调度器的线程安全?
23 0
|
2月前
|
并行计算 安全 算法
Java面试题:Java内存管理与多线程并发处理,设计一个Java应用,该应用需要处理大量并发用户请求,同时要求对内存使用进行优化,如何通过垃圾回收机制优化内存使用?
Java面试题:Java内存管理与多线程并发处理,设计一个Java应用,该应用需要处理大量并发用户请求,同时要求对内存使用进行优化,如何通过垃圾回收机制优化内存使用?
29 0
|
2月前
|
Java 开发者
Java面试题:Java内存管理精要与多线程协同策略,Java内存管理:堆内存、栈内存、方法区、垃圾收集机制等,多线程编程的掌握,包括线程创建、同步机制的原理
Java面试题:Java内存管理精要与多线程协同策略,Java内存管理:堆内存、栈内存、方法区、垃圾收集机制等,多线程编程的掌握,包括线程创建、同步机制的原理
26 0

热门文章

最新文章