【C语言基础】:动态内存管理(含经典笔试题分析)-2

简介: 【C语言基础】:动态内存管理(含经典笔试题分析)

【C语言基础】:动态内存管理(含经典笔试题分析)-1

https://developer.aliyun.com/article/1538335


4. 常见的动态内存错误

4.1 对NULL指针的解引用操作

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

8c89def6b50c8a51679ac4c704716f71_b9a76bacf8c54cb1b6f33cb518fe703c.png

这里一定要对p进行判断,避免p是一个空指针。


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

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

malloc申请的空间和数组非常相似,都是一个连续的空间,所以要对边界进行把控,避免越界访问。


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

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

注意:free只能释放动态申请的空间,而局部变量是在栈区的,无法用free释放。


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

int main()
{
  int* p = (int*)malloc(100);  // 申请100个字节大小的空间
  if (p == NULL)
  {
    return 1;
  }
  for (int i = 0; i < 10; i++)
  {
    *p = i + 1;
    p++;  // 这里的p不再是动态申请的内存的起始位置
  }
  free(p);
  p = NULL;
  return 0;
}

这里的p已经不再指向起始位置,不能对其使用free进行释放。


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

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

这种对动态内存重复释放也是错误的,但可以避免,就是在第一次释放后及时将p置为空指针。


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

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

动态申请空间未释放会导致这一部分内存无法再次被申请,会一直占用,导致内存泄漏。

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


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

【题目1】:


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

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


分析:Test函数里面调用GetMemory函数,而GetMemory函数里面开辟了一个100个字节大小的空间,但由于是传值调用,出GetMemory函数时这个申请的空间就被销毁了,所以Test函数里的str还是一个空指针,将字符串拷贝到空指针中必定会对空指针进行解引用操作,导致程序崩溃。

解决:GetMemory使用传址调用,直接对str进行开辟空间,使用完之后及时对开辟空间进行释放,str置为空指针。


【题目2】:


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

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


分析:Test函数里面将GetMemory的返回值返回给str,但由于GetMemory函数调用后p就被销毁了,导致str成为了一个野指针,无法打印hello world,这就是返回栈空间地址的问题。


【题目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 函数会有什么样的结果?


分析:Test函数里面将str进行传址调用,在GetMemory函数里面申请100个字节大小的空间,将hello拷贝到str所指向的空间中,但是使用之后并没有使用free函数进行释放,导致内存泄漏。

解决:申请的空间使用完之后要使用free函数进行释放,并将str置为空指针。


【题目4】:


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

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


分析:Test函数里面str申请了100个字节的空间,将hello拷贝到str所指向的空间中,就直接用free释放掉了,导致str成了野指针,之前将hello拷贝到str中,所以str一定不是空指针,所以下面的if语句一定会执行,打印str空间里的内容会对野指针进行操作导致程序崩溃。


二、柔性数组

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

【示例】:


struct S
{
  int i;
  int arr[0];  // 柔性数组成员
  //有些编译器会报错⽆法编译可以改成:int arr[];
};

1. 柔性数组的特点

  • 结构中的柔性数组成员前面必须至少一个其他成员。
  • sizeof 返回的这种结构大小不包括柔性数组的内存。
  • 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大于。

【示例】:


struct S
{
  int n;//4个字节
  int arr[];//柔性数组
};
int main()
{
  printf("%zd\n", sizeof(struct S));
  return 0;
}

200fb3e72dddd071e23d4c6c407d29e6_23a575443e644e649cc8e4967f5f86f0.png

sizeof只计算不包括柔性数组的内存,所以柔性数组成员前面必须至少一个其他成员,不然结构体的大小将为0。


2. 柔性数组的使用

【示例】:

代码1


#include<stdlib.h>
struct S
{
  int n;//4个字节
  int arr[];//柔性数组
};
int main()
{
  struct S* ps = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
  if (ps == NULL)
  {
    perror("malloc");
    return 1;
  }
  ps->n = 100;
  for (int i = 0; i < 5; i++)
  {
    ps->arr[i] = i;
  }
  // 释放
  free(ps);
  ps = NULL;
  return 0;
}

