点击链接加入群【C语言】:http://jq.qq.com/?_wv=1027&k=2H9sgjG


    

好吧,上一小节我们介绍了函数的递归形式,就是函数自己调用自己,系统会占用大量的栈空间,而且栈空间是由系统进行管理的,我们知道PUSH POP CALL RET 等操作都是在操作ESP指针,那么我们如何才能自己定义一些空间呢?

还记得进程的四大分区吗?代码区、堆区、栈区、数据区。恩,你记得的话,证明你学的还不错啦,我还还有一个两个区域没怎么介绍过,一个就是堆区,另一个是数据区,数据区我们会在后续小节中慢慢展开,因为会涉及到C++的一些概念,其实C里面的很简单的啦。

今天我们就来研究一下堆区。堆区是由系统为我们分配的一块大内存,我们可以自由的使用这块空间。好吧,那么如何使用呢?以及为什么要使用这个空间呢?且听我慢慢道来。

提问:结构体我想大家都知道它大概是什么意思了吧,那么我们有一个定义十分复杂的结构体,总大小比如是256个字节,这是一个非常大的结构体啦,如果我们有5000个这种结构体实例,那么需要的内存空间是很大的,全都在栈中估计是不行的滴,所以,当我们需要大容量空间的时候就要用堆啦,堆是由系统管理,而且可以动态增长的哦。

我们来讲一下怎么在堆区中申请内存空间,malloc函数,我们先在MSDN中查看一下这个函数的声明形式(也叫原形)

void *malloc( size_t size );

唉,很好呀,出现了两个类型,一size_t二void *,size_t 其实就是无符号整数啦,还记得typedef 嘛,哈哈,看到了吧,它的功能还是很强大的哦,它是大小类型,描述大小的类型当然不需要负数啦。那么void *呢,它比较特别,我们之前没有接触过,它也是一种类型哦,更通用一些,就是可以记录所有地址的类型,是不是很爽呢,什么意思呢,比如你有一个int * 的变量,可以直接把它赋给void *的变量哦, 同样其它类型的指针变量也可以直接赋给void * 的变量哦,这样有什么好处?通用。再举一个例子,我们再来看一个函数声明

void *memcpy( void *dest, const void *src, size_t count );

我们看这个名字,就是将内存中某一地址的数据复制到另一个地址处去,总共复制count个字节,看参数类型都是void *,const我们暂时不管它,也就是你只要传两个地址过来就行,不管他们是什么类型的,好理解吧,如果你前面学的好,这里我感觉应该不是难题哦,我们称这个函数叫内存拷贝函数。

再回来看malloc,申请size个字节的内存,返回的是void * 类型的地址,我们可以想像,当你有一个地址后,如果给他赋以某种意义,那么我们就可以使用这块空间了。

#include <stdio.h>

#include <stdlib.h>

int main()

{

void *pa=malloc(12);

char *pb=(char *)malloc(16);

short *pc=(short *)malloc(8);

int *pd=(int *)malloc(20);

free(pa);

free(pb);

free(pc);

free(pd);

return 0;

}

好,pa,pb,pc,pd这4个变量本身是栈上,但是它们的值指向了堆区空间,如果大家在内存地址窗口中看一下它们指向的空间,会发现一个有意思的事,申请的空间的值都被初始化为CD啦,哦,对了,忽然想起一件事来,就是void *既然可以指向任何类型,所以*运算符就不能再应用到它上面啦,如果直接 *pa是不行的哦,还记得 *号的作用嘛。哈哈,所以嘛,基础打牢了,才能爬的更高滴。还有一点要记住的事情就是,我们申请的内存系统是不会给我主动释放滴,需要程序员自己去释放,也就是我们下面的四个free函数的调用啦。大家也可以看看内存释放后,它们原来指向的数据变成了什么呢?也就是CD 会有什么变化哦,这个任务留给大家了。好,问题是我们释放了我们申请的内存之后,我们再来观察我们的4个指针变量的值,它们依旧指向了之前申请的空间,这是我们所不想的,既然内存已经释放了,那么我们就不能再有指针指向那块区域了哦,不然,你以后不经意再用到这个指针,会导致访问的数据不正确,也就是程序会出现BUG,所以我们一般会在释放完内存空间后,将指针值变为NULL(你认识它吗),也就是应该在return 0;之前加上下面的代码 pa = NULL; pb = NULL; pc = NULL; pd = NULL;先来解释一下NULL吧,在代码中右击NULL,选择转到定义,可以看到如下的指令,什么指令呢?以#开头的,我们讲过两个,#include #define ,说白了,就是预处理指令啦

#ifndef NULL

#ifdef __cplusplus

#define NULL 0

#else

#define NULL ((void *)0)

#endif

#endif

那么它是什么意思呢,如果没有定义NULL,但是定义了__cplusplus,那么就把NULL定义为0,否则就将NULL定义成 ((void *)0),结束第二个如果,结束第一个如果。大白话,你能看懂吧,这些指令都是由预处理器进行处理呢,是不是看你的心痒痒呢(语出 月光宝盒),哈哈。

