当前位置: 首页 > ds >正文

【c语言】动态内存管理

目录

1.为什么存在动态内存管理

2.动态内存管理的介绍

2.1malloc函数和free函数 

2.2 calloc函数

2.3 realloc 

3. 常见的动态内存错误

3.1 对NULL指针的解引用操作

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

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

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

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

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

4.几个经典的笔试题

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

6.柔性数组

 6.1 柔性数组的特点

6.2 柔性数组的使用 

6.3 柔性数组的优势


1.为什么存在动态内存管理

已经存在的内存开辟方式:
int val = 20; //在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
上述的开辟空间的方式有两个特点:
1. 空间开辟大小是固定的。
2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。 这时候就只能试试动态存开辟了
动态内存开辟是c语言给程序员的一种权利

2.动态内存管理的介绍

2.1malloc函数和free函数 

注意:动态内存管理是在堆区上进行的

动态内存开辟的函数:  需要包含头文件stdlib.h

这个函数向内存申请一块 连续可用 的空间,并返回指向这块空间的指针。
  • 如果开辟成功,则返回一个指向开辟好空间的指针。
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  • 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
  • 如果参数 size 0malloc的行为是标准是未定义的,取决于编译器。

注意:内存空间谁申请,谁释放 

malloc函数主动释放——free函数

被动释放——程序退出后,申请的空间会被操作系统回收

用来做动态内存的释放和回收的函数:

free 函数用来释放动态开辟的内存。
  • 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
  • 如果参数 ptr NULL指针,则函数什么事都不做。
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main() {//申请十个整型内存int* p = (int*)malloc(INT_MAX*4);//需要包含头文件limits.hif (p == NULL) {//NULL需要包含头文件stdio.hperror("malloc");return 1;//返回异常}//int* p = (int*)malloc(10 * sizeof(int));//if (p == NULL) {//NULL需要包含头文件stdio.h//	perror("malloc");//	return 1;//返回异常//}//使用int i = 0;for (i = 0; i < 10; i++) {*(p + i) = i;}for (i = 0; i < 10; i++) {printf("%d ", p[i]);}//释放free(p);//注意:free将申请的内存释放后,p变为野指针,需要赋空指针p = NULL;return 0;
}

 

2.2 calloc函数

与malloc函数的区别:

  1. calloc函数申请好空间以后,会将空间初始化为0
  2. calloc的函数参数为元素个数和大小(是将malloc的函数参数拆开)
#include <stdio.h>
#include <stdlib.h>
int main() {int* p = (int*)malloc(10 * sizeof(int));if (p == NULL) {perror("malloc");return 1;}int i = 0;for (i = 0; i < 10; i++) {printf("%d\n", p[i]);}return 0;
}int main() {int* p = (int*)calloc(10, sizeof(int));if (p == NULL) {perror("calloc");return 1;}int i = 0;for (i = 0; i < 10; i++) {printf("%d\n", p[i]);}return 0;
}
malloc函数没有初始化
calloc函数初始化为0

总结:malloc/calloc与free要成对使用,检查malloc/calloc的返回值是否为NULL,用完free要赋NULL 

2.3 realloc 

realloc函数的使用:

int main() {int* p = (int*)malloc(10 * sizeof(int));if (p == NULL) {perror("malloc");return 1;}int i = 0;for (i = 0; i < 10; i++) {*(p + i) = i;}for (i = 0; i < 10; i++) {printf("%d ", p[i]);}//空间不够,希望调整为20个整型空间//p = realloc(p, 20 * sizeof(int));//注意:realloc开辟失败会返回NULL,如果赋值给p会导致原来的空间地址丢失int* ptr = realloc(p, 20 * sizeof(int));if (ptr != NULL) {p = ptr;}free(p);p = NULL;return 0;
}

 注意:realloc申请空间有两种情况:

  1. 原有空间之后有足够大的空间
  2. 后续空间可能被占有,不能直接增加,realloc会找一块新的足够的空间一次性开辟.
    扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用
  • 旧的空间的数据会拷贝到新的空间中
  • 释放掉旧的空间
  • realloc函数返回新的空间的地址

 注意:realloc的第一个参数为NULL时,相当于malloc函数

