文章目录

  • 前言
  • 一、为什么存在动态内存分配
    • 1、已掌握的内存开辟方式
    • 2、上述开辟空间方式的特点
    • 3、为什么存在动态内存分配
  • 二、动态内存函数的介绍
    • 1、malloc
    • 2、free
    • 3、calloc
    • 4、realloc
  • 三、常见的动态内存错误
    • 1、对NULL指针的解引用操作
    • 2、对动态开辟内存的越界访问
    • 3、对非动态开辟内存使用free释放
    • 4、使用free释放动态开辟内存的一部分
    • 5、对同一块动态内存的多次释放
    • 6、动态开辟内存忘记释放(内存泄漏)
  • 四、几个经典的笔试题
    • 1、题目1
    • 2、题目2
    • 3、题目3
    • 4、题目4
  • 总结

前言

自从前两次博客写完以后,感觉对于我本人来说,收获很大,尤其是将学过的知识再度温习一遍,感觉基础扎实了很多,所以就养成了一个习惯,每学完一个模块,都会写一篇博客,不仅仅是写个我自己的,也是想通过这篇博客,与大家分享一些我的见解。本来周四就学完了动态内存分配,但是周末博主去玩了,于是忘记了写博客,现在加班奉上。

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

1、已掌握的内存开辟方式

在C语言中,我们将内存分为了4个区间:
代码区,全局变量与静态变量区,局部变量区即栈区,动态存储区,即堆(heap)区或自由存储区(free store)。

为了方便大家理解,图解如下:

通过之前的学习,我们了解了一些内存的使用方法:

(1)创建一个变量
当我们想要使用单一变量的时候,我们可以通过创建一个变量,来使用内存。

int a = 10;//局部变量 - 栈区
int g_a = 10;//全局变量 - 静态区

(2)创建一个数组
当我们需要使用多个相同类型变量的时候,我们可以通过创建一个数组,来使用内存。

int arr[10];//局部变量 - 栈区
int g_arr[10];//全局变量 - 静态区

2、上述开辟空间方式的特点

以上两种使用内存的方式是我们学过的,也是常用的,但是在某些情况下,仅仅有这两种方法是不足的。

例如:我们需要创建一个数组来存放一个班级的学生信息的时候。

我们在创建这个arr数组的时候,当我们直接给定数组的长度arr[50]的时候,这样是很简单,但是这样合理吗?
例1:

#include<stdio.h>
struct s
{char name[20];int age;
};
int main()
{struct s arr[50];// 50个struct s 类型的数据// 30 :不够// 60 :浪费return 0;
}

假设这个班级只有30个人,那么我们是不是就浪费了一部分的空间;假设这个班级有60个人,那么我们给定的50又不够。所以说这里给定多少都是不合理的。

这里有人又会说了,很简单啊:要多少给多少就好了嘛!就像这样
例2:

#include<stdio.h>
struct s
{char name[20];int age;
};
int main()
{int n = 0;scanf_s("%d", &n);struct s arr[n];//错误(活动)    E0028   表达式必须含有常量值return 0;
}

运行结果为:报错

事实证明,我们的想法很美妙,但是现实却很残酷:
这里的错误名称叫:表达式必须含有常量值
说明对于 struct s arr[n]; 这里的n是变量,那就不行了。

这里延伸一下:例2这种代码的写法叫做变长数组
对于变长数组这种写法目前仅对于C99是可运行通过的。

总结:上述开辟空间方式的特点
(1)开辟空间的大小是固定的;
(2)数组在声明的时候,必须制定数组的长度,它所需的内存在编译时分配。

3、为什么存在动态内存分配

我们对于内存开辟空间的需求,不仅仅局限于这些方式,有时候我们需要的空间大小在程序运行的时候才能知道,这时上述方式就不能达成目的了,所以动态内存分配就应运而生了。

二、动态内存函数的介绍

1、malloc

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

 void* malloc (size_t size);

malloc的全称是memory allocation,中文叫动态内存分配,用于申请一块连续的指定大小的内存块区域以void*类型返回分配的内存区域地址,当无法知道内存具体位置的时候,想要绑定真正的内存空间,就需要用到动态的分配内存,且分配的大小就是程序要求的大小。

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
(1)如果开辟成功,则返回一个指向开辟好空间的指针;
(2)如果开辟失败,则返回一个NULL指针,因此 malloc 的返回值一定要做检查;
(3)返回值的类型是 void*,所以 malloc 函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定;
(4)如果参数 size 为 0 ,malloc 的行为是标准是未定义的,取决于编译器。

