C语言-动态内存管理

文章目录

🎯引言

在C语言编程中,动态内存管理是一个至关重要的概念。它不仅涉及到程序如何高效地使用内存资源,还影响到程序的稳定性和性能。通过动态分配内存,程序可以根据运行时的需求灵活地调整内存使用,这对于处理不确定的数据量和实现高效的资源利用尤为重要。在这篇文章中,我们将深入探讨C语言中的动态内存管理机制,了解如何使用标准库函数如 malloccallocreallocfree,并讨论一些常见的内存管理陷阱和最佳实践。

👓动态内存管理

1.为什么要有动态内存分配

1.1 内存使用的灵活性

静态内存分配在编译时确定内存的大小,适用于数据量已知且固定的情况。但在许多实际应用中,程序在运行时的数据量是动态变化的,难以预先确定。动态内存分配允许程序在运行时根据实际需要分配内存,从而提高了内存使用的灵活性和效率。

示例:

思考如果当我们想将每个班级的成绩都录入时,我们是不是应该开辟一个数组,将成绩存放在数组之中,但每个班级的人数不同,所需的数组大小也不同,所以我们想通过输入班级人数的方式,控制数组的大小.如:

c 复制代码
#include <stdio.h>

//该代码时错误示例:
int main()
{
    int n=0;
    printf("请输入班级的人数:");
    scanf("%d",&n);
    
    int arr[n];//错误
    
    return 0;
}

上述的代码是错误示例,因为数组只能支持静态开辟

这种开辟空间的特点是:

  1. 空间开辟大小是固定的
  2. 数组在申明的时候,必须指定数组的长度,且数组空间大小一旦确定不能调整

所以我们需要学习动态内存分配,为以后能够解决更加复杂的问题

1.2 高效利用内存

动态内存分配使得程序可以按需分配和释放内存,避免了内存的浪费。例如,一个程序可能需要处理大量数据,但这些数据只在特定的情况下存在。在这种情况下,动态分配内存可以在需要时分配内存,并在不再需要时释放内存,从而高效利用系统资源。

1.3 适应复杂的数据结构

某些数据结构(如链表、树和图)在创建时需要动态地调整大小。这些数据结构的节点数量在运行时可能会不断变化,静态分配内存无法满足这些需求。动态内存分配可以在运行时为这些数据结构的每个新节点分配内存,并在节点被删除时释放内存,从而支持复杂数据结构的实现。

2.malloc和free

2.1malloc 的使用

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

语法

c 复制代码
void* malloc(size_t size);

size的大小是字节

示例

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr;
    int n, i;

    // 分配内存用于存储n个整数
    n = 5;
    //(int*)强制类型转换,保持类型一致
    ptr = (int*)malloc(n * sizeof(int));

    // 检查内存分配是否成功
    if (ptr == NULL) {
        printf("Memory not allocated.\n");
        exit(0);
    } else {
        printf("Memory successfully allocated using malloc.\n");

        // 初始化并显示分配的内存
        for (i = 0; i < n; ++i) {
            ptr[i] = i + 1;
        }

        printf("The elements of the array are: ");
        for (i = 0; i < n; ++i) {
            printf("%d ", ptr[i]);
        }
    }

    // 使用完后释放内存
    free(ptr);
    ptr=NULL;

    return 0;
}

ptr = (int*)malloc(n * sizeof(int));该代码中(int*)是强制转换,因为malloc返回的是void*类型,需要转换成int*才能赋给ptr.

2.2free 的使用

free 函数用于释放先前由 malloccallocrealloc 分配的内存。释放的内存可以被重新分配,但未被释放的内存会导致内存泄漏。

语法

c 复制代码
void free(void* ptr);

示例

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr;
    int n;

    // 分配内存
    n = 5;
    ptr = (int*)malloc(n * sizeof(int));

    if (ptr == NULL) {
        printf("Memory not allocated.\n");
        exit(0);
    } else {
        // 使用内存
        for (int i = 0; i < n; ++i) {
            ptr[i] = i + 1;
        }

        // 释放内存
        free(ptr);
        prt=NULL;
        printf("Memory successfully freed.\n");
    }

    return 0;
}

