首页 > 系统相关 >C语言动态内存管理(重点)

C语言动态内存管理(重点)

时间:2024-03-24 20:30:50浏览次数:16  
标签:malloc 函数 int realloc C语言 内存 动态内存 NULL 重点

目录

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

2、malloc 和 free

2.1 malloc函数

2.2 free函数

3、calloc 和 realloc

3.1  calloc函数 

3.2  realloc 函数

3.3  realloc 和 malloc 区别

3.4  realloc 函数存在的问题

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

5、动态内存经典笔试题分析

6、柔性数组 

6.1 补充 typedef 创建结构体

6.2 柔性数组的特点

6.3 柔性数组的使用

6.4 柔性数组的优势 

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


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

我们已经掌握的内存开辟方式有:

int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间

2、malloc 和 free

2.1 malloc函数

       malloc 函数是 C 语言中用于动态分配内存的函数之一,其声明在 <stdlib.h> 头文件中。该函数用于在程序运行时从堆(heap)中动态分配指定大小的内存空间,并返回一个指向该内存空间起始地址的指针。

void *malloc(size_t size);
  • size_t size:需要分配的内存空间大小,以字节为单位。

   malloc 函数返回一个 void 类型的指针,需要根据实际情况进行类型转换后使用。如果内存分配成功,则返回指向分配内存起始地址的指针;如果内存分配失败,则返回 NULL

2.2 free函数

   free 函数是 C 语言中用于释放动态分配内存的函数,其声明在 <stdlib.h> 头文件中。当程序不再需要动态分配的内存空间时,应该使用 free 函数将该内存空间释放,以便系统可以重新利用这部分内存。 

void free(void *ptr);
  • void *ptr:指向先前由 malloccalloc 或 realloc 返回的内存空间起始地址的指针。

       使用 free 函数后,内存空间将被标记为可用,并可以被后续的内存分配操作重新使用。但需要注意的是,尝试释放已经释放过的内存空间或者尝试释放静态分配的内存空间会导致未定义的行为,因此务必确保只对动态分配的内存空间使用 free 函数。

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

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

    // 动态分配包含5个整数的内存空间
    arr = (int *)malloc(n * sizeof(int));

    if (arr == NULL) 
    {
        perror("malloc");
        return 1;
    }

    // 将数组元素初始化为 0
    for (int i = 0; i < n; i++) 
    {
        arr[i] = 0;
    }

    // 打印数组元素
    for (int i = 0; i < n; i++) 
    {
        printf("%d ", arr[i]);
    }

    // 释放动态分配的内存
    free(arr);
    arr=NULL;
    return 0;
}

注:在 free 释放开辟的动态内存后,原来指向该内存首地址的指针仍然指向该地址,为了避免该指针成为野指针,在 free 释放完开辟的动态内存后最好将指向该内存的指针设置为NULL;

    free(arr);
    arr=NULL;

3、calloc 和 realloc

3.1  calloc函数 

calloc 函数是 C 语言中用于动态分配内存并初始化为零的函数,其声明在 <stdlib.h> 头文件中。与 malloc 函数不同的是,calloc 函数在分配内存空间的同时会将其初始化为零,这是 callocmalloc 的主要区别之一。 

void *calloc(size_t num, size_t size);
  • size_t num:需要分配的元素个数。
  • size_t size:每个元素的大小,以字节为单位。

   calloc 函数会分配 num*size 个字节的内存空间,并将所有位初始化为零。如果内存分配成功,则返回指向分配内存起始地址的指针;如果内存分配失败,则返回 NULL

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (NULL != p)
	{
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i));
		}
	}
	free(p);
	p = NULL;
	return 0;
}
3.2  realloc 函数

   realloc 函数是 C 语言中用于重新分配动态分配内存空间的函数,其声明在 <stdlib.h> 头文件中。当需要调整先前分配的内存空间的大小时,可以使用 realloc 函数来实现。realloc 函数会尝试扩大或缩小先前分配的内存块,并保留原始内存块中的数据。 