举一个例子
例3:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{// 向内存申请10个整形的空间int* p = (int*)malloc(10 * sizeof(int));// malloc -> #include<stdlib.h>//int* p = malloc(10 * sizeof(int));// 错误     C2440   “初始化” : 无法从“void * ”转换为“int* ”if (p == NULL){// 打印错误原因的一个方式printf("%s\n", strerror(errno));// strerror -> #include<string.h>// errno -> #include<errno.h>}else{// 正常使用空间int i = 0;for (i = 0; i < 10; i++){*(p + i) = i;}for (i = 0; i < 10; i++){printf("%d ", *(p + i));}}return 0;
}

运行结果为:
0 1 2 3 4 5 6 7 8 9

在例3中,我们如果采用 “ int* p = malloc(10 * sizeof(int)); ” 的方式来开辟空间,在大部分检测严格的编译器中,会报错,这是因为变量类型的不同,从这里我们也可以看出,malloc 开辟空间的返回值是 void* 类型;

上面我们也提到了:如果 malloc 开辟失败,则返回一个NULL指针,所以malloc 的返回值一定要做检查,所以我们用了一种特殊的方式来打印错误原因——“ printf("%s\n", strerror(errno)); ” ,这样如果开辟失败,编译器就不会报错了,而是在运行后将错误的原因打印出来。

易错提示:
因为我们计算机的内存也是有限的,所以我们不能为所欲为的开辟空间,当我们需要开辟的空间不够时,打印错误就会出现“Not enough space”。

2、free

紧接上文,我们不能为所欲为的开辟空间,因为空间是有限的,所以应当有借有还,我们在前边向系统借用了这么多内存,当我们用完以后,我们应该把这块内存还给系统,那么怎么还呢?这里就需要用到我们的 free 函数了。

C语言为我们提供了另外一个函数,专门用来做动态内存的释放和回收的:

 void free(void *ptr)

free函数用来释放动态开辟的内存:
(1)如果参数 ptr 指向的空间不是动态开辟的,那 free 函数的行为是未定义的;
(2)如果参数 ptr 是NULL指针,则函数什么操作都不进行。

先来看一个例子
例4:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{// 向内存申请10个整形的空间int* p = (int*)malloc(10 * sizeof(int));// malloc -> #include<stdlib.h>//int* p = malloc(10 * sizeof(int));// 错误     C2440   “初始化”  :  无法从“void * ”转换为“int* ”if (p == NULL){// 打印错误原因的一个方式printf("%s\n", strerror(errno));// strerror -> #include<string.h>// errno -> #include<errno.h>}else{// 正常使用空间int i = 0;for (i = 0; i < 10; i++){*(p + i) = i;}for (i = 0; i < 10; i++){printf("%d ", *(p + i));}}// 当动态申请的空间不再使用的时候// 就应该还给操作系统free(p);p = NULL;return 0;
}

运行结果为:
0 1 2 3 4 5 6 7 8 9

对比例3和例4,例4只是多了两行代码:
free( p );
p = NULL;

有人会疑惑了,例3和例4的运行结果明明是一样的啊,那为什么我们还要多此一举,加上这两行代码呢?
没错看上去运行结果是一样的,但这仅仅只是对于我们代码量很少的情况下,我们申请的内存够用了,所以目的达到了;但是假设我们要做一项任务量巨大的工程的时候,我们只借不还,系统的内存在不断减少,我们还能继续写程序吗?所以应该从现在养成一个习惯,申请的内存,用完以后一定要进行 free()操作。

这里有人又有疑问了,那我们用完了内存,释放了不就好了吗?为什么还要把这个指针p置为空指针呢?
其实当我们free(p)操作结束以后,这块空间是释放了,但是p的值并没有改变,如果有人找到了这个p,进行了破坏,我们的程序就有可能出问题,所以我们不妨主动将p置为空指针,让有非分之想的人断绝这些念想。

光说不练,是学习编程语言的大忌,我们趁热打铁,来做一道练习题:

正确答案为:
例5:

#include "string.h"
#include <stdio.h>
#include<stdlib.h>
int main()
{char* src="hello,world"; char* dest=NULL;int len=strlen(src);dest=(char*)malloc(len+1);// 要为\0分配空间char* d=dest;char* s=src+len-1;// 指向最后一个字符while(len--!=0){ *(d++)=*(s--);// 注意不要丢掉*号*d ='\0';// 字符串的结尾不要忘记'\0'} printf("%s",dest);free(dest);// 使用完要释放空间,避免内存泄露dest = NULL; // 释放不等于安全,将其置为空指针的操作不可省略return 0;
}

3、calloc

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

 void* calloc(size_t num,size_t size)

