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

我们已经掌握的开辟方式:

int val = 20;// 在栈空间上开辟四个字节
char arr[10] = { 0 };//在栈空间上开辟10个连续的空间

上述开辟空间由两个特点:

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

2. 数组在声明的时候,必须指定数组长度,数组空间一旦确定大小就不能调整。


C语言引入了动态内存开辟,让程序员自己可以申请和释放空间。

2. malloc 和 free 函数

2.1 malloc函数

#include<stdlib.h>

void* malloc( size_t size );

功能:向内存的堆区申请一块连续可用的空间,并返回指向这块空间的起始地址

参数:

size:要分配的内存块的字节数。

返回值:

开辟成功:返回这块空间的起始地址。

开辟失败:返回一个NULL指针,因此一定要检查malloc的返回值。

注:

1. 返回值的类型是 void* ,具体在使用的时候需要我们强转为我吗需要的类型。

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

2.2 free函数

#include<stdlib.h>

void free( void* ptr );

功能:释放先前动态分配函数(malloc、calloc、realloc)申请的内存空间。

参数:

ptr:指向要释放内存的指针。

注:

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

2. 如果参数 ptr 是NULL指针,则函数不运行。

代码演示:

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

int main()
{

int num;
scanf("%d",&num);
int arr[num];

int* ptr =(int*)malloc(num * sizeof(int));

if(ptr != NULL)
{
 for(int i = 0;i < num;i++)
{
  *(ptr + i) = 0;
}else
{
  perror("malloc");
  retrun 1;
}

free(ptr);
ptr = NULL;

return 0;
}

3.calloc 和 realloc 函数

3.1 calloc函数

#include<stdlib.h>

void* calloc( size_t num , size_t size );

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

(与malloc函数的区别在于,会把每个字节初始化为0)

代码示例:
 

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

int main()
{

int* p = (int*)calloc(10,sizeof(int));
if(p != NULL)
{
  for(int i = 0;i < 10;i++)
{
  printf("%d ",*(p + i);
}

free(p);
p = NULL;

return 0;
}

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


3.2 realloc函数

#include<stdlib.h>

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

功能:重新调整之前分配的内存块大小,它可以在不丢失原有数据的情况下,扩大或缩小动态分配的内存块。

参数:

ptr:指向要调整的内存空间的起始地址,如果 ptr 为NULL,realloc函数的功能类似于malloc函数。

size:调整后的内存块大小,单位字节。

返回值:

成功:返回新内存块的起始地址(可能与原地址不同)。

失败:返回NULL,原内存块保持不变。

注意事项:

realloc在调整内存空间大小的时候,存在两种情况:

情况1:原有空间后面又足够大的空间,要扩展的空间就直接在原来内存后面追加空间,原来空间的数据不发生变化,最终返回的地址还是旧地址。

情况2:原有空间之后没有太多空间,会在内存堆区寻找新的满足要求的空间,返回新的起始地址,事件过程如下:

(1)寻找新的空间

(2)将旧空间的数据拷贝到新空间,保证数据不丢失

(3)释放旧空间,返回新空间的起始地址

代码演示:

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

int main()
{
  int* ptr = (int*)malloc(100);
  if(ptr != NULL)
{
 *(ptr + i) = i; 
}else{
  perror("malloc");
  retrun 1;
}

//代码1,直接放到原指针中
ptr = (int*)realloc(ptr,1000);

//代码2,先放到其他指针中进行判断

int* p = realloc(ptr,1000);
if( p != NULL )
{
  ptr = p;
  p = NULL;
}

free(ptr);
ptr = NULL;

return 0;
}

4.常见的动态内存的错误

4.1 对NULL指针的解引用操作

void test()
{
  int* p = (int*)malloc(INT_MAX/4);
  *p = 20;// 如果指针p是NULL,就会报错
  free(p);
  p = NULL;
}

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

void test()
{
  int* p = (int*)malloc(10 * sizeof(int));
  if(p == NULL)
{
  return 1;
}
for(int i = 0;i <= 10;i++)
{
  *(p + i) = i;//当i为10时越界访问
}

 free(p);
 p = NULL;
}

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

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

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

void test()
{
  int* p = (int*)malloc(100);
  p++;
  free(p);// p不在指向动态内存的起始空间,空间未完全释放。
}

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

void test()
{
  int* p = (int*)malloc(10);
  if(p == NULL)
{
  return 1;
}

free(p);
free(p);//重复释放
}

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

void test()
{
  int* p = (int*)malloc(100);
  if(p == NULL)
{
  rturn 1;
}else
{
  *p = 20;
}
}

int main()
{
  test();
 
 return 0;
}

动态开辟内存一定要释放,并且正确释放。


5.动态内存管理经典笔试题分析

5.1 题目1:

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

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

 return 0;
}

这里str为传值调用,str并没有接收到开辟空间的起始地址,依旧为 NULL。

5.2 题目2:

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

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

void Test(void)
{
  char *str = NULL;
  str = GetMemory();
  printf("%s\n",str);
}

int main()
{
  Test();
  return 0;
}

问题:'p'是函数里的临时数组,函数一结束就被系统回收了,把它的地址返回去,外面拿到的地址已经无效。

改法:用 'malloc' 分配内存并返回,或者直接返回字符串常量 "hello world"。

5.3 题目3:

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

void GetMemory(char** p, int num)
{
  *p = (char*)malloc(num);
}

void Test(void)
{
  char* str = NULL;
  GetMemory(&str, 100);
  strcpy(str, "hello");
  printf("%s\n",str);
}

int main()
{
  Test();

  return 0;
}

问题:没检查 'malloc' 是否成功、没用 'free'释放空间。

5.4 题目4:

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

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

int main()
{
  Test();
  return 0;
}

问题:free(str) 之后没有把 str 置为 NULL,导致 if( str != NULL) 判断为真,接着对已释放的内存执行 strcpy 和 printf,这是 "释放后使用" 的未定义行为。


6.柔性数组

在C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组。

struct Example
{
    int len;         // 其他成员
    int data[];      // 柔性数组,不占结构体本身的空间
};

柔性数组特点:
1. 前面必须至少有一个其他成员。

2. 只能作为结构体最后一个成员。

3. 声明时不写数组大小,语法为 type name[ ] 或 type name[0](0长度时GCC扩展,非标准,有些编译器写第二个语法时会报错)

4.柔性数组不占用结构体的内存大小,sizeof(struct Example)只包含前面的成员内存大小。

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

代码演示:

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

struct Stu
{
  int age;
  char name[];
}s;

int main()
{
 printf("%d\n",sizeof(s));

 return 0;
}

柔性数组的使用:

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

struct Stu
{
  int age;
  char name[];
};

int main()
{
                                              // 柔性数组获得10字节空间
  struct Stu* p = (struct Stu*)malloc(sizeof(s) + sizeof(char) * 10); 

  for(int i = 0;i < 10;i++)
{
  p->name[i] = i;
}

free(p); 
 
return 0;
}

7. 总结C/C++中程序内存区域划分

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

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

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

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

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


最后,感谢阅读,欢迎大家交流与指正。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