C语言 - 内存管理

1、内存分区

可将内存简单分为:栈区、堆区、静态区,其中静态区包含数据段、代码段的内容,主要存储常量、字符串常量等只读数据、已初始化的全局变量和静态变量以及未初始化的全局变量和静态变量。

堆栈主要指的是栈,而不是堆。

 

1.1 栈区(Stack)

定义:栈区用于存储函数的局部变量、函数参数和返回地址。栈区内存由系统自动分配和释放,具有后进先出(LIFO)的特性。

特点:1、分配效率高,但空间有限;

           2、编译器自动管理,无需程序员手动释放。

用于保存局部变量。栈上的内容只在函数的范围内存在,当函数运行结束,这些内容 也会自动被销毁。

 

1.2 堆区(Heap)

定义:堆区用于动态分配内存,即程序运行时使用malloc、new等函数分配的内存。

特点:1、可以动态分配和释放大块内存。

           2、内存由程序员手动管理,需要手动释放(使用free函数)。

           3、内存分配效率相对较低,但空间较大。

其生命周期由 free 或 delete 决定。 在没有释放之前一直存在,直到程序结束。

 

1.3 数据段(Data Segment)

数据段又可以进一步细分为:

1、已初始化数据段:存储已初始化的全局变量和静态变量(static)。这些变量在程序加载时分配内存,并在程序结束时释放。

2、未初始化数据段(BSS段):存储未初始化的全局变量和静态变量。BSS段在程序启动时自动初始化为零或空指针,但它在程序加载时并不占用磁盘空间(因为其内容默认为零)。

 

1.4 代码段(Code Segment)

定义:代码段也称为文本段,存储程序的机器指令。

特点:1、通常是只读的,以防止程序意外修改指令。

           2、包含常量字符串等只读数据。

函数定义和字符串常量存储在代码段。

 

2、内存分配

2.1 静态内存分配

        静态内存分配是在程序编译时进行的,它将内存分配给全局变量和静态变量,存在周期最长。

优点:内存分配和释放的效率高。

缺点:内存使用不灵活,无法根据需要动态调整内存大小。

char UART_RxBUF[100] = {0};//已初始化全局变量
char flag;//未初始化全局变量
 
static int count;//静态全局变量,只能在本文件内使用
 
void fun()
{
    static int i;//静态变量
    //fun主体
}

 

2.2 栈内分配

        栈内存分配是在程序运行时进行的,它将内存分配给函数内部的局部变量。

优点:内存管理简单,不需要程序员手动释放。

缺点:内存空间有限,不适合分配大内存,且存在栈溢出的风险。

void fun()
{
    int i = 0;
    char buf[100] = {0};//函数运行结束,就会释放
//fun主体
}

 

2.3 动态内存分配

        动态内存分配是在程序运行时根据需要进行的内存分配。

优点:内存使用灵活,可以根据需要动态调整内存大小。

缺点:内存管理复杂,需要程序员手动分配和释放,容易出现内存泄漏等问题。

        常用的动态内存分配函数包括malloc( )、calloc( )realloc( ),分别用于分配内存、分配并初始化为0的内存、以及重新调整已分配内存的大小。

内存管理函数:

序号 函数 描述
1 void *calloc(int num, int size); 在内存中动态地分配 num 个长度为 size 的连续空间,并将每一个字节都初始化为 0。所以它的结果是分配了 num*size 个字节长度的内存空间,并且每个字节的值都是 0。
2 void *malloc(int size); 在堆区分配一块指定大小的内存空间,用来存放数据。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。
3 void *realloc(void *address, int newsize); 该函数重新分配内存,把内存扩展到 newsize。
4 void free(void *address); 该函数释放 address 所指向的内存块,释放的是动态分配的内存空间。

注意:void * 类型表示未确定类型的指针。C、C++ 规定 void * 类型可以通过类型转换强制转换为任何其它类型的指针。

 

malloc()动态内存申请:

编程时,如果您预先知道数组的大小,那么定义数组时就比较容易。例如,一个存储人名的数组,它最多容纳 100 个字符,所以您可以定义数组,如下所示:

char name[100];

