动态内存的管理

目录

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

2.1malloc和free

2.1malloc

2.2free

3.calloc和realloc

3.1calloc

3.2realloc

4.常见动态内存错误

4.1对NULL指针的解引用操作

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

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

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

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

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

5.笔试题分析

5.1题目1:

5.2题目2:

5.3题目3:

5.4题目4:

5.5题目5:

5.6题目6:

6.柔性数组

6.1柔性数组特点:

6.2柔性数组的使用

6.3柔性数组的优势

7.c/c++内存区域划分


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

我们已经掌握的内存开辟方式有

int val=20;//栈空间开辟4个字节
char arr[10]={0};开辟10字节
这两个内存开辟是固定的
数组声明的时候必须说明数组长度,一旦确定长度就不能更改
但对于空间的需求是会发生变化的,因此编译时的开辟空间方式就不合适了
因此需要动态内存开辟,让程序员自己申请和释放空间,相对灵活
即使是变长数组,一旦被确定大小,也不能再改变了。

2.1malloc和free

2.1malloc

c语言提供了动态内存开发的函数:

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

可以将一个返回值赋给一个整型指针,这个指针变量的变量名可以理解为数组名(
如果设置了大于整型的字节数)

空间的释放,可以是用free函数,或者程序结束后,操作系统回收
malloc是在堆区上申请的内存空间

栈区上放:局部变量、形式参数、临时的变量
堆区:动态分配、malloc、free、calloc、realoc
静态区:静态变量、全局变量

2.2free

void free(void * ptr);
ptr指向的空间如果不是动态开辟的,那free函数的行为是未定义的
如果是NULL指针,则什么事都不做
malloc和free都在stdlib.h头文件中


 #include <stdio.h>
 #include <stdlib.h>
int main()
{
int num = 0;
scanf("%d", &num);
int arr[num] = {0};
int* ptr = NULL;
ptr = (int*)malloc(num*sizeof(int));
if(NULL != ptr)//判断ptr指针是否为空
{
int i = 0;
for(i=0; i<num; i++)
{
*(ptr+i) = 0;
}
}
free(ptr);//释放ptr所指向的动态内存
ptr = NULL;//避免成为野指针
return 0;

3.calloc和realloc

3.1calloc

用于动态内存分配

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

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

#include<stdio.h>
#include<stdio.h>

int main()
{
    int *p=(int *)calloc(10,sizeof(int));//申请有10个元素的整型数组空间
    if(NULL!= p)//假如p不是空指针
    {
        int i=0;
        for(i=0;i<10;i++)
        {
            printf("%d ",*(p+i));//将这10个整型大小的空间进行打印
        }
    }
    free(p);//释放p指针指向的空间
    p=NULL;//将p指针变成空指针
    return 0;
}

3.2realloc

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

有时我们在之前进行动态内存申请后,发现申请的空间还是小了,那么就可以用realloc调整动态内存开辟的大小

void * realloc(void*ptr,size_t size);
ptr是调整的空间的内存起始地址
size是调整之后的大小
返回值是调整之后的内存起始位置
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
realloc在调整内存空间的是存在两种情况:
原来空间之后有足够大的空间,还有一种是之后没有足够大的空间

如果是第一种情况,会直接追加空间,数据迁移后不发生变化,返回值的地址还是原来内存的起始地址

第二种情况,原有之后的空间不够,那么就会在堆区中另找一块区域存放,迁移数据后,返回的会是新的内存起始地址,并且释放旧的空间

#include <stdio.h>
#include <stdlib.h>

int main()
{
int *ptr = (int*)malloc(100);
if(ptr != NULL)
{
          //具体内容
}
else
{
return 1;//返回错误
}

//代码1 - 直接将realloc的返回值也就是该空间的内存起始地址放到ptr中
ptr = (int*)realloc(ptr, 1000);//
//代码2 - 先将realloc函数的返回值放在p中,不为NULL,在放ptr中
int*p = NULL;
p = realloc(ptr, 1000);
if(p != NULL)
{
ptr = p;
}
//具体内容
free(ptr);//释放空间
return 0;
}

num参数部分如果传入NULL,size参数正常,那么就是等于malloc(size),也就是申请了动态内存空间

4.常见动态内存错误

4.1对NULL指针的解引用操作

void test()
{
    int *p=(int*)malloc(INT_MAX/4);
    *p=20;//如果这个空间本身不存在,也就是说p指针指向的是空指针,
那么这时候解引用p指针后赋值,就会出现问题
}

4.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的时候越界访问,因为p是数组起始地址
            //那么p+9就已经是数组的最后一个元素了
            //p+10就会越界访问了
}
free(p);
}

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

void test()
{
int a = 10;
int *p = &a;
free(p);//这里对非动态内存使用了free释放
会造成编译器卡死,是错误行为
}

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

void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置,也是个错误行为
free的参数必须是某个空间的起始地址
}

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