void *realloc(void *ptr, size_t size);
  • void *ptr:指向先前由 malloccalloc 或 realloc 返回的内存空间起始地址的指针。
  • size_t size:重新分配后的内存空间大小,以字节为单位。

            如果重新分配成功,则返回指向新分配内存起始地址的指针;如果重新分配失败,则返回 NULL。值得注意的是,realloc 函数可能会将原内存块移动到新的位置,因此在调用 realloc 后应该谨慎处理原指针。

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

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

    // 动态分配包含5个整数的内存空间
    arr = (int *)malloc(n * sizeof(int));

    if (arr == NULL) 
    {
        printf("内存分配失败\n");
        return 1;
    }

    // 重新分配为包含10个整数的内存空间
    arr = (int *)realloc(arr, 10 * sizeof(int));

    if (arr == NULL) 
    {
        printf("内存重新分配失败\n");
        return 1;
    }

    // 打印数组元素
    for (int i = 0; i < 10; i++) 
    {
        printf("%d ", arr[i]);
    }
    // 释放动态分配的内存
    free(arr);
    arr=NULL;
    return 0;
}
3.3  realloc 和 malloc 区别

        malloc返回类型是 void 型指针,再根据实际需求进行强制类型转换,那么根据两个函数的定义就可以发现,当 realloc 函数第一个参数是空指针时那么就和 malloc 函数功能相同:

int* p = (int*)realloc(NULL,40);
3.4  realloc 函数存在的问题

        3.2 部分的示例代码首先使用 malloc 分配了包含5个整数的内存空间,然后使用 realloc 函数将内存空间重新分配为包含10个整数的空间。但是存在一个问题: 若 realloc 函数重新分配失败怎么办?会返回NULL,那么想重新分配的指针 arr = NULL,其原来指向的内存也不存在了。 

realloc 函数在重新分配内存空间时可能会返回 NULL,主要是由于以下几种情况导致的:

  1. 内存分配失败:当系统无法满足请求的内存空间大小时,realloc 函数会失败并返回 NULL。这通常发生在内存不足或者系统资源受限的情况下。

  2. 内存空间连续性不足:如果原内存块之后没有足够的连续空间来扩展到请求的大小,realloc 也会失败并返回 NULL。这种情况下,系统无法在原地扩展内存空间,需要将原内存块移动到新的位置,如果新位置无法提供足够的连续空间也会导致失败。

  3. 其他系统限制:某些系统可能会施加其他限制,例如内存碎片化程度过高、操作系统限制等,都有可能导致 realloc 返回 NULL

情况1:

当是情况1的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发⽣变化。

情况2: 

        realloc函数会在内存的堆区重新找一个空间(满足新的空间大小需求的),同时会把旧的数据拷贝到新的空间,然后释放旧的空间,同时返回新的空间的起始地址。

由于上述的两种情况,realloc函数的使⽤就要注意⼀些:

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* ptr = (int*)malloc(100);
	if (ptr != NULL)
	{
		//...
	}
	else
	{
		return 1;
	}
	//扩展容量
	//代码1 - 直接将realloc的返回值放到ptr中
	ptr = (int*)realloc(ptr, 1000);//如果申请失败会如何?

	//代码2 - 先将realloc函数的返回值放在p中,不为NULL,再放ptr中
	int* p = NULL;
	p = realloc(ptr, 1000);
	if (p != NULL)
	{
		ptr = p;
	}
    else
    {
        perror("realloc");
        return 1;
    }
	free(ptr);
	return 0;
}

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

(一) 对NULL指针的解引用操作

//可以先进行判断
#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(100);

	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	*p = 20;
	free(p);
	return 0;
}

(二) 对动态开辟空间的越界访问

(三)对非动态开辟内存使用free释放

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

使用 free 释放空间时给的不是起始地址。

(五)对同一块动态内存多次释放

(六)动态开辟内存忘记释放(内存泄漏)

        如下情况,在 test 函数中申请了100个字节的空间,并定义了p指向了这个空间。当函数调用结束后,函数定义的局部变量 p 随之销毁,同时也没有变量保存这块空间的地址,申请的这块内存空间后续将无法使用,因为不知道这块空间的地址。

#include<stdio.h>
#include<stdlib.h>
void test()
{
	int* p = (int*)malloc(100);
	if (NULL != p)
	{
		*p = 20;
	}
}
int main()
{
	test();
	while (1);
}

5、动态内存经典笔试题分析

题目一: 

