在之前实现通讯录博客中简单提到了动态内存分配,没有仔细展开来讲。其实,动态内存分配是C语言中一个很重要的知识点。本文将对动态内存分配进行相关的介绍。
简单来讲动态内存分配就是利用动态内存函数向内存申请一块连续的空间的给使用者使用。这个空间的大小由使用者自己决定,使用者可以对这块空间进行访问使用或者增容或者释放等相关的管理操作。
我们常见的内存开辟方式创建变量直接在栈空间上开辟内存。
int a=4;
int arr[4];
这种方法开辟的空间有以下的特点:
1.空间开辟大小是固定的。
2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是在实际编程过程中对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。这时候就只能试试动态存开辟了。比如在编写通讯录的时候,如果只有3个联系人信息需要保存,那么在使用数组开辟100大小的空间无疑就是浪费,但是如果需要保存200个联系人的信息,那么100大小的数组就不够使用了。动态内存分配会更加灵活的开辟空间,对内存管理更加合理。
先前讲到了动态内存分配是需要用到动态内存函数,大致有4个。
malloc函数 calloc函数 realloc函数 free函数
前3个函数是用来分配动态内存空间的,free函数是用来释放被分配的内存空间的。
在使用这些函数时需要包含#include< stdlib >头文件的
malloc函数会向内存申请一块连续的空间,如果申请成功就会返回这块空间的起始地址,如果申请失败就会返回空指针。要注意一点malloc函数的返回值类型是void*,所以在使用之前需要强制类型转换,同时判断一下返回值是否为空,它只有一个整型参数,表示要申请内存的字节数。
代码示例
#include
#include
int main()
{
int* ptr = (int*)malloc(100);
if (ptr != NULL)
{
int * ps = ptr;
}
free(ptr);
ptr = NULL;
//ps是局部变量,出了作用域就被销毁了 ,所以没有置为空
return 0;
}
至为什么要free释放空间,以及释放后为什么要置为空指针。在后面会有详细解释。
calloc函数和malloc函数是类似的,不同点是calloc函数有两个整型参数第一个参数是表示分配的空间中要存放多少个元素,第二个参数是每个元素的大小(字节)。同时,calloc函数还会将分配的空间先全部初始化为0
代码示例
#include
#include
int main()
{
int* ptr = (int*)calloc(10,4);
if (ptr != NULL)
{
int * ps = ptr;
for (int i = 0; i < 10; i++)
{
printf("%d ", *(ps+i));
}
}
free(ptr);
ptr = NULL;
//ps是局部变量,出了作用域就被销毁了 ,所以没有置为空
return 0;
}
可以看到calloc分配的空间中确实都存放着0
realloc函数作用相当于对分配的动态内存空间进行再分配。当用malloc和relloc函数分配的空间不够用时,就可以利用realloc函数再追加一段空间。用calloc和malloc函数分配的空间的后续空间没有被系统使用的部分大于等于要追加的空间大小,就会直接在原空间末尾处续接上,如果后续的空间大小不够要追加的空间大小,realloc函数就会重新开辟一块内存空间。这个空间的大小是原空间大小和要追加空间大小之和,同时原空间的元素也会按顺序拷贝过来。
realloc函数有两个参数,一个是要追加的动态内存的首地址,另一个是调整之后最终要分配的大小。使用方法和malloc calloc函数类似。在使用之前都需要强转。
代码示例
#include
#include
int main()
{
int* ptr = (int*)calloc(10,4);//40个字节
ptr = (int*)realloc(ptr, 48);//48个字节
if (ptr != NULL)
{
int * ps = ptr;
for (int j = 0; j < 12; j++)
{
*(ps + j) = j;
}
for (int i = 0; i < 12; i++)
{
printf("%d ", *(ps+i));
}
}
free(ptr);
ptr = NULL;
//ps是局部变量,出了作用域就被销毁了 ,所以没有置为空
return 0;
}
free函数是用来释放申请的内存空间的,前面3个函数都是在向申请空间,类似借钱,free是将申请的空间还给系统,相当于还钱。free函数只有一个参数就是分配的动态内存首地址。因为分配的动态内存空间也是一段连续的空间的,通过首地址就可以释放掉整个空间
为什么要用free释放掉分配的空间呢?
因为如果使用动态内存函数分配空间后没有及时释放空间,当启动使用程序时,内存又被重新分配,但是内存资源是有限的,迟早有一天会耗干内存。这就是内存泄漏。机器的性能也会大幅度下降。
在任务资源管理器中我们可以看到在启动程序之前,内存占用情况是53%,但是随着程序启动之后很快飙升至66%,因为电脑系统有保护机制所以没有继续飙升,但内存泄漏确实存在。这就是没有及时释放分配的空间所导致的。这也是在先前代码中每次开辟动态内存空间后都要free释放空间的原因
常见错误
1. 对NULL指针的解引用操作
2.对动态开辟空间的越界访问
3.对非动态开辟内存使用free释放
4.使用free释放一块动态开辟内存的一部分
5.对同一块动态内存多次释放
6.动态开辟内存忘记释放(内存泄漏)
对空指针的解引用操作
void test()
{
int *p = (int *)malloc(100000000/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
在使用动态内存函数分配空间时,需要先建立个临时变量判断是否申请空间成功,如果成功在使用,不然就会对空指针解引用。引发程序错误。
对开辟的动态空间越界访问
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);
}
malloc函数只是开辟了10个整型大小的空间,但是在for循环中访问到了第11个整型空间。跟数组的越界访问类似,这是对分配的空间越界访问。如果后续的第11个整型空间处没有被系统使用,程序可能不会报错,但是实际上这是非法的行为。在使用动态内存分配的空间时,要注意一点。分多少空间就用多少,不够再申请,千万不能越界访问。
对非动态开辟内存使用free释放
#include
#include
int main()
{
int a = 10;
int* p = &a;
free(p);
return 0;
}
free函数是专门用来释放回收动态内存函数开辟出来的空间的,如果用free释放其他的空间,程序就就会出错。就好比宠物医院是专门给生病的动物治疗的,人生病了肯定不会去宠物医院看病。
使用free释放一块动态开辟内存的一部分
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
在这个代码段中p++后就不在指向分配空间的起始地址了,如果对其使用free也会造成程序出错,所以如果需要改变指针指向时,需要创建一个临时指针变量来保存分配空间的起始地址。这样才能不出错释放空间。
对同一块动态内存多次释放
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
在使用free释放空间后,不能接着在再次释放。这也会导致程序出错,free对分配的空间释放一次就可以了
动态开辟内存忘记释放(内存泄漏)
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
分配的动态内存空间在使用完毕以后一定要及时的释放,不然就会造成内存泄漏,内存会被慢慢耗尽
请问运行Test 函数会有什么样的结果?
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
简单分析一下,首先,字符指针str被赋值成空指针。接着str传入Get Memory函数中,GetMemory函数形参p接收传入的str也就是空指针。然后用malloc开辟100字节的空间并且将空间首地址复制给p.到了这一步我们要明确一点,GetMemory函数是传值调用,形参是实参的临时拷贝,当调用GetMemory结束后p就会被销毁,所以当该函数调用之后,strcpy中的str还是空指针,如果将hello world拷贝到str中势必会对str解引用,实际上就会对空指针解引用,从而引发程序错误导致程序崩溃。
printf函数中直接放指针这样的写法其实是没有问题的
char *s="abc";
printf("%s",s);
这个代码中指针s指向字符串abc的首地址,然后通过这个首地址直接打印字符串。printf(str)和这个代码段的例子是一样的,所以printf使用方法是没错的,但是str是空指针,这就有问题了。归根结底问题就是出现了对空指针的解引用。
请问运行Test 函数会有什么样的结果?
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
简单分析,str首先被置为空指针,接着调用GetMemory函数,刚才提到了Get Memory是传值调用,在调用完该函数是后,p是被销毁掉了的,那就声明为p开辟的空间就会被系统给收回,当我们把这个已经被回收了的地址给了str时,实际上str就是野指针。打印str指向的地址中存放的内容时,会输出一堆乱码。str指向的空间中的内容是未知的。
请问运行Test 函数会有什么样的结果?
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
简单分析,str首先被置为空指针,GetMemory函数这次不是传值调用了,是传址调用。实参是str的地址,通过str的地址将str指向的内容更改成malloc函数开辟的空间的首地址,hello是可以被拷贝到str指向的空间中的,并且打印出hello,但是由于没有用free对开辟出的空间进行释放,会造成内存泄漏,同时还缺少对空指针的判断,因为malloc函数申请空间失败就会返回空指针,缺少空指针判断,可能又会造成对空指针解引用。
请问运行Test 函数会有什么样的结果?
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
简单分析,str首先指向malloc申请的100字节大小空间的首地址。然后strcpy函数进行字符串拷贝,拷贝之后用free释放,到了一步str现在就是野指针了。后面对野指针进行空指针判断并且进行字符串拷贝是一件很危险的事。当free释放掉分配的动态内存空间后,str的指向是不确定的。这段代码可能会正常在屏幕上打印出hello但是实际上存在着对野指针使用访问这个巨大的隐患的。
同时也提醒我们在使用free释放空间后,一定要将指针置为空,不然可能会造成野指针使用访问使得程序崩溃。
C/C++程序内存分配的几个区域:
1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由系统回收 。分配方式类似于链表。
3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
这张图解释了c/c++中的内存划分,实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序
结束才销毁所以生命周期变长。
动态内存分配的空间都是在堆区上,像我一样的初学者可以先试着理解栈区和堆区上的内存分配特点,在后续的学习过程中慢慢理解其他的内区区域。
了解内存区域的划分和区域特点,才能对程序掌握的更好,可以避免一些不必要使得程序错误。
可能有些人从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。
在C99 中标准,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
柔性数组的特点
结构中的柔性数组成员前面必须至少一个其他成员。
sizeof 返回的这种结构大小不包括柔性数组的内存。
包含柔性数组成员的结构体用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小。
我们看到柔性数组成员是不计入整个结构体大小的,打印结果是4,和上述相符。同时要注意一点,如果想让结构体成员其中一个成员是柔性数组,在声明定义结构体时,最后一个成员一定数组,数组元素个数要么不定义,要么就是0.这样才符合柔性数组的定义
柔性数组的使用
代码示例
#include
#include
struct A
{
int a;
int b[0];
};
int main()
{
struct A* ps = (struct A*)malloc(sizeof(struct A) + 5 * 4);
//开辟的空间较小就没有进行指针判断
ps->a = 10;
for (int i = 0; i < 5; i++)
{
ps->b[i] = 1;
}
for (int j = 0; j < 5; j++)
{
printf("%d ", ps->b[j]);
}
printf("%d", ps->a);
free(ps);
ps = NULL;
}
前面提到包含柔性数组成员的结构体应该用动态内存分配空间,所以用malloc函数分配结构体成员的空间。可以看到分配的空间是两部分的,前一部分是给结构体其他成员分配的空间,后一部分的20字节是分配给柔性数组的。然后用指针ps去访问结构体成员对其初始化,并且打印它们的值,这里提一下,因为分配的空间是比较小的就没有对ps进行空指针判断,然后在程序结束前用free释放空间,并且将ps置为空指针
上述介绍柔性数组的使用示例代码,可以不使用柔性数组达到类似的效果
#include
struct A
{
int a;
int* b;
};
int main()
{
//申请的空间是比较小的就没有判断空指针
struct A* ps = (struct A*)malloc(sizeof(struct A));
ps->a = 100;
ps->b = (int*)malloc(ps->a* sizeof(int));
//业务处理
for (int i = 0; i < 100; i++)
{
ps->b[i] = i;
}
//释放空间
free(ps->b);
ps->b = NULL;
free(ps);
ps = NULL;
}
因为柔性数组是所有成员内存都是由动态空间分配的,所以为了保持一致性,示例中的结构体内存空间都是由malloc分配的,我们看到实际上是开辟了两次空间的,然后是释放了两次空间。并对结构体指针和成员指针置为空
相比柔性数组对动态内存的申请和释放,这样的代码显然更是繁琐一点的
柔性数组的好处如下:
第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度.
连续的内存有益于提高访问速度,也有益于减少内存碎片。因为申请一次空间,空间是连续一片的,多次申请空间,每次申请的内存块都是随机的彼此间是没有联系的。这样也节省了空间
1.在动态内存分配时,一定要注意对空指针的判断。使用过后及时对申请的内存释放,并将野指针置为空。
2.柔性数组是在c99标准提出来的,可能这个概念比较陌生,在一些比较适合的场景下可以试着使用,加深印象。
3.以上内容如有问题欢迎指正。谢谢!