[C]语言动态内存管理

简介: [C]语言动态内存管理

前言

者:小蜗牛向前冲

名言我可以接收失败,但我不能接收放弃

如果觉的博主的文章还不错的话,还请 点赞,收藏,关注👀支持博主。如果发现有问题的地方欢迎❀大家在评论区指正。 大家好啊!小蜗牛又来为大家分享新的文章了。不知道大家有没有在使用数组时发现超出空间的情况,这时我们往往要重新定义数组的大小。那么这个数组的空间到底定义多大呢?定小了可能等下数组空间又不够了,定大了可能会存在许多内存空间的浪费。那么有什么更好的办法解决吗?

有的,那就是动态内存分配,下面博主会为大家一一道来。

一为什么存在动态内存分配

我们以往开辟空间的方式:

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

特点:

1 开辟的空间大小是固定的。

2数组在声明时,必须指数组大小或者直接初始化数组,它所需要的内存在编译时分配

但有时侯,对于空间的需求在我们写代码的时候是并不知道的,要编译完之后才会知道,这就导致我们可能又要去修改分配内存空间的大小,这是不便的。这时候我们便可以试试用动态内存分配来解决这个问题。

二动态内存函数的介绍

1 malloc函数

C语言提供了一个动态内存开辟的函数:

参数

stze:

内存块的大小,以字节为单位。
size_t是无符号整数类型。

返回值

成功时,指向函数分配的内存块的指针。

此指针的类型始终为 void*,可以将其转换为所需类型的数据指针,以便可取消引用。

如果函数未能分配请求的内存块,则返回空指针。

注意:

malloc函数成功开辟空间返回的是,指向开辟空间的指针

开辟失败的时候返回的是,一个空指针(NULL),所以在用malloc函数开辟空间时一定要去判断是否能否开辟成功。

返回值是void*,所以在接收用malloc开辟的空间时,要强转为自己需要的类型

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

2 free函数


C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:

参数

ptr

指向先前使用 malloc、calloc 或 realloc 分配的内存块的指针。

注意:

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

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

如果参数 ptr 是NULL指针,则函数什么事都不做。

alloc和free都声明在 stdlib.h 头文件中

代码举例:

int main()
{
  //动态内存的开辟
  int* ptr= (int *)malloc(40);
  //判断空间是否开辟成功
  int i = 0;
  if (ptr != NULL)
  {
    //使用
    for (i = 0;i < 10;i++)
    {
      *(ptr + i) = i;
    }
  }
  for (i = 0;i < 10;i++)
  {
    printf("%d ", *(ptr+i));
  }
  free(ptr);//回收空间
  ptr = NULL;//防止出现野指针
  return 0;
}

3 calloc函数

C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。原型如下:

作用

分配和零初始化数组

为 num 元素数组分配一个内存块,每个元素的大小都长字节,并将其所有位初始化为零。

有效结果是分配了零初始化的(数字*大小)字节的内存块

如果 size 为零,则返回值取决于特定的库实现(它可能是也可能不是空指针),但返回的指针不应被取消引用

参数

num

要分配的元素数。

size

每个元素的大小。

size_t 是无符号整数类型。

代码举例:

int main()
{
  int i = 0;
  scanf("%d", &i);//要分配的元素数
  int* data = (int*)calloc(i, sizeof(int));
  //判断
  if (data == NULL)
  {
    perror(data);
    return 1;
  }
  //使用
  int j = 0;
  for (j = 0;j < i;j++)
  {
    data[j] = j;
    printf("%d ", data[j]);
  }
  //释放
  free(data);
  data = NULL;
  return 0;
}

首先我来看道calloc函数的第一个功能,能将分配空间中的元素都初始化为0。

其次,否真的分配的内存空间。

所以如何我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。

4 realloc函数

有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时 候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小 的调整。

函数原型如下:

功能

更改 ptr 所指向的内存块的大小。

该函数可以将内存块移动到新位置(其地址由函数返回)。

内存块的内容将保留到新旧大小中较小的一个,即使该块被移动到新位置也是如此。如果新大小较大,则新分配部分的值不确定。

如果 ptr 是空指针,则该函数的行为类似于 malloc,分配一个新的大小字节块并返回指向其开头的指针。

参数

ptr

指向先前使用 malloc、calloc 或 realloc 分配的内存块的指针。

或者,这可以是一个空指针,在这种情况下,分配一个新块(就像调用malloc一样)。

size

内存块的新大小,以字节为单位。

size_t是无符号整数类型