但是,如果您预先不知道需要存储的文本长度,例如您想存储有关一个主题的详细描述。在这里,我们需要定义一个指针,该指针指向未定义所需内存大小的字符,后续再根据需求来分配内存,如下所示:

实例

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

//#define _CRT_SECURE_NO_WARNINGS

int main()
{
    char name[100];
    char* description;

    strcpy(name, "Zara Ali");

    /* 动态分配内存 */
    description = (char*)malloc(200 * sizeof(char));
    if (description == NULL)
    {
        printf("错误-内存申请失败!\n");
    }
    else
    {
        strcpy(description, "Zara Ali是DPS 10班的学生");
    }
    printf("Name = %s\n", name);
    printf("Description: %s\n", description);

    free(description);
}

 当上面的代码被编译和执行时,它会产生下列结果:

 上面的程序也可以使用 calloc() 来编写,只需要把 malloc 替换为 calloc 即可,如下所示:

calloc(200, sizeof(char));

当动态分配内存时,您有完全控制权,可以传递任何大小的值。而那些预先定义了大小的数组,一旦定义则无法改变大小。

 

 realloc()重新调整内存的大小和释放内存:

当程序退出时,操作系统会自动释放所有分配给程序的内存,但是,建议您在不需要内存时,都应该调用函数 free() 来释放内存。

或者,您可以通过调用函数 realloc() 来增加或减少已分配的内存块的大小。让我们使用 realloc() 和 free() 函数,再次查看上面的实例:

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

int main()
{
    char name[100];
    char* description;

    strcpy(name, "Zara Ali");

    /* 动态分配内存 */
    description = (char*)malloc(30 * sizeof(char));
    if (description == NULL)
    {
        printf("错误-内存申请失败!\n");
    }
    else
    {
        strcpy(description, "Zara ali是DPS的学生.");
    }
    /* 假设您想要存储更大的描述信息 */
    description = (char*)realloc(description, 100 * sizeof(char));
    if (description == NULL)
    {
        printf("错误-无法分配所需的内存!\n");
    }
    else
    {
        strcat(description, "他在10班");
    }

    printf("Name = %s\n", name);
    printf("Description: %s\n", description);

    /* 使用 free() 函数释放内存 */
    free(description);
}

当上面的代码被编译和执行时,它会产生下列结果:

您可以尝试一下不重新分配额外的内存,strcat() 函数会生成一个错误,因为存储 description 时可用的内存不足。

 

free()内存释放:

在 C 语言中,内存释放是非常重要的。如果忘记释放不再使用的内存,就会导致内存泄漏。内存泄漏会导致程序的性能下降,甚至可能导致程序崩溃。

int *ptr = (int*)malloc(10 * sizeof(int));
if(ptr != NULL)
{
    printf("内存申请成功!\n");
}
 
free(ptr);
 
ptr = NULL;

切记,申请次数和释放次数要对应。malloc 两次只 free 一次会内存泄漏;malloc 一次 free 两次肯定会出错。也就是说,在程序 中 malloc 的使用次数一定要和 free 相等,否则必有错误。虽然使用 free 函数释放了内存,但指针变量 p 本身保存的地址并没有改变,就需要重新把 p 的值变为 NULL:否则这个指针就成为了“野指针”,也有书叫“悬 垂指针”。这是很危险的,而且也是经常出错的地方。所以一定要记住一条:free 完之后, 一定要给指针置 NULL

 

 

3、常见内存错误--野指针

野指针是一个指针变量,所指向的地址是未知的、随机的、不正确的或没有明确限制的,以及那些已经被释放的内存地址。这种指针在尝试访问或修改其所指向的内存时,会导致不可预测的行为,甚至程序崩溃。野指针的存在是编程中的一个严重问题,因为它可能导致程序崩溃、数据损坏或其他不可预期的行为。在严重的情况下,野指针还可能被恶意利用,造成安全漏洞。

3.1 未初始化的指针

        在C或C++中,声明一个指针变量时,如果没有立即为其分配内存或初始化为NULL,它将包含一个随机的地址。尝试访问这个随机地址会导致不可预知的后果。如:

#include <stdio.h>
 
struct student
{
char *name;
int score;
}stu;
 
