<C语言> 动态内存管理

news2024/9/28 23:26:11

1.动态内存函数

为什么存在动态内存分配?

int main(){
    int num = 10;  //向栈空间申请4个字节
    int arr[10];   //向栈空间申请了40个字节
    return 0;
}

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

  1. 空间开辟大小是固定的。
  2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。这时候就需要动态内存开辟了。

1.1 malloc和free

malloc()是用于在程序执行期间动态分配内存。它的全称是"memory allocation",意为内存分配。malloc()函数是C标准库的一部分,它的声明在stdlib.h头文件中。

函数原型如下:

void* malloc(size_t size);

在这里,size是你想要分配的字节数,函数返回一个指向分配的内存块起始地址的指针。malloc()函数的返回类型是void*,这意味着返回的指针可以赋值给任何指针类型而无需显式转换。

下面简要解释一下malloc()的工作原理:

1.你提供想要分配的字节数,malloc()在堆内存中搜索一个足够大的连续内存块来存储这些字节。

2.如果找到了合适的内存块,它将其标记为已使用,并返回该内存块的起始地址的指针。

3.如果找不到足够大的内存块,它将返回一个NULL指针,表示内存分配失败。

注意:使用malloc()分配的内存需要使用free()函数显式地释放,否则会导致内存泄漏。

void free(void* ptr);

free()函数接受之前分配的内存块的指针,并将其释放,使其可供将来的动态分配使用。如果忘记释放之前分配的内存,程序每次运行分配代码时都会消耗更多内存,最终可能导致内存耗尽。

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

int main() {
    int n = 5;
    int* dynamicArray = (int*)malloc(n * sizeof(int));

    if (dynamicArray == NULL) {
        printf("内存分配失败!\n");
    } else {
        // 使用分配的内存块
        for (int i = 0; i < n; i++) {
            dynamicArray[i] = i + 1;
        }

        // 当不再需要分配的内存时,记得释放它
        free(dynamicArray);
        dynamicArray = NULL}

    return 0;
}

在调用free()函数释放动态分配的内存后,将指针dynamicArray设置为NULL是一个良好的习惯,但不是必须的。

设置指针为NULL的优点:

  1. 避免悬挂指针(Dangling Pointer):如果在释放内存后不将指针设置为NULL,该指针将仍然保留先前的地址。如果你在后续代码中继续使用该指针,可能会导致悬挂指针,即指针指向的内存已经被释放,这可能导致程序崩溃或产生难以调试的错误。将指针设置为NULL可以帮助你避免这种情况,因为如果尝试使用空指针,程序将产生明确的错误(空指针解引用)。
  2. 避免重复释放:在释放内存后,如果将指针设置为NULL,你可以通过检查指针是否为NULL来确定是否已经释放了内存。如果你在后续代码中错误地再次调用free(),会导致未定义的行为。

如果你在后续代码中小心地避免悬挂指针和重复释放内存,那么不设置为NULL也不会导致问题。然而,这是一个简单且有助于防范错误的额外保护措施,所以建议在释放内存后将指针设置为NULL

1.2 calloc

calloc()是另一个动态内存分配函数,也属于标准C库(stdlib.h头文件)。与malloc()功能类似,但在使用上有一些区别。

calloc()函数的原型如下:

void* calloc (size_t num, size_t size);

其中num是你想要分配的元素数量,size是每个元素的大小(以字节为单位)。calloc()函数会为num * size字节的内存块分配空间,并将该内存块中的所有位初始化为零。

相对于malloc()calloc()的一个优势是它会自动初始化分配的内存,这意味着你不需要手动将分配的内存清零。在某些情况下,这可能是非常有用的,特别是当你需要确保分配的内存一开始就是零值时。

实例:

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

int main() {
    int n = 5;
    int* dynamicArray = (int*)calloc(n, sizeof(int));

    if (dynamicArray == NULL) {
        printf("内存分配失败!\n");
    } else {
        // 使用分配的内存块,这里的内存已经被初始化为零
        for (int i = 0; i < n; i++) {
            printf("%d ", dynamicArray[i]); // 输出: 0 0 0 0 0
        }

        // 当不再需要分配的内存时,记得释放它
        free(dynamicArray);
    }

    return 0;
}