realloc函数调整空间后存在二种情况

情况1:原有空间之后有足够大的空间

情况2:原有空间之后没有足够大的空间

对于情况1要扩展的空间直接在原有空间之间后扩展,原空间的数据不发生变化。

对于情况2由于后续空间不足以扩展,所以realloc函数会重新在堆区找足够大一块空间,把原空间的数据放到新空间中,并让ptr重新指向新空间。

代码举例:

int main()
{
  int* ptr = (int*)calloc(5, sizeof(int));
  if (ptr != NULL)
  {
    int i = 0;
    for (i = 0;i < 5;i++)
    {
      *(ptr + i) = i;
      printf("%d ", *(ptr + i));
    }
  }
  else
  {
    perror(ptr);//报错信息
  }
  printf("\n");
  //增容
  int* p = NULL;
  p = realloc(ptr, 10 * sizeof(int));
  printf("增容成功\n");
  if (p != NULL)
  {
    ptr = p;
    int i = 0;
    for (i = 0;i < 10;i++)
    {
      *(ptr + i) = i;
      printf("%d ", *(ptr + i));
    }
  }
  //释放
  free(ptr);
  ptr = NULL;
  return 0;
}

三 常见的动态内存错误

虽然我们在使用malloc,calloc和realloc开辟动态空间很方便,但也容易引起一些错误。下面我们就一起看看吧。

1 对NULL指针的解引用操作

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

其中INT_MAX是一个比较大的数,内存分配可能会失败,当分配空间失败时,p被置为NULL,后面对空指针解引用是不可以取的。

我们可以这样避免

void test()
{
  int* p = (int*)malloc(INT_MAX);
  if (p == NULL)
  {
    return 1;
  }
  *p = 20;//如果p是NULL,就会有问题
  free(p);//释放
}

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

void test()
{
  int i = 0;
  int* p = (int*)malloc(10 * sizeof(int));
  if (NULL == p)
  {
    perror(p);  
  }
  for (i = 0; i <= 10; i++)
  {
    *(p + i) = i;//当i是10的时候越界访问
  }
  free(p);
  p = NULL;

这里就告诉我们在写代码还是要多思考。

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

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

这里我们是对非动态空间继续释放,这肯定是不可取的,a变量开辟的空间是在栈区是,而free释放的空间是在堆区。

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);
}

忘记释放不再使用的动态开辟的空间会造成内存泄漏。

切记: 动态开辟的空间一定要释放,并且正确释放 。

几个经典的笔试题

学完上面的知识点,我们来实战在训练一下。

题目1:

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

请问运行Test 函数会有什么样的结果?

我们发现什么都没打印,为什么呢?

其中在于p变量是个形参,出函数就会销毁,那么分配的空间就找不到了,使用str并没有分到内存空间,就无法完成拷贝。

其实我们稍作改动将可以完成代码的实现,将传值调用改为传址调用就可以了。

题目2:

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

请问运行Test 函数会有什么样的结果?

我们发现打印出来了随机值,为什么呢?

这是因为函数使用完后就会销毁,p其实是个野指针,p指向的空间在出函数后就会被回收。

题目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);
}

请问运行Test 函数会有什么样的结果?

虽然结果是对的,但大家发现没malloc函数开辟的空间并没有被回收,这会造成内存空间的泄漏,所以我们在使用完动态函数开辟的空间后一定要回收内存。

题目4:

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

 请问运行Test 函数会有什么样的结果?

怎么会是world呢?不少会一疑问,str空间不是被free释放吗?

其实空间虽然是释放了,但str仍然记得那块空间的地址,我们*还是能找到那块空间,但是那块空间已经是不属于我们了,这时str相当于野指针是相当危险的。为了避免这种错误的结果出现,我们最好在free释放了str所指向的空间时,在将str置为空指针(str=NULL).

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

这里简单和大家分享一下,C/C++程序的内存开辟。

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

1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。

2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分 配方式类似于链表。

3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。

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

六 柔性数组

也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。 C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

下面就是柔性数组:

typedef struct s
{
  int a;
  int arr[];//柔性数组
}s;

1 柔性数组的特点

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

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

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

       

我们可以看到,这个结构体的大小是不包含柔性数组的大小的。

2 柔性数组的使用

//代码1
typedef struct s
{
  int a;
  int arr[];//柔性数组
}s;
 