2c8a84315083c785779fec39ebaf7a5e_371854b6b8364d35896e6f2c8cc8280d.png

malloc函数中sizeof(struct S)计算的是除了柔性数组的空间,后面的才是给柔性数组申请的空间。当然


代码2


struct S
{
  int n;
  int* arr;
};
int main()
{
  struct S* ps = (struct S*)malloc(sizeof(struct S));
  if (ps == NULL)
    return 1;
  ps->arr = (int*)malloc(5 * sizeof(int));
  if (ps->arr == NULL)
    return 1;
  ps->n = 100;
  for (int i = 0; i < 5; i++)
  {
    ps->arr[i] = i;
  }
  // 调整空间大小
  int* ptr = (int*)realloc(ps->arr, 10 * sizeof(int));
  if (ptr == NULL)
    return 1;
  ps->arr = ptr;
  // 使用
  //...

  // 释放
  free(ps->arr);
  free(ps);
  return 0;
}

3. 柔性数组的优点

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

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

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


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

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


三、C/C++中程序内存区域划分

9640d8bc6b53aa780eb08254f2340bae_70d5f7195b5841bb898fef9d3381a9b7.png


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


相关文章
|
16天前
|
Web App开发 监控 JavaScript
监控和分析 JavaScript 内存使用情况
【10月更文挑战第30天】通过使用上述的浏览器开发者工具、性能分析工具和内存泄漏检测工具,可以有效地监控和分析JavaScript内存使用情况,及时发现和解决内存泄漏、过度内存消耗等问题,从而提高JavaScript应用程序的性能和稳定性。在实际开发中,可以根据具体的需求和场景选择合适的工具和方法来进行内存监控和分析。
|
1月前
|
编译器 C语言
动态内存分配与管理详解(附加笔试题分析)(上)
动态内存分配与管理详解(附加笔试题分析)
49 1
|
4天前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
29 12
|
11天前
|
开发框架 监控 .NET
【Azure App Service】部署在App Service上的.NET应用内存消耗不能超过2GB的情况分析
x64 dotnet runtime is not installed on the app service by default. Since we had the app service running in x64, it was proxying the request to a 32 bit dotnet process which was throwing an OutOfMemoryException with requests >100MB. It worked on the IaaS servers because we had the x64 runtime install
|
21天前
|
Web App开发 JavaScript 前端开发
使用 Chrome 浏览器的内存分析工具来检测 JavaScript 中的内存泄漏
【10月更文挑战第25天】利用 Chrome 浏览器的内存分析工具,可以较为准确地检测 JavaScript 中的内存泄漏问题,并帮助我们找出潜在的泄漏点,以便采取相应的解决措施。
135 9
|
24天前
|
C语言
【c语言】动态内存管理
本文介绍了C语言中的动态内存管理,包括其必要性及相关的四个函数:`malloc`、``calloc``、`realloc`和`free`。`malloc`用于申请内存,`calloc`申请并初始化内存,`realloc`调整内存大小,`free`释放内存。文章还列举了常见的动态内存管理错误,如空指针解引用、越界访问、错误释放等,并提供了示例代码帮助理解。
36 3
|
25天前
|
并行计算 算法 IDE
【灵码助力Cuda算法分析】分析共享内存的矩阵乘法优化
本文介绍了如何利用通义灵码在Visual Studio 2022中对基于CUDA的共享内存矩阵乘法优化代码进行深入分析。文章从整体程序结构入手,逐步深入到线程调度、矩阵分块、循环展开等关键细节,最后通过带入具体值的方式进一步解析复杂循环逻辑,展示了通义灵码在辅助理解和优化CUDA编程中的强大功能。
|
1月前
|
程序员 编译器 C语言
动态内存分配与管理详解(附加笔试题分析)(下)
动态内存分配与管理详解(附加笔试题分析)(下)
46 2
|
1月前
|
编译器 程序员 C语言
深入C语言:动态内存管理魔法
深入C语言:动态内存管理魔法
|
1月前
|
存储 程序员 编译器
C语言——动态内存管理与内存操作函数
C语言——动态内存管理与内存操作函数