turbopascal高级教程的第一章 TURBO PASCAL高级编程技术
TURBO PASCAL是美国BORLAND国际公司的产品,在微机PASCAL市场上占有绝对优势。它克服了往常PASCAL编译系统占用大量内存的缺陷,并对标准PASCAL作了许多有益的扩充,如它具有与低层软件和硬件打交道的能力、具有强大的图形图象功能、支持面向对象的程序设计方法、支持WINDOWS程序设计等等。它是一个名副其实的通用系统程序设计语言,十分适合开发一些高级应用软件、数据库管理系统、编译程序等。另外,TURBO PASCAL还配备有一个高性能的集成软件开发环境,包括编辑、编译、调试、文件管理等一系列功能。
本章就使用TURBO PASCAL开发高级软件的实用技术进行阐述,介绍如何使用一些工具和技术,为TURBO PASCAL程序员提供方便。本章将讲述在程序设计时使用单元的技术、TURBO PASCAL与汇编语言和C语言混合编程技术、实现和使用动态数组的技术、编写中断例程的方法、在程序中使用扩展内存(EMS)和扩充内存(XMS)的方法以及将程序的标准数据作代码处理的方法等。 单元是能与TURBO PASCAL程序分开编译的一组TURBO PASCAL过程和函数。因为单元是单独编译的,所以使用单元的程序编译速度快。而且,一个独立的单元可以为多个程序使用。充分利用单元的优点,不仅可以加快软件的开发速度,而且可以提高程序可维护性。
§1.1.1 单元的结构
一个单元有两部分组成──接口部分和实现部分。如:
unit <标识符>; {单元头}
interface {接口部分开始}
uses <单元列表> {可选项}
{公共说明部分}
implementation {实现部分开始}
{私有说明部分}
{过程或函数的定义}
begin {初始化部分开始}
{初始化代码}
end.
1.接口部分 单元的接口部分由保留字interface开始,在单元头和实现部分之间。在此部分,说明公用的常量、类型、变量与过程和函数的头部。一个程序如果使用了一个单元,那么它就能访问该单元的接口部分所定义的所有变量、数据类型、过程和函数。
接口部分仅包含过程和函数的头部。过程和函数的实现部分在单元的实现部分定义。在程序中使用一个单元只需要知道怎样调用单元中的过程,而不需要知道过程是怎样实现的。
2.实现部分
实现部分是由保留字implementation开始。实现部分定义所有在接口部分声明的过程和函数的程序体。另外实现部分可以有自己的说明,这些说明是局部的,外部程序是不知道它们的存在的,也不能调用它们。
因为在实现部分中声明的一切对象在作用域上是局部的,所以实现部分的改变对其它单元和程序来讲是不可见的。因此,修改一个单元的实现部分,并不需要重新编译使用该单元的单元,只需要编译这个修改单元和使用此单元的程序。然而,如果接口部分做了修改,所有使用该单元的单元和程序,均需要重新编译,甚至需要修改。
在实现部分,如果有uses子句,则必须紧跟在保留字implementation之后。
如果过程说明为external类型,则需用{$L 文件名.OBJ}编译指令将其连入程序。
在接口部分说明的函数或过程,除了inline类型之外,都必须在实现部分再现,它们的头部必须和接口部分一致或用简写格式。
3.初始化部分
单元的整个实现部分通常包括在保留字implementation和end之间。然而,如果把保留字begin放在end之前,在它们中间写一些语句,这些语句就是单元的初始化部分。
在初始化部分可以初始化任何变量,这些变量可由单元使用,也可通过接口部分由程序使用。可以在这部分打开文件供程序使用。例如,标准单元Printer用它的初始化部分使所有输出调用都指向文本文件Lst,这样在write语句中就可以使用它。
当使用单元的程序执行时,在程序的主体执行之前,它所使用的所有单元的初始化部分按uses子句中说明的先后依次被调用。
§1.1.2 单元的使用
当使用单元时,需在uses语句中将使用的所有单元的名字列出来,单元与单元之间用逗号(,)隔开。如:
uses dos,crt;
当编译器扫描到uses子句时,它把每个单元的接口信息加到符号表中,同时又把实现部分的机器码与程序代码连接起来。
1.单元的直接引用
一个模块(程序或单元)的uses子句只须列出该模块直接使用的单元名。例如:
program prog;
uses unit2;
const
a = b;
begin
writeln('a=',a);
end.
unit unit2;
interface
uses unit1;
const
b = c;
implementaion
end.
unit unit1;
interface
const
c = 1;
implementation
const
b = 2;
end.
unit2用了unit1,主程序用了unit2,间接地使用了unit1。
单元的接口部分如果有改动,则所有使用该单元的单元或程序必须重新编译。但如果改动了单元的实现部分,则用到它的单元不必重新编译。在上例中,如果unit1的接口部分改动了(如C=2),unit2就必须重新编译;如果只改动实现部分(b=1),则unit2不必重新编译。
编译一个单元时,TURBO PASCAL计算出该单元的版本数,这个数是单元的接口部分的校验和。上例中,在编译unit2时,unit1的当前版本数存入unit2的编译版本中,编译主程序时,unit1的版本数就和存在unit2中的版本数比较,若二者不同,说明unit2编译后,unit1的接口部分改动过,编译器给出错误信息并重新编译unit2。
2.单元的循环引用
由于在实现部分使用的单元对用户是不可见的,因此把uses子句放在单元的实现部分,进一步隐藏了单元的内部细节,而且有可能构造出相互依赖的单元。
下面的例子说明两个单元如何相互引用。主程序Circular使用Display单元,而Display单元在接口部分说明了Writexy过程,它有3个参数:坐标值x和y和要显示的文本信息,若(x,y)在屏幕内,Writexy移动光标到(x,y)并显示信息,否则,调用简单的错误处理过程ShowError,而ShowError过程反过来又调用Writexy来显示错误信息,这样就产生了单元的循环引用问题。
主程序:
program circular;
uses
crt,display;
begin
writexy(1,1,'Upper left corner of screen');
writexy(100,100,'Way of the screen');
writexy(81-length('Back to reality'),15,'Back to reality');
end.
display单元:
unit display;
interface
procedure Writexy(x,y:integer;Message:string);
implementation
uses CRT,Error;
procedure Writexy;
begin
if (x in [1..80]) and (y in [1..25]) then
begin
gotoxy(x,y);
writeln(message);
end
else
ShowError('Invalid Writexy coordinates');
end;
end.
Error单元:
unit Error;
interface
procedure ShowError(ErrMessage);
implementation
uses display;
procedure ShowError;
begin
Writexy(1,25,'Error: '+ErrMessage);
end;
end.
Display和Error单元的实现部分的uses子句互相引用,TURBO PASCAL能完整编译两个单元的接口部分,只要在接口部分不相互依赖,在实现部分可以相互调用。
§1.1.3 单元与大程序
单元是TURBO PASCAL模块化编程的基础,它用来创建能够为许多程序使用但不需要源程序的过程和函数库,它是把大程序划分为多个相关的模块基础。
通常,一个大程序可以划分为多个单元,这些单元按过程的功能将其分组。例如,一个编辑程序可以划分成初始化、打印、读写文件、格式化等若干个部分。另外,也可以有一个定义全局常量、数据类型、变量、过程及函数的“全局”单元,它能被所有单元和主程序使用。
一个大程序的框架如下:
program Editor;
uses
dos,crt,printer,
EditGlobal;
EditInit;
EditPrint;
EditFile;
EditFormat;
......
begin
...
end.
在大程序开发中使用单元的另一个原因是与代码段的限制有关。8086处理器要求代码段长度最大为64K。这意味着主程序及任何单元都不能超过64K。TURBO PASCAL将每个单元放在一个单独的段中来解决这个问题。
§1.2 与汇编语言混合编程
TURBO PASCAL以编译速度快、生成的目标代码高速和紧凑而著称。在大多数情况下,只使用TURBO PASCAL即可以完成各种各样的程序编制,但是,在硬件接口程序、实时控制程序及大规模浮点运算时,都需要用汇编语言来编程。虽然TURBO PASCAL提供了INLINE语句和命令,以及内嵌式汇编语言(TURBO PASCAL 6.00),但这是远远不够的。本节详细讨论TURBO PASCAL与汇编语言混合编程的技术,并列举了大量的实例。
§1.2.1 TURBO PASCAL的调用协定
TURBO PASCAL程序与外部汇编子程序混合编程时,要涉及到子程序的调用方式、函数或过程的参数传递方法和函数如何返回值的问题,现分述如下。
§1.2.1.1 调用子程序的方式和子程序的返回方式
TURBO PASCAL程序在调用汇编子程序时,可以是近调用也可以是远调用,因此,TURBO PASCAL程序在对汇编子程序进行调用时,根据调用方式的不同,有两种不同的保存返回地址的方法:①近调用时,因是段内调用,仅将偏移地址IP入栈,占2字节;②远调用时,因是段间调用,要将代码段值CS和偏移地址IP入栈,占4字节。
在主程序中直接调用汇编子程序时,一般采用近调用,汇编子程序采用近返回方式,用RET指令;在TURBO PASCAL的单元中使用汇编子程序时分两种情况:①在单元接口部分说明的子程序,在汇编子程序中要用远返回,用RETF指令;②在单元解释部分说明的子程序,汇编子程序要用近返回方式,用RET指令。
汇编子程序在运行结束后,为了能正确地返回到调用程序,栈顶指针必须指向正确的返回地址,它通过在返回指令RETF(或RET)中给出参数入栈时所占的字节数的方法实现的。
§1.2.1.2 参数传递的方法
TURBO PASCAL是利用堆栈向过程和函数传递参数的,参数按从左到右的顺序(说明顺序)被压入堆栈中,例如调用过程PROC(A,B,C : INTEGER; VAR D)时,其堆栈情况见图1-1。
殌 ┌────────────┐
│+0E│ 参数A的值 │ ↑
│ ├────────────┤ │
参│+0C│ 参数B的值 │ │参
│ ├────────────┤ │
数│+0A│ 参数C的值 │ │数
│ ├────────────┤ │
压│ +8│ 参数D的段地址 │ │出
│ ├────────────┤ │
栈│ +6│ 参数D的偏移地址 │ │栈
│ ├────────────┤ │
顺│ +4│ 过程返回的段地址 │ │顺
│ ├────────────┤ │
序│ +2│ 过程返回的偏移地址 │ │序
↓ ├────────────┤ │
│ BP寄存器的值 │
└────────────┘
殣 图1-1.TURBO PASCAL远调用汇编程序PROC的堆栈情况
TURBO PASCAL在调用子程序时,有两种传递参数的方法,即传值和传地址的方法。下面分别说明这两种参数传递方法。各种类型参数入栈的方法见表1-1。
(1)传值方式
在TURBO PASCAL的过程或函数的形式参数表中,以值参形式定义的参数,且类型是记录、数组、字符串、指针等复合类型以外的各种类型,如字节型(BYTE)、短整型(SHORTINT)、整型(INTEGER)、字型(WORD)、长整型(LONGINT)、字符型(CHAR)、布尔型(BOOLEAN)、实数型(REAL)等,TURBO PASCAL在调用子程序时,直接将实参值依次从左到右顺序压入堆栈中,汇编子程序可以直接从堆栈中取得实参的值。
(2)传地址方式
在TURBO PASCAL的过程或函数的形式参数表中,以变量形式定义的参数,及以记录、字符串、数组、指针等复合类型定义的值参,TURBO PASCAL在调用子程序时,是将调用程序的实参地址依次按从左到右的顺序压入堆栈的。汇编子程序从堆栈中取得实参的地址,即可得到参数的值。同样汇编子程序可以把运算结果存放到对应的变量中,以便传回调用程序。
表1-1.各种类型参数入栈的方法
殌┌───────┬────┬─────┐
│形参类型 │传递方式│栈中字节数│
├───────┼────┼─────┤
│char,boolean │ │ │
│byte,shortint,│ 传值 │ 2 │
│integer,word │ │ │
├───────┼────┼─────┤
│longint,single│ 传值 │ 4 │
├───────┼────┼─────┤
│real │ 传值 │ 6 │
├───────┼────┼─────┤
│double │ 传值 │ 8 │
├───────┼────┼─────┤
│string,pointer│ 传地址 │ 4 │
│变量 │ │ │
└───────┴────┴─────┘殣
§1.2.1.3 函数返回值的传递
TURBO PASCAL函数返回值的传递方式根据函数返回值类型的不同而异,有采用传地址的方式进行,也有采用寄存器方式进行,如采用传地址的方式,其地址(4字节)首先入栈,然后才压入函数参数,最后压入函数的返回地址。各种函数返回类型的传递方式见表1-2。
表1-2.各种函数返回类型的传递方式
殌┌───────┬──────────┬──────┐
│ 函数返回类型 │ 返 回 方 式 │ 所占字节数 │
├───────┼──────────┼──────┤
│boolean,byte, │ 在寄存器AL中 │ 1 │
│char,shortint │ │ │
├───────┼──────────┼──────┤
│word,integer │ 在寄存器AX中 │ 2 │
├───────┼──────────┼──────┤
│longint │ 高位在DX,低位在AX │ 4 │
├───────┼──────────┼──────┤
│real │由高到低在DX,BX,AX中│ 6 │
├───────┼──────────┼──────┤
│pointer │段地址在DX,偏移在AX │ 4 │
├───────┼──────────┼──────┤
│string │ 在DS:SI指的地址中 │ 不限 │
└───────┴──────────┴──────┘
殣
§1.2.2 汇编子程序的编写格式
根据TURBO PASCAL的调用协定,外部汇编子程序的通用编写格式如下:
TITLE 程序名
DOSSEG
LOCALS @@
.MODEL TPASCAL
.CODE
ASSUME CS:@CODE
PUBLIC 过程或函数名
过程或函数名:
PUSH BP
MOV BP,SP
…
POP BP
RETF 参数占堆栈字节数
END
上述汇编子程序是TURBO ASSEMBLER的格式,本文汇编子程序均采用这种格式。对此汇编子程序格式说明如下:
. 汇编模块要采用TPASCAL模式;
. 在汇编模块中,必须把TURBO PASCAL调用的过程或函数说明为PUBLIC属性;
. 子程序返回指令视具体情况而定,近调用用RET,远调用用RETF;
. 返回指令后的参数是指该子程序形式参数表中所有参数入栈后所占堆栈的字节数;
. 汇编模块结束要写END。
§1.2.3 TURBO PASCAL程序的编写格式
在TURBO PASCAL中,声明外部子程序的格式如下:
procedure prc(a, b : integer; var c : real);external;
function func(a, b : integer) : real; external;
即在通常的TURBO PASCAL过程或函数的声明后加上external关键字。在声明了外部过程或函数的主程序或程序单元中,要用编译指令{$L},把汇编好的目标模块加载进来。
在TURBO PASCAL程序中使用外部汇编过程或函数时,方法和一般的TURBO PASCAL过程和函数没有两样。
§1.2.4 主程序中使用外部汇编子程序的典型例子分析
在TURBO PASCAL主程序中直接使用外部汇编子程序时,一般采用近调用方式,所以汇编子程序返回指令为RET,在特别指明采用远调用方式时,要用RETF返回指令。
1.无参数传递的过程
program prog1;
{$L prog1.obj}
procedure DisplayOk;external;
begin
DisplayOk;
end.
Title PROG1
LOCALS @@
DOSSEG
.MODEL TPASCAL
.CODE
ASSUME CS:@CODE
OkMsg db 'OK !',0dh,0ah,'$'
; Procedure DisplayOk
PUBLIC DisplayOk
DisplayOk:
push ds ;保存数据段
push cs ;代码段入栈
pop ds ;弹出数据段
mov ah,09 ;显示字符串
mov dx,offset OkMsg ;字符串地址
int 21h ;DOS功能调用
pop ds ;恢复数据段
ret ;近返回
end ;汇编子模块结束
2.传递字符型值参的过程
program prog2;
{$L prog2.obj}
procedure DisplayInt(ch : char);external;
begin
DisplayInt('a');
end.
Title PROG2
LOCALS @@
DOSSEG
.MODEL TPASCAL
.CODE
ASSUME CS:@CODE
; Procedure DisplayInt
PUBLIC DisplayInt
DisplayInt:
push bp
mov bp,sp
mov ah,02 ;显示字符
mov dl,[bp+4] ;从栈中取参数
int 21h ;DOS功能调用
pop bp
ret 2 ;形式参数在栈中占2字节
end
3.传递字符型值参和整型变参的过程
program prog3;
{$L prog3.obj}
procedure ProcArg(ch : char;var i : integer);external;
var i : integer;
begin
ProcArg('d',i);
writeln(i);
end.
Title PROG3
LOCALS @@
DOSSEG
.MODEL TPASCAL
.CODE
ASSUME CS:@CODE
; Procedure ProcArg
PUBLIC ProcArg
ProcArg:
push bp
mov bp,sp
xor ax,ax
mov al,byte ptr [bp+8] ;取字符参数
les si,[bp+4] ;取整数变量的地址
mov es:[si],al
pop bp
ret 6 ;形式参数在栈中占6字节
end
4.传递字符值参返回整型的函数
program prog4;
{$L prog4.obj}
function func(ch : char) : integer; external;
begin
writeln(func('a'));
end.
Title PROG4
LOCALS @@
DOSSEG
.MODEL TPASCAL
.CODE
ASSUME CS:@CODE
; Procedure func
PUBLIC func
func:
push bp
mov bp,sp
xor ax,ax
mov al,byte ptr [bp+4] ;取字符参数值
pop bp
ret 2 ;形式参数在栈中占2字节
end ;子程序返回值在寄存器AX中
5.传递字符串型值参和返回字符串型的函数
program prog5;
{$L prog5.obj}
function func(s : string) : string; external;
begin
writeln( func('abcd') );
end.
Title PROG5
LOCALS @@
DOSSEG
.MODEL TPASCAL
.CODE
ASSUME CS:@CODE
; Procedure func
PUBLIC func
func:
push bp
mov bp,sp
push ds
xor ch,ch
lds si,[bp+4] ;取字符串S的地址
les di,[bp+8] ;取返回值地址
mov cl,[si]
inc cl
cld
@@1:
lodsb
stosb
loop @@1
pop ds
pop bp
ret 4 ;字符串S的地址在栈中占4字节
end
6.传递长整型值参和返回长整型的函数
program prog6;
{$L prog6.obj}
function func(li : LongInt) : Longint; external;
var i : longint;
begin
i := func(111111110);
writeln(i);
end.
Title PROG6
LOCALS @@
DOSSEG
.MODEL TPASCAL
.CODE
ASSUME CS:@CODE
; Procedure func
PUBLIC func
func:
push bp
mov bp,sp
mov ax,[bp+4] ;取长整型数高位
mov dx,[bp+6] ;取长整型数低位
les si,[bp+8] ;取函数返回地址
mov es:[si],dx
mov es:[si+2],ax
pop bp
ret 4 ;长整型数LI在栈中占4字节
end
7.传递实型数值参和返回实型数的函数
program prog7;
{$L prog7.obj}
function func(r : real) : real; external;
var r : real;
begin
r := func(11111.1110);
writeln(r);
end.
Title PROG7
LOCALS @@
DOSSEG
.MODEL TPASCAL
.CODE
ASSUME CS:@CODE
; Procedure func
PUBLIC func
func:
push bp
mov bp,sp
mov ax,[bp+4] ;取实数R的值
mov bx,[bp+6] ;
mov dx,[bp+8] ;
les si,[bp+0ah] ;取函数的返回地址
mov es:[si],dx
mov es:[si+2],bx
mov es:[si+4],ax
pop bp
ret 6 ;实数R在栈中占6字节
end
----------------------------------------------