目录
一、为什么存在动态内存分配
我们以往都是采用使用创建变量或者数组的方式开辟空间,但是这种方式往往不太灵活,无法达到对内存空间的任意使用,为了达到这种目标,我们采用动态内存分配的方式。
动态内存开辟都是在堆区开辟的;
二、动态内存函数
2.1malloc函数和free函数
C语言提供动态内存开辟的函数
malloc函数:allocates memory blocks
头文件:<stdlib.h>
malloc函数:void* malloc(size_t size) //函数声明
参数解释:
size:开辟的字节数
void*:返回一个无类型的指针,指向开辟的空间,或者当堆区没有可用空间时,返回一个空指针
函数的使用:开辟10个整型的空间
采用数组的方式: int arr1[10]; //开辟在栈区的空间
采用动态存开辟的方式:void* p=malloc(10*sizeof(int))
malloc函数返回的指针类型是void*
如果要使用,应当根据自己的需要进行相应的强制类型转换
用于整型:int* p=(int*)malloc(10*sizeof(int)) //最好对malloc进行强制类型转换
使用这些空间前应该先判断指针是否指向空指针,指向空指针就报错
开辟的内存空间的使用--采用指针+偏移量的方法
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* pm = (int*)malloc(10 * sizeof(int));
if (pm == NULL)
{
perror("main:");
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(pm + i) = 10 - i;
printf("%d ", *(pm + i));
}
return 0;
}
free函数:专门用来动态内存的回收与释放
free函数:void free (void* ptr)
头文件:<stdlib.h>
使用完该空间,使用free函数释放空间,把空间还回去
free函数的参数是刚刚开辟的指针,但是没有并没有把该指针的内容置为空指针
需要手动将该指针置空
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* pm = (int*)malloc(10 * sizeof(int));
if (pm == NULL)
{
perror("main:");
}
int i = 0;
if(pm!=NULL) //加一个判断可以取消警告:取消对空指针的引用
{
for (i = 0; i < 10; i++)
{
pm[i] = 10 - i;
printf("%d ", *(pm + i));
}
}
free(pm); //传递的是pm的值,不可能改变pm的内容,传递其地址倒是有可能
pm = NULL; //自己动手把指针置空,防止以后使用pm指针,导致非法越界访问
return 0;
}
注意:
1.如果 free(ptr) 中ptr不是动态内存开辟的,那么free的行为就是未被定义的行为
2..如果 free(ptr) 中ptr是空指针,那么free什么也不做
3.malloc函数和free函数要配合使用,成对出现
2.2calloc函数和free函数
calloc函数:动态内存分配
头文件:<stdlib.h> and <malloc.h> //两者之一即可?
allocate cates an array in memory with elements initialized to 0
开辟一个元素初始化为0的数组
calloc函数:void* calloc(size_t num,size_t size)
参数解释:
1.为num个大小为size字节的元素开辟一块空间,并把空间的每个字节初始化为0
2.与malloc函数不同的是:calloc函数再返回地址之前会把申请的空间的每个字节全部初始化为0
注意:calloc函数也是和free函数搭配使用
2.3realloc函数:灵活管理动态内存
realloc函数可以做到对动态开辟内存大小的调整
头文件:<stdlib.h> and <mallo.h>
realloc函数:void* realloc(void* ptr,size_t size)
参数解释:
1.ptr是要调整的内存地址
2.size调整后的大小
3.返回值为调整后的内存起始位置
4.函数在调整原来内存空间大小的基础上,会将原来内存中的数据移动到新的空间
5.realloc函数在调整内存空间时,存在两种情况
5.1原有空间之后有足够的空间去开辟填补的空间,此时新的空间首地址不变
5.2原有空间之后的空间不足以开辟填补的空间,此时,系统会在其他地址新开辟一个空间,
如果其他地址也不足开辟新的空间,realloc函数会返回一个空指针
所以为了保护原有空间的数据,先定义一个临时变量,接受realloc函数返回的指针
对此临时变量进行判断,倘若不为空指针,就将该地址赋给指向原空间的指针
代码演示如下:
int* ptr = (int*)realloc(pm, 10 * sizeof(int));
if (ptr != NULL)
{
pm = ptr;
}
注意:当realloc函数单独使用时,效果与malloc效果类似
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* pm = realloc(NULL, 40);
for (int i = 0; i < 10; i++)
{
pm[i] = i;
printf("%d\n", pm[i]);
}
free(pm);
pm = NULL;
return 0;
}
利用以上函数可以实现动态内存版本的通讯录小程序
三、动态内存中常见的错误
3.1对NULL指针进行解引用操作
当开辟内存失败的的时候,函数会返回一个空指针,此时对该指针解引用会导致非法访问内存;
所以对malloc函数的返回值做判断,如果不为空指针,再做接下来的处理
3.2对动态开辟空间的越界访问
#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
//printf("\033[1;5;47;34mhello\033[0m\n");
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("main:");
}
int i = 0;
for (i = 0; i < 10; i++)
{
p[i] = i;
printf("\033[1;5;40;34m%d\033[0m\n", p[i]);
}
free(p);
p = NULL;
return 0;
}
指针p的内容是新开辟空间的首地址,对p解引用会得到该空间的数据
p来维护动态内存开辟的这一空间
申请了40个字节的动态空间,却使用了40*4=160个字节,会导致越界访问
3.3对非动态内存使用free函数
一定要对动态开辟的内存使用free函数,不然会导致程序崩溃
3.4使用free函数释放动态内存中的一部分
动态内存空间使用完后,要释放掉整个的内存空间,不可以部分释放
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("main:");
}
int i = 0;
for (i = 0; i < 5; i++)
{
*p++ = i;
}
free(p);
p = NULL;
return 0;
函数在运行后,p的指针指向的位置发生了变化
这样做有两个风险:
1.p指针指向的位置发生了变化,释放指针时可能会导致部分释放
2.丢失了p本来指向的位置,可能会忘记这块空间的起始位置,可能会导致内存泄露的情况
3.5对同一块开辟的内存空间重复释放
重复释放同一块开辟的内存空间会导致程序崩溃
但是当第一次释放完指针以后,将该指针置空,就算重复释放也不会出现问题;
3.6开辟的动态内存空间忘记释放了
对于开辟的内存空间要记得释放,特别是在子函数中,不然会导致内存泄露的问题
内存泄漏(memory leak):开辟的堆区的动态内存空间,因为种种原因未被释放或无法释放,造成系统内存浪费,减慢系统运行速度,甚至会使系统崩溃;
动态内存开辟的空间只有两种释放方式
1.主动释放:使用free函数释放
2.程序结束:当程序结束时,申请的内存空间都返回给系统
例子:
#include <stdio.h>
#include <stdlib.h>
void test()
{
int* ptr = (int*)malloc(100);
if (ptr == NULL)
{
return;
}
}
int main()
{
//printf("\033[1;5;47;34mhello\033[0m\n");
test(); //子函数执行完后,会丢失ptr的数据
return 0;
}
程序在子函数中开辟了一块内存空间,整型指针指向该内存空间
但是子函数执行完后,ptr 指针的生命周期也就结束了,这也就意味着找不到了之前开辟的内存空间,会造成系统内存浪费,导致内存泄露的问题【重启可以解决】
子函数中开辟栈区和堆区空间的问题
分析如下代码:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* getmemory(char* pm)
{
pm = (char*)malloc(15);
return pm;
}
int main()
{
//printf("\033[1;5;47;34mhello\033[0m\n");
char* pm = NULL;
pm = getmemory(pm); //传递的参数是pm的值,相当于是值传递,无法改变pm的值
strcpy(pm, "hello world!");
printf(pm);
free(pm);
pm = NULL;
return 0;
}
在getmemory函数中,pm指向的是在堆区开辟的动态空间,在getmemory函数结束后,其内存空间不会释放,等待被主动释放或者程序结束
#include <string.h>
char* getmemory(void)
{
char pm[] = "hello world!";
return pm;
}
int main()
{
//printf("\033[1;5;47;34mhello\033[0m\n");
char* pm = NULL;
pm = getmemory(); //传递的参数是pm的值,相当于是值传递,无法改变pm的值
strcpy(pm, "hello world!");
printf(pm);
return 0;
}
在这个代码的getmemory函数中,pm是字符数组名,指向的是在栈区开辟的空间,存放的是常量字符串“hello world!”,但是当getmemory函数运行结束后,栈区的空间被释放(意味着常量字符串被销毁),此时pm指向的地址已经过时了,返回的地址没有实际意义,此时,如果通过返回的地址去访问内存,会造成非法访问内存的问题;
定义指针时应初始化,赋空值或其他值,不然会导致野指针的问题
这类问题统称为返回栈空间地址的问题
注意:栈区和堆区的生命周期不同,使用时应注意;
C/C++程序内存分配的几个区域
内存的划分并不是只有栈区、堆区、静态区这三个区简单地划分
下图是C/C++程序内存分配的几个区域:
1.栈区(stack):
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,打爆事故分配的内存容量有限。栈区主要运行函数而分配的局部变量、函数参数、返回数据、返回地址等;
2.堆区(heap):
一般由程序员分配释放,若程序员不能释放,程序结束时可由OS回收,分配方式类似于链表
3.数据段(又名静态区)(static):
存放全局变量、静态数据,程序结束后由系统释放
4.代码段:
存放函数体(类成员函数和全局函数)的二进制代码
四、柔型数组(flexible array)
C99中,结构中最后一个元素允许是未知大小的的数组,这就叫做柔性数组成员
4.1柔型数组的定义:
struct S
{
int n;
int arr[]; //大小是未知
//或者写成
//int arr[0];
};
4.2柔型数组的特点
1.结构体中柔型数组的前面必须至少有一个其他成员
2.sizeof操作符返回的结构体的大小不包括柔型数组的内存
3.包含柔型数组成员的数组结构用malloc函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔型数组预期的大小
4.3柔型数组的使用
使用malloc函数来为结构体开辟动态内存空间
如果空间不够用就使用realloc函数扩展空间
使用代码示例:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct S
{
int n;
int arr[]; //大小是未知
//或者写成
//int arr[0];
};
int main()
{
struct S* pm = malloc(sizeof(struct S) + 10 * sizeof(int));
if (pm == NULL)
{
return;
}
pm->n = 10;
for (int i = 0; i < 10; i++)
{
pm->arr[i] = i;
printf("%d\n", pm->arr[i]);
}
struct S* ptr = (struct S*)realloc(pm,sizeof(struct S) + 15 * sizeof(int));
if (ptr != NULL)
{
pm = ptr;
}
for (int i = 0; i < 5; i++)
{
pm->arr[10 + i] = 10 + i;
printf("%d\n", pm->arr[10+i]);
}
free(pm);
pm = NULL;
return 0;
}
注意:结构体指针pm指向的是动态开辟的内存空间,如果柔型数组空间不够用的话,可以使用realloc函数申请扩容内存空间,pm指向的依然还是动态开辟的内存空间;
但是如果将结构体设计成以下形式:
struct S
{
int n;
int* pm;
};
如果想实现柔型数组的功能,需要两次动态开辟内存空间,一次给该结构体,另外一次是给指针pm申请的,如果pm指向的空间不够,再使用realloc函数扩容,最后释放内存空间时,需要先释放pm指向的内存空间,再释放结构体的动态内存空间;但是这种方式容易产生内存碎片,没有荣幸数组的方式简便高效;
4.4柔型数组的优点:
1.方便内存释放:只需要释放一次就就可以了
2.有利于来提高访问速度:连续的内存有利于提高访问速度,也有益于减少内存碎片;
特别鸣谢:哔哩哔哩比特鹏哥视频教程