C语言知识总结(十一)内存管理

1、绪论

在编写程序时,通常并不知道需要处理的数据量,或者难以评估所需处理数据量的变动程度。在这种情况下,要达到有效的资源利用——使用内存管理,必须在运行时动态地分配所需内存,并在使用完毕后尽早释放不需要的内存,这就是动态内存管理原理。动态内存管理同时还具有一个优点:当程序在具有更多内存的系统上需要处理更多数据时,不需要重写程序。

一个正在运行着的C编译程序占用的内存分为代码区、静态数据区、未初始化数据区、堆区 和 栈区5个部分。

C语言中定义4个内存区间是: 代码区, 静态存储区, 栈区, 堆区. 其中栈区和堆区是属于动态存储区 ​ 可执行文件在存储(也就是还没有载入到内存中)的时候,分为:代码区静态区未初始化数据区3个部分。

2、代码区

只读区域,程序运行过程中无法做任何修改的存储区域。用于存放代码和常量。

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

代码区 指令根据程序设计流程依次执行,对于顺序指令,则只会执行一次(每个进程),如果反复,则需要使用跳转指令,如果进行递归,则需要借助栈来实现。

代码段: 代码段通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。

在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等

。代码区的指令中包括操作码和要操作的对象(或对象地址引用)。如果是立即数(即具体的数值,如5),将直接包含在代码中;如果是局部数据,将在栈区分配空间,然后引用该数据地址;如果是BSS区和数据区,在代码中同样将引用该数据地址。另外,代码段还规划了局部数据所申请的内存空间信息。

数据区:可读可写区域,程序运行过程中可做任意修改的存储区域。用于存放变量

3、静态数据区

该区包含了在程序中明确被初始化的全局变量、静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量),注意 (只初始化一次)。例如,一个不在任何函数内的声明(全局数据):

int max = 99;复制代码

使得变量max根据其初始值被存储到初始化数据区中。

static min = 100; 复制代码

这声明了一个静态数据,如果是在任何函数体外声明,则表示其为一个全局静态变量,如果在函数体内(局部),则表示其为一个局部静态变量。另外,如果在函数名前加上static,则表示此函数只能在当前文件中被调用。

数据段:通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。数据段中的静态数据区存放的是程序中已初始化的全局变量、静态变量和常量。

4、未初始化数据区

未初始化数据区。亦称BSS区,存入的是全局未初始化变量。BSS这个叫法是根据一个早期的汇编运算符而来,这个汇编运算符标志着一个块的开始。BSS区的数据在程序开始执行之前被内核初始化为0或者空指针(NULL)。例如一个不在任何函数内的声明:

long sum[1000];复制代码

将变量sum存储到未初始化数据区。

BSS 段:通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS 是英文Block Started by Symbol 的简称。BSS 段属于静态内存分配,即程序一开始就将其清零了。一般在初始化时BSS段部分将会清零。

5、栈区