#include<stdio.h>
#include<stdlib.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 函数中,参数 p 被传递为指针的副本,而不是指针本身。所以函数内部对 p 进行的内存分配不会影响到 Test 函数中原始指针 str 的值。当 GetMemory 函数返回时,分配的内存空间将丢失,因为只是修改了传递进来的副本指针 p 的值,而原始指针 str 仍然是 NULL。因此,strcpyprintf 函数中使用的 str 指针仍然是 NULL,对空指针解引用就会导致程序的崩溃。

修改后代码如下:

 题目二:

        分析上述代码。在 GetMemory 函数中,它尝试返回一个指向局部变量 p 的指针。然而,一旦 GetMemory 函数执行完毕并返回,p 所在的内存空间将被释放,因为它是一个自动变量(在栈上分配)。因此,当 Test 函数尝试使用从 GetMemory 返回的指针时,实际上它指向的是一个无效的内存位置,这可能导致未定义的行为。

可以在GetMemory函数中定义char p[ ]前加static,这样当执行完函数后这块空间仍然不会释放:

6、柔性数组 

6.1 补充 typedef 创建结构体

        使用 typedef 可以将复杂的数据类型简化为一个更容易记忆和理解的别名,使得代码更加清晰,减少出错的可能性。在 C 语言中,typedef 经常与结构体、枚举等复杂数据类型一起使用,方便在程序中定义新的类型名称。

        在如下代码中,通过 typedefstruct st_type 定义的结构体类型命名为 type_a,这样以后可以直接使用 type_a 来声明结构体变量,而不需要每次都写完整的 struct st_type。这种方式使得代码更加简洁明了,提高了代码的可读性和可维护性。

#include <stdio.h>

// 定义结构体 st_type
struct st_type 
{
    int i;
    char c;
};

// 使用 typedef 创建结构体别名 type_a
typedef struct st_type type_a;

int main() 
{
    // 声明一个结构体变量并赋初值
    type_a my_struct;
    my_struct.i = 10;
    my_struct.c = 'A';

    // 打印结构体变量的成员值
    printf("i: %d\n", my_struct.i);
    printf("c: %c\n", my_struct.c);

    return 0;
}
6.2 柔性数组的特点

        在 C 语言中,柔性数组(Flexible Array Member)是一种特殊的结构体成员,它允许在结构体的末尾定义一个长度不确定的数组。柔性数组通常用于动态分配内存,使得结构体可以容纳可变长度的数据。(1)在结构体中;(2)最后一个成员;(3)未知大小的数组。例如:

typedef struct st_type
{
	int i;
	int a[0];//柔性数组成员
}type_a;

有些编译器会报错⽆法编译可以改成:

typedef struct st_type
{
int i;
int a[];//柔性数组成员
}type_a;

(1)结构中的柔性数组成员前⾯必须至少⼀个其他成员。
(2)sizeof 返回的这种结构大小不包括柔性数组的内存。
(3)包含柔性数组成员的结构用 malloc() 函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

6.3 柔性数组的使用
#include <stdio.h>
#include <stdlib.h>

// 定义包含柔性数组的结构体
struct FlexArray 
{
    int length;
    int data[]; // 柔性数组,实际长度在运行时确定
};

int main() 
{
    int n = 5;

// 计算结构体大小并分配内存,前面的sizeof(struct FlexArray)分配给柔性数组前面的变量,后面的 n * sizeof(int)为柔性数组的大小
    struct FlexArray* flex = (struct FlexArray*)malloc(sizeof(struct FlexArray) + n * sizeof(int));

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

    flex->length = n;

    // 使用柔性数组存储数据
    for (int i = 0; i < n; i++) 
    {
        flex->data[i] = i * 2;
    }

    // 打印柔性数组中的数据
    printf("Data stored in flexible array:\n");
    for (int i = 0; i < flex->length; i++) 
    {
        printf("%d ", flex->data[i]);
    }

    // 释放内存
    free(flex);

    return 0;
}
6.4 柔性数组的优势 
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{
	int i;
	int* p_a;
}type_a;
int main()
{
	int i = 0;
	type_a* p = (type_a*)malloc(sizeof(type_a));
	p->i = 100;
	p->p_a = (int*)malloc(p->i * sizeof(int));
	//处理
	for (i = 0; i < 100; i++)
	{
		p->p_a[i] = i;
	}
	for (int i = 0; i < 100; i++) 
    {
        printf("%d ", p->p_a[i]);
    }
	//释放空间
	free(p->p_a);
	p->p_a = NULL;
	free(p);
	p = NULL;
	return 0;
}

