我眼中的‘C’——动态内存+柔型数组

Posted 陈大大陈

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了我眼中的‘C’——动态内存+柔型数组相关的知识,希望对你有一定的参考价值。

目录

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

柔性数组方案

柔性数组的特点

结构中指针方案

两种方案那个比较好?

 

😎博客昵称:陈大大陈

😊座右铭:所谓觉悟,就是在漆黑的荒野上开辟出一条理当前进的光明大道。

😋博主简介:一名热爱C/C++和算法等技术,喜欢运动,爱胡思乱想却胸怀大志的小博主!

😚博主&唠嗑:早午晚哈喽Ciao!😄各位CSDN的朋友!😄我是博客新人陈大大陈,希望我的文章能为你带来帮助!欢迎大家在评论区畅所欲言!也希望大家多多为我提出您宝贵的建议!😘如果觉得我写的不错的话还请点个赞和关注哦~😘😘😘

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

c语言的内存区域可以划分为5个区——内核空间,栈,内存映射段,堆,数据段和代码段

请看我作的图。 

在学习计算机语言时,我们在语言层面一般把内存划分为下面几个区域——栈区,堆区和静态区

栈区对应上面的栈,堆区对应上面的堆,而静态区所对应的是静态段。

 内存中有一段区域是不允许用户来使用的,这块区域就是内核空间,内核空间是留给操作系统来使用的,用户代码是不能读写的。

我们写的代码编译之后会留下汇编指令,这些指令需要存储起来才能正常运行,储存这些指令的地方就是代码段了。

  • 栈区(stack):主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。如函数执行时,函数的形参以及函数内的局部变量是分配在栈区的,函数运行结束后,形参和局部变量去栈(自动释放)。栈内存分配运算内置与处理器的指令集中,效率高但是分配的内存空间有限。上面代码里红框的部分全都存储在栈区。
  • 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由操作系统回收 。分配方式类似于链表。
  • 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
  • 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

ptr1指向一段空间,这块空间是由malloc来申请的,malloc申请的空间在堆区储存,ptr1是维护那段空间的,它所储存的地址所指向的空间存储在堆区,但是ptr1本身是局部变量,存储在栈区。calloc和realloc同理。

柔性数组方案

下面两个都是柔型数组:

struct S

	int n;
	char c;
	int arr[];
;
struct S

	int n;
	char c;
	int arr[0];
;

需要注意的是,第二种有的编译器会报错。

柔性数组的特点

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

也就是说,下面这样的写法是绝对不行的:

struct S

	int arr[];
;

2.sizeof返回的带有柔型数组的结构体的大小不包括柔型数组。

#include<stdio.h>
struct S

	int n;
	char c;
	int arr[];
;
int main()

	printf("%d", sizeof(S));
	return 0;

 我们已经了解了最大对齐数,n和c一共是5字节,但是结构体大小必须是最大对齐数的整数倍,int的对齐数是4,char的对齐数是1,所以浪费三个字节,结构体大小为8,柔型数组的大小并没有被计算在里面。

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

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
struct S

	int n;
	char c;
	int arr[0];
;
int main()

	struct S* ps = (struct S*)malloc(sizeof(struct S) + sizeof(int) * 10);
	if (ps == NULL)
	
		printf("%s\\n", strerror(errno));
		return 1;
	
	ps->n = 100;
	ps->c = 'w';
	int i = 0;
	for (i = 0; i < 10; i++)
	
		ps->arr[i] = i;
		printf("%d ", ps->arr[i]);
	
	struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 20 * sizeof(int));
	if (ptr == NULL)
	
		printf("%s\\n", strerror(errno));
		return 1;
	
	else
	
		ps = ptr;
	
	free(ps);
	ps = NULL;
	
	return 0;

柔性数组的柔性就体现在这里,如果我们将结构体中的数组大小定义成一个常量,那么它在一次程序的运行中是不可修改的,但是如果我们用柔性数组来定义的话,就可以动态地用realloc来修改。

需要注意的是,realloc的后一个参数不是要扩容的大小,而是算上本来就有的空间之后的新大小,这一点比较容易搞混。

结构中指针方案

我们也可以这样模拟实现

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
struct S

	int n;
	char c;
	int *arr;
;
int main()

	struct S* ps = (struct S*)malloc(sizeof(struct S));
	if (ps == NULL)
	
		perror("malloc");
		return 1;
	
	
	int* ptr = (int *)malloc(10*sizeof(int));
	if (ptr == NULL)
	
		perror("malloc2:");
		return 1;
	
	else
	
		ps->arr = ptr;
	
	ps->n = 100;
	ps->c = 'w';
	int i = 0;
	for (i = 0; i < 10; i++)
	
		ps->arr[i] = i;
	
	for (i = 0; i < 10; i++)
	
		printf("%d ", ps->arr[i]);
	
    ptr = (int*)realloc(ps->arr, sizeof(int) * 20);
	if (ptr == NULL)
	
		perror("realloc");
		return 1;
	
	else
	
		ps->arr = ptr;
	
	free(ps->arr);
	ps -> arr = NULL;
	free(ps);
	ps = NULL;


	return 0;

这种写法可以模拟实现上面柔型数组的功能,我们称它为结构中指针方案。

那么结构中指针和柔型数组这两个方案到底哪一个比较好呢?

两种方案那个比较好?

如果我们用柔型数组的话,程序里面总共malloc一次,free一次。

如果用结构中指针的话,则是malloc两次,free两次。

这两个方案可以完成同样的功能,但是柔型数组的实现方式有两个好处

第一个好处是:内存的使用率会高一些

malloc次数少,内存之间的内存碎片就会少一些,内存的使用率就会相应高一些。malloc次数多,维护难度增大,容易出错。

第二个好处是:访问速度会提高

柔型数组是一片连续的空间,访问速度理所应当地会快一些,而结构中指针空间之间可能不连续,我们在访问内存的时候,可能不是直接去内存里面拿,而是先去寄存器里找,寄存器没有再找内存要,如果空间连续,后面空间的内容可能会加载到寄存器里面,那么访问速度就会加快,而如果空间不连续,先去寄存器找却没找到,这么一来效率就会降低。

这篇博客旨在总结我自己阶段性的学习,要是能帮助到大家,那可真是三生有幸!😀如果觉得我写的不错的话还请点个赞和关注哦~我会持续输出编程的知识的!🌞🌞🌞 

dynamic_memory_allocation(动态内存分配)

Dynamic Memory Allocation(动态内存分配)

1.为什么存在动态内存分配

2.动态内存函数的介绍

malloc

free

calloc

realloc

3.常见的动态内存错误

4.几个经典的笔试题

5.柔型数组



在开始之前,我们需要回顾一下 我们当前所掌握的 内存使用方法

当前我们知道的内存使用方法

1. 创建一个 变量

int a =10;// (假设)局部变量 - 栈区

int g_a =10; // (假设)全局变量 - 静态区


2. 创建 一个数组

局部变量 - 栈区

全局变量 - 静态区



程序一:

#include<stdio.h>

struct s // 班级成员信息
{
	char name[20];
	int age;
};

int main()
{
	struct s arr[50];// 50 个 struct s 类型的数据
	// 但是如果班级没50人,那么就意味着要浪费空间
	// 反之班级人数超过50个,空间就不够用

	// 如果 在前面加上 int n =0; scanf("%d",&n); n 取代 50 的位数行不行呢?
	// 答案不行 因为 n 是一个变量, 而数组的元素个数应该给一个常量


	return 0;
}

特点:

1. 空间开辟大小是固定的

2,数组 在申明 的时候,必须制定数组的长度,它所需要的的内存 在 编译时 分配

C语言是可以创建变长数组 - c99 中增加了(vs不支持,gcc支持 c99标准 -> gcc test.c - std = c99)


下面正式进入正文:

为什么存在动态内存分配?

对于空间的需求,不仅仅是上述的情况,有时候我们需要的空间大小在程序运行的时候才知道

那数组的编译时开辟空间的方式就不能满足了,这时候就只能试试动态开辟了(在堆上申请空间)




动态内存函数的介绍

malloc 和 free

malloc :void* malloc(size_t size); // size 开辟的空间大小,单位字节; void* 是一个指针, 指向的是开辟的空间的起始地址

free:void free(void* memblock); // memblock - 内存块


程序二:

#include<stdio.h>
#include<stdlib.h>// malloc 所在头文件
#include<errno.h>// errno,h 错误码库
int main()       // 与 函数 strerror(错误信息报告)  搭配使用,将错误码转化为错误信息,并返回它的地址
{
	// 我想向内存申请 10个 整形的 空间
	int* p = (int*)malloc(/*INT_MAX*/10 * (sizeof(int)));// 因为  malloc 函数 返回的是 万能*(无类型)指针,所以需要转化类型
	// malloc 函数 也有 开辟空间失败的时候,
	// 比如 内存 只有 4 个 g ,我要开辟 8 g 空间,这肯定是会失败的,返回 空指针 NULL
	
	if (p == NULL)
	{
		// 找出打印错误原因的方式
		printf("%s\\n", strerror(errno));// malloc(INT_MAX)
		                //输出 Not enough space
	}
	else
	{
		// 正常使用 空间
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			*(p + i) = i;// 赋值 0 1 2 3 4 5 6 7 8 9
		}
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i));// 输出 0 1 2 3 4 5 6 7 8 9
		}
		printf("\\n");
	}
	// 当动态申请的空间 不再使用的时候
	// 就会把空间 还给 操作系统
	// 这时候 就会用到 free 函数
	return 0;
}


说到 free 函数, free 函数 是专门用来做 动态内存 的 释放 和 回收 的


程序三:

#include<stdio.h>
#include<stdlib.h>// malloc,free 所在头文件
#include<errno.h>
int main()
{
	// 我想向内存申请 10个 整形的 空间
	int* p = (int*)malloc(10 * (sizeof(int)));
	if (p == NULL)
	{
		// 找出打印错误原因的方式
		printf("%s\\n", strerror(errno));// malloc(INT_MAX)
		                //输出 Not enough space
	}
	else
	{
		// 正常使用 空间
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			*(p + i) = i;// 0 1 2 3 4 5 6 7 8 9
		}
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i));// 0 1 2 3 4 5 6 7 8 9
		}
		printf("\\n");
	}
	// 当动态申请的空间 不再使用的时候
	// 就会把空间 还给 操作系统
	// 这时候 就会用到 free 函数
	free(p);// 用完了再释放,也就是说 在打印完之后(调用完之后),再释放(p的值没有改变,还可以通过它找到该空间)
	//  虽然当前空间 不属于当前程序,但是依然 可以通过 p  找到  该空间,很有可能会破坏这个空间
	//  所以先在这个 指针 依然很危险

	p = NULL; // free函数 释放完空间后,不会改 p 的值,所以我们自己主动改
	return 0;
}
// 最后请注意:free 是用来 释放 动态开辟的内存

// free 函数 的 特殊情况:
//1. 如果参数 ptr 指向的空间不是动态开辟的,那 free 函数的行为是未定义的
//2. 如果参数 ptr 是 NULL 指针,则函数 什么 都不做。

为了方便你们进一步了解 free 函数

举个 special 例子:

你呢,跟你 girlfriend 前期 相处非常好,你把女友电话记得牢牢的,有助于交流。

但是,有一天 你女朋友看不上你了,就 free§ [ 跟你分手了 ]

但是你(p)还死皮赖脸的记住你前女友的号码(动态开辟空间的地址),随时可能打电话骚扰她,并且找到她,影响她的生活,说明你很危险(访问该空间内容,并修改,该指针很危险)

这时候,你前女友为了你能断开念想 (其实保护自己 == 维护程序安全)

你前女友 给你当头一棒(爆头),让你失忆了。(p = NULL)

我只能说 杀人诛心!!!(还好我单身,暂时不用担心被爆头的危险。)




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

num 元素个数,size每个元素的长度【大小】(字节)

 

1. 函数的功能是:开辟一块空间 num*size ( num 个 大小为 size 的 元素) ,并且把空间的每个字节初始化为 0

2.与函数 malloc 的区别 只在于 calloc 会在返回 地址之前 把申请的空间的 每个字节初始化为全0


程序四:

 用法跟 malloc 函数差不多
#include<stdio.h>
#include<errno.h>
#include<string.h>
int main()
{                     // 开一块空间,空间大小为 10 * int == 40 字节
	int* p = (int*)calloc(10, sizeof(int));// 比 malloc 函数 多一个元素个数 参数
	if (p == NULL)    
	{
		printf("%s\\n", strerror(errno));
	}
	else
	{
		int i = 0;
		for (i = 0; i < 10; i++)// 因为 p 为整形指针变量,指向一个 int 类型 元素,所以有 10个 int 元素
		{
			printf("%d ", *(p + i));// 0 0 0 0 0 0 0 0 0 0
		}  //calloc 在开辟好空间后,会把空间的所有字节内容全部初始化为 零
	}
	free(p);
	p = NULL;// 狗头拿来!!
	return 0;
}


realloc 函数 : void* realloc (void* memblock,size_t size);

memblock - 内存块 (指针 指向 之前已经开辟了的内存块)

size : 新的大小

 

1. realloc 函数 的 出现 让动态内存管理更加灵活

2. 有时我们会发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了, 为了合理的使用内存,我们一定会对 内存的大小 做灵活的调整, realloc 函数就可以做到 对 动态开辟内存 大小的调整。

3. memblock 是要调整的内存块的地址

4.size 调整之后 新空间的大小

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

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

7.realloc 在 调整内存空间 时,存在两种情况:



程序五:

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
int main()
{
	int* p = (int*)malloc(20);// 开辟 20 byte 动态空间
	if (p == NULL) // 防止malloc 开辟动态空间失败
	{
		printf("%s\\n", strerror(errno));// 打印错误信息
	}
	else
	{  // 正常使用空间
		int i = 0;// 20 字节 ==  5* (sizeof(int))
		for (i = 0; i < 5; i++)
		{
			*(p + i) = i;// 此时 就是在使用 malloc 函数开辟的空间(20byte)
		}               
	}
	// 假设 malloc 函数 开辟的 20 byte 的空间,不满足我们的需求(不够)
	// 我们希望能够有 40 个 byte 的空间
	//这里 就可以使用 realloc 函数 来调整动态开辟的内存
	int* p2 = (int*)realloc(p, 40);//  10 * int == 40 byte
	/* p */
    
    //  记住 只要开辟动态空间的时候,最好判断一下,无论是 malloc ,calloc还是realloc 函数 都有可能开辟空间失败
    //  在下面的解决方案中,已得到解决 
    
	// 你会发现 原本 整个空间是 由 p 来维护的
	// 但是 现在你会发现 变成了 p2 来维护 了

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p2/* p */ + i));// 0 1 2 3 4 随机值1 随机值2  随机值3  随机值4  随机值5
	}                                // 随机值 是因为 malloc 不会像 calloc 函数 一样开辟完动态空间之后,初始化为 0
	printf("\\n");
	for (i = 5; i < 10; i++) // 使用 增大后的空间
	{
		*(p2/* p */ + i) = i;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p2/* p */ + i));// 0 1 2 3 4 5 6 7 8 9
	}                 // 如果保持统一 ,p2 换成 p,会出现问题(p已将找不到了)
	             // 因为 realloc 函数,是重新创建一个(40 byte)空间
	             // 把  p 的内容拷贝下来,把拷贝下来的数据 放进 这新创建的空间里
	             // 然后把 p 释放(p = NULL)
	             // 最后 返回新空间的地址
	             // 输出这个新创建的空间 的结果 让你 感觉 空间确实变大了
	             // 实际上 已经 不是 原先的空间 了
	return 0;
}

解决方案:

程序六:

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
int main()
{
	int* p = (int*)malloc(20);
	if (p == NULL)
	{
		printf("%s\\n", strerror(errno));
	}
	else
	{
		int i = 0;// 20 字节 ==  5* (sizeof(int))
		for (i = 0; i < 5; i++)
		{
			*(p + i) = i;// 此时 就是在使用 malloc 函数开辟的空间(20byte)
		}
	}

	int* ptr = (int*)realloc(p, 40);//  拿一个新的指针变量 ptr 先来接收 realloc 的返回地址
	if (ptr != NULL) // 判断 realloc 开辟动态空间是否 成功
	{
		p = ptr;// 防止开辟失败,把 p 改掉了

		int i = 0;
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i));// 0 1 2 3 4 随机值1 随机值2  随机值3  随机值4  随机值5
		}
		printf("\\n");
		for (i = 5; i < 10; i++)
		{
			*(p + i) = i;
		}
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i));// 0 1 2 3 4 5 6 7 8 9
		}
	}
	// 释放内存
	free(p);// 如果 realloc 开辟动态空间失败,返回 NULL,而 free(NULL),free 函数什么都不会做
	p = NULL;
	// 一定 要释放内存,并置为空指针,永绝后患!
	return 0;
}

realloc 如果 p 指向的空间之后 有 足够的内存空间可以追加,则追加上,返回原先的旧地址(p)

如果 p 指向的空间之后 没有 足够的内存空间可以追加,则重新找一个新的内存区域,开辟一块新的空间, 来满足你对空间需求,同时把 原来内存 的 数据 拷贝下来,把拷贝下来的数据放到新空间里,且把原来的空间释放,最后返回新空间的地址


另外再补充一点

realloc 函数 可以实现与 malloc 函数 一样的功能

程序七:

#include<stdio.h>
int main()
{
	int* p = (int*)realloc(NULL, 40);// 此时它的功能 与 malloc 函数一样
	                     //  == (int*)malloc(40);
}




常见的动态内存错误

1. 对 NULL 指针的解引用 操作

程序八:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(40);
	//  万一 malloc 失败,p 被赋值为 NULL
	for (int i = 0; i < 10; i++)
	{
		*(p + i) = i;// 非法地址,对 NULL 指针 进行解引用
	}
	free(p);
	p = NULL;
	return 0;
	//解决方案 :在使用开辟的动态空间之前,用 assert(p),记得加上头文件 assert.h; 或 加上 if(p != NULL) 判断语句,对 p 进行相关的判断
}



2,对动态开辟的内存 的 越界访问

程序九:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(20);// 开辟了 5个 int 元素的空间
	if (p == NULL)// 判断 malloc 函数 开辟动态空间 是否成功
	{
		return  0; //  失败
	}
	else//  成功
	{  
		int i = 0;    
		for (i = 0; i < 10; i++) // 我只有 5个 int元素,而现在 要访问 第 6~10 个 int 元素,形成了越界访问,导致程序崩溃
		{                      // 改成 5 就没问题了
			*(p + i) = i;           
		}
	}
	free(p);
	p = NULL;
}



3, 对 非动态 开辟内存使用 free(释放)函数

程序十:

#include<stdio.h>
int main()
{
	int a = 10;// 在 栈上 开辟空间
	int* p = &a;
	*p = 20;


	free(p); // free 对 非动态 开辟内存 使用,会造成程序崩溃
	p = NULL;
	return 0;
}



4.使用 free 释放动态开辟内存的一部分

程序十一:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		return 0;
	}
	else
	{
		int  i = 0;
		for (i = 0; i < 10; i++)
		{
			*p++ = i;// 解决方法  *(p+i)= i; 这样避免了 p 的地址被改变
		}
		// 回收空间
		free(p);// 如果这样直接这样释放空间,会导致程序崩溃
		// 因为 p 已经不再指向该 动态开辟空间 的 起始位置,它释放第 10 个元素之后的空间(释放一部分动态开辟的空间)
		// free 只能释放 动态开辟空间 的起始地址(要释放就全释放)
	}
	p = NULL;
	return 0;
}




5. 对同一块 动态内存 的多次释放

free 释放 一块动态开辟空间后,那个 空间 的地址并没有改变,此时再 free 就可能出现问题

程序十二:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(40);
	if (p = NULL)
	{
		return 0;
	}
    // 使用
	// 释放
	free(p);
	free(p);// free 释放 一块动态开辟空间后,那个 空间 的地址并没有改变,此时再 free 就可能出现问题
	return 0;
}	//  谁申请 谁回收
	//  在 free(); 后面 手动 把 p = NULL; 后面就算再接上一个 free,也不会有问题
	// 因为 根据C语言标准:free(NULL); free 什么都不做



6. 对动态开辟内存 忘记释放(内存泄露)

这种错误的出现,会导致计算机内存被耗干,导致卡死,毕竟计算机内存有限

程序十三:

#include<stdio.h>
#include<windows.h>
int main()
{
	
	while (1)
	{
		malloc(1);
		// 通过 三点(Ctrl、Alt 点 .)一线(同时按),选择任务管理器 -》 性能 
	    // 通过观察 内存 占用 显示发现,内存正在被大量消耗,
		// 原因是 程序一直在申请空间,不回收,导致大量内存空间被占用(这些你不用,别人也用不了,因为你没还),这就是 内存泄露
	}

}



接下来我们来看几道面试题,来加深我对 动态内存分配 函数的 理解

题目 1

程序十四:

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


void GetMemory(char* p)// p 是 str 的一份临时拷贝,即 p= NULL;
{
	// malloc  在堆上开辟了一块空间,把该动态开辟空间的起始地址赋给 p
	p = (char*)malloc(100);// 此时 p 指向 malloc 开辟的动态空间

	// 动态开辟空间,调用完了,没有释放空间,会造成 内存泄露
    // 而且 这函数调用完后之后,这块函数空间会被回收
	// 到时我们再也找不到 这动态开辟的空间的起始地址
	// 也就是说 出了这个函数  想解决 内存泄露 这个问题,都解决不了(p 是该函数的形参,只在该函数内部有效)

 再举个例子,
  假设有一个卧底(malloc开辟的动态空间),他的信息(空间的地址)在警察系统中都删掉了,只有 p 知道,
 后来,有一天 p 死掉了(出了函数,销毁了 局部变量 p).他也没有记本本告诉别人(没有返回这块空间的地址),
 那么这个卧底再也证明不了自己的身

以上是关于我眼中的‘C’——动态内存+柔型数组的主要内容,如果未能解决你的问题,请参考以下文章

[java学习]java容器源码初探

我眼中的数组和冒泡排序

动态内存分配(c语言)

JAVA中Vector怎样存放一个动态的二维数组

剖析C动态内存管理 (malloc,calloc,realloc,柔性数组)

C++ 动态内存分配与结构中的数组