总结:

calloc = malloc+memset 初始化为0

1.3 realloc

realloc是一个用于重新分配内存块大小的函数。具体而言,它可以用于更改之前通过malloccalloc分配的内存块的大小。

realloc函数的声明如下:

void *realloc(void *ptr, size_t size);

参数说明:

  • ptr:指向之前已分配内存块的指针。如果ptr为NULL,则realloc的行为就相当于malloc,即分配一个新的内存块。
  • size:新的内存块大小,以字节为单位。

realloc的工作原理如下:

  1. 如果ptr为NULL,那么realloc的行为就等同于malloc(size),它将分配一个新的大小为size字节的内存块,并返回指向该内存块的指针。
  2. 如果size为0,且ptr不为NULL,那么realloc的行为就等同于free(ptr),即释放掉之前分配的内存块,并返回NULL指针。
  3. 如果ptr不为NULL且size不为0,realloc将尝试重新分配之前分配的内存块。可能发生以下几种情况:
    • 如果之前分配的内存块大小大于或等于size,则不会分配新的内存块,而是简单地返回原始内存块的指针,不会改变原内存块的内容。
    • 如果之前分配的内存块大小小于sizerealloc会尝试将原始内存块扩展到新的大小。这可能会**在原始内存块后面的可用内存空间进行扩展,如果没有足够的连续空间来扩展,则realloc可能会在另一个地方重新分配一个新的内存块,并将原始内容复制到新的内存块中。**这意味着realloc有可能返回一个新的指针,而不是原始指针,所以在使用realloc后,最好将返回的指针赋值给原来的指针。
    • 如果realloc在新的内存块分配失败时,将返回NULL,并且之前分配的内存块仍然保持未更改。

使用realloc时,应该特别注意以下几点:

  • 如果realloc返回NULL,表示重新分配失败,原来的指针仍然有效,为避免内存泄漏,应该保存原来的指针,并根据需要释放之前的内存块。
  • 当使用realloc时,最好不要直接修改原始指针,而是将realloc的结果赋值给原始指针,以防止意外的内存问题。

实例:

#include <stdio.h>
#include <stdlib.h>
int main() {
    int *p = (int *) malloc(40);
    if (p == NULL)
        return 1;
    //使用
    int i = 0;
    for (i = 0; i < 10; i++) {
        *(p + i) = i;
    }

    for (i = 0; i < 10; i++) {
        printf("%d ", *(p + i));
    }
    //增加空间
    // p = (int *)realloc(p, 80); //如果开辟失败的话,p变成了空指针,不能这么写
    int *ptr = (int *) realloc(p, 80);
    if (ptr != NULL) {
        p = ptr;
        ptr = NULL;
    }
    //当realloc开辟失败的时候,返回的也是空指针
    //使用
    for (i = 10; i < 20; i++) {
        *(p + i) = i;
    }

    for (i = 10; i < 20; i++) {
        printf("%d ", *(p + i));
    }
    //释放
    free(p);
    p = NULL;
    return 0;
}

//输出结果:0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

2.常见的动态内存错误

2.1 对NULL指针的解引用操作

#include <stdio.h>
#include <stdlib.h>
int main() {
    int* p = (int*)malloc(20);
    *p = 5;  //错误,空指针解引用
    //为了不对空指针解引用  需要进行判断
    if (p == NULL) {
        perror("malloc");
        return 1;
    }
    else {
        *p = 5;
    }
    free(p);
    p = NULL;
    return 0;
}

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

#include <stdio.h>
#include <stdlib.h>
int main() {
    int *p = (int *) malloc(20);
    if (p == NULL)
        return 1;
    int i = 0;
    for (i = 0; i < 20; i++)//越界访问    20个字节 只能访问5个整型
    {
        *(p + i) = i;
    }

    free(p);
    p = NULL;
    return 0;
}

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

#include <stdio.h>
#include <stdlib.h>
int main() {

    int a = 10;
    int* p = &a;
    free(p);// ok?

    return 0;
}

在这里插入图片描述

编译器会直接报错

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

