C进阶-动态内存管理+柔性数组(2)

简介: C进阶-动态内存管理+柔性数组

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

看下面一段代码:

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

上述代码中,我们在test函数中用malloc申请了100个字节空间,当test函数结束时,指针变量p就自动销毁了,但是malloc申请的那100个字节的空间还在(没有用free释放),只要程序不结束,它就永远不会销毁,而我们在主函数中写了一个while(1)死循环,所以动态开辟的空间泄露了。

内存泄漏造成的问题很严重,它可能会使电脑崩溃,像我们生活中使用的各种APP,之所以我们不论何时登录上去都能使用,是因为它每时每刻都在运行,而要是内存泄漏的话,它每运行一次内存就泄漏一点,直到有一天内存被泄露完了,你的电脑也就崩溃了,这时如果重启一下电脑会发现电脑又好了,但是一旦你打开那个APP,多次使用,总有一天你的电脑又会崩溃。

总结一下,动态内存开辟的空间不会因为出了作用域就销毁,只有两种方式销毁(还给操作系统):1.free  2.程序结束(退出)。

4.几个经典的笔试题

4.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函数会出现什么样的结果?能不能打印出“hello world”?

答案是不能,我们来分析一下原因:

要想成功打印出“hello world”,传参的时候就应该传&str。

上述代码还有一处错误,就是没有释放动态内存开辟的空间,所以改正后的代码应该是如下写法:

#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);
  //释放内存
  free(str);
  str = NULL;
}
int main()
{
  Test();
  return 0;
}

有人说,上面printf(str)的写法不是错误的吗?

其实这种写法没有错,因为我们在打印字符串时用的是printf("hello world"),这里我们传给printf函数的其实只是首字符h的地址,而如果我们把字符串赋给指针char*p="hello world";这里p中存的也是首字符h的地址,那要打印的时候就可以用printf(p)。

同理,上述写法也能打印出"hello world"

4.2 题目2:

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

运行这段代码会发现,打印出来一串随机值,这是为什么呢?

上述代码中p只是一个局部变量,它在出了函数GetMeory后就会销毁,而出函数GetMeory之前return p返回了p所指向空间的地址,当str根据p的地址找过去时,p所指向的空间已经销毁,str就成了野指针,所以就会非法访问了。

如果要改正上述代码,我们只需要加上static延长p的生命周期就行:

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

下面我们再来举一个相似的例子:

#include<stdio.h>
#include<stdlib.h>
int* test()
{
  int a = 10;
  return &a;
}
int main()
{
  int* p = test();
  printf("%d\n", *p);
}

这段代码实际上和上文的代码是一样的道理,但是我们运行一下会发现,竟然打印出了10,这是为什么?

其实很容易解释,当进入test函数后,为a变量开辟了一块空间存放10,返回了a的地址,我们在主函数中用p接收到了这个地址,然后根据*p打印,此时虽然a已经销毁,我们依旧侥幸找到了存放10的空间,但是如果我们在printf函数之前任意写一段代码,那要为这段代码开辟空间,就会立即覆盖掉a的空间,这样打印出的值就不是10了。

以上题目统称为返回栈空间地址的问题

在栈上开辟空间的变量,进入作用域创建,出了作用域,它就销毁了,如果你在出作用域之前将该变量的地址返回了,并且在其他地方用指针接受了,那这个指针就变成了一个野指针。

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

这个代码看起来完全正常,但是有一点它忘记了,就是对动态开辟空间的释放,所以只要加上free就行:

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

4.4 题目4:

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

这段代码也是非法访问内存了,在给str开辟了100个字节的空间后,用strcpy函数将"hello"拷贝进去了,接着就释放了这块空间,但是在if语句中,又对str用strcpy函数想将"world"拷贝进去,此时str所指向空间已经被释放,所以非法访问了。

改正(释放空间后,将str置为NULL):

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

5.C/C++的内存开辟

下面通过一张图来了解一下C/C++中程序内存区域的划分:

有了这幅图,我们就可以更好的理解在《C语言初识》中讲的static关键字修饰局部变量的例子了

实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。

但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁,所以生命周期变长。

上文中我们也讲过,全局变量和静态变量是存放在静态区中的,局部变量和形式参数是存放在栈区中的,而堆区中存放的是malloc、calloc、realloc开辟的空间,下面我们来看一个例子:

通过上图打印出来的地址,会发现存放在栈区的a、b的地址接近,存放在静态区的c、d的地址接近。

以上就是动态内存分配的全部内容,下面我们来讲一个特殊的数组--柔性数组

6.柔性数组

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

例如:

struct S
{
    int n;
    int arr[];//柔性数组成员
}
int main()
{
     retrun 0;
}

有些编译器中编译不过,可以将arr[]写成arr[0]。

struct S
{
    int n;
    int arr[0];//柔性数组成员
}
int main()
{
     retrun 0;
}

6.1柔性数组的特点

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

这个前面的代码就可以看出来,struct S中的柔性数组前面有一个成员n。

2.sizeof返回的结构大小不包含柔性数组内存

下面我们可以用sizeof计算一下结构的大小:

可以看到计算的结果是4,一个int型的变量n的大小就是4,由此可见,sizeof在计算结构大小的时候不包含柔性数组的大小。

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

下面我们来为struct S开辟空间:

#include<stdio.h>
struct S
{
  int n;
  int arr[];
};
int main()
{
  struct S*ps=(struct S*)malloc(sizeof(struct S) + 40);
  return 0;
}

上述代码是我们在结构体成员变量n的基础上一次性开辟44个字节的空间,40是柔性数组预期的大小。

6.2柔性数组的使用

我们也可以对上述结构体类型的变量初始化:

