文章目录
- 前言
- 一、引子
- 二、malloc
- 三、calloc
- 四、realloc
- 五、free
- 六、常见的动态内存错误
- 对NULL指针进行解引用操作
- 对动态开辟空间的越界访问
- 对非动态开辟的内存使用free释放
- 使用free释放动态开辟内存的一部分
- 对同一块内存多次释放
- 动态开辟内存忘记释放(内存泄漏)
- 七、柔性数组(flexible array)
- 定义
- 特性
- 对比
- 八、C/C++中程序内存区域规划
- 总结
前言
如果未来要想学好数据结构,那么你对指针、结构体还有本篇的动态内存的理解掌握能力是要很高的
所以跟我一起开始本篇的学习吧!
一、引子
假设我们现在想开辟一个根据用户需求大小的数组
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
int arr[n];
return 0;
}
这个代码在C99标准下是可以运行的,但大多数编译器并不支持C99标准,所以这种代码缺乏了跨平台性(可移植性)
例如常用的VS的编译器就不支持这么做
那么我们有没有办法写出一个既可以满足题目要求,又可以在任何一个编译器下都编译得过去的代码呢?答案是肯定的。这就和C语言中的动态内存的开辟有关了,动态开辟,即可以按照需求开辟内存的大小
另外,下面介绍的几个函数的操作对象都是堆区的内存
局部变量存放在内存中的栈区;全局变量、静态变量(static修饰的变量)存放在内存中的静态区(也叫数据段),以后我们会再详细介绍这个,不用着急
二、malloc
void* malloc(size_t size);
malloc函数的功能是开辟指定字节大小的内存空间,如果开辟成功就返回该空间的首地址,如果开辟失败就返回一个NULL(所以我们必须要加个条件判断一下)。传参时只需传入需要开辟的字节个数
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
// 因为malloc函数的返回值为void*,所以需要强制类型转换为对应类型。
if (p == NULL)
{
printf("内存开辟失败\n");
}
else
{
printf("内存开辟成功\n");
// 使用...
free(p);
p = NULL;
}
return 0;
}
这是假设我们要开辟一个可以存放10个整型的空间的话,应进行的步骤
同时,我们也要注意malloc的几个特性:
- 向内存申请空间不完成初始化,返回指向这块空间的大小
- malloc是void*类型,当我们申请空间时候,需要知道申请空间交给什么类型去维护,也就是说这需要指针强转
- 如果参数size为0,malloc可能会报错(取决于编译器)
- 同时申请空间有时候不一定会成功。如果失败的话,将会返回一个空指针,比如申请的空间太大,就会申请失败,这一点使用的时候要去注意
三、calloc
void* calloc(size_t num, size_t size);
calloc函数的功能也是开辟指定大小的内存空间,如果开辟成功就返回该空间的首地址,如果开辟失败就返回一个NULL。但calloc函数传参时需要传入两个参数(开辟的内存用于存放的元素个数和每个元素的大小)
真要说用法的话,与malloc大同小异:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)calloc(10 , sizeof(int));
// 开辟一个可以存放10个整型的内存空间
if (p == NULL)
{
printf("内存开辟失败\n");
}
else
{
printf("内存开辟成功\n");
// 使用...
free(p);
p = NULL;
}
return 0;
}
但是,跟malloc有一个很大的区别在于:calloc函数开辟好内存后会将空间内容中的每一个字节都初始化为0
四、realloc
void* realloc(void* memblock, size_t size);
realloc函数可以调整已经开辟好的动态内存的大小,第一个参数是需要调整大小的动态内存的首地址,第二个参数是动态内存调整后的新大小
realloc函数与上面两个函数一样,如果开辟成功便返回开辟好的内存的首地址,开辟失败则返回NULL
具体使用方法如下:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
printf("内存开辟失败\n");
}
else
{
printf("内存开辟成功\n");
// 使用...
int* ptr = (int*)realloc(p, 100);
// 将空间扩展为100个字节大小
if (ptr != NULL)
{
p = ptr; // 开辟成功时
// 使用...
}
// 使用结束,释放内存(后面介绍)
free(p);
p = NULL;
}
return 0;
}
realloc相当于是重新开辟一块空间,因此会有几种不同情况:
一、当内存空间足够的时候,直接在申请好的空间追加
realloc函数直接在原空间后方进行扩展,并返回该内存空间首地址(即原来的首地址)
二、当内存空间不够的时候,会在内存中寻找一块更大的空间存放,将目前的数据拷贝一份到新的空间位置中,再将原来的空间释放掉
realloc函数会在堆区中重新找一块满足要求的内存空间,把原空间内的数据拷贝到新空间中,并主动将原空间内存释放(即还给操作系统),返回新内存空间的首地址
三、需扩展的空间后方没有足够的空间可供扩展,并且堆区中也没有符合需要开辟的内存大小的空间
结果就是开辟内存失败,返回一个NULL
五、free
void free(void* memblock);
free函数的作用就是将malloc、calloc以及realloc函数申请的动态内存空间释放,其释放空间的大小取决于之前申请的内存空间的大小
使用方式非常简单,就是在使用完后加上两个语句:
free(p); // p为要释放的代码块的首地址
p = NULL; // 必不可少
我们也已经看到了,上面每一个开辟了动态内存的代码,在使用完该动态内存后,都将该内存空间释放了(即还给操作系统),如果使用完动态内存后忘记将其空间释放,便会造成内存泄漏的问题
- 内存泄漏(MemoryLeak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果
- 内存泄漏缺陷具有隐蔽性、积累性的特征,比其他内存非法访问错误更难检测。因为内存泄漏的产生原因是内存块未被释放,属于遗漏型缺陷而不是过错型缺陷。此外,内存泄漏通常不会直接产生可观察的错误症状,而是逐渐积累,降低系统整体性能,极端的情况下可能使系统崩溃
就像对一个人来说,有时候急性病并不可怕,现发现治,怕得就是躲藏深处的慢性病,等到发作那天,纵是八方助力也无力回天
就像一段感情来说,有时候激烈的争吵并不可怕,这叫磨合,怕得就是两人不说话不沟通,矛盾隔阂由小积大,往后就是有力挽回也再难回到从前
请注意!
- 在释放代码块后,必须将该代码块的首地址改为NULL,否则该指针将变为野指针,我们都知道,野指针非常危险
- 如果传入free函数的为空指针(NULL),则free函数什么也不做
六、常见的动态内存错误
对NULL指针进行解引用操作
如果我们在调用了malloc、calloc以及realloc函数之后,没有检测返回的指针的有效性(即是否为NULL指针),那我们在后面使用该指针的时候就可能会导致对NULL指针进行解引用操作,对于,我们应当予以避免
对动态开辟空间的越界访问
我们需要时刻注意,不能访问未申请的动态内存空间。比如你向动态内存申请了10个字节,那就绝不能访问第11个字节
对非动态开辟的内存使用free释放
free函数只能释放动态开辟的内存空间
使用free释放动态开辟内存的一部分
free函数只能从开辟好的动态内存空间的起始位置开始释放,所以使用free函数释放动态内存时,传入的指针必须是当时开辟内存时返回的指针
对同一块内存多次释放
对同一块动态内存空间只能释放一次。避免这个问题的出现也很简单,我们只要记住在第一次释放完空间后立即将该指针置为NULL即可,因为当传入free函数的指针为NULL指针时,free函数什么也不做(也就不会出现对同一内存多次释放的问题)
动态开辟内存忘记释放(内存泄漏)
一定要做到自己开辟的动态内存自己记得释放,也许你觉得这件事没什么,但当你需要从几十万甚至几百万行代码中找出一个因忘记释放动态内存而造成的内存泄漏问题时,你就会真正知道这件事的重要性
七、柔性数组(flexible array)
定义
在C99中,结构体最后一个成员为未知大小的数组,这个被称为柔性数组的成员,帮助用户根据要求自己给大小,更加轻松地处理可变长度的数据结构
struct st_type
{
int i;
int nums[0]; // 有些编译器可能会编译失败,可以化成nums[]
}type_a;
几个定语修饰词要搞明白,最后一个成员、未知大小、数组
特性
- 结构体中至少有一个成员在柔性数组前面(如果顺序错了,也会报错)
- sizeof返回的这种结构大小是不包含柔性数组的内存,编译器在计算结构体大小时会忽略柔性数组成员
- 对包含柔性数组的结构体,申请空间的时候适度大于结构体的大小,以便于适应柔性数组的大小
typedef struct st_type
{
int i;
int a[0]; // 柔性数组成员
}type_a;
int main()
{
printf("%zd\n", sizeof(type_a)); // 输出的是4
return 0;
}
对比
下面来看两份代码:
struct S
{
char c;
int n;
int arr[0]; //柔性数组
};
struct S
{
int n; //4
int arr[]; //
};
int main()
{
// printf("%zd\n", sizeof(struct S));
struct S* ps = (struct S*)malloc(sizeof(struct S) + 5*sizeof(int));
if (ps == NULL)
{
perror("malloc");
return 1;
}
ps->n = 100;
int i = 0;
for (i = 0; i < 5; i++)
{
ps->arr[i] = i;
}
// 调整空间
struct S* ptr = (struct S*)realloc(ps, sizeof(struct S)+10*sizeof(int));
if (ptr != NULL)
{
ps = ptr;
}
//....
// 释放
free(ps);
ps = NULL;
return 0;
}
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;
int i = 0;
for (i = 0; i < 5; i++)
{
ps -> arr[i] = i;
}
// 调整数组大小
int* ptr = (int*)realloc(ps -> arr, 10 * sizeof(int));
if (ptr != NULL)
{
ps -> arr = ptr;
}
// 使用
// ...
// 释放
free(ps->arr);
free(ps);
return 0;
}
对比之下,你更喜欢哪一种方式?
对于第一种使用柔性数组的办法
一个好处是如果里面做了二次内存分配,并把整个结构体返回给用户。当用户需要释放空间时候,并不知道这个结构体内成员也需要free,如果结构体的内存以及其成员要的内存一次性分配好,返回一个结构体指针,用户只需要一次free就可以把所有的内存也给释放掉了
另一个好处是连续的内存有益于提高访问速度,也有益于减少内存碎片
八、C/C++中程序内存区域规划
你可以先大概有个认识,具体一点可以移步Cpp专栏的动态内存管理,那边介绍得更为详尽一些
Cpp内存管理
我们可以来稍微了解一下几个内存区域的作用:
一、栈区(stack):
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放,栈内存分配运算内置于处理器的指令集中,效率很高,但是分的内存容量有限,栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等
二、堆区(heap):
一般由程序员分配释放,若程序员不释放,程序结束时可能由OS(操作系统)回收
三、数据段(静态区):
(static)存放全局变量、静态数据。程序结束后由系统释放
四、代码段:
存放函数体(类成员函数和全局函数)的二进制代码
总结
应该不算太难,算修炼内功,但很重要,部分代码必须自己去实践!