#include <stdio.h>
#include <stdlib.h>
int main() {
    int *p = (int *) malloc(40);
    if (p = NULL)
        return 1;
    int i = 0;
    for (i = 0; i < 5; i++) {
        *p = i;
        p++;
    }
    //释放
    //在释放的时候,p指向的不再是动态内存空间的起始位置
    free(p);// p不再指向动态内存的起始位置
    p++;
    return 0;
}

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

#include <stdio.h>
#include <stdlib.h>
int main() {
    int* p = (int*)malloc(40);
    if (p == NULL)
        return 1;
    int i = 0;
    for (i = 0; i < 5; i++) {
        *(p + i) = i;
    }
    //重复free
    free(p);
    p = NULL;//如果将p赋值为NULL  就可以在free,否则编译器会直接报错
    free(p);

    return 0;
}

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

#include <stdio.h>
#include <stdlib.h>
int *get_memory() {
    int *p = (int *) malloc(40);

    return p;
}

int main() {
    int *ptr = get_memory();
    //使用

    //释放  如果不释放 就会导致内存泄漏
    free(ptr);
    return 0;
}

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

在这里插入图片描述

C/C++程序内存分配的几个区域:

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
  3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

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

4.经典笔试题

4.1 题目1

#include <stdio.h>
#include <stdlib.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;
}

请问运行Test 函数会有什么样的结果?

运行Test函数会导致未定义行为。

GetMemory函数中,传入的char *p是一个局部变量,当在函数内部对其进行修改,并不会影响到原始调用函数中的指针。这是因为函数的参数是通过值传递的,即函数得到的是实参的副本,对参数的修改不会影响原始的实参。

Test函数中,将一个NULL指针str传递给GetMemory函数,然后在GetMemory函数中分配了内存并将新的地址赋给p。但这对str并没有影响,str仍然是一个NULL指针,指向未分配的内存。

接着,在Test函数中使用strcpy将字符串拷贝到str指向的内存,但是str指向的内存并没有被分配,这将导致未定义行为。

为了正确地分配内存并使用指针,需要修改GetMemory函数,使其返回分配的内存地址,并在Test函数中接收返回的指针。另外,别忘了在使用完内存后,需要使用free函数来释放动态分配的内存。

改写1:

#include <stdio.h>
#include <stdlib.h>
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;
}

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

改写2:

#include <stdio.h>
#include <stdlib.h>
char *GetMemory() {
    char *p = (char *) malloc(100);
    return p;
}

void Test(void) {
    char *str = NULL;
    str = GetMemory();   //接受返回的p
    strcpy(str, "hello world");
    printf(str);
    //释放
    free(str);
    str = NULL;
}

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

4.2 题目2

char *GetMemory(void) {
    char p[] = "hello world";
    return p;
}

void Test(void) {
    char *str = NULL;
    str = GetMemory();
    printf(str);
}

请问运行Test函数会有什么样的结果?

GetMemory函数中,定义了一个局部数组char p[] = "hello world";,然后将该数组的地址返回给调用者。但是,一旦GetMemory函数执行完毕,其局部变量(p数组)将被销毁,因为它是一个自动存储类别的局部变量。所以,返回的指针指向的是已经无效的内存。

Test函数中,你将GetMemory的返回值赋给指针str,然后使用printf打印str指向的内容。由于GetMemory返回的是一个无效的指针(指向已经被销毁的局部数组),printf可能会打印出垃圾值,或者程序崩溃,或者导致其他不可预测的结果。

这个问题被称为"悬挂指针"问题,因为指针悬挂在指向已经无效的内存位置上。

要解决这个问题,可以考虑使用动态内存分配来分配存储字符串的内存,并在使用完后记得使用free来释放内存。

修改后的代码示例:

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

char *GetMemory(void) {
    char *p = (char *)malloc(strlen("hello world") + 1);
    if (p != NULL) {
        strcpy(p, "hello world");
    }
    return p;
}

void Test(void) {
    char *str = NULL;
    str = GetMemory();
    if (str != NULL) {
        printf("%s\n", str);
        free(str); // 释放内存
    }
}

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

4.3 题目3

void GetMemory(char **p, int num) {
    *p = (char *) malloc(num);
}