栈区(stack)。由编译器自动分配释放内存的区间,所得的内存空间一般都是连续的,是用来存放函数的参数值、局部变量的值等。存放函数的参数值、局部变量的值,以及在进行任务切换时存放当前任务的上下文内容。其操作方式类似于数据结构中的栈。每当一个函数被调用,该函数返回地址和一些关于调用的信息,比如某些寄存器的内容,被存储到栈区。然后这个被调用的函数再为它的自动变量和临时变量在栈区上分配空间,这就是C实现函数递归调用的方法。每执行一次递归函数调用,一个新的栈框架就会被使用,这样这个新实例栈里的变量就不会和该函数的另一个实例栈里面的变量混淆。 ​ 栈(stack) :栈又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数括弧"{ }"中定义的变量,如int[ ] arr = {1, 2, 3};变量arr ( 数组名) 存储在栈中,变量arr的值(数组元素)存储在堆中(普通结构)(但不包括static 声明的变量,static 意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/ 恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。

6、堆区

堆区(heap)。用于动态内存分配。堆在内存中位于bss区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时有可能由OS回收。堆中的内存区域不是连续的,还是将有效的内存区域经过链表指针连接起来的 ​ 堆(heap): 用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc 等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free 等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。在将应用程序加载到内存空间执行时,操作系统负责代码段、数据段和BSS段的加载,并将在内存中为这些段分配空间。栈段亦由操作系统分配和管理,而不需要程序员显示地管理;堆段由程序员自己管理,即显式地申请和释放空间。

另外,可执行程序在运行时具有相应的程序属性。在有操作系统支持时,这些属性页由操作系统管理和维护。

C语言程序编译完成之后,已初始化的全局变量保存在数据段中,未初始化的全局变量保存在BSS段中。数据段和代码段都在可执行文件中,由系统从可执行文件中加载;而BSS段不在可执行文件中,由系统初始化。BSS段只保存没有值的变量,所以事实上它并不需要保存这些变量的映像。运行时所需要的BSS段大小记录在目标文件中,但是BSS段并不占据目标文件的任何空间。

堆区与栈区的差异:

在栈上所申请的内存空间是系统自动分配的,所以当我们出了变量所在的作用域后,系统会自动我们回收这些空间,而在堆上申请的空间是要我们自己手动操作的,当出了相应的作用域以后,我们需要调用free或者delete来释放所申请的内存空间,如果我们不及时得对这些空间进行释放,那么内存中的内存碎片就越来越多,从而我们的实际内存空间也就会变的越 来越少,即,孤立的内存块越来越多。

7、memset函数

memset(翻译:清零)是计算机中C/C++语言初始化函数。作用是将某一块内存中的内容全部设置为指定的值, 这个函数通常为新申请的内存做初始化工作。 ​ 以前说过,定义变量时一定要进行初始化,尤其是数组和结构体这种占用内存大的数据结构。在使用数组的时候经常因为没有初始化而产生“烫烫烫烫烫烫”这样的野值,俗称“乱码”。

每种类型的变量都有各自的初始化方法,memset() 函数可以说是初始化内存的“万能函数”,通常为新申请的内存进行初始化工作。它是直接操作内存空间,mem即“内存”(memory)的意思。 该函数的原型为:

## include <string.h>
void *memset(void *s, int c, unsigned long n);
将s中当前位置后面的n个字节 (typedef unsigned int size_t )用 c 替换并返回 s复制代码

函数的功能是:将指针变量 s 所指向的前 n 字节的内存单元用一个“整数” c 替换,注意 c 是 int 型。s 是 void* 型的指针变量,所以它可以为任何类型的数据进行初始化。

memset() 的作用是在一段内存块中填充某个给定的值。因为它只能填充一个值,所以该函数的初始化为原始初始化,无法将变量初始化为程序中需要的数据。用memset初始化完后,后面程序中再向该内存空间中存放需要的数据。

memset 一般使用“0”初始化内存单元,而且通常是给数组或结构体进行初始化。一般的变量如 char、int、float、double 等类型的变量直接初始化即可,没有必要用 memset。如果用 memset 的话反而显得麻烦。

当然,数组也可以直接进行初始化,但 memset 是对较大的数组或结构体进行清零初始化的最快方法,因为它是直接对内存进行操作的。

这时有人会问:“字符串数组不是最好用'\0'进行初始化吗?那么可以用 memset 给字符串数组进行初始化吗?也就是说参数 c 可以赋值为'\0'吗?”

可以的。虽然参数 c 要求是一个整数,但是整型和字符型是互通的。但是赋值为 '\0' 和 0 是等价的,因为字符 '\0' 在内存中就是 0。所以在 memset 中初始化为 0 也具有结束标志符 '\0' 的作用,所以通常我们就写“0”。

memset 函数的第三个参数 n 的值一般用 sizeof() 获取,这样比较专业。注意,如果是对指针变量所指向的内存单元进行清零初始化,那么一定要先对这个指针变量进行初始化,即一定要先让它指向某个有效的地址。而且用memset给指针变量如p所指向的内存单元进行初始化时,n 千万别写成 sizeof(p),这是新手经常会犯的错误。因为 p 是指针变量,不管 p 指向什么类型的变量,sizeof(p) 的值都是 4。 (网上找别人的)

#include<stdio.h>
#include<string.h>
int main(void) {
	int i;
	char str[10];
	char *p = str;

	memset(str, 1, sizeof(str));//参数1就是变量名,中间的1就是指定要初始化的值(可以是任意的值包括字符和浮点数)
				    //最后那个初始化是长度  (可以是填数字,但没必要)					
	for (i = 0; i < 10; i++) {

		printf("%d\t", str[i]);
	}

	return 0;

}复制代码

根据memset函数的不同,输出结果也不同,分为以下几种情况: memset(p, 0, sizeof(p)); //地址的大小都是4字节 0 0 0 0 -52 -52 -52 -52 -52 -52

memset(p, 0, sizeof(

p)); //

p表示的是一个字符变量, 只有一字节 0 -52 -52 -52 -52 -52 -52 -52 -52 -52

memset(p, 0, sizeof(str)); 0 0 0 0 0 0 0 0 0 0

memset(str, 0, sizeof(str)); 0 0 0 0 0 0 0 0 0 0

memset(p, 0, 10); //直接写10也行, 但不专业 0 0 0 0 0 0 0 0 0 0

8、calloc函数

有时候,我们在程序中需要一段内存来处理数据,但是又不确定是要多大内存的情况下,比如 我们申请一个数组 a[100] 但是事前我们并不知道会不会用得完这100个元素,比如我们只会用到10个,那么剩下的90个就会还在占用空间,就显得很浪费空间,这时候使用calloc函数是用来在内存的动态存储区中(堆中)分配一个连续存储空间

函数原型:

void* calloc(unsigned int num,unsigned int size)
在内存的动态存储区中分配num个长度为size的连续空间
num:对象个数,size:对象占据的内存字节数,相较于malloc函数,calloc函数会自动将内存初始化为0复制代码

calloc在动态分配完内存后,自动初始化该内存空间为零,而malloc不做初始化,分配到的空间中的数据是随机数据。

注意:size仅仅为申请内存字节大小,与申请内存块中存储的数据类型无关,故编程时建议通过以下方式给出,"长度 * sizeof(数据类型)";并不需要人为的计算空间的大小,比如如果他要申请20个int类型空间,就可以 int *p = (int *)calloc(20, sizeof(int)) 这样就省去了人为空间计算的麻烦。

函数返回值: calloc函数返回一个指向分配起始地址的指针;如果分配不成功,返回NULL。

#include<stdio.h>
int main(void) {
	int *p = (int *)calloc(10, sizeof(int));
	int i;

	printf("申请得的空间有:\n");
	for (i = 0; i < 10; i++) {
		printf("%d ", *p++);
	}

	return 0;
}

结果:
0 0 0 0 0 0 0 0 0 0
//可以看到,使用calloc函数分配时,它最自动赋值零,而下面要介绍的malloc函数则不会复制代码

那么会有人有疑问:既然calloc不需要计算空间并且可以直接初始化内存避免错误,那为什么不直接使用calloc函数,那要malloc要什么用呢? 实际上,任何事物都有两面性,有好的一面,必然存在不好的地方。这就是效率。calloc函数由于给每一个空间都要初始化值,那必然效率较malloc要低,并且现实世界,很多情况的空间申请是不需要初始值的,这也就是为什么许多初学者更多的接触malloc函数的原因

9、realloc函数

realloc()函数可以重用或扩展以前用malloc()、calloc()及realloc()函数自身分配的内存。

函数原型:

extern void *realloc(void *mem_address, unsigned int newsize);
//指针名 = (数据类型*) realloc (要改变内存大小的指针名,新的大小)。
//新的大小一定要大于原来的大小,不然的话会导致数据丢失!
//如果newsize大小为0,那么释放mem_address指向的内存,并返回NULL。复制代码

先判断当前的指针是否有足够的连续空间,如果有,扩大mem_address指向的地址,并且将 mem_address返回,如果空间不够,先按照 newsize 指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来 mem_address 所指内存区域(注意:原来指针是自动释放,不需要使用free),同时返回新分配的内存区域的首地址。即重新分配存储器块的地址。

1、 realloc()函数需两个参数:一个是包含地址的指针(该地址由之前的malloc()、calloc()或realloc()函数返回),另一个是要新分配的内存字节数。

2、 realloc()函数分配第二个参数指定的内存量,并把第一个参数指针指向的之前分配的内容复制到新配的内存中,且复制的内容长度等于新旧内存区域中较小的那一个。即新内存大于原内存,则原内存所有内容复制到新内存,如果新内存小于原内存,只复制长度等于新内存空间的内容。

3、realloc()函数的第一个参数若为空指针,相当于分配第二个参数指定的新内存空间,此时等价于malloc()、calloc()或realloc()函数。

4、如果是将分配的内存扩大,则有以下3种情况: 1) 如果当前内存段后面有需要的内存空间,则直接扩展这段内存空间,realloc()将返回原指针。 2) 如果当前内存段后面的空闲字节不够,那么就使用堆中的第一个能够满足这一要求的内存块,将目前的数据复制到新的位置,并将原来的数据块释放掉,返回新的内存块地址位置。 3) 如果申请失败,将返回NULL,此时,原来的指针仍然有效。

