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

C动态内存管理

前言:不知不觉又过去了很长的一段时间。今天对C语言中的动态内存管理进行一个系统性的总结。

1 为什么要有动态内存分配

C语言中,使用int,float,double,short等数据内置类型以及数组不是也可以开辟内存空间吗?为什么还要有动态内存分配呢?这是因为以上方式开辟出来的内存空间有两个缺点

. 空间开辟大小是固定的

. 数组在声明的时候,必须指定数组的长度,数组空间一旦确定了大小无法调整

但是对于空间的需求不仅仅是上述情况。有时候我们需要的空间大小在程序运行起来的时候才能知道。那么上述开辟空间的方式就不能满足了。因此C语言中引入了动态内存分配,,让程序员自己申请,释放空间,相对灵活。

2 malloc和free

2.1 malloc函数的介绍

//返回值类型是void*指针,参数类型是size_t,size是申请内存块的大小,单位是字节
//size_t是一个unsigned int类型
void* malloc(size_t size);

malloc函数向内存申请一块连续可用的空间,并返回指向这块内存空间的指针

. 如果开辟成功,则返回一个指向开辟好空间的指针**。

. 如果失败,则返回一个NULL指针,因此malloc函数的返回值一定要做检查

. malloc函数的返回类型是void*类型的指针所以malloc函数并不知道开辟空间的类型,使用的时候由使用者自己来决定

. 如果参数size为0,malloc的行为是标准未定义的,取决于编译器。

2.2 free函数的介绍

C语言提供了一个free函数,是专门用来释放,回收动态开辟出来的内存空间

//返回值类型是void,参数类型是void*,ptr是指向先前由malloc或realloc或calloc分配的内存空间
void free(void* ptr);

free函数是用来释放动态开辟的内存

. 如果参数ptr指向的空间不是动态开辟的,那么free函数的行为是未定义的

. 如果参数ptr是NULL指针,那么free函数什么事都不做

#include<stdio.h>
#include<stdlib.h>
int main()
{//动态申请内存空间int* ptr = (int*)malloc(10 * sizeof(int));//检查是否开辟成功if (NULL == ptr){printf("malloc fail\n");exit(-1);}for (int i = 0; i < 10; i++){*(ptr + i) = i + 1;}for (int i = 0; i < 10; i++){printf("%d ", *(ptr + i));}//释放空间free(ptr);//改变ptr指针的指向ptr = NULL;return 0;
}

3 calloc和realloc

3.1 calloc函数的介绍

void* calloc(size_t num,size_t size);

calloc函数也是用来动态开辟内存的

. calloc函数的功能是为num个大小为size的元素开辟一块空间,并把空间的每个字节初始化为0

. 与malloc函数的区别在于calloc函数在返回地址之前会将申请的空间每个字节初始化为0

#include<stdio.h>
#include<stdlib.h>
int main()
{//动态开辟10*sizeof(int)个字节int* p = (int*)calloc(10, sizeof(int));//检查是否开辟成功if (NULL == p){printf("calloc fail\n");exit(-1);}//观察calloc函数开辟空间的内容for (int i = 0; i < 10; i++){printf("%d ", *(p + i));}free(p);p=NULL;return 0;
}

3.2 realloc函数的介绍

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

realloc函数的功能是对空间的大小进行调整

. ptr是要调整的内存地址

. size是调整之后新的大小

. 返回值是调整之后内存空间的起始地址

. 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的内存空间

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

1.原有内存空间之后有足够的空间

2.原有内存空间之后没有足够大的空间

在这里插入图片描述


情况1:在原有空间之后追加空间,原有空间的数据不发生变化

情况2:原有空间之后没有足够的空间,扩展的方法是:在堆空间上找一个合适大小的连续空间来使用,并将原有空间的数据拷贝一份给新空间,释放原有空间,返回一个新的地址

#include<stdio.h>
#include<stdlib.h>
int main()
{int* ptr = (int*)calloc(20);if (NULL == ptr){printf("calloc fail\n");exit(-1);}//如果扩容失败会怎么样//原有数据也会丢失,不推荐这种写法ptr = realloc(ptr, 40);//ok?//这种写法更为安全int* tmp = (int*)realloc(ptr, 40);if (NULL == tmp){printf("realloc fail\n");exit(-1);}ptr = tmp;free(ptr);ptr = NULL;return 0;
}

4 常见的动态内存错误

4.1 对NULL指针的解引用操作

void test()
{int* p = (int*)malloc(sizeof(int));if (NULL == p){printf("malloc fail\n");exit(-1);}*p = 20;//如果p是NULL,就会有问题free(p);p = NULL;
}

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

