引入
typedef struct Con
{
char name[20];
char sex[10];
int age;
char tel[30];
char addr[100];
}Con;
int main()
{
Con con[100];
return 0;
}
例子:一个固定开辟了100个元素的通讯录中的人员结构体数组,那么当我们只有10个人的时候,那么就有90个元素的空间被浪费了,如果我们有110个人的时候,数组就不够了。因此,我们为了内存能够得到有效的利用,能够适配具事件的不同情况,不至于 浪费,也不至于不够,能够有效的节省空间,所以需要在堆区动态开辟内存,以供我们更加灵活高效的使用内存
动态内存函数
malloc和free
函数原型:void* malloc (size_t size);
size - 我们需要开辟的动态空间大小,如果size为0,malloc的行为未定义,完全取决于编译器
返回值 - 因为返回类型是void* 所以常常搭配强制类型转换,如果返回成功,那么得到开辟的动态空间的首地址,如果开辟失败,返回NULL
所以在使用malloc函数时,需要判空NULL(判成功),防止在开辟失败后让变量得到NULL,还在进行后续各类操作(解引用、赋值…)。
#include<errno.h>errno
#include<string.h>strerror
#include<stdlib.h>malloc
#include<stdio.h>printf
int main()
{
int* p = (int*)malloc(40);
if(p == NULL)
{
printf("%s\n", strerror(errno));//打印错误信息
return 1;
}
return 0;
}
这是一种判空方法,这种方法很完善,能够知道错误原因,而如果直接用assert§的话,只能知道在哪一行出现的问题,但是为了使用printf("%s\n", strerror(errno));
这一行代码,需要用到两个不常用的头文件,所以简洁的判空的处理方法是用perror函数,此函数的头文件是<stdio.h>,不需要额外的头文件
在堆区申请空间都需要自行归还给操作系统,malloc,以及后面的realloc,calloc函数都是在堆区申请的空间,因此都需要自行归还,都需要使用free函数
函数原型:void free (void* ptr);
ptr - 动态开辟的内存的首地址。如果ptr为NULL,free函数什么也不做,如果ptr不是动态开辟的,那么free的行为未定义,取决于编译器
free函数仅仅是释放掉ptr指针所指向开辟的动态内存,也就是说ptr释放后,ptr指向的空间还存在,但是此时该空间就不是我们能够使用的空间了,也就是变成野指针了,为了防止非法访问,我们就需要将野指针置空:ptr = NULL
当然,在程序结束后,编译器会自动帮助我们归还动态开辟的内存,但这样依靠编译器的方式非常不妥
#include <stdio.h>
#include<stdlib.h>
int main()
{
int* ptr = NULL;
ptr = (int*)malloc(10 * sizeof(int));
if (ptr == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(ptr + i) = i;
}
free(ptr);//释放ptr所指向的动态内存
ptr = NULL;
return 0;
}
在我们调试的时候,想要看到某个指针后面每一个元素的情况,只需要在监视窗口输入p,10(10为元素总个数)
malloc失败案例
//x86(32位)上跑代码,x64(64位)上INT_MAX还不够大,还能开辟成功
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(INT_MAX);//21亿大小的一个数字
if (p == NULL)
{
perror("malloc");//malloc: Not enough space
return 1;
}
free(p);
p = NULL;
return 0;
}
calloc
其函数原型:void* calloc (size_t num, size_t size);
函数的功能是给num个大小为size的元素开辟空间,并且把空间的每个字节初始化为0。
与函数malloc的区别只在于calloc会在返回之前把申请的空间的每个字节初始化为全0,也正因如此,malloc的效率更高,但当有初始化的需求,用calloc也是不错的选择。
#include <stdio.h>
#include<stdlib.h>
int main()
{
int* ptr = NULL;
ptr = (int*)calloc(10, sizeof(int));
if (ptr == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(ptr + i));//0 0 0 0 0 0 0 0 0 0
}
free(ptr);//释放ptr所指向的动态内存
ptr = NULL;
return 0;
}
realloc
realloc函数的出现让动态内存管理更加灵活。
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,会对内存的大小做调整。那realloc函数就可以做到对动态开辟的内存大小进行调整。
函数原型:void* realloc(void* ptr, size_t size);
ptr - 是要调整的内存地址,如果ptr是NULL,那么realloc就相当于是malloc
size - 调整之后新大小,可比原来空间大,也可以比原来空间小
返回值为调整之后的内存起始位置,如果调整空间失败,会返回NULL,因此在使用realloc时,先用一个临时的指针变量tmp去接收realloc的返回值,如果不为NULL,再把tmp赋值给ptr
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
realloc在调整内存空间的是存在两种情况:
情况1:扩大空间,原有空间之后有足够大的空间,此时会保留原来的数据,在原有内存的后面直接追加空间。
情况2:扩大空间,原有空间之后没有足够多的空间时,扩展的方法是在堆空间上另找一个合适大小的连续空间,并把原来的旧数据拷贝到新空间上去,然后realloc函数内会把旧地址的动态空间释放掉,这样函数返回的是一个新的内存地址。
情况3:缩小空间,缩小原有空间能够访问到的空间大小,保留缩小后能够访问到的原有数据。
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(5 * sizeof(int));
if ( p == NULL)
{
perror("malloc");
return 1;
}
//使用
int i = 0;
for (i = 0; i < 5; i++)
{
* (p + i) = 1;
}
//不够了,增加5个整形的空间
int* ptr = realloc(p, 10 * sizeof(int));
if (ptr != NULL)
{
//当动态申请空间成功后
p = ptr;//我们不知道是否用到了新空间,所以再将ptr赋值给p
//然后就可以用p正常使用新的动态空间了
ptr = NULL;//这里ptr和p指向了同一块空间,可以使用ptr,但是大多数情况,只需要一个指针,所以一般就立刻将ptr置空,如果需要使用ptr,就需要记得在free(p)这块动态内存后,将p和ptr都给置空以防止野指针。
}
//使用动态内存
//...
free(p);
p = NULL;
//ptr = NULL;
return 0;
}
int main()
{
int* p = (int*)realloc(NULL, 40);
//当第一个参数是NULL,那么realloc就相当于malloc
if(p != NULL)
{
//使用动态内存
}
free(p);
p = NULL;
return 0;
}
动态内存常见使用错误
一:malloc的空间没有判成功,对NULL指针的解引用操作
int main()
{
int* p = (int*)malloc(100);//没有assert或者if判成功,会报警告:对NULL指针的解引用操作
int i = 0;
for(i = 0; i < 20; i++)
{
*(p + i) = 0;
}
return 0;
}
二:把空间大小当作个数,越界访问
int main()
{
int* p = (int*)malloc(100);
if(p == NULL)
{
return 1;
}
int i = 0;
for(i = 0 ; i < 100; i++)
{
*(p + i) = 0;
}
return 0;
}
三:对非动态开辟内存使用free释放
int main()
{
int a = 10;//a存放在栈区
int* p = &a;
free(p);//不能free栈区空间
p = NULL;
return 0;
}
四:使用free释放一块动态开辟内存的一部分
void test1()
{
int* p = (int*)malloc(100);
p++;
free(p);//此时p不是动态开辟的内存的起始位置,所以不同free
}
void test2()
{
int* p = (int*)malloc(100);
if(p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for(i = 0; i< 25; i++)
{
*p = i;
p++;
//更好的方式:for(i = 0; i < 5; i++){ *(p + i) = 1;}
}
free(p);
p = NULL;
return 0;
}
五:对同一块动态内存多次释放
int main()
{
int* p = (int*)malloc(100);
if(p == NULL)
{
return 1;
}
free(p);
p = NULL;
free(p);//这样从语法上是没有问题的,因为free函数当参数为NULL时,什么也不做,但如果p没有置空,多次free,程序会崩。
}
六:动态开辟内存忘记释放
void test()
{
int* p = (int*)malloc(100);
//使用
}
//如果test函数中没有释放空间,那出了函数就无法找到这块动态内存
int main()
{
test();
return 0;
}
malloc 判成功 free 置空 一个不能少
int* test()
{
int* p = (int*)malloc(100);
if(p == NULL)
{
return 1;
}
//使用
return p;
}
//test中有malloc操作,当因为具体需求,不能在test中free时,那么就用返回值的方式,取得malloc开辟的动态地址。并且记得注释提示程序员主函数中需要free动态内存。
int main()
{
int* ptr = test();
free(ptr);
ptr = NULL;
}
void GetMemory(char* p)
{
int* p = (int*)malloc(100);
if(p == NULL)
{
return;
}
//使用
//if分支中未释放,发生内存泄露
if(1)
return;
free(p);
p = NULL;
}
内存泄露的影响?比如某些app重启可以解决卡顿问题,但是开机后使用该app一段时间又开始卡了,其实有时就是内存泄露,使得cpu占用率越来越高。
内存泄露只会因为动态内存分配malloc、calloc、realloc发生(C语言范围内)
经典笔试题
1、传值、释放问题
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
这个GetMemory函数是传值调用,并且没有返回值,所以无法在Test中改变str的值,并且p的生命周期在离开GetMemory函数时就结束了,也就是str还是NULL,我们无法对0x00000000位置进行(写入)解引用操作,除此之外,没有释放,所以还发生了内存泄露。
所以可以采用返回值,传址调用的方式,并且在Test函数中进行free、置空。
2、返回局部变量或临时变量的地址(返回了栈空间地址)
//对字符串
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
p是char数组类型,生命周期只在GetMemory函数中,离开函数后,p指向的空间已经归还给操作系统了,此时返回得到的p的地址其实是野指针,是非法访问内存的问题
此题的修改方式是将char p[] = "hello world;"
改成char* p = "hello world;
此时p存储的就是常量字符串"hello world"的地址了,常量字符串的地址是在静态区的,生命周期是整个程序,因此离开了GetMemory函数后,p指针指向的地址还能够正常访问。
ps:常量字符串是无法进行修改的
为了理解这里的char* p = "hello"
和char p[] = "hello"
,我举一个例子
int* test()
{
int a = 10;
return &a;
}
int main()
{
int* p = test();
printf("%d", *p);
return 0;
}
这里能够打印出10只是巧合,如果在printf语句前后有其他的语句,那么printf打印p的值会发生改变。(函数的栈帧被销毁了,但是内容没有被修改)
这里p并未改变,但是打印的值会发生变化。(栈帧问题)
3、内存泄露
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
//没有free和置空,内存泄露
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);//传址正确
strcpy(str, "hello");//没问题
printf(str);//打印没问题
}
int main()
{
Test();
return 0;
}
4、在free后使用动态空间
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
//在释放后,str还是指向的那块空间,内容可能还未改变。
if(str != NULL)
{
//str此时变成了野指针,发生非法访问
strcpy(str, "world");
printf(str);
}
}
栈区堆区静态区的简单讲解:
栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS(操作系统)回收 。分配方式类似于链表。
数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
代码段:存放函数体(类成员函数和全局函数)的二进制代码以及只读常量(常量字符串)。
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁。因此static修饰的局部变量的生命周期变长了
柔性数组
结构体中的最后一个成员允许是未知大小的数组,这就叫做『柔性数组』成员(C99标准),柔性数组的大小通过函数中动态内存开辟的大小来确定
结构体中可以用[0]这种方式代表大小位置,但是函数中的数组大小不能用这种方式表示
特点:
1、结构中的柔性数组成员前面必须至少一个其他成员(第二点即为第一点的原因)
2、sizeof 返回的这种结构体大小不包括柔性数组的内存
3、包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小
如果后面对于这个数组大小需要改变,那么就可以用realloc对柔性数组大小进行改变
柔性数组可以使用多次动态内存的方式达到效果
这种方式会多次malloc,会导致出现很多内存碎片,让代码的空间利用率低,多次free,也需要考虑free的顺序,因此柔性数组有其存在的价值(1、方便内存释放 2、有利于提高访问速度)