动态内存管理、柔性数组

动态内存分配的由来

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

cs 复制代码
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
  1. 上面开辟的空间大小是固定不可变的
  2. 数组申请的大小也是确定了之后就不能改变

这里就引入了动态内存的开辟,可以根据内存的需要进行更改

动态内存函数的介绍

malloc和free

cs 复制代码
 void* malloc (size_t size);
  1. malloc的功能是申请size个连续可用size_t类型的字节空间,并返回指向这块空间的void*类型的指针
  2. 如果开辟失败,则返回⼀个 NULL 指针,因此malloc的返回值⼀定要做检查。
  3. 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使⽤的时候使⽤者⾃⼰来决定。
  4. 如果参数 size 为0,malloc的⾏为是标准是未定义的,取决于编译器。

在稍微了解了malloc后还不能直接上代码,因为动态内存申请后需要进行释放,我们先学习一下free

free

cs 复制代码
void free(void* ptr);
  1. free函数⽤来释放动态开辟的内存。
  2. 如果参数 ptr 指向的空间不是动态开辟的,那free函数的⾏为是未定义的。
  3. 如果参数 ptr 是NULL指针,则函数什么事都不做。
  4. malloc和free都声明在 stdlib.h 头⽂件中。

看一段代码

cs 复制代码
#include<stdio.h>
#include<stdlib.h>
int main()
{
	//开辟40个字节/10个整形大小空间
	//int* p = (int *)malloc(40);
	int* p = (int*)malloc(10*sizeof(int));

	//检查空间是否开辟成功
	if (p == NULL)
	{
		perror("malloc");
		return 1;//异常退出
	}

	//打印出来
	for (int i = 0; i < 10; i++)
	{
		printf("%d\n", *(p + i));
	}

    free(p);//释放p指向的动态内存
    p=NULL;//内存释放掉了但是还是指向这块空间

	return 0;
}

结果如下

  1. 发现结果是随机值
  2. 调试查看p的地址,发现free前后p的地址并没有发生改变,p还是指向那个地址,所以释放后这里p就是野指针,在进行解引用就是非法访问了,结论就是free之后我们还需要手动将p置为空指针

对于melloc和free总结为

1.malloc为你申请一定大小的空间,成功则返回void*的指针,失败则返回空指针

2.由于返回的是void*,所以需要强制转换一下才可以使用;

3.在使用完之后要使用free手动释放空间;

4.释放后要将旧指针置为空指针;

calloc

cs 复制代码
void* calloc (size_t num, size_t size);
  1. 函数的功能是为 num 个⼤⼩为 size 的元素开辟⼀块空间,并且把空间的每个字节初始化为0。malloc是直接申请所有字节。
  2. calloc与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0,所以如果申请内存空间要求初始化,那么使用calloc是很好的选择。

举个例子

cs 复制代码
#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* p = (int*)calloc(10, 4);
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	for (int i = 0; i < 10; i++)
	{
		printf("%d\n", *(p + i));
	}
	free(p);
	p = NULL;
	return 0;
}

程序结果如下

realloc

cs 复制代码
void* realloc (void* ptr, size_t size);
  1. 如果在用malloc或者calloc时,发现空间大了或者小了,为了合理分配内存会使用realloc来修改内存大小。
  2. ptr是需要调整的动态内存首元素地址,size是调整后新的大小。
  3. 返回是void*类型,会返回修改后的内存起始地址。
  4. 需要注意接受修改后的地址不能用原来的ptr接受,如果申请失败会把修改之前的数据也丢失变成NULL。需要创建一个新的指针变量
  5. realloc在调整内存空间的是存在两种情况:
    ◦ 情况1:原有空间之后有⾜够⼤的空间,直接在后面加上size个字节,返回指向原有空间起始位置的指针。
    ◦ 情况2:原有空间之后没有⾜够⼤的空间,会在内存中申请一块新的地方,将旧空间里的数据复制到新的空间,并在新空间的末尾加上size个字节,返回指向新空间起始位置的指针。(可以通过调试修改size大小对比发现区别)

