当前位置:网站首页>C-动态内存管理
C-动态内存管理
2022-08-11 05:30:00 【CHAKMING1】
1. 为什么存在动态内存分配
int val = 10;
int arr[10] = {0};
这是我们已知的内存开辟的方式,第一条语句是在栈空间开辟一个整型大小的空间,将val的值放到内存里面。第二条语句是在栈空间开辟10个连续的整型大小的空间,分别对这些空间初始化为0.
上述开辟空间的方式有三个特点:
1.空间开辟的大小是固定的,无法改变。
2.数组在创建时,必须指定数组的长度,它所需的内存在编译时分配。
3.这些空间都是开辟在栈区的,函数结束时,会自动回收,不需要手动释放。
那么,我们可以理解为上述的开辟方式是静态开辟的,而且需要提前知道要分配的空间大小,那么,遇到不符合上述的要求时,这些方法就没办法满足我们了,就只能通过动态内存分配实现。
2. 动态内存函数介绍
2.1 malloc和free
c语言提供了一个动态内存开辟的函数:
void* malloc (size_t size);
这个函数的功能是向内存申请一块连续空间的空间,并返回指向这块空间的指针。
- 如果开辟成功,则返回一个指向这块空间的指针。
- 如果开辟失败,则返回一个NULL指针,通常我们会对开辟失败做相应处理。
- 返回值的类型时void*,因为malloc是可以开辟任何类型的空间,我们在具体使用的时候需要根据我们的需求进行强制类型转换,因为void*是无法进行解引用的。
- size_t 是一个无符号类型
- size 是需要开辟空间的字节大小,如果size为0,这种标准是为定义的,取决于编译器。尽量避免这些未定义的行为。
C语言专门提供了一个free函数,是用来对动态内存进行释放和回收,函数如下:
void free (void* ptr);
这个函数的功能是用来释放动态开辟的内存。
- ptr参数是一个void*类型的指针,可以接受任何的指针类型。
- 如果ptr指向的空间不是动态开辟的,这种在c语言标准是未定义的,尽量避免。
- 如果ptr是NULL指针,那么这个函数什么都不做。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *ptr = NULL;
prt = (int*)malloc(sizeof(int)*10); // 在内存开辟了十个整型空间的大小
if(ptr == NULL){
printf("开辟失败\n");
return 0;
}
int i = 0;
for(i = 0;i<10;i++)
*(ptr+i) = 0; // 等价于ptr[i] = 0;
free(ptr); // 释放ptr指向这块空间
ptr = NULL; // 这是防止ptr释放变成野指针,所以置为NULL
}
2.2 calloc
c语言还提供了一个函数calloc进行动态内存开辟,函数如下:
void* calloc (size_t num, size_t size);
- 函数的功能是开辟num个大小为size字节的空间,并且把每个空间的每个字节初始化为0
- 该函数与malloc函数的区别在于开辟的空间的值自动初始化为0。
举个例子:
#include <stdio.h> /* printf, scanf, NULL */
#include <stdlib.h> /* calloc, exit, free */
int main ()
{
int i,n;
int * pData;
printf ("请输入要创建几个int型的空间:");
scanf ("%d",&i);
pData = (int*) calloc (i,sizeof(int)); // 开辟i个int型的空间,全部为0
if (pData == NULL)
return 0;
free (pData);
return 0;
}
2.3 realloc
C语言提供了一个函数realloc,可以对动态申请的内存进行灵活的调整。函数原型如下:
void* realloc (void* ptr, size_t size);
- ptr是要调整的内存地址
- size是调整后的新大小
- 返回值为调整之后内存的起始位置
- 这个函数调整原内存空间大小的基础上,会将原来内存的数据移到新空间上。
- realloc函数调整会存在两种情况
- 情况1:原有空间之后有足够大的空间,这时内存会直接在原有空间后面直接追加空间,原来空间的数据不发生变化。
- 情况2:原有空间之后没有足够大的空间,这时会重新在堆空间(动态分配的都在堆空间开辟)找到一个合适大小的位置,将原有空间的数据移动到这个新空间上,再返回一个新的内存地址。
举个例子:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *ptr = (int*)malloc(100);
if(ptr == NULL)
return 0;
// 拓展容量
// 代码1
ptr = (int*)realloc(ptr,1000);
// 代码2
int *ptr_new = NULL;
ptr_new = (int*)realloc(ptr,1000);
if(ptr_new != NULL)
{
ptr = p;
}
free(ptr);
ptr = NULL;
return 0;
}
我们分析一下上述两种代码:
代码1:这样进行realloc函数看似好像没什么问题,给ptr指针指向的位置拓展1000个字节的大小,再用ptr指向这块新区域的起始位置,实际上也是这样的,但是如果申请失败呢?
我们知道申请失败的内存会返回一个NULL指针,那么这里假设我们申请失败,那么代码会变成ptr = NULL;这样子会造成内存丢失,原来的内存是没有被释放的,这就是内存泄漏的问题。
代码2:这段才是正确的处理,我们重新创建一个指针指向这块区域,即使申请失败也不会改变原来的指针。
3. 常见的动态内存错误
3.1 对NULL指针进行解引用
我们来看一段代码:
void test()
{
int *p = (int*)malloc(INT_MAX);
*p = 20;
free(p);
}
我们先来分析一下上述代码的问题,首先我们在内存开辟一个INT_MAX大小的区域,INT_MAX是整型范围内的最大值,系统肯定不会真的给这么大一块的区域给我们,这时候会造成申请失败,返回一个NULL指针,那么我们没有对申请失败的情况进行判断,会造成直接对p进行解引用。正确如下:
void test()
{
int *p = (int*)malloc(INT_MAX);
if(p == NULL)
return 0;
*p = 20;
free(p);
}
3.2 对动态开辟空间的越界访问
代码如下:
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
return 0;
}
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);
}
3.4 使用free释放一块动态开辟内存的一部分
void test()
{
int *p = (int *)malloc(100);
p++;
free(p); //p不再指向动态内存的起始位置
}
这段代码问题出在p++上面,在进行p++后,p指向起始位置后一个int大小的位置,这会造成我们在free这个p指针的时候,有一小块区域没有进行释放,造成出错。那么解决这种情况的办法如下:
void test()
{
int *p = (int *)malloc(100);
int *p1 = p;
p++;
free(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);
}
虽然,我们动态开辟的内存,在系统结束后自动释放,但是上面这种代码,如果一直循环下去,那么这个程序不会结束,也就不会释放,这个时候就会造成内存泄漏的问题。
4. 经典笔试题目
4.1 题目1
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
这段代码会有什么结果。
可能你会以为是ptrintf函数出现了问题,但其实没错,printf函数本身就可以直接打印一个字符串,不一定需要
printf("%s",str);
其实这段代码问题出在GetMemory函数上,我们在传参数的时候通常有两种方式,一种是传值,一种是传地址,那么传值的时候,函数会创建临时变量,看下面代码:
void fun(int x)
{
x = 10;
}
int main()
{
int a = 5;
fun(a);
printf("%d\n", a);
}
这是我们在调试过程中,发现了x和a两个的地址根本不是同一个,所以这个x只是a的一份临时拷贝,那么我们修改x,也不会对a的值有任何影响。我们再回到原来的面试题,我们传进去str,用一个p指针接收 ,这里传的只是一个值,并不是一个地址,那么我们只是对str的临时拷贝p进行分配内存,那么在这个函数退出的时候,p就会自动销毁掉。
那么,如何解决这种情况呢?
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();
}
只需要传str的地址进去,这里传的是一个一级指针的地址,需要用一个二级指针接收,*p访问的是这个二级指针存放的地址,再进行内存开辟。
4.2 题目2
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
这段代码能否顺利打印结果呢?
答案是不能的,这段代码不是一个动态分配的问题,他问题出在返回p这个指针的时候,返回的是一个什么东西。
我们看到GetMemory函数的内部,内部创建了一个字符数组,存放了一个字符串,之后将这个字符数组首元素的地址返回给str,那么在这个函数结束的时候,创建的p会自动释放了,这个时候返回的一个地址也确确实实是p原本的地址,但是这一块内存已经没有原来的数据了。我们之后进行打印,也会是打印一些奇奇怪怪的值,而不是我们想要的结果。
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);
}
这段代码看似没有任何问题,也是通过用传地址的方式动态开辟内存,但是有一个很多新手都会犯的错误,就是忘记释放,这段代码在使用完这个内存,并没有进行释放。
4.4 题目4
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
这段代码的问题比较明显,那么就是对已经被free函数释放的指针继续使用。
5. C/C++程序的内存开辟
我们刚刚提到了栈空间、堆空间,那么这些是什么意思呢?有什么关联?
其实,在我们的内存中有分为好几块区域:
- 栈区(Stack) : 在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结
束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是
分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返
回地址等。 - 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分
配方式类似于链表。 - 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
- 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
6. 柔性数组
柔性数组(flexible array),这是c99 新增的一个东西,允许结构中最后一个元素是未知大小的数组。
struct st
{
int i;
int a[]; // 柔性数组成员
};
6.1 柔性数组的特点:
- 结构中柔性数组成员前面至少包含一个其他成员
- sizeof返回的这种结构大小不包括柔性数组的大小
- 包含柔性数组成员的结构用malloc函数进行内存的动态分配,而且分配的内存应大于结构的大小。
struct st
{
int i;
int a[]; // 柔性数组成员
};
sizeof(struct st); // 结果为4
6.2 柔性数组的使用
typedef struct st
{
int i;
int a[]; // 柔性数组成员
}st;
int main(){
//代码1
int i = 0;
st *p = (st*)malloc(sizeof(st)+100*sizeof(int));
p->i = 100;
for(i=0; i<100; i++)
{
p->a[i] = i;
}
free(p);
}
你可以理解对这个结构进行malloc相当于对它的柔性数组成员进行malloc,上面这段代码就得到了100个整型元素的连续空间。
边栏推荐
- [Meetup Preview] OpenMLDB+OneFlow: Link feature engineering to model training to accelerate machine learning model development
- heap2 (tcache attack,house of orange)
- 微信小程序云开发项目wx-store代码详解
- Day 70
- Wonderful linkage | OpenMLDB Pulsar Connector principle and practical operation
- Js method commonly used objects and attributes
- 星盟-pwn-babyheap
- vim 编辑器使用学习
- OpenMLDB Pulsar Connector:高效打通实时数据到特征工程
- Day 69
猜你喜欢
Open Source Machine Learning Database OpenMLDB Contributor Program Fully Launched
Day 84
Jetpack's dataBinding
The official website of OpenMLDB is upgraded, and the mysterious contributor map will take you to advance quickly
活动预告 | 4月23日,多场OpenMLDB精彩分享来袭,不负周末好时光
虚拟机更改IP地址
127.0.0.1 connection refused
OpenMLDB Pulsar Connector: Efficiently connect real-time data to feature engineering
Day 76
He Kaiming's new work ViTDET: target detection field, subverting the concept of layered backbone
随机推荐
Scene-driven feature calculation method OpenMLDB, efficient implementation of "calculate first use"
编译异常解决
OpenMLDB + Jupyter Notebook: Quickly Build Machine Learning Applications
OpenMLDB: Consistent production-level feature computing platform online and offline
Intelligent risk control China design and fall to the ground
Day 69
厂商推送平台-华为接入
jdbc接口文档参考,jdbc接口方法逻辑探究
精彩联动 | OpenMLDB Pulsar Connector原理和实操
Day 73
Interpretation of the paper: Cross-Modality Fusion Transformer for Multispectral Object Detection
JS事件循环机制
星盟-pwn-babyfmt
ARM assembly instruction ADR and LDR
Byte (byte) and bit (bit)
Day 82
本地服务配置内网穿透实现微信公众号整合
Compilation exception resolution
Manufacturer Push Platform-Huawei Access
js learning advanced (event senior pink teacher teaching notes)