目录
1、为什么要有动态内存分配
2、动态内存函数介绍
1、malloc
2、free
3、calloc
编辑
4、realloc
3、动态内存常见的错误
4、动态内存开辟相关好题
5、c/c++程序内存开辟示意图
int a, int arr[10] 是固定地向内存申请连续的一块空间,但不能变长或变短随时调整。
在我们之前写的静态版通讯录中,我们创建了一个peopleinfo类型的数组date[100]用来存放100个人的个人信息,但是当我们的人员信息较小时,100个结构体显得有些浪费,而当我们所需存放的信息超过100时又不够用,随之我们就得修改这个数字,但是学了动态内存管理之后,我们可以动态地分配内存空间变大或变小,从而有效利用空间。
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
但是上述的开辟空间的方式有两个特点:
1. 空间开辟大小是固定的。
2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,
那数组的编译时开辟空间的方式就不能满足了。
这时候就只能使用动态内存开辟了。
#include
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
如果开辟成功,则返回一个指向开辟好空间的指针。
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己
来决定。
如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
先在堆区上动态申请一定空间,使用后应该还给操作系统,如果不主动还,程序结束后会自动还,但是如果程序一直不结束,就一直“占着不用”,就会造成空间的浪费。
用来释放、归还申请的内存
int main()
{
//申请40个字节,用来存放10个整型
int* p = (int*)malloc(40);
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//存放 1--10
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i + 1;
}
for (i = 0; i < 10; i++)
{
printf("%d ", *(p+i));
}
//free释放申请的内存
free(p);
return 0;
}
用malloc申请后,内存中为随机值,使用时可给它们赋值,当free后,又变为随机值。
仔细观察,我们可以发现,虽然这块空间的值发生了变化,但是指针p指向的地址free前后没有变化。因此,当我们free还给操作系统后,p仍指向这块空间。此时*p就会导致非法访问,因此我们需要将p制为NULL,避免非法访问。
free(p);
p = NULL;
总结:malloc申请空间后不会初始化,使用前要判断是否成功申请(是否返回NULL),使用后要free还给操作系统,然后将用于接收这块内存空间的指针p置为NULL。
申请失败时返回NULL,并打印错误原因(没有足够空间)。
void free(void * ptr)
free函数用来释放动态开辟的内存。
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
如果参数 ptr 是NULL指针,则函数什么事都不做。
由上图我们可以知道,malloc不会初始化,calloc会将每个元素先初始化为0,可以按需用。由于calloc需要初始化,效率比malloc稍低,此外没有其它区别,都需要进行相关步骤。
int main()
{
int* p = (int*)malloc(5 * sizeof(int));
if (NULL == p)
{
perror("malloc");
return 1;
}//使用
int i = 0;
for (i = 0; i < 5; i++)
{
p[i] = 1;
}//空间不够,增加5个整型的空间
//此时不能用p接收
int* ptr = (int*)realloc(p, 10 * sizeof(int));
//先用ptr接收,再赋给p,防止返回NULL,p找不到原来的数据
if (ptr != NULL)
{
p = ptr;
ptr = NULL;//释放但不置空,需手动置空
}
//继续使用
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}//realloc会把旧的空间释放,不用自己释放
free(p);
p = NULL;
}
1、对NULL解引用(未判断是否为空)
2、非法访问内存,越界访问(解引用野指针)
3、对非动态开辟内存的空间用free释放
void test()
{
int a = 10;
int *p = &a;
free(p);//ok?
}
对栈区的空间进行释放(x)
4、使用free释放一块动态开辟内存的一部分
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
p=NULL;
}
5、对同一块内存多次释放
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
free一个NULL空指针时,什么事都不会发生。
malloc等函数是申请一块空间,获得使用它的权限,free释放后,将权限还给操作系统,如果再访问或是释放,就是非法访问。
6、动态开辟的内存忘记释放(内存泄漏)
malloc和free要成对出现,防止出现内存泄漏
int* test()
//函数内部进行了malloc操作,返回了malloc开辟的空间的起始地址
//谁接收了 要记得释放和置空
{
int* p = (int*)malloc(100);
if (NULL == p)
{
return 1;
}
return p;
}
int main()
{
int* ptr = test();
free(ptr);
ptr = NULL;
}
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();
}
这里GetMemory函数是值传递,且没有返回值,对str无影响。因此str还是指向NULL,即0地址处,然后调用strcpy需访问0地址处内容,导致非法访问。
同时,上面用malloc申请了一块空间,但是函数内没有释放,函数销毁后,p也销毁,这块空间就会内存泄漏。记得malloc要与free搭配使用。
修改后,可传入&str,用char**p接收,*P=(char*)malloc(100),使p指向开辟的100byte,进而存放拷贝的内容,打印后free(str) str=NULL
或者将p(char*)直接返回,用str接收,因为没有free,所以malloc(100)仍然存在,进而可以strcpy,如果此时不是malloc申请,而是利用数组,函数销毁后,数组的空间也会释放,如果再进行打印就不行了。(如下图)这类问题简称为 返回栈空间地址的问题
可以在p前面加上static使其变为静态区变量,函数销毁后它不会销毁,或者去掉[],p变为char*类型,即从数组变为常量字符串,常量字符串也是在静态区,也就是把在栈区存储的数据放入静态区中存储,从而避免了返回栈空间地址的问题。
printf(str)括号内直接加str是可以的,str是一个地址,例如printf(“hehe”),括号内有引号+字符串,也就是首字符的地址,相当于括号内直接加一个地址,最终结果都是打印字符串。
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();
}
这段代码的整体思路没有问题,但是会导致内存泄漏, malloc等函数需要与free共同使用
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
}
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
free(str)后已经没有权限访问了,但后面又调用strcpy访问空间,导致非法访问
开辟--释放--置空
习题来自《高质量C/C++编程》
1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结
束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是
分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返
回地址等。
2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分
配方式类似于链表。
3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码,只读常量(不能被修改),字符串常量。