一、基础研究
我们之前研究过变量、数组、函数和指针,他们都可以看作是内存中存储的一段数据,当程序需要用到它们时,会通过它们的地址找到它们并进行调用,只是调用的用途不同而已:变量和数组元素是作为常量来处理,对它们进行赋值、运算、取址等操作,而程序是从首地址开始执行直到返回,指针是用来对地址进行操作,或者对指向的内容进行操作。但是我们要知道,它们在内存中都是以一个字节一个字节的数据形式存储的,我们可将他们的存储空间都看作是一个char型数组。
现在定义了一个有200个元素的char型数组a,要我们向a中加入数组元素,使程序可以在屏幕中间打印一个字符“c”。在执行程序main里只有一句语句:((void (far *)())(long)a)();分析语句:a是数组的首地址,将它强制转换成long型使它的数据包含段地址和偏移地址的数据,但此时它还不是一个地址而是一个long型变量,那么我们将它再强制类型转换成一个void(far *)()型的函数指针,它是一个远指针,指向一个void型的函数,所以这个函数的入口地址就是数组a的首地址。即程序是要执行以数组a里的元素构成的语句所组成的函数。那么我们要向数组a里填充的是一段内存空间的数据,这些数据连起来能被翻译成一段语句,这段语句的功能是在屏幕中间打印一个字符“c”。
那么我们先考虑怎么在屏幕中间打印一个字符“c”。我们知道输出函数都是在当前光标的位置输出,而我们要在屏幕中间打印是在固定位置输出,即在段地址为b800的数据段的某一个位置存放要输出的数据,我们知道dos窗口的大小是80*25的,一共占4000个字节,那么屏幕中间是,即偏移地址为:(13*80+40-1)*2=2158=0x86e,那么要打印的地址为0xb800086e。我们怎么把字符c放到内存中地址0xb800086e处呢?要对地址操作首先想到的就是指针,因为要存放段地址加偏移地址,所以要定义一个far指针。我们先写一个输出的程序如下:
输出结果为:
可见结果是正确的。现在我们要找到这个程序在内存中存储的数据,我们用debug加载程序:
可以看到,程序从01fa开始,到0215结束,我们查看这一段的内存:
可见这一段的内存里的数据为55 8b ec 83 ec 04 c7 46 fe 00 b8 c7 46 fc 6e 08 c4 5e fc 26 c6 07 63 8b e5 5d c3,我们将这些数据存到数组a中,看能否打印出c,结果发现虽然输出了c,但是还在屏幕上输出了两个字符,还有程序输出了之后没有正确返回:
但是查看内存,我们向数组a里补充的数据完全正确,用u命令查看也发现和我们之前写的输出语句的汇编语句一样,那是为什么呢?我们之前写的输出语句是在main函数里输出的,但是这里数组a是在数据段里的,不是在一个段里,需要返回值,所以我们在输出函数里把输出语句写在一个far型的子函数里,程序如下:
查看函数f的汇编语句为:
可以发现最后的返回语句变成了retf,再查看内存空间:
只有21e处的c3变成了cb,修改之后发现能够正常显示:
所以我们在写程序要转换程序和数据时一定要注意这个程序的位置,它的数据能否直接移植到其他程序里面使用。还有一定要注意,我们将一个数组的首地址强制转换成函数指针时,一定要先将它强制转换成long型数据,这样它才能包含段地址和偏移地址,函数指针才能正确指向到函数。
二、扩展研究
到现在为止我们已经学过了c语言一些比较重要的也是主要的部分:变量、数组、函数、指针,我们还了解了一些编译原理和编译器的命令,现在来总结一下:
变量:
变量是一种存放数据方式,与常量不同,它的内容是可以改变的。它可以分为全局变量和局部变量,它们的本质区别是存储的位置不同,全局变量是在内存中存储的,而局部变量是在栈段中存储的,这个差别导致了它们的一系列区别:全局变量存储的内存空间是没有内存对齐的情况的,而局部变量有;全局变量作为参数传递是直接用地址调用,而局部变量是入栈的方式;全局变量的生命周期是整个程序,而局部变量的生命周期是当前函数;全局变量的段地址在ds寄存器里,局部变量的段地址在ss寄存器里;全局变量定义是自动清零的,而局部变量定义时在栈中的空间还是原来的数据。比较特别的静态局部变量的存储位置和生命周期都和全局变量一样,只是静态局部变量只能在定义的函数中使用。
比较重要的变量类型有char、int、long、double和结构体,它们分别占的大小为1字节、2字节、4字节、8字节,结构体的大小是结构体中数据项之和。结构体也存在内存对齐的情况,结构体中各数据项存储位置是相邻的。结构体作为参数传递和返回比一般变量要复杂,一般变量都是直接入栈,而结构体必须创建一个临时变量,用块搬移函数将结构体的各数据项复制到临时变量里,在子函数里再将临时变量的值搬移到栈段里面,返回的原理也是相同的。
数组:数组是利用一段连续的内存空间存放一系列相同类型的数据。一维数组是存储的数据按照线性的顺序来排列,二位数组是存储的数据按照网状的顺序来排列,多维数组是存储的数据以多维的形式存储,我们可以通过当前哪一组不断向下查找到某一个元素。数组也可以根据存放的元素类型不同来分类:整型数组的元素是int型数据、指针型数组的元素是指针型数据、结构体数组的元素是结构体数据。数组中的元素是连续存放的。数组名相当于数组的首地址,也是数组第一个元素的地址,它的使用和指针有相似之处,如果p是一个指针,那么p[n]等同于*(p+n),即跳到下一个元素就相当于在当前地址上加上数组的类型大小。数组还有函数指针数组,存放的元素是函数指针,指向函数指针,函数指针数组可以将要运行的程序以数据的形式写入并对函数进行调用。
函数:函数是一段语句的集合。函数名相当于一个函数指针,存储函数的入口地址,程序由这个入口地址跳转到当前函数。函数的参数是局部变量,在调用该函数的函数中将参数压入栈中,在子函数里用bp寄存器找到参数的地址进行调用。函数可以有返回值,void函数没有返回值,函数的返回值一般是存储在寄存器中,如果返回值为结构体,则将结构体的内容传递到一个临时变量里。函数是一段数据,它同样存储在内存空间里,这样我们可以以数据的形式将一个函数写到内存中执行。
指针:指针存储的数据是一个地址,我们可以通过“*”来取得指针存储的这个地址处的内容,通过“&”来取得一个内存空间的地址赋给指针。指针加减一个数并不是以它的值加减一个数字,而是加减它所指向的存储空间的数据类型的大小,即如果它指向的是int型数据,那么加1就是在当前地址上加上2个字节。我们可以将一个地址赋给一个整形变量,但我们不能对一个整形变量使用“*”取得它所存储的地址处的值,因为它不是一个指针,同时如果一个指针是一级指针,即定义成*p,那么只能用“*”对它取一次值,如果一个指针是二级指针,可以用“*”对它取两次值,总之,一个指针是几级指针,就可以对它取几次值。对于指针的使用我们一定要注意它和其他的变量的类型匹配问题,近指针占2个字节,存储偏移地址,远指针占4个字节,存储段地址加偏移地址,我们要用%p输出近指针的值,用%Fp输出远指针的值。虽然指针的值的大小是固定的,但是指针指向的值的大小和指针的定义有关。指针在地址和内容之间建立了一条联系,这种联系是c语言最重要的基础,我们可以用它来实现多种数据结构。指针可以指向任意的数据类型,结构体指针指向的是一个结构体,它可以以->符号调用结构体的数据项。函数指针是指向一个函数入口的指针,当定义一个函数指针时要指明函数的类型和参数类型和个数,通过函数指针可以调用指定位置的函数。
三、研究总结
c语言十分精简也十分快捷,它的优点是建立在用户可以直接对内存进行操作的基础上,这样用户实现一种功能可以有很多种写法,可以说是能够充分发挥用户的想象力和创造性,但是这样也有缺点,就是错误可能很多。我们需要将这些知识融合起来,融合的最好的方式就是多写程序,发挥自己的想象力和创造性,对于一个问题,思考多种解决办法,如果碰到了问题,要仔细思考问题在哪里,这样才能够提高。对于有问题的地方,不要随便问问题,要自己先写程序来试验自己的猜想。