侧边栏壁纸
博主头像
张种恩的技术小栈博主等级

行动起来,活在当下

  • 累计撰写 748 篇文章
  • 累计创建 65 个标签
  • 累计收到 39 条评论

目 录CONTENT

文章目录

C语言学习小记(11)-动态内存分配

zze
zze
2024-03-21 / 0 评论 / 0 点赞 / 42 阅读 / 20918 字

概述

动态内存分配是编程语言中一种在程序运行时动态请求和释放内存的机制。程序员可以在运行时根据实际需求决定分配多少内存,而且可以在不再需要内存时将其释放回系统。在C/C++中,主要通过malloc()calloc()realloc()等函数进行内存分配,通过free()函数释放内存。动态内存分配通常发生在内存堆(heap)区域。

静态内存分配(栈内存分配)

特点

  • 自动分配和回收:当声明一个局部变量或函数参数时,编译器会在程序执行时自动为其分配内存(在栈上),并在超出作用域时自动释放。

  • 空间固定:在编译期间就可以确定变量所占内存的大小,不能在运行时改变。

  • 速度较快:栈内存的分配和回收效率相对较高。

  • 有限空间:栈内存大小受到限制,一般不适合分配大量或不定大小的数据。

动态内存分配(堆内存分配)

特点

  • 手动分配和回收:通过malloc()calloc()等函数手动分配内存,并通过free()函数手动释放内存。如果忘记释放,会导致内存泄漏。

  • 空间灵活:可以根据运行时的需求动态分配任意大小的内存,且在适当时候可以使用realloc()更改已分配内存的大小。

  • 空间较大:堆内存相较于栈内存更大,更适合存储大块数据或动态大小的数据结构。

  • 速度相对较慢:动态内存分配和回收的开销相对较大,尤其频繁分配和释放时会影响程序性能。

  • 需要管理:动态内存分配需要程序员自行管理,防止内存泄漏和悬挂指针等问题。

总结起来,静态分配适合于大小已知且生命周期清晰的小型数据结构,而动态分配则适用于大小不确定或生命周期较长的大块数据或复杂数据结构。两者结合使用可以满足程序的不同内存需求。

关键点

  1. 内存区域:操作系统为程序分配内存的主要区域包括栈(stack)和堆(heap)。栈用于存储局部变量和函数调用的上下文,而堆用于存储程序在运行时动态分配和释放的内存块。

  2. 分配函数

    • malloc(): 用于在堆上分配指定大小的内存块,并返回一个指向该内存块的指针。如果分配失败,返回NULL

    • calloc(): 类似于malloc(), 但分配的内存会被初始化为0。

    • realloc(): 改变之前通过malloc()calloc()分配的内存块的大小,可以扩大或缩小内存。

  3. 释放函数

    • free(): 用于释放之前通过malloc()calloc()realloc()分配的内存块,释放后内存不再属于该程序,可供其他内存分配请求使用。

  4. 注意事项

    • 内存泄漏:如果分配了内存却没有释放,随着时间的推移,程序会逐渐消耗掉所有的可用内存,导致性能下降甚至程序崩溃。

    • 悬挂指针:释放了内存之后仍然使用指向该内存区域的指针,这是一种严重的错误,可能导致不可预测的行为,甚至程序崩溃。

    • 越界访问:在动态分配的内存区域之外进行读写操作,同样是危险的操作。

    • 空指针引用:malloc 分配失败时会返回 NULL,程序需检查返回值是否为空指针,否则在试图访问或修改空指针指向的内存时,程序会因无效内存访问而崩溃。

    • 双重释放:同一个内存区域不能被多次释放,也就是不能对同一个指针多次调用 free 函数,否则会造成程序错误或崩溃。

示例代码

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

int main() {
    int *arr;
    int n = 10;

    // 动态分配内存
    arr = (int*)malloc(n * sizeof(int));
    if (arr == NULL) {
        printf("Memory allocation failed!\n");
        return 1;
    }

    // 使用分配的内存
    for (int i = 0; i < n; ++i) {
        arr[i] = i;
    }

    // 使用分配的内存
    for (int i = 0; i < n; ++i) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    // 释放内存
    free(arr);

    return 0;
}

动态内存申请相关API

malloc

malloc 主要用于动态内存分配。在 C 语言中,内存分为栈(stack)和堆(heap)两大部分,栈内存通常由编译器自动管理,而堆内存则是通过 malloc 函数来手动分配和管理。

函数原型:

void* malloc(size_t size);

