000、简易的C语言变量视角空间申请图示
- 局部变量、函数形式参数在栈区申请内存空间
- 动态内存在堆区申请内存空间
- 静态变量、全局变量在静态区申请内存空间
001、为什么存在动态内存分配
(1)之前创建空间的方法
int a = 10;//在栈空间开辟4个字节
int arr[10];//在栈空间开辟4*10个字节
- 可以看到这样开辟的空间是无法修改大小的,数组也必须指定其长度,其所需内存在“编译时”分配
- 而有的时候需要在程序运行的时候才能知道所需空间大小,这样就需要动态分配内存了
(2)动态内存分配的相关函数
002、动态内存函数
(1)malloc和free
void* malloc (size_t size);
void free (void* ptr);
-
malloc函数的功能是先向内存申请一块连续可用的空间,并且返回指向这块空间的地址
- 如果开辟空间失败,就返回一个NULL,为了避免解引用空指针,所以一般都要对malloc的返回值做检查
- malloc的返回值是void*,malloc并不知道使用者需要开辟空间的类型,在使用malloc的时候由使用者决定指针类型,通常使用强制类型转换来转化
- 参数size的值为0,其结果是未知的(C标准没有对此进行定义),要看编译器的具体实现(另外这种需求也很奇怪:如果说要开辟空间,又想得到0个空间,这是很矛盾的)
- 注意size是以字节为单位申请空间的
-
free函数的功能是将某个指针指向的动态内存释放掉
- 通常释放掉某个指针指向的动态内存后,要将该指针置空,避免其成为空指针
//例子一
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
//1.申请动态内存
int* a = (int*)malloc(sizeof(int));
if(a == NULL)//对返回值指针进行检查
{
printf("申请失败\n");
printf("%s\n", strerror(errno));
exit(-1);//或者return 1;
}
//2.使用动态内存
*a = 5;
printf("在%p地址处存储了数值:%d\n", a, *a);
//3.释放动态内存
free(a);//释放动态内存
a = NULL;//置空,避免野指针的出现
return 0;
}
//例子二
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define NUMBER 10
int main()
{
//1.申请动态内存
int* arr = (int*)malloc(sizeof(int) * NUMBER);
if(arr == NULL)//对返回值指针进行检查
{
printf("申请失败\n");
printf("%s\n", strerror(errno));
exit(-1);//或者return 1;
}
//2.使用动态内存
for(int i = 0; i < NUMBER; i++)
{
arr[i] = i + 1;
}
for(int i = 0; i < NUMBER; i++)
{
printf("%d ", arr[i]);
}
//3.释放动态内存
free(arr);//释放动态内存
arr = NULL;//置空,避免野指针的出现
return 0;
}
(2)calloc
void* calloc (size_t num, size_t size);
- 函数的功能是为num个大小为size的元素开辟一块连续可用的空间,并且把空间的每个字节都初始化为0
- 与malloc的区别
- calloc会在返回地址之前,把申请空间的每个字节都初始化为全0
- 同时注意该函数参数的参数设计和malloc不太一样
- 若是calloc的参数num==1,则后面参数就和malloc是一样的
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define NUMBER 10
int main()
{
//1.申请动态内存
int* arr = (int*)calloc(NUMBER, sizeof(int));//注意参数设计有点不太一样
if (arr == NULL)//对返回值指针进行检查
{
printf("申请失败\n");
printf("%s\n", strerror(errno));
exit(-1);//或者return 1;
}
//2.使用动态内存
for (int i = 0; i < NUMBER; i++)
{
printf("%d ", arr[i]);
}
//3.释放动态内存
free(arr);//释放动态内存
arr = NULL;//置空,避免野指针的出现
return 0;
}
(3)realloc
void* realloc (void* ptr, size_t size);
- 该函数的功能是可以灵活调整“由malloc/calloc开辟的动态内存空间”的大小
- ptr是要调整的内存地址,size是调整之后新大小,返回值为调整之后的内存起始位置。
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
- realloc调整内存空间的时候存在两种情况:
- 情况1:原有空间后面若有足够大的空间,要扩展内存就直接在原有内存后追加空间,原有空间的数据不发生变化
- 情况2:原有空间后面若无足够大的空间,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的将会是一个新的内存地址
- 因此对realloc函数的使用,其返回值是需要极其注意的
- 如果realloc申请空间变小,则多余的空间就会自动还给操作系统
- 另外realloc是可以替代malloc函数的,即:如果第一个参数是一个空指针的话,就可以跟malloc一个效果
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define NUMBER 10
int main()
{
//1.申请动态内存
int* arr = (int*)malloc(NUMBER * sizeof(int));//注意参数设计有点不太一样
if (arr == NULL)//对返回值指针进行检查
{
printf("申请失败\n");
printf("%s\n", strerror(errno));
exit(-1);//或者return 1;
}
//2.使用动态内存
//①输入数据
for (int i = 0; i < NUMBER; i++)
{
arr[i] = i * i;
}
//②打印数据
for (int i = 0; i < NUMBER; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
//3.再次扩大动态内存
int* arr1 = realloc(arr, sizeof(int) * (NUMBER + 10));
if (arr1 == NULL)//对返回值指针进行检查
{
printf("申请失败\n");
printf("%s\n", strerror(errno));
exit(-1);//或者return 1;
}
//4.使用扩大后的动态内存
//①查看原数据是否还存在
for (int i = 0; i < NUMBER; i++)
{
printf("%d ", arr1[i]);
}
//②查看是否可以在新申请的空间赋予新的数据
for (int i = NUMBER; i < NUMBER + 10; i++)
{
arr1[i] = i * i;
}
printf("\n");
for (int i = 0; i < NUMBER + 10; i++)
{
printf("%d ", arr1[i]);
}
//5.释放动态内存
free(arr1);//释放动态内存
arr1 = NULL;//置空,避免野指针的出现
return 0;
}
//如果为了代码的变量统一性,也可以这么做
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define NUMBER 10
int main()
{
//1.申请动态内存
int* arr = (int*)malloc(NUMBER * sizeof(int));//注意参数设计有点不太一样
if (arr == NULL)//对返回值指针进行检查
{
printf("申请失败\n");
printf("%s\n", strerror(errno));
exit(-1);//或者return 1;
}
//2.使用动态内存
//①输入数据
for (int i = 0; i < NUMBER; i++)
{
arr[i] = i * i;
}
//②打印数据
for (int i = 0; i < NUMBER; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
//3.再次扩大动态内存
int* arr1 = realloc(arr, sizeof(int) * (NUMBER + 10));
if (arr1 == NULL)//对返回值指针进行检查
{
printf("申请失败\n");
printf("%s\n", strerror(errno));
exit(-1);//或者return 1;
}
else
{
arr = arr1;//这样后面的代码就会统一一些,这其实没有太大问题,尽管编译器还是有可能报警告
}
//4.使用扩大后的动态内存
for (int i = NUMBER; i < NUMBER + 10; i++)
{
arr[i] = i * i;
}
printf("\n");
for (int i = 0; i < NUMBER + 10; i++)
{
printf("%d ", arr[i]);
}
//5.释放动态内存
free(arr1);//释放动态内存
arr1 = NULL;//置空,避免野指针的出现
return 0;
}
003、常见的动态内存错误
(1)对NULL指针的解引用操作
#include <stdlib.h>
void test()
{
int *p = (int*)malloc(4);//申请4个字节
*p = 20;//如果malloc申请失败,则p的值是NULL,则这里就会出现对空指针的解引用
free(p);
}
int main()
{
test();
return 0;
}
(2)对动态开辟空间的越界访问
#include <stdlib.h>
void test()
{
int i = 0;
int *p = (int*)malloc(10 * sizeof(int));
if(NULL == p)
{
return 1;
}
for(i=0; i<=10; i++)
{
*(p+i) = i;//当i等于10的时候就发生越界访问
}
free(p);
}
int main()
{
test();
return 0;
}
(3)对非动态开辟内存使用free释放
void test()
{
int a = 10;
int *p = &a;
free(p);//释放了非动态内存
}
int main()
{
test();
}
(4)使用free释放一块动态内存的一部分
#include <stdlib.h>
void test()
{
int *p = (int*)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
int main()
{
test();
return 0;
}
(5)对同一块动态内存多次释放
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放了该指针,但是有一个解决方案就是将已经释放的指针置空,free(NULL)不会产生任何效果,这样即使多次释放也没关系
}
int main()
{
test();
return 0;
}
(6)动态开辟内存忘记释放(内存泄漏,在工程中属于比较严重的问题)
- 注意以下代码不能运行太久,不然有可能你的计算机会变“卡”,不过现在大多数系统都会对此做保护
#include <stdlib.h>
#include <windows.h>
int main()
{
while (1)
{
int* p = (int*)malloc(100);
Sleep(1000);
}
}
004、经典题目
(1)题目一:没有正确区分栈内存和堆内存
void GetMemory(char *p)
{
p = (char*)malloc(100);//这个p出了这个函数就再也找不到了,会一直内存泄漏,没有机会释放
}
void Test(void)
{
char *str = NULL;
GetMemory(str);//而且p没有被带回来,这里的str应该是传指针变量的地址,而不是传指针变量本身的值,因此strcpy会解引用空指针
strcpy(str, "hello world");
printf(str);
}
int main()
{
Test();
return 0;
}
//上述代码应该修改如下
#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;
}
(2)题目二:返回栈空间地址错误
char *GetMemory(void)
{
char p[] = "hello world";//局部变量除了出了该函数会被销毁,无法像动态内存一样被带回
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();//带回来了一个野指针
printf(str);
}
(3)题目三:忘记使用free
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
005、C/C++程序的内存开辟(开头的三区是以下的简化版,两者是包含关系)
(1) 使用柔性数组的方法
//code1
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
typedef struct limou
{
int n;
char c;
int arr[0];
}limou;
int main()
{
//申请结构体内存
limou* plimou = (limou*)malloc(sizeof(limou) + 5 * sizeof(int));
if (plimou == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//使用柔性数组的内存
for (int i = 0; i < 5; i++)
{
plimou->arr[i] = i;
printf("%d ", plimou->arr[i]);
}
printf("\n");
//调增柔性数组的大小
limou* plimou1 = realloc(plimou, sizeof(limou) + 10 * sizeof(int));
if (plimou1 == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
for (int i = 5; i < 10; i++)
{
plimou1->arr[i] = i;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", plimou1->arr[i]);
}
free(plimou1);
plimou1 = NULL;
return 0;
}
(2)柔性数组的另一种等价实现
柔性数组使得结构体的内存是可变的,当然,这种场景下,在C99之前还有一种解决方案
//code2
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
typedef struct limou
{
int n;
char c;
int* arr;
}limou;
int main()
{
limou* plimou = (limou*)malloc(sizeof(limou));
if (plimou == NULL)
{
perror("malloc1");
return 1;
}
int* ptr = (limou*)malloc(sizeof(int) * 10);
if (ptr == NULL)
{
perror("malloc2");
return 1;
}
else
{
plimou->arr = ptr;
}
//使用动态内存
for (int i = 0; i < 10; i++)
{
plimou->arr[i] = i * i;
printf("%d ", plimou->arr[i]);
}
//调整内存大小
ptr = realloc(plimou->arr, 20 * sizeof(int));
if (ptr == NULL)
{
perror("malloc3");
return 1;
}
else
{
plimou->arr = ptr;
}
printf("\n");
//使用调整后内存
for (int i = 0; i < 20; i++)
{
plimou->arr[i] = i * i;
printf("%d ", plimou->arr[i]);
}
//两次释放内存
free(plimou->arr);
plimou->arr = NULL;
free(plimou);
plimou = NULL;
return 0;
}
(3)对比两种实现的优缺点
- 出错概率:code2方法需要多次free,从次数上来讲,这种方法比较容易出错,code1柔性数组的实现更加简单
- 使用频率:只可惜现在code1中的柔性数组的普及力还不够大,往往较为少用
- 内存碎片:在code2中,多次malloc生成的空间大概率是不连续的,申请的越多,内存碎片就会增多,内存使用率就会降低,而code1就会提高内存使用效率
- 访问速度:由于是code1中申请的内存是连续的,可以提高访问效率(不是特别明显)