注意事项:

1、第一个参数要么是空指针,要么是指向以前分配的内存。如果不指向以前分配的内存或指向已释放的内存,结果就是不确定的。

2、 如果调用成功,不管当前内存段后面的空闲空间是否满足要求,都会释放掉原来的指针,重新返回一个指针,虽然返回的指针有可能和原来的指针一样,即不能再次释放掉原来的指针。

返回值:如果重新分配成功则返回指向被分配内存的指针,否则返回空指针NULL。

注意:这里原始内存中的数据还是保持不变的。当内存不再使用时,应使用free()等函数将内存块释放

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int i;
	int *t;

	int*pn = (int*)malloc(10 * sizeof(int));//这里只是申请10个int的空间
	t = pn;

	for (i = 0; i < 10; i++) {  //赋值
		pn[i] = i;
	}

					//如果将这里的数值改大就将有可能出现空闲空间不足,从而申请一块新内存
	pn = (int*)realloc(pn, 20 * sizeof(int)); //多扩充10个int空间加上之前的就是一共20个int

	for (i = 10; i < 20; i++) {//再赋值  注意从第10个开始的
		pn[i] = i;
	}

	for (i = 0; i < 20; i++) {//输出
		printf("%3d", pn[i]);
	}

	printf("\n");
	printf("p=%p \nt=%p\n", pn, t);//输出地址


	free(pn);//释放空间
	pn = NULL;//指针指空

	return 0;


}

