C语言之动态内存分配与释放
本文首先介绍了通用指针类型void的特点,接着给出了在堆上动态分配内存空间主要依赖的三个函数(malloc、calloc和realloc)和内存释放函数free的使用方法和注意事项。
通用指针类型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>当中:
malloccallocrealloc
内存分配函数malloc
函数全名:memory allocation
函数声明:void* malloc(size_t size);
函数作用:
- 此函数会在堆空间上分配一片连续的,size个字节大小的内存块。
- 此函数不会对内存块中的数据进行初始化,内存块中的数据是随机未定义的。
函数返回值:
- 如果分配成功,此函数会返回指向该内存块地址(首字节地址)的指针。注意返回的指针类型是void指针,在操作之前需要进行转换。
- 如果分配失败,此函数会返回一个空指针(NULL)。
内存泄漏
以往我们创建数组,创建结构体都是在栈上完成的,它们是栈数组、栈结构体,它们的内存空间都是由栈自动管理完成的。
但使用动态内存分配函数创建的数组、结构体都被存储在堆上,栈上存储的只不过是它的指针,如下图所示:
堆上存储的数据由程序员手动管理生命周期,手动申请内存资源,也需要手动释放内存空间。
内存泄漏是指程序在运行过程中,未能适时释放不再使用的内存区域,导致这部分内存在程序的生命周期内始终无法被重用。
内存泄漏在短时间内可能对程序而言,不是巨大的、致命的风险,但:长时间运行或频繁执行的程序中如果存在内存泄漏,随着时间的推移,被泄漏的内存累积会越来越多,最终可能导致程序运行缓慢甚至崩溃,特别是在内存有限的系统中。
内存释放函数free
为了避免内存泄漏,在确定动态分配的内存不再使用后,要及时调用free函数释放它。
函数声明:void free(void *ptr);
函数参数:必须是堆上申请内存块的地址(首字节地址),不能传递别的指针,否则会引发未定义行为。
函数功能:
- free函数并不会修改它所释放的内存区域中存储的任何数据。free 的作用仅仅是告诉操作系统这块内存不再被使用了,可以将其标记为可用状态,以供将来的内存分配请求使用。
- 释放后的内存区域中的数据一般仍然会继续存在,直到被下一次的内存分配操作覆盖。当然即便free前的原始数据一直存在未被覆盖,这片内存区域也不再可用了,因为你不知道什么时候数据就会被覆盖掉了。
- free函数不会修改传入指针指向的内容,更不会对实参指针本身做任何修改。
free函数调用后,指针指向的内存块就被释放了。但free函数不会改变传入的实参指针本身,所以free后的实参指针就变成了指向一片已释放区域的指针。这就是"悬空指针",悬空指针是野指针的一种特例,使用悬空指针同样会引发未定义行为。
为了避免悬空指针为程序安全带来隐患,推荐在free掉指针指向的内存块后,及时将指针置为空指针。
// 伪代码,p是一个指针类型
p = malloc(...);
free(p);
p = NULL;
总结:
正确传参free函数。free函数需要传入指向动态分配内存块首字节的指针,free之前不妨检查指针是否已被移动。
在free内存块后,建议立刻将指针设置为NULL。这样可以规避一些常见的问题:
- 避免了"double free"的风险。对空指针调用
free函数是安全的,它不会有任何效果。 - 减少悬空指针出现的风险。解引用空指针导致程序崩溃,比悬空指针带来的未定义行为要更容易检测和修正。
- 避免了"double free"的风险。对空指针调用
慎重改变堆区指针的指向。指向堆区域的指针,如果需要改变它的指向,在改变之前应当考虑指向的内存块是否需要free。
多函数共同管理同一块内存区域时,应严格遵循单一原则。尤其是,哪个函数用于分配内存,哪个函数用于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);。
函数参数:
num表示要分配的元素数量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);
函数参数:
ptr:指向原来已分配内存的内存块。new_size:新的内存块大小。
函数功能:
该函数根据参数取值的不同,可能表现为malloc或free函数的行为:
- 如果ptr指针是一个空指针,那么该函数的行为和malloc一致——分配new_size字节的内存空间,并且返回该内存块的指针。
- 如果new_size的取值为0,那么该函数的行为就是free函数,会释放ptr指向的内存块。
如果没有出现上述两种特殊情况,realloc用于重新调整已分配内存块的大小(也就是ptr指针指向的已分配内存块的大小):
- 当new_size的取值和已分配的内存块大小一致时,此函数不会做任何操作。
- 当new_size的取值比已分配的内存块小时,会在旧内存块的尾部(高地址)截断,被截断抛弃的内存块会被自动释放。
- 当new_size的取值比已分配的内存块大时(新内存块比旧内存块大时),会尽可能地尝试原地扩大旧内存块(这样效率高);
- 如果无法原地进行扩大,则会在别处申请空间分配new_size大小的新内存块,并将旧内存块中的数据全部复制进去后,将旧内存块自动释放。
- 不管采用哪种方式扩展旧内存块,新扩展部分的内存区域都不会初始化,仍只具有随机值。
函数返回值:
如果realloc函数分配内存空间成功,它会返回指向新内存块的指针,若失败,仍会返回空指针,且不会改变旧内存块。
总之,realloc函数适用于调整已分配内存块的大小,特别是在动态数组或数据结构的大小需要在程序运行时增加或减少时使用。
惯用法:
// p和arr_p指针类型一致
p = realloc(arr_p, new_size);
if (p == NULL){
// 分配失败处理
// 退出
}
// 代码运行到这里,p != NULL,realloc分配内存成功
arr_p = p;
这样写既避免了arr_p成为悬空指针,也不会因为realloc分配失败导致内存泄漏。
C语言之动态内存分配与释放的更多相关文章
- C语言中的内存分配与释放
C语言中的内存分配与释放 对C语言一直都是抱着学习的态度,很多都不懂,今天突然被问道C语言的内存分配问题,说了一些自己知道的,但感觉回答的并不完善,所以才有这篇笔记,总结一下C语言中内存分配的主要内容 ...
- C语言中动态内存分配的本质是什么?
摘要:C语言中比较重要的就是指针,它可以用来链表操作,谈到链表,很多时候为此分配内存采用动态分配而不是静态分配. 本文分享自华为云社区<[云驻共创]C语言中动态内存分配的本质>,作者: G ...
- 数据结构基础(1)--数组C语言实现--动态内存分配
数据结构基础(1)--数组C语言实现--动态内存分配 基本思想:数组是最常用的数据结构,在内存中连续存储,可以静态初始化(int a[2]={1,2}),可以动态初始化 malloc(). 难点就是数 ...
- C++语言之动态内存分配
在C语言中,我们熟悉的内存分配与释放的最常用的接口分别是malloc , free .在C++中: 存在着更加方便的动态存储分配: 1.new 和delete 机制,new 它能更可靠控制存储区的分配 ...
- 重拾c语言之动态内存分配
动态内存分配 传统数组的缺点: 1数组长度必须事先制定,且仅仅能是长整数不能是变量 2传统形式定义的数组该数组的内存程序无法手动释放 3数组一旦定义,系统就会为该数组分配的存储空间就会一直存在直到该函 ...
- C语言学习--动态内存分配(未完待续)
内存分配的类型: 在C/C++中内存分为5个区,分别为栈区.堆区.全局/静态存储区.常量存储区.代码区. 静态内存分配:编译时分配.包括:全局.静态全局.静态局部三种变量. 动态内存分配:运行时分配. ...
- C语言学习笔记--动态内存分配
1. 动态内存分配的意义 (1)C 语言中的一切操作都是基于内存的. (2)变量和数组都是内存的别名. ①内存分配由编译器在编译期间决定 ②定义数组的时候必须指定数组长度 ③数组长度是在编译期就必须确 ...
- C和指针 第十一章 动态内存分配
声明数组时,必须指定数组长度,才可以编译,但是如果需要在运行时,指定数组的长度的话,那么就需要动态的分配内存. C函数库stdlib.h提供了两个函数,malloc和free,分别用于执行动态内存分配 ...
- 《C和指针》 读书笔记 -- 第11章 动态内存分配
1.C函数库提供了两个函数,malloc和free,分别用于执行动态内存分配和释放,这些函数维护一个可用内存池. void *malloc(size_t size);//返回指向分配的内存块起始位置的 ...
- C动态内存分配(C与指针实例)
主要初步介绍malloc.free.calloc.realloc的基本.日后会有更详细的内容. malloc.free分别用于动态内存分配和释放. malloc会从内存池里提取一块合适的内存(连续的) ...
随机推荐
- C# WinForm NumericUpDown 控件全选其中文字 (Numeric 全选文本) 全选文本Numeric
num_length.Focus(); UpDownBase updbText = (UpDownBase)num_length; ...
- 想让自己的Ubuntu更漂亮嘛?
近期没啥大项目在进行,今天闲来无事,值班空闲时间,忽然发现自己的Ubuntu竟是如此的"丑"(虽然用过这么多Linux发行版,但不得不承认Ubuntu唯一的优点就是好看!),于是准 ...
- 剑指offer 22 链表中倒数第K个节点.
简介 链表中倒数第K个节点. 思路 双指针, 然后一个指针延迟运行. code class Solution { public: ListNode* getKthFromEnd(ListNode* h ...
- CGI 简单的python显示的页面
简介 python 进行服务器的页面的显示 cgi common gateway interface 公用网关接口 简单操作 python3 -m http.server --cgi 8001 新建一 ...
- OceanBase数据库结合ETLCloud快速实现数据集成
一.背景 随着信息技术的迅猛发展和数据量的急剧增加,企业面临着前所未有的数据管理挑战.传统的数据库系统在处理大规模.多样化的数据时往往显得力不从心.因此,分布式数据库应运而生,以其优越的性能和扩展性逐 ...
- iPaaS中API接口管理平台的作用
在iPaaS中,API接口管理平台具有关键作用,主要起到集成和连接不同系统之间的API,支持API文档和元数据管理,从代码注解扫描生成API.Swagger导入API.API自动识别.手工注册等多种方 ...
- 从ESB总线到iPaaS集成平台,如何选择最佳集成方案
随着信息化发展不断深入,企业在不同的阶段引入了不同的应用.系统和软件.这些原始的应用系统互不连通,如同一个个独立的岛屿.但是企业业务是流程化的,这就需要业务数据如流水般在不同岛屿间流转.在过去20年前 ...
- SciTech-Mathmatics-ProbabilitiesAndStatistics-Distribution-is-all-you-need: 概率统计到深度学习
Distribution-is-all-you-need 概率统计到深度学习,四大技术路线图谱,都在这里! https://github.com/graykode/distribution-is-al ...
- 如何入门并深入学习Linux-九五小庞
作者:程序员良许链接:https://www.zhihu.com/question/23564190/answer/757891495来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载 ...
- win11系统把扫描仪快捷方式到桌面的问题
说到扫描仪已经成为了很多雨林木风官网用户在日常工作生活中不可或缺的一个工具.扫描仪都能帮助我们快速.高效地将纸质文档转化为数字格式.然而,有一位Windows 11系统用户,却发现在桌面上没有扫描仪的 ...