注意事项

  1. 检查分配是否成功 : 始终检查 malloc 的返回值是否为 NULL,以确保内存分配成功。

    c 复制代码
    if (ptr == NULL) {
        // 处理内存分配失败
    }
  2. 避免内存泄漏 : 在不再需要动态分配的内存时,使用 free 释放内存。

    c 复制代码
    free(ptr);
  3. 避免重复释放 : 同一块内存不能被 free 多次,释放后将指针设置为 NULL 以避免重复释放。

    c 复制代码
    free(ptr);
    ptr = NULL;
  4. 使用未初始化或已释放的内存: 使用未初始化或已释放的内存会导致未定义行为,确保在使用内存前正确初始化并在释放后避免访问。

    c 复制代码
    int *ptr = (int*)malloc(sizeof(int));
    *ptr = 10;  // 使用前初始化
    free(ptr);
    ptr = NULL; // 避免再次使用
  5. 内存分配大小正确 : 确保 malloc 分配的大小正确,尤其是在分配数组或结构体时。

    c 复制代码
    int *arr = (int*)malloc(n * sizeof(int)); // 正确分配n个整数的内存

3.calloc和realloc

3.1calloc 函数的使用

calloc 函数用于分配一块指定数量和大小的内存,并将内存初始化为零。它的原型为:

c 复制代码
void* calloc(size_t num, size_t size);
  • num:需要分配的元素个数。
  • size:每个元素的大小(字节数)。

示例

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr;
    int n = 5;

    // 使用 calloc 分配内存
    ptr = (int*)calloc(n, sizeof(int));

    if (ptr == NULL) {
        printf("Memory allocation failed\n");
        return 1;
    }

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

    // 输出分配的内容
    printf("Allocated memory initialized with zeros:\n");
    for (int i = 0; i < n; ++i) {
        printf("%d ", ptr[i]);
    }
    printf("\n");

    // 释放内存
    free(ptr);

    return 0;
}

注意事项

  1. 内存初始化 : calloc 在分配内存时会将内存初始化为零,malloc在分配时不会将内存进行初始化.这对于某些应用来说是非常方便的,特别是在需要确保新分配的内存中所有位都为零时。
  2. 返回值检查 : 和 malloc 一样,使用 calloc 后应该检查返回的指针是否为 NULL,以确保内存分配成功。
  3. 多次调用 : calloc 可以用来分配数组和结构体等数据结构,能够确保新分配的内存块是对齐的。

3.2realloc 函数的使用

realloc 函数用于重新分配之前分配的内存块的大小,可以扩展或缩小内存块的大小。它的原型为:

c 复制代码
void* realloc(void* ptr, size_t size);
  • ptr:指向之前由 malloccallocrealloc 分配的内存块的指针。如果为 NULL,则等效于 malloc(size)
  • size:新的内存块的大小(字节数)。

示例

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr;
    int n = 5;

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

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

    // 输出分配的内容
    printf("Allocated memory before realloc:\n");
    for (int i = 0; i < n; ++i) {
        printf("%d ", ptr[i]);
    }
    printf("\n");

    // 重新分配内存
    n = 10;
    ptr = (int*)realloc(ptr, n * sizeof(int));
    if (ptr == NULL) {
        printf("Memory reallocation failed\n");
        return 1;
    }

    // 输出重新分配后的内容
    printf("Allocated memory after realloc:\n");
    for (int i = 0; i < n; ++i) {
        printf("%d ", ptr[i]);
    }
    printf("\n");

    // 释放内存
    free(ptr);

    return 0;
}

注意事项

  1. 指针为空的处理 : 如果 realloc 的第一个参数 ptrNULL,则等效于 malloc(size),会分配新的内存块。
  2. 返回值检查 : realloc 可能会返回一个新的指针,因此在使用新的指针前应该检查是否为 NULL,以避免内存分配失败。
  3. 内存块的扩展和收缩 : realloc 可以扩展或缩小之前分配的内存块的大小,但是不能保证在原地进行扩展。如果原来的内存块无法扩展到新的大小,realloc 可能会分配一个新的内存块,并将旧的数据复制到新的内存块中。

图示:

​ 1.下图增加的大小没有占用到已经分配的内存空间,所以可以在原地进行扩展