void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
//如果想避免,可以在第二次释放前,记得将p指针指向的空间变位NULL
因为free如果指向了的是空指针,则什么都不会操作
}

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

void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}//因为等程序结束后操作系统才会回收,而这里while条件会一直成立
因此程序不会退出,还有些24小时都执行的程序,那么申请的空间不回收
之后可能还申请新的空间,那么最后空间不足
就造成了内存泄漏。

5.笔试题分析

5.1题目1:

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

//str这个变量本身存的是NULL。
调用函数时,将NULL这个值传给了p变量。
p变量本身存的值就是NULL。
后面申请了一块空间,并将申请的新空间
的起始地址传给了p变量本身,改变了p变量
本身的值,但问题是:
这个函数没有返回值,是void,且下面也没有
进行链式访问,因此当函数调用结束后,p指针本身
都会被销毁,而str指针指向的还是空指针;
这时再给空指针复制内容进去,就会报错。

而且没有对申请的空间进行释放,导致内存泄漏。

5.2题目2:

char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
首先str本身存的是NULL,接着调用函数GetMemory之后
,会常见一个字符数组,然后返回字符数组的首元素地址
str本身存的值就会变成字符数组的首元素地址,但问题是:
函数调用结束后,这个字符数组的空间也会被释放掉,那么
str存的地址,就变成了一个没有被申请使用的地址,也就是非法访问
那么就只会打印随机值(如果没有进行额外操作,那么可能还是原来的值,
一旦有新操作,那么这个空间可能就会被覆盖掉)了,
比如cc,打印出来就是烫烫烫

5.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);
}
唯一的问题就是没有free,释放空间
容易造成内存泄漏

5.4题目4:
 

void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
free之后没有将str赋值NULL,造成str变成了野指针
之后strcpy的操作就是非法访问了。
虽然结果可能可以直接打印,但这是没有新操作造成覆盖的前提下

5.5题目5:

int *f1(void)
{
    int x=10;
    return (&x);
}
将会返回野指针,因为函数调用结束后,
会释放空间

5.6题目6:

int *f2(void)
{
    int *ptr;
    *ptr=10;
    return ptr;
}
//因为没有初始化,所以ptr的值是随机的,
这时候对ptr进行解引用赋值,等于是对野指针进行
赋值操作,是错误行为。

6.柔性数组

 c99中,结构中的最后一个元素允许是位置大小的数组,这就叫做柔性数组成员

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

如果编译器报错,则可以改成
typedef struct st_type
{
int i;
int a[];//柔性数组成员
}type_a;

6.1柔性数组特点:

结构中的柔性数组成员前面至少一个其他成员

sizeof返回的结构大小不包括柔性数组的内存

包含柔性数组成员的结构用malloc函数进行动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小,比如1个普通成员,一个柔性数组,那么应该用动态分配,给结构一个预期足够容纳普通成员和柔性数组的内存大小

typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
int main()
{
printf("%d\n", sizeof(type_a));//输出的是4,是不包含柔性数组成员大小的
return 0;
}

6.2柔性数组的使用

//代码1
#include <stdio.h>
#include <stdlib.h>

int main()
{
int i = 0;
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
//因为sizeof(type_a)给的大小是不包含柔性数组的,所以这里我们应该还要加上
一定大小,以满足容纳柔性数组
p->i = 100;
for(i=0; i<100; i++)
{
p->a[i] = i;
}
free(p);
return 0;
}
这里我们还可以用realloc进行灵活调整空间大小。

6.3柔性数组的优势

//代码2
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{
int i;
int *p_a;
}type_a;

int main()
{
    type_a *p = (type_a *)malloc(sizeof(type_a));
    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;
return 0;
}
//跟之前的代码相比,这种代码需要释放两次内存空间,
而一般用户只能选择释放最外面的空间,但里面的成员的空间却不会选择释放,
容易造成内存泄漏
其次是容易造成内存碎片,让利用率变低,还有就是非连续的空间运行效率比连续空间慢

7.c/c++内存区域划分

内存空间:内核空间(用户的代码不能执行,只能执行操作系统的代码)-栈区(向下增长)-内存映射段(文件映射、动态库、匿名映射)-堆区(向上增长)-数据段-代码段。

全局变量和静态变量放在 数据段即静态区,可执行代码和只读常量放在代码段中;

局部变量放在栈区,动态开辟的空间是在堆区上开辟的。

栈区:执行函数时,函数内的局部变量的空间(存储单元)在栈上创建,函数结束,这些空间会释放掉。栈区内存分配运算内置于处理器的指令集中,效率高,但分配内存容量有限。主要存放函数内局部变量,函数参数、返回数据、返回地址

堆区:由程序员申请开辟动态空间,也由程序员释放,程序结束时由os即操作系统回收,分配方式类似于链表

数据段:存放全局变量和静态变量,程序结束后由系统释放

代码段:存放函数体(类成员函数和全局函数)的二进制代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值