一、为什么存在动态内存管理
我们已经掌握的内存开辟方式有:
int val = 20; //在栈空间开辟四个字节
char arr[10] ={0}; //在栈空间开辟10个字节的连续空间
但上述开辟空间的方式有两个特点:
- 空间开辟大小是固定的
- 数组在声明的时候,必须指定数组的长度,他需要内存在编译时分配对于空间的需求,不仅仅是上述的情况,有时候我们需要的空间大小在程序运行的时候才知道,那数组的编译开辟空间的方式就不能满足了
二、动态内存分配函数的介绍
1.malloc
void* malloc (size_t size);
函数向内存申请一块连续可用的空间,
返回值:返回这个空间的指针。
参数:size为需要开辟空间的字节数
头文件:#include<stdlib.h>
注:malloc开辟的空间并没有初始化!
int main()
{
int* p = NULL;
p = (int*)malloc(10 * sizeof(int)); //开辟10个整型大小的空间 赋值给指针p
//判断是否开辟成功
if (p != NULL)
{
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = 0; //将空间元素置0
}
}
return 0;
}
- 如果开辟成功,则返回一个指向开辟好空间的指针
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查
- 返回值类型是void* ,所以,malloc函数并不知道开辟空间的类型,具体在使用的时候自行强制转换
- 如果参数size 为0,malloc的行为是标准时未定义的,取决于编译器
2.free
void free (void* ptr);
用来释放动态开辟的内存,
只是空间释放,开辟的地址没有清空,所以要将置NULL(防止野指针)
参数:需要释放的动态内存空间
int main()
{
int* p = NULL;
p = (int*)malloc(10 * sizeof(int)); //开辟10个整型大小的空间 赋值给指针p
free(p);
p = NULL; //置NULL,防止野指针
return 0;
}
3.calloc
void* calloc (size_t num, size_t size);
calloc函数也可用来动态地址分配
参数: num 个大小为 size 的元素开辟一块空间;
与函数malloc的区别:
只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全0,
int main()
{
int* p = NULL;
p =(int*) calloc(10, sizeof(int));
if (p != NULL)
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
}
free(p);
p = NULL;
return 0;
}
4.realloc
void* realloc (void* ptr, size_t size);
realloc函数就可以做到对动态开辟内存大小的调整。(动态内存大小调整)
参数:
1、ptr是要调整的内存地址
2、size是调整后的新大小
返回值:是调整之后的内存起始地址
这个函数在调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
realloc在调整内存空间存在两种情况:
情况1:原有空间之后有足够大的空间:
直接在原有内存上直接追加空间,原有空间的数据不发生变化
情况2:原有空间之后的空间不够大:
在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是个新的内存地址
int main()
{
int* p = NULL;
p =(int*) calloc(10, sizeof(int));
if (p != NULL)
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
}
printf("\n");
p = realloc(p, 20);
if (p != NULL)
{
int i = 0;
for (i = 0; i < 20; i++) //对新开辟的空间全部置0
{
*(p + i) = 0;
}
for (i = 0; i <20; i++)
{
printf("%d ", *(p + i)); //打印出来
}
}
free(p);
p = NULL;
return 0;
}
三、常见的动态内存错误
1、对NUUL指针的解引用操作
int main() {
//动态地址分配
int* p = (int*)malloc(10 * sizeof(int));
*p = 20; //如果p = NULL,则为非法访问内存
free(p);
p = NULL;
}
malloc()申请一块动态地址可能申请失败,失败返回NULL,
2、对动态开辟的空间的越界访问
int main() {
//动态地址分配
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
return 0;
}
int i = 0;
//for循环,i的越界
for (i = 0; i < 40;i++)
{
*(p + i) = i;
}
free(p);
p = NULL;
}
malloc动态开辟了10*sizeof(int) 大小的空间,而for循环访问了40个元素,越界访问了
3、对非动态开辟的内存使用free释放
int main() {
int arr[] = { 0 }; //栈区
int* p = arr;
//释放
free(p);
p = NULL;
}
free只适用于malloc,realloc,calloc开辟的动态内存
4、使用free释放一块动态开辟内存的一部分
int main() {
//动态地址分配
int* p = (int*)malloc(10);
if (p == NULL) {
return 0;
}
int i = 0;
//for循环,i的越界
for (i = 0; i < 5;i++) {
*p ++ = i; //error
}
free(p);
p = NULL;
}
找不到动态分配的起始地址
5、对同一块动态开辟的空间,多次释放
int main(){
int* p = (int*)malloc(100);
//使用
//释放
free(p);
//再释放
free(p);
return 0;
}
避免方法:释放后,将p置为NULL;
6、动态开辟的空间,没有释放(内存泄漏)
void test() {
int* p = (int*)malloc(100);
if (p = NULL) {
return 0;
}
}
int main() {
test();
return 0;
}
动态开辟的空间有2种回收方式:
1、主动free()
2、程序结束
当,忘记释放后,程序一直开着,就会存在内存泄漏
四、几个经典的笔试题
1、题目1
void getmemory(char* p){
p = (char*)malloc(100);
}
void test(void) {
char* str = NULL;
getmemory(str);
strcpy(str, "Hello");
printf(str);
}
int main() {
test();
return 0;
}
拷贝失败,没有释放,内存泄漏,引发异常,没有输出结果
解析:
1、str传给getmemory函数的时候是值传递,所以getmemory函数中的形参p是str的一份临时拷贝。在动态申请空间地址时,不会影响外面的str,所以getmemory函数返回之后,str依然是NULL,进而strcpy()失败
2、当getmemory函数返回之后,形参p销毁,使得动态开辟的空间100字节存在内存泄漏,无法释放
修改:
void test(void) {
char* str = NULL;
str = getmemory(str);
strcpy(str, "Hello");
printf(str);
free(str);
str = NULL;
}
int main() {
test();
return 0;
}
C/C++程序内存分配的几个区域:
1.栈区(stack):在执行函数时,函数内局部变量的存循单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
2.堆区(heap) :一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。
3.数据段(静态区) (static):存放全局变量、静态数据。程序结束后由系统释放。
4.代码段:存放函数体(类成员函数和全局函数)的二进制代码。
五、柔性函数
c99中,结构体中的最后一个元素允许是未知的数组,这叫做柔性数组成员
typedef struct st_type{
int i;
int a[0];
}type_a
或者
typedef struct st_type{
int i;
int a[0];
//或 int a[];
}type_a
柔性数组的特点
1.结构体中的柔性数组成员前面必须至少有一个其他成员。
2.sizeof()返回的这种结构的大小不包括柔性数组的内存。
3.包含柔性数组成员的结构用malloc()函数进行内存动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小
struct stu
{
char ch;
int age;
char name[];
};
2、柔性数组的使用
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
printf("%d\n", sizeof(type_a));//输出的是4
int i = 0;
type_a* p = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int));
//判断
if (p == NULL)
{
perror("malloc");
return 1;
}
p->i = 100;
for (i = 0; i < 100; i++)
{
p->a[i] = i;
}
free(p);
p = NULL;
3、柔性数组的优势
好处1、方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个绍构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
好处2、有利于提升访问速度
连续的内存有益于提高访问速度,也有益于减少内存碎片(个人认为,不会提升多少,反正都需要用做偏移量的加法来寻址)
致谢
以上就是我对【C语言】动态内存管理的看法和介绍,身为初学者,自知有很多不足,文中不恰当,错误之处还望得到大佬指点!!!感激不尽
愿星光照亮赶路人!
另外 ,分享一篇关于C语言的文章C语言结构体里的成员数组和指针