动态内存管理的知识点笔记总结

server/2024/11/29 8:42:05/

开始之前,我们解释一为什么存在动态内存分配?

在我们之前写的:

int arr[10]={0};   连续开辟40个字节的空间
int a=10;   在内存开辟4个字节

但是,

1.这种大小是固定死的,我们是无法改变的。

2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

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

对此,动态存的开辟就显得十分优异的做法了:

所以,开始我们的正文吧!

1.我们先了解各区的分布情况

一.malloc函数

头文件:#include <stdlib.h>

(内存块的大小,以字节为单位)

void* malloc (size_t size);

使用规则:

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

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

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

返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。

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

int main()
{int* pf=(int *)malloc(20);     这里要强制类型转换if(NULL==pf)                   
建议这里将NULL写在前面,因为不知道有没有一天,你会不小心将==写成了一个,
如果这样写的话它直接出现错误,告知你,
如果你写成if(pf=NULL),它不会显示错误的,对新手很不友好(不容易发现)。
问就是我做过{ perror("malloc");       
这里如果为空指针,打印出来,你也可以使用其他形式,我喜欢这种,方便return 1;  }return 0;
}

二.free函数

头文件:#include <stdlib.h>

1.作用:释放动态开辟的内存(只能动态)

2.如果参数str指向的空间不是动态,free的行为是未被定义的

3.str指向的空间是空指针,则什么事都不做。