举个例子

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

int main()
{
	//开辟10个整型空间
	int* p = (int*)calloc(10, sizeof(int));
	//如果开辟失败打印错误信息
	if (p == NULL)
	{
		perror("calloc");
        return 1;
	}
	//假设我们要扩容到20个整型空间
	int* str = (int*)realloc(p, 20 * sizeof(int));
    //执行任务
	if (str != NULL)
	{
    //不为空指针才使用,不然会内存泄漏
		p = str;
		str = NULL;

	}
	free(p);
	p = NULL;
	return 0;
}

关于realloc还有一点需要补充的是如果给realloc传递的指针为NULL,那么此时realloc的功能和malloc一样。此时realloc相当于malloc。

例如:

int* p=(int* )realloc(NULL,10*sizeof(int));

典型的动态内存错误

对NULL指针解应用

cs 复制代码
void test()
{
    int *p = (int *)malloc(INT_MAX/4);
    *p = 20;//如果p的值是NULL,就会有问题
    free(p);
}

对动态开辟空间越界访问

cs 复制代码
void test()
{
    int i = 0;
    int *p = (int *)malloc(10*sizeof(int));
    if(NULL == p)
    {
        exit(EXIT_FAILURE);
    }
    for(i=0; i<=10; i++)
    {
        *(p+i) = i;//当i是10的时候越界访问
    }
    free(p);
}

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

cs 复制代码
void test()
{
    int a = 10;
    int *p = &a;
    free(p);//error
}

使用free释放部分动态空间

cs 复制代码
void test()
{
    int *p = (int *)malloc(100);
    p++;
    free(p);//p不再指向动态内存的起始位置
}

对一块动态内存多次释放

cs 复制代码
void test()
{
    int *p = (int *)malloc(100);
    free(p);
    free(p);//重复释放
}

忘记释放动态内存

cs 复制代码
void test()
{
    int *p = (int *)malloc(100);
    if(NULL != p)
    {
        *p = 20;
    }
}
int main()
{
    test();
    while(1);
}

动态内存经典笔试题分析

题目1

cs 复制代码
void GetMemory(char *p)
{
    p = (char *)malloc(100);
}
void Test(void)
{
    char *str = NULL;
    GetMemory(str);
    strcpy(str, "hello world");
    printf(str);
}

这段代码中的问题是在GetMemory函数中,试图分配内存给指针p,但它实际上并未改变传入的指针str的值。因为C语言中的函数参数传递是按值传递的,所以当你传递一个指针给函数时,实际上是传递了这个指针的拷贝。这意味着在GetMemory函数内部,你修改的是拷贝的指针,而不是原始的str指针

为了解决这个问题,你可以使用指针的指针。这样,你就可以在函数内部修改指针的值,从而影响到函数外部的指针。以下是修改后的代码:

cs 复制代码
#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); // 注意这里使用了 & 运算符来传递 str 的地址
    strcpy(str, "hello world");
    printf("%s\n", str);
    free(str); // 不要忘记释放内存!
}

int main() 
{
    Test();
    return 0;
}
复制代码

题目2

cs 复制代码
char *GetMemory(void)
{
    char p[] = "hello world";
    return p;
}
void Test(void)
{
    char *str = NULL;
    str = GetMemory();
    printf(str);
}

GetMemory函数中,创建了一个字符数组p,并试图返回其地址。然而这个数组是在函数内部创建的,所以当函数返回时,该数组的内存将被释放,返回的指针将变得无效。这会导致未定义的行为。所以str接收到的是野指针修改就是在static char p[]="hello world";延长生命周期。

题目3

cs 复制代码
void GetMemory(char **p, int num)
{
    *p = (char *)malloc(num);
}
void Test(void)
{
    char *str = NULL;
    GetMemory(&str, 100);
    strcpy(str, "hello");
    printf(str);
}

题目4

cs 复制代码
void Test(void)
{
    char *str = (char *) malloc(100);
    strcpy(str, "hello");
    free(str);
    if(str != NULL)
    {
        strcpy(str, "world");
        printf(str);
    }
}