int main()
{
  //代码1
  int i = 0;
  s* p = (s*)malloc(sizeof(s) + 100 * sizeof(int));//为结构体和柔性数组开辟空间
  //业务处理
  p->a = 100;
  for (i = 0; i < 100; i++)
  {
    p->arr[i] = i;
  }
  free(p);
  p = NULL;
  return 0;
}

用malloc为柔性数组开辟的100的连续的空间。

3 柔性数组的优势

下面我们在另外一个方式,实现代码1的结果。

//代码2
struct s
{
  int n;
    int* arr;
};
 
 
int main()
{
  struct s* p = (struct s*)malloc(sizeof(struct s));//为结构体开辟空间
  if (p == NULL)
  {
    return 1;
  }
  p->n = 100;
  p->arr = (int*)malloc(40);//为arr指针开辟空间
  if (p->arr == NULL)
  {
    perror("");//报错信息
    return 1;
  }
  //使用
  int i = 0;
  for (i = 0;i < 10;i++)
  {
    p->arr[i] = i;
  }
  //空间释放
  free(p->arr);
  free(p);
  p = NULL;
}

上述 代码1 和 代码2 可以完成同样的功能,但是 方法1 的实现有两个好处:

第一个好处是:方便内存释放

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

第二个好处是:这样有利于访问速度.

连续的内存有益于提高访问速度,也有益于减少内存碎片。

总结

在这篇博客中,我们主要学习了动态内存函数和柔性数组,对于动态内存函数我们主要还是要注意,使用后一定要记得用free释放空间,柔性数组的优点方便内存释放和提高访问速度。

最后送大家于自己一句话:

纸上得来终觉浅 绝知此事要躬行!

 


相关文章
|
6月前
|
存储 Go iOS开发
掌握Go语言:探索Go语言指针,解锁高效内存操作与动态数据结构的奥秘(19)
掌握Go语言:探索Go语言指针,解锁高效内存操作与动态数据结构的奥秘(19)
|
6月前
|
存储 缓存 安全
Go语言内存模型深度解析
【2月更文挑战第16天】Go语言以其简洁的语法、强大的并发编程能力和高效的内存管理而备受开发者青睐。本文将对Go语言的内存模型进行深度解析,探讨其内存布局、内存分配与回收机制以及内存安全等方面的内容,帮助读者更好地理解和应用Go语言的内存管理特性。
|
10天前
|
Java 编译器 测试技术
go语言避免不必要的内存分配
【10月更文挑战第18天】
23 1
|
11天前
|
存储 算法 Java
Go语言的内存管理机制
【10月更文挑战第25天】Go语言的内存管理机制
17 2
|
3月前
|
Rust 安全 程序员
揭秘Rust语言的内存安全秘籍:如何构建坚不可摧的系统级应用?
【8月更文挑战第31天】Rust语言凭借其独特内存安全机制在编程领域脱颖而出,通过所有权、借用与生命周期等概念,在保证高性能的同时避免了缓冲区溢出等常见错误。本文深入探讨Rust的内存安全机制,并通过示例代码展示如何利用这些机制构建高效且可靠的系统。尽管这些机制增加了学习难度,但为软件开发奠定了坚实基础,使Rust成为系统、嵌入式及网络编程的理想选择。随着社区的发展,Rust将在未来软件开发中扮演更重要角色。
81 0
|
5月前
|
算法 Java
垃圾回收机制(Garbage Collection,GC)是Java语言的一个重要特性,它自动管理程序运行过程中不再使用的内存空间。
【6月更文挑战第24天】Java的GC自动回收不再使用的内存,关注堆中的对象。通过标记-清除、复制、压缩和分代等算法识别无用对象。GC分为Minor、Major和Full类型,针对年轻代、老年代或整个堆进行回收。性能优化涉及算法选择和参数调整。
69 3
|
5月前
|
Rust 安全 开发者
探索Rust语言的内存安全特性
【6月更文挑战第8天】Rust语言针对内存安全问题提供了创新解决方案,包括所有权系统、借用规则和生命周期参数。所有权系统确保值与其所有者绑定,防止内存泄漏;借用规则保证同一时间只有一个可变引用或多个不可变引用,消除数据竞争和野指针;生命周期参数则强化了引用的有效范围,提升安全性。通过这些特性,Rust帮助开发者编写出更健壮、安全的高性能软件,有望成为系统编程领域的领头羊。
|
6月前
|
存储 安全 程序员
C++语言中的内存管理技术
C++语言中的内存管理技术
|
5月前
|
存储 C++ C语言
【C++语言】动态内存管理
【C++语言】动态内存管理
17.C语言内存函数
17.C语言内存函数