假如将6.3中的柔性数组换成 int*的指针,同样可以实现相同的功能 ,但是柔性数组更有优势:

拓展:C语言结构体里的数组和指针 

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

标签:malloc,函数,int,realloc,C语言,内存,动态内存,NULL,重点
From: https://blog.csdn.net/2301_81723939/article/details/136863032

相关文章

  • C语言-扫雷游戏的简单实现
    文章目录扫雷游戏的简单实现1.初始化棋盘2.打印棋盘3.在棋盘中布置雷4.排查雷扫雷游戏的简单实现本篇博客采用了多文件的方式来实现扫雷游戏geme.h----------函数的声明及符号的定义game.c-----------函数的实现test.c-----------游戏的运行主体代码如下g......
  • 实验一 熟悉C语言运行环境
    c语言程序设计——实验报告一实验项目名称:实验一熟悉C语言运行环境实验项目类型:验证性实验日期:2023年3月14日一、实验目的下载安装Devc6.0程序。了解在该系统上如何进行编辑、编译、连接和运行一个C程序。|通过运行简单的C程序了解C程序的特点。二、实验硬、软件环境......
  • C语言-结构体类型的认识
    1.前言:        因为C语言中的基本数据类型很少,不能描述很多现实中复杂的事物了,所以就引入了结构体类型。比如要描述生活中的人,用什么类型呢?如果只有基本数据类型,答案是什么都用不了,用char?int?double?好像都不太行。因为你人有年龄,姓名,身高等等信息要描述,一种基本数据......
  • c语言程序设计-实验报告2
    实验项目名称:实验报告2-数据描述实验项目类型:验证性实验日期:2024年3月21日一、实验目的1、掌握C语言数据类型,熟悉如何定义一个整型、字符型和实型的变量,以及对它们赋值的方法。2、掌握不同数据类型之间赋值的规律。3、学会使用C的有关算术运算符,以及包含这些运算符的表......
  • 【保姆级讲解C语言中的运算符的优先级】
    ......
  • c语言基础(5)
    一、字符串函数        我们经常要处理字符和字符串,为了⽅便操作字符和字符串,C语⾔标准库中提供了⼀系列库函数,接下来我们就学习⼀下这些函数。1、字符分类函数    字符分类函数就是用来判断字符类型的函数。2、字符转换函数2.1 toupper    i......
  • c语言程序设计——实验报告二
    实验项目名称:实验报告2数据描述实验项目类型:验证性实验日期:2024年3月21日一、实验目的1、掌握C语言数据类型,熟悉如何定义一个整型、字符型和实型的变量,以及对它们赋值的方法。2、掌握不同数据类型之间赋值的规律。3、学会使用C的有关算术运算符,以及包含这些运算符的......
  • c语言程序设计——实验报告二
    实验项目名称:实验报告2数据描述实验项目类型:验证性实验日期:2024年3月21日一、实验目的1、掌握C语言数据类型,熟悉如何定义一个整型、字符型和实型的变量,以及对它们赋值的方法。2、掌握不同数据类型之间赋值的规律。3、学会使用C的有关算术运算符,以及包含这些运算符的......
  • c语言程序设计--实验报告二
    实验项目名称:实验报告2数据描述实验项目类型:验证性实验日期:2024年3月21日一、实验目的1、掌握C语言数据类型,熟悉如何定义一个整型、字符型和实型的变量,以及对它们赋值的方法。2、掌握不同数据类型之间赋值的规律。3、学会使用C的有关算术运算符,以及包含这些运算符的表达式。......
  • c语言程序设计——实验报告一
    实验项目名称:实验一熟悉C语言运行环境实验项目类型:验证性实验日期:2023年3月14日一、实验目的下载安装Devc6.0程序。了解在该系统上如何进行编辑、编译、连接和运行一个C程序。通过运行简单的C程序了解C程序的特点。二、实验硬、软件环境Windows计算机、Devc6.0三、实验......