void Test(void) {
    char *str = NULL;
    GetMemory(&str, 100);
    strcpy(str, "hello");
    printf(str);
}

请问运行Test函数会有什么样的结果?

没有释放内存,导致内存泄漏

修改后的代码实例:

void GetMemory(char **p, int num) {
    *p = (char *)malloc(num);
}

void Test(void) {
    char *str = NULL;
    GetMemory(&str, 100);
    if (str != NULL) {
        strcpy(str, "hello");
        printf("%s\n", str);
        free(str); // 释放内存
    }
}

4.4 题目4

void Test(void) {
    char *str = (char *) malloc(100);
    strcpy(str, "hello");
    free(str);
    if (str != NULL) {
        strcpy(str, "world");
        printf(str);
    }
}

请问运行Test 函数会有什么样的结果?

str被提前释放,再次访问str会导致野指针行为

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/803787.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用RunnerGo来简化测试流程

在软件开发过程中&#xff0c;测试是一个重要的环节&#xff0c;需要投入大量时间和精力来确保应用程序或网站的质量和稳定性。但是&#xff0c;随着应用程序变得更加复杂和庞大&#xff0c;传统的测试工具在面对比较繁琐的项目时非常费时费力。这时&#xff0c;一些自动化测试…

MAC电脑设置charles,连接手机的步骤说明(个人实际操作)

目录 一、charles web端设置 1. 安装charles之后&#xff0c;先安装证书 2. 设置 Proxy-Proxy Settings 3. 设置 SSL Proxying 二、手机的设置 1. 安卓 2. ios 资料获取方法 一、charles web端设置 1. 安装charles之后&#xff0c;先安装证书 Help-SSL Proxying-Inst…

高压放大器模块的作用是什么呢

高压放大器模块是一种集成了高压放大器芯片、控制电路、保护电路等多种元件和功能的模块化设备。它可以将输入信号进行放大处理&#xff0c;并输出到负载上&#xff0c;具有高性能、高可靠性、高稳定性等优点。下面安泰电子将详细介绍高压放大器模块的作用&#xff1a; 信号放大…

【LLM】浅析chatglm的sft+p-tuning v2

note GLM将针对不同类型下游任务的预训练目标统一为了自回归填空&#xff0c;结合了混合的注意力机制和新的二维位置编码。本文浅析sft&#xff0c;并基于GLM在广告描述数据集上进行sftp-tuning代码的数据流讲解 文章目录 note零、ChatGLM2模型一、Supervised fine-tuning1. 数…

位图和布隆过滤器+哈希切分思想