int main()
{
strcpy(stu.name,"Jimy");
return 0;
}
/*这里定义了结构体变量 stu,但是他没想到这个结构体内部 char *name 
这成员在定义结构体变量 stu 时,只是给 name 这个指针变量本身分配了 
4 个字节。name 指针并没有指向一个合法的地址,这时候其内部存的只是
一些乱码。所以在调用 strcpy 函数时,会将字符串"Jimy"往乱码所指的内
存上拷贝,而这块内存 name 指针根本就无权访问,导致出错。解决的办法
是为 name 指针 malloc 一块空间。*/

 

3.2 指针释放后未置空

        使用free()delete等函数释放了指针指向的内存后,如果不将指针置为NULL,那么这个指针就变成了野指针。在后续的代码中,如果错误地尝试通过这个指针访问内存,将会导致不可预测的行为。

int *ptr = (int*)malloc(10 * sizeof(int));
if(ptr != NULL)
{
    printf("内存申请成功!\n");
}
free(ptr);
 
// ptr = NULL;
*ptr = 100;
*(ptr+1) = 200;
//ptr已经被释放,错误操作

 

3.3 局部变量指针逃逸

        当函数返回时,其栈上的局部变量将不再有效。如果指针仍然指向这些局部变量,它们将成为野指针。如:

#include <stdio.h>  
  
int* fun() 
{  
    int value = 10; // 局部变量  
    return &value; // 返回局部变量的地址  
}  
  
int main()
 {  
    int* ptr = fun(); // ptr 指向了 fun函数中的局部变量 value 的地址  
    printf("%d\n", *ptr); // 这里可能打印出 10,但这是未定义行为  
    // 当 fun函数执行完毕后,value 所占用的内存已经被释放  
    // 此时 ptr 是一个野指针,访问它会导致未定义行为  
    // 但在某些情况下(比如没有立即重用该内存区域),它可能看起来还在工作  
    return 0;  
}

 正确方式:

#include <stdio.h>  
#include <stdlib.h>  
// 函数声明,返回一个指向int的指针  
int* createInteger(int value) 
{  
    // 在堆上分配内存来存储一个int值  
    int* ptr = (int*)malloc(sizeof(int));  
    if (ptr == NULL) {  
        // 如果malloc失败,返回NULL  
        return NULL;  
    }  
    *ptr = value; // 初始化内存中的值  
    return ptr; // 返回指向新分配内存的指针  
}  
  
// 注意:在C语言中,我们通常不编写专门的函数来释放内存,因为这通常是调用者的责任  
  
int main() 
{  
    int* myInteger = createInteger(42); // 调用函数,分配内存并初始化  
    if (myInteger != NULL) 
    {  
        // 检查指针是否为NULL,以避免解引用空指针  
        printf("The value is: %d\n", *myInteger); // 使用指针访问值  
        // ... 在这里可以使用myInteger做一些事情 ...  
        // 释放之前分配的内存  
        free(myInteger);  
        // 将指针置为NULL,避免成为野指针(这是一个好习惯)  
        myInteger = NULL;  
    } 
    else 
    {  
        // 处理内存分配失败的情况  
        printf("Memory allocation failed!\n");  
    }  
  
    return 0;  
}

 

3.4 指针运算错误

        对指针进行算术运算时,如果运算后的指针指向了未知或无效的内存区域,也会形成野指针。如:

#include <stdio.h>  
  
int main() 
{  
    int arr[5] = {1, 2, 3, 4, 5}; // 定义一个有5个元素的数组  
    int *ptr = arr; // 指针ptr指向数组的第一个元素  
    // 正确访问数组元素  
    for (int i = 0; i < 5; i++) {  
        printf("%d ", *(ptr + i)); // 输出1 2 3 4 5  
    }  
    // 指针运算错误:尝试访问数组之外的内存  
    ptr += 5; // ptr现在指向arr[5],但实际上arr[5]不存在,这是越界  
    printf("%d\n", *ptr); // 尝试解引用ptr,这是未定义行为  
    // 在某些情况下,上面的代码可能不会立即崩溃,但会导致不可预测的结果  
    // 因为ptr现在指向了一个未定义的内存位置  
    return 0;  
}

 

 

posted @ 2023-10-11 13:53  [BORUTO]  阅读(9)  评论(0编辑  收藏  举报