如果申请空间的数值较小,原来申请的动态内存后面还有空余内存,系统将直接在原内存空间后面扩容
并返回原动态空间基地址;如果申请空间的数值较大,原来申请的空间后面没有足够大的空间扩容,
系统将重新申请一块新的内存,并把原来空间的内容拷贝过去,原来空间OS自动free;如果申请空间的数值非常大,
系统内存申请失败,返回NULL,原来的内存不会释放。注意:如果扩容后的内存空间较原空间小,将会出现数据丢失,
如果直接realloc(p, 0);相当于free(p).复制代码

使用总结:

  1. realloc失败的时候,返回NULL

  2. realloc失败的时候,原来的内存不改变,不会释放也不会移动

  3. 假如原来的内存后面还有足够多剩余内存的话,realloc的内存=原来的内存+剩余内存,realloc还是返回原来内存的地址; 假如原来的内存后面没有足够多剩余内存的话,realloc将申请新的内存,然后把原来的内存数据拷贝到新内存里,原来的内存将被free掉,realloc返回新内存的地址

  4. 如果size为0,效果等同于free()。这里需要注意的是只对指针本身进行释放,例如对二维指针**a,对a调用realloc时只会释放一维,使用时谨防内存泄露

  5. 传递给realloc的指针必须是先前通过malloc(), calloc(), 或realloc()分配的

    6.传递给realloc的指针可以为空,等同于malloc。

10、malloc与free函数

malloc中文叫动态内存分配,用于申请一块连续的指定大小的内存块区域以void*类型返回分配的内存区域地址,当无法知道内存具体位置的时候,想要绑定真正的内存空间,就需要用到动态的分配内存,且分配的大小就是程序要求的大小。

函数原型:

void * malloc(size_t size);

在以前 malloc返回的是char型指针,新的ANSIC标准规定,该函数返回为void型指针,因此必要时要进行类型转换。
它能向系统申请分配一个长度为num_bytes(或size)个字节的内存块。

其作用是在内存的动态存储区中分配一个长度为size的连续空间。当函数申请内存分配成功时,
此函数的返回值是分配区域的起始地址,或者说,此函数是一个指针型函数,返回的指针指向该分配域的开头位置。
(它返回的是分配得到的内存的首字节地址),如果无法获得符合要求的内存块,malloc函数会返回空指针复制代码

size为要申请的空间大小,需要我们手动的去计算,如int *p = (int * )malloc(20*sizeof(int)),如果编译器默认int为4字节存储的话,那么计算结果是80 Byte,一次申请一个80 Byte的连续空间,并将空间基地址强制转换为int类型,赋值给指针p,此时申请的内存值是不确定的。

malloc函数的实质体现在,它有一个将可用的内存块连接为一个长长的列表的所谓 空闲链表 的功能。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。如果无法获得符合要求的内存块,malloc函数会返回NULL指针(空指针),因此在调用malloc动态申请内存块时,一定要进行返回值的判断。

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