其实预处理指令还有许多,如果想知道都有哪些,怎么办,MSDN LIBRARY,这个工具可是很给力呢,配套VC6的是MSDN 2001,孙鑫老师讲MFC用的就是这个哦,如图

然后双击# (number sign),右边的界面会显示一些数据,里面有一个链接,preprocessing directives 点开它,就可以查看所有的预处理指令啦。

那么我们来简单介绍几个简单的指令#define 你已经知道了,用于定义宏,其实就是在预处理的时候进行等价替换啦,好,#error,用于提示错误信息,当预处理器读到这个东西的时候,会在输出窗口输出#error后面跟的数据,一般用于提示用户,以后我们会给大家演示的,先暂时做一个了解啦,如下面的代码说明,

//#error 安全问题:连接字符串可能包含密码。

// 此连接字符串中可能包含明文密码和/或其他重要

// 信息。请在查看完此连接字符串并找到所有与安全

// 有关的问题后移除 #error。可能需要将此密码存

// 储为其他格式或使用其他的用户身份验证。


默认这个#error是不被注释的,当我们编译的时候,就会提示这里有问题,然后双击错误就会到这里来,一看就知道,人家说这里有安全问题呢,也就是要我们注意一下啦,这是微软自动生成的,所以我们要想使用,需要把这句话注释起来。

#if #else #endif #elif #ifdef #ifndef 都很好理解吧,就跟程序中的if else 差不多,只不过 #if 需要和#endif配对使用啦。

#undef 这个是用来取消已经定义的宏的,如

#define DUIBAI "曾经有一份真挚的爱情摆在我面前,我没有珍惜,真到失去后才后悔莫及,人世间最痛苦的事莫过于此....."

int main(){

char *p=DUIBAI;

#undef DUIBAI

#define DUIBAI "如果上天能够给我一个再来一次的机会,我一定会对那个女孩子说3个字,我爱你,如果非要为这份爱加上一个期限,我希望是1万年..."

char *pp = DUIBAI;

return 0;

}

好理解吧,就是用来取消宏定义用的。#include 这个不用我说了吧。其它的暂时不讲啦。好了,你明白了,那么我们来深入一下,为什么在申请内存的时候需要指明大小,而释放内存的时候却只要个首地址就行了呢,这个问题其实是很有意义的哦。

怎么会这样子,累哦累哦累。

其实我们调用malloc申请内存空间的时候,系统自动做了一件事情,那就是为我们申请的空间做了记录,怎么讲,就是我们在哪个位置申请多少字节的内存系统都有记录。

好,我们来看一下系统是怎么记录的,重点到了哦,这里会讲到数据结构呢,打起精神来!!!!!还是上面那个申请了四次内存空间的代码,我们来分析它,在free(pa) 那句那里下断点,然后调试然后我们定位到四个指针所指向的数据(未初始化的都是 CD啦),然后将数据的上面的32个字节和下面的4个字节都复制出来pa对应的:

00380FE0 98 07 38 00

00380FE4 28 10 38 00

00380FE8 00 00 00 00

00380FEC 00 00 00 00

00380FF0 0C 00 00 00

00380FF4 01 00 00 00

00380FF8 2D 00 00 00

00380FFC FD FD FD FD

00381000 CD CD CD CD

00381004 CD CD CD CD

00381008 CD CD CD CD

0038100C FD FD FD FD

pn对应的:

00381028 E0 0F 38 00

0038102C 80 10 38 00

00381030 00 00 00 00

00381034 00 00 00 00

00381038 10 00 00 00

0038103C 01 00 00 00

00381040 2E 00 00 00

00381044 FD FD FD FD

00381048 CD CD CD CD

0038104C CD CD CD CD

00381050 CD CD CD CD

00381054 CD CD CD CD

00381058 FD FD FD FD

pc对应的:

00381080 28 10 38 00 整数1

00381084 C8 10 38 00 整数2

00381088 00 00 00 00 整数3

0038108C 00 00 00 00 整数4

00381090 08 00 00 00 整数5

00381094 01 00 00 00 整数6

00381098 2F 00 00 00 整数7

0038109C FD FD FD FD 整数8

003810A0 CD CD CD CD

003810A4 CD CD CD CD

003810A8 FD FD FD FD 整数9

pd对应的:

003810C8 80 10 38 00

003810CC 00 00 00 00

003810D0 00 00 00 00

003810D4 00 00 00 00

003810D8 14 00 00 00

003810DC 01 00 00 00

003810E0 30 00 00 00

003810E4 FD FD FD FD

003810E8 CD CD CD CD

003810EC CD CD CD CD

003810F0 CD CD CD CD

003810F4 CD CD CD CD

003810F8 CD CD CD CD

003810FC FD FD FD FD

好了,我在写的时候就考虑了,这里应该会很长,所以请大家将数据复制到 文本中,好了,我们来观察数据,我们申请的pa,pb,pc,pd 分别指向00381000,00381048 , 003810A0 ,003810E8 ,也就是我们看到的CD CD的起始位置。

