Chapter 3 - continued - - Part 3 of 3 parts - of the Turbo Pascal Reference The Turbo Pascal Language This chapter is part of the Turbo Pascal Reference electronic freeware book (C) Copyright 1992 by Ed Mitchell. This freeware book contains supplementary material to Borland Pascal Developer's Guide, published by Que Corporation, 1992. However, Que Corporation has no affiliation with nor responsibility for the content of this free book. Please see Chapter 1 of the Turbo Pascal Reference for important information about your right to distribute and use this material freely. If you find this material of use, I would appreciate your purchase of one my books, such as the Borland Pascal Developer's Guide or Secrets of the Borland C++ Masters, Sams Books, 1992. Thank you. Functions Syntax: function : Examples: function Minimum ( A, B : Integer ) : Integer; { Returns the value of the smaller number A, or B } begin if A < B then Minimum := A else { if B < A or B = A then } Minimum := B; end; { Example of a function returning a String value } function LowerCase (S : String ) : String; { Convert string S to lower case, returning the result } var I : Integer; begin for I := 1 to length(S) do if ((S[I]>='A') and (S[I]<='Z')) then S[I] := Chr( Ord( S[I] ) + 32 ); LowerCase := S; end; { LowerCase } Description: A function is similar to a procedure, except that a function is called from within an expression and it returns a value that is then used in evaluating the overall expression. Functions can have both value and variable parameters, and may be declared as near, far, forward, and external, or may be implemented entirely in assembly language. Functions may not, however, be declared as interrupt functions. Calling a function The function is called by appearing within an expression, such as, I := Min ( X1, X2 ) + 1; Assigning a value to a function identifier The function's block definition must include a statement that assigns a value to the function's identifier. This is how a function gets a value that it can return to the caller. If you omit this assignment statement, or the assignment statement does not get executed as a result of conditional statements in the function's code, then the function returns an undefined or potentially random value. If during the course of debugging a program you find your functions returning erratic values, be certain that the function identifier is correctly assigned a value. Examples of assigning values to function identifiers appear above in the Min and LowerCase functions. Acceptable Function Return Values The data type that a function returns can be any of the following: Any ordinal value, including Boolean, Byte, Char, Smallint, Integer, Word, Longint, and enumarated data types and user defined sub range types. Real, Single, Double, Extended and Comp data types, Pointer values Strings. Functions may not return records or sets, although they may return pointers to records or sets. Recursive functions Functions may call themselves. Such a function is called a recursive function. A popular and simple example of a recursive function is a function that computes the factorial of a number. The factorial of a number n, is n * (n-1) * (n-2) ... until n reaches 1. For example, the factorial of 5, is 5 * 4 * 3 * 2 * 1, which equals 120. An illustration of how this might be solved recursively is shown in listing 3.10. (Actually, its not necessary to use a recursive function to compute a factorial; this method is used here for illustration only.) Listing 3.10. An example of a recursive function. 1 program DemoRecursion; {DEMORECU.PAS} 2 3 function Factorial ( n : real ) : real; 4 begin 5 if n = 1 then 6 Factorial := 1 7 else 8 Factorial := N * Factorial ( N - 1.0 ); 9 end; 10 11 var 12 X : Real; 13 14 begin 15 Write('Enter a number: '); 16 Readln( X ); 17 Writeln; 18 Writeln('Factorial of ',X,' = ', Factorial ( X ) ); 19 Writeln; 20 Write('Press Enter to finish.'); 21 Readln; 22 end. 23 Important note: The effect of short-circuit evaluation on functions By default, Turbo Pascal generates short-circuit evaluation code, so it is possible that a function may not be called within a particular expression. For example, consider a function defined as: function ValueInRange ( X1 : Integer ) : Boolean; begin ... if X1 > 0 then ValueInRange := True else ValueInRange := False; if X1 < LowestCoordinate then LowestCoordinate := X1; end; In this function, a global variable LowestCoordinate may have its value changed during the course of execution. If this function is called in an expression such as, if (X1<>X2) and ValueInRange(X1) then ... In normal short-circuit evaluation, if X1 is not equal to X2, then the remainder of the expression will not be evaluated. If your code depends upon the value of LowestCoordinate being set as a side effect of the ValueInRange function call, this may result in an error. In general, it is best to avoid side-effects within functions and procedures, but if you must make use of side-effect such as this, you should disable short-circuit evaluation to force the entire expression to be fully evaluated. See the section "Compiler Directives" for more information on using the {$B+} option to enable full expression evaluation. Procedures and Functions as Parameters A procedure or function may itself be passed to another procedure or function as a parameter value. To pass a procedure or function as a parameter requires that a type declaration define a procedure type that matches the appropriate procedure or function header. This type becomes the parameter type used in the procedure parameter list. Listng 3.11 demonstrates the use of a procedure parameter. Note that the type declaration describes the procedure or function that is called, and does not include the actual procedure or function identifier. Listing 3.11. Using a procedure type as a procedure parameter. 1 Program ProcParameter;{PROCPARM.PAS} 2 { Demonstrates using a procedure type as a procedure parameter } 3 4 type 5 FormatProc = procedure ( X : Integer ); 6 7 8 const 9 MaxListSize = 15; 10 Values: array[1..MaxListSize] of Integer = 11 (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15); 12 13 14 15 function Hexadecimal ( X: Word ) : String; 16 var 17 HiByte, LoByte : Word; 18 19 function HexConvert( B : Byte ) : String; 20 const 21 HexTable : Array[0..15] of Char = 22 ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 23 'A', 'B', 'C', 'D', 'E', 'F'); 24 begin 25 HexConvert := HexTable[B div 16] + HexTable[B and 15]; 26 end; 27 28 begin 29 HiByte := X Div 256; 30 LoByte := X and 255; 31 Hexadecimal := HexConvert( HiByte ) + HexConvert(LoByte); 32 end; 33 34 35 36 procedure PrintInteger( X : Integer ); far; 37 begin 38 39 Writeln( X : 5 ); 40 41 end; 42 43 44 45 procedure PrintHex( X : Integer ); far; 46 begin 47 48 Writeln ( Hexadecimal ( X ) ); 49 50 51 end; 52 53 54 procedure PrintPercent( X : Integer ); far; 55 begin 56 57 Writeln( X, '%'); 58 59 end; 60 61 62 procedure Traverse ( Proc : FormatProc ); 63 var 64 I : Integer; 65 begin 66 for I := 1 to MaxListSize do 67 Proc( Values[I] ); 68 end; 69 70 71 begin 72 Traverse ( PrintInteger ); 73 Traverse ( PrintHex ); 74 Traverse ( PrintPercent ); 75 76 readln; 77 78 end. Conditional Compilation While not specifically a part of the Pascal programming language, Turbo Pascal provides special compiler directives to create different executable programs from a single source. These features are called conditional compilation directives. The basic statements are, {$IFxxxx} {$ENDIF} and {$IFxxxx} {$ELSE} {$ENDIF} where the {$IFxxxx} may be one of the following conditional tests, {$IFDEF symbol}: If symbol is defined, then compile the statements between the {$IFDEF} and the {$ENDIF} or {$ELSE}. {$IFNDEF symbol}: If symbol is not defined, then compile the statements between the {$IFNDEF} and the {$ENDIF} or {$ELSE}. {$IFOPT switch}: Tests the state of compiler directive switches (such as $R+ or $I-, for instance). For example, {$IFOPT I+} compiles the following Pascal statements if the $I options is currently set, while {$IFOPT I-} compiles the following statements if the $I options is not set. These statements may be embedded into Pascal source code like any other program comment. Symbols used in conditional compilation statements are created with the {$DEFINE symbol} directive, and eliminated or undefined with the {$UNDEF symbol} directive. A frequent use of conditional compilation is to embed debugging code into a program during development. By changing a single option, all of the debug code can be eliminated from the final compilation, but switched back on if bugs are detected or new program features are added later. Listing 3.12 illustrates the use of the conditional compiler directives in a section of code. Listing 3.12. This program section shows how conditional compilation might be used to add debug statements into a program. 1 {$DEFINE DEBUGGING} {DEBUG.PAS} 2 {$IFDEF DEBUGGING} 3 procedure DebugTrace( S : String; X : Integer ); 4 begin 5 Writeln( DebugFile, S,', ', X ); 6 end; 7 {$ENDIF} 8 9 {---------------------------------------------------------} 10 function UpperCase ( S : String ) : String; 11 var 12 I : Integer; 13 begin 14 {$IFDEF DEBUGGING} 15 DebugTrace('UpperCase', 0 ); 16 {$ENDIF} 17 for I := 1 to Length( S ) do 18 S[I] := UpCase ( S[I] ); 19 UpperCase := S; 20 end; { UpperCase } 21 {------------------------------------------------------------} 22 function HeapFunc ( Size : Word ) : integer; 23 begin 24 {$IFDEF DEBUGGING} 25 DebugTrace('HeapFunc', 0 ); 26 {$ENDIF} 27 28 HeapFunc := 1; 29 end; 30 31 {-----------------------------------------------------------} 32 function LowerCase (S : String ) : String; 33 Var 34 I : Integer; 35 begin 36 {$IFDEF DEBUGGING} 37 DebugTrace('LowerCase', 0 ); 38 {$ENDIF} 39 40 for I := 1 to length(s) do 41 if ((S[I]>='A') and (S[I]<='Z')) then 42 S[I] := Chr( Ord( S[I] ) + 32 ); 43 LowerCase := S; 44 end; 45 46 {------------------------------------------------------------} 47 Function Max ( A, B : Integer ) : Integer; 48 Begin 49 {$IFDEF DEBUGGING} 50 DebugTrace('Max', 0 ); 51 {$ENDIF} 52 53 IF A>B THEN Max := A Else Max := B; 54 End; {Max} 55 Important Note: Conditional Symbols The symbols defined by {$DEFINE} must follow the same rules as those applied to normal Pascal language identifiers. However, symbols defined by {$DEFINE} are completely unrelated to identifiers declared within Pascal programs. For example, it is permissible to have, {$DEFINE Debug} var Debug : Boolean; because these identifiers have no relation to one another. Similarly, if you wrote, {$DEFINE Options} if Options then .... you would receive an error because the symbol Options used in the if-then statement has not been declared (at least as a Pascal identifier). In addition to the {$DEFINE} directive, symbol may also be defined with the /D command line option (See Chapter 10, "Turbo Pascal Standalone Programs" in the Borland Pascal Developer's Guide) or the Conditional Defines option of the IDE (see chapter 2 of the Turbo Pascal Reference). Built-in Conditional Compilation Symbols Turbo Pascal automatically defines the following 4 conditional compilation symbols: VER60: Turbo Pascal automatically defines this symbol, meaning that this is version 6.0 of the Turbo Pascal compiler. VER70: Turbo Pascal defines this symbol when you are compiling under version 7.0 of the Turbo Pascal compiler. MSDOS: Defined for all MS-DOS versions of the Turbo Pascal. CPU86: Defined for all versions of Turbo Pascal running on 80x86 microprocessors. CPU87: If, at compile time, the compiler detects an 80x87 math coprocessor, this symbol is automatically defined. When used in conjunction with {$N}, this symbol can be used to set the floating-point libraries for use with or without the 80x87 coprocessor. Example: {$IFDEF CPU87} {$N+} {$ELSE} {$N-} {$ENDIF} Compiler Directives Compiler directives are special program comments that select compiler options. Two types of compiler directives are provided (in addition to the conditional compiler options just described): Switch directives: These are written in the form {$I+} or {$I-}, where $I specifies a particular option, and a + symbol switches the option on, and a minus - symbol switches the option off. Parameter directives: Parameters specify a value to the option, such as, {$I filename} to include another file during compilation. Compiler options may be set by embedding compiler directives into the source text, or may be set globally using the IDE's Options menu, Compiler selection. (See Chapter 2, in the section titled "Options Menu/Compiler)". Many of the options may also be specified as command line options to the command line version of the compiler (See Chapter 10 in the Borland Pascal Developer's Guide). Table 3.3 describes the embedded compiler directive options. Table 3.3. The $ compiler directives. Word align data: $A, default $A+ Normally, the compiler generates byte aligned data assignments. Setting this option forces the compiler to generate word aligned assignments, so that if one value ends on an odd byte, the compiler will skip a byte to insure that the next assignment is made to an even byte position. The 80x86 CPUs, including the 8086, operate slightly faster when fetching word-aligned data than when fetching byte aligned data. Complete Boolean evaluation: $B, default $B- The Turbo Pascal compiler normally performs short-circuited boolean expression evaluation. This means that as the expression is evaluated at run time, as soon as the result is definitely known, the remainder of the expression will be ignored. This is particularly useful in testing an index value before using it in an array subscript, in for example, if Index < 10 and (X[Index]=9) then... If Index is 10 or greater, the program will not execute the array subscript. However, by enabling this compiler option, $B+, you can force the program to always fully evaluate the expression. This might be used when calling a function within an expression, and your program must use an intended side affect of the function call. Debug information: $D, default $D+ If you are going to use either the built-in or the standalone debuggers, set this option on so that the compiler will generate internal line number tables to assist in matching generated code to program source statements. You can selectively disable debug information on a per unit basis; such units cannot have their procedures or functions traced into. Emulation: $E, default $E+ When this option is set, the compiler will link in routines that automatically detect the prescence or abscence of the floating point processors. If the math processor is installed, then it will be used; if the math processor is not installed, the routines that simulate the processor are called instead. To generate code for all types of machines, regardless of whether or not they have a floating point chip, select both the 8087/80287 and the Emulation options. If you know that the target machine definitely has a coprocessor, then disable this option but enable the 8087/80287 option. See Table 3.1 and A Note on the Use of Floating Point Values earlier in this chapter for additional information concerning Pascal floating point data types and use of the math coprocessor and emulation libraries. Force far calls: $F, default $F- Normally, the compiler generates near calls for all procedure and functions in the current file, and far calls for all procedures or functions appearing in the interface portion of a unit. You can force far call generation by selecting this option. Also see the section @ and Procedures and Functions, for examples using the far option for passing procedures and functions as parameters to procedures. 286 instructions: $G, default $G- When set, the compiler generates code using the additional instructions available in the 80286 instruction set. Programs created with this option will not run on 8088 or 8086 processors. I/O checking: $I, default $I+ The default setting is to have the compiler check for input/output errors on all input/output operations, such as reading or writing to a file. However, usually you will want to have your own checks for such errors so you will want to disable this option. Usethe $I- compiler directive to locally disable I/O checking. You can then check the value of the IOResult function to determine if an error occurred. See Checking for File-related Errors, later in this chapter. Include File parameter directive: $I filename This form of the $I compiler directive causes the specified filename to be incorporated directly into the Pascal source text at the location of the directive. Its operation is as if the included file has been typed at this location in the original source file. The $I is a convenient way of breaking large source files into smaller, more manageable pieces. Included files may themselves include other files, up to a maximum nesting depth of 15 files. The only restriction is that all statements between a begin - end pair must appear in the same source file. For example, Program Demo; var ... {$I IOPROCS.PAS} {$I ERRCODE.PAS} begin { Demo } ... end; { Demo } Local symbols: $L, default $L+ When set, the compiler builds a table of all local variables and constant identifiers for use during debugging session. If {$D-} is in effect, disabling the generation of debug information, then {$L+} is ignored. See also the $D option, above. Link object file parameter directive: $L filename When using external assembly language routines, the $L parameter directive tells the compiler which .OBJ file contains the externally defined routine. Memory allocation parameter directive: $M stacksize, heapmin, heapmax, Default $M 16384, 0, 655360 The $M parameter directives specifies, for a Program module, how much memory to allocate to the program stack, and the minimum and maximum size permitted for the heap memory allocation. The default values specify a stack size of 16k bytes, and a maxium heap allocation up to 640k bytes. During program initialization, the heap memory is allocated from whatever is left over after loading the program code and setting aside space for global variables and the stack. As a result, the actual heap allocation may be less than the maximum value specified in this directive. The stacks size may be set to any value from 1,204 to 65,520 bytes. The heap values may be set anywhere from 0 to 655,360 bytes. 8087/80287: $N, default $N- When set, this results in the compiler generating code for the 8087 or 80x87 math coprocessor chip. This option is often used in conjunction with the Emulation mode option (see above). Also see See Table 3.1 and A Note on Floating Point Values earlier in this chapter for additional information concerning Pascal floating point data types and use of the math coprocessor and emulation libraries. Overlays allowed: $O, default $O- You must set this option for all units that will subsequently be overlaid. Its also okay to set this option for units that are not overlaid as it merely adds some additional run time checks to the generated code. Chapter 3, "Overlays" in the Borland Pascal Developer's Guide describes the creation and use of overlay programs. Overlay unit name parameter directive: $O unitname When a Program module will use overlay units, immediately after the uses statement, the program must specify which units should be placed in a separate .OVR overlay file instead of the main .EXE program executable file. All such overlay units must be compiled with the $O+ option. See Chapter 2, "Units and Dynamic Link Libraires" for information on creating and using program units, and chapter 3, "Overlays" for information on using overlays, both in the Borland Pascal Developer's Guide. Range checking: $R, default $R- Range checking can be performed on all array and string subscript values. If the index is out of range, then a run time error occurs and the program halts. Set this option to enable range checking, and disable this option to turn off range checking. Programs may run noticeably faster with range checking turned off, and will require somewhat less code. Stack checking: $S, default $S+ When this option is set, the compiler generates extra code for every procedure and function call to insure that adequate memory remains in the stack area. If the call would cause the program to run out of memory, the program is halted with a run-time error. Strict var-strings: $V, default $V+ Most Pascal compilers will not let you pass a string variable as a var parameter unless both var parameters are exactly the same type and length. For example, you normally cannot pass a STRING[80] typed variable to a procedure having a var parameter defined (via a type declaration) as STRING[128]. However, by selecting the $V- option, Turbo Pascal allow you to pass mismatched string types as procedure parameters. Extended syntax: $X, default $X- When $X+ is in effect, Turbo Pascal lets you call a function just like it was a procedure. In effect, the result of the function will be ignored and thrown away. This feature does not apply to any System unit functions. Disk File Operations Turbo Pascal provides disk file access through file declarations, standard library procedures, functions and system variables. Turbo Pascal programs can read and write text files (containing text information), as well as reading and writing random access disk files containing complex data records. This section describes basic file operations, and then presents example code for reading and writing text files, reading and writing sequential and random access data files, and an example using the high speed BlockRead and BlockWrite file functions. Defining a File Identifier Text files are accessed through a special data type called Text, such that a file identifier is declared as, var TextFileID : Text; Data files (those containing data other than text files) are accessed via a File identifier declared using the syntax, var FileId : File of ; where is any type identifier, including built in data types, records and user defined types. Some examples: type TPersonInfo = record Name : String[20]; Age : Integer; end; var DataFile1 : File of Real; DataFile2 : File of Integer; DataFile3 : File of TPersonInfo; Opening a File A filename is associated with a file identifier by calling the Assign procedure, like this: Assign ( DataFile1, 'QTR-DATA.DAT' ); which assigns the filename 'QTR-DATA.DAT' to file identifier DataFile1. This, however, does not actually open the file. To open an existing file, use the Reset procedure (or the Append procedure, summarized later): Reset ( DataFile1 ); To create a new file and open it so that data can be written to the file, write: Rewrite ( DataFile1 ); Writing and Reading File Data Data is output to a data file using the standard Write procedures and specifying the file identifier as the first parameter. Example: Write ( DataFile1, 3.14159 ); Each time data is output to the file, an internal file pointer is advanced forward into the file. When a file is first opened, the file pointer is located at the beginning of the file. Once a record is written to the file, the file pointer advances to just beyond that record, so that it points to the next position in the file. If a previously created file was opened using Reset, data may be read by using Read, specifying the file identifier as the first identifier, followed by a data variable to store the results. Example: Read ( DataFile1, X ); Checking for File-related Errors By default, if a file operation error is detected, such as unable to open a file, the program halts with a run-time error. However, your program can disable automatic file error detection and process file errors directly by checking the value returned by the IOResult function. To test for file errors, you must set the {$I-} option to disable automatic I/O (input/output) result checking. Then, perform the desired option. Example: {$I-} Reset( DataFile1 ); {$I+} ErrorCode := IOResult; if ErrorCode <> 0 then begin Writeln('Problem opening the file'); ... end; While you can test the value of IOResult directly, you will probably want to copy its integer result to a variable, as shown in the code fragment above. This is because IOResult returns the result code of the previous I/O operation, and then resets the internal error code to 0. Consequently, if you test IOResult in a conditional statement, and then attempt to access the error code again to display an error message or perform other processing, IOResult will now be zero. Text Files Listing 3.12 demonstrates reading and writing text files. This simple program prompts for a filename to read from, and the name of file to copy to. It opens the input file, InFile using the Reset procedure. The output file, OutFile is opened using Rewrite to create the new file. However, to insure that an existing file is not inadvertently overwritten, the program first attempts to open the file using the Reset procedure. If the IOResult is zero, this means that the file already exists, so the program prompts to see if you really wish to overwrite the existing file. Then the file is opened using the Rewrite procedure. Finally, a while loop reads each line from InFile, and writes it to OutFile, displaying a dot or period '.' on the screen for each line that is processed, providing user feedback that the program is doing something. (Its a good idea to present some type of feedback whenever your program performs time consuming tasks. If nothing happens for too long, the user might suspect the program has crashed, or perhaps is reformatting the hard disk, neither of which makes for user friendly software.) The while loop calls the function Eof, which returns True when the end of the input file has been reached. If the program did not call Eof, it would eventually attempt to read beyond the end of the file, resulting in a run-time error. While not shown in this example, you could disable I/O result checking using {$I-} and check the IOResult function after each Read and Write. You may wish to modify this program to perform various types of data manipulation. For instance, you can easily alter this program to create a program that puts line numbers on Pascal source statements. Add a TotalLines line counter variable, set to zero, and then increment by 1 for each line read from the input file. Add TotalLines to the Writeln output statement, like this: Writeln( OutFile, TotalLines:4, ': ', DataLine ); To send output to the printer, instead of another disk file, specify LPT1: (or other appropriate DOS printer device) for the output file name. Listing 3.12. An example program showing how process text files. 1 program TextFiles; {TEXTFILE.PAS} 2 { Demonstrates copying a text file to another file } 3 4 var 5 DataLine : String; 6 Error : Integer; 7 InFileName : String[80]; 8 InFile : Text; 9 Line : Integer; 10 OutFileName : String[80]; 11 OutFile : Text; 12 TotalLines : Word; 13 Response : String[80]; 14 15 16 begin 17 18 { Get name of input file and open the file } 19 Repeat 20 21 Write('Enter the name of the file to read (CR=done): '); 22 Readln( InFileName ); 23 if Length( InFileName ) > 0 then 24 begin 25 Assign( InFile, InFileName ); 26 {$I-} 27 Reset( InFile ); 28 {$I+} 29 Error := IoResult; 30 if Error <> 0 then 31 writeln('Unable to open ',InfileName,'.'); 32 end; { if begin } 33 34 Until (Error = 0) or (Length( InFileName ) = 0); 35 36 37 { Get name of output file and open the file } 38 If Length( InFileName ) > 0 then 39 begin 40 41 repeat 42 repeat 43 Error := 0; 44 Write('Enter the name of the file to COPY TO (CR=done): '); 45 Readln( OutFileName ); 46 if Length( OutFileName ) > 0 then 47 begin 48 Assign( OutFile, OutFileName ); 49 {$I-} 50 Reset( OutFile ); 51 {$I+} 52 Error := IoResult; 53 if Error = 0 then 54 begin 55 Close ( OutFile ); 56 Write(OutFileName,' already exists. Overwrite (Y/Cr=No)? '); 57 Readln( Response ); 58 if (Response = 'Y') or (Response = 'y') then 59 Error := 1; { Allow exit from the filename query loop } 60 end; { if begin } 61 end; { if begin } 62 until (Error <> 0) or (Length( OutFileName ) = 0); 63 64 if Error <> 0 then 65 begin 66 {$I-} 67 Rewrite( OutFile ); 68 {$I+} 69 70 Error := IoResult; 71 if Error <> 0 then 72 Writeln('Problem creating ',OutFileName,'.'); 73 end; { if begin } 74 75 until (Error = 0) or (Length( OutFileName ) = 0); 76 77 78 { Copy the input file to the output file } 79 if Length( OutFileName ) > 0 then 80 begin 81 82 while not Eof(InFile) do 83 begin 84 Readln( InFile, DataLine ); 85 Writeln( OutFile, DataLine ); 86 87 { Write a dot on the screen for each line copied } 88 Write('.'); 89 end; { while } 90 Writeln; 91 92 Close( InFile ); 93 Close( OutFile ); 94 end; { if begin } 95 96 end; { if begin } 97 98 end. { program } Important Note: Other Text File Features Additional text file procedures and facilities are provided in Turbo Pascal. The Append procedure operates like Reset, to open an existing file, but automatically positions the file pointer to the end of the file so that the next Write operation will be made to the end of the file. Normally, text file I/O is performed through a 128-byte sized internal file buffer maintained by Turbo Pascal. For faster performance, this default buffer may be replaced with a larger buffer using the SetTextBuf procedure. Normally, the buffer is only output to the disk when it becomes full, but programs can force the buffer to be emptied to disk by calling the Flush procedure. These routines are briefly described below in the section Other File Operations, and described in detail in the library reference section. Sequential Access Data Files A frequent use of file operations is reading and writing record-oriented data. In this form, the entire contents of a record structure, including all of its fields, are written to the disk file. Data values within the record, such as integer or real data types, are stored on the disk file in their internal format. Listing 3.13 illustrates by creating a small array of a TDataRecord, containing name, phone number and age values. The contents of each record are written directly to the data file with the statement, for I := 0 to MaxRecords do Write( SeqFile, DataRecords[I] ); After each record is written, the internal file pointer advances to the next position in the file. In this example program, the data file is then closed and reopened for reading, using Reset. Entire records are then read from the file using, Read ( SeqFile, SampleRecord ); Each record is written back, in sequence. Hence, this particular data file is accessed as a sequential data file. Turbo Pascal also provides random access data files, permitting reading and writing of specific records anywhere within the file. Listing 3.13. An example program that writes and then reads data to and from a sequential file. 1 { SEQFILE.PAS } 2 program SeqFiles; 3 { Demonstrates writing data to a sequential record file 4 and reading it back in } 5 6 const 7 MaxRecords = 4; 8 9 type 10 TDataRecord = record 11 Name : String[20]; 12 PhoneNumber : String[14]; 13 Age : Integer; 14 end; 15 16 const 17 DataRecords : Array[0..MaxRecords] of TDataRecord 18 19 = ( (Name : 'George'; PhoneNumber : '262-1234'; Age : 10 ), 20 (Name : 'John' ; PhoneNumber : '262-1235'; Age : 20 ), 21 (Name : 'Lisa' ; PhoneNumber : '262-1236'; Age : 22 ), 22 (Name : 'Marcia'; PhoneNumber : '262-1237'; Age : 30 ), 23 (Name : 'Gwen' ; PhoneNumber : '262-1238'; Age : 4 ) ); 24 25 26 var 27 I : Integer; 28 SeqFile : File of TDataRecord; 29 SampleRecord: TDataRecord; 30 31 begin 32 Assign( SeqFile, 'SEQFILE.DAT'); 33 Rewrite( SeqFile ); 34 35 for I := 0 to MaxRecords do 36 Write( SeqFile, DataRecords[I] ); 37 38 Close ( SeqFile ); 39 40 Reset( SeqFile ); 41 For I := 0 to MaxRecords do 42 begin 43 Read( SeqFile, SampleRecord ); 44 with SampleRecord do 45 Writeln('Name=', Name,', Phone #=', PhoneNumber, ', Age=', Age); 46 end; 47 48 Close ( SeqFile ); 49 50 Write('Press Enter when done.'); 51 Readln; 52 53 54 end. { program } Random Access Data Files Program RandFile, in listing 3.14 demonstrates the use of random access data files to read and write any record within the data file. The process is nearly the same as a sequential data file except that a new procedure, Seek, is used to position the internal file pointer to specific records in the file. Seek has two parameters: the file identifier and the record number (each data record within the file counts as a single record). A statement such as, Seek ( RandomFile, 10 ); positions the internal file pointer to the 10th record in the data file. The next read (or write) will occur at this location in the file. Listing 3.14. How to use random access file operations. 1 program RandFile; {RANDFILE.PAS} 2 { Demonstrates random access to a record file } 3 4 const 5 MaxRecords = 100; 6 7 type 8 TDataRecord = record 9 Name : String[20]; 10 PhoneNumber : String[14]; 11 Age : Integer; 12 Available : Boolean; 13 end; 14 15 var 16 Command : Char; 17 RandomFile : File of TDataRecord; 18 19 20 21 procedure AddRecord; 22 var 23 DataRecord : TDataRecord; 24 NewName : String[20]; 25 NewPhone: String[14]; 26 NewAge : String[20]; 27 RecordNum : Word; 28 29 30 function FindFreeRecord : Integer; 31 { Scans through the file until finding a free or unused record. 32 Returns: The record number of the free record, or if no free 33 records are found, then returns MaxRecords + 1. 34 } 35 var 36 DataRecord : TDataRecord; 37 RecNum : Integer; 38 begin 39 RecNum := 0; 40 repeat 41 Seek( RandomFile, RecNum ); 42 Read( RandomFile, DataRecord ); 43 if not DataRecord.Available then 44 RecNum := RecNum + 1; 45 until (RecNum > MaxRecords) Or (DataRecord.Available); 46 FindFreeRecord := RecNum; 47 end; { FindFreeRecord } 48 49 50 begin { AddRecord } 51 52 RecordNum := FindFreeRecord; 53 if RecordNum > MaxRecords then 54 Writeln('The maximum number of records (',MaxRecords,') are in use.') 55 else 56 begin 57 58 with DataRecord do 59 begin 60 Writeln('Adding record #', RecordNum ); 61 Write('Enter Name ?'); 62 Readln( Name ); 63 Write('Enter # PhoneNumber ?'); 64 Readln( PhoneNumber ); 65 Write('Enter Age ?'); 66 Readln( Age ); 67 Available := False; 68 end; 69 70 Seek( Randomfile, RecordNum ); 71 Write( RandomFile, DataRecord ); 72 end; 73 74 end; { AddRecord } 75 76 77 procedure DisplayRecords; 78 var 79 DataRecord : TDataRecord; 80 I : Integer; 81 begin 82 for I := 0 to MaxRecords do 83 begin 84 Seek( Randomfile, I ); 85 Read( RandomFile, DataRecord ); 86 if not DataRecord.Available then 87 { Found a record that is in use } 88 begin 89 Writeln('Record #', I); 90 with DataRecord do 91 Writeln('Name=', Name,', Phone #=', PhoneNumber,', Age=', Age:3); 92 Writeln; 93 end; { if begin } 94 end; { for begin } 95 end; { DisplayRecords } 96 97 98 procedure EditRecord; 99 var 100 DataRecord : TDataRecord; 101 Error : Integer; 102 NewName : String[20]; 103 NewPhone: String[14]; 104 NewAge : String[20]; 105 RecordNum : Word; 106 begin 107 108 Repeat 109 110 Write('Enter number of record to edit: '); 111 Readln( RecordNum ); 112 113 Seek( RandomFile, RecordNum ); 114 Read( RandomFile, DataRecord); 115 if DataRecord.Available then 116 writeln('Record #', RecordNum, ' does not contain any data.'); 117 118 Until not DataRecord.Available; 119 120 121 with DataRecord do 122 begin 123 Write('Name =', Name,'(CR=no change) ? '); 124 Readln( NewName ); 125 if NewName <> '' then 126 Name := NewName; 127 Write('PhoneNumber =', PhoneNumber, '(CR=no change) ? '); 128 Readln( NewPhone ); 129 if NewPhone <> '' then 130 PhoneNumber := NewPhone; 131 Write('Age =', Age, '(CR=no change) ? '); 132 Readln( NewAge ); 133 if NewAge <> '' then 134 Val( NewAge, Age, Error); 135 end; 136 Seek( RandomFile, RecordNum ); 137 Write( RandomFile, DataRecord ); 138 end; { EditRecord } 139 140 141 procedure OpenFile; 142 var 143 SampleRecord : TDataRecord; 144 Error : Integer; 145 I : Integer; 146 begin 147 { Open existing file or create a new random access data file } 148 Assign( RandomFile, 'RANDFILE.DAT'); 149 {$I-} 150 Reset( RandomFile ); 151 {$I+} 152 Error := IoResult; 153 if Error <> 0 then 154 begin 155 { Create and initialize a new file. Set all records to available } 156 Rewrite( RandomFile ); 157 SampleRecord.Available := True; 158 for I := 0 to MaxRecords do 159 write( RandomFile, SampleRecord ); 160 end; { if begin } 161 end; { OpenFile } 162 163 164 begin { RandFile } 165 166 OpenFile; 167 168 repeat 169 Write('A)dd record, D)isplay records, E)dit record, Q)uit? '); 170 Readln( Command ); 171 Command := Upcase( Command ); 172 case Command of 173 'A' : AddRecord; 174 'D' : DisplayRecords; 175 'E' : EditRecord; 176 end; { case } 177 until Command = 'Q'; 178 179 Close( RandomFile ); 180 181 182 Write('Press Enter when done.'); 183 Readln; 184 185 186 end. { program } The actual data record is defined in TDataRecord, containing 4 fields: Name, PhoneNumber, Age, and a Boolean flag Available. The Available flag is used to keep track of which records are in use and which have yet to be used. Procedure OpenFile first tries to open an existing 'RANDFILE.DAT', since if the file already exists, it will then be used as-is. However, if no such file exists, a new one is created by calling Rewrite. For each of the records in file (the total number of records is specified in the MaxRecords constant), the Available flag is set to True, using this code: SampleRecord.Available := True; for I := 0 to MaxRecords do write( RandomFile, SampleRecord ); Note that we don't care about the contents of the other fields. As long as the Available flag is set to True, we know that the record is available for writing new data to, and we will ignore the contents of the other fields. In the main body of the program, a repeat-until loop is used to prompt for a command, A)dd, D)isplay, E)dit or Q)uit the program (this notation is used to suggest that you should only enter the first letter of each command). After converting to upper case, the program uses a case statement to determine which procedure should execute. AddRecord calls its local function FindFreeRecord to find the next free record (one with Available = True). FindFreeRecord merely reads through the file, one record at a time, until either finding a free record, or reaching the end of the data file. Back inside AddRecord, the user is prompted for a new name, phone number and age values; the Available flag is set to False and the record is written out to the file. Note the use of Seek to position to the free record in the file, and then a Write procedure to output the record data to the file. The EditRecord procedure takes a record number as input, seeks to the given record, reads the data from the file, and gives you a chance to make changes. The altered data is then output to the file. DisplayRecords scans through the file, displaying the record number, name, phone number and age values for each record that is in use. For practice, you may wish to modify this program to include a Delete function. All that's needed is to output a new record with the Available flag set to True, for each record that is deleted. Important note: Writing Pointer values to disk files When writing a record structure to a disk file, any pointer type values contained in the record are also written to the file. However, when these values are read back into memory from a disk file, their value is meaningless. Pointers to internal memory addresses are only valid when used within the original execution of the program. Instead of storing pointers in the disk file, you must be able to recreate the internal data structure without referencing the pointer values. A simple approach is to output the data records in the exact order that they will be read back. Upon reading each record from the file, you must recreate the internal data strcuture, via calls to New to allocate new data records from the heap, and then explicitly reassign any pointer values to the new allocations. More complicated structures may require that you create a file record number field, and instead of using pointer values in the data file, store actual file record numbers. Upon reading the file, you would then use New to create a new internal record, and copy the values read from disk into the record, setting pointers as appropriate, to point to other records in the internal memory structure. BlockRead and BlockWrite: The Use of Untyped Files In addition to reading and writing specific data types, Turbo Pascal provides for untyped data files, using just the File keyword, and two procedures BlockRead and BlockWrite, for reading and writing data to such a file. By default, each untyped files is treated as having 128 bytes per block (but can be set to other values). This means that data is read or written in 128-byte sized chuncks called blocks. BlockRead and BlockWrite are especially useful for performing high speed file copy operations because of their ability to read and write very large blocks of data in a single statement. Block I/O is also used by programs that perform their own internal data formatting. For example, a word processor may store its internal representation of a document directly to disk using BlockWrite to output the document's internal memory structure. Data base and spreadsheet applications may also build buffers of data for faster reading and writing of data base and spreadsheet files. An untyped file is declared using the File keyword by itself, as in this declaration of InFile and OutFile: var InFile, OutFile : File; BlockRead has 3 or 4 parameters, depending on its mode of operation. The same is true of BlockWrite. At a minimum, each procedure has 3 parameters: BlockRead( FileId, Buffer, NumBlocks ); BlockWrite( FileId, Buffer, NumBlocks ); where, FileId is a file identifier specifying the file where the operation will be performed, Buffer, is typically an array of char or an array of byte large enough to hold all the data, NumBlocks is the number of blocks to read into the buffer, where each block is either the default of 128 bytes, or is a different block size as specified with the Reset or Rewrite procedures (see below). An optional 4th parameter returns the number of blocks actually read, which, in the event that something prevented the program from reading the number of blocks that were requested, may be different than the number of blocks requested. For example, a program might request 4 blocks but only read 3 blocks before reaching the end of the file. In this case, the 4th parameter returns a value of 3. An example, given the definition of Buffer as: var Buffer : Array [0..2047] of Char; then, the statement, BlockRead ( FileId, Buffer, 16 ); reads sixteen 128 byte-sized blocks from the file and places them in Buffer. Specifying Different Block Sizes The Reset and Rewrite procedures have an optional 2nd parameter to specify a different block size. For example, Reset ( FileId, 1024 ); specifies that each block of data read from the untyped FileId will contain 1024 bytes of data. In this instance, you may write, BlockRead ( FileId, Buffer, 2 ); and read two 1,204 byte blocks from the disk file. A problem with BlockRead is that it cannot read data that is less than the block size. For instance, what happens if you attempt to read a 1,204 byte sized block from a file having only 300 bytes? If you use BlockRead with the optional 4th parameter, as for instance, BlockRead ( FileId, Buffer, 1, Result ); then Result will be 0, indicating that the amount of data you requested could not be read. The partial record is stored in Buffer, but you have no way of knowing how many bytes were actually read. The solution to this problem is to open the file with a block size of 1 byte, and set the NumBlocks parameter to the Buffer size. For example, Reset ( FileId, 1 ); BlockRead ( FileId, Buffer, SizeOf(Buffer), Result ); Now, if the amount of data to read is less than 2048 bytes, the actual number read is returned in Result. Listing 3.15 is a procedure CopyFile that copies one file to another, using a blocksize of 1, and buffer size of 1,204 bytes (see the declaration of TFileBuffer). For faster performance you can change the declaration of TFileBuffer to a much larger size, such as 32,768 bytes. As with other file operations, use the {$I-} compiler option to disable automatic I/O result checking. Write your own error checking routines to check the contents of IOResult to determine success or failure of block operations. Listing 3.15. Demonstration of the BlockRead and BlockWrite functions for fast file copying. 1 function CopyAFile ( Source, Dest : String ) : Integer; 2 {COPYFILE.PAS} 3 { Purpose: 4 Copies the filenamed in 'Source' to the file named in 'Dest'. 5 6 Returns: 7 0 = copy was okay 8 1 = not enough RAM memory to copy the file 9 2 = error occurred when writing to the 'Dest' file 10 3 = Could not open the source file 11 4 = Could not open the destination file 12 } 13 label 14 ExitProc; 15 16 type 17 TFileBuffer = Array [0..1023] of Byte; 18 { Copies the file in 1k chunks. For a larger buffer, 19 increase the size of the byte array. } 20 var 21 BytesIn : Integer; { Number of bytes read in } 22 BytesOut : Integer; { Number of bytes written out } 23 F1 : File; { F1=file to read, F2=file to write } 24 F2 : File; 25 FileBuffer : ^TFileBuffer; 26 Dialog : PDialog; 27 Bounds : TRect; 28 29 begin { CopyFile } 30 31 New( FileBuffer ); 32 if FileBuffer = NIL then 33 CopyAFile := 1 {Not enough RAM memory to do a copy} 34 else 35 begin 36 37 { Open the source file } 38 Assign ( F1, Source ); 39 {$I-} 40 Reset ( F1, 1 ); 41 if IoResult <> 0 then 42 begin 43 CopyAFile := 3; {Error on opening source file} 44 goto ExitProc; 45 end; 46 47 { Open the destination file } 48 Assign ( F2, Dest ); 49 Rewrite ( F2, 1 ); 50 if IoResult <> 0 then 51 begin 52 CopyAFile := 4; {Error on opening the destination file} 53 goto ExitProc; 54 end; { if } 55 56 repeat 57 BlockRead ( F1, FileBuffer^, SizeOf(FileBuffer^), BytesIn ); 58 if BytesIn > 0 then 59 begin 60 { Since we read something, go ahead and write it to the destination } 61 BlockWrite ( F2, FileBuffer^, BytesIn, BytesOut ); 62 if BytesIn <> BytesOut then 63 begin 64 CopyAFile := 2; {Error occurred while writing the output} 65 {$I-} 66 Close ( F2 ); 67 Erase ( F2 ); 68 Close ( F1 ); 69 {$I+} 70 goto ExitProc; 71 end; { begin } 72 end; { if } 73 until BytesIn = 0; 74 75 CopyAFile := 0; 76 Close ( F1 ); 77 Close ( F2 ); 78 79 end; { begin } 80 81 ExitProc: 82 83 if FileBuffer <> NIL then 84 Dispose (FileBuffer); 85 86 end; { CopyAFile } 87 Important Note: Maximum buffer size and maximum data to read or write The maximum amount of data that can be read with BlockRead or written with BlockWrite is limited in the following ways: Individual data structures in Turbo Pascal can be no larger than 65,521 bytes. The file record size times the count of bytes to read or write, must be 65,535 (or less) since its stored in a Word data type. In other words, SizeOf(Buffer) * NumBlocks must be less than or equal to 65,535. Other File Operations In addition to the basic file operation facilities covered in the previous sections, Turbo Pascal provides a variety of standard procedures for creating directories, switching to other directories, erasing and renaming files and so on. Other functions and variables provide additional features. The following is a summary of the available functions; they are described in detail in the library reference section. Procedures ChDir ( S: String ): Changes the current directory to the path specified by S. Erase ( var F): Erases or deletes the file associated with the closed file identifier F (with the name previously given to it by Assign), GetDir (D: Byte; var S: String): Where D=1 for drive A, 2 for drive B, etc, or 0 for the current drive, GetDir returns the current directory name in variable S. MkDir( S: String ): Used to create new subdirectories, where S is the path, including the subdirectory name, to create. Rename( var F; Newname : String ): Where F is a closed file identifier having a name set with the Assign procedure, Rename changes the name of that file to the name specified by Newname. RmDir( S : String ): Deletes an empty subdirectory specified by S. Truncate( var F ): The current file position in open file F becomes the new end of file, effectively disgarding any records that may follow the current record. Functions FilePos (var F): Longint: Returns the current position in the non-text file as the record number of the file pointer. FileSize (var F): Longint : Where F is an open non-text file, this returns the size of the file, in number of components. If F is a file of Char, then FileSize(F) returns the size in bytes. If F is a file of Integer, then FileSize(F) returns the number of integer records in the file. Text file procedures Append (var F: Text): Like Reset, for opening an existing text file, but automatically positions the current file position to the end of the file so that the next output will be written at the end of the current file. Flush( var F: Text ): Empties the internal buffer associated with text file F. to the disk file. SetTextBuf(var F: Text; var Buf [; Size : Word ] ): By default, all text files use an internal 128 byte-sized buffer. For faster text file performance, programs can establish their own text file buffer by calling SetTextBuf and passing to it their own data block to be used for the buffer. SizeOf(Buf) becomes the new buffer size, or a different size may be explicitly provided with the optional Size parameter. Calls to SetTextBuf should occur just prior to Append, Reset or Rewrite. Eoln ( var F : Text ): Returns True if the next character is the text file is an end of line character (e.g. carriage return). SeekEof( var F: Text ): For text file use only, SeekEof is equivalent to the Eof function except it skips over any trailing blanks, tabs or blank lines. SeekEoln( var F: Text ): For text file use only, SeekEoln is equivalent to Eoln, except that is skips over trailing blanks and tabs at the end of a line. Untyped Files FileMode: By assigning a value to FileMode prior to opening a file with Reset, you can specify an access mode for the file. A value of 0 specifies Read only, 1 specifies Write only, and 2, the default value, specifies Read/write access to the file. The value of FileMode remains in effect for all subsequent Reset operations, until set to some other value. Turbo Pascal Memory Limitations Maximum statement width: 126 characters Maximum size of generated code per code unit: 64k bytes The program module and each Unit may contain up to 64k bytes of code. Ifthe program or unit exceeds this value, then you must split it up into smaller sections. Maximum size of global and typed constants: 64k bytes If your program must have several large values, such as arrays, as global values, and their combined size will exceed this value, then you must recode your program to reduce the memory usage. A simple approach is to dynamically allocate the arrays and define only a pointer to the array as a global value. Maximum program stack size (used for procedure and function call return address and all local variable storage): 64k bytes The default stack size is 16k bytes but may be adjusted up or down using the $M compiler directive or the IDE's Options/Memory Sizes menu selection. Maximum heap size: 0 to 640k bytes The heap's default memory allocation is all the memory left over after the code is loaded and the stack and data segments are allocated. The heap will take all the available memory up to its maximum declared size, which is 640k by default. You will want to set the default value to a lower limit especially if your program will be memory resident with other software, or if your program launches other applications using the Exec procedure. Maximum data allocation size: 65,521 bytes You cannot allocate a single data structure (such as an array) that is larger than this size.