功能:malloc 函数从程序的堆区分配一块连续的、大小为 size 字节的内存区域,并返回一个指向这块内存区域的指针(类型为 void*)。如果内存分配成功,返回的指针可以被强制类型转换为任何类型的指针,进而存储相应类型的数据。如果内存分配失败(例如,系统没有足够可用的内存),malloc 会返回 NULL

free

通过调用 free 函数,程序可以将不再需要的内存归还给操作系统,以供后续的内存分配请求使用。

函数原型:

void free(void* ptr);

功能:free 函数接受一个参数 ptr,这是一个指向之前动态分配的内存块的指针。当 free(ptr) 被调用时,它会释放 ptr 所指向的那片内存区域,使其不再是程序的一部分,可以被操作系统重新分配给其他内存请求。

memset

memset 用于将一段内存区域填充为特定的字符。其功能是对指定的内存块进行初始化,将指定长度的字节填充为指定的字符值。

函数原型:

void* memset(void* dest, int c, size_t len);
  • dest:指向要填充的内存块起始地址的指针。

  • c:要填充的字符值,实际上它会被转换成对应的整数值填充到内存中。

  • len:要填充的字节数。

举例来说,如果我们有一个字符数组 char buffer[100],并希望将其全部初始化为零,可以这样做:

memset(buffer, 0, sizeof(buffer));

这将把 buffer 数组里的所有字节(共100字节)都设置为零值(在C语言中,0 作为整数常量会被转换成对应的 ASCII NUL 字符\0,所以在字符串上下文中,这通常用于清零字符串)。

calloc

它与 malloc 类似,用于在程序运行时动态分配内存。calloc 的独特之处在于它不仅可以分配内存,还能将分配的内存区域初始化为0,这对于初始化大量数据,尤其是结构体或数组非常有用。

函数原型:

void* calloc(size_t num, size_t size);
// malloc(5 * sizeof(int)) 申请内存大小等价于 calloc(5, sizeof(int))
  • num:表示需要分配的元素数量。

  • size:表示每个元素的大小,以字节为单位。

功能:calloc 会一次性分配 num * size 个字节的连续内存空间,并将这些内存区域的内容初始化为 0。返回一个指向所分配内存区域的指针,类型为 void,可以转换为任何类型的指针。

realloc

它允许在程序运行时调整已分配的内存区域的大小。当原先分配的内存空间不足或过大时,可以通过 realloc 函数来扩展或收缩内存区域。

函数原型:

void* realloc(void* ptr, size_t size);
  • ptr:指向之前通过 malloccallocrealloc 分配的内存区域的指针。如果 ptrNULL,则 realloc 的行为类似于 malloc(size),即分配一个新的内存块。

  • size:新的内存大小(以字节为单位)。如果 size 为零,并且 ptr 不为 NULL,则 realloc 会释放 ptr 指向的内存区域,并返回 NULL

功能:

  • 如果 size 大于原内存区域的大小,realloc 尝试扩大内存区域,可能在内存中找到一块更大的连续区域,将原内容复制过去,并返回新内存区域的指针。如果内存不足,则可能保持原内存区域不变,返回原指针。

  • 如果 size 小于原内存区域的大小,realloc 尝试收缩内存区域,也可能重新分配一块较小的内存,并复制原内容过去。多余的部分将会被释放。

  • 如果内存调整失败,realloc 通常会返回 NULL,同时保留原有内存区域不变。

使用 realloc 时需注意:

  • realloc 返回 NULL 且原内存区域未改变的情况下,必须继续使用旧指针,并在合适的时候释放旧内存。

  • 不要对 realloc 返回的新指针进行解引用操作,除非确定内存调整成功。在进行内存调整后,旧指针可能失效。

  • 连续调用 realloc 以增加内存大小可能导致内存碎片,降低内存利用率。

使用示例

  • 例 1:mallocmemsetfree

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    int *array;
    const int length = 5;

    // 分配长度为 5 个整数的内存空间
    array = (int*)malloc(length * sizeof(int));
    if (array == NULL) {
        printf("Memory allocation failed.\n");
        return 1;
    }

    // 将堆区空间清零
    memset(array, 0, length * sizeof(int));

    // 使用分配的内存
    for (int i = 0; i < length; ++i) {
        array[i] = i;
    }

    // 输出数组内容
    for (int i = 0; i < length; ++i) {
        printf("Element at index %d: %d\n", i, array[i]);
    }

    // 释放内存
    free(array);

    return 0;
}
  • 例 2:realloc

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

