c语言|《伏C录》凝气篇-初战动态内存管理四兄弟

古岂无人,孤标凌云道为朋。剑宿吾命,亦狂亦侠亦超尘。
c语言|《伏C录》凝气篇-初战动态内存管理四兄弟
文章图片


目录
?一、本章重点
?二、C/C++内存四区
?三、动态内存分配
2.1什么是动态内存分配
2.2为什么存在动态内存分配
?四、动态内存分配函数
4.1malloc
4.2free
4.3calloc
4.4realloc
?五、常见动态内存错误
5.1对NULL指针的解引用操作
5.2对动态开辟空间的越界访问
5.3对非动态内存使用free
5.4使用free释放一块动态开辟内存的一部分
5.5对同一块动态内存多次释放
5.6动态开辟内存忘记释放(内存泄漏)
?六、经典笔试题
6.1题目一
6.2题目二
6.3题目三
6.4题目四
?一、本章重点
  1. 介绍C/C++内存四区
  2. 什么是动态内存分配
  3. 为什么存在动态内存分配
  4. 动态内存函数四兄弟:malloc、calloc、realloc、free
  5. 总结常见动态内存错误
  6. 几个经典的笔试题解析
?二、C/C++内存四区 众所周知内存是用来存储程序运行时的数据,为了方便管理这些数据,内存被划分为4个区域,分别为栈区、堆区、数据段、代码段。
c语言|《伏C录》凝气篇-初战动态内存管理四兄弟
文章图片

栈区:

栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
栈区特点: 一般栈区变量的生命周期和作用域较短,只存在于函数栈帧中(函数体),当函数栈帧销毁时(函数返回),变量所属的那块空间也随之被销毁。因此函数一般不返回它的局部变量的地址,因为那块空间在函数返回时就被销毁了,再次访问就是非法访问内存。 (注:函数栈帧即为函数专门在栈区开辟的一块空间。) 堆区:
这块区域由程序员自己分配和释放空间,若没有释放分配的空间,则在程序结束时由操作系统释放。
堆区特点:
由程序员分配和释放空间,堆区数据只有程序员释放或者程序结束由操作系统才能释放,因此对于一些想保存时间长一点的数据一般把它放在堆区中,同时堆区的容量比栈区要大。
数据段:
数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
数据段特点:
存放全局变量和静态变量,也被称作全局区/静态区。全局变量生命周期和作用域一般是整个程序,静态变量即由static修饰的变量,static修饰局部变量时它被放在数据段中,此时它的生命周期变长,但作用域不变,static修饰全局变量,会限制全局变量的作用域,此时它的作用域变为当前文件,但生命周期不变。
代码段:
代码段:存放只读常量 /函数体(类成员函数和全局函数)的二进制代码。
代码段特点:
存放函数体的二进制代码,同时也会存放一些字符常量,这块区域与数据段的区域相接近。
总结:
关于内存四区,现阶段了解这些即可,不必太过深入,只是为了方便我们理解和学习,真正的内存分布并不是这样。
?三、动态内存分配 2.1什么是动态内存分配 百度百科:所谓动态内存分配(Dynamic Memory Allocation)就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不象数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。
2.2为什么存在动态内存分配
因为有些时候我们需要动态的增长我们的空间容量,比如我们写一个通讯录,我们向栈区申请100个连续的空间,那么这块空间是不可扩增和缩减的,因此也被称作静态内存分配,而动态内存分配正好可以解决这一问题,我们可以先在堆区申请10个空间,然后当检测函数检查到空间已满的情况下,那么就自动增加容量。所谓动态内存,就是指这块申请的内存可以扩增或者缩减,因此这一特性通常也被用在链表这一方面上。
?四、动态内存分配函数 4.1malloc
函数原型: void*malloc( size_tsize );
这个函数向堆区申请一块连续可用的空间,并返回指向这块空间的指针。
  • 如果开辟成功,则返回一个指向开辟好空间的指针。
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  • 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自 己来决定。
  • 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器
4.2free C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:
函数原型:
void free (void* ptr);
free函数用来释放动态开辟的内存。
  • 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
  • 如果参数 ptr 是NULL指针,则函数什么事都不做。
malloc和free都声明在 stdlib.h 头文件中。
举个例子:
#include int main() { int* ptr = NULL; ptr = (int*)malloc(10 * sizeof(int)); if (NULL != ptr)//判断ptr指针是否为空 { //将堆区开辟的10个整形数据都置为0 int i = 0; for (i = 0; i < 10; i++) { *(ptr + i) = 0; } } free(ptr); //释放ptr所指向的动态内存 ptr = NULL; //是否有必要? return 0; }

