目录
1.为什么需要动态内存分配
首先,为什么需要动态内存管理? 我们之前学习过的申请空间的方式有两种:1.在栈空间上开辟空间 2.在栈空间上开辟连续空间。
int n=10; //固定申请4个字节
int array[10]; //申请连续的一块空间
但上述的开辟空间的方式有两个特点:
- 空间开辟大小是固定的,无法改变
- 数组在申明时,必须指定数组长度,所需内存在编译时就分配好了
为此C语言提供了动态内存管理的功能,我们可以根据自己的需要开辟或释放内存,接下来本文将为大家详细讲解几个动态内存函数、常见的动态内存错误、几道经典笔试题目
2.动态内存函数使用注意事项
需要先说明的是,我们在主函数中创建的局部变量储存在栈区,而动态内存是在堆区进行管理的
2.1.malloc和free
malloc和free都声明在stdlib.h头文件中
void* malloc(size_t size);
malloc函数向堆区的内存申请一块连续可用的内存空间,申请成功时,返回指向申请内存空间起始位置的指针,若申请失败则返回NULL指针(如果size的值为0,malloc的行为是未定义的)
用malloc申请空间
int main()
{
//申请40字节,存放10个整型
int* p = (int*)malloc(40);
return 0;
}
由于已经确定了申请的空间要存放的变量类型,我们可以将malloc返回的void*类型的指针强制类型转换为int*类型的指针。
为了应对申请失败的情况,我们需要在失败时打印出错误原因
int main()
{
int* p = (int*)malloc(INT_MAX);//整型变量的最大值,20多亿
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1; //非正常返回
}
return 0;
}
(注:errno是存放错误码的变量,包含在头文件errno.h内,strerror能把错误码翻译为错误信息)
可以看到打印结果:
局部变量在栈区上创建,生命周期结束后自动销毁,而在堆区上申请的空间尽管在程序运行结束后便会被自动回收,但作为负责任的程序员,我们应该在使用完后及时释放,防止程序运行周期较长时造成的内存浪费。C语言同样提供了释放空间的函数。
free
void free(void* ptr);
free能够释放向堆区申请的内存,释放的是ptr指针指向的空间
接下来和大家讲解free函数的使用
int main()
{
int* ptr=(int* )malloc(40);
if (ptr == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
for (int i = 0;i < 10;i++)
{
*(ptr + i) = i + 1;
}
for (int i = 0;i < 10;i++)
{
printf("%d ", *(ptr + i));
}
free(ptr);
return 0;
}
在监视窗口中,我们可以查看第一个for循环语句执行完后ptr指向的空间内变量的值,注意此时ptr的地址为 0x013a9700
而在执行完调用free函数的语句后,再次观察监视窗口:
可以看到ptr,ptr+1,……,ptr+10指向的空间都被释放了,然而ptr指针指向的依然是 0x013a9700!
可见,free函数虽然能释放函数指向的空间,但是不会把该指针变量保存的地址清空!而此时这个指针变成了个野指针,指向的已经是未定义空间的地址。万一我们在写代码时出现疏忽,忘记了我们已经free过,那么这将造成非法访问。
所以我们应该在free后主动将ptr置为NULL
ptr=NULL;
- 如果参数ptr指向的空间不是动态开辟的,那么free函数的行为就是未定义的
- 如果传入free的是空指针,将什么也不会发生
2.2.calloc
void* calloc(size_t num,size_t size);
size为变量的大小,num为变量的数目,calloc申请size*num字节的空间
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
perror("calloc");
return 1;
}
for (int i = 0;i < 10;i++)
{
printf("%d ", *(p + i));
}
free(p);
p - NULL;
return 0;
}
打印结果如下:
可以看到,与malloc不同的是,calloc在申请好空间后,会将其中的变量均初始化为0.
2.3. realloc
void* realloc(void* ptr,size_t size);
其中ptr指向的是malloc,calloc,realloc申请好的空间,size则是动态内存在调整后内存的大小
可以说,realloc让动态内存管理的灵活性大大提升,我们可以通过realloc依据自己的需要对内存进行灵活地调整。
接下来为大家介绍realloc调整空间时的可能情况
- 当ptr指向的空间的后续空间充足时,realloc函数直接在后续空间中申请所需的空间,同时返回指向原来空间起始地址的指针
- 当ptr指向的空间的后续空间不足时,realloc函数将会在内存中寻找到一块符合要求的新连续空间,把原来空间中的数据拷贝到新的空间,并把原来空间释放,返回指向新空间起始地址的指针
realloc扩容失败时,会返回NULL指针
realloc的使用
int main()
{
int* ptr = (int*)malloc(20);
if (ptr == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0;i < 5;i++)
{
*(ptr + i) = i + 1;
}
int* p = (int*)realloc(ptr, 20);
if (p != NULL)
{
ptr = p;
}
for (int i = 0;i < 10;i++)
{
printf("%d ", *(ptr + i));
}
free(ptr);
ptr = NULL;
return 0;
}
打印结果为:
有几个需要注意的点 :
- realloc的返回值需要用指针接受,原因前面已经提到了,即如果后续空间不足,realloc创建新空间后会返回新空间的地址,由于旧空间被释放,再访问就会造成非法访问
- 接收realloc返回值需要先用一个新的指针,因为如果realloc失败了,返回的就是NULL指针,我们不但没扩容成功反而找不到原来的空间了
特殊情况:
当传入realloc的参数为realloc时,realloc的作用相当于malloc
3.常见的一些动态内存错误
3.1.对空指针的解引用操作
这段代码中
int main()
{
int* p = (int*)malloc(100);
for (int i = 0;i < 20;i++)
{
*(p + i) = 0;
}
for (int i = 0;i < 20;i++)
{
printf("%d", *(p + i));
}
free(p);
p = NULL;
return 0;
}
没有对p是否为空指针进行判断,如果malloc失败,那么p接受到的就是空指针,而后续的p+1,p+2,……,p+9全部都是野指针了,对野指针的解引用操作是非常危险的!
所以这段代码应该进行修改:
int main()
{
int* p = (int*)malloc(100);
if (p == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0;i < 20;i++)
{
*(p + i) = 0;
}
for (int i = 0;i < 20;i++)
{
printf("%d", *(p + i));
}
free(p);
p = NULL;
return 0;
}
3.2.对动态内存的越界访问
int main()
{
int* p = (int*)malloc(100);
for (int i = 0;i < 100;i++)
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}
这段代码造成了越界访问,将导致程序崩溃
3.3.对非动态内存用free释放
void test()
{
int a = 10;
int* p = &a;
free(p);
p = NULL;
}
由于free只能释放堆区上的动态内存,所以这样同样会导致程序崩溃
3.4.free释放动态内存的一部分
int main()
{
int* p = (int*)malloc(40);
for (int i = 0;i < 5;i++)
{
*p = i;
p++;
}
free(p);
p = NULL;
return 0;
}
在初始化过程中,p指向的位置被改变,不再指向起始地址,此时用free释放内存释放的仅时动态开辟内存的一部分,运行程序时会崩溃.
3.5.对同一块动态内存多次释放
int main()
{
int* p = (int*)malloc(20);
for (int i = 0;i < 5;i++)
{
*p = i;
p++;
}
free(p);
//......
free(p);
return 0;
}
如果我们忘记了先前已经释放过动态内存,再次进行释放,程序运行时就会崩溃,所以我们在使用动态内存函数时应该记住一个malloc/calloc对应一个free.
3.6.忘记释放动态内存
void test()
{
int* p = (int*)malloc(40);
//使用
//......
//忘记释放了
}
int main()
{
test();
//......
return 0;
}
由于我们在函数内创建的指向动态空间的指针在出函数作用域就被销毁,而这块内存并不会被销毁,但是我们后续就不知道这块动态内存的地址了,而这块内存将被浪费掉,这被称为内存泄漏
4.几道经典笔试题
4.1.题目一
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test()
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
int main()
{
Test();
return 0;
}
请问调用Test函数的结果?
- 在函数GetMeroy此时是传值调用,p指针仅是str的临时拷贝,无法改变str的指向,在出了函数的作用域后自动被销毁,所以str仍然是空指针,而strcpy把“hello world”拷贝到这个空指针(没有任何指向空间)所指向的内存,造成非法访问,因此程序将会崩溃
- malloc申请的空间没有被释放,而且由于p指针被销毁,我们再也无法知道这块空间的位置了,这将导致内存泄漏
下面我们来对这段代码进行修改:
void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
void Test()
{
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
我们可以将str指针的地址传入GetMemory函数,即把GetMemory函数的参数设为一个二级指针,这样就能通过解引用操作修改str指向的地址了!
4.2.题目二
char* GetMemory()
{
char p[] = "hello world";
return p;
}
void Test()
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
请问调用Test函数的结果?
这段代码的打印结果如下:
并不是hello world而是一串乱码,请问该代码出了什么问题?
问题在于,在GetMemory函数中,字符数组p[]在栈区上被创建,出了作用域后就被销毁,此时虽然函数返回p指针,但指针指向的已经是未定义空间了,打印出的就是乱码。此问题也被称作返回栈空间地址的问题
下面来修改这段代码:
const char* GetMemory()
{
const char *p = "hello world";
return p;
}
void Test()
{
char* str = NULL;
str = (char*)GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
由于字符串常量存放在静态区,出了函数作用域也不会被销毁,故我们可以将字符数组p换成const char* p,然后将GetMemory函数返回值强制类型转化赋给str即可
4.3.题目三
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;
}
请问调用Test函数的结果是什么?
能够正常打印,然而这段代码还是存在着 内存泄漏的问题需要free释放str指向的空间,并把str置为NULL
4.4.题目四
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;
}
请问调用Test函数所得到的结果是什么?
可以看到此时打印结果为world,似乎没有什么问题,但我们还是要说,这已经造成了非法访问,原因在于free把str指向的空间释放后,尽管str仍然保留着原来空间的地址,但是我们对这块空间不再具有访问权限 ,此时再进行strcpy操作,属于非法访问。
5.写在最后
本篇文章就为大家讲到这里,接下来我还将持续分享我在学习过程中学到的值得记录的知识,如果觉得我的文章有帮助的话,希望大家多多支持!