柔性数组

C99中,结构中的最后⼀个元素允许是未知⼤⼩的数组,这就叫做【柔性数组】成员。

cs 复制代码
typedef struct st_type
{
    int i;
    int a[];//或者a[0] 柔性数组成员
}type_a;
int main()
{
    printf("%d\n", sizeof(type_a));//输出的是4
    //sizeof返回的这种结构⼤⼩不包括柔性数组的内存。
    return 0;
}

柔性数组的特点:

• 结构中的柔性数组成员前⾯必须⾄少⼀个其他成员。

• sizeof返回的这种结构⼤⼩不包括柔性数组的内存。

• 包含柔性数组成员的结构⽤malloc()函数进⾏内存的动态分配,并且分配的内存应该⼤于结 构的⼤⼩,以适应柔性数组的预期⼤⼩。

柔性数组的使用

cs 复制代码
int main()
{
	type_a* p = (type_a*)malloc(sizeof(type_a) + sizeof(int) * 10);
//这样相当于在原来基础上加了40个字节在柔性数组上
	p->a = 10;
    //业务处理
	for (int i = 0; i < 10; i++)
	{
		p->arr[i] = i;
	}
	printf("%d \n", p->a);
    
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", p->arr[i]);
	}
	return 0;
}

柔性数组的优势

cs 复制代码
typedef struct st_type
{
	int a;
	int * arr;
}type_a;

int main()
{
	type_a* p = (type_a*)malloc(sizeof(int));
	p->a = 10;
	p->arr = (int*)malloc(10*sizeof(int));

	for (int i = 0; i < 10; i++)
	{
		p->arr[i] = i;
	}
	printf("%d \n", p->a);
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", p->arr[i]);
	}
    //释放空间
    free(p->p_a);
    p->p_a = NULL;
    free(p);
    p = NULL;
	return 0;
}

第⼀个好处是:⽅便内存释放

如果我们的代码是在⼀个给别⼈⽤的函数中,你在⾥⾯做了⼆次内存分配,并把整个结构体返回给⽤⼾。⽤⼾调⽤free可以释放结构体,但是⽤⼾并不知道这个结构体内的成员也需要free,所以你不能指望⽤⼾来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返回给⽤⼾⼀个结构体指针,⽤⼾做⼀次free就可以把所有的内存也给释放掉。
第⼆个好处是:这样有利于访问速度

连续的内存有益于提⾼访问速度,也有益于减少内存碎⽚。(其实,我个⼈觉得也没多⾼了,反正你跑不了要⽤做偏移量的加法来寻址

c/c++中内存函数的分配

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

  1. 栈区(stack):在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内存容量有限。栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。

  2. 堆区(heap):⼀般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配⽅式类似于链表。

  3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。

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

相关推荐
测试界萧萧2 分钟前
外包干了4年,技术退步太明显了。。。。。
自动化测试·软件测试·功能测试·程序人生·面试·职场和发展
百事老饼干13 分钟前
Java[面试题]-真实面试
java·开发语言·面试
可均可可35 分钟前
C++之OpenCV入门到提高004:Mat 对象的使用
c++·opencv·mat·imread·imwrite
杨荧1 小时前
【JAVA毕业设计】基于Vue和SpringBoot的服装商城系统学科竞赛管理系统
java·开发语言·vue.js·spring boot·spring cloud·java-ee·kafka
白子寰1 小时前
【C++打怪之路Lv14】- “多态“篇
开发语言·c++
小芒果_011 小时前
P11229 [CSP-J 2024] 小木棍
c++·算法·信息学奥赛
gkdpjj1 小时前
C++优选算法十 哈希表
c++·算法·散列表
王俊山IT1 小时前
C++学习笔记----10、模块、头文件及各种主题(一)---- 模块(5)
开发语言·c++·笔记·学习
为将者,自当识天晓地。1 小时前
c++多线程
java·开发语言
-Even-1 小时前
【第六章】分支语句和逻辑运算符
c++·c++ primer plus