int main()
{int num = 0;scanf("%d", &num);int arr[num] = {0};int* pf = NULL;pf= (int*)malloc(num*sizeof(int));if(NULL != pf)//判断pf指针是否为空{int i = 0;for(i=0; i<num; i++){*(pf+i) = 0;}}free(pf);释放pf所指向的动态内存pf = NULL;return 0;   主动弄为NULL,如果不弄可能会造成非法访问}

三.calloc

1.其实与malloc的用法差不多

区别:若把申请的值放得足够大
malloc申请到的空间,没有初始化,直接返回起始地址
calloc申请好空间后,会把空间初始化为0,然后返回起始地址

对此以后按照需求使用malloc还是calloc。

对于,我们要申请的内存空间,需要初始化时,使用这个就特别方便了。

2.使用规则:

void* calloc (size_t num, size_t size);

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

#include<stdio.h>
#include<stdlib.h>
int main()
{int *pf=calloc(20,sizeof(int)); if(NULL==calloc){perror("calloc");return 1;}free(pf);pf=NULL;return 0;
}

四.realloc

1.作用:扩容

realloc函数的出现让动态内存管理更加灵活。

有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,对此,我们会对内存的大小做灵活的调整

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

*pf 就是你要扩容的指针

size 调整之后新大小

返回值为调整之后的内存起始位置。

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

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

第一种:原有空间之后有足够大的空间

这种会直接在后面追加,原来空间的数据不会发生改变。

第二种:原有空间之后没有足够多的空间时

这时,我们应该在堆空间上另找一个合适大小的连续空间来使用。

这样函数返回的是一个新的内存地址

也就是把旧的空间的数据,拷贝新空间的前面位置,并且把旧的空间释放掉,同上返回新的空间的地址

使用时需要注意的一些情况:

#include <stdio.h>
int main()
{int *ptr = malloc(100);if(ptr != NULL){perror("malloc");}//扩展容量//代码1ptr = realloc(ptr, 1000);这样可以吗?(如果申请失败会如何?)直接返回NULL,如果像这样直接ptr,会把旧的数据搞丢这也是为什么,建议使用新的指针来//代码2int*p = NULL;p = realloc(ptr, 1000);    所以新指针if(p != NULL){ptr = p;}//业务处理free(ptr);return 0; }

列举一些使用动态内存使用时容易出现的错误:

1.对NULL指针的解引用操作

void test()
{
int *p = (int *)malloc(20);
*p = NULL;    问题出现在空指针这里
free(p);
}

失败的问题:当申请空间时太大而造成堆区内存申请的不够。此时,返回空指针。

试图通过空指针对数据进行访问,而导致运行时的错误,

程序试图通过解引用一个非空,(但实际确实是空)的数,会发生空指针解引用错误,导致成了未定义的行为。这种情况大多数的平台会导致程序异常和拒绝服务的情况。

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

int main()
{int *p=(int*)malloc(20);  //这里申请了4个intif(NULL==p){perror("malloc");return 1;}int i=0;for(i=0;i<=4;i++)     当=4时,它就超过了申请空间,是越界访问。此时就是野指针了{  *(p+i)=i;}free(p);p=NULL;return ;
}

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

void test()
{int a = 10;int *p = &a;free(p);p=NULL;return 0;
}

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

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

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

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

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

void test()
{int *p = (int *)malloc(100);if(NULL != p){*p = 20;}
}
int main()
{test();while(1);
}

!!!重要!!!!:动态开辟的空间一定要释放,并且正确释放

下面再给几道题,使得更加清晰的了解:

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

分析:

1.str传给p的时候,p是str的临时拷贝,有独立的空间
当GetMemory函数内部申请了空间后,地址放在p中时,str依然是NULL,
当GetMemory函数返回之后,strcpy拷贝的时候,形成了非法访问内存
2.在GetMemory函数内部,动态申请了内存,但是没有释放,会内存泄漏 

那么,怎么修改呢?
因为str本来就是一级指针,取地址后,那是不是变成了二级指针了
所以应该是char**p,而p里面放的是str的地址,想要找到str,
那么就应该是*p被赋值给函数

改正版本:

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

有小伙伴会问:为什么这个可以printf(str)?

例如:

char* p="hehe\n"
printf(“hehe\n”);
printf(p);
这两者本质上传的都是h元素的地址,所以一样的

同样,这个代码也是将放在str的地址
str是不是指向这个典型空间,然后strcpy
把hello world拷贝放在str里面,然后str不是指向那字符串的起始位置上吗?

题目二:

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

返回栈空间地址的问题:
这里虽然确实得到了p中首元素h的地址
但是当你出去返回函数了之后,char p[]里面的数就销毁了
但是出去了之后,在Test函数中记住了之前的的地址,就会进行访问,从而造成了非法访问

怎么改呢? -按照实际需求改就行
方法1:
改为char* p就可以了
为什么呢?“hello world”是一个常量字符串,这个就在内存里面存放着(某区域)
然后把这个地址交给p,p这个地方可以销毁,但是我把这个地址放到str里面去了,
所以仍然可以通过str去找到这个常量字符串,常量字符串并不是像刚刚的局部变量一样
局部变量进来创建,出去销毁
但是p是局部变量,p指向常量字符串,常量字符串并不是局部范围的一部分,所以并不会随函数返回而销毁
方法2
可以加static,因为这个不会还给操作系统

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

题目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:

void Test(void)
{char *str = (char *) malloc(100);strcpy(str, "hello");free(str);  <---注意,没有NULLif(str != NULL){strcpy(str, "world");printf(str);}
} 

虽然已经释放,不属于我们了,但是看清,它没有设为NULL,所以仍然指向h那里,但它已经没有权限访问了
在一步中,只要前面申请成功了,基本不会NULL,所以变成了野指针,将world\0覆盖到了hello\0里,强行,造成非法访问

柔性数组

C99 (才出现的)中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

struct S 
{int a=20;char arr[];   表示数组的大小是未知的也可以改成//char arr[0];   一些编译器不可这样,那就看两个哪个可以吧};

柔性数组的特点:

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

2.sizeof 返回的这种结构大小不包括柔性数组的内存。

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

柔性数组结构体中计算大小时,不算进去

struct S
{int i;int a[0];//柔性数组成员
};
int main()
{printf("%d\n", sizeof(struct S));
}答案为4

 柔性数组的使用

int main()
{int i = 0;struct S *p = (struct S*)malloc(sizeof(struct S*)+100*sizeof(int));//业务处理p->i = 100;for(i=0; i<100; i++){p->a[i] = i;}free(p);    这里释放了一次p=NULL;return 0;
}

不使用柔性数组:

typedef struct S
{int i;int *p_a;
}S;int main()
{ S *p = malloc(sizeof(S));p->i = 100;p->p_a = (int *)malloc(p->i*sizeof(int));//业务处理for(i=0; i<100; i++){p->p_a[i] = i;}//释放空间free(p->p_a);p->p_a = NULL;     这里释放了两次free(p);p = NULL;

总结:

好处:1.方便内存释放   

           2.这样有利于访问速度
 

最后的最后,让我们一起在心中默念点赞今天努力的自己吧!

点赞我们无论多么困难,仍不断奋斗,在努力使自己变强的路上,做一个孤独的奋斗者!


http://www.ppmy.cn/server/145858.html

相关文章

C++和C中的volatile 关键字

在 C/C 中volatile 关键字的作用 1.防止编译器优化 编译器在编译程序时&#xff0c;为了提高程序的执行效率&#xff0c;会对代码进行优化。例如&#xff0c;当编译器发现一个变量的值在一段代码中没有被显式地改变时&#xff0c;它可能会将这个变量的值缓存到寄存器中&#…

AWS海外注册域名是否需要实名认证?

在全球化的互联网环境中&#xff0c;注册域名已成为企业和个人建立在线存在的重要步骤。亚马逊网络服务&#xff08;AWS&#xff09;作为全球领先的云服务提供商&#xff0c;其域名注册服务也备受关注。然而&#xff0c;对于在AWS上注册海外域名是否需要实名认证&#xff0c;许…

javax.net.ssl.SSLHandshakeException: Received fatal alert: protocol_version

查了一上午&#xff0c;这个错误的原因好像是 发送端和接收端采用的 TLS 协议版本不一致导致的&#xff1a; 解决步骤 确认双方使用的 TLS 协议版本更改一方的 TLS 使两方相同 1.确认双方使用的 TLS 协议版本 捣鼓了半天&#xff0c;终于发现一个简单靠谱的方法来确认双方的…

深入理解Go语言中的`sync.Pool`与常规内存分配

在Go语言的并发编程中&#xff0c;内存管理是一个不可忽视的话题。sync.Pool作为Go标准库中的一个特殊工具&#xff0c;提供了一种对象池化机制&#xff0c;以优化内存分配和垃圾回收&#xff08;GC&#xff09;。本文将深入探讨sync.Pool与常规内存分配的主要区别&#xff0c;…

NR 5G SIB1读取失败应该怎么办?

如上图UE SIB1 read fail导致UE无法获取cell 的camp info&#xff0c;进而将对应cell bar 300s&#xff0c;也有bar 30s的设定。 这里的根据在38.331和38.304中。 如果UE不能获取MIB &#xff0c;根据38.304的描述&#xff0c;UE可能会将对应freq/pci对应的小区 最多bar 300s &…

人工智能 实验8 搜索技术:A*八数码,一字棋游戏

每日例行赊赞 一、实验目的 &#xff08;1&#xff09;掌握搜索技术的相关理论&#xff0c;能根据实际情况选取合适的搜索方法&#xff1b; &#xff08;2&#xff09;进一步熟悉盲目搜索技术&#xff0c;掌握其在搜索过程中的优缺点&#xff1b; &#xff08;3&#xff09;…

ctfshow

1,web153 大小写绕过失败 使用.user.ini 来构造后⻔ php.ini是php的⼀个全局配置⽂件&#xff0c;对整个web服务起作⽤&#xff1b;⽽.user.ini和.htaccess⼀样是⽬录的配置⽂件&#xff0c;.user.ini就是⽤户⾃定义的⼀个php.ini&#xff0c;我们可以利⽤这个⽂件来构造后⻔和…

html+css+js打字游戏网页

1. 效果 2. html代码 <!doctype html> <html><head><meta charset"utf-8" /><title>打字练习</title><!--引入第三方动画库--><link rel"stylesheet" href"animate.css"><style>html {h…