int main(void)
{
	int count, *array; /*count是一个计数器,array是一个整型指针,也可以理解为指向一个整型数组的首地址*/

	if ((array=(int *) malloc(10 * sizeof(int))) == NULL)//把类型强制转换为int 申请内存空间 10个int的空间 
							     //一个int大小是sizeof(int)
	{
		printf("不能成功分配存储空间。");
		exit(1); //强制结束程序
	}

	for (count = 0; count < 10; count++) { /*给数组赋值*/
		array[count] = count;
	}

	for (count = 0; count < 10; count++) { /*打印数组元素*/
		printf("%2d", array[count]);
	}
	return 0;

}复制代码

  • free函数: free()是C语言中释放内存空间的函数,通常与申请内存空间的函数malloc()结合使用,可以释放由 malloc()、calloc()、realloc() 等函数申请的内存空间。

函数原型:

void free(void *ptr);
ptr-- 指针指向一个要释放内存的内存块,该内存块之前是通过调用 
malloc、calloc 或 realloc 进行分配内存的。如果传递的参数是一个空指针,则不会执行任何动作。
该函数不返回任何值。复制代码

上面的例子:

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

int main(void)
{
	int count, *array; /*count是一个计数器,array是一个整型指针,也可以理解为指向一个整型数组的首地址*/

	if ((array=(int *) malloc(10 * sizeof(int))) == NULL)//把类型强制转换为int 申请内存空间 10个int的空间 
															//一个int大小是sizeof(int)
	{
		printf("不能成功分配存储空间。");
		exit(1); //强制结束程序
	}

	for (count = 0; count < 10; count++) { /*给数组赋值*/
		array[count] = count;
	}

	for (count = 0; count < 10; count++) { /*打印数组元素*/
		printf("%2d", array[count]);
	}
	
	free(array);  //刚刚没有进行释放内存

	return 0;

}

******free的重要性:*******

静态内存的数量在编译时是固定的,在运行期间也不会改变,
自动变量使用的内存数量在程序执行期间自动增加或减少,但是动态内存分配内存的数量只会增加,除非使用free函数进行释放

它创建了指针array,并调用了malloc函数进行内存分配了(10* 4(int) )40个字节的内存,假设,如代码注释所示,
遗漏了free,当函数结束时,作为自动变量的指针array也会消失,但是它所指向的40个字节的内存却仍然存在,
由于array指针已被销毁,所以无法访问这块内存,它也不能被重复使用,因为代码中没有调用free函数释放这块内存,
如果是一个函数,当第二次调用它时,它又创建了array指针,并调用malloc分配40个字节的内存,第一次调用的40个字节的内存已不可用,
所以malloc函数分配了另外的内存,当函数结束时该内存也无法被访问和再使用,如果循环要进行1000次,那么每一次的调用都会分配内存,
持续增加,实际上,等不到程序结束,内存早已被耗尽,这类问题被称为内存泄漏,所以 为防止这类问题的发生,
必须要在动态内存分配函数后加上free函数释放内存。复制代码

总结:

malloc 必须要由我们计算字节数,并且在返回后强行转换为实际类型的指针。另外有一点不能直接看出的区别是,malloc 只管分配内存,并不能对所得的内存进行初始化,所以得到的一片新内存中,其值将是随机的

一般使用后要使用 free(起始地址的指针) 对内存进行释放,不然内存申请过多会导致内存泄漏会影响计算机的性能,以至于得重启电脑。如果使用过后不清零,还可以使用该指针对该块内存进行访问。

通常,malloc函数要和free函数一起配对使用,free函数的参数是之前mallloc函数返回的地址(指针),该函数释放之前malloc函数分配的内存,因此,动态内存分配的存储期是从动态内存分配函数malloc(或其他)到f调用ree函数释放内存为止,涉嫌malloc和free函数管理着一个内存池。每次调用malloc分配内存给程序使用,每次调用free函数把内存空间归还给内存池中,这样便可以重复使用这些内存,free函数的参数应该是一个指针,指向由malloc函数分配的一块内存,不能用free函数释放通过其他方式(如 :声明一个数组),分配的内存,malloc函数和free函数的原型都在stdio.h头文件中。

11、动态内存分配(动态存储期)

在程序执行并使用该变量的时候分配内存空间,使用完毕立即释放.

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

当程序运行到需要一个动态分配的变量或对象时,必须向系统申请取得堆中的一块所需大小的存贮空间,用于存贮该变量或对象。当不再使用该变量或对象时,也就是它的生命结束时,要显式释放它所占用的存贮空间,这样系统就能对该堆空间进行再次分配,做到重复使用有限的资源。