(1)函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把这块空间的每个字节初始化为0;
(2)与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0.

举个例子:
例6:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{int* p = (int*)calloc(10 , sizeof(int));// calloc -> #include<stdlib.h>if (p == NULL){// 打印错误原因的一个方式printf("%s\n", strerror(errno));// strerror -> #include<string.h>// errno -> #include<errno.h>}else{// 正常使用空间int i = 0;for (i = 0; i < 10; i++){printf("%d ", *(p + i));}}// 当动态申请的空间不再使用的时候// 就应该还给操作系统free(p);p = NULL;return 0;
}

运行结果为:
0 0 0 0 0 0 0 0 0 0

由此可见:calloc 函数会将动态开辟空间的每个字节初始化为0

4、realloc

回归今天的核心问题,如果我们在使用内存的过程中需要对内存的大小进行调整怎么办呢?

C语言同样为我们提供了一个函数叫 realloc ,realloc 函数可以让动态内存管理更加灵活:

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

(1)ptr 是要调整的内存地址;
(2)size 是调整后的新大小;
(3)返回值为调整之后的内存起始位置;

举个例子:
例7:

#include<stdio.h>
#include<stdlib.h>
int main()
{int* p = (int*)malloc(20);for (int i = 0; i < 5; i++){*(p + i) = i;}for (int i = 0; i < 5; i++){printf("%d ", *(p + i));}int* p2 = (int*)realloc(p, 40);for (int j = 5; j < 10; j++){*(p + j) = j;}for (int j = 5; j < 10; j++){printf("%d ", *(p + j));}free(p);p = NULL;return 0;
}

运行结果为:
0 1 2 3 4 5 6 7 8 9

(4)这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间;

(5)realloc 在调整内存空间的过程中存在两种情况
①原有空间之后有足够大的空间
此时,直接在原有内存之后追加空间,原来空间的数据不发生变化。

②原有空间之后没有足够大的空间
在堆空间上另找一个合适大小的连续空间来使用,这样函数返回的是一个新的内存地址。

图解如下:

三、常见的动态内存错误

1、对NULL指针的解引用操作

例8:

#include<stdio.h>
#include<stdlib.h>
int main()
{// 1.对NULL指针解引用操作int* p = (int*)malloc(40);// 万一malloc失败了,p就被赋值为NULL// 不安全// 记得判断p是否为空int i = 0;for (i = 0; i < 10; i++){*(p + i) = i;}free(p);p = NULL;return 0;
}

对于例8,如果 malloc 开辟空间失败,此时 p 被赋值为NULL,而下面对于空指针进行操作, *(p + i) 始终为非法地址,我们的操作始终为非法操作,所以我们一定要在使用前记得判断p是否为空。

2、对动态开辟内存的越界访问

例9:

#include<stdio.h>
#include<stdlib.h>
int main()
{// 2.对动态开辟内存的越界访问int* p = (int*)malloc(40);// 10个int -> 0-9if (p == NULL){return 0;}int i = 0;// 越界for (i = 0; i <= 10; i++){*(p + i) = i;}free(p);p = NULL;return 0;
}

对于例9,我们使用 malloc 向系统申请了 10个int 类型,但是我们在后边访问了 11个int 类型,运行程序的时候就会出现假死的情况,虽然是动态内存,但是也是有边界的,一但越界访问,程序就会出现问题。

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

例10:

#include<stdio.h>
#include<stdlib.h>
int main()
{// 3.对非动态开辟内存使用free释放int a = 10;int* p = &a;free(p);p = NULL;return 0;
}

对于例10,a的空间是存放与栈区的,它并不是动态开辟的空间,free函数释放的一定是堆区上开辟的空间,如果对非动态开辟内存使用free释放,程序就会出现假死的情况。

4、使用free释放动态开辟内存的一部分

例11:

#include<stdio.h>
#include<stdlib.h>
int main()
{// 4.使用free释放动态开辟内存的一部分int* p = (int*)malloc(40);if (p == NULL){return 0;}int i = 0;for (i = 0; i < 10; i++){*p++ = i;}// 回收空间free(p);p = NULL;return 0;
}

对于例11,我们有这样一个操作 “*p++ = i;” ,当这个操作结束的时候,我们的指针p指向的空间已经不是我们动态开辟的完整空间了,不仅仅局限指向末尾,只要这里的p不再指向空间的初始位置,都会导致程序的崩溃。

5、对同一块动态内存的多次释放

例12:

#include<stdio.h>
#include<stdlib.h>
int main()
{// 5.对同一块动态内存的多次释放int* p = (int*)malloc(40);if (p == NULL){return 0;}// 假设使用了空间// 释放free(p);// ...很多行代码过后 free(p);// 再次释放 return 0;
}