那么在这个地址上面有32个字节,可以理解成8个整数啦,其实是某种数据结构啦,我们先来看看它里面记录了哪些东西呢?

以pc为例:

整数1 记录的 00381028,很显然,我们可以想像,它应该是个地址,但现在不去关心它记录的哪个地址

整数2 记录的 003810C8,同上

整数3 记录的 0,猜不出是什么东西

整数4 记录的 0,同上

整数5 记录的 8,有一定的意义,如果大家看下这句话short *pc=(short *)malloc(8); 我想应该就明白了,是我们申请的空间的大小吧,哦哦。

整数6 记录的 1,猜不出是什么东西

整数7 记录的 2F,暂时猜不出是什么东西,不过,我们可以多看看几个,如pa的是2D,pb的是2E,pd的是30,哦,看出来了吗,是一个编号,呼呼。

整数8 和 整数9 固定的值 FDFDFDFD,这个我就直接告诉你们吧,是一个边界检测用的,就像是门卫一样,在它们里面的才是数据。

好了,我们简单的分析了几个,那么我们再来看一下整数1和整数2以及整数1的首地址,我做一个表格吧


字段名papbpcpd
整数1的首地址00380FE00038102800381080003810C8
整数10038079800380FE00038102800381080
整数20038102800381080003810C800000000

好,表格已经有了,我们要的整数1的首地址,整数1和整数2的值,我们来分析一下吧,

pa的整数2的值=pb的整数1的首地址, 其它向后依次类推

pd的整数1的值=pc的整数1的首地址, 其它向前依次类推


也就是我们现在有四个内存块,而每个内存块用前8个字节记录了下一个内存块的起始位置和上一个内存块的起始位置,这种内存分部是散乱的,也就是数据不需要像数组一样必须存储在连续的内存地址空间中,而它们之前是用8个字节相互联系的,我们称记录上一个内存块起始位置的值叫做前向指针,而记录下一个内存块的起始位置的值叫做后向指针,其实怎么叫它都无所谓,关键是大家要明白。


我们看到pd的后向指针为0,意味着它后面没有内存块了,而pa的前向指针依然有值,说明pa前面还有内存块,而我们之前还分析了一个编号,其实就是这个内存块的编号,系统每申请一次内存就会为申请的空间指定一个编号,一般是递增的,以加1的方式。所以,pd是第48个内存块。


好了,大家应该理解这种数据结构了吧,我们为它起个名字吧,就叫它 双向链表吧,为什么双向,因为我们可以用整数1从前往后找每一个内存块,也可以用整数2从后向前遍历每一个内存块,哈哈,很简单吧,至于怎么写一个双向链表呢?


等我们学了C++之后再来介绍吧。记住,大家不需要自己去写滴,也就是说你写不出来也没有什么关系啦。

那么系统为什么要用这种数据结构呢?它之所以会被使用,肯定有它的道理啦。


我们可以想像一下,如果我们新申请一块内存,然后要放在pa,pb之间,怎么办,是不是只要将整数1和整数2的值稍微调整一下就行了呀,是的。

那么要删除pb呢?只需要将pa,pb,pc之前的整数1和整数2稍微调整一下,让pb从它们中间消失,不就行了,哈哈,其实就是这么回事啦。

我们用双向链表主要是因为它进行数据的插入和删除操作速度比较快。

可以想像,如果你要删除数组中的一个数,比如1-100,删除1,那么你得把2-100 从原来的位置搬运到 1-99上去,也就是后面的每一个数据都得向前移动一下,所以说数组不适合做数据的经常插入和删除操作。

既然系统记录了相关的数据结构,所以释放内存的时候只需要给一个地址,它就可以知道应该释放多少内存啦。

既然明白了上面的内容,我们就来看看一些常用的函数吧:

void *malloc( size_t size );

void free( void *memblock );

void *realloc( void *memblock, size_t size );

void *calloc( size_t num, size_t size );


这四个中的前两个大家应该很清楚了,第4个的意思是 申请 num个大小为size的连续内存块,比如num =14,size=4 则相当于malloc(14*4),不过它们有个区别就是calloc会将申请的空间的数据置为0,而malloc是不会主动置0的哦,那么第3个呢?意思是再分配,比如我们已经有了一个指针p,它指向一块内存空间,大小为100B,如果这块空间大小不够用了,怎么办,再分配,就调用realloc,第一个参数是你想为哪个内存块再分配,这里写p,size新大小,我们可以想像,p指向的内存块的后面的空间有没有被使用,还有多少空间可以用,比如后面还有80个字节可以用,如果size是160,那么我们只要将原内存块扩展为160就行了,但是如果是190,则80就不够啦,就需要再进行新空间的分配啦,然后将原内存块里的数据拷贝到新空间中,并且返回新空间的位置,还要释放原空间。这个过程其实很好理解滴,嘻嘻!

好了,这里就分析这么多,主要是为了加深大家对内存的认识,以及如何动态申请内存空间。再见!