前言
1、为什么存在动态内存分配
int main()
{
int arr1[10];
char arr2[40];
return 0;
}
- 上述开辟空间的方式有两个特点
- 空间开辟的大小是固定的。
- 数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
- 但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。
这时候就只能试试动态内存开辟了。
2、动态内存函数的介绍
2.1 malloc函数和free函数
malloc函数是动态内存开辟函数。动态内存开辟的空间在使用完毕以后,一定要释放,free函数就是释放函数。 malloc和free都声明在 stdlib.h头文件中。
void* malloc(size_t size);
size是申请的字节大小
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
- 返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候,使用者自己来决定。
- 如果参数 size 为0,malloc的行为是未定义的,却决于编译器。
void free(void* ptr);
free函数是用来释放动态开辟的内存。
- 参数 ptr是指向先前使用malloc、calloc或realloc分配的内存块的指针。
- 如果参数 ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。
- 如果参数 ptr是NULL指针,则函数什么事都不做。
malloc函数与free函数展示
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(40);
int* ptr = p;
if (ptr == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*ptr = i;
ptr++;
}
free(p);
p = NULL;
return 0;
}
通过内存查看(p,10)
- 想使用动态内存开辟的空间用来存放整型,想将malloc函数的返回值为 void* 强制转为为 int*,因为指针类型决定了其能访问什么样的数据。因此开辟了40个字节,就能访问操作10个 int型的数据。
- 我们可以看到指针 p指向的空间已经被赋值为 0 1 2 3 4 5 6 7 8 9
- 在使用动态开辟的空间之前,要先判断开辟成功没有,即判断 ptr是否为空指针。
- 指针 p所指向的空间释放后及时赋值为NULL指针,避免成为野指针。
- 因为在赋值时,p的位置在变,因此 p指向的就不是之前所开辟空间的位置了,为了正确使用 free函数,先将 p指向的地址赋值给指针 ptr,在给开辟的空间赋值时,使用 ptr来操作,这样指针 p所指向的位置保持不变,在使用完后用free函数释放之前开辟空间的内容 free( p);
上述写法,因为指向动态内存开辟空间的指针随着使用,指针指向的位置会发生变化,下面代码进行优化。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
free(p);
p = NULL;
return 0;
}
打印结果
- 这才是我们平时使用指针的常规写法,p+i,p本身并没有发生改变,即p指向的位置没有发生改变 。
- 展示一下申请空间过大,返回空指针所得到的结果。
在32位环境下,malloc申请 NT_MAX大的空间会失败,返回空指针,进而执行代码中的 if条件语句。 - 关于perror函数的细节,请看文章链接: 《字符串函数和内存操作函数》第10节。
使用完毕动态开辟的内存空间后,我们一定要释放掉动态开辟的内存空间中的内容。 当我们不释放动态申请的内存的时候,如果程序结束,动态申请的内存由操作系统自动回收,但是如果程序不结束,动态内存不会自动回收,就会形成内存泄漏的问题。如下代码:
int main()
{
while (1)
{
malloc(1000);
}
return 0;
}
运行前 运行后
- 可以看到在程序运行后,一直在动态开辟内存空间,并且没有释放掉,所以电脑的内存占用增大了。不过由于电脑系统保护的原因,在开辟到一定大小后,停止开辟了。
- 所以在动态开辟的内存空间使用完毕后,一定要用free函数及时进行释放,避免出现内存泄漏的情况
2.2 calloc函数
calloc函数也用来动态内存分配。 calloc函数的区别是:calloc函数在动态内存开辟后,能够自动将内存空间中的元素全都初始化为0。malloc函数没有初始化。
void* calloc(size_t num, size_t size);
num是申请元素的个数,size是申请的每个元素的大小。 calloc函数展示
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
perror("calloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
free(p);
p = NULL;
return 0;
}
打印结果
- 可以看到,calloc函数将每个元素初始化为0了。
- malloc需要注意的如,使用前判断是否为NULL指针、使用后及时释放动态开辟内存空间中的内容这些注意事项,calloc函数全部都要注意。
- 同理,与malloc一样,使用者自己决定calloc函数开辟空间的类型。
2.3 realloc函数
- realloc函数的出现让动态内存管理更加灵活。
- 有时候我们发现过去申请的空间太小了,有时候我们又觉得申请的空间过大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。realloc函数就可以做到对动态开辟内存大小的调整。
void* realloc(void* ptr, size_t size);
- ptr是要调整的内存地址
- size是调整之后的新大小,比如已经开辟了40个自节,需要再开辟40个字节,那么size为80.
- 返回值为调整之后的内存起始位置。
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
- relloc函数在调整内存空间时是存在两种情况的:
-情况1:原有空间之后有足够大的空间,可以继续往后开辟。
当是情况1的时候,要扩展内存就原有内存之后直接追加空间,原来空间的数据不发生变化。 -情况2:原有空间之后没有足够大的空间可以往后开辟
当是情况2的时候,原有空间之后没有足够的多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。
情形1代码展示
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("calloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
int* ptr = (int*)realloc(p, 80);
if (ptr != NULL)
{
p = ptr;
}
free(p);
p = NULL;
return 0;
}
调试过程监视 realloc前 realloc后
- realloc再多开辟40个字节,可以发现realloc前后,p指向的地址始终没有变,说明后面有足够的空间用来动态开辟。
情形2代码展示
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("calloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
int* ptr = (int*)realloc(p, 1000000);
if (ptr != NULL)
{
p = ptr;
}
free(p);
p = NULL;
return 0;
}
调试过程监视 realloc前 realloc后
- realloc再多开辟999960个字节,可以发现realloc前后,p指向的地址发生了改变,说明后面没有有足够的空间用来动态开辟。
- 在堆空间上另找一个合适大小的连续空间来使用,并且将原来内存中的数据移动到新的空间,返回值是调整之后的内存起始位置。
3、常见的动态内存错误
3.1 对NULL指针解引用操作
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(1000);
int i = 0;
for (i = 0; i < 250; i++)
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}
- 这里对开辟的空间直接使用,没有对指针p是否为 NULL指针的判断,我们发现在使用时,对指针p进行了解引用,如果开辟空间失败,那么返回值是NULL指针,对NULL指针解引用,有可能产生不可预见的错误,程序会崩。
解决办法:对动态开辟内存的返回值进行判断。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(1000);
if (p == NULL)
{
return 1;
}
int i = 0;
for (i = 0; i < 250; i++)
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}
3.2 对动态开辟空间的越界访问
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(100);
if (p == NULL)
{
return 1;
}
int i = 0;
for (i = 0; i <= 25; i++)
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}
运行
- malloc函数一共开辟了25个整型空间,但是在使用时一共访问了 26个整型空间,越界访问。
3.3 对非动态开辟内存使用free释放
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdlib.h>
int main()
{
int a = 10;
int* p = &a;
free(p);
p = NULL;
return 0;
}
运行
- a是局部变量,是在栈区上开辟的。而动态开辟内存是在堆区上开辟的。
3.4 使用free释放一块动态开辟内存的一部分
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(100);
if (p == NULL)
{
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*p = i;
p++;
}
free(p);
p = NULL;
return 0;
}
运行
- malloc函数一共开辟了100个字节,当开辟的空间在使用完后,指针p已经不再指向动态开辟空间的起始位置,因此释放出错。
3.5 对同一块动态内存多次释放
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdlib.h>
int main()
{
int* p = malloc(100);
if (p == NULL)
{
return 1;
}
free(p);
free(p);
p = NULL;
}
运行
- 动态开辟的内存在已经释放一遍的情况下,再次进行释放,出错。
3.6 动态开辟内存忘记释放(内存泄漏)
#define _CRT_SECURE_NO_WARNINGS 1
void test()
{
int* p = malloc(100);
}
int main()
{
test();
while (1)
{
;
}
return 0;
}
- 动态开辟的内存在使用完后没有释放,并且程序一直在运行,那么之前动态开辟的内存的使用权一直没有还给操作系统,这样就会造成内存泄漏。
4、几个经典的笔试题
4.1 题目1
下面代码能打印出 hello world 吗?
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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();
return 0;
}
打印结果
- 代码结果没有打印出 hello world,为什么?
- GetMemory(str); 将实参指针str 传过去,用形参指针p 接收,这里用的是传值调用。在函数GetMemory函数中动态开辟内存,开辟内存空间的地址传给指针p。这里从始至终,指针str的值都是NULL,因为传值调用,函数的形参和实参分别占用两个不同的内存块,形参是实参的临时拷贝,对形参的操作不会影响实参。
- strcpy函数的两个参数都不能为空指针,strcpy函数的详情请参考文章《字符串函数和内存操作函数》的第2节。
上述代码修改
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
打印结果
- 这里是函数实参传递的是指针的地址,形参用二级指针p 接收,传址调用可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。对二级指针p解引用,也就找到了指针str,str直接指向动态开辟的内存。
4.2 题目2
下面代码能打印出 hello world 吗?
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
打印结果
- 函数中的局部变量一出函数就会销毁,虽然返回了局部变量的地址,能通过地址找到局部变量的位置,但是已经没有权限访问局部变量了。
下面代码错位原因与上相同
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int* test()
{
int a = 10;
return &a;
}
int main()
{
int* p = test();
printf("%d\n", *p);
return 0;
}
打印结果
- 这段代码依然是一个错误代码,并且错误原因也是局部变量在其他函数中没有访问权限,那么为什么这个打印出来了?
因为这个局部变量还没有被其他数据写入,如果在打印函数之前进行一些操作,局部变量就有可能被其他数据写入,进而打印不出来。如下,再多写一个打印函数: 第二个打印函数访问到的就不是10了。
4.3 题目3
下面代码运行结果是什么,有什么问题?
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}
打印结果
- 这段代码成功运行,且跟第一个题目的修改版相似,但是这个代码在使用完动态开辟的内存后没有释放。
正确写法如下
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
打印结果
- 动态开辟的空间在使用完后一定要释放。
4.4 题目4
下面代码有什么问题?
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}
- 打印结果为word,但是这段代码进行了非法访问,为什么?
- 动态开辟空间在使用完毕后,已经通过free函数将空间中存储的内容进行了释放,这块空间的使用权限还给了操作系统。
- 但是释放后,没有及时将指向这块空间的指针置为NULL指针。因此条件语句 if 成立,再次在 if 语句块中使用了动态开辟的空间,造成了非法访问。
|