C语言知识点---动态内存

发布于:2022-11-08 ⋅ 阅读:(390) ⋅ 点赞:(0)

目录

一、为什么存在动态内存分配

二、动态内存函数

2.1malloc函数和free函数

2.2calloc函数和free函数

2.3realloc函数:灵活管理动态内存

三、动态内存中常见的错误

3.1对NULL指针进行解引用操作

3.2对动态开辟空间的越界访问

3.3对非动态内存使用free函数

3.4使用free函数释放动态内存中的一部分

3.5对同一块开辟的内存空间重复释放

3.6开辟的动态内存空间忘记释放了

子函数中开辟栈区和堆区空间的问题

C/C++程序内存分配的几个区域

四、柔型数组(flexible array)

4.1柔型数组的定义:

4.2柔型数组的特点

4.3柔型数组的使用

4.4柔型数组的优点:


一、为什么存在动态内存分配

我们以往都是采用使用创建变量或者数组的方式开辟空间,但是这种方式往往不太灵活,无法达到对内存空间的任意使用,为了达到这种目标,我们采用动态内存分配的方式。

动态内存开辟都是在堆区开辟的;

二、动态内存函数

2.1malloc函数和free函数

C语言提供动态内存开辟的函数

malloc函数:allocates memory blocks

头文件:<stdlib.h>

malloc函数:void* malloc(size_t size)   //函数声明

参数解释:

size:开辟的字节数

void*:返回一个无类型的指针,指向开辟的空间,或者当堆区没有可用空间时,返回一个空指针

函数的使用:开辟10个整型的空间

采用数组的方式: int arr1[10];  //开辟在栈区的空间

采用动态存开辟的方式:void* p=malloc(10*sizeof(int))

malloc函数返回的指针类型是void*

如果要使用,应当根据自己的需要进行相应的强制类型转换

用于整型:int* p=(int*)malloc(10*sizeof(int))  //最好对malloc进行强制类型转换

使用这些空间前应该先判断指针是否指向空指针,指向空指针就报错

开辟的内存空间的使用--采用指针+偏移量的方法

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* pm = (int*)malloc(10 * sizeof(int));
	if (pm == NULL)
	{
		perror("main:");
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(pm + i) = 10 - i;
		printf("%d ", *(pm + i));
	}
	return 0;
}

free函数:专门用来动态内存的回收与释放

free函数:void free (void* ptr)

头文件:<stdlib.h>

使用完该空间,使用free函数释放空间,把空间还回去

free函数的参数是刚刚开辟的指针,但是没有并没有把该指针的内容置为空指针

需要手动将该指针置空

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* pm = (int*)malloc(10 * sizeof(int));
	if (pm == NULL)
	{
		perror("main:");
	}
	int i = 0;
	if(pm!=NULL)  //加一个判断可以取消警告:取消对空指针的引用
	{
		for (i = 0; i < 10; i++)
		{
			pm[i] = 10 - i;
			printf("%d ", *(pm + i));
		}
	}
	free(pm);     //传递的是pm的值,不可能改变pm的内容,传递其地址倒是有可能
	pm = NULL;   //自己动手把指针置空,防止以后使用pm指针,导致非法越界访问
	return 0; 
}

注意:

1.如果 free(ptr) 中ptr不是动态内存开辟的,那么free的行为就是未被定义的行为

2..如果 free(ptr) 中ptr是空指针,那么free什么也不做

3.malloc函数和free函数要配合使用,成对出现

2.2calloc函数和free函数

calloc函数:动态内存分配

头文件:<stdlib.h> and <malloc.h>                   //两者之一即可?

allocate cates an array in memory with elements initialized to 0

开辟一个元素初始化为0的数组

calloc函数:void* calloc(size_t num,size_t size)

参数解释:

1.为num个大小为size字节的元素开辟一块空间,并把空间的每个字节初始化为0

2.与malloc函数不同的是:calloc函数再返回地址之前会把申请的空间的每个字节全部初始化为0

注意:calloc函数也是和free函数搭配使用

2.3realloc函数:灵活管理动态内存

realloc函数可以做到对动态开辟内存大小的调整

头文件:<stdlib.h> and <mallo.h>

realloc函数:void* realloc(void* ptr,size_t size)

参数解释:

1.ptr是要调整的内存地址

2.size调整后的大小

3.返回值为调整后的内存起始位置