​ 2.下图增加的大小占用到已经分配的内存空间,所以所以不能在原来的内存块扩展,会分配到一个新的内存块,并将数据复制过去

  1. 潜在的性能开销: 在重新分配内存时,如果无法在原地扩展,可能需要将数据复制到新的内存块中,这可能会引入性能开销。因此,最好在需要调整大小时考虑一次性分配足够大的内存。
  2. 不要使用未初始化或已释放的指针 : 在使用 realloc 后,原来的指针 ptr 可能已经无效或指向新分配的内存块。避免使用已释放或无效的指针,可以将 ptr 设置为 NULL

4.常见的动态内存的错误

4.1内存泄漏 (Memory Leak):

  • 错误描述: 动态分配的内存在不再需要时未被正确释放。
  • 原因 : 忘记调用 free 函数释放 malloccallocrealloc 分配的内存,或者在程序的逻辑中没有正确的释放内存。
  • 解决方法 : 确保每次动态分配内存后,都有对应的释放操作,及时释放不再使用的内存。(很重要)

示例:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

void demo_memory_leak() {
    // 未释放内存的情况
    int *ptr = (int *)malloc(sizeof(int));
    if (ptr == NULL) {
        printf("Memory allocation failed\n");
        return;
    }

    // 没有释放内存,导致内存泄漏
    // free(ptr); // 演示内存泄漏,故意注释掉释放内存的语句
}

int main() {
    demo_memory_leak();
    // 在实际应用中应该确保在程序退出前释放所有动态分配的内存
    return 0;
}

4.2重复释放内存:

  • 错误描述 : 同一块内存被多次调用 free 函数释放。
  • 原因 : 可能是因为在释放后未将指针设置为 NULL,导致后续误操作释放相同的内存块。
  • 解决方法 : 每次使用 free 后,将指针设置为 NULL,避免重复释放同一块内存。

示例:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

void demo_double_free() {
    int *ptr = (int *)malloc(sizeof(int));
    if (ptr == NULL) {
        printf("Memory allocation failed\n");
        return;
    }

    // 释放内存
    free(ptr);
    
    // 错误的再次释放相同的内存块
    free(ptr); // 这里会导致运行时错误
}

int main() {
    demo_double_free();
    return 0;
}

4.3使用已释放的内存:

  • 错误描述 : 在已经通过 free 释放的内存块上进行读写操作。
  • 原因: 在释放内存后未及时更新指针,或者程序逻辑错误导致继续使用已释放的指针。
  • 解决方法 : 在 free 后将指针设置为 NULL,并且避免在之后的代码中继续使用该指针。

示例:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

void demo_use_after_free() {
    int *ptr = (int *)malloc(sizeof(int));
    if (ptr == NULL) {
        printf("Memory allocation failed\n");
        return;
    }

    *ptr = 10;
    printf("Value before free: %d\n", *ptr);

    // 释放内存
    free(ptr);

    // 错误的在释放后使用已释放的内存
    printf("Value after free: %d\n", *ptr); // 这里会导致未定义行为
}

int main() {
    demo_use_after_free();
    return 0;
}

4.4内存越界访问:

  • 错误描述: 访问超出动态分配内存块范围的位置。
  • 原因: 错误地计算数组索引或指针操作,导致访问了未分配给程序的内存区域。
  • 解决方法: 确保在访问数组或指针指向的内存时,始终检查边界并确保不越界访问。

示例:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

void demo_out_of_bounds() {
    int *ptr = (int *)malloc(5 * sizeof(int));

    // 访问超出分配的内存块
    for (int i = 0; i <= 5; ++i) {
        ptr[i] = i + 1; // 越界访问
    }

    free(ptr);
}

int main() {
    demo_out_of_bounds();
    return 0;
}

4.5使用free释放一块动态开辟内存的一部分

c 复制代码
#include <stdio.h>
#include <stdlib.h>

void test()
{
	int* p=(int*)malloc(100);
	p++;
	free(p);
}

int main()
{
    test();
    
    return 0;
}

解释问题:

  1. 指针移动 (p++) :
    • malloc 分配了一块包含 100 个整数大小的内存块后,指针 p 指向该内存块的起始位置。
    • p++ 将指针 p 向前移动了一个 int 的大小(通常为 4 字节或 8 字节,取决于系统),使 p 指向该内存块的第二个 int 位置,而不是第一个。
  2. 错误的释放 (free(p)) :
    • free 函数只能释放由 malloc 或类似函数分配的内存块的起始地址。由于 p 指向的是移动后的地址,而不是 malloc 返回的地址,因此调用 free(p) 是未定义行为,可能会导致程序崩溃或其他意外行为。

