动态内存管理函数malloc、calloc、realloc、free函数,以及练习,程序的内存开辟,柔性数组

news/2024/11/27 8:43:01/

文章目录

  • 为什么存在动态内存的分配
  • 动态内存函数的介绍
    • 介绍malloc函数的使用
    • 介绍calloc函数的使用
    • 介绍realloc函数的使用
  • 常见的动态内存错误
    • 对NULL指针的解引用操作
    • 对动态开辟空间的越界访问
    • 对非动态开辟内存使用free释放
    • 使用free释放一块动态开辟内存的一部分
    • 对同一块动态内存多次释放
    • 动态开辟内存忘记释放(内存泄露)
  • 练习题讲解
    • 题目一:
    • 题目二:
    • 题目三:
    • 题目四:
  • c/c++程序的内存开辟
  • 柔性数组
    • 柔性数组的特点
    • 柔性数组的优势

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

我们已经学习过了对变量和数组的内存开辟方法,那就是定义变量和定义数组。但是它们是在栈区上开辟空间。函数运行完成后直接会销毁栈空间上的变量。
看下面的代码:

int a = 0; //在栈空间上开辟四个字节的内存空间
char arr[10] = {0}; //在栈空间上开辟十个字节的内存空间

但是上述的开辟空间的方式有两个特点:
1、开辟的空间的大小式不可变的。
2、数组在声明的时候必须指定数组的大小,也就是方括号里的值,它所需要的内存空间在编译时分配。
但是对于上述的情况,我们事先知道要分配多大的内存空间,有的时候我们需要在空间大小在程序运行的时候才能知道,这时候我们就可以引入动态内存开辟了。

动态内存函数的介绍

动态内存管理函数有四个分别是:malloc、calloc、realloc、free函数。

介绍malloc函数的使用

malloc函数是在栈区进行动态开辟一块连续的内存空间的函数,如果没有开辟成功它会返回一个空指针,开辟成功会返回这块地址的起始地址,也就是指向这块地址的指针。因为它会返回一个空指针,所以使用malloc函数一定要进行空指针的检查。返回值是void*指针,malloc函数不知道开辟空间的类型,具体情况按照程序员的决定,可以去把malloc的返回值强制类型转换为自己想要的类型的指针。如果参数size_t size 为0,是标准未定义的,是否开辟内存空间还是报错或者其他情况,这取决于编译器。我们来用一个代码来演示malloc函数的使用。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{int* a = (int*)malloc(20);//进行访问内存空间,按照数组的形式可以访问到这块内存空间的五个元素//(20个字节,int类型是4个字节,所以为五个元素)//对a指针变量进行判空,排除malloc返回空指针,访问内存空间失败的情况//那就是使用野指针的情况。if (a == NULL){printf("访存失败,原因是:%s", strerror(errno));//strerror函数已经在前面介绍过了exit(-1);}int i = 0;int* p = a;//赋值for (i = 0; i < 5; i++){*(p + i) = i;}//打印for (i = 0; i < 5; i++){//利用数组的方式进行访问存储空间,因为//p[i] == *(p+i)printf("%d\n", p[i]);}
}

使用free函数可以对堆区上动态开辟的内存空间进行释放,不能释放栈区上的内存空间。
如果参数free参数指向的空间不是动态开辟的,free函数的行为是未定义的。参数为NULL指针时,free函数什么也不会做。我们来看下面的代码

int main()
{int* a = (int*)malloc(20);//进行访问内存空间,按照数组的形式可以访问到这块内存空间的五个元素//(20个字节,int类型是4个字节,所以为五个元素)//对a指针变量进行判空,排除malloc返回空指针,访问内存空间失败的情况//那就是使用野指针的情况。if (a == NULL){printf("访存失败,原因是:%s", strerror(errno));//strerror函数已经在前面介绍过了exit(-1);}int i = 0;int* p = a;//赋值for (i = 0; i < 5; i++){*(p + i) = i;}//打印for (i = 0; i < 5; i++){//利用数组的方式进行访问存储空间,因为//p[i] == *(p+i)printf("%d\n", p[i]);}//释放堆区上动态开辟的内存空间。free(a);//注意这里,释放a指向的空间后,a就不知道指向哪块内存空间了//所以要把它置为空,避免野指针。a = NULL;
}

介绍calloc函数的使用

calloc函数也是用来在堆区上进行内存的动态开辟。它相较于malloc函数会把开辟的内存初始化。它的原型是这样的。
void* calloc (size_t num, size_t size);
返回void*类型的指针,指定开辟num个大小为size的元素的空间,并且把每个空间初始化为0。
calloc函数和malloc函数的对比:
1、参数不同。
2、都是在堆区上申请空间,但是malloc不初始化,calloc会初始化为0,如果要初始化,就是用calloc,不需要初始化,使用malloc函数。