int main() {int* p = (int*)realloc(NULL, 40);//malloc(40)return 0;
}

3. 常见的动态内存错误

3.1 对NULL指针的解引用操作

void test()
{int *p = (int *)malloc(INT_MAX/4);*p = 20;//如果p的值是NULL,就会有问题free(p);p=NULL;
}

3.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);
}

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

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

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

void test()
{int *p = (int *)malloc(100);p++;free(p);//p不再指向动态内存的起始位置
}

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

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

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

void test()
{int* p = (int*)malloc(100);if (NULL != p){*p = 20;}
}
int main()
{test();while (1);
}
//注意:即使函数内不释放,也要想办法释放
int* test()
{int* p = (int*)malloc(100);if (NULL != p){*p = 20;}return p;
}
int main()
{int* ret = test();free(ret);ret = NULL;while (1);
}

4.几个经典的笔试题

题目一:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void GetMemory(char* p) {p = (char*)malloc(100);//形参是实参的临时拷贝,改变形参,不影响实参//注意:p的空间出函数以后会被回收,但malloc开辟的空间没有被释放,内存泄漏
}
void Test(void) {char* str = NULL;GetMemory(str);strcpy(str, "hello world");//str仍为空指针,程序对空指针进行解引用操作,程序崩溃,后续代码不再执行printf(str);
}
int main() {Test();return 0;
}//修改
//方法一:
void GetMemory(char** p) {*p = (char*)malloc(100);//使开辟的空间地址传给str
}
void Test(void) {char* str = NULL;GetMemory(&str);strcpy(str, "hello world");printf(str);free(str);str = NULL;
}
int main() {Test();return 0;
}
//方法二:
char* GetMemory() {char* p = (char*)malloc(100);//使开辟的空间地址传给str
}
void Test(void) {char* str = NULL;str = GetMemory();strcpy(str, "hello world");printf(str);free(str);str = NULL;
}
int main() {Test();return 0;
}

 题目二:

#include <stdio.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;
}
//返回栈空间地址的问题:栈空间变量出函数就会销毁,如果销毁后把变量地址返回记住,就会将该指针变为野指针(可以根据地址找到对应的空间,但没有该空间的使用权限)//返回栈空间问题,非法访问的其他例子
int* test() {int a = 0;return &a;
}
int main() {int* p = test();printf("%d\n", *p);return 0;
}

运行发现,*p的值还是10,为什么? 

test函数调用结束后,栈空间还给操作系统,如果没有分配空间给其他成员使用,原来的值不会改变,如果调用一个printf函数,为printf函数分配栈帧时,原内容就可能会被修改

题目三: 

#include <stdio.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);//没有free函数,存在内存泄漏//修改free(str);str = NULL;
}
int main() {Test();return 0;
}

 题目四:

void Test(void) {char* str = (char*)malloc(100);strcpy(str, "hello");free(str);if (str != NULL)//不能使用str,条件判断为假{strcpy(str, "world");//str是野指针,对野指针操作,非法访问printf(str);}
}
int main() {Test();return 0;
}//修改
void Test(void) {char* str = (char*)malloc(100);strcpy(str, "hello");if (str == NULL) {perror("malloc");return;}free(str);str = NULL;if (str != NULL){strcpy(str, "world");printf(str);}
}
int main() {Test();return 0;
}

注意:返回栈空间地址问题是返回函数局部变量的地址,返回局部变量时,通过寄存器赋值没有异常

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

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

6.柔性数组

C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员
要求:
  1. 结构体中
  2. 最后一个成员
  3. 未知大小的数组

 6.1 柔性数组的特点

  • 结构中的柔性数组成员前面必须至少一个其他成员。
  • sizeof 返回的这种结构大小不包括柔性数组的内存。
  • 包含柔性数组成员的结构malloc ()函数进行内存的动态分配(在堆上开辟),并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小