void test()
{int* p = (int*)malloc(sizeof(int)*10);if (NULL == p){printf("malloc fail\n");exit(-1);}for (int i = 0; i <= 10; i++){*(p + i) = i;//i为10的时候就越界访问了}free(p);p = NULL;
}

4.3 释放一部分动态开辟空间

void test()
{int* p = (int*)malloc(sizeof(int) * 10);if (NULL == p){printf("malloc fail\n");exit(-1);}int i = 0;for (i = 0; i < 10; i++){*(p + i) = i;}for (i = 0; i < 5; i++){printf("%d ", *p);p++;}//此时p不再指向动态开辟空间的起始地址,只释放了一部分空间,p就是野指针free(p);p = NULL;
}

4.4 对非动态开辟空间的释放

void test()
{int a = 10;int* p = &a;//对非动态开辟内存的释放free(p);p = NULL;
}

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

void test()
{int* p = (int*)malloc(sizeof(int) * 10);if (NULL == p){printf("malloc fail\n");exit(-1);}free(p);free(p);//重复释放p = NULL;
}

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

void test()
{int* p = (int*)malloc(sizeof(int) * 10);if (NULL == p){printf("malloc fail\n");exit(-1);}//忘记释放
}

5 动态内存经典笔试题解析

5.1 题目一:

#include<stdio.h>
#include<stdlib.h>
void GetMemory(char* p)
{//动态开辟空间未进行释放,内存泄露p = (char*)malloc(100);
}
void Test(void)
{char* str = NULL;//str作为参数,这里传递的是NULL,形参的改变不会影响实参GetMemory(str);//因此str指向的内容还是NULL,无法对NULL指针进行访问,程序会崩溃strcpy(str, "hello world");printf(str);
}
int main()
{Test();return 0;
}

5.2 题目二:

#include<stdio.h>
#include<stdlib.h>
char* GetMemory(void)
{//局部变量char p[] = "hello world";//调用这个函数时为这个函数创建栈帧空间//调用之后这个函数的栈帧空间被销毁//局部变量也会被销毁,因此返回局部变量的地址会造成野指针return p;
}
void Test(void)
{char* str = NULL;//此时str就是一个野指针,对其进行访问就是非法访问str = GetMemory();printf(str);
}
int main()
{Test();return 0;
}

5.3 题目三:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void GetMemory(char** p, int num)
{//p是一个二级指针,接收的是一级指针变量str的地址//*p就是str*p = (char*)malloc(num);
}
void Test(void)
{char* str = NULL;//传递的是一级指针变量str的地址//形参的改变会影响实参GetMemory(&str, 100);strcpy(str, "hello");printf(str);//唯一的缺点就是没有进行动态内存释放,导致内存泄漏
}
int main()
{Test();return 0;
}

5.4 题目四:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void Test(void)
{char* str = (char*)malloc(100);strcpy(str, "hello");//free之后操作系统回收内存空间,但未将str置空,此时str就是野指针free(str);if (str != NULL){//非法访问strcpy(str, "world");printf(str);}
}
int main()
{Test();return 0;
}

6 柔性数组

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

typedef struct st_type
{int i;int arr[];//柔性数组成员
}type_a;

6.1 柔性数组的特点

. 结构体中柔性数组成员前面必须至少有一个其他成员

. sizeof 计算结构体大小是不包括柔性数组大小的

. 包含柔性数组成员的结构用malloc函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的大小

#include<stdio.h>
#include<stdlib.h>
typedef struct st_type
{//0~3int i;//4   8   4int arr[];//柔性数组成员
}type_a;
int main()
{printf("%zd\n", sizeof(struct st_type));//4return 0;
}

6.2 柔性数组的使用

#include<stdio.h>
#include<stdlib.h>
typedef struct st_type
{int i;int arr[];//柔性数组成员
}type_a;
int main()
{//100*sizeof(int)是为了适应柔性数组成员的大小type_a* p = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int));if (NULL == p){printf("malloc fail\n");exit(-1);}p->i = 100;int i = 0;for (i = 0; i < 100; i++){p->arr[i] = i;}for (i = 0; i < 100; i++){printf("%d ", p->arr[i]);}free(p);p = NULL;return 0;
}

柔性数组的使用还可以这样完成。

#include<stdio.h>
#include<stdlib.h>
typedef struct st_type
{int i;int *a;//柔性数组成员
}type_a;
int main()
{type_a* p = (type_a*)malloc(sizeof(type_a));if (NULL == p){printf("malloc fail\n");exit(-1);}p->i = 100;p->a = (int*)malloc(p->i * sizeof(int));if (p->a == NULL){printf("p->a malloc fail\n");exit(-1);}int i = 0;for (i = 0; i < 100; i++){p->a[i] = i;}for (i = 0; i < 100; i++){printf("%d ", p->a[i]);}free(p->a);p->a = NULL;free(p);p = NULL;return 0;
}

比较两种方式,哪一种更好呢?第一种方法会更好一点。

1.方便内存释放

2.有利于访问速度连续的空间有利于提高访问速度,也有利于减少内存碎片)。

7 C/C++中程序内存区域划分

在这里插入图片描述

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

http://www.mrgr.cn/news/41409.html

相关文章:

  • 【重学 MySQL】五十一、更新和删除数据
  • 在manim中实现抛物线的切线和它的视觉效果
  • 数据结构--集合框架
  • 关于 Kamailio|OpenSIPS 双中心之简介
  • 【计算机网络】传输层UDP和TCP协议
  • Ascend C 算子运行资源管理简介
  • TS(type,属性修饰符,抽象类,interface)一次性全部总结
  • 【MySQL】数据类型
  • 【C++】“list”的介绍和常用接口的模拟实现
  • 快餐时代读快书,十分钟左右就能读懂一本书
  • C++初学者指南-5.标准库(第二部分)–特殊迭代器
  • 成为AI产品经理,应该具备哪些条件?
  • 【JavaEE初阶】深入理解多线程阻塞队列的原理,如何实现生产者-消费者模型,以及服务器崩掉原因!!!
  • 【Linux进程间通信】Linux匿名管道详解:构建进程间通信的隐形桥梁
  • 二叉树进阶练习——根据二叉树创建字符串
  • 高斯消元 笔记
  • 110.WEB渗透测试-信息收集-ARL(1)
  • L8910 【哈工大_操作系统】CPU管理的直观想法多进程图像用户级线程
  • go结构体默认值和校验器(go-defaults、go-validator)
  • 【C++】C++基础