对于例12,我们在使用完空间后,释放了空间,在很多行代码过后,又释放了一次空间,这样程序同样会假死,那么我们如何改进呢?
例13:

#include<stdio.h>
#include<stdlib.h>
int main()
{// 5.对同一块动态内存的多次释放int* p = (int*)malloc(40);if (p == NULL){return 0;}//可以这样free(p);p = NULL;free(p);return 0;
}

像例13这样,每次释放完空间,主动将p置为空指针,这样就可以有效避免了上述情况,因为我们之前提到过:
对于free函数:如果参数 ptr 是NULL指针,则free函数什么操作都不进行。

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

例14:

#include<stdio.h>
#include<stdlib.h>
int main()
{// 6.动态开辟内存忘记释放(内存泄漏)while (1){malloc(1);//警告    C6031   返回值被忽略 : “malloc”。}return 0;
}

对于例14,当我们开辟内存忘记释放的时候,就会造成内存泄漏。我们的电脑可能就会出现死机的情况,遇到这种情况我们一般都会重启,但是当我们写程序达到几万行的时候,出现了这种问题,那将是一个十分恐怖的事情。

四、几个经典的笔试题

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 函数会有什么样的结果?
答案为:程序崩溃

对于本题,很多人的注意力会集中于 “printf(str);” ,实际上这里并没有问题,它等价于 “printf("%s\n",str);” 。

解析代码:
看到 “GetMemory(str);” ,我们在这里传递的是 str 本身的值,而不是 str 的地址,进入 GetMemory 函数以后,我们在堆上开辟了100个空间,我们将这些空间放置在 p 中,这里的 p 作为一个形参变量,在 GetMemory 函数结束以后,这个 p 就销毁了, 实际上 str 仍然是NULL,而接下来我们想要将 “hello world” copy 到 str 中去,但是 str 作为NULL,它并没有指向一个有效的空间,进行操作的时候,无法避免的进行了非法访问,虽然后边的 printf 操作没有问题,但是程序在 strcpy 操作时就已经崩溃了。

总结:
(1)运行代码程序会出现崩溃现象;
(2)程序存在内存泄漏问题:
str 以值传递的形式给 p
p 是 GetMemory 函数的形参,只在函数内有效
等 GetMemory 函数返回之后,动态开辟内存尚未释放
并且无法找到,所以会造成内存泄漏

2、题目2

“返回栈空间地址问题”

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

请问:运行 Test 函数会有什么样的结果?
答案为:随机值(或者崩溃)

解析代码:
看到 “str = GetMemory();” ,进入 GetMemory 函数的时候,p[] 这个数组是GetMemory 函数内的形参,它申请了一个空间,这个空间只在 GetMemory 函数内存在,在 GetMemory 函数结束的时候,的确将 p 的地址返回了,放置在 str 中,但是当 GetMemory 调用完成之后,p 这个数组开辟的空间返还给操作系统了,这个空间里存放的值,我们是不清楚的,接下来 “printf(str);”
打印出来的值我们不清楚,所以结果为随机值。

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

请问:运行 Test 函数会有什么样的结果?
答案为:
(1)输出hello
(2)但是有内存泄漏

解析代码:
看到 “GetMemory(&str, 100);” ,将 str的地址传入 GetMemory 函数,用二级指针p 来接收,那么 *p 指向的地址即为 str ,然后将 “hello” copy 到 str 当中,再打印出来,这些操作都没有问题,但是当我们使用完 str 以后,忘记释放动态开辟的内存,导致了内存泄漏。

4、题目4

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

请问:运行 Test 函数会有什么样的结果?
答案为:
(1)world
(2)非法访问内存(篡改动态内存区的内容,后果难以预测,非常危险)

解析代码:
首先,我们向系统申请了100个字节,地址存放在 str 中;然后,我们把 “hello” copy 到 str 当中去;接下来,我们释放了这块空间,之后, str 指向的这块空间已经还给操作系统了;然后,进行判断: str 是否为空指针,虽然之前我们对申请的动态内存进行了释放,但是 str 的值并没有改变,仍然是 “hello”,所以它不为空指针;进入if语句后,将 “world” copy 到 str 当中,world 就把 hello 给覆盖了;所以打印 str 以后结果为 world。

虽然打印了world,但是这个程序依然出了问题,对于 “free(str);” 操作:已经把空间释放掉了,这表明这块空间已经不属于我们了,我们已经不能再使用这块空间了,但是接下来我们还将 world 放进去,并且打印,这就属于非法访问内存了。

提示:free(p)和p=NULL一定要连贯使用!