int main()
{int* a = (int*)calloc(5, 4);if (a == NULL){printf("%s\n", strerror(errno));exit(-1);}//查看开辟的内存空间是否被初始化for (int i = 0; i < 5; i++){printf("%d ", *(a + i));}free(a);a = NULL;
}

运行时内存状态
在这里插入图片描述

介绍realloc函数的使用

realloc的出现让动态内存的分配更加灵活,有的时候我们觉得内存空间分配少了,有的时候又觉得大了,这个时候就需要realloc函数出马了,realloc函数可以做到动态内存大小的调整。函数原型如下:
void* realloc (void* ptr, size_t size);
返回void* 的指针,调整ptr指向的内存空间的大小,调整为几个字节,这里需要注意size要包含之前的内存空间的大小。还需要注意,如果ptr所指向的空间后面没有空间了,realloc函数会另寻找一块空间进行分配。并且把原有的空间进行拷贝,返回新空间的地址,然后free掉之前的空间。这里可能会返回空指针。
realloc函数的两种情况:
情况一:原有空间之后有足够大的空间。
情况二:原有空间之后没有足够大的空间。
在这里插入图片描述
情况一:当是情况一的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
情况二:当时情况二的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。
我们来看下面的代码:

int main()
{int* ptr = (int*)malloc(20);//需要判空if (ptr == NULL){printf("空间分配失败,原因是:%s\n", strerror(errno));return;}else{//业务处理}int* temp = (int*)realloc(ptr, 40);//需要进行判空。if (temp == NULL){printf("空间分配失败,原因是:%s\n", strerror(errno));return;}else{ptr = temp;//业务处理}//free掉内存空间free(ptr);ptr = NULL;return 0;
}

执行时内存空间状态
在这里插入图片描述

还需注意一点:如果realloc函数ptr接收到的是一个空指针,那么它的功能就相当于malloc函数了。

常见的动态内存错误

对NULL指针的解引用操作

#define MAX 20
void test()
{int* p = (int*)malloc(MAX);//解决办法,判空if (p == NULL) {return;}else{*p = 20;}//如果p是空指针,那么会发生对空指针的//解引用操作。free(p);
}

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