//柔性数组成员
struct S {char c;int i;int arr[];//大小未知//int arr[0];//大小也可以写作0,但有些编译器会报错无法编译
};
int main() {printf("%zd\n", sizeof(struct S));
}

6.2 柔性数组的使用 

柔性数组使用内存方式: 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//柔性数组成员的使用
struct S {char c;int i;int arr[];//大小未知//int arr[0];也可以写作0
};
int main() {//申请空间struct S* ps = (struct S*)malloc(sizeof(struct S) + 20);if (ps == NULL) {perror("malloc");return 1;}ps->c = 'w';ps->i = 4;int i = 0;for (i = 0; i < 5; i++) {ps->arr[i] = i;}//打印for (i = 0; i < 5; i++) {printf("%d ", ps->arr[i]);}//释放free(ps);ps = NULL;
}

其他方式:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//使用其他方式实现申请内存的动态变化
struct S {char c;int i;int* data;//data是一个指针变量,没有空间储存数据
};
int main() {struct S* ps = (struct S*)malloc(sizeof(struct S));if (ps == NULL) {perror("ps->malloc");return 1;}ps->c = 'w';ps->i = 4;ps->data = (int*)malloc(20);if (ps->data == NULL) {perror("ps->data->malloc");}int i = 0;for (i = 0; i < 5; i++) {ps->data[i] = i;}//打印for (i = 0; i < 5; i++) {printf("%d ", ps->data[i]);}//空间不够,增容int* ptr = (int*)realloc(ps->data, 40);if (ptr != NULL) {ps->data = ptr;}else {perror("ps->data->realloc");return 1;}//增容成功,继续使用//...//释放空间free(ps->data);ps->data = NULL;free(ps);ps = NULL;return 0;
}

6.3 柔性数组的优势

第一个好处是: 方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free 可以释放结构体,但是用户并不知道这个结构体内的成员也需要 free ,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free 就可以把所有的内存也给释放掉。
第二个好处是: 这样有利于访问速度 .
连续的内存有益于提高访问速度,也有益于减少内存碎片,提高内存利用率。(其实,我个人觉得也没多高了,反正你跑不了要用做偏移量的加法来寻址)
http://www.xdnf.cn/news/5229.html

相关文章:

  • 各种注解含义及使用
  • 心 光 -中小企实战运营和营销工作室博客
  • 微机控制高温扭转试验机
  • 关于AI 大数据模型的基础知识 杂记
  • 数字化与信息化的关系
  • 4.3 Thymeleaf案例演示:图书管理
  • 军事目标无人机视角坦克检测数据集VOC+YOLO格式4003张1类别
  • 44.辐射发射整改简易摸底测试方法
  • 企业名录搜索软件哪家好?
  • 6.01 Python中打开usb相机并进行显示
  • 动态创建链表(头插法、尾插法)
  • RISC-V CLINT、PLIC及芯来ECLIC中断机制分析 —— RISC-V中断机制(一)
  • Linux探秘坊-------12.库的制作与原理
  • java-----------------多态
  • 跨平台编码规范文档
  • c++:标准模板库 STL(Standard Template Library)
  • 【Go底层】http标准库服务端实现原理
  • 设计模式-迭代器模式
  • 【MySQL数据库】--SQLyog创建数据库+python连接
  • 26考研——中央处理器_CPU 的功能和基本结构(5)
  • 机器学习-数据集划分和特征工程
  • Rust 中的 `PartialEq` 和 `Eq`:深入解析与应用
  • 数据库审计如何维护数据完整性:7 种工具和技术
  • 解决Win11下MySQL服务无法开机自启动问题
  • 数巅智能携手北京昇腾创新中心深耕行业大模型应用
  • 卷积神经网络实战(4)代码详解
  • 第二章 如何安装KEIL5和新建工程
  • 【论文解读】| ACL2024 | LANDeRMT:基于语言感知神经元路由的大模型机器翻译微调框架
  • 2025年数维杯C题完整求解思路讲解+代码分享
  • AI星智协脑:智能驱动的高效协作管理平台全解读