总结

关于动态内存分配的讲解就到此结束了,动态内存分配其实并不困难,更多的还是一些概念的背诵,只要我们牢记这些易错点,拿捏起来,还是轻轻松松的!加油,冲冲冲!
ps:动态内存分配拖了蛮久的,关于文件的博客,博主会快马加鞭的肝的(doge)

C语言动态内存分配详解相关推荐

  1. 【C语言】动态内存分配详解

    目录 一.为什么有动态内存分配 二.动态内存分配函数 (1)malloc()函数 (2)calloc()函数 (3)realloc()函数 三.常见的动态内存错误 1.越界访问 2.内存泄漏 3.对N ...

  2. C语言动态内存开辟详解(malloc,calloc,realloc,free,柔型数组)

    目录 一.概述 二.相关库函数的使用 1.malloc 2.calloc malloc vs. calloc 异同 3.free的使用 4.realloc 三.易错点 四.C\C++程序的内存开辟规则 ...

  3. 【C++】动态内存分配详解(new/new[]和delete/delete[])

    原文链接:https://blog.csdn.net/qq_40416052/article/details/82493916 代码还是原文看着方便,在此不调整格式了 一.为什么需要动态内存分配? 在 ...

  4. spark on yarn 内存分配详解

    spark on yarn 内存分配详解

  5. 浅谈C语言动态内存分配及柔性数组

    文章目录 前言 1.动态内存的简单介绍 1.动态内存分配是什么? 2.为什么存在动态内存分配? 3.动态内存分配具体方法 1.动态内存函数 2.动态内存注意事项 2.经典面试题分析 3.C/C++程序 ...

  6. c语言动态内存分配数组,【C】动态内存分配

    ## 动态内存分配的意义 C语言中的一切操作都是基于内存的 变量和数组都是内存的别名 内存分配由编译器在编译期间决定 定义数组的时候必须指定数组长度 数组长度是在编译期就必须确定的需求: 程序在运行过 ...

  7. linux 在指定区域分配内存 c语言,C语言动态内存分配:(一)malloc/free的实现及malloc实际分配/释放的内存...

    一.malloc/free概述 malloc是在C语言中用于在程序运行时在堆中进行动态内存分配的库函数.free是进行内存释放的库函数. 1.函数原型 #include void *malloc( s ...

  8. C语言动态内存分配:(一)malloc/free的实现及malloc实际分配/释放的内存

    最新个人博客 shankusu.me 以下内容转载或参考或引用自 https://blog.csdn.net/zxx910509/article/details/62881131 一.malloc/f ...

  9. C语言:动态内存分配+经典面试题

    前言: 通常,我们在栈空间开辟的内存都是固定的,这是十分不方便使用的.为了更加灵活的分配和使用内存,我们要学习C语言中一些常用的与内存分配相关联的函数.顺便,我们会补充数组中柔性数组的知识. 内存分区 ...

最新文章

  1. ubuntu 14.10安装zabbix(lnmp环境)
  2. 高效模式编写者的7个习惯
  3. Java代码优化(长期更新)
  4. 判断直线与线段是否相交,相交则输出交点x轴坐标
  5. powershell 下独立silent 安装 浏览器问题
  6. Android AIDL使用介绍(1)基本使用
  7. Spring中的事件机制
  8. 五轴编程_沙井万丰数控数控编程五轴编程那个软件好用
  9. C字符串指针遇到的问题
  10. PHP+node搞一下58微聊的聊天内容的获取
  11. android中期检查表,基于Android的车载视频播控系统的中期检查表.docx
  12. 【游记】CQOI2021
  13. C#冷门系列之Lazy
  14. python中常用英语口语_1000句常用英语口语
  15. 牛逼了,用Python破解wifi密码
  16. 备份一体机的制作原理以及工艺
  17. 猫扑实战分享:如何在预算几乎为0的情况做活动运营
  18. c语言质变量变,量变和质变的根本区别是( )
  19. php设计模式经典实例集合
  20. npm ERR code EEXIST的问题 node-sass安装问题

热门文章

  1. 【Node】Node核心模块
  2. AECC全球留学趋势报告解读
  3. equalsIgnoreCase( )方法
  4. html图标右上角添加小图标,图片右上角增加删除图标(css布局示例)
  5. SpringBoot修改tomcat配置
  6. Python爬虫实战(5)斗图啦表情包下载(单线程)
  7. 【源码那些事】超详细的ArrayList底层源码+经典面试题
  8. 丝瓜藤 de JAVA学习
  9. 一个人做两件事和两个人做一件事
  10. vue实现一个鼠标滑动预览视频封面组件