一,为什么存在动态内存分配
我们已经掌握的内存开辟方式有:
int val = 20; //在栈空间上开辟四个字节
char arr[10] = {0}; //在栈空间上开辟10个字节的连续空间
但是上述的开辟空间的方式有两个特点:
1. 空间开辟大小是固定的。
2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。
这时候就只能试试动态存开辟了
二,动态内存函数的介绍
2,1 malloc和free
2.1.1,C语言提供了一个动态内存开辟的函数
#include<stdlib.h> #include<malloc.h>//两个都行,但是第一个较为常用 void* malloc (size_t size);
//1, 这里的参数(size)是要开辟空间的字节数。
//2, 这里是向内存堆区申请空间,不是栈区。
//3, 这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
//5, 如果开辟成功,则返回一个指向开辟好空间的指针。
//5, 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
//6, 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
//7, 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器
举例:
int main()
{
//int arr[40];//向栈区申请了40个字节,用来存放arr数组。
//char arr[40];//向栈区申请了40个字节,用来存放arr数组。
int* ptr = (int*)malloc(40);//向堆区申请40个字节 把他的首地址传给 p ,并且强制类型转换为(int*)类型的
int* p = ptr;//一般未来防止ptr地址丢失或者不知道ptr在运行之后指向哪里
//一般会创建另一个变量p来代替ptr参与修改。
if (p == NULL) //如果开辟错误他就会打印错误信息。
{
perror("malloc");
return 1;
}
for (int i = 0; i < 10; i++)//给这块空间赋值
{
*(p+i) = i;
}
for (int i = 0; i < 10; i++)//打印这块空间
{
printf("%d", *(p + i));
}
//打印结果为:0123456789
return 0;
}
2.1.2,C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的
#include<stdlib.h> #include<malloc.h>//两个都行,但是第一个较为常用 void free (void* ptr);
//ptr是一块动态内存开辟的空间的起始地址。
free函数用来释放动态开辟的内存。
//1, 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
//2, 如果参数 ptr 是NULL指针,则函数什么事都不做。//3, 一般在使用完后还会把刚刚创建的ptr变量置空。防止非法访问。
//4, 在释放完后 ptr 就变成一个野指针了。
//5,free释放的空间必须是动态开辟的。
在malloc开辟空间后可以不free释放吗?
//1, 当我们不释放动态申请的空间的时候,如果程序结束,动态申请的内存由操作系统自动回收。
//2, 如果程序不结束,动态内存是不会自动回收的,就会形成内存泄漏的问题。
//3, 内存泄漏就会一直吃电脑的内存,内存被消耗完后电脑就会死机。
//3, 一般都是要用free释放自己申请的动态内存,并把指针变量置空。
举例:
#include<stdlib.h>
int main()
{
//申请一块空间
int* ptr = (int*)malloc(40);
//检验是否开辟成功
if (ptr == NULL)
{
perror("malloc");//打印错误原因
return 1;
}
//使用这块空间----
//----
//释放ptr所指向的动态内存
free(ptr);
//参数是开辟的起始地址。
//之后ptr已经不属于这个程序了再去使用的话就变为一个野指针了。
ptr = NULL;//一般在释放空间后会再一次给ptr置空
//因为ptr还记得上层开辟的空间的地址。
//为了避免非法访问,需要给ptr置空。
//以后在使用的时候判断一下。
if(ptr != NULL)
{
//使用就可以了。
}
//*ptr = 100; //但是直接使用就可能构成非法访问。
return 0;
}
2,2 calloc
在内存中申请数组(动态内存),并且初始化为0
#include<stdlib.h>
#include<malloc.h>
void* calloc (size_t num, size_t size);
//参数 num 是元素的个数
//参数 size 是每一个元素的大小,单位是字节。
说明
//1, 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
//2, 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0
//3, 这里也是向内存堆区申请空间,不是栈区。
//3, 这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
//5, 如果开辟成功,则返回一个指向开辟好空间的指针。
//5, 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
//6, 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
//7, 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器
举例:
#include<stdio.h>
#include<stdlib.h>
int main()
{
//申请40个字节的动态内存
int* p1 = (int*)malloc(40);//未初始化 值为随机数
//申请10个整型的空间的动态内存。
int* p2 = (int*)calloc(10, sizeof(int));//已初始化为0.
//检查是否申请成功
if (p2 == NULL)
{
perror("calloc");
return 1;
}
//使用
for (int i = 0; i < 10; i++)
{
printf("%d", *(p2 + i));
}
free(p2);//使用完成后也是需要释放的
p2 = NULL; //也是需要置空的。
return 0;
}
调试起来我们就能看到初始化为0的结果。
2,3 realloc
重新开辟 (不够用了再次开辟)(调大调小)
#include<stdlib.h>
#include<malloc.h>
void* realloc (void* ptr, size_t size);
//参数 ptr 是要调整的内存地址
//参数 size 是调整之后新大小。
//返回的时候会分为两种情况
//1 如果想要扩容的空间后面有足够的未使用的空间的时候,直接扩容后返回输入的ptr地址。
//本来的数据不会发生改变。
//2 如果想要扩容的空间后面没有足够的未使用的空间的时候,
//会在内存中重新找一块符合条件的区域,返回这块区域的首地址(新的地址),
//并把数据拷贝到新的区域,并free释放本来想要扩容的空间。
说明:
//1, 返回值为调整之后的内存起始位置。
//2, 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。//3, realloc在调整内存空间的是存在两种情况:
情况1:原有空间之后有足够大的空间
情况2:原有空间之后没有足够大的空间
情况1
当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。返回原来的地址。
情况2
当是情况2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。
由于上述的两种情况,realloc函数的使用就要注意一些。
举例:
#include<limits.h>
int main()
{
int* p1 = (int*)malloc(40);
if (p1 == NULL)
{
perror("malloc");
return 1;
}
使用
//for (int i = 0; i < 10; i++)
//{
// *(p1 + i) = i;//0,1,2,3,4----
// //放了是10个元素
//}
//假如空间不够了, 需要20个元素(int)类型
//p1 = realloc(p1, 80);//这样写是不合理的 ,这样会有一种情况,如果扩容失败,他会返回一个NULL
//这个时候p1就被置空了。p1==NULL
//建议不要直接用p1来接受扩容后的空间地址,应该做一步周转。
int* ptr = realloc(p1, 80);
if (ptr != NULL)
{
p1 = ptr;
}//这样就会更加合理一点。
if (ptr == NULL)//检验是否扩容成功
{
perror("realloc");
return 1;
}
for (int i = 0; i < 20; i++)
{
*(p1 + i) = i;//0,1,2,3,4----
//放了是20个元素
}
free(p1);
p1 = NULL;
return 0;
}
三, 常见的动态内存错误
3,1 对NULL指针的解引用操作
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
p = NULL;
}
//改进
void test()
{
int *p = (int *)malloc(INT_MAX/4);
//检验
if(p == NULL)
{
perror("malloc");//打印错误信息
return 1; //注意这里一定要返回,不然后面的代码就可能会构成非法访问
}
*p = 20;//如果p的值是NULL,就会有问题
free(p);
p = NULL;
}
3,2 对动态开辟的空间越界访问
void test()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));
//if (NULL == p)
//{
// exit(EXIT_FAILURE);//结束进程
//}
//也可以这样写
if (p == NULL)
{
perror("malloc");//打印错误信息。
return 1;//返回
}
for (i = 0; i <= 10; i++)
{
*(p + i) = i;//当i是10的时候越界访问
//构成非法访问。
//要自己好好检查
}
free(p);
}
3,3对非动态内存的释放
void test()
{
int a = 10;
int* p = &a;//非动态内存空间的地址
free(p);//ok?
p = NULL;
//这样写法是不行的,不是动态内存空间就不能用free释放。
//这种局部变量的内存空间是不需要我们自己释放的
//编译器会自己回收。
return 0;
}
3,4 使用free释放一块动态开辟的内存空间的一部分
int test()
{
int* p = (int*)malloc(100);
if (p == NULL)
{
return 1;
}
//p在使用的时候被改变。
p++;
//所以建议创建一个零时的来代替。
//注意一定要记住初始位置。
free(p);//p不再指向动态内存的起始位置,
//这里会报错的,
p = NULL;
return 0;
}
3,5 对同一块动态内存多次释放
int test()
{
int* p = (int*)malloc(100);
if (p == NULL)
{
return 1;
}
free(p);
free(p);//重复释放,这也是不允许的
p = NULL;
return 0;
}
3,6 动态开辟内存忘记释放(内存泄漏)
void test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
//内存未释放
//free(p);
//p = NULL;
}
int main()
{
test();//出函数以后就无法释放了
//假设这里写了一个死循环程序无法结束
while (1);
//假如一个项目中这样的内存很多的话
//内存就会被吃完了
//这样机器就会死机。
//假如这样一个程序一天24小时都在运行的话
//每天都在吃内存,这是很可怕的。
//而且这样的错误很难查找
}
四:经典例题
4,1 例题一:
讲解:
#include<string.h>
void GetMemory(char* p)//这个p是str的零时拷贝传过来的时候p里面放的是空指针
{
p = (char*)malloc(100);//注意这里没有改变str 只是改变了p
//未使用free释放,也是不对的。(内存泄漏了)
}
void Test(void)
{
char* str = NULL;
GetMemory(str);//出了函数指针p就被销毁了
//所以开辟的动态内存空间就找不到了,
strcpy(str, "hello world");
//str现在是空指针,程序就会崩溃
printf(str);//这里是没有问题的
printf("haha\n");//这个时候就是把常量字符串的首地址传给printf函数。
//并不是把整个字符串都传过去了。
//同样上面也是一样的原理。是可以的。没有错误
}
int main()
{
Test();
return 0;
}
正确版本
void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
4,2 例题二:(返回栈空间地址的问题)
讲解:
char* GetMemory(void)
{
char p[] = "hello world";
return p;//这里虽然返回了数组的地址
//但是数组p[] 已经被编译器回收了
//地址被记住了,但是可以通过地址找到那块空间,
//但是那块空间已经还给操作系统了,值也变为随机数了
//返回的地址已经是一个野指针了,
}
void Test(void)
{
char* str = NULL;
str = GetMemory();//那块空间的地址被记住了,但是那块空间已经被释放了
//str就是一个野指针了,会构成非法访问。那块空间已经不是该函数的了
//已经还给操作系统了。
printf(str);
}
int main()
{
Test();
return 0;
}
4,3例题三:
#include<stdio.h>
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
int main()
{
Test();
return 0;
}
讲解:
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);//申请了100字节的空间,
//把地址通过解引用传给了指针变量str
//注意看到malloc后要注意,释放申请的空间。
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
//这里有个释放空间
//free(str);
//str = NULL;
}
int main()
{
Test();
return 0;
}
4,4例题四:
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);//到这,空间已经被释放了。
//但是str没有置空,还存放着原来的地址。
//str已经是野指针了(使用个不属于自己的空间)
//应该在这里把str置空
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}//这里使用就是非法访问了
}
int main()
{
Test();
return 0;
}
5,5 例题五:
int* Test(void)
{
int a = 10;
return (&a);
}//这个代码就是返回栈空间地址的问题了
//返回变量的地址就是不对的,这块栈空间已经被释放了
//再次通过这个地址去访问的时候就会出现问题了
int Test(void)
{
int a = 10;
return a;
}//这个代码是没有任何问题的
//返回这个变量是没有问题的。
int main()
{
Test();
return 0;
}
五,C/C++程序的内存开辟
c程序内存分配的几个区域:
1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结
束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是
分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返
回地址等。
2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS(操作系统)回收 。分。配方式类似于链表。
3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
有了这幅图,我们就可以更好的理解在《C语言初识》中讲的static关键字修饰局部变量的例子了。
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序
结束才销毁
所以生命周期变长。
六,柔性数组
也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。
C99 中,结构体中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
举例:
typedef struct s
{
int numn;
double a;
int arr[];//柔性数组成员
}s;
或者
typedef struct s
{
int numn;
double a;
int arr[0];//柔性数组成员
}s;
柔性数组的特点:
- 结构体中柔性数组前面必须有其他成员(至少一个)。
- sizeof计算结构体大小的时候,不计算柔性数组的内存。
- 但是在确定最大对齐数的时候会参与确定。
- 包含柔性数组的结构体用malloc()函数进行内存动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小。
使用:
#include<stdlib.h>
struct s1
{
int a;
int arr[];//给柔性数组成员开辟10个整型的大小
};
int main()
{
//分配的内存应该大于结构体的大小,以适应柔性数组的预期大小。
struct s1* ps = (struct s1*)malloc(sizeof(struct s1) + 40);//说的就是这个意思。
//访问a的时候
ps->a = 10;
//访问arr的时候
for (int i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
for (int i = 0; i < 10; i++)
{
printf("%d", ps->arr[i]);
}//0123456789
free(ps);
ps = NULL;
return 0;
}
柔性数组的优势:
我们来观察一下下面代码:
#include<stdio.h>
#include<stdlib.h>
typedef struct sss
{
int a;
int* arr;
}sss;
int main()
{
sss ret;
ret.a = 10;
ret.arr = (int*)malloc(40);
if (ret.arr == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i < 10; i++)
{
*(ret.arr + i) = i;
}
for (int i = 0; i < 10; i++)
{
printf("%d", *(ret.arr + i));
}
free(ret.arr);
ret.arr = NULL;
return 0;
}
这个代码和柔性数组所达到的目的相同。但是柔性数组比他好:
1,如果我们把数据开辟在堆区,就需要连续两次free,这样不好。
2,第二种内存不连续,连续的内存有利于提高访问速度,第一种也有利于减少内存碎片。
拓展
1,内存的结构: