首页 > 系统相关 >C语言之动态内存分配与释放

C语言之动态内存分配与释放

时间:2024-09-06 12:37:17浏览次数:11  
标签:释放 函数 void free C语言 内存 动态内存 size 指针

C语言之动态内存分配与释放

通用指针类型void

通用类型指针具有以下特点:

类型无关,赋值灵活:由于指针本质上是一个存储内存地址的变量,而内存地址是没有类型的,所以void指针可以存储任意类型数据的地址,指向任意类型对象。无论是整数、浮点数、字符或数组、结构体等类型都可以用void指针指向。表现在代码中就是:可以将任意类型指针(地址)赋值给void指针,这个过程一般是没有风险的。如下列代码:

int a = 42;
void* void_ptr = &a;

// void指针转换成其它类型指针,在C++语法中必须加上显式类型转换说明
// 但C语言支持void指针隐式类型转换成各种其它指针类型,所以这个强转语法可加可不加
// float* float_ptr = (float*)void_ptr;

// 错误的类型转换
float* float_ptr = void_ptr;

// 解引用产生未定义行为
printf("%f\n", *float_ptr);

关于通用指针类型转换成其它指针类型,C语言和C++在语法上会有明显差别:

  • C语言更灵活允许隐式的类型转换,所以强转语法可加可不加。

  • C++则不支持这类隐式类型转换,必须要加上强转的语法,否则编译无法通过。


在C语言中,想要在堆上动态分配内存空间,主要依赖三个函数来完成,它们都声明在头文件<stdlib.h>当中:

  1. malloc
  2. calloc
  3. realloc

内存分配函数malloc

函数全名:memory allocation

函数声明:void* malloc(size_t size);

函数作用:

  • 此函数会在堆空间上分配一片连续的,size个字节大小的内存块

  • 此函数不会对内存块中的数据进行初始化,内存块中的数据是随机未定义的。

函数返回值:

  • 如果分配成功,此函数会返回指向该内存块地址(首字节地址)的指针。注意返回的指针类型是void指针,在操作之前需要进行转换。

  • 如果分配失败,此函数会返回一个空指针(NULL)。

内存泄漏

以往我们创建数组,创建结构体都是在栈上完成的,它们是栈数组、栈结构体,它们的内存空间都是由栈自动管理完成的。

但使用动态内存分配函数创建的数组、结构体都被存储在堆上,栈上存储的只不过是它的指针,如下图所示:

栈上指针指向堆上内存区域-示意图

堆上存储的数据由程序员手动管理生命周期,手动申请内存资源,也需要手动释放内存空间。

内存泄漏是指程序在运行过程中,未能适时释放不再使用的内存区域,导致这部分内存在程序的生命周期内始终无法被重用。

内存泄漏在短时间内可能对程序而言,不是巨大的、致命的风险,但:长时间运行或频繁执行的程序中如果存在内存泄漏,随着时间的推移,被泄漏的内存累积会越来越多,最终可能导致程序运行缓慢甚至崩溃,特别是在内存有限的系统中。

内存释放函数free

为了避免内存泄漏,在确定动态分配的内存不再使用后,要及时调用free函数释放它。

函数声明:void free(void *ptr);

函数参数:必须是堆上申请内存块的地址(首字节地址),不能传递别的指针,否则会引发未定义行为。

函数功能:

  1. free函数并不会修改它所释放的内存区域中存储的任何数据。free 的作用仅仅是告诉操作系统这块内存不再被使用了,可以将其标记为可用状态,以供将来的内存分配请求使用。
  2. 释放后的内存区域中的数据一般仍然会继续存在,直到被下一次的内存分配操作覆盖。当然即便free前的原始数据一直存在未被覆盖,这片内存区域也不再可用了,因为你不知道什么时候数据就会被覆盖掉了。
  3. free函数不会修改传入指针指向的内容,更不会对实参指针本身做任何修改。

free函数调用后,指针指向的内存块就被释放了。但free函数不会改变传入的实参指针本身,所以free后的实参指针就变成了指向一片已释放区域的指针。这就是"悬空指针",悬空指针是野指针的一种特例,使用悬空指针同样会引发未定义行为。

为了避免悬空指针为程序安全带来隐患,推荐在free掉指针指向的内存块后,及时将指针置为空指针。

// 伪代码,p是一个指针类型
p = malloc(...);
free(p);
p = NULL;

总结:

  1. 正确传参free函数。free函数需要传入指向动态分配内存块首字节的指针,free之前不妨检查指针是否已被移动。

  2. 在free内存块后,建议立刻将指针设置为NULL。这样可以规避一些常见的问题:

    • 避免了"double free"的风险。对空指针调用free函数是安全的,它不会有任何效果。

    • 减少悬空指针出现的风险。解引用空指针导致程序崩溃,比悬空指针带来的未定义行为要更容易检测和修正。

  3. 慎重改变堆区指针的指向。指向堆区域的指针,如果需要改变它的指向,在改变之前应当考虑指向的内存块是否需要free。

  4. 多函数共同管理同一块内存区域时,应严格遵循单一原则。尤其是,哪个函数用于分配内存,哪个函数用于free释放内存,这两个函数一定要明确单一的职责。

// 在堆上动态分配内存拼接两个字符串
char* dynamic_strcat(const char* prefix, const char* suffix) {
// 计算拼接后字符串的长度
 int new_str_len = strlen(prefix) + strlen(suffix);
char *new_str = malloc(new_str_len + 1);    // char在各平台上长度都是1,所以不用乘了

 if (new_str == NULL) {
     printf("ERROR: malloc failed in dynamic_strcat!\n");
    exit(1);
 }
// 长度是精确计算得出的,不用担心越界访问
 strcat(strcpy(new_str, prefix), suffix);
return new_str;
}

int main(void) {
 char str1[] = "hello";
char str2[] = " world!";
 char* result_str = dynamic_strcat(str1, str2);  // 注意只要涉及动态内存分配,一律用指针类型。这里不能用数组类型
puts(result_str);
 // 现在不再使用result字符串了,不要忘记free它
free(result_str);
 return 0;
}

清零内存分配函数calloc

函数全名:cleared allocation,该函数的最大特点是分配内存空间时会自动初始化0值

函数声明: void* calloc(size_t num, size_t size);

函数参数:

  1. num 表示要分配的元素数量
  2. size 表示每个元素的内存大小

此函数也会在堆空间上分配一片连续的内存空间,但不同的是,它基于元素的个数以及每个元素的大小来进行内存分配,所以calloc常用于在堆上分配数组的内存空间。

函数返回值:返回值在分配成功和失败时,和malloc是一致的。

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>

int main(void) {
 // 分配一个长度为10的整数数组
 int len = 10;
 int *arr = calloc(len, sizeof(int));

 int* p = arr;
 for (int i = 0; i < len; i++){
     printf("%d\n", *p++);   // 此时数组中的元素都具有0值,而不是随机未定义的
 }

 // 使用完毕,不要忘记free
 free(arr);
 return 0;
}

总得来说,推荐在动态分配数组内存空间时,尤其是需要将内存空间初始化为0值时,使用calloc函数

内存重分配函数realloc

函数全名:reallocation,表示内存重新分配。

函数声明:void* realloc(void* ptr, size_t new_size);

函数参数:

  1. ptr:指向原来已分配内存的内存块。
  2. new_size:新的内存块大小。

函数功能:

该函数根据参数取值的不同,可能表现为malloc或free函数的行为:

  1. 如果ptr指针是一个空指针,那么该函数的行为和malloc一致——分配new_size字节的内存空间,并且返回该内存块的指针。
  2. 如果new_size的取值为0,那么该函数的行为就是free函数,会释放ptr指向的内存块。

如果没有出现上述两种特殊情况,realloc用于重新调整已分配内存块的大小(也就是ptr指针指向的已分配内存块的大小):

  1. 当new_size的取值和已分配的内存块大小一致时,此函数不会做任何操作。
  2. 当new_size的取值比已分配的内存块小时,会在旧内存块的尾部(高地址)截断,被截断抛弃的内存块会被自动释放。
  3. 当new_size的取值比已分配的内存块大时(新内存块比旧内存块大时),会尽可能地尝试原地扩大旧内存块(这样效率高);
  4. 如果无法原地进行扩大,则会在别处申请空间分配new_size大小的新内存块,并将旧内存块中的数据全部复制进去后,将旧内存块自动释放
  5. 不管采用哪种方式扩展旧内存块,新扩展部分的内存区域都不会初始化,仍只具有随机值

函数返回值:

如果realloc函数分配内存空间成功,它会返回指向新内存块的指针,若失败,仍会返回空指针,且不会改变旧内存块。