5.动态内存经典题目

题目:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void GetMemory(char* p)
{
	p = (char*)malloc(100);
}

void Test(void)
{
	char* str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
}

int main()
{
	Test();

	return 0;
}
//运行代码会有什么样的结果?

答案:程序会崩溃

问题分析和解决方案

函数 GetMemory 的设计问题

  • 函数 GetMemory 接受一个 char* 类型的指针 p,但在函数内部重新分配了内存给 p,这只改变了 p 的拷贝,而不影响原始指针的值。因此,函数调用 GetMemory(str) 中的 str 并没有被正确地分配内存。为了使得函数能够正确分配内存并将分配的内存地址返回,可以修改为接受 char** 类型的指针,通过指针的指针来修改原始指针的值。

未初始化指针问题

  • Test 函数中,char* str 被初始化为 NULL,然后传递给 GetMemory 函数。由于 GetMemory 函数中的参数 p 是按值传递的,因此在函数外部并不会真正地修改 str 指针的值,所以 str 仍然是 NULLstrcpy接收到空指针就会报错。可以通过将 GetMemory 函数改为返回分配的内存地址来解决这个问题。

未释放动态开辟的内存空间

解决方案:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 修改 GetMemory 函数,使其返回分配的内存块的指针
char* GetMemory(void)
{
    char* p = (char*)malloc(100);
    return p;
}

void Test(void)
{
    char* str = NULL;
    str = GetMemory(); // 接收 GetMemory 返回的指针

    if (str != NULL) {
        strcpy(str, "hello world");
        printf("%s\n", str);

        // 使用完毕后释放内存
        free(str);  // 释放动态分配的内存
        str = NULL;  // 将指针置为 NULL,避免成为悬垂指针
    } else {
        printf("Memory allocation failed\n");
    }
}

int main()
{
    Test();

    return 0;
}

6.1概念

柔性数组(Flexible Array Member)是C语言中的一种特性,**它允许在结构体的末尾定义一个长度不定的数组,这个数组的大小可以根据结构体的实际大小动态确定。**这种特性通常用于构建可变长度的数据结构,如变长数据包的头部信息、动态字符串和动态数组等。

在C语言中,柔性数组的定义方式如下:

c 复制代码
struct flex_array_struct {
    size_t num_elements;
    int data[];  // 柔性数组成员,可以动态调整大小
};
  • data[] 是结构体 flex_array_struct 的最后一个成员,没有指定具体的数组大小。
  • 在实际使用时,可以动态分配足够大小的内存来包含结构体本身加上柔性数组成员的空间,例如:
c 复制代码
struct flex_array_struct *ptr;

// 分配足够大小的内存,包含结构体本身和柔性数组成员
ptr = (struct flex_array_struct *)malloc(sizeof(struct flex_array_struct) + n * sizeof(int));

if (ptr != NULL) {
    ptr->num_elements = n;
    // 现在可以通过 ptr->data[i] 访问柔性数组中的元素
}

柔性数组的特点:

动态长度: 柔性数组允许在结构体的末尾定义一个未命名的数组成员,其大小可以在运行时动态确定。这使得结构体可以根据需要动态调整大小,而不需要预先知道数组的最大长度。

用途灵活: 主要用于构建可变长度的数据结构,如变长数据包的头部信息、动态字符串、动态数组等。它允许在不浪费空间的情况下,处理需要动态增长或动态大小的数据需求。

结构体末尾: 柔性数组必须是结构体的最后一个成员,且柔性数组前面至少有一个其他的成员。这样可以确保柔性数组成员后面没有其他成员,从而便于计算整个结构体的大小。

sizeof计算:使用sizeof计算包含柔性数组结构体的大小的时候,不包含柔性数组的内存

内存分配 : 使用柔性数组时,需要注意在分配内存时,必须包括结构体本身的大小加上柔性数组成员的大小。例如,使用 malloc 分配内存时需要确保分配的大小足够容纳结构体及其柔性数组成员。

6.2柔性数组的使用

示例:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

// 定义包含柔性数组的结构体
struct flex_array_struct {
    size_t num_elements;
    int data[];  // 柔性数组成员
};