在使用数组的时候,总有一个问题困扰着我们:数组应该有多大?在很多的情况下,你并不能确定要使用多大的数组,比如上例,你可能并不知道我们要定义的这个数组到底有多大,那么你就要把数组定义得足够大。这样,你的程序在运行时就申请了固定大小的你认为足够大的内存空间。即使你知道你想利用的空间大小,但是如果因为某种特殊原因空间利用的大小有增加或者减少,你又必须重新去修改程序,扩大数组的存储范围。这种分配固定大小的内存分配方法称之为静态内存分配。但是这种内存分配的方法存在比较严重的缺陷,特别是处理某些问题时:在大多数情况下会浪费大量的内存空间,在少数情况下,当你定义的数组不够大时,可能引起下标越界错误,甚至导致严重后果。

我们用动态内存分配就可以解决上面的问题. 所谓动态内存分配就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不象数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。

从以上动、静态内存分配比较可以知道动态内存分配相对于静态内存分配的特点:

1、不需要预先分配存储空间

2、分配的空间可以根据程序的需要扩大或缩小。

常见的动态内存错误:

  1. 对NULL指针进行解引用操作

  2. 对分配的内存进行操作时越过边界

  3. 释放并非动态分配的内存

  4. 试图释放一块动态分配的内存的一部分以及一块内存被释放之后被继续使用。

说明:

  1. 动态分配最常见的错误就是忘记检查所请求的内存是否成功分配。

  2. 动态内存分配的第二大错误来源是操作内存时超出了分配内存的边界。

  3. 当你使用free时,可能出现各种不同的错误:

    1、传递给free的指针必须是一个从malloc、calloc或realloc函数返回的指针。

    2、传递给free函数一个指针,让它释放一块并非动态分配的内存可能导致程序立即终止或在晚些时候终止。

    3、试图释放一块动态分配内存的一部分也有可能引起类似问题。

//实例:动态内存分配实现可变长一维数组

#define  _GRT_SECURE_NO_WARNNGS
#include<stdio.h>
#include<stdlib.h>
#include"array.h"//这个头文件   里边包含一个结构表示数组和下列函数的声明原型

const Block_size = 20;///一次增容20个存储空间

/*
Array array_creat(int ints_size); //创建一个数组
void array_free(Array  *a);//回收空间
int array_size(const Array *a);//查看当前数组大小
int *array_at(Array *a, int index);//访问数组
void array_inlate(Array *a, int more_size);//增容
*/

int main(void) {

	Array a;//表示数组初始值的大小
	int i, j,n,m=0;

	while (1) {
		printf("请输入你需要多大的数组:\n");
		scanf("%d", &n);
		a = array_creat(n);//这个可得到a里边返回的参数
			
		printf("输入数据 \n");

	
		for (i = 0; i < n; i++) {

			scanf("%d", &j);
			*array_at(&a, i) = j;//这个函数相当与是数组   把j的值保存到数组里边的元素中去
		}

		printf("输出数据:\n");
		for (i = 0; i < n; i++) {//遍历输出
			printf("%d ", a.arrray[i]);
			printf("\n");
		}
		printf("\n");
		printf("输入1初始化数组大小,输入其他表示退出程序:\n");
		scanf("%d", &n);

		if (n == 1) {
			m = 0;//清零
			j = 0;
			array_free(&a);//释放之前的内存
		}

		else {
			exit(0);//退出程序
		}

	}

	return 0;
}


Array array_creat(int ints_size) //创建一个数组
{
	Array a;//定义一个数组的结构体
	a.size=ints_size; //表示数组的长度

	a.arrray = (int *)malloc(sizeof(int)*a.size);//前一个int*是强制类型转换,后面的表示一个int 是4个字节 总共就是长度乘以

	return a;//返回的作用是 让主函数调用它时,能够得到它的参数
}

void array_free(Array  *a)//回收空间
{
	free(a->arrray);
	a->arrray = NULL;//让指针指空  不至于成为野指针
	a->size = 0;

}

//封装
int array_size(const Array *a)//查看当前数组大小
{
	return a->size;
}


int *array_at(Array *a, int index)//访问数组
{
	if (index >= a->size) {
		//下面的公式是为了算出Block_size的底在哪
		//比如130,如果直接加20要加两次,但是用公式就一次完成
		array_inlate(a, (index / Block_size + 1)*Block_size - a->size);//在原来的基础上加20个
	}

	//返回指针   加括号是为了保持优先级不出错
	return &(a->arrray[index]);	//如果返回的是值,那将不能被改变,返回指针就可以进行操作了
}

