如果我们被问道:如何创建一个可以根据用户需求来开辟大小的数组?
可能有些博友会写出如下代码:
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
int arr[n];
return 0;
} 这个代码在C99标准下是可以运行的,但大多数编译器并不支持C99标准,所以这种代码缺乏了跨平台性(可移植性),那么我们有没有办法写出一个既可以满足题目要求,又可以在任何一个编译器下都编译得过去的代码呢?
答案是肯定的。这就和C语言中的动态内存的开辟有关了,动态开辟,即可以按照需求开辟内存的大小。
下面介绍几个操作动态内存的常用函数。这几个函数的操作对象都是堆区的内存。
扩展:局部变量存放在内存中的栈区;全局变量、静态变量(static修饰的变量)存放在内存中的静态区(也叫数据段)。
Ⅰ. 四个重要的内存函数 -- 头文件<stdlib.h>
1、malloc
C语言提供了一个动态内存开辟的函数:
void *malloc(size_t size);这个函数向申请一块连续可用的空间,并返回指向这块空间的指针。
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个NULL指针,因此 malloc 的返回值一定要检查。
- 返回值的类型是 void*,所以 malloc 函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
- 如果参数 size 为 0,malloc 的行为的标准是未定义的,取决于编译器。
假设我们要开辟一个可以存放10个整型的空间:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
// 因为malloc函数的返回值为void*,所以需要强制类型转换为对应类型。
if (p == NULL)
{
printf("内存开辟失败\n");
}
else
{
printf("内存开辟成功\n");
// 使用...
// 使用结束,释放内存(后面介绍)
free(p);
p = NULL;
}
return 0;
} 注:malloc函数开辟好空间后,不对空间内容做任何初始化,所以空间内的数据为随机值。
2、free
C语言还提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:
void free(void *ptr);free函数用来释放动态开辟的内存。
- 如果参数 ptr 指向的空间不是 **动态开辟(如数组名)**的,那 free 函数的行为是未定义的。
- 如果参数 ptr 是 NULL指针,则函数什么事也不做。
其使用方式非常简单,就是在使用完后加上两个语句:
free(p); // p为要释放的代码块的首地址
p = NULL; // 必不可少我们也已经看到了,上面每一个开辟了动态内存的代码,在使用完该动态内存后,都将该内存空间释放了(即还给操作系统)。
如果在使用完动态内存后忘记将其空间释放,便会造成内存泄漏的问题:
- **内存泄漏(MemoryLeak)**是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
- 内存泄漏缺陷具有隐蔽性、积累性的特征,比其他内存非法访问错误更难检测。因为内存泄漏的产生原因是内存块未被释放,属于遗漏型缺陷而不是过错型缺陷。此外,内存泄漏通常不会直接产生可观察的错误症状,而是逐渐积累,降低系统整体性能,极端的情况下可能使系统崩溃。
☢️注意:
- 在释放代码块后,必须将该代码块的首地址改为 NULL,否则该指针将变为野指针,我们都知道,野指针非常危险。
- 如果传入 free 函数的为空指针(NULL),则free函数什么也不做。
3、calloc
C语言还提供了一个函数叫calloc,calloc函数也用来动态内存开辟。原型如下:
void *calloc(size_t num, size_t size); calloc 函数的功能也是开辟指定大小的内存空间,如果开辟成功就返回该空间的首地址,如果开辟失败就返回一个 NULL。但 calloc 函数传参时需要传入两个参数(开辟的内存用于存放的元素个数和每个元素的大小)。
- 函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节都初始化为0。
- 与函数malloc的区别只在于calloc会返回地址之前把申请的空间的每个字节初始化为全0。
calloc 函数与 malloc 函数的用法也是大同小异:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)calloc(10 , sizeof(int)); // 使用calloc开辟
if (p == NULL)
{
printf("内存开辟失败\n");
}
else
{
printf("内存开辟成功\n");
free(p);
p = NULL;
}
return 0;
}4、realloc
- realloc函数的出现让动态内存管理更加灵活。
- 有时我们会发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。那realloc函数就可以做到对动态开辟内存大小的调整。
函数原型如下:
void *realloc(void *ptr, size_t size);- ptr是要调整的内存地址。
- size调整之后的新大小。
- 返回值为调整之后的内存起始位置。
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移到新的空间 。
- realloc在调整内存空间时候存在两种情况:
- 原有空间之后有足够大的空间,此时,realloc 函数直接在原空间后方进行扩展,并返回该内存空间首地址(即原来的首地址)。
- 原有空间之后没有足够大的空间:
- realloc 函数会在堆区中重新找一块满足要求的内存空间,把原空间内的数据拷贝到新空间中,并主动将原空间内存释放(即还给操作系统),返回新内存空间的首地址。
- 此时,需扩展的空间后方没有足够的空间可供扩展,并且堆区中也没有符合需要开辟的内存大小的空间。结果就是开辟内存失败,返回一个 NULL。
Ⅱ. 常见的动态内存错误
1、对NULL指针进行解引用操作
#include<stdio.h>
int main()
{
int *p = (int*)malloc(100000000000);//开辟内存过大,可能会开辟失败,返回空指针
int i = 0;
for(i = 0;i<10;i++)
{
*(p+i) = i;
}
}解决方法:在申请空间后对p进行判断。
if(p == NULL)
return 1;2、对动态开辟内存的越界访问
#include<stdio.h>
int main()
{
int *p = (int*)malloc(10*sizeof(int));//申请10个int空间大小
if(p==NULL)
return 1;
int i=0;
// 越界访问
for(i=0;i<40;i++)
{
*(p+i)=i;
}
}3、 使用free释放非动态开辟的空间
int main()
{
int arr[10]={0}//存于栈区
int *p=arr;
free(p);//使用free释放了数组申请的空间
p=NULL;
return 0;
}4、使用free释放了动态开辟内存的一部分
int main()
{
int* p=(int*)malloc(10*sizeof(int));
if(p==NUL)
return 1;
int i=0;
for(i=0;i<5;i++)
{
*p++ = i;
}
free(p);
p=NULL;
return 0;
} 解读:p指向开辟内存的首地址,而在循环赋值中,p只对其中5个int大小进行了赋值,而后就对该空间释放。除此之外p受++运算符不断向后移动,使得p的初始位置改变,这很容易导致内存泄漏。
5、对同一块动态内存开辟的空间多次释放
int main()
{
int* p=(int*)malloc(100);
//使用
//释放
free(p);
p=NULL;
//释放
free(p);
return 0;
}6、动态开辟的空间忘记释放,造成内存泄漏
解决方法:在该指向这块空间的指针的生命周期结束前使用 free 函数释放。
Ⅲ. 经典面试题
第一题
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str=NULL;
GetMemory(str);
strcpy(str,"hello,liren");
printf(str);
} 请问运行Test函数会有什么样的结果?
答案:程序奔溃!
解读:str传给GetMemory函数的时候是值传递(因为并不是使用str所指地址的值而是改变str本身的值),所以GetMemory函数的形参p是str的一份临时拷贝。在GetMemory函数内部动态申请空间的地址,存放在p中,不会影响外边str,所以当GetMemory函数返回之后,str仍然是NULL,所以strcpy会失败。(当GetMemory函数返回之后,形参p销毁,使得动态开辟的100个字节存在内存泄露)
第二题
char* getmemory(void)
{
char p[]="hello,liren";
return p;
}
void test(void)
{
char* str=NULL;
str=getmemory();
printf(str);
}
int main()
{
test();
return 0;
} 这种题称为返回栈空间地址的问题
答案:一堆乱码!
解答:getmemory函数内部创建数组是在栈区上创建的,出了函数,p数组的空间就还给了操作系统,返回的地址是没有实际的意义的,如果通过返回的地址,去访问内存就是非法访问内存的。
第三题
void getmemory(char** p,int num)
{
*p = (char*)malloc(num);
}
void test(void)
{
char* str=NULL;
getmemory(&str,100);
strcpy(str,"liren");
printf(str);
}
int main()
{
test();
return 0;
}答案:程序奔溃!
解答:应在test函数里面使用free函数释放str,并将其设为空指针。
第四题
void test(void)
{
char* str=(char*)malloc(100);
strcpy(str,"liren");
free(str);
if(str!=NULL)
{
strcpy(str,"love tongtong");
printf(str);
}
}
int main()
{
test();
return 0;
}答案:程序奔溃!
解答:test函数中提前free了一次,造成下面的非法访问内存。(注:使用free后并不会将指针置空,必须要手动置空)