文章目录 一.位图(bitset)底层实现: 二.布隆过滤器(bloomFilter)底层实现: 三.哈希切分思想 一.位图(bitset) 位图是一种以一个比特位为数据记录单元的哈希表 ,以无符号整数为key值,采用直接定址法(不存在哈希冲突的问题),其哈希映射函数为 f ( k e y ) k e y ( k e y 的存在…

有关合泰BA45F5260中断的思考

最近看前辈写的代码&#xff0c;发现这样一段代码&#xff1a; #ifdef SUPPORT_RF_NET_FUNCTION if(UART_INT_is_L()) { TmrInsertTimer(eTmrHdlUartRxDelay,TMR_PERIOD(2000),NULL); break; } #endif 其中UART_INT_is_L&am…

【lesson6】Linux下:第一个小程序,进度条代码

文章目录 准备工作sleep问题fflush回车与换行的区别 进度条代码 准备工作 sleep问题 首先我们来看一段代码&#xff1a; 这时候有个 问题这个代码是输出“hello world”还是先sleep三秒&#xff1f; 再来一段代码 这个代码是先sleep三秒还是先输出“hello world”&#xff…

「乐天世界」NFT 作品集

进入「乐天世界」NFT 作品集的迷人世界&#xff0c;这里仿佛就是乐天世界探险主题公园里充满活力的礼品店。 准备好随着想象力的飞跃而沉浸其中吧&#xff0c;因为主题公园里的普通物品已经变得非凡。沉浸在游乐园美食的魔力中&#xff0c;如香脆的玉米热狗、令人垂涎的巧克力蛋…

立创EDA学习

学习树莓派3B的板子发现有个扩展板比较好&#xff0c;自己最好画一个&#xff0c;反正免费。 学习视频&#xff1a;立创EDA&#xff08;专业版&#xff09;电路设计与制作快速入门。 下载专业版&#xff0c;并激活。【分专业版和标准版&#xff0c;专业版也是免费的】 手机…

基于物联网技术的能耗在线监测平台的架构设计与应用

安科瑞 华楠 摘要&#xff1a;围绕工业生产等领域节能降耗实际需求&#xff0c;提出基于物联网的能耗在线监测平台总体方案&#xff0c;面向政府、行业、企业提供能耗管理信息化管理与服务;研究设计能耗监测终端&#xff0c;支持多种工业总线及工业协议&#xff0c;实现电表、…

jenkins执行jmeter时,报Begin size 1 is not equal to fixed size 5

jenkins执行jmeter脚本的时候一直提示如下错误&#xff1a; Tidying up ... Fri Jul 28 17:03:53 CST 2023 (1690535033178) Error generating the report: org.apache.jmeter.report.dashboard.GenerationException: Error while processing samples: Consumer failed wi…

this关键字和同步异步宏认为微任务理解

目录 js面试常见问题&#xff1a;1.this指向 2.闭包定义和作用 3.原型链 4.异步协程 this关键字 this主要有以下几个使用场合。 1&#xff09;全局环境 &#xff08;2&#xff09;构造函数 &#xff08;3&#xff09;对象的方法 避免多层this 避免数组处理方法中的 this 避免回…

RabbitMQ 教程 | RabbitMQ 入门

&#x1f468;&#x1f3fb;‍&#x1f4bb; 热爱摄影的程序员 &#x1f468;&#x1f3fb;‍&#x1f3a8; 喜欢编码的设计师 &#x1f9d5;&#x1f3fb; 擅长设计的剪辑师 &#x1f9d1;&#x1f3fb;‍&#x1f3eb; 一位高冷无情的编码爱好者 大家好&#xff0c;我是 DevO…

VS CODE 20230728

VSCode添加至右键菜单 2.Visual Studio Code(VS Code)中文显示乱码的解决方法 1.按 快捷键 ctrl, 在搜索栏中输入“files:auto Guess Encoding” 勾选 还是乱码

UG\NX二次开发 获取2D制图视图中可见的对象,并获取类型

文章作者:里海 来源网站:https://blog.csdn.net/WangPaiFeiXingYuan 简介: 使用UF_VIEW_ask_visible_objects获取2D制图视图中可见的对象,并获取类型。 下面是将一个六面体以不同的视图投影,获取视图对象和类型的效果。 效果: 1个部件事例,1个体,4条边 1个部件事…

C++,类和对象-多态,制作饮品

#include<iostream> using namespace std;//多态案例&#xff0c;制作饮品class AbstractDrinking { public://煮水virtual void Boil() 0;//冲泡virtual void Brew() 0;//倒入茶杯virtual void PourInCup() 0;//加入辅料virtual void PutSomething() 0;//制作饮品vo…

外文期刊影响因子去哪里查询,如何查询

期刊影响因子(Impact factor&#xff0c;IF)&#xff0c;是代表期刊影响大小的一项定量指标。也就是某刊平均每篇论文的被引用数&#xff0c;它实际上是某刊在某年被全部源刊物引证该刊前两年发表论文的次数&#xff0c;与该刊前两年所发表的全部源论文数之比。那么&#xff0c…

4.操作元素属性

4.1操作元素常用属性 ●通过 JS 设置/修改 标签元素属性&#xff0c;比如通过src更换图片 ●最常见的属性比如&#xff1a;href、 title、 src 等 ●语法: 对象.属性 值【示例】 // 1.获取元素 const pic document.querySelector( img ) // 2.操作元素 pic.src ./images/b…

vector使用

文章目录 vector的介绍vector的使用vector的初始化vector iterator迭代器的使用vector 空间增长问题vector的增删改查 迭代器失效总结 vector的介绍 文档介绍 vector是表示可变大小数组的序列容器。就像数组一样&#xff0c;vector也采用的连续存储空间来存储元素。也就是意味着…