目录
一.为什么存在动态内存分配
我们已经掌握的内存开辟方式有:
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
但是上述的开辟空间的方式有两个特点:
1. 空间开辟大小是固定的。
2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,有时候我们需要内存空间扩大或者缩小,这时候我们只能使用动态内存开辟了
比如说,之前我写的通讯录一次开辟了1000个数据空间,但我可能只需要3个人,所以其他的空间就会浪费,我们要是能随时改变开辟空间的大小,就可以让空间不再浪费,而我们开辟的动态内存,是在堆区上进行的
二.动态内存函数的介绍
1.malloc
void* malloc (size_t size);
头文件:<stdlib.h> 或<malloc.h>
功能:这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针
1.如果开辟成功,则返回一个指向开辟好空间的指针。
#include <stdio.h> #include <stdlib.h> int main() { int* p = (int*)malloc(90);//开辟90个字节的空间,并将起始位置地址返回给指针p }
2.如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
这里利用INT_MAX值超大的特性,向系统申请开辟一块超大的内存,系统发现没有这么大的空余空间,就会开辟失败,返回NULL
3.返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
4.如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
size为0,可以返回地址,但是地址不能解引用去使用,即使编译器可以去使用,也不要去使用,因为该地址中的内存空间为0,你去使用会出现非法访问的问题
2.free
void free (void* ptr);
头文件:<stdlib.h> 或 <malloc.h>
功能:free函数用来释放动态开辟的内存
1.如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
free经常配合malloc去使用,当malloc开辟玩内存并进行完操作后,就要去释放开辟的动态内存#include <stdio.h> #include <stdlib.h> int main() { int* p = (int*)malloc(INT_MAX); if (p == NULL) { perror("malloc::"); return 0; } free(p);//释放内存 p = NULL;//如果不将其置为空,再去访问p的内存时就会形成非法访问! return 0; }
2.如果参数 ptr 是NULL指针,则函数什么事都不做
3.如果我们不释放动态内存申请的内存的时候,如果程序结束,动态申请的内存由操作系统回收,如果程序不结束,动态内存是不会自动回收的,就会造成内存泄漏
3.calloc
void* calloc (size_t num, size_t size);
头文件:<stdlib.h> 或<malloc.h>
功能:这个函数向内存申请 num 个大小为 size 的元素个内存空间,并且把空间的每个字节初始化为0,返回指向这块空间的指针
1.函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
所以如何我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。
4.realloc
void* realloc (void* ptr, size_t size);
头文件:<stdlib.h> 或<malloc.h>
功能:增加或减少内存空间原有的大小,使内存管理更加灵活
realloc函数的出现让动态内存管理更加灵活有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为
ptr 是要调整的内存地址
size是调整之后新大小,也就是整块内存增加后的总大小
返回值为调整之后的内存起始位置。
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
realloc在调整内存空间的是存在两种情况:
情况1:原有空间之后有足够大的空间,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
1.扩展空间
2.返回新空间
情况2:原有空间之后没有足够大的空间,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。
1.找到足够大的空间
2.拷贝原有内容
3.free掉原空间
4.返回新空间地址
realloc有可能找不到合适的空间来调整大小,这时就会返回空指针
如果出现了这种情况,又用原有的指针去接受的realloc返回的空指针,原有空间就会出现泄漏,你无法再找回原有空间了,为了避免出现这种情况,我们可以创建一个临时的指针去接受,检验一下realloc是否找到了合适的空间
int main() { int* ptr = (int*)realloc(NULL, 40); if (ptr == NULL) { perror("realloc::"); return 1; } int* p = NULL; p = (int*)realloc(ptr, 1000); if (p != NULL) { ptr = p; } else { perror("realloc::"); } return 0; }
realloc单独使用也会有malloc的效果
这里的realloc的功能类似于malloc,就是直接在堆区开辟40个字节int main() { int* ptr = (int*)realloc(NULL, 40); if (ptr == NULL) { perror("realloc::"); return 1; } free(ptr); ptr = NULL; return 0; }
常见的动态内存错误
1. 对NULL指针的解引用操作
void test() { int* ptr = (int*)malloc(INT_MAX / 4); *ptr = 20;//如果p的值是NULL,就会有问题 free(p); }
解决办法:对malloc函数返回值进行判空操作
用INT_MAX开辟动态内存,malloc会返回NULL,这样我们解引用ptr就相当于对NULL指针进行解引用,这样就会出现问题,所以我们应该先进行判空操作再去解引用
2.对动态开辟空间的越界访问
当i=10时会访问第41-44个字节空间,会出现越界访问,内存空间会出现问题
3. 对非动态开辟内存使用free释放
a是在栈上开辟的,free无法释放栈上的空间,运行后会崩
4.使用free释放一块动态开辟内存的一部分
即:
不能单独free掉p++部分
5.同一块动态内存多次释放
对同一快动态内存多次释放会出现问题
6.动态开辟内存未释放(内存泄漏)
一直运行test函数每次都会吃掉100字节内存,每当结束一次test函数,地址p就会无法找到,而且test函数在堆上开辟的内存,函数结束后无法销毁,所以不在test函数中及时释放内存,每调用一次test函数就会出现这100个字节内存无法释放的问题
void test() { int* p = (int*)malloc(100); if (NULL != p) { *p = 20; } } int main() { test(); while (1); }
经典笔试题
题目1
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
int main()
{
Test();
return 0;
}
这个代码首先调用Test函数,然后创建一个为NULL的空指针ptr,调用GetMemory函数,将ptr传值调用,因为是传值,p是一个临时拷贝,p和ptr一样都是空指针,在GetMemory函数内开辟了100个字节的动态内存,因为是传值调用所以p的地址无法将开辟的动态内存返回到ptr指针中,函数结束后销毁指针p,p销毁后无法得知动态内存的起始地址,动态内存空间有没有释放会丢失100个字节,出现内存泄漏,回到Test函数后,str还是空指针,strcpy会先解引用传来的目标地址,因为解引用NULL,会出现问题,属于非法访问内存,所以程序到了strcpy会崩掉,
修改其让其正确:
题目2
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
在GetMemory中创建的字符串是创建在栈区上的,会在函数结束后销毁,所以str虽然能接受到字符串的首地址,但是只要一printf解引用就会出现非法访问内存的问题
题目3
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;
}
虽然也能打印hello,但是忘记去释放创建的动态内存
修改:void GetMemory(char** p, int num) { *p = (char*)malloc(num); } void Test(void) { char* str = NULL; GetMemory(&str, 100); strcpy(str, "hello"); printf(str); free(str); str = NULL; } int main() { Test(); return 0; }
题目4
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
free(str)后再去strcpy会进行非法访问,因为原有的动态内存被释放掉了,可以在free(str),后面将指针变为空,这样后面的判空操作就可以不再去追加字符串了
修改:
三.C/C++程序的内存开辟
C/C++程序内存分配的几个区域:
栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
代码段:存放函数体(类成员函数和全局函数)的二进制代码。
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁所以生命周期变长。
四.柔性数组
柔性数组(flexible array)这个概念在C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
有些编译器会报错无法编译可以改成
typedef struct st_type
{
int i;
int a[];//柔性数组成员
}type_a;
1.柔性数组的特点
1.结构中的柔性数组成员前面必须至少一个其他成员。
![]()
2.sizeof 返回的这种结构大小不包括柔性数组的内存。
3.包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小
2.柔性数组的使用
下面例举了一个柔性数组,怎样开辟动态内存空间和怎样扩容
#include<stdio.h>
#include<stdlib.h>
struct S1
{
int num;
int arr[];
};
int main()
{
struct S1* p = (struct S1*)malloc(sizeof(struct S1) + 40);
if (p == NULL)
{
perror("malloc");
return 1;
}
p->num = 100;
for (int i = 0; i < 10; i++)
{
p->arr[i] = i;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", p->arr[i]);
}
struct S1* p2 = (struct S1*)realloc(p, sizeof(struct S1) + 80);
if (p2 == NULL)
{
perror("malloc");
return 1;
}
else
{
p = p2;
}
for (int i = 10; i < 20; i++)
{
p->arr[i] = i;
}
for (int i = 0; i < 20; i++)
{
printf("%d ", p->arr[i]);
}
//释放
free(p);
p = NULL;
p2 = NULL;
}
3.柔性数组的优势
上述的两个代码都可以完成同样的功能,但是柔性数组的实现有两个好处:
第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给
用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你
不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好
了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度.
连续的内存有益于提高访问速度,也有益于减少内存碎片。因为柔性数组的内存是紧挨着的,他们开辟的内存都紧挨着,大部分会紧凑在一起,大多数会集体放在寄存器中,运行速度会快很多,不用柔性数组实现时,需要在别处开辟内存,会出现内存碎片的现象