当前位置:网站首页>C语言动态内存开辟和柔性数组
C语言动态内存开辟和柔性数组
2022-07-17 04:31:00 【KissKernel】
文章目录
为什么存在动态内存开辟
因为平时我们创建的数组都是提前指定好大小,然后编译器会在编译的时候给数组分配空间,但是有时候我们的空间是需要在编译后程序运行起来的时候才知道。并且如果要内存大小可变的话,这时候就只有动态内存开辟可以满足了。
动态内存开辟函数
malloc和free
malloc以及其他的动态内存开辟函数都是在堆上开辟空间。
从malloc的原型可以看出来,参数就是要开辟的字节数,这里为什么返回空类型的指针呢?因为函数并不知道他在给什么类型的指针开辟空间,可能是int或者char或者自定义类型,所以malloc开辟出来的空间一定需要强制类型转换
再来看一下free函数,free函数就是用来释放动态开辟的内存的空间的。
int main()
{
int* p = (int*)malloc(sizeof(int) * 5);//int p[5]
int** pp = (int**)malloc(sizeof(int*) * 5);
for (int i = 0; i < 5; i++)
{
*(pp + i) = (int*)malloc(sizeof(int) * 6);
}
free(p);
for (int i = 0; i < 5; i++)
{
free(pp[i]);
pp[i] = NULL;
}
free(pp);
return 0;
}
上面这段代码就是一维数组和二维数组的开辟和释放,可以看到一维数组的申请和释放我们都是只需要一次释放就可以了,但是如果申请二维数组,这个二维数组实际上就是一个数组五个元素每个元素都是一个int*的类型指针,然后又用循环给每个指针开了一块内存空间,来模拟二维数组,但是这和我们直接创建出来的二维数组是不一样的。
直接创建出来的二维数组虽然用法上都是一样,但是他的内存是连续的。而malloc出来的因为不是一次malloc出来的空间所以他们的空间一定是不连续的。
malloc和free的特点
malloc如果开辟失败会返回空指针,成功则返回开辟好的空间的首地址
如果malloc参数size是0,这种行为是标准未定义的
malloc返回的是空类型的指针,需要进行强制类型转换
free
如果free的参数是空指针,那么就什么事情都不干
free之后的那个指针并不会变为空指针,而是还是指向那块空间,但是已经被释放了,所以这个指针就变成了野指针,需要我们在释放完内存之后手动置为NULL
不可以free不是动态开辟的内存
如果对一块开辟的内存释放两次,程序会崩溃。因为内存的释放只能进行一次,万一系统将这块内存又分配给别人的话,那么第二次释放肯定是不被允许的。
释放动态开辟的一部分空间也是会导致程序崩溃的。
calloc
calloc的作用也是在堆上开辟一块内存空间,但是会将开辟的空间全部初始化成0,这是calloc和malloc的区别。
int main()
{
int* p = (int*)calloc(5, sizeof(int));
for (int i = 0; i < 5; i++)
{
p[i] = i;
}
return 0;
}
这是calloc的用法,calloc有两个参数,第一个是要开辟的空间的个数,第二个是每个空间的大小,这里开辟的是一段5个int大小的空间。
realloc
realloc是调整动态开辟的内存的大小,可以大也可以小
函数原型为:
void* realloc (void* ptr, size_t size);
realloc有两个参数,第一个参数是要调整的内存空间的首地址,第二个参数是调整后的空间大小单位是字节。
如果第一个参数是NULL,那么这时候realloc的功能等价于malloc
realloc扩容有两种情况:
1,原来的内存空间后面的连续空间足够,那么直接原地扩容,返回的是原来的地址。
2,异地扩容,原来的内存空间后面的空间不够,会在一个新的地址处开辟好空间,然后将旧的数据拷贝到新空间,然后释放旧空间。
来看一段代码
int main()
{
int* p1 = (int*)malloc(40);
int* p2 = (int*)realloc(p1, 80);
free(p2);
//free(p1);
return 0;
}
这段代码是不需要free掉p1的,如果写了freep1那么程序就会崩溃,因为发生了对同一块内存释放了两次。首先如果是原地扩容,那么p1和p2内的地址是相同的,所以只需要释放一次。如果是异地扩容,那么原来的p1指向的旧地址已经被释放了,自然不需要我们再去释放。
动态内存常见错误
对NULL进行解引用
int *p = (int*)malloc(INT_MAX);
p[0] = 10;
因为malloc开辟失败的话会返回NULL,所以使用动态内存之前要进行判断。
对动态内存的越界访问
这个就像是数组越界访问一样,一旦越界就会导致程序崩溃,发生访问冲突。
对于不是动态开辟的内存free
int main()
{
int arr[20] = {
0 };
int* parr = arr;
//.....
free(parr);
return 0;
}
这段代码也会导致程序崩溃
使用free释放动态开辟内存的一部分
int main()
{
int* p = (int*)malloc(sizeof(int) * 10);
for (int i = 0; i < 5; i++)
{
*p = i;
p++;
}
free(p);
return 0;
}
这种也是导致程序崩溃,所以在使用的时候,尽量不要改变原来的指针的位置。
对同一块动态内存多次释放
这个就是多次释放崩溃,解决方案是释放后的指针直接置成NULL,因为释放过一次后如果不置NULL这个指针就是野指针了,显然置为NULL更安全。
内存泄漏
内存泄漏就是动态开辟的内存忘记释放了。有时候就算我们记得释放也会导致内存泄漏比如下面的代码
int test()
{
int n;
scanf("%d", &n);
int* p = (int*)malloc(sizeof(int) * 10);
if (n)
{
return 1;
}
free(p);
return 0;
}
这种类似逻辑的代码就算我们记得释放有时候也会造成内存泄漏。
所以防止内存泄漏,要注意谁申请谁释放。释放完之后指针置NULL
经典习题
void GetMemory(char *p) {
p = (char *)malloc(100);
}
void Test(void) {
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
这段代码运行起来是会崩溃的,并且还有内存泄露问题,因为这里的传参是传值调用,里面指针的改变并不会影响外面的str,所以str还是空指针,对空指针进行拷贝这里就会崩溃。所以这里要改造的话要记得传str的地址给p并且在外面要记得释放开辟的内存空间。
char *GetMemory(void) {
char p[] = "hello world";
return p; }
void Test(void) {
char *str = NULL;
str = GetMemory();
printf(str);
}
这段代码可以打印出来东西吗?
答案是:可以打印出来,但是是乱码,因为在这GetMemory返回的是栈空间的地址,要注意,局部变量在栈上开辟空间,但是随着函数的结束,栈帧销毁,栈上面的数据也就不存在了,所以这里的str就成了野指针。对野指针进行访问,打印出来的就是随机值啦。
这里访问野指针为什么没有程序崩溃呢?因为这里编译器对于越界的检查其实并不是完全严格的,并不是只要越界一定能检查出来,编译器只是在你可能越界的地方进行检查。也就是抽查,如果全部都检查的话,那对于性能消耗也是很大的。
所以一般情况下,越界读一般不会报错(若是内核空间地址一定报错),越界写一般都是会报错的。
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void) {
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
这里的唯一问题就是没有释放内存,这里的打印的可以正常打印出来的。就是第一段代码的优化版本。
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
这里是可以打印出来字符串的但是,这里实际str已经变成了野指针了。因为释放了str指向的内存空间之后,没有将str置为空指针,所以下面对于str的访问实际就是对于野指针的访问。所以这里我们释放完内存之后一定要将指针置为空指针,防止出现野指针。
关于C语言内存分段

这里的内存分为栈区,主要是函数开辟的栈帧,以及局部变量,存放的位置,是由系统自动开辟自动释放的。
堆区这里是管理动态内存开辟的,里面的内存需要我们自己开辟自己释放。数据段,分为静态区存放静态变量。已初始化全局数据区和未初始化全局数据区。代码段,用来存放只读的常量,比如字符串等。还有代码转换成的一条条二进制机器码的指令。
柔性数组
柔性数组这个概念是C99中提出来的,就是结构体的最后一个变量可以是一个未知大小的数组,这个数组的大小可以通过malloc和realloc来调整,这个就叫做柔性数组成员。
struct test
{
int n;
int arr[0];//或者是int arr[]
};
这个就是柔性数组的基本形式。
那么这时候如果我们sizeof(test)结果是什么呢?
答案是:4,因为柔性数组是不会计算在结构体的大小内的。因此包含柔性数组的结构体计算大小的时候不需要考虑柔性数组的大小。
柔性数组的使用
struct test
{
int n;
int arr[0];
};
int main()
{
struct test* t = (struct test*)malloc(sizeof(struct test) + sizeof(int) * 10);
for (int i = 0; i < 10; i++)
{
t->arr[i] = 1;
}
struct test* tmp = (struct test*)realloc(t, sizeof(struct test) + sizeof(int) * 20);
if (tmp == NULL)
{
perror("realloc:");
return 1;
}
t = tmp;
for (int i = 0; i < 20; i++)
{
t->arr[i] = 1;
}
for (int i = 0; i < 20; i++)
printf("%d ", t->arr[i]);
free(t);
t = NULL;
return 0;
}
这段代码要注意给柔性数组开辟大小的时候,用malloc,算出来不包含柔性数组时候的大小+柔性数组要开辟的大小,最后上面代码给柔性数组开辟的大小是10个int的大小。
当然柔性数组也可以用realloc调整大小。比如上面代码使用realloc调整大小,将柔性数组调整到了20个int类型的大小。
上面的perror呢就是在发生错误的时候打印错误信息,里面的字符串会被加载打印信息的前面并且自动加上冒号
就像这样,因为我加了一个:,所以这里出现了两个冒号
柔性数组的优点
这样有的人会出现疑问,比如这里我可以用一个int*代替柔性数组,下面代码就是这种情况,为了统一,因为柔性数组都是在堆上,所以下面的结构体我也会在都在堆上malloc
struct test
{
int n;
int* arr;
};
int main()
{
struct test* t = (struct test*)malloc(sizeof(struct test));
t->arr = (int*)malloc(sizeof(int) * 10);
for (int i = 0; i < 10; i++)
{
t->arr[i] = 1;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", t->arr[i]);
}
free(t->arr);
free(t);
return 0;
}
上面这段代码就是用int* 实现的和柔性数组一样的功能,首先第一个问题就是这里出现了两次malloc,我们知道malloc的次数越多,内存中的内存碎片就越多,那么内存的使用效率就下降了。并且这里因为malloc了两次那么释放的时候,也是需要释放两次的。并且这里我们释放的顺序不可以更改,一旦先释放了t那么就找不到arr了,就造成了内存泄漏。
柔性数组的特点
1,柔性数组成员前面至少包含一个其他类型的成员
2,一个结构中只能有一个柔性数组成员
3,sizeof计算结构体大小的时候不包含柔性数组的大小。
4,包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大
小,以适应柔性数组的预期大小。
所以综上,我们可以的出来柔性数组的优点
1,方便释放,使用柔性数组只需要一次是释放就可以了,但是不使用柔性数组我们就需要按顺序释放,很容易就造成了内存泄露。
(如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给
用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你
不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好
了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。)
2,有利于提高访问速度(连续的内存可以提高访问速度,减少内存碎片)
边栏推荐
- Machine learning 11: cost sensitive learning
- 北斗时钟服务器(NTP服务器)让高考时间更加精准
- Codeforces Round #807 (Div 2.) AB
- Data types of basic knowledge of C language
- Brief introduction to cmtime
- Analysis of network attack detection technology for NDN
- Common errors in golang compilation
- MySql 一行变多行(根据特定符号分割)
- word使用技巧
- Machine learning 09: unsupervised learning
猜你喜欢

使用kaggle跑李宏毅机器学习作业

Delete the file unable to find or create trash directory

PowerDesigner显示Comment注释

Install go:tools failed to install in vscode

基于JIRA7.9.2定制修改

donet framework4. X==windows form application new project, through system Data. SqlClient connects to sqlserver to query

Touchid and faceid~2

Overview of Baidu map technology, and application development of basic API and webapi

Xcode11 add a boot page (the launch images source option is missing after the upgrade)

2022/7/16 周赛
随机推荐
OSPF基础优化
minio安装部署及使用
C# List 集合对象去重 Linq去重 带时间去重
Autojs learning - map finding data generation
surging作者出具压测结果
The author of surging issued the pressure test results
Eas (energy aware scheduling) green energy-saving scheduler
Codeforces Round #807 (Div. 2) A~D
Install go:tools failed to install in vscode
HCR慧辰北坡而行,一只游入数字营销服务的巨兽
[Unity] Input.gettouch[index]的index
百度地图技术概述,及基本API与WebApi的应用开发
CAD视频课程推荐 b站
Vs Code common shortcut keys
JS modal box
MySQL中的删除:delete、drop、Truncate三者的区别
Typeorm MySQL upsert operation
RK1126实现画中画功能 picture in picture for RK 1126
Delete the file unable to find or create trash directory
Golang reverse slice code example