void array_inlate(Array *a, int more_size)//增容
{
	int *p = (int*)malloc(sizeof(int)*(a->size+more_size));//重新申请一块更大的内存  100 +20
	int i;

	for (i = 0; i < a->size; i++) {//把之前数组的内容拷贝到新的数组中去
		p[i] = a->arrray[i];
	}


	free(a->arrray);//把之前的数组释放
	a->arrray = p;//将指针改变指向  重定向
	a->size += more_size;//大小加上新增的
}

/*程序演示:

请输入你需要多大的数组:
5
输入数据
1 2 3 4 5
输出数据:
1
2
3
4
5

输入1初始化数组大小,输入其他表述退出程序:
1
请输入你需要多大的数组:
6
输入数据
1 2 3 4 5 6
输出数据:
1
2
3
4
5
6

输入1初始化数组大小,输入其他表述退出程序:
0

进程1520已退出.返回代码为 0.
按任意键关闭此窗口...复制代码
//实例:动态内存分配实现可变长二维数组

#include<stdio.h>
#include<malloc.h>

int main(void)
{

	int n, m;
	scanf("%d %d", &n, &m);//n=5  m=2  按照自己输入 来确定二维数组的大小

	int **p = (int **)malloc(sizeof(int *) * n);//利用二级指针  申请五行元素
			
	//p是一个二级指针  malloc函数返回一个int* 的类型  sizeof(int*)表示乘以的指针类型的大小

		/*、申请m个能够能够存放 int* 类型的空间,并将首地址返回给一个二维指针p;
			内存可能的分布情况:

		int a < -- int *; < -- int **p;
		int b < -- int *;
		int c < -- int *;
		int d < -- int *;

		*/

	// (int **) 一个*表示强制类型转换,另一个表示指针 int *
	//sizeof(int*),不能少*,一个指针的内存大小,每个元素是一个指针。用指针长度乘以数量 (int*)*n
	// 这个p指针的数据类型是个二级指针,它指向的这个空间里放的是些一级指针



	for (int i = 0; i < 5; i++)//每行有两列元素
	{
		p[i] = (int *)malloc(sizeof(int) * m);//每个元素是int大小  4*m  将元素保存到每一行
		//每一个一级指针值的大小     指向一个实际大小的空间
		// *(p+i) = p[i]   每一次移动表示行的移动
	}

	//赋值
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < m; j++)
	{
			p[i][j] =1;
			//*(*(p + i) + j) = p[i][j]
		}
	}


	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < m; j++)

		{
			//输出数组每个元素值和地址
			printf("%d=%p\t", p[i][j],&p[i][j]);
			
		}
		printf("\n");
	}

	for (int i = 0; i < n; i++) {//按 行 释放指针
		free(p[i]);
	}

	free(p);//释放整体

	return 0;
}