4.函数在调整原来内存空间大小的基础上,会将原来内存中的数据移动到新的空间

5.realloc函数在调整内存空间时,存在两种情况

5.1原有空间之后有足够的空间去开辟填补的空间,此时新的空间首地址不变

5.2原有空间之后的空间不足以开辟填补的空间,此时,系统会在其他地址新开辟一个空间,

如果其他地址也不足开辟新的空间,realloc函数会返回一个空指针

所以为了保护原有空间的数据,先定义一个临时变量,接受realloc函数返回的指针

对此临时变量进行判断,倘若不为空指针,就将该地址赋给指向原空间的指针

代码演示如下:

int* ptr = (int*)realloc(pm, 10 * sizeof(int));
	if (ptr != NULL)
	{
		pm = ptr;
	}

注意:当realloc函数单独使用时,效果与malloc效果类似

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* pm = realloc(NULL, 40);
	for (int i = 0; i < 10; i++)
	{
		pm[i] = i;
		printf("%d\n", pm[i]);
	}
	free(pm);     
	pm = NULL;  
	return 0;
}

利用以上函数可以实现动态内存版本的通讯录小程序

三、动态内存中常见的错误

3.1对NULL指针进行解引用操作

当开辟内存失败的的时候,函数会返回一个空指针,此时对该指针解引用会导致非法访问内存;

所以对malloc函数的返回值做判断,如果不为空指针,再做接下来的处理

3.2对动态开辟空间的越界访问

#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    //printf("\033[1;5;47;34mhello\033[0m\n");

    int* p = (int*)malloc(10 * sizeof(int));
    if (p == NULL)
    {
        perror("main:");
    }
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        p[i] = i;
        printf("\033[1;5;40;34m%d\033[0m\n", p[i]);
    }
    free(p);
    p = NULL;
    return 0;
}

指针p的内容是新开辟空间的首地址,对p解引用会得到该空间的数据

p来维护动态内存开辟的这一空间

申请了40个字节的动态空间,却使用了40*4=160个字节,会导致越界访问

3.3对非动态内存使用free函数

一定要对动态开辟的内存使用free函数,不然会导致程序崩溃

3.4使用free函数释放动态内存中的一部分

动态内存空间使用完后,要释放掉整个的内存空间,不可以部分释放

int* p = (int*)malloc(10 * sizeof(int));
    if (p == NULL)
    {
        perror("main:");
    }
    int i = 0;
    for (i = 0; i < 5; i++)
    {
        *p++ = i;
    }
    free(p);
    p = NULL;
    return 0;

函数在运行后,p的指针指向的位置发生了变化

这样做有两个风险:

1.p指针指向的位置发生了变化,释放指针时可能会导致部分释放

2.丢失了p本来指向的位置,可能会忘记这块空间的起始位置,可能会导致内存泄露的情况

3.5对同一块开辟的内存空间重复释放

重复释放同一块开辟的内存空间会导致程序崩溃

但是当第一次释放完指针以后,将该指针置空,就算重复释放也不会出现问题;

3.6开辟的动态内存空间忘记释放了

对于开辟的内存空间要记得释放,特别是在子函数中,不然会导致内存泄露的问题

内存泄漏(memory leak):开辟的堆区的动态内存空间,因为种种原因未被释放或无法释放,造成系统内存浪费,减慢系统运行速度,甚至会使系统崩溃;

动态内存开辟的空间只有两种释放方式

1.主动释放:使用free函数释放

2.程序结束:当程序结束时,申请的内存空间都返回给系统

例子:

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

void test()
{
    int* ptr = (int*)malloc(100);
    if (ptr == NULL)
    {
        return;
    }
}
int main()
{
    //printf("\033[1;5;47;34mhello\033[0m\n");

    test();  //子函数执行完后,会丢失ptr的数据
    return 0;
}

程序在子函数中开辟了一块内存空间,整型指针指向该内存空间

但是子函数执行完后,ptr 指针的生命周期也就结束了,这也就意味着找不到了之前开辟的内存空间,会造成系统内存浪费,导致内存泄露的问题【重启可以解决】

子函数中开辟栈区和堆区空间的问题

分析如下代码:

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

char* getmemory(char* pm)
{
    pm = (char*)malloc(15);
    return pm;
}
int main()
{
    //printf("\033[1;5;47;34mhello\033[0m\n");
    char* pm = NULL;
    pm = getmemory(pm);   //传递的参数是pm的值,相当于是值传递,无法改变pm的值
    strcpy(pm, "hello world!");
    printf(pm);
    free(pm);
    pm = NULL;
    return 0;
}