int main() {
    size_t num_elements = 5;

    // 直接分配足够大小的内存,包含结构体本身和柔性数组成员
    struct flex_array_struct *flex_array = malloc(sizeof(struct flex_array_struct) + num_elements * sizeof(int));

    if (flex_array != NULL) {
        flex_array->num_elements = num_elements;

        // 初始化柔性数组
        for (size_t i = 0; i < num_elements; ++i) {
            flex_array->data[i] = i * 10;
        }

        // 使用柔性数组的数据
        printf("Number of elements: %zu\n", flex_array->num_elements);
        printf("Flexible array elements:\n");
        for (size_t i = 0; i < flex_array->num_elements; ++i) {
            printf("%d ", flex_array->data[i]);
        }
        printf("\n");

        // 释放内存
        free(flex_array);
    } else {
        printf("Failed to allocate memory\n");
    }

    return 0;
}

解释示例:

  • 结构体定义struct flex_array_struct 包含一个 num_elements 的成员用来存储柔性数组的元素数量,以及一个未命名的柔性数组成员 data[]
  • 直接分配内存 :在 main 函数中,使用 malloc 分配足够大小的内存,包括结构体本身和柔性数组成员。
  • 初始化柔性数组 :设置 num_elements 并初始化柔性数组中的元素。在示例中,柔性数组的元素按照 i * 10 的方式进行初始化。
  • 使用柔性数组数据 :在分配和初始化完成后,可以直接通过 flex_array->data[i] 访问和修改柔性数组中的元素,就像访问普通数组一样。
  • 释放内存 :使用 free 函数释放动态分配的内存,确保在程序结束时释放已分配的资源。

总结C/C++中程序内存区域划分

图示:

代码段 (Text Segment):

  • 用途: 存储程序的可执行代码以及只读常量,也就是编译后的机器指令。
  • 特性: 该区域通常是只读的,以防止程序意外修改其指令。

数据段 (Data Segment):

  • 用途: 存储全局变量和静态变量,这些变量在程序的生命周期内保持其值。

  • :
    数据段通常进一步分为两个子段:

    • 初始化数据段: 存储已初始化的全局变量和静态变量。
    • 未初始化数据段 (BSS Segment): 存储未初始化的全局变量和静态变量。在程序开始执行时,这些变量被自动初始化为零。

堆 (Heap):

  • 用途 : 用于动态内存分配。程序在运行时可以使用如 malloccallocreallocfree 等函数在堆上分配和释放内存。
  • 特性: 堆的大小不是固定的,可以在程序运行时动态增长或缩小,受限于系统的可用内存。

栈 (Stack):

  • 用途: 存储函数调用时的局部变量、参数和返回地址。每次函数调用都会在栈上分配一个栈帧,用于存储该函数的局部变量和其他相关信息。
  • 特性: 栈是LIFO(后进先出)结构,栈的大小通常是固定的,由操作系统设置一个最大值。栈的增长方向通常是向下的,即从高地址向低地址增长。

🥇结语

动态内存管理是C语言编程中的一项核心技能,掌握它不仅可以提升程序的性能,还能避免许多常见的内存问题。通过合理使用 malloccallocreallocfree 等函数,我们可以在需要时分配和释放内存,从而实现更灵活和高效的内存管理。在实际开发中,注意避免内存泄漏、悬空指针等问题,将有助于编写更加健壮和可靠的代码。希望这篇文章能帮助你更好地理解和应用C语言的动态内存管理,为你的编程之旅增添新的动力。

相关推荐
SRY122404192 小时前
javaSE面试题
java·开发语言·面试
lb36363636363 小时前
介绍一下数组(c基础)(详细版)
c语言
无尽的大道3 小时前
Java 泛型详解:参数化类型的强大之处
java·开发语言
ZIM学编程3 小时前
Java基础Day-Sixteen
java·开发语言·windows
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
一丝晨光4 小时前
编译器、IDE对C/C++新标准的支持
c语言·开发语言·c++·ide·msvc·visual studio·gcc
阮少年、4 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
代码小鑫4 小时前
A027-基于Spring Boot的农事管理系统
java·开发语言·数据库·spring boot·后端·毕业设计
程序猿-瑞瑞5 小时前
11 go语言(golang) - 数据类型:结构体
开发语言·golang
奶味少女酱~5 小时前
常用的c++特性-->day02
开发语言·c++·算法