在编写C代码的时候,我们时常会遇到数组越界的问题;时常会不确定需要多少内存而开辟过多或不足的内存空间导致内存浪费或数组长度不足的问题。
当然,首先我们需要时常关注自己编写的代码而防止数组越界。
之前曾在初识C语言部分里了解过变长数组,数组的长度可以使用一个变量(C99标准之后允许变长数组,但是变长数组不允许初始化):
int main()
{
int n = 10;
int arr[n];
return 0;
}
但是这个数组虽然在定义数组长度时可以使用变量,但是在变长数组定义之后空间已经开辟结束,不能改变空间的大小。当遇到开辟过多或开辟不足时,依旧不能调整。
所以C语言标准提供了一些动态内存函数用于实现动态的内存管理。
通过动态内存函数可以实现开辟一个动态的空间,并且可以通过动态内存函数实现内存的扩大或减小:
库函数malloc用于在内存中申请一段连续的空间。
我们可以查询到这个函数的声明:
malloc声明在头文件stdlib.h中。
它有一个参数,类型为size_t(无符号整型)。用于接收一个整型变量,表示在内存中申请一块大小为size字节的连续的空间。当然,开辟的空间不可能是一个负数,所以使用无符号整型作为参数类型会更安全。
返回值为void*,可以返回任意类型的地址 。当动态内存函数成功开辟内存时,返回首地址;当开辟失败时返回一个空指针。
由于库函数不能确定要开辟什么类型的指针,所以返回void*型的。我们在使用这个指针时,就需要先将其强制类型转换为需要的类型。
需要注意的是,当size为0时,函数的结果取决于环境。可能返回空指针,也可能什么都不干等。
例如:
我们可以利用malloc开辟一块存放int类型的空间,大小为40字节:
#include
#include
int main()
{
int* p = (int*)malloc(40);
if (p)
{
printf("开辟成功\n");
}
else
{
printf("开辟失败\n");
}
free(p);
return 0;
}
使用malloc函数开辟了一块40字节的连续的空间。将这块空间的首地址强转为int*型,并赋值给一个整型指针p。
当然,我们可以使用if语句来判断malloc函数是否成功开辟空间。
重要的一点是:动态分配的空间在使用之后必须要用库函数free释放(后面会介绍这个函数)。
我们在访问动态开辟的空间时,与数组类似:
#include
#include
int main()
{
int* p = (int*)malloc(40);
if (p)
{
printf("开辟成功\n");
}
else
{
printf("开辟失败\n");
}
int i = 0;
for (i = 0; i < 10; i++)
{
p[i] = i + 1;
}
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
free(p);
return 0;
}
首先malloc动态开辟了40字节的连续空间,再将返回值强制类型转换为int*型。
p[i]本质上就是*(p+i)。p是一个整型指针,p+i即向后移动i*4个字节。指向一个长度为int的空间。
所以,我们可以通过for循环,将这40个字节的每四个字节使用整型变量赋值。并通过for循环打印。
当然,对于动态开辟的空间也需要注意越界的问题。
calloc也是用于申请一块连续的空间。
我们可以查询到这个函数的声明:
calloc声明在头文件stdlib.h中。
它的参数有两个:
第一个是size_t(无符号整型)。用于接收要申请的空间的元素的个数;
第二个也是size_t。用于接收每个元素的大小。
表示向内存申请一块大小为num*size的空间。
返回值与malloc相同,都是void*,返回动态空间的首地址或空指针(NULL)。
同样的,当size为0时,函数的结果取决于环境。
例如:
#include
#include
int main()
{
int* p = (int*)calloc(10,sizeof(int));
if (p)
{
printf("开辟成功\n");
}
else
{
printf("开辟失败\n");
}
int i = 0;
for (i = 0; i < 10; i++)
{
p[i] = i + 1;
}
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
free(p);
return 0;
}
在这段代码中:
先使用calloc开辟了连续的一块大小为10*4的空间。有10个元素,每个元素都是int的大小即4字节。
然后if判断函数的返回值,是否成功开辟。
然后使用for循环将这个40字节的空间赋值并打印。
当然,需要注意以避免出现越界的问题。
realloc函数用于改变申请的动态空间的大小。
我们可以查询到这个函数的声明:
realloc声明在头文件stdlib.h中。
它有两个参数:
第一个参数类型是void*。表示要更改的动态内存空间的首地址;
第二个参数类型是size_t。表示要将这块空间改为size个字节。
返回值是void*。当更改成功时,返回更改后内存块的首地址;失败时返回NULL。
当原空间后有足够的空间用来扩容时,realloc会直接在原空间后申请空间;当后面空间不足时,realloc会重新开辟一块大小为size的空间,并将原空间中的数据拷贝过去。
由于这种不确定性,我们在使用realloc更改空间大小时,需要使用另外的指针变量来接收。即传入的指针变量与接收返回值的指针变量不能相同。
如果更改成功,realloc会释放原空间;
当然,当更改内存失败后,realloc不会将原空间释放。
当传入的指针为NULL时,realloc的作用就与malloc相同。可开辟一块大小为szie的空间。
当szie为0时:
C99标准前,realloc会释放原空间。相当于free的作用;
C99标准后,realloc的结果取决于环境。
例如:
#include
#include
int main()
{
int* p1 = (int*)realloc(NULL, 10 * sizeof(int));
if (p1)
{
printf("开辟成功\n");
}
else
{
printf("开辟失败\n");
}
int i = 0;
for (i = 0; i < 10; i++)
{
p1[i] = i + 1;
}
int* p2 = (int*)realloc(p1, 80);
if (p2)
{
printf("增容成功\n");
p1 = p2;
}
else
{
printf("增容失败\n");
}
for (i = 10; i < 20; i++)
{
p1[i] = i + 1;
}
for (i = 0; i < 20; i++)
{
printf("%d ", p1[i]);
}
free(p1);
return 0;
}
我们首先通过realloc申请了40字节的连续的空间,将返回值赋给整型指针p1,并if验证是否开辟成功。
再for循环给这40个字节赋值。
然后用realloc扩容,将扩容后的首地址赋值给一个整形指针p2。并if验证是否增容成功:若增容成功,将p2赋值给p1.
再for循环给剩下的40个字节赋值。并打印这80个字节的内容。
最后,free释放p1所指向的空间。
其实,malloc与calloc同样是申请动态内存,它们是有一定的区别的:
除了参数方面的区别,malloc申请到的空间不会被初始化;而calloc申请到的内存会被初始化为0。
我们可以通过调试观察它们的区别:
#include
#include
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p)
{
printf("开辟成功\n");
}
else
{
printf("开辟失败\n");
}
free(p);
return 0;
}
#include
#include
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (p)
{
printf("开辟成功\n");
}
else
{
printf("开辟失败\n");
}
free(p);
return 0;
}
并且,前面提到:
当realloc的原指针参数为NULL时,相当于malloc。它申请到的空间也不会初始化:
#include
#include
int main()
{
int* p = (int*)realloc(NULL, 10 * sizeof(int));
if (p)
{
printf("开辟成功\n");
}
else
{
printf("开辟失败\n");
}
free(p);
return 0;
}
库函数free用于释放动态开辟的空间。
free声明在头文件stdlib.h中。
它有一个参数,类型是void*。接收要释放的动态空间的首地址(由malloc、calloc、realloc开辟的空间)。
空返回值。
当传入的指针不是malloc、calloc、realloc开辟的动态空间时,free的结果未定义。
当传入空指针时free不干任何事。
需要注意的是:free虽然释放这个指针指向的空间,但是不会改变传入的指针变量的值。就是free后,虽然这个地址是无效的,但这块地址仍会被找到。此时再访问这个空间就会出现问题。
所以,为了避免这种情况,建议在free释放空间后,将这个指针置为NULL:
#include
#include
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p)
{
printf("开辟成功\n");
}
else
{
printf("开辟失败\n");
}
free(p);
p = NULL;
return 0;
}
这样在后面使用时就会避免出现一些问题。
上面已经提到了一些在动态内存分配时会遇到的问题。
接下来就来介绍一下常见的动态内存错误:
空指针是不能被解引用的。
我们使用动态开辟的空间时,是将动态内存函数的返回值解引用来访问。虽然一般会开辟成功,释放动态开辟内存的首地址,但是当开辟失败时就会返回空指针(NULL)。这时再去对空指针解引用就是有问题的。
这时,建议使用if判断一下是否成功开辟:
#include
#include
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p)
{
printf("开辟成功\n");
}
else
{
printf("开辟失败\n");
}
free(p);
p = NULL;
return 0;
}
动态开辟的空间也不是自动增容的,需要使用realloc进行空间大小的修改。
所以,在使用动态开辟空间的时候也需要注意越界访问的问题:
//错误示范
#include
#include
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p)
{
printf("开辟成功\n");
}
else
{
printf("开辟失败\n");
}
int i = 0;
for (i = 0; i < 10; i++)
{
p[i + 1] = i + 1;
}
free(p);
p = NULL;
return 0;
}
在这段代码中,访问到了*(p+10)指向的空间。这块空间是在动态开辟的空间之外的,越界访问当然会出现问题。
这一点在上面提到过:
free只能释放动态开辟的空间,当使用free去释放非动态开辟的空间时,就会出现一些不可预知的结果:
//错误示范
#include
int main()
{
int n = 0;
int* p = &n;
free(p);
return 0;
}
动态开辟的空间如果不释放,就会造成内存泄露。即内存使用之后不释放,该进程一直占用这部分空间,使这部分空间不能被使用。
所以,在动态开辟内存后必须确认这部分动态开辟的内存释放掉了。
当然,我们非动态的空间是不需要free释放的。因为这部分空间是开辟到栈区的,会随着生命周期的结束而自动释放这部分空间;而动态开辟的空间是开辟到堆区的,不会随着工程的结束而自动释放,所以需要手动释放(存储的区域马上就会介绍)。
在我们访问动态开辟的空间的某一个元素时,有时会将这块空间的首地址自增:
//错误示范
#include
#include
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p)
{
printf("开辟成功\n");
}
else
{
printf("开辟失败\n");
}
int i = 0;
while (i < 10)
{
*(p++) = i + 1;
i++;
}
free(p);
p = NULL;
return 0;
}
这时这个指针就不是动态开辟空间的首地址,free释放这个指针就没有释放全部的动态空间。也会导致内存泄露的问题。
所以我们可以尽量不对动态开辟空间的首地址自增。当然也可以在自增后恢复后再释放。
对动态空间free释放后,这块空间已经不属于此次进程了,再次释放就会出现问题:
//错误示范
#include
#include
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p)
{
printf("开辟成功\n");
}
else
{
printf("开辟失败\n");
}
free(p);
free(p);
return 0;
}
为避免这种情况,我们可以在free释放后,将该指针置为空指针(NULL)。如果出现了重复释放,第二次释放空指针就不会有问题。
#include
#include
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p)
{
printf("开辟成功\n");
}
else
{
printf("开辟失败\n");
}
free(p);
p = NULL;
free(p);
return 0;
}
前面提到,动态开辟的空间是存放在堆区的。我们就来简单了解一下内存的区域:
内存中大致分为这几个区域:内核空间、栈区、堆区、静态区、代码段。
当然,这里只做简单的了解,后面的数据结构会详细介绍。
最后,我们来了解一下柔性数组:
C99标准中,允许结构体中最后一个元素是未知大小的数组。这就叫做柔性数组成员:
struct A
{
int i;
int arr[0];
};
这样写也是可以的:
struct A
{
int i;
int arr[];
};
1、结构体中,柔性数组成员前必须存在至少一个其他成员;
2、sizeof计算结构体大小时,不计算柔性数组的大小:
#include
#include
struct A
{
int i;
int arr[0];
};
int main()
{
printf("%d\n", sizeof(struct A));
return 0;
}
3、包含柔性数组的结构体需要使用malloc进行动态内存开辟,并且开辟的大小需要大于结构体的大小,以适应柔性数组:
#include
#include
struct A
{
int i;
int arr[0];
};
int main()
{
printf("%d\n", sizeof(struct A));
struct A* p = (struct A*)malloc(sizeof(struct A) + 40);
if (p)
{
printf("开辟成功\n");
}
else
{
printf("开辟失败\n");
}
free(p);
p = NULL;
return 0;
}
此时,我们这个结构体中就有了一个成员int以及一个大小为40字节的柔性数组成员。
在使用时,与正常使用结构体与成员数组类似。用"."访问结构体成员、用[]访问数组即可:
#include
#include
struct A
{
int i;
int arr[0];
};
int main()
{
struct A* p = (struct A*)malloc(sizeof(struct A) + 40);
if (p)
{
printf("开辟成功\n");
}
else
{
printf("开辟失败\n");
}
struct A a = { 10, { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 } };
printf("%d\n", a.i);
int k = 0;
for (k = 0; k < 10; k++)
{
printf("%d ", a.arr[k]);
}
free(p);
p = NULL;
return 0;
}
柔性数组在结构体中只动态开辟一次,可以减少动态内存开辟的次数,而方便内存的释放。
并且由于动态开辟了一块连续的空间,有利于提高访问速度,减少内存碎片。
在这篇文章中,我们了解了动态内存管理的相关知识。包括动态内存函数malloc、calloc、realloc、free。通过这些函数可以实现在堆区上动态空间的开辟、修改与释放;
又了解了常见的动态内存管理时的错误;
最后了解了柔性数组的使用。
如果对本文有任何问题,欢迎在评论区进行讨论哦
最后,祝大家兔年大吉,万事如意,越来越能code!!!