C语言(动态内存管理)

一、动态内存分配

1.1 为什么会出现动态内存分配

我们已知的内存开辟方式,大多是以 int a或者 int arr[10] 这种。

int a; 表示在栈空间中申请4个字节用来放一个整型。

int arr[10],在栈空间中申请连续的40个字节,用来存放10个int类型。

这两种方式的特点就是:
(1)空间开辟大小是固定的,无法变化
(2)数组在申明的时候,必须指明数组的长度,他所需要的内存存在编译时分配

但是有时候我们需要的空间大小在程序运行的时候才能知道,是有变大变小的需求的,这种时候就需要动态内存分配了

1.2 动态内存分配函数

下面四个函数头文件都为 <stdlib.h>
malloc

  • void* malloc (size_t size);    向内存申请一块连续可用的空间,并返回指向这块空间的指针
  • 创建size个字节的空间,创建成功返回指向这个空间的指针,创建失败则返回NULL
  • 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
  • 注意如果创建失败了,但是访问了,是非法访问。因为空指针是不能访问的,所以malloc创建空间后,要进行一次是否为空指针的判断
  • 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。

free

  • void free (void* ptr);    动态内存的释放和回收
  • 使用场景:在动态内存开辟之后,如果要把这个内存主动释放掉,就需要free函数,如果没有主动释放,程序结束之后,会自动销毁。但在程序结束之前,有【闲置】的隐患
  • free释放空间是指,用户没有了对这块区域的使用权限,但是本身的值是没有改变的,为了避免误用指向释放空间的指针,在free使用后,需要赋值NULL
  • 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
  • 如果参数 ptr 是NULL指针,则函数什么事都不做。

calloc

  • void* calloc (size_t num, size_t size);
  • 为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0
  • 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0
  • 相比malloc,效率更低(多了初始化的操作)

realloc

  • void* realloc (void* ptr, size_t size);    重新分配空间
  • 使用场景:我想要这个空间的大小改变,变大或变小。
  • ptr指的是要重新分配空间的那个空间的地址,size指的是重新分配的空间的新大小,返回值为调整之后的内存起始位置
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
  • realloc在调整内存空间的是存在两种情况:
    • 情况1:原有空间之后有足够大的空间
      • 要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化(返回旧的起始地址)
    • 情况2:原有空间之后没有足够大的空间
      • 原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用,把旧空间的数据拷贝到新空间,并且把旧的空间释放掉,这样函数返回的是一个新的内存地址
  • (int*)realloc(NULL,40); == malloc(40);

1.3 常见的动态内存分配错误

对NULL指针的解引用操作

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

解析:
INT_MAX是个极大的数字,除以4之后依旧很大,堆区里无法开辟如此大的空间,所以开辟失败,返回空指针。解引用空指针,是非法操作

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

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;
 }
 free(p);
}

解析:
malloc里面开辟的是10个int类型的字节大小,即40字节大小。但是只会for循环访问却是共访问了11个int类型,当i=10的时候,会发生越界访问

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

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

解析:
如果free形参指向的空间不是动态开辟的,那free函数的行为是未定义的

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

void test()
{
 int *p = (int *)malloc(100);
 p++;
 free(p);
}

解析:
p原本指向新开辟的内存空间,但是后置++后,p指的空间就变成了新开辟空间的第四个字节的位置,因为p指向的不是动态内存的起始位置,所以free只释放了动态开辟内存的一部分,是非法的

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

void test()
{
 int *p = (int *)malloc(100);
 free(p);      //但是如果在第一次释放之后,赋值为空,可以过。【空指针可以重复释放】
 free(p);//重复释放
}

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

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

解析
free的作用就是断开联系,将这块内存还给操作系统。这里指针p忘记释放了,同时因为p是局部变量,出了函数就要被销毁,p用不上这个空间了,别不知道这个空间的地址,也用不了,没人记得这块空间,所以也再也找不到这个空间了。

返回栈空间地址的问题

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

解析
p是str的临时拷贝,p所指向的空间和str的空间是分开的,所以下面有两个错误:
(1)str仍然是空指针,空指针是不能被访问的
(2)p是局部变量,会被销毁,也没有主动释放,无法再找到申请的这100个字节空间,会造成内存泄漏

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

解析
打印的结果是烫烫烫烫
str里面存的是p的地址,p里面存的是常量字符串首元素的地址,即’h’的地址,但是因为p是局部变量,出函数范围会被销毁,所以到printf函数的时候,str里面存的东西已经还回去了,再打印访问就是非法访问

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

解析
能正常打印,但是没有主动释放,存在内存泄漏的问题

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

解析
打印的结果是world,但是存在问题(非法访问)

free会释放空间,即【对于这块空间,没有权限访问】,但是里面的值未动,str依旧指的是’h’的地址,而后通过strcpy复制,所以打印的是world

访问没有权限的空间,是非法访问

二、C/C++程序的内存开辟

C/C++程序内存分配的几个区域:

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

三、柔性数组

3.1 什么是柔性数组

概念
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员

使用场景
数组的创建内存和数据一般是固定的,如int arr[10];但是如果我想要这个数组里面的空间变化,就需要柔性数组来实现空间的变大或变小

结构形式

typedef struct st_type
{
 int i;
 int a[0];//柔性数组成员    数组的大小是未知的
}type_a;
typedef struct st_type
{
 int i;
 int a[];//柔性数组成员     数组的大小是未知的
}type_a;

特点

  • 结构中的柔性数组成员前面必须至少一个其他成员(如果一个成员都没有,没办法确定内存大小,从而开辟空间)
  • sizeof 返回的这种结构大小不包括柔性数组的内存
  • 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

3.3 柔性数组的使用

int i = 0;
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
p->i = 100;
for(i=0; i<100; i++)
{
 p->a[i] = i;
}
free(p);       //这样柔性数组成员a,相当于获得了100个整型元素的连续空间

下面的代码可以实现同样的效果

typedef struct st_type
{
 int i;
 int *p_a;
}type_a;

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;

区别
方法1相比于方法2有两个好处:

方便释放内存
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给
用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你
不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好
了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。

有利于访问速度
连续的内存有益于提高访问速度,也有益于减少内存碎片。(虽然都需要用偏移量的加减来找位置,并没有省很多)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值