动态内存管理(万字讲解)(2)

简介: 动态内存管理(万字讲解)(2)

几个经典的笔试题

1.

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


程序运行起来之后直接崩溃了。

在这之前,我们再来看一段程序:


1.png

#include<stdio.h>
int main()
{
  char* str = "abcdef";
  printf("%s\n", str);
  printf(str);
  printf("abcdef");
  return 0;
}



程序运行起来之后会造成程序崩溃,而且程序存在内存泄露的问题。

那为什么会造成程序泄露呢?


str以值传递的形式给p

p是GetMemory函数的形参,只能函数内部有效。

等GetMemory函数返回之后,动态开辟内存尚未释放并且无法找到,所以会造成内存泄漏。


那我们如何解决问题呢?

法一(利用参数):


#include<stdio.h>
#include<string.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);
  free(str);
  str = NULL;
}
int main()
{
  Test();
  return 0;
}

1.1.png


法二(利用返回值):


#include<stdio.h>
#include<string.h>
#include<stdlib.h>
char* GetMemory(char* p)
{
  p = (char*)malloc(100);
  return p;
}
void Test(void)
{
  char* str = NULL;
  str = GetMemory(str);
  strcpy(str, "hello world");
  printf(str);
  free(str);
  str = NULL;
}
int main()
{
  Test();
  return 0;
}


2.

#include<stdio.h>
char* GetMemory(void)
{
  char p[] = "hello world";
  return p;
}
void Test(void)
{
  char* str = NULL;
  str = GetMemory();
  printf("str");
}
int main()
{
  Test();
  return 0;
}



请问运行之后会有什么现象?

2.png

我们可以看到结果是一些随机值。

首先这是一个返回栈空间地址的问题,栈空间的地址是不能随意返回的,否则就会出现问题。,局部变量是放在栈区上的,栈区的这块空间一旦出了这块空间就会销毁,既然销毁了,但依然把这块空间地址返回去,这是没有意义的。

所以在一个函数内部返回栈空间的地址是有问题的,返回来之后如果有指针记得住这块地址,然后盲目的去访问这个地址所指向的空间的时候,此时就造成了非法访问了,因为这块空间已经还给操作系统了。另外这块空间里面存放的是什么我们其实是不知道的,所以打印出来的是一个随机值。

再看一个与这道题类似的:


#include<stdio.h>
//这也是一个返回栈空间地址的问题
int* test()
{
  int a = 10;//栈区
  return &a;
}
int main()
{
  int* p = test();
  *p = 20;
  return 0;
}


我们可以这样写:


#include<stdio.h>
int* test()
{
  static int a = 10;//a此时在静态区
  return &a;
}
int main()
{
  int* p = test();
  *p = 20;
  return 0;
}


即把int a = 10;的前面加上static,就是static int a = 10;这样的话static修饰局部变量的时候,它的声明周期就变长了,此时变量a出来函数范围后不会销毁,即这块内存空间还存在,那这个时候,指针p就能找到那块空间,*p就把里面的值给为了20。另外局部变量a被static修饰后就不放在栈上了,而是放在静态区中。

总之,返回栈空间地址是有危险的。


还有一种写法:


#include<stdio.h>
int* test(void)
{
  int* ptr = malloc(100);//堆区
  return ptr;
}
int main()
{
  int* p = test();
  return 0;
}
//注意这里没有释放空间


补充一点

int* f2(void)
{
  int* ptr;
  *ptr = 10;
  return ptr;
}


这是个错误的写法。因为*ptr是一个野指针,并没有初始化,默认其内部放的是一个随机值,对其进行解引用(ptr=10)的话就相当于访问一个随机的空间,就会造成非法访问内存。


3.

#include<stdio.h>
void GetMemory(char** p, int num)
{
  *p = (char*)malloc(num);
}
void Test(void)
{
  char* str = NULL;
  GetMemory(&str, 100);
  strcpy(str, "hello");
  printf(str);
}
int main()
{
  Test();
  return 0;
}


这段代码的问题就是没有释放空间,我们可以这样改进:



         


4.

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void test()
{
  char* str = (char*)malloc(100);
  strcpy(str, "hello");
  free(str);
  if (str != NULL)
  {
  strcpy(str, "world");
  printf(str);
  }
}
int main()
{
  test();
  return 0;
}


这段代码考察的主要是free,要注意:free释放str指向的空间后,并不会把str置为NULL。free(str);之后,str成为野指针,if(str!=NULL)不起作用。所以这段代码存在非法访问内存的问题。我们需要在free释放空间后并把str置为NULL。请看:


#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void test()
{
  char* str = (char*)malloc(100);
  strcpy(str, "hello");
  free(str);
  str = NULL;
  if (str != NULL)
  {
  strcpy(str, "world");
  printf(str);
  }
}
int main()
{
  test();
  return 0;
}



额外知识(C/C++程序的内存开辟)

int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
  static int staticVar = 1;
  int localVar = 1;
  int num1[10] = { 1,2,3,4 };
  char char2[] = "abcd";
  char* pChar3 = "abcd";
  int* ptr1 = (int*)malloc(sizeof(int) * 4);
  int* ptr2 = (int*)calloc(4, sizeof(int));
  int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
  free(ptr1);
  free(ptr3);
}


4.png

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


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

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

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

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


实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。但是被static修饰的变量放在数据段(静态区),数据段的特点是在上面创建的变量,知道程序结束才销毁,所以生命周期变长。


柔性数组

柔性数组(flexible array)。在C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做【柔性数组】成员。


