目录
1.为什么会存在动态内存分配
已经学习过的两种开辟内存的方式:
int val = 20; // 在栈空间上开辟4个字节
char arr [10] = {0};// 在栈空间上开辟10个字节的连续空间
但是上述的开辟空间的方式有两个特点:
- 空间开辟大小是固定的。
- 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。
这时候就只能试试动态存开辟了。
动态内存开辟,是在堆区开辟空间。
2.内存函数
2.1 malloc和free
malloc(动态内存开辟)
头文件:<stdlib.h>
void* malloc ( size_t size ) ;
//参数:要开辟的内存块的大小,以字节为单位。size_t是一个无符号整数类型。
函数功能:
向内存申请一块连续可用的空间,并返回指向这块空间的指针。如果开辟成功,返回一个指向开辟好的空间的指针。如果开辟失败,返回一个NULL。因此用malloc申请空间,使用这块空间之前应该检查malloc的返回值,以确定能否正常使用。(如果参数size为0,malloc的行为标准是未定义的,取决于编译器)
free(动态内存的释放和回收)
头文件:<stdlib.h>
void free ( void* ptr ) ;
//参数:指向以前使用malloc、calloc或realloc分配的内存块的指针。
函数功能:
释放调用malloc、calloc或realloc分配的内存块,使其再次可用于进一步的分配。如果ptr没有指向用上述函数分配的内存块,则会导致未定义的行为。如果ptr是一个空指针,那么函数将不执行任何操作。
例:
#include<stdio.h>
#include<stdlib.h>
int main()
{
//开辟10个int整型的空间
//栈区
int arr[10] = { 0 };
//堆区
int* p = (int*)malloc(10 * sizeof(int));
//要将malloc的返回值强制转换为int*类型(根据需求转换)
//对void*p解引用,是非法访问
if (p == NULL)//判空
{
perror("malloc");//开辟失败,报错
return 0;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);//p[i]-->*(p+i)
}
//回收空间
free(p);//free把p所指向的空间还给系统,但不会把p置空,p依然保存着原来的地址
p = NULL;//如果之后不小心使用了p,就会造成非法访问,为避免风险,最好手动把p置成空指针
//malloc和free一定要成对使用
return 0;
}
输出:
0 1 2 3 4 5 6 7 8 9
2.2 calloc
头文件:<stdlib.h>
void* calloc ( size_t num, size_t size ) ;
// 元素个数
// 每个元素的大小
// 要开辟的内存块的大小,也是以字节为单位
函数功能:
为num个大小为size的元素开辟一块空间(或者说分配一块内存),并且把空间的每个字节初始化为0。calloc与malloc的区别只在于:在返回地址前把申请的空间的每个字节初始化为0
例:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)calloc(10,sizeof(int));
if (p == NULL)
{
perror("calloc");
return 0;
}
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
free(p);
p = NULL;
return 0;
}
输出:
0 0 0 0 0 0 0 0 0 0
2.3 realloc
之前申请的内存可能过大或者过小,为了合理地使用内存,就要对内存的大小进行灵活的调整。
realloc函数,可以对动态开辟的内存进行调整。
头文件:<stdlib.h>
void* realloc ( void* ptr , size_t size ) ;
// ptr是指向要调整的内存块的指针(如果ptr是一个空指针,在这种情况下会分配一个新的内存块,就像调用了malloc一样)
// size是调整之后新的大小,以字节为单位
// 返回值为调整之后的内存起始位置
realloc具体是如何运行呢?
realloc在调整内存空间时,存在2种情况:
情况1:原有空间之后有足够大的空间
情况2:原有空间之后没有足够大的空间
例:
int main()
{
//开辟10个int整型的空间
int* p = (int*)calloc(10,sizeof(int));
if (p == NULL)
{
perror("calloc");
return 0;
}
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
printf("\n");
//增加10个int整型的空间
//如果realloc找不到合适的空间,无法增容,就会返回空指针。
//用p接收,p变成空指针后,就可能导致calloc申请的那块空间找不着了,造成内存泄漏。
//因此不能直接用p接收,创建一个临时变量来接收最稳妥
int* tmp =(int*) realloc(p, 80);
if(tmp!=NULL)
{
p = tmp;//tmp不为空指针,增容成功
//打印一下,看一下结果
for (i = 10; i < 20; i++)
{
printf("%d ", *(p + i));//未赋值,打印出来是随机值
}
}
//回收空间
free(p);
p = NULL;
return 0;
}
输出:
0 0 0 0 0 0 0 0 0 0
-842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451 -842150451
上述代码是属于realloc的哪种情况呢?
可以通过调试,打开内存窗口,观察p的地址可知,上述代码是属于realloc的情况1。
p的地址没有发生变化。
3.常见的动态内存错误
3.1 对NULL指针的解引用操作
int main()
{
int* p = (int*)malloc(4);
*p = 20;//如果p的值是NULL,就会出现问题,引发异常
free(p);
p=NULL;
return 0;
}
这再一次提示我们:使用malloc,calloc,realloc函数时,一定要判断它的返回值是否为空指针。
3.2 对动态开辟空间的越界访问
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
return 1;
}
int i = 0;
for (i = 0; i < 40; i++)
{
*(p + i) = i;//越界访问
}
free(p);
p = NULL;
return 0;
}
3.3 对非动态开辟内存使用free释放
int main()
{
int arr[10] = { 0 };
int* p = arr;
free(p);//不能释放栈上的内存
return 0;
}
如果使用了多个指针,释放空间时要注意不要混淆,free用来释放动态内存(堆区)上的空间,不能释放如上述代码一样的栈区上的内存。
3.4 使用free释放一块动态开辟内存的一部分
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
return 1;
}
int i = 0;
for (i = 0; i < 5; i++)
{
*p++ = i;
}
free(p);
p = NULL;
return 0;
}
p指向的位置发生了变化,释放空间时,只把后20个字节的空间还给了系统,而后又把p置成了空指针,导致malloc申请的前20个字节的空间的地址永远也找不到了,可能导致内存泄漏。
3.5 对同一块动态内存多次释放
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
return 1;
}
//使用
//释放
free(p);
//其他代码
//……
free(p);//代码太多,忘记已经释放过,释放了2次,就会出现问题
return 0;
}
如何避免这种情况呢?
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
return 1;
}
//使用
//释放
free(p);
p = NULL;//置成空指针
//其他代码
//……
free(p);//代码太多,忘记已经释放过,释放了2次,就会出现问题
return 0;
}
第一次释放完,把p置成空指针,第二次释放时,由于p是一个空指针,free函数将不执行任何操作,这样避免一些问题。
3.6 动态开辟内存忘记释放(内存泄漏)
void test()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
return ;
}
//使用
//……
}
int main()
{
test();
//……
return 0;
}
动态开辟的空间,2种回收方式
- 主动释放(free)
- 程序结束
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
切记:动态开辟的内存一定要正确释放!
4.经典面试题
题目一:
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
请问运行Test函数会有怎样的结果?
引发异常
怎么改对?
改法1:
char* GetMemory(char* p)
{
p = (char*)malloc(100);
return p;
}
void Test(void)
{
char* str = NULL;
str=GetMemory(str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
输出:
hello world
改法2:
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;
}
输出:
hello world
题目二:
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
请问运行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);
}
int main()
{
Test();
return 0;
}
输出:
hello
但是有一个严重的错误:malloc申请的空间没有释放。
改正:
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;
}
题目四:
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;
}
输出:
world
free后,虽然这块空间勉强能用,但其实这样做是不合法,或者说非法访问,既然这块空间既然还给了系统,就不能因为free后str仍然指向这块空间(仍然记得这块空间的地址)再使用了。
鹏哥经典比喻:就像你知道银行的地址,难道就能去抢银行吗?
有人说关键是把free(str);放到Test函数的最后,但其实这道题的关键并不在于free先后的问题,就算free(str);放到Test函数的最后,没有置空,依然存在漏洞。
这道题的关键在于free后及时置空,以防后续代码中误用。
5. C/C++程序的内存开辟
C/C++程序内存分配的几个区域:
1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结
束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是
分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返
回地址等。
2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分
配方式类似于链表。
3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
有了这幅图,我们就可以更好的理解在《C语言初识》中讲的static关键字修饰局部变量的例子了。
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序
结束才销毁所以生命周期变长。
6.柔性数组
C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员。
6.1 柔性数组的特点
- 结构中的柔性数组成员前面必须至少一个其他成员。
- sizeof 返回的这种结构大小不包括柔性数组的内存。
- 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
例:
//写法1:
struct A
{
int n;
int arr[];//大小未知
};
//写法2:
struct B
{
int n;
int arr[0];//也可以写出0
};
int main()
{
struct A a = { 0 };
printf("%d\n", sizeof(a));
return 0;
}
输出:
4
6.2 柔性数组的使用
代码1:
struct A
{
int n;
int arr[];//大小未知
};
int main()
{
//期望arr的大小是10个int整型
struct A* pa = (struct A*)malloc(sizeof(struct A)+10 * sizeof(int));
pa->n = 100;
int i = 0;
for (i = 0; i < 10; i++)
{
pa->arr[i] = i;
}
//增加
struct A* ptr = (struct A*)realloc(pa,sizeof(struct A) + 20 * sizeof(int));
if (ptr != NULL)
{
pa = ptr;
}
//使用
//释放
free(pa);
pa = NULL;
return 0;
}
6.3 柔性数组的优势
既然柔性数组是通过内存开辟函数malloc,realloc实现的,那也可以通过malloc,realloc直接实现柔性数组的功能。
代码2:
struct A
{
int n;
int* arr;
};
int main()
{
struct A* ps = (struct A*)malloc(sizeof(struct A));
if (ps == NULL)
return 1;
ps->n = 100;
ps->arr = (int*)malloc(10*sizeof(int));
if (ps->arr == NULL)
return 1;
int i = 0;
for (i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
//增加
int* ptr = (int*)realloc(ps->arr, 20 * sizeof(int));
if (ptr != NULL)
{
ps->arr = ptr;
}
//使用
//释放
free(ps->arr);//先free第二块空间
free(ps);//再free第一块空间
ps->arr = NULL;
ps = NULL;
return 0;
}
代码1和代码2可以完成同样的功能,但是代码1的实现有两个好处:
第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给
用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你
不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好
了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度
连续的内存有益于提高访问速度,也有益于减少内存碎片。
内存碎片: