C语言之动态内存开辟

目录

Win系统内存空间分配概述

动态内存概述

动态内存函数介绍

动态内存实践

Win系统内存空间分配概述

在了解动态内存前,我们先简单介绍一下关于Win系统的内存分配问题,有助于对内存空间的进一步理解:

在Win(X86)系统中,共分配4G内存空间,而这4G内存空间中,其中有1个G的内存空间为内核态,内核区的内存空间程序员没有访问权限,剩下的3个G可供程序员访问使用,其共分为4个区,分别为:代码区,数据区,堆区,栈区。将大致的示意图表示如下:

接下来对以上这4个区进行具体的介绍:

代码区:存放 CPU 执行的机器指令(函数体的二进制代码),即将编译器中的代码进行汇编后得到的代码。

特点:代码区具有共享性只读性;

共享性:即另外的执行程序可以调用它,使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。
只读性:即只能读取,不能修改。原因是防止程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。

数据区:数据区又称为静态区、全局区、全局静态区、静态全局区,其中,它内部存储的数据有全局变量、静态变量、常量(如字符串常量),同时根据存储数据的不同又将数据区分为三小部分:.data   .bss   .rodata

先将具体内容介绍如下:

.data  区(全局初始化区) :存放已初始化的全局变量和静态变量

.bss 区 (全局未初始化区):存放未初始化的全局变量和静态变量

区别初始化和非初始化是为了空间效率

未初始化变量不占据实际内存空间(bss变量只在段表中记录大小,在符号表中记录符号。当程序运行时,才分配空间以及初始化),bss段在程序执行之前会被系统自动清0,所以这些未初始化的数据在程序执行前会自动被系统初始化为0或者NULL

 .rodata 区(常量区):用于存放各类常量,如:const、字符串常量、#define 

堆区:堆类似一个大容器,其内存空间大小远大于栈区,用于动态内存分配。由程序员通过相应函数进行内存申请分配,在使用结束后,需要程序员通过free()函数进行手动释放,若程序员不释放,则内存空间会一直存在,造成内存泄漏。直至程序结束时由操作系统回收。

堆区在Win系统下内存空间大约为1.4G--1.9G(不同电脑,不同配置下堆内存空间不同),在Linux系统中接近3G

堆内存开辟:从低地址到高地址进行内存开辟;

栈区:由编译器根据代码自动分配内存,并在调用结束后系统自动释放相应的内存。存放函数的形参以及局部变量的值等。其操作方式类似于数据结构中的栈。

栈区在Win系统下内存空间大约为1M,在Linux系统中为10M

栈内存开辟:从高地址到低地址进行内存开辟;

注意:不要返回局部变量的地址!原因为在栈区所开辟的内存空间在函数执行完后将自动释放,故而相应的地址也不复存在,返回后若对该地址进行解引用,则会程序崩溃,因为当函数调用结束时,局部变量被内存回收,无法对其进行访问。

动态内存概述

首先我们来了解一下什么是动态内存分配?

所谓动态内存分配,就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不像数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。

而我们为什么要选择动态内存分配?

在c/c++语言中,编写程序有时不能确定数组应该定义为多大,因此这时在程序运行时要根据需要从系统中动态多地获得内存空间,以满足对内存空间的需要。

下面用代码进行具体的解释:

一般情况下,我们通常通过定义局部变量来开辟栈区上的内存空间,例如,我们来定义一个数组:

char arr[ ];

定义的该数组所占的内存空间是否由“[ ]”内的数字所决定,首先,我们要明确“[ ]” 中的数字代表什么?用代码进行简单的实现:

#include <stdio.h>
int main() {
	char arr[4];
	printf("%d", sizeof(arr));
	return 0;
}

由编译器运行后,所得的结果为4, 所以由此我们可以得出数组arr中所占字节数为4,那括号中的数字是否代表字节数?

#include <stdio.h>
int main() {
	int arr[4];
	printf("%d", sizeof(arr));
	return 0;
}

由编译器运行后,所得的结果为16, 所以由此我们可以得出数组arr中所占字节数为16,

上述问题答案,括号中的数字不代表字节数,由此我们得知,数组内存空间所占字节数由该数组的基类型与“[ ]”中的数据共同决定。而“[ ]”中的数字可以理解为“格子数”,而一个格子所占的内存空间是由该数组的基类型决定。

注意:

首先回顾一下sizeof()的作用:sizeo是获取了数据在内存中所占用的存储空间,以字节为单位来计数。而当它想计算一个数组的所占内存空间的字节大小时,用法为:sizeof(数组名)(当sizeof与数组的定义在同一作用域时,数组名代表整个数组字节数的大小)

若想计算数组中数据的个数,则可以用sizeof(arr)/sizeof(arr[0])进行计算

为了方便起见,我们与上文一样,将数组定义为char类型,来具体研究:
在研究之前,再介绍一下有关内存的相关单位:

内存单位从大到小的排列依次为:TB、GB、MB、KB、B

内存单位的换算为:

1TB=1024GB

1GB=1024MB

1MB=1024KB

1KB=1024Byte

1Byte=8bit

系统对内存的识别是以Byte(字节)为单位,每个字节由8位二进制数组成,即8bit(比特,也称位)。内存是计算机的重要部件之一,也称内存储器和主存储器,它用于暂时存放CPU中的运算数据,与硬盘等外部存储器交换数据。

Byte(字节):Byte的缩写是B,是计算机文件大小的基本计算单位。比如一个字符就是1Byte,如果是汉字,则是2Byte

1字节 = 1个英文字母或1个数字或一个字符

2字节 = 1个中文汉字


接下来,我们进行内存空间的开辟

#include <stdio.h>
int main() {
	char arr[1024 * 1024];
	return 0;
}

 程序执行后,虽然可以执行下来,但是在返回值处值不为0,很显然,程序非正常退出则内存空间开辟失败。

那为什么会开辟失败呢?原因在于每个进程会有两个 ,一个用户,存在于用户空间,一个,存在于内核空间。虽然栈内存空间在Win系统下分配为1M,但是内核栈会占用一部分内存空间,导致栈的内存空间会小于1M,所以,当开辟1M内存空间时,自然会开辟失败。

此时在栈空间不够用的情况下,选用堆区进行内存空间开辟(即动态内存开辟),而堆区与栈区不同,堆区内存由程序员通过相应函数进行内存申请分配,在使用结束后,需要程序员通过free()函数进行手动释放。

动态内存函数介绍

malloc函数

我们在堆区上开辟5个单元格的数组,自然不能用 int arr[5] 来进行定义,这样只会在栈区上进行内存开辟,而在堆区上进行内存开辟的,则要用malloc()函数进行动态内存开辟

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

int main() {
	int* arr = (int*)malloc(5 * sizeof(int));
	if (arr != NULL) {
		printf("开辟成功\n");
	}
	else
		printf("开辟失败\n");
		return 0;
	}

如图所示,若指针arr为空,则表示开辟失败,若arr不为空,则说明内存空间开辟成功

在上文中,我们说到堆区在Win系统下内存空间大约为1.4G--1.9G

我电脑的堆内存为1.4G

#include <stdio.h>
#include <stdlib.h>
int main() {
	int* arr = (int*)malloc(1.4 * 1024 * 1024 * 1024);
	if (arr != NULL) {
		printf("开辟成功\n");
	}
	else
		printf("开辟失败\n");
		return 0;
	}

当为1.5G时随机开辟失败

#include <stdio.h>
#include <stdlib.h>
int main() {
	int* arr = (int*)malloc(1.5* 1024 * 1024 * 1024);
	if (arr != NULL) {
		printf("开辟成功\n");
	}
	else
		printf("开辟失败\n");
		return 0;
	}

 

在这里我们会遇到这样一个问题:

#include <stdio.h>
#include <stdlib.h>
int main() {
	int* arr = (int*)malloc(5* 1024 * 1024 * 1024);
	if (arr != NULL) {
		printf("开辟成功\n");
	}
	else
		printf("开辟失败\n");
		return 0;
	}

当我们申请5G的内存空间时,为什么奇迹般地可以申请成功?

我们通过Ctrl键+鼠标点击我们可以深入malloc函数进行具体分析

点击进入后,再通过Ctrl键+鼠标点击size_t,来对形参进行具体分析

首先,由size_t 类型定义的_Size形参变量指的是所申请的空间的字节数

图中,我们可以看出通过typedef的形式,将原有的数据类型unsigned int 重命名为size_t,在这里二者是相互替换的,所以在这里我们申请的_Size(字节数)的数据类型本质为unsigned int ,同时我们也要知道无符号四字节整形的取值范围为0-4294967295,即我们申请的最大空间的字节数不能超过此范围,而输入的字节数一旦超过这个范围,此时以小类型接受大值会进行类型的强制转化,直接转为小类型的数据——输入内容超过了类型的表示区间,会以二进制的形式强制截取四字节(32位)的数据,之后再与最大值进行比较,此时就容易出现,申请内存空间远远大于堆区的实际内存空间却还申请成功的现象。

总结

动态开辟内存空间的成功与否的原理是:堆内存是否有要求的那么多?

但是实际程序执行的结果则取决于:1.传入的数据是否超过函数形参类型的范围?

                                                          2.在未超过形参类型的范围时,与堆区的大小有关

                                                          3.在超过形参类型的范围时,会对输入的数据进行类型的强制转换,将强制转换后的数据与堆区的大小进行进一步比较。

 同时,我们还可以看出,malloc函数是以void*为返回值的函数,即该函数返回的是一个地址,所以需要用一个地址变量对其进行存储

malloc()函数虽然在堆上进行内存开辟,但是并不对其进行解析,需要由自己进行解析即申请完连续的内存空间后通过强转类型的方式进行解析

定义方式为: int* arr = (int*)malloc(字节数)     指针指向开辟内存空间的首地址

内存空间初始化

我们在申请完内存空间后,即可对申请的该内存空间进行使用

#include <stdio.h>
#include <stdlib.h>
#include <cassert>
int main() {
	int* arr = (int*)malloc(10 * sizeof(int));
	assert(arr != NULL);
	for (int i = 0; i <= 9; i++) {
		printf("%d\n", arr[i]);
	}
	free(arr);//释放堆内存
	arr = NULL;//防止多次free释放出现崩溃
	return 0;
}

malloc()函数头文件为:<stdlib.h>    (ralloc,calloc,free等函数均是)   稍后介绍

通过代码运行,我们可以看出,在堆区上申请的内存空间并未进行初始化,所以在打印后会打印随机值,所以我们对该内存在使用前需要进行初始化,我们可以通用过以下两种方式进行

方法一:

#include <stdio.h>
#include <stdlib.h>
#include <cassert>
int main() {
	int* arr = (int*)malloc(10 * sizeof(int));
	assert(arr != NULL);
	for (int i = 0; i <= 9; i++) {
		arr[i] = 0;
		printf("%d\n", arr[i]);
	}
	free(arr);//释放堆内存
	arr = NULL;//防止多次free释放出现崩溃
	return 0;
}

由此我们通过循环赋值的方式将内存空间初始化为0

方法二:

在这里我们用到memset()函数头文件为:<string.h>

现在我们对memset()函数进行具体的介绍:

功能介绍:初始化内存空间

 继续通过Ctrl键+鼠标点击我们可以深入memset函数内部,通过参数对其进行具体分析

memset(void* _Dst , int Value , size_t Size)

void* _Dst : 初始化内存空间的首地址

int Value : 初始化的值

size_t Size : 初始化值的个数  (到底是字节的个数还是格子数,通过代码进行验证)

#include <stdio.h>
#include <stdlib.h>
#include <cassert>
#include <string.h>
int main() {
	int* arr = (int*)malloc(10 * sizeof(int));
	assert(arr != NULL);
	memset(arr, 0, 10);
	for (int i = 0; i <= 9; i++) {
		printf("%d\n", arr[i]);
	}
	free(arr);//释放堆内存
	arr = NULL;//防止多次free释放出现崩溃
	return 0;
}

 由此得出,Size不为格子数,而是字节数,验证如下:

#include <stdio.h>
#include <stdlib.h>
#include <cassert>
#include <string.h>
int main() {
	int* arr = (int*)malloc(10 * sizeof(int));
	assert(arr != NULL);
	memset(arr, 0, 10 * sizeof(int));
	for (int i = 0; i <= 9; i++) {
		printf("%d\n", arr[i]);
	}
	free(arr);//释放堆内存
	arr = NULL;//防止多次free释放出现崩溃
	return 0;
}

 初始化成功,接下来,我们引申到另一个问题,该函数是如何对内存空间进行初始化处理的呢?

我们将初始化的数字换成1进行,那结果会是十个1吗?

 结果不为十个1,那16843009又是什么含义?

我们可以打开计算器进行逆向的计算分析:

 所以由此可得,该函数的初始化方式为以字节为单位,对内存空间进行初始化

同时,在我们开辟的这片内存空间中,我们通过int类型进行解析,综上所述,memset函数以类型字节为整体,以字节为单元,对内存空间进行初始化

最后,我们可以在代码中发现,在使用开辟的内存空间后,需要程序员通过free()函数,手动释放该内存空间,防止造成内存泄漏,同时也会将原来的指针置空,防止多次free释放出现程序崩溃,用代码进行验证:

#include <stdio.h>
#include <stdlib.h>
#include <cassert>
#include <string.h>
int main() {
	int* arr = (int*)malloc(10 * sizeof(int));
	assert(arr != NULL);
	memset(arr, 0, 10 * sizeof(int));
	for (int i = 0; i <= 9; i++) {
		printf("%d\n", arr[i]);
	}
	free(arr);//释放堆内存
	free(arr);//多次释放导致程序崩溃
	return 0;
}

 为什么会出现这种情况?

原因在于,当使用完申请的内存空间后,使用free函数申请的内存空间进行释放,归还给系统,此时arr变为野指针,野指针所指向的内存没有访问权限,所以若对其继续进行释放,必定会导致程序的崩溃

但是,将arr置空后,free释放arr是否会崩溃?不会

#include <stdio.h>
#include <stdlib.h>
#include <cassert>
#include <string.h>
int main() {
	int* arr = (int*)malloc(10 * sizeof(int));
	assert(arr != NULL);
	memset(arr, 0, 10 * sizeof(int));
	for (int i = 0; i <= 9; i++) {
		printf("%d\n", arr[i]);
	}
	free(arr);//释放堆内存
	arr = NULL;//防止多次free释放出现崩溃
    free(arr);
	return 0;
}

原则上,空指针所指的地址也没有访问权限,但是因为free函数的会进行参数的安全检测,在检测到空指针后,程序自动退出,从而解决防止多次free释放导致程序出现崩溃的现象

总结

动态内存开辟步骤:

步骤1:调用函数进行动态内存的开辟

步骤2:设置断言,指针不为空

步骤3:使用内存空间

步骤4:进行内存空间的释放

步骤5:将指针进行置空处理

 calloc函数

下面对calloc函数进行介绍

首先,对calloc函数的功能进行介绍

calloc ()  =  malloc ()  +  memset ()

从上面的式子中我们便可以得出,calloc函数的功能为既开辟内存空间,同时也会对内存空间进行初始化

下面我们对该函数的参数进行具体分析

 下面我们用代码进行实现

#include <stdio.h>
#include <stdlib.h>
#include <cassert>
int main() {
	double* arr = (double*)calloc(10, sizeof(double));
	assert(arr != NULL);
	for (int i = 0; i <= 9; i++) {
		printf("%5.1f\n", arr[i]);
	}
    free(arr);
    arr = NULL;
	return 0;
}

在这里我们可以得出,calloc函数在开辟的内存空间赋值为默认值--0

realloc函数

首先对其功能进行介绍:

通过malloc(),calloc()函数在堆区上进行动态开辟内存空间后,若发现所开辟的内存空间不够时,即可通过realloc函数进行扩容操作

先对realloc函数参数进行分析

 接下来用代码进行实现:

#inclde <stdio.h>
#include <stdlib.h>
#include <cassert>
int main() {
		int* arr = (int*)calloc(2, sizeof(int));
		assert(arr != NULL);
		int* brr = (int*)realloc(arr,5*sizeof(int));
		assert(brr != NULL);
		for (int i = 0; i <= 4; i++) {
			printf("%d\n", brr[i]);
		}
		free(brr);
		brr = NULL;
		return 0;
}

 看结果,动态内空间已经扩容成功

但是我们发现,realloc函数虽然可以进行扩容,但是不会对其进行初始化,仍然需要通过memset函数进行初始化

#inclde <stdio.h>
#include <stdlib.h>
#include <cassert>
int main() {
		int* arr = (int*)calloc(2, sizeof(int));
		assert(arr != NULL);
		int* brr = (int*)realloc(arr,5*sizeof(int));
		assert(brr != NULL);
        memset(brr, 0, 5 * sizeof(int));
		for (int i = 0; i <= 4; i++) {
			printf("%d\n", brr[i]);
		}
		free(brr);
		brr = NULL;
		return 0;
}

 扩展

realloc函数扩容的三种情况

首先,我们通过两段代码来发现一个问题

第一段

#inclde <stdio.h>
#include <stdlib.h>
#include <cassert>
int main() {
		int* arr = (int*)calloc(2, sizeof(int));
		assert(arr != NULL);
		int* brr = (int*)realloc(arr,5*sizeof(int));
		assert(brr != NULL);
		for (int i = 0; i <= 4; i++) {
			printf("%d\n", brr[i]);
		}
		free(brr);
		brr = NULL;
		return 0;
}

通过代码调试,我们可以在监视窗口看出,arr与brr的地址相同

第二段

#inclde <stdio.h>
#include <stdlib.h>
#include <cassert>
int main() {
		int* arr = (int*)calloc(200, sizeof(int));
		assert(arr != NULL);
		int* brr = (int*)realloc(arr,2000*sizeof(int));
		assert(brr != NULL);
		for (int i = 0; i <= 4; i++) {
			printf("%d\n", brr[i]);
		}
		free(brr);
		brr = NULL;
		return 0;
}

通过代码调试,我们可以在监视窗口看出,arr与brr的地址不相同

 同样是动态内存的扩容,为什么前后的地址不一样?

这便是下面我们要讨论的realloc函数扩容的三种情况:

情况一:后续未分配内存空间足够大,可以分配内存空间

情况二:后续未分配内存空间不够大,不可以分配内存空间

这以上两种情况,也分别对应开头所对应的两段代码

情况三:堆内存不足,扩展空间失败,realloc函数返回NULL

#include <stdio.h>
#include <stdlib.h>
int main() {
	int* arr = (int*)malloc(100 * sizeof(int));
	assert(arr != NULL);
	if (NULL == arr) {
		exit(EXIT_FAILURE);
	}
	//错误做法
	/*
	* arr = (int*)realloc(arr,2000 * sizeof(int));
	* assert(arr != NULL);
	* if(NULL == arr){
	* free(arr);
    * arr = NULL;
	* exit(EXIT_FAILURE);
	* }
	*/
	//正确做法
	int* brr = (int*)realloc(arr, 2000 * sizeof(int));
	assert(brr != NULL);
	if (NULL == brr) {
		free(arr);
        arr = NULL;
		exit(EXIT_FAILURE);
	}
	else {
		arr = brr;
	}
	free(arr);
	arr = NULL;
	return 0;
}

通过以上代码,我们可以总结出以下问题:

问题一:如果堆内存不足,扩展空间失败,realloc函数返回NULL

 即在底层结构中,NULL即为0,将指针置为空(NULL),即将地址为0x 0000 0000 传给指针,然后对其进行判断,若判断为空,则系统调用exit(EXIT_FAILURE)程序退出

问题二:exit的具体介绍以及对exit()与return 的区别

exit()函数:终止函数,关闭所有文件,终止正在执行的进程

通常是用在子程序中用来终结程序用的,使用后程序自动结束,跳回操作系统。

头文件:<stdlib.h>

exit()函数通常使用的两种方式:

exit( EXIT_SUCCESS ) 正常运行程序并退出程序
exit( EXIT_FAILURE )   非正常运行导致退出程序

二者的具体的含义是什么?

在头文件<stdlib.h>中,用两个宏定义:

#define EXIT_SUCCESS 0
#define EXIT_FAILURE 1

exit(0)与exit(1)的含义为:

exit(0):正常运行程序并退出程序;
exit(1):非正常运行导致退出程序。

exit()与return 的区别

在C语言mian函数中,我们通常使用return  0;这样的方式返回一个值,但这是限定在非void情况下的,也就是非void main()这样的形式。

但在如果把exit用在main内的时候无论main是否定义成void返回的值都是有效的,并且exit不需要考虑类型,exit⑴等价于return 1

具体区别表现如下:

1. return返回的是函数值,而他本身是关键字(例如break,continue); exit 是一个函数;
2. return是语言级别的,是函数的退出(返回),它表示了调用堆栈的返回;而exit是系统调用级别的,它表示了一个进程的结束;

3. return是C语言提供的,exit是操作系统提供的;

4. return用于结束一个函数的执行,将函数的执行信息传出给其他调用函数使用;exit函数是退出应用程序,删除进程使用的内存空间,并将应用程序的一个状态返回给OS(操作系统),这个状态标识了应用程序的一些运行信息,这个信息和机器和操作系统有关,一般是 0 为正常退出,非0 为非正常退出;

5. 非主函数中调用return和exit效果很明显,但是在main函数中调用return和exit的现象就很模糊,多数情况下现象都是一致的。

问题三:在assert()断言后,为什么还要及进行if()的判断

通过断言的方式,使其扩容空间不为空,即assert(brr!= NULL);

但当brr真的为空时,程序便会被终止,同时,之前所开辟的内存空间也会被释放

不过我们需要注意的是,assert断言只适用于Debug调试版本中,而在Release发布版本中,便会失效,相于透明,所以加一个判断操作(此处也可以不进行断言,直接进行判断即可)

问题四:为什么要用一个新的指针来接收realloc()函数扩容的地址

如果仍然用旧的指针接收,当realloc()函数扩容失败后,便会将NULL返回给旧的指针,此时原先开辟的内存空间的地址便会丢失,从而造成内存泄漏。所以我们选择用新的地址来接收扩容后的空间地址,即使扩容失败,也可以通过以前的指针来对之前开辟的内存空间进行释放

int* brr = (int*)realloc(arr, 2000 * sizeof(int));
	assert(brr != NULL);
	if (NULL == brr) {
		free(arr);
        arr = NULL;
		exit(EXIT_FAILURE);
	}
	else {
		arr = brr;
	}
	free(arr);
	arr = NULL;

问题五:if(NULL == brr)的优点

通常情况下,我们一般写为if(brr == NULL);但是有时会粗心写为if(brr = NULL)这样程序是不会报错的,但是若写为if(NULL = brr);系统会直接报错,因为语法上无法对一个常数进行赋值,即表达式必须是可修改的左值。

总结:

扩容的大体步骤为:

动态申请内存空间——断言指针不为空——判断指针是否为空(指针为空退出程序)——进行扩容(用新的指针接收所扩容空间的内存)——断言指针不为空——判断指针是否为空(指针为空,释放原先内存,原先指针置空,退出程序,指针不为空,将新的指针赋值给原先的指针)——进行使用——释放空间,指针置空——退出程序

  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值