在getmemory函数中,pm指向的是在堆区开辟的动态空间,在getmemory函数结束后,其内存空间不会释放,等待被主动释放或者程序结束

#include <string.h>

char* getmemory(void)
{
    char pm[] = "hello world!";
    return pm;
}
int main()
{
    //printf("\033[1;5;47;34mhello\033[0m\n");
    char* pm = NULL;
    pm = getmemory();   //传递的参数是pm的值,相当于是值传递,无法改变pm的值
    strcpy(pm, "hello world!");
    printf(pm);
    return 0;
}

在这个代码的getmemory函数中,pm是字符数组名,指向的是在栈区开辟的空间,存放的是常量字符串“hello world!”,但是当getmemory函数运行结束后,栈区的空间被释放(意味着常量字符串被销毁),此时pm指向的地址已经过时了,返回的地址没有实际意义,此时,如果通过返回的地址去访问内存,会造成非法访问内存的问题;

定义指针时应初始化,赋空值或其他值,不然会导致野指针的问题

这类问题统称为返回栈空间地址的问题

注意:栈区和堆区的生命周期不同,使用时应注意;

C/C++程序内存分配的几个区域

内存的划分并不是只有栈区、堆区、静态区这三个区简单地划分

下图是C/C++程序内存分配的几个区域:

1.栈区(stack):

在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,打爆事故分配的内存容量有限。栈区主要运行函数而分配的局部变量、函数参数、返回数据、返回地址等;

 2.堆区(heap):

一般由程序员分配释放,若程序员不能释放,程序结束时可由OS回收,分配方式类似于链表

3.数据段(又名静态区)(static):

存放全局变量、静态数据,程序结束后由系统释放

4.代码段:
存放函数体(类成员函数和全局函数)的二进制代码

四、柔型数组(flexible array)

C99中,结构中最后一个元素允许是未知大小的的数组,这就叫做柔性数组成员

4.1柔型数组的定义:

struct S
{
    int n;
    int arr[];  //大小是未知
    //或者写成
    //int arr[0];
};

4.2柔型数组的特点

1.结构体中柔型数组的前面必须至少有一个其他成员

2.sizeof操作符返回的结构体的大小不包括柔型数组的内存

3.包含柔型数组成员的数组结构用malloc函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔型数组预期的大小

4.3柔型数组的使用

使用malloc函数来为结构体开辟动态内存空间

如果空间不够用就使用realloc函数扩展空间

使用代码示例:

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


struct S
{
    int n;
    int arr[];  //大小是未知
    //或者写成
    //int arr[0];
};

int main()
{
    struct S* pm = malloc(sizeof(struct S) + 10 * sizeof(int));
    if (pm == NULL)
    {
        return;
    }
    pm->n = 10;
    for (int i = 0; i < 10; i++)
    {
        pm->arr[i] = i;
        printf("%d\n", pm->arr[i]);
    }
    struct S* ptr = (struct S*)realloc(pm,sizeof(struct S) + 15 * sizeof(int));
    if (ptr != NULL)
    {
        pm = ptr;
    }
    for (int i = 0; i < 5; i++)
    {
        pm->arr[10 + i] = 10 + i;
        printf("%d\n", pm->arr[10+i]);
    }
    free(pm);
    pm = NULL;

    return 0;
}

注意:结构体指针pm指向的是动态开辟的内存空间,如果柔型数组空间不够用的话,可以使用realloc函数申请扩容内存空间,pm指向的依然还是动态开辟的内存空间;

但是如果将结构体设计成以下形式:

struct S
{
    int n;
    int* pm;
};

如果想实现柔型数组的功能,需要两次动态开辟内存空间,一次给该结构体,另外一次是给指针pm申请的,如果pm指向的空间不够,再使用realloc函数扩容,最后释放内存空间时,需要先释放pm指向的内存空间,再释放结构体的动态内存空间;但是这种方式容易产生内存碎片,没有荣幸数组的方式简便高效;

4.4柔型数组的优点:

1.方便内存释放:只需要释放一次就就可以了

2.有利于来提高访问速度:连续的内存有利于提高访问速度,也有益于减少内存碎片;

特别鸣谢:哔哩哔哩比特鹏哥视频教程

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到