例如(首先来看第一种写法):


typedef struct st_type
{
  int i;
  int a[0];//柔性数组成员(作为结构体的最后一个元素,大小是未知的)
}type_a;


第二种写法:


struct S
{
  int n;
  int arr[0];//未知大小的柔性数组成员(与上面那种写法的形式不一样,但都是一个意思)
};
int main()
{
  struct S s;
  return 0;
}


柔性数组的柔性体现在这个数组的大小是可变的。

我们该如何使用柔性数组呢?


#include<stdio.h>
struct S
{
  int n;
  int arr[0];//与上面那种写法的形式不一样,但都是一个意思
};
int main()
{
  struct S s;
  printf("%d\n", sizeof(s));
  return 0;
}


运行结果如下:

5.png


在包含柔性数组成员的这个结构体中,当我们计算其大小时,是不包含这个柔性数组成员的。

既然柔性数组的大小是可变的,那我们如何调整其大小呢?


#include<stdio.h>
struct S
{
  int n;
  int arr[0];//与上面那种写法的形式不一样,但都是一个意思
};
int main()
{
  /*struct S s;
  printf("%d\n", sizeof(s));*/
  struct S* ps = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
  return 0;
}

6.png

其实当我们写出struct S* ps = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));这段代码的时候就相当于我们开辟了一块空间,空间大小为24个字节。


接下来我们来访问这块空间:


1

#include<stdio.h>
#include<stdlib.h>
struct S
{
  int n;
  int arr[0];//与上面那种写法的形式不一样,但都是一个意思
};
int main()
{
  /*struct S s;
  printf("%d\n", sizeof(s));*/
  struct S* ps = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
  ps->n = 100;
  int i = 0;
  for (i = 0; i < 5; i++)
  {
  ps->arr[i] = i;
  }
  return 0;
}

7.png

此时如果我们想继续调整这块空间呢?



#include<stdio.h>
#include<stdlib.h>
struct S
{
  int n;
  int arr[0];//与上面那种写法的形式不一样,但都是一个意思
};
int main()
{
  /*struct S s;
  printf("%d\n", sizeof(s));*/
  struct S* ps = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
  ps->n = 100;
  int i = 0;
  for (i = 0; i < 5; i++)
  {
  ps->arr[i] = i;
  }
  struct S* ptr = realloc(ps, 44);
  if (ptr != NULL)
  {
  ps = ptr;
  }
  for (i = 5; i < 10; i++)
  {
  ps->arr[i] = i;
  }
  for (i = 0; i < 10; i++)
  {
  printf("%d ", ps->arr[i]);
  }
  //释放
  free(ps);
  ps = NULL;
  return 0;
}


8.png

以上代码是我们控制数组大小的一种方式(柔性数组)。

接下来看另外一种方式(动态开辟数组):


#include<stdio.h>
#include<stdlib.h>
#include<string.h>
struct S
{
  int n;
  int* arr;
};
int main()
{
  struct S* ps = (struct S*)malloc(sizeof(struct S));
  ps->arr = malloc(5 * sizeof(int));
  int i = 0;
  for (i = 0; i < 5; i++)
  {
  ps->arr[i] = i;
  }
  for (i = 0; i < 5; i++)
  {
  printf("%d ", ps->arr[i]);
  }
  //调整大小
  int* ptr = realloc(ps->arr, 10 * sizeof(int));
  if (ptr != NULL)
  {
  ps->arr = ptr;
  }
  for (i = 5; i < 10; i++)
  {
  ps->arr[i] = i;
  }
  for (i = 0; i < 10; i++)
  {
  printf("%d ", ps->arr[i]);
  }
  //两次释放(有先后顺序)
  free(ps->arr);
  ps->arr = NULL;
  free(ps);
  ps = NULL;
  return 0;
}

9.png

10.png

既然有柔性数组的概念,那么柔性数组一定有它的优势。


柔性数组特点

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

柔性数组好处

第一:方便内存释放


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


第二:有利于访问速度


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


可以参照大佬的文章:C语言结构体里的成员数组和指针


以上就是动态内存管理的全部内容,有一点点多。😁但的确很重要,需要我们循序渐进的去学习并且消化吸收。

好了,下次见吧,再见啦!!!

目录
相关文章
|
6月前
|
安全 程序员 编译器
动态内存管理学习分享
动态内存管理学习分享
73 0
|
C语言
关于动态内存管理中的常见练习题
关于动态内存管理中的常见练习题
76 0
|
1月前
|
C语言
保姆级教学 - C语言 之 动态内存管理
保姆级教学 - C语言 之 动态内存管理
19 0
|
5月前
|
存储
程序与技术分享:C内存池的实现
程序与技术分享:C内存池的实现
|
安全 编译器
【C进阶】动态内存管理
【C进阶】动态内存管理
51 0
【C进阶】动态内存管理
|
程序员 C语言 C++
动态内存管理之经典笔试题
动态内存管理之经典笔试题
80 0
|
程序员 C语言 C++
动态内存管理【下篇】
动态内存管理【下篇】
87 0
|
编译器 C语言
动态内存管理【上篇】
动态内存管理【上篇】
85 0
【一文教你学会动态内存管理】
1.为什么会存在动态内存分配? 我们现在知道的开辟内存方式是创建变量,创建数组。而这些东西是在栈区上开辟空间的,一旦创建完成,是无法更改的,是固定死的。就像数组,在创建数组时,已经指定了数组的大小,编译后无法再更改。 这时候就需要动态内存分配。