为什么要存在动态内存分配呢?
int val = 20; // 在栈空间上开辟4个字节
char arr[10] = { 0 }; //在栈空间上开辟10个字节的连续空间
但是上述的开辟空间的方式有两个特点:
1.空间开辟大小是固定的(太过具有局限性)
2.数组在什么的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况,有时候我们需要的空间大小在程序运行的时候才知道,那数组的编译时开辟空间的方式就不能满足了。所以这时候就需要引入动态内存开辟了。
malloc
#include<stdlib.h> //malloc的头文件
void* malloc(size_t size); // 声明 (()里面是总大小)
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
//向内存申请10个整形的空间
int* p = (int*)malloc(10 * sizeof(int)); //当申请成功的时候就会返回申请到的起始地址
if (p == NULL) // 但也有一定的可能会申请失败,就会返回一个空指针
{
//打印错误的原因
printf("%s\n", strerror(errno));
}
// 当动态申请的空间不再使用的时候
//就应该还给操作系统,引入了free这个函数。
free(p);
p = NULL;
return 0;
}
calloc
void* calloc(size_t num,size_t size); //声明
1.函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0.
2.与malloc的区别只是在于calloc会在返回地址之前把申请的空间的每个字节初始化为0.
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
printf("%s\n", strerror(errno));
}
}
realloc
1.realloc函数的出现让动态内存管理更加的灵活
2.有时我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理内存,我们一定会对内存的大小做灵活的调整。
void* realloc(void* ptr, size_t size); // 原型
其中:
1.ptr是要调整的内存地址
2.size调整之后新的大小
3.返回值为调整之后的内存起始位置
4.这个函数调整原内存空间的大小基础上,还会将原来内存中的数据移动到新的空间
5.realloc在调整内存空间是存在两种情况的:如果realloc后面的空间足够大,就直接追加,然后返回原来的旧地址,但是当后面的空间不是足够大的时候,就需要重新考虑拿一块很大的空间来进行开辟,把原有的数据会拷贝下来,释放旧的内存空间,然后返回新开辟的地址。
int* p = realloc(NULL,40); // 此时这个函数相当于malloc(40)
注意:在开辟的时候把realloc返回的地址用一个新的指针来接收
int* ptr = realloc(p, INT_MAX);
if (ptr != NULL)
{
p = ptr;
}
// realloc也要记住释放空间
free(p);
p = NULL;
常见动态内存错误
1.对空指针的解引用(对其所开辟的指针一定要进行判断)
int main()
{
int* p = (int*)malloc(40);
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}
对于这段代码,你没有对p进行判断,万一他开辟内存失败了,就会返回一个空指针,那么就报错了。
2.越界访问
int main()
{
int* p = (int*)malloc(5*sizeof(int));
if (p == NULL)
{
return 0;
}
else
{
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
}
free(p);
p = NULL;
return 0;
}
3.对非动态开辟内存使用free释放(free的是堆区的空间)
int main()
{
int a = 10; // 这里是局部变量,是栈区的空间
int* p = &a;
*p = 20;
free(p);
p = NULL;
return 0;
}
4.使用free释放动态开辟内存的一部分
这个程序会崩溃,因为你的p一直在变化,最终指向了开辟的最后一个地址的下一个位置处,你在对其进行释放,后面的空间是不属于你的。
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return 0;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*p++ = i; // 把这里改成p+下标解引用来进行赋值,就会好
}
free(p);
p = NULL;
return 0;
}
5.对同一块动态内存的多次释放
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return 0;
}
free(p);
// ....
free(p);
p = NULL; // 在每个free后面进行指针赋值为NULL,也可以很好的解决这个错误,因为你一旦被赋值为NULL,那么后面对这个指针的任何操作都是不管用的。
return 0;
}
6.动态开辟的内存忘记释放(内存泄漏)
一直在开辟内存,但是从来都不回收。
nt main()
{
while (1)
{
malloc(1);
}
return 0;
}
经典例题
错误的
//运行程序会造成程序的崩溃
//str以值传递的形式给p,p是Getmemory函数的形参,只能函数内部有效,等Getmemory函数返回以后,动态开辟内存尚未释放,并且无法找到,会造成内存泄漏
void GetMemory(char *p) // p是所创建的形参变量,在这个函数调用完成以后,这个这个形参就会自动的销毁,且无法在找到这个空间
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world"); // 此时的str还是一个NULL
printf(str);
}
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"); // 此时的str还是一个NULL
printf(str);
free(str);
str = NULL;
}
int main()
{
test();
return 0;
}
第二种改正的方式
//程序可以理解为:p是上司,他所开辟的空间是卧底,在p死掉之前,如果他能告诉别人卧底的身份,那就还会有人知道卧底
char* GetMemory(char *p)
{
p = (char *)malloc(100);
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory(str);
strcpy(str, "hello world"); // 此时的str还是一个NULL
printf(str);
free(str);
str = NULL;
}
int main()
{
test();
return 0;
}
第二个例题:
返回栈空间地址的问题(只有栈区会出现这样的问题,在堆区和静态区都不会随便轻易出现非法访问内存的问题)
这里会出现非法访问内存的问题。结果是随机值
char *GetMemory(void)
{
char p[] = "hello world"; //这里p创建的是一个局部的数组,当除了这个函数内部以后,就会销毁掉,但是他依旧返回了这个创建了字符数组的
//地址,虽然此时str接受了这个p的地址,但是此时p里面是什么已经没有人知道了,所以结果是随机值
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
int* test()
{
static int a = 10; // 如果不加static 那么这里所创建的a在栈区的,除了这个函数,就会释放掉,然后你返回了a的地址,但是此时a里面的内容
//已经被销毁了,所以就会出现非法访问内存的问题。
//但是因为增加了static是这个变量从栈区变到了静态区,增加了这个局部变量的寿命,遍可以进行后续的修改,也不会报错
return &a;
}
int main()
{
int* p = test();
*p = 20;
return 0;
}
第三题:
//一定要有意识:看见malloc 和calloc,realloc就要进行free释放,他们两个是一定一定要成对出现的,不然就会出现内存泄漏的问题
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 mian()
{
Test();
return 0;
}
第四题:
结果会打印world,但是问题很大,因为非法访问内存了。
void Test(void)
{
char *str = (char *)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL) // 虽然我把str释放掉了,但是释放并不会把str自动的置成空指针。
{
strcpy(str, "world");//此时这一块空间已经被释放了,你还要对他进行操作,造成了非法访问内存的问题。
printf(str);
}
}
int main()
{
Test();
return 0;
}
- 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些 存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有 限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
- 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似 于链表。
- 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
- 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
有了这幅图,我们就可以更好的理解在《C语言初识》中讲的static关键字修饰局部变量的例子了。
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁所以生命周期变长。
柔性数组
C99 中,结构中的最 后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
struct S
{
int n;
int arr[];//结构体中最后一个是未知大小的--柔性数组成员--数组的大小是可以调整的
};
int main()
{
//struct S s;
//printf("%d\n", sizeof(s));// 结果是4,因为在计算柔性数组的大小的时候,是不包含柔性数组成员的
struct S* ps = (struct S*)malloc(sizeof(struct S)+5*sizeof(int));
//这里我进行开辟的时候是不知道这个柔型数组成员的大小的,所以我手动的给他设置一个大小,5*sizeof(int)这么大。
ps->n = 100;
int i = 0;
for(i=0;i<5;i++)
{
ps->arr[i] = i;
}
struct S* ptr = realloc(ps,44);//此时我认为我原先开辟的柔性数组成员大小不够,我给他调整一下
if(ptr != NULL)
{
ps = ptr;
}
for(i=0;i<10;i++)
{
ps->arr[i] = i;
}
//释放
free(ps);
ps = NULL;
return 0;
}
如果只是希望我的结构体中的数组可大可小,还有一种方法。
struct S
{
int n;
int* arr;
};
int main()
{
//这里一共使用了两次malloc所以也要进行两次的释放
struct S* ps = (struct S*)malloc(sizeof(struct S));
ps->arr = malloc(5 * sizeof(int));
int i = 0;
for (i = 0; i < 5; i++)
{
ps->arr[i] = i;
}
for (i = 0; i < 5; i++)
{
printf("%d ", ps->arr[i]);
}
//调整大小
int* ptr = realloc(ps->arr, 10 * sizeof(int));
if (ptr != NULL)
{
ps->arr = ptr;
}
for (i = 5; i < 10; i++)
{
ps->arr[i] = i;
}
for (i = 5; i < 10; i++)
{
printf("%s ", ps->arr[i]);
}
//释放内存
free(ps->arr); // 因为这里的ps->arr是包含在ps里面的,如果先释放ps指针,那么就找不到ps->arr这个指针了
ps->arr = NULL;
free(ps);
ps = NULL;
return 0;
}
第二种从形式上来看时更容易出错的,因为你两次使用malloc就要对应两次的free,容易遗忘并且还需要考虑先释放哪一个指针,如果释放错了也会犯错。但是使用柔型数组只需要进行一次malloc内存开辟,每使用一个malloc进行内存的开辟,就会造成内存碎片,使用的越多,内存碎片也就越多,内存的空间利用率就会很低,因为造成了很多不必要的内存浪费。(内存是连续的时候,访问效率更高,访问的速度更快)(寄存器 cache(高速缓冲区) 内存 硬盘 )这个是一个CPU访问的顺序
第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用 户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发 现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体 指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度
连续的内存有益于提高访问速度,也有益于减少内存碎片。