int main() {
    int *array = (int*)malloc(5 * sizeof(int)); // 分配初始内存
    if (array == NULL) {
        printf("Initial memory allocation failed.\n");
        return 1;
    }

    // 使用分配的内存...

    // 尝试增大内存容量
    array = (int*)realloc(array, 10 * sizeof(int)); // 将内存扩大到能容纳10个整数
    if (array == NULL) {
        printf("Memory reallocation failed.\n");
        free(array); // 若失败,仍需释放原内存
        return 1;
    }

    // 继续使用扩大的内存...
    
    // 使用完毕后释放内存
    free(array);

    return 0;
}

释疑 & 补充

1、free 函数参数是一个指针地址,free 是如何判断从这个指针开始释放多少内存?

free 函数在释放内存时,并不是简单地根据传入的指针地址来确定释放多少内存,而是根据指针指向的内存块的内部信息来判断。当使用 malloccallocrealloc 等函数分配内存时,内存分配器会在分配的内存块前面或后面添加一些额外的信息,通常称之为元数据(metadata)。

在大多数现代内存分配器(如 glibc 的 ptmalloc2 或者jemalloc等)中,每个分配的内存块包含如下信息:

  • 头部信息(Header):存储了关于此内存块的大小和其他管理信息,包括但不限于分配的内存大小、标志位(比如是否已被释放)、可能还有指向下一个或前一个内存块的指针(用于管理内存碎片)。

  • 用户数据(User Data):紧跟在头部之后的就是用户真正可以访问和使用的内存区域,就是你通过指针访问的那一部分。当调用 free 函数时,它首先会检查传入的指针是否有效,然后根据指针所指向的内存块头部信息,得知分配的内存大小。基于这个大小,内存分配器会知道从该指针开始应释放多少内存。

请注意,程序员无需也无法直接访问这些元数据信息,它们是由内存分配器自身管理和维护的。因此,free 函数能够准确地知道从给定指针开始释放多少内存,不会影响到其他内存区域。

2、内存泄漏是什么?

内存泄漏指的是程序在动态分配内存后,未能在不再需要该内存时正确释放它。

在C语言中,内存分配通常通过malloccallocrealloc等函数完成,而释放内存则需要调用free函数。当程序分配了一块内存,但在后续代码中未能调用free函数释放这块内存,就发生了内存泄漏。

内存泄漏会导致系统资源(即内存)的持续占用,随着程序运行时间的增长,泄漏的内存会越来越多,最终可能导致系统可用内存耗尽,影响程序的稳定性和性能,甚至可能导致整个系统崩溃。

例如,考虑以下简单的内存泄漏场景:

char *p;
p = (char *)malloc(100); 
// 接下来可以使用 p 指向的内存了
p = "hello world"; // p 指向了文字常量区
// 申请的 100 字节的内存地址丢失无法 free 了

从此,已经丢失申请的 100 字节的内存地址,则动态申请的 100 个字节就被泄漏了。

3、字符串内存回顾。

// 定义一个字符数组 str1 并初始化为字符串 "hello world"
char str1[] = "hello world";

// 定义一个字符指针 str2,并让它指向一个存储在静态内存区域的字符串字面值 "hello world"
// 注意:在C语言中,字符串字面值通常存储在只读存储区,尝试修改 str2 指向的内存是未定义行为
char *str2 = "hello world";

// 使用 calloc 函数动态分配一个长度为128字节的连续内存区域,并将其内容初始化为零
// 返回的指针 str3 指向这块内存区域
char *str3 = (char *)calloc(128);

// 使用 strcpy 函数将字符串 "hello world" 复制到 str3 指向的内存区域
// 由于 str3 已经通过 calloc 分配了足够的空间,因此这次复制是安全的
strcpy(str3, "hello world");

// 注:在实际使用中,为了防止缓冲区溢出,在调用 strcpy 之前应确保目标缓冲区有足够的空间容纳源字符串及终止 null 字符'\0'

这段代码演示了在C语言中不同类型的字符串存储方式:

  1. 数组 str1:存储在栈上,大小与初始化的字符串相匹配,并且自动包含终止null字符'\0',可以直接修改其内容。

  2. 指针 str2:指向一个存储在静态存储区的字符串字面值,不可修改,且其内存由编译器自动管理。

  3. 动态分配的指针 str3:通过 calloc 函数动态分配内存,可以自由修改和释放。在调用 strcpy 时,由于已确保分配的空间大于等于复制的字符串长度,所以可以安全地存储和修改字符串。

0

评论区