/*程序演示:
5 2
1=010F44C0	1=010F44C4
1=010F4378	1=010F433C
1=010F4330	1=010F4374
1=010FAB60	1=010FAB64
1=010FAD98	1=010FAB94
进程8432已退出.返回代码为 0.
按任意键关闭此窗口...复制代码

12、const 函数(补充)

之前一直把这个关键字漏掉了现在补上,const 限定符,它把一个对象转换成一个常量,C语言中const关键字是constant的缩写,通常翻译为常量、常数等,有些朋友一看到const关键字马上就想到了常量。事实上在C语言中const功能很强大,它可以修饰变量、数组、指针、函数参数等。

1、 修饰变量:

在程序中使用const修饰变量,就可以对变量声明为只读特性,并保护变量值以防被修改。如下:

const int i = 5; 变量i具有只读特性,不能够被更改;若想对i重新赋值,如i = 10;则是错误的。

值得注意的是,定义变量的同时,必须初始化。定义形式也可以写成int const i=5,同样正确。

此外,const修饰变量还起到了节约空间的目的,通常编译器并不给普通const只读变量分配空间,而是将它们保存到符号表中,无需读写内存操作,程序执行效率也会提高。

2、 修饰数组

C语言中const还可以修饰数组,举例如下:

const int array[5] = {1,2,3,4,5};

array[0] = array[0]+1; //错误

数组元素与变量类似,具有只读属性,不能被更改;一旦更改,如程序将会报错。

3、 修饰函数参数

const关键字修饰函数参数,对参数起限定作用,防止其在函数内部被修改。所限定的函数参数可以是普通变量,也可以是指针变量。举例如下:

void fun1(const int i)

i++; //对i的值进行了修改,程序报错

void fun2(const int *p)

(*p)++; //对p指向空间的值进行了修改,程序报错

保护数组中的元素: 为了避免函数的意图不是为了修改数组当中的数据内容,那么在函数原始声明定义中时应使用关键字const,如: int sum(const a[ ],int n); 这段代码告诉编译器,该函数不能修改a所指向的数组中的内容,如果在函数中不小心使用类似a[i]++;的表达式,那么程序将会报错。 要注意的是,这样使用const并不是要求原数组是常量,而是该函数在处理数组时将其视为常量,不可修改,这样使用const可以保护数组当中的数据不被修改。

4、 修饰指针

C语言中const修饰指针要特别注意,共有两种形式,一种是用来限定指向空间的值不能修改;另一种是限定指针不可更改。举例说明如下:

int i = 5;

int j = 6;

int k = 7;

const int * p1 = &i; //定义1

int * const p2 =&j; //定义2

上面定义了两个指针p1和p2。

在定义1中const限定的是 * p1,即其指向空间的值不可改变,若改变其指向空间的值如*p1=20,则程序会报错;但p1的值是可以改变的,对p1重新赋值如p1=&k是没有任何问题的。

在定义2中const限定的是指针p2,若改变p2的值如p2=&k,程序将会报错;但*p2,即其所指向空间的值可以改变,如 * p2=80是没有问题的,程序正常执行。

关于指针赋值和const需要注意一些规则:

1、把const数据或非const数据的地址初始化为指向const的指针或为其赋值是合法的 2、可以声明并初始化一个不能指向别处的指针,关键是const的位置,这时候,这个指针可以修改它所指向的值,但是只能指向初始化时设置的地址。 3、在创建指针时还可以使用两次const,表示该指针既不能修改它所指向的地址,也不能修改它所指向地址上的值

清单:

int a[10];
const double b[10];

const double *p=a;	//有效
p=b;				//有效
p=&a[3];			//有效

---------------------------
int a[10];
const double b[10];
				//只能将非const数据的地址赋给普通指针  (否则,通过指针就能修改const数组中的值了)
double *p=a //有效
p=b;		//无效*
p=&a[3];	//有效

---------------------------
void sum(const double *a,int n);
		//此函数可以接受普通数组和const数组名作为参数,因为这两种参数都可以用来初始化指向const的指针
int a[10];
const double b[10];
sum(a,5);//合法
sum(b,4);//合法

---------------------------
int a[10];
double *const p=a; //p指向数组的开始
p=&a[0];		   //不允许,因为该指针不能指向别处
*p=9.9;				//可以做,更改a[0]的值

---------------------------
int a[10];
const double *const p=a;
p=&a[0];	//不允许
*p=9.9;		//不允许
复制代码

13、块

块指的是一块数据,是个抽象的概念,和C语言没有关系,这种抽象的东西,别说其他语言也能用,就是日常生活中也会把东西分块管理,C语言中没有对块进行定义,因为这只是个抽象的概念,块可以是内存块,数据块,程序块,哪怕是豆腐块也能是块... 意思就是在管理中被划分为一类的一个基本单位

存储期:

存储期这也是变量的特点,它称为生存期,表示变量在内存中存在的时间的长短。

1、静态存储期:在程序编译时就分配内存空间并保持不变,程序执行结束后才释放。

2、线程存储期:thread_local,其声明后会给每个线程分配一个单独的私有备份

3、自动存储期:局部变量通常都自动为auto 存储期

4、动态存储期:就是用new 或者malloc分配的内存,如果不主动释放,在整个程序都占有内存

作用域:

这个是表示变量在哪些范围内起作用,由链接点决定。

1、块作用域:用{}括起来的,从声明开始到“}” 结束

2、函数作用域:goto(标识符) 的作用域为整个函数。

3、函数原型作用域:函数声明开始,函数声明结束而结束

4、文件作用域:整个文件或者程序

链接属性: 表示变量能在哪些范围内使用.

1、内部链接 :只能在源文件内部使用.

2、外部链接 : 能在源文件内部和外部文件中使用.

3、空连接 : 只能在代码块内(函数内部)使用.

限定符

volatile:

const:

restrict:

_Atomic:


猜你喜欢

转载自juejin.im/post/5ef1c7946fb9a0585d2385a6