当free(ptr)后,建议将ptr置为NULL,否则ptr就是野指针,存在隐藏风险。

4.3calloc C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。
函数原型: void*calloc( size_tnum ,size_tsize );
  • 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
  • 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。

4.4realloc
函数原型:
void* realloc (void* ptr, size_t size);

  • ptr 是要调整的内存地址
  • size 调整之后新大小
  • 返回值为调整之后的内存起始位置。
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间。
  • realloc函数的出现让动态内存管理更加灵活。
  • 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
realloc在调整内存空间的是存在两种情况:
1.原地扩容
直接从内存后面追加空间,原数据不发生变化。
图解:
c语言|《伏C录》凝气篇-初战动态内存管理四兄弟
文章图片


2.异地扩容
由于后面要追加的空间太大,堆区不足以向后面继续开辟空间(后面可能被其他变量占用空间),只能找一块新的大内存空间,找到后将原数据拷贝至新空间,并且返回新空间的起始地址。
图解:
c语言|《伏C录》凝气篇-初战动态内存管理四兄弟
文章图片


?五、常见动态内存错误 5.1对NULL指针的解引用操作
void test() { int *p = (int *)malloc(INT_MAX/4); *p = 20; //如果p的值是NULL,就会有问题 free(p); }

开辟堆区空间后要检查返回值是否为空,否则由可能开辟失败,返回NULL导致后续出现空指针解引用问题。

5.2对动态开辟空间的越界访问
void test() { int i = 0; int* p = (int*)malloc(10 * sizeof(int)); if (NULL == p) { exit(EXIT_FAILURE); } for (i = 0; i <= 10; i++) { *(p + i) = i; //当i是10的时候越界访问 } free(p); }

*(p + i) = i; //当i是10的时候越界访问

5.3对非动态内存使用free
void test() { int a = 10; int* p = &a; free(p); //ok? }

这里的a是堆区的变量,不属于堆区开辟的内存,不能使用free释放,否则报错。

5.4使用free释放一块动态开辟内存的一部分
void test() { int* p = (int*)malloc(100); p++; free(p); //p不再指向动态内存的起始位置 }

开辟的一块空间,要么不释放,要么都释放,不能只释放一部分

5.5对同一块动态内存多次释放
void test() { int* p = (int*)malloc(100); free(p); free(p); //重复释放 }

这个一般c++中深拷贝和浅拷贝问题遇到的就是堆区内存重复释放,这会报错。

5.6动态开辟内存忘记释放(内存泄漏)
void test() { int* p = (int*)malloc(100); if (NULL != p) { *p = 20; } } int main() { test(); while (1); }

使用完堆区内存后,一定要释放那块空间,不然导致内存泄露,是一件很严重的事情,在公司里这就是一场事故了,可能取消年终奖。 可能有人说:程序运行完,操作系统不是会自动回收堆区的空间吗?但如果那个程序要24小时运转呢?这就是一个很严重的事情了。

?六、经典笔试题 6.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,因为GetMemory(str),不会改变str,str仍然为NULL,所以在调用strcpy()时,,会出现NULL的解引用问题,关于这一点要知道strcpy的模拟实现,具体可以参考我之前的文章。
  • 由于堆区申请的空间没有释放,会出现内存泄露。

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

分析:
  • 会打印hello world,经过str=GetMemory(),str指向存放“hello world”的那块空间了。
  • 这里需要解析的是"hello world"传的是字符串的首地址给了p,该字符串属于字符串常量,它通常被放在内存的字符常量区。
  • 但没有释放堆区的空间,会出现内存泄露的情况。
6.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); }

分析:
  • 会打印hello ,这里GetMeory()传的是str的地址,二级指针的地址,*p就是str本身,随后str指向堆区的100个字符的空间。在通过strcpy()函数,能够将hello拷贝给str指向的那块堆空间。
  • 这里需要二级指针的知识,和strcpy的实现原理,关于这些可以看我之前的文章,那里有非常清楚的解析。
  • 同样的,没有free(str),会有内存泄露。

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

分析:
  • 不会打印world,str指向堆区的100个字符的空间,strcpy(str,“hello”),堆区空间放入"hello",free(str),堆区空间被释放,str此时还指向那块堆区空间,str属于野指针,很危险。
  • 下一个if判断语句,str是不等于NULL的,执行条件语句,然后将“world”拷贝至那块堆区空间,但由于堆区空间提前释放,会出现内存读写访问冲突的问题。
  • 这就说明了,在free(str)的时候,str=NULL是非常有必要的。

【c语言|《伏C录》凝气篇-初战动态内存管理四兄弟】

    推荐阅读