总之,realloc函数适用于调整已分配内存块的大小,特别是在动态数组或数据结构的大小需要在程序运行时增加或减少时使用。

惯用法:

// p和arr_p指针类型一致
p = realloc(arr_p, new_size);
if (p == NULL){
   // 分配失败处理
}
// 代码运行到这里,realloc分配内存成功
arr_p = p;

这样写既避免了arr_p成为悬空指针,也不会因为realloc分配失败导致内存泄漏。

标签:释放,函数,void,free,C语言,内存,动态内存,size,指针
From: https://www.cnblogs.com/Invinc-Z/p/18400002

相关文章

  • 新手c语言讲解及题目分享(十九)--数据类型专项练习
    本文主要讲解c语言的基础部分,常见的c语言基础数据类型,这个也非常重要。参考书目和推荐学习书目:通过网盘分享的文件:C语言程序设计电子教材(1).pdf链接:https://pan.baidu.com/s/1JFqSaCKZ0A2Lr944e72NUA?pwd=p648提取码:p648目录前言一.常量与变量1.常量2.变量二.......
  • 新手c语言讲解及题目分享(十八)--基本输入输出函数专项练习
    本文主要讲解c语言的基础部分,基本的输入与输出,通过手动的输入从而得到自己想要的预期值。参考书目和推荐学习书目:通过网盘分享的文件:C语言程序设计电子教材(1).pdf链接:https://pan.baidu.com/s/1JFqSaCKZ0A2Lr944e72NUA?pwd=p648提取码:p648目录前言一.格式输出......
  • C语言 10 数组
    简单来说,数组就是存放数据的一个组,所有的数据都统一存放在这一个组中,一个数组可以同时存放多个数据。一维数组比如现在想保存12个月的天数,那么只需要创建一个int类型的数组就可以了,它可以保存很多个int类型的数据,这些保存在数组中的数据,称为元素://12个月的数据全部保存......
  • C语言习题--程序改错
     1.待修改代码#include<stdio.h>#include<stdlib.h>#include<string.h>intmain(){ char*src="hello,world"; char*dest=NULL; intlen=strlen(src); dest=(char*)malloc(len); ch......
  • C语言猜数小游戏
    问题:用C语言写一个猜数小游戏,要求数字是整数小于1000且随即生成,玩家需要输入数字,程序给出提示,直至最终猜到最终正确的数字,游戏结束。小游戏实现代码如下:#include<stdio.h>#include<stdlib.h>//lib头文件调用随机函数#include<time.h>//time头文件调用时间......
  • 快速排序(动图详解)(C语言数据结构)
    快速排序:        快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:        任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左......
  • 新手c语言讲解及题目分享(十七)--运算符与表达式专项练习
    本文主要讲解c语言的基础部分,运算符与表达式的学习,在这一部分中,往往有许多细节的东西需要去记住。当各种运算符一起用时,就会存在优先级的关系,本文末尾有各种运算符的优先级顺序表。参考书目和推荐学习书目:通过网盘分享的文件:C语言程序设计电子教材(1).pdf链接:https://pa......
  • 新手c语言讲解及题目分享(十六)--文件系统专项练习
    在我刚开始学习c语言的时候就跳过了这一章节,但在后面慢慢发现这一章节还是比较重要的,如果你报考了计算机二级c语言的话,你应该可以看到后面的三个大题有时会涉及到这章。所以说这章还是非常重要的。目录前言一.打开文件1.Fopen()函数返回值2.文件打开方式二.关闭文件......
  • 新手c语言讲解及题目分享(十五)--结构体专项练习
    目录前言一.结构体1.结构体一般形式:2.定义结构体变量:Ⅰ.先声明结构体类型,再定义变量:Ⅱ.在声明结构体类型的同时定义变量:Ⅲ.不包含结构体类型名,直接定义结构体类型变量:3.引用结构体变量:4.定义结构体数组:Ⅰ.先定义结构体类型,后定义结构体数组:Ⅱ.在定义结构体类型的同......
  • 【时时三省】(C语言基础)指针进阶 例题
    山不在高,有仙则名。水不在深,有龙则灵。            ----CSDN时时三省字符数组例题: arr后面放了六个字符所以这个数组的元素个数就是6第一个arr因为他计算的是一整个数组的大小就是打印6第二个arr+0arr没有单独放在它的内部所以它计算的就......