1.为什么会存在动态内存的分配呢
我们之前开辟的空间又是什么呢?
char a;
int arr[10];
这些都属于静态内存。
静态内存与动态内存的不同
1.空间开辟的大小是固定的(只有修改程序才可以该空间大小)
2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
有时候我们需要在程序运行是才能确定内存大小需要多少,如果用静态内存的话,可能造成空间浪费,也可能造成空间不足,使数据丢失。为了避免这种现象,就有了动态内存的操作。
2.动态内存函数
2.1 malloc
void* malloc (size_t size);
malloc和声明在 stdlib.h头文件中。
这个函数的作用是向内存申请一个连续的空间,并且返回申请到空间的地址。
开辟空间不一定会成功,如果失败则返回NULL空指针。
返回类型为void*:因为malloc函数开辟空间并不知道你要开辟什么空间类型,自己用的时候加上强制类型转换就好
size表示开辟空间的大小(单位:字节)
如果参数size为0,malloc的行为是未定义(取决于编译器)
2.2 free
void free (void* ptr);
free声明在stdlib.h头文件中。
free是用来释放动态开辟的内存的。(动态内存开辟的空间在堆区)
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的.
如果参数 ptr 是NULL指针,则函数什么事都不做。
2.3 例子
#include <stdio.h>
#include <stdio.h>
#include<stdlib.h>
int main()
{
int num = 0;
scanf("%d", &num);
int i = 0;
int* ptr = NULL;
ptr = (int*)malloc(num * sizeof(int));
if (NULL != ptr)//判断ptr指针是否为空
{
for (i = 0; i < num; i++)
{
*(ptr + i) = i+1;
}
}
for (i = 0; i < num; i++)
{
printf("%d ",*(ptr+i));
}
free(ptr);//释放ptr所指向的动态内存
ptr = NULL;//是否有必要?
return 0;
}
比如上面这个程序,他需要在运行时才可以确定内存大小,所以我们需要用到动态内存,因为静态内存达不到。
还有有几个重点:
1.在用malloc开辟空间后,后面要跟一个判断它是否为NULL,如果为空则开辟失败(不能把一个空指针赋给其他变量去操作,养成一个好习惯动态内存开辟后用if判断是否为空)
2.将指针释放后,也要将他置为空,避免不小心在次用到这个指针(野指针会对程序造成严重问题)
2.4 calloc
void* calloc (size_t num, size_t size);
这个函数的作用也是是向内存申请一个连续的空间,并且返回申请到空间的地址。
他和malloc函数很像但是也有不同。
相同:都是开辟一段连续的内存空间(堆区),返回空间首地址
不同:参数不同,malloc一个参数,代表开辟大小(字节)
calloc两个参数一个是元素个数,一个是元素大小。空间大小 = 个数 * 大小
malloc开辟的空间没有初识化,calloc开辟的空间进行了初始化(空间内数据都为0)
例子:
这就是两者的不同。
同样用完后需要用free释放,将其置为空指针。
2.5 realloc
realloc函数的出现是动态内存更加的灵活了
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时
候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小
的调整。
void* realloc (void* ptr, size_t size);
ptr 是要调整的内存地址
size 调整之后新大小(单位为字节)
返回值为调整之后的内存起始位置。
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
realloc调整内存有两种情况:
第一种 :原有内存太小
第二种 :原有内存太大
举个例子
#include<stdio.h>
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* ptr = (int*)malloc(100);
if (ptr == NULL)
{
return 1;
}
//扩展容量
//不严谨书写
ptr = (int*)realloc(ptr, 1000);//这样写是不好的,如果申请失败,返回的NULL赋给ptr则之前的数据也找不到了
//正确
int* p = NULL;
p = realloc(ptr, 1000);
if (p != NULL)
{
ptr = p;
}
//业务处理
free(ptr);
return 0;
}
3.动态内存常见错误
3.1 对NULL指针解引用操作呢
#include<stdio.h>
#include<stdio.h>
#include<stdlib.h>
void test()
{
int* p = (int*)malloc(INT_MAX*4 );
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
int main()
{
test();
return 0;
}
这里malloc申请失败,返回了NULL,将NULL赋值给p,再对p进行解引用就会出错。
3.2 对开辟的空间进行越界访问
#include<stdio.h>
#include<stdio.h>
#include<stdlib.h>
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);
}
int main()
{
test();
return 0;
}
这里之开辟了十个整型的空间,而你却访问第十一个整型空间会出现越界访问,程序会崩掉。
3.3 对非动态开辟内存进行free
#include<stdio.h>
#include<stdio.h>
#include<stdlib.h>
void test()
{
int a = 10;
int* p = &a;
free(p);//ok?
}
int main()
{
test();
return 0;
}
对非动态内存进行free也是非法的
3.4 使用free释放一块动态内存的一部分
#include<stdio.h>
#include<stdio.h>
#include<stdlib.h>
void test()
{
int* p = (int*)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
int main()
{
test();
return 0;
}
释放动态内存必须用起始地址释放。
3.5对同一块内存进行多次释放
#include<stdio.h>
#include<stdio.h>
#include<stdlib.h>
void test()
{
int* p = (int*)malloc(100);
free(p);
free(p);//重复释放
}
int main()
{
test();
return 0;
}
对同一块空间进行多次释放也会出错。
3.6 动态开辟内存忘记释放(内存泄漏)
忘记释放不再使用的动态开辟的空间会造成内存泄漏。
切记: 动态开辟的空间一定要释放,并且正确释放 。
(尽管程序结束时会自己释放,但是如果程序运行时间过长就会使程序一直吃内存)
#include<stdio.h>
#include<stdio.h>
#include<stdlib.h>
void test()
{
int* p = (int*)malloc(100);
}
int main()
{
while (1)
test();
return 0;
}
这个程序会一直吃你的内存,知道吃完为止。
4.经典的试题
来看看自己是否学会了,检验一下自己
4.1 题目1:
void GetMemory(char* p)
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void test()
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
这个程序是运行不过去的
这个相当于值传递,p相当于临时拷贝了str的内容,然后进行动态内存的开辟,将地址存放再p中,改变p并不会改变str
相当与下面这个程序
题目1如何修改就正确了呢?
void GetMemory(char** p)
{
p = (char*)malloc(100);
}GetMemory(&str);
这样就正确了
4.2题目2:
char* GetMemory(void)
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void test()
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
test();
return 0;
}
这个会出现非法访问,有些编译器可以打印但也是乱码
因为你开辟了一个数组在栈区,当你退出这个函数是这块空间就已经还给操作系统。如果你再访问就属于非法访问,因为空间不是你的了。
4.3题目3:
void GetMemory(char** p, int num)
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void test()
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
int main()
{
test();
return 0;
}
这个函数有一个问题(仔细看)
问题:没有free释放掉动态内存
4.4 题目4:
#include<stdio.h>
#include<stdio.h>
#include<stdlib.h>
void test()
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
test();
return 0;
}
这个程序free释放后没有置空NULL,再次对一块不是你的内存进行访问,内存的非法访问
5.C程序的内存开辟
栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结
束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是
分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返
回地址等。
堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分
配方式类似于链表。
数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
代码段:存放函数体(类成员函数和全局函数)的二进制代码。
6.柔性数组
也许你从来没有听说过柔性数组这个概念,但是它确实是存在的。
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
例如:
struct S
struct S
{
int i;
int a[];
};
struct S1
{
int i;
int a[0];
};
这两种都对,有些编译器支持第一种,有些支持第二种。
6.1 柔性数组的特点
结构中的柔性数组成员前面必须至少一个其他成员。
sizeof 返回的这种结构大小不包括柔性数组的内存。
包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大
小,以适应柔性数组的预期大小。
这里计算结构体大小时,柔性数组并不计算在内。
6.2 柔性数组的使用
直接看代码
#include<stdio.h>
#include<stdio.h>
#include<stdlib.h>
struct S
{
int i;
int a[];
};
int main()
{
int i = 0;
struct S* p = (struct S*)malloc(sizeof(struct S) + 100 * sizeof(int));
p->i = 100;
for (i = 0; i < 100; i++)
{
p->a[i] = i;
}
free(p);
return 0;
}
这便是柔性数组的使用
这里的a相当于再堆区开辟了一个100个整型的连续空间
6.3柔性数组的优点
#include<stdio.h>
#include<stdio.h>
#include<stdlib.h>
struct S
{
int i;
int *a;
};
int main()
{
struct S* p = (struct S*)malloc(sizeof(struct S));
p->i = 100;
p->a = (int*)malloc(p->i * sizeof(int));
//业务处理
for (int i = 0; i < 100; i++) {
p->a[i] = i;
}
//释放空间
free(p->a);
p->a = NULL;
free(p);
p = NULL;
return 0;
}
有人会说用动态内存也可以完成上面柔性数组的任务
为什么要用柔性数组呢?
第一个好处是 :方便内存释放
柔性数组只需要释放一次
动态开辟需要释放两次
第二个好处是 :这样有利于访问速度.
连续的内存有益于提高访问速度,也有益于减少内存碎片。
柔性数组是将结构体所有元素放在一块连续的空间
而动态开辟的将所有元素放在两块连续空间内
#include<stdio.h>
#include<stdio.h>
#include<stdlib.h>
struct S
{
int i;
int *a;
};
int main()
{
struct S* p = (struct S*)malloc(sizeof(struct S));
p->i = 100;
p->a = (int*)malloc(p->i * sizeof(int));
//业务处理
for (int i = 0; i < 100; i++) {
p->a[i] = i;
}
//释放空间
free(p->a);
p->a = NULL;
free(p);
p = NULL;
return 0;
}
有人会说用动态内存也可以完成上面柔性数组的任务
为什么要用柔性数组呢?
第一个好处是 :方便内存释放
柔性数组只需要释放一次
动态开辟需要释放两次
第二个好处是 :这样有利于访问速度.
连续的内存有益于提高访问速度,也有益于减少内存碎片。
柔性数组是将结构体所有元素放在一块连续的空间
而动态开辟的将所有元素放在两块连续空间内
访问没有柔性数组快