void test()
{int i = 0;int* p = (int*)malloc(10 * sizeof(int));if (NULL == p){exit(-1);}for (i = 0; i <= 10; i++){*(p + i) = i; //当i是10的时候越界访问}free(p);
}

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

void test()
{int a = 10;int* p = &a;free(p); //这是对栈区上的内存空间free,会发生错误
}

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

void test()
{int* p = (int*)malloc(100);p++;//p不在指向内存空间的起始位置。free(p);
}

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

void test()
{int* p = (int*)malloc(100);free(p);free(p); //对p空间重复释放
}

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

void test()
{int* p = (int*)malloc(100);if (p != NULL){*p = 20;}
}
int main()
{while (1){test();}//如果这个程序不间断的跑下去,会把内存空间全部泄露,别人就不能用了。
}

忘记释放不在使用的动态开辟的空间会造成内存泄漏。所以说动态开辟的空间一定要用free释放,并且正确释放。

练习题讲解

题目一:

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

在这里插入图片描述

题目二:

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

在这里插入图片描述

题目三:

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

在这里插入图片描述

题目四:

void Test(void)
{char* str = (char*)malloc(100);strcpy(str, "hello");free(str);//对已释放的堆区的内存空间进行访问。//程序会崩溃,写出了内存上的错误。if (str != NULL){strcpy(str, "world");printf(str);}
}

c/c++程序的内存开辟

在这里插入图片描述

c/c++ 程序内存分配的几个区域
1、栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时,这些存储单元自动释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存空间容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
2、堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。
3、数据段(静态区):(static)存放全局变量、静态数据。程序结束后由系统释放。
4、代码段:存放函数体(类成员函数和全局函数)的二进制代码。
观察上面的图,我们就可以很好的理解static关键字修饰局部变量的例子了。
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就被销毁了。但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,知道程序结束才销毁。
所以声明周期会延长。

柔性数组

在C99标准中,结构体中的最后一个元素允许是未知大小的数组,这就叫做柔性数组的成员。

struct st_type
{int i;int a[0] //柔性数组成员
};

有些编译器报错无法编译可以写成

struct st_type_1
{int i;int a[];
};

柔性数组的特点

结构中的柔性数组成员前面前面至少有一个其他成员。
sizeof 返回的这种结构大小不包括柔性数组的内存。
我们来用代码说话:
在这里插入图片描述
包含柔性数组成员的结构用malloc() 函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小。
我们来用代码说话:

int main()
{int i = 0;struct st_type* st = (struct st_type*)malloc(sizeof(struct st_type)+100*sizeof(int));st->i = 100;//判空if (st == NULL){return;}for (i = 0; i < 100; i++){st->a[i] = 0;}for (i = 0; i < 100; i++){printf("%d ", st->a[i]);}//调整内存大小struct st_type* temp = (struct st_type*)realloc(st, sizeof(struct st_type*) + 200 * sizeof(int));if (temp == NULL){return;}else{st = temp;}free(st);return 0;
}

柔性数组的优势

实现柔性数组功能的两种方案。

//方案一:
int main()
{int i = 0;struct st_type* st = (struct st_type*)malloc(sizeof(struct st_type)+100*sizeof(int));st->i = 100;//判空if (st == NULL){return;}for (i = 0; i < 100; i++){st->a[i] = 0;}for (i = 0; i < 100; i++){printf("%d ", st->a[i]);}//调整内存大小struct st_type* temp = (struct st_type*)realloc(st, sizeof(struct st_type*) + 200 * sizeof(int));if (temp == NULL){return;}else{st = temp;}free(st);return 0;
}//方案二:
typedef struct st_type
{int i;int* a //柔性数组成员
}type_a;
int main()
{type_a* st = (type_a*)malloc(sizeof(type_a));if (st == NULL){return;}int* ptr = (int*)malloc(100 * sizeof(int));if (ptr == NULL){return;}else{st->i = 100;st->a = ptr;}//使用//调整内存大小int* pt = (int*)realloc(st->a, 200 * sizeof(int));if (pt == NULL){return;}else{st->a = pt;st->i = 200;}//使用//释放free(st->a);free(st);st = NULL;
}

上述代码都可以实现同样的功能,但是方案一有两个好处:
第一个好处是:方便内存的释放
如果我们的代码是在一个给别人用的函数中,你在里面做了两次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其他成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度。
连续的内存有益于提高访问速度,也有益于减少内存碎片。
在这里插入图片描述


http://www.ppmy.cn/news/35038.html

相关文章

【操作系统笔记02】操作系统之多线程模型、处理机调度及其相关调度算法

这篇文章,主要介绍操作系统之多线程模型、处理机调度及其相关调度算法。 目录 一、多线程和处理机调度 1.1、什么是线程 1.2、线程的实现方式 (

html+css制作

<!DOCTYPE html> <html><head><meta charset"utf-8"><title>校园官网</title><style type"text/css">*{padding: 0;margin: 0;}#logo{width:30%;float: left;}.nav{width: 100%;height: 100px;background-color…

安全防御之入侵检测篇

目录 1.什么是IDS&#xff1f; 2.IDS和防火墙有什么不同&#xff1f;3.IDS的工作原理&#xff1f; 4.IDS的主要检测方法有哪些&#xff1f;请详细说明 5.IDS的部署方式有哪些&#xff1f; 6.IDS的签名是什么意思&#xff1f;签名过滤器有什么用&#xff1f;例外签名的配置作…

Redis单线程还是多线程?IO多路复用原理

目录专栏导读一、Redis版本迭代二、Redis4.0之前为什么一直采用单线程&#xff1f;三、Redis6.0引入多线程四、Redis主线程和IO线程是如何完成请求的&#xff1f;1、服务端和客户端建立socket连接2、IO线程读取并解析请求3、主线程执行请求命令4、IO线程会写回socket和主线程清…

艹,终于在8226上把灯点亮了

接上次点文章ESP8266还可以这样玩这次&#xff0c;我终于学会了在ESP8266上面点亮LED灯了现在一个单片机的价格是几块&#xff0c;加上一个晶振&#xff0c;再来一个快递费&#xff0c;十几块钱还是需要的。所以能用这个ESP8266来当单片机玩&#xff0c;还是比较不错的可以在ub…

python自动发送邮件,qq邮箱、网易邮箱自动发送和回复

在python中&#xff0c;我们可以用程序来实现向别人的邮箱自动发送一封邮件&#xff0c;甚至可以定时&#xff0c;如每天8点钟准时给某人发送一封邮件。今天&#xff0c;我们就来学习一下&#xff0c;如何向qq邮箱&#xff0c;网易邮箱等发送邮件。 一、获取邮箱的SMTP授权码。…

LeetCode算法 打家劫舍 和 打家劫舍II C++

目录题目 打家劫舍参考答案题目 打家劫舍II参考答案题目 打家劫舍 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统&#xff0c;如果两间相邻的房屋在同一晚上被小偷闯…

spring-cloud-feign实战笔记

feign 配置 针对单个feign接口进行配置feign:client:config:# feignName 注意这里与contextId一致&#xff0c;不能写成name&#xff08;FeignClientFactoryBean#configureFeign&#xff09;# 不能写成 client-b (微服务名称)&#xff0c;否则不生效helloFeignClient: # conte…