#include<stdio.h>
struct S
{
  int n;
  int arr[];
};
int main()
{
  struct S*ps=(struct S*)malloc(sizeof(struct S) + 40);
  if (ps == NULL)
  {
    perror("malloc");
    return 1;
  }
  ps->n = 100;
  int i = 0;
  for (i = 0; i < 10; i++)
  {
    ps->arr[i] = i + 1;
  }
  //释放
  free(ps);
  ps = NULL;
  return 0;
}

上述代码中开辟的内存空间如下图所示:

当我们觉得空间不够用了,也可以用realloc增容:

#include<stdio.h>
struct S
{
  int n;
  int arr[];
};
int main()
{
  struct S*ps=(struct S*)malloc(sizeof(struct S) + 40);
  if (ps == NULL)
  {
    perror("malloc");
    return 1;
  }
  ps->n = 100;
  int i = 0;
  for (i = 0; i < 10; i++)
  {
    ps->arr[i] = i + 1;
  }
  struct S* ptr = (struct S*)realloc(ps,sizeof(struct S) + 60);
  if (ptr != NULL)
  {
    ps = ptr;
  }
  else
  {
    perror("realloc");
    return 1;
  }
  ps->n = 15;
  for (i = 0; i < 15; i++)
  {
    printf("%d\n", ps->arr[i]);
  }
  //释放
  free(ps);
  ps = NULL;
  return 0;
}

打印结果:

 

6.3柔性数组的优势

其实上述柔性柔性数组的使用,我们也可以用一下代码来模拟它:

#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
struct S
{
  int n;
  int* arr;
};
int main()
{
  struct S* ps = (struct S*)malloc(sizeof(struct S));
  if (ps == NULL)
  {
    perror("malloc");
    return 1;
  }
  ps->n = 100;
  ps->arr = (int*)malloc(40);
  if (ps->arr == NULL)
  {
    perror("malloc->arr");
    return 1;
  }
  int i = 0;
  for (i = 0; i < 10; i++)
  {
    ps->arr[i] = i + 1;
  }
  //调整
  int* ptr = (int*)realloc(ps->arr, 60);
  if (ptr != NULL)
  {
    ps->arr = ptr;
  }
  else
  {
    perror("realloc");
    return 1;
  }
  //打印
  for (i = 0; i < 15; i++)
  {
    printf("%d\n", ps->arr[i]);
  }
  //释放
  free(ps->arr);
  ps->arr = NULL;
  free(ps);
  ps = NULL;
  return 0;
}

注意使用了两次malloc,所以要free两次,先free内层的,后free外层的。

 运行结果:

上述代码开辟空间的方式与柔性数组不同,经过两次开辟得到:

虽然开辟空间方式不同,但是第二种方式和柔性数组一样,n和arr都在堆区,也能通过realloc实现增加空间,也可以进行赋值和打印,由此可见我们第二种实现方法也能像柔性数组一样,那为什么还要使用柔性数组呢?

柔性数组也是有优势的,

首先,第二种方式虽然实现了柔性数组的功能,但是开辟空间是用了两次malloc,而用了malloc就要free,一旦你忘记free就有可能出现错误。

其次,我们说开辟的内存和内存之间是有缝隙的,malloc用的越多,缝隙(内存碎片)越多,对内存的利用率就越低,而柔性数组只用了一次malloc,所以柔性数组对内存的利用率比较高。

还有,柔性数组开辟的内存是连续的,这就意味着柔性数组的访问速度更快。

总结一下柔性数组的优势:

1.方便内存释放,对内存的利用率高

2.有利于提高访问速度

那么到这就是我们今天的全部内容了,未完待续。。。

目录
相关文章
|
9月前
|
编译器
动态内存管理与柔性数组 1
动态内存管理与柔性数组
33 0
|
9月前
|
程序员 C语言 C++
动态内存管理函数的使用与优化技巧(内存函数、柔性数组)(下)
动态内存管理函数的使用与优化技巧(内存函数、柔性数组)(下)
29 0
|
9月前
|
程序员 编译器 C语言
动态内存管理函数的使用与优化技巧(内存函数、柔性数组)(上)
动态内存管理函数的使用与优化技巧(内存函数、柔性数组)(上)
40 0
|
9月前
|
程序员 编译器 C语言
动态内存函数,内存开辟,柔性数组(超详细)
动态内存函数,内存开辟,柔性数组(超详细)
48 0
|
9月前
|
编译器 程序员 测试技术
详解动态内存管理【malloc/calloc/realloc/free函数/柔性数组】【C语言/进阶/数据结构基础】
详解动态内存管理【malloc/calloc/realloc/free函数/柔性数组】【C语言/进阶/数据结构基础】
180 0
|
9月前
|
编译器 程序员 C语言
【C语言】动态内存管理(malloc,free,calloc,realloc,柔性数组)
【C语言】动态内存管理(malloc,free,calloc,realloc,柔性数组)
|
8天前
|
程序员 C语言 C++
【C语言】:柔性数组和C/C++中程序内存区域划分
【C语言】:柔性数组和C/C++中程序内存区域划分
9 0
|
2月前
|
存储 安全 编译器
【C语言】动态内存管理 -- -- 深入了解malloc、calloc、realloc、free、柔性数组(万字深入了解)
【C语言】动态内存管理 -- -- 深入了解malloc、calloc、realloc、free、柔性数组(万字深入了解)
18 0
【C语言】动态内存管理 -- -- 深入了解malloc、calloc、realloc、free、柔性数组(万字深入了解)
|
2月前
|
程序员 编译器 C语言
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free(下)
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free
28 0
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free(下)
|
2月前
|
编译器 数据库 C语言
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free(上)
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free
27 0
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free(上)