Home of the original IBM PC emulator for browsers.
The following document is from the Microsoft Programmer’s Library 1.3 CD-ROM.
Microsoft QuickC Programming
════════════════════════════════════════════════════════════════════════════
Microsoft(R) QuickC(TM) Programming
The Microsoft(R) Guide to Using the QuickC Compiler
By The Waite Group
Mitchell Waite, Stephen Prata, Bryan Costales, and Harry Henderson
════════════════════════════════════════════════════════════════════════════
PUBLISHED BY
Microsoft Press
A Division of Microsoft Corporation
16011 NE 36th Way, Box 97017, Redmond, Washington 98073-9717
Copyright (c) 1988 by The Waite Group, Inc.
All rights reserved. No part of the contents of this book may be reproduced
or transmitted in any form or by any means without the written permission of
the publisher.
Library of Congress Cataloging in Publication Data
Microsoft QuickC programming.
Includes index.
1. C (Computer program language) 2. Microsoft QuickC (Computer program)
I. Waite, Mitchell. II. Title: Microsoft Quick C programming.
QA76.73.C15M53 1988 005.13'3 88-5203
ISBN 1-55615-048-2
Printed and bound in the United States of America.
1 2 3 4 5 6 7 8 9 MLML 3 2 1 0 9 8
Distributed to the book trade in the United States by Harper & Row.
Distributed to the book trade in Canada by General Publishing Company, Ltd.
Distributed to the book trade outside the United States and Canada by
Penguin Books Ltd.
Penguin Books Ltd., Harmondsworth, Middlesex, England
Penguin Books Australia Ltd., Ringwood, Victoria, Australia
Penguin Books N.Z. Ltd., 182-190 Wairau Road, Auckland 10, New Zealand
British Cataloging in Publication Data available
IBM(R) is a registered trademark, and PC/AT(TM) and PC/XT(TM) are trademarks
of International Business Machines Corporation.
Microsoft(R) and MS-DOS(R) are registered trademarks, and QuickC(TM) is a
trademark of Microsoft Corporation.
────────────────────────────────────────────────────────────────────────────
Acquisitions editor: Claudette Moore
Project editor: Eric Stroo
Copy editor: Gary Masters
Technical reviewer: Doug Henderson
────────────────────────────────────────────────────────────────────────────
────────────────────────────────────────────────────────────────────────────
Contents
Preface
Acknowledgments
PART 1 INTRODUCTION TO C
Chapter 1 Introduction
Why Learn C?
Why QuickC?
Hardware Requirements
Knowledge Requirements
Conventions and Style
Chapter 2 Starting with QuickC
Our Book and Their Book
Directories and Files Used by QuickC
Running the QuickC SETUP Program
Setting Up QuickC
Starting QuickC
Getting Help
Fixing Errors
Preparing for the Next Chapter
PART 2 CORE OF C
Chapter 3 C Fundamentals
Basic Elements of C Programs
Punctuation and Spacing in C Programs
Using Comments in C
Data Types and Declarations of Variables
The Power of printf()
Arithmetic Operators
Getting Input with scanf()
Shortcut Assignments, Increments, and Decrements
Relational Operators
Logical Operators
Chapter 4 Repetition and Looping
The for Loop
The while Loop
The do Loop
Debugging and Loops
Chapter 5 Decisions and Branching
The if Statement
The Conditional Assignment Statement ?
Multipath Branching
The switch Statement
The break Statement
The continue Statement
The goto Statement
More Complex Conditions for Branching
Chapter 6 Functions and Function Calls
Functions and Program Design
Declaring and Defining a Function
Local and Automatic Variables
Static Variables
External Variables
Register Variables
Passing Information to a Function
Functions with Many Parameters
Functions That Return Information
Recursion
Noninteger Functions
Function Prototypes
Putting It All Together: A Larger Program
PART 3 ADVANCED C TOPICS
Chapter 7 Arrays
How Arrays Are Stored in Memory
How to Declare Arrays
Referencing and Using Array Items
Bounds Checking Arrays in Your Code
How to Initialize Arrays
Arrays and Functions
How Array Offsets Advance
Multidimensional Arrays
Advanced Topics and Tricks
The Bitwise Operators, Tiny Arrays
Chapter 8 Addresses and Pointers
Addresses Reviewed
What Is a Pointer?
Accessing Variables with Pointers
Passing Pointers to Functions
Pointers and Arrays
Pointer Arithmetic
The Interchangeability of *amts and amts[]
lvalue vs rvalue
Type Casting Pointers and Addresses
far Pointers
Functions That Return Addresses
Dynamic Arrays
Advanced Pointer Techniques
Chapter 9 Strings
Declaring and Initializing Strings
The String Pool and String Addresses
Pointers and Initialized Strings
Formatting Strings with printf()
String Input and Output
String Manipulation Routines
Arrays and Strings
The Arguments to main()──argv and argc
Character Classification and Transformation
Chapter 10 Managing Files
Top-level I/O
Mid-Level (Unbuffered) File I/O
The File System
Advanced Error Handling
Chapter 11 Advanced Data Types
Structure──An Array of Different Types
Union──Multiple Types in the Same Space
Enumerated Data with enum
Bit Fields
Advanced typedef
Chapter 12 Large Projects
Advanced C Preprocessor
Using QuickC for Large Projects
PART 4 C AND THE HARDWARE
Chapter 13 Keyboard and Cursor Control
Keyboard Input Functions
Reading Non-ASCII Keys
Console I/O Functions
Keyboard Control with ANSI.SYS
Using QuickC to Access the BIOS
Cursor and Screen Control with BIOS Calls
Chapter 14 Monitors and Text Modes
Monitors and Controllers
Text Modes and Portability
Device-independent Programming
Direct Memory Access
Paging
Ports
The EGA and VGA
Chapter 15 Graphics and QuickC
The Graphics Modes
CGA Graphics
EGA Graphics
VGA Graphics
Chapter 16 Debugging
Keyboard-Entry Errors
Syntax Errors
Run-Time Errors
Common Run-Time Errors
Design Errors
Appendix A Some Resources for C Programmers
Appendix B Built-in QuickC Functions
Index
────────────────────────────────────────────────────────────────────────────
Preface
The Waite Group has written books on all aspects of the C language, from
primers to advanced texts that mix C with assembly language☼ But when
Microsoft Press suggested we write a book on Microsoft's then unreleased
QuickC compiler, we were skeptical. Having spent years trying to teach C
both in the classroom and through our books, we were painfully aware of
how difficult a language it is to learn. Despite its power, the Microsoft
C Compiler has confounded many an eager student. Daunted by the cryptic
syntax of its command-line interface, they slipped from C's learning curve
and disappeared beneath its power curve.
Our first look at the QuickC beta version sparked our attention──this was
no standard C compiler. QuickC's interface was profoundly different: The
editor completely integrated into the compiler, optional mouse
compatibility, drop-down menus, full color display, the list went on and
on. Finally we had point-and-click compiling. To us, all this meant a
radical shift in the way we could teach C──we could take a friendly
approach that would make C accessible to a much larger group of people.
But was QuickC really a performance program? Were we talking fast object
code or code more suited to timing traffic signals at snail races? Well,
after creating hundreds of examples for this book, we can report that
QuickC lives up to its speedy expectations. A product that can compile
more than 10,000 lines per minute should satisfy all but the most jaded of
hackers.
There was more. QuickC was fully compatible with the libraries of version
5.0 of the Microsoft C Optimizing Compiler. We could develop our C code in
QuickC's fast and friendly interface, get the errors out quickly with its
integrated debugger and tracer, and then optimize our program for
execution time by compiling it under the standard optimizing compiler.
With the introduction of QuickC, we felt that C had finally reached the
point of rivaling BASIC in ease of use. We saw in QuickC a product that
could vitalize the learning of C and reinforce its dominance in
professional program development.
As educators, we saw that QuickC was an opportunity to build a complete
course in the C language, one that could be pursued without an instructor
and that would be attractive to beginners and professionals alike. The
seductive interface made QuickC a breeze to run──we could focus on the
syntax rather than on complicated command-line options and batch files. On
top of this, QuickC's large and comprehensive Graphics Library meant that
we could make our examples visually appealing and challenging. By assuming
an IBM personal computer running MS-DOS or PC-DOS, we could tackle the
sensitive issues of the interface between MS-DOS and C while at the same
time flexing the keyboard, text, and bitmapped displays.
After all these grandiose thoughts, the next thing we did was very
natural──we gulped loudly. In the time available, no single author could
possibly master a product as rich as QuickC with the aim of writing a book
that examines thoroughly its vast repertoire. The solution was to pool the
energies of four experienced C authors with over 25 years of combined
programming and teaching experience, each writer focusing on a different
section of the book. We think you will find that this book goes beyond
most C books on the market (including our own). If you have any questions
or comments, please address them to: The Waite Group, 3220 Sacramento
Street, San Francisco, California 94115.
Mitchell Waite
Stephen Prata
Bryan Costales
Harry Henderson
────────────────────────────────────────────────────────────────────────────
Acknowledgments
The authors would like to take this opportunity to thank the people at
Microsoft Press for helping to make this book a success: Claudette Moore,
for acquiring this title and putting up with the authors' numerous
requests throughout the project; Gary Masters, for editing and blending
the different styles into one clear message; Doug Henderson, for his
technical review; Eric Stroo, for coordinating the progress of the final
manuscript through the production process and for being so diligent about
the final look of the book; Alison Conn, for her review of the book's
technical coverage; and Greg Lobdell, for providing a continual flow of
alpha and beta copies of the QuickC compiler. Also, thanks to Reed Koch
for his many hours of explaining QuickC's internals to the authors.
Dedication
To Bobbie Lee,
who touched me in a way no one ever has
Mitchell
────────────────────────────────────────────────────────────────────────────
PART 1 INTRODUCTION TO C
────────────────────────────────────────────────────────────────────────────
────────────────────────────────────────────────────────────────────────────
Chapter 1 Introduction
Why Learn C?
If you have experience with C, you are probably familiar with its
advantages over alternatives such as BASIC or Pascal, and you may want to
skip to the next section, which discusses the specific advantages of
QuickC for C programmers. Here we compare C with two other popular
languages, BASIC and Pascal.
Although Pascal has its enthusiasts, and our old friend BASIC certainly
has been improved in many ways (Microsoft's QuickBASIC for example), C has
quickly become the premier language for professional programming both on
micros, such as the IBM PC family, and on larger machines, such as those
running the UNIX/XENIX operating system. Why is C so popular?
Portability and Standards
One reason is portability. The core of standard C is so designed that the
same program runs on an IBM PC, a VAX mini, and an IBM mainframe.
Portability comes about from adhering to standards that guarantee common
features and functions regardless of the vendor, implementation, or
hardware environment. The first, informal C standard was proclaimed by the
famous "white book," Brian W. Kernighan and Dennis M. Ritchie's The C
Programming Language (New Jersey: Prentice-Hall, 1978). The specifications
in this book have been widely adopted in the design of C compilers, but
the definitions are not comprehensive and specific enough to provide a
true standard. Therefore, the American National Standards Institute (ANSI)
has proposed a draft standard for the C language. (At the time of this
writing, the standard has not been officially adopted, but most of its
features seem stable.) Most current and future C compilers will be written
to conform with the ANSI standard. QuickC is compatible with the ANSI
standard. It also permits you to verify that your code uses only
ANSI-compatible functions and definitions or to identify nonstandard
features, such as those needed to support functions specific to MS-DOS and
to IBM hardware.
Another reason for the popularity of C is its close ties to the UNIX
operating system. UNIX was written in C, and a variety of standards
support the use of C in the UNIX environment. QuickC is functionally
compatible with the UNIX System V standard library specifications.
But what does all of this mean to you, the QuickC programmer?
A C program written under QuickC on an IBM PC can, if it uses only
ANSI-standard features, be moved to an Apple Macintosh, and you can
compile it with an ANSI-standard Macintosh C compiler and run it in the
new environment.
This level of standardization is not common in programming languages.
Pascal is only partially standardized: A Turbo Pascal program for the IBM
PC, for example, cannot run under standard IBM Pascal without
modification. In the IBM PC world, the ubiquitous BASICA program has
offered a kind of standard, but other models of computers are provided
with quite different dialects of BASIC, and you must do an extensive
conversion to get a BASIC program written on one machine to run on another
manufacturer's hardware.
Notice that this discussion applies specifically to the "core" of C: the
control structures, data structures, and basic input/output functions.
Outside of this standard core, however, a number of areas of a C
implementation are machine-dependent, such as the size of various kinds of
numbers, keyboard codes, the video screen, graphics, and features of the
operating system that handle files. To be worth its salt, a C compiler
that runs on the IBM PC must include functions that give programs access
to MS-DOS features, the underlying BIOS, and the hardware. Similarly, a C
compiler for the Macintosh must include functions that give a program
access to such elements as the machine's system toolbox. These functions
are hardware-dependent and implementation-specific──by definition, they
are not portable, but they are essential to getting the most out of your
machine. C, as you will discover, provides a way to gather the
machine-dependent parts in an organized manner, something other languages
can't do.
BASIC and, to a lesser extent, Pascal approach hardware dependence by
customizing the language itself to include commands or functions that take
care of the machine-dependent features. For example, a BASIC statement to
control the speaker might be called PLAY. Another version of BASIC might
call it MUSIC. The problem with this approach is apparent when you try to
convert a program to run on a different machine; you cannot easily find
the parts of the program that you must change to manipulate proprietary
features. Also, such hardware-dependent statements may work differently on
computers with different hardware configurations.
A Modular Approach
The programmer's task is more manageable with C. Each C compiler includes
files of definitions, called include files, and collections of precompiled
functions, called function libraries, which you can use to supplement the
core of C to take full advantage of the features of a given machine. Your
QuickC function library includes a rich collection of definitions and
functions for MDA, CGA, EGA, MCGA, and VGA graphics (as well as Hercules
graphics, starting with version 1.01); the whole set of MS-DOS function
calls; and much more.
The result is that a C programmer has several choices. If you don't need
graphics or machine-specific features, you can write an ANSI-standard
text-only C program and easily move it to other machines and operating
systems. If you do need machine-dependent features in your program, you
can use the "no-frills" version of the program and then add graphics and
other hardware-dependent features in easily identified include files and
libraries. For a particular hardware environment, you can then merge the
appropriate include files and libraries into your program. Figure 1-1 on
the following page illustrates the concept of portability.
Portability requires many trade-offs. In general, the less portable (in
other words, the more hardware-dependent) a program is, the faster it
runs, and the more it takes advantage of graphics and other special
hardware features. On the other hand, the more portable a program is, the
easier it is to maintain, modify, or convert it to work with new hardware.
Throughout this book, we point out portability issues and suggest ways to
deal with them. For example, we note those features of QuickC that are
compatible with ANSI and UNIX System V. We also look at portability versus
performance in the MS-DOS world. For example, we discuss alternative ways
for dealing with devices such as the keyboard and video display on MS-DOS
machines (standard I/O, console I/O, and BIOS) and point out the
portability trade-offs involved with each.
┌────────────────────────────┐
│ │ Not
│ Customized statements │ portable
│ │
│┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ │
├┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┤
│ │
│ MS-DOS and BIOS │
│ │
├────────────────────────────┤
│ Hardware │
└────────────────────────────┘
(A) BASIC--A SNUG FIT BUT NOT PORTABLE
ANSI/UNIX
┌────────────────────────────┐ Can run on
│ Standard functions │ IBM PC, VAX,
└─────────┐ ┌─────────┘ Macintosh,
│ │ and others.
├────────┤
└────────┘
│
┌─────────┐ ▼ ┌─────────┐ Implementation
│ │ │ │ of C, such as
│ │ │ │ Microsoft C or
│ └────────┘ │ Quick C.
│ Machine-specific libraries │
│┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ │
└┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘
│
▼
┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐
┌┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┐ Specific
│ │ machine-
│ MS-DOS and BIOS │ IBM PC,
│ │ for example.
├────────────────────────────┤
│ Hardware │
└────────────────────────────┘
(B) C--A PORTABLE CORE
Figure 1-1. Portability in C.
C Is Powerful
Portability is desirable, but you also want to write code that takes full
advantage of the hardware. In this age of drop-down menus, windows, mice,
and help screens, users expect a lot more out of software than they did
only a few years ago. As a programmer you are often pushing the limits of
the hardware, whether in processing speed, I/O, or graphics.
When it comes to harnessing the hardware, C really shines. For example,
other languages try to hide the fact that you are manipulating the
contents of memory when you write code; with C pointers, you can easily
manipulate memory directly. With Pascal, you can also directly manipulate
memory with pointers, but the syntax is not as simple or as powerful as
that of C. And in BASIC, you can use a PEEK and a POKE to access memory,
but they lack the flexibility of pointers.
Another important indicator of the power of a language is its ability to
use machine resources efficiently. All high-level compiled languages
translate program statements into machine instructions. With most
languages you have little control over the efficiency of the resulting
machine instructions. You are at the mercy of the assumptions the compiler
or interpreter makes about your program and how it will be used. Suppose,
for example, that your program uses one or two variables frequently in a
loop that will be executed many times. In C, you declare register
variables that are stored, if possible, in internal CPU registers; thus,
delays in loading or retrieving their values in memory are avoided. The
result is faster execution speed.
Another important feature of C is its ability to create a variety of
memory models. A memory model describes the way RAM is used during
compilation and the way program code and data are shared in RAM. With most
older BASICs you can use only 64 KB of memory to hold program code and
data. Today, most MS-DOS machines have at least 256 KB (and often 640 KB
of memory or more). Thus, newer compilers for BASIC, Pascal, and other
languages often allow access to a larger amount of RAM. But C compilers go
a step further: You──the programmer──decide how the computer will allocate
memory. Depending on the needs of your program, you can choose to use most
of the machine's memory for storing compiled instructions, you can use
most of the memory to store data (such as arrays, structures, or lists),
or you can allocate varying numbers of 64 KB memory segments to both.
Figure 1-2 on the following page shows the concepts of register
variables, pointers, and memory models.
Pointers, register variables, and memory models are only some of the
options C gives you for controlling the machine. In addition, most C
compilers let you improve, or "optimize," the machine code generated from
your program. You can optimize for program size (a smaller .EXE file) or
for faster execution or for a combination of these. For example, QuickC
performs some optimization for you and lets you choose other features as
appropriate. In addition, you can use QuickC in combination with Microsoft
C (the professional, industrial-strength C compiler) to provide
optimization that is truly state of the art.
Pointers ┌──────────────────┐ ┌────────────────┐
for direct │ main () │ ├────────────────┤
access to │ int * ptr; ──────┼──────┐ ├────────────────┤
memory │ ptr ++; ─────────┼───┐ │ ├────────────────┤
│ ... │ │ │ ├────────────────┤
│ │ │ │ ├────────────────┤
│ │ │ │ ├────────────────┤
└──────────────────┘ │ │ ├────────────────┤
Program │ └──────►├────────────────┤
│ ├────────────────┤
└─────────►├────────────────┤
└────────────────┘
Memory
(A) POINTERS
Fast ┌──────────────────┐ ┌────────────────┐
register │ main () │─────► CPU ├────────────────┤
access │ register int i; │◄───── ├────────────────┤
│ ... │ ├────────────────┤
Regular │ │◄─ ─ ─ ─ ─ ─ ├────────────────┤
memory │ int regular_varn │ ─ ─ ─ ─ ─► ├────────────────┤
access │ ... │ ├────────────────┤
└──────────────────┘ ├────────────────┤
Program ├────────────────┤
├────────────────┤
├────────────────┤
└────────────────┘
Memory
(B) REGISTER VARIABLES
┌──────┐
│ │
┌──────┐ │ Code │ ┌──────┐
│ Code │ │ │ │ │
┌──────┐ ├──────┤ ├──────┤ │ Code │
│ Code │ │ │ │ │ │ │
├──────┤ │ Data │ │ Data │ ├──────┤
│ Data │ │ │ │ │ │ Data │
└──────┘ └──────┘ └──────┘ └──────┘
(C) MEMORY MODELS
Figure 1-2. C gives you control of the machine.
C Is Extensible
C also lets you customize the contents of include files and libraries so
that they contain only the definitions and functions your program needs.
These custom files can contain functions for anything from manipulating a
database to formatting text. After you write and test these definitions
and functions, your main program can use them as easily as it can use the
standard include files and libraries provided with your compiler. On large
real-world programming projects, teams of programmers can receive
specifications for each set of routines needed, and each team can create
resources that can be used anywhere in the project. Although most
languages offer a version of this building-block methodology, the C
approach is the simplest, the most flexible, and the easiest to use.
The very popularity of C enhances the value of such language extensions.
Hundreds of vendors have created C function libraries for almost every
imaginable task. Figure 1-3 shows conceptually how you can use function
libraries from both QuickC and other vendors in your programs. You can
easily integrate vendor libraries into your own code, and because they are
the products of professional C programmers, they are likely to be fast and
efficient. You can almost always avoid the age-old problem of reinventing
the wheel.
┌──────────┐
│ MS-DOS │
│ ┌──────────┐
│ │ I/O │
│ │ ┌──────────┐
└─────│ │ Graphics │
┌─────────────┐ │ │ │
│ │ └─────│ │
│ Included ◄─┼──────────────────────│ │
│ definitions │ └──────────┘
│ ─────────── │
│ Your code │ Third- Your
│ main () │ Microsoft party custom
│ ... │ libraries libraries library
└─────────────┘ ┌─┐┌─┐┌─┐ ┌─┐┌─┐ ┌─┐
│ Compile │ ││ ││ │ │ ││ │ │ │
┌────────▼──────────┐ └─┘└─┘└─┘ └─┘└─┘ └─┘
│ Compiled Modules │───────────┘ │ │ │
├───────────────────┤ │ │ │
│ Standard Library │──────────────┘ │ │
├───────────────────┤ │ │
│ Graphics │──────────────────────────────┘ │
├───────────────────┤ │
│ Database │────────────────────────────────────────────┘
├───────────────────┤
│ Special functions │
└───────────────────┘
│ Link
┌────────▼──────────┐
│ Ready-to-run │
│ program │
└───────────────────┘
Figure 1-3. Using include files and libraries.
C Is Structured
The syntax of the C language itself supports structured programming. C
provides the control structures of a modern structured language, such as
if/then/else, for, while, while...do, and switch. (The last is like
Pascal's case statement.) If you are experienced in Pascal or in one of
the newer BASICs (such as Microsoft QuickBASIC), you will find these
control structures conceptually familiar. However, you will have to learn
syntax differences for C, and boxes in the text point these out. If you
are used to one of the older BASICs, you will be pleasantly surprised at
how these structures enable you to avoid nearly all goto statements that
lead to disorganized "spaghetti code."
C Is Concise
Although C is a well-structured language, it encourages concise rather
than verbose statements. For example, it uses braces to begin and end
blocks of code, rather than Pascal's begin and end. C provides shorthand
operators for assigning values to variables and for incrementing
variables. To show the flavor of C, the following table presents a few
comparisons of C, Pascal, and BASIC assignment statements:
Some Comparisons of BASIC, Pascal, and C
BASIC Pascal C
──────────────────────────────────────────────────────────────────────────
1. Set a, b, and c to 0 a = 0 a := 0; a = b = c = 0;
b = 0 b := 0;
c = 0 c := 0;
2. Set i to i + 1 i = i + 1 i := i + 1; i++;
3. Set a to a + 5 a = a + 5 a := a + 5; a += 5;
──────────────────────────────────────────────────────────────────────────
Such conciseness speeds the typing of programs and makes C source files
more compact and easier to edit. C functions are more accessible than
their Pascal counterparts and much more efficient than the awkward
subroutine mechanism of BASIC. With the C preprocessor, you can create
your own shorthand, or macro, definitions with which you insert
expressions or whole blocks of code in text by typing the name of the
definition.
This brief overview of the general features of C should suggest why the
language is so popular. Let's now look more closely at the product with
which this book is concerned, Microsoft QuickC, and see how its particular
features and advantages make programming in C even more attractive.
Why QuickC?
Traditionally, C has had one big drawback compared with interpreted
languages such as BASIC──a complex compilation and debugging process. You
probably know that C is a compiled language, and MS-DOS─based compiled
languages traditionally have required that you go through a lengthy series
of steps to produce an executable file.
The steps to compiling a traditional C program are the following:
1. Start a text editor or word processor and write a program.
2. Save the program to disk and exit the editor.
3. Run the compiler program by issuing a command line from the DOS
prompt, usually with several filenames and options included, that tell
it, for example, what memory model to use and whether to generate a
listing file.
4. Look at the listing produced by the compiler, and study every error
message.
5. Print out this error list for reference.
6. Start the editor again, open your C program file, and for each error
try to find the exact line in which the error occurred and correct the
program.
7. Go back to step 3 and try again until the program compiles without
errors into an object code file.
8. Now run the linker, and tell it what libraries to combine with your
object code file to produce an executable program (an MS-DOS .EXE
file). If you used an incorrect function name or failed to specify the
correct libraries, you will now get a new batch of error messages,
this time from the linker. (They may, for example, report an
"unresolved external," which probably means the name you used for a
function in your code did not match the name of the function defined
in the library.) To fix these errors, you may need to look at listings
of include files. Or you may have to go back to the editor and correct
your program. In any case, you must recompile and then try to link
again.
9. When the code links without errors, you can finally run the program.
Did it execute as you expected? No? Do you want to make some changes?
Well, go back to the editor and try again.
Just reading through these steps suggests how tedious a traditional
compiled language can be. With interpreted languages, such as BASIC, LOGO,
or HyperTalk, you can type a line or two of code, execute it immediately,
and see the results. If your line of code contains errors or if you want
to add or change something, the interpreter usually provides a simple text
editor or line editor you can use immediately.
But interpreted languages have one critical drawback──they're slow. Each
line in a program in an interpreted language has to be translated into
machine-executable instructions each time it is encountered. Therefore,
only the simplest interpreted-language applications run fast enough for
use in the real world.
The philosophy behind QuickC is to provide a programming environment that
is as easy to use as an interpreter, but with the execution speed
obtainable only through a compiler. With QuickC, writing and testing
programs is so easy that C can be a beginning programmer's first language.
The QuickC Programming Environment
With QuickC, you do all of your program development in and from the same
place──the QuickC integrated programming environment. (Figure 1-4 shows
the way your screen looks when you start QuickC.) This environment offers
many advantages:
1. You can open a file for editing by using the Open command on the File
menu, or you can simply start typing a new program. The QuickC
full-screen editor is immediately available, with insert/delete,
cut/paste, indention──all the features you need to type a program as
easily as you type a letter with a word processor. And you never
really "leave" this editor. You merely select whatever service you
need from the menus.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 1-4 can be found on p.12 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 1-4. The initial QuickC screen.
2. To run the program you can click on the mouse or type a command. When
you work with a program that has not yet been compiled, the compiler
and linker are called as needed. There are no complex command-line
options to type. If your program is error free, the program runs in
seconds on the output screen.
The main reason your programs compile, link, and run so quickly with
QuickC is that, unlike traditional C compilers that compile and link
to disk, QuickC by default compiles to memory. Thus, it can compile
10,000 lines a minute on a standard IBM PC/AT. Another reason is that
some of the most commonly needed functions are held in memory. You
also can create libraries that can be loaded into memory. The result
is that QuickC uses available memory very efficiently.
3. As you view an error message, the cursor follows along through your
program text; you can instantly correct each error with the built-in
editor. No printed listings to pore over; no error numbers to look up!
4. Suppose your program compiles correctly but doesn't work as you
expected. Without leaving QuickC, you can turn on the debugging and
trace features, rerun your program, and then watch the changing values
of selected variables, follow the flow of execution, and check the
values being passed to and from functions called by your program.
What about multiple-module programs──C programs that have several
separately compiled libraries and code files? Traditionally, you had
to run a special "make" program and give it a file with a unique
syntax that told the compiler how to rebuild such a complex program
after any change was made. With QuickC's program list feature, you
simply tell QuickC what libraries and source code files you want to
use. QuickC keeps track of all the other details, such as the
relationship between modules and the date each module was last
compiled.
5. Do you need access to MS-DOS? Need to make a new directory or back up
some programs? Maybe you want to run some previously compiled C
programs from MS-DOS. With a traditional, command-line-driven C
compiler, you exit the compiler, work in MS-DOS, and then run the
compiler again and figure out where you left off. With QuickC, you
never leave the integrated programming environment. Using QuickC's DOS
Shell feature, you exit to MS-DOS, take care of your business, and
return to QuickC where you left off.
You can select many other features from the QuickC programming environment
in the same easy way. With a command-line compiler, most features require
that you type obscure flags or option switches on the command line or
create batch files to simplify complicated compiler commands. With QuickC,
you select with a mouse click or keystroke such features as the error
warning level, language extensions, and optimization. But don't let the
convenience deceive you──underneath the covers, QuickC is constructing the
proper list of options so that you can use the same linker. (QuickC also
includes a command-line-driven compiler for those times you have a special
need, such as compiling under certain memory models, or when you want to
work outside the QuickC environment.)
QuickC Performs
QuickC is faster in almost all cases than its nearest competitors, and it
beats them hands down in floating-point operation.
QuickC also is fully compatible with its "big brother," the Microsoft C
Optimizing Compiler, versions 5.0 and later. Any program that compiles
under QuickC compiles under Microsoft C, version 5.0. Therefore, you can
develop programs with QuickC and then effortlessly recompile them under
Microsoft C for fine tuning, using a variety of optimization techniques.
QuickC: Standard and Comprehensive
Earlier we discussed ANSI and other official standards for C. There are
also unofficial industry standards that are almost as important. When you
use QuickC, you have the benefits of using a compiler that has become the
industry standard for PCs: Microsoft C. QuickC is fully compatible with
that standard. Thus, dozens of third-party C code libraries work with your
programs because the programs you write are compatible with the ANSI or
UNIX System V standards or with the MS-DOS─specific features of
Microsoft C.
The extras that come with the QuickC product are also impressive. Each
standard-model library (small, medium, compact, large) supports the 8087
coprocessor. There are libraries for every kind of PC graphics from
monochrome and CGA to the latest VGA graphics for the IBM PS/2. The QuickC
Graphics Library routines feature easy-to-use routines for drawing points
and lines and manipulating complete images, including filling and
animation, all with impressive speed. QuickC also has libraries that allow
your programs complete access to MS-DOS and BIOS calls. And, because of
QuickC's UNIX compatibility, you can also use UNIX System V functions for
writing programs that can be ported to work in the UNIX environment.
Hardware Requirements
To run QuickC you need an IBM PC/XT, PC/AT, PS/2, or compatible computer
with at least 448 KB of RAM and at least two floppy-disk drives. We
suggest, however, that you develop QuickC programs on a hard disk.
Compiling or linking to disk with floppy disks is time-consuming compared
with hard disks. Also, fitting all the files you need for developing
programs onto two disks can be tricky. But because some of you will be
using floppy-disk-based systems, we will give you some tips later that
should help you make the best of the situation. (And the situation is
anything but grim: You can certainly develop programs that run great under
QuickC on a floppy-based system.)
We also recommend (but don't presuppose) that you use QuickC with a
compatible mouse. You can handle all QuickC functions from the keyboard,
but why get bogged down learning the keystroke combinations? With a mouse
in hand, you simply point at what you want and select it.
On the other hand, many people don't have (or choose not to use) a mouse.
With QuickC, you can use short keystroke combinations. For example,
Alt-r-s selects the Run menu's Start option to compile and run a program.
(Even if you have a mouse, typing is sometimes faster.)
Graphics capability is optional for most of this book. Chapter 15, which
deals with graphics, requires a CGA, of course; for advanced graphics, you
need an EGA; and the VGA section requires a PS/2, or a VGA board for older
PCs. (If you have the new VGA, you also have CGA and EGA capability.) Even
if you have only the basic monochrome adapter, you can create many
interesting QuickC programs with the built-in IBM graphics character set.
Finally, we recommend that you have a printer (although a printer is not
required for this book).
Knowledge Requirements
Some programming experience──with BASIC or Pascal, for example──will help.
But thanks to the ease of use of QuickC, C can be your first programming
language, although you may have to work a bit harder than more experienced
programmers.
Because many of you have programmed in BASIC (such as Microsoft's BASICA
or QuickBASIC) or Pascal (Borland's Turbo Pascal, for example), we scatter
"asides" throughout the text for BASIC and Pascal programmers. These point
out the ways in which C is similar to and different from those languages.
Familiarity with another language is a two-edged sword when it comes to
learning C. On the one hand, you already know many programming concepts
used in C. On the other hand, differences in syntax and usage can trip you
up if you aren't careful.
If you are a UNIX programmer, you will feel right at home──as soon as you
get used to QuickC's much more comfortable living room! The QuickC
environment is far easier to use than the UNIX cc compiler and ln linker,
and you won't have to write any make scripts. You probably already know
the fundamentals of C, but watch out for features that are different in
the IBM PC/MS-DOS environment, especially graphics and MS-DOS system
calls. But as we noted, with a few minor exceptions, Microsoft C supports
the standard I/O and other library functions used on UNIX systems.
Occasional boxes point out matters of interest to UNIX programmers.
Conventions and Style
We have chosen the following typographical conventions for the
descriptions of a C program in the text:
■ Names of ordinary (local) variables are lowercase italic. Example:
count, sum
■ Names of external or global variables are also italic, but the first
letter is capitalized. Example: Model
■ Underscores join the words of multiple-word variable names. Example:
Grand_total (an external variable) or line_count (an ordinary variable)
■ Constants created with #define are uppercase italic. Example: PI
■ Macro definitions are uppercase italic. Example: PRINT_ERROR(MSG)
■ Function names are lowercase italic. Underscores join multiple-word
function names. Examples: main(), count_lines(), printf()
You'll also notice that names of Microsoft library functions that are
non-ANSI-standard (such as the graphics functions) are lowercase italic
and preceded by an underscore. Example: _getvideoconfig() (a Graphics
Library function)
■ In #include statements, names of header files that we create are in
double quotation marks. Example: #include "chr_graphics"
Names of header files provided by Microsoft are in angle brackets.
Example: #include <graphics.h> (This convention affects the way in
which the compiler searches for header files on disk.)
■ Built-in "keywords," or reserved words, of the C language are lowercase
italic. Examples: int, do, while
■ Program names are uppercase roman. Example: HELLO.C
■ Filenames and pathnames are uppercase roman. Examples:
\LIB\GRAPHICS.LIB, SCREEN.DAT
■ Names of special keys are spelled as they appear on the standard IBM PC
extended keyboard. Examples: Enter (not Return), Ctrl-C, the Esc key
Program Listings
Program listings are set off from the text in a monospace font. Constants,
variables, and function names are capitalized as indicated in the
preceding list but are not italicized.
In many cases, we provide a sample session that demonstrates how a program
interacts with the user. In these listings, user input is italic.
guess────────────────────────────────────────────Run the guess.c program
What number am I thinking of?───────────────────────────Program response
7─────────────────────────────────────────────────────────────User input
Wrong! Try Again?
3
Right! You win!
NOTE: The comments at the right in the sample session above are not part
of actual program dialogue.
Program Style Conventions
A clear, consistent typographical style makes programs easier to read. No
single style is universally accepted for C program listings. Ultimately,
you fashion your own, based on your judgment and the prevailing usage. In
some cases, more than one kind of syntax can be used. Although C itself
doesn't care about spacing between the elements of a statement or an
expression, we use a space between elements unless removing the space is
clearer. Also, we use a 4-space indention for nested statements and the
braces that enclose them.
We always align braces ({ and }) vertically──a major stylistic departure
from Kernighan and Ritchie. That is, we put
function_name()
{
<body of function>
}
rather than
function_name() {
<body of function>
}
We believe this style enables you to read the listings and identify blocks
of code more easily. Be warned, however, that you will find lots of C
listings that contain the second style.
Finally, because experienced C programmers often make a virtue of saying a
lot with a little, we point out concise, idiomatic coding styles that you
are likely to see in program listings from various sources, and we
sometimes show two or more ways to code a statement.
────────────────────────────────────────────────────────────────────────────
Chapter 2 Starting with QuickC
You are now ready to explore the QuickC environment. In this chapter we
describe the environment, show how to set up QuickC on your computer
system, present an overview of the QuickC menus and dialog boxes, and help
you create and run your first QuickC program. We also show how to get help
from QuickC and how to fix program errors. When you finish this chapter,
you will be comfortable with QuickC and ready to learn the C language
itself.
Our Book and Their Book
QuickC comes with an excellent user manual that details the mechanics of
using QuickC. It explains how to configure the system, how to use the
menus, the meaning of the options in each menu and dialog box, and how to
use the programs that comprise the QuickC programming tools. The QuickC
package also includes two reference guides, the Microsoft QuickC Language
Reference and the Microsoft QuickC Run-Time Library Reference. The first
guide describes the rudiments of the C language; the second provides
specific information for using each of the more than 350 C library
routines.
Our book is designed to complement the QuickC user manual by focusing on
teaching C programming with QuickC rather than rehashing operational
details from the manual. However, because you need to master QuickC's
features to write effective C code, we sometimes present a brief overview
of a procedure or subject and then refer you to the manual for a complete
discussion. We use this approach particularly when we discuss system setup
and configuration, for which the manual provides extensive and detailed
guidance.
We also do not discuss all the editor commands and keystrokes. You can
learn these from the QuickC manual and through one of QuickC's excellent
help screens. However, when we know of some useful tricks that aren't
covered in the manual, we pass them along immediately.
Directories and Files Used by QuickC
Programming in C usually involves combining several files to eventually
form an executable program. These files include definitions of data
structures and functions (header files), libraries of precompiled
functions, and your own program code. The QuickC environment uses several
directories to organize the files into distinct groups, according to
purpose, such as function libraries, include files, and so on. QuickC also
uses distinctive filename extensions to identify files that are used or
created in the compiling and linking process.
Why So Many Files?
If you use languages such as BASIC or some versions of Pascal, you might
wonder why QuickC needs such an elaborate system of files and directories.
With most versions of BASIC, for example, you need only two files: the
BASIC interpreter program that creates and runs your programs, and the
file that contains your BASIC program. Although the QuickC environment can
look quite complicated by comparison, QuickC sets up most of the
directories and files for you (especially if you have a hard disk) and
makes it easy for you to move among all the files of a programming
project. Nevertheless, it is important to understand how QuickC organizes
files, especially if you need to modify the default organization to avoid
conflicts with existing directories or for some similar reason.
To explain the "environment" you work in, we must examine QuickC's
directories and the files they contain. We use the QuickC default names in
our discussion, because the actual name and location of the directories
depend on how you invoke the SETUP program and whether you use a
floppy-disk or hard-disk system.
Base Directory and Subdirectories
QuickC installs directories as subdirectories of a "base" directory. If
you use QuickC by itself, the base directory usually is c:\qc; if you use
QuickC as part of the Microsoft C 5.0 Optimizing Compiler package, the
command is usually c:\c5\qc. Thus the actual pathname for the \BIN, or
base, directory probably is C:\QC\BIN or C:\C5\QC\BIN.
The most important QuickC directories are \BIN, \INCLUDE, and \LIB. Let's
look at these and some optional directories that you might find useful.
The \BIN Directory, Compiler, and Linker Programs
The \BIN directory contains the program QC.EXE, which runs QuickC,
provides the integrated programming environment, and lets you write,
compile, link, and execute QuickC programs. (The name "BIN," by the way,
is short for "binary." The \BIN directory is usually reserved for "binary
files," or files containing executable programs.)
The QuickC package actually contains two compiler programs: QC.EXE, which
comprises the integrated programming environment with its editor, menus,
and so on; and QCL.EXE, a much shorter program, which generates a
"command-line" version of the QuickC compiler. (To help you distinguish
between the programs, think of QCL as "QuickC Line-oriented.") QCL is much
like the traditional C compiler we described in the Introduction. Rather
than using menus and dialog boxes, you can compile a program only by going
to the MS-DOS prompt and typing a command line with options. Another
program in the \BIN directory, called LINK.EXE, combines your compiled
programs and stand-alone libraries into a single executable program.
QuickC usually performs this linking as an invisible process, although
you can specify linker options when necessary. When you use QCL, you
control the linker directly with a series of command-line options.
QC.EXE, with its integrated programming environment, is more convenient to
use, and we assume in most parts of this book that you will use it to
compile and run your programs. However, the command-line compiler QCL.EXE
is very useful for doing what are called batch compilations, for setting
specific combinations of compile options, for compiling with alternate
memory models, or for using an alternative program editor with QuickC. QCL
also lets you use "make" files created with the Microsoft C Optimizing
Compiler, version 4.0 or 5.0. (Make files are files that keep track of the
compilation of multiple program modules. QuickC offers an easy-to-use
alternative called "program lists," which we discuss in Chapter 6.)
The \INCLUDE Directory and Header Files
The "core" of C is greatly extended by compiler vendors who develop new
sets of predefined constants, macros, data structures, and functions for
such areas as graphics, device I/O, and DOS. Some of these are standard
(proposed ANSI standard or UNIX System V standard) and are found in
virtually all compilers; others are specific to the IBM PC or to
Microsoft. The QuickC \INCLUDE directory contains many text files of both
types. These are known as "include files" because your program can include
definitions from one or more of these files. (They are also known as
"header files," because their names must be specified at the beginning, or
head, of a program.) This is also where you'll put any third-party
libraries you obtain.
Include files are not executable files or complete C source programs; they
are ordinary text files that contain useful function definitions; they
provide an interface between your program and the compiled code in
stand-alone libraries. When a program references an include file, the code
in the include file is inserted into and compiled with the code you
actually typed in.
For example, the include file stdio.h contains many of the most commonly
used input and output functions, and graphics.h contains definitions for
data structures and functions in the Graphics Library. The following table
lists the standard QuickC include files. Note that include files have
filenames with the .h extension. (Don't worry about understanding this
comprehensive list yet; we will discuss many of them in detail as we use
them in programs throughout the book.)
QuickC Include Files
╓┌─┌──────────┌──────────────────────────────────────────────────────────────╖
File Main Purpose
──────────────────────────────────────────────────────────────────────────
assert.h Debugging expressions
conio.h PC-specific console (keyboard) and port (device) I/O
ctype.h Character testing and conversion
direct.h Creating, removing, and changing MS-DOS directories
dos.h Setting and reading 8086 registers for MS-DOS calls
errno.h System-wide error numbers
fcntl.h Opening MS-DOS files with various modes
float.h Implementation-dependent values for advanced floating-point
File Main Purpose
──────────────────────────────────────────────────────────────────────────
float.h Implementation-dependent values for advanced floating-point
operations
graph.h Microsoft-specific data structures and functions for monochrome
(MDA), CGA, EGA, MCGA, and VGA graphics
io.h Low-level file-handling and I/O routines
limits.h Implementation-dependent values for sizes and ranges for data
types, etc.
malloc.h Memory allocation functions
math.h Definitions used by math library
memory.h Memory manipulation routines (buffer setup, etc.)
process.h Used with routines that allow a program to "spawn" (run)
another program as a "child process"
search.h Sorting and searching routines
setjmp.h Used for saving and restoring the program state during a "long
jump" (jump to a different memory segment)
share.h Flags controlling sharing of a file among several users (i.e.
on a network)
signal.h Values for "signals" that can be sent to interrupt handlers,
etc.
File Main Purpose
──────────────────────────────────────────────────────────────────────────
etc.
stdarg.h Allows a function to use a variable number of arguments (ANSI
style)
stddef.h Miscellaneous constants, types, and variables
stdio.h UNIX-compatible standard I/O, such as functions to get and put
characters to the console or a file
stdlib.h Definitions for miscellaneous library functions
string.h Definitions for string manipulation functions
time.h Data structures used for accessing system time
varargs.h Allows a function to use a variable number of arguments
(XENIX-style)
The \SYS subdirectory of \INCLUDE contains:
locking.h Flags for locking files (for networks)
stat.h Defines structure used to return status of an MS-DOS file or
directory
timeb.h Types used by ftime() (used to get current time)
types.h Types used in values returned by functions for time and file
status information
File Main Purpose
──────────────────────────────────────────────────────────────────────────
status information
utime.h Used by utime() to update access and modification times for
MS-DOS files
──────────────────────────────────────────────────────────────────────────
In QuickC, the \INCLUDE directory also contains a subdirectory called
\SYS. This subdirectory contains "system specific" include files for IBM
personal computers and compatibles.
The \LIB Directory and Libraries
Much of C programming involves writing code that uses standard C functions
to perform such tasks as getting a character from the keyboard or sending
a text string to the screen. Microsoft has already compiled these
functions for you and has placed them in files called "libraries." The
\LIB directory contains these library files, which have either the
filename extension .LIB or .QLB. As noted earlier, when QuickC starts, it
includes in memory the code for a considerable number of commonly used
functions. In addition, Microsoft provides "Quick Library" versions of
some libraries, and you can specify that these be loaded as well to
provide fast, in-memory access. You can also create your own custom Quick
Libraries. Quick Libraries all have the same extension .QLB.
If you examine the PACKING.LST file on the QuickC Product disk, you will
see many libraries with similar names, such as SLIBC.LIB, SLIBFP.LIB, or
MLIBC.LIB. Why are there so many libraries? The architecture of the Intel
8086 and 80286 processors used by the IBM PC family requires that memory
be divided into 64 KB segments. As a result, special instructions are
needed to access program instructions or data that go beyond a single
segment. The designers of C compilers address this problem by providing
programmers with multiple memory models, each containing a different
allocation of segments for code and data. (QuickC uses compact, small,
medium, and large memory models. Microsoft C 5.0 adds a "huge" model.)
Additional libraries handle floating-point (decimal) calculations: Some
use the 8087 floating-point coprocessor chip, others use software that
emulates its functions. Also included is an optional graphics library,
GRAPHICS.LIB.
Combined Libraries
You can use libraries in two ways. When you compile, you can tell the
linker to include specified libraries (a memory-model library, a
floating-point library, a graphics library, and so on). Although this is
most easily done using a "program list," it can involve a bit of
bookkeeping. The easier way to use libraries is to use the SETUP program
(discussed later in this chapter), to build one or more combined
libraries. A combined library is a package that contains one library for
the floating-point option, one standard library for the specified memory
model, some general purpose "helper" libraries, and possibly the optional
GRAPHICS.LIB. The advantage of creating a combined library is that QuickC
uses it by default, so you don't have to specify library names when you
compile and link. The \LIB directory contains any combined libraries you
create with the setup process.
Note: If you intend to write graphics programs, use SETUP to combine the
Graphics Library with your standard library. That way, QuickC always
includes this library in compilations.
The \TMP Directory
QuickC uses the \TMP directory to store temporary files created during
compilation. Normally, QuickC removes these files when it finishes with
them. However, if something "hangs" the system during a compile, you might
want to check the \TMP library and delete any vestigial files.
The \SAMPLE Directory
If your computer has a hard disk, the QuickC SETUP program creates a
\SAMPLE directory and stores in it several example programs. You can use
these to practice loading, editing, compiling, and running QuickC
programs.
The \PROG or \SOURCE Directory
By default, QuickC stores your programs in the current directory when you
invoke the compiler. All other files created by the compiling and linking
process are also stored there. You also can create directories to store
the source code (the actual program text) for the C programs you write and
the various files made from your program by QuickC. Although this is
entirely optional, it makes for a more orderly directory and helps you
organize and find your programs more easily.
Whatever your current directory, compiling programs creates the following
kinds of files, depending on the compiler and linker options you select:
NAME.C──Source code for the C program name
NAME.OBJ──Object code produced by the compiler for the C program name
NAME.MAP──A "map" file showing the addresses used by the linker when it
linked the program name
NAME.EXE──The compiled and linked object code for the program name, which
can be executed by typing name at the MS-DOS prompt
NAME.MAK──A "make" file containing instructions that QuickC uses to
recompile or "rebuild" your program if you change it
Figure 2-1 summarizes our tour of QuickC directories and files. Without
listing all the QuickC files, the chart shows a typical directory
structure for QuickC on a hard disk. (The structure of directories on a
floppy-disk system has several modifications that we will describe in the
section "Setting Up QuickC for Floppy-Disk Systems" on page 32.)
C:\
──┬──
│
│
c:\qc
┌─────────────────────┬─────────────────────┬─────────────
│ │ │
c:\qc\bin c:\qc\lib c:\qc\include
┌─────────────────┐ ────┬──── ┌────────────────┐
│ │ │ │ │
c:\qc\bin\sample qc.exe mlibce.lib assert.h c:\qc\include
───────┬──────── qcl.exe slibc7.lib bios.h ────────┬────
│ qc.hlp graphics.lib conio.h │
cflow.c link.exe graphics.qlb (etc.) locking.h
new-conf.sys lib.exe (etc.) stat.h
new-vars.bat (etc.)
Figure 2-1. Typical directory structure for QuickC.
From Source to Object: An Overview
Now that we've surveyed the compiler, linker, include files, and
libraries, let's see how they work together when you run a program with
QuickC. Let's assume your program uses two include files, stdio.h and
graph.h. When you "run" or "start" the QuickC compile/link phase, the
compiler starts by "reading" your source code in the editor buffer. First,
it sees the instructions to add the include files. The compiler then loads
the stdio.h file and compiles the code found there. (The code in an
include file is not already compiled.) Next it loads and compiles graph.h.
These include files contain, among other things, definitions of functions
whose compiled code resides in libraries. (The standard library for each
memory model contains the code corresponding to standard header files such
as stdio.h; GRAPHICS.LIB contains graph.h.) As it compiles the include
file, the compiler notes these references to library code and passes them
to the linker.
After the compiler generates the object code for the part of the program
you wrote yourself, the linker "resolves" all library references: It
extracts the "modules" that contain the necessary code from the
appropriate libraries and combines them with the rest of the code. The
result is a compiled object program. QuickC's default creates an object
program that runs from within the QuickC environment. This enables you to
run the program immediately after you link it and lets you quickly test
programs without leaving the QuickC environment. However, you can also
create a .EXE file, or executable MS-DOS file, that you can run from the
MS-DOS prompt. Figure 2-2 on the next page summarizes this process
graphically.
Editor
┌───────────────────┐
Program │ #include stdio.h │
references │ #include graph.h │
include │ ──────────── │
files │ ──────────── │
│ ──────────── │
│ ──────────── │
└───────────────────┘
│
│ Preprocessor ┌──────────┐
┌─────────▼─────────┐ │stdio.h │
Included ▒ │ ─────────── │ ┌───── │ ───── │
source ▒ │ ─────────── │ │ │ ───── │
code ─── ▒ │ ─────────── │◄────┘ └──────────┘
│ │◄────┐ ┌──────────┐
Your ─── ▒ │ ─────────── │ │ │graph.h │
source ▒ │ ─────────── │ └───── │ ───── │
code └───────────────────┘ │ ───── │
│ └──────────┘
│ Compiler
┌─────────▼─────────┐
Compiled ▒ │ ---------───? │
library ▒ │ ---------───? │
references │ │
│ │
Your ─── ▒ │ ------------ │
compiled ▒ │ ------------ │
code └───────────────────┘
│
│ Linker
┌─────────▼─────────┐ ┌─────────────┐
Final │ ----------- │ │ Libraries │
object │ ----------- │◄─────────│ (combined │
program │ ----------- │ │ or │
(in memory │ ----------- │ │ separate) │
or .EXE) │ ----------- │ └─────────────┘
│ ----------- │
└───────────────────┘
Figure 2-2. Compiling and linking with include files and libraries.
Running the QuickC SETUP Program
Microsoft distributes QuickC on five floppy disks. These disks and their
hundreds of files contain the two compilers (integrated-environment and
command-line), a full set of libraries for each memory model with a choice
of 8087 hardware or emulation, a rich assortment of more than 30 include
files, several utility programs, and many other goodies. The QuickC SETUP
program lets you set up a working QuickC environment with directories
containing only those files that you need and provides automatic access to
directories as you specify.
SETUP performs the following operations:
■ Sets up variables and commands in the MS-DOS environment that tell the
operating system where to find all QuickC programs and files
■ Sets up a home directory for QuickC, creates the \BIN, \INCLUDE, \LIB,
and \TMP subdirectories, and moves files from the floppy disks to these
directories
■ Creates one or more combined libraries, depending on the memory
model(s) and form of floating-point support you specify
Note: SETUP for a floppy-disk system creates only the combined library.
You must do the rest partly "by hand." See the section "Setting Up QuickC
for Floppy-Disk Systems" on page 32.
MS-DOS Variables and QuickC
As we mentioned above, QuickC sets up and uses some MS-DOS commands and
variables. MS-DOS uses variables (sometimes called MS-DOS "environmental"
variables) to specify the location of system resources. When you boot an
MS-DOS disk, the operating system calls on two files to configure the
system: AUTOEXEC.BAT and CONFIG.SYS. Commands in these files control the
environment that QuickC uses when you run it.
When you run the SETUP program for a hard-disk system, QuickC sets
environmental MS-DOS variables in two files: NEW-VARS.BAT and
NEW-CONF.SYS. You can use these files as is or insert their contents into
the AUTOEXEC.BAT and CONFIG.SYS files respectively. We recommend the
latter procedure unless there are serious conflicts with your existing
settings.
You can use any editor (such as SideKick or EDLIN) to insert NEW-VARS.BAT
in your AUTOEXEC.BAT file. If you have no AUTOEXEC.BAT, use MS-DOS to
rename NEW-VARS.BAT as AUTOEXEC.BAT. The resulting file might look like
this:
setclock────────────────────────────────────────────────Set system clock
fastopen c:────────────────────────────────────Install file access cache
sk──────────────────────────────────────────────────────────Run SideKick
set PATH=c:\;c:\wp;c:\c5\bin;c:\qc\bin;a:\───Combined with your old path
set INCLUDE=c:\qc\include
set LIB=c:\qc\lib
set TMP=c:\qc\tmp
After you insert the SET commands found in NEW-VARS.BAT, you will probably
have two PATH= commands in your AUTOEXEC.BAT file. Combine the directories
in the path provided by SETUP with your existing path, as shown in Figure
2-3. You can usually use the rest of the SET commands without
modification. (By default, MS-DOS permits only 128 bytes of space for
storing MS-DOS variable values.) If this amount proves insufficient,
modify it as described in the sidebar on the next page.
AUTOEXEC.BAT NEW-VARS.BAT
┌──────────────────┐ ┌──────────────────────┐
│ ───────── │ │set PATH=c:\qc\bin │
│ ───────── │ │set INCLUDE... │
│ set PATH=c:\ │ │set LIB... │
│ │ │set TMP... │
│ │ │ │
└──────────────────┘ └──────────────────────┘
│ │
└────────────┬─────────────┘
│
┌─────────────▼──────────────┐
│ ─────────── │
│ ─────────── │
│ set PATH=c:\; c:qc\bin ◄───┼──── Combined paths from
│ set INCLUDE=c:\qc\include │▒ AUTOEXEC.BAT and NEW-VARS
│ set LIB=c:\qc\lib │▒─── As is, from
│ set TMP=c:\qc\tmp │▒ NEW-VARS.BAT
└────────────────────────────┘
CONFIG.SYS NEW-CONF.SYS
┌──────────────────┐ ┌──────────────────┐
│ ───────── │ │ FILES=20 ◄───────┼── This is larger, so
│ ───────── │ │ BUFFERS=10 │ replace existing FILES
│ ───────── │ │ │ command
│ FILES=10 │ │ │
│ BUFFERS=10 │ │ │
└──────────────────┘ └──────────────────┘
│ │
└────────────┬─────────────┘
│
┌─────────────▼──────────────┐
│ ─────────── │
│ ─────────── │
│ ─────────── │
│ ─────────── │
│ FILES=20 │
│ BUFFERS=10 │
└────────────────────────────┘
Figure 2-3. Editing AUTOEXEC.BAT and CONFIG.SYS.
Here's what the NEW-VARS.BAT commands do. PATH is an MS-DOS command that
specifies the directories that MS-DOS searches to execute a program.
Whenever you tell MS-DOS to execute a program on your hard disk (such as
the QuickC linker or library manager), it first looks in the root
directory of drive C:, and then checks the specified directories in the
order they are listed. The next command tells QuickC that include files
are in the \INCLUDE subdirectory of the main QuickC directory. Similarly,
the other variables show that libraries are found in \QC\LIB and temporary
files are in \QC\TMP.
Setting Up QuickC
Now let's set up the QuickC working environment. The QuickC manual should
be your source for detailed information about setup procedures and the
various options involved, but here are "quick start" instructions that can
simplify the process and probably save you time.
The basic steps you should follow are:
■ Check the PACKING.LST file on the first QuickC distribution disk. Be
sure you have a complete set of disks and manuals.
■ Back up the QuickC disks to floppy disks (use the MS-DOS DISKCOPY
command to ensure you have an exact copy). Then use the backups during
the setup process.
■ Run the SETUP program.
Before you run SETUP and before you use QuickC to develop programs, be
sure that you have at least 448 KB of free memory. QuickC may appear to
run fine with somewhat less than 448 KB until you try to compile certain
programs.
To verify the amount of free memory, type the CHKDSK command at the MS-DOS
prompt. To increase the amount of free memory, you might be able to change
your AUTOEXEC.BAT file so that some memory-resident programs are not
loaded. Then, reboot to free the memory those programs were reserving.
──────────────────────────────────────────────────────────────────────────
Out of Environment Space?
If your current AUTOEXEC.BAT has many SET commands or a long PATH=
statement, you might get an MS-DOS "out of environment space" error when
you add the QuickC variables. If this happens, expand the available
environment space by putting this command in your CONFIG.SYS file:
shell=c:\command.com /e:<size>/p
For MS-DOS versions 3.0 and 3.1, size is the number of 16-byte
"paragraphs" you want to reserve for the MS-DOS environmental variables;
for MS-DOS versions 3.2 and later it is the actual number of bytes. The
default size is 10 paragraphs, or 160 bytes. To set the environment to 256
bytes, use:
shell=command.com /e:16/p──────────────────────MS-DOS version 3.0 or 3.1
shell=command.com /e:256/p───────────────────MS-DOS version 3.2 or later
──────────────────────────────────────────────────────────────────────────
If you normally use memory-resident programs or a RAM disk, we recommend
that you reboot without installing them before running SETUP. The SETUP
program will fail without at least 385 KB of available RAM. After you set
up the QuickC environment, experiment with memory-resident programs or RAM
disks if you wish.
Setting up a hard-disk system for QuickC differs from setting up a
floppy-disk system. Therefore, we have developed separate walkthroughs for
hard-disk and floppy-disk users. If you have a floppy-disk system, skip
the next section and read "Setting Up QuickC for Floppy-Disk Systems" on
page 32.
Setting Up QuickC for Hard-Disk Systems
First, put Libraries Disk #1 in drive A and type the SETUP command. The
following line is a typical SETUP command:
C>SETUP H C:\QC M EM GR
The H specifies that your system has a hard disk.
C:\QC is the pathname of your QuickC "base" directory. By default, QuickC
creates the following subdirectories under the base directory:
C:\QC\BIN Compiler, linker, and other executable
programs
C:QC\BIN\SAMPLE Sample C programs
C:\QC\INCLUDE Include (header) files
C:\QCINCLUDE\SYS System-specific include files
C:\QC\LIB Libraries
C:\QC\TMP Temporary files
──────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────
Are You Using both QuickC and Microsoft C 5.0?
If you use both QuickC and the Microsoft C Optimizing Compiler 5.0, you
can install both compilers on your hard disk without causing any conflict.
Because both compilers use the same library and include files, and because
both compilers use the same environment variable names to locate these
files, you won't have to create separate directories for each compiler's
library and include files. The only planning and organizational work
you'll need to do is to organize the compiler files and the source code
files.
Any program that you can compile with QuickC can be recompiled without
change by Microsoft C 5.0. QuickC's fast compiler can save time in program
development, and then the sophisticated optimizations of Microsoft C 5.0
can speed the execution of your program. Furthermore, QuickC provides
syntax checking for features unique to Microsoft C 5.0. If a program is
syntactically correct but uses features of the larger compiler (the huge
memory model, for instance), QuickC simply ignores those features when you
run the program.
──────────────────────────────────────────────────────────────────────────
Note that SETUP overwrites any file with the same name as a QuickC
distribution file. If you follow our recommendation to create a new
directory for QuickC, \QC, in your root directory, you eliminate this
problem.
The M option in the SETUP command lets you use the "medium memory model"
to compile programs. Note that you can specify other or additional memory
models when you run SETUP. (See the manual for details.) Although we will
explain all memory models in later chapters, we use the medium model
throughout this book because it is the only supported model for programs
compiled in the QuickC environment. (If you use the command-line compiler,
which assumes a small model, you might want to create a small model
combined library that conveniently collects all of the functions you
normally use with QCL. You can create new combined libraries without
running SETUP again.)
The EM in the SETUP command specifies that all floating-point arithmetic
be performed by software "emulation" of the 8087 math coprocessor chip. If
you have an 8087/80287/80387 chip in your PC, you might prefer to use the
87 option which directs floating-point calculations to the coprocessor.
When QuickC builds your core library, it uses this specification to select
the appropriate floating-point library. Note, however, that any .EXE file
you create with the 87 option will not run on machines without a math
coprocessor. If you are concerned about portability, use EM when you set
up QuickC. The QuickC environment uses only the emulator; if a coprocessor
is present the emulator detects that fact and uses it.
Finally, the GR option specifies that QuickC's Graphics Library functions
be included in your combined libraries. We recommend that you use this
option so that you can run the graphics programs in this book without
specifying the GRAPHICS.LIB every time you link. (Of course, if your
computer has only a monochrome text display, you should not use this
option. This installation will proceed, but programs that you subsequently
create that use graphics will not work.)
After you enter the initial SETUP command, the program asks you if you
want to delete the "library subcomponents," or parts of libraries that are
not needed for the configuration you chose. Unless you plan to use memory
models or floating-point packages other than those specified in the SETUP
command, you can save a lot of disk space by typing y at this prompt. The
SETUP program then prompts you to insert the appropriate distribution
disks in drive A.
Without any further input, SETUP creates the QuickC directories, places
header files in the \INCLUDE subdirectory, creates a "combined library"
for each specified memory model using the floating-point option you
selected, and places the combined libraries in the \LIB subdirectory. This
library is called your "standard library" because it contains compiled
versions of all the standard C routines (with specified options, such as
graphics).
If you have already edited your MS-DOS AUTOEXEC.BAT and CONFIG.SYS files,
your QuickC environment is now set up and ready to use. Please skip the
next section, which is for floppy-disk users.
Setting Up QuickC for Floppy-Disk Systems
Setting up QuickC for a floppy-disk system differs from hard-disk setup in
two principal ways: First, the floppy-disk setup does not create the
NEW-VARS.BAT and NEW-CONF.SYS files, so you have to set your own MS-DOS
variables; second, because you have only 720 KB of disk space on two
floppy drives, you must be more choosy about which files to install. (If
you have two 1.4 MB 3.5-inch disk drives, as found in the IBM PS/2 line,
you need not be so constrained.)
As explained in the Microsoft QuickC Programmer's Guide, you need to
format at least two floppy disks: one disk for each memory model and one
"scratch" disk to hold temporary files created during the setup process.
Insert your copy of the Libraries Disk #1 in drive A and a blank formatted
disk in drive B. You are now ready to run SETUP. We recommend that you
type the following command:
setup f b: m em gr
This specifies a floppy-disk setup that places the combined library on
drive B. The M option lets you use the "medium memory model" to compile
programs. Although we will explain all memory models in later chapters, we
use the medium model throughout this book because it is the only model
supported for programs compiled in the QuickC environment.
The EM in the setup command specifies that all floating-point arithmetic
be performed by software "emulation" of the 8087 math coprocessor chip. If
you have an 8087/80287/80387 chip in your PC, you might want to use the 87
option instead. When QuickC builds your core library, it uses this
specification to select the appropriate floating-point library. Note,
however, that any .EXE file you create with the 87 option will not run on
machines without a math coprocessor. If you are concerned about
portability, use EM when you set up QuickC.
Finally, the GR option specifies that QuickC's Graphics Library functions
be included in your combined libraries. We recommend that you use this
option so that you can run the graphics programs in this book without
specifying the library GRAPHICS.LIB every time you link. (Of course, if
your computer has only a monochrome text display, you should not use this
option. The installation will proceed, but programs that you subsequently
create that use graphics will not work.)
As it builds the QuickC combined library, SETUP prompts you for the
necessary disks. The setup process on floppy disks can take as long as 15
minutes, so don't be alarmed at the seemingly interminable grinding of the
disk drives. To create library disks for additional memory models or other
floating-point options, run the SETUP program again.
Setting Up the MS-DOS Environment
Because the floppy-disk setup procedure does not create the NEW-VARS.BAT
and NEW-CONFIG.SYS files, you need to set the MS-DOS variables yourself.
To do this, add the following two variables to your AUTOEXEC.BAT file:
set include=a:\include
set lib=b:
(If you do not have an AUTOEXEC.BAT, create one and type in the preceding
variables.)
This tells QuickC to look for include files in A:\INCLUDE and for
libraries on drive B.
Also, edit your CONFIG.SYS so that it assigns values of at least:
files=15
buffers=20
Note that you will have to reboot your system if you are planning on
running QuickC right away, so the new setting will take effect. Figure
2-4 summarizes how QuickC is set up and run on floppy disks.
Drive A Drive B
┌──────────────────┐ ┌──────────────────┐
│ QC.EXE │ │ MLIBCE.LIB │ ───── Libraries
│ │ │ │
│ │ │ QCHELLO.C │ ▒──── Your source
│ │ ◄──────► │ CIRCLE.C │ ▒ files
│ │ │ │
│ │ │ QCHELLO.EXE │ ▒──── Temporary
┌──│ ◄──┐ │ ──────── │ ▒ and object
│ └──────────────────┘ │ └──────────────────┘ files
│ │
│ Swapped after startup │
│ ┌──────────────────┐ │
└──► ├──┘
│ \INCLUDE │───── Include files
│ │
│ QC.OVL │───── Overlay file
│ │
│ QC.HLP │───── Help screens
│ │
│ LINK.EXE │───── Linker
└──────────────────┘
Figure 2-4. Floppy-disk setup for QuickC.
Differences for Floppy-Disk Users
The examples in this book assume you have a hard disk with QuickC residing
in a directory on drive C. Floppy-disk users can use these examples by
substituting references as follows:
Hard Disk Floppy Disks
──────────────────────────────────────────────────────────────────────────
c:\qc\bin a:
c:\qc\include a:\include
c:\qc\lib b:
──────────────────────────────────────────────────────────────────────────
Starting QuickC
Now we're ready to start using QuickC.
If you have QuickC on a hard disk and have correctly included \QC\BIN in
the PATH variable in the AUTOEXEC.BAT file, run QuickC by typing
qc
at the C> prompt. (If you haven't changed your PATH variable to include
\QC\BIN, you must change to this directory before you can run QuickC.)
To use QuickC on a floppy-disk system:
1. Boot your system with an MS-DOS disk that contains the new QuickC
AUTOEXEC.BAT and CONFIG.SYS files
2. Put your copy of the Product Disk in drive A
3. Start QuickC by typing qc at the A> prompt
4. When the QuickC screen appears, replace the disk in drive A with a
copy of the Work Disk
Drive A now contains an "overlay" file (this lets QuickC access files
without further disk swapping), the Help menus, the linker, and the
\INCLUDE directory.
Improving the QuickC Display
When you type qc on the MS-DOS command line, QuickC assumes you have a
color monitor. If you have a monochrome monitor, this default setting can
reduce the contrast of the characters on your screen and make them hard to
read. To fix this, exit QuickC by selecting Exit from the File menu, and
start QuickC in its "black-and-white" mode by typing qc /b.
If you use a computer that refreshes the screen at a faster rate than
standard ATs, such as some higher-performance models of COMPAQ computers,
you can speed screen displays by using the command qc /g to start QuickC.
If your computer has an EGA card, you can set the screen to display 43
lines, instead of the normal 25, by starting QuickC with the qc /h
command. Note that unless you have a high-resolution monitor, text can be
very hard to read in this mode.
You can combine these modes by separating them with a space. For example,
qc /b /g starts QuickC in monochrome mode and accelerates the screen
refresh rate. You can also put the qc command and options in a batch file
so you don't have to type them each time you start.
Overview of the QuickC Screen
If you've used menu-based integrated programming environments such as
Turbo Pascal and Microsoft QuickBASIC before, the QuickC screen should
look familiar. (See Figure 2-5.)
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 2-5 can be found on p.35 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 2-5. QuickC startup screen.
Notice the following screen elements:
■ The menu bar across the top of the screen lists the following options:
File, Edit, View, Search, Run, Debug, Calls, and Help.
■ The "title bar" displays the name of the program currently loaded into
the editor. (Because we haven't written a program yet, it now reads
untitled.c.)
■ The main area of the screen, now blank, is the workspace for your
program.
■ Two "scroll bars," a vertical one on the right side of the screen and a
horizontal one near the bottom of the screen, let you use an optional
mouse to scroll text up and down or side to side.
■ The status line at the bottom of the screen keeps track of the name of
the current program, the status of your program, and the current cursor
position. Note the Context section of the line. QuickC uses this area
to remind you of your current stage of program development. Because no
program is loaded, it reads <Program not compiled>.
Making Selections
The Microsoft QuickC Programmer's Guide gives exhaustive information on
how to select menu items, move among parts of a dialog box, accept or
cancel selections, and so on. Following is a brief and convenient summary
of this material, explaining both keyboard and mouse commands. The QuickC
manual also discusses several alternative selection methods you might want
to explore. To save space and time we show only one method each for mouse
and keyboard.
Keyboard Shortcuts ("Hot Keys")
QuickC lets you select certain frequently used menu items without opening
the menu first. These "shortcut" or "hot" keys are particularly handy when
you use the editor. Here are some of the most useful ones:
Key Function
──────────────────────────────────────────────────────────────────────────
F2 Open last file used
Alt-Backspace Undo last edit
Shift-Del Cut marked text
Ctrl-Ins Copy marked text
Del Clear editor buffer
F4 View output screen
Ctrl-/ Search for selected text
F3 Repeat last search
Shift-F3 Find next error
Shift-F4 Find previous error
Shift-F5 Start program
F5 Continue stopped program
──────────────────────────────────────────────────────────────────────────
(The Microsoft QuickC Programmer's Guide contains additional
combinations.)
The Mouse
Although you can select all QuickC functions from the keyboard, you might
want to try using a mouse if you have one. With a mouse, you need only to
point and click to select anything on the screen. Because you don't have
to learn all the keystroke combinations for making selections or using the
editor, you can concentrate on learning C right away. Further, the mouse
makes it easier to select items from a dialog box. You might want to learn
both the mouse and keyboard methods and see which one best suits you. Or
you can mix them, using the keyboard for making menu selections and the
mouse for making selections in dialog boxes, for example.
You must use a Microsoft mouse or a compatible mouse (such as the IBM PS/2
mouse or the Logitech serial mouse) with QuickC. Before you can use any
mouse with QuickC, however, you must install a "mouse driver," either in
your CONFIG.SYS file or as a .COM file in your AUTOEXEC.BAT. (See your
mouse documentation for instructions.) The driver is the software that
lets QuickC recognize the mouse and respond to its movements as though
they were commands. If you currently use a mouse for other programs, your
system is probably set up correctly already.
Writing a Program
Now we're ready to write a simple C program, which we will call QCHELLO.C.
First, select the File menu. If you have a mouse, move the mouse until the
pointer on the screen is on the File menu, and click the left button. This
reveals the menu, as shown in Figure 2-6. Now, move the pointer to the
Open option, and click the left button again. To reveal the File menu
using the keyboard, press the Alt key and then press the f key. Notice
that each menu item has a highlighted letter (often, but not always, the
first letter in the word or phrase). Type this letter to select the menu
item. Select the Open option by typing o.
Note the Exit option in the File menu. Choose this option when you're
ready to end your QuickC session. If you select Exit after changing your
current program, QuickC first asks if you want to save the changed
program. When you exit QuickC, you return to the MS-DOS prompt.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 2-6 can be found on p.37 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 2-6. QuickC File menu.
Selecting a File
When you select the Open option on the File menu, a dialog box appears.
(See Figure 2-7.) QuickC uses dialog boxes to obtain the information it
needs to carry out your request.
You can select a file from a dialog box in two ways. Notice the long
rectangle near the top of the dialog box with a cursor blinking in it.
Typing a filename in this rectangle is the most straightforward method of
selecting a file. Below is a larger rectangle with some names in it. This
box lists the contents of the current directory. Names in ALL CAPS are
directories; names in lowercase are files. (The contents of the current
directory in your system may vary from those in the example.)
To make a selection from a dialog box:
■ With a mouse, move the pointer to the item you want. Click the left
button to select the item.
■ With the keyboard, use the Tab or back-Tab (shifted tab) key to move
from one section of the dialog box to another. Press Enter to select
the item.
When you select a directory, QuickC lists all files and subdirectories in
that directory. Each list you display also has a .. entry. Selecting this
entry moves you back to the parent directory of the directory shown. Thus
you can easily browse through the file system with only a few keystrokes.
With the back-Tab or your mouse, move the cursor to the File Name text
box. Type qchello.c and then press the Enter key. Another small dialog box
appears to inform you that this file does not exist. Accept the default of
Yes to create it.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 2-7 can be found on p.38 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 2-7. File "Open" dialog box.
Typing in the Program
You are now ready to type in a program. QuickC's default mode is in fact
"edit mode," and the large area of the screen with the cursor in it is the
Edit window. As you type the listing below, use the arrow keys to move the
cursor, the Backspace key to make corrections, and press Enter at the end
of each line. After you enter the text shown in Listing 2-1, your screen
should look like Figure 2-8.
──────────────────────────────────────────────────────────────────────────
/* qchello.c -- a simple C program */
main()
{
printf("Hello, and welcome to QuickC!\n");
}
──────────────────────────────────────────────────────────────────────────
Listing 2-1. The QCHELLO.C program.
What Does It Do?
Although we won't look at the structure and anatomy of C until the next
chapter, this program gives you a hint of C style. The first line
(enclosed by the characters /* and */) is a comment that briefly describes
the program. It is optional but highly recommended. The word main()
indicates the beginning of the main function or related group of
statements in the program. (Most C programs have many functions in
addition to the main one.) As the name suggests, printf() prints the
string in the parentheses that follow. The braces, { and }, set off the
group of statements (only one in this case) that make up the main
function. So, it's easy to see what this program does: It prints Hello,
and welcome to QuickC! on the screen. (The \n at the end of the string
simply moves the cursor to the beginning of a new line.)
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 2-8 can be found on p.39 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 2-8. QCHELLO.C as typed into the edit window.
Running QCHELLO.C
Running the program is simple. Select the Run menu. As you probably know,
before we can run our program we must first compile and link it. The Start
option in the Run menu executes all of these steps for you.
When you select Start, a dialog box tells you that the program has been
"modified" and asks if you want to "rebuild" (compile and link) it.
Whenever you change a program, you must recompile. QuickC treats this new
program as a changed program, so press Enter or click on Yes to compile
it.
Before you can blink an eye, QuickC compiles and runs the program. QuickC
is fast, as you will see when you write longer programs, and because this
little program doesn't use any include files or libraries, it compiles
instantaneously.
After the program runs, the screen displays the following:
Hello, and welcome to QuickC!
Program returned (13). Press any key
You are now looking at the "output screen." QuickC keeps track of the
output screen, which always holds the results of your programs, so you can
switch back and forth between it and the QuickC environment screen. Press
any key to return to QuickC. For now, don't worry about the return value
mentioned in the second output line.
Saving the Program
To save this program to disk for future reference, open the File menu
again. Notice the Save and Save As options. Select Save to write the
program to disk. If you want to save the program with a new name, select
Save As. When the dialog box appears, type the new name and press Enter.
(You might try QCHELLO2.C.)
Compiling to a .EXE File
QuickC compiles programs to memory by default. Because it is fast, this is
often the best way to compile while developing a new program. However, the
compiled version of a program compiled to memory disappears when you
compile another program or quit QuickC. Eventually you need a compiled
version of the program on disk, so you can run it without recompiling.
Also, you eventually want to create programs that a user can run directly
from MS-DOS without QuickC available. To produce an MS-DOS-executable
file, we need to "compile to .EXE."
Select the Run menu. Now select the Compile item. The dialog box shown in
Figure 2-9 appears.
This large dialog box lets you select many options. (We will explain the
options later as we use them.) Notice the center column, Output Options.
The small black dot in the parentheses next to the word Memory indicates
that it is the currently selected output option. We want to change this
option to Exe. If you have a mouse, move the pointer between the
parentheses next to Exe and click. From the keyboard, you can move the
cursor to this position with the Tab and Down Arrow keys and press Enter.
But there's an even faster way. Note that the letter x in Exe is
highlighted. To select this item, you need only type the letter x.
Now you can compile the program. Note the four small rectangles at the
bottom of the dialog box. The first one, Build Program, has a double
border, which signifies that it is the default. You can select it in one
of three ways: tab to it and press Enter, click on it with a mouse, or
type b.
The Compile box displays the numbers of the program lines being processed
as the program is compiled. Because this program is being compiled as a
stand-alone .EXE file, it must be linked to various disk files.
Very quickly, the program returns you to the familiar QuickC environment
screen. Note that the program didn't run and produce output as it did when
you compiled it earlier. This compile created a .EXE file, and these
executable files cannot be run directly from QuickC. However, QuickC
provides an easy way to run it.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 2-9 can be found on p.41 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 2-9. Compile dialog box.
Escaping to MS-DOS
At the File menu, select the item DOS Shell. This option switches the
display to the output screen where the MS-DOS sign-on message and prompt
appear. You can now run any MS-DOS command, as well as most programs and
batch files.
To run QCHELLO.EXE, type:
C>qchello
The screen displays the expected output. (No instruction to press a key
appears, of course, because QuickC is not running. We are at the MS-DOS
level.)
Now type:
C>exit
to return to QuickC exactly where you left off.
Getting Help
We will not cover every feature of QuickC in this book so that we can
devote more time to C itself. Although we occasionally refer you to the
QuickC manual, there's another source of help as near as your keyboard──
the QuickC Help facility. In fact, you can select from three levels of
help: general, topic, and keyword.
General Help Screens
Press the F1 key to select the General help option (or use the mouse to
make the selection). The first screen you see is shown in Figure 2-10.
Notice that it displays a summary of some editor commands as well as some
other frequently used commands. The small rectangles at the bottom of the
dialog box let you select the Next or Previous help screen. Next, with its
double border, is the default. Press Enter or click on the box with the
mouse to display the next screen. Don't try to memorize or even understand
these screens. Just get an idea of the general information that is
available for future reference.
Topic Help
If you select Topic help, you can page through lists of topics until you
find the information you are looking for. (See Figure 2-11.) For example,
you could select "preprocessor directives," and then select the particular
directive for which you want help. To choose Topic help directly from the
editor window, press Shift-F1.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 2-10 can be found on p.43 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 2-10. A QuickC help box.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 2-11 can be found on p.43 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 2-11. Topic help.
Keyword Help
Return to the editor window, and move the cursor to the word printf.
Suppose you are writing a program and you are not sure how this C function
works. By pressing Shift-F1, you can retrieve information about the C
keyword or standard function currently marked by the cursor. (See Figure
2-12.)
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 2-12 can be found on p.44 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 2-12. Keyword Help screen.
Fixing Errors
The last section of this chapter discusses how to fix errors in a C
program. The QCHELLO.C program should still be in the Edit window. Let's
make some changes in the program so we can practice fixing errors.
(Normally, we programmers don't have to manufacture errors; we run into
enough of them on our own!)
Use the arrow keys or the mouse to move the cursor to the word printf.
Change it to primtf. Next, go to the end of the line and delete the
semicolon.
Now select Run and Start to compile and run the program. QuickC soon
displays a rectangular error window at the bottom of the screen as shown
in Figure 2-13.
The error message tells you that a semicolon is missing before the closing
}. Notice on your screen that the cursor in the edit window is on the
character immediately following the error. This makes it easy to find and
correct the error. (In this case, the next character is on the next line,
however, so you have to move the cursor to the end of the preceding line
to insert the semicolon after the ).
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 2-13 can be found on p.45 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 2-13. Error window.
Now run the program again. The next error message, `primtf' : unresolved
external, is less clear than the preceding one. Simply stated, it means
that primtf is not one of the standard QuickC functions. When you change
the m back to an n, the program again runs correctly.
Preparing for the Next Chapter
In the next chapter we begin our study of the elements of the C language.
Although we discuss additional QuickC features as needed, we will not
concentrate on using the QuickC environment. So now is a good time to get
comfortable with your new QuickC environment.
We recommend that you try the following:
■ Save QCHELLO.C under another name, and then use the Open option in the
File menu to load it into the editor.
■ Practice compiling and running the program to memory and to a .EXE
file.
■ Use the DOS Shell item of the File menu to exit to MS-DOS, run a .EXE
program, and then use Exit to return to QuickC.
■ Make some errors in QCHELLO.C and try running the program. Observe the
error messages, fix the errors, and run the program again. What happens
if the last } is missing? What happens if you change the word "Hello"
to "Hi"?
■ Read Chapter 6 in the Microsoft QuickC Programmer's Guide to learn
about the advanced features of the editor. We suggest you study them
when you want a break from reading this book. None of these editor
features are needed for you to use this book, but they make it easier
to enter and modify long programs. Remember to use the Help screen to
remind you of common editing functions.
────────────────────────────────────────────────────────────────────────────
PART 2 CORE OF C
────────────────────────────────────────────────────────────────────────────
────────────────────────────────────────────────────────────────────────────
Chapter 3 C Fundamentals
Now that you feel comfortable in the QuickC environment, we can turn our
attention to the fundamentals of C. First, let's look at the basic
elements of a C program.
Basic Elements of C Programs
The simplest possible C program, which we call TINY.C, is shown in Listing
3-1 on the following page. Type this program into the QuickC editor; then
run it with the Start option from the Run menu. (We recommend that you
enter and run all sample programs in this book──we believe this will help
you better understand and remember the concepts we discuss.)
As you probably suspected, this program doesn't actually do anything when
you run it. QuickC generated the message Program returned 1. Press any
key, but the program produced no output at all. The main() function
returns the value 1, in this sample, to the operating system. (The actual
value might be different on your machine.) This value is significant only
if you control it deliberately, as you might want to do when you call a C
program from another program, for example.
──────────────────────────────────────────────────────────────────────────
/* tiny.c -- the smallest possible C */
/* program with comments */
main() /* function name and argument list */
{
/* function definition in braces */
}
──────────────────────────────────────────────────────────────────────────
Listing 3-1. The TINY.C program.
A Program Consists of Function Definitions
As simple as it is, however, this program illustrates a basic element of
C──A C program is essentially a set of function definitions. A function
contains statements (instructions) that the program "calls" to perform
specific tasks. A function definition must contain at least the following
elements:
■ The function name
■ An "argument list" enclosed in parentheses
■ A group of statements that define the function
In practice, and especially with programs written in the new ANSI C
standard, function definitions can be more complicated than this. But this
simplest definition is all we need until we look at functions in more
detail in Chapter 6.
TINY.C has only one function, main(). The argument list, which follows the
function and is enclosed in parentheses, often contains "parameters," or
formal descriptions of information, that the function uses when it is
called (executed). Although an argument list can also be empty, as it is
in main(), the parentheses are still required. Because main() contains no
function definition statements, the program does nothing when you run it.
The QCHELLO.C program we developed in the last chapter is an even better
example of the elements of a C program. Figure 3-1 identifies the parts
of QCHELLO.C.
Function name Argument list
│ │
└──────────┐ ┌──────────┘
main ( )
┌─
│ {
Function definition ───┤ printf("Hello, and welcome to QuickC!\n");
enclosed in braces │ } │ │
└─ └──────────────────┬─────────────────────┘
│
Statement in function definition
Figure 3-1. Parts of the QCHELLO.C program.
A Function Definition Consists of a Group of Statements
In C, a pair of braces ({ and }) encloses a group of statements. Notice
the part of the program between the braces in Figure 3-1. The statement
here defines the function main(). All stand-alone C programs begin with
main(). The statements within braces are sometimes called the "function
body," to distinguish them from the function name and argument list, which
together form the "function header."
The function body can consist of any number of program statements. Note,
however, that the braces are still required even if the definition
contains no statements. Think of braces as symbols that delimit
"paragraphs" of C code.
A Statement Is like a Sentence
A statement in C consists of keywords, variable and function names, and
operators, and, like an English sentence, describes a complete action.
Statements always end with a semicolon. Below are some example C
statements and their meanings:
printf("This is a statement");─────────────────Print This is a statement
count = 1;───────────────────────────────────Set the variable count to 1
getche(ch);───────────────────────────Wait for user to type a character,
assign it to the variable ch, and
echo (display) it on the screen
QCHELLO.C has only one statement, printf("Hello, and welcome to
QuickC!\n"); this statement translates as "Print the string `Hello, and
welcome to QuickC!' and then go to the next line." (The \n specifies a
newline character that moves the cursor to the next line.) This statement
completely defines the function main() and describes what happens when the
program executes the function.
A Statement Can Contain Expressions
Can an expression, such as count + 2, be a statement? Well, it doesn't end
with a semicolon. But more importantly, it is not a complete statement.
The word and number merely express a quantity ("two more than the value of
the variable count"): They don't do anything with the quantity.
Although an expression by itself is not a statement, it can be an
important element of a C statement. For example, count = count + 2; is a
complete C statement that assigns the quantity of the expression to the
variable count.
A Statement Can Call Functions
Let's look at QCHELLO.C in more detail. (See Figure 3-2 on the following
page.) What exactly is the printf() function at the start of the statement
that defines the main() function? If you know BASIC, you might say, "It's
the command you use to print in C." This isn't really correct, however. In
BASIC, PRINT is a built-in BASIC command (or keyword) that prints a string
or number. In C, printf doesn't execute a built-in command; it calls a
function named printf() and gives ("passes") it an argument (or parameter)
that tells it what to print.
Function name Argument list
│ │
└──────────┐ ┌──────────┘
main ( )
┌─
│ {
Definition of main() ───┤ printf ("Hello, and welcome to QuickC!\n");
function in braces │ } │ │ │
└─ │ └───────────┬─────────────────────┘
│ │
Name of Argument list (string to print)
function
being called
Figure 3-2. Parts of QCHELLO.C revisited.
Compare the printf() statement with the line containing main(). Both
consist of a name followed by parentheses: that is, a function name and an
argument list──the list for main() is empty. (Note that when we show
function names in text, we use a trailing set of parentheses to
distinguish them from other C elements.)
The main() function name with its empty argument list are followed by a
pair of braces that enclose the function definition. (You'll notice in
QCHELLO.C that no semicolon follows main() because the line isn't a
complete statement: It's the header for the function definition that
follows.) The line with printf(), however, needs no defining group of
statements because we are not defining printf() here; we're merely using,
or "calling," the function in a statement. To call a function, simply use
its name and argument list in a statement. We refer to statements such as
the printf() line as "function calls."
Always remember that every function must be defined before you can call
it, otherwise QuickC would not know what statements to use when it tries
to compile the function name. So where is the definition of the printf()
function we called in Figure 3-2? The printf() function is a "core
library function." Its definition is built into QuickC so that your
program always has access to it. When you link your program, QuickC
inserts the appropriate machine code for printing.
──────────────────────────────────────────────────────────────────────────
Quick Tip
If you know Pascal, you recognize the use of the semicolon to end
statements in C. However, there is one important difference between its
use in C and in Pascal. In Pascal, the semicolon can be omitted if the
statement is the last statement in a group (the statement before the word
END). In C, every statement ends with a semicolon.
Also notice that the braces in C serve the same function as the Pascal
keywords BEGIN and END: They delimit a group of statements.
──────────────────────────────────────────────────────────────────────────
We stress the difference between C's library functions and the built-in
commands of some other languages to emphasize the all-important role that
functions play in C. C makes no distinction in syntax between QuickC
library functions, such as printf(); functions that you define yourself,
such as main(); and C header files developed by Microsoft or other
vendors.
The Flow of Execution Starts with main()
When you run a C program, execution always begins with the function named
main(), which must be present. What QuickC executes next depends on the
functions that main() calls in its definition. In QCHELLO.C, execution
starts with main(). In the definition of main(), QuickC encounters the
name printf() and executes that function.
Punctuation and Spacing in C Programs
Generally speaking, QuickC lets you break lines of code almost anywhere or
insert many spaces (or none) between program elements. For example, you
could rewrite the QCHELLO.C program as:
main(){printf("Hello, and welcome to QuickC!\n");}
or, at the other extreme, you could add line breaks to produce the
NARROW.C program shown in Listing 3-2. There are, however, some
exceptions to C's tolerance of white space and "free-form" syntax. You
can't split a function name across two lines because the compiler reads
the newline character at the end of the line as part of the function name.
Also, you can't break a quoted string, such as the "Hello, and welcome to
QuickC!" in our printf() statement, between two lines because the compiler
won't let you use the newline character in a "string constant" (although
you can specify a newline with the escape sequence \n, as we have seen).
Because C is a somewhat cryptic language, you should use spacing and
alignment of code to make it easier for other programmers to read and
revise your programs. (Remember, after a few weeks you, too, are "another
programmer" when you look at your code.) You'll also find that aligning
braces vertically helps you avoid errors: The vertical alignment lets you
easily match beginning and ending braces.
──────────────────────────────────────────────────────────────────────────
/* narrow.c -- a choppy c program */
main
(
)
{
printf
("Hello, and welcome to QuickC!\n");
}
──────────────────────────────────────────────────────────────────────────
Listing 3-2. The NARROW.C program.
Using Comments in C
Listing 3-1 on p. 50 contains several lines or parts of lines that begin
with /* and end with */; for example:
/* tiny.c ── the smallest possible C program */
These lines are comments, or nonexecuting remarks, that explain how a
program works. We strongly encourage you to use comments in your programs;
they make the program much easier for a reader to understand. Because
QuickC ignores comments, they can follow a program statement on the same
line or cover many separate lines. The program examples in this book have
an introductory comment, and we insert other comments where appropriate.
Below are several different styles you can use for comments:
/* Comment line one */
/* Comment line two */
or
/* Comment line one
comment line two */
or
/* Comment line one
/* Comment line two
/* Comment line three */
However, you can't insert a comment within a comment as follows:
/* Comment line one
/* Nested comment line two */
Comment line three */
The reason you can't "nest" comments is that once the compiler sees the
beginning of a comment (the /*), it considers everything that follows
(including another /*) to be part of the comment until it sees */. In the
nested comment above, the compiler considers the comment ended at the */
after the word two. It then treats the word Comment on the next line as an
undefined function or variable name.
──────────────────────────────────────────────────────────────────────────
Quick Tip
Many versions of Pascal use both /*...*/ and {...} to enclose comments. In
C, you can never use braces for comments: They serve only to begin and end
groups of statements.
──────────────────────────────────────────────────────────────────────────
Data Types and Declarations of Variables
Variables are names for memory storage areas used by a program. Variables
come in many shapes and sizes. Many BASIC programmers get along reasonably
well using only two types of variables: numeric (representing a number)
and string (representing a series of characters). A BASIC programmer might
write:
ITEM$="WIDGET"
SERIAL=32767
to define two variables. The $ at the end of ITEM signifies a string
variable; its absence in SERIAL specifies a numeric variable. A BASIC
interpreter sets up these variables "on the fly" as it analyzes the lines
of code, without storing them in a particularly efficient way.
With C, the situation is more complicated. In order to use computer memory
more efficiently, the C compiler reserves a specific location in memory
for each variable. To do this efficiently, it needs to know exactly how
many bytes of storage to use and how to store the data in those bytes.
Therefore, C uses many "data types" to specify such things as the range of
numbers that a variable can hold, whether negative values should be
accommodated, whether values can be integers only or include decimal
fractions, and so on. If you are a BASIC programmer, this constant
attention to data types takes a little getting used to. However, by the
end of this chapter, you will know all the available types and when each
should be used.
Let's begin our survey of data types by considering some different types
of data we might store in variables:
■ 30 (the number of students in a class)
■ 557,617,814 (number of seconds since a date in 1970)
■ 22.95 (price of a computer book?)
■ 1,000,000,000,000.00 (future U.S. budget?)
■ a (the letter a)
As you probably know, data is stored in a computer as patterns of bits: 1s
and 0s, "ons" and "offs." In the IBM PC family of computers, bits are
organized in groups of eight (called bytes), or in groups of two bytes
(called words), or in groups of four bytes (double words), depending on
the operation involved and the processor used. Figure 3-3 on the
following page shows how many bytes are needed to store the different
sizes and kinds of numbers in the above list. The figure also shows the
name of the data type that describes the storage involved. The addresses
shown are arbitrary, but they demonstrate how successive items are stored
with lower addresses.
The QuickC sizeof operator returns the number of bytes that a given data
type uses. The program VARSIZE.C (see Listing 3-3 on the following page)
uses this operator and a series of printf() statements to print out the
sizes (in bytes) of the following data types: char, int, long, float, and
double.
ADDRESSES DATA TYPE
Stored ┌──────────────┐
"downward" │ │ 5003
in memory ├──────────────┤ char
│ 1 ─── ▒│ │ 5002 ▒ ──── a
│ byte └──────────────┘
│ ┌──────────────┐
│ ▒│ │ 5001 ▒
│ 2 ─── ▒├──────────────┤ ▒ ──── 30 int
│ bytes ▒│ │ 5000 ▒
▼ └──────────────┘
┌──────────────┐
▒│ │ 4999 ▒
▒├──────────────┤ ▒
▒│ │ 4998 ▒
4 ─── ▒├──────────────┤ ▒ ──── 557,617,814 long
bytes ▒│ │ 4997 ▒
▒├──────────────┤ ▒
▒│ │ 4996 ▒
└──────────────┘
┌──────────────┐
▒│ │ 4995 ▒
▒├──────────────┤ ▒
▒│ │ 4994 ▒
4 ─── ▒├──────────────┤ ▒ ──── 22.95 float
bytes ▒│ │ 4993 ▒
▒├──────────────┤ ▒
▒│ │ 4992 ▒
└──────────────┘
┌──────────────┐
▒│ │ 4991 ▒
▒├──────────────┤ ▒
▒│ │ 4990 ▒
▒├──────────────┤ ▒
▒│ │ 4989 ▒
▒├──────────────┤ ▒
▒│ │ 4988 ▒
8 ─── ▒├──────────────┤ ▒ ──── 1,000,000,000,000.00 double
bytes ▒│ │ 4987 ▒
▒├──────────────┤ ▒
▒│ │ 4986 ▒
▒├──────────────┤ ▒
▒│ │ 4985 ▒
▒├──────────────┤ ▒
▒│ │ 4984 ▒
└──────────────┘
Figure 3-3. Storing information in memory.
──────────────────────────────────────────────────────────────────────────
/* varsize.c -- shows amount of memory */
/* by various types */
main()
{
printf("Size of a char in bytes is %d\n", sizeof(char));
printf("Size of an int in bytes is %d\n", sizeof(int));
printf("Size of a long in bytes is %d\n", sizeof(long));
printf("Size of a float in bytes is %d\n", sizeof(float));
printf("Size of a double in bytes is %d\n", sizeof(double));
}
──────────────────────────────────────────────────────────────────────────
Listing 3-3. The VARSIZE.C program.
Here's the output of VARSIZE.C:
Size of a char in bytes is 1
Size of an int in bytes is 2
Size of a long in bytes is 4
Size of a float in bytes is 4
Size of a double in bytes is 8
Declaring Variables
To declare a variable, specify the data type and then the variable name.
Here are some examples:
int account_no;
float balance;
double budget;
char acct_type;
The first statement declares account_no as an integer (int) variable. The
remaining statements declare variables as floating-point decimal (using
the keyword float), "jumbo" 8-byte floating-point (double), and 1-byte
character (char) data types.
When you declare a variable, QuickC sets aside the appropriate number of
bytes and notes the variable's starting address. The next program,
VARADDRS.C (Listing 3-4), declares several types of variables and then
prints out their starting addresses.
──────────────────────────────────────────────────────────────────────────
/* varaddrs.c -- uses & operator to get */
/* addresses of variables */
main()
{
char c1, c2;
int i;
long l;
float f;
double d;
printf("Address of c1 %d\n", &c1);
printf("Address of c2 %d\n", &c2);
printf("Address of i %d\n", &i);
printf("Address of l %d\n", &l);
printf("Address of f %d\n", &f);
printf("Address of d %d\n", &d);
}
──────────────────────────────────────────────────────────────────────────
Listing 3-4. The VARADDRS.C program.
Although the output of this program varies with different system
configurations, it should look something like this:
Address of c1 6146
Address of c2 6144
Address of i 6142
Address of l 6138
Address of f 6134
Address of d 6126
VARADDRS.C obtains the addresses of the variables by using an ampersand
(&) prefix with each variable name. The ampersand is the "address
operator," and it returns the starting address for each variable
specified. Compare the output of VARADDRS.C with Figure 3-3 to see how
variables declared with different data types require different amounts of
memory. When QuickC allocates the required number of bytes for a declared
data type, the last byte allocated (moving downward in memory) is the
variable's starting address. For example, the integer variable i has an
address of 6142, indicating that it uses two bytes (6144 - 6142 = 2); the
double type variable d uses eight bytes (6134 - 6126 = 8). Note that the
compiler allocates two bytes for char values c1 and c2, although each
value requires only one byte. The extra byte is convenient for
manipulating (2-byte) words in memory.
Rules for Naming Variables
In C, the names of variables and functions are called "identifiers." An
identifier can contain any uppercase or lowercase alphabetic characters
(A-Z or a-z), digits (0-9), and the underscore character (_). However, the
name must begin with a letter or underscore. Below are some examples of
legal and illegal names:
bignum─────────────────────────────────────────────────────────────Legal
BigNum───────────────────────────────────Legal, and distinct from bignum
_video───────────────────────────────Legal, can begin with an underscore
bal_due─────────────────────────Legal, underscore used to separate words
player2───────────────────────────────────Legal, number in variable name
8ball─────────────────────────────────Illegal, can't begin with a number
tally-ho!─────────────────Illegal, contains hyphen and exclamation point
int───────────────────Illegal, keyword reserved for name of integer type
As you can see, you have considerable flexibility in choosing names for
your variables. Because QuickC can distinguish the first 31 characters of
a variable name, you can use long, descriptive names that help make the
program easier to understand and modify. (You might want to use shorter
names if your program must run a compiler that does not support long
names.) C distinguishes between uppercase and lowercase characters, so
that BigNum and bignum are different variables. Note that you can't begin
a variable name with a number, use punctuation marks such as ! or $, or
use C-language keywords as variable names. (You can embed a keyword in a
variable name, however: interest is a legal name even though it contains
the keyword int.) Fortunately, C has few keywords compared to languages
such as BASIC: Most specify data types (such as int) or control and
decision-branching operations (such as while and if).
We use specific conventions for naming variables and functions. (See
"Conventions and Style" in Chapter 1.) These are not required by QuickC,
but are used here to differentiate among types of variables and functions.
We also begin our variable names with a character other than an
underscore──Microsoft uses the underscore as the initial character for its
QuickC library functions.
Assignment Statements
How do you assign values to variables? In C, the simplest assignment
statement consists of a variable name followed by an equal sign (=) and
the value to be assigned. Below are some examples:
a = 5;
b = a + 5;
c = a + b;
In these assignment statements, the value to the right of the equal sign
is assigned to the variable on the left. The value can be a number or an
expression involving variables and/or numbers, such as a + 5 or a + b. If
the value is an expression, QuickC determines the result and then assigns
it to the variable.
You can also assign the same value to several variables at once. Usually,
you do this to initialize variables by setting them all to 0 or 1:
line_count = word_count = 0;
line_no = page_no = 1;
Initializing Variables
Many languages (including most versions of BASIC) automatically initialize
numeric variables to 0 and character variables to blank or, perhaps,
"null." C does not. For example, if your program has the following two
lines:
int length;
printf("The length is %d\n", length);
and you do not initialize length, it might produce the following output:
The length is -25480
The default value of a C variable is whatever pattern of bits happens to
be in the memory locations of the variable when the compiler assigns them.
Therefore, if you want to use a variable called total, for example, in a
program that keeps track of some quantity, you should assign that variable
an initial value of 0. You might modify the declaration above as follows:
int length = 0;
Because C is a concise language, it lets you combine the declaration and
assignment of a variable. That is, you can declare the data type, the
variables, and their values in the same statement:
int a = 10, b = 50, c = 100;
Type int
Now that you know how to declare numeric variables and assign values to
them, let's look at the int, or integer, data type more closely. An
integer is a whole number, such as 30, -5, or 93,000,000. In QuickC, an
int variable can hold numbers in the range of -32,768 through 32,767. This
rather odd-looking range is established because the int type uses two
bytes (16 bits) of memory. Two bytes can actually hold a range of 0
through 65,535. But, in the regular int type, the high (leftmost) bit of
the 2-byte combination stores the integer's sign (positive or negative),
leaving only 15 bits for the number.
If your variable will never store negative integers, use the unsigned int
type. Because the sign bit is not used, you can use the full two bytes to
store values from 0 through 65,535.
Now let's look at the INTVARS.C program (Listing 3-5), which declares
three integer variables, assigns values to them, and then prints out
values that describe the World War II German battleship Bismarck.
We declare the variables length and beam as int types because the length
and beam (width) of the ship are less than 32,767 feet. For the
displacement variable (the "weight" of the ship), we use the unsigned int
type because we need a larger number (41,676) than 32,767 (the int limit)
but a smaller number than 65,535 (the unsigned int type limit).
The next three lines assign the values to the variables, and the three
printf() statements print the values out. Notice that the printf()
statements use two arguments within the parentheses: a string, such as
"The battleship Bismarck was %d feet long", followed by a comma and the
variable name whose value is to be printed. The %d in the string is a
printf() "format specifier," and the value of the variable is printed in
its place. (The %d specifier denotes a decimal [base 10] integer. C uses a
variety of specifiers for different types and formats of numbers and
characters. We'll discuss them when we look at printf().) When you run
INTVARS.C, it generates the output that appears below the listing (on the
following page).
──────────────────────────────────────────────────────────────────────────
Quick Tip
ANSI C lets you specify any basic variable type as unsigned. It also lets
you specify signed types. Therefore, although QuickC considers the int
type to be signed by default, the C language doesn't guarantee that all C
compilers do so. To write portable programs, you need to specify all
variables as either signed or unsigned types.
──────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────
/* intvars.c -- declares, defines, and prints */
/* some integer variables */
main()
{
/* declare variables */
int length, beam;
unsigned int displacement;
/* assign values to variables */
length = 824;
beam = 118;
displacement = 41676;
/* print out values */
printf("The battleship Bismarck was %d feet long",
length);
printf(" with a beam of %d feet,\n", beam);
printf("and displaced %u tons.\n", displacement);
}
──────────────────────────────────────────────────────────────────────────
Listing 3-5. The INTVARS.C program.
The battleship Bismarck was 824 feet long with a beam of 118 feet,
and displaced 41676 tons.
Long Integer Type
We've seen that unsigned int variables can hold values to 65,535. But what
if you must use larger numbers? Type long uses four bytes (32 bits) of
memory (1 bit is reserved for the sign), and can store numbers from -2^31
to +2^31, or -2,147,483,648 to 2,147,483,647 in base 10. Once again, if
your variable will contain only positive numbers, you can double the high
end of this range by specifying unsigned long. This lets you assign your
variable a whole number value in the range 0 through 4,294,967,295.
The SCORE.C program (Listing 3-6 on the following page) combines the
declaration and assignment of the int variables home, visitors, inning,
and attendance. Because total_attendance is a different data type, long,
you must declare it in a separate statement. Again, the printf()
statements display the values assigned to the variables and produce the
following output:
The score after 7 innings is
Home team 5, Visitors 2.
The attendance today is 31300.
Attendance this year to date is 1135477.
──────────────────────────────────────────────────────────────────────────
/* score.c -- defines and prints */
/* int and long vars */
main()
{
/* declare some int variables and assign values */
/* to them in the same statement */
int home = 5, visitors = 2, inning = 7, attendance = 31300;
long total_attendance = 1135477; /* long int */
/* print out the values */
printf("The score after %d innings is \n", inning);
printf("Home team %d, Visitors %d.\n\n", home, visitors);
printf("The attendance today is %d.\n", attendance);
printf("Attendance this year to date is %ld.",
total_attendance);
}
──────────────────────────────────────────────────────────────────────────
Listing 3-6. The SCORE.C program.
Floating-Point Types
You should store whole numbers as integers wherever possible──integers use
the least amount of memory and integer arithmetic is fast. However, many
numbers (such as dollars-and-cents amounts) require decimal fractions. In
computers, these types of numbers are stored in "floating-point format."
Consider the number 22.95. This number can be stored by dividing it into
two parts: the digits themselves and an exponent showing the magnitude of
the number in terms of powers of ten. Thus, 22.95 could be represented as
22.95 * 10^0. For uniformity in performing operations, however, C always
expresses the digits with only one digit to the left of the decimal point.
Therefore, the above number is actually stored as 2.295 * 10^1 (the same
as 22.95 * 10^0). C represents this notation with the expression
2.295e+001. The first element, 2.295, is the number's digits (the
"mantissa"), and the e+001 represents "exponent 1," or 10^1.
Type float
The most commonly used floating-point type in C is float. In QuickC, type
float uses three bytes to store digits (the mantissa) and one byte to
store the exponent. Because exponents can be negative (for example,
1.4e-002 = .014), one bit of the exponent byte stores the sign. Converted
into decimal terms, this means you can store a mantissa with seven
significant digits and an exponent ranging from -38 to +38. In fact, with
QuickC's float type, you can store numbers as large as 3.4e+038, or 34
with 37 zeros after it.
The FLOATS.C program (Listing 3-7) displays three float values, each
printed in both traditional decimal and exponential notation.
──────────────────────────────────────────────────────────────────────────
/* floats.c -- shows floating values in regular */
/* and exponential format */
main()
{
float f1 = 2500.125, f2 = 0.0033, f3 = -50.99;
printf("%f\t %e\n\n", f1, f1);
printf("%f\t %e\n\n", f2, f2);
printf("%f\t %e\n", f3, f3);
}
──────────────────────────────────────────────────────────────────────────
Listing 3-7. The FLOATS.C program.
The following is the output of the FLOATS.C program:
2500.125000 2.500125e+003
0.003300 3.300000e-003
-50.990002 -5.099000e+001
Notice that because 0.0033 is less than 1, it has a negative exponent
(represented by the minus sign after the e). -50.99, on the other hand, is
a negative number, but because its absolute (unsigned) magnitude is
greater than 1, it has a positive exponent. FLOATS.C prints each variable
first in decimal notation and then in exponential notation by varying the
format specifier in the printf() statement: The %f produces traditional
decimal format, and the %e produces exponential format.
Type double
For numbers larger than
340,000,000,000,000,000,000,000,000,000,000,000,000
QuickC provides a "jumbo" floating-point type called double (double
float). It uses eight bytes of storage and has a range of (plus or minus)
1.7e-308 to 1.7e+308. That's 308 decimal places before or after the
decimal point, thus accommodating even the most expansive physicist or
astronomer.
──────────────────────────────────────────────────────────────────────────
Type Variations on Different Machines
The C language doesn't define the number of bytes used by the int and
unsigned int types. Instead, the number of bytes is based on the size of
number a particular processor can handle in a single operation. This way,
C compilers can always take advantage of a machine's architecture. Because
the IBM PC uses the Intel 8086, 8088, or 80286 processor, an int uses two
bytes, or 16 bits, and this is the implementation QuickC uses. However, on
larger personal computers, such as those using the Intel 80386 processor,
and on many minicomputers and mainframes, an int uses four bytes, or 32
bits. Even if you write your program in "standard" C, you must be aware of
these differences in implementation and machine architecture when you
"port" the program to another machine.
──────────────────────────────────────────────────────────────────────────
Precision for Floating-Point Numbers
You must consider more than size, however, when storing numbers in a
computer. We referred to a trillion-dollar budget ($1,000,000,000,000.00)
earlier in the chapter. If size were the only consideration, we could use
float to store this number. (A float can handle about 10^38, and a
trillion is merely 10^12.)
However, you also must consider the precision available to each data type
in order to choose the right type for a given variable. Precision refers
to the number of digits guaranteed to be exactly correct after a
calculation. The float type has a precision of seven digits. Consider the
following statements and the resulting output:
float trillion = 1000000000000.00;
printf("%f\n", trillion);
999999995904.000000
We lost $4,096.00 in this operation. Although we might be happy if the
government lost only that much of a trillion-dollar budget, we must expect
full precision in financial calculations and probably an even higher
precision in most scientific calculations. With its seven-digit precision,
float can't accurately represent a trillion dollars. We attain the
required precision by declaring:
double trillion = 1000000000000.00;
Because double has 15-digit precision, the result is completely accurate.
Type char
Let's look at one last data type, char (character). Characters include the
uppercase and lowercase letters, numerals, punctuation marks, and
nonprinting control characters. Characters on most computers, including
the IBM PC, are represented by numbers between 0 and 127, according to the
ASCII code. The CHARS.C program (Listing 3-8) shows some examples.
Running CHARS.C produces the following output:
The character A has ASCII code 65
If you add ten, you get K
The character a has ASCII code 97
The first line of the main() function declares two char type variables,
ch1 and ch2, and assigns them the values of `A' and `a' respectively. The
`A' and `a' are called "character constants" or "character literals," and
you assign them to char variables the same way you assign numeric
constants. (Note that you must use single quotes around the character
constant.)
Consider the first printf() statement in the program:
printf("The character %c has ASCII code %d\n", ch1, ch1);
The variable ch1 is specified twice at the end of the argument list. The
first format specifier, %c, prints the value of ch1 as a character. Then
the %d specifier prints ch1 as an integer. A character is actually stored
as a 1-byte version of int, and unless you specify that QuickC treat it as
a character, it is treated as an integer. This enables us to use the
expression ch1 + 10 in the second printf() statement. The variable ch1
contains an integer value (the ASCII code for `A', or 65), so adding 10 to
it produces 75. When the %c specifier then prints this value, it displays
the character with the ASCII value of 75, or `K'.
──────────────────────────────────────────────────────────────────────────
/* chars.c -- shows some variables of type char */
/* as both characters and integers */
main()
{
char ch1 = 'A', ch2 = 'a';
printf("The character %c has ASCII code %d\n", ch1, ch1);
printf("If you add ten, you get %c\n", ch1 + 10);
printf("The character %c has ASCII code %d\n", ch2, ch2);
}
──────────────────────────────────────────────────────────────────────────
Listing 3-8. The CHARS.C program.
Type unsigned char
A char value is a signed, 1-byte value that stores values in the range of
-128 to +127. However, the IBM PC's version of ASCII uses the values 0 to
255 as character codes. The first half of extended ASCII contains the
regular ASCII character set. Codes from 128 to 255, however, consist of
special characters and graphics shapes, which together are called the
"extended character set." You can use the extended character set by
declaring variables as the unsigned char type. For example:
unsigned char box = 178;
printf("%c\n", box);
displays a rectangular box, or extended ASCII character number 178. (Note
that two QuickC general help screens show the complete extended ASCII
character set.)
Using typedef
C lets you rename any data type with the typedef statement. For example,
if you use unsigned char type variables to hold characters from the full
256-character extended set, you could define an easily remembered
mnemonic:
typedef xchar unsigned char;
xchar highlight_char, border_char;
The typedef statement tells QuickC that the word xchar now represents
unsigned char. Next, we declare two variables as type xchar. Note that you
can still declare variables as unsigned char at any time. Also note that
typedef does not create new data types, it merely provides synonyms for
existing ones.
The HARDWARE.C program (Listing 3-9) ends our survey of QuickC data
types.
──────────────────────────────────────────────────────────────────────────
/* hardware.c -- shows a mixture of int, */
/* float, and char types */
main()
{
int threads = 8; /* threads per inch */
float length = 1.25, /* length in inches */
diameter = 0.425, /* diameter in inches */
price = 0.89; /* price per hundred */
char bin = 'A'; /* kept in bin A */
long quantity = 42300; /* number in bin */
printf("Screws: %d threads/inch\n %f inches long\n",
threads, length);
printf ("%f diameter\n\n", diameter);
printf("Price per 100: %f\n", price);
printf("Stored in bin: %c\n Quantity on hand: %ld",
bin, quantity);
}
──────────────────────────────────────────────────────────────────────────
Listing 3-9. The HARDWARE.C program.
Be sure you understand why we declared the different types. The printf()
statements display the values of the variables and some descriptive text:
Screws: eight threads/inch
1.250000 inches long
0.425000 diameter
Price per 100: 0.890000
Stored in bin: A
Quantity on hand: 42300
Although the program works correctly, it would look better if the output
were formatted more neatly. Also, QuickC printed several extra decimal
places and filled them with zeros. To gain more control over the
appearance of program output, we need to study printf() in more detail.
Summary of Data Types
You don't need to memorize the precise numbers associated with each data
type; one of QuickC's help screens lets you check which data type you
should use in a given situation. Display this summary of QuickC data types
by pressing the F1 key and then proceeding to the appropriate screen. As
you work with various data types in this chapter, you can always consult
this chart, shown in Figure 3-4, to refresh your memory.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 3-4 can be found on p.67 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 3-4. Data types help screen.
The Power of printf()
Thus far we've used printf() statements merely to display values. But
printf() is actually quite versatile for formatting numbers and character
strings.
Using Escape Sequences
Let's look at the parts of the printf() statement from our first program,
QCHELLO.C:
printf ("Hello, and welcome to QuickC!\n");
This is the simplest printf() statement: It merely prints out a string; no
variables are involved. Earlier, we briefly discussed the one unusual
feature of this printf() statement, the \n at the end of the string. This
combination of backslash and following character is called an "escape
sequence." Escape sequences tell printf() to print special characters as
part of the given string. The \n, for example, adds the newline character,
which moves the cursor or printer head to the beginning of the next line.
Many languages use two kinds of statements for printing: one to print some
information, and one to print some information and then start a new line.
With typical conciseness and versatility, C lets you use one function to
print any ASCII character, including newline, Tab, and carriage-return
characters, giving you complete control of the position of the cursor or
printer head.
One QuickC help screen, shown in Figure 3-5 on the following page, lists
all of the escape sequences. The newline \n and tab \t sequences are the
most frequently used. The \a escape sequence causes an "alert," or beep,
at the terminal.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 3-5 can be found on p.68 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 3-5. Character escape sequences.
The ONELINE.C program (Listing 3-10) shows what happens when you don't
use the newline escape sequence. When you run the program, the output is
all on one line as follows:
All displayed onthe same line, with no space unless specified.
Not only do the strings from all three printf() statements end up on the
same line, the word "on" at the end of the first string runs into the word
"the" at the start of the second string. To print two strings on the same
line with a space between them, you must include the space in the string.
In the third string of ONELINE.C, we added a space before the word
"unless."
──────────────────────────────────────────────────────────────────────────
/* oneline.c -- shows how printf() continues */
/* on the same line */
main ()
{
printf("All displayed on");
printf("the same line, with no space");
printf(" unless specified.");
/* note added space in line above */
}
──────────────────────────────────────────────────────────────────────────
Listing 3-10. The ONELINE.C program.
The program STRINGS.C (Listing 3-11) demonstrates the two basic ways to
print strings with printf().
The first printf() statement has only one argument, the string to be
printed, and the newline escape sequence. The second statement has two
arguments, the format specifier %s (for "string") and the string to be
printed. It replaces the specifier with the string and prints it. This is
the same procedure we used to print numeric variables and literals with
specifiers such as %d. The STRINGS.C program produces the following
output:
This uses a string literal by itself
This plugs the literal into %s
TABS.C (Listing 3-12) illustrates the use of the tab escape sequence \t.
──────────────────────────────────────────────────────────────────────────
/* strings.c -- shows two ways to print */
/* a string with printf() */
main()
{
printf("This uses a string literal by itself\n");
printf("%s", "This plugs the literal into %s\n");
}
──────────────────────────────────────────────────────────────────────────
Listing 3-11. The STRINGS.C program.
──────────────────────────────────────────────────────────────────────────
/* tabs.c -- shows formatting with the \t */
/* tab escape sequence */
main()
{
int q1 = 338, q2 = 57, q3 = 1048, q4 = 778,
/* quantity in bin */
t1 = 6, t2 = 8, t3 = 12, t4 = 16;
/* threads per inch */
float s1 = 0.250, s2 = 0.500, s3 = 0.750, s4 = 1.0;
/* size in inches */
/* print table header */
printf("number\t\t size\t\t threads\n");
printf("in bin\t\t (inches)\t per inch\n\n");
/* print lines of table */
printf("%d\t\t %f\t %d\n", q1, s1, t1);
printf("%d\t\t %f\t %d\n", q2, s2, t2);
printf("%d\t\t %f\t %d\n", q3, s3, t3);
printf("%d\t\t %f\t %d\n", q4, s4, t4);
}
──────────────────────────────────────────────────────────────────────────
Listing 3-12. The TABS.C program.
This program prints four sets of data in a neat table. The program prints
the table headers first, using \t to tab to the next field. Using \t to
position each item at the next tab stop causes the output to be left-
justified in each field. To make the table easier to read, we added a
blank line between the header and the data by including an extra \n in the
second printf() statement. The program then prints the values of the
variables in the same tab fields as the headers. The result of all this is
as follows:
number size threads
in bin (inches) per inch
338 0.250000 6
57 0.500000 8
1048 0.750000 12
778 1.000000 16
Formatting Numbers with printf()
The printf() function can also print numbers in a variety of formats.
Let's look at a printf() statement from SCORE.C, which is analyzed in
Figure 3-6.
String to print is
enclosed in quotes ┌─────────────────────────────┐
│ │ │
│ ┌─▼──┐ ┌────┐ ┌───▼────┐
printf ("The score after │ %d │ innings is │ \n │ ", │ inning │ );
└────┘ └────┘ └────────┘
│ │ │
│ │ │
Format specifier Newline Variable whose
for an int value escape value is to be
sequence printed
Figure 3-6. The printf() statement from SCORE.C.
──────────────────────────────────────────────────────────────────────────
Return and Newline Are Different
If you program in other languages on MS-DOS machines, you might expect \r
(carriage return) to move the cursor to the start of a new line. Change
the \n in TABS.C to \r and run the program again. What happens? Each line
prints over the preceding one. Although many languages on MS-DOS machines
incorporate a line feed in a carriage return, C treats newline and return
as distinct operations. Return moves the cursor to the beginning of the
current line but does not advance it to a new line. Newline causes output
to start on the next line. It commences with output at the beginning of
the next line (rather than directly below the old position) because MS-DOS
interprets it as though it contains a carriage return as well.
──────────────────────────────────────────────────────────────────────────
Notice the %d in our example, SCORE.C. This, as we have already mentioned,
is the format specifier for a decimal integer. The string "The score after
%d innings is" is followed by a comma and the variable inning. Thus, when
the printf() statement executes, the string is printed with the value of
inning. You can also print more than one value in the same string. For
example, if you define int apples = 12, oranges = 9, pears = 3;, then
execute the following printf() statement:
printf("I have %d apples, %d oranges, and %d pears. \n", apples, oranges,
you see the following output:
I have 12 apples, 9 oranges, and 3 pears.
Specifying Formats with printf()
Variables are printed according to their type and the format specifiers
used. One of the QuickC General help screens (Figure 3-7) shows format
specifiers and additional symbols that can specify formats.
The program SPECS.C (Listing 3-13 on the following page) prints different
types of variables with their appropriate specifiers.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 3-7 can be found on p.71 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 3-7. Format specifiers.
──────────────────────────────────────────────────────────────────────────
/* specs.c -- shows printf()format */
/* specifiers for numbers */
main()
{
int i = 122; /* ASCII code for 'z' */
long l = 93000000; /* distance to Sun (miles) */
float f = 192450.88; /* someone's bottom line */
double d = 2.0e+030; /* mass of Sun (kg.) */
printf("%d\n", i); /* integer as decimal */
printf("%x\n", i); /* integer as hex */
printf("%ld\n", l); /* long */
printf("%f\n", f); /* float as decimal */
printf("%e\n", f); /* float as exponential */
printf("%f\n", d); /* double as decimal */
printf("%e\n", d); /* double as exponential */
}
──────────────────────────────────────────────────────────────────────────
Listing 3-13. The SPECS.C program.
Compare the following output with the printf() statements in the SPECS.C
program:
122
7a
93000000
192450.875000
1.924509e+005
2000000000000000000000000000000.000000
2.000000e+030
The first printf() statement prints the value of the int variable i, 122,
as an ordinary decimal integer, using the now familiar %d specifier. The
next statement prints the same value with the %x specifier, which prints
values in hexadecimal. Next, we print the long integer value 93000000.
Notice that this specifier, %ld, combines the %l (long) and %d (integer)
specifiers.
The SPECS.C program then prints the value of the variable f, 192450.88,
using the %f floating-point specifier. In the next statement, we use %e to
print the same number in exponential notation. Which is better? If the
value represents money, the regular decimal format is more appropriate,
but remember that both representations are slightly inaccurate because the
original value, 192450.88, has eight places and float has a maximum
precision of seven places. (If you want absolute accuracy, use the double
specifier.)
We print the final value, 2,000,000,000,000,000,000,000,000,000,000, two
ways: as a double (note that you can use %f for double as well as for
float) and as exponential notation with %e. Clearly, the latter is easier
to read and understand.
Format Specifiers and Data Types
Remember, the format specifier merely controls how a value is displayed.
The data type of the value represents how it is actually stored in the
computer. The program FORMATS.C (Listing 3-14) displays the comedy of
errors that can occur if you carelessly use the wrong format specifier
with a data type. The following is the output of the program; compare it
with the printf() statements in the program.
As integer: 5
As long integer: 8519685
As exponential: 7.084198e-309
As float: 0.000000
The program uses four different specifiers to print the value of the int
variable i, which we set to 5. Only the first representation, using %d, is
correct. The other results vary widely (even from one run to another). How
can the last three methods be so far off the mark? Consider the second
printf() statement, in which we told QuickC to print the value of i as a
long integer %ld. A long integer uses four bytes of memory, but this
variable, as an int type, uses only two. When you specify a long integer,
QuickC takes four bytes starting at the address of i and converts them
into a long integer. Two of these bytes, however, have nothing to do with
the variable i. You can see how similar problems arise when we try to
interpret an integer variable as a float. All of this demonstrates that
the format specifier must be compatible with the data type being handled.
Table 3-1 correlates the most commonly encountered specifiers and data
types.
──────────────────────────────────────────────────────────────────────────
/* formats.c -- shows what happens when format */
/* doesn't match data type */
main()
{
int i = 5;
printf("As integer: %d\n", i);
printf("As long integer: %ld\n", i);
printf("As exponential: %e\n", i);
printf("As float: %f\n", i);
}
──────────────────────────────────────────────────────────────────────────
Listing 3-14. The FORMATS.C program.
Table 3-1. Compatibility of Specifiers and Data Types
Specifier Types
──────────────────────────────────────────────────────────────────────────
%d int (signed or unsigned); char (ASCII value)
%ld long
%f float or double (decimal format)
%e float or double (exponential format)
%c char (as character)
──────────────────────────────────────────────────────────────────────────
Field Width Specifiers
We can also improve the appearance of printf() output by controlling how
many decimal places are printed and how the number is aligned in the
output field. To do this, C lets us precede the format specifier with a
"field specifier." The field specifier takes the following form:
<field width>.<decimal places>
The "field width" is the total number of character positions that will be
printed, and "decimal places" is the number of places printed after the
decimal point. (Use the decimal place specifier only for float and double
values.) Following are two examples of field specifiers:
"5.2f"────────────────────float; 5 places, 2 of which are decimal places
"8d"───────────────────────────────integer; 8 places (no decimal places)
The program FIELDS.C (Listing 3-15) shows how field width specifiers
work.
The program prints a single variable with varying field specifiers:
123.456001────────────────────────────────────12.6f (field specifier)
123.4560────────────────────────────────────────────────────────────8.4f
123.456────────────────────────────────────────────────────────────8.3f
123.46────────────────────────────────────────────────────────────8.2f
In the first printf() statement, the field specifier %12.6f sets up a
12-character-wide field, 6 characters of which are decimal places. Because
the variable has only 10 characters to be printed (9 digits and a decimal
point), printf() indents the number two spaces. By default, numbers are
right-justified (printed starting in the rightmost position of the
specified field width). To print numbers that start at the left side of
the field (left-justified), put a minus sign in front of the field width
specifier, "%-4.2f".
──────────────────────────────────────────────────────────────────────────
/* fields.c -- shows the same number with different */
/* field widths and number of decimals */
main()
{
float f = 123.4560;
printf("%12.6f\n", f);
printf("%8.4f\n", f);
printf("%8.3f\n", f);
printf("%8.2f\n", f);
}
──────────────────────────────────────────────────────────────────────────
Listing 3-15. The FIELDS.C program.
Note also in the first printf() statement that we asked for six decimal
places, even though the variable number contained only the first four
places. Although printf() prints these extra places, they add nothing to
the precision of the number, and, in fact, give a misleading impression of
precision. You should specify decimal places only to the expected
precision of the value. For example, if you know that a value will range
between 0 and 9999 with decimal places, you might specify %4.3f because
the value can have as many as four places to the left of the decimal
point, and a float has only seven places of precision. Thus, a total of
seven places (4.3) displays an accurate value. Specifying %8.6 for this
example would give a false impression of precision.
In the second statement, the specifier establishes a field width of 8
(with 4 decimal places). The third statement specifies the same field
width of 8, but with only 3 decimal places. Notice that the variable's
fourth decimal place, the zero, is dropped, and that the number is
indented one space because the variable has only 7 characters. The last
statement specifies the same field width of 8, but with only 2 decimal
places. The printf() function not only drops the third decimal place, it
also rounds up the second decimal place to 6. Also, because the number has
one fewer digit to fit in the 8-character field, printf() indents the
number another space.
Arithmetic Operators
Like most languages, C offers a complete set of arithmetic operators: +
(addition), - (subtraction), * (multiplication), and / (division). C also
provides a fifth operator that is not quite as common in other languages──
%, the remainder operator, sometimes called the "modulus" operator. This
operator returns only the remainder of a division operation. For example,
5 % 2 is 1 (5 divided by 2 has a remainder of 1), and 9 % 3 is 0 (9
divided by 3 has no remainder).
The modulus operator has many uses: You can use it for creating counters
that cycle within counters or for resetting variables such as line counts
by checking for a remainder of zero (if line_cnt % page_length = 0, then
you know that you must start a new page).
Operators are used with values to form expressions that yield new values.
Below are some examples:
10 * 5─────────────────────────────────────────────Multiply two literals
a / 5─────────────────────────────────────────────Divide value of a by 5
count + 1────────────────────────────────────────Add 1 to value of count
(a * 80) + b──────────────Multiply value of a by 80, then add value of b
In a program, you combine expressions with other elements to form
statements. The MATH.C program (Listing 3-16 on the following page)
contains statements that use expressions involving arithmetic operators.
──────────────────────────────────────────────────────────────────────────
/* math.c -- shows arithmetic and */
/* precedence via expressions */
main()
{
int a = 10, b = 4, c = 2;
/* simple arithmetic expressions */
printf("99 + 2 = %d\n", 99 + 2); /* ints */
printf("5 - 12 = %d\n", 5 - 12);
printf("7.25 + 3.5 = %f\n", 7.25 + 3.5);
/* floats */
/* compare precedence on these */
printf("20 * 20 + 40 = %d\n", 20 * 20 + 40);
printf("20 * (20 + 40) = %d\n", 20 * (20 + 40));
printf("a * a - c + b = %d\n", a * a - c + b);
printf("a * (a - (c + b)) = %d\n",
a * (a - (c + b)));
/* compare integer and float division */
printf("Integers: 5 / 2 = %d\n", 5 / 2);
printf("Floats: 5.0 / 2.0 = %f\n", 5.0 / 2.0);
}
──────────────────────────────────────────────────────────────────────────
Listing 3-16. The MATH.C program.
Each printf() statement prints the expression and then its value, as
follows:
99 + 2 = 101
5 - 12 = -7
7.25 + 3.5 = 10.750000
20 * 20 + 40 = 440
20 * (20 + 40) = 1200
a * a - c + b = 102
a * (a - (c + b)) = 40
Integers: 5 / 2 = 2
Floats: 5.0 / 2.0 = 2.500000
The first three statements simply add and subtract literal numbers and
print the results. Notice in the third statement that when QuickC sees a
number with a decimal point, it assumes a float type and prints the answer
accordingly (10.750000).
Operator Precedence
The next set of statements in MATH.C illustrates "precedence," or the
rules that determine the order in which operators are applied. Generally,
QuickC performs multiplication and division first, then addition and
subtraction. When operators have equal precedence (such as division and
multiplication), QuickC performs them from left to right. The following
QuickC help screen, Figure 3-8, lists all the operators in the language
(including many covered in later chapters) and arranges them in groups
from highest to lowest precedence.
Thus, in Listing 3-16 the first printf() statement in the second group of
statements multiplies 20 by 20, then adds 40, resulting in 440. However,
you can use parentheses to impose a different order of precedence, as
shown in the next statement. To evaluate the expression 20 * (20 + 40),
QuickC performs the addition first (resulting in 60) and then multiplies
20 by 60 to produce a value of 1200.
The next two statements use combinations of variables. As an exercise,
perform the calculations on paper before you run the program. Remember to
observe the rules of precedence. Did your answers agree with QuickC's?
The final two statements in MATH.C illustrate a common problem for the
unwary beginning C programmer. QuickC divides integer and floating-point
types differently. When you specify numbers as integers, as in the first
statement, integer division is performed. Accordingly, 5 divided by 2 is 2
because this type of division discards any remainder. (A remainder in
division is a fraction, and int types cannot represent fractions.)
However, when you specify numbers with decimal points, QuickC treats them
as float types, resulting in the expected answer of 2.5. Variables of int
and float types are handled the same way as the literals above.
The RECEIPTS.C program (Listing 3-17 on the following page) performs
practical calculations with QuickC's math operators. Notice that we
declare the units variable as an int type (you can't sell half a unit!)
and the price and tax rates as float types.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 3-8 can be found on p.77 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 3-8. Operator precedence help screen.
──────────────────────────────────────────────────────────────────────────
/* receipts.c -- calculates gross and net */
/* receipts on sales */
main()
{
int units = 38; /* number sold */
float price = 149.99, /* price per item */
rate = 0.06; /* sales tax rate */
/* variables to hold calculated totals */
float gross, tax, net;
/* perform calculations */
net = units * price;
tax = net * rate;
gross = net + tax;
/* print results */
printf("\tSales Report\n");
printf("Net sales: \t%6.2f\n", net);
printf("Tax:\t\t %5.2f\n", tax);
printf("Gross sales:\t%6.2f\n", gross);
}
──────────────────────────────────────────────────────────────────────────
Listing 3-17. The RECEIPTS.C program.
The "calculations" section uses expressions to generate values for the
variables net, tax, and gross. The printf() statements combine tab escape
sequences \t and field width specifiers to align the output. Specify only
two decimal places for money amounts. (Makes cents, doesn't it?) The
program produces the following report:
Sales Report
Net sales: 5699.62
Tax: 341.98
Gross sales: 6041.60
Arithmetic with Mixed Types
The accuracy of a number generated by QuickC depends on its data type and
the format in which it is printed. An additional problem arises when you
perform arithmetic operations on literals (constants) or variables of
different data types: For example, what happens when you divide an int by
a float?
For calculations with mixed data types, C ranks data types roughly
according to the number of bytes of storage they require. From highest to
lowest, they are:
double 8 bytes
float 4 bytes
long 4 bytes
int 2 bytes
char 1 byte
Generally, QuickC converts the lower-ranking type to the higher-ranking
one before it performs the calculation. Thus, when QuickC divides 49 by
12.5, it first converts 49 to 49.0 (a float), then performs the division.
(If QuickC chose a lower-ranking type, the calculation would lose
precision. The above calculation, for example, would be 49 / 12 = 4 in
integer division.) Although the long and float types both use four bytes,
a float can contain a fractional part that would be lost when converted
"down" to a long: Therefore, float is ranked as the "higher" type.
Finally, QuickC converts float types to double types before it calculates
the result.
Although it's convenient that QuickC performs conversions for you, some
real problems can occur if you assign the results of a calculation to a
variable of an incorrect data type. The following example illustrates such
a mistake:
int sales, units = 50;
float price = 1.99;
sales = units * price;
printf("Total sales are %d\n", sales);
QuickC calculates price * units correctly by converting units from 50 to
50.0 (to make it a float), and then multiplying it by the float value of
price (1.99). The value of the expression is now the float value of 99.50.
So far, so good. However, we assigned this value to the variable sales,
which we declared as an int type. As a result, the fractional part of the
value (.50) is dropped, making the value of sales an incorrect 99.00. The
solution to this problem is simple──consider all of the potential values
for a variable before you declare it. In this case, you need to declare
the variable sales as a float.
QuickC can help remind you of potential problems with data type
conversions. When you select the Compile option from the Run menu, the
left side of the dialog box lists four levels of compiler warning messages
(levels 0 through 3). If you select level 2 before you compile programs,
QuickC sends a warning message for each program statement that causes a
data type conversion. A typical message follows:
warning C4051: (1 of 4)
data conversion
When you see this type of message, note the statement the cursor is on,
examine the data types involved, and look up the meaning of warning (4051)
in the Microsoft QuickC Programmer's Guide. There you will note that this
is an advisory message, and QuickC issues it for perfectly legitimate
conversions, such as the int to float conversion in our earlier division
example.
The MIXED.C program (Listing 3-18 on the following page) shows more
examples of operations with mixed data types.
──────────────────────────────────────────────────────────────────────────
/* mixed.c -- shows the effects of mixing */
/* data types in an expression */
main()
{
int i = 50, iresult;
long l = 1000000, lresult;
float f = 10.5, fresult;
double d = 1000.005, dresult;
fresult = i + f; /* int + float to float */
printf("%f\n", fresult);
fresult = i * f; /* int * float to float */
printf("%f\n", fresult);
lresult = l + i; /* long + int to long */
printf("%ld\n", lresult);
printf("%f\n", d * f); /* double * float */
/* to double */
fresult = d * f; /* assigned to a float */
printf("%f\n", fresult); /* loses some precision */
/* debugging a division problem */
iresult = i / l; /* int / long to int*/
printf("%d\n", iresult); /* whoops! loses result */
printf("%ld\n", iresult); /* this won't fix it */
fresult = i / l; /* store in float result */
printf("%f\n", fresult); /* doesn't work */
dresult = i / l; /* try a double */
printf("%f\n", dresult); /* doesn't work */
fresult = (float) i / l; /* try type cast */
printf("%f\n", fresult); /* correct result */
}
──────────────────────────────────────────────────────────────────────────
Listing 3-18. The MIXED.C program.
Compare this output to the program statements:
60.500000
525.000000
1000050
10500.052500
10500.052734
0
8519680
0.000000
0.000000
0.000050
The first set of statements adds an int and a float value and prints the
result, which is 60.5, a float value. This shows QuickC's default type
conversion at work. The second set of statements shows the same conversion
with a multiplication operation. The third pair of statements adds a long
to an int. Note that the result is correct (100,000 + 50 = 100,050), and,
from its size, you can guess that it must be a long. QuickC converts the
value 50 to a long before it does the calculation.
Next, the program works with double and float types. When we specify d * f
in the printf() statement, QuickC converts the float value f to a double
and calculates a double result, which we print. (Remember, you can use the
%f format specifier with either float or double types.) Because the answer
requires nine places of precision, converting from float to double
preserves the accuracy of the value.
Next, we perform the same calculation, but we assign the result to a float
value, f. Notice that the result, 10,500.052734, becomes inaccurate
starting at the fourth decimal place. Converting from double to float can
produce both large and subtle errors, depending on the numbers involved.
To be safe, use a double variable to hold the result of this type of
calculation.
The last lengthy set of statements illustrates various approaches for
dividing an int value i by a long value l. Only the last method produces
the correct result.
Assigning the result of the division to an int variable returns a 0,
because the result is a very small decimal fraction (50 / 1,000,000), and
integer division does not recognize remainders.
In the next statement we assume that the result of the division is a
decimal fraction and that we can store it in a float. But this doesn't
work either. When we add more decimal places by using a double variable
for the result, we still get a result of 0. The problem here is that when
the two integer variables i and l are divided, the integer portion of the
result, 0.000050, is 0. At this point, we can't retrieve the decimal
fraction. Assigning it to a float or a double merely gives you a
floating-point representation of 0!
Type Casting
C provides a solution to our division dilemma with a construction called a
"type cast." A type cast explicitly converts a value to a specified type
before any operations are done on that value. Consider the following
example:
int i1 = 10, i2 = 3;
printf("%d\n", i1 / i2);
printf("%f\n", (float) i1 / i2);
In the first printf() statement, we divide the two integers and produce
the integer result of 3. In the second printf() statement, we add (float)
before i1. This is the type cast: It converts the value of i1 to a float.
Because a type cast has a higher precedence than the arithmetic operators,
it converts i1 to a float before the division operation. Now the division
operation contains a float and an int! QuickC's default type conversion
then converts i2 to a float as well, and the result is the float value
3.33333. If you look at the last two statements of the MIXED.C program,
you can see we used a type cast in the same way, with the correct result
of 0.000050.
Type casts are useful for handling variables of lower-ranking data types
(int, for example) that must occasionally be used in calculations to
produce a result of a "higher" type (such as float). It is more efficient
in terms of both storage and processing time to declare such variables as
the lower type and to use type casts when necessary. Later, you will find
type casts valuable when you must convert values to a specific type.
Getting Input with scanf()
In order to write programs that have real-world utility, we must first
understand how a C program gets input from the user. The all-purpose C
function for getting input and storing it in a variable is called scanf().
(Like printf(), scanf() is a "built-in" QuickC core library function.)
Figure 3-9 shows how it works.
Let's assume we have a program with a declared integer variable named
acct_no. When the scanf() statement executes, the program waits for input
from the user. After the user types the number and a carriage return, the
input is stored in the variable acct_no, as if it had been assigned by an
assignment statement. Notice that the acct_no variable in the scanf()
argument list is preceded by an ampersand (&). Do you remember when we
placed ampersands in front of variable names in the VARADDRS.C program
(Listing 3-4 on p. 57) to retrieve the storage addresses of the
variables? The scanf() function requires as its second argument an address
at which it can store the input. The & returns the address of the
following variable. If you omit the address operator from the front of the
variable name, the value of the variable is interpreted as though it were
an address, and the input is stored at that address. This can produce
frightful results if it overwrites information that your program needs!
Format specifier for
type of value wanted
│
┌┴┐
scanf ("%d", &acct_no);
│└────┬─┘
│ │
"address of" Variable name
└─────────────┬──────────────┘
│
Variable to store input in
Figure 3-9. Parts of a scanf() statement.
The first argument in the scanf() statement in Figure 3-9 is "%d". This
looks and works like the format specifiers we used with printf()──it
specifies the type of the value that the program expects. As with
printf(), the "%d" specifies an integer. You can also use most of the
other specifiers you used with printf(). For example:
scanf("%f", &deposit);
gets a value for the float variable deposit.
Notice that scanf(), by itself, does not print a prompt for the user; it
merely presents the user with a blinking cursor. Therefore, you should
precede a scanf() statement with a printf() statement that tells the user
what information to supply. In the example above, we might precede the
scanf() statement with:
printf("How much is your deposit?");
The cursor now appears following a space after the prompt. You don't need
to include a newline character: The cursor will move to the next line when
the user presses Enter after typing the input.
You can also use scanf() to get values for more than one variable at a
time:
printf("What is your age and weight?");
scanf("%d %d", &age &weight);
In this example, the user types an age, a space (to separate the values),
and then a weight. Note that the user can substitute an Enter or a Tab for
the space.
The CONVERT.C program (Listing 3-19) uses scanf() to prompt a user for a
temperature in Fahrenheit and then converts the temperature to Centigrade.
──────────────────────────────────────────────────────────────────────────
/* convert.c -- converts Fahrenheit temperature */
/* to Centigrade; gets value from user */
main()
{
float ftemp, ctemp;
printf("What is the temperature in Fahrenheit? ");
scanf("%f", &ftemp);
ctemp = (ftemp - 32.0) * 5 / 9.0;
printf("The temperature in Centigrade is %5.2f", ctemp);
}
──────────────────────────────────────────────────────────────────────────
Listing 3-19. The CONVERT.C program.
A sample user dialog with CONVERT.C follows:
What is the temperature in Fahrenheit? 87
The temperature in Centigrade is 30.56
We print the prompt with a printf() statement, then use a scanf()
statement with a floating-point specifier %f to get the input value for
the float variable ftemp.
The AVGTEMP.C program (Listing 3-20) averages the daily high temperatures
for a week. When you run the program, it prompts for the high temperature
for each day of the week, beginning with Monday.
──────────────────────────────────────────────────────────────────────────
/* avgtemp.c -- finds average temperature */
/* for the week */
main()
{
int t1, t2, t3, t4, t5, t6, t7;
float avg;
printf("Enter the high temperature for:\n");
printf("Monday: ");
scanf("%d", &t1);
printf("Tuesday: ");
scanf("%d", &t2);
printf("Wednesday: ");
scanf("%d", &t3);
printf("Thursday: ");
scanf("%d", &t4);
printf("Friday: ");
scanf("%d", &t5);
printf("Saturday: ");
scanf("%d", &t6);
printf("Sunday: ");
scanf("%d", &t7);
/* calculate and display average */
avg = (t1 + t2 + t3 + t4 + t5 + t6 + t7) / 7.0;
/* divide by 7.0 to ensure float result */
printf("The average high temperature for");
printf(" this week was %5.2f degrees.\n", avg);
}
──────────────────────────────────────────────────────────────────────────
Listing 3-20. The AVGTEMP.C program.
The int variables t1 through t7 store the daily high temperatures, which
are obtained by a series of scanf() statements. The program then
calculates an average and prints it out. A sample dialog with this program
might look as follows:
Enter the high temperature for:
Monday: 82
Tuesday: 91
Wednesday: 97
Thursday: 104
Friday: 95
Saturday: 88
Sunday: 78
The average high temperature for this week was 90.71 degrees.
Note: It is important to note that scanf() does not check to make certain
that the input is compatible with the data type of the variable in which
it is stored.
Shortcut Assignments, Increments, and Decrements
Now that you know how to assign a value to a variable with the assignment
operator = and how to use arithmetic operators to calculate new values, we
can show you a few tricks and shortcuts. In the course of a program, it is
often useful to add a value to a variable repeatedly or to subtract a
value from a variable repeatedly. For example, a program that counts lines
needs to add one to a variable (such as total_lines) each time it counts a
new line. We could do this as follows:
total_lines = total_lines + 1;
That's the way most languages do it. However, because changing the value
of a variable is such a common occurrence in programming, C provides
special, concise "arithmetic assignment operators" for the purpose.
Arithmetic Assignment Operators
The arithmetic operators are the +, -, *, /, and %, and the assignment
operator is the =. The arithmetic assignment operator, as the name
suggests, is a combination of an arithmetic operator and the assignment
operator: for example, +=. When a statement executes, QuickC performs the
specified arithmetic on the variable's value and then assigns the result
of the calculation to the variable as its new value. Using an arithmetic
assignment operator, we can write a shorter version of the statement that
increases the value of total_lines by one:
total_lines += 1;
Below are more examples that use arithmetic assignment operators:
count -= 1;───────────────────────────Subtract 1 from the value of count
fare += 0.75;──────────────────────────────────Add 0.75 to value of fare
value *= 10;────────────────────────────────────────Multiply value by 10
You can use any arithmetic operator in an arithmetic assignment operation.
Table 3-2 lists the five possible arithmetic assignment operators. The
addition and subtraction assignment operators are the most commonly used.
The OPEQUAL.C program (Listing 3-21) demonstrates the use of arithmetic
assignment statements. The printf() statements print several arithmetic
assignment expressions and their results.
Be sure that when you read the printf() statements in the program you can
correctly predict the following output:
Starting values: m = 10 n = 5
m += 2 makes m 12
m -= n makes m 7
m *= 2 makes m 14
m = m + 1 makes m 15
m += 1 makes m 16
Table 3-2. Arithmetic Assignment Operators
Operator Meaning
──────────────────────────────────────────────────────────────────────────
+= Add to value and assign
-= Subtract from value and assign
*= Multiply by value and assign
/= Divide by value and assign
%= Get remainder from division and assign
──────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────
/* opequal.c -- shows combination math/assignment */
/* operators and increment operators */
main()
{
int m = 10, n = 5;
printf("Starting values: m = %d n = %d\n",
m, n);
/* combination of arithmetic and assignment */
printf("m += 2 makes m %d\n", m += 2);
printf("m -= n makes m %d\n", m -= n);
printf("m *= 2 makes m %d\n", m *= 2);
/* two ways to increment m */
printf("m = m + 1 makes m %d\n",
m = m + 1);
printf("m += 1 makes m %d\n",
m += 1);
}
──────────────────────────────────────────────────────────────────────────
Listing 3-21. The OPEQUAL.C program.
Increment and Decrement Operators
As the last program demonstrated, both m = m + 1 and m += 1 added one to
the value of m. If you've done any programming, you know how frequently
the value of a variable must be increased or decreased by one. This is
especially true when you create a "counter" variable that keeps track of
the number of times a statement in a loop executes. C provides an
ultra-concise operator, the increment operator ++, to add one to the value
of a variable. Similarly, --, the decrement operator, subtracts one from
the value of a variable. Consider the following examples:
count++ ;────────────────────────────────────────Add 1 to value of count
index-- ;─────────────────────────────────Subtract 1 from value of index
Note that these increment and decrement operators are really arithmetic
assignment statements. They add (or subtract) one and assign the resulting
value to the variable.
count++;──────────────────is equivalent to───────────────────count += 1;
index--;──────────────────is equivalent to───────────────────index -= 1;
(Most programmers do not use a space between the increment [or decrement]
operator and the variable name. However, in C it is perfectly legal to use
intervening spaces, as in count + +.) INCDEC.C (Listing 3-22) shows how
the increment and decrement operators change the value of a variable.
Compare the program statements to the following output:
a is 10
++a is 11
--a sets a back to 10
──────────────────────────────────────────────────────────────────────────
/* incdec.c -- shows effect of */
/* increments and decrements */
main()
{
int a = 10;
printf("a is %d\n", a);
printf("++a is %d\n", ++a);
printf("--a sets a back to %d\n", --a);
}
──────────────────────────────────────────────────────────────────────────
Listing 3-22. The INCDEC.C program.
Pre-increment vs Post-increment
In the INCDEC.C program we put the increment or decrement operator in
front of the variable name. However, you also can use it after the
variable name. In either case the variable is incremented or decremented;
but there is one important difference. When you use the operator in front
of a variable name, the incrementing or decrementing is done immediately.
When you use the operator after the variable name, the incrementing or
decrementing is not done until the next use of the variable. The PREPOST.C
program (Listing 3-23) shows how this works. The output of the program
illustrates how incrementing is delayed:
b is 100
b++ is still 100
but after it's used, b is incremented to 101
++b, on the other hand, is immediately 102
Notice what happens to b when we use the increment operator after it,
rather than before it. The first printf() statement with the value b++
prints the original value of 100, showing that it has not yet been
incremented. The next printf() statement, however, prints 101.
As a practical matter, the distinction between pre-increments and
post-increments (or decrements) is usually important only when the
variable is incremented or decremented while it is being used with other
operators in a single expression. For example, suppose you want to
increment counter and also assign it to total in the same statement.
Assuming counter is currently 10:
total = counter++;
assigns 10 to total, because counter is assigned to total but not
incremented until the next time it is used. On the other hand:
total = ++counter;
assigns 11 to total, because counter is incremented immediately and then
assigned.
──────────────────────────────────────────────────────────────────────────
/* prepost.c -- shows effect of pre- */
/* and post-increments */
/* and decrements */
main()
{
int b = 100;
printf("b is %d\n", b);
printf("b++ is still %d\n", b++);
printf("but after it's used, ");
printf("b is incremented to %d\n\n", b);
printf("++b, on the other hand, ");
printf("is immediately %d\n", ++b);
}
──────────────────────────────────────────────────────────────────────────
Listing 3-23. The PREPOST.C program.
Relational Operators
If you have some programming experience, you know that most programs must
make decisions based on the values of certain variables. Variables are
tested or compared, and the result of the test determines which program
statement will execute next. The next two chapters cover the variety of
"control structures" that C provides for this purpose. Let's build the
foundation for those discussions by looking at the operators that C uses
for testing or comparing values.
A relational operator compares two values, which can be variables, literal
numbers, or whole expressions. A combination of relational operators and
values is called a relational expression. An example is count > 10, which
translates as "is the value of count greater than 10?" The > in this
expression is the "greater than" relational operator. The expression is
true or false depending on the current value of the variable count. If
count is 8, for example, the expression is false.
Table 3-3 illustrates the ways we can compare two values, a and b. In
reality, the values can be constants, variables, or expressions──anything
that expresses a numeric value. (Remember from our discussion of ASCII
that characters, too, are essentially numeric values.)
We described the value of a relational expression as being "true" or
"false." These terms are useful ways for us to follow the logic of a
program, but the actual value of a relational statement, like everything
else in the computer, is numeric. When a statement is true, its value is
1; when a statement is false, its value is 0. On the following page, the
RELATION.C program (Listing 3-24) uses printf() statements to show the
values of some statements that use relational operators.
The program generates the following output:
a = 5 b = 3 c = 4
Expression a > b has a value of 1
Expression a == c has a value of 0
Expression a > (b + c) has a value of 0
Because a is 5 and b is 3, the expression a > b has a value of 1, or true.
Because c is 4, a == c has a value of 0, or false. The third expression
combines relational and arithmetic operators: It first calculates the
quantity (b + c), and then it compares the value to a.
Table 3-3. Relational Operators
Expression Meaning
──────────────────────────────────────────────────────────────────────────
a < b Is a less than b?
a > b Is a greater than b?
a == b Is a equal to b?
a != b Is a not equal to b?
a <= b Is a less than or equal to b?
a >= b Is a greater than or equal to b?
──────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────
/* relation.c -- shows effect of */
/* relational operators */
main()
{
int a = 5, b = 3, c = 4;
printf("a = %d\t b = %d\t c = %d\n", a, b, c);
printf("Expression a > b has a value of %d\n",
a > b);
printf("Expression a == c has a value of %d\n",
a == c);
printf("Expression a > (b + c) has a value of %d\n",
a > (b + c));
printf("Expression a = b has a value of %d\n",
a = b); /* what happened here? */
}
──────────────────────────────────────────────────────────────────────────
Listing 3-24. The RELATION.C program.
Relational == vs Assignment =
Here's a pitfall to watch out for: In C, a single equal sign = is the
assignment operator, but a double equal sign == is the relational "equals"
operator. In some languages (such as BASIC), a single operator, =, serves
both purposes. So, if you are familiar with the BASIC usage, you might
make errors with these operators until you get used to the difference. A
common symptom of this error is a test that always appears to be either
true or false. For example, if you type the assignment count = 10 instead
of the relational count == 10 and then use the result in a control
structure (such as a loop or if statement), QuickC always sees the result
of the test as "true." Why? Because although relational expressions return
a value of 1 for "true," QuickC considers any nonzero value to be "true"
in this type of test. Because the sample statement with = is actually an
assignment, its value is 10 (the number assigned), which QuickC interprets
as "true" during a relational test.
──────────────────────────────────────────────────────────────────────────
Assignment and "Equals" Relation
The following table lets you compare the assignment and relational
"equals" operators in C to those in other common languages:
Language Assignment Relation
──────────────────────────────────────────────────────────────────────
C
=
==
BASIC = =
Pascal := =
FORTRAN = .EQ.
Logo make =
COBOL MOVE EQUAL TO
──────────────────────────────────────────────────────────────────────
────────────────────────────────────────────────────────────────────────
Precedence of Relational Operators
In RELATION.C, we used parentheses in the expression a > (b + c). If you
check QuickC's operator precedence help screen (Figure 3-8 on p. 77), you
will see that relational operators have a lower precedence than arithmetic
operators. Therefore, even if you don't use parentheses, b + c is
calculated first, and only then is the result compared to a. Nevertheless,
it is a good programming practice to use parentheses to visually clarify
an expression.
Logical Operators
Sometimes it is necessary or useful to test for more than one thing in the
same expression or statement. For example, you might want to test to see
if either the temperature or pressure in a boiler has exceeded the safety
limit. Let's assume the test for temperature is (temp < 900) and the test
for pressure is (pressure < 5000). We can combine the two tests as
follows:
(temp < 900) && (pressure < 5000)
The && is called the AND logical operator. It compares the results of two
relational values and returns a value of true (1) only if both are true.
QuickC first makes the temp test, then it makes the pressure test (testing
is from left to right). Then the && operator checks to see if both tests
were true.
The OR logical operator, ||, works like the AND operator, except that it
returns a value of true (1) if either or both of the tests are true. Thus,
the statement
(ch == 'q') || (turn > last_turn)
is true if either the current value of ch is `q' or the current value of
turn is greater than that of last_turn, or both. You could use this
statement to check if a game is over.
Using two relational operators, && and ||, and two possible results of a
test (true and false), there are four possible results for a relational
statement involving two tests. The TRUTH.C program (Listing 3-25 on the
following page) prints these out by making comparisons using ones and
zeros that represent the result of already completed relational tests.
Recall that QuickC regards a value of 1 to be "true" and a value of 0 to
be "false." Thus, 1 AND 1 is 1 means "True and true is true."
1 AND 1 is 1
1 AND 0 is 0
0 AND 0 is 0
1 OR 1 is 1
1 OR 0 is 1
0 OR 0 is 0
──────────────────────────────────────────────────────────────────────────
/* truth.c -- shows logical operators */
main()
{
printf("1 AND 1 is %d\n", 1 && 1);
printf("1 AND 0 is %d\n", 1 && 0);
printf("0 AND 0 is %d\n", 0 && 0);
printf("1 OR 1 is %d\n", 1 || 1);
printf("1 OR 0 is %d\n", 1 || 0);
printf("0 OR 0 is %d\n", 0 || 0);
}
──────────────────────────────────────────────────────────────────────────
Listing 3-25. The TRUTH.C program.
Once again, if you check the QuickC operator precedence help screen, you
will notice that logical operators, such as && and ||, have a lower
precedence than the relational operators, such as < or ==. Therefore, we
didn't need parentheses around the relational expressions in our examples
because QuickC evaluated them before it checked the logical operators.
Again, we used parentheses because they make these complex expressions
easier to read.
The last operator we need to discuss is the !, or "not" operator. Its
function is simple enough──it reverses the truth value of a relational
expression. For example, if a is 10, a > 5 is true, but !(a > 5) is false.
────────────────────────────────────────────────────────────────────────────
Chapter 4 Repetition and Looping
In all of our programs so far, QuickC has executed the program statements
sequentially, from the first statement to the end of the program. However,
most of a program's important work involves controlled repetition, in
which a group of statements repeatedly does a particular job until the
work is done. For example, consider the data-entry routine of a database
program. This group of statements (used to receive, validate, and store
data) must be repeated as long as the user wants to enter new data
records. This set of repeating statements is called a loop because it is
executed as though the statements were arranged in a circle. However, when
the user wants to stop entering data, the program must be able to
recognize a "quit" command and stop repeating the data-entry statements.
As you study C, you will find many other examples of the need for
controlled repetition. For example, a program that retrieves data from a
file must repeatedly read and process data items until it reaches the end
of the file. If you program in another language, you probably use loops
regularly to initialize and access elements of an array or a set of
variables.
C uses three types of loops: the for loop, the while loop, and the do
loop. Although these loops are fundamentally similar, they let you control
the looping action in different ways to suit different needs. This chapter
focuses on how to use these three types of loops and some of their common
variations.
The for Loop
The for loop repeats a group of program statements as long as a specified
condition is true. Generally, you use it to specify a fixed number of
repetitions: for example, processing the accounts for each month of the
year.
The anatomy of a for loop is as follows:
for (start; condition; update)
{
statements;
}
In this generalized for loop, start is one or more statements that
initialize the variables used by the loop; condition is a relational
expression that is tested to see whether the loop should continue to run;
and update is one or more statements that change the values of variables
in the loop. The group of statements between the braces that follow the
for line is called the "body" of the loop. These statements execute as
long as the condition in the parentheses is true. (The body can also
consist of only one statement, in which case the braces are optional. We
tend to use braces for even a single statement, however, because they make
the body of the loop easier to distinguish.)
The FORLOOP.C program (Listing 4-1) uses a for loop to count from 1 to
10. After we declare the variable i, we begin the loop structure with the
keyword for. The parentheses that follow the for contain the control
statements for the loop. Note that semicolons separate the control
statements.
──────────────────────────────────────────────────────────────────────────
/* forloop.c -- a simple for loop that */
/* counts to ten */
main()
{
int i;
for (i = 1; i <= 10; i++)
{
printf("%d\n", i); /* body of loop */
}
printf("All done!\n"); /* executed when i > 10 */
}
──────────────────────────────────────────────────────────────────────────
Listing 4-1. The FORLOOP.C program.
The start statement establishes the variable i as the loop's control
variable. This is the variable whose value is tested to determine when the
loop will stop running. (Many people use i, j, or k for loop control
variables. This tradition owes its roots to FORTRAN. However, any legal
name will do.)
The next statement, i <= 10, is the loop's test, or condition. It
specifies that the body of the loop execute repeatedly as long as the
value of i is less than or equal to 10. The test condition is a relational
statement that compares the loop control variable to an assigned value and
returns a value of 1 (true) or 0 (false).
The last statement in the for loop parentheses is i++. This update
statement changes the value of the loop control variable each time the
loop body executes. Here we use the ++ increment operator to increase i by
one each time it executes, and, in fact, most for loops use update
statements that either increment or decrement the control value by one.
Using values other than one, however, is almost as easy: The statement
value += 10, for example, adds 10 to value each time it executes. You can
also use multiplication or division rather than addition or subtraction.
Let's step through FORLOOP.C one statement at a time to see how it works:
■ Set i to 1.
■ Check i to see if it is less than or equal to 10.
■ Because the result of this test is true, execute the body of the loop.
(The body consists of a printf() statement that prints the value of i.)
■ Execute the update statement, i++. (Set i to i + 1, or 2.)
■ Check the test statement again to see if i is still less than or equal
to 10. If it is, execute the body of the loop again. Continue the cycle
until the test condition is false (when the value of i increases to
11).
Figure 4-1 on the following page shows this program as a flowchart. You
can follow the arrows to trace the flow of execution.
──────────────────────────────────────────────────────────────────────────
Choosing a Control Variable
If you are used to writing loops in BASIC, remember that with C, you must
declare the loop control variable before you use it in the loop. Select a
data type for the control variable that can accommodate the full range of
values the variable will hold when the loop is run.
For example, a loop that will run 50,000 times requires a control variable
of type unsigned int because a signed int value cannot exceed 32,767.
──────────────────────────────────────────────────────────────────────────
┌────────────────┐
│ Initialize │◄▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
│ i = 1 │ ┌─────────▒─────────────────
└───────┼────────┘ │° for (i = 1; i <= 10; i++°
│ │° ▒ ▒ °
▼ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒{▒▒▒▒▒▒▒▒▒▒ ▒ °
/\ ▒ ▬ ▬ ▬ │° printf("%d\n", i);▒ °
/ \ ◄▒▒▒▒▒▒▒ ▬ ▬ │° } ▒ ▒ °
/ TEST \ No ▬ ▬ │° ▒ ▒ °
┌───►/ i <= 10 \ ──────► ▬ END ▬ │° ▒ ▒ °
│ \ ▬ / ▬ ▬ │° ▒ ▒ °
│ \ ▬ ? ▬ / ▬ ▬ │° ▒ ▒ °
│ \ ▬ / ▬ ▬ ▬ │° ▒ ▒ °
│ \/ │° ▒ ▒ °
│ │ Yes └───────────────▒────────▒──
│ ┌───────▼────────┐ ▒ ▒
│Do body of loop │ ▒ ▒
│ │ print f... │◄▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒
│ └───────┬────────┘ ▒
│ │ ▒
│ ┌───────▼────────┐ ▒
│ │ Add 1 to i │◄▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
│ └───────┬────────┘
│ │
└───◄──────┘
Figure 4-1. The for loop.
Why does the loop stop running? Let's look at the situation when i = 10:
The printf() statement in the body of the loop prints the number 10. The
update statement then increments the loop by 1 and the test statement
executes. Because the value of i is now 11, the test fails (returns a
value of "false"). This causes the program to skip the loop body and
execute the next statement, which prints the message All done!
for Loop Style
As with other C statements, the statements within the parentheses of a for
loop can extend to more than one line if necessary. As noted in our
discussion of conventions, we align the braces vertically for the loop
body, as shown in FORLOOP.C. An older style aligns the braces as follows:
for (i = 1; i <= 10; i++) {
printf("%d\n",i);
}
With this style, the braces can get lost in a long listing, making it
difficult to find where the body of the loop begins and ends. Aligning the
braces vertically makes them easier to spot and highlights the body of the
loop.
Also note that we indent the body of a C loop to the right of the line
that specifies it. To indent text in the QuickC editor, simply press the
Tab key. The default indention in QuickC is eight characters, but you can
change this value at the Options box of the View menu. We use a tab of
four characters in our listings.
Pitfalls to Avoid in for Loops
An easy mistake to make when writing for loops is to put a semicolon after
the closing parenthesis:
for (i = 1; i <= 10; i++);───────────────────────────────Semicolon added
This does not cause a compiler error: In C, a semicolon by itself is a
"null statement." Such a statement does nothing, but it counts as a legal
statement. Using a semicolon after the parenthesis makes the null
statement the body of the loop. Adding the semicolon to FORLOOP.C causes
the loop to do "nothing" 10 times; the program then prints the value of i
(which is 11 after the loop exits) and All Done!
Also, always remember to put braces before and after a loop body that
consists of more than one statement. If you do not use braces, only the
first statement following the parentheses executes as the body of the
loop. The remaining statements will execute only once, after the loop
terminates. (This is another reason for adopting the practice of always
putting braces around the statements in a loop body, even when the body
has only one statement.)
Multistatement for Loops
FORLOOP.C has only one statement in the body of the loop, but most
programs are much more complex. Let's develop a program that will print a
table of square roots, squares, and cubes for the integers from 1 through
9. Because this program must calculate and print three values for each
number, it needs several statements in the body of the for loop.
Using QuickC Library Functions
To write such a program, we need a means of producing the square root of a
number. Although C does not have operators for calculating squares or
cubes directly, we can get these values simply by multiplying a variable
by itself two and three times respectively. To get the square root,
however, we must call on QuickC's sqrt() function. This function returns
the square root of any value you pass to it. For example, if i = 4, then
sqrt(i) = 2.
The square root function, sqrt(), is an example of a QuickC library
function (sometimes called a "library routine"). We've already used
several QuickC "core" functions, such as printf() and scanf(). Because
these functions are part of the QuickC environment, you can use them
without any special commands. (Appendix B lists all the built-in core
functions.)
──────────────────────────────────────────────────────────────────────────
Quick Tip
Sometimes it is convenient to break out of a loop during its execution.
Perhaps you recognize a problem with its output, or perhaps you find
yourself in a runaway loop──one whose test will not or cannot fail. To
break out of a loop, press Ctrl-Break.
──────────────────────────────────────────────────────────────────────────
However, sqrt() is not on this list. It is one of many library routines
that are defined in the header files (often called "include files") of the
\INCLUDE directory. One of the early tasks in learning QuickC is becoming
familiar with its external library functions. Fortunately, QuickC makes it
easy to explore the function library.
QuickC's extensive on-line help screens let you call up a summary of any
function to find out whether it is a core function or an external library
function. To find out about sqrt(), select Topic from the Help menu. Next,
select the appropriate topic, math; this produces a list of library
functions that include the sqrt() function. When you select this function,
a small help window appears at the top of the QuickC screen. (See Figure
4-2.) The first entry in this window informs you that sqrt() resides in
both the float.h and math.h include files.
QuickC also lets you browse through include files while you are working on
a program. Simply select Include from the View menu, select the \INCLUDE
directory from the window (if necessary), and then select the include file
you want to view. When you finish, select Open Last File from the File
menu, and QuickC returns you to the program you were working on.
Of course, the preferred reference for all QuickC library functions is the
Microsoft QuickC Run-Time Library Reference, one of the manuals that come
with QuickC. It introduces the library functions by category.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 4-2 can be found on p.98 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 4-2. Library function help window.
Using an Include File in a Program
To use functions or other definitions from an include file in your
program, you must specify the name of the file you want to call before the
start of main(). For example:
#include <graph.h>
includes the file that contains graphics functions and definitions in your
program. (The angle brackets that enclose the filename tell QuickC to look
for the file in the default \INCLUDE directory, whose pathname the setup
procedure stored in the environmental variable INCLUDE.) This statement is
actually a "directive" to the QuickC preprocessor, a program that examines
your C program code and looks for special commands that tell it to make
changes in the program text before compilation begins. In this case, the
#include preprocessor directive reads the contents of the specified
include file into the program and compiles it as though you had typed it
in. Only after it reads and compiles all the include files does QuickC
compile your program statements. Note that preprocessor statements such as
#include are not actually C language statements and do not end with a
semicolon.
Creating a Program List
We have seen that we need to use a #include statement if we want to refer
to the sqrt() function in the program we want to run, TABLE.C (Listing
4-2). In addition, before we compile the program, we need to tell QuickC
where to find the compiled library code that corresponds to the definition
of sqrt() in the include file math.h. We can do this by creating what is
called a "program list." To do this, first select Set Program List from
the File menu. In the dialog window (Figure 4-3 on the following page),
type in the name of your program followed by the extension .mak. (Thus,
for TABLE.C you type table.mak.) Next, select Edit Program List from the
File menu.
──────────────────────────────────────────────────────────────────────────
/* table.c -- prints square root, square, and cube */
/* for the numbers 1 thru 9 */
#include <math.h> /* include math functions so we */
/* can do square root */
main()
{
int i;
printf("i\t sqrt(i)\tsquare(i)\tcube(i)\n\n");
for (i = 1; i < 10; i++)
/* beginning of body of loop */
{
printf("%d\t", i);
printf("%f\t", sqrt(i));
printf("%d\t\t", i * i);
printf("%d\n", i * i * i);
}
/* end of body of loop */
}
──────────────────────────────────────────────────────────────────────────
Listing 4-2. The TABLE.C program.
The dialog window, shown in Figure 4-4, lists all the C programs in the
current directory. Select TABLE.C from the window with the mouse or
keyboard, and then select the Add/Remove button. Finally, select the Save
button to save the edited program list. You are now ready to run TABLE.C.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 4-3 can be found on p.100 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 4-3. Set Program List dialog window.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 4-4 can be found on p.100 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 4-4. Edit Program List dialog window.
We use the #include <math.h> statement before the definition of the main()
function to tell QuickC to use the math.h include file in the program. A
printf() statement then prints the table header. Because we only want to
print the header once, we don't place this statement inside the loop!
Next comes the for loop. The loop specifications establish the test
condition as i < 10 and the update as increments of 1.
The body of the loop consists of four printf() statements: The first
prints the value of i; the next three print, in order, the square root,
the square, and the cube for each value. The program results in the
following neatly formatted table:
i sqrt(i) square(i) cube(i)
1 1.000000 1 1
2 1.414214 4 8
3 1.732051 9 27
4 2.000000 16 64
5 2.236068 25 125
6 2.449490 36 216
7 2.645751 49 343
8 2.828427 64 512
9 3.000000 81 729
Multiple Initializations and Calculations in for Loops
An almost universal rule in C states that anywhere you can put a single C
statement, you can put multiple statements. For example, in a for loop,
you can initialize two variables in the first part of the loop
specification, as follows:
for (count = 1, total = 0; count < values; count++)
{
total += count;
}
Here we initialize the for loop by setting the loop control variable count
to 1. At the same time, we set the variable total to zero. This loop adds
all the integers between 1 and the number specified in values. Note that a
comma separates the two statements in the initialization: Semicolons
separate the three parts of the loop specification (start or
initialization, test, and update); however, commas separate multiple
statements within each part.
──────────────────────────────────────────────────────────────────────────
Quick Tip
If you want QuickC to search for a file in the current directory instead
of the default include directory, enclose the filename in quotes: #include
"graph.h". If you want QuickC to search another directory, specify the
full pathname: #include "c:\qc\mydefs\defs.h".
──────────────────────────────────────────────────────────────────────────
You can also use multiple calculations in the update portion of the loop
specification. For example, we can rewrite the above loop as follows:
for (count = 1, total = 0; count < values;
total += count, count++)
{;}
Here we moved the statement that added each new value of count to total
out of the loop body and put it in the update part of the loop
specification. However, a loop must have a body to be legal, so we added a
single semicolon (a null statement) as the loop body. (The null statement
is somewhat dangerous because you might accidentally delete the stray
semicolon. We try to avoid this by indenting the semicolon to the loop
body position. We also enclose the semicolon in braces. The braces are
unnecessary, but they help to emphasize the importance of the semicolon in
the loop structure.)
The use of multiple statements and null bodies in loops is a matter of
programming style. Many C programmers try to be as concise as possible, so
you will often encounter these usages in C code. We present these variants
to acquaint you with common C programming practices; you gain no
performance advantage by doing all initializations and calculations within
the loop specification.
The INFLATE.C program (Listing 4-3) is another example that uses multiple
initializations and calculations. At first glance, you might think that
the braces in the for loop have been forgotten or misplaced.
──────────────────────────────────────────────────────────────────────────
/* inflate.c -- shows multiple initialization */
/* and calculations in for loop */
main()
{
int year;
float value, rate;
printf("What do you think the inflation rate will be?");
scanf("%f", &rate);
printf("If the dollar is worth 100 cents in 1987\n");
printf("and the inflation rate is %2.2f, then:\n", rate);
for (year = 1988, value = 1.0; year <= 1999;
value *= (1.0 - rate),
printf("in %d the dollar will be worth", year),
printf(" %2.0f cents\n", value * 100), ++ year)
{;}
}
──────────────────────────────────────────────────────────────────────────
Listing 4-3. The INFLATE.C program.
The program asks you to estimate the average inflation rate for the next
decade or so. (We're sure your guess is as good as ours!) If you enter
.06, the program generates the following:
What do you think the inflation rate will be? .06
If the dollar is worth 100 cents in 1987
and the inflation rate is 0.06, then:
in 1988 the dollar will be worth 94 cents
in 1989 the dollar will be worth 88 cents
in 1990 the dollar will be worth 83 cents
in 1991 the dollar will be worth 78 cents
in 1992 the dollar will be worth 73 cents
in 1993 the dollar will be worth 69 cents
in 1994 the dollar will be worth 65 cents
in 1995 the dollar will be worth 61 cents
in 1996 the dollar will be worth 57 cents
in 1997 the dollar will be worth 54 cents
in 1998 the dollar will be worth 51 cents
in 1999 the dollar will be worth 48 cents
The program uses scanf() to obtain the estimated inflation rate. Then it
prints the introduction to the table and enters a for loop. Because the
table prints yearly values, we call the loop control variable year. (Note,
by the way, that control variables need not start at 0 or 1.) The
initialization part of the loop also sets value to 1.0. (In other words,
the dollar starts at its full value.) The test part of the loop causes the
printing of values for the years 1988 through 1999.
The update part of the loop specification does the work of this loop──the
loop has a null body. Each year the current value is multiplied by 1.0 -
rate to show the effects of inflation. The arithmetic assignment operator,
*=, causes this new amount to become the new value. The printf()
statements print the amount in cents by first multiplying value by 100;
the format specifier %2.0f rounds it off to whole cents. Finally, ++year
increments year, and the loop is ready for another pass.
──────────────────────────────────────────────────────────────────────────
A for Loop Using Characters
Because the PC's ASCII character set is merely a set of integer values
from 0 to 255, a for loop can process characters as easily as it does
ordinary integers. For example, the int control variable can be
initialized by setting it to the character value `a'. There is no problem
with this, because `a' is simply the integer value 97. Then, by specifying
a loop test condition such as i <= `m', you determine that the body of the
loop will be repeated 13 times, once for each letter in the first half of
the alphabet.
──────────────────────────────────────────────────────────────────────────
Nesting for Loops
Sometimes it is useful to have one of the statements in the body of a loop
be another loop. This is called nesting loops. For example, you might
design a program to read a disk data file that is arranged so that each
line contains four data fields. An "outer" loop could process each line,
and an "inner" loop could process each field. The outline for this program
might be:
open_file(name);
for (line = 1; line <= last_line; line++)
{
for (field = 1; field <= 4; field++)
{
process_field;
}
}
save_file(name);
The first, or outer, loop uses the control variable line and the test line
<= last_line to read each line of the file in turn. (In this example, we
assume that the number of lines in the file has been previously
determined.) The inner for loop uses the control variable field to step
through the four fields of each line. The body of this nested loop calls
the function process_field to do the actual reading of data. When the last
field in the line is processed, the inner loop exits. Because we are still
in the body of the outer loop, the outer loop continues by moving to the
next line; then the inner loop runs again. Only when the inner loop runs
for the last line do the nested loop structure and the save_file()
statement execute.
Our next sample program, GRAPHBOX.C (Listing 4-4 on page 106), uses
several for loops, including a pair of nested ones, to draw a box on the
screen using PC graphics characters.
As we mentioned earlier, the IBM PC uses the ASCII values from 128 to 255
to represent the "extended character set," which includes many shapes that
you can use to create effective graphics. To find the appropriate
characters for drawing a box, select the help screens for ASCII characters
from the Help menu (or press the F1 key). The second of the two screens
contains the extended characters. For example, with character number 201
you can draw the upper-left corner of the box.
To display these characters, you must use the QuickC core library function
called putch(). Call the function by specifying the ASCII code of the
desired character in parentheses. For example, to draw the corner
character mentioned above, specify:
putch(201);
Using #define
Our box-drawing program uses many different characters to represent the
corners and sides of the box, plus the newline, return, and blank
characters. Remembering the ASCII codes for all these characters is a
difficult task, and relying on memory could lead to coding mistakes. But C
has a feature that helps eliminate this problem.
C provides a mechanism for assigning symbolic names to frequently used
values in a program. The preprocessor directive #define lets you specify a
name and assign a value to it, as in the following example:
#define UPLEFT 201
Before QuickC compiles your program, the preprocessor finds each
occurrence of the name UPLEFT and replaces it with the number 201. You
remember the name; QuickC remembers the number. You can also use #define
with characters. If you use the definition #define color "green" and you
use the statement printf(color); in your program, the preprocessor
translates the statement into printf("green"); before QuickC compiles it.
Always place #define statements before the definition of main(), and do
not end them with a semicolon. (If you use a semicolon, the preprocessor
will treat it as part of the value to be substituted. This often leads to
a bug that causes a compiler error.)
You can use #define to make your code more readable by substituting
easy-to-remember names for numbers. For example, you could use NL for the
newline character instead of 10, the newline ASCII value. Also, #define
makes it easy to change many values in a program without having to change
many individual statements.
──────────────────────────────────────────────────────────────────────────
#define vs Variables
You might ask why you should use #define when you could do the same thing
more easily with ordinary variables. After all, you could declare int nl =
10; and then use putch(nl); (to simplify punctuation) and thereby avoid
the preprocessor step. But there are two reasons why this isn't a good
idea.
First, using #define produces more efficient code than does using a
variable. When your program uses variables, QuickC must compile extra
machine instructions to store, change, or fetch the needed values. With
#define, on the other hand, the preprocessor compiles the values directly
into the compiled code: The program doesn't need any extra instructions.
As a result, your compiled code is faster and more compact.
Second, a variable should represent a quantity that is subject to change
by the program. The ASCII value 10 for a newline character, however, is a
constant. Using #define guarantees that the value you define cannot
accidentally be changed while the program is running.
Incidentally, the new ANSI C standard creates the keyword const to let you
avoid #define directives. Using the new keyword, you might declare const
int nl = 10; to define the constant nl.
──────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────
/* graphbox.c -- defined to use PC-specific graphics characters */
#define NL 10
#define CR 13
#define BLANK 32
#define UPLEFT 201
#define UPRIGHT 187
#define LOWLEFT 200
#define LOWRIGHT 188
#define LINE 205
#define SIDE 186
main()
{
int i, j, height, width;
/* get height and width from user */
printf("How high a box do you want? ");
scanf("%d", &height);
printf("How wide do you want it to be? ");
scanf("%d", &width);
/* draw top of box */
putch(UPLEFT);
for (i = 0; i < (width - 2); i++)
putch(LINE);
putch(UPRIGHT);
putch(NL);
putch(CR); /* go to next line */
/* draw sides of box */
for (i = 0; i < (height - 2); i++) /* outer loop */
{
putch(SIDE); /* left side */
for (j = 0; j < (width - 2); j++) /* inner loop */
{
putch(BLANK);
}
putch(SIDE); /* right side */
putch(NL);
putch(CR); /* move to next line */
}
/* draw bottom of box */
putch(LOWLEFT);
for (i = 0; i < (width - 2); i++)
putch(LINE);
putch(LOWRIGHT);
putch(NL);
putch(CR); /* box is done, move cursor to new line */
}
──────────────────────────────────────────────────────────────────────────
Listing 4-4. The GRAPHBOX.C program.
To continue with the previous example, if you convert your program to run
on a mainframe that uses a non-ASCII character set, you need only to
change the value of NL in the #define statement to reflect the new
character value throughout the program.
When you run the GRAPHBOX.C program, it asks:
How high a box do you want? 8──────────────────────Enter height in lines
How wide do you want it to be? 20──────────────Enter width in characters
Figure 4-5 shows the graphics box that this program generates on your
screen.
╔═══════════════════╗
║ ║
║ ║
║ ║
║ ║
║ ║
║ ║
║ ║
╚═══════════════════╝
Figure 4-5. Character graphics box produced by GRAPHBOX.C.
The program begins with nine #define statements that name the needed
characters and their values. The first section of main() prompts the user
for a height and width. Then a putch() displays the character for the
upper-left corner of the box. Next, a for loop prints a graphics
double-line character width - 2 times. (We print two less than width
characters to leave room for the upper-left and upper-right corner
characters.)
The third section of main() draws the sides of the box. After subtracting
the top and bottom lines, we want to print height - 2 lines: This is
provided for by the test statement in the next for loop. For each line,
the program prints the SIDE character (the double bar) and then uses a
nested for loop to print width - 2 blank characters to position the cursor
at the right side of the box. Another SIDE character completes the line;
then an NL and a CR move the cursor to the next line.
The statements that print the bottom line are the same as those that
printed the top line, except that they use the special characters for the
lower-left and lower-right corners of the box.
The while Loop
C contains another loop structure, called the while loop, which takes the
following general form:
while (test)
{
statements;
}
Structurally, the while loop is a for loop with only the test part of the
specification, its condition, in parentheses. You initialize loop
variables in a statement before the while, and you update or increment the
loop with a statement in the loop body. Thus, although the for loop
features compactness and holds the entire loop specification in the
parentheses, the while loop is easier to read because the parentheses
contain only the test expression. The WHILE.C program (Listing 4-5 at the
bottom of the page) shows a simple example.
The program produces the following output:
1
2
3
4
5
6
7
8
9
10
Done!
The statement int count = 1; declares and initializes the loop control
variable. At the while statement, the condition count < 11 is tested.
Because it is true, the body of the loop executes. The body consists of a
printf() statement that prints the current value of count, and the
statement count++; which increments count. The test condition is then
checked again, and the loop continues printing numbers until count reaches
11. At this point the test fails, the loop terminates, and the statement
printf("Done!\n"); executes. Figure 4-6 shows a flowchart of this while
loop.
──────────────────────────────────────────────────────────────────────────
/* while.c -- a simple while loop */
main()
{
int count = 1;
while (count < 11) /* loop condition */
/* body of loop */
{
printf("%d\n", count);
count++;
}
printf("Done!\n");
}
──────────────────────────────────────────────────────────────────────────
Listing 4-5. The WHILE.C program.
At this point you might ask why you need while statements if they are
merely variant forms of for loops. The answer is simple. A for loop is
designed to work with a specific series of values (such as numbers from 1
to 10 or 1 to total_lines), and it usually counts up or down. A while
loop, however, is designed to run indefinitely as long as some condition
remains true. It also can test many kinds of conditions.
For example, suppose you want to write a program that draws endlessly
changing graphic patterns until the user presses a key to stop it. A while
loop is ideal for this purpose when used with the QuickC library function
kbhit(), which returns a 1 (true) if a key is pressed and a 0 (false) if
no key is pressed. (A loop that waits for some external event to take
place is called a "polling loop.") The main loop of your graphics program
might appear as follows:
while (!kbhit())
{
draw statements;
}
The draw statements create the graphics while the test part of the while
loop specification polls the keyboard. As long as the user does not press
a key, the kbhit() function returns a 0, or false. Notice that we use an
!, which is the "logical not" operator, in front of kbhit(). "Not false"
is the same as "true," so the test for the while loop is satisfied and the
body of the loop executes as long as the user doesn't press a key. If you
find this reverse logic difficult to understand, try translating the loop
specification into words:
while───────────────────────────────────────────────────────"As long as"
!───────────────────────────────────────────────────────────────────"no"
kbhit()─────────────────────────────────────────────────"key is pressed"
┌────────────────┐ ┌─────────────────────────┐
│ Initialize │◄▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒int count = 1 °│
│ count = 1 │ │° °│
└────────────────┘ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒while (count < 11) °│
▼ ▒ │° { °│
/\ ▒ ▬ ▬ ▬ ▒▒▒▒printf("%d\n", count);°│
/ \ ◄▒▒▒▒▒▒▒ ▬ ▬ ▒│° °│
/ TEST \ No ▬ ▬ ▒▒▒▒count++; °│
┌───►/count < 11 \ ──────► ▬ END ▬ ▒│° } °│
│ \ ▬ / ▬ ▬ ▒│° °│
│ \ ▬ ? ▬ / ▬ ▬ ▒│° °│
│ \ ▬ / ▬ ▬ ▬ ▒└─────────────────────────┘
│ \/ ▒
│ │ Yes ▒
│ ┌───────▼────────┐ ▒
│Do body of loop │ ▒
│ │ print f... │◄▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
│ │ count++; │
│ └───────┬────────┘
│ │
└───◄──────┘
Figure 4-6. The while loop.
When the user presses a key, kbhit() returns "true," but the logical not
operator reverses the result into "not true," or 0, and the loop exits.
Note that a polling while loop needs no counter variable or incrementing.
The while loop needs only to test a condition that will eventually change.
(If the condition never changes, the program never stops.)
Using while to Animate a Character
We can nest while loops much as we nested for loops. The ANIMATE.C program
(Listing 4-6) uses a set of nested while loops to produce simple
animation──making a character appear to move back and forth across the
screen.
When you run ANIMATE.C, a "double arrow" graphics character (» or «) and
the flashing cursor move back and forth across the screen until you press
a key.
ANIMATE.C starts with #define statements that specify the right arrow and
left arrow PC graphics characters as well as the backspace and blank
characters, which we use for moving the cursor back and for erasing the
previously drawn arrow.
The outer loop uses while (!kbhit()), which keeps the program running
until you press a key. The inner while() loop moves the arrow to the right
by repeatedly
1. displaying the right arrow character,
2. backing up the cursor,
3. erasing the previously displayed arrow by overprinting it with a blank
space character, and
4. incrementing pos to display an arrow in the next space.
(Remember, the blank moves the cursor to the space with the displayed
arrow.) A while loop then tests pos to stop the arrow when it reaches the
right side of the screen (position 79).
Notice the two nested while loops that we use to slow the display so the
eye can follow it. This loop simply counts to 1000. We often use delay
loops such as this to slow a program to accommodate human perception or
peripheral devices that cannot keep up with the CPU. (Sophisticated delay
loops read and use the system clock.)
Another set of while loops moves the arrow from the right side of the
screen to the left side. You should have little trouble figuring out how
it works. Note that the arrow must move backward, so we decrement (rather
than increment) pos and test for pos > 1 to see when the arrow reaches the
left side of the screen.
What happens when the arrow reaches the left side of the screen? The body
of the outer loop finishes, and control returns to the test statement in
the outer loop. Assuming no key has been pressed, the body of the loop
then executes again.
──────────────────────────────────────────────────────────────────────────
/* animate.c -- animates a graphics character */
/* until a key is pressed */
/* Special characters */
#define RTARROW 175
#define LFTARROW 174
#define BLANK 32
#define BACKSPACE 8
main()
{
int pos, i, j = 1;
while (!kbhit())
{
pos = 1;
while (pos < 79)
{
putch(RTARROW);
i = 1;
while (i < 1000)
{
j = i + 10;
i++;
}
putch(BACKSPACE);
putch(BLANK);
pos++;
}
while (pos > 1)
{
putch(LFTARROW);
i = 1;
while (i < 1000)
{
j = i + 10;
i++;
}
putch(BACKSPACE);
putch(BLANK);
putch(BACKSPACE);
putch(BACKSPACE);
pos--;
}
}
}
──────────────────────────────────────────────────────────────────────────
Listing 4-6. The ANIMATE.C program.
Combining while and for Loops
The following program, MIXLOOPS.C (Listing 4-7), accepts a character from
the user and counts through the alphabet until it reaches the character,
beeping once for each count. The loop continues to accept characters until
the user enters a blank. (Note: As written, the program works only with
lowercase alphabetic characters; you can extend it to accept others by
changing the starting value of the variable i.)
A session with MIXLOOPS.C might run as follows:
c──────────────────────────────────────────────────User enters character
In FOR loop!─────────────────────────────Beeps each time line is printed
In FOR loop!
In FOR loop!
───────────────────────────────────────User enters blank to end program
The outer loop, a while loop, contains the test:
while ((ch = getche()) != ' ')
This introduces another noncore QuickC library function called getche(),
which stands for "get character with echo." This function accepts a
character from the user and echoes (displays) it on the screen. Because
this function is located in the conio.h include file, you must add the
appropriate #include line before the definition of main().
An important feature of this loop is its use of a function call whose
value is both assigned to a variable and tested in the loop condition.
We'll learn more about function calls in the next chapter.
──────────────────────────────────────────────────────────────────────────
/* mixloops.c -- reads characters, */
/* beeps for ASCII count, */
/* uses a while and a for */
#include <conio.h>
main()
{
char ch;
int i;
while ((ch = getche()) != ' ') /* get a char. */
{
for (i = 'a'; i <= ch; ++i) /* count up to alphabet pos.*/
{
printf("In FOR loop!\n");
printf("\a"); /* sound beep each time */
}
}
}
──────────────────────────────────────────────────────────────────────────
Listing 4-7. The MIXLOOPS.C program.
When the user enters a character, the assignment ch = getche() assigns the
ASCII value of the character to the variable ch. The not equals operator
!= then compares the character value to the ASCII value for the blank
character, specified as ' '. This results in a true or false value that
the while loop tests.
If the user does not enter a space, the for loop, which makes up the body
of the while loop, executes. The for loop tests for i <= ch. Thus, if the
user enters the character f, the loop counts from the ASCII value of `a'
to that of `f': The body of the for loop executes one time each for the
values `a' through `f', and you hear six beeps.
MIXLOOPS.C is a good example of the appropriate use of while and for
loops. The outer, while loop waits indefinitely for a condition to change
(the user enters a space); the inner loop, the for loop, counts to a
definite value (the ASCII value of the character entered in the while
loop).
The do Loop
The third (and final) C looping structure is the do loop, which takes the
following general form:
do
{
statements;
}
while (test);
The do loop is very similar to the while loop, with one major exception──
the while loop performs the test and then executes the body of the loop;
the do loop executes the body of the loop and then performs the test.
Thus, the body of a do loop always executes at least once, even if the
result of the first test is false.
The DO.C program (Listing 4-8) demonstrates a simple do loop that
performs the now-familiar task of counting from 1 to 10.
──────────────────────────────────────────────────────────────────────────
/* do.c -- a simple do-while loop */
main()
{
int i = 1;
do
{
printf("%d\n", i);
i++;
}
while (i < 11);
printf("Done!\n");
}
──────────────────────────────────────────────────────────────────────────
Listing 4-8. The DO.C program.
Of the three C looping structures, the do loop is by far the least used.
Usually when you test for a change in condition, a while loop is more
appropriate because you want the program to react immediately to user
input, especially a "quit" command.
Use the do loop to repeat an action until some condition changes only when
the test need not be made immediately. A good example is the TIMER.C
program (Listing 4-9).
──────────────────────────────────────────────────────────────────────────
/* timer.c -- uses do loop to */
/* check elapsed time */
#include <time.h>
main()
{
long start, end, /* starting and ending times */
/* measured in seconds since */
/* Jan. 1, 1970 */
ltime; /* used to get val from time function */
int seconds; /* elapsed time to be set */
printf("QuickC Egg Timer\n");
printf("Enter time to set in seconds: ");
scanf("%d", &seconds);
start = time(<ime); /* get system elapsed seconds */
/* since 1-1-70 */
end = start + seconds; /* calculate alarm time */
do
{;} /* null statement for loop body */
while (time(<ime) < end); /* wait for alarm time */
printf("Time's Up!\a\a\a\n");
}
──────────────────────────────────────────────────────────────────────────
Listing 4-9. The TIMER.C program.
──────────────────────────────────────────────────────────────────────────
Quick Tip
Pascal programmers should note the similarity of C's do and Pascal's
repeat until loops. The difference is that the C do loop repeats the body
until the specified condition is false, whereas the Pascal loop repeats
the body until the condition is true.
──────────────────────────────────────────────────────────────────────────
This program lets you specify a time in seconds, after which the program
beeps three times and prints Time's Up! The program uses the noncore
library function time(), which, when given the address of a long type
variable, stores in that variable the number of seconds that have elapsed
since Jan. 1, 1970, as measured by your PC's clock. As you would expect,
the function also returns this value for use in the calling statement.
After the initial messages are printed and the user enters a number, the
program calls the time() function. Because this function, like scanf(),
requires an address as its parameter, you must call the function as
time(<ime), using the address operator & to specify the address of the
long variable ltime. The returned value, the elapsed seconds from Jan. 1,
1970 to the second the user enters a number, is assigned to the variable
start. We add the number of seconds specified by the user to this variable
and store the result in the variable end. This variable thus contains the
number of elapsed seconds at which the program will terminate.
The do loop then begins. Because this is a timer program and the user
wants to wait some period of time, the test does not need to be performed
before the body of the loop executes, so the do loop is appropriate. The
body of the loop is a null statement──all we want to do is wait. The test
while (time(<ime) < end) repeatedly calls the time() function and checks
the returned value until the elapsed time exceeds the value in end.
Debugging and Loops
It's rare for a program to work correctly the first time you run it.
Debugging is the art of knowing what to look for in a program that has
errors and of correctly interpreting what you see. Some common errors in C
programs that involve elements we have already discussed include:
■ Syntax errors
■ Uninitialized variables
■ Wrong or incompatible data types
■ Incorrectly specified loops
Throughout this book we point out common programming errors. Syntax errors
are the easiest to fix: The compiler enforces the rules of C syntax and
informs you when and where you have erred. (Sometimes, though, you must
sort out the real problems from the syntax errors that occur as a result
of an earlier error!)
True bugs are much harder to detect and fix because they cannot be
detected by QuickC. We'll define a "logic bug" as an error that does not
violate the rules of C but generates program results that are either
completely or partially incorrect. For example, C contains no rule that a
variable must be initialized. There is no rule preventing you from
assigning the result of a double calculation to an int variable. And
loops, with their sometimes complex conditions and specifications, offer
plenty of opportunity for bugs, such as the problem that arises when you
use a semicolon after a for loop specification.
Until recently, debugging a C program was a tedious process that involved
putting printf() statements in strategic parts of a program to reveal the
values of key variables or the order in which program statements executed
(or both). Then came programs called "debuggers" that could run and report
on a C program. (If you bought QuickC with Microsoft C 5.0, you also
received CodeView, a sophisticated debugger.) QuickC represents the next
step in the evolution of debugging: The debugging features are built into
the QuickC environment itself. (QuickC's debugger only works in the medium
model.)
The BUGS.C program (Listing 4-10) is a bug-ridden program that we will
fix using the QuickC Debug menu and facilities. It features a while loop
and is supposed to let the user enter as many as five numbers and get
their total and average.
Type this program exactly as shown and run it. (If you spot some bugs
along the way, give yourself a star. But please type the program as shown
so you can step through the debugging exercise properly.) When you run the
program, this is what happens:
Continue (y/n)? y───────────────────────────────────────────────Type `y'
Enter a number:
Enter a number:
Enter a number:
Enter a number:
Enter a number:
──────────────────────────────────────────────────────────────────────────
/* bugs.c -- for practice with debugger */
main()
{
char response;
int number, max_numbers = 5, count = 0, total = 0;
float average;
printf("Continue (y/n)? ");
response = getche();
while ((response != 'n') && (count < max_numbers))
printf("\nEnter a number: ");
scanf("%d", &number);
total += number;
printf("Continue (y/n)? ");
response = getche();
average = total / count;
printf("\nTotal is %d\n", total);
printf("Average is %f\n", average);
}
──────────────────────────────────────────────────────────────────────────
Listing 4-10. The BUGS.C program.
The program is running out of control. To break out of it and return to
the QuickC environment, press Ctrl-Break. Now the screen displays the
following messages:
Enter a number:
Enter a number:
E^C
run-time error R6014 : (1 of 1)
- control-BREAK encountered
Program returned (255). Press any key
Press a key to return to QuickC. (You can ignore this error message.)
Now you need to figure out which bug or bugs caused the program to fail.
Because the program uses a while loop, it seems likely that something is
causing one statement of the loop to be repeated endlessly. To debug the
program, you must first select the Compile menu and then select the Debug
option. (The Debug menu is shown in Figure 4-7.) This tells QuickC to
gather debugging information as it compiles the program. Now select Build
Program from the bottom of the Compile menu to recompile the program with
debug information. Then go to the Debug menu and select Trace On. (This is
a toggle setting──select it to turn it on, denoted by a check mark to the
left of Trace On; select it again to turn it off.) Trace On highlights the
statement currently being executed (in color if you have a color display).
This lets you easily follow the flow of the program as it executes.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 4-7 can be found on p.117 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 4-7. The Debug menu.
Another option in this menu is Screen Swapping. When you turn on Screen
Swapping, QuickC alternates between the output screen and the QuickC
environment screen as every statement executes. This causes a flickering
effect that can be annoying. Even with Screen Swapping off, QuickC shows
the output screen whenever any statement produces output or requests
input. We suggest you leave Screen Swapping off. Finally, select Start
from the Run menu to run the program.
As the program runs, notice that the statement currently being executed is
highlighted. (If you have a color monitor, you can change colors by
selecting Options from the View menu.) The first printf() statement
executes and is followed by the scanf() statement that solicits a
response. Type y to continue.
After that, the program runs away. Notice that the loop specification line
and the next printf() line execute continually. Do you see why? We didn't
use braces to mark the beginning and end of the loop body. Therefore, the
printf() line is executed as the body of the loop. Because this statement
doesn't get or change values for either of the loop's two control
variables, response and count, the loop test never becomes false, and the
loop never terminates. Now you can stop the program (with Ctrl-Break) and
insert the braces before and after the indented lines.
This example illustrates how easy it is to use the QuickC debugger. You
simply turn on the debugging features, observe the problem, and go back to
the program to fix it. Because everything is done in the same QuickC
environment, you don't have to save or reload any files.
──────────────────────────────────────────────────────────────────────────
Controlling the Debugger from the Keyboard
It is often easier to use keyboard commands rather than menu selections to
debug a running program. You can use the following QuickC keyboard
commands while debugging a program:
Function Key Result
──────────────────────────────────────────────────────────────────────
F8 Execute next statement, trace through function
F10 Execute next statement, trace around function
F7 Execute until current cursor position is reached
F4 Display the output screen
──────────────────────────────────────────────────────────────────────
(When a statement calls a function you have defined elsewhere, F8 traces
through the definition of the function. F10, on the other hand, does not
detour to trace a called function.)
──────────────────────────────────────────────────────────────────────────
Now run the program again. As the highlight moves on the screen, notice
that the whole body of the loop executes. That's an improvement. You now
can run BUGS.C and enter a series of numbers to be totaled and averaged.
Let's say you enter three numbers──8, 12, and 10. This is what happens:
Continue (y/n)? y
Enter a number: 8
Continue (y/n)? y
Enter a number: 12
Continue (y/n)? y
Enter a number: 10
Continue (y/n)? n
run-time error R6003
- integer divide by 0
Program returned (255). Press any key
Clearly the program still doesn't work right. A look at the listing shows
that the program is supposed to add each new number to total and, after
the last number is entered, divide total by count to get average.
Apparently count is still zero when the loop exits, thus triggering the
divide by zero error. Why? To find out, let's use another feature of the
QuickC debugger, "watch variables."
Move the cursor in the text area to the variable name count. Select the
Debug menu again, and then select Add Watch. The window shown in Figure
4-8 appears. The Watch window is a device that lets you designate program
variables for QuickC to monitor. When the value of one of these variables
changes, QuickC displays its new value in a window at the top of the
screen. This eliminates the need to put extra printf() statements in your
program to monitor variables.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 4-8 can be found on p.119 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 4-8. Adding a watch variable.
Because you already selected it with the cursor, the word count appears in
the window. Simply select OK to add count to the list of watch variables.
(To remove watch variables later, select Delete Last Watch or Delete All
Watch from the Debug menu.)
You can also specify another display format for the value of the watch
variable by adding a comma and a format specifier to the variable──for
example, count,d──which specifies an integer format display for count. The
format specifiers are similar to those you used with printf() and scanf().
Next, select Add Watch twice and add the variables number and total to the
watch list. (Either position the cursor on the names or type them in the
dialog box.)
Finally, let's set a breakpoint. This is a place in the program at which
execution will stop. This lets you examine the status of the watch
variables. Move the cursor to the last line in the body of the while
loop──response = getche();──and choose Toggle Breakpoint (or press F9)
from the Debug Menu. (Select the toggle again with the cursor on the same
line to remove the breakpoint, or choose Clear All Breakpoints. You can
set any number of breakpoints.)
Now you're ready to run the program again, so choose Start from the Run
menu. Again, the program prompts you for numbers, and you can watch the
statements in the while loop as they execute. The program stops at the
breakpoint at the end of the body of the loop. At the top of the screen, a
small window lists the watch variables and their current values. After you
inspect them, select Continue from the Run menu (or press F5) to resume
program execution. (Figure 4-9 shows the screen display with the current
statement and breakpoint highlighted; the watch variable information
window is at the top.)
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 4-9 can be found on p.120 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 4-9. Debugging in progress.
As the loop cycles, notice that number accepts the value of the number you
entered, and total grows as you add new numbers. But count always remains
zero. Have you figured out why? We forgot to put a statement in the body
of the loop that increments count. Adding count++; after total += number;
completes our debugging of the program.
You can do more complex things with the debugger, so be sure to read
Chapter 8 of the Microsoft QuickC Programmer's Guide for more information.
For example, when you learn about arrays and structures, you can use watch
variables to display them, too. Meanwhile, you also can use the debugger
as a learning tool for tracing the flow of programs in this book, the
sample programs provided by Microsoft, or other C programs.
────────────────────────────────────────────────────────────────────────────
Chapter 5 Decisions and Branching
All programming languages must be able to perform controlled "branching."
Branching uses the result of a test or condition to determine which
statement (or group of statements) will execute next. In this chapter we
discuss the variations of branching in C and learn how to use them with
looping statements.
The if Statement
In C, as in most languages, the if keyword introduces a branching
statement. The following structure is the simplest form of branching:
if (condition)
statement(s);
An if statement, like a while loop, evaluates a condition first. The
condition can be any combination of values and relational or logical
operators that yields a true (nonzero) or false (zero) value──answer ==
`y', for example. If the condition is true, the following statement (or
group of statements in braces) executes. (As with loops, the statement or
statements controlled by the condition are called the "body" of the
statement.) If the condition is false, the following statement or group of
statements does not execute, and execution continues with the next
statement or group of statements. A simple example follows:
if (balance < 0)
printf("Your account is overdrawn!\n");
printf("Your current balance is %8.2f\n", balance);
If the customer's balance is less than zero, the first printf() statement
executes, telling the customer the account is overdrawn; then the second
printf() statement, which prints the current balance, executes. If the
customer's balance is zero or more, the condition is false, and the first
printf() statement does not execute──the program skips it. Only the second
printf() statement executes.
In the above example, we indent the first printf() statement to show that
the if controls it──it is the body of the if statement. (In C, we indent
statements for our benefit only: The compiler doesn't require indention.)
Always enclose the condition in parentheses, and do not use a semicolon
directly after the parentheses because the complete if statement includes
the if, the condition, and the statement body.
The IF.C program (Listing 5-1) features the if statement.
──────────────────────────────────────────────────────────────────────────
/* if.c -- simple IF statement */
char ch;
main()
{
printf("Do you want to continue y/n? "); /* prompt */
if (ch = getche() == 'y')
printf("\nLet's continue ...\n"); /* if true */
printf("\nAll done.\n"); /* always executed */
}
──────────────────────────────────────────────────────────────────────────
Listing 5-1. The IF.C program.
──────────────────────────────────────────────────────────────────────────
Quick Tip
If you know BASIC or Pascal, note that C does not use the then keyword
before the body of the if statement. Most other languages use the
following form for the if statement:
if (condition)
then statement(s)
If you mistakenly use then with if, the QuickC compiler will catch the
error, of course, and you will soon stop making it.
──────────────────────────────────────────────────────────────────────────
IF.C asks the user if he or she wants to continue. The test expression ch
= getche() == `y' gets the response character, assigns it to ch, and tests
it. The program generates one of two responses:
Do you want to continue y/n? y──────────────────────────────User types y
Let's continue ...
All done.
Do you want to continue y/n? n─────User types any character other than y
All done.
Note that the program prints Let's continue ... only if the user types y;
however, it always prints All done.
The if statement represents a fork in the road: One of two possible
courses is followed, depending on the result of the test. The flowchart in
Figure 5-1 depicts such a branch, with the test shown in a diamond-shaped
box.
┌────────────────►────────────────┐
No │ │
┌───────────┐ /\ ┌───────────┐ ┌─────▼─────┐
│ printf │ / \ │ printf │ │ printf │
│ ("Do you │ / if \ │ ("\nLet's │ │ ("\nAll │
│ want to ├──►/ch=getche() \───►│ continue ├───►│ done.\n");│
│ continue │ \ =='y') / │ ...\n"); │ │ │
│ y/n"); │ \ / │ │ │ │
│ │ \ / │ │ │ │
└───────────┘ \/ └───────────┘ └───────────┘
Figure 5-1. Flowchart for the if statement.
Comparing if and while
Notice the structural similarity of the following two statements:
if (score > 90)
printf("Excellent!");
and
while (question <= total_questions)
ask_question();
Both statements test a condition and (if the condition is true) execute
the following statement. The important difference between the two
constructions is that the if statement executes the body of the statement
only once; but the while statement executes the body repeatedly (as long
as the test continues to be true).
If you think about the two statements, you can see why each is appropriate
for its assigned task. A statement that prints the final score of a quiz
needs to be executed only once. On the other hand, a statement that calls
a function that asks the next question in the quiz must be executed
repeatedly.
Using a Group of Statements with an if
The body of an if statement can contain any number of statements. Consider
the following example:
if (choice == 'd')
{
printf("How much do you want to deposit? ");
scanf("%f", &deposit);
balance += deposit;
printf("Thank you. Your new balance is ");
printf("%8.2f", balance);
}
When choice is `d', the program executes all five statements between the
braces. Notice that we use the same indention for the braces and the
statements.
Nested if Statements
Just as the body of a loop can contain another loop, the body of an if
statement can contain another if statement. For example, a simple text
formatter might use the following code fragment:
if (pos == line_length)
if (++line_count > lines_page)
{
print_footer;
putch(FF);
++page_number;
print_header;
}
If the first if statement is true (the character position equals the line
length), the program executes the body of the statement, which is itself
an if statement. This statement increments line_count by one, and if the
result is greater than lines_page, the body of the inner if statement
executes. These statements print a footer, output a "form feed" character,
add one to the page number, and print a header for the next page. (Because
we must repeatedly test for the end of line and the end of page, we would
actually place these if statements inside a while loop. As you might
expect, you will often use if statements inside loops, and we will show
you examples of these later in this chapter.)
Providing Alternatives with else
The if statement has an adjunct──else──that is useful for executing a
statement or group of statements only if the given condition is false. The
general form of the if-else statement is simply an extension of the simple
if statement:
if (condition)
statement(s);
else
statement(s);
Consider the following example:
if (age >= 18)
{
printf("To vote, enter number of candidate: ");
scanf("%d", &candidate);
}
else
printf("Sorry, you must be at least 18 to vote.\n");
The first group of statements executes only if age is greater than or
equal to 18. The statement following else executes only if that condition
is false. If-else statements let you provide appropriate responses for
both true and false results.
Note that we align the else with its corresponding if because together
they form one if statement of two parts. Correspondingly, we also indent
the statement(s) controlled by the else to match the statements under the
if.
The IFELSE.C program (Listing 5-2) uses an if statement with an else to
simulate the logon sequence for a bulletin board system.
──────────────────────────────────────────────────────────────────────────
/* ifelse.c -- IF with ELSE */
char ch;
int num;
main()
{
printf("Are you a new user? y/n? ");
if (ch = getche() == 'y')
{
/* executed if IF is true */
printf("\n\nYou must register to use this\n");
printf("bulletin board. Please read\n");
printf("Bulletin #1 first. Thank You.\n");
}
else
/* executed if IF is false */
{
printf("\n\nEnter your secret number: ");
scanf("d", &num);
}
}
──────────────────────────────────────────────────────────────────────────
Listing 5-2. The IFELSE.C program.
If the user replies y to the question Are you a new user?, the statements
following the if execute. If the user types n (or anything else), the
statements following the else execute instead.
Here is a sample dialogue:
Are you a new user? y/n? y
You must register to use this
bulletin board. Please read
Bulletin #1 first. Thank You.
Are you a new user? y/n? n
Enter your secret number: 31415
Matching an else to an if
As you write more advanced programs, you will need to use more complex if
statements, such as:
if (temp < 900)
if (temp > 750)
printf("Warning! Boiler overheating!\n");
else
printf("Start emergency shutdown!\n");
This program is meant to check the temperature and print a warning if the
temperature is between 750 degrees and 900 degrees, or print an emergency
warning if the temperature is greater than 900 degrees. It might look
correct, but it's not.
When this if statement actually executes, it prints nothing if the
temperature exceeds 900, and it prints the emergency warning if the
temperature is less than 750! We actually want the else to go with the
outer if to print the emergency warning only if the temperature exceeds
900. However, although we physically aligned the else so that it appears
to go with the outer if, the compiler reads the statement differently. It
considers the else to belong to the inner (nested) if.
Always remember that QuickC matches a given else with the preceding
unenclosed if that doesn't already have an else. Now that we understand
this rule, we can fix the program by enclosing the inner if in braces so
that the else is not attached to it:
if (temp < 900)
{
if (temp > 750)
printf("Warning! Boiler overheating!\n");
}
else
printf("Start emergency shutdown!\n");
The Conditional Assignment Statement ?
Compared to BASIC or even Pascal, C might seem to be a sparse language
that provides the essential tools for programming but few frills. However,
we've already seen several elements (such as the special increment and
decrement operators) by which C provides "shorthand" expressions to
simplify commonly encountered programming chores. Another such common
programming task is assigning one of two values to a variable, depending
on the result of a test. For example, suppose we want to set the variable
max to the larger of the values of the variables n1 and n2. Of course, we
can use an if and else, as follows:
if (n1 > n2)
max = n1;
else
max = n2;
But we can also use C's conditional assignment statement to do the job──
and in one line of code. The general form for the conditional assignment
statement is:
variable = (expression) ? value1 : value2;
QuickC evaluates the expression in parentheses first. In this form of
assignment statement, if the expression is true (nonzero), value1 is
assigned to variable; if the expression is false (zero), value2 is
assigned to variable. Note that a question mark follows (expression) and a
colon (:) separates the two values.
We can now rewrite our earlier statement for assigning a value to max as
follows:
max = (n1 > n2) ? nl : n2;
│ │ │ └─────────────────────────────── Assign if false
│ │ └───────────────────────────────────── Assign if true
│ └────────────────────────────────────────── Expression to test
└──────────────────────────────────────────── Variable to receive value
This translates as "If n1 > n2, then assign the value of n1 to max;
otherwise, assign the value of n2 to max." Although this statement might
look odd, it's easy to use and quite handy.
Assigning Truth Values
If the two possible values for a variable are actually "true" and "false,"
you don't need to use the conditional assignment statement. Simply assign
the result of the expression to the variable. For example,
frozen = (temp <= 32)
sets the value of frozen to true (nonzero) if the temperature is less than
or equal to 32 and sets it to false otherwise.
The SHORTIF.C program (Listing 5-3 on the following page) illustrates
some shorthand and conditional assignments.
──────────────────────────────────────────────────────────────────────────
/* shortif.c -- shows 'shorthand' IF / ELSE */
/* -- gets absolute value of number */
main()
{
int num, pos, abs;
printf("Enter a whole number: ");
scanf("%d", &num);
pos = (num >= 0); /* is number positive? */
abs = (pos) ? num : -num; /* assigns negative of */
/* number if number is negative */
if (pos)
printf ("The number is positive.\n");
else
printf("The number is negative.\n");
printf("Absolute value of number is: %d\n", abs);
}
──────────────────────────────────────────────────────────────────────────
Listing 5-3. The SHORTIF.C program.
First, the program gets a number from the user. Then it tests the number
to see if it is positive. Notice we do this by assigning the result of the
expression (num >= 0) to the variable pos. This value now contains "true"
if the number is positive, or "false" if it is not. (Remember that these
are actually numeric values, 1 and 0, that have the logical effects of
"true" and "false" when used in tests.)
Next, the program uses a conditional assignment statement to calculate the
absolute value of the number. (The absolute value of a number is its value
disregarding its sign. Thus, both 5 and -5 have an absolute value of 5.)
Recall that a conditional assignment statement assigns one of two values
to a variable based on the truth result of an expression. However, you can
also use a single variable that has a truth value instead of an
expression. Because the variable pos was assigned a truth value earlier,
we can use it here as the test for the conditional assignment.
Now let's look at the assignment statement and the if-else branches. If
the entered number (num) is positive, pos contains "true," and the
statement assigns num to the absolute value abs. In other words, the
absolute value of a positive number is simply the number itself.
If num is negative, however, then pos contains "false," and the statement
assigns the second value, -num, to abs. The negative of a negative number
is a positive number and, therefore, the absolute value. The examples on
the following page demonstrate the output when the program is run twice──
first with a positive number and then with a negative number.
Enter a whole number: 23
The number is positive.
Absolute value of number is: 23
Enter a whole number: -58
The number is negative.
Absolute value of number is: 58
Multipath Branching
Thus far, we've discussed simple branches (the single if) and two-way
branches (the if and else). Simple branches are most useful for testing a
condition that can have only one of two values──typically "true" and
"false." But what about those situations in which you must test for one of
several values? This commonly occurs in a menu from which a user must
choose one of several items.
Consider, for example, a program that offers the user a choice of readings
from a home weather station. Let's say the user can choose among
temperature, humidity, pressure, and wind velocity. Here's one way we
could set up the menu:
printf("Enter reading wanted: t = temp h = humidity\n");
printf("p = pressure w = wind velocity ");
ch = getche();
if (ch == 't')
printf("Current temperature is %5.2\n", temp)
else
if (ch == 'h')
printf("Humidity is %4.2f\n", humidity);
else
if (ch == 'p')
printf("Air pressure is %5.2f\n", pressure);
else
if (ch == 'w')
printf("Wind velocity is %d\n", wind);
else
{/* default */
printf("Invalid choice. Choose ");
printf(" t, h, p, or w.\n");
}
This chain of if statements, each hooked to the preceding statement's
else, will work, but it has many disadvantages. Its many levels of nesting
are difficult to read. Also, the many indentions run the code off the edge
of the screen, requiring awkward line breaks.
This type of code also creates a conceptual problem. The structure of
these statements suggests that each if statement is dependent on all of
the preceding if statements. This suggests that we are checking for some
kind of special case that is true only if all the ifs in the series are
true. But nothing is further from the truth──we merely want to compare ch
to four possible values and provide a branch for each value. (We also need
a default branch to handle invalid user-entry values.)
We can improve the visual organization of our branches in the following
manner:
if (ch == 't')
printf("Current temperature is %5.2\n", temp)
else if (ch == 'h')
printf("Humidity is %4.2f\n", humidity);
else if (ch == 'p')
printf("Air pressure is %5.2f\n", pressure);
else if (ch == 'w')
printf("Wind velocity is %d\n", wind);
else { /* default */
printf("Invalid choice. Choose ");
printf("t, h, p, or w.\n");
}
This arrangement is clearer and more compact. All of the branches now have
the same level of indention, showing that they are co-equal and not
dependent on each other. However, it is important to note that else if is
not a distinct command: Changing indention doesn't change the way the
compiler handles this code.
The switch Statement
C offers a special switch statement that makes writing multiple branches
much easier. The general form of switch follows:
switch (variable)
{
case 'constant1':
statement(s);
break;
case 'constant2':
statement(s);
break;
case 'constant_n':
statement(s);
break;
default:
statement(s);
}
Specify the name of the variable to be tested in parentheses after the
word switch. As with the other loops and branches, don't use a semicolon
at the end of the first line: The entire structure comprises one
statement.
The body of the switch statement (enclosed in braces) is a list of
possible branches. Each branch consists of the word case followed by a
constant value (a number or character) in parentheses. During execution,
this constant is compared with the switch variable: If they are equal, the
statements for that case execute. Note that single quotes enclose each
constant, and the line ends in a colon.
One or more statements follow each case line. (Do not enclose a group of
statements in braces──the compiler handles all statements under a given
case as a single unit.) The last statement in each branch is the keyword
break. The break statement immediately ends execution of the switch
statement; program execution resumes at the statement that follows the
body of the switch statement. Usually you will conclude each case in a
switch statement with the keyword break. If a switch statement behaves
erratically, look for missing break statements in the individual cases.
Also, include a default: to handle invalid values.
Sometimes, however, you will want to execute a set of statements if the
switch variable has any one of several values. You can do this by placing
the set of statements after a series of switch values, as in the
following:
switch (ch)
{
case 'q':
case 'Q':
show_score();
end_game();
break;
case ...
}
No statement is associated with 'q', so execution falls through to the
code for 'Q', the show_score() and end_game() functions execute, and break
is encountered.
A switch statement can contain any number of branches, which is why the
last branch in our format description uses the notation constant_n. A
special case, default:, is an optional branch that is usually placed after
the last explicit case in the switch statement. It specifies the branch
that executes if none of the conditions for the other cases match the
value of variable. Although default: is optional, programmers frequently
use it to respond to erroneous values, such as an invalid choice.
Let's use the switch statement to rewrite our weather station menu:
switch (ch)
{
case 't':
printf("Current temperature is %5.2\n", temp)
break;
case 'h':
printf("Humidity is %4.2f\n", humidity);
break;
case 'p':
printf("Air pressure is %5.2f\n", pressure);
break;
case 'w':
printf("Wind velocity is %d\n", wind);
break;
default:
printf("Invalid choice. Choose ");
printf("t, h, p, or w.\n");
}
Figure 5-2 on the following page illustrates this switch statement. The
multiple branches suggest tracks in a railroad switching yard, the
probable origin of the name.
┌───────┐ ┌──────────┐
┌───────────►│ 't' │───►│printf... │
│ │ │ │break; │
│ └───────┘ └──────────┘
│ ┌───────┐ ┌──────────┐
│ ┌──────►│ 'h' │───►│printf... │
│ │ │ │ │break; │
Value of ▬ ▬ ──────┘ │ └───────┘ └──────────┘
┌────────┐ ▬ ────────┘ ┌───────┐ ┌──────────┐
│ │ │ ▬ ─────────────►│ 'p' │───►│printf... │
switch │ ( ch ) │ * ▬ │ │ │break; │
│ │ ▬ └───────┘ └──────────┘
└────────┘ ▬ ────────┐ ┌───────┐ ┌──────────┐
▬ ▬ ──────┐ └──────►│ 'w' │───►│printf... │
│ │ │ │break; │
│ └───────┘ └──────────┘
│ ┌───────┐ ┌──────────┐
└───────────►│default│───►│printf... │
│ │ │break; │
└───────┘ └──────────┘
Figure 5-2. The switch statement.
The break Statement
The break statement has other uses than as the last statement in each case
of a switch statement. A break statement can also be used with the three
looping statements: (for, while, and do). In all cases, however, break has
the same effect──it immediately "breaks out of" the enclosing structure
and causes execution to resume after the end of the switch or loop
structure. The BREAK.C program (Listing 5-4) uses break to exit from a
while loop:
This program uses the rand() library function to generate a series of
random numbers. On each pass through the while loop, the program generates
and displays one random number in the range 0 through 32,767. The
statement
if (number < 32000)
break;
terminates the while loop if the program generates a random number greater
than 32,000. The output might look something like the following:
41
18467
28145
16827
491
2995
11942
5436
32391
Broken out of WHILE loop.
──────────────────────────────────────────────────────────────────────────
/* break.c -- shows how to get out of loop with BREAK */
#include <stdio.h>
#define TRUE 1
main()
{
int number;
while (TRUE) /* endless loop */
{
/* get a random number between 0 and 32767 */
number = rand();
printf("%d\n", number);
/* break out of loop if random number */
/* is greater than 32000 */
if (number > 32000)
break; /* exit WHILE loop */
}
printf("Broken out of WHILE loop.\n");
}
──────────────────────────────────────────────────────────────────────────
Listing 5-4. The BREAK.C program.
The last value, 32391, triggered the break statement. The while loop
terminates, and the printf() statement following the body of the while
loop executes.
The SWITCH.C program (Listing 5-5 on the following page) shows you how to
create a simple menu using a while loop containing a switch statement. The
program asks the user to select one of four math routines (octal
representation, hex representation, square, or square root), prompts for a
number to be converted, and prints the result. The user types q to exit
the program.
Following is a sample dialogue with SWITCH.C:
Select a math routine:
o = octal h = hex s = square
r = square root q = quit: o
Enter a whole number: 30
Result: 36
Select a math routine:
o = octal h = hex s = square
r = square root q = quit: r
Enter a whole number: 10
Result: 3.162278
Select a math routine:
o = octal h = hex s = square
r = square root q = quit: q
Program returned (113). Press any key
──────────────────────────────────────────────────────────────────────────
/* switch.c -- demonstrates switch statement */
/* prints values according */
/* to user's choice */
#include <math.h> /* for sqrt() */
#define TRUE 1
main()
{
char choice; /* routine wanted by user */
int number; /* number entered by user */
while (TRUE) /* endless loop */
{
printf("\nSelect a math routine:\n");
printf("o = octal h = hex s = square\n");
printf("r = square root q = quit: ");
choice = getche(); printf("\n");
if (choice == 'q')
break; /* exits WHILE loop; ends program */
/* rest of program executed if choice <> 'q' */
printf("Enter a whole number: ");
scanf("%d", &number);
switch (choice) /* print according to */
/* choice requested */
{
case 'o': /* print octal */
printf("Result: %o\n", number);
break; /* break here in each case */
/* exits the switch statement */
case 'h': /* print hex */
printf("Result: %x\n", number);
break;
case 's': /* square */
printf("Result: %d\n", number * number);
break;
case 'r': /* square root */
printf("Result: %f\n", sqrt(number));
break;
default:
printf("Choice must be o, h, s, r, or q\n");
}
}
}
──────────────────────────────────────────────────────────────────────────
Listing 5-5. The SWITCH.C program.
We enclose the menu in an endless while loop because the user will be
making choices indefinitely. Notice that we use while (TRUE) instead of
while (1). Both have the same effect, but the code is clearer when we use
a #define statement to make TRUE equal to 1 and use the descriptive name
in the program.
After the program displays the menu and getche() gets the user's choice,
an if with a break statement tests for the possibility that the user wants
to quit. If the user quits, the while loop terminates and the program
ends.
If the user did not enter q, the program obtains the number to be
processed. A switch statement then processes the number. The constants in
the various cases correspond to the menu options so the switch statement
can match the user's choice with the appropriate case. If the choice is
`o' or `h', a format specifier returns the appropriate value; if the
choice is `s' or `r', the value is calculated.
The default: case handles any value not specified in the menu by printing
a list of valid values. Note that the default: case needs no break because
there are no further statements in the switch statement that can be
executed.
switch vs if-else
Using switch gives you a structure that is at least as clear as the series
of else ifs shown earlier, because each case is clearly distinct. The
switch has the additional advantage that the variable to be compared is
stated clearly once, at the beginning of the structure, rather than being
buried inside the individual tests.
Whether you decide to use switch or the if-else form is a matter of style.
A good rule of thumb is to use switch whenever four or more possible
values (including a default) are involved. Some programmers argue that
switch is clearer for even three possible branches.
There is, however, one situation in which you cannot use switch──even when
many branches must be used. A switch statement can be used only to test
simple constant values. You cannot, for example, do the following:
switch (expenditure)
{
case < 10.00:───────────────────────Expression is illegal for switch
printf("petty cash");
break;
case < 100.00:
printf("see office manager");
break;
case < 500.00:
printf("see district manager");
break;
default:
printf("see head office");
}
Unlike many versions of the Pascal case statement and similar structures
in other languages, the C switch statement cannot compare a value against
ranges of values. It also cannot be used with relational expressions. For
these programming tasks, you must use multiple if-else structures.
The continue Statement
Under some conditions we might need to skip some of the statements in the
body of a loop and return to the loop's test condition. For example, if a
program offers a menu operation that has potentially irrevocable
consequences (such as overwriting the contents of a file), you might want
to ask Do you really want to overwrite this file? If the user answers no,
the program must skip the remaining statements and return to the menu. The
continue statement lets you do this.
A continue statement takes the following general form. We illustrate it
here with a while loop, although you can use it in any kind of loop (but
not a switch).
while (condition)
{
some statements;
if (condition)
continue;
rest of statements;
}
The if statement tests a condition as usual: If its condition is true, the
continue executes. This restarts the loop before the rest of the
statements in the body of the loop execute, and the while loop condition
is tested again.
The CONTINUE.C program (Listing 5-6) uses a simple example of a continue
statement. The program also illustrates a "toggle switch" variable, sw. A
toggle switch changes to the opposite of its current value each time you
use it. If it's "on," the next time you use it you turn it off.
The body of the endless while loop first prints out the current status of
the sw switch. Next, the program uses a break statement to give the user
an opportunity to quit. The program then asks the user whether the switch
should be toggled. If the answer is not `y,' a continue statement skips
the last statement in the loop body and the switch is not toggled. If the
answer is `y,' the continue doesn't execute, and the last statement sw =
!sw toggles the switch. (Recall that the ! operator reverses the truth
value of the associated variable.)
The next program, M.C (Listing 5-7), demonstrates various combinations of
for loops and if statements and includes a continue statement. The program
draws a letter M within the dimensions specified in the #define statements
at the beginning of the program.
──────────────────────────────────────────────────────────────────────────
/* continue.c -- shows CONTINUE in a loop */
main()
{
int sw = 0;
char ch;
while (1) /* endless loop */
{
/* print current status */
if (sw)
printf("\nSwitch is ON\n");
else
printf("\nSwitch is OFF\n");
printf("Do you want to quit? ");
if (ch = getche() == 'y')
break; /* exit loop on yes */
printf("\nDo you want to toggle the switch? ");
if (ch = getche() != 'y')
continue; /* restart loop on no */
sw = !sw; /* toggle switch */
}
}
──────────────────────────────────────────────────────────────────────────
Listing 5-6. The CONTINUE.C program.
──────────────────────────────────────────────────────────────────────────
/* m.c -- draws a letter M */
/* using IF and CONTINUE */
/* define characters */
#define CH 'M' /* character to "draw" with */
#define BLANK ' '
#define NL 10
#define CR 13
#define LEFT 20 /* left side of M */
#define RIGHT 46 /* right side of M */
#define BOTTOM 22 /* last line to use */
main()
{
int pos, line;
/* space to left side */
for (line = 1; line <= BOTTOM; line++)
{
for (pos = 1; pos < LEFT; pos++)
{
putch(BLANK);
}
putch(CH); /* draw left side */
/* are we past midpoint? */
if (line > ((RIGHT - LEFT) / 2))
{
/* yes, so just draw right side */
for (pos = LEFT; pos < RIGHT; pos++)
{
putch(BLANK);
}
putch(CH);
putch(NL);
putch(CR);
continue; /* start loop over, do next line */
}
/* not past midpoint, check for interior */
for (pos = LEFT; pos < RIGHT; pos++)
{
if ((pos == (LEFT + line)) ||
(pos == (RIGHT - line)))
putch(CH);
else
putch(BLANK);
}
putch(CH);
putch(NL);
putch(CR); /* could also use printf("\n"); */
}
}
──────────────────────────────────────────────────────────────────────────
Listing 5-7. The M.C program.
The M.C program generates the following output:
M M MM
M M M M
M M M M
M M M M
M M M M
M M M M
M M M M
M M M M
M M M M
M M M M
M M M M
M M M M
M M M
M M
M M
M M
M M
M M
M M
M M
M M
M M
Drawing the letter M involves drawing two distinct sections──the V-shaped
inner part and the straight-sided outer part. The overall control for
drawing all the individual lines resides in the outermost for loop. Each
line is started by a small for loop that moves to the left side of the M
and draws the character "M" there.
What happens next depends on whether the current line number is in the top
or bottom part of the M. We determine this by testing to see if the
distance down the screen in lines is greater than half the distance across
the M in characters. (This test is arbitrary. Feel free to try other
formulas and to vary the size of the M by changing the #define
directives.)
If we are not below the bottom of the V part of the M, the for statement
in the body of the if moves to the right side, which is then drawn. The
continue (which doesn't require an if here) then skips the rest of the
statements, which aren't needed.
If the if statement is false, we are still in the upper portion of the M:
The body of the if is skipped, and the rest of the statements in the body
of the outer loop execute. The if statement works on the principle that
the inner lines of the M are drawn one space further to the right and to
the left from the sides of the M for each line further down the screen.
Thus, the appropriate positioning for the inner lines is found by adding
and subtracting the current line number.
The goto Statement
C's goto statement transfers control to the line containing the specified
label. For example, you might use goto with an if statement, as follows:
printf("Do you want to continue? \n");
if (ch = getche() == 'y')
goto yes;
printf("Goodbye\n")
goto end;
yes: printf("Let's continue ...\n");
end:
Here, if the user enters y, the goto immediately causes execution to skip
to the printf() statement that follows the label yes: (which must end with
a colon). If the user does not enter y, the second goto skips the "yes"
branch.
──────────────────────────────────────────────────────────────────────────
The Appropriate Use of continue
The continue statement is rarely used in C programs. Often (in Microsoft
C, for example), continue's function is handled by an else branch for the
relevant if statement or by a new if statement. However, if you have a
complicated, multiple-nested set of if-else statements, using a continue
statement might simplify things.
──────────────────────────────────────────────────────────────────────────
If this looks confusing, that's because it is. You can do the operation
much more clearly with an if and an else:
printf("Do you want to continue? \n");
if (ch = getche() == 'y')
printf("Let's continue ...\n");
else
printf("Goodbye\n");
Nearly all contemporary computer scientists discourage the use of goto
because it obscures program logic and makes code difficult to decipher, as
anyone who has ever tried to debug an old-style BASIC program knows. If
your programming background is in the older versions of BASIC or FORTRAN,
resist the impulse to use goto statements. Examine the logic of your
program: You probably will see, as in the above example, that an if-else
with appropriate conditions (or a switch, break, or continue) lets you
express the operation more clearly. That's why you can go for months
without encountering a goto in C programs. (An occasional exception is the
goto that breaks out of a multiple-nested loop. You can't use break in
this situation because it only breaks out of the current loop. But even in
this case, you can avoid a goto by redesigning the program structure to
use "flag" variables. We will discuss flag variables later.)
More Complex Conditions for Branching
Because we have been concentrating on the mechanics of branching, we have
used only simple test conditions in our branching statements. In the last
chapter, we showed you how to use logical operators (&& and ||) to create
multiple conditions for controlling loops. You can also use these compound
conditions to control the execution of if statements.
The next program, PIXELS.C (Listing 5-8), introduces the QuickC Graphics
Library. It generates random positions for pixels (points of light on the
screen) and uses a compound condition to display only selected pixels.
Running the Program
We've already included header files in several of our programs. The
machine code represented by these header files was in the standard library
(such as MLIBCE.LIB, the medium memory model with floating-point
emulation), so all you needed to specify was #include and the appropriate
header file. QuickC knew where to find the default library.
To use graphics, however, you must specify the graph.h header file. The
definitions it contains reside in a separate Graphics Library,
GRAPHICS.LIB. You might need to tell QuickC where to find this library. As
with the TABLE.C program in the last chapter, you need a program list.
Create a program list called PIXELS.MAK and add PIXELS.C to it. (If you
are unsure how to proceed, reread "Creating a Program List" in Chapter
4.)
──────────────────────────────────────────────────────────────────────────
/* pixels.c -- creates shapes */
/* from random pixels */
#include <graph.h> /* for graphics */
main()
{
int pixels, xpos, ypos;
/* window coordinates */
int xmin = 100, xmax = 540;
int ymin = 50, ymax = 150;
srand(0); /* init random nums */
_setvideomode(_HRESBW); /* CGA 640 x 200 */
_setcolor(1); /* white foreground */
/* generate random pixel locations */
for (pixels = 1; pixels < 10000; pixels++)
{
xpos = rand() % 639;
ypos = rand() % 199;
/* set pixel if within window */
if ((xpos > xmin && xpos < xmax) &&
(ypos > ymin && ypos < ymax))
_setpixel(xpos, ypos);
}
getch(); /* freeze screen until key pressed */
/* restore original video mode */
_setvideomode(_DEFAULTMODE);
}
──────────────────────────────────────────────────────────────────────────
Listing 5-8. The PIXELS.C program.
You will need to add the Graphics Library (GRAPHICS.LIB) to the program
list as well unless you specified that the Graphics Library was to be
included in your standard library when you ran the SETUP program. (See
Chapter 2.)
For future programs in this book, we will not remind you to create a
program list. In general, if the program uses any standard functions that
are not part of the core library (listed in Appendix :ARB), you must
create a program list with the name of the program in it before you can
compile the program to memory. If you wish, you can simply try to compile
the program and create the program list if you get the error unresolved
external.
PIXELS.C starts by including the graph.h header file, which contains
definitions for graphics at every resolution and color supported by the
IBM graphics adapters (CGA, EGA, VGA, and so on, each with several modes).
We discuss graphics modes and graphics routines in Chapter 15. Here,
simply note that the _setvideomode() statement in PIXELS.C sets the video
mode to the constant _HRESBW, which represents the two-color
high-resolution CGA mode having a resolution of 640 pixels by 200 pixels.
The _setcolor statement sets the foreground color (the color of the
displayed pixels) to white.
The values xmin, xmax, ymin, and ymax contain the coordinate positions of
the screen "window" in which the program plots pixels. Figure 5-3 shows
the screen and the coordinates of the selected window, as well as some
sample output for the program.
The heart of the program is the for loop that plots 10,000 random pixel
positions. Notice that we use the % (modulus) operator to select values in
the range 0 through 639 for X, and 0 to 199 for Y. (This corresponds to
the 640-by-200 resolution for the specified CGA high-resolution mode.)
The if statement checks for an X and a Y position within the window
specified by xmin, xmax, ymin, and ymax. Notice that the && logical AND
operator ensures that each value is greater than or equal to the minimum
and less than or equal to the maximum. The && between the two expressions
in parentheses tests the random value to see if it fits in both the X and
Y ranges.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 5-3 can be found on p.144 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 5-3. Screen coordinates and output for PIXELS.C.
Variations of PIXELS.C
You can create many interesting shapes by substituting different
conditions in the if statement. The GALAX.C program (Listing 5-9)
establishes a center point (center_x, center_y) and a radius. The if
statement in the for loop uses a formula that determines if a point is
within the circle──if it is, the program plots the pixel.
The result of running GALAX.C (Figure 5-4 on the following page) looks
more like an ellipse than a circle because in the 640-by-200 mode, pixels
are spaced together more closely horizontally than they are vertically.
Because the program must calculate many square roots, it runs a little
slowly. We use an if (kbhit()) to let you stop the program whenever you
press a key.
──────────────────────────────────────────────────────────────────────────
/* galax.c -- creates an ellipse by selecting */
/* from random pixels */
#include <graph.h> /* for graphics */
#include <math.h> /* for sqrt() */
#include <conio.h> /* for kbhit() */
main()
{
int pixels, radius = 50;
double center_x = 320, center_y = 100,
xpos, ypos;
srand(0);
_setvideomode(_HRESBW);
_setcolor(1);
for (pixels = 1; pixels < 25000; pixels++)
{
/* draws filled ellipse, due */
/* to dimensions of hi-res screen */
/* generate random location */
xpos = rand() % 639;
ypos = rand() % 199;
if (sqrt /* is distance within radius? */
((xpos - center_x) * (xpos - center_x)
+ (ypos - center_y) * (ypos - center_y))
< radius)
_setpixel(xpos, ypos);
if (kbhit())
break; /* exit if key pressed */
}
getch(); /* freeze screen until key pressed */
_setvideomode(_DEFAULTMODE);
}
──────────────────────────────────────────────────────────────────────────
Listing 5-9. The GALAX.C program.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 5-4 can be found on p.146 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 5-4. Output of GALAX.C.
────────────────────────────────────────────────────────────────────────────
Chapter 6 Functions and Function Calls
One of the great advantages that C offers a programmer is its huge variety
of library functions, which cover everything from manipulating text to
controlling memory allocation. The seasoned C programmer soon learns that
the C library contains most of the tools needed to perform a given task.
However, the real power of C derives from the ease with which you can
design customized C functions that perform the specific and unique
operations your program requires. In this chapter, we will show you how to
create and use these functions.
Functions and Program Design
Every C program must have at least one user-defined function, namely
main(). Most real-world C programs, however, consist of many user-written
functions, because the procedure for processing data usually involves many
different steps. A program that calculates statistics, for example, might
have to ask the user for data, check the data for validity, store the data
in memory or on disk, process the data (often according to several
criteria), and report the results, possibly in a variety of formats. If
all of this program code were in the main() function, the resulting jumble
would hamper a programmer trying to visualize where one step ends and the
next one begins. Debugging the program would be nearly impossible because
you would have difficulty figuring out which of the intertwined parts
worked correctly and which ones did not. And if you decided to revise the
program to add new capabilities, you could not easily find the appropriate
place to add new code.
Most experienced programmers design programs using a "top-down" approach.
This method resembles writing an outline for a report. First, list the
principal ideas or steps. Then divide those ideas or steps into subtopics,
and continue subdividing until you feel ready to write the actual
sentences. Using a similar approach, the main() function for our
statistics program might read:
main()
{
data_menu();
while (more_data)
{
get_data();
check_data();
store_data();
}
process_data();
report_menu();
do_report();
}
Each of the names followed by parentheses in the definition of main() is a
call to a user-defined function. Notice how this "outline" clearly shows
the overall flow of the program. First, the program offers the user
choices in a data-entry menu; then it enters a loop that receives,
validates, and stores data as long as more data is entered. The data is
then processed. Another menu lets the user generate a specific type of
report, and finally the program prints that report.
Note that using separate functions does more than merely keep the parts of
a program conceptually separate; it also provides an orderly way of
communicating information between different sections of the program. Each
function receives information, such as the values of certain variables,
and after it finishes executing, returns the transformed information to
another section of the program.
In a C program, any function can call any other function. This means that
your user-defined functions can call other user-defined functions as well
as C library functions. For example, the definition of do_report(), shown
on the opposite page, might call other user-defined functions, each of
which prints a different kind of report, corresponding to the choices
offered in the report_menu() part of the program.
do_report()
{
switch (choice)
{
case 'b' :
bar();
break;
case 'p' :
pie();
break;
case 'l' :
line();
break;
case 't' :
table();
break
}
}
Normally, you declare each user-defined function in main() before the
program calls it. The definitions of these user-defined functions usually
follow the end of the definition of main(). (You also can define groups of
functions in separate files: We will discuss this in Chapter 12.) Figure
6-1 proposes a general outline for a program that declares and uses three
user-defined functions.
Dividing a program into logically organized user-defined functions also
lets you develop the program one piece at a time. You can start by putting
"stub" definitions in the functions, such as:
pie()
{
printf("executing pie()\n");
}
Function
declarations
┌────────────────┐ Function definitions
│ main ( ) │ ┌──────────────────────────────────────────┐
│ { │ ┌─┼─────────────────────────┐ │
│ func1 ( ); │ ─┼─┼───────┐ │ │
│ func2 ( ); │ ─┘ │┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ func3 ( ); │ ───┘│ func1 ( ) │ │ func2 ( ) │ │ func3 ( ) │
│ ───────── │ │ { │ │ { │ │ { │
│ ───────── │ │ ─────── │ │ ─────── │ │ ─────── │
│ ───────── │ │ ─────── │ │ ─────── │ │ ─────── │
│ ───────── │ │ ─────── │ │ ─────── │ │ ─────── │
│ } │ │ } │ │ } │ │ } │
└────────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
Figure 6-1. Outline for a C program with functions.
These let you test the overall control structures of the program (the
loops, branches, and switches) before you write the actual routines that
perform the various tasks. Then you can replace functions one at a time
with their real definitions. After you are certain that a function works
properly, you can move to the next function. The basic philosophy of this
type of programming is "divide and conquer."
Declaring and Defining a Function
Now that we've discussed the advantages of user-written functions in
program development, let's look at the mechanics of declaring, defining,
and using your own functions. Always remember that you must declare your
functions before you use them. In earlier chapters, when you used the
#include directive to allow calling C library functions in your program,
QuickC inserted function declarations and definitions into the program.
For functions you create yourself, however, you must provide the
declaration and a definition.
Declaring a Function
Let's declare and define a function that prints an error message. The
general format of a simple function declaration is as follows:
return_type name();
In the declaration, return_type refers to the data type (int, float, etc.)
of the value that the function returns. Because our first example is a
function that does not return a value, we must use the special word void
as the return type. (The word void, in this instance, doesn't mean
"invalid," but rather "empty.")
The function name──error, in our example──must be followed by parentheses,
which hold the parameters the function is designed to use. In the case of
an error message function, parameters might include a string to print and
values that would indicate that the message be printed in high-intensity
text, accompanied by a beep, and so on. You must always include
parentheses──even if, as in the case of our error() example, you use no
parameters. (The compiler uses the parentheses to distinguish a function
from a variable.) Note also that you must use a semicolon at the end of
the line. Thus, we declare our error message─printing function as follows:
void error();
As with variable declarations, you can declare several functions of the
same type on one line, as in the following declaration:
void error(), greeting(), warning();
Usually, programmers place the declarations for user-defined functions in
the definition of main(), before any other statements (except possibly
comments).
main()
{
void error(); /* function declaration */
other function declarations;
...
other statements;
...
}
However, if a program has many user-defined functions, you might want to
put the declarations before main(). This enables you to see the
declarations more easily and separates them from the code of main()
proper.
Defining a Function
After you declare a function, you must define it with statements that will
execute when the program calls the function. The first line of the
function definition essentially repeats the original function declaration:
void error()
However, remember that, with a definition, you use no semicolon at the end
of the line. The next line should contain an opening brace ({ ); then come
the statements that define the function; and finally, the definition ends
with a closing brace (} ). Thus, we write our error() function definition
as follows:
void error()
{
printf("Error!\a\n");
}
When a statement calls this function, it prints the word Error! and sounds
a beep. (Notice the \a [alert] escape sequence, which is listed in Figure
3-5 on p. 68.)
──────────────────────────────────────────────────────────────────────────
ANSI and Function Declarations
QuickC and other current versions of C do not require you to declare
functions that return no value or an int value. The new ANSI standard and
modern programming practice encourage you to declare all functions,
however. This is to help the reader see how the functions work and to
allow the compiler to check for inconsistencies between the definition of
a function and the way it is called. For these reasons, which we explain
in greater detail later, we declare all functions in our example programs.
──────────────────────────────────────────────────────────────────────────
Calling the User-defined Function
We call our user-defined error() function the same way we call a library
function like printf()──by naming it in a statement in main() or in the
body of another function. Consider the following example:
main()
{
...
if ((number < 1) || (number > 9))
error(); /* function call */
}
The if statement calls the error() function only if number is either less
than 1 or greater than 9. Again, note that the function call must include
the parentheses with the function name.
The DBLBAR.C program (Listing 6-1) calls the user-defined function line()
to print a double bar before and after a program title. Note that we
declare line() before the statements in main(), that we call line() twice
from within main(), and that we define line() following the end of main().
Figure 6-2 shows the flow of control in this program.
──────────────────────────────────────────────────────────────────────────
/* dblbar.c -- prints header using */
/* line() function */
#define DOUBLE_BAR 205
main()
{
void line(); /* declare line() function */
line(); /* call line() function */
printf("dblbar.c -- prints header using\n");
printf("line() function\n");
line(); /* call line() again */
}
void line() /* function definition */
{
int pos;
for (pos = 1; pos <= 40; pos++)
putch(DOUBLE_BAR);
printf("\n");
}
──────────────────────────────────────────────────────────────────────────
Listing 6-1. The DBLBAR.C program.
┌──First call
┌───────────────────┐ │ and return
│ main ( ) │ │ ┌ - - - - - - - - - - - - -
│ { │ │ | |
│ void line ( ); │ │ | ┌───────────────────┐ |
┌─┼►line ( ); ────────┼─────────────|───┼►void line ( )◄- - - -
│ │ printf ("...); │ | │ { │
│ │ printf ("...); │ | │ int pos; │
- -│►line ( );- - - - -│- - - - - - - │ for ( ) │
| │ │ } │ │ │ printf ("...); │
| │ └───────────────────┘ │ ┌───┼─}- - - │
| │ │ │ │ | │
| │ Second call─┘ │ │ | │
| │ and return │ └───────|───────────┘
| └───────────────────────────────────┘ |
| |
└ - - - - - - - - - - - - - - - - - - - - - - - - ┘
Figure 6-2. Flow of control in DBLBAR.C.
You can also follow the flow of this program by using QuickC's debugger.
Select the Debug option in the Compile dialog box, and then select Trace
On from the Debug window. When you recompile the program, you see the
highlighted statement move through main() until it reaches the first call
to line(). The debugger then highlights the statements in line().
Highlighting returns to main() with the statement following the call to
line(). Control shifts in the same manner when the program encounters the
second call to line().
A Disadvantage in Using Functions
The double-bar program demonstrates the advantage of using functions──they
reduce the size of the program. Any time we need to draw a line, we simply
call line() rather than repeat the whole for loop. However, using
functions has a potential disadvantage. At the machine-code level, each
time the program calls a function, the current status of the calling
program (including the contents of CPU registers) has to be saved,
information must be passed to the function via a memory area called the
"stack," and various other housekeeping operations must be performed to
return control to the calling statement. Therefore, calling a function
involves a lot more "overhead" than merely using a copy of the desired
code wherever you need it in your program. (The performance difference is
not noticeable with only a few function calls, but you might notice it
when you execute a function call thousands of times within a loop.) The
loss of efficiency is not critical in most cases and usually is far
outweighed by the benefits of using functions. But keep in mind that the
more function calls you use, the more important the overhead factor
becomes.
Local and Automatic Variables
A defined function, such as line(), can contain its own variable
declarations within its definition. Variables declared within a function
definition are called local variables, and they are accessible only within
the function in which they are declared. Outside the function braces, the
variables don't exist. To be accessible to all the functions in your
program, a variable must be declared globally──outside of any function.
The extent of a variable's accessibility, or "visibility," is called the
"scope" of a variable.
In Figure 6-3, note that variables defined in func1() cannot be accessed
from main(). They are "invisible" to main(), func2(), or anywhere else
outside of the definition of func1(). For example, the definition of
line() in DBLBAR.C declares an int variable pos, which is used in the for
loop. (See Listing 6-1 on p. 152.) Suppose the main() function in
DBLBAR.C contained the following line:
printf("The variable pos in line() has a value of %d\n", pos);
This line would produce a compiler error, because main() doesn't "know"
that a variable called pos exists──pos is the private property of the
line() function.
┌─────────────┐ ADDRESS
│ main ( ) │
│ { │
│ int n;◄- - - - - -Visible only 8706
│ func1 ( );│ in main () (remains
│ func2 ( );│ until end
│ } │ of program)
└─────────────┘
┌─────────────┐
│ func1 ( ) │
│ { │
│ int n;◄- - - - - -Visible only 8694
│ ─────── │ in func1 () (reused)
│ ─────── │
│ } │
└─────────────┘
┌─────────────┐
│ func2 ( ) │
│ { │
│ int n;◄- - - - - -Visible only 8694
│ ─────── │ in func2 () (reused)
│ ─────── │
│ } │
└─────────────┘
Figure 6-3. Local variables.
Similarly, if you declare a variable called pos within main(), it is
private to main() and not accessible from within a function called by
main().
Because variables defined inside functions are local in C, you can use
variables with the same name in different functions. Thus, many of your
program functions can use a variable named count, yet QuickC maintains and
refers to each one separately.
Variables Used in Functions Are Automatic
Another important characteristic of a local variable is that it is
"automatic": The variable is created (meaning that internal storage is
allocated and the address recorded) each time its function is called.
Conversely, the variable is destroyed and its internal storage released
when the function ends and control returns to the calling statement. Only
a local variable can be automatic (and temporary) because the compiler
knows that it is valid only while the function executes. (A global
variable must be stored permanently, because the compiler must always
assume that the program will need its value again.) Automatic variables
permit more efficient storage allocation because the same block of memory
can store many temporary variables as the program executes. C programs
that use local, automatic variables also are smaller than comparable
programs that make the same variables global.
The LOCAL.C program (Listing 6-2 on the following page) illustrates the
way local variables work. (Again, refer to Figure 6-3.) The main()
function declares an int variable, n, and prints its value and internal
address. The program then calls the func1() and func2() functions. Each of
these functions also defines a variable called n and prints its value and
address.
──────────────────────────────────────────────────────────────────────────
The Scope of Variables
In Pascal, you can "nest" function or procedure definitions within each
other. A variable defined in a function or procedure is not only
accessible to that function or procedure, but also to any definitions
nested within the outer definition. This can make questions of the scope
of certain variables rather complex. In C you cannot nest function
definitions; therefore, a variable defined within a function is accessible
only within that function.
If you've programmed in older versions of BASIC, you probably expect all
variables to be global, that is, accessible throughout the program. You
also might remember times that this "feature" created nasty bugs. For
example, you might have used count as the control variable of a loop in
one subroutine and then days later used another variable called count in a
different subroutine. Depending on the order in which BASIC called the
subroutines, hard-to-trace bugs probably resulted because BASIC remembered
the last value in count when it started the new count. If you are a BASIC
programmer coming to C, rest assured that QuickC will never confuse the
count of one function with the count of another.
──────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────
/* local.c -- local variables defined */
/* within functions */
main()
{
int n = 12;
int func1(), func2();
printf("n in main(): val %d ", n);
printf("address %d\n", &n);
printf("Calling func1()\n");
func1();
printf("Calling func2()\n");
func2();
}
int func1()
{
int n = 8; /* local variable */
printf("n in func1(): val %d ", n);
printf("address %d\n", &n);
}
int func2()
{
int n = 20; /* local variable */
printf("n in func2(): val %d ", n);
printf("address %d\n", &n);
}
──────────────────────────────────────────────────────────────────────────
Listing 6-2. The LOCAL.C program.
The program's output (which may vary from setup to setup) demonstrates
that QuickC recognizes each variable's correct value and address without
any confusion:
n in main(): val 12 address 8706
Calling func1()
n in func1(): val 8 address 8694
Calling func2()
n in func2(): val 20 address 8694
Also notice that n in func1() and n in func2() use the same address. This
occurs because when func1() ends, it discards its reference to the now
useless local variable n. When the program calls func2(), QuickC reuses
the same address for the new func2() local automatic variable n.
Note that the address of the n in main() wasn't reused when the n in
func1() or func2() was created. Variables declared in main() have no
special status──the n in main() is as automatic and local as the variable
in the other functions. Remember, however, that QuickC discards an
automatic variable only when the function in which it is defined
terminates. Because main() doesn't end execution until the program itself
ends, its variables (including n) are not destroyed and reused.
The auto Storage Class
All variables declared within function definitions are, by default,
automatic. Consider the following example:
void plot_object()
{
auto int length, width;
...
}
The variables length and width are automatic because they are declared
within a function. The auto designation merely reminds us of this. Note
that auto is not a data type. Rather, it (and the keywords register,
static, and extern) is what is called a "storage class." It refers to how
QuickC manages the variable as the program runs. Specify the storage class
before and in addition to the variable's data type (int in the previous
example).
Static Variables
Occasionally you will need a variable to retain its value after its
function terminates. To do this, you must declare the variable used in the
function with the static storage type, as follows:
static int total;
Now the value of total calculated during a previous call is still there
when you call the function again. You use this storage class to keep
running totals, for example. Note that a static variable is still local
and accessible only within the function in which it is defined.
The STATIC.C program (Listing 6-3 on the following page) uses the
function countline() to count the words and characters in a line of text.
(It simply counts a word whenever it encounters a space.) The static
variables chars and words accumulate the counts. Because the variables are
static, they retain the previous total each time the function is called.
A sample of the program's output follows:
Type some lines of text.
Start a line with a . to quit.
By now you should be able to
Words so far 7. Chars. so far 28
function very well in C!
Words so far 12. Chars. so far 52
.
──────────────────────────────────────────────────────────────────────────
/* static.c -- demonstrates a static variable */
/* that holds count of lines, */
/* words, and characters */
main()
{
void countline();
printf("Type some lines of text.\n");
printf("Start a line with a . to quit.\n\n");
while (getche() != '.')
countline(); /* accumulate word and */
/* line counts */
}
void countline()
{
static int words = 0; /* static variables */
static int chars = 0;
char ch;
++chars; /* count char typed when */
/*function was called */
while ((ch = getche()) != '\r')
{
++chars;
if (ch == ' ')
++words;
}
++words; /* count last word */
printf("\nWords so far %d. Chars. so far %d\n", words, chars);
}
──────────────────────────────────────────────────────────────────────────
Listing 6-3. The STATIC.C program.
External Variables
You can declare global variables in C when you want two or more functions
to share relevant values or to communicate with each other by periodically
changing the value of a common variable. A global variable in C is
referred to as external because it is defined outside the function
definitions in the program.
To declare an external variable, simply put its declaration outside of any
function definition. External variables are usually placed after any
#includes and #defines, but before the definition of main(). In the
following example, we declare scale and palette outside of the function,
making them external (global) and accessible throughout the program. The
variables length and width, on the other hand, are local and accessible
only within main().
#include <stdio.h>
#define VERSION 1.0
int scale = 1.5, /* global variables */
palette = 1; /* go here */
main()
{
int length, width; /* local variables */
... /* go here */
The EXTERNAL.C program (Listing 6-4) shows how you might use an external
variable. We declare the variable length before main() to make it an
external variable. After the user supplies a value for length, main()
calls three functions: square(), triangle(), and circle(). Each of these
functions accesses and uses the value of length to calculate the
appropriate area.
──────────────────────────────────────────────────────────────────────────
/* external.c -- shows an external variable */
#define PI 3.14159
int length; /* external (global) variable */
/* declared before main() */
main()
{
void square(), triangle(), circle();
printf("What length do you want to use? ");
scanf("%d", &length);
square(); /* calculate areas */
triangle();
circle();
}
void square()
{
float area;
area = length * length;
printf("A square with sides of %d has an area of %f\n",
length, area);
}
void triangle()
{
float area;
area = (length * length) / 2;
printf("A right triangle with sides of %d has an area %f\n",
length, area);
}
void circle()
{
float area;
area = (length * length * PI);
printf("A circle with radius of %d has area of %f\n",
length, area);
}
──────────────────────────────────────────────────────────────────────────
Listing 6-4. The EXTERNAL.C program.
Try to resist the temptation to make all your variables external; this
invites the problems we discussed earlier. Variables used in only one
function should remain local. Variables used by only two or three
functions might be better handled as parameters passed from one function
to another. (We will discuss function parameters shortly.)
Register Variables
Let's look at one more storage type for variables, the register type.
Microprocessors such as the 8088 and 80286 have several built-in storage
locations called registers. A program can store and retrieve data from a
register more quickly than from a location in regular memory, where C
usually stores variables. As a result, you gain a performance advantage by
assigning a register to a frequently used variable in a time-sensitive
application.
The only problem with using registers is that usually there aren't enough
registers to store all the data of a given operation. As shown in Figure
6-4, the IBM family of Intel microprocessors (8088, 8086, and 80286) have
four general-purpose, 16-bit registers that hold data being manipulated at
the machine level.
16 bits
(2 bytes)
│
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
┌─────────────┬─────────────┐
│ AH │ AL │ AX
├─────────────┼─────────────┤
│ BH │ BL │ BX
├─────────────┼─────────────┤
│ CH │ CL │ CX
├─────────────┼─────────────┤
│ DH │ DL │ DX
└─────────────┴─────────────┘
8 bits 8 bits
(1 byte) (1 byte)
Figure 6-4. General-purpose registers for the Intel 8086 family.
In other languages, the compiler software determines which variables, if
any, will be assigned registers. In C, however, you can tell the compiler
to assign a register to a specific variable when you declare that
variable, as follows:
register int count;
This declaration tells QuickC to store the count variable in a CPU
register. (You can't specify which physical register to use, however.)
Because registers in the 8088, 8086, and 80286 cannot store variables with
values larger than two bytes, only char and int variables can be
accommodated. Additionally, you cannot declare external or static
variables as register variables, because registers cannot serve as
permanent storage locations. Finally, QuickC assigns only two variables
per function as register variables: You can make more declarations, but
only the first two are honored. In fact, depending on the CPU workload,
there is no guarantee that both, or even one, of the registers will be
available. If speed is important to your program, try the declarations to
see if they help.
Which variables should you declare to be register variables? Obvious
candidates include loop control variables or variables that are part of
statements performed in a loop. But even though most loops execute many
times, you shouldn't specify these variables as register storage type.
QuickC uses optimization techniques to try to produce the fastest machine
code possible from your program, and one of its basic speedup techniques
is the assigning of registers to variables in loops. Thus, you gain little
by specifying these as register variables──in fact, you might even confuse
the compiler and end up with less efficient code. The best variables to
specify as register type are variables that are not involved with loops
yet are used three or more times each time the function is called.
Passing Information to a Function
Thus far, our user-defined functions have not required that any
information be passed when we called them. Most functions, however,
require one or more items of information, called arguments, or parameters.
This is often true of the QuickC core library functions: printf() needs to
know what to print and how to print it; scanf() needs to know what
information to get from the user and where to store it; and so on. Indeed,
because it uses no parameters, the line() function in Listing 6-1 on p.
152 is extremely limited. As it stands, it always prints a line of 40
characters. However, suppose we want 20 or 60 characters. Let's define
this function so that when you call it, a parameter tells it how long the
line should be:
void line (length) /* length is parameter */
int length; /* declaration of parameter */
{
int pos;
for (pos = 1; pos <= length; pos++)
putch(DOUBLE_BAR);
}
When you call this function, the length parameter (placed in parentheses
in the function definition) receives the current value of the variable
length. The items named in the definition of a function are called "formal
parameters." The definition of the line() function includes one formal
parameter, length. When you call a function, you must include an actual
value, such as a constant or a variable, for each formal parameter the
function requires. Thus, if we use the statement line(10); to call the
line() function, the value 10 is the "actual parameter" corresponding to
the formal parameter, length. Inside the line() function, length becomes a
variable of type int with the value 10.
Notice that we declare the parameter length as an int in the function
definition for line(). This declaration serves the same purpose as the
declaration of an ordinary variable: It tells the compiler what type of
data the parameter represents. Notice also, however, that unlike the
declaration of an ordinary variable, which follows the opening brace of
the function definition body, the declaration of a function parameter
precedes the opening brace. An alternative syntax, favored by many
programmers, places the parameter type before the name within the
parentheses, as in the following:
void line (int length)
Note that when a function uses parameters, the parameter should also be
put in the declaration of the function in or before main(). Thus, in a
program that uses line(), the declaration would be:
main()
{
void line (length);
...
}
or:
void line (int length);
It's easiest to use the same form for the function declaration and for the
first line of the function definition──but remember that the declaration
ends with a semicolon, whereas the definition does not.
Once you declare the parameter length, the line() function refers to it as
though it had been declared and initialized as an ordinary variable. In
our example, length sets the limit for the loop condition, thus
controlling the length of the line.
Parameters make functions versatile. The program now can call line() and
set the length of the line with any appropriate value: a number, a
variable name, a #define constant, or an expression.
By the way, you can call the line() function using a variable with the
same name as the parameter (length). The variable in the function that
calls line() belongs to the calling function, and the parameter "belongs"
to the called function. They are, in effect, separate local variables──the
only connection between them is the value.
Let's look at the ALERT.C program (Listing 6-5), which uses a function
with a parameter. The beep() function uses a parameter named times to
control the number of times a beep is sounded. When the function executes,
the if statement checks to see if times has a value in the range of 1 to
4. If it doesn't, the program prints an error message. Notice that the
error message prints the name of the function and the value that length
passed to it. Including this type of information helps you debug your
programs.
If the value of times is in the correct range, a for loop with the limit
of times generates the correct number of beeps. Try changing the calling
statement in main() to beep(0) or beep(100).
──────────────────────────────────────────────────────────────────────────
/* alert.c -- sounds alarm by calling a */
/* beep() function with a parameter */
main()
{
void beep(times); /* function declaration */
printf("*** Alert! Alert! ***\n");
beep(3); /* call beep() with parameter */
}
void beep(times)
int times; /* declare function parameter */
{
int count;
/* check that parameter is between 1 and 4 */
if ((times < 1) || (times > 4))
{
printf("Error in beep(): %d beeps specified.\n",
times);
printf("Specify one to four beeps");
}
else /* sound the beeps */
for (count = 1; count <= times; count++)
printf("\a"); /* "alert" escape sequence */
}
──────────────────────────────────────────────────────────────────────────
Listing 6-5. The ALERT.C program.
How Parameters Work
Now that you know how to use parameters, let's take a detailed look at how
they work. Figure 6-5 on the following page shows what happens when a
program calls a function that has a parameter. When it executes the
function call line(short), QuickC places the value of the variable short
in an internal memory area called the "stack" and passes control to the
line() function. The line() function "knows" from its definition that it
should expect one parameter, called length. It also knows that it is an
int value. Thus, the function reads two bytes (an int) from the stack and
creates a temporary storage location for them. This value can now be
accessed by length within the line() function. It is basically a local,
automatic variable that behaves as if it had been declared and initialized
within the function definition.
Caller
┌──────────────────────┐
│ main ( ) │ ┌─────────►Discarded when
│ int short=10; │ │ function terminates
│ { ││ │ │
│┌───────────┘└───────┐│ │
││ line (short); ├┼───►10 ───┐
│└────────────────────┘│ ──── │ Called function
│ ───────────────── │ ──── │ ┌──────────────────────┐
│ ───────────────── │ ──── │ │ void line (length) │
│ } │ ──── └──►│ int length; (=10) │
└──────────────────────┘ Stack │ { │
│ ────────── │
│ ────────── │
│ ────────── │
│ } │
└──────────────────────┘
Figure 6-5. Function parameters and the stack.
Note that the parameter in a function call is not normally affected by the
operation of the function. The function operates with a local variable it
creates, not with the variable in the calling statement.
Multiple User-written Functions
The TIMER2.C program (Listing 6-6) uses a callable custom function,
delay(), to improve the timer program we used in Chapter 4. The main()
function asks the user for the number of seconds to be timed and the
interval by which the program should count off the time, beeping once at
each interval. The while loop repeatedly calls delay(interval) to wait for
interval seconds; then it sounds the beep (by calling the beep() function)
and prints the elapsed seconds. Be sure you understand the positions and
components of the function declarations, the function definitions, and the
parameter declarations for the delay() and beep() functions.
──────────────────────────────────────────────────────────────────────────
Passing Parameters in Pascal and C
In Pascal, you can pass either the value of a variable or its address in a
parameter. (The first is a "call by value," the second is a "call by
reference.") In C, function parameters are always passed by value: The
variable itself is never passed. The value can, however, represent the
address of a variable. In this case the variable itself, and not merely
the value, can be accessed and changed by the called function. (This is
called a "pointer," which we discuss in Chapter 8.)
──────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────
/* timer2.c -- interval timer */
/* calls delay(), uses beep */
main()
{
/* function declarations */
void beep (times);
void delay (seconds);
/* variable declarations */
int seconds, interval, tick;
printf("Set for how many seconds? ");
scanf("%d", &seconds);
printf("Interval to show in seconds? ");
scanf("%d", &interval);
printf("Press a key to start timing\n");
getch();
tick = 0; /* run "clock" for */
while (tick < seconds) /* time specified */
{
delay(interval); /* wait interval seconds */
tick += interval;
printf("%d\n", tick);
beep(1);
}
beep(3);
}
void delay(seconds)
/* wait for number of seconds specified */
/* See TIMER.C in chapter 4 for details */
/* on the library function time(). */
int seconds; /* parameter declaration */
{
/* variable declarations */
long start, end, /* starting and ending times */
/* measured in seconds since */
/* Jan. 1, 1970 */
ltime; /* used to get value from time function */
start = time(<ime); /* get system-elapsed seconds */
/* since 1-1-70 */
end = start + seconds; /* calculate alarm time */
do
{;} /* null statement for loop body */
while (time(<ime) < end); /* wait for end of time */
}
void beep(times)
/* parameter declaration */
int times;
{
/* variable declaration */
int count;
/* check that parameter is between 1 and 4 */
if ((times < 1) || (times > 4))
{
printf("Error in beep(): %d beeps specified.\n",
times);
printf("Specify one to four beeps\n");
}
else /* sound the beeps */
for (count = 1; count <= times; count++)
printf("\a"); /* "alert" escape sequence */
}
──────────────────────────────────────────────────────────────────────────
Listing 6-6. The TIMER2.C program.
The delay() function has the same code as the TIMER.C program in Chapter
4, except that it adds the parameter seconds to the current time to
determine what time should be compared with the system time in the do
loop. A sample run follows:
Set for how many seconds? 30
Interval to show in seconds? 5
Press a key to start timing
5────────────────────────────────────────────────Beeps after each number
10
15
20
25
30
Putting User Functions in an Include File
Notice in TIMER2.C that the definition of the beep() function follows that
of delay(). We've used the beep() function before, and we will use it
again in several other programs. You would save program space if you put
the definitions of beep() and related functions (perhaps line(), another
function that prints characters in reverse video, a function that draws a
box around a string, and so on) in a header file. Then you could include
these functions in any program without typing or pasting them in by hand.
Simply save the definitions in a file that uses the traditional .h
extension (hilite.h, for example). Then insert the line #include
"hilite.h" at the beginning of your program. (You can use the existing
INCLUDE subdirectory, but your file system would be better organized if
you created a subdirectory called INCLUDE\USER to hold these user-written
include files.)
Functions with Many Parameters
Functions can use any number of parameters. The actual parameters
specified in the function call parentheses are assigned, in order, to the
corresponding formal parameters in the function definition.
As a general rule, however, functions that use more than five parameters
become cumbersome to use. If you must use more than this number,
reconsider the operations your function performs and try to do the
operations in two or more simpler functions. After all, one of the chief
benefits of using functions is that they keep each piece of a program at a
manageable size. The LINES.C program (Listing 6-7) uses five parameters
in its line() function, which draws a colored line on the screen. (To run
this program, you need a CGA, EGA, or Hercules board.)
──────────────────────────────────────────────────────────────────────────
/* lines.c -- calls line() with */
/* five parameters */
#include <graph.h>
main()
{
void line (x1, y1, x2, y2, color);
int x1, x2, y1, y2, i, color;
_setvideomode(_MRES16COLOR); /* 320-by-200 16 col. */
srand(2); /* new random seed */
for (i = 0; i < 100; i++)
{
x1 = rand() % 319; /* random coordinates */
x2 = rand() % 319;
y1 = rand() % 199;
y2 = rand() % 199;
color = (rand() % 14) + 1; /* random color 1-15 */
line(x1, y1, x2, y2, color); /* draw a line */
}
while(!kbhit()); /* wait for key to be hit */
_setvideomode(_DEFAULTMODE); /* restore video mode */
}
void line (x1, y1, x2, y2, color)
int x1, y1, x2, y2, color;
{
_moveto(x1, y1); /* position at first endpoint */
_setcolor(color);
_lineto(x2, y2); /* draw line to second endpoint */
}
──────────────────────────────────────────────────────────────────────────
Listing 6-7. The LINES.C program.
The line() function draws a line between the points (x1,y1) and (x2,y2)
using a specified color. As you learned from PIXELS.C in Chapter 5, you
must use a system of coordinates to specify pixel locations on the
graphics screen. This program uses the 320-by-200, 16-color mode, with
coordinates starting at (0,0) in the upper-left corner of the screen and
ending with (319,199) in the lower-right corner.
After the program sets the video mode and initializes the random number
generator with the QuickC library function srand(2), a for loop executes.
The body of the loop generates random sets of endpoints for lines. (The
QuickC random number function rand() generates random integers between 0
and 32,767. To produce a random number between 0 and a number n, we use
the expression rand() % n. Because the modulus operator gets the remainder
by dividing the first number by the second, the result is a number greater
than or equal to 0 [no remainder] and less than the second number.) A
similar expression generates a random color between 1 and 15. (We can't
use color 0 because that is the background color──a line drawn in that
color would be invisible.)
Finally, the program calls line() and passes five parameters: the two
pairs of endpoint coordinates and the color number. Notice that commas
must separate the parameters. The line() function draws the line by first
calling the QuickC graphics function _moveto() to position the cursor at
the randomly specified point (x1,y1). Another graphics function,
_setcolor(), sets the current drawing color to the value of color.
Finally, the QuickC _lineto() function draws the line to the second
endpoint (x2,y2). The for loop in main() then repeats the process to draw
100 random lines in random colors.
Functions That Return Information
Sending values to a function is only one way that information flows
between a calling program and a function in C. A function can also send
information to the program. For example, in the expression
ch = getche()
getche() returns a value (the character) to the calling statement, which
in turn assigns the value to the variable ch. To have a function return a
value to its caller, add a statement to the function definition that
consists of the keyword return with the value to be returned enclosed in
parentheses, as follows:
return (value);
Replace value with any information you want the function to return──the
value of a variable, the result of a calculation, a character the function
has read, or anything else that can be expressed using one of C's data
types. When the flow of execution reaches a return statement, the function
immediately terminates, returning the specified value to the calling
statement. (A function can have several return statements to return
different values under different conditions, such as in the branches of an
if or switch statement. However, the function always terminates at the
first return statement it encounters.) In the calling statement, the
returned value replaces the function call, and execution of the statement
continues.
Let's look at a very simple example, a function that accepts a quantity in
yards, converts it to feet, and returns the result:
int ytof(yards)
int yards;
{
return(yards * 3);
}
This function takes the number of yards passed to it through its formal
parameter yards. The return statement calculates the number of feet with
the expression yards * 3 and returns the value to the caller. Suppose the
calling program uses the following statement:
distance = ytof(course_length);
and assume that the user supplies a course_length of 750 yards. When the
statement calls ytof(), it passes the value of course_length to the ytof()
function. The function returns 2250 (750 * 3), which replaces the function
call in the statement. The statement now reads:
distance = 2250;
and the assignment operator assigns this value to distance.
Also note that the definition of the ytof() function begins not with the
type void but rather with the type int. The return type void signifies
that the function does not return a value. (We also must declare the
parameter yards to be an int in the definition of ytof().)
Now let's look at a more useful example. Some languages have a built-in
exponentiation operator; C does not. However, the math.h include file
contains a function called pow() that you can call as pow(x,y). It raises
the first parameter (x) to the power specified in the second parameter
(y). Because this function uses double values, it can handle both integer
and floating-point values with great precision. The EXPO.C program
(Listing 6-8 on the following page) creates an integer version of this
function that can respond to various types of input.
The expo() function takes two parameters (the number to be raised to a
power and the power to raise it to) and returns a value to the calling
statement. However, part of designing functions that return values is
deciding how to handle special cases. This program must be able to handle
three special cases: an exponent less than 0 (negative), an exponent equal
to 0, and an exponent equal to 1. Thus, we designed EXPO.C to respond to
valid inputs, special inputs, and error conditions with appropriate
messages and return values.
──────────────────────────────────────────────────────────────────────────
/* expo.c -- uses exp() function to */
/* calculate powers */
main()
{
int expo(number, power);
int number, power;
printf("Enter a number: ");
scanf("%d", &number);
printf("Raise to what power? ");
scanf("%d", &power);
printf("Result: %d", expo(number, power));
}
int expo(number, power)
{
int count, value;
int total = 1; /* store value of calculation */
if (power < 0) /* reject negative exponents */
{
printf("Error in expo(): negative exponent\n");
return(0);
}
if (power == 0) /* any number to 0 power is 1 */
return(1);
if (power == 1) /* any number to 1 power is itself */
return(number);
/* calculate for power > 1 */
for (count = 1; count <= power; count++)
total *= number;
return(total);
}
──────────────────────────────────────────────────────────────────────────
Listing 6-8. The EXPO.C program.
A negative exponent results in a fraction that is less than 1. For
example, 2 to the -3 power is the same as:
1 1
─── which is equal to ─── or 0.125
2^3 8
This function handles only positive powers because it uses int type
variables that can't handle fractions. The first if statement tests the
power parameter and prints an error message if it is less than zero. The
return statement returns a value of zero to the calling program, and the
function terminates.
Now let's look at the remaining two cases in expo(). If the power
specified in calling expo() is 0, a return statement returns 1 (any number
to the zero power is one). If the power specified is 1, expo() returns the
number itself.
Finally, a for loop calculates all other cases (positive powers greater
than 1). Because we initialized total to 1 at the beginning of the
function, the expression total *= number in the for loop multiplies number
by itself power times.
The main() function simply lets you test the expo() function by assigning
it a number and a power.
Recursion
Thus far, calling functions and returning values from them has been a
simple and straightforward matter. However, you can use function calls in
a way that disturbs these rules slightly──with amazing results. Recursion
is an idea that some find difficult to grasp at first, yet a little
perseverance will lead you to a programming tool of great beauty,
elegance, and power. Here's how it works: In C, any function can call any
other function. In fact, a function can call itself──and that is the
essence of recursion.
A classic example of recursion is the calculating of the factorial of a
number. Recall that the factorial of a number is the product of all the
integers between 1 and that number, inclusive. For example, the factorial
of 4 (written by mathematicians as 4!) is 1 * 2 * 3 * 4, or 24.
A program could calculate a factorial by the "brute force" method, using a
for loop and multiplying the loop control variable by the previous total.
But there is another way to calculate factorials. Consider that 4! = 4 * 3
* 2 * 1, and 3! = 3 * 2 * 1. From this we can deduce that 4! = 4 * 3!, or
in general terms, that the factorial of a number n is equal to n * (n -
1)!, which in turn is equivalent to n * (n - 1) * (n - 2)!, and so on.
Eventually we get to 0!, which, by definition, is equal to 1.
──────────────────────────────────────────────────────────────────────────
Special Return Values and Error Numbers
Why return a value if an error occurs? Because the value returned can warn
the caller that an error has occurred. The warning value selected is a
value that the function will not return by normal operation. If normal
operation of the function returns a positive number, a number such as 0 or
-1 sometimes indicates an error. For functions that don't normally return
a number, the return value usually indicates something relevant about the
function's operation: For example, the return value of scanf() is the
number of data fields read, and a 0 indicates an error. Other functions
return 0 if their operation was successful; a nonzero return value not
only indicates an error but also represents an error number that specifies
the precise problem. As you continue to work with QuickC, you will become
familiar with how library functions handle errors and what the return
values mean.
──────────────────────────────────────────────────────────────────────────
Figure 6-6a depicts the calculation of 4! and then generalizes the
calculation of a factorial in Figure 6-6b. Note that you keep breaking
down the expression by subtracting 1 from the current value of n, taking
its factorial, and multiplying it by the preceding value. Eventually the
expression becomes equal to n - n, or 0. But 0! = 1, so we no longer need
to break down n any further. Multiplying all the values together gives the
factorial of the original number n.
The RECURSE.C program (Listing 6-9) demonstrates how to use this
recursive method to calculate factorials.
4!
/ \
4 * 3!
/ \
3 * 2!
/ \
2 * 1!
/ \
1 * 0!
│
│
1
4! = 24
(A) BREAKING DOWN 4!
n!
/ \
n * (n-1)! ┌───────────────────┐
/ \ ▒▒▒▒▒▒▒▒►│ (n-(n-1) * (n-n)! │
(n-1) * (n-2)! ▒ │ = 1 * 0! │
/ \ ▒ │ = 1 * 1 = 1 │
(n-2) * (n-3)! ▒ └───────────────────┘
. ▒
. ▒
. ▒
(n-(n-1) * (n-n)!
(B) BREAKING DOWN n!
Figure 6-6. Breaking down factorial expressions.
──────────────────────────────────────────────────────────────────────────
/* recurse.c -- demonstrates recursion */
int level = 1; /* recursion level */
main()
{
int num, result;
printf("Factorial of what number? ");
scanf("%d", &num);
result = factorial(num);
printf("Result is: %d\n", result);
}
factorial(int number)
{
int result;
printf("entering: ");
printf("level %d. number = %d. &number = %d\n",
level++, number, &number);
if (number == 0)
result = 1;
else
result = number * factorial(number - 1);
printf("exiting : ");
printf("level %d. number = %d. &number = %d. ",
--level, number, &number);
printf("result = %d\n", result);
return(result);
}
──────────────────────────────────────────────────────────────────────────
Listing 6-9. The RECURSE.C program.
A Recursive Function at Work
Let's examine how the factorial(number) function in RECURSE.C works. An if
statement terminates the function and returns 1 if number is 0 (because 0!
is 1). For any other value of number, the else branch executes
result = number * factorial(number - 1);
This is a recursive call because factorial() is actually calling itself.
Now let's follow execution from start to finish. The main() function makes
the first call to factorial() with the value of number. Because number
initially is not 0, factorial() executes the else branch and calls itself
with number - 1. In this new call, execution again encounters the else
branch, and another call to factorial() results. Although this call also
uses number - 1, the number here is actually the value factorial()
received from its previous call, or number - 2.
As a result of the repeated calls of factorial() to itself, the decreasing
values passed with each call accumulate on the stack (because the values
passed to a function call are not removed from the stack until the call
terminates). Also, remember that each call to factorial() creates a new
set of the automatic variables number and result. These variables
accumulate (one set for each call) until the function terminates. It is as
though QuickC places a "bookmark" in each function call when the next call
executes so that the compiler can keep track of what remains to be done in
each.
The printf() statements in the program record the above process, as
follows:
Factorial of what number? 4
entering: level 1. number = 4. &number = 8720
entering: level 2. number = 3. &number = 8706
entering: level 3. number = 2. &number = 8692
entering: level 4. number = 1. &number = 8678
entering: level 5. number = 0. &number = 8664
exiting : level 5. number = 0. &number = 8664. result = 1
exiting : level 4. number = 1. &number = 8678. result = 1
exiting : level 3. number = 2. &number = 8692. result = 2
exiting : level 2. number = 3. &number = 8706. result = 6
exiting : level 1. number = 4. &number = 8720. result = 24
Result is: 24
Notice the new address for each call's version of the number variable:
This proves that separate automatic variables are being created. (The
actual addresses the program returns may vary from setup to setup.)
The "turning point" in the recursive process occurs when number decreases
to 0, the call factorial(0) is made, and the if branch finally executes.
Finally the function returns to the caller (the preceding version of
factorial()), after assigning the value 1 to result. Now the preceding
call can "pick up its bookmark" and replace factorial(number - 1) with 1,
multiply it by the value of number that was saved in its automatic
variable, and then return this value to its preceding caller. You can see
this happening in the second half of the output listing: The calls move
back through the recursion levels, back through the addresses of the
accumulated automatic variables, with each result being multiplied by the
preceding one, until the function returns to main() with the correct
result, 24.
Recursion and Stack Size
Some problems are naturally recursive──searching through directories and
their subdirectories, parsing commands into subcommands, or working with
tree structures. One thing to keep in mind, however, is that recursion
uses a lot of memory for storing automatic variables and the stack. (The
stack holds not only the parameters passed for each call but also the
register values and return addresses.) Try running RECURSE.C with larger
input numbers. You can only use numbers to 7 before type int, which stores
the result, overflows. If you rewrite the program to use long, you can use
numbers to 16. With type double, you can generate some truly impressive
factorials, but trying to generate 62! causes a stack overflow error. The
stack, which by default can store 2048 bytes, simply cannot hold any more
recursions. (You can specify a larger stack size by selecting Set Runtime
Options from the Run menu. Still, there is a limit in our medium memory
model because the stack and the program data share one 64 KB segment. In
Chapter 11, we also will show you how to use a compiler command-line
switch to use memory models in which the stack has an entire 64 KB segment
to itself.)
Noninteger Functions
The value returned by a function does not have to be an integer type.
Functions can return a float, a char, or any other standard C data type.
So far, we have declared and defined functions using a return data type,
such as:
int expo(number, power)
An older style of C programming omits the return type when the function
returns an int, because the compiler defaults to int if no type is
specified. We base our style on the new ANSI standard, which encourages
declaring return types for all functions and using void for functions that
do not return values. For example, we might define a function that
calculates the cube root of a number as:
float cube_root(number)
This specifies that the cube_root() function returns a value of type
float. Remember that we also must declare this function in or before
main():
main()
{
char response;
int x, y;
float result;
float cube_root(number); /* function declaration */
The GETYN.C program (Listing 6-10 on the following page) demonstrates the
declaration and use of a noninteger function. It defines a function,
getyn(), that prompts for a "yes or no" answer, checks to make sure the
character entered by the user is either a y or an n, and returns the
entered character. We declare the function at the start of main() and
define it as follows:
char getyn()
because it returns a value of type char. Notice that this function does
not use a parameter. Although many functions that return values, such as
the expo() function, require parameters, some functions receive their
information not from the calling statement but from some other source. In
the case of getyn(), and indeed with the standard functions that read
characters (getch(), getche(), and so on), the user supplies the value.
──────────────────────────────────────────────────────────────────────────
/* getyn.c -- calls char function getyn() */
/* with error checking */
#define TRUE 1
main()
{
char ch;
char getyn();
printf("Do you want to continue? ");
if ((ch = getyn()) == 'y')
printf("Answer was y\n");
else
printf("Answer was n\n");
printf("Value of ch was %c\n", ch);
}
char getyn()
{
char ch;
while (TRUE)
{
printf(" (y or n) ");
ch = getche();
printf("\n");
if ((ch == 'y') || (ch == 'n'))
/* valid response, break out of loop */
break;
/* give error message and loop again */
printf("please enter ");
}
return(ch);
}
──────────────────────────────────────────────────────────────────────────
Listing 6-10. The GETYN.C program.
In the getyn() function, a while loop prompts the user to enter a y or n,
a getche() gets a character, and an if statement checks to see if the
character is a y or an n. If it is, a break statement exits the loop, and
the return statement returns the character. If the character is something
other than y or n, a prompt asks the user to reenter a value, and then the
loop repeats. Putting an input-type statement in a loop provides a
framework for error checking.
The getyn() function is a handy tool that you can use in place of getch()
or getche() whenever you want the user to enter a valid response to a
yes/no question.
Function Prototypes
We've seen that you can declare the return type for a function at the
beginning of your program, such as:
float distance(x1, y1, x2, y2);
This declaration tells QuickC that your function returns a float value.
That takes care of the value coming out of the function. But what about
the values going into the function──the function parameters?
As it stands, this definition does not specify a data type for the
parameters. Consequently, we might design the function to work with
integer values and then accidentally give it some double values as real
parameters. If you don't specify the data type of the function parameter,
QuickC isn't even aware of the potential problem.
The MIXTYPES.C program (Listing 6-11) shows what can happen when the type
of data passed to the function does not match the expected type.
We wrote this program in the older C style: It has no function declaration
and only the minimum function definition. After all, the examine()
function merely prints its parameter. (It expects a parameter of the
default int type, hence the "%d" format specifier in the printf() control
string.)
In main() we incorrectly declare the variable n as float and call
examine() with it.
──────────────────────────────────────────────────────────────────────────
/* mixtypes.c -- shows problem with calling */
/* a function with wrong type parameter */
main()
{
/* didn't bother to declare int function */
float n = 5.0;
int i;
printf("n in main() is %f\n", n);
i = examine(n); /* pass float to function */
printf("examine() returned n as %d\n", i);
}
examine(num) /* function didn't declare return type */
{
printf("examine() says n is %d\n", num);
return(num);
}
──────────────────────────────────────────────────────────────────────────
Listing 6-11. The MIXTYPES.C program.
What happens when you run MIXTYPES.C?
n in main() is 5.000000
examine() says n is 0
examine() returned n as 0
The printf() statement in main() verified a float type. But when we try to
print the value of this parameter inside examine(), we see that its value
is now 0. Because we didn't specify a type for the num parameter in
examine(), the float passed to the function without comment. However,
because the function expects num to be an int (the default), the float is
fetched from the stack as if it were an int. (Treating the 4-byte value
5.0 as a 2-byte int returns two zero bytes, or 0.) Finally, examine()
returns this incorrect value to main().
How can we avoid this problem? In Chapter 3, we discussed strategies for
ensuring sensible type conversions, such as using type casts. Here we need
to tell QuickC the data type of the function's parameters. You do this by
providing a complete declaration called a function prototype.
A function prototype declares the name of the function, its return type,
the data type of each parameter, and, optionally, the parameter's name.
Below are some sample declarations:
int factorial(int number);
int expo(int number, int power);
void line(int x1, int y1, int x2, int y2, int color);
char getyn(void);
These are all functions we used earlier in this chapter. Although you have
already used return type declarations, the full ANSI prototypes add an
additional specification──the data type for each formal parameter. The
prototype for factorial() indicates that it returns a value of type int
and accepts one parameter of type int. The prototype for expo() specifies
that it returns an int and accepts two int parameters.
──────────────────────────────────────────────────────────────────────────
Type Checking in Pascal and C
In the type-mixing situation described above, a Pascal compiler would show
an error. Pascal checks actual parameter types against the expected
parameter types and is very strict about making you define types for
everything. The traditional C philosophy, on the other hand, expects the
programmer to anticipate problems carefully, so the compiler permits the
mixing of function parameter types. Thus, because C does not force you to
declare types for function parameters, it often cannot tell you that
anything is wrong when the types of the actual and expected parameters
don't match. If you follow modern C programming practice and define
function return and parameter types, the compiler will alert you to many
potential problems.
──────────────────────────────────────────────────────────────────────────
The line() function uses a return type of void because it does not return
a value; it does, however, take five int parameters. Finally, getyn()
returns a char but has a parameter of void, which signifies that the
function takes no parameters. In addition, the prototypes specify the
names of the parameters. (Although the names are optional──you could say,
for example, int factorial(int)──we recommend using the parameter names in
the declaration so that all the information is in one place.)
The beginning of the function definition should also contain the function
return type, function name, and the list of formal parameters (the
parameter names), as in the following:
int factorial(int number)
int expo(int number, int power)
void line(int x1, int y1, int x2, int y2, int color)
char getyn(void)
You also could declare the parameter types separately on the following
lines. (Again, the void in parentheses after getyn signifies that the
function takes no parameters. Also, notice that the function definitions
do not end with semicolons.)
Advantages of Using Prototypes
Using prototypes might involve a little more thought and a little more
typing, but it offers many advantages, which is why the ANSI C standard
and QuickC support it. First, the complete prototype contains all of the
information you need to use the function: what you can put in and what you
can expect to get out. Indeed, if you look up a library function using the
on-line help, you will find the complete prototype prominently displayed.
It is important to note that if you use prototypes, QuickC will check both
the type and number of the parameters in your function calls against the
type and number of the parameters you specify in the prototype. In cases
where types are mixed, QuickC will try to promote smaller to larger types.
(See the examples in Chapter 3.) If you use the wrong number of
parameters, QuickC will display an error message.
The PROTO.C program (Listing 6-12 on the following page) is a revision of
the problem program MIXTYPES.C, rewritten to use function prototypes. This
program produces more reasonable output than that produced by MIXTYPES.C:
n in main() is 5.000000
examine() says n is 5
examine() returned n as 5
With the prototype, QuickC knew that examine() needed an int. When the
program tried to pass it a float, QuickC converted the value to an int
before passing it to the function. (Some conversions can cause variables
to lose precision, but the resulting value will be much more likely to be
acceptable.)
──────────────────────────────────────────────────────────────────────────
/* proto.c -- demonstrates function prototyping */
/* and parameter checking */
main()
{
float n = 9995.997;
int i;
int examine(int num); /* declare function */
/* with prototype */
printf("n in main() is %f\n", n);
i = examine(n); /* pass float to function */
printf("examine() returned n as %d\n", i);
}
int examine(num)
{
printf("examine() says n is %d\n", num);
return(num);
}
──────────────────────────────────────────────────────────────────────────
Listing 6-12. The PROTO.C program.
Putting It All Together: A Larger Program
To sum up our work with functions and function calls, we present the
number game GETCLOSE.C (Listing 6-13), which uses 10 functions (counting
main()). Although this program is much longer than previous programs,
notice how the use of functions breaks this large, more complex program
into manageable pieces. Look at the following listing; then we'll see how
easy to understand this program actually is.
──────────────────────────────────────────────────────────────────────────
/* getclose.c -- a number game using */
/* random numbers */
#define TRUE 1
#define FALSE 0
/* external variables */
int number, /* total number in current game */
moves, /* number of moves in current game*/
target, /* target number to reach */
done, /* true if game is over */
score, /* score of current game */
wins = 0, /* number of games won */
losses = 0, /* number of games lost */
total = 0; /* total score */
char move;
/* function prototype declarations */
void intro(void); /* tell player about game */
char getyn(void); /* get yes/no response */
int random(int num); /* random between 1 and num */
void new_target(void); /* target number for game */
char get_move(void); /* get player's move */
void do_move(void); /* generate num from move */
void check_move(void); /* won, lost, or continue? */
void show_score(void); /* show score for game */
void show_total(void); /* show total score */
main()
{
intro(); /* print instructions */
while (TRUE) /* new games until user quits */
{
printf("\nDo you want to continue? ");
if (getyn() != 'y')
break; /* exit program */
done = FALSE;
number = moves = score = 0;
new_target(); /* target number for this game */
while (!done) /* play one game */
{
get_move();/* user selects random number */
do_move(); /* generate random number */
/* and add */
check_move(); /* win, lose, or continue? */
}
show_score(); /* score for this game */
show_total(); /* total score */
}
}
void intro(void)
{
printf("Welcome to Getclose\n\n");
printf("The object of this game is to\n");
printf("try to get as close to the target\n");
printf("number as possible in as few\n");
printf("moves as possible by choosing from\n");
printf("various ranges of random numbers.\n");
printf("You score if you get within 4 of the\n");
printf("target. You get a 100-point bonus for\n");
printf("hitting the target, but you get no score\n");
printf("if you go over.\n\n");
}
char getyn(void)
/* get yes or no answer */
/* repeats until valid entry */
{
char ch; /* character to read and return */
while (TRUE)
{
printf(" (y or n) ");
ch = getche();
printf("\n");
if ((ch == 'y') || (ch == 'n'))
/* valid response, break out of loop */
break;
/* give error message and loop again */
printf("please enter ");
}
return(ch);
}
int random(int num)
/* generate random number between 1 and num */
/* doesn't use library function srand() because */
/* we don't want the same seed each time */
{
long seconds, result;
time(&seconds); /* randomize with system time */
return(abs ((int)seconds * rand() % num) + 1);
}
void new_target(void)
/* generate a new target number */
/* between 50 and 99 */
{
target = 50 + random(49);
printf("\nYour target for this game is %d\n",
target);
}
char get_move(void)
{
while (TRUE)
{
printf("\nPick a random number from 1 to\n");
printf("a) 5 b) 10 c) 25 d) 50 e) 100 ");
move = getche();
if ((move >= 'a') && (move <= 'e'))
{
++moves; /* count the move */
break; /* valid response */
}
/* invalid response, try again */
printf("\nPlease type a, b, c, d, or e\n");
}
}
void do_move(void)
{
int num = 0; /* random value to obtain */
switch (move)
{
case 'a' :
num = random(5);
break;
case 'b' :
num = random(10);
break;
case 'c' :
num = random(25);
break;
case 'd' :
num = random(50);
break;
case 'e' :
num = random(100);
break;
}
number += num; /* add new number to total */
printf("\n\nYou got a %d. Number is now: %d ", num, number);
printf("(Target is %d)\n", target);
}
void check_move(void)
{
int temp;
if (number > target)
{
printf("\nYou went over! ");
printf("No score this game.\n");
losses++;
done = TRUE; /* to break out of loop */
}
if (number == target)
{
printf("\nYou hit the target ");
printf("for 100 bonus points!\n");
score = (100 / moves) + 100;
total += score;
wins++;
done = TRUE;
}
if ((number >= (target - 4)) && (number < target))
{
temp = 100 / moves;
/* does player want to go for broke? */
printf("\nTake %d points (y) or continue (n)? ",
temp);
if (getyn() == 'y')
{
score = temp;
total += score;
wins++;
done = TRUE;
}
}
}
void show_score(void)
{
printf("\nYou scored %d points in %d moves.\n",
score, moves);
}
void show_total(void)
{
printf("You have won %d game(s) ", wins);
printf("and lost %d.\n", losses);
printf("Your total score is %d\n", total);
}
──────────────────────────────────────────────────────────────────────────
Listing 6-13. The GETCLOSE.C program.
Overview of the Game
GETCLOSE.C is a number game in which you try to reach a randomly generated
target number between 50 and 99 in as few moves as possible. Each "move"
consists of choosing one of five possible ranges of random numbers: 1─5,
1─10, 1─25, 1─50, and 1─100. You start at zero, and each number you choose
is added to your total. Thus, each move brings your total closer to the
target number. If you get within 4 of the target, you can settle for a
score that depends on the number of moves you've made, or you can continue
to try to hit the target exactly. If you actually hit the target number,
you get a 100-point bonus. If you go over, however, you lose and score
nothing. The program lets you play new games, and it keeps track of your
total score and the number of games you have won and lost.
The strategy of the game involves deciding how large a range from which to
pick the next random number. If you pick from the larger ranges, you can
reach the target number in only a few moves, gaining you a high score. But
the big ranges also present you with a greater chance of overshooting the
target, giving you no points at all. (Playing the game is a little like
playing blackjack, except that you have five different decks from which to
choose cards.)
Playing the Game
Type in the program, run it, and play a few games to get an idea of the
different operations the program performs. Then read the next section to
explore the program's inner workings. The following is a sample game:
Welcome to Getclose
The object of this game is to
try to get as close to the target
number as possible in as few
moves as possible by choosing from
various ranges of random numbers.
You score if you get within 4 of the
target. You get a 100-point bonus for
hitting the target, but you get no score
if you go over.
Do you want to continue? (y or n) y
Your target for this game is 93
Pick a random number from 1 to
a) 5 b) 10 c) 25 d) 50 e) 100 e
You got a 31. Number is now: 31 (Target is 93)
Pick a random number from 1 to
a) 5 b) 10 c) 25 d) 50 e) 100 d
You got a 13. Number is now: 44 (Target is 93)
Pick a random number from 1 to
a) 5 b) 10 c) 25 d) 50 e) 100 d
You got a 19. Number is now: 63 (Target is 93)
Pick a random number from 1 to
a) 5 b) 10 c) 25 d) 50 e) 100 d
You got a 13. Number is now: 76 (Target is 93)
Pick a random number from 1 to
a) 5 b) 10 c) 25 d) 50 e) 100 c
You got a 16. Number is now: 92 (Target is 93)
Take 20 points (y) or continue (n)? (y or n) y
You scored 20 points in 5 moves.
You have won 1 game(s) and lost 0.
Your total score is 20
The main() Function
As you have seen, the main() function of a C program controls the overall
flow of the program, while calls to various functions do the actual work.
To explore GETCLOSE.C, look at the structure of main(). Even without
knowing how the functions work, you can see the general structure of the
program. First, it calls the intro() function to explain the game briefly.
(Notice that we commented each of the function prototype declarations in a
block before main(). This quickly lets you understand the purpose of every
function.)
The outer while loop permits unlimited games until the user decides to
quit. At the start of each game, new_target() generates a new target
number, and then the inner while loop processes the player's moves until
the game ends. In this loop, getmove() prints a menu and lets the user
select a range of random numbers for the next move. The do_move() function
gets the random number and adds it to the player's current total;
check_move() then compares the current total and the target number and
decides if the player has won, lost, or can continue playing.
Finally, when the inner loop (which represents one game) ends,
show_score() displays the score of the last game, and show_total()
displays the total score and the games won and lost thus far.
You can also use the QuickC Calls menu to help you understand how the
function calls work. First, compile GETCLOSE.C with Debug selected, and
then move the cursor to the body of a function you want to examine. Use
the Debug menu to set a breakpoint there, then run the program. When the
program stops at the breakpoint, pull down the Calls menu to see a list of
all called functions. Then display the text of any listed function by
selecting it. The Calls menu is especially useful for examining programs
that nest function calls.
Modifying GETCLOSE.C
One of the best ways to improve your knowledge of C is to take a program
such as GETCLOSE.C and try to add features to it. You might already have
some ideas in mind from playing and studying the game. Here are some
possibilities:
1. Give points for how close the player gets to the target number.
2. Generate a "poison number" between 1 and the target number minus 10.
If the player gets within 4 of this number without going over, he or
she loses.
3. Similarly, you could specify a "free number" that, when reached, is
not counted as a turn, thus potentially increasing the player's score.
────────────────────────────────────────────────────────────────────────────
PART 3 ADVANCED C TOPICS
────────────────────────────────────────────────────────────────────────────
────────────────────────────────────────────────────────────────────────────
Chapter 7 Arrays
In programming, it is often advantageous to collect variables into sets or
lists, so that many values can be stored and manipulated as a single
conceptual unit. Such a collection of variables, when all those variables
are the same type, is called an array. Arrays are used to organize values
in applications that range from the "top ten" scores in a video game to
the payroll records for thousands of employees.
To visualize the advantage of arrays over simple variables, imagine that
you run a business and that you want to store each employee's working
hours in your computer. If you have even two employees, a year's worth of
variables might look like this:
int emp1_jan_1, emp2_jan_1;
int emp1_jan_2, emp2_jan_2;
...──────────────────────────────────And so on for 362 intervening lines
int emp1_dec_31, emp2_dec_31;
Clearly, even the typing is a monumental undertaking. You can express the
same number of variables as an array in C in a single line of code:
int employees[2][365];
Without a doubt, arrays let you organize data more concisely than do
simple variables. This is convincingly illustrated in Figure 7-1.
Although the details of declaration, storage, and retrieval differ from
language to language, the basic nature of an array does not. In its
simplest form, an array consists of one or more variables of the same
storage type (size and number of bytes), arranged one after the other,
continuously upward in memory. All variables in an array are referenced by
a single name, called an identifier.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 7-1 can be found on p.190 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 7-1. Arrays provide superior organization over simple variables for
many common C programs.
How Arrays Are Stored in Memory
The elements in an array are stored consecutively in memory. An array
consisting of only four int values, for example, is stored in memory as
shown in Figure 7-2. Four int values (of two bytes each on the IBM PC)
are arranged together in ascending order in memory. That is, the array
begins with the leftmost int──the one lowest in memory──and continues
upward in memory with one or more adjoining int values.
┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
Array of ──── │ int │ int │ int │ int │
4 ints └──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
Memory ───────── 46 47 48 49 50 51 52 53
location
Figure 7-2. An array of four int values as stored in memory.
How to Declare Arrays
When you declare an array, you must tell the compiler how many items of
which data type to set aside in memory for storage and the name to use
when referencing that storage.
The rules for declaring an array in C are relatively straightforward:
■ First, declare the type (char, float, int, etc.).
■ Second, declare the name. Array names use the same naming conventions
as variable names.
■ Third, declare the number of items in the array by placing an integer
constant expression inside square brackets; for example, [15].
You recall, of course, that an "integer constant expression" is any
integer constant value or any combination of integer constant values and
arithmetic operators. Thus, 15 is an integer constant value, and so are 3,
0x0F, and `a'. The last is a character constant that C views as an integer
constant. But 3.0 is a floating-point constant and thus illegal for
specifying the array size. The expression 15 * 2 is an integer constant
expression because it is one integer constant value multiplied by another.
Remember, you cannot specify the number of elements in an array with a
variable. The following array declaration, for example, is illegal:
int line_num = 15;
int widths[line_num];
──────────────────────────────────────────────────────────────────────────
BASIC and Pascal Array Declarations
Declaring an array in C is similar to declaring an array in BASIC and
Pascal. In BASIC, an array is declared as widths%(15); in Pascal, the same
array is declared as widths : ARRAY [1...15] OF INTEGER. In C, we declare
this array as:
int widths[15];
All three of these declarations identify an array named widths composed of
15 integer variables. Each sets aside room in memory for 15 int values (30
bytes on the IBM PC because each int occupies two bytes). As you can see,
declaring an array in C retains the simplicity of BASIC but is clearer
than BASIC.
The 1...15 expression in the Pascal declaration tells the compiler that
the elements in the array are represented by the numbers 1 through 15. In
C, array elements are always numbered beginning with 0 and ascending to
the specified number.
──────────────────────────────────────────────────────────────────────────
You cannot use the expression line_num to declare the number of elements
in the array widths. The C compiler looks at only one small piece of a
program at a time. It first sees int line_num = 15 and generates code to
store the value 15 into the variable line_num. When it reaches the array
declaration, all it knows of line_num is that it currently contains the
value 15; QuickC has no idea how that value will change as the program
executes.
In C, a constant variable, that is, one declared with the ANSI keyword
const, is not considered a constant value. Therefore, the following
example is illegal:
const int line_num = 15;
int widths[line_num];
Using the const keyword declares to the compiler that you do not intend to
change the value of line_num. It does not prevent a bug in your program
from accidentally changing that value.
If you attempt to declare the size of an array with a variable, even a
const variable, QuickC will print the following error message:
Error C2057:
expected constant expression
Examine the ARRAY1.C program (Listing 7-1). This "do-nothing" program
demonstrates the correct way to declare arrays.
In this program, SIZEOARRAY is specified using #define as 26. The number
of items in ages is therefore declared with 26 * 2, which is legal.
──────────────────────────────────────────────────────────────────────────
/* array1.c -- how to declare arrays legally */
#define SIZEOARRAY 26
main()
{
char initials[26];
int num_men[26], num_women[SIZEOARRAY];
float ages[SIZEOARRAY * 2];
}
──────────────────────────────────────────────────────────────────────────
Listing 7-1. The ARRAY1.C program.
Referencing and Using Array Items
The way you reference the items in an array (whether to store values in
them or to fetch values from them) looks very much like the declaration.
You merely state the identifier for the array and place the offset of the
item within square brackets. The offset is always measured from the
beginning of the array, with the first item having an offset of 0.
For example, the expression
widths[1] = 3;
stores the value 3 in the second item of the array named widths; that is,
the item whose offset from the beginning of the array is 1.
Conversely, the expression
this_width = widths[1];
retrieves the value of the second item of the array widths and assigns
that value to the variable this_width.
In C, the offset into an array can be the result of any expression that
returns a value. It can be the value of a variable, the result of a
computation or logical test, or even the returned value of a function
call. The only restriction is that the array offset must be specified with
an integer value. The following are legal specifications:
widths[1]───────────────────────────────────────────Offset is a constant
widths[i]──────────────────────────────────────Offset is an int variable
widths[i++]──────────────────────────────────────Offset is a computation
widths[getnum()]───────────────────────────Return value of function call
widths['a']────────────────────────────────────Offset is a char constant
However, the following float type offset causes a compiler error:
widths[fltval]──────────────────────────────────────float offset (error)
Because there are no fractional memory locations, specifying an array's
offset with a float causes the following QuickC error:
error C2108:
non-integral index
With a legal offset specification, an array element is no different from
an ordinary variable of that type. You can perform the same operations
with an array item as you can with any ordinary variable. Consider, for
example:
widths[1] = 3;
total = widths[0] + widths[1] + widths[2];
In the first operation, the value 3 is stored in the second element of the
array named widths. In the second, values stored in three array elements
are added together. Notice that we use the same notation to access each──
an offset in brackets.
An Example
Now that you have the basic rules for declaring arrays and accessing items
in those arrays, examine the XMAS.C program (Listing 7-2).
In XMAS.C, we declare the array widths to contain 20 items of the type
int. That array is then filled with values by the for loop; this is one
way to store values in an array. Finally, each item in the array widths is
passed to the function Center_out(); this is one way to access the values
in an array.
──────────────────────────────────────────────────────────────────────────
/* xmas.c -- fills an array with values, then passes */
/* each of those values to a function */
main()
{
int i, j, widths[20];
void Center_out();
for (i = 0, j = 1; i < 18; ++i, j += 2)
{
widths[i] = j;
}
widths[i++] = 3;
widths[i] = 3;
for (i = 0; i < 20; i++)
{
Center_out('X', widths[i]);
}
}
void Center_out(char character, int width)
{
int i;
for (i = 0; i < ((80 - width) / 2); ++i)
{
putch(' ');
}
for (i = 0; i < width; ++i)
{
putch(character);
}
putch('\r');
putch('\n');
}
──────────────────────────────────────────────────────────────────────────
Listing 7-2. The XMAS.C program.
Whenever you reference an array element, the value of that element becomes
available for use──you can assign the value to other variables or pass it
to a function. Note, however, that passing an array element by giving its
name and an offset merely passes a copy of that element, not the element
itself. This is the method for passing ordinary variables.
Bounds Checking Arrays in Your Code
In XMAS.C, the for loop prevents your specifying an offset beyond the end
of the array widths:
for (i = 0; i < 20; ++i)
In many programs, however, the ability to exceed an array's bounds is not
prevented by your code but is controlled by the user. For example, the
SADD.C program (Listing 7-3) is a simple adding machine that lets the
user enter as many as three numbers, one per line, and terminate entry
with any non-numeric character such as an x.
Now run SADD.C and enter (on separate lines) the numbers 1, 2, and 3,
followed by an x character. The program displays the correct sum,
which is 6:
1
2
3
x
----------
6
──────────────────────────────────────────────────────────────────────────
/* sadd.c -- a small adding machine that illustrates */
/* the need for array bounds checking */
main()
{
int offset = 0, i, result = 0;
int stack[3];
while (scanf("%d", &stack[offset]) == 1)
{
++offset;
}
for (i = 0; i < offset; ++i)
{
result += stack[i];
}
printf("----------\n");
printf("%d\n", result);
}
──────────────────────────────────────────────────────────────────────────
Listing 7-3. The SADD.C program.
Now run the program again, but this time enter four numbers:
1
2
3
4
x
----------
20
The result shown is 20, which is wrong. But QuickC doesn't recognize that
an error occurred and prints no error message. The C language itself,
unlike Pascal and BASIC, contains no provisions to prevent offsets beyond
the end of an array.
To understand why SADD.C referenced stack[3], even though no fourth item
exists (remember that arrays begin with item 0), let's look again at the
way arrays are arranged in memory. As a bonus, we'll find out where the
value 20 came from. Figure 7-3a shows how the compiler translates into
memory the declarations from SADD.C (shown on the next page).
┌──────────────────────────────────────────────────────────────┐
│° Initialized °│
│° int offset=0, i, result=0;▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ °│
│° int stack[3]; ▒ °│
│° ▒▒▒▒▒while (scanf("%d", &stack [offset]) == 1) ▒ °│
│° ▒ ++offset; ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ °│
│° ▒ ▒ ▒ °│
│° ▒ ┌────────┬────────┬────────┬────▼───┬────────┬────▼───┤
│° ▒ │ ? │ ? │ ? │ 0 │ ? │ 0 │
│° ▒ └────────┴────────┴────────┴────────┴────────┴────────┤
│° ▒ stack[0] stack[1] stack[2] result i offset◄├─────┐
│° ▒ °│ │
└─────────────▒────────▒────────▒────────▒─────────────────────┘ │
▒▒▒▒▒▒▒▒▒▒1▒▒▒▒▒▒▒▒2▒▒▒▒▒▒▒▒3▒▒▒▒▒▒▒▒4 │
│ │ │
└──────────────┬─────────────┘ │
│ │
Stores these input values │
while incrementing offset──────────────────────────┘
(A)
┌────────┬────────┬────────┬────────┬────────┬────────┐
│ 1 │ 2 │ 3 │ 4 │ ? │ 4 │
└────────┴────────┴────────────────┴────────┴────────┘
stack[0] stack[1] stack[2]│ result i offset
│
Array ends here───┘ │
│
Position of stack[3]
(B)
Figure 7-3. Consequence of referencing beyond the end of an array.
int offset = 0, i, result = 0;
int stack[3];
First, the variables offset and result are set to 0. Then the while loop
fills out the items in the 3-element array stack. The last value entered
was 4, which was the fourth value placed into stack. But stack contains no
fourth item (that is, no stack[3] ), so the fourth value is placed into
result because result occupies the place in memory that follows the stack
array.
The arrangement depicted in Figure 7-3a occurs because QuickC places auto
variables into memory from the top down, but it places static and global
variables into memory from the bottom up.
Now look closely at Figure 7-3b. Because result begins with a value of 4
instead of 0, the for loop adds 1, 2, and 3 to it, resulting in a sum of
10. The for loop continues (because offset is 4) by adding stack[3] to
result (both reference the same address and the same value, 10), thus
yielding the erroneous value of 20.
To make programs less sensitive to improper user input, always provide
code that detects array out-of-bounds conditions. You can do this by
simply terminating the program when an error is detected, but writing your
program so that it can recover from errors is better. The SADD2.C program
(Listing 7-4) shows a common approach to array bounds checking that
corrects the previous program's weakness.
──────────────────────────────────────────────────────────────────────────
/* sadd2.c -- a small adding machine that includes */
/* array bounds checking */
#define MAXSTAK 3
main()
{
int offset = 0, i, result = 0;
int stack[MAXSTAK];
while (scanf("%d", &stack[offset]) == 1)
{
if (++offset >= MAXSTAK)
{
printf("Stack Full\n");
break;
}
}
for (i = 0; i < offset; ++i)
{
result += stack[i];
}
printf("----------\n");
printf("%d\n", result);
}
──────────────────────────────────────────────────────────────────────────
Listing 7-4. The SADD2.C program.
In this revision of SADD.C, we use the preprocessor #define directive to
create an alias for the size of the array stack. Thus, you can later
easily change the number of items in stack by changing a single #define
and then recompiling.
Next, we insert an if statement that checks a preincremented offset to be
sure it does not exceed the number of items in stack, that is, MAXSTAK. If
offset becomes too large, the if statement first causes a warning message
to be printed and then breaks out of the while loop. The user gets an
accurate sum for the numbers entered, despite the error, and any
out-of-range numbers are simply ignored.
How to Initialize Arrays
When you declare an auto array (an array declared inside a function and
without the keyword static), it is initially filled with garbage values,
regardless of its type and size. (This also occurs when you create auto
variables.) On the other hand, static arrays and arrays declared outside
of functions always have their initial values set to zero──again just like
ordinary variables. For example:
char Letters[26];───────────────────────────────────Initialized to zeros
main()
{
char vowels[5];──────────────────────────────────Starts with garbage
static char consonants[21];─────────────────────Initialized to zeros
When zero is not an appropriate initial value, you can give static and
global variables starting values of your choice. To initialize an array:
■ Follow the right square bracket (]) in the array identifier with an
assignment operator (=) and a left brace ({).
■ Then state each initializing value, followed by a comma.
■ Finally, terminate your comma-separated list of initializers with a
right brace (}) and the usual semicolon.
For example, to initialize the array Letters with the letters of the
alphabet, you need simply declare it as:
char Letters[26] = { 'a', 'b', 'c', 'd', 'e',
'f', 'g', 'h', 'i', 'j',
'k', 'l', 'm', 'n', 'o',
'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y',
'z', };
Or, for an array of numbers you could declare:
int Values[5] = { 12, 2, 44, 19, 7 };
In both of the above examples, the type of the initializing values matches
the type of the array and is a constant value. This is mandatory. Values
in array initializing
lists must be constant values or constant expressions, and those values
must fit in the number of bytes declared for each array item. The array
Letters is initialized with the type char, and `a' is a char constant. The
array Values is initialized with type int, and 12 is an integer constant.
A float array would have to be initialized with the type float, such as
76.98, which is a floating-point constant.
The ASIMOV.C program (Listing 7-5) contains in the initialized array
Letters the name of a famous Isaac Asimov character. By entering the
correct series of numbers, you can reveal that name.
──────────────────────────────────────────────────────────────────────────
/* asimov.c -- illustrates how to initialize an */
/* array with starting values */
#define MAXL 16
char Letters[MAXL] = {
'e', 'I', 'a', 'N', 'o', 'R', 'O', 'o',
'u', 't', 'o', 'R', 'l', 'o', 'B', 'b', };
main()
{
int num, i;
printf("Guess my identity with numbers.\n");
printf("(any non-number quits)\n\n");
while (scanf("%d", &num) == 1)
{
if (num <= 0)
{
printf("Guesses must be above zero\n");
continue;
}
for (i = 1; i <= num; ++i)
{
printf("%c", Letters[(i * num) % MAXL]);
}
printf("\n");
}
}
──────────────────────────────────────────────────────────────────────────
Listing 7-5. The ASIMOV.C program.
──────────────────────────────────────────────────────────────────────────
Quick Tip
The list of initializers for Letters ends with a trailing comma. That is
not an error. Trailing commas in initializer value lists are optional but
enable long lists to be rearranged easily with your text editor.
──────────────────────────────────────────────────────────────────────────
Letting the Compiler Supply the Size
When you declare the values for an initialized array, it is not always
possible, or necessary, to state explicitly the number of items in the
array. C provides an alternative. For example, the following declaration
omits the size of the array:
int Primes[ ] = {1, 2, 3, 5, 7, 11,};
└─────────────────────────────────── Number of items omitted
Whenever you omit the size, the C compiler counts the number of
initializers and dimensions the array to the correct size for that count.
Because the preceding example contains six initializers, that declaration
is equivalent to declaring:
int Primes[6] = {1, 2, 3, 5, 7, 11,};
When the size of an array is omitted, you might expect bounds checking in
your code to be difficult. Fortunately, you can use the sizeof operator to
find the number of bytes in an array and thus specify, using #define, a
bounds-checking value. When used with the preceding declaration, the
expression
#define MAXL (sizeof(Primes)/sizeof(int))
gives MAXL a value of 6.
The sizeof keyword, when given the name of an array, yields the number of
bytes in that array. The #define, then, is the number of bytes in the
array Primes (12 bytes) divided by the number of bytes in an int (2
bytes).
Overinitializing and Underinitializing
One good reason to omit the size of an array is that C permits a mismatch
between the number of initializing values and the size you declare. When
there are fewer initializers, the compiler silently fills the remainder
with zero values. When there are too many initializers, the compiler
complains and stops. Some compilers (especially under UNIX) will issue a
warning and truncate the array.
The UNDOVER.C program (Listing 7-6) demonstrates this behavior. As the
program stands, it prints the following message:
The first 6 primes are: 1 2 3 5 7 11
Now change Primes[6] to Primes[8] in the declaration and run the program
again. This time it prints:
The first 8 primes are: 1 2 3 5 7 11 0 0
The new result shows that underinitializing causes the compiler to fill
the leftover items with zero values. Now change the declaration again,
this time from Primes[8] to Primes[3]. This time the QuickC compiler stops
and issues the message:
error C2078:
too many initializers
──────────────────────────────────────────────────────────────────────────
/* undover.c -- illustrates the effect of underinitializing and */
/* overinitializing arrays */
int Primes[6] = { 1, 2, 3, 5, 7, 11 };
#define NUMP (sizeof(Primes) / sizeof(int))
main()
{
int i;
printf("The first %d primes are: ", NUMP);
for (i = 0; i < NUMP; ++i)
{
printf("%d ", Primes[i]);
}
printf("\n");
}
──────────────────────────────────────────────────────────────────────────
Listing 7-6. The UNDOVER.C program.
Arrays and Functions
As you saw earlier in XMAS.C, passing one of an array's elements to a
function is like passing an ordinary variable to a function. That is, a
copy of the value of that element is passed, not the element itself.
However, when you pass whole arrays to functions, the situation changes.
Although you are still passing by value, what you are actually passing is
the address of the array (its location in memory). The effect of this is
that you appear to be passing the array itself and that the array itself
will be changed. (See the next chapter for further details.) For now,
we'll simply show you how to pass arrays to functions and how to use those
arrays when they get there.
Passing Arrays to Functions
To pass an array to a function, merely state the array's name (minus the
offset) in the function call. For example, if you have declared an array
as follows:
static int list[7] = { 5, 1, 3, 7, 2, 4, 6 };
you would pass that array to a function, called Bub_sort() for example, by
stating its name, as follows:
Bub_sort(list);
This tells the compiler to send the entire array named list to Bub_sort().
At the receiving end, in Bub_sort(), you need to declare the type of the
received array. To do so, declare an array in the normal C manner and
leave the square brackets empty:
Bub_sort(vals)
int vals[];
{
This declares that a function named Bub_sort() will receive one argument,
the int array vals. Because Bub_sort() is receiving an array──via the
array's address──it receives the array itself and not a copy of that
array. This allows Bub_sort() to change the original array.
The BUBSORT.C program (Listing 7-7) demonstrates the differences between
passing array elements to functions and passing entire arrays.
──────────────────────────────────────────────────────────────────────────
/* bubsort.c -- passing an array to a function */
/* affects the original array */
#define NUMINTS 6
extern void Bub_sort();
extern void Triple();
main()
{
int num = 2, i;
static int list[NUMINTS] = { 6, 5, 4, 3, 2, 1 };
printf("num before Triple = %d\n", num);
Triple(num);
printf("num after Triple = %d\n", num);
printf("list[0] before Triple = %d\n", list[0]);
Triple(list[0]);
printf("list[0] after Triple = %d\n", list[0]);
printf("Before sorting -> ");
for (i = 0; i < NUMINTS; ++i)
{
printf("%d ", list[i]);
}
printf("\n");
Bub_sort(list);
printf("After sorting -> ");
for (i = 0; i < NUMINTS; ++i)
{
printf("%d ", list[i]);
}
printf("\n");
}
void Triple(int x) /* function doesn't affect original */
{
x *= 3;
}
void Bub_sort(int vals[NUMINTS]) /* function changes original */
{
int i, j, temp;
for (i = (NUMINTS - 1); i > 0; --i)
{
for (j = 0; j < i; ++j)
{
if (vals[j] > vals[j+1])
{
temp = vals[j];
vals[j] = vals[j+1];
vals[j+1] = temp;
}
}
}
}
──────────────────────────────────────────────────────────────────────────
Listing 7-7. The BUBSORT.C program.
This program first calls the Triple() function, passing it both an
ordinary variable and one of the elements in the array list. The value of
the original isn't changed in either case; only the value (a copy) of what
is sent is changed.
Next, we pass the array list to the function Bub_sort() (a simple bubble
sorting function). The program prints the array before and after the
function call to demonstrate that the Bub_sort() function changes the
values in the original array; it does not sort a copy.
Variations
When a function receives an array, the number of elements in the
declaration of that array is usually omitted because the size
specification is optional:
Bub_sort(vals)
int vals[ ];
└──────────────────────────────────────── Size of array omitted
Restating that size in the function declaration, however, is often a good
idea. As we have seen, C does no array bounds checking on your behalf, so
restating the size of the received array helps to clarify and document
your program:
Bub_sort(vals)
int vals[NUMINTS];
└────────────────────────────────────── Restated for clarity
The type of the received array should also match that of the original. If
the types do not match, your program might not work properly. The reason
we say "might not" is that you might want to mismatch types intentionally.
The HEXOUT.C program (Listing 7-8) demonstrates such a planned mismatch.
This program asks you to enter a floating-point number and then prints out
that number, one byte at a time, in hexadecimal notation.
HEXOUT.C first reads a floating-point value into the array fary──a float
type array consisting of only one element. That array is then passed to
Hexout(). In Hexout(), we declare the type of the received array as
unsigned char. This "deception" causes Hexout() to handle the array fary
as if it were an array of single unsigned bytes, whereas the original was
actually a 4-byte float type. We will explain this shortly. This deception
illustrates one of C's primary strengths, the freedom of the programmer to
change types in midstream.
──────────────────────────────────────────────────────────────────────────
/* hexout.c -- prints a floating-point variable in */
/* hexadecimal format */
extern void Hexout();
main()
{
float fary[1];
printf("Enter a floating-point number\n");
printf("(Any non-numeric entry quits)\n\n");
while (scanf("%f", &fary[0]) == 1)
{
Hexout(fary);
}
}
void Hexout(unsigned char chary[])
{
int i;
for (i = 0; i < sizeof(float); ++i)
{
printf("%02X ", chary[i]);
}
printf("\n\n");
}
──────────────────────────────────────────────────────────────────────────
Listing 7-8. The HEXOUT.C program.
How Array Offsets Advance
When you reference an array element with an array name and an offset,
QuickC invisibly converts the element offset to a bytes-in-memory offset.
To illustrate this, let's look at how two different type arrays are
organized in memory. Figure 7-4 shows that an array of char type occupies
one byte of memory for each element and that an array of float type
occupies four bytes of memory for each element.
For the char array, chary[4], each element occupies one byte, so each
element offset specification corresponds to the bytes-in-memory offset.
For the float array fary[2], however, each element occupies four bytes, so
each element offset specification corresponds to a bytes-in-memory offset
of 4 bytes. In the latter case, when you specify
fary[1]
you are really telling the compiler to reference a value that is four
bytes into the array fary[]. Because a float value occupies four bytes of
memory, your element offset of 1 becomes a bytes-in-memory offset of 4
bytes.
This explains how, in the preceding program, HEXOUT.C, it is possible to
print out a float one byte at a time. Inside the function Hexout(), the
compiler thinks it is handling an array of unsigned char type, in which
each element occupies a single byte. Thus, i increments in unmultiplied
steps of single bytes.
char chary[4] = {'t', 'e', 's', 't'};
│ 1 byte │
│ │
├────────┼────────┬────────┬────────┐
│ 't' │ 'e' │ 's' │ 't' │
└────────┴────────┴────────┴────────┘
(A) ARRAY OF char VALUES
float fary [2] = {1.2, 4.3};
│ 1 byte │
│ │
├────────┴────────┬────────┬────────┬────────┬────────┬────────┬────────┐
│ 1.2 │ 4.3 │
└────────┴────────┴────────┴────────┴────────┴────────┴────────┴────────┘
fary[0] fary[0]
(B) ARRAY OF float VALUES
Figure 7-4. How offsets differ by number of bytes based on the type of the
array.
Multidimensional Arrays
In C you can easily create and use arrays of two, three, or many
dimensions. Two-dimensional arrays correspond to such useful items as
calendars, spreadsheets, and maps. We begin with the rules for
two-dimensional arrays and then apply them to arrays of three and more
dimensions. Three-dimensional arrays find application in items such as 3-D
graphics, layered indexes, and solid topology. Many-dimensioned arrays
delve into the arcane worlds of higher math and complex games.
Two-dimensional Arrays
Two-dimensional arrays represent rectangular grids of data. As illustrated
in Figure 7-5, they are organized in rows first and then columns. Because
computer memory is linear, those rows and columns are stored in memory one
row after the other.
The rules for declaring a two-dimensional array are similar to those for
declaring a one-dimensional array. They take the following form:
type name[rows][columns];
As with one-dimensional arrays, you must specify the number of rows and
the number of columns as integer constant expressions. Thus, the
expression
int week[7][8];
declares a two-dimensional array of type int, named week, with 7 rows (for
7 days) and 8 columns (for 8 working hours per day). You can fill it with
any number of useful items, for example, the number of lines of code
written per hour per day.
Columns
│
┌───────────┴───────────┐
│ │
┌───────┬───────┬───────┐ ───┐
│ 1 │ 2 │ 3 │ │
├───────┼───────┼───────┤ │
│ 4 │ 5 │ 6 │ ├── Rows
├───────┼───────┼───────┤ │
│ 7 │ 8 │ 9 │ │
└───────┴───────┴───────┘ ───┘
│
│ Arranged in memory as
▼
┌───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┐
│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │
└───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘
│ │ │ │
└───────────┬───────────┴───────────┬───────────┴───────────┬───────────┘
│ │ │
Zeroth row First row Second row
Figure 7-5. Two-dimensional arrays are arranged in memory row by row.
Referencing two-dimensional array elements is as simple as referencing
elements of one-dimensional arrays. Specify the row and column with
integer expressions:
int day = 6, /* Saturday */
hour = 2; /* 10:00 A.M. */
printf("Wrote %d lines of code.\n", week[day][hour]);
Initializing Two-dimensional Arrays
Before we go into the somewhat more complex rules for initializing
two-dimensional arrays, enter the MAGIC.C program (Listing 7-9). This
program initializes an array of type int with scrambled numbers and then
asks the user to rearrange those numbers into the correct order by
continually swapping squares adjacent to the 0 with the 0. The object is
to rearrange the numbers into ascending order, with 0 at the top left,
counting up row by row.
──────────────────────────────────────────────────────────────────────────
/* magic.c -- demonstrates use of a two-dimensional */
/* array of type int */
main()
{
static int square[3][3] = { 5, 8, 3, 4, 2, 0, 7, 1, 6 };
int zrow = 1, zcol = 2; /* location of the zero */
int num, row, col, i, j, rowdist, coldist;
while (1)
{
printf("Swap what with zero?\n");
printf("(Q to quit)\n");
/* Print the square. */
for (i = 0; i < 3; ++i)
{
for (j = 0; j < 3; ++j)
{
printf(" %d ", square[i][j]);
}
printf("\n");
}
/* Enter the user number. */
if ((num = getch()) == 'Q')
exit(0);
num -= '0';
if (num < 1 || num > 9)
{
printf("Not a legal number.\n\n");
continue;
}
/* Find that square. */
for (row = 0; row < 3; ++row)
{
for (col = 0; col < 3; ++col)
{
if (num == square[row][col])
{
goto GOTIT;
}
}
}
GOTIT:
/* Check for a legal move. */
if (row > 2 || col > 2)
{
printf("Bad Box Specification\n\n");
continue;
}
rowdist = zrow - row;
if (rowdist < 0)
rowdist *= -1;
coldist = zcol - col;
if (coldist < 0)
coldist *= -1;
if (rowdist > 1 || coldist > 1)
{
printf("Not a Neighbor\n\n");
continue;
}
/* Make the move. */
square[zrow][zcol] = square[row][col];
square[row][col] = 0;
zrow = row;
zcol = col;
/* See if done, and solved. */
for (i = 0; i < 3; ++i)
{
for (j = 0; j < 3; ++j)
{
if (square[i][j] != ((i * 3) + j))
{
break;
}
}
}
if ((i * j) == 9)
break;
}
printf("\n\aYOU GOT IT !!!\n");
}
──────────────────────────────────────────────────────────────────────────
Listing 7-9. The MAGIC.C program.
We initialize the two-dimensional array in MAGIC.C by filling it row by
row. When, for clarity, you want to specify where one row ends and the
next begins, you can enclose each row's initializers in another set of
braces:
static int square[3][3] = {
{5, 8, 3},─────────────────────────────────────────────────────Row 0
{2, 4, 0},─────────────────────────────────────────────────────Row 1
{7, 1, 6}──────────────────────────────────────────────────────Row 2
};
This amounts to specifying each row as its own subset of initializers,
clearly a more readable arrangement.
To underinitialize a 3-by-3 array, we could use the following:
static int square[3][3] = {
5, 8, 3, 2, 4, 7, 1, 6 };
└─────────────────────── One initializer short
Here the last column of the last row is omitted and thus is 0 by default.
We also could underinitialize by a selected row, as follows:
static int square[3][3] = {
{5, 8, 3},
{2, 4},──────────────────────────────────────────────────Row 1 short
{7, 1, 6}
};
Here we omit the third column of the second row, thus setting the value to
0.
Two-dimensional Arrays and Functions
As with one-dimensional arrays, you pass a two-dimensional array to a
function by merely stating its name:
Make_move(board);
Again, this passes the two-dimensional array itself, not a copy.
For the receiving function, Make_move(), you must always declare the size
of the columns. The number of rows──as with one-dimensional arrays that
have only one row──is optional, as shown in the following legal example:
Make_move(field)
int field[][3];
The TTT.C program (Listing 7-10 on the next page) is a somewhat
unsophisticated tic-tac-toe game that will help you understand how to use
two-dimensional arrays. It's an easy game to win. Forcing a tie, however,
is difficult. For clarity, the declaration for field in Make_move()
includes the number of rows.
──────────────────────────────────────────────────────────────────────────
/* ttt.c -- a tic-tac-toe game demonstrates */
/* passing two-dimensional arrays */
/* to functions */
main()
{
static char board[3][3] = {
{ '-', '-', '-' },
{ '-', '-', '-' },
{ '-', '-', '-' },
};
int row, col, ch;
extern char Check_winner();
extern void Make_move(), Draw_field();
printf("You are X and make the first move.\n");
while (1)
{
printf("Specify coordinate for your X.\n");
printf("(For example, a2, or Q to quit)\n");
/* Print the square. */
Draw_field(board);
/* Enter the user's coordinates. */
if ((row = getch()) == 'Q')
exit(0);
row -= 'a';
col = getch() - '1';
/* Check for a legal move. */
if (row < 0 || row > 2 || col < 0 || col > 2)
{
printf("Bad Square Specification\n\n");
continue;
}
if (board[row][col] != '-')
{
printf("Sorry, Square Occupied\n\n");
continue;
}
/* Make the move. */
board[row][col] = 'X';
if ((ch = Check_winner(board)) != '-' || ch == 't')
break;
Make_move(board);
if ((ch = Check_winner(board)) != '-' || ch == 't')
break;
}
Draw_field(board);
if (ch == 't')
printf("It's a tie!\n");
else if (ch == 'X')
printf("You win!\n");
else
printf("I win!\n");
}
char Check_winner(char field[][3])
{
int row, col;
for (row = col = 0; row < 3; ++row, ++col)
{
if (field[row][0] != '-' /* horizontal */
&& field[row][0] == field[row][1]
&& field[row][1] == field[row][2])
{
return(field[row][0]);
}
if (field[0][col] != '-' /* vertical */
&& field[0][col] == field[1][col]
&& field[1][col] == field[2][col])
{
return(field[0][col]);
}
}
if (field[0][0] != '-' /* right diagonal */
&& field[0][0] == field[1][1]
&& field[1][1] == field[2][2])
{
return(field[0][0]);
}
if (field[0][2] != '-' /* left diagonal */
&& field[0][2] == field[1][1]
&& field[1][1] == field[2][0])
{
return(field[0][2]);
}
for (row = 0; row < 3; ++row) /* any moves left */
{
for (col = 0; col < 3; ++col)
{
if (field[row][col] == '-')
{
return('-');
}
}
}
return ('t');
}
void Make_move(char field[3][3])
{
int row, col;
for (row = 2; row >= 0; --row)
{
for (col = 2; col >= 0; --col)
{
if (field[row][col] == '-')
{
field[row][col] = 'O';
return;
}
}
}
}
void Draw_field(char field[][3])
{
int row, col;
printf("\n 1 2 3\n\n");
for (row = 0; row < 3; ++row)
{
printf("%c ", 'a' + row);
for (col = 0; col < 3; ++col)
{
printf(" %c ", field[row][col]);
}
printf("\n");
}
printf("\n");
}
──────────────────────────────────────────────────────────────────────────
Listing 7-10. The TTT.C program.
Arrays of Three and More Dimensions
In C you can give an array an unlimited number of dimensions, but remember
that the more dimensions an array has, the more unmanageable it becomes.
You have already seen how two-dimensional arrays are declared,
initialized, and passed to functions. The rules for using more dimensions
are a simple extension of those same concepts.
Declaring Multidimensional Arrays
The general rule for declaring multidimensional arrays is as follows:
type name[exp][exp][exp][exp] ...
│ │ │ │ └─────────────────────────────────── Etc.
│ │ │ └──────────────────────────── Fourth dimension
│ │ └────────────────────────────────── Third dimension
│ └────────────────────────────────────── Second dimension
└──────────────────────────────────────────── First dimension
First, specify the type that will be stored in each array item, and then
name the entire array. Each exp is an integer constant expression that
specifies the number of elements in that dimension. Each succeeding
square-bracketed [exp] defines another dimension.
Think of a three-dimensional array as a cube. Figure 7-6 shows a
conceptualization of a cube that corresponds to the following declaration:
#define DEPTH 3
#define ROWS 3
#define COLS 3
int cube[DEPTH][ROWS][COLS];
│ └─────┬────┘
│ └─────────────── Size of each two-dimensional array
└─────────────────── Number of two-dimensional arrays (depth)
You can also think of this three-dimensional array as an array of three
two-dimensional arrays.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 7-6 can be found on p.213 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 7-6. Three-dimensional arrays can be thought of as a cube.
Initializing Multidimensional Arrays
When you use a list of values to initialize a multidimensional static or
global array, the compiler reads that list from left to right, filling the
array row by row for each plane of the depth. The following declaration
places the initializing values into cube, beginning with the value 1, as
shown in Figure 7-7.
int cube[3][3][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9,
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
22, 23, 24, 25, 26, 27};
To specify the order for initializing, you can enclose any group with
braces. Those braces correspond to the depth first, then to the rows and
columns. You can therefore rewrite the above declaration more clearly as
follows:
int cube[3][3][3] =
{
{ {1, 2, 3}, {4, 5, 6}, {7, 8, 9} },
{ {10, 11, 12}, {13, 14, 15}, {16, 17, 18} },
{ {19, 20, 21}, {22, 23, 24}, {25, 26, 27} }
};
Here each inner set of braces encloses a given row's list of initializers.
Use this technique when you need to underinitialize a given row, column,
or depth. Clearly, as you progress beyond three dimensions, initializing
can become very confusing. Just remember the general rules, or, in
despair, simplify your algorithm.
┌───────────────────────\
│\┌───────┬───────┬───────\
│ │ 19 │ 20 │ 21 │ \
│\├───────┼───────┼───────┤ \
│ │ 22 │ 23 │ 24 │ \
│\├───────┼───────┼───────┤ \
┌─\ │ 25 │ 26 │ 27 │ \
│ \└───────┴───────┴───────┘ \
│ \ ┌───────────────────────\
│ \ │\┌───────┬───────┬───────\
│ \ │ │ 10 │ 11 │ 12 │ \
│ \ │\├───────┼───────┼───────┤ \
│ \ │ │ 13 │ 14 │ 15 │ \
│ \ │\├───────┼───────┼───────┤ \
│ │ │ 16 │ 17 │ 18 │ \
Depth──┤ \└───────┴───────┴───────┘ \
│ \ ┌────────────────────────\
│ \ │\┌───────┬───────┬───────┐──┐
│ \ │ │ 1 │ 2 │ 3 │ │
│ \ │\├───────┼───────┼───────┤ │
│ \ │ │ 4 │ 5 │ 6 │ ├──Row
│ \│\├───────┼───────┼───────┤ │
│ \ │ 7 │ 8 │ 9 │ │
└─────────────────────────────────\└───────┴───────┴───────┘──┘
│ │
└───────────┬───────────┘
Columns
Figure 7-7. Initializing a three-dimensional array.
Using Multidimensional Arrays in Functions
To pass a multidimensional array to a function, you need specify only the
name of that array as an argument:
Draw_planes(cube);
On the receiving end──in the function Draw_planes()──you must specify the
sizes of all but the leftmost dimension. That size is optional, as in the
following:
int
Draw_planes(box);
int box[][3][3];
{
The BOX.C program (Listing 7-11) shows the initialization of a
three-dimensional array and then prints out the result.
──────────────────────────────────────────────────────────────────────────
/* box.c -- demonstrates the result of initializing */
/* a three-dimensional array */
main()
{
static int cube[3][3][3] = {
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
23, 24, 25, 26, 27 };
int plane;
extern void Draw_plane();
for (plane = 0; plane < 3; ++plane)
{
Draw_plane(cube, plane);
}
}
void Draw_plane(int box[3][3][3], int slice)
{
int row, col;
printf("Plane[%d] =\n", slice);
for (row = 0; row < 3; ++row)
{
for (col = 0; col < 3; ++col)
{
printf( "%2d ", box[slice][row][col]);
}
printf("\n");
}
printf("\n");
}
──────────────────────────────────────────────────────────────────────────
Listing 7-11. The BOX.C program.
Advanced Topics and Tricks
In this section we discuss three advanced techniques that can be very
handy:
■ Negative subscripting
■ Large and huge arrays
■ Passing pieces of arrays
Negative Subscripting
Recall that unless you include code to perform bounds checking, C lets you
reference items both beyond the end and before the beginning of an array.
You have already seen the consequences of referencing beyond an array's
end. Referencing before its beginning is something new, as Figure 7-8
demonstrates.
If you declare three consecutive arrays such as the following:
int first[3], second[3], third[3];
and then reference with a negative subscript:
second[-1]
you actually reference either third[2] or first[2], depending on whether
the arrays are auto or static, as shown in Figure 7-8. QuickC places auto
arrays into memory from right to left (top down) and static arrays from
left to right (bottom up).
second [-1]
│
│
┌───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┐
│ 7 │ 8 │ 9 │ 4 │ 5 │ 6 │ 1 │ 2 │ 3 │
└───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘
│ │ │ │
└───────────┬───────────┴───────────┬───────────┴───────────┬───────────┘
│ │ │
third [3] second [3] first [3]
(A) ARRAY DECLARED auto
second [-1]
│
│
┌───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┐
│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │
└───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘
│ │ │ │
└───────────┬───────────┴───────────┬───────────┴───────────┬───────────┘
│ │ │
first [3] second [3] third [3]
(B) ARRAY DECLARED static
Figure 7-8. Effect of negative subscripting on auto and static arrays.
The L2WORDS.C program (Listing 9-11 on pp. 282─83) illustrates this
technique.
Large and Huge Arrays
On the IBM PC an integer is 2 bytes long, so you have to be careful when
declaring arrays larger than 32,767 elements. That is because 32,767 is
the highest positive value a 2-byte integer can hold. If you successively
reference the elements in an array with the following loop:
int i;
for (i = 0; i < 40000; i++)
printf("%dn", array[i]);
the 0th through 32,767th elements print out correctly, but the 32,768th
element prints out as array[-32768].
Integers are signed variables, so they wrap to negative numbers when they
exceed their highest positive value. Therefore, to reference elements in
large arrays, use either unsigned int or long offsets.
Another problem occurs when arrays grow to more than 65,536 bytes total on
the IBM PC. In this case, use the huge keyword in the array declaration,
as follows:
int huge bigbox[100][100][100];
Here the keyword huge is required because the total size of the array
bigbox is 100 x 100 x 100 times 2 (two bytes per int), or 2,000,000 bytes
total. This tells the compiler to set aside more space for this array than
the space reserved for ordinary variables. Whenever you use large arrays
that require the huge keyword, compile with the large memory model. That
model will be discussed in greater detail in Chapter 12. (Of course, you
will need lots of memory in your computer, too.)
Passing Pieces of Arrays
When you reference array elements with fewer dimensional offsets than were
present in that array's declaration, you are actually referencing the
address of a subarray. If, for example, you declare:
int square[3][3];
and then later reference that array without specifying the second
dimension:
Print_row(cube[1]);
you would pass the address of cube's second row (a one-dimensional
subarray) to Print_row(). Then declare Print_row() to receive a
one-dimensional array:
Print_row(row)
int row[];
{
The Bitwise Operators, Tiny Arrays
Just as arrays can get larger and larger and more and more complex, it is
also possible to go the other direction and store data in the individual
bits of a single byte. You can manipulate individual bits of a byte using
the bitwise operators. Those operators are:
Operator Description
──────────────────────────────────────────────────────────────────────────
& The bitwise AND operator
| The bitwise OR operator
^ The bitwise exclusive-OR operator
~ The unary inversion operator (ones-complement)
>> The unary right-shift operator
<< The unary left-shift operator
──────────────────────────────────────────────────────────────────────────
Each of these affects the individual bits in the bytes of a value, which
can be either a constant or a variable. Remember, a char uses 8 bits, an
int 16 bits, and a long 32 bits. First, we demonstrate the application of
the bitwise operators, and then discuss the logic of each.
The BITWISE.C program (Listing 7-12) lets you enter values interactively
and then apply the bitwise operators to them. By running this program, you
will better understand the discussion that follows. (Note that a set bit
is represented with a 1 and a clear bit is represented with a 0.)
──────────────────────────────────────────────────────────────────────────
/* bitwise.c -- demonstrates the bitwise operators */
#include <stdio.h>
main()
{
unsigned int val1, val2, result;
int ch;
extern void show();
while(1)
{
printf("\nval1: ");
if (scanf("%d", &val1) != 1)
break;
printf("val2: ");
if (scanf("%d", &val2) != 1)
break;
printf("\tval1 = ");
show(val1);
printf("\tval2 = ");
show(val2);
printf("Bitwise Operator: ");
while ((ch = getchar()) == '\n')
{
continue;
}
if (ch == EOF)
break;
switch (ch)
{
case '&':
result = val1 & val2;
printf("Executing: result = val1 & val2;\n");
break;
case '|':
result = val1 |= val2;
printf("Executing: result = val1 | val2;\n");
break;
case '^':
result = val1 ^= val2;
printf("Executing: result = val1 ^ val2;\n");
break;
case '~':
result = ~val1;
printf("Executing: result = ~val1;\n");
printf("\tresult = ");
show(result);
result = ~val2;
printf("Executing: result = ~val2;\n");
break;
case '<':
result = val1 <<= val2;
printf("Executing: result = val1 <<val2;\n");
break;
case '>':
result = val1 >>= val2;
printf("Executing: result = val1 >>val2;\n");
break;
case 'q':
case 'Q':
return(0);
default:
continue;
}
printf("\tresult = ");
show(result);
}
}
void bitout(unsigned char num[])
{
int bytes = 2, i, j;
/* IBM PC stores ints low/hi. */
for (i = bytes-1; i >= 0; --i)
{
for (j = 7; j >= 0; --j)
{
putchar((num[i]&(1<<j))?'1':'0');
}
}
}
void show(unsigned int val)
{
extern void bitout();
printf("(%05u decimal)", val);
bitout(&val);
printf(" binary\n");
}
──────────────────────────────────────────────────────────────────────────
Listing 7-12. The BITWISE.C program.
The Binary Bitwise Operators
The bitwise AND, the bitwise OR, and the bitwise exclusive-OR are binary
operators. That is, like the addition operator, they operate on two
values──not one. You can invoke them as follows:
result = val1 & val2; /* bitwise AND */
result = val1 | val2; /* bitwise OR */
result = val1 ^ val2; /* bitwise exclusive-OR */
Or you can invoke them with the op= form:
result &= val1; /* bitwise AND */
result |= val1; /* bitwise OR */
result ^= val1; /* bitwise exclusive-OR */
The Bitwise AND Operator
The bitwise AND operator, &, compares the bits in two values and produces
a value based on the comparison of the same bits in each:
var1 & var2 yields result
──────────────────────────────────────────────────────────────────────────
1 1 1
0 1 0
1 0 0
0 0 0
──────────────────────────────────────────────────────────────────────────
For the bitwise AND, the result bit is set only if the same bit in both
values is set. Otherwise, the result bit is cleared.
The bitwise AND operator is useful for turning off (clearing) a selected
bit in a variable. A typical application for the & operator is to turn off
a blinking cursor when you are accessing screen memory directly:
var1 = 3;
var1 &= 0xFFFE;─────────────────────────────────────────Turn off low bit
This results in the following calculation:
0000000000000011 ───────────────────────────────────── var1
& 1111111111111110 ─────────────────────────────────── OxFFFE
──────────────────
Yields ───── 0000000000000010
│
└─────────────────────────── Low bit cleared
The Bitwise OR Operator
The bitwise OR operator, |, compares the bits in two values and sets the
result bit for any bit that is set in either of the values:
var1 | var2 yields result
──────────────────────────────────────────────────────────────────────────
1 1 1
0 1 1
1 0 1
0 0 0
──────────────────────────────────────────────────────────────────────────
For the bitwise OR, the result bit is set if either or both corresponding
bits in both values are set. Otherwise, the result bit is cleared.
The bitwise OR operator is useful for turning on (setting) a selected bit
in a variable. A typical application for the | operator is to turn on the
high bit of a character variable before sending that character to the
printer:
var1 = 0;
var1 |= 1;───────────────────────────────────────────────Turn on low bit
This results in the following calculation:
0000000000000000 ───────────────────────────────────── var1
| 0000000000000001 ────────────────── Equivalent to 1 decimal
──────────────────
Yields ───── 0000000000000001
│
└─────────────────────────────── Low bit set
The Bitwise exclusive-OR Operator
The bitwise exclusive-OR operator, ^, compares the bits in two values and
produces a set bit only if one bit or the other is set, but not both:
var1 ^ var2 yields result
──────────────────────────────────────────────────────────────────────────
1 1 0
0 1 1
1 0 1
0 0 0
──────────────────────────────────────────────────────────────────────────
For the bitwise exclusive-OR, ^, the result bit is set if one or the other
of the corresponding bits in the values is set, but not both.
The bitwise exclusive-OR operator is useful for toggling (setting,
clearing, setting, etc.) a selected bit in a variable. A typical
application for the ^ operator is to toggle a flag in a game, thereby
determining which of two players is to make the next move:
var1 = 0;
var1 ^= 1;
var1 ^= 1;
This results in the following calculations:
0000000000000000 ───────────────────────────────────── var1
^ 0000000000000001 ────────────────── Equivalent to 1 decimal
──────────────────
Yields ───── 0000000000000001
▓ │
▓ └──────────────────── Low bit set (Player 2)
▓
▓
▼
0000000000000001 ──────────────────────── New value of var1
^ 0000000000000001 ────────────────── Equivalent to 1 decimal
──────────────────
Yields ───── 0000000000000000
│
└─────── Low bit toggled to clear (Player 1)
The Unary Bitwise Operators
The unary bitwise operators affect the bits of a single value. These
operators are:
Operator Description
──────────────────────────────────────────────────────────────────────────
~ The unary ones-complement operator
>> The unary right-shift operator
<< The unary left-shift operator
──────────────────────────────────────────────────────────────────────────
The Unary Ones-Complement Operator
The ones- complement of a variable is derived by inverting all the bits in
that value. If a bit is set, that bit changes to clear, and vice versa. In
C, the ~ causes the bits in a value to be inverted, as follows:
var1 0000000000000111
~var1 1111111111111000──────────────────────────────────────────Result
Two applications are common for the ones-complement operator. One is to
set selected bits in a variable regardless of the number of bytes occupied
by that variable. Suppose you have an int and you want all but the zeroth
bit set. One way to do this is by:
number = 0xFFFE;
This does the job, but pays the price of assuming an int always occupies
two bytes of storage. Although that is true on the IBM PC, it is not the
case on most 32-bit machines. The correct way to set all but the zeroth
bit──and the portable way──is to use the ones-complement operator:
number = ~1;
The second common application for the ones-complement operator is turning
off selected bits. One way to turn off the zeroth bit, while leaving all
the other bits in a variable unchanged, is:
masks &= 0xFFFE;
But again, the ones-complement operator should be used for portability:
masks &= ~1;
The Unary Shift Operators
The shift operators move all the bits in a variable right or left by the
number of bit positions specified. The shift operators are used as
follows:
result = value <<bits;
result = value >>bits;
Here the first line shifts the value in value left──from the low toward
the high bit──by the number of bit positions specified by bits. The second
line shifts value in the opposite direction──right, or from the high
toward the low bit. For example:
val1 0000000000111000
val1 <<3 0000000111000000───────────────────────────────────Left shift
val1 >>3 0000000000000111──────────────────────────────────Right shift
When shifting left, the bits on the right are filled with clear bits. With
QuickC, the fill bits for a right shift are always set. For portability,
however, always use unsigned variables when right-shifting.
The shift operators are useful for aligning a bit prior to ORing it into a
variable. Shifting also provides a quick way to multiply or divide by 2.
Each bit you shift to the left multiplies a number by 2; each bit you
shift to the right divides it by 2.
int val = 1;
val <<= 1;──────────────────────────────────────────────val now equals 2
val <<= 1;──────────────────────────────────────────────val now equals 4
val <<= 1;──────────────────────────────────────────────val now equals 8
Summary of Bitwise Operators
If you haven't already done so, enter, compile, and run the BITWISE.C
program (Listing 7-12 on p. 218). Watching the actions of bits as the
program applies each bitwise operator will give you a feel for bits and
will lead you to develop sophisticated applications of your own. You will
find the bitwise operators used a great deal in the hardware-specific
chapters at the end of this book.
────────────────────────────────────────────────────────────────────────────
Chapter 8 Addresses and Pointers
One of the chief strengths of C is its ability to manipulate individual
areas of memory with almost the same precision that assembly language
provides. This chapter discusses this ability in detail by showing you a
new kind of variable called a "pointer"──a variable whose contents
identify a memory address. Using pointers can greatly increase the speed
at which your programs execute, lets you access your computer's hardware
directly, and allows you to write subroutines that manipulate variables
directly (via the address).
Addresses Reviewed
The concept of memory addresses is vital to C programming. Recall, for
example, that all arguments passed to scanf() must be preceded by an
ampersand (&). In the following expression:
scanf("%d", &num);
the & tells scanf() to read an integer from the keyboard and to place that
input value into the variable num, whose address is passed with the
expression &num.
You also used addresses with arrays. In the previous chapter we mentioned
that when an entire array is passed to a function, it is passed as
addresses. For example, the code fragment
char choices[4] = {'Q', 'E', 'S', 'L'};
Get_move(choices);
passes the address of the choices array to the function Get_move(), rather
than the individual elements of that array. When you use an array name
without specifying an element in square brackets, the compiler uses the
internal starting memory address of that array as its value.
One of C's strengths is the ease with which it lets you manipulate the
values of variables by way of their addresses. This type of address
manipulation, known as indirection, is accomplished with pointers.
What Is a Pointer?
A pointer, in its simplest form, is a variable whose value (contents) is
an address, or a number corresponding to a specific location in memory.
That is, if address_var is a pointer-type variable, and num is an integer
variable, the expression
address_var = #
causes the address of the variable num to be placed into the pointer
variable named address_var. This assignment ignores the actual value of
num.
You can use pointers in your programs to:
■ Save information from functions that return addresses
■ Indirectly return more than one value from a function
■ Speed up execution by manipulating pointers rather than large blocks of
data
■ Access and modify text screen memory
■ Call functions using their addresses, thus creating more flexible code
■ Access and manipulate strings
Before you can use a pointer, also called a "pointer to," you must declare
it. Declaring a pointer is much like declaring an ordinary variable, the
only difference being that you must always precede the pointer's name with
the * character.
The following example declares two variables: an integer called num and a
pointer called address_var.
int num, *address_var;
The * before address_var tells the compiler that address_var is a pointer
whose contents will be an address. Because address_var is declared as type
int, the compiler knows that address_var will contain the address of an
integer variable. Figure 8-1 illustrates this process.
In this example, we declare two variables and two pointers (Figure 8-1a).
The variables are num (an int) and fval (a float). We also declare two
pointers, address_var and faddress_var.
The pointer address_var contains the address of an int type of variable;
pointer faddress_var contains the address of a float type of variable. The
two assignment statements in Figure 8-1b store the addresses in the
appropriate pointers. The result of the assignment is that address_var now
holds num's address (and thus points to num), and faddress_var holds
fval's address (and thus points to fval).
int num/ *address_var; ───┐
│▒▒▒▒▒▒▒▒▒▒ These declarations
float fval, *faddress-var;──┘ ▒ create...
▼
Memory── 4096 4097 4098 4099 4050 4051 4052 4053 4054 4055
locations ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
in bytes │ │ │ │ │ │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
│ │ │ │ │
│ │ └───────────┬───────────┘ │
│ │ │ │
num address-var fval faddress-vars
(A)
address_var = # ───┐
│▒▒▒▒▒▒▒▒▒▒ These assignments
faddress-var = &fval; ─┘ ▒ yield...
▼
┌────►&num──────┐ ┌──────────►&fval──────────┐
│ │ │ │
Memory── 4096 4097 4098 │4099 4050 4051 4052 4053 4054 │4055
locations ┌─────┬─────┬─────▼─────┬─────┬─────┬─────┬─────┬─────▼─────┐
in bytes │ │ 4096 │ │ │ 4050 │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
│ │ │ │ │
│ │ └───────────┬───────────┘ │
│ │ │ │
num address-var fval faddress-var
▒ ▒
▒ ▒ ▒ ▒
▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
Points to Points to
(B)
Figure 8-1. Result of assigning an address to a pointer.
Accessing Variables with Pointers
The * operator (pronounced "star"), when used to signify a pointer, is
called the indirection operator because it lets you access variables
indirectly. When you use the * operator in front of a pointer (other than
in its declaration) you tell the compiler to fetch or store the value that
the pointer points to. For example, in the fragment
int num, *address_var;
address_var = #
*address_var = 3;
you first declare an int variable (num) and a pointer to an int
(address_var). Next, the value you store in address_var is the address of
the variable num. Finally, the * in front of address_var tells the
compiler to store the value 3 in the variable whose address is stored in
address_var. Because address_var contains the address of num, that value
is stored in num. (See Figure 8-2 on p. 234.)
The POINTER.C program (Listing 8-1) illustrates the procedure for
declaring, assigning a value to, and using pointers.
──────────────────────────────────────────────────────────────────────────
/* pointer.c -- demonstrates pointer declaration, */
/* assignment, and use */
#define WAIT printf("(press any key)"); getch(); \
printf("\n\n")
main()
{
int num, *address_var;
num = 0;
address_var = #
printf("The address of the variable ");
printf("\"num\" is: 0x%04X\n", &num);
printf("The value in the pointer ");
printf("\"address_var\" is: 0x%04X\n", address_var);
printf("The value in the variable ");
printf("\"num\" is: %d\n", num);
WAIT;
printf("Since \"address_var\" points to \"num\"\n");
printf("the value in ");
printf("\"*address_var\" is: %d\n", *address_var);
WAIT;
printf("To verify this, let's store 3 in\n");
printf("\"*address_var\", then print out ");
printf("\"num\" and \"*address_var\"\n");
printf("again.\n");
WAIT;
printf("Doing: *address_var = 3;\n\n");
*address_var = 3;
printf("The address of the variable ");
printf("\"num\" is: 0x%04X\n", &num);
printf("The value in the pointer ");
printf("\"address_var\" is: 0x%04X\n", address_var);
printf("The value in the variable ");
printf("\"num\" is: %d\n", num);
WAIT;
printf("Since \"address_var\" points to \"num\"\n");
printf("the value in ");
printf("\"*address_var\" is: %d\n", *address_var);
WAIT;
printf("Now we will add 15 to \"num\" and print\n");
printf("\"num\" and \"*address_var\" again.\n");
WAIT;
printf("Doing: num += 15;\n\n");
num += 15;
printf("The address of the variable ");
printf("\"num\" is: 0x%04X\n", &num);
printf("The value in the pointer ");
printf("\"address_var\" is: 0x%04X\n", address_var);
printf("The value in the variable ");
printf("\"num\" is: %d\n", num);
WAIT;
printf("Since \"address_var\" points to \"num\"\n");
printf("the value in ");
printf("\"*address_var\" is: %d\n", *address_var);
WAIT;
printf("Doing: return (*address_var);\n\n");
return (*address_var);
}
──────────────────────────────────────────────────────────────────────────
Listing 8-1. The POINTER.C program.
The output of this program follows. Compare it to the listing.
The address of the variable "num" is: 0x1388
The value in the pointer "address_var" is: 0x1388
The value in the variable "num" is: 0
(press any key)
Since "address_var" points to "num"
the value in "*address_var" is: 0
(press any key)
To verify this, let's store 3 in
"*address_var", then print out "num" and "*address_var"
again.
(press any key)
Doing: *address_var = 3;
The address of the variable "num" is: 0x1388
The value in the pointer "address_var" is: 0x1388
The value in the variable "num" is: 3
(press any key)
Since "address_var" points to "num"
the value in "*address_var" is: 3
(press any key)
Now we will add 15 to "num" and print
"num" and "*address_var" again.
(press any key)
Doing: num += 15;
The address of the variable "num" is: 0x1388
The value in the pointer "address_var" is: 0x1388
The value in the variable "num" is: 18
(press any key)
Since "address_var" points to "num"
the value in "*address_var" is: 18
(press any key)
Doing: return (*address_var);
In the POINTER.C program, the pointer address_var contains the address of
num (as a result of the assignment address_var = &num) and therefore
yields the value stored in num. That is, we indirectly access num via its
address (*address_var = 3). Because address_var contains num's address,
you can use *address_var anywhere you would use num. For example, we could
have ended the program with return (num) to produce the same result.
Passing Pointers to Functions
Until now, with the exception of arrays, we have passed arguments to
functions by value. Thus, you might think we could write a function that
squares the argument passed to it as follows:
square(int num)
{
num *= num;
}
This doesn't work, however, because the variable num is a local variable
to the function square(), and the result is not accessible by other
functions. Thus, calling square() with
main()
{
int val = 5;
square(val);
}
does not result in main()'s variable val being squared, because main()
doesn't "see" the variable num.
You can get around this by having square() return a value, as follows:
main()
{
int val = 5;
val = square(val);────────────────────────Value returned by square()
}
square(int num)
{
num *= num;
return (num);─────────────────────────────────square() returns value
}
Another approach is to use pointers. When you pass a pointer to a
function, you still pass a copy of its value, but the value you pass is an
address. Therefore, in square(), you must declare num as a pointer because
it will receive an address:
square(int *address_var) ─────────────────── Pointer receives an address
{
*address_var *= *address_var;
} └────────────────────────────── Multiplication operator
This form of square() receives an address as its argument. The pointer to
hold that address, address_var, is declared as int *address_var because it
receives the address of an int variable.
To use this new square() function, we must pass it an address. We can do
this in either of two ways. We can use the & operator, as follows:
main()
{
int val = 5;
square(&val);────────────────────────────────────────Pass an address
}
Or we can pass a pointer:
main()
{
int val = 5, *here;
here = &val;
square(here);─────────────────────The value of here is val's address
}
After making our declarations, we place the address of val into the
pointer here. When we pass here to square(), its value──the address of
val──is what is actually passed. This results in val being squared.
The SQUARE.C program (Listing 8-2) summarizes this passing of pointers
and addresses in an interactive quiz. In it, we've expanded on our
original square() subroutine. In the new Square(), we return two values
from a single function! The first, returned by return, is an error
status──zero for a successful square and -1 for any attempt to square a
number larger than 181 or less than -181 (the square root of 32,767, the
largest signed int on the IBM PC). We return the second value with the
pointer *where.
──────────────────────────────────────────────────────────────────────────
/* square.c -- a quiz to demonstrate passing */
/* pointers and addresses in functions */
main()
{
int val, count, guess;
for (count = 1; count < 255; ++count)
{
val = count;
printf("What is the square of %d?\n", val);
if (scanf("%d", &guess) != 1)
return(0); /* non-number exits */
if(Square(&val) != 0) /* pass val's address */
{
printf("Range Error\n");
exit(1);
}
if (val != guess)
printf("Wrong. It is %d.\n", val);
else
printf("Right!\n");
printf("Continue? ");
if (getche() != 'y')
break;
}
}
int Square(int *where)
{
if (*where > 181 || *where < -181)
return (-1);
*where = (*where) * (*where);
return (0);
}
──────────────────────────────────────────────────────────────────────────
Listing 8-2. The SQUARE.C program.
In this program, we use a separate variable, count, in the for loop
because the value of val is indirectly changed by the call to Square(). If
we had used val as follows:
for (val = 1; val < 255; ++val)
you would be prompted only for the numbers 1, 2, 5, and 26, and then you
would receive a Range Error.
Pointers and Arrays
Pointers let you manipulate strings and arrays more succinctly and
efficiently. We'll learn about strings in the next chapter. Here we will
discuss the relationship between arrays and pointers, detailing potential
pitfalls along the way.
Recall from the previous chapter that referencing an array by name,
without an offset, yields that array's address. What we didn't tell you
was that the address of an array is the same as the address of the array's
first element. For example, in the following array declaration:
int coins[4] = {25, 10, 5, 1};
the reference
Find_change(coins, amount);
actually causes the address of the array coins[4] to be passed to the
function Find_change(). Because the address of an array is the location in
memory of its beginning, we can also reference that array with the
expression
&coins[0]
Here the address operator & yields the address of the first item in the
array coins and, therefore, the address of the array itself.
You can assign the address of another variable to a pointer with the &
operator (address_var = &num[1]). Because each item in an array is a
variable, the assignment
address_var = &coins[0];
results in address_var containing the address of the first integer in the
array coins.
Because &coins[0] and coins are equivalent, the following expression is
the same as the one above:
address_var = coins;
Now here comes the exciting part. When a value, say 1, is added to a
pointer, it increments the address in that pointer by the number of bytes
in the type to which it points. For example, in Figure 8-2 the variable
address_var begins with a value that is the address of coins. Notice what
happens when we add 1 to address_var. Because address_var is a pointer to
the type int, and because an int occupies two bytes (on the IBM PC), the
value in address_var is increased by 2. The new value in address_var is
thus the address of coins[1] (the next element in the array).
Points to
▒▒▒▒▒▒▒▒▒▒
▒ ▒
┌─────────────┐ ▒
│ │ ▼
Memory locations── 4392 4393 4394 4395 4396 4397 4398 4399
in bytes ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ 4394 │ │ │ │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
│ │ │ │
│ │ │ │
│ │ │ │
address-var coins[0] coins[1] coins[2]
•
• ▒ ▒
• ▒ ▒
4392 4393 ▒ ▒
┌─────┬─────┐ ▒ ▒
│ 4394 │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒
└─────┴─────┘ Next points to ▒
│ ▒
│ ▒
│ ▒
Increment value───address_var += 1; ▒
of pointer • ▒
• ▒
• ▒
4392 4393 ▒
┌─────┬─────┐ ▒
│ 4398 │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
└─────┴─────┘ Then points to
│
│
│
Increment pointer───address_var += 1;
again
Figure 8-2. The value of a pointer increases by multiples of the number of
bytes in the data type to which it points.
The CHANGE.C program (Listing 8-3) demonstrates how the pointer coin_ptr
advances through the array coins, each step determined by the number of
bytes for the type int. Compile the program with the Debug option set
because we want to trace its execution.
──────────────────────────────────────────────────────────────────────────
/* change.c -- a change-making program demonstrates */
/* how pointers advance the correct */
/* number of bytes based on type */
#define NCOINS (4)
#define CENT (0x9b) /* IBM PC cent character */
#define WAIT printf("(Press any key to continue)"); \
getch(); printf("\n\n")
main()
{
static int coins[NCOINS] = {25, 10, 5, 1};
int *coin_ptr, i = 0;
int pennies1, pennies2, count;
float amount;
printf("Enter an amount and I will ");
printf("give you change.\nAmount: ");
if (scanf("%f", &amount) != 1)
{
printf("I don't know how to change that!\n");
exit(1);
}
pennies2 = pennies1 = (int)(amount * 100.0);
coin_ptr = coins;
for (i = 0; i < NCOINS; ++i)
{
WAIT;
count = 0;
while ((pennies1 -= coins[i]) >= -1)
++count;
if (count > 0)
{
printf("%4d %2d%c", count, coins[i], CENT);
printf(" coins by array offset.\n");
}
if (pennies1 == 0)
break;
pennies1 += coins[i];
count = 0;
while ((pennies2 -= *coin_ptr) >= 0)
++count;
if (count > -1)
{
printf("%4d %2d%c", count, *coin_ptr, CENT);
printf(" coins by pointer indirection.\n");
}
if (pennies2 == 0)
break;
pennies2 += *coin_ptr;
++coin_ptr;
}
}
──────────────────────────────────────────────────────────────────────────
Listing 8-3. The CHANGE.C program.
After you compile CHANGE.C, turn off screen swapping and specify the
following four watch variables:
coin_ptr
*coin_ptr
i
coins[i]
(See pp. 119-20 if you forgot how to specify watch variables. You don't
need to specify the types with a comma; the defaults are correct.) Now
step through the program with the F8 function key. Observe that as ++i
followed by coins[i] steps through the array, so does ++coin_ptr followed
by *coin_ptr. Figure 8-3 shows the screen as the program is being traced.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 8-3 can be found on p.236 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 8-3. Incrementing a pointer moves it through an array in steps
that correspond to the number of bytes in the data type.
This equivalence between arrays and incrementing pointers is one of C's
chief strengths. It can also be confusing and can lead to some unexpected
bugs. In the CHANGE.C program, we perform bounds checking with the for
loop. If we rewrite that loop without i, we need to do one of two things.
One option is to put some stop value into our array, such as the last
element (0) in the following:
int coins[5] = {25, 10, 5, 1, 0};
This approach is often used with string variables.
The other option requires that we add some means of detecting when the
address in coin_ptr becomes too large, as follows:
for (coin_ptr = coins; coin_ptr < &coins[4]; ++coin_ptr)
This approach is more common for situations where a stop value is not
practical. In a database, for example, you might have 32 bytes available
and want to use all 32 for a mailing address, with none reserved for a
terminating value.
Pointer Arithmetic
QuickC permits fewer arithmetic operations on pointers than on other kinds
of variables. Because pointers contain addresses as their values, whenever
you change one, you reference a new location inside your computer's
memory. Obviously, you don't want to reference random locations──not only
would they be meaningless, but they might overwrite crucial memory
locations and crash your PC.
To help avoid such meaningless addresses, C permits only a handful of
mathematical operations to be performed on pointers. They are:
Addition You can add values to addresses (like incrementing with ++).
This is most useful with arrays.
Subtraction You can subtract values from addresses (decrementing with --
and subtracting with -).
Comparison You can compare one address to another to see if it is greater
than, less than, equal to, or not equal to the other.
──────────────────────────────────────────────────────────────────────────
Quick Tip
C does not provide many safeguards against referencing incorrect
addresses. QuickC, however, lets you compile with Pointer Check turned on.
Although this provides a measure of safety (by verifying that pointer
values address program data), it results in slower-executing programs.
──────────────────────────────────────────────────────────────────────────
The operations allowed on pointers are a small set when compared to the
operations allowed on numeric variables and array items. Let's examine why
you cannot use the other arithmetic operations:
Multiplication Doubling an address or even multiplying it by, let's say,
523 would yield a new address value that, at best, would be somewhere in
your data and at worst would be beyond the end of your program, possibly
in the code of another memory resident program (like QuickC or
COMMAND.COM).
Division Halving an address or even dividing it by, let's say, 10 would
yield a new address value that, at best, would be somewhere inside your
own code and at worst would be inside the MS-DOS interrupt vectors.
Bitwise operators You cannot manipulate the bits in an address. This
would result in a totally random address.
Unary negation You can't reverse the sign of an address because addresses
are always unsigned.
Now let's look at the CHANGE2.C program (Listing 8-4). This rewrite of
CHANGE.C illustrates the incrementing of pointers and the comparison of
two pointers.
──────────────────────────────────────────────────────────────────────────
/* change2.c -- modified to demonstrate passing */
/* an address to a function */
#define NCOINS (4)
#define CENT (0x9B) /* IBM PC cent character */
main()
{
static int coins[NCOINS] = {25, 10, 5, 1};
int pennies;
float amount;
printf("Enter an amount and I will ");
printf(" give you change.\nAmount: ");
if (scanf("%f", &amount) != 1)
{
printf("I don't know how to change that!\n");
exit(1);
}
pennies = (int)(amount * 100.0);
Show_change(coins, &coins[NCOINS], pennies);
}
Show_change(int amts[], int *end, int due)
{
int count;
while (amts < end) /* compare pointers */
{
count = 0;
while ((due -= *amts) >= -1)
{
++count;
}
if (count > 0)
printf("%4d %2d%c\n", count, *amts, CENT);
if (due == 0)
break;
due += *amts;
++amts; /* increment a pointer */
}
}
──────────────────────────────────────────────────────────────────────────
Listing 8-4. The CHANGE2.C program.
The function Show_change() receives addresses of the array coins and the
fourth element in that array (one past its end). This introduces some new
concepts: the interchangeability of the declaration coins[] with the
declaration *coins and the importance of left versus right values.
The Interchangeability of *amts and amts[]
In the following declaration:
Show_change(int amts[]);
{
the expression int amts[] tells the compiler to pass this array to the
function Show_change(). However, you can also use an array declaration of
the form *amts interchangeably with amts[]. The two are equivalent. In
fact, if you declare an array as amts[], you can use that array's name as
though it were a pointer:
Show_change(int amts[])
{ └──────────────────────────── Declared as an array
...
due = *amts;
└──┴──────────────────────────────── but used as a pointer
and vice versa:
Show_change(int *amts[])
{ └────────────────────────── Declared as a pointer
...
due = amts[i];
└──┴─────────────────────────────── but used as an array
Note, however, that this interchangeability works only when the array is
declared as one of a function's received arguments. An attempt to use that
singular equivalence elsewhere results in either a Syntax or an lvalue
error.
lvalue vs rvalue
An lvalue is any variable whose value can change (have a new value
assigned to it). An rvalue is a variable whose value cannot change. The
easiest way to differentiate between the two is to remember that rvalues
go to the right of the assignment operator and lvalues go to the left. Why
is this important?
Arrays are usually rvalues because of the way C generates its intermediate
code. C treats an array as a label (like the target of a goto is a label).
As an address of a location, an array is a constant value much like the
number 3 is a constant.
Confusing lvalues and rvalues with array names is a common source of
errors for the beginning C programmer. Always remember that array names
cannot be assigned to, incremented, or decremented, except when they are
declared as one of the received arguments of a function, as follows:
char *Amount;───────────────────────────────────Global pointer or lvalue
int Bills[4] = {20,10,5,1};────────────────────────────Array or rvalue
some_function(char amts[];)──────────────────────Equivalent to a pointer
{
char *address_var,───────────────────────────Local pointer or lvalue
old_coins[];──────────────────────────────────────Syntax error
++Amounts;─────────────────────────────────────────────────────Legal
++Bills;─────────────────────────────────Illegal operation on rvalue
++amts;────────────────────────────────────────────────────────Legal
++address_var;─────────────────────────────────────────────────Legal
++old_coins;───────────────────────────────────────────────────Legal
}
In this sample program, *Amount and *address_var are pointers, values that
can be incremented. Although amts[] is declared as an array, the
interchangeability we discussed earlier permits us to increment it as
though it were a pointer. On the other hand, because Bills is not a
function's argument (it is a global array), it is an rvalue that cannot be
incremented. Finally, old_coins[] generates a syntax error because only
arrays in function argument declarations can be used without specifying
the size of their leftmost dimension.
Type Casting Pointers and Addresses
Occasionally you will need to perform an arithmetic operation on a pointer
other than addition, subtraction, or comparison. Fortunately, C is very
flexible, and it permits you to perform those other operations on pointers
by using type casts (also called "casts"). In Chapter 3 you used a type
cast to convert one type to another: You can also use that technique with
pointers. For example, suppose you need to divide a pointer's value by 2.
You could use the method:
unsigned long temp;
int *point = some_address;
temp = (unsigned long)point;
temp /= 2;
point = (int *)temp;
First, we assign the address some_address to the pointer int *point. Next,
we type cast the value in point (the address of some_address) to force a
change to unsigned long, and then we store the resulting value in temp.
Because it is legal to divide an unsigned long, we divide temp by 2. Then
we cast that result, still an unsigned long, to the type int * (meaning
pointer to an int). Finally, we place the correctly typed new value in
point.
The PEEK.C program (Listing 8-5) illustrates this use of type casting.
PEEK.C asks the user for a number, then treats that number as an address
and shows you the value stored at that address.
──────────────────────────────────────────────────────────────────────────
/* peek.c -- demonstrates how to cast an int to a */
/* pointer */
main()
{
char *mem_ptr;
unsigned int address;
while (1)
{
printf("Examine what memory location?\n");
printf("Enter location in decimal: ");
if (scanf("%u", &address) != 1)
break;
mem_ptr = (char *)address; /* cast */
printf("The value in %u is 0x%02X\n",
address, (unsigned char)*mem_ptr);
}
}
──────────────────────────────────────────────────────────────────────────
Listing 8-5. The PEEK.C program.
far Pointers
So far, we've assumed that all pointers occupy two bytes of memory. Two
bytes can represent only addresses in the range 0 through 65535, however──
not nearly enough to reference every location in the latest PCs.
Fortunately, QuickC provides a 4-byte pointer, called a far pointer, that
can address more than four billion bytes of memory.
In particular, far pointers are useful for directly accessing the text
screen's memory and for producing sophisticated graphics programs. In this
section we'll show you how to manipulate the text screen of a graphics
adapter.
To declare a far pointer, merely add the keyword far to the pointer
declaration, as follows:
int far *screenp;
We must use a far pointer to access screen memory because that memory is
located at 0xB000000 (for machines with CGA) or 0xB800000 (for machines
with EGA or VGA), locations that clearly will not fit into a 2-byte
pointer (two bytes can hold only four hex digits, not eight). To place
this hexadecimal constant into a far pointer, use the following type cast:
screenp = (int far *)0xB000000;──────────────────────────────────────CGA
screenp = (int far *)0xB800000;───────────────────────────────EGA or VGA
This tells the compiler to handle the constant 0xB000000 (or 0xB800000) as
a far address and to assign that address to the far pointer variable
screenp.
The SCRINV.C program (Listing 8-6) demonstrates a simple technique for
manipulating text screen memory. Every time you press a key, the screen
flips over. (Type Q to quit.) In the listing, adjust the constant assigned
to screenp to suit your hardware: For EGA or VGA, replace 0xB000000 with
0xB800000.
This program uses a pointer as if it were an array. Although we declare
screenp as a far pointer:
int far *screenp;
we reference its elements using an offset in square brackets, as follows:
temp = screenp[i];
──────────────────────────────────────────────────────────────────────────
Quick Tip
Be careful when casting pointers to integers. You should always type cast
to an unsigned long because that type will be large enough to hold all
addresses. Specifying unsigned will prevent addresses from being (wrongly)
considered negative, which could lead to incorrect results.
──────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────
/* scrinv.c -- using a far pointer to access text */
/* screen memory */
#define ROWS 25
#define COLS 80
main()
{
int far *screenp;
int temp, i;
do
{
/* use 0xB800000 for EGA or VGA */
screenp = (int far *)0xB000000;
for (i = 0; i < ((ROWS*COLS)/2); ++i)
{
temp = screenp[i];
screenp[i] = screenp[(ROWS*COLS)-i-1];
screenp[(ROWS*COLS)-i-1] = temp;
}
} while (getch() != 'Q');
}
──────────────────────────────────────────────────────────────────────────
Listing 8-6. The SCRINV.C program.
Functions That Return Addresses
In Chapter 6, we demonstrated that functions can return values and that
those values are of type int unless you declare otherwise. You can also
declare functions that return addresses. The C library contains many
functions of this type, and your functions can also take advantage of the
speed and compactness this procedure offers.
You declare a function that returns an address the way you declare a
pointer variable──with a type, a *, and a name. For example, the following
function returns the address of a char type:
char *function(int arg)
{
──────────────────────────────────────────────────────────────────────────
Quick Tip
We declared screenp as a pointer to an int because each character on your
PC text screen is represented by two bytes of information──one byte is the
character and the other is that character's attribute (normal, blinking,
inverse, etc.). (We will discuss this organization and the various
attributes in Chapter 14.)
──────────────────────────────────────────────────────────────────────────
This is like using a function as a pointer. Let's try another example.
Examine the function Range() in the following fragment from an upcoming
program.
char *Range(int key)
{
static char k2[] = {'a', 'b', 'c'},
k3[] = {'d', 'e', 'f'},
k4[] = {'g', 'h', 'i'};
char *kp;
if (key == 2)
{
return (k2);───────────────────────────────────────Address of k2
}
else if (key == 3)
{
return (&k3[0]);───────────────────────────────────Address of k3
}
kp = k4;
return (kp);───────────────────Return value of a pointer, an address
}
This example demonstrates that you can return an address in three ways: as
an array name (k2), as the address of the first element in an array
(&k3[0]), or as a pointer (kp).
Now let's call the above function from another named main():
main()
{
char *keys;
extern char *Range();────────Range will return the address of a char
Notice that you can only use the return value of Range() after you
correctly declare it, both in its own declaration and in (or before) any
functions that call it.
You can use a pointer value returned by a function the same way you use a
pointer──with one exception. The address returned by a function is an
rvalue: Thus, you can neither place it to the left of the assignment
operator nor change it by computation. The following examples illustrate
three correct ways to use the value returned by Range():
keys = Range(2);────────────────────Address assigned to keys (a pointer)
printf("%cn", Range(2)[1]);─────────────────────Address used as an array
printf("%cn", *(Range(2)+1));──────────────────Address used as a pointer
The first example assigns the address value returned by Range() to a
pointer variable (keys). The second example uses the address returned by
Range() as if it were an array, printing the second element. The third
example uses the address returned by Range() as if it were a pointer,
printing the value stored in that address plus one.
The PHWORD.C program (Listing 8-7) asks the user for a telephone number
and then, using the letters of the telephone dial, prints out all the
possible words that can be made from that number.
──────────────────────────────────────────────────────────────────────────
/* phword.c -- generates all the possible words */
/* in a phone number; demonstrates */
/* functions that return addresses */
#define MAXD (7) /* 7 digits max */
main()
{
int digits[MAXD], ndigits = 0, line = 0;
char *letters;
signed char digit;
int a, b, c, d, e, f, g;
extern char *Range();
printf("Enter Phone Number (7 digits): ");
do
{
digit = getch() - '0';
if (digit == ('-' - '0'))
continue;
if (digit < 0 || digit > 9)
{
printf("\nAborted: Nondigit\n");
return(1);
}
digits[ndigits++] = digit;
printf("%d", digit);
} while (ndigits < 7);
printf("\n");
for( a = 0; a < 3; ++a)
for( b = 0; b < 3; ++b)
for( c = 0; c < 3; ++c)
for( d = 0; d < 3; ++d)
for( e = 0; e < 3; ++e)
for( f = 0; f < 3; ++f)
for( g = 0; g < 3; ++g)
{
printf("%c", Range(digits[0])[a]);
printf("%c", Range(digits[1])[b]);
printf("%c", Range(digits[2])[c]);
printf("%c", Range(digits[3])[d]);
printf("%c", Range(digits[4])[e]);
printf("%c", Range(digits[5])[f]);
printf("%c", Range(digits[6])[g]);
printf("\n");
if (++line == 20)
{
printf("Press any key for more");
printf(" (or q to quit): ");
if (getch() == 'q')
return (0);
printf("\n");
line = 0;
}
}
}
char *Range(int key)
{
static char keys[10][3] = {
{'0', '0', '0'},
{'1', '1', '1'},
{'a', 'b', 'c'},
{'d', 'e', 'f'},
{'g', 'h', 'i'},
{'j', 'k', 'l'},
{'m', 'n', 'o'},
{'p', 'r', 's'},
{'t', 'u', 'v'},
{'w', 'x', 'y'}
};
return (keys[key]);
}
──────────────────────────────────────────────────────────────────────────
Listing 8-7. The PHWORD.C program.
The PHWORD.C program also illustrates another point about arrays. When you
reference a multidimensional array with only a partial list of offsets,
the value generated is the address of the portion you referenced. Thus,
although keys in Range() is a two-dimensional array, referencing with only
a single dimension, as follows:
return (keys[key]);
yields the address of only the row specified. In other words, it yields
the address of a one-dimensional array that is a subset of the
two-dimensional array.
──────────────────────────────────────────────────────────────────────────
Quick Tip
The extern keyword tells QuickC that the variable or function named will
be found elsewhere, either later in the current file or in another file
that you plan to compile separately. It can also be used to tell QuickC
that you plan to use a variable found in a library routine.
──────────────────────────────────────────────────────────────────────────
Notice in PHWORD.C that main() screens your telephone number for illegal
characters. The function Range() would be more portable if we checked for
illegal values inside of it and returned an error code. The trick is to
return an error address that is always illegal. Defined in the standard
header file stdio.h is the perfect value to convey address errors──NULL.
This special zero address value is guaranteed to be illegal. By using NULL
rather than 0, you ensure the portability of your programs.
The following is a rewrite of Range() that uses NULL:
#include <stdio.h> /* for NULL */
char *Range(int key)
{
static char keys[10][3] = ...
if (key < 0 || key > 9)
{
return (NULL);
}
Now Range() does its own error checking. It can return NULL, even though
it is declared char *, because NULL is a special address value that is
illegal regardless of the expected return type.
Dynamic Arrays
In the previous chapter, we explained that arrays in C must be
dimensioned with integer constant expressions, and therefore you cannot
change the size of a declared array. But now that you have pointers at
your disposal, the situation is somewhat different. By using standard C
Library routines, you can allocate memory while the program is running
(that is, "dynamically") and thus create arrays "on the fly." You can also
use other C library routines to change the size of dynamically allocated
arrays, again while the program is running.
The ability to create, change the size of, and discard arrays from within
your running program opens a host of new programming possibilities. It
frees your program from having to know ahead of time how many lines of
text a user will type, for example, or how many characters it will receive
via a modem. When you design a database, it is clearly better to allow
users to add fields at will, rather than restricting them to a
predetermined record structure. Games are generally more interesting when
players can add characters at any time. Text editors are more powerful
when the user can interactively define keyboard macros.
The standard library routines for the dynamic allocation and reallocation
of memory are listed in Table 8-1 on the following page. The return types
for these functions are declared in the header file <malloc.h>. If you
look at those declarations (using the Include option on the View menu),
you will see that they are all declared as pointer type void *. This new
type, when applied to a function's return value, permits the returned
address to be legally assigned to any type of pointer. This makes it very
easy for us to create dynamic arrays of any type.
Table 8-1 Memory Allocation Library Routines
──────────────────────────────────────────────────────────────────────────
malloc() Memory allocate
calloc() Computed memory allocate
realloc() Reallocate memory
free() Free allocated memory
sbrk() Request memory from system
──────────────────────────────────────────────────────────────────────────
The malloc() Memory Allocation Function
The malloc() function is the most frequently used library allocation
function. It takes a single argument, the number of bytes of memory you
wish to allocate (reserve), and returns the address of that memory. If
malloc() cannot find as much free memory as you specify, it returns a NULL
value. The correct form for using malloc() (including a check for failure)
follows:
#include <stdio.h>──────────────────────────────────────────────For NULL
#include <malloc.h>─────────────────────────────For malloc() declaration
...
int *iptr;────────────────────────────────────────────To receive address
size_t bytes = 100;──────────────────────────────────────Number of bytes
if ((iptr = malloc(bytes)) == NULL)
{
/* handle error here */
}
printf("Now let's fill the array iptr[]\n");
The parentheses in the malloc() expression force the result of the
assignment──the value of iptr──to be compared to NULL. If malloc succeeds
in allocating memory, iptr contains the address of that dynamically
allocated memory.
Because the value of iptr evaluates as an address, you can use iptr as if
it were an array. For example, the following expression is perfectly
legal:
iptr[5]
──────────────────────────────────────────────────────────────────────────
Quick Tip
Note in the above example that we declare bytes as type size_t. This type
is defined in <malloc.h> as an unsigned int for QuickC. Because the type
size_t is a part of the ANSI standard, you should use it rather than
unsigned int to ensure the portability of your programs.
However, to transport programs written with size_t to different machines,
you might need to use #define to make size_t an unsigned int.
──────────────────────────────────────────────────────────────────────────
The TOTAL.C program (Listing 8-8) asks you to type numbers, one per line,
and dynamically builds an array of those numbers. When you enter a
non-numeric character, the program displays your list of numbers from the
array and totals them.
This program introduces two new elements to our memory allocation
routines: free() and realloc(). The free() function releases memory that
you reserve with malloc() or realloc(). The realloc() function copies
memory into a larger or smaller block of memory.
──────────────────────────────────────────────────────────────────────────
/* total.c -- how to build an array on the fly */
#include <stdio.h> /* for NULL */
#include <malloc.h> /* for size_t */
main()
{
int *iptr, count = 0, i, total;
size_t bytes = sizeof(int);
/* Start the array with room for one value. */
if ((iptr = malloc(bytes)) == NULL)
{
printf("Oops, malloc failed\n");
exit(1);
}
printf("Enter as many integer values as you want.\n");
printf("I will build an array on the fly with them.\n");
printf("(Any non-number means you are done.)\n");
while (scanf("%d", &iptr[count]) == 1)
{
++count;
/* Enlarge the array. */
if ((iptr = realloc(iptr,bytes*(count+1))) == NULL)
{
printf("Oops, realloc failed\n");
exit(1);
}
}
total = 0;
printf("You entered:\n");
for (i = 0; i < count; i++)
{
printf("iptr[%d] = %d\n", i, iptr[i]);
total += iptr[i];
}
printf("\nTotal: %d\n", total);
}
──────────────────────────────────────────────────────────────────────────
Listing 8-8. The TOTAL.C program.
The free() function takes a single argument, the address returned by
malloc() or realloc(), and uses it to release that memory. Note that if
you pass free() an address other than one returned by one of these
functions, your program may crash.
The realloc() function takes two arguments: first, the address returned by
malloc() or one returned from a previous call to realloc(); and second, a
new size in bytes. The function copies the contents of the old memory to
the new memory (truncating if the new size is smaller) and returns the
address of the new memory. Like malloc(), realloc() returns a NULL address
if it fails.
The calloc() Memory Allocation Routine
QuickC supplies a companion routine to malloc() called calloc() (for
"calculated allocate"). The calloc() function also allocates memory, but
with a twist that makes it ideal for arrays. Instead of merely allocating
a number of bytes, it takes a pair of arguments: the number of items and
the number of bytes (sizeof) of each item. The form for using calloc() is
as follows:
address = calloc(items, sizeof(item));
Like malloc(), calloc() returns the address of successfully allocated
memory or NULL if not enough memory is available.
The advantage offered by calloc() is that it initializes allocated memory
to zero values, whereas malloc() can leave memory that is filled with
garbage. The free() function also releases memory reserved by calloc().
The sbrk() Memory Extension Function
The malloc() family of routines keeps track of all the memory you
allocate. These routines use extra bytes to keep a list of allocated
memory and still more bytes to ensure that all addresses are even. If your
program is short on space, those pieces of memory might be too valuable to
waste on mere housekeeping. These functions also need to search through
lists of available memory to find blocks of the requested size. If you
have allocated many chunks of memory, that search slows the execution of
your program.
QuickC's sbrk() function offers a quick and efficient way to allocate
memory when you don't need to keep track of how much has been allocated.
When you load a program into memory, it is arranged as shown in Figure
8-4. The address of the end of the data segment serves to record the
highest memory location you can legally access. The sbrk() function
requests that the highest location be extended by a specified number of
bytes, as follows:
address = sbrk(bytes);
The value returned by sbrk() is the address of the old limit before it was
extended (in other words, the address of the first byte of the newly
acquired memory). The address of the first byte beyond the new memory
allocation then replaces the previously stored value.
┌- - - - - - - - - - -─┐◄───New highest after sbrk()
| |
| |
├──────────────────────┤◄───Offset from DS
├─────────────────────┤
│ └─────────┼────Highest memory location
│ │ your program can use
│ │
│ DATA │────Uninitialized data
│ │
│ │
├──────────────────────┤
│ │
│ DATA │────Initialized data
│ │
├──────────────────────┤
│ │
│ │ │
│ │ CODE │────Your program's code
│ │ │
│ │ │
Increasing │ │
memory └──────────────────────┘◄───DS
Figure 8-4. The sbrk() function lets you extend the limit of accessible
memory.
Because sbrk() returns an address of -1 on failure, the full call to
sbrk(), including an error check, is as follows:
#include <malloc.h> /* for size_t */
...
int *iptr;
size_t bytes = 100;
...
if ((iptr = sbrk(bytes)) == (int *)-1)
{
/*handle error here */
}
/* you have 100 bytes at the address in iptr */
Note that we must typecast the -1 to (int *) so that the comparison will
be to the same type as iptr.
The TOTAL2.C program (Listing 8-9 on the following page) uses sbrk() to
transform the earlier TOTAL.C program into an adding machine of unlimited
capacity. We can use sbrk() in TOTAL2.C because we only take memory and
never release or exchange any. Because sbrk() extends memory continuously,
our array always remains intact. With malloc(), on the other hand, memory
may not be allocated continuously, so you must call realloc() to enlarge
and possibly move your array.
Unfortunately, giving back pieces of memory that you acquired with sbrk()
requires advanced programming expertise. If you need to juggle memory
(taking, then giving back part, and so on), malloc() and realloc() are
much easier to use. Do not, however, mix sbrk() and the malloc() routines
in the same program.
──────────────────────────────────────────────────────────────────────────
/* total2.c -- how to build an array on the fly */
/* using sbrk() */
#include <stdio.h> /* for NULL */
#include <malloc.h> /* for size_t */
main()
{
int *iptr, count = 0, i, total;
size_t bytes = sizeof(int);
/* Start the array with room for one value. */
iptr = sbrk(0);
if (sbrk(bytes) == (int *)-1)
{
printf("Oops, sbrk failed\n");
exit(1);
}
printf("Enter as many integer values as you want.\n");
printf("I will build an array on the fly with them.\n");
printf("(Any non-number means you are done.)\n");
while (scanf("%d", &iptr[count]) == 1)
{
++count;
/* Enlarge the array. */
if (sbrk(bytes) == (int *)-1)
{
printf("Oops, sbrk failed\n");
exit(1);
}
}
total = 0;
for (i = 0; i < count; i++)
total += iptr[i];
/* just print the total this time */
printf("%d\n", total);
}
──────────────────────────────────────────────────────────────────────────
Listing 8-9. The TOTAL2.C program.
Advanced Pointer Techniques
Perhaps you've heard horror stories about C pointers and incomprehensible
code. Well, some of those stories are true. Reading and understanding
poorly written code is like trying to untangle a plate of spaghetti. C
gives you the freedom to design many types of strange but useful
constructs. But C also gives you the freedom to design the
incomprehensible. This section discusses some of C's magnificent but
potentially arcane constructs──those dealing with the more elaborate and
sophisticated uses of pointers.
Arrays of Pointers
C lets you create arrays of any type of elements. Thus, you can even
create an array whose elements are pointers. For example, to create an
array of 10 pointers, in which each item is a pointer to a float, simply
declare the following:
float *array_name[10];
The * preceding the array name in this declaration tells the compiler that
the array is an array of pointers; therefore, each element holds an
address. The float signifies that all pointers will point to float
variables.
You can use this technique for speeding up sorting routines, for example.
Because an address on a PC occupies only two bytes (except for far
pointers), while the data it points to occupies four bytes (for a float),
it's faster to exchange two 2-byte addresses than to exchange the data.
The advantage offered by arrays of pointers becomes even more evident when
we use them with strings in the next chapter.
The REVERSE.C program (Listing 8-10 on the following page) reads in lines
of characters. The addresses of those lines are stored in an array of
pointers to char. (See Figure 8-5.) An empty input line causes the lines
of text pointed to by the array of pointers to be printed in reverse
order.
cptrs[]
┌─────┐
Pointer to first line────│ │─────────►This is the text
├─────┤
Pointer to second line────│ │─────┐ that we typed in.
├─────┤ │
etc. │ │───┐ └─────►It will be reve
├─────┤ │
│ │──┐└─────►rsed. Line 3 is
├─────┤ └─────────────┘
│ │─────┐ here. Line 4 is
├─────┤ └──────────┘
│ │─────┐ here. Line 5 is
├─────┤ └──────────┘
│ │─────┐ Here. And so on...
├─────┤ └──────────┘
│ │
├─────┤ Continuous memory
│ │ allocated with srbk()
└─────┘
Array of
pointers
Figure 8-5. An array of pointers, each element of which contains a line of
text in allocated memory.
──────────────────────────────────────────────────────────────────────────
/* reverse.c -- demonstrates an array of pointers */
/* by reversing lines of text */
#include <stdio.h> /* for NULL */
#include <malloc.h> /* for size_t */
#define MAXL 20
main()
{
char *cptrs[MAXL]; /* array of pointers */
char *cp;
int count, i, j, ch;
extern char *Getbyte();
printf("Type in several lines of text, and I will\n");
printf("print them back out in reverse order.\n");
printf("(Any blank line ends input):\n");
for (i = 0; i < MAXL; ++i)
{
cp = Getbyte();
cptrs[i] = cp; /* assign address to pointer */
count = 0;
while ((ch = getchar()) != '\n') /* gather line */
{
*cp = ch;
cp = Getbyte();
++count;
}
*cp = '\0';
if (count == 0) /* all done if blank line */
break;
}
printf("---------<reversed>---------\n");
for (j = i-1; j >= 0; --j)
{
printf("%s\n", cptrs[j]);
}
}
char *Getbyte(void)
{
char *cp;
if ((cp = sbrk(1)) == (char *)-1)
{
printf("Panic: sbrk failed\n");
exit(1);
}
return (cp);
}
──────────────────────────────────────────────────────────────────────────
Listing 8-10. The REVERSE.C program.
The fact that we can print an array of characters with printf()
illustrates the correspondence between arrays of char and strings. We will
discuss that relationship in detail in the next chapter.
Pointers to Pointers
As you have seen, a pointer is a variable whose value is an address, and
that address is usually the location in memory of another variable.
However, in C, that other variable can also be a pointer. There is no
limit to how far you can extend this "pointer-to-a-pointer" relationship──
you can have pointers to pointers to pointers and so on, ad infinitum.
Here, however, we'll minimize the danger of creating "spaghetti code" by
restricting ourselves to pointers to pointers, sometimes referred to as
"handles."
Figure 8-6 illustrates the relationship of a pointer to a pointer. The
variable pp contains as its value the address of p. The variable p in turn
contains as its value the address of num, an ordinary integer. Because p
points to an int, pp is a pointer to a pointer to an int.
The following example shows how to declare a pointer to a pointer:
int **pp, *p, num;
│ └─────────────────────────────────────────── Pointer to an int
└──────────────────────────────────── Pointer to a pointer to an int
The two * characters tell the compiler that pp is a pointer to a pointer
and holds as its value the address of another pointer.
When accessing the values pointed to by pp, the number of *s determines
which value is obtained. Consider the following initialization:
p = #─────────────────────────────────────────────────Address of num
pp = &p;────────────────────────────────────────────────────Address of p
Points to a variable
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒ ▒
┌───────────┐ ▒
│ │ ▼
Memory── 621 622 623 624 625 626 627 628 629 630
locations ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
in bytes │ 629 │ │ 621 │ │ 3 │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
│ │ │
│ │ │
p pp num
▒
▒ ▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
Points to a pointer
Figure 8-6. Pointer to a pointer: a variable whose value is the address of
another pointer.
The following statement yields the value stored in the address that pp
points to:
*pp
Because pp points to p, *pp yields the address stored in p, that of num.
Placing another * in front of pp:
**pp
tells the compiler to fetch the value stored in the pointer pointed to by
pp. Because pp points to p, and p in turn points to num, **pp fetches the
value of num. Thus, all three of the following yield the value stored in
the variable num:
**pp
*p
num
One useful application for a pointer to a pointer is in traversing arrays
of pointers. The REVERSE2.C program (Listing 8-11) is a rewrite of the
previous REVERSE.C. In this version, we replace the final for loop with a
while loop that decrements pp, a pointer to a pointer.
──────────────────────────────────────────────────────────────────────────
/* reverse2.c -- demonstrates a pointer to a pointer */
#include <stdio.h> /* for NULL */
#include <malloc.h> /* for size_t */
#define MAXL 20
main()
{
char *cptrs[MAXL];
char **pp; /* pointer to pointer */
char *cp;
int count, i, ch;
extern char *Getbyte();
printf("Type in several lines of text, and I will\n");
printf("print them back out in reverse order.\n");
printf("(Any blank line ends input):\n");
for (i = 0; i < MAXL; ++i)
{
cp = Getbyte();
cptrs[i] = cp; /* assign address to pointer */
count = 0;
while ((ch = getchar()) != '\n') /* gather line */
{
*cp = ch;
cp = Getbyte();
++count;
}
*cp = '\0';
if (count == 0) /* all done if blank line */
break;
}
printf("---------<reversed>---------\n");
pp = &cptrs[i];
while (pp >= cptrs)
{
printf("%s\n", *(pp--));
}
}
char *Getbyte(void)
{
char *cp;
if ((cp = sbrk(1)) == (char *)-1)
{
printf("Panic: sbrk failed\n");
exit(1);
}
return (cp);
}
──────────────────────────────────────────────────────────────────────────
Listing 8-11. The REVERSE2.C program.
This program shows that a pointer to a pointer is decremented (or
incremented) by the number of bytes in a pointer:
printf("%s\n", *(pp--))
└──┬──┘
└──┬───────────────────────────────────────── The same
┌─────┴──┐
printf("%s\n", cptrs[i--])
Recall that the address in a pointer changes by a number of bytes that
corresponds to the type to which it points. A char pointer changes by one
byte, while a float pointer changes by four bytes. A pointer to a pointer
changes by the number of bytes in an address because it points to a
pointer, and thus to an address. Because cptrs[] is an array of pointers,
and pp points to one of those addresses, decrementing pp causes it to
point to the immediately preceding element in that array. Figure 8-7 on
the following page illustrates this process.
──────────────────────────────────────────────────────────────────────────
Pointer Pointer
Pointers are so versatile that they can contain the address of almost
anything. However, you cannot use pointers to obtain the address of the
following C elements: constants (such as 5); variables declared with the
keyword register; labels (the targets of goto); and keywords (such as if,
while, and so on).
──────────────────────────────────────────────────────────────────────────
cptrs[]
┌─────┐
┌───end────│ │─────────►This is the text
│ ├─────┤
│ --pp────│ │─────┐ that we typed in.
│ ├─────┤ │
pp │ --pp────│ │───┐ └─────►It will be reve
┌───────┐ │ ├─────┤ │
│ │ ──┤ --pp────│ │──┐└─────►rsed. Line 3 is
└───────┘ │ ├─────┤ └─────────────┘
Points to │ --pp────│ │─────┐ here. Line 4 is
pointer │ ├─────┤ └──────────┘
│ --pp────│ │─────┐ here. Line 5 is
│ ├─────┤ └──────────┘
└─start────│ │─────┐ Here. And so on...
├─────┤ └──────────┘
│ │
├─────┤ Continuous memory
│ │ allocated with srbk()
└─────┘
Array of
pointers
Figure 8-7. Decrementing a pointer to a pointer moves it down through an
array of pointers.
Pointers to Functions
It is often useful to know the address of a function. You declare a
pointer to a function as follows:
int (*pointer_name)();
This declares the variable pointer_name to be a pointer *pointer_name. The
trailing parentheses tell the compiler that the pointer *pointer_name
contains the address of a function. The int specifies that the function
pointed to returns an int.
To obtain the address of a function, merely state its name. However, be
sure you declare the function before you take its address:
int (*funptr)();─────────────────────────────────A pointer to a function
extern int Quit();───────────────────────────────────A function declared
funptr = Quit;──────────────────────Address of Quit() assigned to funptr
In this example, funptr contains the address of Quit(), and we can call
Quit() through funptr, as follows:
*funptr();
The preceding * tells the compiler to use the value pointed to by funptr
(the address of Quit()). The trailing parentheses tell the compiler to
call the function whose address we just fetched.
The CHOOSE.C program (Listing 8-12) goes one step further by creating an
array of pointers to functions. First, the program asks you to choose a
menu item. Then it translates your choice into an array offset and calls
the function whose address is stored at that offset.
──────────────────────────────────────────────────────────────────────────
/* choose.c -- an array of pointers to functions */
/* used to create a menu */
void Choice1(), Choice2(), Choice3();
void (*Dochoice[3])() = {Choice1, Choice2, Choice3};
main()
{
int ch;
printf("Select 1, 2 or 3: ");
ch = getch(); putch(ch);
ch -= '1';
if (ch < 0 || ch > 2)
printf("\nNo such choice.\n");
else
Dochoice[ch]();
}
void Choice1(void)
{
printf("\nThis is choice 1\n");
}
void Choice2(void)
{
printf("\nThis is choice 2\n");
}
void Choice3(void)
{
printf("\nThis is choice 3\n");
}
──────────────────────────────────────────────────────────────────────────
Listing 8-12. The CHOOSE.C program.
Arrays of pointers to functions are best applied in interactive programs.
Believe it or not, you'll find it easier to design word processors and
complex games once you master this technique.
The following example illustrates the advantage of using an array of
pointers to functions instead of a simpler switch statement. Examine the
following fragment from a hypothetical text processor:
int (*commands[128])() = {
...
Go_left, /* l */
Mark_line, /* m */
Next_search, /* n */
...
};
This array has 128 pointers to functions, each of which corresponds to a
key on the keyboard. Pressing l causes Go_left() to be called, moving the
cursor left. If the user wishes to change the meaning of the keys,
swapping the functions of l and n, for example, you need only use the
following:
int (*temp)();───────────────────────────────────────────Scratch pointer
int from, to;
from = 'l';
to = 'n';
temp = commands[from];
commands[from] = commands[to];
commands[to] = temp;
Here we first declare a scratch variable to be used in the swap. We
declare it as a pointer to a function because we will be swapping pointers
to functions. We then assign to temp the address stored in commands[from],
where from is the offset that corresponds to the numeric value of the
letter `n'. Because that array item is a pointer to the function
Next_search(), we are saving the address of that function. We then copy
the address in commands[to] into commands[from]. Finally, we assign the
address saved in temp to commands[to]. The result of this exchange is that
typing n now causes the Go_left() function to be called, and typing l
causes the Next_search() function to be called, thereby reversing their
roles.
Contrast this flexible form of programming with an inflexible switch
statement, such as the following:
switch(key)
{
...
case 'l':
Go_left();
break;
case 'm':
Mark_line();
break;
case 'n':
Next_search();
break;
...
}
Clearly, a program that a user can customize is more difficult to write,
yet a versatile program is always worth the extra effort.
Unscrambling the Spaghetti
In the previous sections of this chapter you've seen some complicated
declarations. You will see more of them in the chapters to follow, so it
behooves us to establish some rules that will help us understand complex
declarations.
Remember: Always start reading at the inside of a declaration with the
name (identifier); then work your way outward. For example, to unscramble
the following declaration:
int (*name)();
follow the definition from the inside out: name is a pointer to a function
of type int. Thus, this declaration is a pointer to a function that
returns an int.
Let's try this same technique on a different declaration:
float (*name)[3];
In this example, name is a pointer to an array of three float variables.
Thus, it is a pointer to an array of three floats. Contrast that
declaration with the following:
float *name[3];
Here name is an array of three pointers to float variables. This example
is an array of three pointers to floats. The difference lies in the
parentheses. Be sure to obey the order of precedence for operators.
As an example of using parentheses, try to decipher the following
declaration from CHOOSE.C:
int (*funs[4])();
Here the * operator has a higher precedence than the [] operators, so *
binds to funs first. Therefore, funs is a pointer, and four such pointers
exist in an array; these pointers point to functions that return the type
int. Thus, the declaration is an array of four pointers to functions that
return int.
────────────────────────────────────────────────────────────────────────────
Chapter 9 Strings
A "string" is a sequence of ASCII characters──this sentence, for example,
is a string. Strings give your programs life by enabling them to
communicate with the user. Nearly all programs──from our simple printf()
statements to the sophisticated dialogues of complex interactive
programs──use strings of one type or another.
Unlike BASIC and Pascal, the C language has no built-in string-type
variable. Instead, C uses the convention that a string is an array of type
char whose final, or terminating, value is the special character '\0'──a
one-byte zero value. Figure 9-1 on the following page illustrates such an
array.
We refer to this arrangement as a convention because nothing in C prevents
you from handling strings in another manner. For example, you might store
strings as arrays of short variables, using one byte to hold the character
and the other to hold the character's attributes (more on this in Chapter
13). Or you might store strings as a value length followed by length
number of characters. However, because you will most often handle strings
in the conventional way, we will emphasize that method in our discussion
of strings.
Address of bytes── 9876 9877 9878 9879 9880 9881 9882 9883
in memory ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ 'H' │ 'e' │ 'l' │ 'l' │ 'o' │'\n' │'\O' │ │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
│ │
└──────────────┬──────────────┘ │
│ │
ASCII characters Terminating zero
Figure 9-1. In C, a string is an array of type char terminated by a zero
value.
Declaring and Initializing Strings
A string is merely an array of type char, and you initialize it the same
way you would any other array. The following example fills the char array
named phrase with ASCII character constants that spell "Hello" followed by
the newline character:
char phrase[] = {'H', 'e', 'l', 'l', 'o', '\n', '\0'};
We made this array a conventional C string by adding a terminating zero
value (the character constant '\0'). As with all arrays, string arrays can
be initialized only if you use the keyword static or declare them
globally──outside of all functions.
The HELLO.C program (Listing 9-1) illustrates the proper way to
initialize string arrays. It also demonstrates the printf() format command
%s, which tells printf() to print the next argument as a string.
Because zero-terminated char arrays so commonly represent strings in C,
the language provides a built-in shorthand. When C finds text enclosed in
full quotation marks (called string constants), it immediately stores that
text as an array of type char and adds the terminating '\0'. This
characteristic of C provides you an alternate way to initialize arrays.
──────────────────────────────────────────────────────────────────────────
/* hello.c -- legal ways to initialize strings as */
/* arrays of char values */
char Gphrase[] = {
'H','e','l','l','o','\n','\0' }; /* global initialization */
main()
{
static char gphrase[] = {
'h','e','l','l','o','\n','\0' }; /* local initialization */
printf("Global: %s", Gphrase);
printf("Local: %s", gphrase);
}
──────────────────────────────────────────────────────────────────────────
Listing 9-1. The HELLO.C program.
For example, you can create the same arrays as those declared in HELLO.C
by substituting the following lines of code:
char Gphrase[] = "Hello\n";────────────────────────Global initialization
static char lphrase[] = "Hello\n";──────────────────Local initialization
As an aid in declaring long string constants, the compiler combines
adjacent quoted strings into a single string constant. This feature lets
you easily initialize long strings, as in the following example:
static char long_phrase[] = "This is one long "
"sentence that the compiler "
"combines into a single string.";
C uses the rule that if nothing but white space (spaces, tabs, or
newlines) separates two quoted strings, those strings are concatenated to
form a single string. Thus, the above QuickC declaration is equivalent to
the following:
static char long_phrase[] =
"This is one long sentence that the compiler combines into a single
string.";
Under pre-ANSI C, long string initializers can be emulated with the
#define preprocessor directive. Recall that you can extend #define lines
by ending each with a backslash and a newline character (that is, type \
and press Enter). Because this #define technique is portable to all
compilers, we will use it throughout the rest of the book:
#define PHRASE \
"This is one long sentence that the compiler \
combines into a single string."
static char long_phrase = PHRASE;
──────────────────────────────────────────────────────────────────────────
A Constant Reminder
When you declare string constants, remember that it is illegal for a
newline character to appear anywhere between full quotation marks. The
following example is illegal:
static char long_phrase = "This is one
long sentence that the...";
and results in the following QuickC error message:
error C2001:
newline in constant
If you want to insert a newline character into a string constant, use the
escape sequence for a newline character (\n) instead:
static char long_phrase[] = "This is one \n long sentence that the ..."
──────────────────────────────────────────────────────────────────────────
The String Pool and String Addresses
QuickC copies all of a program's quoted strings into a common area of
memory called the string pool. They are copied there, one after the other,
in the order that they occur in the program. (Figure 9-2 illustrates this
process.)
The STRPOOL.C program (Listing 9-2) dumps the contents of the string pool
to your terminal screen. Note in STRPOOL.C that any char array that ends
with a zero value, such as Cent_string, is placed into the string pool.
We place nonprinting characters into quoted strings as we did with
printf()──that is, a newline character, with \n; a carriage return, with
\r; and a tab, with \t. Other special characters that you can place in
string constants are the full quotation mark, with \"; the formfeed
character, with \f; the backspace character, with \b; and the bell (beep)
character, with \a.
You can include any character from the PC's extended character set in a
string constant by using a \x followed by a two-digit hexadecimal number.
For example, \x9B is used to represent the ¢ character. (QuickC's General
help screens include a handy table that lists these escape sequences.)
Note also in the program that we assigned the address of a string to a
pointer (cp = Start). Nowhere are pointers used more heavily than with
strings.
Your program String pool
┌────────────────────────────────┐ ┌─────┬─────┬─────┬─────┬─────┬─────
│°char phrase [ ] = "Hello\n";───┼─────┼►'H' │ 'e' │ 'l' │ 'l' │ 'o' │'\n'
│°main ( ) °│ ├─────┼─────┼─────┼─────┼─────┼─────
│°{ °│ ┌──┼'\O'─┼►'T' │ 'y' │ 'p' │ 'e' │ ' '
│° printf("Type in a line of");─┼──┘ ├─────┼─────┼─────┼─────┼─────┼─────
│° printf(" text and I will");──┼──┐ │ 'i' │ 'n' │ ' ' │ 'a' │ ' ' │ 'l'
│° . °│ │ ├─────┼─────┼─────┼─────┼─────┼─────
│° . °│ │ │ 'i' │ 'n' │ 'e' │ ' ' │ 'o' │ 'f'
│° . °│ │ ├─────┼─────┼─────┼─────┼─────┼─────
│° °│ └──┼'\O'─┼►' ' │ 't' │ 'e' │ 'x' │ 't'
│° °│ ├─────┼─────┼─────┼─────┼─────┼─────
└────────────────────────────────┘ │ ' ' │ 'a' │ 'n' │ 'd' │ ' ' │ 'I'
├─────┼─────┼─────┼─────┼─────┼─────
│ ' ' │ 'w' │ 'i' │ 'l' │ 'l' │'\O'
├─────┼─────┼─────┼─────┼─────┼─────
| | | | | |
| | | | | |
Figure 9-2. Quoted string constants are placed one after the other into
the string pool.
──────────────────────────────────────────────────────────────────────────
/* strpool.c -- dumps the string pool to show how */
/* quoted strings are stored */
#define PHRASE \
"This is one long sentence that the compiler \
combines into a single string."
char Start[] = "start";
char Long_phrase[] = PHRASE;
char Short_phrase[] = "This is a short phrase";
char Cent_string[] = "\x9B";
main()
{
static char local_phrase[] = "This is local";
char *cp;
printf("Dump of the string pool:\n");
printf("-----------------------\n");
printf("\""); /* print leading quote */
/*
* Note that the address of a string can be
* assigned to a pointer: cp = Start
*/
for (cp = Start; *cp != '^'; ++cp)
{
if (*cp == '\0') /* print '\0' as a quote */
printf("\"\n\"");
else if (*cp == '\n' ) /* print '\n' as '\' 'n' */
printf("\\n");
else
printf("%c", *cp);
}
printf("^"); /* marks end */
}
──────────────────────────────────────────────────────────────────────────
Listing 9-2. The STRPOOL.C program.
Pointers and Initialized Strings
In the last chapter we assigned the address of an array to a pointer. We
can also initialize a pointer to char with the address of a quoted string
constant, as follows:
char *str = "This is a phrase";
This example initializes the char pointer *str to contain the address of
the quoted string constant. Because the compiler places all string
constants into the "string pool," the address in *str is that of the
letter "T" (the first character of the char array) in the string pool.
Recall that an array declaration creates an rvalue and a pointer
declaration creates an lvalue. Consider the following declarations:
char ary[] = "This is a phrase";
char *str = "This is another";
The ary[] declaration creates an rvalue (an address reference, such as a
label) that cannot be changed with calculations.
The *str declaration, on the other hand, creates an lvalue (a pointer
variable whose value is an address), which can be changed with
calculations. You can, for example, increment the pointer as follows:
++str;
The distinction between lvalue and rvalue can be a confusing one for
beginning C programmers. Remember that an array name (such as ary[]) is a
fixed location and cannot be changed; a pointer (such as *str) is a
variable and can be changed.
The BIFFRED.C program (Listing 9-3) demonstrates that you can use
pointers to manipulate strings in the string pool. Examine the program
before you run it. Can you predict what it will do?
──────────────────────────────────────────────────────────────────────────
/* biffred.c -- strings in the string pool can be */
/* manipulated via pointers */
char Start[] = "start";
main()
{
char *cp;
int pass;
for (pass = 0; pass < 2; ++pass)
{
printf("My name is FRED\n");
cp = Start;
while (*cp != 'F')
++cp;
*cp = 'B';
*++cp = 'I';
*++cp = 'F';
*++cp = 'F';
}
}
──────────────────────────────────────────────────────────────────────────
Listing 9-3. The BIFFRED.C program.
Formatting Strings with printf()
So far, we've used printf() to print and format numbers (int with %d and
float with %f, for example), individual characters with %c, and quoted
strings with %s. The ability of printf() to print strings, however, goes
far beyond the mere echoing of quoted string constants. In the following
example:
printf("%s\n", ary);
the expression ary can be the address of any char type array that ends
with the character constant value '\0'. It can be a quoted string constant
such as
printf("%s\n", "This is a phrase");
or the address of a string from either a char array or a value in a
pointer, as in the following examples:
char *str, ary[] = "This is a phrase";
str = ary;
printf("%s\n", ary);─────────────────────────────────Address of an array
printf("%s\n", str);──────────────────────────────────Value in a pointer
Because all quoted strings are placed into the string pool and replaced
with their starting address in that string pool, it follows that the
format specification in the control string of this example:
printf("%s\n", str);
└──┬─┘
└────────────────────────────────────────────── Control string
can also be expressed as either an array address or the value in a
pointer, as follows:
char *str, ary[] = "This is a phrase.";
char *cp, ctl[] = "%s\n";
str = ary;
cp = ctl;
printf(ctl, ary);────────────────────────────────────Addresses of arrays
printf(cp, str);──────────────────────────────────────────Pointer values
printf(ctl, str);─────────────────────────────────────Mixture of the two
The CONTROL.C program (Listing 9-4 on the following page) demonstrates
this equivalence. This program asks you to type either an l or an r, and
then it prints out a string with the corresponding left or right
justification.
CONTROL.C lets you see how the printf() format specifier %s is used to
format strings. The various options you can use with %s are summarized in
Table 9-1 on the following page. You can also combine them as in the
following statement, which prints the first four letters of computer
right-justified in a 25-character field.
printf("%25.4s\n", "computer");
──────────────────────────────────────────────────────────────────────────
/* control.c -- demonstrates string justification */
/* using printf() */
char Some_text[] = "Some Text";
char Left_control[] = "<<%-15s>>";
char Right_control[] = "<<%15s>>";
main()
{
char ch;
while (1)
{
printf("Select l)eft r)ight or q)uit: ");
ch = getch();
putch(ch);
printf("\n\n");
switch((int) ch)
{
case 'l':
case 'L':
printf(Left_control, Some_text);
break;
case 'r':
case 'R':
printf(Right_control, Some_text);
break;
case 'q':
case 'Q':
exit (0);
default:
printf("Huh?");
break;
}
printf("\n\n");
}
}
──────────────────────────────────────────────────────────────────────────
Listing 9-4. The CONTROL.C program.
Note: In these format specifiers, num must be a decimal integer. You can
combine the last option, %.nums, with any of the others, producing, for
example, %25.5s.
Table 9-1 Variations of the printf() %s Specifier
──────────────────────────────────────────────────────────────────────────
%s Prints the string exactly as it is
%nums Prints the string right-justified in a field of width num
%-nums Prints the string left-justified in a field of width num
%.nums Prints num characters of string
──────────────────────────────────────────────────────────────────────────
String Input and Output
The standard C Library contains several functions specifically designed to
facilitate input and output of strings. Here we discuss some that read
from your keyboard or print to your screen. The next chapter ("Managing
Files") deals with file-handling counterparts to these functions. In
Chapter 13, we will present additional routines that directly access the
keyboard and screen hardware.
String Input with scanf()
We've already used scanf() several times: Now let's discuss it in detail.
The scanf() function uses the same % specifiers that printf() does, but it
uses them to read values, not to print them. Unfortunately, scanf()
handles strings a little differently than does printf(). Where printf()
prints the entire string to a terminating '\0', scanf() reads only
space-delimited words of text. That is, for each %s in its control string,
scanf() reads all characters up to, but not including, space, tab, or
newline. Therefore, scanf() is best used for reading words rather than
lines of text.
The scanf() routine, when used with %s to read words of text, takes the
form
scanf("%s", buf);
where buf is the address of a char array (buffer) into which scanf()
places the text it reads from the keyboard. The array buf can be either a
char array or a pointer to memory created by malloc(). (Note that you do
not need to use an ampersand with an array name.) The scanf() function
appends a terminating '\0' to the text in buf.
The short SCANLINE.C program (Listing 9-5 on the following page)
illustrates a simple way to use scanf() for reading words of text from the
keyboard. It asks you to type in a line of text and then uses scanf() to
print the words of that text, one word per line.
When you run SCANLINE.C, notice that it prints nothing until you press the
Enter key. This is because scanf() is a "buffered I/O" routine. It reads
from the standard input (the keyboard), but it "sees" nothing until you
"flush the standard input buffer" by pressing the Enter key. (We discuss
this concept of buffered versus unbuffered I/O in the next chapter.)
The scanf() function provides two variations for the %s specifier. (See
Table 9-2.) These let you read more than individual words.
Table 9-2 Variations of the scanf() %s Specifier
──────────────────────────────────────────────────────────────────────────
%nums Reads num characters including space, tab, or newline
characters (Specify num as a decimal integer.)
%[range] Reads a specified range of characters
──────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────
/* scanline.c -- demonstrates how scanf() reads */
/* the individual words of a line */
#define INTRO \
"Type in lines of text. They will be printed out\n\
one word per line, thus demonstrating scanf().\n\
(Type Ctrl-Z to Quit)\n"
main()
{
char buf[512]; /* should be big enough */
printf(INTRO);
/*
* scanf() returns the number of items
* its control string matched
*/
while (scanf("%s", buf) == 1)
{
printf("%s\n", buf);
}
}
──────────────────────────────────────────────────────────────────────────
Listing 9-5. The SCANLINE.C program.
The following example reads 127 characters from the keyboard and places
them into the array buf:
char buf[128];
scanf("%127s", buf);──────────────────────────────────────────%nums form
buf[127] = '\0';
This form of scanf() has two disadvantages. First, because newline
characters can be read into buf, you can't easily tell whether buf
contains a complete line or a partial line or a number of lines. Second,
because this form does not append a terminating '\0' to the text, you must
add it yourself.
For better control, use the more complex scanf() %[range] directive. Here
range is any list of characters that you want to include in buf. The
following example:
scanf("%[0123456789]", buf);
reads in only the digits 0 through 9. Anything else causes scanf() to stop
reading and terminate buf with a '\0'.
A more useful variation of the %[range] directive can be constructed using
the ^ character. When you use a ^ as the first character in range, scanf()
reads all characters up to, but not including, any characters in range and
stops reading at the first excluded character. This version of scanf()
also appends a terminating '\0' to the characters it reads. The following
example shows how to use this variation:
scanf("%[^\n]", buf);─────────────────────────────Read all but a newline
scanf("%[\n]", dummy);───────────────────────────────Read only a newline
The first line tells scanf() to read all characters up to, but not
including, the newline character and to place those characters into buf.
The second line tells scanf() to read only a newline character (the one
that terminated the first scanf()) and to place it into dummy. The scanf()
function can be tricky to use (witness the need for the second statement),
but with practice, you will find it a valuable and powerful programming
tool.
The SCRANGE.C program (Listing 9-6) summarizes the scanf() function. It
prompts for, and reads in, several lines of text, displaying exactly what
scanf() reads as it executes.
──────────────────────────────────────────────────────────────────────────
/* scrange.c -- illustrates scanf()'s control */
/* directives */
main()
{
char buf[512], /* should be big enough */
dummy[2]; /* for \n and \0 */
int num;
do
{
printf("Running:\n");
printf("\tscanf(\"%%d\", &num);\n");
printf("\tscanf(\"%%[^\\n]\", buf);\n");
printf("\tscanf(\"%%[\\n]\", dummy);\n");
printf("\nType enough to satisfy this:\n");
printf("(Set num equal to zero to quit)\n");
scanf("%d", &num);
scanf("%[^\n]", buf);
scanf("%[\n]", dummy);
printf("\n\tnum = %d\n", num);
printf("\tbuf[] = \"%s\"\n", buf);
printf("\n\n");
} while (num != 0) ;
}
──────────────────────────────────────────────────────────────────────────
Listing 9-6. The SCRANGE.C program.
Lines of Text with gets() and puts()
Although we can use variations of scanf() to read lines of text, the
QuickC library contains a pair of routines specifically tailored for
reading and writing strings as lines of text. A line of text, in this
case, is any string that includes a terminating newline. This is the most
natural form of text entry because it corresponds to a line of text on the
screen.
Although the newline, '\n', is used throughout C to represent the end of a
line of text, it does not correspond to the characters produced or
expected by your hardware. The Enter key, for example, actually produces
the '\r' character. And printing a '\n' to your screen moves the cursor
down but not to the left on the screen. Fortunately, scanf() and gets()
convert an Enter keypress ('\r') to a newline ('\n'), and both printf()
and puts() convert a newline ('\n') into a carriage return/linefeed
combination ('\r' '\n') when writing to your screen.
The gets() (pronounced "get s") function reads all typed characters up to
and including a newline (generated when you press Enter) and places those
characters into a char array. The newline is then replaced with a '\0' to
form a C string. The puts() (pronounced "put s") function prints a string
on the screen and adds a newline to the end.
The DIALOG.C program (Listing 9-7) uses gets(), puts(), and printf() to
carry on a simple conversation. Note that because the gets() function
returns NULL if it fails, we must use the directive #include <stdio.h> to
incorporate the definition of NULL.
──────────────────────────────────────────────────────────────────────────
/* dialog.c -- a conversation using gets() and puts() */
#include <stdio.h> /* for NULL and BUFSIZ */
#define THE_QUESTION \
"And what is your view on the current price of corn\n\
and the stability of our trade import balance?"
main()
{
char name[BUFSIZ],
buf[BUFSIZ];
extern char *gets();
name[0] = '\0'; /* clear the name */
puts("\n\nHi there. And what is your name?");
if (gets(name) != NULL && name[0] != '\')
{
printf("\nPleased to meet you, %s.\n", name);
puts(THE_QUESTION);
/*
* force an extra <enter> before replying
*/
do
{
if (gets(buf) == NULL)
break;
} while (*buf != '\0'); /* wait for empty line */
puts("Sorry. I needed to think about that.");
printf("Nice talking to you, %s.\n", name);
}
else
puts("How rude!");
puts("Goodbye.");
}
──────────────────────────────────────────────────────────────────────────
Listing 9-7. The DIALOG.C program.
String Manipulation Routines
As you can see, the string I/O routines in DIALOG.C are not very
sophisticated. Fortunately, the QuickC library contains a host of
functions that permit more complex string manipulations. We won't describe
all of the functions here──each has its own page in the Microsoft QuickC
Run-Time Library Reference──but we do list many of them in Table 9-3 on
page 279. We will, however, use many of these functions in one large
program and then discuss those selected string-handling routines.
The ACME.C program (Listing 9-8) asks the user to fill out an employment
application for a fictional company. It isn't particularly user friendly,
and it terminates if you type something it can't understand.
──────────────────────────────────────────────────────────────────────────
/* acme.c -- illustrates an assortment of the */
/* C library string-handling routines */
#include <stdio.h> /* for NULL */
#include <string.h> /* for strchr(), et al */
#define NAME_PATTERN \
"first<space>last or\n\
first<space>middle<space>last"
#define ADDRESS_PATTERN \
"number<space>street<comma><space>city"
char Buf[BUFSIZ]; /* global I/O buffer */
main()
{
char *ocp, *cp, *first, *last, *street, *city;
void Prompt(), Cant();
printf("Acme Employment Questionnaire\n");
/*
* Expect first<space>last or
* first<space>middle<space>last
*/
Prompt("Full Name");
/* search forward for a space */
if ((cp = strchr(Buf,' ')) == NULL)
Cant("First Name", NAME_PATTERN);
*cp = '\0';
first = strdup(Buf);
*cp = ' ';
/* search back from end for a space */
if ((cp = strchr(Buf,' ')) == NULL)
Cant("Last Name", NAME_PATTERN);
last = strdup(++cp);
/*
* Expect number<space>street<comma><space>city<comma>
*/
Prompt("Full Address");
/* search forward for a comma */
if ((cp = strchr(Buf,',')) == NULL)
Cant("Street", ADDRESS_PATTERN);
*cp = '\0';
street = strdup(Buf);
/* Search forward from last comma for next comma */
if ((ocp = strchr(++cp,',')) == NULL)
Cant("City", ADDRESS_PATTERN);
*ocp = '\0';
city = strdup(++cp);
printf("\n\nYou Entered:\n");
printf("\tFirst Name: \"%s\"\n", first);
printf("\tLast Name: \"%s\"\n", last);
printf("\tStreet: \"%s\"\n", street);
printf("\tCity: \"%s\"\n", city);
}
void Cant(char *what, char *pattern)
{
printf("\n\n\bFormat Error!!!\n");
printf("Can't parse your %s.\n", what);
printf("Expected an entry of the form:\n\n");
printf("%s\n\nAborted\n", pattern);
exit(1);
}
void Prompt(char *str)
{
while (1)
{
printf("\n%s: ", str );
if (gets(Buf) == NULL || *Buf == '\0')
{
printf("Do you wish to quit? ");
if (gets(Buf) == NULL || *Buf == 'y')
exit (0);
continue;
}
break;
}
}
──────────────────────────────────────────────────────────────────────────
Listing 9-8. The ACME.C program.
The strchr() String Function
The first new function in ACME.C is strchr() (for "string character").
This routine requires two arguments──a string to search and a character to
look for in that string:
strchr(Buf, ' ')
└────┴────────────────────── Search string for a space character
If strchr() finds the character in the string, it returns the address of
that character. If it doesn't find the character, it returns NULL. Thus,
we can handle the error as follows if the character is not in the string:
┌────┬─────────────── Search Buf for a space character
if ((cp = strchr(Buf, ' ')) == NULL)
└──────────────────────────┼───────────────────────── Save address
└─────────────── Then test for an error
In the example, cp is a pointer to char into which we assign the address
returned by strchr(). If the result of that assignment (the value of cp)
is NULL, the string Buf contains no space character.
Because strchr() returns the address of a string, you must either declare
it in your program as char *strchr(); or use the statement #include
<string.h> (as we did in ACME.C), to supply the declaration for strchr().
The strdup() String Function
The second new function in ACME.C is strdup(). This is a Microsoft QuickC
function that does not exist in other C libraries. When passed a string,
strdup() makes a copy of that string and returns the address of the copy.
Because this type of "string duplication" is not portable, we'll show you
a version (Listing 9-9) that is. The implementation of this portable
version of strdup() introduces two new string-handling functions, strlen()
and strcpy().
──────────────────────────────────────────────────────────────────────────
#include <stdio.h> /* for NULL */
#include <malloc.h> /* malloc */
char *
strdup(str)
char *str;
{
char *newstr;
int bytes;
bytes = strlen(str);
if ((newstr = malloc(bytes + 1)) == NULL)
return (NULL);
88 (void)strcpy(newstr, str);
return (newstr);
}
──────────────────────────────────────────────────────────────────────────
Listing 9-9. The strdup() function.
The strlen() String Function
The strlen() function counts the number of characters in a string
(excluding the terminating '\0') and returns that count. For example, the
assignments
static char word[] = "Biff";
bytes = strlen(word);
cause bytes to be assigned the value 4 because the string word contains
four letters.
The strcpy() String Function
The strcpy() function copies its second argument (a string) into its
first, a buffer large enough to hold that copy. The value returned by
strcpy() is the address of its first argument. Because we wanted to ignore
that return value in our version of strdup(), we typecast the call as type
void:
if ((newstr = malloc(bytes + 1)) == NULL)
return (NULL);
(void)strcpy(newstr, str);
To create the space for the copy, we call malloc() with an argument of
bytes + 1, which creates room for both the copy of the string and the
appended terminating '\0'. (Remember strlen(), which gave us the value in
bytes, does not count the terminating '\0'.)
Table 9-3 QuickC Library String Manipulation Functions
──────────────────────────────────────────────────────────────────────────
strlen(str) Returns the length of a string str, not counting the
terminating '\0'
strcat(s1, s2) Concatenates the second string (s2) to the end of the
first (s2)
strcmp(s1, s2) Compares two strings (s1 and s2); returns 0 if they
are the same, otherwise returns the arithmetic
difference of the first two nonmatching characters
stricmp(s1, s2) Compares two strings without regard to case
strncmp(s1, s2, n) Compares n characters in the two strings (s1 and s2)
strcpy(buf, str) Copies a string (str) into a char buffer buf, which
must be large enough to hold both the string and its
terminating '\0'
strncpy(buf, str, n) Copies n characters of the string str into the buffer
buf
strchr(str, ch) Finds a character (ch) in a string (str); returns the
address of ch if found, otherwise returns NULL
strcspn(s1, s2) Finds a substring in s1 that begins with anything
other than one of the characters in s2; returns the
address of that substring if found, otherwise returns
NULL
strstr(s1, s2) Finds the first occurrence of the substring s1 in the
larger string s2; returns the address of that
substring if found, otherwise returns NULL
strrev(str) Reverses the characters in the string str; returns
the address of that reversed string
strupr(str) Converts a string (str) to uppercase characters
strset(str, ch) Clears a string (str), converting all its characters
to the character ch
strdup(str) Duplicates a string (str), returning the address of
the new copy
sprintf(str, cntl, Formatted print into a string (str), converting args
args,...) based on the control string cntl
sscanf(str, cntl, Formatted convert, like scanf(), but converts from
addrs,...) the string rather than from the keyboard
──────────────────────────────────────────────────────────────────────────
You should be aware that although stricmp(), strcspn(), and strupr() are
supplied with the Microsoft QuickC library, they are not a part of ANSI C.
Do not use them if you want your programs to be portable to other
compilers and computers.
C vs BASIC String Functions
As you have seen, sophisticated C string handling can require complicated
programming. Although the C library string-handling routines can emulate
much of BASIC, the following example demonstrates that such emulation is
usually less straightforward:
A$ = B$────────────────────────────────────────────────────────────BASIC
first = strdup(Buf);───────────────────────────────────────────────────C
Some functions common to BASIC are missing from C. Among them are LEFT$,
MID$, and RIGHT$. Listing 9-10 shows a C version of LEFT$. We leave it as
an exercise for you to write C versions of the other two BASIC commands.
C offers two principal advantages over BASIC──it permits the programmer to
extend string-handling library routines with customized routines, and it
allows easy access to strings from pointers.
──────────────────────────────────────────────────────────────────────────
#include <stdio.h> /* for NULL */
#include <string.h> /* for strdup() */
char *
leftstr(str, cnt)
char *str;
int cnt;
{
char *cp;
if (strlen(str) < cnt || cnt <= 0)
return (NULL);
if (strlen(str) == cnt)
return (strdup(str));
cp = strdup(str);
cp[cnt - 1] = '0';
return (cp);
}
──────────────────────────────────────────────────────────────────────────
Listing 9-10. The leftstr() function.
Arrays and Strings
Because a string is nothing more than an array of type char, you can use a
two-dimensional array of type char as an array of strings. However, you
must be sure to terminate each row (string) with a '\0' character, as
follows:
char names[3][6] = {
{ 'J','o','e','\0' },
{ 'D','u','k','e','\0' },
{ 'O','z','z','i','e','\0' }
};
You also can take the easier route of using string constants (quoted
strings) as array initializers:
char names[3][6] = { "Joe", "Duke", "Ozzie" };
Both forms create identical arrays, as illustrated in Figure 9-3. Also
notice that underinitializing rows sets the trailing characters in rows 0
and 1 to '\0'.
char names [3] [6] = {"Joe", "Duke", "Ozzie"};
Columns
│
┌───────────────────────┴───────────────────────┐
│ ┌────────┼─┐
┌─┌───────┬───────┬───────┬───────┬──────▼┬───────┐ │
│ │ 'J' │ 'o' │ 'e' │ '\O' │ '\O' │ '\O' ◄─┼─ Auto-initia
│ ├───────┼───────┼───────┼──────┼───────┼───────┤ │ trailing ze
Rows of ─┤ │ 'D' │ 'u' │ 'k' ││ 'e' │ '\O' │ '\O' ◄─┘
strings │ ├───────┼───────┼───────┼┼──────┼──────┼───────┤
│ │ 'O' │ 'z' │ 'z' ││ 'i' ││ 'e' │ '\O' │
└─└───────┴───────┴───────┴┼──────┴┼──────┴──────┘
│ │ │
└───────┴───────┴────────── String-
terminating
zeros
Figure 9-3. A two-dimensional array of char values as an array of strings.
As we've already seen, strings can be easily manipulated by pointers.
Because of this, arrays of pointers to strings are often used in place of
the two-dimensional arrays of char. The previous sample arrays, declared
and initialized as an array of pointers, appear as follows:
char *names[3] = { "Joe", "Duke", "Ozzie" };
└──────────────────────────────────────────── Array of pointers
This pointer form also uses storage space more efficiently than the
two-dimensional array. Compare the memory use of this form, depicted in
Figure 9-4, with that of the preceding approach (shown in Figure 9-3).
┌──────────┐ ┌───────┬───────┬───────┬───────┐
│ │──────►│ 'J' │ 'o' │ 'e' │ '\O' │
├──────────┤ ├───────┼───────┼───────┼───────┼───────┐
│ │──────►│ 'D' │ 'u' │ 'k' │ 'e' │ '\O' │
├──────────┤ ├───────┼───────┼───────┼───────┼───────┼───────┐
│ │──────►│ 'O' │ 'z' │ 'z' │ 'i' │ 'e' │ '\O' │
└──────────┘ └───────┴───────┴───────┴───────┴───────┴───────┘
└─────┬────┘
│
Array of 3 pointers
│
┌──────┴─────┐
char *names[3] = {"Joe", "Duke", "Ozzie"};
Figure 9-4. Arrays of pointers to strings use memory efficiently.
The L2WORDS.C program (Listing 9-11) illustrates one application for an
array of pointers to strings. It asks you to enter a line of text, then it
breaks that line into individual words and returns an array of pointers to
the substrings that form those words. Line2words() assumes that spaces
separate words, but it can take multiple words as a single word if you
surround them with full quotation marks. A routine like Line2words() is
useful for writing your own command-line interpreter (COMMAND.COM).
──────────────────────────────────────────────────────────────────────────
/* l2words.c -- employs an array of pointers to */
/* strings to break a line of text */
/* into its component words */
#include <stdio.h> /* for NULL and BUFSIZ */
main()
{
char **Line2words(); /* declare function type */
char **list; /* pointer to pointer */
char buf[BUFSIZ]; /* buffer for input */
int count, i, quote_flag;
printf("Enter a line of text and I will break\n");
printf("it up for you.\n");
if (gets(buf) == NULL)
exit(1);
list = Line2words(buf, &count);
for (i = 0; i < count; i++)
{
quote_flag = 0;
printf("<");
if (list[i] != buf)
{
if( list[i][-1] == '"') /* negative subscript */
{
++quote_flag;
printf("\"");
}
}
printf("%s", list[i]);
if (quote_flag)
printf("\"");
printf(">\n");
}
}
#define MAXW 64
char **Line2words(char *line, int *count)
{
static char *words[MAXW];
int index;
index = 0; /* zero internal index */
while (*line != '\0')
{
/* turn spaces and tabs into zeros */
if (*line == ' ' || *line == '\t')
{
*(line++) = '\0';
continue;
}
words[index] = line++; /* found a word */
/* is it quoted? */
if ( *(words[index]) == '"')
{
/* Yes, advance pointer to just past quote. */
++words[index];
/* find next quote. */
while (*line && *line != '"')
{
++line;
}
/* and turn it into a '\0'. */
if (*line)
*(line++) = '\0';
}
else
{
/* otherwise skip to next space */
while (*line && *line != ' ' && *line != '\t')
{
++line;
}
}
if (++index == MAXW)
break;
}
*count = index; /* set count via pointer */
return (words); /* return address of array */
}
──────────────────────────────────────────────────────────────────────────
Listing 9-11. The L2WORDS.C program.
L2WORDS.C does a few tricky things: First, notice that we declare the
function Line2words() as char **. This means that it returns a pointer to
a pointer. That pointer contains the address of the first element of our
array of pointers. The first element in that array points to the first
word.
Second, notice that when the program prints words, it checks lines[i][-1]
(negative subscripting) to see if the string has full quotation marks
around it. If it does, the program replaces them when it prints the word.
The Arguments to main()──argv and argc
When you run a program from the command interpreter (COMMAND.COM under
MS-DOS, or sh or csh under UNIX), you can specify arguments for the
program on the command line. For example, when you run QuickC by typing
C> qc file.c
QuickC starts with the file named file.c already loaded. All C programs,
including QuickC, retrieve arguments from the command line in the same
way. That is, every C program begins execution with the function named
main(), and that function, like any other, can receive arguments.
Traditionally called argc and argv, these arguments are received by main()
as follows:
main(argc, argv)
int argc;
char *argv[];
These arguments to main() contain all the information that you need to
access the command-line arguments: argc is the number of command-line
arguments, and argv is an array of pointers to those arguments.
The SHOWARGS.C program (Listing 9-12) shows how to access and use the
arguments passed to main(). To run this program from within QuickC, you
must first set the command-line arguments with Set Runtime Options on the
Run menu.
When you run SHOWARGS.C with the following command-line preset in the Set
Runtime Options dialog box:
kit makes lovely paper
the program prints the following:
argc = 5───────────────────────────────────────────Five pointers in argv
argv[0] -> "C"
argv[1] -> "kit"
argv[2] -> "makes"
argv[3] -> "lovely"
argv[4] -> "paper"
argv[5] -> NULL
──────────────────────────────────────────────────────────────────────────
/* showargs.c -- shows how to access the arguments */
/* passed to main() */
#include <stdio.h> /* for NULL */
main(argc, argv)
int argc;
char *argv[];
{
int i;
printf("argc = %d\n", argc);
printf("\n");
for (i = 0; i < argc; ++i)
{
printf("argv[%d] -> \"%s\"\n", i, argv[i]);
}
printf("argv[%d] -> NULL\n", i);
printf("\n");
}
──────────────────────────────────────────────────────────────────────────
Listing 9-12. The SHOWARGS.C program.
The first string that argv points to (an array of pointers to strings) is
usually the name of your program. (Under QuickC, your program will always
be named C when you run it from the Run menu, but argv[0] is correct when
you run your program later as a .EXE file.)
Because argv is an array of pointers to char, you often will see it
alternatively declared as follows:
main(argc, argv)
int argc;
main(argc, argv)
int argc;
char **argv;
{ └─────────────────────────────────────────── A pointer to a pointer
Recall that this pointer to a pointer and the declaration char *argv[] are
interchangeable.
The main() function is actually passed three arguments, but the third
argument, called envp, is seldom used. Like argv, it is an array of
pointers to strings and must be declared as follows:
main(argc, argv, envp)
int argc;
char *argv[], *envp[];
{
The strings that envp points to are your system's environmental variables,
such as PATH.
Take a moment to modify SHOWARGS.C so that it matches the SHOW2.C program
(Listing 9-13). After you run this program, choose DOS Shell from the
File menu and type set. Compare the output produced by the MS-DOS SET
command to that produced by this program.
──────────────────────────────────────────────────────────────────────────
/* show2.c -- shows how to use main()'s envp */
#include <stdio.h> /* for NULL */
main(argc, argv, envp)
int argc;
char *argv[], *envp[];
{
int i;
printf("argc = %d\n", argc);
printf("\n");
for (i = 0; i < argc; ++i)
{
printf("argv[%d] -> \"%s\"\n", i, argv[i]);
}
printf("argv[%d] -> NULL\n", i);
printf("\n");
for (i= 0; envp[i] != NULL; ++i)
{
printf("envp[%d] -> \"%s\"\n", i, envp[i]);
}
printf("envp[%d] -> NULL\n", i);
}
──────────────────────────────────────────────────────────────────────────
Listing 9-13. The SHOW2.C program.
Character Classification and Transformation
You often need to be able to classify individual characters of a string
(such as uppercase versus lowercase) and then transform them (such as
converting uppercase to lowercase). QuickC includes a standard C header
file called ctype.h, which defines many character classifications and
transformation routines. (Use the View Include menu to examine it.) To
access ctype.h, merely use #include to include it at the head of your
program.
The routines in ctype.h are not true functions: They are #define macros.
We'll describe #define macros in detail in Chapter 12. In the meantime,
you can use these routines because they work like function calls.
The Character Classification Routines
Each of the character classification routines in Table 9-4 takes a single
argument──the character to classify──and returns a 1 for true or a 0 for
false.
The WHATCHAR.C program (Listing 9-14) prints all possible classifications
for each character in a line of entered text. The program limits the line
of text to 20 characters so that the display doesn't scroll off the
screen.
Table 9-4 The Character Classification Routines in ctype.h
──────────────────────────────────────────────────────────────────────────
isalnum() Tests for alphanumeric ('A' through 'Z,' 'a' through 'z,' and
'0' through '9')
isalpha() Tests for a letter ('A' through 'Z' and 'a' through 'z')
isascii() Tests for an ASCII character (0x00 through 0x7F)
iscntrl() Tests for a control character (less than ' ' or equal to 0x7F)
isdigit() Tests for a digit ('0' through '9')
isgraph() Tests for printable character (inverse of iscntrl() but
excludes space)
islower() Tests for lowercase letter ('a' through 'z')
isprint() Tests for printable character (inverse of iscntrl())
ispunct() Tests for punctuation character
iswhite() Tests for white space ('\t,' '\n,' '\f,' and ' ')
isupper() Tests for uppercase letter ('A' through 'Z')
isxdigit() Tests for a hexadecimal digit ('A' through 'F,' 'a' through
'f,' '0' through '9')
──────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────
/* whatchar.c -- demonstrates the character */
/* classification routines in ctype.h */
#include <stdio.h> /* for NULL and BUFSIZ */
#include <ctype.h> /* for iscntl(), et al */
#define MAXL 20
main()
{
char buf[BUFSIZ];
int i;
printf("Enter a line of text (20 chars max):\n");
if (gets(buf) == NULL)
exit(1);
for (i = 0; i < MAXL; ++i)
{
if (buf[i] == '\0')
break;
printf("'%c' ->", buf[i]);
if (isalpha(buf[i])) printf(" isalpha");
if (isascii(buf[i])) printf(" isascii");
if (iscntrl(buf[i])) printf(" iscntrl");
if (isgraph(buf[i])) printf(" isgraph");
if (isprint(buf[i])) printf(" isprint");
if (isdigit(buf[i])) printf(" isdigit");
if (isupper(buf[i])) printf(" isupper");
if (islower(buf[i])) printf(" islower");
if (ispunct(buf[i])) printf(" ispunct");
if (isspace(buf[i])) printf(" isspace");
if (isalnum(buf[i])) printf(" isalnum");
if (isxdigit(buf[i])) printf(" isxdigit");
printf("\n");
}
}
──────────────────────────────────────────────────────────────────────────
Listing 9-14. The WHATCHAR.C program.
The include file ctype.h also defines routines to transform characters.
Each of the routines in Table 9-5 takes a single argument, the character
to transform, and returns the transformed character, as in the following
example:
ch = toupper('a');
Here toupper() is given a lowercase 'a'. Because 'a' is lowercase,
toupper() transforms it to an uppercase 'A' and assigns that value to the
variable ch.
The INVERT.C program (Listing 9-15) uses both the character
classification and transformation routines to reverse a line of entered
text. That is, it prints the line backward and inverts the case of each
character.
Table 9-5 The Character Transformation Routines in ctype.h
──────────────────────────────────────────────────────────────────────────
toascii() Converts a non-ASCII character to an ASCII character (clears
all but the low-order seven bits)
toupper() Converts a lowercase character to an uppercase character
tolower() Converts an uppercase character to a lowercase character
──────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────
/* invert.c -- combines character classification and */
/* transformation to invert text */
#include <stdio.h> /* for NULL */
#include <ctype.h> /* for toupper, et al. */
main()
{
char buf[BUFSIZ];
int i;
printf("Type in a line of text and I will invert it.\n");
if (gets(buf) == NULL)
exit(1);
/* Print the string backward. */
for (i = (strlen(buf) - 1); i >= 0; --i)
{
if (isupper(buf[i])) /* upper to lower */
putchar(tolower(buf[i]));
else if (islower(buf[i])) /* lower to upper */
putchar(toupper(buf[i]));
else
putchar(buf[i]);
}
putchar('\n');
}
──────────────────────────────────────────────────────────────────────────
Listing 9-15. The INVERT.C program.
────────────────────────────────────────────────────────────────────────────
Chapter 10 Managing Files
C files are primarily disk files that contain text, executable images of
programs, or data. These disk files represent stored programs and data
that form a common "library" of information that is available to a wide
range of programs.
The QuickC library functions that handle file input and output are
arranged in three categories, or levels, as illustrated in Figure 10-1 on
the following page. At the top level are the buffered (stream I/O)
routines; below those are the unbuffered (raw I/O) routines; and at the
bottom are the direct BIOS interfaces. The low-level routines are not a
part of portable C because they access PC-specific internal routines. The
higher-level routines, however, are universal to all C compilers. We will
not cover the low-level BIOS routines in this book.
The top-level file I/O routines are called "buffered stream" routines
because they interpose themselves between your program and files. They
read and write large blocks of information (buffering), and then they pass
a continuous series (stream) of bytes to your program, as needed.
┌────────────────────┐
│ │
│ Buffer │◄────────────────┐
│ │ │
└───────────────────┘ │
│ │
│ │
┌─────────▼──────────┐ │
┌─────────►│Top-level │ │
│ │functions like │ ┌───────▼────────┐
┌──────────────▼─┐ │fopen() and fgetc() │ │ │
│ │ └────────────────────┘ │ Your │
│ Disk │ ┌────────────────────┐ │ program │
│ │ │Mid-level │ │ │
└──────────────┘ │functions like │ └──────────────┘
│ └─────────►│open() and read() │◄──────────┘ │
│ └────────────────────┘ │
│ ┌────────────────────┐ │
│ │Low-level │ │
└───────────────►│functions like │◄────────────────┘
│_bios_disk() │
└────────────────────┘
Figure 10-1. The three levels of file I/O.
The middle file level is called unbuffered because it lets your program
access files directly. Reads and writes do not pass through an
intermediate buffer; they pass directly between the operating system and
your program. These mid-level routines can execute faster than the
top-level routines, but they are more complex to use.
Both top-level and mid-level file routines have two modes──text and
binary. Text mode is used with text files, or files that contain ASCII
text (which is readable by persons). Binary mode is used with files that
contain binary information, such as executable programs. In text mode,
Ctrl-Z (a byte containing the value 0x1A) marks the end of a file. In
binary mode, Ctrl-Z can legally be a part of the file; the operating
system keeps track of file length.
Top-Level I/O
All buffered file I/O functions require that you begin your program with
#include <stdio.h>. That header file contains the definition for FILE, the
data type that you use to manipulate files. The type FILE is used as shown
on the next page.
#include <stdio.h>
...
FILE *fp;
Remember, always use #include <stdio.h> for the definition of the type
FILE. Then declare a file pointer to point to the data type FILE.
Opening Files with fopen()
Before you can access a file for reading or writing or both, you must
first open that file. For buffered I/O routines (those that use a file
pointer), open the file with the fopen() function, as follows:
fp = fopen(filename, activity);
│ └───────────────── Open to read, write, or both
└──────────────────────────────────── Name of file to open
The fopen() function requires two arguments: the name of the file to open
(a string or the address of a string) and an activity (also a string) as
listed in Table 10-1. The activity determines whether the file is open
for reading, writing, or appending. (In this case, read means to take
information sequentially from a file, write means to put information
sequentially into a file, and append means to add information to the end
of a file.)
The fopen() function returns a value of type FILE *. In our example we
assigned this value to file pointer fp, which we will use to access and
manipulate the file. If fopen() fails, it returns NULL. Therefore, the
complete call to fopen(), including error handling, is as follows:
fp = fopen("test.c", "r");
if (fp == NULL)
{
/* handle error here */
}
This opens the TEST.C file for reading (activity "r"). After the file
pointer returned by fopen() is assigned to fp, we test fp to see if it is
NULL. We test for an error here because it is possible that the file
TEST.C does not exist.
Table 10-1 Possible Modes (Activities) for fopen()
Mode Description
──────────────────────────────────────────────────────────────────────────
"r" Open for reading only. The file must already exist.
"w" Open for writing only. Creates the file if it does not exist.
"a" Open for appending (write-only, starting at the end of a file).
Creates the file if it does not exist.
"r+" Open for both reading and writing. The file must already exist.
"w+" Open for both reading and writing. Creates the file if it does
not already exist.
"a+" Open for both reading and writing, starting at the end of the
file. Creates the file if it does not exist.
──────────────────────────────────────────────────────────────────────────
Each open file requires its own file pointer. The following two open
files, for example, require two separate file pointers:
#include <stdio.h>
...
FILE *fp_in, *fp_out;
...
fp_in = fopen("test.txt", "r");
fp_out = fopen("test.bak", "w");
In this example, fp_in is the file pointer for the file opened for reading
(activity "r"), and fp_out is the file pointer for the file opened for
writing (activity "w").
──────────────────────────────────────────────────────────────────────────
File Access in BASIC and C
If you're used to BASIC file handling, you'll find that QuickC offers
fewer "built-in" conveniences but ultimately provides more power and
flexibility. In BASIC, you might open a random access file with the
following statement, which specifies the file identification number and
record length:
OPEN "C:\ACCT\TRANS" FOR RANDOM AS #1 LEN = 256
Before you can use the file, you have to use FIELD statements to associate
whatever numeric or string variables you are going to use with the
corresponding data fields in the file record. Because most versions of
BASIC don't have a data type similar to the C struct, you have to
manipulate numerous separate variables to move data to and from the file.
The built-in random access support does allow you to get a record by its
record number directly using the GET statement, however.
C has a different approach: A file can contain any valid C data type, such
as a struct, which already has its fields defined, so you don't have to
set up file data fields. On the other hand, file manipulation methods,
such as random access, are not built-in in C. You can achieve random
access, however, by converting a record number to an offset and then using
the library function fseek() to position C's file pointer to the correct
record. You can also use the fgetpos() and fsetpos() functions to
manipulate the file pointer.
Also, because C uses function calls rather than BASIC's procedural
commands to manipulate files, you can quickly check for errors by putting
the function call in an if statement.
──────────────────────────────────────────────────────────────────────────
Reading Characters with fgetc()
There's more to reading a file than merely opening the file to read. To
see what we mean, examine the STRINGS.C program (Listing 10-1), which
reads a file one character at a time and looks for strings of five or more
printable characters. The program takes a command-line argument, so before
you run it, you must create the argument using the Set Runtime Options
screen from the Run menu. In the Command Line box, type c:\qc\qc.exe (or
the name of any existing file). Figure 10-2 on the next page shows the
screen after you type the command.
──────────────────────────────────────────────────────────────────────────
/* strings.c -- opens a file and searches it for */
/* possible strings */
#include <stdio.h> /* for FILE, BUFSIZ, & EOF */
#include <ctype.h> /* for isprint() */
main(argc, argv)
int argc;
char *argv[];
{
FILE *fp;
char buf[BUFSIZ];
int ch, count;
if (argc != 2)
{
fprintf(stderr, "usage: strings file\n");
exit(1);
}
if ((fp = fopen(argv[1], "rb")) == NULL)
{
fprintf(stderr, "Can't open %s\n", argv[1]);
exit(1);
}
count = 0;
while ((ch = fgetc(fp)) != EOF)
{
if (! isprint(ch) || count >= (BUFSIZ - 1))
{
if (count > 5)
{
buf[count] = 0;
puts(buf);
}
count = 0;
continue;
}
buf[count++] = ch;
}
}
──────────────────────────────────────────────────────────────────────────
Listing 10-1. The STRINGS.C program.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 10-2 can be found on p.296 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 10-2. The Set Runtime Options dialog box lets you enter a command
line.
When we run STRINGS.C on a large file such as QC.EXE, the program prints
many screens of possible strings. For convenience, you might add a
"paging" feature to the program.
STRINGS.C uses the fgetc() function, a file-oriented version of the
getchar() routine we've used before. After it is passed a single argument
(a file pointer), the function returns the next character read from the
file pointed to. Assigning that character to a variable of type int lets
us detect EOF (End Of File) easily.
int ch;
if ((ch = fgetc(fp)) == EOF)
{
/* handle end of file here */
}
Notice that STRINGS.C calls fopen() with the activity argument "rb". This
is a PC-specific extension of the normal "open for reading" argument. The
b tells fopen() to open the file in binary mode but to do no character
translation for us──that is, to give fgetc() every byte from the file as
is. If we did not specify the b, fopen() would have opened the file in
text mode. Had we used text mode, however, our program would not have read
all of QC.EXE because the Ctrl-Z character, which is a legal byte in
binary files, would have marked the end of the file. Table 10-2 shows the
difference between these two modes.
Also notice that STRINGS.C ends without closing the file. C, unlike BASIC,
closes all open files when you exit the program. This is true whether you
exit main() with a return or from another function with an exit().
Table 10-2 Text vs Binary Modes for fopen()
──────────────────────────────────────────────────────────────────────────
t text mode Translates carriage return/linefeed combinations
into single linefeeds on input and makes the
reverse translation on output. Ctrl-Z marks the
end of the file.
b binary mode Suppresses the above translations. The operating
system keeps track of the file's length.
──────────────────────────────────────────────────────────────────────────
Closing Files with fclose()
Although MS-DOS lets you have as many as 20 simultaneously open files, you
might want to close each open file before you open another one. Closing a
file writes everything to disk, updates the directory entry for that file,
and frees a file pointer.
When you open files with fopen(), you can close them with fclose(), as
follows:
if (fclose(fp) == EOF)
{
/* unable to close file */
}
/* fp may be reused here */
If fclose() cannot close a file (because the floppy disk containing that
file was removed, for example), it returns EOF.
Line I/O with fgets() and fputs()
The standard C Library contains a pair of file-oriented routines called
fgets() ("file get string") and fputs() ("file put string"). They are
similar to the gets() and puts() pair we discussed in the last chapter:
fgets() reads lines of text from files and fputs() writes lines of text
into files. Use them as follows:
#include <stdio.h>
#define SIZE 512
...
FILE *fp_in, *fp_out;
char buf[SIZE];
...
/* open fp_in for reading and fp_out for writing */
...
if (fgets(buf, SIZE, fp_in) == NULL)
{
/* error reading or EOF */
}
/* a line of text is now in buf */
...
if (fputs(buf, fp_out) == EOF)
{
/* error writing */
}
The fgets() function takes three arguments: the address of a char buffer,
the maximum number of characters to read into that buffer, and a file
pointer to a file opened for reading. In the example, fgets() reads a
maximum of SIZE characters (up to and including the first newline
character) and appends a terminating '\0' to the characters to form a
string. The fputs() function requires two arguments: the address of a
zero-terminated string (in buf) and a file pointer to a file opened for
writing. In the example, fputs() writes the string in buf to fp_out──
including any newline in that string. Note as well that fputs() does not
add any newlines.
The fputs() and fgets() functions differ from their counterparts puts()
and gets(). Each handles the newline character in a different way, as
follows:
──────────────────────────────────────────────────────────────────────────
gets(buf) Reads characters from keyboard and places them into buf.
Replaces the trailing newline character ('\n') with a '\0'.
fgets(buf, Reads a maximum of len characters from a file opened for
len, fp) reading. Places len or fewer characters (up to and
including a newline) into buf. Retains the newline
character and adds a terminating '\0'.
puts(buf) Prints the string in buf to the screen and adds a newline
character to the output on the screen.
fputs(buf, fp) Prints (writes) the string in buf into the file (opened for
writing) pointed to by the file pointer fp. Does not add a
newline character to the output.
──────────────────────────────────────────────────────────────────────────
The CCOPY.C program (Listing 10-2) reads one file and writes to a second.
The "C" preceding "COPY" (in the program name) signals that this COPY
"crunches" its input──eliminating all empty lines, leading tabs, and
spaces. You could use this program to prepare files before sending them
over a slow modem.
──────────────────────────────────────────────────────────────────────────
/* ccopy.c -- copies a file, cutting blank lines and */
/* leading space from lines of copy */
#include <stdio.h> /* for FILE, BUFSIZ, NULL */
#include <ctype.h> /* for iswhite() */
main(argc, argv)
int argc;
char *argv[];
{
FILE *fp_in, *fp_out;
char buf[BUFSIZ];
char *cp;
if (argc != 3)
{
printf("usage: ccopy infile outfile\n");
exit(1);
}
if ((fp_in = fopen(argv[1], "r")) == NULL)
{
printf("Can't open %s for reading.\n", argv[1]);
exit(1);
}
if ((fp_out = fopen(argv[2], "w")) == NULL)
{
printf("Can't open %s for writing.\n", argv[2]);
exit(1);
}
printf("Copying and Crushing: %s->%s ...",
argv[1], argv[2]);
while (fgets(buf, BUFSIZ, fp_in) != NULL)
{
cp = buf;
if (*cp == '\n') /* blank line */
continue;
while (isspace(*cp))
{
++cp;
}
if (*cp == '\0') /* empty line */
continue;
if (fputs(cp, fp_out) == EOF)
{
printf("\nError writing %s.\n", argv[2]);
exit(1);
}
}
printf("Done\n");
}
──────────────────────────────────────────────────────────────────────────
Listing 10-2. The CCOPY.C program.
To run this program you need to set its command line from the Set Runtime
Options dialog box. The Command Line text box requires two filenames as
arguments──first, the file to read, and second, the file to write to. For
example, you might enter the filenames strings.c temp. The first name is
the existing text file to be read (note that the fopen() in CCOPY.C uses
"r" for text mode). The second name is the new file that will be created
(activity "w").
Within a loop, fgets() reads a line of text from the first file, the
program crunches that line, and fputs() writes the condensed line into the
second file. After you run CCOPY.C, choose DOS Shell from the File menu
and look at the newly created file using the TYPE command and Ctrl-S.
Error Detection with feof() and ferror()
Using fgets() has a drawback──it returns NULL for both EOF (which you
expect) and read errors (which you don't expect). However, you can
differentiate between the two by using feof() and ferror().
The feof() function tests a file opened for reading and associated with a
file pointer to see if the end of that file has been reached. It returns
true (nonzero) at the end of the file; otherwise it returns 0. The
ferror() function returns true if there is any error with the file──
including reaching the end of file. The following example shows how to use
them together to differentiate between the two conditions:
if (feof(fp_in))
{
/* reached end of file while reading */
}
else if (ferror(fp_in))
{
/* some read error has occurred */
}
EOF is meaningful only when reading; use ferror() alone when writing to a
file:
if (ferror(fp_out))
{
/* some write error has occurred */
}
Always include error-checking routines in your programs to protect
yourself from careless users. Users sometimes remove floppy disks while
the drive light is on or try writing to disks that are write-protected.
Error detection lets you either take corrective action or notify users of
their mistakes.
Block I/O with fread() and fwrite()
So far we've treated files as lines of text. However, you will often want
to read and write files in specific blocks whose size is measured in
bytes. (Executable program files and data files, for example, generally
contain no meaningful lines of text.) To do this, the standard C Library
provides a pair of routines called fread() and fwrite(). Their forms are
nearly identical:
fread(buffer, size, count, fp_in);
│ │ │ └──────────────────────── A file pointer
│ │ └────────────────────────── How many size items
│ └──────────────────────────── How many bytes per item
└───────────────────── Address of (size * count) bytes buffer
fwrite(buffer, size, count, fp_out);
│ │ │ └──────────────────────── A file pointer
│ │ └────────────────────────── How many size items
│ └──────────────────────────── How many bytes per item
└───────────────────── Address of (size * count) bytes buffer
Both routines require that you specify #include <stdio.h> to define FILE
for the file pointer and to define the new type size_t for the variables
size and count:
size_t size;
size_t count;
FILE *fp;
QuickC defines the type size_t in the <stdio.h> header file as an unsigned
long. Because it might be defined differently with other compilers, you
should use size_t for portability.
Both functions return the number of bytes actually read or written. When
that number is less than size times count, an error has occurred. In the
case of fread(), however, that error can also indicate that you've reached
the end of the file. Therefore, you need to use feof() to distinguish end
of file from other errors.
The UPPITY.C program (Listing 10-3) shows one way to use fread() and
fwrite(). It reads an entire file into memory (using malloc() to obtain
that memory), converts it to uppercase, then writes the entire file to a
new file having the .UP extension.
──────────────────────────────────────────────────────────────────────────
/* uppity.c -- makes an uppercase copy of a file using */
/* fread() and fwrite() */
#include <string.h> /* for strrchr() */
#include <stdio.h> /* for NULL */
#include <malloc.h> /* for malloc() */
#include <ctype.h> /* for isupper() */
#define HUNK 512
main(argc, argv)
int argc;
char *argv[];
{
char *cp, newname[128], *np;
FILE *fp;
int hunks = 0, bytes = 0, totbytes = 0;
int i;
if (argc != 2)
{
printf("usage: uppity file\n");
exit(1);
}
if ((fp = fopen(argv[1], "rb")) == NULL)
{
printf("\"%s\": Can't open.\n", argv[1]);
exit(1);
}
if ((cp = malloc(HUNK)) == NULL)
{
printf("Malloc Failed.\n");
exit(1);
}
while ((bytes = fread(cp + (HUNK * hunks), 1, HUNK, fp)) == HUNK)
{
totbytes += bytes;
++hunks;
if ((cp = realloc(cp, HUNK + (HUNK * hunks))) == NULL)
{
printf("Realloc Failed.\n");
exit(1);
}
}
if (bytes < 0)
{
printf("\"%s\": Error Reading.\n", argv[1]);
exit(1);
}
totbytes += bytes;
for (i = 0; i < totbytes; ++i)
if (islower(cp[i]))
cp[i] = toupper(cp[i]);
(void)fclose(fp);
if ((np = strchr(argv[1], '.')) != NULL)
*np = '\0';
strcpy(newname, argv[1]);
strcat(newname, ".up");
if ((fp = fopen(newname, "wb")) == NULL)
{
printf("\"%s\": Can't open.\n", argv[1]);
exit(1);
}
if (fwrite(cp, 1, totbytes, fp) != totbytes)
{
printf("\"%s\": Error writing.\n", argv[1]);
exit(1);
}
}
──────────────────────────────────────────────────────────────────────────
Listing 10-3. The UPPITY.C program.
UPPITY.C continually reallocates memory for each HUNK (512 bytes) of the
file read in. A more direct approach would find the size of the file, then
read in that many bytes with a single fread(). You can do this with the
stat() function. Unfortunately, to use stat() you must understand
"structures," and we won't be describing those until the next chapter.
Keep in mind that you might want to modify UPPITY.C when you learn how to
use structures.
Predeclared File Pointers
When you run any QuickC program, five file pointers are always provided
for five preopened files. Those file pointers are stdin, stdout, stderr,
stdaux, and stdprn. (See Table 10-3.) Because these preopened file
pointers are defined in stdio.h, you must include that header file if you
want to use them.
To demonstrate the use of these file pointers, we revised CCOPY.C (the
"crunch-and-copy program") to produce the CCOPY2.C program (Listing
10-4). This revision checks for the presence of a second (output)
filename. If it is missing, fputs() directs the output to stdout (your
screen).
Table 10-3 QuickC's Preopened File Pointers
──────────────────────────────────────────────────────────────────────────
stdin The standard input. Your keyboard viewed as a file. Also, input
to your program provided by redirection using <file from the
MS-DOS command line.
stdout The standard output. Your screen viewed as a file. Also, output
to disk files provided by redirection using >file from the
MS-DOS command line.
stderr The standard error output. Always your screen. This file
pointer is unaffected by redirection from the MS-DOS command
line.
stdaux The standard auxiliary. Usually your serial port or COM1. This
file pointer provides easy access to your modem.
stdprn The standard printer output. Usually your parallel port or PRN.
This file pointer provides an easy way to generate hard copy
from within a QuickC program.
──────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────
/* ccopy2.c -- copies a file, cutting blank lines and */
/* leading space from lines of copy */
/* Modified to demonstrate stdout and stderr */
#include <stdio.h> /* for FILE, BUFSIZ, NULL */
#include <ctype.h> /* for iswhite() */
main(argc, argv)
int argc;
char *argv[];
{
FILE *fp_in, *fp_out;
char buf[BUFSIZ];
char *cp;
if (argc < 2)
{
fprintf(stderr, "usage: ccopy2 infile {outfile}\n");
exit(1);
}
if ((fp_in = fopen(argv[1], "r")) == NULL)
{
fprintf(stderr, "\"%s\": Can't open.\n", argv[1]);
exit(1);
}
if (argc == 3)
{
if ((fp_out = fopen(argv[2], "w")) == NULL)
{
fprintf(stderr, "\"%s\": Can't open.\n", argv[2]);
exit(1);
}
}
else
fp_out = stdout;
while (fgets(buf, BUFSIZ, fp_in) != NULL)
{
cp = buf;
if (*cp == '\n') /* blank line */
continue;
while (isspace(*cp))
{
++cp;
}
if (*cp == '\0') /* empty line */
continue;
if (fputs(cp, fp_out) == EOF)
{
fprintf(stderr, "Error writing.\n");
exit(1);
}
}
if (! feof(fp_in)) /* error reading? */
{
fprintf(stderr, "\"%s\": Error reading.\n", argv[1]);
exit(1);
}
}
──────────────────────────────────────────────────────────────────────────
Listing 10-4. The CCOPY2.C program.
Formatted File I/O with fprintf() and fscanf()
CCOPY2.C didn't print error messages with printf(); instead, it used the
file-oriented version of printf(), called fprintf(), to send error
messages to stderr, which is always your screen. This ensures that you
will always see error messages, even when the program is printing its
output to a file.
C's file-oriented counterparts to printf() and scanf() are called
fprintf() and fscanf(). They are identical to their nonfile brethren, with
one exception: Each requires a file pointer as its first argument, as
follows:
fprintf(fp_out, control, args ...);
└─┬──┘ └───────┬────────┘
│ └─────────────────────────── Same as printf()
└─────────────────────────────────────────── A file pointer
fscanf(fp_in, control, addresses ...);
└─┬─┘ └─────────┬───────────┘
│ └──────────────────────────── Same as scanf()
└──────────────────────────────────────────── A file pointer
Random Access with fseek()
Sophisticated applications, such as databases, must be able to move around
in files (recall that files are continuous streams of bytes) reading and
writing selected portions. The fseek() function lets a program access any
file element by determining the position of the next read or write in a
file, as follows:
fseek(fp, offset, origin)
│ │ │ └───────────────────────────────────── From where
└──┬──┘ └───────────────────────────────── How far to reposition
└───────────────────────────────────────────────── File pointer
The offset, in bytes, tells fseek() how far to move in the file, and it
must be type long. The origin determines where to measure offset from; it
can be any one of three values──the beginning, current position, or end of
the file. (See Table 10-4.)
If fseek() cannot reposition in a file, it returns the value -1L (the L is
needed because fseek() returns the type long). If fseek() is successful,
it returns the new position in the file, measured in bytes from the
beginning of the file.
Table 10-4 origin Positions for fseek()
──────────────────────────────────────────────────────────────────────────
SEEK_SET From the beginning of the file; offset must always be
positive.
SEEK_CUR Relative to the current position. A negative offset moves
toward the beginning of the file; a positive offset moves
toward the end of the file. (You can move beyond the end of
the file, thus enlarging the file.)
SEEK_END From the end of the file; offset can be positive or
negative. Movement is the same as SEEK_CUR, but relative to
the end of the file.
──────────────────────────────────────────────────────────────────────────
The PHONE.C program (Listing 10-5 on the following page) is a miniature
telephone number database. When run without command-line arguments, it
asks you for numbers to add to its database file. Run with a command-line
argument, it searches for an entry that matches the argument and prints
the data it finds.
──────────────────────────────────────────────────────────────────────────
/* phone.c -- a telephone number mini-database that */
/* demonstrates fseek() */
#include <stdio.h> /* for FILE, BUFSIZ, NULL */
#define MAXL (128)
char Name[MAXL];
char Number[MAXL];
char File[] = "C:\\TMP\\PHONE.DB";
int Count;
FILE *Fp;
int Distance = (MAXL * MAXL);
main(argc, argv)
int argc;
char *argv[];
{
if (argc == 1)
Ask();
else
Find(argv[1]);
}
Find(char *str)
{
int i;
if ((Fp = fopen(File, "r")) == NULL)
{
fprintf(stderr, "\"%s\": Can't Read\n", File);
exit (1);
}
if (fread(&Count, 1, sizeof(int), Fp) != sizeof(int))
{
fprintf(stderr, "\"%s\": Error Reading\n", File);
exit (1);
}
for (i = 0; i < Count; i++)
{
fread(Name, 1, MAXL, Fp);
fread(Number, 1, MAXL, Fp);
if (ferror(Fp))
{
fprintf(stderr, "\"%s\": Error Reading.\n", File);
exit (1);
}
if (strcmp(*str, *Name) == 0)
{
printf("Name: %s\n", Name);
printf("Number: %s\n", Number);
return;
}
}
fprintf(stderr, "\"%s\": Not in database.\n", str);
return;
}
Ask()
{
if ((Fp = fopen(File, "r+")) == NULL)
Make();
else if (fread(&Count, 1, sizeof(int), Fp) != sizeof(int))
{
fprintf(stderr, "\"%s\": Error Reading\n", File);
exit (1);
}
printf("Name: ");
if (gets(Name) == NULL || *Name == '\0')
return;
printf("Number: ");
if (gets(Number) == NULL || *Number == '\0')
return;
if (fseek(Fp, (long)(Distance * Count), SEEK_CUR) != 0)
{
fprintf(stderr, "\"%s\": Error Seeking.\n", File);
exit (1);
}
fwrite(Name, 1, MAXL, Fp);
fwrite(Number, 1, MAXL, Fp);
if (ferror(Fp))
{
fprintf(stderr, "\"%s\": Error Writing.\n", File);
exit (1);
}
if (fseek(Fp, 0L, SEEK_SET) != 0)
{
fprintf(stderr, "\"%s\": Error Seeking.\n", File);
exit (1);
}
++Count;
if (fwrite(&Count, 1, sizeof(int), Fp) != sizeof(int))
{
fprintf(stderr, "\"%s\": Error Writing\n", File);
exit (1);
}
return;
}
Make()
{
if ((Fp = fopen(File, "w+")) == NULL)
{
fprintf(stderr, "\"%s\": Can't Create\n", File);
exit (1);
}
Count = 0;
if (fwrite(&Count, 1, sizeof(int), Fp) != sizeof(int))
{
fprintf(stderr, "\"%s\": Error Creating\n", File);
exit (1);
}
}
──────────────────────────────────────────────────────────────────────────
Listing 10-5. The PHONE.C program.
The PHONE.C program might seem more complex than it really is. We included
many error-checking routines to prevent the user from making careless
errors. Note how the program checks the first character of each input line
for a zero character (*Name == '\0'). This shows that the user pressed
Enter without typing any information.
Moving with the rewind() Function
Moving to the beginning of a file (rewinding) is so common in C programs
that the standard C Library includes a special function to perform that
task. Called rewind(), it takes a single argument──a file pointer for the
opened file──and moves the position of the next read or write to the
beginning of the file. Consider the following:
rewind(fp);
rewind() returns no value and therefore gives no indication of failure.
Other than this difference, however, the above rewind() is identical to
the following fseek():
fseek( fp, OL, SEEK_SET)
│ └────────────────────── Move from beginning of file
└────────────────────────────────── Offset must be a long
Determining Position in a File with ftell()
Moving through a file with fseek() often requires that you first know your
current position in the file. When you pass the ftell() function a file
pointer, it returns your present position in that file. That position, a
long value, is the measure in bytes from the beginning of the file.
Consider the following:
if ((pos = ftell(fp)) == -1L)
{
/* can't find position */
}
/* current position is pos bytes from beginning */
Used in that way, ftell() is identical to the following fseek() call:
if ((pos = fseek(fp, 0L, SEEK_CUR)) == -1L)
{
/* can't find position */
}
/* current position is pos bytes from beginning */
As you progress in learning C, you will find need for functions that we
have not covered in our discussions. For a complete summary of top-level
(stream) I/O routines, refer to Section 4.8 of the Microsoft QuickC
Run-Time Library Reference.
Mid-level (Unbuffered) File I/O
Most of the top-level (buffered) stream file input/output functions have
mid-level, unbuffered counterparts that permit direct access to disk
files. Because they do not buffer data, they are frequently faster and
more efficient, often allowing disk files to be read directly into a
program's memory. (The top-level fread() function, for example, actually
calls the mid-level read() to do its work.)
One disadvantage of the unbuffered routines is that they offer only the
most basic of services. Although these routines offer a read() and a
write(), there are no corresponding mid-level versions of fgets(),
fputs(), fscanf(), fprintf(), or fgetc(). Another disadvantage is that you
cannot use unbuffered functions in the same program that uses calls to
top-level functions. If you mix them, as shown in Figure 10-3, you risk
losing synchronization of data. That is, if you first call fgetc(), then
call read(), the read() will not begin with the next byte following the
fgetc(). The fgetc() reads and buffers 512 bytes from the file, then it
returns the first one of those buffered bytes. The call to read(),
however, reads a single byte directly from the disk.
512-byte buffer
┌─┌────────────────────┐
│ │ Now is the │ 'N'
│ │ time... ├──────────┐
fgetc() reads and buffers │ └───────────────────┘ │
512 bytes at a time ───────┤ │ │
│ ┌─────────┴──────────┐ │
┌────────────────┐ │ │ Top-level │ ┌─▼─────────────
│ ├──────┼►│ fgetc() │ │
│ File: "Now is │ └─└────────────────────┘ │ Your
│ the time..." │ │ program
│ │ ┌────────────────────┐ │
│ ├───────►│ Mid-level │ └──────────────
└────────────────┘ │ read() ├──────────┘
└────────────────────┘ 'N'
│
read() reads ─────┘
only one byte
Figure 10-3. Data synchronization is lost when you mix buffered with
unbuffered file I/O routines.
Opening a File with open()
Unlike fopen(), open() returns its identifying value as a simple integer.
This value, called a "file descriptor," is later passed to all other
mid-level routines. To use the open() function, you must specify #include
<fcntl.h> (not <stdio.h>, as you would with fopen()), as follows:
#include <fcntl.h>
...
int fd;──────────────────────────────────────────────The file descriptor
...
if ((fd = open(filename, oflag)) < 0)
{
/* handle error here */
}
The file descriptor, fd, is type int. The first argument, filename, is a
string or the address of a string, and the second, oflag, is an int that
supplies open() with a file activity (read, write, append, create) and a
file mode (text or binary). The values for oflag are defined in fcntl.h,
and their meanings are listed in Table 10-5. Note that you can combine
oflag values by using the bitwise OR operator (|). For example, the
following declaration opens the file TEST.EXE for reading in binary mode:
fd = open("TEST.EXE", O_BINARY | O_RDONLY)
│ │ └────────────── For reading only
│ └─── Combined with bitwise OR operator
└────────────────────── Open in binary mode
If it fails, open() returns a negative integer value. Thus, all file
descriptor values are greater than or equal to zero.
Table 10-5 Values for oflag Declared in fcntl.h
Value Description
──────────────────────────────────────────────────────────────────────────
O_RDONLY Accesses as read-only.
O_RDWR Accesses as read-write.
O_WRONLY Accesses as write-only.
O_BINARY Sets mode for a binary file.
O_TEXT Sets mode for a text file.
O_APPEND Opens for appending.
O_CREAT Creates file if it doesn't exist.
O_EXEL Returns error if file already exists.
O_TRUNC Truncates existing file to zero
length.
──────────────────────────────────────────────────────────────────────────
Of the possible values for oflag, you must use one of the first three
activities in Table 10-5, (O_RDONLY, O_RDRW, or O_WRONLY); all others in
the table are optional and can be added using bitwise OR. Unless specified
otherwise, the mode is set for a text file; reads and writes begin at the
start of the file; the file is not created if it doesn't exist; and the
file is not truncated.
If you combine the O_CREAT value with O_RDWR or O_WRONLY to create a file,
open() requires a third argument called pmode ("permissions" mode). Use
the argument as follows:
fd = open(filename, oflag, pmode);
│ └The permissions of the newly created file
└──────────────────(O_RDWR or O_WRONLY|O_CREAT)
The possible values for pmode, listed in Table 10-6, determine whether
the created file will be a read-only file, a write-only file, or a
readable and writable file. (You must combine the two defined pmode values
with a bitwise OR operator to create a readable and writable file.)
Because pmode values are defined in sys\stat.h>, you must specify the
following include files to create a file with open():
#include <fcntl.h>──────────────────────────────────────For oflag values
#include <sys\types.h>────────────────────────────────────────For stat.h
#include <sys\stat.h>───────────────────────────────────For pmode values
Note that #include <sys\types.h> must always precede #include <sys\stat.h>
because the first contains the definitions needed by the second.
With MS-DOS, you cannot create a file that is write-only. Because all
files are always readable, you can omit the S_IREAD value for pmode. (If
you do use the value, MS-DOS ignores it.)
The following example is a complete call to open(), including all the
#include directives:
#include <fcntl.h>
#include <sys\types.h>
#include <sys\stat.h>
int fd;
fd = open("TEST.EXE", O_RDWR|O_BINARY|O_CREAT|O_TRUNC,S_IREAD|S_IWRITE);
This example opens a file named TEST.EXE in binary mode for reading and
writing. It creates the file if it doesn't exist, and truncates it if it
does.
Table 10-6 Values for pmode from <sys\stat.h>
──────────────────────────────────────────────────────────────────────────
S_IWRITE Creates a writable file.
S_IREAD Creates a readable file.
──────────────────────────────────────────────────────────────────────────
Closing a File with close()
Just as fclose() closes a file based on a file pointer, the unbuffered
close() library function closes a file based on a file descriptor, as
follows:
if (close(fd) != 0)
{
/* handle error closing here */
}
A successfully executed close() returns a zero value; any nonzero return
value indicates an error.
When your program exits, QuickC closes all files opened with the mid-level
open(). Because you can have only 20 files open at one time, you should
close files inside your program. Closing a file with close() frees that
file's file descriptor for reuse.
Writing to a File with write()
The write() function is used to write to files. It is simpler to use than
the top-level fwrite() function because it requires only three arguments,
as in the following:
write(fd_out, buf, bytes)
└──┬─┘ │ └────────────────────── Number of bytes to write
│ └───────────────────── Where to write those bytes from
└───────────── File descriptor for a file opened for writing
The expression buf is the address in memory of the first byte that you
want to write to the file. That address can be any address expression, but
it is usually the address of an array. The final argument, bytes,
represents the number of characters you want to write to the file.
The write() function normally returns the number of bytes written (the
same value as bytes). If write() fails, however, it returns a smaller or
negative number.
The SCRSAVE.C program (Listing 10-6) demonstrates one way to use open()
and write(). It copies the contents of the text screen into a local
buffer, which is then written to a disk file. (The program will not
overwrite an existing file.)
──────────────────────────────────────────────────────────────────────────
/* scrsave.c -- demonstrates write() by saving the */
/* text screen to a file */
#include <stdio.h> /* for stderr */
#include <fcntl.h> /* for O_CREAT | O_BINARY */
#include <sys\types.h> /* for stat.h */
#include <sys\stat.h> /* for S_IREAD | S_IWRITE */
#define SCRCHARS (25 * 80)
int Buf[SCRCHARS];
main(argc, argv)
int argc;
char *argv[];
{
int *cp, *ep, fname[16];
int far *sp;
int fd_out, bytes;
if (argc != 2)
{
fprintf(stderr, "usage: scrsave file\n");
exit(0);
}
if (strlen(argv[1]) > 8)
{
fprintf(stderr, "\"%s\": Filename too long.\n", argv[1]);
exit(1);
}
strcpy(fname, argv[1]);
strcat(fname, ".SCR");
if (access(fname, 0) == 0)
{
fprintf(stderr, "\"%s\": Won't overwrite.\n", fname);
exit(1);
}
if ((fd_out = open(fname, O_WRONLY | O_CREAT | O_BINARY,
S_IREAD | S_IWRITE)) < 0)
{
fprintf(stderr, "\"%s\": Can't create.\n", fname);
exit(1);
}
/* Copy the screen into a near buffer. */
ep = &Buf[SCRCHARS - 1];
cp = Buf;
/* use 0xB8000000 for EGA or VGA */
sp = (int far *)(0xB0000000);
for (; cp < ep; ++cp, ++sp)
*cp = *sp;
/* Write it. */
bytes = write(fd_out, Buf, SCRCHARS * 2);
if (bytes != SCRCHARS * 2)
{
fprintf(stderr, "\"%s\": Error writing.\n", fname);
exit(1);
}
}
──────────────────────────────────────────────────────────────────────────
Listing 10-6. The SCRSAVE.C program.
Note that we copy the screen rather than write it directly because write()
expects a normal pointer, whereas accessing the screen requires a far
pointer.
Reading a File with read()
Use the read() function to read from files. It is a simpler function to
use than fread() because it takes only three arguments, as follows:
read(fd_in, buf, bytes)
│ │ └───────────────────────── Number of bytes to read
│ └──────────────────────────── Where to place those bytes
└─────────────── File descriptor for a file opened for reading
In this example, buf is either an array or the address of allocated
memory. Be sure it is large enough to hold the number of bytes specified
by the argument bytes, however, because the compiler does not check this
for you.
If the call to read() is successful, it returns the same value as bytes.
If it returns a smaller value, then that value represents the number of
bytes left in the file. A zero return value signifies the end of the file,
and a -1 return value shows that a read error occurred.
The SCRREST.C program (Listing 10-7) reads a file, copying as much as a
screenful of what it reads to text-screen memory. It works with any file
type, but reading files created with SCRSAVE.C (Listing 10-6) is its most
useful application. Before you run the program, pull down the Debug menu
and activate Screen Swapping On.
──────────────────────────────────────────────────────────────────────────
/* scrrest.c -- demonstrates read() by restoring */
/* text screen from any file */
#include <stdio.h> /* for stderr */
#include <fcntl.h> /* for O_RDONLY | O_BINARY */
#define SCRCHARS (25 * 80)
int Buf[SCRCHARS];
main(argc, argv)
int argc;
char *argv[];
{
int *cp, *ep;
int far *sp;
int fd_in, bytes;
if (argc != 2)
{
fprintf(stderr, "usage: scrrest file.scr\n");
exit(0);
}
if ((fd_in = open(argv[1], O_RDONLY | O_BINARY)) < 0)
{
fprintf(stderr, "\"%s\": Can't open to read.\n", argv[1]);
exit(1);
}
/* Read it. */
bytes = read(fd_in, Buf, SCRCHARS * 2);
if (bytes < 0)
{
fprintf(stderr, "\"%s\": Error Reading.\n", argv[1]);
exit(1);
}
if (bytes == 0)
{
fprintf(stderr, "\"%s\": Empty File.\n", argv[1]);
exit(1);
}
/* Copy the buffer to screen memory. */
ep = &Buf[bytes / 2];
cp = Buf;
/* use 0xB8000000 for EGA or VGA */
sp = (int far *)(0xB0000000);
for (; cp < ep; ++cp, ++sp)
*sp = *cp;
}
──────────────────────────────────────────────────────────────────────────
Listing 10-7. The SCRREST.C program.
Positioning with lseek()
The unbuffered lseek() function lets a program position its next read or
write to begin anywhere in a file. Almost identical to the buffered
fseek(), lseek() takes a file descriptor as its first argument, rather
than a file pointer. Therefore, use the lseek() function as follows:
#include <io.h> /* defines lseek() */
#include <stdio.h> /* for origin, etc. */
long newpos, offset = 100L;
int fd;
newpos = lseek(fd, offset, origin);
│ │ │ └─── From where (current, begin, or end)
│ │ └───────────────── Move this many bytes forward
│ └────────────────────── In this file (file descriptor)
└───────────────────────────────────────────── New position in file
In this example, fd is a file descriptor for a file previously opened with
open(). The second argument, offset, is the number of bytes to move in the
file and must be of the type long. If offset is negative, you move toward
the beginning of the file. The last argument, origin, can be one of the
three possible definitions that specify where the move begins. These
definitions are the same as those used by fseek(), which were mentioned in
Table 10-4 on p. 305. Also, as with fseek(), you must specify #include
<stdio.h> to access those definitions.
After a successful repositioning, lseek() returns the new position in the
file. A return value of -1L indicates an error. (Note that lseek() returns
the type long.)
The VIEW.C program (Listing 10-8) is a simple file-viewing program that
illustrates how to use lseek() to move through a file. Pressing + moves
you forward in the file, pressing - moves you backward, and typing q or Q
ends the program.
──────────────────────────────────────────────────────────────────────────
/* view.c -- demonstrates lseek() by displaying */
/* a file and moving around in it */
#include <fcntl.h> /* for open() */
#include <stdio.h> /* for SEEK_CUR, etc. */
#define HUNK 512
#define MOVE 512L
main(argc, argv)
int argc;
char *argv[];
{
char ch, buf[HUNK];
long position = 0L;
int bytes, eofflag = 0, fd_in;
if (argc != 2)
{
fprintf(stderr, "Usage: view file\n");
exit(0);
}
if ((fd_in = open(argv[1], O_RDONLY)) < 0)
{
fprintf(stderr, "\"%s\": Can't open.\n", argv[1]);
exit(1);
}
for (;;)
{
bytes = read(fd_in, buf, HUNK);
if (bytes == 0)
{
if (! eofflag)
{
fprintf(stderr, "\n<<at end of file>>\n");
++eofflag;
}
else
exit(0);
}
else if (bytes < 0)
{
fprintf(stderr, "\"%s\": Error Reading.\n", argv[1]);
exit(1);
}
else
{
eofflag = 0;
position = lseek(fd_in, 0L, SEEK_CUR);
if (position == -1L)
{
fprintf(stderr, "\"%s\": Error Seeking.\n", argv[1]);
exit(1);
}
Print(buf, bytes);
do
{
ch = getch();
if (ch == 'q' || ch == 'Q')
exit(0);
} while (ch != '+' && ch != '-');
if (ch == '-')
{
position = lseek(fd_in, -2 * MOVE, SEEK_CUR);
if (position == -1L)
{
fprintf(stderr, "\"%s\": Error Seeking.\n", argv[1]);
exit(1);
}
}
}
}
}
Print(char *buf, int cnt)
{
int i;
for (i = 0; i < cnt; ++i, ++buf)
{
if (*buf < ' ' && *buf != '\n' && *buf != '\t')
printf("^%c", *buf + '@');
else
putchar(*buf);
}
}
──────────────────────────────────────────────────────────────────────────
Listing 10-8. The VIEW.C program.
Finding Current Position with tell()
Notice that VIEW.C finds the current position in the viewed file with the
following:
position = lseek(fd_in, 0L, SEEK_CUR);
Because the need to know the current position is so common, the QuickC
library provides the tell() function. Similar to the top-level ftell()
routine, tell() takes a single argument, a file descriptor, and returns
the current position in the file associated with that file descriptor.
That position is a type long measure in bytes from the beginning of the
file. If tell() fails for any reason, it returns a value of -1L.
The File System
Not only do programs read and write to files, they often need to manage
the file system as a whole. By the file system, we mean the MS-DOS
directory hierarchy, the organization of directories, and the naming of
directories and files. For example, your program might need to create or
remove a directory or file, or relocate in the directory hierarchy (change
the working directory), or create unique temporary filenames. In this
section we discuss the file system and the C Library routines that let you
manipulate it. We also warn you of possible pitfalls and present a few
routines that let you handle errors gracefully.
Directories
MS-DOS does not permit you to use fopen() or open() to open a directory.
You can, however, create and remove directories or establish any directory
as your current working directory. The routines for handling directories
are listed in Table 10-7. All of the routines require that you first
specify #include <direct.h>, which contains their declarations.
The directory-handling functions chdir(), mkdir(), and rmdir() take a
single argument──a string or the address of a string that specifies a full
pathname (such as C:\TMP\JUNKDIR), or a directory name relative to the
current working directory (such as JUNKDIR). All three return an integer 0
if they are successful; otherwise, they return a -1. Consider, for
example, the code fragment on the next page.
Table 10-7 The Directory-Handling Library Functions
──────────────────────────────────────────────────────────────────────────
chdir(path) Changes the current working directory to path. Returns 0 if
successful.
mkdir(path) Creates a new directory named path. Returns 0 if
successful.
rmdir(path) Removes the directory whose name is path. Returns 0 if
successful.
getcwd(buf, n) Places the full pathname of your current working directory
into the char buffer buf of length n. Returns NULL if an
error occurs.
──────────────────────────────────────────────────────────────────────────
#include <direct.h>
if (chdir("C:\\TMP") != 0)
{
/* chdir failed, so exit */
}
if (mkdir("JUNKDIR") != 0)
{
/* mkdir failed, so exit */
}
if (rmdir("JUNKDIR") != 0)
{
/* rmdir failed, so exit */
}
The #include <direct.h> directive provides definitions for the three
routines that follow it. The chdir() function changes the current working
directory to C:\TMP. (Note that in C you must use a double backslash to
produce a single backslash.) Next, inside C:\TMP, the program uses mkdir()
to create a new subdirectory called JUNKDIR. The final call to rmdir()
removes that same subdirectory.
The last routine in Table 10-7, getcwd() ("get current working
directory"), takes two arguments and returns the address of a string. You
can call this function using one of two forms. In the following form:
#include <direct.h>
#include <stdio.h> /* for NULL */
char buf[512];
if (getcwd(buf, 512) == NULL)
{
/* couldn't get current working directory */
}
The getcwd() function is passed the address of a char buffer, buf, into
which it places the name of the current working directory. The length 512
is the number of bytes in the buffer. (Remember that the buffer must be
large enough for both the name and a terminating '\0'.)
The second form for calling getcwd() is as follows:
#include <direct.h>
#include <stdio.h> /* for NULL */
char *name;
if ((name = getcwd(NULL, 0)) == NULL)
{
/* couldn't get current working directory */
}
This form passes getcwd(), the special zero address NULL, and a length of
zero. This causes getcwd to use malloc() to allocate enough space for the
name of the current working directory name (plus 1 for the terminating
'\0'), to copy that name into the newly allocated space, and to return the
address of that space. Both forms of getcwd() return NULL if the operation
fails.
The DIRX.C program (Listing 10-9) demonstrates all four of the
directory-handling subroutines. It first creates a subdirectory in the
current directory, then relocates to that subdirectory and creates a
sub-subdirectory. Finally, it returns to the original directory and
attempts to remove the first subdirectory it created. It fails at this
point because it is illegal to remove a subdirectory that is not empty. If
you run the program again, it will fail immediately──it cannot execute the
first mkdir() because a directory with that name already exists.
──────────────────────────────────────────────────────────────────────────
/* dirx.c -- directory examples */
#include <direct.h>
#include <stdio.h>
#define SUBDIR "SUBDIR"
#define SUBSUBDIR "SUBSUB"
main()
{
char *current_dir;
void Err();
if ((current_dir = getcwd(NULL, 0)) == NULL)
Err("getcwd()", "Can't get current directory.");
if (mkdir(SUBDIR) != 0)
Err(SUBSUBDIR, "Can't make directory.");
if (chdir(SUBDIR) != 0)
Err(SUBDIR, "Can't cd into directory.");
if (mkdir(SUBSUBDIR) != 0)
Err(SUBSUBDIR, "Can't make directory.");
if (chdir(current_dir) != 0)
Err(SUBDIR, "Can't cd back to.");
if (rmdir(SUBDIR) != 0)
Err(SUBDIR, "Can't remove directory.");
}
void Err(char *what, char *msg)
{
fprintf(stderr, "\"%s\": %s\n", what, msg);
exit (1);
}
──────────────────────────────────────────────────────────────────────────
Listing 10-9. The DIRX.C program.
Manipulating Files by Name
Several standard C Library routines make it easy for you to remove and
rename files and also to create unique filenames from within a program.
These routines (listed in Table 10-8) are useful in databases, compilers,
games, and any other program that needs to manipulate files.
The routines unlink() and remove() are identical. Each takes a single
argument──the address of a string──and erases (removes from the disk) the
file whose name is specified in that string. The filename that you specify
can either be a full path such as C:\TMP\JUNK, which removes the file JUNK
from the directory C:\TMP, or it can be a relative pathname such as JUNK,
in which case the called routine removes the file JUNK from the current
working directory.
The rename() function can do more than merely rename files. It can rename
directories and move files from one directory to another (but not from one
disk to another). Consider the following example, in which JUNK is a file
and DIR1 and DIR2 are directories:
rename("JUNK", "OLDJUNK");─────────────────────────────────Rename a file
rename("DIR1\\JUNK", "DIR2\\JUNK");──────────────────────────Move a file
rename("DIR1", "OLDDIR1");────────────────────────────Rename a directory
The first line renames the file JUNK in the current working directory as
OLDJUNK; the second line moves the file JUNK in the subdirectory DIR1 into
the subdirectory DIR2. Note that you could have renamed JUNK during the
move. Also remember that, in C, you must use two backslashes to produce a
single backslash.
The third line of the above example renames the directory DIR1 as OLDDIR1.
It is important to note that directories, unlike files, cannot be moved.
Table 10-8 Routines That Manipulate Files by Name
──────────────────────────────────────────────────────────────────────────
unlink(path) Removes (erases) the file whose name is specified by path.
Returns 0 if the call is successful.
remove(path) Same as unlink().
rename(old, Renames the file old, giving it the new name new. Also
new) allows the renaming of directories. Files can be moved with
this routine. Returns 0 if successful.
mktemp(tmplt) Fills out the template tmplt with a filename that does not
already exist.
──────────────────────────────────────────────────────────────────────────
The mktemp() function generates a unique filename that is guaranteed not
to exist on your disk. Use it as follows:
#include <io.h> /* defines mktemp() */
#include <stdio.h> /* for NULL */
static char template[] = "C:\\TMP\\XXXXXX";
if (mktemp(template) == NULL)
{
/* No unique name possible */
}
First we specify #include <io.h> for the definition of mktemp(). In that
header file, mktemp() is defined as returning the address of a string
(that is, char *). We also must use #include <stdio.h> to define NULL,
which mktemp() returns if it fails.
The template passed to mktemp() must take the form baseXXXXXX; that is, it
may be any prefix, path, or part of a filename, ending with six X
characters. The mktemp() function replaces the X characters with one
alphanumeric character followed by five digits, thus forming a unique name
(one that does not already exist on the disk), such as A00000.
The FMENU.C program (Listing 10-10) uses all of these routines within a
small file-handling menu program. It enables you to rename/move a file or
directory, remove a file, or create a unique file. You can use FMENU.C as
the core of your own programs that let the user control files without
exiting to MS-DOS. Before you compile and run FMENU.C, follow the steps
outlined in the box on page 325, "Making New Library Routines Available
to QuickC."
──────────────────────────────────────────────────────────────────────────
/* fmenu.c -- demonstrates file renaming, etc. */
#include <direct.h>
#include <stdio.h>
#include <string.h>
#define MAXPATH (80)
char From_name[MAXPATH],
To_name[MAXPATH];
int Input(char *prompt, char buf[])
{
printf("%s: ", prompt);
if (gets(buf) == NULL || *buf == '\0')
return (0);
return (1);
}
void Rename(void)
{
printf("->Rename/move\n");
if (!Input("From", From_name)) return;
if (!Input("To", To_name)) return;
if (rename(From_name, To_name) != 0)
perror("RENAME");
else
printf("Renamed: \"%s\" -> \"%s\"\n",
From_name, To_name);
}
void Remove(void)
{
printf("->Remove\n");
if (!Input("Remove", From_name)) return;
if (!Input("Are You Sure", To_name)) return;
if (*To_name != 'y' && *To_name != 'Y')
return;
if (remove(From_name) != 0)
perror(From_name);
else
printf("Removed: \"%s\"\n", From_name);
}
void Maketemp(void)
{
printf("->Maketemp\n");
if (!Input("In What Directory", From_name))
return;
(void)strcat(From_name, "\\XXXXXX");
if (mktemp(From_name) == NULL)
printf("Can't create a unique name.\n");
else
printf("Created: \"%s\"\n", From_name);
}
void Quit(void)
{
printf("->Quit\n");
if (!Input("Are You Sure", From_name))
return;
if (*From_name != 'y' && *From_name != 'Y')
return;
exit(0);
}
main()
{
static void (*doit[])() = {Rename, Remove, Maketemp, Quit};
int ch;
while (1)
{
printf("--------------------------------------------\n");
printf("1) Rename/move a file or rename a directory.\n");
printf("2) Remove a file.\n");
printf("3) Make a unique temporary file.\n");
printf("4) Quit.\n");
printf("--------------------------------------------\n");
printf("Select: ");
do
{
ch = getchar();
} while (ch < '1' || ch > '4');
getchar(); /* gobble trailing newline */
printf("%c\n\n", ch);
ch -= '1';
doit[ch]();
}
}
──────────────────────────────────────────────────────────────────────────
Listing 10-10. The FMENU.C program.
FMENU.C uses a technique we discussed in Chapter 8──an array of pointers
to functions. Each menu choice corresponds to a function in that array,
and each of those functions utilizes a different routine for file
manipulation. Note that FMENU.C contains an error-printing routine you
haven't seen before──perror().
Printing Clear and Meaningful Diagnostics with perror()
All C programs use a system-defined global variable called errno, which is
set and cleared with each system or I/O call. A standard C Library routine
called perror() prints an appropriate error message based on the current
value in errno. For example, suppose an fopen() for reading a file named
JUNK fails because the file didn't exist. In that case QuickC sets errno
to 2, and perror(), when called as
perror("JUNK");
prints the following to the standard error output:
JUNK: No such file or directory
Using perror() helps your program generate clearer and more meaningful
diagnostic messages. However, remember to call perror() immediately after
a library routine returns an error. If you call another library routine
before perror(), it might change errno and cause perror() to print an
incorrect message. For example,
if((fp = fopen(fname, "rb")) == NULL)
{
fprintf(stderr, "Program Aborted because\n");
perror(fname);
exit(1);
}
does not work because the fprintf() preceding perror() succeeds and thus
sets errno to zero, causing perror() to print the incorrect message
Undefined error.
Advanced Error Handling
A program that can recover from any error is called "robust." Robust
programs are not merely carefully written programs──they are programs that
include library routines for handling all abnormal conditions and that
issue clear diagnostic messages to the user. Table 10-9 lists the most
useful routines for handling abnormal conditions.
Table 10-9 Abnormal-Condition Handlers and Diagnostic Routines
──────────────────────────────────────────────────────────────────────────
signal() Traps errors that can terminate a program, such as Ctrl-C
and floating-point exceptions.
setjmp() Prepares for a jump between functions.
longjmp() Executes a jump between functions.
──────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────
Making New Library Routines Available to QuickC
To compile and run the FMENU.C program successfully, perform the following
steps to add a few routines to QuickC that are not normally available.
1. Create the following program and save it using F.C as its filename.
#include <direct.h>
main()
{
rename();
mktemp();
perror();
}
2. Exit QuickC and run the following MS-DOS command line (ignore any
warnings about actual arguments):
qcl /c /am f.c
3. Run the following command line to create an add-on Quick Library:
link c:\lib\quicklib.obj+f.obj,f.qlb,,/q;
where c:\lib is the location for your QuickC libraries as determined
when you ran SETUP.
4. Rerun QuickC with the following:
qc /lf
For a more detailed explanation of Quick Libraries, see Chapter 12 of
this book and Section 10.1 of your Microsoft QuickC Programmer's
Guide.
Signals
Signals are conditions that cause a program to terminate prematurely. The
signals for MS-DOS are listed in signal.h: They include Ctrl-C,
Ctrl-Break, and floating-point errors such as division by zero. A text
editor is an example of a program that should not terminate if one of
these conditions occurs. The user might, for example, be editing a
temporary copy of a file──you would want to write a user's changes to disk
before exiting, no matter what.
To handle errors such as these, use the signal() function as follows:
#include <signal.h>
status = signal(sig, funct);
│ │ └────── Function address or SIG_IGN or SIG_DFL
│ └──────────── One of the signals defined in signal.h
└──────────────────────────────────────────────── SIG_ERR on error
──────────────────────────────────────────────────────────────────────────
Error-handling Philosophies: BASIC vs C
Most versions of BASIC build an error-handling mechanism into the language
in the form of the ON ERROR ... GOTO label construct. When an error is
encountered, control switches to the appropriate label or line number.
Although you can turn this facility on and off, you don't have fine
control of it.
To review the situation in C, each function is responsible for reporting
errors back to its caller. This procedure is more flexible than that used
by BASIC, but it admits some inconsistencies. Functions that return
pointers (such as fopen(), which returns a pointer to the file opened)
often return a null pointer, which can be tested against the predefined
value NULL. Other functions return the value -1 to indicate an error and
store the specific error number in the global variable errno, using
error-number values defined in the include file errno.h. Still other
functions cannot return error values because no values are reserved for
that purpose: All values might conceivably be returned by normal
operation. You can, however, use the function ferror() to find out if any
error occurred during input or output to a particular file. If you are not
sure how a particular function handles error conditions, a quick way to
find out is to use QuickC's on-line help facility discussed earlier in
this book.
In return for the greater flexibility C provides, you must explicitly test
for an error (usually by putting the function call in an if or while
statement or by calling ferror()) and then call any error-handling
functions.
The signal mechanism (discussed in this section) provides an additional,
UNIX-compatible way to handle error conditions reported by the operating
system. This mechanism is similar to the BASIC mechanism in that it
establishes a global connection between a particular error condition and
an error-handling function.
──────────────────────────────────────────────────────────────────────────
We specify #include <signal.h> for definitions of signal(), its return
value, and all of the possible values for sig. The signal() function takes
two arguments. The first specifies the type of error, the values of which
are listed in <signal.h> and summarized for MS-DOS in Table 10-10 on the
following page. The second argument is the name (or address) of a function
to be called if sig occurs, or one of the two predefined values: SIG_IGN
(ignore this signal) or SIG_DFL (resume the default action, that is,
terminate the program). Figure 10-4 illustrates the use of signal().
#include <signal.h>
int Sigflag = 0; /* global */
main ()
{
extern int Funct ();
1 if (signal (SIGINT, Funct) == SIG_ERR)
{
printf ("Signal () failed. \n");
exit (0);
}
for (;;) /* forever */
{
┌───────────► printf ("Waiting For Ctrl-C\n");
│ 5 ◄───2 3
│ if (Sigflag != 0) │
│ break 6 │
│ } │
│ } 7 │
│ │
│ Funct ()◄───────────────────────────────────┘
│ {
│ ++Sigflag;
│ } 4
└─────┘
1 Calling siganl() sets up the program to handle the Ctrl-C interrupt.
2 User presses Ctrl-C (or Ctrl-Break) during the perpetual for loop which
is printing Waiting for Ctrl-C at the time.
3 Funct() is immediately called and increments Sigflag.
4 Funct() returns and...
5 the printf() statement previously interrupted is then executed (again).
6 We check Sigflag and because it was set to a nonzero value when Funct()
was called, we exit the perpetual for loop by breaking out of it.
7 Program ends.
Figure 10-4. Analysis of signal().
As a rule, the signal-handling function, Funct(), should not perform any
I/O operation. Rather than handling the error itself, it should set a
global flag variable, then return to let the main body of the code handle
the error. The main program stops, and the signal-handling function,
Funct(), is called with the signal number sig as its argument. When
Funct() finishes and returns, the main program continues from the exact
point at which it stopped.
Handling signals under MS-DOS is fairly simple because only six signals
are defined, and only three of those actually do anything. However, if you
move your code to XENIX or UNIX, you should be prepared to handle thirty
or more signals, all of which can affect your program.
Table 10-10 Signals Defined for MS-DOS
──────────────────────────────────────────────────────────────────────────
SIGABRT Abnormal program termination. Terminates the program and
exits with a return value of 3.
SIGFPE Floating-point exception (such as division by zero or an
invalid operation). Terminates the program.
SIGINT Interrupt for keyboard. Sent when the user types the key
sequence Ctrl-C. Terminates the program.
──────────────────────────────────────────────────────────────────────────
Jumping Between Functions with setjmp() and longjmp()
Sometimes when a signal occurs, your program might not be able to continue
its main body of code. A signal caused by division by zero, for example,
would result in a completely wrong answer should it continue. For
situations such as these, when you need to jump to an earlier stage of the
program, the standard C Library offers two functions: setjmp() and
longjmp().
setjmp() prepares the program for an eventual jump to an earlier state, as
follows:
#include <setjmp.h>
jmp_buf env;
if (setjmp(env) != 0)
{
/* We got here because of a longjmp()
from someplace else */
}
/* all prepared for a longjmp() */
We specify #include <setjmp.h> for the definition of jmp_buf. The variable
env is declared as the type jump_buf and is the buffer that will hold all
the information QuickC needs to perform a jump between functions. Next,
the call to setjmp() prepares for an eventual call to longjmp(). The
result of this preparation is always 0. When setjmp() returns a 0, you
know that the program is set up for a later call to longjmp() but that the
call has not occurred. A later call to longjmp() causes the program to
call setjmp() again, but this time the call returns a nonzero value.
Use the longjmp() routine as follows:
longjmp(env, ret);
The program calls longjmp() with the same env with which it called
setjmp() earlier. The ret argument must be a nonzero number because it is
the value returned by setjmp(). (Figure 10-5 illustrates this
relationship.)
#include <set jmp.h>
jmp_buf Env; /* global */
main ()
{
┌─────► 1 if setjmp (Env) != 0) ◄─────────────────────┐
│ { │
│ printf ("Exiting at A\n"); │
│ exit (0); A │
│ } │
│ │
└───────────► printf ("Calling Foo ()\n"); │
┌────────── 2 Foo(); │
│ │
│ printf ("Exiting at B\n"); │
│ Exit (0); B │
│ } │
│ │
└──► Foo() │
{ │
printf ("In Foo ()\n"); │
│
longjmp (Env, 1); 3 ─────────────────────┘
}
1 The first call to setjmp() returns 0, so flow continues with the first
line after the if.
2 Foo() is called.
3 In Foo(), longjmp() returns us to 1. This time, however, setjmp() retur
1, so we exit at A. Note that B is never reached.
Figure 10-5. Analysis of setjmp() and longjmp().
────────────────────────────────────────────────────────────────────────────
Chapter 11 Advanced Data Types
Many programs, such as databases, spreadsheets, catalogs, and indexes,
group information in such a way that each item needs to be a different C
data type. (See Figure 11-1 on the following page.) To facilitate writing
these programs, C offers a "structure" type──a special array-like form in
which each element can be a different type.
These kinds of programs also need to be able to store different types of
data, at one time or another, at the same place in memory. The street
number in Figure 11-1, for example, could be numeric, such as 212,
requiring an integer variable, or it could be alphanumeric, such as 212B,
requiring a string variable. The C union data type solves this problem by
letting you store different types at the same place in memory.
This chapter shows you how to program with structures and unions. It also
discusses the less frequently used data types enum and bit fields.
Finally, we'll detail typedef, an alternative to the #define preprocessor
directive that lets you create new types from old.
┌──────────────────────────────────────────────────────────┐
│ char char char │
│ NAME _______________ _______________ _______________ │
│ first last middle │
│ │
│ long char │
│ ADDRESS ___________ _________________________________ │
│ street_num street │
│ │
│ char char long │
│ ________________ _____________ _____________ │
│ city state zip │
│ │
│ int long │
│ PHONE (______) ___________________ │
│ area_code phone │
│ │
└──────────────────────────────────────────────────────────┘
Figure 11-1. To enter the information on an address/phone index card into
a computer, you need to use different data types organized as a single
conceptual unit.
Structure──An Array of Different Types
An obvious limitation of arrays is that the variables in a single array
must all be of the same type (all char, all int, and so on). However, you
will frequently need to group variables of different types together so
that you can manipulate them as a single conceptual unit. The information
on the index card in Figure 11-1 is a good example. Because all of the
different "types" of information actually relate to a single person, it is
more convenient and conceptually sound to place all of that information in
a single array. Unfortunately, arrays cannot handle different data types.
To group strings and integers, for example, you must use a structure,
which can hold any mixture of types, including arrays, pointers, and
integers.
Think of a structure as a special kind of array. However, whereas the
variables in an array are called elements and are referenced by an offset,
the variables in a structure are called "members" and are referenced by
name.
You declare a structure with the C keyword struct. The first step in
setting up a structure is to declare a pattern, or template, for the
variables it will contain and to give that pattern a name. A pattern for
the structure that contains the address-book information in Figure 11-1,
for example, appears at the top of the next page.
To declare a structure pattern, follow the keyword struct with the name of
the pattern (cardstruct). Next, list the variables, or members, of the
structure between a set of braces. Note that although this list resembles
a list of variable declarations, you are not allocating memory for storage
of the structure's members──you are merely creating a template that
reserves those names for future use.
┌───────────────────────────────────────────────────────── Keyword
│ ┌─────────────────────────────────────────── Name of pattern
│ │ ┌──────────────────────────── Variables between braces
struct cardstruct {
char *first, *last, *middle;────────────────────── Member list start
long street_num;
char *street, *city, *state;
long zip;
int area_code;
long phone;───────────────────────────────────────── Member list end
};
│└──────────────────────────────────────────────────── Closing semicolon
└────────────────────────────────────────────── Variables between braces
Structure Variables
To reserve memory for a structure's members, you must declare structure
variables that follow the pattern you defined. The following declaration
sets aside memory for two structure variables (card1 and card2) using the
above cardstruct pattern:
struct cardstruct card1, card2;
This declaration starts with the keyword struct, as did the pattern, but
this time struct is followed by the name of a previously declared pattern
and then by the names of the structure variables. Remember, you manipulate
card1 and card2 in the program──the pattern cardstruct merely declares new
structures. This statement reserves memory (allocates enough storage) for
the predefined members of those two structure variables, as shown in
Figure 11-2.
struct cardstruct card 1, card 2;
└────┘ └────┘
│ │
┌────┘ └─────────────────┐
┌───────┬─▼─────┐ ┌─────▼─┬───────┐
│ *first │ │ *first │
├───────┼───────┤ ├───────┼───────┤
│ *last │ │ *last │
├───────┼───────┤ ├───────┼───────┤
│ *middle │ │ *middle │
├───────┼───────┼───────┬───────┐ ├───────┼───────┼───────┬───────┐
│ street_num │ │ street_num │
├───────┼───────┼───────┴───────┘ ├───────┼───────┼───────┴───────┘
│ *street │ │ *street │
├───────┼───────┤ ├───────┼───────┤
│ *city │ │ *city │
├───────┼───────┤ ├───────┼───────┤
│ *state │ │ *state │
├───────┼───────┼───────┬───────┐ ├───────┼───────┼───────┬───────┐
│ zip │ │ zip │
├───────┼───────┼───────┴───────┘ ├───────┼───────┼───────┴───────┘
│ area_code │ │ area_code │
├───────┼───────┼───────┬───────┐ ├───────┼───────┼───────┬───────┐
│ phone │ │ phone │
└───────┴───────┴───────┴───────┘ └───────┴───────┴───────┴───────┘
│1 byte │
│ │
Figure 11-2. Declaring structure variables sets aside enough memory for
the variables defined by cardstruct.
Accessing Structure Members
To access a member of a structure in C, specify the name of the structure
variable that contains the member, then the . (pronounced "dot") operator,
then the name of the member you need to access, as in the following
example:
printf("%d\n", card1.area_code);
│ │ └──────────────────── Name of member of structure
│ └─────────────────────────────────────────── A "dot"
└───────────────────────────────────── Name of structure
This expression prints the value of the integer area_code, one of the
member variables in the structure variable named card1.
You can manipulate members of structures as you would any C variables: You
can assign values to them, use them in computations, and so on. The only
difference is that you must reference each member variable with the name
of its structure (card1 or card2, for example), a dot, and then its own
name.
The CARD.C program (Listing 11-1) demonstrates structures by prompting
you to fill out information for a fictional address-book card; then it
prints out the information you entered.
──────────────────────────────────────────────────────────────────────────
/* card.c -- demonstrates how to declare structures */
/* and how to use structure members */
#include <stdio.h> /* for NULL and stdin */
#include <string.h> /* for strdup() */
#define MAXN 79
struct cardstruct { /* global pattern */
char *first, *last, *middle;
long street_num;
char *street, *city, *state;
long zip;
int area_code;
long phone;
};
main()
{
char *Str_Input();
long Lint_Input();
struct cardstruct card1;
card1.first = Str_Input("First Name");
card1.last = Str_Input("Last Name");
card1.middle = Str_Input("Middle Name");
card1.street_num = Lint_Input("Street Number");
card1.street = Str_Input("Street Name");
card1.city = Str_Input("City");
card1.state = Str_Input("State");
card1.zip = Lint_Input("Zip Code");
card1.area_code = (int)Lint_Input("Area Code");
card1.phone = Lint_Input("Phone Number");
printf("\n\n");
printf("%s %s %s\n", card1.first, card1.middle,
card1.last);
printf("%ld %s, %s, %s %ld\n", card1.street_num,
card1.street, card1.city, card1.state,
card1.zip);
printf("(%d) %ld\n", card1.area_code, card1.phone);
}
char *Str_Input(char *prompt)
{
char buf[MAXN+1], *ptr;
printf("%s: ", prompt);
if (fgets(buf, MAXN, stdin) == NULL)
exit(0);
buf[strlen(buf) - 1] = '\0'; /* strip '\n' */
if (strlen(buf) == 0)
exit(0);
if ((ptr = strdup(buf)) == NULL)
exit(0);
return (ptr);
}
long Lint_Input(char *prompt)
{
char buf[MAXN + 1];
long num;
printf("%s: ", prompt);
if (fgets(buf, MAXN, stdin) == NULL)
exit(0);
if (sscanf(buf, "%ld", &num) != 1)
exit(0);
return (num);
}
──────────────────────────────────────────────────────────────────────────
Listing 11-1. The CARD.C program.
CARD.C uses the members of the structure card1 exactly as it would
ordinary variables. It assigns values to them with the = operator and
passes those values to printf() to be printed.
Shorthand Structure Declarations
As a bit of shorthand, you can declare structure patterns and allocate
storage for structure variables in a single statement, as follows:
When you allocate storage for structure variables as a part of the
declaration, the name of the pattern becomes optional and you can omit it:
┌───────────────────────────────────────── Name of pattern omitted
struct {
/* list of members here */
} card1, card2;
└──────┴─────────────────────────────── Structures allocated storage
You must use the pattern name, however, if you intend to declare
additional structure variables using that pattern name later in the
program:
struct cardstruct card3, card4;
Structure Assignment
When you declare structure variables with the same pattern, you can assign
one to another, as follows:
card2 = card1;
This assignment copies the values of all card1 members into the
corresponding members of card2.
If you try to assign one structure variable to another when those
structures are declared with different pattern names (even if the members
of both are identical), QuickC returns the following error message:
error C2115:
'=' : incompatible types
──────────────────────────────────────────────────────────────────────────
Quick Tip
One way to make a program such as CARD.C more robust, or user friendly, is
to enable the program to handle telephone numbers that contain a hyphen
(-) character. Consider the necessary revisions to CARD.C. Why is this
enhancement difficult in a program that uses scanf() to parse user input?
──────────────────────────────────────────────────────────────────────────
If you need to assign values from one structure to another of a different
pattern, you must assign the members individually. For example, if card1
uses the pattern cardstruct and memo uses another pattern, memostruct, you
could assign the members of one to the other in the following way:
card1.first = memo.first_name;
card1.last = memo.last_name;
card1.middle = memo.mid_name;
Passing Structures to Functions
Passing a structure to a function passes a copy of its members. This
prevents the called function from changing the original structure. To pass
a structure to a function, simply state the structure's name, as follows:
Showcard(card1);
In this example, a copy of the structure variable card1──including copies
of all its members──is passed to the function Showcard(). Remember,
structures differ from arrays in this regard: When you pass a structure to
a function, you pass only a copy of that structure; when you pass an
array, you pass the address of that array, thus allowing the original
array to be changed by the calling function.
In the receiving function (such as Showcard() below), you must declare the
type of the received argument with struct and the pattern name
(cardstruct). This tells the compiler that Showcard() is receiving a
structure as its argument, and that the pattern for that structure is
named cardstruct:
┌────────────────────────── Receive copy of
Showcard(struct cardstruct card)
{ └────────────────── structure based on this pattern
/* body of function */
}
The CARD2.C program (Listing 11-2 beginning on the following page) is a
revised CARD.C program. In it, we fill out two cards and then print those
cards using the Showcard() function.
──────────────────────────────────────────────────────────────────────────
Quick Tip
There are two drawbacks to passing structures to functions. First, not all
compilers support the passing of structures, so if portability is
important, you might want to avoid this technique. Second, as structures
get larger, QuickC takes longer to copy them for each function call. This
can become very time-consuming if it occurs in the middle of a loop. Thus,
to speed the processing of your programs and enable the original to be
changed, we advise you to use pointers to structures.
──────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────
/* card2.c -- demonstrates structure assignment and */
/* how to pass a structure to a function */
#include <stdio.h> /* for NULL and stdin */
#include <string.h> /* for strdup() */
#define MAXN 79
struct cardstruct { /* global pattern */
char *first, *last, *middle;
long street_num;
char *street, *city, *state;
long zip;
int area_code;
long phone;
};
main()
{
int i;
char *Str_Input();
long Lint_Input();
struct cardstruct card1, card2;
for (i = 0; i < 2; i++) /* do twice */
{
printf("\nCard %d:\n\n", i + 1);
card1.first = Str_Input("First Name");
card1.last = Str_Input("Last Name");
card1.middle = Str_Input("Middle Name");
card1.street_num = Lint_Input("Street Number");
card1.street = Str_Input("Street Name");
card1.city = Str_Input("City");
card1.state = Str_Input("State");
card1.zip = Lint_Input("Zip Code");
card1.area_code = (int)Lint_Input("Area Code");
card1.phone = Lint_Input("Phone Number");
if (i == 0)
card2 = card1; /* structure assignment */
}
Showcard(card2);
Showcard(card1);
}
Showcard(struct cardstruct card)
{
printf("\n\n");
printf("%s %s %s\n", card.first, card.middle, card.last);
printf("%ld %s, %s, %s %ld\n", card.street_num,
card.street, card.city, card.state, card.zip);
printf("(%d) %ld\n", card.area_code, card.phone);
}
char *Str_Input(char *prompt)
{
char buf[MAXN + 1], *ptr;
printf("%s: ", prompt);
if (fgets(buf, MAXN, stdin) == NULL)
exit(0);
buf[strlen(buf) - 1 ] = '\0'; /* strip '\n' */
if (strlen(buf) == 0)
exit(0);
if ((ptr = strdup(buf)) == NULL)
exit(0);
return (ptr);
}
long Lint_Input(char *prompt)
{
char buf[MAXN + 1];
long num;
printf("%s: ", prompt);
if (fgets(buf, MAXN, stdin) == NULL)
exit(0);
if (sscanf(buf, "%ld", &num) != 1)
exit(0);
return (num);
}
──────────────────────────────────────────────────────────────────────────
Listing 11-2. The CARD2.C program.
In CARD2.C, Showcard() receives a copy of card1 from main(). Note that the
members of the Showcard() structure, card, are accessed with the same
"dot" notation as the originals in main().
Pointers to Structures
Passing a pointer to a structure, rather than a copy of a structure, to a
function has two advantages. It permits the function to modify the members
of the original structure. Also, far fewer bytes must be copied when a
pointer is passed than are copied when a structure is passed──the result
is faster executing code.
You declare a pointer to a structure the same way that you declare a
pointer to any other type──by preceding its name with a *, as follows:
struct cardstruct *cardptr;
│ └────── A pointer to a structure of pattern cardstruct
└─────────────────────────────────────────── Name of pattern
This example declares a pointer variable, cardptr, whose contents will be
an address. The struct cardstruct in the declaration tells the compiler
that cardptr will point to a structure variable based on the pattern
cardstruct. (See Figure 11-3.)
Before you can use the pointer cardptr, it must be given a value. Because
it is a pointer to a structure, we will assign it the address of the
structure variable card1 from CARD.C:
cardptr = &card1;
The & operator fetches the address of a structure. (Note that this differs
from arrays, where the array name itself yields the address.) To assign
the address of a structure variable to a pointer to a structure, declare
both the pointer and the structure with the same pattern name. If you
declare them with different pattern names, QuickC returns the following
warning message:
warning C4049:
'=' : indirection to different types
The & operator can also pass the address of a structure directly to a
function:
Enter(&card1);
└───────────────────────────── Pass address of card1 to a function
struct cardstruct *cardptr;
└───────┘
│
┌───────┬───▼───┐ Points to ┌───────┬───────┐
│ address │────────────►│ *first │
└───────┴───────┘ ├───────┼───────┤
│ *last │
├───────┼───────┤
│ *middle │
├───────┼───────┼───────┬───────┐
│ street_num │
├───────┼───────┼───────┴───────┘
│ *street │
├───────┼───────┤
│ *city │
├───────┼───────┤
│ *state │
├───────┼───────┼───────┬───────┐
│ zip │
├───────┼───────┼───────┴───────┘
│ area_code │
├───────┼───────┼───────┬───────┐
│ phone │
└───────┴───────┴───────┴───────┘
Structure of pattern cardstruct
Figure 11-3. A pointer to a structure contains the address of a structure
variable.
We also must declare the received argument for the Enter() function as a
pointer to a structure, as follows:
Enter(struct cardstruct *item)
{ └───────────────── Pointer to receive an address
Again, be sure that you declare the same pattern name for both the passed
and the received structures.
Accessing Structure Members with a Pointer
To access the members of a structure with a pointer, you need to use a new
symbol, ->. Called "to", -> is actually two characters──a "minus"
character followed by a "greater than" character. The following code
illustrates the use of the -> operator. In it, the pointer cardptr
accesses the phone member of the structure card1:
struct cardstruct {──────────────────────────────────── Define a pattern
char *first, *last, *middle;
int age;
};
struct cardstruct card1, *cardptr;
│ │ └──────────────────── Declare a pointer and
│ └───────────────────────────── a structure variable
└────────────────────────────────────── both of that pattern
cardptr = &card1;───────────────────── Assign card1's address to cardptr
cardptr->phone = 5551212;──────────── Access member of card1 via cardptr
└────────────────────────────── Points "to" member phone of card1
The CARD3.C program (Listing 11-3) is another revision of CARD.C. This
modification has Showcard() receiving the address of a structure. Rather
than printing a copy, it prints the original via a pointer to the
structure.
──────────────────────────────────────────────────────────────────────────
/* card3.c -- demonstrates pointers to structures */
#include <stdio.h> /* for NULL and stdin */
#include <string.h> /* for strdup() */
#define MAXN 79
struct cardstruct { /* global pattern */
char *first, *last, *middle;
long street_num;
char *street, *city, *state;
long zip;
int area_code;
long phone;
};
main()
{
int i;
char *Str_Input();
long Lint_Input();
struct cardstruct card1, card2;
for (i = 0; i < 2; i++) /* do twice */
{
printf("\nCard %d:\n\n", i + 1);
card1.first = Str_Input("First Name");
card1.last = Str_Input("Last Name");
card1.middle = Str_Input("Middle Name");
card1.street_num = Lint_Input("Street Number");
card1.street = Str_Input("Street Name");
card1.city = Str_Input("City");
card1.state = Str_Input("State");
card1.zip = Lint_Input("Zip Code");
card1.area_code = (int)Lint_Input("Area Code");
card1.phone = Lint_Input("Phone Number");
if (i == 0)
card2 = card1;
}
Showcard(&card2); /* pass addresses of structures */
Showcard(&card1);
return (0);
}
Showcard(cardptr)
struct cardstruct *cardptr; /* pointer receives an address */
{
printf("\n\n");
printf("%s %s %s\n", cardptr->first, cardptr->middle,
cardptr->last);
printf("%ld %s, %s, %s %ld\n", cardptr->street_num,
cardptr->street, cardptr->city, cardptr->state,
cardptr->zip );
printf("(%d) %ld\n", cardptr->area_code, cardptr->phone);
}
char *Str_Input(char *prompt)
{
char buf[MAXN + 1], *ptr;
printf("%s: ", prompt);
if (fgets(buf, MAXN, stdin) == NULL) exit(0);
buf[strlen(buf) - 1 ] = '\0'; /* strip '\n' */
if (strlen(buf) == 0)
exit(0);
if ((ptr = strdup(buf)) == NULL)
exit(0);
return (ptr);
}
long Lint_Input(char *prompt)
{
char buf[MAXN + 1];
long num;
printf("%s: ", prompt);
if (fgets(buf, MAXN, stdin) == NULL)
exit(0);
if (sscanf(buf, "%ld", &num) != 1)
exit(0);
return (num);
}
──────────────────────────────────────────────────────────────────────────
Listing 11-3. The CARD3.C program.
Arrays of Structures
Structures can be organized in arrays like any other type of variable. You
declare an array of structures as follows:
struct cardstruct {
/* members declared here */
} cards[3];
└───────────────────────────── An array of three structures
This example declares an array of three structures (cards[3]) and defines
the pattern cardstruct at the same time. If you had already defined the
pattern, you could declare the same array as follows:
struct cardstruct cards[3];
Use an array of structures the same way you use any other array. For
example, the following statement prints the first member of the second
card:
printf("%s", cards[1].first);
The expression cards[1] accesses the second structure of the array, and
the .first yields the member named first from that structure.
To pass the address of one of the structures in the array cards, use the &
operator followed by the structure's offset in square brackets.
┌──────────────────────────────────────────────────────────── Address of
&cards[i]
└─────────────────── the <FI>i<FS>th structure in the array of str
The ROLO.C program (Listing 11-4) is a complete address book built from
the earlier CARD.C program. It asks you to fill out the three cards in our
array of structures. Then it prints out the information in those cards. By
combining this use of structures with the file-handling routines of
PHONE.C (from the previous chapter), you have the basis for a truly
useful phone-index program.
──────────────────────────────────────────────────────────────────────────
/* rolo.c -- demonstrates pointers to structures */
#include <stdio.h> /* for NULL and stdin */
#include <string.h> /* for strdup() */
#define MAXN 79
#define MAXCARDS 3
struct cardstruct { /* global pattern */
char first[MAXN],
last[MAXN],
middle[MAXN];
unsigned long street_no;
char street[MAXN],
city[MAXN],
state[MAXN];
unsigned long zip;
unsigned int area;
unsigned long phone;
};
struct cardstruct cards[MAXCARDS];
main()
{
int i;
for (i = 0; i < MAXCARDS; ++i)
{
printf("\n<card %d of %d>\n", i + 1, MAXCARDS);
Input(&cards[i]);
}
for (i = 0; i < MAXCARDS; ++i)
{
printf("\n<%d> ", i + 1);
Showcard(&cards[i]);
}
}
Input(struct cardstruct *cardp)
{
char *Str_Input();
long Lint_Input();
strcpy(cardp->first,Str_Input("First Name"));
strcpy(cardp->last,Str_Input("Last Name"));
strcpy(cardp->middle,Str_Input("Middle Name"));
cardp->street_no = Lint_Input("Street Number");
strcpy(cardp->street,Str_Input("Street"));
strcpy(cardp->city,Str_Input("City"));
strcpy(cardp->state,Str_Input("State"));
cardp->zip = Lint_Input("Zip Code");
cardp->area = (int)Lint_Input("Area Code");
cardp->phone = Lint_Input("Phone Number");
}
char *Str_Input(char *prompt)
{
char buf[MAXN + 1], *ptr;
printf("%s: ", prompt);
if (fgets(buf, MAXN, stdin) == NULL)
exit(0);
buf[strlen(buf) - 1 ] = '\0'; /* strip '\n' */
if (strlen(buf) == 0)
exit(0);
if ((ptr = strdup(buf)) == NULL)
exit(0);
return (ptr);
}
long Lint_Input(char *prompt)
{
char buf[MAXN + 1];
long num;
printf("%s: ", prompt);
if (fgets(buf, MAXN, stdin) == NULL)
exit(0);
if (sscanf(buf, "%ld", &num) != 1)
exit(0);
return (num);
}
Showcard(struct cardstruct *cardptr)
{
printf("\n\n");
printf("%s %s %s\n", cardptr->first, cardptr->middle,
cardptr->last);
printf("%ld %s, %s, %s %ld\n", cardptr->street_no,
cardptr->street, cardptr->city, cardptr->state,
cardptr->zip);
printf("(%d) %ld\n", cardptr->area, cardptr->phone);
}
──────────────────────────────────────────────────────────────────────────
Listing 11-4. The ROLO.C program.
ROLO.C uses an array of three structures. Notice that the cards[] array
consists of structures that themselves contain arrays.
Arrays of Pointers to Structures
Not only can you create arrays of structures, you can also create arrays
of pointers to structures. These arrays of pointers offer the advantage of
increased efficiency. For example, when sorting, it is faster to swap two
pointers than it is to exchange the vastly greater number of bytes of the
structures themselves.
You declare an array of pointers to structures as follows:
struct cardstruct *cardps[3]
This example declares an array of three pointers in which each pointer
points to a structure of the pattern cardstruct. Figure 11-4 illustrates
such an arrangement.
You can initialize cardps[] (an array of pointers to structures) to
contain the address of the corresponding elements in the array of
structures cards[] as follows:
cardps[0] = &cards[0];
cardps[1] = &cards[1];
cardps[2] = &cards[2];
This lets you use the -> operator to indirectly reference the members of
each structure in cards[] with the pointers in cardps[]. For example, the
street member of the second structure of the array of structures cards[]
can be indirectly referenced through the array of pointers to structures
in cardps[], as follows:
strcpy(cardps[1]->street, "Any St.");
└───────────── Points "to" the member street of cards[1]
Structure Recursion and Linked Lists
Structures are so versatile that they can hold every possible type in C,
including themselves. This remarkable ability to be self-inclusive opens
whole new sets of programming possibilities. The most common of these is
the technique shown in Figure 11-5 (on p. 348) that uses "linked lists."
┌─────────────────────┐
┌─►│ first [] │
│ ├─────────────────────┤
│ │ last [] │
│ ├─────────────────────┤
│ │ middle [] │
│ ├───────────┬─────────┘
│ │street_num │
│ ├───────────┴─────────┐
│ │ street [] │
│ ├─────────────────────┤
│ │ city [] │
│ ├─────────────────────┤
│ │ state [] │
│ ├───────────┬─────────┘
│ │ zip │
│ ├──────┬────┘
│ │area │
┌────┬────┐ │ ├──────┴────┐
│ address │───┘ │ phone │
├────┼────┤ ├───────────┴─────────┐
│ address │─────►│ first [] │
├────┼────┤ ├─────────────────────┤
│ address │───┐ │ last [] │
└────┴────┘ │ ├─────────────────────┤
cardps [3] │ │ middle [] │
│ ├───────────┬─────────┘
│ │street_num │
│ ├───────────┴─────────┐
│ │ street [] │
│ ├─────────────────────┤
│ │ city [] │
│ ├─────────────────────┤
│ │ state [] │
│ ├───────────┬─────────┘
│ │ zip │
│ ├──────┬────┘
│ │area │
│ ├──────┴────┐
│ │ phone │
│ ├───────────┴─────────┐
└─►│ first [] │
├─────────────────────┤
│ last [] │
├─────────────────────┤
│ middle [] │
├───────────┬─────────┘
│street_num │
├───────────┴─────────┐
│ street [] │
├─────────────────────┤
│ city [] │
├─────────────────────┤
│ state [] │
├───────────┬─────────┘
│ zip │
├──────┬────┘
│area │
├──────┴────┐
│ phone │
└───────────┘
cards [3]
Figure 11-4. An array of pointers to structures. Each element points to a
structure in an array of structures.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ │ ┌─►│ │ ┌─►│ │
├─────────────────┤ │ ├─────────────────┤ │ ├─────────────────┤
├─────────────────┤ │ ├─────────────────┤ │ ├─────────────────┤
├─────────┬───────┘ │ ├─────────┬───────┘ │ ├─────────┬───────┘
├─────────┴───────┐ │ ├─────────┴───────┐ │ ├─────────┴───────┐
├─────────────────┤ │ ├─────────────────┤ │ ├─────────────────┤
├─────────────────┤ │ ├─────────────────┤ │ ├─────────────────┤
├─────────┬───────┘ │ ├─────────┬───────┘ │ ├─────────┬───────┘
├────┬────┘ │ ├────┬────┘ │ ├────┬────┘
├────┴────────────┐ │ ├────┴────────────┐ │ ├────┴────────────┐
├─────────────────┤ │ ├─────────────────┤ │ ├─────────────────┤
│ nextcard ├──┘ │ nextcard ├──┘ │ nextcard │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Figure 11-5. In a linked list, each structure contains a pointer to
another structure of the same type.
A linked list is an arrangement of structures in which each structure
contains a pointer to (the address of) its neighbor. For example, to
declare such a linked list in ROLO.C, we must modify the structure pattern
as follows:
struct cardstruct {
char first[MAXN],
last[MAXN],
middle[MAXN];
unsigned long street_no;
char street[MAXN],
city[MAXN],
state[MAXN];
unsigned long zip;
unsigned int area;
unsigned long phone;
struct cardstruct *nextcard;────────────────────────────────── Added
└ Pointer to another structure of this same pattern
The new member *nextcard is a pointer to a structure, but it points to a
structure of its own pattern. By declaring several structures of this
pattern with
struct cardstruct card1, card2, card3, card4;
and then initializing the nextcard member of each to contain the address
of its neighbor, you create a linked list:
card1.nextcard = &card2;
card2.nextcard = &card3;
card3.nextcard = &card4;
The ROLO2.C program (Listing 11-5) uses malloc() to build a linked list
of structures while the program is running. Using this approach, we can
add as many cards to our address book as we want (subject to the limit of
the computer's memory).
──────────────────────────────────────────────────────────────────────────
/* rolo2.c -- demonstrates a linked list */
#include <stdio.h> /* for NULL and stdin */
#include <string.h> /* for strdup() */
#include <malloc.h> /* for malloc() */
#define MAXN 79
struct cardstruct { /* global pattern */
char first[MAXN],
last[MAXN],
middle[MAXN];
unsigned long street_no;
char street[MAXN],
city[MAXN],
state[MAXN];
unsigned long zip;
unsigned int area;
unsigned long phone;
struct cardstruct *nextcard;
};
main()
{
int i;
struct cardstruct card, *first, *current;
first = (struct cardstruct *)malloc(sizeof(struct cardstruct));
if (first == NULL)
exit(1);
if (Input(&card) != 0)
exit(1);
*first = card;
current = first;
while (Input(&card) == 0)
{
current->nextcard =
(struct cardstruct *)malloc(sizeof(struct cardstruct));
if (current->nextcard == NULL)
exit(1);
current = current->nextcard;
*current = card;
}
current->nextcard = NULL;
Dumplist(first);
}
Dumplist(struct cardstruct *head)
{
do
{
Showcard(head);
} while ((head = head->nextcard) != NULL);
}
Showcard(struct cardstruct *cardptr)
{
printf("\n\n");
printf("%s %s %s\n", cardptr->first, cardptr->middle,
cardptr->last);
printf("%ld %s, %s, %s %ld\n", cardptr->street_no,
cardptr->street, cardptr->city, cardptr->state,
cardptr->zip );
printf("(%d) %ld\n", cardptr->area, cardptr->phone);
}
Input(struct cardstruct *cardp)
{
char *Str_Input();
long Lint_Input();
printf("\n<new card> (Empty first name Quits)\n");
strcpy(cardp->first,Str_Input("First Name"));
if (*(cardp->first) == '\0')
return (1);
strcpy(cardp->last,Str_Input("Last Name"));
strcpy(cardp->middle,Str_Input("Middle Name"));
cardp->street_no = Lint_Input("Street Number");
strcpy(cardp->street,Str_Input("Street"));
strcpy(cardp->city,Str_Input("City"));
strcpy(cardp->state,Str_Input("State"));
cardp->zip = Lint_Input("Zip Code");
cardp->area = (int)Lint_Input("Area Code");
cardp->phone = Lint_Input("Phone Number");
return (0);
}
char *Str_Input(char *prompt)
{
char buf[MAXN + 1], *ptr;
printf("%s: ", prompt);
if (fgets(buf, MAXN, stdin) == NULL)
exit(0);
buf[strlen(buf) - 1 ] = '\0'; /* strip '\n' */
if ((ptr = strdup(buf)) == NULL)
exit(0); return (ptr);
}
long Lint_Input(char *prompt)
{
char buf[MAXN + 1];
long num;
printf("%s: ", prompt);
if (fgets(buf, MAXN, stdin) == NULL)
exit(0);
if (sscanf(buf, "%ld", &num) != 1)
num = 0;
return (num);
}
──────────────────────────────────────────────────────────────────────────
Listing 11-5. The ROLO2.C program.
Notice that the last structure in the list always has its nextcard member
set to NULL. That's how the program marks the end of the linked list.
This program also illustrates two other interesting properties of
structures. First, when you apply the sizeof operator to a structure or to
a structure's pattern, it yields the total number of bytes for all the
members of the structure:
malloc(sizeof(struct cardstruct));
Second, we had to type cast the value returned by malloc() to a type
appropriate for the pointer to which the value is assigned:
first = (struct cardstruct *)malloc(sizeof(struct cardstruct));
└────────┬────────┘
└─────────────── Type cast to a pointer to a structure
Note that you must use the structure pattern name in the type cast, not
the structure variable name. Had we omitted the type cast, QuickC would
complain with:
Warning C4049:
'=' indirection to different types
Initializing Structures with Starting Values
As in arrays, you can initialize structures that are static or global when
you declare them. The type of the initializing value must, of course,
match the type of the corresponding member. An attempt to initialize with
the wrong type will yield the following QuickC warning:
Warning C4047:
"initializing": different levels of indirection
The following structure is declared correctly:
static struct cardstruct card = {
"Bob",─────────────────────────────────────────────Member first
"Roberts",──────────────────────────────────────────Member last
"Mason",──────────────────────────────────────────Member middle
42─────────────────────────────────────────────Member street_no
"Willow Way",─────────────────────────────────────Member street
"Tonopah",──────────────────────────────────────────Member city
"Nevada",──────────────────────────────────────────Member state
84521L,──────────────────────────────────────────────Member zip
916,────────────────────────────────────────────────Member area
5551212L───────────────────────────────────────────Member phone
};
As with arrays, if you specify fewer initializers than members, QuickC
gives the trailing uninitialized members the default value of zero.
Union──Multiple Types in the Same Space
You can think of a "union" as the opposite of a structure. While struct is
a collection of many types, each with its own location in memory, a union
is a collection of many types that all share the same location in memory.
Thus, a union can contain different types at various times, but it can
contain only a single value of a single type at any given time.
Although its uses are limited, a union is a blessing when you do encounter
a need for one. For example, consider writing a function that needs to
print either an int or a float, yet doesn't know ahead of time what type
it will receive as its argument. Before we can show you how to write such
a function, however, we need to cover the basics of declaring and using
unions.
You declare a union as you would a structure, except you use the keyword
union instead of struct:
┌────────────────────────────────────────────── Name of pattern
union twotype {
float ftype;──────────────────────────────────────────────── Members
int itype;
} one_of_many ;
└──────────────────────── Name of a union variable of pattern twotype
This example tells the compiler to reserve memory for the variable
one_of_many, which will hold either a float or an int. Because the float
is larger, union reserves four bytes──enough space to hold either type.
As a general rule, you should place the largest member first in a union
declaration. Some compilers allocate memory based only on the first
member, rather than searching all members for the largest. QuickC is well
behaved in this regard, however. It allocates the correct number of bytes
for a union, regardless of the order of the member declarations.
As with structure members, you access the members of a union with the
"dot" operator. However, the compiler interprets the type of the union as
the type specified by the member name, as follows:
one_of_many.ftype = 1.0;────────────────────────────Interpret as a float
one_of_many.itype = 1;───────────────────────────────Interpret as an int
The UDEMO.C program (Listing 11-6) is a simple demonstration of how a
union works. After asking the user to enter a type, it uses scanf() to
read that type and printf() to echo it to the screen.
──────────────────────────────────────────────────────────────────────────
/* udemo.c -- demonstrates a union at work */
#include <stdio.h>
char *Strings[6] = {
"Quit",
"line of text",
"floating-point double value",
"long integer value",
"floating-point value",
"integer value"
};
struct Unitstruct {
union {
char wtype[BUFSIZ];
double dtype;
long ltype;
float ftype;
int itype;
} manyu;
int type_in_union;
};
main()
{
struct Unitstruct one_of_many;
while ((one_of_many.type_in_union = Menu()) != 0 )
{
Inputval(&one_of_many);
Printval(&one_of_many);
}
}
Inputval(struct Unitstruct *one_of_many)
{
printf("\nEnter a %s: ", Strings[one_of_many->type_in_union]);
switch(one_of_many->type_in_union)
{
case 1:
fgets(one_of_many->manyu.wtype, BUFSIZ, stdin);
break;
case 2:
scanf("%lf", &(one_of_many->manyu.dtype));
while (getchar()!= '\n');
break;
case 3:
scanf("%ld", &(one_of_many->manyu.ltype));
while (getchar()!= '\n');
break;
case 4:
scanf("%f", &(one_of_many->manyu.ftype));
while (getchar()!= '\n');
break;
case 5:
scanf("%i", &(one_of_many->manyu.itype));
while (getchar()!= '\n');
break;
}
}
Printval(struct Unitstruct *one_of_many)
{
printf("The %s you entered\nwas: ", Strings[one_of_many->type_in_union]
switch (one_of_many->type_in_union)
{
case 1:
fputs(one_of_many->manyu.wtype, stdout);
break;
case 2:
printf("%lf", one_of_many->manyu.dtype);
break;
case 3:
printf("%ld", one_of_many->manyu.ltype);
break;
case 4:
printf("%f", one_of_many->manyu.ftype);
break;
case 5:
printf("%i", one_of_many->manyu.itype);
break;
}
printf("\n\n");
}
Menu()
{
int i;
char ch;
for (i = 0; i < 6; ++i)
{
printf("%d) %s\n", i, Strings[i]);
}
printf("Which: ");
do
{
ch = getch();
} while (ch < '0' || ch > '5');
printf("%c\n", ch);
return (ch - '0');
}
──────────────────────────────────────────────────────────────────────────
Listing 11-6. The UDEMO.C program.
Unions and Functions
Unlike a structure, you cannot pass a union to a function. Instead, you
must pass the value of the type currently stored in that union. For
example, the statement
printf("%f", one_of_many.ftype);
│ └───────── Sends the float value in one_of_many
└────────────────────────────────────────────── Expects a float
sends printf() the float value in one_of_many, which matches the printf()
%f format specifier. Note that it is meaningless in C to use a union
variable (such as one_of_many) without a corresponding "dot" and member
name.
──────────────────────────────────────────────────────────────────────────
Quick Tip
The UDEMO.C program illustrates a common technique for managing unions.
Because a union contains no inherent indication of the type it contains,
unions are often made members of structures, with another member used to
store that indication:
struct Unitstruct{
union {
char wtype[BUFSIZE];
double dtype;
long ltype;
float ftype;
int itype;
} manyu;
int type_in_union;
};
By packaging a union and an int together in a structure like this, we are
better able to keep track of the type stored in the union at any given
time.
──────────────────────────────────────────────────────────────────────────
Unions Received by Functions
C permits you to use a union as the type of an argument received by a
function, but the procedure can be risky. The following statement
illustrates one way to declare a received variable in a subroutine as a
union:
#define FLT 0 /* floating-point type */
#define INT 1 /* integer type */
Printval(val, type)
union twotype val;
int type;
{
switch (type)
{
case FLT: printf("%f", val.ftype); break;
case INT: printf("%d", val.itype); break;
}
}
This function receives two arguments: a union of two possible types and an
int that specifies which of the two possible types is in that union.
But beware. Depending on how the compiler passes arguments to functions,
this approach can fail. In QuickC, a float is four bytes and an int is two
bytes; therefore, the stack (received arguments) resembles Figure 11-6a
when passing a float and Figure 11-6b when passing an int. However,
because the pattern for twotype reserves four bytes, passing an int to
Printval() causes the type argument to appear in the wrong place.
You can resolve this dilemma by constraining union members to types that
use the same number of bytes. That is, if you declare twotype as follows:
union twotype {
float fval;
long ival;
};
it would contain either of two types, but each type requires four bytes. A
better solution is to package a union and an int together inside a
structure, as you saw earlier. That approach avoids the potential pitfalls
of declaring a function that receives a bare-bones union.
Pointers to Unions
Pointers to unions behave like pointers to structures. You retrieve the
address of a union with the & operator and the union variable name, as
follows:
&one_of_many
Printval (float, int)
│ │ High address
│ │ ┌────────────────┐
│ └────►├── int ──┤2-byte int
│ ├────────────────┤
│ ├── ──┤
└──────────►├── float ──┤4-byte float
├── ──┤
Start of arguments ────►└────────────────┘
Low address
(A)
Printval (int, int)
│ │ High address
│ │ ┌────────────────┐
│ │ ├── ──┤Second argument missing
│ │ ├────────────────┤
│ └──────►├── int ──┤2-byte int
│ ├────────────────┤
└───────────►├── int ──┤2-byte int
Start of arguments ────►└────────────────┘
Low address
(B)
Figure 11-6. Passing different-size data types to the same function can
cause confusion.
To fetch the address of a union member, specify the & operator, the union
variable name, the "dot" operator, and the member name, as follows:
&one_of_many.ftype
Declaring pointers to unions and manipulating values via the addresses in
those pointers is also identical to the form used by structure pointers.
Declare a pointer to a union as follows:
union manytype *up;
└───────────── Pointer to a union of the pattern manytype
Place a value (an address) into that pointer in the following form:
up = &one_of_many;
To access the type of the value stored in the union whose address is in
up, use the -> operator as follows:
up->ftype = 1.0;
Structures and unions are closely related. The main difference is that a
structure holds many values simultaneously; a union holds only a single
type of value at any one time. As you have seen, structures can include
unions as members. It is also legal for unions to contain structures as
members. We'll use this latter technique at the end of this chapter, when
we discuss bit fields.
Enumerated Data with enum
Many kinds of information are best represented by a finite list of
discrete integer values──for example, the days of the week, the months of
the year, or even the phases of the moon. Such kinds of information, in
which every possibility is known in advance, lend themselves to
enumeration──a listing of all possible values for a given topic or
concept.
If you need to represent the days of the week in a program as discrete
integers, you could make the following declarations and assignments:
int monday = 0, tuesday = 1, wednesday = 2, thursday = 3,
friday = 4, saturday = 5, sunday = 6;
and later use those values as follows:
pay_day = friday;
The previous approach, although reasonable, has a potential pitfall.
Because the days of the week are int variables, the program might change
their values, and so render them meaningless. To avoid this problem we can
use the following directives:
#define MONDAY 0
#define TUESDAY 1
[etc.]
The program can't change these values because they are integer constant
aliases. But this is still not an ideal solution because you cannot group
#define definitions under a single conceptual name.
The best solution uses the C enumerated data type, enum, whose members are
constants grouped under a single name. To represent the days of the week
using enum, first declare a pattern similar to a structure or union
pattern:
┌────────────────────────────────────────────── Name of pattern
enum week_days {
monday,───────────────────────────────────────────────────── Members
tuesday,
wednesday,
thursday,
friday,
saturday,
sunday
} pay_day;
└────────────────────────────────────────────── Enumerated variable
This example declares a pattern called week_days, an enumerated data type,
and the enumerated variable pay_day. Note that the members don't need to
be preceded by a type keyword because the members of enum are always of
type int. Also notice that you don't need to assign the members any
values: The declaration itself gives the members constant integer values,
starting with 0 for monday and counting through 6 for sunday.
Another difference between enum and struct or union is that you access
members of enum simply by stating the member's name without the "dot" or
"->" notation:
payday = monday;
Any attempt to change the value of an enumerated member (monday = 5, for
example) results in the following QuickC error message:
error C2106:
'=' : left operand must be lvalue
This reminds you that the members of an enumerated data type, like all
other constants, are rvalues and can appear only to the right of an
assignment operator.
Also note that you cannot use a pointer to indirectly change the value of
an enumerated variable member. For example, the following assignment:
int *p;
p = &monday;────────────────────────────Can't take address of a constant
*p = 5;
fails because you can't retrieve the address of a constant. This attempt
generates the following QuickC error message:
error C2101:
'&' on constant
The TODAY.C program (Listing 11-7 on the following page) demonstrates one
advantage to using enum──improved readability. The program asks you to
specify the day on which you want to be paid. It then checks to make
certain that you specified a legal day.
The pattern week_day in TODAY.C shows that you can initialize an enum
member to any integer value. Any uninitialized member, however, is
assigned a value one higher than the member before it. For example, the
declaration
enum folks {
mo = -1,
roseann,
betsy = 0,
kit,
joey = 1
};
sets mo to a -1, roseann and betsy to 0, and kit and joey to 1. This also
shows that enum members can have duplicate values.
──────────────────────────────────────────────────────────────────────────
/* today.c -- demonstrates using enum */
main()
{
enum week_days {
monday = 1, /* start with 1 */
tuesday,
wednesday,
thursday,
friday,
saturday,
sunday
} pay_day;
static char *day_names[] = {
"",
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday"
};
printf("What day do you want to be paid on?\n");
for (pay_day = monday; pay_day <= sunday; ++pay_day)
{
printf("%d. %s\n", pay_day, day_names[pay_day]);
}
printf("Which (%d-%d): ", monday, sunday);
do
{
pay_day = getch();
pay_day -= '0';
} while (pay_day < monday || pay_day > sunday);
printf("%d\n\n", pay_day);
printf("You selected %s\n", day_names[pay_day]);
}
──────────────────────────────────────────────────────────────────────────
Listing 11-7. The TODAY.C program.
Bit Fields
In Chapter 7, we discussed how to use bitwise operators to store data in
the individual bits of bytes. Another, and simpler, way to store and
access information in bits is with "bit fields."
Bit fields offer two advantages over the bitwise operators. First, you can
access bit fields by name (such as blink) rather than by an obscure mask
(such as (1 << 7)). Second, the compiler generates code for bit fields
that you normally would have to write yourself. Examine, for example, the
following bit-field assignment:
blink = 1;
where blink is the name of the sixteenth bit of a 2-byte int. This
statement is comparable to the following assignment using bitwise
operators:
ch |= (1 << 15);
C's bit fields are especially handy when you need to manipulate items with
built-in bit information. The characters in your screen memory are
examples of such items. Recall that each screen character is represented
by a 2-byte int. One byte is the character itself; the other is the
attribute byte. (See Figure 11-7.)
┌────────Most significant bit
┌───▼───┐─┐
Blinking (1 bit) ─────│ 7 │ │
┌─├─ ─┤ │
│ │ 6 │ │
│ ├─ ─┤ │
Background (3 bits) ───┤ │ 5 │ │
│ ├─ ─┤ │
│ │ 4 │ │
└─├─ ─┤ ├───Attributes (1 byte)─┐
Intensity (1 bit) ─────│ 3 │ │ │
┌─├─ ─┤ │ │
│ │ 2 │ │ │
│ ├─ ─┤ │ │
Foreground (3 bits) ───┤ │ 1 │ │ │
│ ├─ ─┤ │ │
│ │ 0 │ │ │
└─├───────┤─┤ ├──1 int
│ 7 │ │ │
├─ ─┤ │ │
│ 6 │ │ │
├─ ─┤ │ │
│ 5 │ │ │
├─ ─┤ │ │
│ 4 │ │ │
├─ ─┤ ├───Character (1 byte)──┘
│ 3 │ │
├─ ─┤ │
│ 2 │ │
├─ ─┤ │
│ 1 │ │
├─ ─┤ │
│ 0 │ │
└──────┘─┘
└────────Least significant bit
Figure 11-7. One character in screen memory is represented by two
consecutive bytes.
The following is an example of one such screen int declared using bit
fields:
┌──────────────┬────────────── One integer...divided like this
unsigned int character :8,
foreground :3,
intensity :1,
background :3,
blink :1;
└─────────┴──────────────────── Name for...this many bits
In this declaration, we tell QuickC to use the bits in one unsigned
integer. Next, we specify the names for each group of bits in that
integer, beginning with 8 bits, to which we give the name character, and
continuing through all 16 bits until we end with blink as the name of the
final bit.
You may name as many bits as there are in the type declared (8 for a char,
32 for a long, and so on). Only integer types can be used as bit fields,
and only integer constants can be used to declare the number of bits.
Always declare the bits from the bottom up (from the least significant to
the most significant bits). A colon separates the name for each group of
bits from the number of bits assigned to it; a comma separates each name
:bits from the next; and, of course, a semicolon must end the entire
declaration.
If you declare fewer bits than there are in a type, the unused bits are
simply ignored. If you declare more, an additional variable of the same
type is allocated:
┌───────────────────────────────────────────────────── 8 bits
unsigned char character :8,
foreground :3,
intensity :1,
background :3,
blink :1;
└─── 16 bits total allocates two char variables
The name :bits combination is what defines a bit field as opposed to an
ordinary variable. For example, the above declaration produces the same
allocation as the following series of declarations:
unsigned char character :8;
unsigned char foreground :3;
unsigned char intensity :1;
unsigned char background :3;
unsigned char blink :1;
In this example, the compiler gathers the bits from the declared bit
fields into the most compact unit, regardless of how many bit fields you
declare.
Because you are not permitted to retrieve the address of a bit field, you
usually will declare bit fields inside structures, as follows:
struct screen_char_struct {
unsigned int character :8,
foreground :3,
intensity :1,
background :3,
blink :1;
} screen_ch ;
└───┬───┘
└───────────────── Structure variable whose members are bit fields
This approach has two advantages. First, you can access the individual bit
fields with the usual structure/member notation. This improves
readability:
screen_ch.blink = 1;─────────────────Retrieve the address of a structure
Second, you can access the address of a structure, but you cannot retrieve
the address of a bit field. This lets you manipulate bit fields with
pointers, which can increase the speed of your program:
&screen_ch
The SCRMENU.C program (Listing 11-8) demonstrates how to use bit fields
to modify text-screen display. It lets you select an attribute; then it
toggles the setting for that attribute for every character on the screen.
──────────────────────────────────────────────────────────────────────────
/* scrmenu.c -- uses bit fields to modify your text */
/* screen's attributes */
char *Choice_Words[] = {
"Quit",
"Foreground",
"Intensity",
"Background",
"Blinking"
};
enum Choices {
Quit,
Foreground,
Intensity,
Background,
Blinking
};
/* use 0xB800000 for EGA or VGA */
#define SCR_START (0xB0000000)
#define SCR_SIZE (25 * 80)
main()
{
enum Choices choice;
printf("Select from the following by number:\n");
for (choice = Quit; choice <= Blinking; ++choice )
{
printf("%d. %s\n", choice, Choice_Words[choice]);
}
printf("\nWhich: ");
do
{
choice = getch();
choice -= '0';
if (choice < Foreground || choice > Blinking)
continue;
Redraw( choice );
} while (choice != Quit);
}
Redraw(enum Choices field)
{
struct screen_char {
unsigned int character :8,
foreground :3,
intensity :1,
background :3,
blink :1;
} scrchar, far *sp, far *ep;
sp = (struct screen_char far *)SCR_START;
ep = sp + SCR_SIZE;
while (sp < ep)
{
scrchar = *sp;
switch (field)
{
case Foreground:
scrchar.foreground = (scrchar.foreground)? 0 : 7;
break;
case Intensity:
scrchar.intensity = (scrchar.intensity)? 0 : 1;
break;
case Background:
scrchar.background = (scrchar.background)? 0 : 7;
break;
case Blinking:
scrchar.blink = (scrchar.blink)? 0 : 1;
break;
}
*(sp++) = scrchar;
}
}
──────────────────────────────────────────────────────────────────────────
Listing 11-8. The SCRMENU.C program.
SCRMENU.C combines bit fields with enum and the #define preprocessor
directive to virtually rid the body of the program of obscure constructs.
Also, notice that we use a pointer to a structure to access the screen.
Advanced typedef
So far, we've used the #define preprocessor directive to create aliases,
both for increased program clarity and as a shorthand method of entering
repetitive code. We have also seen, in Chapter 3, that new types can be
defined by using typedef. Superficially, #define and typedef appear to be
interchangeable. To create simple aliases, you can use either one.
Situations arise, however, in which typedef is suitable, but #define is
not.
For example, suppose you need to create a new type called string, an array
of type char. Now suppose you attempt to create this new type with
#define, as follows:
#define string char s[128]
You later would not be permitted to make the declaration
string str1, str2;
because the preprocessor would expand it to be
char s[128] str1, str2;
which is illegal. (Note the missing comma, among other things.) In
situations such as this one, typedef is ideal. Rather than beginning with
a #define directive, suppose you use the following:
typedef char string[128];
This creates a new type called string, which you can use later to declare
variables of that new type:
string str1, str2;
Because we used typedef to define string, the compiler correctly
translates this into
char str1[128], str2[128];
which is what we intended in the first place.
The secret to using typedef is to follow three simple steps. First,
declare an ordinary variable of the type you want:
char s[128];
Second, place the word typedef at the front:
typedef char s[128];
Third, replace the variable's name with the new type name:
typedef char string[128];
You can now use the newly defined type string exactly as you would one of
C's built-in types, such as int.
In addition to doing what #define cannot, typedef also lends clarity to
otherwise obscure constructs. For example, consider the following two
pointers to functions:
int (*quit_fun)(), (*restart_fun)();
This could be confusing if it were to appear throughout your program.
Using typedef, however, you can create a new type called funptr:
typedef (*funptr)();
Now you can use funptr throughout your program to declare variables of
that new type, as follows:
funptr quit_fun, restart_fun;
Use typedef judiciously──it is the most easily abused concept in C. The
indiscriminate use of typedef, rather than making your program more
readable, can make it more obscure and (sometimes) indecipherable.
────────────────────────────────────────────────────────────────────────────
Chapter 12 Large Projects
As your programs become larger and more complex, revising and maintaining
them become less straightforward. Consequently, as your programming skills
increase, you inevitably will find yourself looking for more efficient
ways of handling programs. For example, you might want to:
■ Use one function in several programs without having to retype it every
time.
■ Compile a program one way for testing and another for actual use──
without having to rewrite it.
■ Combine several .C files into a single program, while recompiling only
those files that need to be changed.
■ Transport one of your programs to another machine or compiler and
compile it without needing to rewrite it.
This chapter offers solutions for these and other common programming
needs. We'll discuss how to use the C preprocessor for conditional
compilation and for creating macros. Next, we'll show you how to create
and manage QuickC's "program lists." Finally, we'll show you how to
develop custom C libraries and how to access them from within QuickC.
Advanced C Preprocessor
Although compiling under QuickC appears to be a single swift process, it
is actually three processes combined into one. First, your C program is
"preprocessed." In this phase, conditional compilation occurs, and other
preprocessing directives are executed: For example, #define MAX 3 converts
all instances of MAX to 3. Second, the QuickC compiler translates your
preprocessed code into machine language, or code that the computer can
understand. Finally, your compiled machine code is combined (linked) with
the precompiled code in the standard C Library of functions (such as
printf()) to form the finished, executable program.
Conditional compilation occurs in the preprocessing stage. Using lines
that begin with a # character (pronounced "pound" or "number"), you can
write code that compiles one way for testing and another for actual use.
You can also write code that compiles differently on different machines or
different compilers.
The C preprocessor recognizes only lines of text that begin with the #
character, such as #define and #include. Table 12-1 lists the complete
set of these "preprocessor directives."
Table 12-1 The Preprocessor Directives
╓┌─┌───────────────┌─────────────────────────────────────────────────────────╖
Directive Description
──────────────────────────────────────────────────────────────────────────
#define x y Uses x as an alias for y throughout the program.
#include <file> Reads file from the INCLUDE subdirectory and inserts it
into your program at this point.
#include "file" Reads file from the current working directory and inserts
it into your program at this point.
#ifdef x If x is defined, compiles all program code between this
and the next matching #endif, #elif, or #else.
#if (x) If the integer constant expression x is true (nonzero),
compiles all program code between this directive and the
next matching #endif, #elif, or #else.
Directive Description
──────────────────────────────────────────────────────────────────────────
next matching #endif, #elif, or #else.
#ifndef x If x is not defined, compiles all program code between
this and the next matching #endif, #elif, or #else.
#else The inverse of the above three if directives. If the if is
true, the code before the #else is compiled. If the if is
false, the code following the #else is compiled.
#elif (x) The else if extension for #if in a chain of conditions.
#endif Terminates the current matching #if, #ifdef, or #ifndef.
#line lineno Sets the current line number to lineno and the current
"file" file to file.
#pragma Sets "compiler-specific" options.
#define x(y) z Defines preprocessor macros.
──────────────────────────────────────────────────────────────────────────
Conditional Compilation
Occasionally, you will need to compile only part of your code──for
example, during debugging, or when you compile different versions for
different users, or while compiling your program on a different computer
or compiler. The C preprocessor offers an assortment of directives to
facilitate this selective compiling process, called "conditional
compilation."
The #if and #endif Directives
The most frequently used conditional directives are #if and #endif. The
#if directive tests what is known as a restricted constant expression in
your code to see if that expression is zero. If it is a nonzero (true)
value, QuickC compiles all the code between that #if and its matching
#endif. Use the directive as follows:
#define BYTES 4
#if (BYTES == 4)
/* compile this code */
#endif
In this example, the expression (BYTES == 4) is a "constant expression"
because it becomes (4 == 4) (the logical comparison of two integer
constants). It is also a "restricted" constant expression, which is a
constant expression that cannot contain:
■ sizeof operations
■ enumerated constants
■ typecasts
■ floating-point constants
Therefore, the following directives are legal:
#if (BYTES < 8)
#if ((6 * 9 / 3) != (2 % 1))
and the following are not:
#if (sizeof(int) == 4)────────────────────────────────────sizeof illegal
enum {true, false} yorn;
#if (true == 0)──────────────────────────────Enumerated constant illegal
#if (NULL == (char *)0)─────────────────────────────────Typecast illegal
#if (MIN < 4.2)───────────────────────────────────float constant illegal
One common use for the #define directive is in debugging. The program in
Listing 12-1 on the following page, BUG.C, illustrates one possible way
to use #define to change the behavior of your program. By using #define to
define DEBUG_LEVEL to one of the values 0, 1, or 2, then recompiling and
running, you will cause the program to print one of three messages to your
screen. For a #define value of 0, nothing is printed; for 1, the calls to
the subroutine sub() are documented; and for 2, entry into and exit from
main() are printed.
──────────────────────────────────────────────────────────────────────────
/* bug.c -- shows how different levels of debugging */
/* output can be produced using #if */
#define DEBUG_LEVEL 2 /* 0 = none, 1-2 for debug */
#include <stdio.h>
main()
{
int ret;
#if (DEBUG_LEVEL == 2)
fprintf(stderr, "Entering main()\n");
#endif
#if (DEBUG_LEVEL == 1)
fprintf(stderr, "Calling sub()\n");
#endif
ret = sub();
#if (DEBUG_LEVEL == 1)
fprintf(stderr, "sub() returned %d\n", ret);
#endif
#if (DEBUG_LEVEL == 2)
fprintf(stderr, "Leaving main()\n");
#endif
}
sub()
{
return (5);
}
──────────────────────────────────────────────────────────────────────────
Listing 12-1. The BUG.C program.
defined and #ifdef
You can use the defined keyword with the #if directive to detect whether
or not a name has been specified by #define:
#if defined(name)
If defined(name) determines that name was used in a #define directive, it
evaluates to true. The keyword defined is used by the preprocessor only in
this context; therefore, you can use it anywhere in your program without
causing a conflict.
The defined variation of #if replaces the pre-ANSI directive #ifdef. That
is, although the following are equivalent:
#if defined(name)
#ifdef name
the first form is preferable.
You can use the same technique to see if a name has not been specified
with #define, as follows:
#if !defined(name)
#ifndef name
Again, the first form is preferable to the second.
The defined variation of #if is especially useful for writing programs
that will be compiled on another type of computer or a different compiler.
The BITOUT.C program (Listing 12-2) is an adaptation of the Bitout()
function used in the BITWISE.C program (Listing 7-12 on p. 218). After
the user enters an integer, the program prints that integer in binary
form. Note that it uses #if defined to print the bits one way on an
80286-based computer and another way on a 68000-based machine.
──────────────────────────────────────────────────────────────────────────
/* bitout.c -- compiles one way on an IBM PC and */
/* another on a 68000 chip─based machine */
#define CHIP_80286 /* don't define on a 68000 machine */
#include <stdio.h>
main()
{
int num;
printf("Enter an integer number and I will print"
" it out in binary\nNumber: ");
if (scanf("%d", &num) != 1)
{
fprintf(stderr, "Not an integer\n");
exit(1);
}
Bitout(num);
}
Bitout(unsigned int num)
{
int i, j;
unsigned char *cp;
cp = (char *)#
#if defined(CHIP_80286) /* IBM PC */
for (i = 1; i >= 0; --i)
#endif
#if !defined(CHIP_80286) /* otherwise 68000 machine */
for (i = 0; i < 4; ++i)
#endif
{
for (j = 7; j >= 0; --j)
putchar((cp[i] & (1 << j)) ? '1' : '0');
}
putchar('\n');
}
──────────────────────────────────────────────────────────────────────────
Listing 12-2. The BITOUT.C program.
#else and elif
We can simplify the two #if directives in BITOUT.C by using the #else
directive:
#if defined(CHIP_80286)
for (i = 1; i >= 0; --i)
#else
for (i = 0; i < 4; ++i)
#endif
In this example, the preprocessor compiles the first for statement if
CHIP_80286 has been defined using #define; otherwise, it compiles the
second for statement.
By using the #elif (else if) directive, you can create a whole chain of
conditions. The following series of directives, for example,
#if defined(CHIP_8086)
for (i = 1; i >= 0; --i)
#elif defined(CHIP_80286)
for (i = 1; i >= 0; --i)
#elif defined(CHIP_68000)
for (i = 0; i < 4; ++i)
#else
fprintf(stderr, "Unknown chip\n");
return;
#endif
tells the preprocessor to compile the first for statement if CHIP_8086 is
defined, to compile the second for statement if CHIP_80286 is defined, or
to compile the third for statement if CHIP_68000 is defined. If none of
these is defined, the preprocessor compiles code to print an error and
return.
Logical Operators and #if
Many of the preceding #if tests use similar code. You can take a coding
shortcut by combining #if expressions using the C logical operators && and
||. For example, you can shorten the previous #elif sequence by using the
logical OR operator as follows:
┌──────────────────────────────────── Logical OR
#if defined(CHIP_8086) || defined(CHIP_80286)
for (i = 1; i >= 0; --i)
#elif defined(CHIP_68000)
for (i = 0; i < 4; ++i)
#else
fprint(stderr, "Unknown chip\n");
return;
#endif
The #if directives and their corresponding #endif and #elif directives can
be nested. However, when you nest them, we recommend that you use indents
to show the levels of nesting, as follows:
#if defined(IBMPC)
#if defined(CGA) || defined(EGA)
sp = (int far *)0xB8000000;
#else
sp = (int far *)0xB0000000;
#endif
#else
fprintf(stderr, "No Screen Memory\n");
return;
#endif
In this example, if IBMPC is not defined, the last #else executes. If
IBMPC is defined, the program checks to see if either CGA or EGA (for the
corresponding graphic adapter cards) is defined. If either is, we assign
the address value 0xB8000000 (the location of screen memory for those
cards) to the pointer sp. Otherwise, we use the address 0xB0000000 (the
location of screen memory for the regular monochrome adapter).
You can avoid problems when using # preprocessor directives by remembering
two general rules. First, the # must always begin a line. Second, each
directive can occupy only one line unless you extend it by typing a
backslash and pressing Enter:
#if defined(EGA) \─────────────────────────────────────────Line extended
|| \────────────────────────────────────────────────Line extended
defined(CGA)
Predefined Names
QuickC always predefines two names: __FILE__ and __LINE__. (Note that both
have two leading and two trailing underscore characters.) The name
__FILE__ is always the name of the current C source file being compiled.
It is a quoted string constant, so you can safely use it anywhere that
strings are legal. The predefined name __LINE__ is an integer constant
number that is always the current line number in the current file. You can
use it anywhere as a legal integer constant.
These two predefined names are generally used to print meaningful
diagnostics during debugging. The ERR.C program (Listing 12-3)
demonstrates their use for tracing the flow of a small program. By placing
a #define ERR inside a #if directive, you can turn on and off custom
tracing with a single change in code:
#define TRACE 0 /* change to 1 to turn on */
#if (TRACE > 0)
#define ERR printf("Tracing: \"%s\" line %d\n",\
__FILE__, __LINE__ );
#else
#define ERR
#endif
If TRACE is defined as a value greater than zero, QuickC traces the
program. If, on the other hand, TRACE is 0, then tracing is disabled.
──────────────────────────────────────────────────────────────────────────
/* err.c -- illustrates __FILE__ and __LINE__ in */
/* tracing a small program */
#define ERR printf("Tracing: \"%s\" line %d\n",\
__FILE__, __LINE__);
main()
{
ERR
err1();
ERR
err2();
ERR
}
err1()
{
ERR
err2();
}
err2()
{
ERR
}
──────────────────────────────────────────────────────────────────────────
Listing 12-3. The ERR.C program.
#pragma Instructions to the Compiler
You can use the #pragma preprocessor directive to give compiler-specific
instructions to the compiler (that is, instructions that usually must be
given as part of the MS-DOS command line or by presetting QuickC's compile
time options). Use it in the following way:
#pragma instruction
#pragma pack(1|2|4)
The pack pragma tells the compiler to place structure members into memory
on 1-byte, 2-byte, or 4-byte boundaries. Ordinarily, QuickC places
structure members into memory so that int and long types always begin in
an even address, which is equivalent to pack(2). (See Figure 12-1a.) By
using the #pragma pack() preprocessor directive, you can tell the compiler
to store structures in a smaller space (see Figure 12-1b) or to spread
them out into a larger space with pack(4). (See Figure 12-1c.)
┌──
│ struct {
│ char a;
│ int b;
For the declaration ──┤ char c;
│ long d;
│ };
└──
┌───────┐─┐
400 │ │ ├─a
├───────┤─┤
401 │ │ │
┌───────┐─┐ ┌───────┐─┐ ├─ ─┤ │
402 │ │ ├─a 402 │ │ ├─a 402 │ │ ├─unused
├───────┤─┤ ├───────┤─┤ ├─ ─┤ │
403 │ │ ├─unused 403 │ │ │ 403 │ │ │
├───────┤─┤ ├─ ─┤ ├─b ├───────┤─┤
404 │ │ │ 404 │ │ │ 404 │ │ │
├─ ─┤ ├─b ├───────┤─┤ ├─ ─┤ ├─b
405 │ │ │ 405 │ │ ├─c 405 │ │ │
├───────┤─┤ ├───────┤─┤ ├───────┤─┤
406 │ │ ├─c 406 │ │ │ 406 │ │ ├─c
├───────┤─┤ ├─ ─┤ │ ├───────┤─┤
407 │ │ ├─unused 407 │ │ │ 407 │ │ ├─unused
├───────┤─┤ ├─ ─┤ ├─d ├───────┤─┤
408 │ │ │ 408 │ │ │ 408 │ │ │
├─ ─┤ │ ├─ ─┤ │ ├─ ─┤ │
409 │ │ │ 409 │ │ │ 409 │ │ │
├─ ─┤ ├─ d └───────┘─┘ ├─ ─┤ ├─d
410 │ │ │ 410 │ │ │
├─ ─┤ │ ├─ ─┤ │
411 │ │ │ 411 │ │ │
└───────┘─┘ └───────┘─┘
(A) RESULT OF USING (B) RESULT OF US
ING (C) RESULT OF USING
#pragma pack (2) #pragma pack (1) #pragma pack (4)
(QuickC default)
Figure 12-1. The pack() pragma determines how structures are placed
into memory.
The PACK.C program (Listing 12-4) illustrates this structure packing.
When you run the program, note the addresses it prints. Then change the 1
in #pragma pack(1) to a 2 and recompile and run PACK.C again. Finally,
change that 2 to a 4 and repeat the process.
An extension to the #pragma pack() directive lets you turn packing on and
off:
#pragma pack(1)─────────────────────────────────────Set one-byte packing
...
#pragma pack() no───────────────────────────────────────Turn packing off
...
#pragma pack() yes──────────────────────────────────Turn packing back on
The example first tells the compiler to pack all structure members to the
nearest 1-byte boundary. Next, the no tells the compiler to stop packing
and revert to its default even-byte boundary arrangement. Finally, the yes
tells the compiler to resume packing on 1-byte boundaries.
──────────────────────────────────────────────────────────────────────────
/* pack.c -- demonstrates structure packing with */
/* the #pragma pack() directive */
#pragma pack(4) /* 1, 2 or 4 */
main()
{
struct {
char ch1;
int int1;
char ch2;
long int2;
} s;
printf("ch1 -> %lu\n", (unsigned long)(&s.ch1));
printf("int1 -> %lu\n", (unsigned long)(&s.int1));
printf("ch2 -> %lu\n", (unsigned long)(&s.ch2));
printf("int2 -> %lu\n", (unsigned long)(&s.int2));
}
──────────────────────────────────────────────────────────────────────────
Listing 12-4. The PACK.C program.
──────────────────────────────────────────────────────────────────────────
Quick Tip
The Intel 80386 chip executes at its fastest if int and long types begin
on modulo 4-byte address boundaries. The Intel 80286 and earlier chips
execute fastest when those types begin on even addresses. If size is more
important to you than speed, use the #pragma pack(1) directive.
──────────────────────────────────────────────────────────────────────────
Preprocessor Macros
The #define preprocessor directive has a second form that is called a
#define macro, or a preprocessor macro. The #define macro is an extremely
powerful tool, used by programmers to place "in-line" code into a program
in a manner that resembles a subroutine call. Take a moment to use
QuickC's View Include feature to look at the <stdio.h> header file. Notice
in line 105 of that file that the getc() function you have been using all
along is not really a function at all. It is a #define macro. Because it
is a macro, the preprocessor expands each occurrence of getc(stdin) in
your program to the following:
(────(stdin)->cnt >= 0 ? 0xFF & *(stdin)->_ptr++ : _filbuf(stdin))
Certainly, it is easier to type getc(stdin) than to type this complex code
sequence.
The form for a #define macro is as follows:
#define TRIPLE(x) (x*3)
In this example, the defined name is TRIPLE and the (x) is its formal
argument. The expression TRIPLE(x) is defined as an alias for the
expression (x*3). This means that anywhere in the program that you use the
following expression:
TRIPLE(2)
the actual argument (here 2) replaces every occurrence of the formal
argument, x, in the original definition. This produces the following
expansion:
TRIPLE(2)
│
├───────────────────────────────────────────────────────── Expands to
│
▼
(2*3)
To illustrate further, examine the following macro definition for MAX, a
macro that compares two values and yields a new value that is the higher
of the two:
┌─┬──────────────────────────────────── Two formal arguments
#define MAX(x,y) (((x) > (y)) ? (x) : (y)) separated by a comma
└──┬───┘ └──────────┬───────────┘
│ └────────────────────────── Macro definition
└────────────────────────────────────────────────────── Macro
This example shows that macros can take more than one formal argument──but
arguments must be separated from one another by commas. The x in the macro
replaces each x in the macro definition with its corresponding actual
argument, and each y replaces its corresponding y. If you use the above
macro definition in your code and then use the following expression:
oldest = MAX(age1, age2);
with int variables age1 and age2, the preprocessor expands the macro as
follows:
oldest = (((age1) > (age2)) ? (age1) : (age2));
Potential Problems with Macros
Use preprocessor macros with care──actual arguments to macros can cause
unexpected changes, such as reading an extra character. You should avoid
using the following types of arguments because they can produce unwanted
side effects:
■ function calls
■ other macros
■ the increment (++) and decrement (--) operators
■ the assignment operator (=)
For example, consider the following ISQ macro:
#define ISQ(letter) ((letter) == 'q' || (letter) == 'Q')
This macro detects whether a letter is an uppercase or lowercase 'Q' and
is useful for testing if a user is quitting a program. You correctly use
this macro as follows:
ch = getchar();
if (ISQ(ch))
exit(0);
In the preceding code, ch is a char variable; therefore, the if statement
expands to
if (((ch) == 'q' || (ch) == 'Q'))
which is what you expect. However, if you use this macro incorrectly──for
example, with a function call such as getchar(),
if (ISQ(getchar()))
it expands to an expression that doesn't do what you expect:
if (((getchar()) == 'q' || (getchar()) == 'Q'))
This example illustrates a common problem. The first call to getchar()
reads a character and compares the value to 'q'. If that character is not
a 'q', getchar() is called again to read a new character and to compare
the new character to 'Q'. This is not what you intended, however. You want
MAX to read only the first character and then to compare that character to
both 'q' and 'Q'.
Macros and Semicolons
Never end a macro definition with a semicolon. For example, the following
macro converts a printable character into a control character value:
#define CTRL(x) ('x' - '@');
The expression CTRL(A) expands to the expression ('A' - '@'); and yields
the desired ASCII value 1 (Ctrl-A). However, the trailing semicolon causes
a syntax error when you use the macro in an expression such as:
printf("And 'A' prints as %c\n", CTRL(A));
Note the syntax error that results when this expands to
printf("And 'A' prints as %c\n", ('A'-'@'););
└────────────────────── Wrong
Macros and Quotes
As in normal #define directives, preprocessor macros do not substitute
actual arguments inside full quotation marks. For example, the following
macro would be a useful tool for debugging:
#define PERR(x) printf("The value of x is %d\n", x)
Unfortunately it won't work. Because the first x is inside full quotation
marks, it isn't expanded. However, the final x is expanded:
int val = 5;
PERR(val);
└────┬───┘
└───────────┬─────────────────────────────────────────── Expands to
│
┌────────────────▼──────────────────┐
printf("The value of x is %d\n", val);
We can rectify this by using the preprocessor's "stringizing" operator #.
When placed before a formal argument in a macro definition, the # causes
that argument to be expanded and quoted. Thus, the correct way to define
PERR is as follows:
#define PERR(x) printf("The value of " #x " is %d\n", x)
└─────────── Stringizing operator
This correctly expands as:
int val = 5;
PERR(val);
└───┬───┘
└───────────┬──────────────────────────────────────────── Expands to
│
┌───────────────▼───────────────────────────┐
printf("The value of " "val" " is %d\n", val);
The example works because the compiler joins adjacent quoted string
constants into a single string. The result is that printf() correctly
prints the following:
The value of val is 5
Using QuickC for Large Projects
Imagine you are writing a text editor program such as the one shown in
Figure 12-2 on p. 381. With sufficient memory, QuickC can easily load and
compile programs of this size. However, the larger a program is, the
longer it takes to compile, load, and save. Therefore, you can manage
large programs more easily when you break them into several smaller files
by logically grouping the subroutines according to use. This approach has
several advantages.
■ When a program consists of several files, you need to recompile only
those files that change.
■ Grouping subroutines by usage lets you easily trace the logic of the
program during debugging.
■ Perfected subroutines that no longer need to be recompiled can be
shared by many programs.
QuickC Program Lists
The QuickC "program list" feature compiles several small files or library
modules and combines them into a single executable program. This lets you
create complex, large programs from many small, easily maintained files.
Before we examine this feature, enter and save the following three files:
TEXED.C (Listing 12-5), KEYS.C (Listing 12-6), and FILE.C (Listing
12-7 on the following page). These are three small pieces of our
imaginary text editor in Figure 12-2. Although these modules don't do
much, they demonstrate the basics of using QuickC program lists.
──────────────────────────────────────────────────────────────────────────
/* texed.c -- main entry point to the editor; the */
/* menu and signal handlers are here */
main(argc, argv)
int argc;
char *argv[];
{
char ch;
while (1)
{
printf("\nTexEd Main Menu\n");
printf("Select from:\n");
printf("0) Quit\n\n");
printf("1) Load File\n");
printf("2) Save File\n");
printf("3) Edit File\n");
printf("Which: ");
do
{
ch = getch();
ch -= '0';
} while (ch < 0 || ch > 3);
printf("%d\n\n", (int)ch);
switch(ch)
{
case 0: exit(0);
case 1: Load_file(); break;
case 2: Save_file(); break;
case 3: Edit_file(); break;
}
}
}
──────────────────────────────────────────────────────────────────────────
Listing 12-5. The TEXED.C file.
2000-line text editor Broken into separate files
┌───────────────────┐ ┌─┌───────────────────┐
│ │ │ │ Main() │
│ │ │ │ Signal handlers │────texted.c
│ │ │ │ menu() │
│ │ │ ├───────────────────┤
│ │ │ │ │
│ │ │ │ Read and process │────keys.c
│ │ │ │ typed keys │
│ │ │ ├───────────────────┤
│ │ │ │ │
│ │───texted.c───►│ │ Update the │────screen.c
│ │ │ │ screen │
│ │ │ ├───────────────────┤
│ │ │ │ │
│ │ │ │ Special commands │────cmds.c
│ │ │ │ like 'delete line'│
│ │ │ ├───────────────────┤
│ │ │ │ │
│ │ │ │ File read and │────file.c
│ │ │ │ write routines │
└───────────────────┘ └─└───────────────────┘
Figure 12-2. A large program is often best split into several smaller and
more manageable files.
──────────────────────────────────────────────────────────────────────────
/* keys.c -- The keyboard input-handling routines */
/* for the texed editor */
Edit_file()
{
char ch;
printf("\nYou are now in the editor.\n");
printf("Press 'Q' to exit back to main menu.\n");
do
{
ch = getch();
putch(ch);
} while (ch != 'Q');
printf("\n\n");
}
──────────────────────────────────────────────────────────────────────────
Listing 12-6. The KEYS.C file.
──────────────────────────────────────────────────────────────────────────
/* file.c -- the file I/O routines for texed */
Load_file()
{
printf("\nLoading ..... done.\n");
}
Save_file()
{
printf("\nSaving ...... done.\n");
}
──────────────────────────────────────────────────────────────────────────
Listing 12-7. The FILE.C file.
Next select Set Program List from the File menu and enter texed.mak in the
File Name text box. This program list name is composed of two parts. The
first, texed, is the name of your finished program. The second, the
extension .mak, signifies that this program list file is a "make" file.
(We'll explain make files in the next section.)
After you enter the name texed.mak, QuickC prompts "`texed.mak' does not
exist Create?". A Yes response displays the Edit Program List dialog box.
This is where you specify the files in your program list. Enter the
filenames TEXED.C, KEYS.C, and FILE.C. As you enter each, its name appears
in the bottom window labeled Program List. After you enter all three
files, your screen appears as in Figure 12-3. Now choose the Save List
option to save your program list on disk and return to the main QuickC
screen.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 12-3 can be found on p.382 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 12-3. The Edit Program List dialog box.
At the bottom left of the QuickC screen you now see Program List: Texed.
This message signals that you will build your program from several files,
not only from the one currently loaded, and that those files are in the
program list named Texed. TEXED.MAK has three files in it, and TEXED.C is
one of them. Now, every time you open TEXED.C, QuickC reminds you that a
.MAK file and various other files are connected to it.
To compile a program from a program list, display the Compile dialog box,
but this time, instead of selecting Compile File, select Build Program.
Notice that each of your files is loaded in turn and compiled. After all
three have been compiled, the Microsoft Overlay Loader executes. This link
program combines your compiled files, along with any precompiled routines
that you use from the standard C Library (such as printf()). This process
creates a single, executable program that you can run from within QuickC.
Program List Files
Program list files contain rules and instructions that tell QuickC how to
build your program. They are composed of four elements: comment lines
(lines that begin with a # character), production rules, dependencies, and
link commands. Look inside the TEXED.MAK file that follows. This is a make
file, a subset of the kind used by the MAKE program.
#
# Program; Texed
#
.c.obj:───────────────────────────Production rule--turn a .C into a .OBJ
qcl -c -W1 -Ze -AM $*.c─────────How to accomplish the above rule
texed.obj : texed.c────────────┬────────────────────────────Dependencies
│
keys.obj : keys.c──────────────┤
│
file.obj : file.c──────────────┘
Texed.exe : texed.obj keys.obj file.ob─More dependencies used to go from
del Texed.lnk .OBJs to. EXEs
echo texed.obj+ >>Texed.lnk
echo keys.obj+ >>Texed.lnk
echo file.obj >>Texed.lnk
echo Texed.exe >>Texed.lnk
echo Texed.map >>Texed.lnk
link @Texed.lnk /NOI $(LDFLAGS);
Production Rules in Program List Files
A production rule is a description of how one file type is changed into
another. For example, the production rule in our program list
.c.obj:
└──┴────────────────────────────────────Turn a .C file into a .OBJ file
tells QuickC to change .C files (such as KEYS.C) into precompiled object
files called .OBJ files (such as KEYS.OBJ). Microsoft's Overlay Loader
later links these .OBJ files with functions from the standard C Library to
produce the executable program.
The line following the production rule is the command line:
qcl -c -W1 -Ze -AM $*.c
This tells QuickC how to make the transformation specified in the first
line. It is the same command line you would enter at the MS-DOS prompt,
except for the $*.c. The $*.c tells QuickC to use the name of a real .C
file, such as KEYS.C, at this position in the command line. The other
elements represent the following:
Element Description
──────────────────────────────────────────────────────────────────────────
qcl The command-line version of QuickC.
-c Compile to a .OBJ file and stop. That is, do not continue by
calling LINK.
-W1 Set the compile time warning level to level 1.
-Ze Handle language extensions, such as far, as reserved keywords.
-AM Use the medium-memory model.
──────────────────────────────────────────────────────────────────────────
Production rules are one of the features of QuickC program lists that make
building programs easy. For example, when you select the Compile menu to
compile KEYS.C, you don't have to enter the command line. Instead, QuickC
runs the following MS-DOS command:
qcl -c -W1 -Ze -AM keys.c
and compiles KEYS.C into KEYS.OBJ.
Dependency Lines in the Program List File
Dependency lines tell QuickC how to create one file from two or more
files. A dependency has the following form:
target : infile1 infile2 ...
This tells QuickC to create a new "target" file (usually a .OBJ or .EXE)
if any infiles have changed and to make that target by running the
specified MS-DOS command line. Examine the following dependency lines in
TEXED.MAK:
texed.obj : texed.c
keys.obj : keys.c
file.obj : file.c
Texed.exe : texed.obj keys.obj file.obj
del Texed.lnk
...
Note that each of the first three lines is followed by a blank line (no
MS-DOS command line). We'll discuss these first three dependencies first,
then we'll cover the last in detail.
When a dependency does not specify an MS-DOS command line, QuickC uses the
previously defined production rule (.c.obj:) in place of the missing
command line. For the first three lines, then, the command line derived
from the production rule becomes the following:
texed.obj : texed.c
qcl -c -W1 -Ze -AM texed.c
keys.obj : keys.c
qcl -c -W1 -Ze -AM keys.c
file.obj : file.c
qcl -c -W1 -Ze -AM texed.c
Running the Linker
The dependency for TEXED.EXE, the executable file of your finished
program, is as follows:
Texed.exe : texed.obj keys.obj file.obj───────────────────────Dependency
The dependency tells QuickC to create TEXED.EXE from TEXED.OBJ, KEYS.OBJ,
and FILE.OBJ.
The LINK program combines your .OBJ files with subroutines from the
standard C Library and creates an executable (.EXE) program as the result.
When you run the LINK program, it asks you the following series of
questions:
Object Modules [.OBJ]:
Run File [.EXE]:
List File [NUL.MAP]:
First, LINK asks for the names of the .OBJ files you want to combine to
form your program. Add a + to each file that you specify to tell LINK that
you will add more .OBJ files. The LINK program continues to prompt for
object modules until you list one without a trailing +:
Object Modules [.OBJ]:texed.obj+
Object Modules [.OBJ]:keys.obj+
Object Modules [.OBJ]:file.obj
Next, LINK prompts for the name of your executable program (Run File). In
our example, we enter the name texed.exe.
Finally, LINK asks for the name of a "map" file. A map file merely
contains a cross-referenced listing of your executable program. Therefore,
you respond as follows:
List File [NUL.MAP]:texed.map
Using LINK from a Text File
When you run LINK, you have the option to use a text file that contains
the prewritten answers to its questions. To do this, when you run LINK,
specify the file by preceding its name with an "at" character, @, as
follows:
link @Texed.lnk
└──────── File questions containing answers to LINK's questions
To understand how this works, let's examine the MS-DOS commands that
follow the dependency for TEXED.EXE in your TEXED.MAK program list file:
Texed.exe : texed.obj keys.obj file.─────────────── Dependency
del Texed.lnk──────────────────────┐
echo texed.obj+ >>Texed.lnk │
echo keys.obj+ >>Texed.lnk │
echo file.obj >>Texed.lnk ├─────── MS-DOS command lines
echo Texed.exe >>Texed.lnk │
echo Texed.map >>Texed.lnk │
link @Texed.lnk /NOI $(LDFLAGS);───┘
First, the file TEXED.LNK is deleted in case it already exists. Next, five
"redirect and append" commands (>>) tell echo to place the text into the
file TEXED.LNK. TEXED.LNK now contains the following lines:
texed.obj+
keys.obj+
file.obj
Texed.exe
Texed.map
Finally, LINK executes using the file @TEXED.LNK as the file that contains
the answers to its questions. This is equivalent to running LINK yourself
and answering those questions as follows:
Object Modules [.OBJ]:texed.obj+
Object Modules [.OBJ]:keys.obj+
Object Modules [.OBJ]:file.obj
Run File [.EXE]:Texed.exe
List File [NUL.MAP]:Texed.map
Other Arguments to LINK
The LINK command in our TEXED.MAK program list file has two arguments in
addition to the @TEXED.LNK argument:
link @Texed.lnk /NOI $(LDFLAGS);
│ └──────────────────────────────── Other flags
└────────────────────────────────── Don't ignore case
The first, /NOI, tells LINK not to ignore case. That is, it tells LINK to
treat uppercase letters as different from lowercase letters in variable
and function names.
The second, $(LDFLAGS), is a make macro definition that QuickC defines as
nothing. To modify your .MAK program list file and add arguments to LINK,
you must define LDFLAGS. See Chapter 11 in your Microsoft QuickC
Programmer's Guide for information about this procedure. But beware, the
LDFLAGS macro is the only macro that QuickC recognizes and preserves.
Keeping Track of Changes
QuickC keeps track of which files have changed in a program list. To see
how this works, select Build Program from the Run menu. Now load the
KEYS.C file and change it by inserting a blank line anywhere. Save that
file and select Build Program again. This time, QuickC recompiles the
KEYS.C file, but it does not compile the other two .C files because they
haven't changed.
Before QuickC runs a command line to create the target file from the
infiles (based on a dependency in its program list), it first checks the
modification dates for the target file and for each of the infiles. If
this target was created after the infiles, QuickC doesn't need to
recompile because the infiles have not changed. Specifically, in the
dependency
texed.obj : texed.c
if TEXED.OBJ is newer than TEXED.C (its modification date and time is more
recent), then QuickC does not recompile because TEXED.C has not changed
since the last time it was compiled. But if TEXED.C is newer or if
TEXED.OBJ doesn't yet exist, QuickC creates a new TEXED.OBJ by applying
the .c.obj production rule and thus running the MS-DOS command line:
qcl -c -W1 -Ze -AM texed.c
This ability to know which files need to be recompiled makes QuickC a
powerful tool for developing large and complex programs that are composed
of many individual .C files.
Header Files
Programs formed from separate .C files often share identical declarations.
For example, examine the two files in Listings 12-8a and 12-8b on the
following page. These parts of a larger text editor program both use
structures of the same pattern, and both use the #define directive to
define the values OK and ERROR. (These listings are not intended to be
compiled and run independently.) If you need to change the structures (by
adding a member, for example), or to change the definition of ERROR (from
1 to -1, for example), you must make changes in both files (and possibly
many other files if the text editor program uses those values throughout).
──────────────────────────────────────────────────────────────────────────
#define OK 1
#define ERROR 0
menu()
{
struct key_struct {
char key;
unsigned char move;
} *kp, *Read_kbd();
int cur_key, cur_move;
kp = Read_kbd();
cur_key = kp->key;
cur_move = kp->move;
if (cur_key == ERROR)
return (cur_move);
return (cur_key);
}
──────────────────────────────────────────────────────────────────────────
Listing 12-8a. The TEXED.C file.
──────────────────────────────────────────────────────────────────────────
#define OK 1
#define ERROR 0
struct key_struct {
char key;
unsigned char move;
};
struct key_struct *Read_key()
{
struct key_struct k;
k.key = getch();
if (k.key == ERROR)
k.move = getch();
return (&k);
}
──────────────────────────────────────────────────────────────────────────
Listing 12-8b. The KEYS.C file.
Therefore, your program is easier to maintain if you gather such common
definitions into a single, separate file called a "header file," or a .h
file. Listing 12-9 shows one such header file for our text editor
program. Now you can easily make changes that affect all files. Simply
modify TEXED.C and KEYS.C to use the #include preprocessor directive, as
shown in Listings 12-10a and 12-10b. Because we use full quotation marks
with that directive (rather than angle brackets as with #include
<stdio.h>), the compiler looks for the header file in our current working
directory.
──────────────────────────────────────────────────────────────────────────
#define OK 1
#define ERROR 0
struct key_struct {
char key;
unsigned char move;
};
──────────────────────────────────────────────────────────────────────────
Listing 12-9. The texed.h header file.
──────────────────────────────────────────────────────────────────────────
#include "texed.h"
menu()
{
struct key_struct *kp, *Read_kbd();
int cur_key, cur_move;
kp = Read_kbd();
cur_key = kp->key;
cur_move = kp->move;
if (cur_key == ERROR)
return (cur_move);
return (cur_key);
}
──────────────────────────────────────────────────────────────────────────
Listing 12-10a. The TEXED.C file (modified).
──────────────────────────────────────────────────────────────────────────
#include "texed.h"
struct key_struct *Read_key()
{
struct key_struct k;
k.key = getch();
if (k.key == ERROR)
k.move = getch();
return (&k);
}
──────────────────────────────────────────────────────────────────────────
Listing 12-10b. The KEYS.C File (modified).
Variables in Header Files
You can also place declarations in header files that make variables global
to all files. However, you cannot initialize variables in header files
that are shared by more than one .C file. That is, in the header file
texed.h,
char Last_key;─────────────────────────────────────────────────────Legal
int Upper_flag = 1;─────────────────────────────────────────────Illegal
the declaration for Last_key is always legal, but the declaration for
Upper_flag is illegal because this .h file is specified by #include in
several .C files.
You can declare and initialize a global variable only once in a program.
If you want to declare and initialize a global variable in one file and
access that variable from another file, you must make this an explicit
operation by placing the extern keyword in the second file, as follows:
/* First file */ /* Second file */ /* Third file */
... ... ...
int Key = 1; extern int Key; extern int Key;
└────────────────────────────────────── Initialized once among
several files
The extern keyword tells QuickC that the integer Key is located in another
file.
If a global variable is not initialized as part of its declaration, you
can declare it in all files without the extern keyword, as follows:
/* First file */ /* Second file */ /* Third file */
... ... ...
int Key = 1; extern int Key; extern int Key;
The extern keyword tells QuickC that the integer Key is located in another
file.
If a global variable is not initialized as part of its declaration, you
can declare it in all files without the extern keyword, as follows:
/* First file */ /* Second file */ /* Third file */
... ... ...
int Key; int Key; int Key;
This is the same as declaring it once in a header file and then specifying
that header with #include, as follows:
/* Header file "head.h" */
...
int Key;
/* First file */ /* Second file */ /* Third file */
... ... ...
#include "head.h" #include "head.h" #include "head.h"
──────────────────────────────────────────────────────────────────────────
Dependencies in Header Files
Because a change in a header file results in a change in a .C file, you
might wonder if you can place header files into your QuickC program list
as a dependency, as follows:
texed.obj : texed.c texed.h
keys.obj : keys.c texed.h
As this dependency is written, it tells QuickC (in our program list) to
recompile TEXED.OBJ if either TEXED.C or texed.h changes and to recompile
KEYS.OBJ if either KEYS.C or texed.h changes.
QuickC does allow you to place header file dependencies into your program
lists: It recognizes and maintains them, but it does not treat .h files as
real dependencies. That means, for example, that TEXED.OBJ is not
recompiled if only texed.h changes. This is intentional and not a bug.
──────────────────────────────────────────────────────────────────────────
Libraries
In addition to listing .C files in a QuickC program list file, you can
also list library (.LIB) files. Libraries are files that contain
precompiled .OBJ files that you can use as part of a program.
During the course of your programming, you will develop many general
subroutines that can be used in many programs. By placing those
subroutines into a special library, you can access them through a QuickC
program list without having to recompile them. For example, consider the
following three subroutines: leftstr.c (Listing 12-11), midstr.c (Listing
12-12), and rightstr.c (Listing 12-13). (These subroutines, shown on
pages 392-93, are C analogs to the BASIC functions LEFT$, MID$, and
RIGHT$.)
To create a library for these three subroutines, enter them using the
QuickC editor, and then save each as an individual .C file. Now exit
QuickC and compile each with qcl and the following MS-DOS commands:
qcl /c /AM leftstr.c
qcl /c /AM midstr.c
qcl /c /AM rightstr.c
qcl is the command-line version of QuickC. The /c tells QuickC to create a
.OBJ file from the .C file, and /AM tells QuickC to use the medium-memory
model.
After you generate the three .OBJ files, you create a library for them by
running the LIB program and answering its questions, as follows:
Library name:basic.lib
Library does not exist. Create?y
Operations:+leftstr.obj&
Operations:+midstr.obj&
Operations:+rightstr.obj
List file:
The first and second lines tell LIB to create a library named BASIC.LIB.
In the three Operations: lines, the + tells LIB that we are adding a .OBJ
file to the library. The & following two of the lines is a signal that
more files will be added. At the List file prompt we simply press Enter
because our library is small, and we don't need a list of its contents.
After a short wait, QuickC produces a library file named BASIC.LIB that we
can place into any program list. To return to the QuickC menu, enter exit
at the MS-DOS prompt.
Now we'll create a program to test our library and demonstrate how to use
a library from a program list. Enter the TEST.C program (Listing 12-14 on
p. 393) and save it on disk. Next, choose Set Program List from the File
menu and enter TEST.MAK as the name of the program list.
After you press Enter and answer Yes to Test.mak doesn't exist. Create?,
the Edit Program List dialog box appears. Select test.c as the first item
in the list. Notice that the name of our library is not displayed. That's
okay; simply type basic.lib. Finally, save this program list.
──────────────────────────────────────────────────────────────────────────
/* leftstr.c -- a C version of BASIC's LEFT$ */
#include <stdio.h>
char *Leftstr(char *str, int cnt)
{
static char *cp = NULL;
char *malloc();
if (cnt > strlen(str))
cnt = strlen(str);
if (cp != NULL)
free(cp);
if ((cp = malloc(cnt + 1)) == NULL)
return (NULL);
strncpy(cp, str, cnt);
return (cp);
}
──────────────────────────────────────────────────────────────────────────
Listing 12-11. The leftstr.c subroutine.
──────────────────────────────────────────────────────────────────────────
/* midstr.c -- a C version of BASIC's MID$ */
#include <stdio.h>
char *Midstr(char *str, int where, int cnt)
{
static char *cp = NULL;
char *malloc();
if (cnt > strlen(str + where))
cnt = strlen(str + where);
if (cp != NULL)
free(cp);
if ((cp = malloc(cnt + 1)) == NULL)
return (NULL);
strncpy(cp, str+where, cnt);
return (cp);
}
──────────────────────────────────────────────────────────────────────────
Listing 12-12. The midstr.c subroutine.
──────────────────────────────────────────────────────────────────────────
/* rightstr.c -- a C version of BASIC's RIGHT$ */
#include <stdio.h>
char *Rightstr(char *str, int cnt)
{
static char *cp = NULL;
char *malloc();
if (cnt > strlen(str))
cnt = strlen(str);
if (cp != NULL)
free(cp);
if ((cp = malloc(cnt + 1)) == NULL)
return (NULL);
strcpy(cp, str + strlen(str) - cnt);
return (cp);
}
──────────────────────────────────────────────────────────────────────────
Listing 12-13. The rightstr.c subroutine.
──────────────────────────────────────────────────────────────────────────
/* test.c -- tests the routines in basic.lib */
/* Program list: test.c and basic.lib */
#include <stdio.h>
main()
{
static char string[] = "This is a test.";
char *cp, *Leftstr(), *Midstr(), *Rightstr();
printf("Testing: \"%s\"\n", string);
if ((cp = Leftstr(string, 4)) == NULL)
{
printf("Error in Leftstr()\n");
exit(1);
}
printf("Leftstr() returned: \"%s\"\n", cp);
if ((cp = Midstr(string, 4, 5)) == NULL)
{
printf("Error in Midstr()\n");
exit(1);
}
printf("Midstr() returned: \"%s\"\n", cp);
if ((cp = Rightstr(string, 5)) == NULL)
{
printf("Error in Rightstr()\n");
exit(1);
}
printf("Rightstr() returned: \"%s\"\n", cp);
}
──────────────────────────────────────────────────────────────────────────
Listing 12-14. The TEST.C program.
At the QuickC editor, choose Compile from the Run menu. Because we are
compiling from a program list, use Build Program to compile TEST.C and
then combine it with the subroutines in BASIC.LIB.
One additional advantage offered by .LIB files is that you can place them
in your environmental LIB directory. From there, QuickC can find them no
matter where you are in the directory hierarchy. The result of all this is
that you need only one copy of common subroutines in a single library, and
you can access those subroutines through a program list from any
directory.
Quick Libraries
QuickC offers another kind of library, called a Quick Library. This
alternative library can be loaded into memory when you first run QuickC.
The advantage it offers is that you don't need to use a program list to
access the subroutines in it.
Let's build a Quick Library using the same subroutines that we used to
build BASIC.LIB: leftstr.c (Listing 12-11), midstr.c (Listing 12-12),
and rightstr.c (Listing 12-13). Begin by running qcl to create the .OBJ
files:
qcl /c /AM leftstr.c
qcl /c /AM midstr.c
qcl /c /AM rightstr.c
Now run LINK to create the Quick Library:
link
Object Modules [.OBJ]:c:\lib\quicklib.obj +
Object Modules [.OBJ]:leftstr.obj +
Object Modules [.OBJ]:midstr.obj +
Object Modules [.OBJ]:rightstr.obj /Q
Run File [C:QUICKLIB.QLB]:basic.qlb /NOI
List File [NUL.MAP]:
Libraries [.LIB]:
In this example, c:\lib\quicklib.obj is the full pathname of a special
object file that you must use as the first listing in your Quick Library.
(We've stored the file in the C:\LIB directory, but you can use any
directory. We suggest that you specify your environmental LIB directory.)
The + characters tell LINK that we will list more object files. The /Q
after the last .OBJ tells LINK that this is a Quick Library. Then we
specify basic.qlb as the name of our Quick Library and follow that name
with /NOI, which tells LINK not to ignore case. Finally, we press Enter to
skip the final prompts, List File and Libraries.
Now you can have QuickC load the BASIC.QLB Quick Library every time you
run QuickC. To do this, use the QuickC /l command-line argument, as
follows:
qc /lbasic.qlb test.c
This tells QuickC first to load the BASIC.QLB Quick Library and then to
load TEST.C in the editor. (Before you do this on your system, erase the
TEST.MAK program list; otherwise, QuickC will try to use the .OBJ files
from the disk rather than from your new Quick Library.)
Now, when you compile TEST.C, QuickC always finds the functions Leftstr(),
Midstr(), and Rightstr() in memory. Notice how much faster TEST.C compiles
when you use this approach.
────────────────────────────────────────────────────────────────────────────
PART 4 C AND THE HARDWARE
────────────────────────────────────────────────────────────────────────────
────────────────────────────────────────────────────────────────────────────
Chapter 13 Keyboard and Cursor Control
Almost every PC program needs to get information from the keyboard and to
display information on a monochrome or color screen. So far, our programs
have used the standard C Library functions such as getchar(), scanf(),
putchar(), and printf(), and occasionally we've used command-line
arguments. Using these approaches produces portable code. However, it also
produces a bland interface that fails to take advantage of many PC
capabilities. If you want your programs to do more than display mere text
on the screen, study this and the next two chapters, which explore PC I/O.
You will learn how to use function keys and cursor control keys, how to
control the location and appearance of text on the screen, how to use
color in text and in graphics, and how to construct graphic figures.
In this chapter, we examine the keyboard and cursor control. We look at
QuickC's numerous I/O functions and provide a more detailed discussion of
the generic getchar() and the PC-specific getche() and getch(). We
describe scan codes, show how to use ANSI.SYS to redefine keys and to
provide cursor control, and discuss BIOS routines. Finally, we use the
int86() function to create a library of BIOS-based screen-control and
cursor-control functions.
Keyboard Input Functions
You use the standard C I/O functions to read and to display a variety of
input: characters, strings, integers, and floating-point numbers. But the
standard input functions don't detect non-ASCII keys, such as the function
keys. And they don't provide many of the input control features typically
required by programs such as word processors, spreadsheets, and games. To
get that control, we need to process input at a "lower" level than that of
standard I/O functions.
Three QuickC functions read keyboard input character by character:
getchar(), getche(), and getch(). Each reads one character at a time and
reports its value to the calling program. (Actually, getchar() is not a
true function; instead, it is defined as a macro in stdio.h.)
Input Examples
The programs on the opposite page illustrate how the three input functions
respond to the same input──in this case, the input is the word hat
followed by Enter.
The GETCHAR.C program (Listing 13-1) produces the following output:
Please enter a word.
hat<Enter>
1.. 2.. 3..───────────────────────Counting delayed until you press Enter
3 characters altogether
Counting doesn't start until you type the word and press Enter.
Next, look at GETCHE.C (Listing 13-2), which generates the following
output:
Please enter a word.
h1.. a2.. t3.. <Enter>───────────────────────────────────Immediate count
3 characters altogether
This time each letter is counted as it is typed.
Finally, examine GETCH.C (Listing 13-3), which produces the following
output:
Please enter a word.
1.. 2.. 3..──────────────────Input not displayed 3 characters altogether
This time the input is invisible; only the output is displayed.
The functions behave differently, and you use them for different purposes.
The getchar() function buffers and echoes input; getche() does not buffer
input but echoes it; getch() neither buffers nor echoes input. Buffered
input goes into a temporary storage area before being transferred to the
calling program. (Pressing Enter "empties" the buffer.) Echoed input is
displayed on the screen.
The getchar() function handles arrow keys or function keys inconsistently
from one system to another. Try using GETCHAR.C with these keys as input
and see how your system responds. The getche() and getch() functions do
read these keys in a consistent manner, however. Try GETCHE.C, for
example, with an arrow key or function key as input. Each of these keys,
as you'll see, is counted as two keystrokes, and characters other than
those you typed are echoed on the screen. This is perfectly proper and
reasonable behavior, as you'll see when we discuss scan codes.
──────────────────────────────────────────────────────────────────────────
/* getchar.c -- using getchar() */
#include <stdio.h>
main()
{
int count = 1;
printf("Please enter a word.\n");
while (getchar() != '\n') /* here it is */
printf("%d.. ", count++);
printf("\n%d characters altogether\n", count - 1);
}
──────────────────────────────────────────────────────────────────────────
Listing 13-1. The GETCHAR.C program.
──────────────────────────────────────────────────────────────────────────
/* getche.c -- using getche() */
#include <conio.h> /* note different file included */
main()
{
int count = 1;
printf("Please enter a word.\n");
while (getche() != '\r') /* changed comparison */
printf("%d.. ", count++);
printf("\n%d characters altogether\n", count - 1);
}
──────────────────────────────────────────────────────────────────────────
Listing 13-2. The GETCHE.C program.
──────────────────────────────────────────────────────────────────────────
/* getch.c -- using getch() */
#include <conio.h>
main()
{
int count = 1;
printf("Please enter a word.\n");
while (getch() != '\r')
printf("%d.. ", count++);
printf("\n%d characters altogether\n", count - 1);
}
──────────────────────────────────────────────────────────────────────────
Listing 13-3. The GETCH.C program.
The getchar() Buffer
The program using getchar() doesn't receive the generated code until this
buffer is flushed. This occurs when you press Enter or when the buffer is
filled. Because the getchar() buffer is 512 bytes, normal keyboard input
does not fill it. QuickC sets up this input buffer when any input function
from the stdio.h family is called, and all the input functions of that
family, such as scanf() and gets(), share it. Thus, when your program uses
both scanf() and getchar(), they share the same input buffer.
Differences in Usage
First, getchar() requires the stdio.h include file, while getch() and
getche() use conio.h, the include file for console I/O functions. Second,
getch() and getche use \r instead of \n to represent the action of Enter,
and they do not interpret Ctrl-Z as an end-of-file indicator.
The reason for these last two differences is that getchar(), by default,
reads input in the text mode, and getch() and getche() read input in the
binary mode. In the text mode, as you may recall, the carriage
return/linefeed combination is converted to a linefeed on input, and the
linefeed is converted to a carriage return/linefeed on output. The binary
mode makes no conversions. As a result, getchar() uses \n to detect the
Enter key, but getch() and getche() must use \r.
The second difference is that getchar(), when used in the text mode,
recognizes the Ctrl-Z character as marking the end of a file. This lets
you simulate the end-of-file condition from the keyboard by entering
Ctrl-Z. The binary mode used by getche() and getch() does not recognize
Ctrl-Z (or any other character) to mark the end of a file. As a result,
constructions such as
while((ch = getche()) != EOF) /* NO */
do not work for keyboard input. When using getch() or getche() in such a
loop, you must specify a keyboard character to indicate the end of input.
We've used \r, and in many later examples we'll use the Esc key.
Although the getchar() function uses text mode by default, you can call
QuickC's setmode() function to place getchar() in binary mode. (See
setmode() in the Microsoft QuickC Run-Time Library Reference for details.)
However, you cannot switch getche() and getch() to text mode.
──────────────────────────────────────────────────────────────────────────
Reminder
Don't mix buffered functions such as getchar() and gets() with unbuffered
functions such as getche() and getch(). The buffered functions transmit
characters from the input buffer when it is flushed; the unbuffered
functions read keys as they are pressed. Thus, a program mixing buffered
and unbuffered input functions might not process the characters in the
order they were typed.
──────────────────────────────────────────────────────────────────────────
Table 13-1 summarizes the different behavior of the character input
functions.
Table 13-1 Character Input Functions
getchar() getche() getch()
──────────────────────────────────────────────────────────────────────────
Buffered o
Echoes o o
Uses \n o
Uses \r o o
Uses stdio.h o
Uses conio.h o o
Text mode (default) o
Binary mode o o
Backspace editing o
Reads ASCII keys o o o
Reads non-ASCII keys o o
──────────────────────────────────────────────────────────────────────────
Typical Uses for Character Input Functions
The primary advantage of using the buffered getchar() is that it lets
users edit input with the Backspace key before they send it to the
program. The nonbuffered form, on the other hand, requires users to type
less because they needn't press Enter. For example, suppose your program
uses the following prompt:
Continue? <y/n>
With getchar(), the user must type y and press Enter, while getche()
requires only a y. Likewise, the getche() function is useful in programs
that use a typed character to select a menu item. Consider the following
fragment:
while ((ch = getchar()) != 'q') /* oops example */
switch (ch)
{
case 'a': ...
case 'b': ...
case 'c': ...
default: printf("Not a valid choice\n");
}
To choose case a, the user types a and presses Enter. The loop processes
the a, recycles and processes the \n generated by the Enter key, and
prints the default message. Replacing getchar() with getche() eliminates
the need to press the Enter key and hence the need to add programming to
process the extraneous \n.
The non-echoed, nonbuffered getch() is useful, of course, when you don't
want to display input on the screen. For example, you might use the k key
to move an image on the screen. Also, a program that requires a user to
type a secret password shouldn't display it on the screen.
Let's use getch() to construct a simple password program. In a real
application, we would ensure password security by also using encryption
and periodic updating. In the PASSWORD.C program (Listing 13-4 on the
following page), we'll build the password into the program and concentrate
on processing the user's input.
──────────────────────────────────────────────────────────────────────────
/* password.c -- requires a password to complete the */
/* program; illustrates a use of getch() */
#include <stdio.h>
#include <conio.h>
#include <string.h>
#define GUESS_LIMIT 4
#define WORD_LIMIT 10 /* maximum length of password */
#define TRUE 1
#define FALSE 0
char *Password = "I'mOk";
main()
{
int g_count = 0; /* guesses taken */
int w_count; /* letters accepted */
int in_count; /* letters entered */
char entry[WORD_LIMIT + 1];
char ch;
int correct, go_on;
do
{
puts("Enter the secret password.");
in_count = w_count = 0;
/* the following loop accepts no more chars */
/* than entry[] will hold, but keeps track */
/* of total number typed */
while ((ch = getch()) != '\r')
{
if (w_count < WORD_LIMIT)
entry[w_count++] = ch;
in_count++;
}
entry[w_count] = '\0';
if (in_count != w_count)
correct = FALSE; /* too many chars */
else
correct = (strcmp(entry, Password) == 0);
g_count++;
go_on = !correct && g_count < GUESS_LIMIT;
if (go_on)
puts("\nNo good; try again.");
} while (go_on);
if (!correct)
{
puts("Sorry, no more guesses. Bye.");
return(1);
}
puts("Welcome to Swiss bank account 2929100.");
puts("Your current balance is $10,232,862.61.");
}
──────────────────────────────────────────────────────────────────────────
Listing 13-4. The PASSWORD.C program.
Note the following loop:
while ((ch = getch()) != '\r')
{
if (w_count < WORD_LIMIT)
entry[w_count++] = ch;
in_count++;
}
It uses an if statement to prevent overflowing the array, yet it continues
to read additional characters if the limit is exceeded. We could have made
this loop stop at the character limit, but that would tell the illicit
user the number of characters in the actual password.
The structure of the do while loop reflects the two conditions that
terminate the loop: a correct password or too many attempts. If the loop
ends and correct is still false, the program knows that the reason for
termination was too many attempts.
──────────────────────────────────────────────────────────────────────────
Character and String Input in BASIC and C
If you are used to BASIC, you know that you can read a character from the
keyboard (with no echo) using the INKEY$ function. This function is
similar to C's getch(). C conveniently provides the alternative getche()
function for character input with echo, while in BASIC you would need a
separate PRINT statement to echo the input character. Note that neither
the BASIC function nor the C functions mentioned recognize Ctrl-Z as a
signal for the end of file.
Both BASIC and C provide generalized input functions that can handle a
series of numeric or string variables. In BASIC, the INPUT statement
allows you to supply a prompt string and accept input into one or more
variables. For example:
INPUT "ENTER NAME AND AGE: ",NAME$,AGE
The scanf() function in C is similar in that it allows you to receive
input for a series of variables of different types. The scanf() function,
however, allows you a much greater degree of control over the format of
each input value, the interpretation of white space, and the characters
used to separate input values. Unlike the INPUT function, scanf() makes no
provision for a prompt string, so it is normally preceded by a separate
printf() statement with the desired string.
In a typical trade-off for these two languages, BASIC's INPUT statement
provides very rudimentary error checking and editing of the input line.
While scanf() will reject any input that does not match the
specifications, it does not terminate or restart when bad input is
encountered. The C programmer is responsible for error checking to
determine whether the values entered are actually reasonable and complete.
──────────────────────────────────────────────────────────────────────────
Reading Non-ASCII Keys
Some keys, such as the function keys, the cursor control keys, and Alt-key
combinations, have no ASCII code. How can a QuickC program read them?
Before answering this question, we need to discuss how the keyboard
actually works.
The Keyboard Processor and Scan Codes
Information does not flow directly from the keyboard to a C program.
Instead, pressing (or closing) a given key generates a "closure" code that
indicates the physical location of the key. A microprocessor within the
keyboard reads this code and then generates a new code, called a "system
code." It also reports if the user is holding down the Shift, Ctrl, or Alt
key. Finally, it generates a third code (two bytes called the "extended
scan code") for the keystroke (or keystroke combination) and places it in
a storage area called the "keyboard buffer." If the key is still "closed"
after a predetermined period of time elapses, another keystroke is placed
in the buffer. Thus, you can generate a string of characters by holding
down a key. Releasing the key generates an "opening" code that tells the
keyboard microprocessor that you are finished with that key. By default,
the keyboard buffer holds a maximum of 16 extended scan codes.
The purpose of the keyboard buffer is to hold characters that are typed
faster than an application can process them. It is distinct from the
buffer created for the stdio.h input functions.
The getch() and getche() functions do not read the keyboard directly.
Instead, they read the extended scan codes in the keyboard buffer. Because
this code is more extensive than the standard ASCII code, programs can use
it to identify function keys, cursor keys, and other keys lacking an ASCII
code. (The only difference between getch() and getche() is that getche()
echoes input; therefore, our next discussions about getch() actually apply
to both functions.)
Using Scan Codes
Each extended scan code is two bytes. The first byte, which we call the
"ASCII byte," contains the ASCII code, if any, for the keystroke. The
second byte, which we call the "scan byte," contains a scan code for the
key. This code is based on the physical position of the key on the
keyboard, and, in some cases, on whether the Shift, Ctrl, or Alt key is
pressed.
The contents of an extended scan code reveal whether or not it represents
an ASCII character. If it does, the ASCII byte is nonzero. If it does not,
the ASCII byte is set to zero, and the numeric value of the scan byte
encodes the keystroke or keystroke combination. For example, in Figure
13-1, the uppercase Q character is represented by an ASCII byte of 81
because that is its ASCII code. The scan code of 16 means the Q key is the
sixteenth key in the keyboard numbering scheme. The F1 key has no ASCII
representation, so the ASCII byte is 0. However, because it is the 59th
key on the keyboard, the scan byte is 59.
┌────────┬────────┐ ┌────────┬────────┐
Press Q │ 81 │ 16 │ Press F1 │ 00 │ 59 │
└────────┴────────┘ └────────┴────────┘
ASCII Scan └────────┬────────┘
byte byte Extended scancode
Figure 13-1. Scan codes.
How does getch() use these extended codes? First, it looks at the ASCII
byte. If the byte is nonzero, getch() knows it has found an ASCII
character. It returns that value and then skips the scan byte and moves to
the next ASCII byte. For example, it returns 0x41 for Shift-A, 0x61 for a,
and 0x01 for Ctrl-A.
When the ASCII byte is 0, getch() lets the program know it has found a
non-ASCII keystroke by returning a value of 0. Because getch() needs to
know which non-ASCII character was pressed, it does not skip to the next
ASCII byte; instead, it goes to the scan byte. Thus, the next call to
getch() results in it reading the scan code that goes with the 0 ASCII
byte. In other words, only one call of getch() is needed to read an ASCII
keystroke, but two calls are needed to read a non-ASCII keystroke. Also,
the scan codes are returned only for the non-ASCII keystrokes.
Suppose, for example, that you type the Shift-Q combination and then press
the F1 key. The codes 81 16 00 59 are placed in the keyboard buffer. The
first call to getch() returns the 81. The next call to the function skips
to the 00 and returns that value, and the third call returns the 59. Thus,
a program that plans to use the F1 key must look for return values of 0.
When it encounters one, the program should check to see if the next call
returns 59. If so, F1 was pressed. The return value of 0 is a flag that
says, "Special processing required here."
Now, how does getchar() process non-ASCII characters? It copies ASCII
values into the program buffer created by the standard I/O buffer. When it
finds a 0 ASCII byte in the buffer, it skips to the next input character.
The 0 ASCII bytes and the scan codes never make it to the I/O buffer, let
alone to the program.
A Scan Code Example
The SCANCODE.C program (Listing 13-5 on the following page) demonstrates
these functions by reading input. If the input is ASCII, the program
prints the ASCII code. If the input is non-ASCII, the program prints the
scan codes.
Following is some sample output:
Press keys and see the codes!
Press the Esc key to quit.
Q has ASCII code 81──────────────────────────────────────────────Shift-Q
Scan code is 59───────────────────────────────────────────────────────F1
t has ASCII code 116────────────────────────────────t has ASCII code 116
^T has ASCII code 20──────────────────────────────────────────────Ctrl-T
──────────────────────────────────────────────────────────────────────────
/* scancode.c -- displays ASCII or scan code */
/* This program illustrates using getch() to detect */
/* special keys such as function keys. */
#include <conio.h>
#define ESC '\033' /* ESC key */
main()
{
int ch;
printf("Press keys and see the codes!\n");
printf("Press the Esc key to quit.\n");
while ((ch = getch()) != ESC)
{
if (ch != 0)
{
if (ch <= 32) /* control characters */
printf("^%c has ASCII code %d\n",
ch + 64, ch);
else
printf("%c has ASCII code %d\n", ch, ch);
}
else /* ch IS 0 */
{
ch = getch(); /* get scan code */
printf("Scan code is %d\n", ch);
}
}
}
──────────────────────────────────────────────────────────────────────────
Listing 13-5. The SCANCODE.C program.
What happens if you use getch() and getche() without checking for the zero
value? They would interpret the ASCII byte and scan byte as two ASCII
bytes, thus interpreting 00 59 as code for Ctrl-@ and for the semicolon
character, instead of F1.
Scan Code Values
In this book we will use only those codes listed below in an include file
called keys.h. When we need to use these keys, you can include that file,
which is shown in Listing 13-6.
Not all keystrokes produce scan codes. For example, Shift, Ctrl, and Alt
modify the scan codes produced when other keys are pressed. The SCANCODE.C
program demonstrates this. For example, press Alt. Nothing happens until
you simultaneously press a second key.
The operating system normally intercepts the Ctrl-Break combination as the
code for terminating a program. Thus, getch(), getche(), and getchar()
never read Ctrl-Break. (We will discuss how to handle Ctrl-Break later in
this chapter.)
──────────────────────────────────────────────────────────────────────────
/* keys.h -- scan codes for several keys */
#define F1 59 /* function key F1 */
#define F2 60 /* function key F2 */
#define F3 61 /* and so on */
#define F4 62
#define F5 63
#define F6 64
#define F7 65
#define F8 66
#define F9 67
#define F10 68
#define HM 71 /* Home key */
#define UP 72 /* Up Arrow */
#define PU 73 /* Page Up */
#define LT 75 /* Left Arrow */
#define RT 77 /* Right Arrow */
#define END 79 /* End key */
#define DN 80 /* Down Arrow */
#define PD 81 /* Page Down */
──────────────────────────────────────────────────────────────────────────
Listing 13-6. The keys.h include file.
Console I/O Functions
The getch() and getche() functions belong to the console I/O family of
functions. These functions communicate with the console (the keyboard and
screen) more directly than do the I/O functions of the stdio.h family.
However, unlike the stdio.h family, console I/O functions are not in the
standard C Library and are therefore not necessarily portable. They are
important because they provide special services not offered by the
standard I/O package. The console I/O functions declared in the conio.h
header file are:
cgets()
cprintf()
cputs()
cscanf()
getch()
getche()
putch()
ungetch()
kbhit()
inp()
outp()
The first eight functions in this list closely resemble the stdio.h
functions with corresponding names. For example, cgets() resembles gets(),
cprintf() is similar to printf(), and so on. We've already seen the
kbhit() function. We'll discuss the inp() and outp() functions in Chapter
14.
Character Output Functions
Now that we've used the character input functions, let's look at the
console character output functions. The putch() function works much like
putchar(). One difference is that putchar() is buffered and putch() is
not. This means that putch() output goes to the screen directly; putchar()
output goes to an intermediate storage area first. The second difference
is that putchar() works in text mode by default, while putch() works in
binary mode. The main practical consequence of this is in how newlines are
handled. The C newline character (\n) represents going to the beginning of
the next line. This actually consists of two operations: a linefeed (LF)
and a carriage return (CR). In QuickC, the newline character is
represented by the LF character, ^J. The text mode produces the desired
effect by mapping an LF to a CR-LF combination on the screen. In the
binary mode, no such mapping takes place, so you must explicitly generate
both an LF and a CR character (\n and \r).
A third difference is that the text mode used by putchar() interprets a
tab character (\t) as a tabbing instruction; the binary mode used by
putch() interprets it as an ASCII value to be displayed. With the IBM
character set, using putch() to generate a tab character results in a
small circle on the screen.
The REKEY.C program (Listing 13-7) demonstrates how to use the console
I/O functions getch() and putch() to map the characters you type to a
different set of characters on the screen.
Note that we initialize the Newchars[] array to 26 letters. The
construction Newchars[ch - 'a'] causes the array index to be zero when ch
is a, corresponding to the array value q. Similarly, if ch is b, the index
is 1; and the array value is the next letter in the initialization string,
w. The initialization continues in this fashion, as shown in Figure 13-2.
The toupper() and tolower() QuickC macros (defined in ctype.h) convert
cases; thus, we don't need to use another 26-element array for uppercase
letters. Note the way in which the program explicitly translates Enter
(read by getch() as \r) to an output of \r and \n.
┌────────┬────────┬────────┬────────┬────────┬────────┐
│ q │ w │ e │ r │ t │ │
└────────┴────────┴────────┴────────┴────────┴────────┘
'a'-'a' 'b'-'a' 'c'-'a' 'd'-'a' 'e'-'a'
Newchars [0] [1] [2] [3] [4]
Figure 13-2. The Newchars[ch - a] array.
──────────────────────────────────────────────────────────────────────────
/* rekey.c -- transliterates typed input */
/* This program illustrates getch() and putch(). */
#include <stdio.h>
#include <conio.h>
#include <ctype.h>
#define ESC '\033' /* the escape key */
char Newchars[] = "qwertyuiopasdfghjklzxcvbnm";
/* values to be assigned to the a,b,c keys, etc. */
main()
{
char ch;
printf("Type characters and see them transformed;\n");
printf("Press the Esc key to terminate.\n");
while ((ch = getch()) != ESC)
if (islower(ch))
putch(Newchars[ch - 'a']);
else if (isupper(ch))
{
ch = tolower(ch);
putch(toupper(Newchars[ch - 'a']));
}
else if (ch == '\r')
{
putch('\n');
putch('\r');
}
else
putch(ch);
}
──────────────────────────────────────────────────────────────────────────
Listing 13-7. The REKEY.C program.
Console String I/O
Often we want a program to read a string──for example, the name of a file.
Or we want to generate a string. These activities can be done character by
character, but it is more convenient to use functions designed to handle
strings. The console functions cgets() and cputs() perform these tasks. In
action, these functions are similar to gets() and puts(), but there are
some differences.
Like gets(), cgets() reads an input string into an array. However, the
first element of the array holds the maximum allowable size of the input
string, including a terminating null character. You must initialize this
element correctly. The second element holds the actual number of bytes
used, and it is set by cgets() after it reads the input. The string itself
starts at the third element. Thus, the array must be two bytes longer than
the maximum string size, including a null character, as shown in Figure
13-3 on the following page.
┌─────────Available space───────────┐
0 1 │ 2 3 4 5 6 7 │
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ 6 │ 4 │ L │ A │ R │ A │ \O │ │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
│ │ └──cgets() puts string here
│ └──cgets() puts string size here
└──You put available space here
Figure 13-3. Storage of an array read by cputs().
The cgets() function reads input until the maximum length of the string
(not counting the null character) is reached or until the user presses
Enter. The console beeps if you try to read beyond the limit, and you
can't enter additional characters. The function will, however, let you use
the Backspace key to correct input. This function returns a pointer to the
beginning of the stored string; that is, if the array name is str, cgets()
returns a pointer to str[2].
The cputs() function takes a pointer to a string as its argument and
displays that string on the console. Unlike puts(), cputs() does not
append a newline character; therefore, you must explicitly include the
\r\n combination to generate a new line. The return value is the last
character written. The function returns 0 if the string is a null string
and -1 if there is an error.
The short STRIO.C program (Listing 13-8) illustrates how the two
functions work. Notice how we use store + 2 as an argument for cputs(). We
do this because the string starts at the location pointed to by store + 2.
We kept the character limit small to make it easy to see what happens when
you try to exceed it.
──────────────────────────────────────────────────────────────────────────
/* strio.c -- uses cgets() and cputs() */
/* program list -- strio.c (cgets() not in core library) */
#include <conio.h>
#define MAXSIZE 6
main()
{
char store[MAXSIZE + 2];
store[0] = MAXSIZE; /* puts limit in first element */
cputs("What's your name?\n\r");
cgets(store);
cputs("\n\rI'll remember you, ");
cputs(store + 2);
cputs("!\n\r");
}
──────────────────────────────────────────────────────────────────────────
Listing 13-8. The STRIO.C program.
The following is a sample run of the program:
What's your name?
Steph
I'll remember you, Steph!
Note the \n\r at the beginning of the second cputs() statement. This
prevents the message from being printed over the input line.
Instead of using cputs(store + 2), we could have used cputs(&store[2]).
Or, because the return value of cgets() points to the start of the
string──not to the start of store[]──we could have declared a pointer and
used it as follows:
char *start;
...
start = cgets(store);
...
cputs(start);
Formatted I/O
Finally, the cscanf() and cprintf() functions provide console analogues to
the standard I/O functions scanf() and printf(). The main differences are
that cscanf() and cprintf() work directly with the console, that cprintf()
requires you to use the \r\n combination instead of \n, and, of course,
that they are less portable.
Keyboard Control with ANSI.SYS
Using getch() or getche() and the scan codes, a QuickC program can detect
a function key or a cursor control key. But how can you turn that
information into action? How, for example, can pressing the Left Arrow key
be made to move the cursor one space to the left? There are three common
techniques: One uses the ANSI.SYS driver provided with MS-DOS and PC-DOS;
the second uses BIOS calls; and the third directly accesses video memory.
Table 13-2 compares the three methods.
The first method is the simplest, so we begin with it. Many terminals have
internal hardware that lets you control cursor position and other screen
attributes by sending "escape sequences" from your program to the
terminal. These all begin with the ESC character, followed by different
sequences corresponding to different actions. For example, the sequence
ESC[2B moves the cursor down two lines in the same column. By using
printf() to generate such a string, you can move the cursor around. The
original IBM PC hardware design omitted this convenient feature.
Table 13-2 Cursor and Screen Control Methods
Method ANSI.SYS BIOS Direct Memory Access
──────────────────────────────────────────────────────────────────────────
Speed ranking 3 2 1
Ease-of-use 1 2 3
ranking
Portability ANSI-compatible BIOS-compatible Display-specific
──────────────────────────────────────────────────────────────────────────
MS-DOS version 2.0 came to the rescue, however, by providing the ANSI.SYS
"driver" as a software fix. (A driver is software designed to handle
specific hardware I/O devices.) The ANSI.SYS software intercepts output,
examining it for escape sequences. When it finds a valid sequence, it
performs the requested action. To use this method, you need ANSI.SYS up
and running, and you need to know the proper escape sequences.
Starting ANSI.SYS
Running ANSI.SYS is not like running an ordinary program. You don't, for
example, type ansi. Instead, you place this line in your CONFIG.SYS file:
DEVICE=ANSI.SYS
If the ANSI.SYS file is in a different directory from the CONFIG.SYS file,
give the full pathname, as in the following example:
DEVICE=C:\DOS\ANSI.SYS
Now, when you boot your computer, ANSI.SYS is installed as part of MS-DOS.
Using ANSI.SYS Escape Sequences
One handy escape sequence lets you assign a string to a particular key.
That is, it makes typing a single key have the same effect as typing the
string. First, let's examine the format of the escape sequence required by
ANSI.SYS:
ESC[ASCIIcode;"string";ASCIIcodep
Here ESC represents the escape character (ASCII 033). The first ASCIIcode
represents the ASCII number of the key to which you assign the string. For
non-ASCII keys, such as F1, use 0ancode, where the number following the 0;
is the scan code for the key. Next, string represents, in string form, the
characters you want to assign to the key. For example, the string could be
dir/w. The final ASCIIcode lets you represent an assigned character in
ASCII form instead of as a string. For example, you can use 13 instead of
a carriage return. Finally, the character p terminates the escape
sequence. You can use as many strings and ASCII codes as you like as long
as you separate them with semicolons. For example, you can represent CD by
"CD", by "C";68, or by 67;68, where 67 and 68 are ASCII codes for C and D.
The ASGNKEY.C program (Listing 13-9), for example, assigns meanings to
the F5 through F10 keys. These meanings remain in effect until you reboot.
Because all the key assignments follow the same form, we use a macro to
represent the general form. In the macro, printf() displays the escape
sequence. First comes \033, the octal code for ESC. Then the left bracket
and the 0; indicate a scan code. (The scan code itself is the variable K
of the macro.) Next comes another semicolon and an open quote. (You must
escape these with a \ when you use them within a string.) Next, the string
itself is represented by the variable S. Then come the closing quote,
another semicolon, a 13 (to represent a carriage return), and the
closing p.
──────────────────────────────────────────────────────────────────────────
/* asgnkey.c -- uses ansi.sys to assign meanings */
/* to several function keys */
/* Note: This requires ANSI.SYS to be installed. */
#define KASSIGN(K, S) printf("\033[0;%d;\"%s\";13p", K, S)
/* This macro assigns string S to key K */
#define F5 63
#define F6 64
#define F7 65
#define F8 66
#define F9 67
#define F10 68
main()
{
KASSIGN(F5, "DIR *.C");
KASSIGN(F6, "DIR *.H");
KASSIGN(F7, "DIR *.OBJ");
KASSIGN(F8, "DIR *.EXE");
KASSIGN(F9, "DIR /W");
KASSIGN(F10,"CD \\");
}
──────────────────────────────────────────────────────────────────────────
Listing 13-9. The ASGNKEY.C program.
Running this program changes the function key assignments, but you have to
go to MS-DOS before you can see the effects. Once you exit to MS-DOS,
pressing F5 through F8 causes MS-DOS to list the specified types of files
(*.c, *.h, and so on). The F9 function key lists your directories in the
compact form (the /W option). Also, F10 switches to the root directory.
The defining string uses \\ for root because that is how you express a
single \ in a C string. Because the code itself includes 13 for Return,
you don't press Enter when using these function keys.
You can easily modify this program to read in the desired function key
number and the string interactively. But bear in mind that these
assignments supersede existing ones and that they hold until you reboot.
If, for example, you assign a function to F1, you override the editing
function given to it by MS-DOS.
Note that QuickC uses its own routines to read the keyboard, and it
bypasses these function key definitions. So, while in QuickC, you still
can use F5 to run a program. But if you call up an MS-DOS shell from
QuickC, the new assignments apply.
Cursor and Screen Control
Now let's apply the ANSI.SYS method to a simple menu model. The goal is to
write a program that clears the screen and displays a simple menu with one
choice highlighted. The Up Arrow and Down Arrow keys move the cursor and
highlighting to a different choice, and pressing Enter selects the
highlighted choice. To do this, we need more escape codes. Table 13-3 on
the following page lists some representative examples from which we'll
select the ones we need. Our program will use the various cursor control
sequences to move the cursor. The highlighting of a choice is handled
using the SGR (Set Graphics Rendition) escape sequence, which lets you
specify certain "attributes." Each character to be displayed can be
assigned an attribute that controls its presentation: color, reverse
video, blinking, and so on. In Table 13-3, ESC[ represents the Escape
character, and num is a numeric parameter for which you substitute a
specific number. Numbering of rows and columns starts with 1. For all but
the last code sequence, any omitted num is assumed to be 1.
Table 13-3 ANSI.SYS Escape Sequences
╓┌─┌──────────────┌──────────────┌─────────────┌─────────────────────────────╖
Name Mnemonic Escape Code Description
──────────────────────────────────────────────────────────────────────────
Cursor CUP ESC[num;numH Moves the cursor to the
Position position specified by the
numeric parameters. The first
num is the line number; the
second is the column number.
Cursor Up CUU ESC[numA Moves the cursor up num lines
in the same column.
Cursor Down CUD ESC[num Moves the cursor down num
lines in the same column.
Cursor Forward CUF ESC[numC Moves the cursor right num
Name Mnemonic Escape Code Description
──────────────────────────────────────────────────────────────────────────
Cursor Forward CUF ESC[numC Moves the cursor right num
columns.
Cursor Back CUB ESC[numD Moves the cursor left num
columns.
Erase Display ED ESC[u Erases the entire display and
homes the cursor.
Set Graphics SGR ESC[numm Sets character attributes as
Rendition indicated by num. Possible
values include 0 for normal, 1
for high intensity, 5 for
blink, and 7 for reverse
video.
──────────────────────────────────────────────────────────────────────────
To highlight a line of text, we must first print the escape code for
highlighting on that line and then print the text. To confine highlighting
to the menu line, we turn off highlighting at the end of the menu output.
To move the cursor and highlighting, we use getch() and the scan codes to
detect when the arrow keys are pressed. If the Down Arrow key is pressed,
for example, the program moves the cursor and reprints the menu, changing
which line is highlighted. Listing 13-10 shows the completed MENU.C
program, and Figure 13-4 on p. 419 shows the menu at work.
──────────────────────────────────────────────────────────────────────────
/* menu.c -- uses ANSI.SYS for cursor control and */
/* for video reverse in a sample menu */
/* Note: Requires that ANSI.SYS be installed. */
#include <conio.h>
#define ITEMS 5 /* number of menu items */
#define UP 72 /* scan code for up arrow */
#define DOWN 80 /* scan code for down arrow */
#define VIDREV "\033[7m" /* reverse video attribute */
#define ATTOFF "\033[0m" /* turn attributes off */
#define ED() printf("\033[2J") /* erase display */
#define HOME() printf("\033[H") /* home the cursor */
#define CUU(Y) printf("\033[%dA", Y); /* cursor up */
#define CUD(Y) printf("\033[%dB", Y); /* cursor down */
char *Menu[ITEMS] = {"Add a number to the list",
"Delete a number from the list",
"Clear the list",
"Sum the list",
"Quit"};
char *Heading =
"Use arrow keys to highlight choice. "
"Use Enter key to select choice.";
void showmenu(int);
int getmesg(int);
main()
{
int messno = 0; /* message to be highlighted */
ED();
showmenu(messno);
while (messno != ITEMS - 1)
{
messno = getmesg(messno);
ED();
switch (messno)
{
case 0 :
case 1 :
case 2 :
case 3 : printf("...pretending to work...");
printf("Hit any key to continue\n");
getch();
ED();
showmenu(messno);
break;
case 4 : printf("Quitting!\n");
break;
default: printf("Programming error!\n");
break;
}
}
}
/* showmenu() displays the menu */
void showmenu(highlite)
int highlite; /* message number to be highlighted */
{
int n;
char *start;
HOME();
printf("%s", Heading);
for (n = 0; n < ITEMS; n++)
{
if (n == highlite)
start = VIDREV; /* turn on reverse video */
else
start = ATTOFF;
printf("\n\n%s%s%s", start, Menu[n], ATTOFF);
}
HOME();
CUD(2 + 2 * highlite);
}
/* getmesg() selects a menu item */
int getmesg(mnum)
int mnum; /* current message number */
{
char ch;
while ((ch = getch()) != '\r')
if (ch == 0)
{
ch = getch();
switch (ch)
{
case UP : if (mnum > 0)
{
CUU(2);
showmenu (--mnum);
}
else
{
CUD(2 * ITEMS - 2);
showmenu(mnum = ITEMS - 1);
}
break;
case DOWN : if (mnum < ITEMS - 1)
{
CUD(2);
showmenu(++mnum);
}
else
{
CUU(2 * ITEMS - 2);
showmenu(mnum = 0);
}
break;
}
}
return mnum;
}
──────────────────────────────────────────────────────────────────────────
Listing 13-10. The MENU.C program.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 13-4 can be found on p.419 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 13-4. The MENU.C program at work.
──────────────────────────────────────────────────────────────────────────
Watching the Keyboard in BASIC and C
The ANSI.SYS techniques discussed here allow you to achieve the
functionality of the KEY statement in BASIC. The BASIC KEY n, string
statement allows you to assign a string to the PC function key Fn──that
is, to create a simple "keyboard macro." The ANSI method is more general
(it can be used with any key, not just a function key) and is also not
limited to assigning short strings.
BASIC statements such as ON KEY provide a very useful facility called
"event-driven programming." After you use the KEY statement to assign a
key number to one of the keyboard keys, a press of that key while the
program is running will be "trapped." The ON KEY(n) subroutine statement
causes subroutine to be executed whenever the key that's assigned number n
is pressed. This allows programs to respond to input immediately.
C has no such built-in facilities. You can, however, put the program in an
outer loop that calls the kbhit() function to see if a key has been
pressed. If a key has been pressed, you can use getch() to read the key.
After assigning the key to a variable of type char, you can use it in a
switch statement that calls the appropriate function to handle the command
received. This isn't true event-driven programming, because the response
to a key comes only when the program is at the top of the loop, but QuickC
programs run fast enough that the effect is often the same. The use of a
special device driver or an environment such as Microsoft Windows can
allow for true event-driven programming.
──────────────────────────────────────────────────────────────────────────
The MENU.C program first defines several macros using printf() and the
escape codes to represent some of the ANSI.SYS sequences from Table 13-3
on p. 416. If you plan to use such macros often, you should create an
include file for the macro definitions.
Because we are illustrating ANSI.SYS and not numeric analysis, the program
does no actual calculation. However, the switch statement in main()
provides the skeleton for controlling program flow. The getmesg() function
returns the array index of the selected message, and the switch selects a
response based on that value. The switch is in a loop, so you can
repeatedly make choices until you select Quit.
In main(), the HOME() macro uses the CUP escape code to home the cursor.
Because we omitted the two numeric parameters, the default values of 1 are
used, which effectively home the cursor.
The showmenu() function displays the menu. It receives the array index of
the element to be highlighted. That message then starts with highlighting
turned on; the other messages have it turned off.
The getmesg() function, as we mentioned, returns the array index of the
selected item. It also handles the cursor movement. In this function,
getch() checks for the Up Arrow and the Down Arrow keys. If, for example,
the Down Arrow key is pressed, CUD moves the cursor down two lines to the
next message. The array index is also incremented to tell showmenu() which
message to highlight. To keep the cursor inside the menu, we compare its
position to the menu limits. If the cursor is on the bottom line of the
menu, then pressing the Down Arrow key moves it to the top line.
This program works as designed, but it runs slowly, and the redrawing of
the screen is not very smooth. The ANSI.SYS approach to cursor and screen
control is relatively simple, but using BIOS calls or direct memory access
gives better performance.
Using QuickC to Access the BIOS
One way to create programs that take advantage of the special capabilities
of an IBM PC/XT, PC/AT, or compatible without getting too involved in the
hardware is to use BIOS calls.
Background for the IBM BIOS
BIOS is an acronym for Basic Input/Output System. It consists of a set of
assembly-language routines permanently stored in what is called Read-Only
Memory, or ROM, of the IBM PC. The computer can read and utilize
information in ROM, but it cannot alter ROM. That preserves the integrity
of the routines. The BIOS includes routines to read the keyboard, to
control the video display, and to read from and write to disk drives. Most
higher-level programming ultimately makes use of these routines. For
instance, QuickC's getch() uses one of the keyboard routines, and many
MS-DOS commands ultimately use the BIOS routines to do low-level work.
In short, you can think of the BIOS as a built-in library of functions.
All you need to do is find out what services are offered and how to use
them.
The ultimate source of information about the BIOS is the IBM Personal
Computer Technical Reference Manual. This manual includes
assembly-language listings of all the routines. We'll describe those
routines as we use them.
Using the BIOS
Two problems face the QuickC programmer who wants to use the BIOS. One is
that the routines, which are written in assembly language, don't work the
same as C functions, so you have to learn a little about assembly language
and about the hardware to understand them. The second is that these BIOS
routines are accessed not by function calls but by "interrupt signals."
For this reason, these routines are commonly called "interrupt routines,"
or simply "interrupts." Let's clarify this topic first.
Interrupt Routines
The heart of a PC is its central processing unit, or CPU, but a PC
contains other processors, too. For example, the keyboard processor
handles keyboard input, and another processor handles data flow between
the CPU and memory. To enable the CPU to keep in touch with its
environment, an interrupt system was developed. Certain devices and
assembly-language instructions can generate signals that take control of
the microprocessor. The Intel 8086 family of microprocessors permits as
many as 256 distinct interrupt signals, but fewer are actually used. When
the CPU detects an interrupt signal, it "interrupts" its current activity
and executes the set of assembly-language instructions identified with
that particular signal.
──────────────────────────────────────────────────────────────────────────
How Interrupts Work
When you boot a PC, it sets up a table of addresses known as the
"interrupt vectors." At the first address is the routine to be executed if
interrupt signal 0 is detected. At the second address is the routine to be
executed if interrupt signal 1 is detected, and so on. When an interrupt
is detected, the corresponding address is found in the table, and the
instructions beginning at that address are executed. At the end of those
instructions, a "return from interrupt" instruction tells the
microprocessor to resume its interrupted activity.
The operating system also uses the interrupt table. When MS-DOS or PC-DOS
is first loaded, it adds its own batch of interrupt routines. Memory
resident programs also work by storing their addresses in the interrupt
vector table. Incidentally, MS-DOS can substitute its own version of a
ROM-based BIOS routine by overwriting the appropriate interrupt vector
with a new address. The ROM itself is unchanged, but the computer is
directed to the new address instead when the interrupt is issued. This
method is sometimes used as a software "fix" for faulty BIOS routines.
(The only way to update the actual BIOS is to get a newer version of the
ROM chip.)
──────────────────────────────────────────────────────────────────────────
Software Interrupts
In assembly language, generating interrupts is simple. For example, to
generate interrupt signal 0x10 (the video I/O interrupt), you use the
following instruction:
int 10h
What if one interrupt arrives while another interrupt routine is
executing? This situation is handled by a priority ranking. A
higher-priority interrupt can interrupt a lower-priority routine, but not
vice versa.
C, as a general, portable language, doesn't have a built-in interrupt
instruction. But the QuickC library offers several non-ANSI C functions
designed to serve the same purpose. Seven functions make specific BIOS
calls; they all have names beginning with _bios_ and are declared in the
bios.h file. The dos.h file declares another 40 functions, most of which
call specific MS-DOS functions. (Interrupt number 0x21 can be used to
access many functions loaded into the system by MS-DOS; these are the
MS-DOS system calls.) Five of the dos.h functions, however, are more
general and can invoke a choice of interrupts. (See Table 13-4 for a
summary of these functions.)
We will use the int86() function because it is generally applicable. As
its name suggests, it generates a specified interrupt for the 8086 family
of microprocessors. However, before we can use this function, we have to
see how interrupt routines use registers to transfer data.
Table 13-4 Interrupt-accessing Functions in Order of Decreasing Generality
Name Use
──────────────────────────────────────────────────────────────────────────
intx86() Invokes interrupts requiring the use of segment
registers.
int86() Invokes interrupts not requiring use of the segment
registers.
intxdos() Invokes MS-DOS system calls requiring the use of
segment registers.
intdos() Invokes MS-DOS system calls not requiring use of the
segment registers.
bdos() Invokes MS-DOS system calls that use only the DX and AL
registers.
_bios_...() family Invokes specific BIOS interrupts.
_dos_...() family Invokes specific MS-DOS calls.
──────────────────────────────────────────────────────────────────────────
Interrupts, Assembly Language, and Registers
Like C functions, interrupts pass information back and forth between the
routine and the calling program. Instead of using arguments, however,
interrupts use the microprocessor registers. The int86() function gets
around this difference by using unions to pass the register information to
and from the calling C program.
Registers are small work and storage areas built into the CPU. For
example, the 8088 chip, the most commonly used member of the 8086 family,
has 13 registers, each capable of holding 16 bits. Four of the registers
are general-purpose registers used for arithmetic and logical operations;
they are called AX, BX, CX, and DX. Four "segment" registers store the
addresses of various memory segments; these registers are called CS, DS,
SS, and ES. Four more "pointer/index" registers keep track of addresses
used in a program; they are called SP, BP, SI, and DI. Finally, the
instruction pointer (IP) keeps track of the address of the next
instruction to be executed. Also, the processor has nine "flags" that can
be turned on or off. The flags can be considered to be individual bits in
a flag register. These, then, are the resources open to an interrupt
routine.
There is one further complication. Each of the general-purpose registers
can be considered to be two 8-bit registers. The AX register, for example,
can be divided into the AH (H for high byte) and the AL (L for low byte)
registers. Assigning a value to the AX register affects the whole
register, but assigning a value to AL or AH affects only half of the
register. Similarly, the BX register is divided into the BH and BL
registers, and so on.
Now that we have some background about registers, let's see how int86()
works.
The int86() Function
The int86() function will be our tool for initiating interrupt routines,
initializing registers, and reading registers. Its library description
begins with the following:
#include <dos.h>
int int86(intno, inregs, outregs);
int intno; /* Interrupt number */
union REGS *inregs; /* Register values on call */
union REGS *outregs; /* Register values on return */
This syntax summary says to include the dos.h header file when using this
function. Also, int86() takes three arguments. The first is the number of
the desired interrupt. The second is the address of a union containing the
register values passed to the interrupt. The third is the address of the
union into which the post-interrupt register values are copied.
To use int86(), you need to know how the type union REGS is defined. That
information resides in the dos.h file, as follows:
/* word registers */
struct WORDREGS {
unsigned int ax;
unsigned int bx;
unsigned int cx;
unsigned int dx;
unsigned int si;
unsigned int di;
unsigned int cflag;
};
/* byte registers */
struct BYTEREGS {
unsigned char al, ah;
unsigned char bl, bh;
unsigned char cl, ch;
unsigned char dl, dh;
};
/* general-purpose registers union -- overlays the
corresponding word and byte registers. */
union REGS {
struct WORDREGS x;
struct BYTEREGS h;
};
Together, these definitions give two views of the registers.
The WORDREGS structure provides the 16-bit view. This structure has seven
members representing the four general-purpose registers, two of the
pointer registers, and the "carry" flag (which we won't use). These are
the registers most commonly used by the interrupts. (The int86x() function
uses an additional structure to give access to more registers.) In this
structure, for example, the ax member represents the AX register.
The BYTEREGS structure gives the 8-bit view. This structure represents the
four general-purpose registers, with each register split into two 1-byte
registers. Thus, the ch member of this structure represents the CH
register, the high byte of CX.
The unusual part of this function is the definition of the union REGS. It
superimposes the word view and the byte view. For example, suppose you use
the following declaration:
union REGS myreg;
To assign a value to the AX register, use a statement such as the
following:
myreg.x.ax = 1026;
The .x notation specifies the WORDREGS member of myreg; therefore,
myreg.x.ax is the AX member of that structure.
To assign a value to the BL register (the low byte of the BX register),
use the following .h notation:
myreg.h.bl = 22;
This specifies the BYTEREGS member of the union.
Recall that a union uses the same storage area for all its members. This
means that myreg.h.al and myreg.h.ah overlie myreg.x.ax. To get the high
byte of the 1026 that was assigned to myreg.x.ax, refer to myreg.h.ah (see
Figure 13-5).
union REGS myreg;
word
┌───────────────────┬───────────────────┐
/ │ │ │ \ AX register
/ │ myreg.x.ax │ \
/ └───────────────────┴───────────────────┘ \
/ / ┌─────────── /───/──┬──\───\────────────┐ \ \
┌───────────────────┐/ / │ \ \┌───────────────────┐
│ one byte │ / │ \ │ one byte │
│ myreg.h.ah │/────────┴────────\│ myreg.h.al │
AH └───────────────────┘─────────┬─────────└───────────────────┘ AL
register │ │ │ register
│ │ │
└───────────────────┴───────────────────┘
┌───────────────────┬───────────────────┐
│ │ │ DX
│ │ │
└───────────────────┴───────────────────┘
For a REGS union: .x means the 16-bit version
.h means the 8-bit version
Figure 13-5. The REGS union.
You now know enough theory to use int86(). However, you still need to know
what values to pass as arguments to the BIOS routines. Let's look at a
simple example.
Interrupt 0x16──The Keyboard I/O Interrupt
Because we have been discussing the keyboard, let's look at interrupt
routine 0x16, which reads the keyboard. The QuickC library provides the
_bios_kbrd() function to access this specific routine. However, we will
use int86() in order to demonstrate a more general approach to using
interrupts.
Like many interrupts, 0x16 includes more than one subroutine. It has three
subroutines; each is termed a "function" or "service." To select a
particular function, you must place the "function code" number in the AH
register before calling the interrupt. Let's take a look at what each
function does.
Interrupt 0x16, Function Code 0──Get Character
This function reads the keyboard buffer if a character is present;
otherwise, it waits until a keystroke is placed in the buffer. When it
reads a key, it places the ASCII byte in the AL register and the scan byte
in the AH register. (Note that the return values are written over the
values we originally placed in AH and AL.) The code is removed from the
keyboard buffer once it is read. The getch() function is based on this
function.
Interrupt 0x16, Function Code 1──Check Keyboard Buffer
This function checks to see if the keyboard buffer is empty or not. If it
is empty, the "zero flag" (ZF) is set to 1; otherwise, the flag is cleared
(set to zero). If a character is present, the ASCII byte is placed in AL
and the scan byte in AH, but the code in the buffer is left there. The
kbhit() function is based on this function.
Interrupt 0x16, Function Code 2──Get Keyboard Status
This function reports on the status of the Shift and Ctrl keys. Each of
eight keys is assigned a particular bit in the AL register. If one of the
eight keys is closed, the corresponding bit is set to 1. Table 13-5 shows
the corresponding bits and keys.
Table 13-5 Keyboard Status Bits
Bit Set to 1 If
──────────────────────────────────────────────────────────────────────────
0 Right Shift is closed
1 Left Shift is closed
2 Ctrl is closed
3 Alt is closed
4 Scroll Lock is active
5 Num Lock is active
6 Caps Lock is active
7 Insert mode is active
──────────────────────────────────────────────────────────────────────────
Reading ASCII and Scan Codes
Let's use function code 0 to construct a more general version of getch()
that we'll call Readkey(). It will return both the ASCII and the scan
bytes. Using it will give you a better picture of how the keyboard codes
work and show you that using interrupts from QuickC is not all that
difficult. The readkey.c program (Listing 13-11) contains the Readkey()
function.
The dos.h file defines the union REGS type and declares the int86()
function. We define symbolic constants to represent the interrupt number
and the function code number. Finally, we define a two-member structure
called SCANCODE for holding the two keyboard codes. Readkey() uses the reg
structure to set the AH register to the proper function code and then it
calls int86(). Because preserving the original register values is
unnecessary, the same structure stores both the input values and the
returned values of the registers. Finally, the program copies the two
relevant register values into the structure that the function returns.
The int86() syntax calls for two pointers to union REGS as arguments. In
practice this usually calls for using the address operator applied to the
appropriate union, as we have done here.
Having developed the Readkey() function, let's use it in the next program,
SHOWCODE.C, (Listing 13-12). To run this program within the QuickC
environment, be sure that Screen Swapping On is active (on the Debug
menu).
This program reads a key, prints it (if it is printable), and displays
both the ASCII and scan codes. Using it can be instructive. Following, for
example, is the output for m, Shift-M, Ctrl-M, Alt-M, Enter, and Esc:
m: ascii = 109, scan = 50────────────────────────────────────────────m
M: ascii = 77, scan = 50──────────────────────────────────────Shift-M
^M: ascii = 13, scan = 50───────────────────────────────────────Ctrl-M
: ascii = 0, scan = 50────────────────────────────────────────Alt-M
^M: ascii = 13, scan = 28────────────────────────────────────────Enter
^[: ascii = 27, scan = 1──────────────────────────────────────────Esc
──────────────────────────────────────────────────────────────────────────
/* readkey.c -- contains the Readkey() function */
#include <dos.h>
#define KEYINTR 0x16 /* keyboard read interrupt */
#define GETCHAR 0 /* read scancode function */
struct SCANCODE {
unsigned char ascii; /* ascii code */
unsigned char scan; /* scan code */
};
struct SCANCODE Readkey()
{
union REGS reg;
struct SCANCODE scancode;
reg.h.ah = GETCHAR; /* specify function */
int86(KEYINTR, ®, ®); /* note use of & oper.*/
scancode.ascii = reg.h.al;
scancode.scan = reg.h.ah;
return (scancode);
}
──────────────────────────────────────────────────────────────────────────
Listing 13-11. The readkey.c function.
──────────────────────────────────────────────────────────────────────────
/* showcode.c -- shows ASCII and scan codes for */
/* keystrokes */
/* Note: Set Screen Swapping On in the Debug menu. */
#include <stdio.h>
#include <dos.h>
#define KEYINTR 0x16 /* keyboard read interrupt */
#define GETCHAR 0 /* read scancode function */
#define ESC '\033' /* escape key */
struct SCANCODE {
unsigned char ascii; /* ascii code */
unsigned char scan; /* scan code */
};
struct SCANCODE Readkey();
main()
{
struct SCANCODE keys;
printf("Press keys to see their scancodes. ");
printf("Press the Esc key to quit.\n");
do {
keys = Readkey();
if (keys.ascii > 0 && keys.ascii < 040)
printf("^%c: ascii = %3d, scan = %3d\n",
keys.ascii + 0100, keys.ascii,
keys.scan);
else if (keys.ascii >= 40)
printf(" %c: ascii = %3d, scan = %3d\n",
keys.ascii, keys.ascii, keys.scan);
else
printf(" : ascii = %3d, scan = %3d\n",
keys.ascii, keys.scan);
} while (keys.ascii != ESC);
}
struct SCANCODE Readkey()
{
union REGS reg;
struct SCANCODE scancode;
reg.h.ah = GETCHAR;
int86(KEYINTR, ®, ®);
scancode.ascii = reg.h.al;
scancode.scan = reg.h.ah;
return (scancode);
}
──────────────────────────────────────────────────────────────────────────
Listing 13-12. The SHOWCODE.C program.
The scan code for the first four characters is the same (50) because the
same primary key (the M key) was used in each case. The modifying key, if
any, then caused the ASCII part of the code to be changed. Note how the
ASCII part for Alt-M is 0. Also note how the Enter key has the same ASCII
code as Ctrl-M but a different scan code. The scan code is different
because a different physical key was pressed. Incidentally, if you need to
write a program that discriminates between input of Ctrl-M and the Enter
key, you can use Readkey() to check the scan code. (The getch() function
cannot distinguish between the two keystrokes.)
The following represents another sample run; this time the input is F1,
Ctrl-F1, Shift-F1, Alt-F1, and Esc:
: ascii = 0, scan = 59───────────────────────────────────────────F1
: ascii = 0, scan = 94──────────────────────────────────────Ctrl-F1
: ascii = 0, scan = 84─────────────────────────────────────Shift-F1
: ascii = 0, scan = 104───────────────────────────────────────Alt-F1
^[: ascii = 27, scan = 1──────────────────────────────────────────Esc
In this example, the scan code changes even though the same primary key
was pressed each time. With ASCII characters, the ASCII code discriminates
among diferent combinations, but with the control keys, the ASCII byte is
always 0, so the scan code itself must change. Also, notice that these
keystrokes are nonprinting; therefore, the program displays only the
codes.
Finally, this example uses the following input: 1, the End key, Shift-End,
and Esc:
1: ascii = 49, scan = 2─────────────────────────────────────────────|
: ascii = 0, scan = 79───────────────────────────────────────────End
1: ascii = 49, scan = 79─────────────────────────────────────Shift-End
Note how Shift-End and 1 produce the same ASCII code (49) but different
scan codes (2 and 79). We mention this because the QuickC editor uses the
Shift-End combination to highlight a line. If the programming for QuickC
relied on getch(), that would be impossible to do. Apparently QuickC, like
our program, checks the scan code too. This lets it assign a different
function to Shift-End.
Cursor and Screen Control with BIOS Calls
Now that you know how to use interrupts, we can extend that technique to
cursor and screen control. To illustrate these applications, we will
construct a rudimentary first step toward a word processor. With this
program, you can do the following:
■ Start with a clear screen.
■ Enter text from the keyboard and see it on the screen.
■ Use the arrow keys to move the cursor.
■ Use the function keys to turn highlighting on and off.
■ Highlight or unhighlight existing text by moving the cursor over it.
To provide these features, we'll construct a library of approximately a
dozen BIOS-based functions. Rather than jumping back and forth between
program development and BIOS use, we'll develop the entire library first.
Incidentally, the QuickC Graphics Library, which we discuss in Chapter
15, provides an alternative means for implementing these features. Using
the Graphics Library, however, produces executable programs substantially
larger than those using the BIOS approach.
The Video I/O Interrupt
The first step is to find the appropriate interrupt routine. Interrupt
0x10, the video I/O interrupt, controls the display. Because maintaining a
video display is more complex than monitoring a keyboard, this interrupt
turns out to be much more involved than the keyboard I/O interrupt. It
provides many subroutines, or functions, and many of them use several
registers. Table 13-6 on the following pages lists and describes the
functions we use in this book. The table mentions "attributes" and
"pages." Attributes, as we saw in our discussion of ANSI.SYS, determine
how a character is to be displayed. A page is a screenful of display. Some
video modes can store more than one page at once, although only one can be
displayed at any given time. We discuss these terms further as needed.
When using int86() to invoke these functions, you set AH to the
appropriate function code number and initialize any other registers given
in the description. The first argument to int86() should be 0x10, the
interrupt number.
Table 13-6 Selected Video I/O Interrupt 0x10 Functions
──────────────────────────────────────────────────────────────────────────
FUNCTION CODE 0: Set the Display Mode
Action: Switches to desired mode and clears display.
Register setup: Place 0 in AH
Place desired mode in AL
Choose from the following modes:
Mode Meaning
0 40 x 25 B/W Text
1 40 x 25 Color Text
2 80 x 25 B/W Text
3 80 x 25 Color Text
4 320 x 200 Color Graphics
5 320 x 200 B/W Graphics
6 640 x 200 B/W Graphics
7 80 x 25 Monochrome
13 320 x 200 Color EGA
14 640 x 200 Color EGA
15 640 x 350 B/W EGA
16 640 x 350 Color EGA
17 640 x 480, 2-Color VGA
18 640 x 480, 16-Color VGA
19 320 x 200, 256-Color VGA
FUNCTION CODE 2: Select Cursor Position
Action: Moves cursor to the specified row and number.
Register setup: Place 2 in AH
Place row number in DH
Place column number in DL
Place page number in BH
Numbering of rows and columns starts with 0, not 1.
FUNCTION CODE 3: Read Cursor Position
Action: Reports the row and column of cursor position.
Register setup: Place 3 in AH
Place page number in BH
Returns: Row number is in BH
Column number is in DL
Cursor type is in CH, CL
FUNCTION CODE 5: Select Active Display Page
Action: Selects the page for modes supporting multiple pages.
Register setup: Place 5 in AH
Place page number in AL
FUNCTION CODE 6: Scroll Up an Area of the Screen
Action: Scrolls up a section of the screen a specified amount.
Register setup: Place 6 in AH
Place number of lines to scroll in AL (0 in AL produces
a blank window)
Place blank-line attribute in BH
Place upper-left row number in CH
Place upper-left column number in CL
Place lower-right row number in DH
Place lower-right column number in DL
FUNCTION CODE 8: Read Character and Attribute
Action: Reports the character and attribute code at the current
cursor position.
Register setup: Place 8 in AH
Place page number in BH (text modes)
Returns: Character at cursor is in AL
Attribute at cursor is in AH
FUNCTION CODE 9: Write Character and Attribute
Action: Writes a specified character and attribute to the
current cursor position.
Register setup: Place 9 in AH
Place page number in BH (text modes)
Place character in AL
Place attribute (text modes) or color (graphics modes)
in BL
Place number of characters in CX
Note: The character is written the indicated number of times starting at
the current cursor position; the cursor position remains unchanged.
FUNCTION CODE 15: Return Current Video State
Action: Reports the video mode, number of text columns, and the
current page value.
Register setup: Place 15 in AH
Returns: Current mode is in AL
Number of columns is in AH
Current active page number is in BH
──────────────────────────────────────────────────────────────────────────
Developing a Library of C Functions
Our next step is to develop a set of C functions that use the video I/O
interrupt. In this section, we will design several functions, each general
enough to be useful for a variety of programs. We'll develop the functions
individually but then collect them in one file so that they can share a
common set of include files and definitions. You'll find the contents of
this combined file, which is called SCRFUN.C, in Listing 13-23 beginning
on p. 441. Finally, we'll use the LIB utility to make a library of the
video functions.
Setting the Cursor
First, we need two C functions: one to set the cursor and another to
report the current cursor position. We use functions 2 and 3 of the video
interrupt to develop our own Setcurs() and Getcurs() functions (Listing
13-13).
Pass the desired row, column, and page to Setcurs(), and it positions the
cursor. Use Getcurs() to place row and column information in variables
whose addresses we pass. What about the page variable? For now, use the
default value of 0. The following SETCURS.C program (Listing 13-14) is a
short example that uses Setcurs() to see if our programming is on the
right track.
After you type in a row and column in the form 10 20, the program places
the cursor there and then prints a message starting at that location. It's
not a spectacular program, but it shows that our function is working
correctly. As we build this library with other functions, you might want
to write similar test programs. With QuickC it doesn't take long to do.
──────────────────────────────────────────────────────────────────────────
#include <dos.h>
#define VIDEO 0x10
#define SETCURSOR 2
#define GETCURSOR 3
/* Setcurs() -- sets cursor to given row, column */
void Setcurs(row, col, page)
unsigned char row, col, page;
{
union REGS reg;
reg.h.ah = SETCURSOR;
reg.h.dh = row;
reg.h.dl = col;
reg.h.bh = page;
int86(VIDEO, ®, ®);
}
/* Getcurs() -- reports current cursor position */
void Getcurs(pr, pc, page)
unsigned char *pr, *pc, page;
{
union REGS reg;
reg.h.ah = GETCURSOR;
reg.h.bh = page;
int86(VIDEO, ®, ®);
*pr = reg.h.dh; /* row number */
*pc = reg.h.dl; /* column number */
}
──────────────────────────────────────────────────────────────────────────
Listing 13-13. The setcurs() function.
──────────────────────────────────────────────────────────────────────────
/* setcurs.c -- moves cursor, checks out Setcurs() */
#include <dos.h>
#include <stdio.h>
#define VIDEO 0x10
#define SETCURSOR 2
void Setcurs(unsigned char, unsigned char,
unsigned char);
main()
{
int row, col;
printf("Enter row and column: (q to quit)\n");
while (scanf("%d %d", &row, &col) == 2)
{
Setcurs(row, col, 0);
printf("Enter row and column: (q to quit)");
}
}
/* Setcurs() -- sets cursor to row, column, and page */
void Setcurs(row, col, page)
unsigned char row, col, page;
{
union REGS reg;
reg.h.ah = SETCURSOR;
reg.h.dh = row;
reg.h.dl = col;
reg.h.bh = page;
int86(VIDEO, ®, ®);
}
──────────────────────────────────────────────────────────────────────────
Listing 13-14. The SETCURS.C program.
Setting the page to 0 worked fine in our example; however, we may need to
use pages later, so let's look at that topic.
Getting and Setting the Page
The information displayed on the screen is read from a dedicated section
of memory called video memory. The amount of memory available depends upon
the video adapter. In some modes, video memory can hold two or more
screenfuls of data. In those cases, you can divide video memory into
separate pages, one page per screenful. This lets a program alter one page
in memory while displaying the other on the screen. To set a page, we will
use the Setpage() function (Listing 13-15 on the following page).
By default, screen modes start at page 0, and we'll also use that page for
a while. But to keep our programming general, we need a function that can
tell our code which is the current page. We use function 15 to develop the
QuickC Getpage() function (Listing 13-16). The interrupt function places
the page number in the BH register, and the function returns that value to
the program.
──────────────────────────────────────────────────────────────────────────
/* Setpage() -- sets page to given value */
#include <dos.h>
#define VIDEO 0x10
#define SETPAGE 5
void Setpage(page)
unsigned char page;
{
union REGS reg;
reg.h.ah = SETPAGE;
reg.h.al = page;
int86(VIDEO, ®, ®);
}
──────────────────────────────────────────────────────────────────────────
Listing 13-15. The Setpage() function.
──────────────────────────────────────────────────────────────────────────
/* Getpage() -- obtains the currently active page */
#include <dos.h>
#define VIDEO 0x10
#define GETMODE 15
unsigned char Getpage()
{
union REGS reg;
reg.h.ah = GETMODE;
int86(VIDEO, ®, ®);
return reg.h.bh;
}
──────────────────────────────────────────────────────────────────────────
Listing 13-16. The Getpage() function.
Clearing the Screen
Another useful function is one that clears the screen. None of the
interrupt functions specialize in that, but the Scroll Up function
(function 6) can perform this task. Note in Table 13-6 that if register
AL is set to zero, the entire designated area is cleared. However, several
other registers also must be set. You define the area to be cleared by
giving the coordinates of the upper-left and the lower-right corners.
The BIOS routine starts numbering with 0, unlike ANSI.SYS, which starts
with 1. This means the upper-left row and column are 0, the lower-right
row is 24, and the lower-right column is 79. (We assume you're using an
80-by-25 display.) The least straightforward register setting is the
attribute setting for blank lines in register BH. An attribute is a value
in the range 0 through 255 that modifies the display. The normal attribute
is 7 for a "white-on-black" display. ("White" is white on a color display,
but on a monochrome monitor "white" usually is green or amber.) Other
values produce reverse video, blinking, underlining (on some monitors),
and colors (on some monitors). We'll use the value 7.
We use these register values to construct the following Clearscr()
function (Listing 13-17).
──────────────────────────────────────────────────────────────────────────
/* Clearscr() -- clears the screen */
#include <dos.h>
#define VIDEO 0x10
#define SCROLLUP 6
#define ROWS 25
#define COLS 80
void Clearscr()
{
union REGS reg;
reg.h.ah = SCROLLUP;
reg.h.al = 0; /* clear the window */
reg.h.ch = 0;
reg.h.cl = 0;
reg.h.dh = ROWS - 1;
reg.h.dl = COLS - 1;
reg.h.bh = NORMAL;
int86(VIDEO, ®, ®);
}
──────────────────────────────────────────────────────────────────────────
Listing 13-17. The Clearscr() function.
Reading and Writing Characters and Attributes
Before we use BIOS routines to read from and write to the screen, you need
to know how the video system works. The video adapter has its own memory,
which it uses to represent the screen. Let's concentrate for now on the
80-by-25 text modes, the ones you probably use most often. All standard
IBM video controllers (Monochrome, CGA, EGA, MCGA, and VGA) use the same
scheme for their 80-by-25 text modes, so this discussion applies to all.
──────────────────────────────────────────────────────────────────────────
Quick Tip
If you are ambitious, you can generalize this function to work with
40-by-25 displays by using function 15 of the 0x10 interrupt to find the
number of columns actually being used. A call to function 15 places that
number of columns into the AH register. Subtract 1 from this number (to
account for the fact that column numbering begins with column 0), save the
result, and assign it to the DL register before you call function 6.
──────────────────────────────────────────────────────────────────────────
You can think of an 80-by-25 screen as holding 2000 cells, each capable of
displaying a character. Each cell is represented by two bytes in the video
memory. One byte holds the code for the character, and the second byte
holds the attribute, which determines how the character is displayed. When
a program sends output to the screen, the characters actually are first
stored in video memory. A microprocessor called a video controller then
scans the video memory, mapping the characters it finds there to the
screen. Video interrupt 0x10 functions 8 and 9, which read and write
characters and attributes to the screen, actually work with the video
memory. (The monochrome display system uses a different memory address
from the others, but the BIOS calls adjust for that.)
The character code consists of the usual ASCII code plus extensions to the
code that enable certain non-ASCII characters to be displayed on the
screen. (IBM provides 128 such additional characters in its extended
character set.) The attribute code also is simple, especially for
black-and-white displays. Think of the attribute byte as a series of eight
bits, numbers 7 to 0, left to right. To generate the normal
black-and-white display, set the bits to 00000111. To produce reverse
video, set the bits to 01110000. Note that these binary values translate
to 0x7 and 0x70, respectively.
In addition, you can intensify the foreground display by setting bit 3 to
1 or put the display in "blink" mode by setting bit 7 to 1. The attributes
we've discussed here produce white-on-black (or black-on-white) characters
for the monochrome display and for color-text displays. We discuss
color-related attributes in Chapter 14.
To produce both normal and reverse video, our program must write the
attribute as well as the character. We use video I/O function 9 instead of
putch(), because the latter writes only characters, not attributes. Our
Write_ch_atr() C function (Listing 13-18) uses that interrupt routine.
This function writes the character-attribute pair num times to display a
single pair several times in a row. We will use a num value of 1, but to
preserve generality, we did not build that value into the function.
One of our program goals was converting normal text to reverse video by
passing the cursor over it. You can do that simply by changing the
attribute at the cursor location. Because no BIOS function merely changes
an attribute, we need to write a character-attribute pair. One way to do
this is to read the current character from the screen and to then rewrite
it using a different attribute. So let's start by devising a Read_ch_atr()
(Listing 13-19) function to read the character and attribute at the
current cursor location.
Because the function must return two values, we pass it the addresses of
the two variables to which the values will be assigned. To read the
character and attribute at the current cursor position on page 0 into the
variables ch and attr, make this call:
Read_ch_atr(&ch, &attr, 0);
We also could have the function return a two-member structure.
──────────────────────────────────────────────────────────────────────────
/* Write_ch_atr() -- writes characters and attributes */
#include <dos.h>
#define VIDEO 0x10
#define WRITECHATR 9
void Write_ch_atr(ch, atr, page, num)
unsigned char ch, atr, page;
unsigned int num;
{
union REGS reg;
reg.h.ah = WRITECHATR;
reg.h.al = ch;
reg.h.bl = atr;
reg.h.bh = page;
reg.x.cx = num;
int86(VIDEO, ®, ®);
}
──────────────────────────────────────────────────────────────────────────
Listing 13-18. The Write_ch_atr() function.
Now we use the last two functions to produce the function our program
requires. The Rewrite() function (Listing 13-20 on the following page)
reads the current character and rewrites it with a potentially changed
attribute.
If speed is an issue, which it usually isn't for keyboard input, you can
speed up Rewrite() by having it use int86() to call the read and write
BIOS functions directly instead of going through Read_ch_atr() and
Write_ch_atr().
──────────────────────────────────────────────────────────────────────────
/* Read_ch_atr() -- reads character and attribute at */
/* cursor location */
#include <dos.h>
#define VIDEO 0x10
#define READCHATR 8
void Read_ch_atr(pc, pa, page)
unsigned char *pc, *pa;
unsigned char page;
{
union REGS reg;
reg.h.ah = READCHATR;
reg.h.bh = page;
int86(VIDEO, ®, ®);
*pc = reg.h.al; /* character at cursor */
*pa = reg.h.ah; /* attribute at cursor */
}
──────────────────────────────────────────────────────────────────────────
Listing 13-19. The Read_ch_atr() function.
──────────────────────────────────────────────────────────────────────────
/* Rewrite() -- changes attribute of on-screen */
character */
void Read_ch_atr(), Write_ch_atr(); /* used by */
/* Rewrite()*/
void Rewrite(at, page)
unsigned char at, page;
{
unsigned char ch, atr;
Read_ch_atr(&ch, &atr, page);
Write_ch_atr(ch, at, page, 1);
}
──────────────────────────────────────────────────────────────────────────
Listing 13-20. The Rewrite() function.
More Cursor Movement
We already have a function to set the cursor at a given row or column. But
our primitive text editor really needs functions to move the cursor one
column to the right when the Right Arrow key is pushed, and so on. We can
use Setcurs() to create such functions. The Cursrt_lim() function (Listing
13-21) demonstrates how to construct a right-movement function.
Getcurs() and Setcurs() require the current page number; the Cursrt_lim()
function uses Getpage() to obtain that information. Also, the function
prevents the cursor from going past the column defined by limit. Our
program will use a limit of 79, corresponding to the right side of the
screen, but the numeric value is not built into the function. This
variable limit lets you use the function with a program that confines the
cursor to a section of the screen or with one that uses a 40-column
screen.
──────────────────────────────────────────────────────────────────────────
/* Cursrt_lim() -- moves cursor one space to the */
right, but not past a set limit */
void Getcurs(), Setcurs(); /* functions used */
unsigned char Getpage(); /* by this function */
unsigned char Cursrt_lim(limit)
unsigned char limit;
{
unsigned char row, col, page;
unsigned char status = 1;
Getcurs(&row, &col, page = Getpage());
if (col < limit)
Setcurs(row, col + 1, page);
else
status = 0;
return status;
}
──────────────────────────────────────────────────────────────────────────
Listing 13-21. The Cursrt_lim() function.
Also, the program uses a return value to inform the calling program
whether the cursor reached its limit. This gives the calling program the
option of responding in some way, such as beeping or moving the cursor to
the beginning of the next line, whenever the limit is reached.
We can modify Cursrt_lim() to create functions corresponding to the other
arrow keys. We'll show you these when we gather all the functions together
into one file.
Putting the Library Together
By now we've created a small library of short, BIOS-based C functions.
Before we use them in our intended sample program, let's reflect on how to
organize this block of functions. One method is to give each its own file.
Then, when we want to use a particular function in a program, we can add
its filename to the QuickC programming list. Or we can simply append the
function file to the program file. Another approach is to consolidate all
the functions into one file and to add that file to the program list. This
is more convenient, but it might result in adding code to your program for
functions it doesn't use. If you use the functions frequently, the most
satisfactory approach is to make a library file for them. (This procedure
was described in Chapter 12.)
Here's one way to make the library. Open the SCRFUN.C file from QuickC.
Choose Compile from the Run menu and specify the Obj option. Then choose
Compile File to produce a file called SCRFUN.OBJ. Now go to MS-DOS and
enter the LIB command. Answer the prompts as shown:
Library name: scrfun
Library does not exist: create? y
Operations: +scrfun
List file: scrfun
The LIB command creates a library file called SCRFUN.LIB in the current
directory. You can then copy it to your library directory. The LIB command
also creates a text file called SCRFUN that lists the names of the
functions in the library.
To help organize these functions, gather all the defined constants
together into an include file. To this file, add function prototypes for
all the functions. Then you can use this include file with your program.
You still must incorporate the actual code by appending the source files
or adding files to the program list or by using a library, but using the
include file saves you the trouble of having to declare the functions. It
also includes definitions useful to a program. We'll use the scrn.h
include file (Listing 13-22 on the following page) for our programs.
──────────────────────────────────────────────────────────────────────────
/* scrn.h -- header file for BIOS video I/O functions */
/* contained in scrfun.c and scrfun.lib */
#define VIDEO 0x10
#define SETMODE 0
#define SETCURSOR 2
#define GETCURSOR 3
#define SETPAGE 5
#define SCROLL 6
#define READCHATR 8
#define WRITECHATR 9
#define GETMODE 15
#define NORMAL 0x7
#define VIDREV 0x70
#define INTENSE 0x8
#define BLINK 0x80
#define COLS 80
#define ROWS 25
#define TEXTBW80 2
#define TEXTC80 3
#define TEXTMONO 7
void Clearscr(void),
Setvmode(unsigned char),
Setpage(unsigned char),
Setcurs(unsigned char, unsigned char,
unsigned char),
Read_ch_atr(unsigned char *, unsigned char *,
unsigned char),
Write_ch_atr(unsigned char, unsigned char,
unsigned char, unsigned int),
Rewrite(unsigned char, unsigned char),
Getcurs(unsigned char *, unsigned char *,
unsigned char);
unsigned char Getvmode(void),
Getpage(void),
Curslt_lim(unsigned char),
Cursrt_lim(unsigned char),
Cursup_lim(unsigned char),
Cursdn_lim(unsigned char);
/* macro definitions */
#define Home() Setcurs(0, 0, Getpage())
/* the next four macros set cursor limits to the */
/* full screen */
#define Curslt() Curslt_lim(0)
#define Cursrt() Cursrt_lim(COLS - 1)
#define Cursdn() Cursdn_lim(ROWS - 1)
#define Cursup() Cursup_lim(0)
──────────────────────────────────────────────────────────────────────────
Listing 13-22. The scrn.h include file.
The scrn.h file includes some function numbers that we won't use until
later chapters. It also has some constants that we'll use in our program.
Finally, note the macros at the end of the file. The Home() macro homes
the cursor, and the cursor-movement macros select a range corresponding to
the entire screen.
For convenience, we've collected all the new functions together as shown
in Figure 13-6 in a file called SCRFUN.C (Listing 13-23). We discuss the
Getvmode() and Setvmode() functions in Chapter 15.
┌───────────────────┐
┌─────┐ │ │
Setcurs() │ ├──────┼─────────── │
└─────┘ │ │
┌─────┐ │ │
Getcurs() │ ├────────────┼───── │
└─────┘ │ │
┌─────┐ │ │
Setpage() │ ├──────┼─────────── │
└─────┘ │ │
┌─────┐ │ │
Setvmode() │ ├────────────┼───── │
└─────┘ │ │
┌─────┐ │ │
Clearscr() │ ├──────┼─────────── │
└─────┘ │ │
┌─────┐ │ │
Read_ch_atr() │ ├────────────┼───── │
└─────┘ │ │
┌─────┐ │ │
Write_ch_atr() │ ├──────┼─────────── │
└─────┘ │ │
┌─────┐ │ │
Rewrite() │ ├────────────┼───── │
└─────┘ │ │
┌─────┐ │ │
Getvmode() │ ├──────┼─────────── │
└─────┘ │ │
┌─────┐ │ │
Getpage() │ ├────────────┼───── │
└─────┘ │ │
┌─────┐ │ │
Curslt_lim() │ ├──────┼─────────── │
└─────┘ │ │
┌─────┐ │ │
Curst_lim() │ ├────────────┼───── │
└─────┘ │ │
┌─────┐ │ │
Cursup_lim() │ ├──────┼─────────── │
└─────┘ │ │
┌─────┐ │ │
Cursdn_lim() │ ├────────────┼───── │
└─────┘ │ │
└───────────────────┘
SCRFUN.C
Figure 13-6. The SCRFUN.C program combines the functions we created
previously.
──────────────────────────────────────────────────────────────────────────
/* scrfun.c -- contains several video BIOS calls */
/* Setcurs() sets the cursor position */
/* Getcurs() gets the cursor position */
/* Setpage() sets the current video page */
/* Setvmode() sets the video mode */
/* Clearscr() clears the screen */
/* Read_ch_atr() reads the character and */
/* attribute at the cursor */
/* Write_ch_atr() writes a character and */
/* attribute at the cursor */
/* Rewrite() rewrites a screen character */
/* with a new attribute */
/* Getvmode() gets the current video mode */
/* Getpage() gets the current video page */
/* */
/* The following functions use Setcurs() to move the */
/* cursor one position at a time up to a limit. */
/* Curslt_lim() moves cursor one column left */
/* Cursrt_lim() moves cursor one column right */
/* Cursup_lim() moves cursor one line up */
/* Cursdn_lim() moves cursor one line down */
/* */
/* Programs using these functions should include the */
/* scrn.h file */
#include <dos.h>
#include "scrn.h"
/* sets cursor to row, column, and page */
void Setcurs(row, col, page)
unsigned char row, col, page;
{
union REGS reg;
reg.h.ah = SETCURSOR;
reg.h.dh = row;
reg.h.dl = col;
reg.h.bh = page;
int86(VIDEO, ®, ®);
}
/* gets current cursor row, column for given page */
void Getcurs(pr, pc, page)
unsigned char *pr, *pc, page;
{
union REGS reg;
reg.h.ah = GETCURSOR;
reg.h.bh = page;
int86(VIDEO, ®, ®);
*pr = reg.h.dh; /* row number */
*pc = reg.h.dl; /* column number */
}
/* sets page to given value */
void Setpage(page)
unsigned char page;
{
union REGS reg;
reg.h.ah = SETPAGE;
reg.h.al = page;
int86(VIDEO, ®, ®);
}
/* sets video mode to given mode */
void Setvmode(mode)
unsigned char mode;
{
union REGS reg;
reg.h.ah = SETMODE;
reg.h.al = mode;
int86(VIDEO, ®, ®);
}
/* clear the screen */
void Clearscr()
{
union REGS reg;
reg.h.ah = SCROLL;
reg.h.al = 0;
reg.h.ch = 0;
reg.h.cl = 0;
reg.h.dh = ROWS - 1;
reg.h.dl = COLS - 1;
reg.h.bh = NORMAL;
int86(VIDEO, ®, ®);
}
/* reads the character and attribute at the cursor */
/* position on a given page */
void Read_ch_atr(pc, pa, page)
unsigned char *pc, *pa;
unsigned char page;
{
union REGS reg;
reg.h.ah = READCHATR;
reg.h.bh = page;
int86(VIDEO, ®, ®);
*pc = reg.h.al; /* character at cursor */
*pa = reg.h.ah; /* attribute at cursor */
}
/* writes a given character and attribute at the */
/* cursor on a given page for num times */
void Write_ch_atr(ch, atr, page, num)
unsigned char ch, atr, page;
unsigned int num;
{
union REGS reg;
reg.h.ah = WRITECHATR;
reg.h.al = ch;
reg.h.bl = atr;
reg.h.bh = page;
reg.x.cx = num;
int86(VIDEO, ®, ®);
}
/* rewrites the character at the cursor using */
/* attribute at */
void Rewrite(at, page)
unsigned char at, page;
{
unsigned char ch, atr;
Read_ch_atr(&ch, &atr, page);
Write_ch_atr(ch, at, page, 1);
}
/* obtains the current video mode */
unsigned char Getvmode()
{
union REGS reg;
reg.h.ah = GETMODE;
int86(VIDEO, ®, ®);
return reg.h.al;
}
/* obtains the current video page */
unsigned char Getpage()
{
union REGS reg;
reg.h.ah = GETMODE;
int86(VIDEO, ®, ®);
return reg.h.bh;
}
/* moves cursor one column left, but not past */
/* the given limit */
unsigned char Curslt_lim(limit)
unsigned char limit;
{
unsigned char row, col, page;
unsigned char status = 1;
Getcurs(&row, &col, page = Getpage());
if (col > limit)
Setcurs(row, col - 1, page);
else
status = 0;
return status;
}
/* moves cursor one column right, but not past */
/* the given limit */
unsigned char Cursrt_lim(limit)
unsigned char limit;
{
unsigned char row, col, page;
unsigned char status = 1;
Getcurs(&row, &col, page = Getpage());
if (col < limit)
Setcurs(row, col + 1, page);
else
status = 0;
return status;
}
/* moves cursor one row down, but not past */
/* the given limit */
unsigned char Cursup_lim(limit)
unsigned char limit;
{
unsigned char row, col, page;
unsigned char status = 1;
Getcurs(&row, &col, page = Getpage());
if (row > limit)
Setcurs(row - 1, col, page);
else
status = 0;
return status;
}
/* moves cursor one row down, but not past */
/* the given limit */
unsigned char Cursdn_lim(limit)
unsigned char limit;
{
unsigned char row, col, page;
unsigned char status = 1;
Getcurs(&row, &col, page = Getpage());
if (row < limit)
Setcurs(row + 1, col, page);
else
status = 0;
return status;
}
──────────────────────────────────────────────────────────────────────────
Listing 13-23. The SCRFUN.C program.
Our small routines certainly create a big file! However, you need only
compile it once. After that, you can use the .OBJ or .LIB versions. We
assume that you create a library file called SCRFUN.LIB.
A Text Program
Finally, after much development, we have at hand all the tools we need for
our program. The ROAMSCRN.C program (Listing 13-24) shows the results of
our efforts. To run the program within the QuickC environment, be sure
that Screen Swapping On is active.
──────────────────────────────────────────────────────────────────────────
/* roamscrn.c -- puts text on screen, positions */
/* cursor with arrow keys, uses F1 */
/* and F2 to control video inverse */
/* program list -- roamscrn.c, scrfun.lib */
/* user include files -- keys.h, scrn.h */
/* Note: Activate Screen Swapping On in Debug menu */
#include <conio.h>
#include "keys.h"
#include "scrn.h"
#define BELL '\a'
#define ESC '\033'
#define PAGE 0
char *Heading =
"Use standard keys to enter text. Use arrow keys to "
"reposition cursor.\nUse F2 to turn on video inverse "
"and F1 to turn it off.\nHit the ESC key to quit.\n";
main()
{
int ch;
unsigned char atr = NORMAL;
Clearscr();
Home();
printf("%s", Heading);
while ((ch = getch()) != ESC)
{
if (ch == '\r')
{
putch('\n');
putch('\r');
}
else if (ch != 0)
{
Write_ch_atr(ch, atr, PAGE, 1);
if (!Cursrt())
putch(BELL);
}
else
{
ch = getch();
switch (ch)
{
case F1 : atr = NORMAL; break;
case F2 : atr = VIDREV; break;
case UP : Rewrite(atr, PAGE);
if (!Cursup())
putch(BELL);
break;
case DN : Rewrite(atr, PAGE);
if (!Cursdn())
putch(BELL);
break;
case LT : Rewrite(atr, PAGE);
if (!Curslt())
putch(BELL);
break;
case RT : Rewrite(atr, PAGE);
if (!Cursrt())
putch(BELL);
break;
default : break;
}
}
}
}
──────────────────────────────────────────────────────────────────────────
Listing 13-24. The ROAMSCRN.C program.
Let's see how it works. The keys.h include file is the one we used earlier
in this chapter; it defines the mnemonics for the function keys and the
cursor control keys. The scrn.h include file is the one we just presented.
We assume that you bring in the BIOS code by including the SCRFUN.LIB file
in the program list, but you can also use one of the other methods we
mentioned if you prefer.
The program begins with the attribute variable atr set to NORMAL. This is
defined in scrn.h as 7, which is the normal attribute for white-on-black
text. Next, the program clears the screen, homes the cursor, and prints an
instructive heading. Finally, in the main part of the program, a large
while loop uses getch() to read keyboard input until Esc is pressed to
terminate input.
Next, the program inspects ch, the input character typed by the user. If
it is \r, the carriage return character generated by the Enter key, the
program translates that into a newline, that is, into \n\r. If the
character is some other ASCII or extended ASCII value, the program uses
Write_ch_atr() to display that character. Why not use putch() here?
Because putch() has no provision for specifying the attribute. Note, too,
the following code fragment:
if (!Cursrt())
putch(BELL);
Write_ch_atr(), like the BIOS call it uses, does not advance the cursor
after writing the character. Therefore, we use Cursrt() to move the
cursor. Recall that we created Cursrt_lim() to stop when it reaches the
right side of the screen and that the macro Cursrt() uses the rightmost
column as the limit. If the limit is reached, Cursrt() returns a value of
0, or false, causing the if statement to execute the putch(BELL) call. The
action, then, is as follows: First the character is printed, then the
program attempts to advance the cursor one column to the right. If it can,
fine; otherwise, the system beeps. If you like, you can replace the
beeping instructions with a Setcurs() command to relocate the cursor at
the beginning of the next line.
Finally, this sequence of if-else lines processes the case of ch being 0.
This means the user entered a non-ASCII character. Another getch() call
fetches the scan code for the key, and a switch checks for two of the
function keys and for the arrow keys. Let's see what these keys do.
If the user presses F1, the attribute variable atr is set to NORMAL; if
the user presses F2, atr is set to VIDREV. This constant, defined in
scrn.h as 0x70, is the reverse video attribute. The selected value for the
variable atr is used in subsequent calls to Write_ch_atr() and Rewrite().
The attribute setting holds until another is selected.
Next, look at what happens when the Up Arrow key is pressed:
case UP : Rewrite(atr, PAGE);
if (!Cursup())
putch(BELL);
break;
The Rewrite() function reads the character, if any, at the current cursor
position and rewrites it using the current attribute. Then the cursor is
moved up a line unless it already is at the top line. In that case, the
system beeps. The purpose of the Rewrite() statement is to cause existing
text to be replaced by text using the current attribute. For example, if
you have selected the inverse attribute, then text passed over by the
cursor is rewritten with that attribute. The coding for the other arrow
keys is similar.
All in all, the main program is fairly simple. Most of the work involved
creating C functions to implement the various BIOS calls we needed to
make.
────────────────────────────────────────────────────────────────────────────
Chapter 14 Monitors and Text Modes
Professional application programs, including QuickC itself, use a much
fancier screen interface than we have used in our programs. In this
chapter we produce some of those screen features in QuickC. First we must
overcome the problem posed by the variety of different display systems.
IBM supports several monitor-video controller systems: monochrome, CGA,
EGA, MCGA, and VGA. In general, these systems use different hardware, and
different memory and port addresses. They also provide different colors,
resolutions, and graphics capabilities.
Writing programs that run on a range of video controller systems can be
troublesome, especially if you want something fancier or faster than the
teletype-like output produced by standard C Library functions. This
chapter concentrates on solving these problems for text-mode programs. We
continue using BIOS calls, and we introduce direct memory access and
ports. We also look at the IBM "graphics character" set, which lets you
create screen graphics without leaving text mode.
Monitors and Controllers
IBM has developed several different video standards, each involving its
own hardware video controller and corresponding monitors. In the PC
series, the hardware controllers are on add-on cards called "adapters." In
the new PS/2 series, however, the circuitry for controlling the monitor is
built into the motherboard. We use the term "video controller" in this
book to encompass both the adapter cards and the built-in control
circuitry.
The most widely used video controller is the Monochrome Display Adapter,
or MDA. When coupled with a monitor called the Monochrome Display, it
produces a high-resolution, text-only display consisting of 25 rows of 80
characters each.
The next most commonly used controller is the Color Graphics Adapter, or
CGA. It can be used with color or B/W monitors capable of either 40-by-25
or 80-by-25 text displays (but not the Monochrome Display). It has seven
separate modes of operation. Although the 80-by-25 display shows as many
characters as the Monochrome Display, its lower resolution creates coarser
text characters.
Recently, the Enhanced Graphics Adapter, or EGA, has become popular. It is
compatible with the Monochrome Display, with normal CGA displays, and with
a high-resolution monitor called the Enhanced Display (ED). Used with a
Monochrome Display, it provides a graphics mode in addition to the text
mode. Used with CGA-style monitors, it provides more colors than the CGA
board does. Used with the Enhanced Display (or equivalent), it emulates
the CGA modes with increased text resolution, and it provides three
additional graphics modes.
The newest controllers are the Multi-Color Graphics Array (MCGA), found on
the PS/2 Model 30, and the Video Graphics Array (VGA), found on the PS/2
Models 50, 60, 70, and 80. The MCGA matches CGA resolution but offers an
enormously greater range of colors. The VGA emulates the EGA modes, adds
three new graphics modes, offers higher resolution for all text modes, and
provides more colors.
Table 14-1 summarizes some of the differences in features offered by the
various video controllers we have introduced. Resolution is given in
pixels, or picture elements, the elementary display elements from which
characters and images are built. The size of a pixel depends on the
controller and the mode. A pixel in mode 0, for example, is twice as wide
as a pixel in mode 2. However, the VGA controller produces smaller pixels
than the CGA controller, even when both are in mode 0. Also, not all
monitors have sufficient resolution to support a controller's use of
pixels. A CGA monitor, for example, is physically incapable of generating
the higher resolution (smaller pixel) modes of the EGA and VGA. In
general, all modes cannot produce the maximum number of colors, and only a
subset of available colors can be shown at any one time.
Table 14-1 Video Controllers
Name Horizontal Vertical Colors Modes Monitors
Resolution Resolution
──────────────────────────────────────────────────────────────────────────
MDA 720 350 2 1 MD☼
CGA 640/320 200 16 7 Color☼, B/W☼
EGA 640/320 350/200 64 12 ED☼, MD☼,
Color☼, B/W☼
MCGA 720/360/640/320 400/480/200 262, 11 PSM☼, ED☼, MD☼,
144 Color☼, B/W☼
VGA 720/360/640/320 400/480/350/200 262, 15 PSM☼, ED☼, MD☼,
144 Color☼, B/W☼
──────────────────────────────────────────────────────────────────────────
Text Modes and Portability
Fortunately, all these video controllers support an 80-by-25 text mode,
and that simplifies the task of writing programs to run with all
combinations.
Controller Similarities
A comparison of 80-by-25 text modes for different hardware combinations
shows both similarities and differences. In all cases, the screen is
treated as an array of characters rather than as an array of pixels. That
is, you can only display or alter entire characters, not the individual
pixels that comprise the characters.
All controllers use two bytes of memory to represent each text-mode
character. One byte holds the character's ASCII code, and the other byte
holds the character's display attribute. All video controllers also
contain random access memory (video RAM) in which character data is mapped
to the display. That is, the controller periodically scans the video RAM
to determine which characters it should display. Therefore, to change the
screen display, you must change the appropriate bytes in the video RAM.
Note that text-mode video RAM always consists of 4000 bytes: One screen
holds 80 x 25, or 2000, characters, each represented by two bytes.
All controllers also maintain a table of character fonts called a
"character generator." The controller uses these pixel patterns to
physically represent characters on the screen. For example, ASCII code 72
in the video RAM tells the controller to put an H at a screen location,
and the character font table specifies the particular "H" pixel pattern to
use. (See Figure 14-1 on the following page.)
These similarities ease the task of writing text programs that are
compatible with the various displays.
Video RAM Font ROM
┌─────────────────┐ ┌─────────────────┐
│ 072 (ASCII code)│ │ H (Font info) │
│ ▒ │ │ ▒ │
└──▒──────────────┘ └─▒───────────────┘
▒ ▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒
╔═════▒═════════════════════╗
║ ┌──▼──────────────────┐ ║
║ │ H │ ║
║ │ │ ║
║ │ │ ║
║ │ │ ║
║ │ │ ║
║ └─────────────────────┘ ║
╚═══════════════════════════╝
Display
Figure 14-1. Producing characters with the MDA.
Differences between displays complicate the programming process. For
example, one powerful video technique called "direct memory access" uses
pointers to video RAM to directly alter RAM contents. However, the MDA
uses a different video RAM address than other controllers, so your program
must always test for its use. Another programming technique uses "ports"
to access registers on the controllers. However, the various controllers
have different numbers of registers, and each register has a different
port address and performs a different function. This makes for very
involved hardware programming.
Video controllers also contain differing amounts of video RAM. The MDA has
only enough memory to hold one screenful, or page, of characters. The
other controllers hold enough memory to hold four or more pages of text.
Most controllers offer different screen resolutions. Although all the
controllers display a maximum of 2000 characters on the screen, some can
generate more pixels than others. For example, the CGA screen consists of
a matrix of 640 horizontal pixels (for 80 characters) by 200 vertical
pixels (for 25 display lines). The net result is that each character is
represented by an 8-by-8-pixel grid, or "character box." The MDA, on the
other hand, generates 720 horizontal pixels and 350 lines, providing a
9-by-14-pixel character box. Thus, MDA characters look better than their
CGA counterparts because each character is drawn with more detail, as
shown in Figure 14-2.
Finally, the color displays can use the attribute byte to specify
foreground and background colors for each character. Table 14-2 provides
a summary of the different characteristics of video controllers operating
in text mode.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 14-2 can be found on p.453 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 14-2. Character boxes.
Table 14-2 Summary of Text-Mode Differences
Controller Video RAM Starting Pages Character Color
Address Box Size
──────────────────────────────────────────────────────────────────────────
MDA 0xB8000 1 9 x 14 No
CGA 0xB0000 4 8 x 8 Yes
EGA (mode 7) 0xB8000 4/8☼ 9 x 14 No
EGA (modes 2, 3) 0xB0000 4/8☼ 8 x 14 Yes
MCGA 0xB0000 8 9 x 16 Yes
VGA 0xB0000 8 9 x 16 Yes
──────────────────────────────────────────────────────────────────────────
Device-independent Programming
When programming for the PC, you have the choice of programming for
specific hardware or ignoring the hardware altogether. The direct memory
access method discussed later in this chapter uses hardware information
explicitly. To write device-independent programs that don't require
explicit hardware information, use one or more of the following methods:
1. Program with the standard C Library output functions such as printf()
and putchar(). This results in portable code, but it limits the
positioning of text and doesn't permit the use of color.
2. Use the ANSI.SYS escape sequences, as described in Chapter 13.
However, if your program utilizes the cursor-control keys, for
example, you must use console I/O functions, which restrict
portability. Also, using ANSI.SYS inhibits some special features of
the EGA, such as the 43-line display. The ANSI approach works on all
systems that recognize the standard ANSI codes; IBM PCs and clones
must have the ANSI.SYS driver installed.
3. Use IBM PC BIOS calls, as described in Chapter 13. The BIOS includes
programs for all PC video controllers, and it selects the appropriate
code for the display and controller you are using. That's why our
examples in Chapter 13 didn't specify a monitor or controller. BIOS
calls also support using more than one page of screen memory; but
because the MDA has only one page, we suggest you restrict
applications to page 0.
We thoroughly covered the first two choices in the last chapter. Although
we also discussed BIOS calls, we skipped some of the detail until you
understood more about the hardware. In the next section we will discuss
BIOS calls in greater detail.
Working with BIOS Again: Attributes
In Chapter 13, we built a small library (SCRFUN.LIB) of BIOS-based C
functions that are hardware-independent. In fact, insulating the user from
the hardware is one of the primary reasons for having a BIOS. For
instance, we can use the same BIOS calls to control the way in which
characters are displayed on an MDA, CGA, EGA, or VGA monitor. The
attribute of a character controls its appearance. Let's see how we can use
the BIOS to investigate and control attributes.
An attribute is a 1-byte value in which the individual bits have
particular meanings that affect the appearance of the associated
character. For example, with the Monochrome Display Adapter, bit 7 of the
attribute controls the blink function, bits 6─4 control the background,
bit 3 controls the intensify foreground function, and bits 2─0 control the
foreground──that is, the pixels constituting the character. (See Figure
14-3.) Table 14-3 lists the standard attribute values used by the MDA.
Bit numbers ─────7 6 5 4 3 2 1 0
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ │ │ │ │ │ │ │ │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
│ └────────┬────────┘ │ └────────┬────────┘
│ Background │ Foreground
Blink bit Intensity bit
Figure 14-3. Monochrome attribute bits.
Table 14-3 Monochrome Attributes
Bit Pattern Hex Value Meaning
──────────────────────────────────────────────────────────────────────────
0000 0000 0x00 No display
0000 0111 0x07 Normal display
0111 0000 0x70 Reverse video
0000 0001 0x01 Underline
0111 0111 0x77 Whiteout
1xxx xxxx 0x80 Blink mode
xxxx 1xxx 0x08 Intensified foreground
──────────────────────────────────────────────────────────────────────────
Note: The last two entries are used with different modes. For example,
10000111 (0x87) is normal display with blinking, while 11110000 (0xF0) is
reverse video with blinking. The x's indicate that those values don't
affect blinking or intensity.
The other video controllers use bits 6─4 to control the color of the
background and bits 2─0 to control the color of the foreground. Bits 7 and
3 serve the same function as they do for the MDA. Figure 14-4 shows the
color that each bit controls.
Bit numbers ─────7 6 5 4 3 2 1 0
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ BL │ R │ G │ B │ I │ R │ G │ B │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
│ │ Red Green Blue │ │ │ Red Green Blue │
│ └────────┬────────┘ │ └────────┬────────┘
│ Background │ Foreground
Blink Intensify
foreground
Figure 14-4. Color attribute bits.
──────────────────────────────────────────────────────────────────────────
The EGA BIOS
The IBM BIOS was created long before the inception of the EGA. How, then,
can you use BIOS routines to control the EGA? The EGA card comes with a
set of BIOS interrupt 0x10 video I/O routines in its own ROM. Recall that
the address of each interrupt routine is stored in the interrupt vector
table. When you boot an EGA system, the entry for video interrupt 0x10 is
loaded with the EGA BIOS address instead of the motherboard BIOS address.
Thus, the old BIOS routines are bypassed and the new EGA-supplied ones are
used.
──────────────────────────────────────────────────────────────────────────
To produce a blue character on a red screen, use an attribute of 01000001.
This turns on the red background bit and the blue foreground bit. To make
the foreground a bright blue, turn on the intensity bit with 01001001.
If you set both the blue and green foreground bits, the blue and the green
phosphors on the display screen are simultaneously turned on, producing a
color called cyan. Setting all three foreground bits turns on all three
colors, which, by the laws of video color mixing, produces white.
Similarly, clearing all three bits causes no pixels to be turned on,
producing black. Therefore, the "normal" monochrome attribute of 00000111
also produces white-on-black characters for the CGA, EGA, and VGA. Table
14-4 shows the colors generated by the various 3-bit combinations.
Table 14-4 Text Color Values
Bit Pattern Hex Foreground Hex Background Color
──────────────────────────────────────────────────────────────────────────
000 0x0 0x00 Black
001 0x1 0x10 Blue
010 0x2 0x20 Green
011 0x3 0x30 Cyan
100 0x4 0x40 Red
101 0x5 0x50 Magenta
110 0x6 0x60 Dark yellow
(brown)
111 0x7 0x70 White (light gray)
──────────────────────────────────────────────────────────────────────────
Note: The hex value 0x8 intensifies the foreground color, and 0x80 makes
the character blink. Also, all attributes can be combined using logical
operators.
──────────────────────────────────────────────────────────────────────────
Colors
All colors can be produced by combining three primary colors together in
varying proportions. The three "additive" primary colors are red, green,
and blue. For example, when you direct a beam of green light and a beam of
red light toward a piece of white paper, the area where the beams overlap
(or are "added") appears yellow.
A color video screen also creates colors by combining the additive
primaries. Each pixel on a color screen contains individual red, green,
and blue dots. Turning a pixel blue amounts to turning on the blue dots in
the pixel. To produce yellow, you turn on the green and the red dots in a
pixel──the eye perceives only the combined light, which is yellow.
The PC's system of using numbers to represent color imitates the physical
color-mixing process. For example, in binary notation, the color number
for red is 100 and the color number for green is 010. Turning on both the
red and the green bits corresponds to specifying the binary number 110,
which is the code for yellow.
──────────────────────────────────────────────────────────────────────────
Suppose you want a yellow character on a blue background. Because yellow
is produced by combining red and green light (bits 2 and 1), and
background blue is bit 4, the corresponding attribute is 00010110, or
0x16. (The actual colors you see depend on your monitor and its
adjustments.)
It's more interesting to display the attributes on the screen than it is
to read about them, so let's develop a program that changes the attribute
bits to demonstrate how the colors change. First we must write a function
that prints a string using a given attribute. Using the functions from our
SCRFUN.LIB library, we produce the following Print_attr() function
(Listing 14-1).
Print_attr() writes a character-attribute pair and moves the cursor one
position to the right. Print_attr() has its limitations. First, it doesn't
recognize the end of a line. Second, it doesn't have all the fancy
formatting that printf() has. (You can modify the function to handle the
end-of-line problem and use the sprintf() function to do the formatting.)
Now let's use the Print_attr() string-displaying function in a program to
display the various attributes. To demonstrate the role of each attribute
bit, the program has you type the attribute byte as a binary number. The
comments in the ATTRIB.C program (Listing 14-2 on the following page)
explain the workings of the various functions. Before you run the ATTRIB.C
program, pull down the Debug menu: Screen Swapping On should be active
(indicated by a check to the left of the option). If it is not active,
choose it from the menu to activate it.
This program works with any of the previously mentioned standard video
controllers. If you have a monochrome monitor, check to see what
nonstandard combinations such as 00000100 produce. If you have a color
monitor, enjoy the many color combinations.
──────────────────────────────────────────────────────────────────────────
/* Print_attr() -- prints the string str using */
/* attribute attr on the indicated page */
/* It uses functions from the scrfun.c file. */
void Print_attr(str, attr, page)
char *str;
unsigned char attr, page;
{
while (*str != '\0')
{
Write_ch_atr(*str++, attr, page, 1);
Cursrt();
}
}
──────────────────────────────────────────────────────────────────────────
Listing 14-1. The Print_attr() function.
──────────────────────────────────────────────────────────────────────────
/* attrib.c -- this program illustrates attributes */
/* program list: attrib.c, scrfun.lib */
/* user include files: scrn.h */
/* Note: activate Screen Swapping On in Debug menu */
#include <stdio.h>
#include <conio.h>
#include "scrn.h"
#define PAGE 0
#define ESC '\033'
char *Format = "This message is displayed using an "
"attribute value of %2X hex (%s).";
int Get_attrib(char *);
void Print_attr(char *, unsigned char, unsigned char);
main()
{
int attribute; /* value of attribute */
char attr_str[9]; /* attr. in string form */
char mesg[80];
Clearscr();
Home();
printf("Enter an attribute as an 8-digit binary "
"number, such as 00000111, and see a\n"
"message displayed using that attribute."
"Hit <Esc> to quit.\n"
"Attribute = ");
while ((attribute = Get_attrib(attr_str)) != -1)
{
Setcurs(10,0,PAGE);
sprintf(mesg, Format, attribute, attr_str);
Print_attr(mesg, attribute, PAGE);
Setcurs(2, 12, PAGE);
printf(" "); /* clear old display */
Setcurs(2, 12, PAGE);
}
Clearscr();
}
/* The following function reads in a binary number */
/* as a sequence of 1s and 0s. It places the 1 and 0 */
/* characters in a string whose address is passed as */
/* an argument. It returns the numeric value of the */
/* binary number. Bad input is summarily rejected. */
/* The function returns -1 when you press Esc. */
int Get_attrib(a_str)
char a_str[]; /* attribute as binary string */
{
int attrib[8];
int index = 7;
int ch;
int attribute = 0; /* attrib. as numeric value */
int pow;
a_str[8] = '\0'; /* terminate string */
while ((index >= 0) && (ch = getch()) != ESC)
{
if (ch != '0' && ch != '1') /* bad input */
putch('\a');
else
{
putch(ch);
a_str[index] = ch; /* string form */
attrib[index--] = ch - '0'; /* numeric */
}
}
if (ch == ESC)
return (-1);
else /* convert numeric array to a number */
{
for(index = 0, pow = 1; index < 8;
index++, pow *= 2)
attribute += attrib[index] * pow;
return attribute;
}
}
/* The following function prints the string str using */
/* attribute attr on the indicated page. */
/* It uses functions from the scrfun.c file. */
void Print_attr(str, attr, page)
char *str;
unsigned char attr, page;
{
while (*str != '\0')
{
Write_ch_atr(*str++, attr, page, 1);
Cursrt();
}
}
──────────────────────────────────────────────────────────────────────────
Listing 14-2. The ATTRIB.C program.
Attributes and Bitwise Operators
You can also manipulate attributes with the C bitwise operators that we
discussed in Chapter 7. Suppose, for example, a program uses the
following definitions:
#define NORMAL 0x07
#define VIDREV 0x70
#define BLINK 0x80
#define INTENSE 0x08
To set mode to an intense, normal attribute, we can use the bitwise
logical OR operator, as follows:
mode = NORMAL | INTENSE;
Because 1 OR anything is 1, all bits set to 1 are left on. Now, suppose
mode has already gone through several changes. At this point it might be
normal, reverse video, or have blinking on or off, etc. To turn on the
intensify mode regardless of the current state, use an instruction like
the following:
mode = mode | INTENSE;
The only bit this instruction can change is bit 3, the intensity bit,
because all the other bits of INTENSE are 0, and 0 OR any bit is merely
that bit. (That is, 0 | 0 is 0, and 1 | 0 is 1.) Furthermore, this
instruction always sets bit 3 to 1, regardless of its previous value. (0 |
1 is 1, and 1 | 1 is 1.) Incidentally, you can also use a combination
assignment operator to rewrite the last C statement as follows:
mode |= INTENSE; /*unconditionally turns INTENSE on */
Sometimes an instruction must "toggle" a bit. That is, the instruction
turns on an off bit or turns off an on bit. For this, we use the EXCLUSIVE
OR operator (^). Recall that this operator produces a "true" value (1) if
one operand or the other is "true" but not if both are "true." The
following expression toggles the intensity bit:
mode = mode ^ INTENSE;
If the intensity bit in mode is initially off, the expression becomes 0 ^
1, which is 1, or on. If the intensity bit in mode is initially on, the
expression is 1 ^ 1, which is 0, or off. Again, we can simplify the
statement with the following combination assignment operator:
mode ^= INTENSE; /* toggles the intensity bit */
Compatible "Graphics"
A system with true graphics capability lets you individually control each
pixel on the screen. By its very makeup, the MDA lacks that ability. The
CGA provides graphics modes, and the EGA and VGA have additional graphics
abilities. Therefore, to produce true graphics, a program must address
specific hardware.
However, IBM has given the PC a limited but more universal graphics
capability by extending the character set. The ASCII character set uses
the values 0 through 127. But because a byte can store any value through
255, IBM added 128 additional characters to the set and assigned them code
values 128 through 255. These constitute the IBM Extended Character Set.
Many of these characters are mathematical symbols, foreign-language
characters, and so on. However, 48 of the characters (codes 176 through
223) constitute the "graphics characters," which are useful for drawing
and filling rectangular forms.
With these characters, you can do a limited amount of hardware-independent
graphics. The QuickC screen, for example, uses these characters to draw
its boxes and borders. In fact, you can use QuickC to examine the extended
ASCII set. Pull down the general Help menu and browse through it until you
reach the screen that displays the extended set.
You can also display the extended set of characters from the keyboard. To
see what character 206 looks like, first press Num Lock; then, at your
system prompt, hold down the Alt key and type 206 using the keys in the
numeric keypad. When you release Alt, the character appears on the
display.
Programming with the Graphics Character Set
Let's develop a QuickC program to help us investigate the extended
character set. Below are some of the features we need to develop in a
program that draws with the graphics characters.
■ Key-mapping so that a single keystroke generates a graphics character
■ Cursor control for drawing at different screen locations
■ An erasing feature
■ An auto-drawing feature that generates strings of characters across the
screen
■ An attribute manipulator for highlighting text or turning on blinking
Many of these goals resemble problems we solved in Chapter 13; therefore,
we can put our previously developed tools to good use now. For example, we
can use the getch() function and scan codes to use the function keys and
the cursor-control keys. We can call the BIOS to clear the screen and to
provide cursor-movement functions. And by using the method developed for
REKEY.C (Listing 13-7 on p. 411), we can map the keys. In short, we have
the tools; now we have to organize them into a workable QuickC program.
The User Interface
Today it is not enough to design a program that works. Interactive
programs require that the programmer think about the user's point of view.
In our case, for example, we need to plan how to best use the keyboard to
control the graphics characters.
For example, which key represents which character? With 48 graphics
characters, there is no obvious mnemonic method for assigning keys. And it
is unreasonable to expect a user to remember 48 random assignments. To
help the user, we display the graphics characters and key assignments at
the bottom of the screen. We also list other important keys.
Next, we must plan how to manage the drawing process. Drawing with
graphics characters often involves repeatedly using the same character.
Merely mapping a graphics character to a key is acceptable only for
repeating the character left to right because that's the way keyboard
input normally works. But drawing characters vertically or from right to
left is more difficult. Cursor control helps, but drawing a vertical line
would entail pressing the character key, using the Down Arrow key to move
down a line, using the Left Arrow key to move under the first character,
and then pressing the character key again. Therefore, we must use a
different technique.
In our solution to the problem, the character keys select, but do not
display, a graphics character. To actually display the character, the user
must press one of the arrow keys. This places the character at the current
position of the cursor, then shifts the cursor in the direction of the
arrow key. Until the user selects another character key, the current
graphics character remains active. Therefore, repeatedly pressing an arrow
key moves the cursor and leaves a display trail of the current graphics
character. This simplifies drawing horizontal and vertical lines.
Now let's add some refinements. The PgUp key disenables drawing so that
the user can move the cursor without displaying characters. The PgDn key
restores the drawing mode. The Spacebar represents the program's "eraser."
The user can select the Spacebar as the current graphics character and use
the cursor keys to delete unwanted characters. Our last refinement lets
the user select character attributes with the function keys.
We take advantage of QuickC's program list feature to split the program
into three file modules. Note that two of the modules use SCRFUN.LIB and
scrn.h and one uses keys.h, all developed in Chapter 13. We also collect
the define statements for the three modules in an include file called
grafchar.h.
First, look at the main program, GRAFCHAR.C (Listing 14-3). To run this
program within the QuickC environment, be sure that Screen Swapping On is
active (on the Debug menu).
GRAFCHAR.C is a simple program──it merely calls the other two files in the
program list: initstuf.c and drawchar.c. We will discuss these modules in
the next two sections. Before you proceed to those sections, however,
examine the grafchar.h header file (Listing 14-4).
──────────────────────────────────────────────────────────────────────────
/* grafchar.c -- draws graphics characters with */
/* attributes on the screen */
/* Program list : grafchar.c, initstuf.c, drawchar.c, */
/* scrfun.lib */
/* User include files: keys.h, scrn.h, grafchar.h */
/* Note: activate Screen Swapping On in Debug menu */
#include "grafchar.h"
unsigned char Grchr[NUMCHARS]; /* to store graphics set */
void Init_stuff(void); /* in initstuf.c */
void Draw_chars(void); /* in drawchar.c */
main()
{
Init_stuff(); /* initialize vital elements */
Draw_chars(); /* map keys to graphics characters */
}
──────────────────────────────────────────────────────────────────────────
Listing 14-3. The GRAFCHAR.C program.
──────────────────────────────────────────────────────────────────────────
/* grafchar.h -- header file for grafchar.c program */
/* Version 1 */
#define NUMCHARS 48 /* number of graphics chars */
#define SPACE '\040'
#define BOTLINE 19 /* line # for end of drawing space */
#define PAGE 0
#define GCSTART 0xB0 /* ascii for first graphics char */
#define BEEP '\a'
#define ESC '\033'
#define TRUE 1
#define FALSE 0
──────────────────────────────────────────────────────────────────────────
Listing 14-4. The grafchar.h header file.
Setting Up the Program: initstuf.c
The initstuf.c module (Listing 14-5 on the following page) sets up the
GRAFCHAR.C program. It initializes the external array grafchar.c to the 48
graphics characters and clears the screen. At the bottom of the screen, it
prints the graphics characters and their corresponding keystrokes. Also
listed are the following non-ASCII keys and their functions: The F1
function key sets the normal white-on-black text attribute; F2 selects
reverse video; F3 toggles blinking; and F4 toggles foreground intensity.
The program uses Print_attr() to show how the last three attributes appear
on screen. For example, the phrase F3 : Blinking is displayed on screen in
the blinking mode.
──────────────────────────────────────────────────────────────────────────
/* initstuf.c -- initializing module for grafchar.c */
/* assigns graphics character codes to an array */
/* and initializes screen */
#include "scrn.h" /* Clearscr(), Home(), Setcurs() */
#include "grafchar.h"
extern unsigned char Grchr[]; /* defined in grafchar.c */
void Print_attr(char *, unsigned char, unsigned char);
[bn]
void Init_stuff()
{
int i;
/* initialize array with graphics characters */
for (i = 0; i < NUMCHARS; i++)
Grchr[i] = GCSTART + i;
Clearscr();
Home();
/* show key meanings at bottom of screen */
Setcurs(BOTLINE + 1, 0, PAGE);
for (i = 0; i < 40; i++) /* graphics chars */
{
putch(Grchr[i]);
putch(SPACE);
}
Setcurs(BOTLINE + 2, 0, PAGE);
for (i = 0; i < 40; i++) /* key assignments */
{
putch('0' + i);
putch(SPACE);
}
Setcurs(BOTLINE + 3, 0, PAGE);
for (i = 40; i < NUMCHARS; i++) /* second row */
{
putch(Grchr[i]);
putch(SPACE);
}
/* show function key assignments */
printf(" SPACE : ERASE PgUp : No Draw ");
printf(" PgDn : Draw ESC : Quit");
Setcurs(BOTLINE + 4, 0, PAGE);
for (i = 40; i < NUMCHARS; i++) /* second row */
{
putch('0' + i);
putch(SPACE);
}
/* more function key assignments */
Print_attr("F1 : Normal ", NORMAL, PAGE);
Print_attr("F2 : Reverse Video ", VIDREV, PAGE);
Setcurs(BOTLINE + 5, 16, PAGE);
Print_attr("F3 : Blinking ", NORMAL | BLINK, PAGE);
Print_attr("F4 : Intense ", NORMAL | INTENSE, PAGE);
Home();
}
void Print_attr(str, attr, page)
char *str;
unsigned char attr, page;
{
while (*str != '\0')
{
Write_ch_atr(*str++, attr, page, 1);
Cursrt();
}
}
──────────────────────────────────────────────────────────────────────────
Listing 14-5. The initstuf.c module.
Drawing the Characters: drawchar.c
The final module, drawchar.c (Listing 14-6), contains the code that
translates keystrokes into action. First, it maps the 48 keystrokes to the
graphics characters. After the program initializes the Spacebar character,
it uses a switch statement to process PgUp, PgDn, the cursor control keys,
and the four function keys. Figure 14-5 on p. 467 shows some sample
output from this program.
──────────────────────────────────────────────────────────────────────────
/* drawchar.c -- drawing module for grafdraw.c */
/* translates keystrokes to graphic characters, */
/* manages cursor control and function keys */
#include <conio.h>
#include "keys.h"
#include "scrn.h"
#include "grafchar.h"
extern unsigned char Grchr[]; /* defined in grafchar.c */
void Draw_chars()
{
int ch, chout;
unsigned char attrib = NORMAL;
unsigned char draw = TRUE;
chout = Grchr[0]; /* default graphics character */
while ((ch = getch()) != ESC)
{
if (ch >= '0' && ch <= '_')
chout = Grchr[ch - '0'];
/* this maps the 0 key to the first */
/* graphics character, etc. */
else if (ch == SPACE)
chout = SPACE;
else if (ch == 0) /* process cursor keys */
{ /* and function keys */
ch = getch();
switch (ch)
{
case PU : draw = FALSE;
break;
case PD : draw = TRUE;
break;
case UP : if (draw)
Write_ch_atr(chout, attrib,
PAGE, 1);
if (!Cursup())
putch(BEEP);
break;
case DN : if (draw)
Write_ch_atr(chout, attrib,
PAGE, 1);
if (!Cursdn_lim(BOTLINE))
putch(BEEP);
break;
case LT : if (draw)
Write_ch_atr(chout, attrib,
PAGE, 1);
if (!Curslt())
putch(BEEP);
break;
case RT : if (draw)
Write_ch_atr(chout, attrib,
PAGE, 1);
if (!Cursrt())
putch(BEEP);
break;
case F1 : attrib = NORMAL; break;
case F2 : attrib = VIDREV; break;
case F3 : attrib ^= BLINK; break;
case F4 : attrib ^= INTENSE; break;
default : putch(BEEP);
}
}
}
}
──────────────────────────────────────────────────────────────────────────
Listing 14-6. The drawchar.c module.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 14-5 can be found on p.467 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 14-5. Drawing with GRAFCHAR.C.
Details of the Program
The easiest way to see how the program works is to try it out. Move the
cursor around with the arrow keys and use the keyboard to select different
graphics characters. (Remember to have Screen Swapping On active if you're
working in the QuickC environment.) To help you see why it works the way
it does, we'll look at some of the programming details next.
In the Draw_chars() function, the following statement maps the keystrokes
to the graphics characters:
if (ch >= '0' && ch <= '_')
chout = Grchr[ch - '0'];
In this function, ch is the input character; chout is the output character
used when the cursor keys are pressed. When the user presses the 0 key,
the program uses the array index of '0' - '0', or 0, which selects the
first graphics character in the array. Similarly, the 1 key selects the
second graphics character, and so on.
Next, look at how the program handles the cursor key:
case UP : if (draw)
Write_ch_atr(chout, attrib, PAGE, 1);
if (!Cursup())
putch(BEEP);
break;
If draw is set to TRUE, the program displays the current output character
(chout) using the current attribute (attrib). In this example, the cursor
then moves up one line unless it already is at the top line, in which case
the program issues a beep.
The other arrow keys are processed similarly. Note that the Down Arrow key
uses Cursdn_lim(BOTLINE) instead of Cursdn(). Recall from Chapter 13 that
Cursdn() is a macro that moves the cursor down as far as line 25;
Cursdn_lim(), however, is limited by a passed argument. Because we reserve
the bottom of the screen for the key table, the BOTLINE limit keeps the
cursor from intruding.
The program uses the following code for selecting an attribute:
case F1 : attrib = NORMAL; break;
case F2 : attrib = VIDREV; break;
case F3 : attrib ^= BLINK; break;
case F4 : attrib ^= INTENSE; break;
F1 and F2 set the attribute to the normal and reverse video modes,
respectively. F3 and F4 use the EXCLUSIVE OR operator to toggle the
intensify and the blink modes. Why directly set two modes and toggle the
other two? The toggled intensify and blink modes can be on simultaneously
and compounded with the other modes. However, NORMAL must be off when
VIDREV is on, and vice versa. If both were on at the same time, the screen
would display a white foreground on a white background──that is, a
featureless white square. If both were off, the display would be black on
black, or no display.
Limitations of the Program
To make the GRAFCHAR.C program compatible with all monitors (except
40-by-25 displays), we must impose some limitations. Most importantly, the
program limits attributes to monochrome values; less importantly, it uses
only page 0 of memory.
If you have a color monitor, you can add color to the program by setting
the function keys as follows:
case F1 : attrib ^= BLUE; break;
case F2 : attrib ^= GREEN; break;
case F3 : attrib ^= RED; break;
case F4 : attrib ^= BG_BLUE; break;
case F5 : attrib ^= BG_GREEN; break;
case F6 : attrib ^= BG_RED; break;
case F7 : attrib ^= BLINK; break;
case F8 : attrib ^= INTENSE; break;
Also, make the following definitions:
#define BLUE 0x1
#define GREEN 0x2
#define RED 0x4
#define BG_BLUE 0x10
#define BG_GREEN 0x20
#define BG_RED 0x40
This lets you independently toggle each bit of the attribute. When the
attribute is initially set to NORMAL, blue, green, and red are all toggled
on. Pressing F2, for example, turns green off and leaves the red-blue
(magenta) combination.
You should also change the key table at the bottom of the screen to
reflect the new uses of the function keys.
Direct Memory Access
Thus far, we've used BIOS routines to place the proper character and
attribute bytes into video memory. This method offers two advantages──it
saves us work and lets us write hardware-independent programs that run on
the MDA, CGA, EGA, VGA, and MCGA. However, we can create faster programs
by bypassing the BIOS and placing data directly into video memory. This
programming technique is called "Direct Video Memory Access," which we
will refer to as DMA for the remainder of this book. (Don't confuse this
use of DMA with the DMA chip built into the IBM PC and compatibles, which
performs a different function.)
DMA Basics
To copy information to or from a memory location, you need to use the
address of that location. Often, you do so symbolically and indirectly by
using variables and array names. Pointers provide a more obvious way to
use addresses. So what do you use to access video memory?
Because video memory is a large block of bytes, it's natural to think of
it as a large array. As you've learned, arrays can often be described by
either array notation or pointer notation. But with video memory, you must
use pointers. The reason is that the compiler chooses the physical
addresses to which an array corresponds, but you can choose the physical
address to which a pointer points. In particular, you can choose to have a
pointer point to the beginning of video memory.
The specific address you use depends on the hardware. The MDA uses 0xB0000
(720,896 in decimal), and the CGA uses 0xB8000 (753,664 in decimal). The
EGA and VGA use 0xB0000 for the monochrome mode and 0xB8000 for the color
text modes.
To use the address you must typecast the numeric value to the proper
pointer type. Also, in the small and medium memory models, a data pointer
is a 16-bit quantity. Neither of the addresses we need fits into 16 bits.
The large memory model uses a 32-bit pointer, but not in a way that lets
us make our simple assignment. So before we can assign the video RAM
address to a pointer, we must first examine how the PC and QuickC handle
memory addresses.
Segmented Memory
The PC has the same problem with large addresses that the small and medium
models do. The 8086 chip normally uses a 16-bit register for addresses.
However, this permits the register to address a maximum of 64 KB of
memory, which falls far short of the address needed to access the video
RAM.
The 8086 family of microprocessors uses segmented memory to overcome this
problem. The maximum size of each segment is 64 KB, the size addressable
by an address register. Typically, a program uses one segment for program
code and a second segment for data.
Addresses for the program code are 16-bit addresses relative to the
beginning of the program segment, and data addresses are relative to the
beginning of the data segment. These relative addresses are called
"offsets." In C, this offset is what is stored in a 16-bit pointer. The
following statement:
printf("Address of x is %u\n", &x);
prints out the offset, in bytes, of x from the beginning of a data
segment. To keep track of where the code and data segments are, the PC
uses special registers: the CS, or Code Segment register, and the DS, or
Data Segment register.
To solve the problem of identifying the location of a segment using only
16 bits, the PC divides the actual address of the segment by 16 (0x10
hex). For example, 0xB0000 divided by 0x10 is 0xB000. This divided
quantity is called the "segment value." (See Figure 14-6.) Thus, a
segment value of 0xA000 corresponds to a segment address of 0xA0000. As a
result of this system, segments must start at addresses that are multiples
of 16.
Suppose you want to specify the 0x20th byte of video memory. The absolute
address of this byte is 0xB0020. The PC represents this by setting the
data segment register DS to the segment value of 0xB000 and setting the
data offset register to 0x20. The following equation expresses the
relationship more generally:
absolute address = 0x10 x segment value + offset
┌─┌──────────────────────┐
│ │ │
│ │ │
│ │ │
│ ├──────────────────────┤
Data segment───┤ │ 01001101 01001111 │───Data at
(64 KB) │ ├──────────────────────┤─┐ specifie
│ │ │ │ location
DS register │ │ │ │
┌───────────────┐ │ │ │ │
│ Segment value │ │ │ │ ├─Offset
└──────┬────────┘ │ │ │ │
│ │ │ │ │
▼ └─└─────────────────────┘─┘
Segment value * 16 = Segment address────┘
Figure 14-6. Data addresses are represented by a segment value and an
offset.
Note that you can represent the same physical address in many ways. For
example, the absolute address 0xB0020 also can be represented with a
segment value of 0xB001 and an offset of 0x10.
C, Segments, and Offsets
As you already know, C has two classes of pointers──near and far. Near
pointers, which are 16 bits, hold only the offset. Far pointers, which are
32 bits, use the high 16 bits to hold the segment value and the low 16
bits to hold the offset, as shown in Figure 14-7. Compact and large
models use far data pointers by default. Small and medium models use near
data pointers by default. However, you can use the nonstandard C keyword
far to create far pointers in the small and medium models. To use the far
keyword in QuickC, choose Language Extensions in the Compile Options
dialog box.
Using a Far Pointer
To access the video memory, we must declare a far pointer, initialize its
high bytes to the segment value for the video RAM, and use its low bytes
for the offset. Declare a far pointer by using the keyword far, as
follows:
unsigned short far *far_pnt; /* far pointer */
This creates a 32-bit pointer that points to a 2-byte unit. Each 2-byte
unit holds the ASCII code and the attribute of a single displayed
character.
Next, let's set the pointer to an absolute screen address of 0xB0020. This
corresponds to a segment value of 0xB000 (0xB0000 / 0x10) and an offset of
0x20. To place the segment value into the high bytes, left-shift it 16
places; because the offset goes into the lower bytes, you needn't
manipulate it at all.
far_pnt = (unsigned short far *) (0xB000L << 16) | 0x20;
Note that we used a typecast to convert the right side (type long) to the
correct type. Next, we used the L suffix to make the segment value type
long. Otherwise, 0xB000 would be treated as type int (a 16-bit type on a
PC), and the 16-bit left-shift would shift all the bits out, leaving only
zeros. (See Figure 14-8 on the following page.)
Note also that the bitwise OR (|) operator combines the segment value and
the offset. Because one resides entirely in the upper bytes and the other
is confined to the lower bytes, this has the same effect as addition, but
is faster.
Segment value Offset
┌────────┴────────┬────────┴────────┐
┌────────┬────────┬────────┬────────┐
│ │ │
└────────┴────────┴────────┴────────┘
High bytes Low bytes
Figure 14-7. Filling a far pointer.
│ 1 byte │
┌────────┬────────┐
│ B 8 │ 0 0 │
└────────┴────────┘
▒
B800 << 16
▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒ ▒
▼ ┌────────▼────────┐
B 8 0 0 │ 0 0 │ 0 0 │
└───────────┘ └────────┴────────┘
Discarded Values
┌────────┬────────┬────────┬────────┐
│ 0 0 │ 0 0 │ B 8 │ 0 0 │
└────────┴────────┴────────┴────────┘
▒
B800L << 16
▒
┌────────┬────────▼────────┬────────┐
│ B 8 │ 0 0 │ 0 0 │ 0 0 │
└────────┴────────┴────────┴────────┘
Figure 14-8. The left-shift operator.
Finally, note that the far pointer does not hold the absolute address. It
holds two quantities: segment value and offset. If, for some reason, you
want the absolute address, you can obtain it by using the following
expression:
abs_addr = 0x10 * (far_ptr >> 16) + far_ptr & 0xFFFF;
The right-shift produces the segment value; multiplying by 0x10 gives the
segment address; and the 0xFFFF mask screens the segment value of the
pointer, leaving just the offset. The variable abs_addr should be type
long or unsigned long so that it can hold the entire address.
Using Direct Memory Access──An Example
To use DMA to access video RAM, we must declare a far pointer and
initialize it to point to the beginning of video memory. To do so, we must
first decide which data type to point to. Think of one page of video
memory as 2000 character-attribute units, with each unit describing a
particular screen location. In QuickC, two bytes constitute a short value,
so our pointer must be a far pointer to unsigned short. Use typedef to
make VIDMEM a synonym for that type:
typedef unsigned short (far * VIDMEM);
Then declare screen as a pointer of that type, as follows:
VIDMEM screen;
Next, you must decide which segment value to use. For the monochrome mode
(mode 7), use 0xB000. For the CGA and CGA-compatible modes (0 through 6),
use 0xB800. Because we must left-shift the segment value 16 bits to use it
with a C far pointer, we represent our values as follows:
#define MONMEM ((VIDMEM) (0xB000L << 16))
#define CGAMEM ((VIDMEM) (0xB800L << 16))
The L suffix makes the addresses 32-bit quantities, and the typecasting to
type VIDMEM gives the numeric values the same type as the screen pointer.
Recall that in the character-attribute pair, the low byte holds the
character and the high byte holds the attribute. Therefore, if screen is
the pointer to the beginning of video memory, if offset is the character
position we wish to set, and if ch and attrib are character and attribute
values, we use the following statement:
*(screen + offset) = (attrib << 8) | ch;
The left-shift puts attrib into the high byte, and the bitwise OR operator
combines the resulting values, as shown in Table 14-5. Note that in
QuickC, attrib must be at least a 16-bit type. If it is an 8-bit type, the
significant bits are lost. (With older versions of Microsoft C, type char
was converted to int for calculation, and no bits were discarded.)
Let's use this information in a simple program. The CH2000.C program
(Listing 14-7 on the following page) echoes any pressed ASCII key.
However, instead of echoing it once, the program uses DMA to echo it 2000
times. Also, the program cycles through all possible attribute values,
making the color version more spectacular than the monochrome version. (Be
sure Screen Swapping On is active. Also, use CGAMEM instead of MONMEM if
you are using a color display.)
The program first reads a character. The for loop displays the character
at all 2000 positions. By using the increment operator on attrib, we
change the attribute at each position and cycle through all 256
possibilities.
Table 14-5 Manipulating Character and Attribute in RAM
Byte(s) Bit Values Hex Equivalents
──────────────────────────────────────────────────────────────────────────
attrib 00000000 00000111 0x0007
attrib << 8 00000111 00000000 0x0700
ch 01000001 0x41
(attrib << 8) | ch 00000111 01000001 0x0741
──────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────
/* ch2000.c -- fills screen with 2000 characters */
/* This program demonstrates direct memory access */
/* of video memory. It is set up for the MDA. */
/* Assign CGAMEM instead of MONMEM to screen for */
/* CGA and CGA-compatible modes. */
/* Press a key to fill; press Esc to quit. */
/* Note: activate Screen Swapping On in Debug menu */
#include <conio.h>
#include "scrn.h"
typedef unsigned short (far * VIDMEM);
#define MONMEM ((VIDMEM) (0xB000L << 16)) /* monochrome */
#define CGAMEM ((VIDMEM) (0xB800L << 16)) /* CGA, EGA */
#define ESC '\033'
#define CHARS 2000
#define AMASK 0xFF /* keep attribute in range */
main()
{
unsigned ch; /* character to be displayed */
unsigned attrib = 7; /* initial attribute */
VIDMEM screen; /* pointer to video RAM */
int offset; /* location on screen */
screen = MONMEM; /* monochrome initialization */
while ((ch = getch()) != ESC)
{
for (offset = 0; offset < CHARS; offset++)
*(screen + offset) = ((attrib++ & AMASK) << 8) | ch;
}
}
──────────────────────────────────────────────────────────────────────────
Listing 14-7. The CH2000.C program.
Notice how quickly the program fills the screen. To appreciate the speed
of this program, rewrite it using the BIOS-based Write_ch_atr() function
from SCRFUN.LIB and note the difference!
Making DMA More Compatible
CH2000.C is fast and simple, but it doesn't work with all controllers. The
program needs to be able to choose the correct memory value itself.
Function 15 of the BIOS 0x10 video I/O interrupt enables it to do so.
Because this routine returns the current video mode, the program can use
that value to select the right video RAM address. Our SCRFUN.C file
includes the Getvmode() function (Listing 14-8) based on that BIOS call.
Note that the constant GETMODE is defined in the scrn.h file.
Now rewrite CH2000.C as shown in the CH2001.C program (Listing 14-9). The
constants TEXTMONO, TEXTBW80, and TEXTC80 are defined in scrn.h; they
represent mode 7 (monochrome), mode 2 (CGA 80-by-25 B/W), and mode 3 (CGA
80-by-25 Color), respectively.
──────────────────────────────────────────────────────────────────────────
#include <dos.h>
#include "scrn.h"
/* Getvmode() -- obtains the current video mode */
unsigned char Getvmode()
{
union REGS reg;
reg.h.ah = GETMODE;
int86(VIDEO, ®, ®);
return reg.h.al;
}
──────────────────────────────────────────────────────────────────────────
Listing 14-8. The Getvmode() function.
──────────────────────────────────────────────────────────────────────────
/* ch2001.c -- fills screen with 2000 characters */
/* This program demonstrates direct memory access */
/* of video memory. It uses the current video mode */
/* value to select the proper video RAM address. */
/* Press a key to fill; press Esc to quit. */
/* Program list: ch2001.c, scrfun.lib */
/* Note: activate Screen Swapping On in Debug menu */
#include <conio.h>
#include "scrn.h"
typedef unsigned short (far * VIDMEM);
#define MONMEM ((VIDMEM) (0xB000L << 16)) /* monochrome */
#define CGAMEM ((VIDMEM) (0xB800L << 16)) /* CGA, EGA */
#define ESC '\033'
#define CHARS 2000
#define AMASK 0xFF
main()
{
unsigned ch, mode;
unsigned attrib = 7;
VIDMEM screen; /* pointer to video RAM */
int offset;
if ((mode = Getvmode()) == TEXTMONO)
screen = MONMEM;
else if (mode == TEXTC80 || mode == TEXTBW80)
screen = CGAMEM;
else
exit(1);
while ((ch = getch()) != ESC)
{
for (offset = 0; offset < CHARS; offset++)
*(screen + offset) = ((attrib++ & AMASK) << 8) | ch;
}
}
──────────────────────────────────────────────────────────────────────────
Listing 14-9. The CH2001.C program.
Storing and Displaying a Screen
Let's use DMA to add some useful capabilities to the GRAFCHAR.C program.
Although this program lets you draw on the screen using the graphics
character set, when you quit the program, the drawing is lost. The program
would be more useful if it let you store the created image in a file so
you could use it later.
DMA is ideally suited to copying information from the screen to a file and
back again. We can incorporate the saving code into the program and make
the recall a separate program.
Saving the Screen
To save the screen, rewrite the main program as SAVEGRAF.C (Listing
14-10). In addition, we need to add some new definitions to the
grafchar.h file. Listing 14-11 shows the new version. Be sure Screen
Swapping On is active (on the Debug menu) before you run the program.
──────────────────────────────────────────────────────────────────────────
/* savegraf.c -- uses DMA to save screen of graphics */
/* characters and attributes */
/* Program list - savegraf.c, initstuf.c, drawchar.c, */
/* savescrn.c, scrfun.lib */
/* User include files - scrn.h, keys.h, grafchar.h */
/* Note: activate Screen Swapping On in Debug menu */
#include "grafchar.h"
unsigned char Grchr[NUMCHARS]; /* to store graphics set */
void Init_stuff(void);
void Draw_chars(void);
void Save_screen(void); /* in savescrn.c */
main()
{
int ch;
Init_stuff(); /* initialize vital elements */
Draw_chars(); /* map keys to graphics characters */
Setcurs(BOTLINE + 1, 0, PAGE);
printf("%-80s", "Save screen? <y/n> ");
Setcurs(BOTLINE + 1, 20, PAGE);
ch = getche();
if (ch == 'y' || ch == 'Y')
Save_screen();
Setcurs(BOTLINE + 2, 0, PAGE);
printf("%-80s\n", "BYE!");
}
──────────────────────────────────────────────────────────────────────────
Listing 14-10. The SAVEGRAF.C program.
──────────────────────────────────────────────────────────────────────────
/* grafchar.h -- definitions for savescrn.c and */
/* recall.c Version 2 */
#define NUMCHARS 48
#define SPACE '\040'
#define BOTLINE 19 /* line # for end of drawing space */
#define PAGE 0
#define GCSTART 0xB0 /* ascii for first graphics char */
#define BEEP '\a'
#define ESC '\033'
#define TRUE 1
#define FALSE 0
#define CHARS (BOTLINE + 1) * 80 /* number of */
/* character positions */
typedef unsigned short (far * VIDMEM);
#define MONMEM ((VIDMEM) (0xB000L << 16)) /* mono */
#define CGAMEM ((VIDMEM) (0xB800L << 16)) /* CGA, EGA */
──────────────────────────────────────────────────────────────────────────
Listing 14-11. The revised grafchar.h header file.
The SAVEGRAF.C program uses Setcurs() to position the text outside the
drawing area. If the user chooses to save the screen, the Save_screen()
function (Listing 14-12) does the work.
Program Notes
Because line numbering starts with 0, there are BOTLINE + 1 total lines.
Therefore, the total number of character-attribute pairs that must be
saved is that figure times 80; CHARS is defined as that value.
──────────────────────────────────────────────────────────────────────────
/* savescrn.c -- saves screen, including attribute */
/* values, in a file */
/* Uses direct memory access. */
#include <stdio.h> /* for file handling */
#include "scrn.h"
#include "grafchar.h"
void Save_screen()
{
FILE *save;
char filename[80];
unsigned char mode;
unsigned short char_attr; /* character, attribute */
int offset;
VIDMEM screen;
if ((mode = Getvmode()) == TEXTMONO)
screen = MONMEM;
else if (mode == TEXTC80 || mode == TEXTBW80)
screen = CGAMEM;
else
exit(1);
Setcurs(BOTLINE + 1, 0, PAGE);
printf("Please enter name for save file: ");
scanf("%s", filename);
if ((save = fopen(filename, "wb")) == NULL)
{
fprintf(stderr, "Can't open %s\n", filename);
exit(1);
}
for (offset = 0; offset < CHARS; offset++)
{
char_attr = screen[offset];
fwrite(&char_attr, 2, 1, save);
}
fclose(save);
}
──────────────────────────────────────────────────────────────────────────
Listing 14-12. The Save_screen()< function.
In general, screen + offset points to the character-attribute offset
positions from the beginning of video RAM. The value of that pair is
*(screen + offset), which can also be written as screen[offset].
The standard I/O function fwrite() copies the contents of screen memory
one character-attribute pair at a time. This function takes four
arguments: a pointer to a memory location, the number of bytes per display
unit (here 2), the number of units to be copied, and a file stream
pointer. The program first copies the video pair to char_attr because, in
the default medium memory model used by QuickC, fwrite() expects a near
pointer. In the large memory model, you could replace the for loop with
the following code:
fwrite(screen, 2, CHARS, save); /* large model */
Thus, in the large memory model, fwrite() uses a far pointer and can
access video RAM directly. By specifying CHARS units to be copied by the
function, you can dispense with the loop.
Recovering the Screen
To recover the stored screen, we need to reverse the storage process. That
is, the program should open the file in the read mode. The
attribute-character pairs found there should then be copied into the video
memory, a natural task for DMA. From these basic techniques we develop the
RECALL.C program (Listing 14-13). (To run the program, be sure that
Screen Swapping On is active.)
──────────────────────────────────────────────────────────────────────────
/* recall.c -- displays previously stored screen, */
/* including attributes. Uses DMA. */
/* Program list: recall.c, scrfun.lib */
/* User include files: scrn.h, grafchar.h */
/* Note: activate Screen Swapping On in Debug menu */
#include <stdio.h>
#include <conio.h>
#include "scrn.h"
#include "grafchar.h"
main(ac, ar)
int ac;
char *ar[];
{
unsigned char mode;
unsigned short char_attr;
FILE *save;
unsigned int offset;
char filename[81];
VIDMEM screen;
if (ac < 2)
{
fprintf(stderr, "Usage: %s filename\n", ar[0]);
exit(1);
}
if ((save = fopen(ar[1], "rb")) == NULL)
{
fprintf(stderr, "Can't open %s\n", ar[1]);
exit(1);
}
if ((mode = Getvmode()) == TEXTMONO)
screen = MONMEM;
else if (mode == TEXTC80 || mode == TEXTBW80)
screen = CGAMEM;
else
exit(1);
Clearscr();
for (offset = 0; offset < CHARS; offset++)
{
fread(&char_attr, 2, 1, save);
screen[offset] = char_attr;
}
fclose(save);
Setcurs(23, 0, PAGE);
getch(); /* anti-scrolling for QC environment */
}
──────────────────────────────────────────────────────────────────────────
Listing 14-13. The RECALL.C program.
In the program, fread() recovers from the files what fwrite() placed into
them. The Setcurs() call positions the MS-DOS prompt outside the drawing
area when the program ends. The getch() call simply requires the user to
press a key to terminate the program. Without this code, when you run the
program in the QuickC environment, the QuickC prompt causes the screen to
scroll up when the program ends. It still causes scrolling with this
program, but not until you press a key.
Paging
Now let's turn to a text topic that lies beyond the scope of the
Monochrome Display Adapter── paging. The CGA, EGA, and VGA have enough
memory to store more than one screenful, or page, of text. The 16 KB video
memory of the CGA, for example, can hold four text pages. The BIOS
supports the use of pages by providing routines for setting the page and
for determining the current page number. Many other BIOS routines require
page information. The SCRFUN.C file we developed in Chapter 13 contains
two page-related functions──Getpage() and Setpage(), which are combined in
Listing 14-14. As usual, the manifest constants are defined in scrn.h.
Paging is very fast, even compared to DMA, because the video RAM doesn't
need to be rewritten. The video controller simply changes the section of
video memory that it reads. A typical application for paging stores a help
screen on one page, while an application uses another page. This permits a
rapid transition between the two screens without calling data from program
memory or a disk file.
──────────────────────────────────────────────────────────────────────────
/* Getpage() -- obtains the current video page */
unsigned char Getpage()
{
union REGS reg;
reg.h.ah = GETMODE;
int86(VIDEO, ®, ®);
return reg.h.bh;
}
/* Setpage() -- sets page to given value */
void Setpage(page)
unsigned char page;
{
union REGS reg;
reg.h.ah = SETPAGE;
reg.h.al = page;
int86(VIDEO, ®, ®);
}
──────────────────────────────────────────────────────────────────────────
Listing 14-14. The Getpage() and Setpage() functions.
Let's develop a basic program that can switch back and forth between page
0 and a help screen on page 1. We could use BIOS calls to write the
contents of the two screens, but direct memory access is faster. However,
to use direct memory access, we need to supply the address of page 1.
Because each page holds 2000 character-attribute pairs, or 4000 bytes, you
might expect that page 1 is offset 4000 bytes from the beginning of video
memory. But computers relate more to powers of 2 than to powers of 10, so
the actual offset is 4096 bytes, 0x1000 in hex. (You can use the extra
bytes between pages to color in page borders.)
We use the VIDMEM type pointer again to point to video memory. Because we
define it to point to a 2-byte unit (the character-attribute pair), the
offset in VIDMEM units is 2048 pairs, which is 0x800 in hex.
The HELP.C program (Listing 14-15) is fairly simple. (Be sure Screen
Swapping On is active before you run it.) The key points to note are its
use of Setpage() to change pages and its use of direct memory access to
write to the screen. The program uses two direct memory access modules.
The writechr.c module (Listing 14-16 on the following page) writes a
character-attribute pair a specified number of times beginning at a
specified memory location. The writestr.c module (Listing 14-17 on p.
483) is similar, but it writes a string once instead of a single character
repeatedly. By choosing the appropriate memory location, you can use these
functions to write to either page, no matter which one is currently
displayed. For convenience, we've collected definitions of the colors in a
file called color.h (Listing 14-18 on p. 483).
──────────────────────────────────────────────────────────────────────────
/* help.c -- uses paging and direct memory access */
/* to display a help screen */
/* Program list: help.c, writestr.c, writechr.c, */
/* scrfun.lib */
/* User include files: scrn.h, color.h */
/* Note: activate Screen Swapping On in Debug menu */
#include <stdio.h>
#include <conio.h>
#include "color.h"
#include "scrn.h"
typedef unsigned int (far * VIDMEM);
#define CGAMEM ((VIDMEM) (0xB800L << 16))
#define PAGESIZE 2000
#define PAGEOFFSET 0x800L
#define ESC '\033'
#define ATTR1 (BG_BLUE | YELLOW)
#define ATTR2 (BG_YELLOW | BLUE)
#define ATTR3 (BG_RED | YELLOW | BLINK | INTENSE)
#define CH1 (unsigned short) '\xB1'
char *str1 = "Press ? key for help.";
char *str2 = "Press Enter key to return.";
char *str3 = "Press ESC key to quit.";
char *str4 = "\xB1HELP!\xB1";
void Write_chars(VIDMEM, unsigned short, unsigned
short, unsigned short);
void Write_str(VIDMEM, unsigned short, char *);
main()
{
int ch;
unsigned char page = 0;
unsigned char mode;
mode = Getvmode();
if (mode != TEXTC80 && mode != TEXTBW80)
{
printf("Only modes 2 and 3 supported. Bye.\n");
exit(1);
}
Setpage(page);
Write_chars(CGAMEM, '\0', ATTR2, PAGESIZE);
Write_str(CGAMEM + 2 * COLS, ATTR1, str1);
Write_str(CGAMEM + 2 * COLS, ATTR1, str1);
Write_str(CGAMEM + 22 * COLS, ATTR1, str3);
Write_chars(CGAMEM + PAGEOFFSET, '\0', ATTR1, PAGESIZE);
Write_str(CGAMEM + PAGEOFFSET + 20 * COLS, ATTR2, str2);
Write_str(CGAMEM + PAGEOFFSET + 22 * COLS, ATTR1, str3);
Write_chars(CGAMEM + PAGEOFFSET + 10 * COLS + 36,
CH1, ATTR3, 7);
Write_str(CGAMEM + PAGEOFFSET + 11 * COLS + 36,
ATTR3, str4);
Write_chars(CGAMEM + PAGEOFFSET + 12 * COLS + 36,
CH1, ATTR3, 7);
while ((ch = getch()) != ESC)
{
if (ch == '?' && page == 0)
Setpage(page = 1);
else if (ch == '\r' && page == 1)
Setpage(page = 0);
}
Write_chars(CGAMEM, '\0', NORMAL, PAGESIZE);
Write_chars(CGAMEM + PAGEOFFSET, '\0', NORMAL, PAGESIZE);
}
──────────────────────────────────────────────────────────────────────────
Listing 14-15. The HELP.C program.
──────────────────────────────────────────────────────────────────────────
/* writechr.c -- writes char and attribute repeatedly */
/* using DMA */
/* write character ch with attribute attr num times */
/* starting at location pstart -- uses array notation */
typedef unsigned int (far * VIDMEM);
void Write_chars(pstart, ch, attr, num)
VIDMEM pstart;
unsigned short ch, attr, num;
{
register count;
unsigned short pair;
pair = (attr << 8) | (ch & 0x00FF) ;
for (count = 0; count < num; count++)
pstart[count] = pair;
}
──────────────────────────────────────────────────────────────────────────
Listing 14-16. The writechr.c module.
──────────────────────────────────────────────────────────────────────────
/* writestr.c -- writes string and attribute using DMA */
/* Write the string str with attribute attr at */
/* location pstart -- uses pointer notation. */
typedef unsigned int (far * VIDMEM);
void Write_str(pstart, attr, str)
VIDMEM pstart;
unsigned short attr;
char *str;
{
while (*str != '\0')
*pstart++ = (attr << 8) | (*str++ & 0x00FF);
}
──────────────────────────────────────────────────────────────────────────
Listing 14-17. The writestr.c module.
──────────────────────────────────────────────────────────────────────────
/* color.h -- defines the color attributes */
/* foreground colors */
#define BLACK 0x0
#define BLUE 0x1
#define GREEN 0x2
#define RED 0x4
#define CYAN 0x3
#define MAGENTA 0x5
#define YELLOW 0x6
#define WHITE 0x7
/* background colors */
#define BG_BLACK 0x00
#define BG_BLUE 0x10
#define BG_GREEN 0x20
#define BG_RED 0x40
#define BG_CYAN 0x30
#define BG_MAGENTA 0x50
#define BG_YELLOW 0x60
#define BG_WHITE 0x70
──────────────────────────────────────────────────────────────────────────
Listing 14-18. The color.h include file.
Most of the HELP.C program involves using the new Write_chars() and
Write_str() functions, so let's examine them. The two functions are quite
similar in behavior, but to illustrate different programming techniques,
one uses array notation and the other uses pointer notation.
Write_chars() starts by combining the attribute and character into a
2-byte unit. It left-shifts the attribute into the high byte and places
the character in the low byte. Next, the function performs a logical AND
operation with the character and 0x00FF to limit the character to the
range 0 through 0xFF, or 0 through 255. Next, a loop assigns the 2-byte
pair to num consecutive locations in memory, beginning with the location
pointed to by pstart. Recall that the notation pstart[count] is equivalent
to *(pstart + count).
In the program, Write_chars() clears the two pages, setting them to yellow
and blue, respectively. To clear the screen, the program sets the
character part of the byte to a null character; the attribute sets the
color.
The Write_str() function uses pointer notation to display a string. Like
the preceding function, it combines the left-shifted attribute with a
masked character value. In this case, str initially points to the first
character in the string, so *str represents the value of that character.
The while loop continues until it reaches the terminating null character
of the string. During each cycle, the increment operator advances the
video memory pointer and the string pointer after they are used.
In the main program, note how we use addresses to specify locations on the
screen. Consider, for example, the following statement:
Write_str(CGAMEM + PAGEOFFSET + 11 * COLS + 36,
ATTR3, str4);
The address CGAMEM locates the beginning of the CGA (and EGA and VGA)
memory. The PAGEOFFSET value is the offset to the beginning of the next
page. Each line contains COLS characters, so the expression 11 * COLS is
the offset to the beginning of line 11 (the twelfth line, because
numbering starts with zero). Finally, the 36 gives the offset, or the
indention measured in character widths, from the left side of the display.
Note that the QuickC Graphics Library provides alternatives to many of our
BIOS-based functions, including functions that clear the screen and set
the page. However, using the Graphics Library produces final code
noticeably larger than that of our examples. We use the Graphics Library
only in graphics programs, in which its power and generality become
evident.
Ports
Any discussion of hardware-dependent programming methods must mention
"ports," which are information conduits between the CPU and the other
devices and processors in a PC. In general, each processor or device has
one or more registers of its own. Values placed in these registers can
control the operation of the processor or, perhaps, test its state of
readiness. In the PC, various registers are assigned "port addresses" that
are completely separate from the memory address system and are handled
differently. The CPU accesses registers through ports by using special
port instructions. (See Figure 14-9.)
An 8086 CPU can address as many as 64,000 8-bit ports, but only a small
fraction of that number (fewer than 200) are actually used. In assembly
language, you access the ports with the instructions IN and OUT: IN reads
a register; OUT writes to it. C does not contain these instructions, so
QuickC supplies the non-ANSI inp() and outp() functions to serve the same
purpose.
Reading Ports with inp()
As mentioned in Chapter 13, the inp() and outp() functions are defined in
conio.h. The following is the syntax for inp():
#include <conio.h>
int inp(port)
unsigned port; /* port number */
This function reads the register at port number port, which can be a value
in the range 0 through 65,535. It then returns the byte it reads. With
write-only ports, inp() returns the value 255, or all bits set to 1.
However, a return value of 255 does not always signal a write-only
register because 255 is also a valid register setting.
┌───────────────────┐
│ Video controller │
│ ┌─────────┐ │
│ │register │ │
└────┴────╥────┴────┘
┌───────────┐ ║
│ ┌─┤ ║
│ │ │◄──Port 0x3B8 ║
│ │ ╞═════════════════╝
│ CPU └─┤
│ ┌─┤
│ │ ╞═════════════════╗
│ │ │◄──Port 0x67 ║
│ └─┤ ║
└───────────┘ ║
┌────┬────╨────┬────┐
│ │register │ │
│ └─────────┘ │
│ Speaker controller│
└───────────────────┘
Figure 14-9. Ports and registers.
The short PORTINFO.C program (Listing 14-19) lets you access and read
various ports. Note that it uses the return value of scanf() to terminate
the input loop. We prompt for hexadecimal port numbers because technical
manuals usually list them in that form. Note that scanf() returns a value
equal to the number of successful reads. Therefore, if it reads a hex
value, it returns 1. If it finds input that is not hex, such as the letter
q, scanf() returns 0, and the loop terminates.
The following is a sample run:
Enter number (in hex) of the port you wish to read: 3da
Value returned for port 3da is 199 (decimal) c6 (hex)
Next port? (q to quit): 61
Value returned for port 61 is 32 (decimal) 31 (hex)
Next port? (q to quit): 42
Value returned for port 42 is 174 (decimal) 60 (hex)
Next port? (q to quit): 3b8
Value returned for port 3b8 is 255 (decimal) ff (hex)
Next port? (q to quit): q
You may get different values from those in this sample run──some of the
registers change values as you use the computer.
In the IBM PC and compatibles, the 0x3DA port reports status information
about the MDA. Port 61 controls the speaker, and port 42 regulates the
frequency of the 8253 timer chip. Finally, port 3B8 is the control port
for the 6845 video controller on the MDA. (Because the last port is a
write-only port, the reported value is not necessarily the true one.)
──────────────────────────────────────────────────────────────────────────
/* portinfo.c -- reads port values */
/* program list -- portinfo.c (inp() not in core lib) */
#include <conio.h>
#include <stdio.h>
main()
{
unsigned int portnum;
int regvalue;
printf("Enter number (in hex) of the port ");
printf("you wish to read: ");
while (scanf("%x", &portnum) == 1)
{
regvalue = inp(portnum);
printf("\nValue returned for port %x is %d (decimal)"
" %x (hex)\n", portnum, regvalue, regvalue);
printf("Next port? (q to quit): ");
}
}
──────────────────────────────────────────────────────────────────────────
Listing 14-19. The PORTINFO.C program.
As you can see, reading a register is a simple procedure. The difficult
part is wading through the technical literature to see which port
addresses correspond to which devices and to find out the meaning of the
register settings.
Writing to a Port with outp()
You write to a port with the outp() function by using the following
syntax:
#include <conio.h>
int outp(port, value)
unsigned port; /* port number */
int value; /* output value */
The function sends value to port number port. Although value is declared
type int, you should use only numbers in the range 0 through 255. The
function returns the same value it sends.
Although it is easy to write to a port, you must do so with caution.
Sending wrong values to some video controller registers, for example, can
damage your monitor. Other ports can disable your keyboard, the system
memory, the monitor, and so on. Do not use the experimental method when
you write to ports! Before we write a sample program that uses port number
0x3B8, the MDA control register, study the function of each bit in the
register as described in Table 14-6.
Let's write a short program that blanks the screen and then restores it.
We can turn off the display by setting bit 3 to 0. Because this does not
affect the video RAM, resetting bit 3 to 1 restores the display.
Ideally, we would use inp() to read and save the current register setting.
Then we could use that value to restore the original setting when we are
done. However, 3B8 is a write-only register, so we must use Table 14-6 to
select the proper setting.
Table 14-6 Video Control Register Functions
Bit Function
──────────────────────────────────────────────────────────────────────────
0 If this bit is 0, no communication is permitted between the
CPU and the video display memory. (This prevents data from
being changed.) If this bit is 1 (the default value),
communication between the CPU and the video display memory is
enabled. (This lets the CPU read from and write to memory.)
1, 2
Not used.
3 If this bit is 0, the display is disabled, which blanks the
screen. The contents of video RAM, however, are unaffected. If
this bit is 1 (the default value), the display is enabled, and
data stored in video RAM is displayed.
4 Not used.
5 If this bit is 0, the blink attribute bit in video RAM
controls the background intensity. If this bit is 1 (the
default value), it controls blinking.
6, 7 Not used.
──────────────────────────────────────────────────────────────────────────
Clearly, we originally want bit 0 to be 1. Normally, bits 3 and 5 should
be 1, too. Because the other bits don't affect the port, we can set them
to 0. This makes our default setting 00101001 in binary, or 0x29 in hex.
To turn the display off, set bit 3 to 0, which changes the setting to 0010
0001 in binary, or 0x21 in hex.
The short BLANK.C program (Listing 14-20) demonstrates the results of our
efforts.
As we mentioned, the port approach is hardware-dependent. For example,
changing the register number from 3B8 to 3D8 makes this program work with
the CGA, but not with the EGA. By accessing the ports directly, you can
make the video controllers do things that are impossible with BIOS calls
alone. However, a new display adapter (and different port assignments) can
render your program nonfunctional. Our BLANK.C program illustrates both
these points.
But sometimes you must use ports. For example, there are no BIOS calls
that control the speaker; therefore, if you want to play a little tune,
programming the port is the only method available.
Eliminating CGA Snow
Let's examine another port example. The CGA can display "snow" when used
with direct memory access. The problem arises from the interference caused
when the CPU sends data to the video RAM at the same time that the CGA
controller reads the RAM. (The other controllers don't have this problem.)
The problem can be solved by not writing to the video RAM when the video
controller is in the horizontal retrace mode. (The video display works by
scanning an electron beam in horizontal lines across the screen. When the
beam reaches the edge of the screen, it must be reset to the beginning of
the next line. That resetting of the beam is called the horizontal
retrace.)
──────────────────────────────────────────────────────────────────────────
/* blank.c -- blanks MDA screen */
/* program list -- blank.c (outp() not in core lib) */
#include <conio.h>
#define CONTROLREG 0x3B8 /* control register MDA */
#define DEFAULTSET 0x29
#define VIDEOOFF 0x21
main()
{
outp(CONTROLREG, VIDEOOFF);
getch();
outp(CONTROLREG, DEFAULTSET);
}
──────────────────────────────────────────────────────────────────────────
Listing 14-20. The BLANK.C program.
The CGA read-only 0x3DA port contains the controller status and reports
when the video RAM can be written to without interfering with the display.
When the video RAM can be accessed without interference, bit 0 is set to
1. The code uses that information to limit access to the proper times:
while ((inp (0x3DA) & 01) != 0)
{;} /* wait to end of current retrace */
while ((inp (0x3DA) & 01) == 0)
{;} /* wait for next retrace */
/* put memory access code here */
The second loop is obvious──it waits until bit 0 is 1 before accessing the
video memory. The first loop prevents memory access from starting part way
through a horizontal retrace. Suppose, for example, a retrace is 90%
complete. Without the first loop, the program skips the second loop
because bit 0 currently would be 1. However, that leaves only 10% of the
retrace time to perform all the memory access, and that might not be
enough time.
The EGA and VGA
The normal text modes for the EGA and VGA systems are modes 3 and 4, which
emulate the CGA 80-by-25 B/W and Color text modes. (Both systems also
support mode 7 so that they can be used with a monochrome monitor.) All
the applications we've discussed so far, aside from the port example, work
with the EGA and VGA. These video controllers, however, have additional
text capabilities that you might want to exploit.
Normally, when used with a high-resolution monitor, the EGA and VGA use
more pixels per character than the CGA to achieve better-looking text.
However, these controllers also can produce smaller characters by using
the CGA 8-by-8 character grid instead of the normal 8-by-14 (EGA) or
9-by-16 (VGA) grid. This lets us generate a 43-line screen with the EGA
and a 50-line screen with the VGA. For simplicity, we'll use the term
"extended-line" to describe either.
The EGA and VGA handle fonts differently than the MDA and CGA. The EGA and
VGA store some standard fonts in ROM, much like the CGA and MDA. However,
rather than scanning the ROM directly to get font information, the EGA and
VGA first copy the fonts to a video RAM area beginning at memory location
0xA0000. Then they scan the RAM for font information. Thus, you can use
BIOS calls to select a font, or you can even load a font of your own
design. To access the extended-line mode, you must load the 8-by-8 font
instead of the default (8-by-14 or 9-by-16) font.
To produce the extended-line display, you must reset several video
controller registers. (New BIOS routines that come with these controllers
simplify the process.) The LINES43.C program (Listing 14-21 on the
following page) sets up the extended-line mode. If ANSI.SYS is running,
this program will not work properly──it displays the small characters, but
it limits the display to 25 lines.
The program first sets the usual text mode. Next, it calls a new routine
added to the EGA and VGA versions of interrupt 0x10. Routine 0x11, labeled
CHAR_GEN in our program, specifies the character font to be used. Setting
register AL to 0x12 selects the 8-by-8-pixel character set stored in the
video ROM and resets the register settings to display 43 (or 50) lines.
Why did the program set a block to 0? The EGA and VGA can simultaneously
store as many as four fonts. Block 0 refers to the first font, block 1 to
the second, and so on. Because block 0 is used unless you explicitly
switch to another, we copied the font to that block.
For this program, use the Run menu to select the .exe compilation. You
must leave QuickC to run the program because it has no effect in the
QuickC environment. Use the MS-DOS MODE CO80 command to restore the usual
mode.
What happens if you set the extended-line mode and then run another
program? Some programs reset the mode and undo the change. Some display
the small characters but assume that only 25 lines can be displayed. Some,
like QuickC, check to see the number of lines in use and display the full
43 (or 50) lines.
──────────────────────────────────────────────────────────────────────────
/* lines43.c -- leaves EGA in 43-line mode */
#include <dos.h>
#include <conio.h>
#define VIDEO 0x10
#define SETVMODE 0
#define CHAR_GEN 0x11 /* an EGA BIOS function number */
#define ROM8X8 0x12
#define BLOCK 0
#define TEXTC80 3
main()
{
union REGS reg;
reg.h.ah = SETVMODE; /* set text mode */
reg.h.al = TEXTC80;
int86(VIDEO, ®, ®);
reg.h.ah = CHAR_GEN; /* char generator routine */
reg.h.al = ROM8X8; /* use 8x8 ROM character box */
reg.h.bl = BLOCK; /* copy to block 0 */
int86(VIDEO, ®, ®);
}
──────────────────────────────────────────────────────────────────────────
Listing 14-21. The LINES43.C program.
────────────────────────────────────────────────────────────────────────────
Chapter 15 Graphics and QuickC
Generating computer graphics is one of the PC's most spectacular uses. All
the video controllers listed in the preceding chapters──except the MDA──
offer graphics modes that permit pixel-by-pixel control of the entire
screen, enabling you to create figures and patterns and to set colors for
individual pixels. The quality of graphics (and graphics programming in
general) is hardware-dependent. The CGA, EGA, and VGA offer various
graphics modes that are not compatible with one another. Fortunately,
QuickC's extensive Graphics Library substantially simplifies graphics
programming. We devote most of this chapter to exploring this library.
The Graphics Modes
First, let's review the available graphics modes. All of these modes are
dependent on specific video controllers and displays. Table 15-1 on the
following page shows which modes are available to various hardware
systems.
Table 15-1 Graphics Modes
Mode Adapters Displays Resolution Colors per Palette
Palette
───────────────────────────────────────────────────────────────────────────
4 CGA, EGA, B/W☼ , CD☼ , ED☼ , 320 x 200 4 2
VGA VD☼
5 CGA, EGA, CD☼ , ED☼ , 320 x 200 4 1 4 gray
VGA VD☼
6 CGA, EGA, B/W☼ , CD☼ , 640 x 200 2 1 2 B/W☼
VGA ED☼ , VD☼
13 EGA, VGA CD☼ , ED☼ , 320 x 200 16 User- 16
VD☼ definable
14 EGA, VGA ED☼ , VD☼ 640 x 200 16 User- 16
definable
15 EGA, VGA MD☼ 650 x 350 2 1 2 B/W☼
16 EGA, VGA ED☼ , VD☼ 640 x 350 4/16 User- 16/64
definable
17 VGA VD☼ 640 x 480 2 User- 262,144
definable
18 VGA VD☼ 640 x 480 16 User- 262,144
definable
19 VGA VD☼ 320 x 200 256 User- 262,144
definable
──────────────────────────────────────────────────────────────────────────
Notes: For the EGA and VGA, mode 5 is the same as mode 4.
For mode 16, the number of colors available to the EGA depends on
the size of EGA memory. The lower figure is for 64 KB of memory;
the higher figure is for 128 KB or more of EGA memory.
To use any of these graphics modes from within a program, specify
the mode with a BIOS call or with one of the Graphics Library
functions.
Modes and BIOS
In Chapter 14 we used the BIOS-based Getvmode() function from our
SCRFUN.LIB library to obtain the current video mode. That library also
provides the Setvmode() function shown in Listing 15-1.
──────────────────────────────────────────────────────────────────────────
/* Setvmode() -- sets video mode to given mode */
#include <dos.h>
#include "scrn.h"
void Setvmode(mode)
unsigned char mode;
{
union REGS reg;
reg.h.ah = SETMODE;
reg.h.al = mode;
int86(VIDEO, ®, ®);
}
──────────────────────────────────────────────────────────────────────────
Listing 15-1. The Setvmode() function.
You use this function to set mode 4, for example, with the following
program lines:
if (Getvmode() != 7) /* not the monochrome */
Setvmode(4);
else
{
printf("Monochrome monitor does not use mode 4.\n");
exit(1);
}
The Graphics Library routines, however, simplify and enhance this
procedure.
Modes and the Graphics Library
The QuickC Graphics Library contains functions specifically designed to
handle the display interface and resides in the library file GRAPHICS.LIB
and in the QuickC library file GRAPHICS.QLB. The graphics routines are not
part of the standard QuickC core library. However, they are easy to
access. First, you can use a program list containing the name of your
program's file. This causes QuickC to use its linker, which accesses the
whole library, including GRAPHICS.LIB. Or you can place the graphics
routines in QuickC's in-memory library by using the /l option:
qc /l graphics.qlb
The second method produces faster compilation, and we assume that you use
it. However, if your computer doesn't have enough memory to follow that
method, you can use a program list instead.
To use the graphics functions, you must include the header file graph.h.
The following sections describe some of its mode-related functions.
The _setvideomode() Function
This more sophisticated version of our Setvmode() function takes as an
argument the number of the desired mode. The graph.h file (Listing 15-2
on the following page) contains the list of manifest constants that you
can use.
The _setvideomode() function has a few important features that Setvmode()
lacks. For one thing, it keeps track of the original video mode, which
means that you can use the _DEFAULTMODE argument to restore that mode.
Another feature of _setvideomode() is that it has a return value. If the
function succeeds in setting the requested mode, it returns a nonzero
value. If the function fails, it returns a zero. Using the return value,
we can rewrite our DMA (Direct Video Memory Access) examples from Chapter
14 so that they don't need to first obtain the current mode. The relevant
code in those examples follows:
if ((mode = Getvmode()) == TEXTMONO)
screen = MONMEM;
else if (mode == TEXTC80 || mode == TEXTBW80)
screen = CGAMEM;
else
exit(1);
──────────────────────────────────────────────────────────────────────────
/* Mode constants from graph.h */
/* arguments to _setvideomode() */
#define _DEFAULTMODE -1
/* restore screen to original mode */
#define _TEXTBW40 0 /* 40 x 25 text, 16-gray */
#define _TEXTC40 1 /* 40 x 25 text, 16/8-color */
#define _TEXTBW80 2 /* 80 x 25 text, 16-gray */
#define _TEXTC80 3 /* 80 x 25 text, 16/8-color */
#define _MRES4COLOR 4 /* 320 x 200, 4-color */
#define _MRESNOCOLOR 5 /* 320 x 200, 4-gray */
#define _HRESBW 6 /* 640 x 200, B/W */
#define _TEXTMONO 7 /* 80 x 25 text, B/W */
#define _MRES16COLOR 13 /* 320 x 200, 16-color */
#define _HRES16COLOR 14 /* 640 x 200, 16-color */
#define _ERESNOCOLOR 15 /* 640 x 350, B/W */
#define _ERESCOLOR 16 /* 640 x 350, 4- or 16-color */
#define _VRES2COLOR 17 /* 640 x 480, 2-color */
#define _VRES16COLOR 18 /* 640 x 480, 16-color */
#define _MRES256COLOR 19 /* 320 x 200, 256-color */
──────────────────────────────────────────────────────────────────────────
Listing 15-2. Mode constants from graph.h.
After we add the #include <graph.h> line to the program, we can replace
the preceding code with the following:
if (_setvideomode(_TEXTMONO))
screen = MONMEM;
else if (_setvideomode(_TEXTC80) || _setvideomode(_TEXTBW80))
screen = CGAMEM;
else
exit(1);
This program attempts to set the MDA mode. If it succeeds, it sets the
video display pointer to the MDA value. If it fails to set the MDA mode,
it attempts to set either the CGA color 80-by-25 mode or the B/W
equivalent. If either of those attempts succeeds, it sets the video
display pointer to the CGA value. If none of these attempts succeed, the
program exits. Note the way the second if works. If _setvideomode()
succeeds in setting the _TEXTC80 mode, the function returns a true value.
Because the first part of the logical OR expression is true, the whole
expression is true, and thus the second half of the expression need not be
evaluated.
The _getvideoconfig() Function
The Graphics Library also lets us retrieve a variety of information about
the current mode. The _getvideoconfig() function fills a structure called
videoconfig with mode-related information. The function is defined in the
graph.h file, as shown in Listing 15-3. The listing also shows the
defined constants that you can use with the videoconfig structure.
──────────────────────────────────────────────────────────────────────────
/* video configuration information from graph.h */
struct videoconfig {
short numxpixels; /* number of pixels on X axis */
short numypixels; /* number of pixels on Y axis */
short numtextcols; /* number of text columns available */
short numtextrows; /* number of text rows available */
short numcolors; /* number of actual colors */
short bitsperpixel; /* number of bits per pixel */
short numvideopages; /* number of available video pages */
short mode; /* current video mode */
short adapter; /* active display adapter */
short monitor; /* active display monitor */
short memory; /* adapter video memory in K bytes */
};
/* videoconfig adapter values */
/* These manifest constants can be used to test adapter */
/* values for a particular adapter using the bitwise AND */
/* operator (&). */
#define _MDPA 0x0001 /* Monochrome Display Adapter (MDPA) */
#define _CGA 0x0002 /* Color Graphics Adapter (CGA) */
#define _EGA 0x0004 /* Enhanced Graphics Adapter (EGA) */
#define _MCGA 0x0008 /* Multi-Color Graphics Array (MCGA) */
#define _VGA 0x0010 /* Video Graphics Array (VGA) */
/* videoconfig monitor values */
/* These manifest constants can be used to test monitor */
/* values for a particular monitor using the bitwise AND */
/* operator (&). */
#define _MONO 0x0001 /* Monochrome */
#define _COLOR 0x0002 /* Color (or Enhanced emulating */
/* color) */
#define _ENHCOLOR 0x0004 /* Enhanced Color */
──────────────────────────────────────────────────────────────────────────
Listing 15-3. Video configuration information from graph.h.
When you pass the _setvideomode() function the address of a struct
videoconfig structure, the function fills the structure with the indicated
data. The MODEINFO.C program (Listing 15-4 on the following pages) cycles
through the modes supported by QuickC and displays the mode-related
information. When the program ends, the _setvideomode(_DEFAULTMODE)
function call restores the original mode setting.
Notice in the output of MODEINFO.C that the _getvideoconfig() function
returns 32 for the number of colors available in all text modes, including
monochrome. This value indicates the range of values accepted by the
_settextcolor() function, not necessarily the number of unique color
options.
Because the actual mode values do not form a set of consecutive integers,
the program holds the values in an array. However, the array indexes are
consecutive, so they can be used in a loop.
──────────────────────────────────────────────────────────────────────────
/* modeinfo.c -- sets modes and obtains information */
/* Demonstrates _setvideomode() and _getvideoconfig() */
/* If you load graphics.qlb, no program list is needed.*/
#include <conio.h>
#include <graph.h>
struct videoconfig vc;
int modes[15] ={_TEXTBW40, _TEXTC40, _TEXTBW80, _TEXTC80,
_MRES4COLOR, _MRESNOCOLOR, _HRESBW, _TEXTMONO,
_MRES16COLOR, _HRES16COLOR, _ERESNOCOLOR, _ERESCOLOR,
_VRES2COLOR, _VRES16COLOR, _MRES256COLOR};
char *Adapt(short), *Display(short);
main()
{
int i;
for (i = 0; i < 15; i++)
{
if (_setvideomode(modes[i]))
{
_getvideoconfig(&vc);
printf("video mode is %d\n", vc.mode);
printf("number of columns is %d\n", vc.numtextcols);
printf("number of colors is %d\n", vc.numcolors);
printf("number of pages is %d\n", vc.numvideopages);
printf("adapter is %s\n", Adapt(vc.adapter));
printf("display is %s\n", Display(vc.monitor));
printf("the adapter has %dK of memory\n",
vc.memory);
}
else
printf("mode %d not supported\n", modes[i]);
printf("press a key for next mode\n");
getch();
}
_setvideomode(_DEFAULTMODE);
}
/* Adapt() returns a pointer to a string describing */
/* the adapter characterized by adapt_num. */
char *Adapt(adapt_num)
short adapt_num; /* videoconfig.adapter value */
{
static char *anames[6] = {"Monochrome", "CGA", "EGA",
"MCGA", "VGA", "Not known"};
char *point;
switch (adapt_num)
{
case _MDPA : point = anames[0];
break;
case _CGA : point = anames[1];
break;
case _EGA : point = anames[2];
break;
case _MCGA : point = anames[3];
break;
case _VGA : point = anames[4];
break;
default : point = anames[5];
}
return point;
}
/* Display() returns a pointer to a string describing */
/* the monitor characterized by disp. */
char *Display(disp)
short disp; /* videoconfig.monitor value */
{
static char *types[5] = {"monochrome", "color",
"enhanced color", "analog",
"unknown"};
char *point;
if (disp & _MONO)
point = types[0];
else if (disp & _COLOR)
point = types[1];
else if (disp & _ENHCOLOR)
point = types[2];
else if (disp & _ANALOG)
point = types[3];
else
point = types[4];
return point;
}
──────────────────────────────────────────────────────────────────────────
Listing 15-4. The MODEINFO.C program.
The Adapt() function uses a switch statement to select the string that
corresponds to the adapter value returned by _getvideoconfig(). The
Display() function uses the manifest constants defined in graph.h and
logical AND testing to identify the monitor. The returned values, being
powers of 2, are such that mode & _MONO is zero (false) unless mode is
_MONO and so on.
CGA Graphics
We now have the tools to select a mode. The first mode we will explore is
mode 4 (_MRES4COLOR) because all the graphics video controllers support
it. This is the medium-resolution CGA mode.
Because the graphics modes allow each screen pixel to be set individually,
they require more video memory than do the text modes. The exact amount of
memory, however, depends on how much information is needed to describe
each pixel. For example, a black-and-white mode requires only one bit per
pixel because the two possible values of the bit (0 and 1) are enough to
describe the two possible values for the pixel (off and on). With color
modes, the amount of memory needed depends on the number of colors
available to each pixel. With a fixed amount of memory, using more bits
per pixel increases the color options but decreases the total number of
pixels that can be mapped. The CGA 320-by-200 four-color mode
(_MRES4COLOR) offers a compromise between resolution and color variety by
restricting the display to four colors at a time.
The Graphics Palette and Background
The CGA four-color mode represents each pixel with two bits of memory.
This unit of memory can be set to a number in the range 0 through 3, thus
providing a choice of four colors. Color 0 represents the background
color, and the other three colors constitute the "palette." The background
color and the palette are set in separate operations.
You can set the background color to any one of 16 values, numbers 0
through 15. The values 0 through 7 are the text foreground choices we
previously listed in the color.h file (Listing 14-18 on p. 483). The
values 8 through 15 are the intensified versions of these same colors.
Note that the graphics background choice applies to the entire screen; the
text background applies only to a particular character box. Thus, in the
graphics mode, only one background choice can be in effect at a time.
In CGA four-color mode, you can choose one of only two palettes, which are
described in Table 15-2. Suppose you set the palette to 0 and the
background to blue. When you set a bit pair in the video memory to 0, the
corresponding pixel is set to blue. Setting the bit pair to 1, 2, or 3
produces the colors green, red, and dark yellow, respectively. Setting the
video mode clears the display because it sets all the video display bits
to 0. Thus, initially, the entire screen is the background color.
Now suppose you create a pattern on the screen. If you select a different
background color, the background for the entire screen changes, but the
video display memory remains unchanged. Changing the background color
essentially tells the controller how to interpret a zero value in the
video memory. Similarly, changing the palette actually tells the
controller how to interpret values of 1, 2, or 3 in the video memory.
Table 15-2 Mode 4 Palette Choices
Palette Color 1 Color 2 Color 3
──────────────────────────────────────────────────────────────────────────
0 Green Red Dark yellow
1 Cyan Magenta Light gray
──────────────────────────────────────────────────────────────────────────
The QuickC Graphics Library
Creating a graphics image requires several steps. First, set a graphics
mode such as _MRES4COLOR. Then select a background color and a palette.
Finally, set the appropriate bits in the video memory to the required
values. To perform these tasks, you can use the BIOS video I/O routines or
the QuickC Graphics Library functions, or you can directly access the
video display memory and the controller registers. We will use the
Graphics Library routines, but first let's briefly outline the other
approaches.
The BIOS provides modest support for the graphics mode. It includes
interrupt routines for selecting a background color and a palette. Other
routines read a pixel from the screen, write a pixel to the screen, and
generate text. After that, you are on your own. To draw a line, you first
must figure out which pixels to turn on; then you must turn them on
individually. The BIOS routines are also quite slow. The write-pixel
routine, for example, takes a long time to fill a square.
On the other hand, the direct memory access approach is extremely fast.
But the programming is difficult. First, because each byte of memory
represents four separate pixels, you must use bitwise manipulations to
alter only one of those pixels. Second, the CGA stores the bit pairs for
the odd-numbered rows in a different section of memory than the
even-numbered rows. To fill a solid figure on the screen, you must jump
back and forth in the video display memory.
The Graphics Library overcomes the difficulties of the other two
programming methods. Its drawing routines are much faster than the BIOS
routines because they use direct memory access. The library functions are
conveniently oriented toward end results, not internal representations.
For example, the library provides functions to draw boxes and circles, and
the functions describe the screen in terms of screen position, not in
terms of memory location. Also, the library simplifies creating programs
that work in more than one graphics mode. The EGA and VGA graphics modes,
for example, use a different memory location (0xA0000) and different
schemes for representing pixels and colors; the library makes those
differences invisible to the user. The main drawback in using the Graphics
Library is the size it adds to a program. However, in our opinion, the
speed, convenience, power, and monitor-compatibility of the library
approach easily outweigh that factor for writing graphics programs.
Choosing in Modes
Let's explore the rudiments of graphics programming by creating a program
that turns on a few pixels. Although we use the CGA medium-resolution mode
4, you might want to try the program in another mode. To make that easy to
do, we use the following code (on the next page) with most of our
examples.
#include <stdlib.h>
main (argc, argv)
int argc;
char *argv[];
{
int mode = _MRES4COLOR;
int ch;
if (argc > 1)
mode = atoi(argv[1]);
if (_setvideomode(mode) == 0)
{
printf("Can't do mode %d.\n", mode);
exit(1);
}
This code fragment sets the mode to _MRES4COLOR by default. However, if
you use a command-line argument (argc > 1), mode is set to that argument.
The function atoi(), which is declared in STDLIB.C, converts the argument
from a string to a numeric value. To select a mode by this method, display
the Runtime Options dialog box before running the program and then enter
the desired mode number in the command-line window. For example, entering
16 causes the program to set mode 16 (_ERESCOLOR).
One convenient feature of the Graphics Library is that the same function
calls work for all modes. Although different modes might require different
argument values because the screen has more or fewer pixels, you can use
_getvideoconfig() information to scale argument values accordingly. That
is the approach we use.
Setting the mode also clears the screen, so you don't need to clear it
explicitly. (When you do need to clear the screen, the Graphics Library
provides a _clearscreen() function.)
Color Basics
Use the _selectpalette() function to choose a palette. This function takes
the palette number as its argument and returns the former palette value.
The Graphics Library supplements the two palettes provided by the BIOS
with intensified versions of each. (See Table 15-3.) In all cases, color
0 is the current background color.
Table 15-3 Palette Values for _selectpalette()
Palette Color 1 Color 2 Color 3
──────────────────────────────────────────────────────────────────────────
0 Green Red Dark yellow
1 Cyan Magenta Light gray
2 Light green Light red Yellow
3 Light cyan Light magenta White
──────────────────────────────────────────────────────────────────────────
To select a particular color from a palette, use _setcolor(). For example,
if palette 0 is in effect, _setcolor(2) sets the color to red. (This
function interprets color values differently in the EGA and VGA modes, as
we'll see later.) When you call a drawing function from the Graphics
Library, it draws with the currently defined color.
The Graphics Library function for setting the background color is
_setbkcolor(). It takes a long color value as an argument and returns the
color value of the background in effect when the function is called. The
numeric value of the argument depends on whether you use _setbkcolor() in
a text mode or in a graphics mode. This complication arises from the need
to make the function compatible with the VGA graphics modes. Table 15-4
lists the color values and the manifest constant names defined in graph.h.
The graphics color values are not consecutive values. Therefore, it is
often convenient to initialize an array to the values so they can be
accessed consecutively with an array index. The DOTS.C program (on p.
503) demonstrates this procedure.
Table 15-4 Background Color Values
Color Text Color Graphics Color Manifest Constant
Value (dec) Value (hex)
──────────────────────────────────────────────────────────────────────────
Black 0L 0x000000L _BLACK
Blue 1L 0x2A0000L _BLUE
Green 2L 0x002A00L _GREEN
Cyan 3L 0x2A2A00L _CYAN
Red 4L 0x00002AL _RED
Magenta 5L 0x2A002AL _MAGENTA
Dark yellow (brown) 6L 0x00152AL _BROWN
White (light gray) 7L 0x2A2A2AL _WHITE
Dark gray 8L 0x151515L _GRAY
Light blue 9L 0x3F1515L _LIGHTBLUE
Light green 10L 0x153F15L _LIGHTGREEN
Light cyan 11L 0x3F3F15L _LIGHTCYAN
Light red 12L 0x15153FL _LIGHTRED
Light magenta 13L 0x3F153FL _LIGHTMAGENTA
Light yellow 14L 0x153F3FL _LIGHTYELLOW
Bright white 15L 0x3F3F3FL _LIGHTWHITE
──────────────────────────────────────────────────────────────────────────
Physical Coordinates
The library drawing functions use coordinates to determine the location
images on the screen. Functions in the library use two forms of
coordinates: physical coordinates and logical coordinates. Physical
coordinates use the upper-left corner of the screen as their origin;
logical coordinates let you select the origin. Most of the drawing
functions use the logical coordinates. However, by default, the logical
coordinates are the same as the physical coordinates, so let's discuss
physical coordinates first.
Both systems measure distances in pixels. The physical coordinate system
uses the upper-left corner as the origin, that is, as the point whose
coordinates are (0, 0). The column number, or x value, is listed first,
and the row number, or y value, is listed second. The column values
increase to the right, and the row values increase downward. Thus, for a
320-by-200 mode, the physical coordinates of the lower-right corner are
(319, 199). Remember that numbering starts with 0, so column 319 is the
320th column. (See Figure 15-1.)
A Simple Example
Let's write a small program that uses the _setpixel() function. This
function takes two arguments──the horizontal and vertical location of a
pixel──then sets that pixel to the color last set by _setcolor(). Our
program uses the default logical coordinate system, whose origin is at the
upper-left corner of the screen. If coordinates fall outside the drawing
region, the function returns a value of -1.
The DOTS.C program (Listing 15-5) uses nested loops to draw a rectangular
pattern of pixels. Recall that you can override the default mode
(_MRES4COLOR) with a command-line argument. After the program draws a
pattern, you can press p to change the palette and press b to change the
background. Each keystroke increments the palette or background number by
one. (Palette changing works only in the CGA modes.) Remember that you
must either use the \l option to load the GRAPHICS.QLB library into memory
or else use a program list to access it. To exit the program, press the
Esc key.
┌──Origin at upper-left corner (x = 0, y = 0)
│
│
│ ┌────────────────────────────────────────────────┐
└──────┼►▒─────────────► │
│ │ x pixels | │
│ │ | │
│ │y pixels | │
│ │ | │
│ ▼-------------▓◄────pixel │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└────────────────────────────────────────────────┘
Figure 15-1. Locating a pixel using physical coordinates.
──────────────────────────────────────────────────────────────────────────
/* dots.c -- illustrates the _setcolor(), _setpixel(), */
/* and _selectpalette() functions from the */
/* QuickC Graphics Library */
/* If you load graphics.qlb, no program list is needed.*/
#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <graph.h>
#define ESC '\033'
#define BKCOLS 8 /* number of background colors */
#define PALNUM 4 /* number of palettes */
long Bkcolors[BKCOLS] = {_BLACK, _BLUE, _GREEN, _CYAN, _RED,
_MAGENTA, _BROWN, _WHITE};
main(argc, argv)
int argc;
char *argv[];
{
struct videoconfig vc;
unsigned int col, row;
short color = 0;
int bkc_index = 1; /* blue background */
short palette = 0; /* red, green, brown */
int firstcol, firstrow, lastrow, lastcol;
int mode = _MRES4COLOR;
int ch;
if (argc > 1)
mode = atoi(argv[1]);
if (_setvideomode(mode) == 0)
{
printf("Can't do that mode.\n");
exit(1);
}
_getvideoconfig(&vc);
firstcol = vc.numxpixels / 5;
firstrow = vc.numypixels / 5;
lastcol = 4 * vc.numxpixels / 5;
lastrow = 4 * vc.numypixels / 5;
_selectpalette(palette);
_setbkcolor(Bkcolors[bkc_index]);
for (col = firstcol; col <= lastcol; ++col)
{
_setcolor((++color / 3) % vc.numcolors);
for (row = firstrow; row <= lastrow; ++row)
_setpixel(col, row);
}
while ((ch = getch()) != ESC)
{
if (ch == 'p')
_selectpalette(++palette % PALNUM);
else if (ch == 'b')
_setbkcolor(Bkcolors[++bkc_index % BKCOLS]);
}
_setvideomode(_DEFAULTMODE); /* reset orig. mode */
}
──────────────────────────────────────────────────────────────────────────
Listing 15-5. The DOTS.C program.
Program Notes
Drawing the pattern dot by dot is a slow process. But palette and
background changes are practically instantaneous because they do not alter
the video memory; they merely alter the interpretation of the bits already
present.
The Bkcolors[] array is initialized to the first eight background colors.
Later, the program steps through these nonsequential background color
values by incrementing the array index.
The program uses mode-dependent information to draw the figure to scale.
The _getvideoconfig() function obtains the number of pixels per row and
column, and the program sizes the figure accordingly. The following code
defines an area covering 60 percent of the rows and of the columns:
firstcol = vc.numxpixels / 5;
firstrow = vc.numypixels / 5;
lastcol = 4 * vc.numxpixels / 5;
lastrow = 4 * vc.numypixels / 5;
Thus, in the 320-by-200 mode, the first column is 64 and the last column
is 256, while in the 640-by-350 EGA mode, the first column is 128 and the
last is 512.
The following statement changes the current color value:
_setcolor((++color / 3) % vc.numcolors);
This increments color each time the program writes a new column. However,
because integer division is truncated, the expression color / 3 increases
only when color increases by 3. Thus, the columns change color every third
column instead of every column. Unbounded incrementing causes color to
exceed the valid range. Therefore, the code uses the modulus operator to
produce a value in the range 0 through vc.numcolors - 1. For the
_MRES4COLOR mode, where vc.numcolors is equal to 4, this range is 0
through 3. (Using vc.numcolors makes the program more portable among
different video modes.)
EGA and VGA Considerations
Recall that the Runtime Options feature lets the program run in EGA and
VGA modes. How does changing the mode affect the program? First, the row
and column limits are set to reflect the new height and width of the
screen in pixels. Second, the vc.numcolors value is reset to the new mode.
The _MRES4COLOR mode sets a value of 4 in vc.numcolors; the _ERESCOLOR
reports a value of 16 if sufficient EGA memory is available, and it
reports a value of 4 otherwise.
Those are the explicit provisions we made for other modes. In addition,
some of the functions work differently. The _selectpalette() function, for
example, is recognized only by the 320-by-200 four-color mode and the
320-by-200 B/W mode; other color graphics modes ignore it because they
don't use the simple CGA palette system. Instead, the EGA and VGA
graphics-mode palettes contain more than four colors and also let you
select the palette colors individually. The default palette for the
EGA/VGA modes is essentially the same as the background colors shown in
Table 15-4 on p. 501. (The EGA/VGA brown, however, is a different tint
than the CGA brown.) The _setcolor() function uses the same numeric values
as shown in the text color column of Table 15-4, except that it uses type
short instead of type long.
Drawing Lines
Two minor modifications to the DOTS.C program produce major changes in its
operation. First, we can speed up the program noticeably by using the
_lineto() function from the Graphics Library. This function takes a column
coordinate and a row coordinate as arguments and draws a line from the
current screen position to the specified position. The _moveto() function
changes the current screen position to the column and row specified by its
two arguments. This function lets you relocate your figurative drawing pen
without drawing. It's easy to modify DOTS.C to use these functions instead
of _setpixel(). First, use the MS-DOS COPY command or, within QuickC,
choose Merge or Save As from the File menu to create a copy of DOTS.C.
Then modify the copy by replacing the following lines:
for (col = firstcol; col <= lastcol; ++col)
{
_setcolor((++color / 3) % vc.numcolors);
for (row = firstrow; row <= lastrow; ++row)
_setpixel(col, row);
}
with these lines:
for (col = firstcol; col <= lastcol; ++col)
{
_setcolor((++color / 3) % vc.numcolors);
_moveto(col, firstrow); /* new and improved */
_lineto(col, lastrow); /* version */
}
This replaces the inner for loop of DOTS.C with two library functions that
make this version of the program approximately 10 times faster than the
original.
A Beautiful Example
If _lineto() draws so much faster than _setpixel(), why bother to use the
latter function at all? One reason is that _setpixel() offers more
detailed control. It lets you create involved and intriguing displays, as
the next example shows.
One way to create interesting patterns is to key the color of a pixel to
the value of its coordinates. Make another copy of DOTS.C and name it
MOIRE.C. Alter the lines
for (col = firstcol; col <= lastcol; ++col)
{
_setcolor((++color / 3) % vc.numcolors);
for (row = firstrow; row <= lastrow; ++row)
_setpixel(col, row);
}
so that they read as follows:
for (col = firstcol; col <= lastcol; ++col)
{
for (row = firstrow; row <= lastrow; ++row)
{
_setcolor(((row * row + col * col) / 10)
% vc.numcolors);
_setpixel(col, row);
}
}
Note that the _setcolor() function has a new argument and that it has been
moved to the inner loop.
This alteration produces a dramatic change in the display──complex,
interlocking patterns called "Moire patterns." The difference is even more
impressive in the EGA and VGA modes because of their higher resolution and
greater number of colors.
Logical Coordinates
Many of the drawing functions, including the ones we've used, take logical
coordinates rather than physical coordinates. By default, they are the
same, so we haven't made a distinction between the two. However, the
_setlogorg() function lets you select another point as the origin of the
logical coordinate system. To use the function, pass it the physical
coordinates of the new origin. For example, using the call _setlogorg(100,
50) makes the point (100, 50) the new origin. Points to the left of the
new origin now have negative row values, and points to the right are
positive. Similarly, points above the new origin have negative row values,
and points below have positive values. (See Figure 15-2.)
┌──Origin set by _setlogorg()
│
│
┌────────────┼───┬──────────────────────────────────┐
│ │ │Negative y │
│ │ │ │
│Negative x └──►│ Positive x │
├────────────────▒─────────────►────────────────────┤
│ │ x pixels | │
│ │ | │
│ │y pixels | │
│ │ | │
│ ▼-------------▓ │
│ │ │
│ │ │
│ │Positive y │
│ │ │
└────────────────┴──────────────────────────────────┘
Figure 15-2. Locating a pixel using logical coordinates.
Using logical coordinates can simplify specifying locations. For example,
the center of the screen is a common choice as the logical origin because
the signs of the coordinates signal which quadrant of the screen a point
is in.
Drawing Rectangles
Now let's draw some rectangles. You could use _lineto(), but the Graphics
Library contains a ready-made function for the task, _rectangle(). This
function lets you do the following:
■ Specify whether the rectangle is drawn in outline or as a filled
figure.
■ Specify the size and location of the rectangle.
■ Select a color.
■ Select a line-drawing style for outlined rectangles.
■ Select a fill pattern for filled rectangles.
Function arguments determine the type, size, and location of the
rectangle. The first argument specifies whether the rectangle is an
outline or solid. The two values for this argument (from the graph.h file)
are as follows:
#define _GBORDER 2 /* draw outline only */
#define _GFILLINTERIOR 3 /* fill using current */
/* fill mask */
Outlined figures are drawn using the current line style, which is, by
default, a solid line. Likewise, filled figures use the current fill
pattern ("fill mask"), which is, by default, a solid color.
The remaining four arguments are the x and y logical coordinates of the
upper-left corner of the rectangle and the x and y logical coordinates of
the lower-right corner of the rectangle.
The drawing color (outline or fill pattern) is the last value of
_setcolor(). The _setlinestyle() function specifies the line style, and
the _setfillmask() function specifies the fill pattern. The _rectangle()
function uses the last value set by these two functions or, if they aren't
used, it uses the default values.
The RECT.C program (Listing 15-6) illustrates logical coordinates,
_rectangle(), and _setlinestyle(). Figure 15-3 shows output from the
program.
Program Notes
Using logical coordinates, you can easily center rectangles on the screen:
Merely assign the upper-left corner the negative coordinates of the
lower-right corner. This program generates a series of centered rectangles
by repeatedly scaling down the dimensions. Specifying this sequence of
rectangles in the physical coordinate system is a more tedious task
because you have to calculate all the coordinates.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 15-3 can be found on p.508 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 15-3. Output of RECT.C.
──────────────────────────────────────────────────────────────────────────
/* rect.c -- illustrates logical coordinates, */
/* the _rectangle() and _setlinestyle() */
/* functions */
/* If you load graphics.qlb, no program list is needed.*/
#include <stdio.h>
#include <graph.h>
#include <conio.h>
#define STYLES 5
short Linestyles[STYLES] = {0xFFFF, 0x8888, 0x7777,
0x00FF, 0x8787};
main(argc, argv)
int argc;
char *argv[];
{
struct videoconfig vc;
int mode = _MRES4COLOR;
int xcent, ycent;
int xsize, ysize;
int i;
if (argc > 1)
mode = atoi(argv[1]);
if (_setvideomode(mode) == 0)
{
printf("Can't open that mode.\n");
exit(1);
}
_getvideoconfig(&vc);
xcent = vc.numxpixels / 2 - 1;
ycent = vc.numypixels / 2 - 1;
_setlogorg(xcent, ycent);
xsize = 0.9 * xcent;
ysize = 0.9 * ycent;
_selectpalette(1);
_setcolor(3);
_rectangle(_GBORDER, -xsize, -ysize, xsize, ysize);
xsize *= 0.9;
ysize *= 0.9;
_setcolor(1);
_rectangle(_GFILLINTERIOR, -xsize, -ysize, xsize, ysize);
for (i = 0; i < 16; i++)
{
_setcolor(((i % 2) == 0) ? 2 : 3);
_setlinestyle(Linestyles[i % 5]);
xsize *= 0.9;
ysize *= 0.9;
_rectangle(_GBORDER, -xsize, -ysize, xsize, ysize);
}
getch(); /* Type a key to terminate. */
_setvideomode(_DEFAULTMODE);
}
──────────────────────────────────────────────────────────────────────────
Listing 15-6. The RECT.C program.
The program draws the outer rectangle in outline and the next rectangle is
solid. Then it draws a series of diminishing outline rectangles using a
variety of line styles. The following statement:
_setcolor(((i % 2) == 0) ? 2 : 3);
chooses color 2 if the index i is even, and color 3 if the index is odd;
this prevents the use of color 1, which would not appear against a
background of the same color.
Line Styles
Now let's see how the rectangle-drawing loop cycles though a list of line
styles.
The _setlinestyle() function takes one argument, a 16-bit "mask." This
mask is a template in which each bit represents a pixel in the line being
drawn. If a bit is set to 1, the corresponding pixel is turned on when the
line is drawn. The pixel is set to the background color if the bit is 0.
This template covers only 16 pixels, but you can visualize it as being
moved along the line 16 pixels at a time.
The default line style is a solid line. That translates into a mask of
sixteen 1s, or the hex value 0xFFFF. Now consider the following statement:
_setlinestyle(0x0F0F);
This function call creates a mask with the bit pattern 0000111100001111,
which produces a pixel pattern of four pixels off, four pixels on, four
off, four on──a dashed line. (See Figure 15-4.)
In RECT.C, the linestyles array contains five line masks. The for loop
near the end of the program cycles through these line styles as it draws a
series of rectangles of decreasing size.
Hex Binary
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
OxOFOF │0│0│0│0│1│1│1│1│0│0│0│0│1│1│1│1│
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
│ │
▼ ▼
▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒
└───────────────┬───────────────┘
Line style
Figure 15-4. A line style mask.
The _ellipse() Function
The Graphics Library does not limit you to lines and rectangles. The
_ellipse() function, for example, lets you draw ellipses. It takes the
same argument list as _rectangle() and draws an ellipse that fits just
inside the rectangular framework. The EGGS.C program (Listing 15-7)
presents an example of this function and also illustrates what happens
when solid figures overlap. (See Figure 15-5 on p. 512.)
──────────────────────────────────────────────────────────────────────────
/* eggs.c -- draws colorful eggs */
/* This program illustrates use of the video configuration */
/* structure, the _ellipse() function, the effect of over- */
/* lapping solid figures, and the use of logical coordinates. */
/* If you load graphics.qlb, no program list is needed. */
#include <stdio.h>
#include <stdlib.h>
#include <graph.h>
#include <conio.h>
#define ESC '\033'
main(argc, argv)
int argc;
char *argv[];
{
struct videoconfig vc;
int mode = _MRES4COLOR;
short xcent[3], ycent[3]; /* egg centers */
short xsize, ysize; /* egg limits */
int egg;
if (argc > 1)
mode = atoi(argv[1]);
if (_setvideomode(mode) == 0)
{
printf("Can't open mode %d\n", mode);
exit(1);
}
_getvideoconfig(&vc);
xsize = 0.3 * vc.numxpixels;
ysize = 0.3 * vc.numypixels;
xcent[0] = 0.3 * vc.numxpixels;
xcent[1] = 0.5 * vc.numxpixels;
xcent[2] = 0.7 * vc.numxpixels;
ycent[0] = ycent[2] = 0.4 * vc.numypixels;
ycent[1] = 0.6 * vc.numypixels;
_selectpalette(0);
_setbkcolor(_MAGENTA);
for (egg = 0; egg < 3; egg++)
{
_setlogorg(xcent[egg], ycent[egg]);
_setcolor(egg + 1);
_ellipse(_GFILLINTERIOR, -xsize, -ysize, xsize, ysize);
}
_settextposition(24, 0);
_settextcolor(1);
_outtext("Strike any key to terminate.");
getch();
_setvideomode(_DEFAULTMODE);
}
──────────────────────────────────────────────────────────────────────────
Listing 15-7. The EGGS.C program.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 15-5 can be found on p.512 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 15-5. Output of EGGS.C.
This program produces three colored Easter eggs. When figures overlap, the
figure drawn last prevails. Again, we use logical coordinates to simplify
specifying the figures. The arrays xcent[] and ycent[] contain the
coordinates for the centers of the three ellipses. The same command draws
all three figures; however, the program changes the logical coordinates
each time so that the ellipses are centered on different points.
We also use Graphics Library text functions in this program. Unlike
printf(), these functions let you specify the location and color of the
text. The _settextposition() function positions the text on the screen.
Unlike the graphics functions, it measures positions in character rows and
columns rather than in pixel columns and rows. Thus, our program positions
text at row 24, column 0. The _settextcolor() command sets the color of
the text. In the CGA graphics modes, you select the colors from the
graphics palette. The EGA and VGA modes use the EGA and VGA palettes. In
the color text modes, the colors follow the usual scheme──1 is blue, 2 is
green, and so on. The _outtext() function takes an argument of a pointer
to a string and prints it using the location and color specified by the
previous functions.
Filling Figures: _setfillmask() and _floodfill()
Just as _rectangle() uses a line mask to determine the line style,
_rectangle() and _ellipse() use a "fill mask" to determine the pattern
that fills a figure. By default, this pattern is a solid color, but you
can redefine the pattern with _setfillmask(). Recall how _setlinestyle()
uses a 16-bit pattern to represent a 16-pixel section of line. The
_setfillmask() function uses an 8-by-8-bit pattern to represent a pixel
pattern. In particular, it uses an array of eight bytes, with each byte
representing one row.
To create a bit map, draw an 8-by-8 pattern of squares. Fill in the
squares that represent turned-on pixels. Then visualize each row as a
binary number, each dark square representing a 1 and each clear square
representing a 0. Take these eight binary numbers and convert them into a
form recognized by C. (Use hexadecimal notation because each set of four
bits corresponds to one hex digit.) After you initialize an array with
those eight values, you can use the array as a fill mask.
Figure 15-6 shows a pattern that you can use repeatedly to create a
brick-like pattern. The first row has a bit pattern of 1111 1111. The
pattern 1111 corresponds to the hex value F; therefore, the entire byte is
0xFF in hex. The remaining rows generate the values shown in the figure.
Create a mask that uses these values with the following declaration:
unsigned char Mask[]= {0xFF, 0x80, 0x80, 0x80,
0xFF, 0x08, 0x08, 0x08};
Use this as the fill mask with the following statement:
_setfillmask(Mask);
directly before you call _rectangle() or _ellipse() in the fill mode. The
color of this mask is the same as the color of the figure.
To make the fill a different color than the outline, draw the figure in
the outline mode, change the current color, and use the _floodfill()
function to fill the figure. This function takes three arguments: x, or
pixel column, position; y, or pixel row, position; and a color number. If
the specified position falls within a closed boundary drawn in the
indicated color, the interior of the figure is filled with the current
fill pattern. If the point is outside the figure, the exterior is filled.
Do not specify a point that lies on the line.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 15-6 can be found on p.513 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 15-6. A fill mask.
The MASKS.C program (Listing 15-8) demonstrates how to use masks. Note
that this program looks better in the CGA mode than it does in either the
EGA or VGA modes. That's because the patterns are larger at lower
resolution and because the colors 1, 2, and 3 in the other modes don't
look as good together as do the colors of the CGA palette 0. The program
first draws a large rectangle and divides it into three parts. Then a for
loop fills the parts using three separate masks and three different
colors. (See Figure 15-7.)
──────────────────────────────────────────────────────────────────────────
/* masks.c -- illustrates _setfillmask() and */
/* _floodfill() */
/* Program list: masks.c */
/* If you load graphics.qlb, no program list is needed.*/
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <graph.h>
unsigned char Inversemask[8];
unsigned char Masks[3][8] = {
{0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00},
{0xFF, 0x80, 0x80, 0x80, 0xFF, 0x08, 0x08, 0x08},
{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}};
main(argc, argv)
int argc;
char *argv[];
{
struct videoconfig vc;
int mode = _MRES4COLOR;
short xc, yc;
short box, i;
if (argc > 1)
mode = atoi(argv[1]);
if (_setvideomode(mode) == 0)
{
fprintf(stderr, "Can't set mode %d\n", mode);
exit(1);
}
_getvideoconfig(&vc);
xc = vc.numxpixels / 2;
yc = vc.numypixels / 2;
for (i = 0; i < 8; i++)
Inversemask[i] = ~Masks[1][i];
_setlogorg(xc, yc);
_selectpalette(0);
_setcolor(1);
_rectangle(_GBORDER, -xc + 1, -yc + 1, xc - 1, yc - 1);
_moveto(-xc + 1, -yc / 3);
_lineto(xc - 1, -yc / 3);
_moveto(-xc + 1, yc / 3);
_lineto(xc - 1, yc / 3);
for (box = 0; box < 3; box++)
{
_setcolor(box + 1);
_setfillmask(Masks[box]);
_floodfill(0, (box - 1) * yc / 2, 1);
}
_settextposition(5, 10);
_outtext("Press a key to continue");
getch();
_setcolor(3);
_setfillmask(Inversemask);
_floodfill (0, 0, 1);
_setcolor(2);
_setfillmask(Masks[0]);
_floodfill(0, yc / 2, 1);
_settextposition(5, 10);
_outtext("Press a key to terminate");
getch();
_setvideomode(_DEFAULTMODE);
}
──────────────────────────────────────────────────────────────────────────
Listing 15-8. The MASKS.C program.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 15-7 can be found on p.515 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 15-7. Output of MASKS.C.
Pressing a key initiates the second phase of the program, which
illustrates what happens when you refill a figure. For example, the
program overlays the bottom third of the rectangle with the mask used in
the top third. Wherever the top mask has a bit set to 1, it covers the
bottom pattern; wherever it has a bit set to 0, the bottom shows through.
Thus, those portions of a mask set to 0 are "transparent."
The middle third of the rectangle demonstrates another aspect of
superimposed masks. Inversemask[] is the opposite of Masks[1][]:
for (i = 0; i < 8; i++)
Inversemask[i] = ~Masks[1][i];
The loop uses the bitwise ~ operator to create a mask containing the
reversed bits of Masks[1][] (1s become 0s, and 0s become 1s).
Superimposing the inverse mask on the original mask but using a different
color produces a two-color pattern. (See Figure 15-8.)
When you use _floodfill(), use a solid border; the pattern "leaks" through
a border with gaps. If you superimpose one pattern on top of another, the
boundary color (the third argument to _floodfill()) must be a different
color from the original pattern. The _floodfill() function turns on pixels
until it reaches pixels set with the specified boundary color. If the
pixels from the first mask have the same color as the boundary, the fill
function stops when it encounters them, and thus it does not fill the
entire figure.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 15-8 can be found on p.516 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 15-8. More output of MASKS.C.
Filling Other Shapes
You can use the _floodfill() command with any area enclosed by a solid
boundary. The ARROW.C program (Listing 15-9) provides an example using
the _lineto() function. Figure 15-9 on the following page shows the
output. Note that the Mask pattern fills the interior of the figure, and
the Outmask pattern fills the exterior.
──────────────────────────────────────────────────────────────────────────
/* arrow.c -- fills inside and outside of a line */
/* drawing */
/* If you load graphics.qlb, no program list is needed.*/
#include <stdio.h>
#include <graph.h>
#include <conio.h>
#define ESC '\033'
#define BKCOLS 16 /* use 16 background colors */
long Bkcolors[BKCOLS] = {_BLACK, _BLUE, _GREEN, _CYAN,
_RED, _MAGENTA, _BROWN, _WHITE,
_GRAY, _LIGHTBLUE, _LIGHTGREEN,
_LIGHTCYAN, _LIGHTRED, _LIGHTMAGENTA,
_LIGHTYELLOW,_BRIGHTWHITE};
char Mask[8] = {0x90, 0x68, 0x34, 0x19, 0x19, 0x34, 0x68,
0x90};
char Outmask[8] = {0xFF, 0x80, 0x80, 0x80, 0xFF, 0x08, 0x08,
0x08};
main(argc, argv)
int argc;
char *argv[];
{
struct videoconfig vc;
int mode = _MRES4COLOR;
float x1, y1, x2, y2, x3, y3, y4, x5, y5;
long bk = _BLUE;
if (argc > 1)
mode = atoi(argv[1]);
if (_setvideomode(mode) == 0)
{
printf("Can't set mode %d.\n", mode);
exit(1);
}
_getvideoconfig(&vc);
x1 = 0.1 * vc.numxpixels;
x2 = 0.7 * vc.numxpixels;
x3 = 0.6 * vc.numxpixels;
x5 = 0.9 * vc.numxpixels;
y1 = 0.45 * vc.numypixels;
y2 = 0.55 * vc.numypixels;
y3 = 0.3 * vc.numypixels;
y4 = 0.7 * vc.numypixels;
y5 = 0.5 * vc.numypixels;
_selectpalette(0);
_setcolor(1);
_moveto(x1, y1);
_lineto(x2, y1);
_lineto(x3, y3);
_lineto(x5, y5);
_lineto(x3, y4);
_lineto(x2, y2);
_lineto(x1, y2);
_lineto(x1, y1);
_setcolor(2);
_setfillmask(Mask);
_floodfill(x2, y5, 1) ;
_setcolor(3);
_setfillmask(Outmask); /* restores default mask */
_floodfill(5, 5, 1) ;
_settextcolor(1);
_settextposition(23, 0);
_outtext("Press <enter> to change background.");
_settextposition(24, 0);
_outtext("Press <esc> to end.");
while (getch() != ESC)
_setbkcolor(Bkcolors[++bk % BKCOLS]);
_setvideomode(_DEFAULTMODE);
}
──────────────────────────────────────────────────────────────────────────
Listing 15-9. The ARROW.C program.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 15-9 can be found on p.518 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 15-9. Output of ARROW.C.
This program uses a 16-element array of background color values to display
all 16 background colors. In the CGA mode, the background extends beyond
the area filled by the Outmask pattern. In the EGA and VGA modes, the
background area is the same as the fill area.
Replicating Images
Sometimes you might want to use an image on the screen more than once.
Rather than redrawing it several times, you can use the Graphics Library
_getimage() and _putimage() functions to transfer the image from the
screen to memory and then back to a different screen location.
The _getimage() Function
The _getimage() function copies a rectangular region into memory using the
following format:
_getimage(xa, ya, xb, yb, storage);
In this syntax, xa and ya are the coordinates of the upper-left corner of
the region to be copied, and xb and yb are the coordinates of the
lower-right corner. The storage parameter is a far pointer to a block of
memory large enough to hold all the pixel information.
Typically, you use the _imagesize() function to calculate the number of
bytes required and then use malloc() to allocate the necessary memory, as
follows:
char far *storage;
...
storage = (char far *) malloc((unsigned int) _imagesize(xa, ya, xb,
yb));
The _imagesize() function requires as arguments the same corner
coordinates used by _getimage(). It returns type long far, so you must use
a typecast to make it agree with malloc().
The _putimage() Function
The _putimage() function copies a previously stored image from memory to a
specified screen location using the following format:
_putimage(x, y, storage, action);
The x and y arguments are the coordinates of the upper-left corner of the
desired position for the image. The storage argument is a pointer to the
memory location containing the image. The action parameter describes how
the new image interacts with any existing image at that location. The
graph.h file contains a list of defined constants, or "action verbs," that
can be used as action arguments. Table 15-5 on the following page lists
and explains these constants.
Table 15-5 Action Verbs for _putimage()
Action Verb Meaning
──────────────────────────────────────────────────────────────────────────
_GAND Combine the new and old images using a logical AND. That is,
the final color for a pixel is newcolor & oldcolor.
_GOR Combine the new and old images using a logical OR. That is,
the final color for a pixel is newcolor | oldcolor.
_GXOR Combine the new and old images using a logical EXCLUSIVE OR.
That is, the final color for a pixel is newcolor ^ oldcolor.
_GPSET Overwrite the old image and display the transferred image as
it originally appeared. That is, the final color for a pixel
is newcolor.
_GPRESET Overwrite the old image and display the transferred image in
inverted color. That is, the final color for a pixel is
~newcolor.
──────────────────────────────────────────────────────────────────────────
The GETPUT.C program (Listing 15-10) demonstrates all the action verbs.
It draws an image in the upper-left quadrant of the screen. Next, it fills
the other three quadrants with striped patterns. Then it copies the
original image five times to each of the other quadrants, using all five
action verbs. The original image consists of vertical stripes of the
palette colors 1, 2, and 3, and the backgrounds consist of horizontal
stripes of the same colors. Therefore, this example shows all possible
interactions. (See Figure 15-10 on p. 523.)
──────────────────────────────────────────────────────────────────────────
/* getput.c -- illustrates _getimage(), _putimage(), */
/* the image-background interaction, and */
/* the aspect ratio */
/* If you load graphics.qlb, no program list is needed.*/
#include <stdio.h>
#include <stdlib.h> /* declares malloc() */
#include <graph.h>
#include <conio.h>
#define ESC '\033'
/* The following variables describe various */
/* coordinates and sizes. */
/* They are declared externally so that they can be */
/* shared easily by several functions. */
int X1, Yb1, X2, Y2, Xdelta, Xside, Yside; /* image */
int Xmid, Xmax, Ymid, Ymax; /* background */
int Xps, Xpr, Xand, Xor, Xxor, Ytop, Ybot; /* copies */
int X[3], Y[3];
float Ar; /* aspect ratio */
struct videoconfig Vc;
char Mask[8] = {0xFF, 0xFF, 0xFF, 0xFF, 0, 0, 0, 0};
void Initialize(void), Drawfig(void),
Drawbackground(void), Drawcopies(void);
main(argc, argv)
int argc;
char *argv[];
{
int mode = _MRES4COLOR;
if (argc > 1)
mode = atoi(argv[1]);
if (_setvideomode(mode) == 0)
{
fprintf(stderr, "Can't handle mode %d\n", mode);
exit(1);
}
Initialize();
Drawfig();
Drawbackground();
Drawcopies();
_settextposition(1, 1);
_outtext("Press a key to end");
_settextposition(3, 1);
_outtext("_GPSET _GPRESET _GAND");
_settextposition(11, 5);
_outtext("_GOR _GXOR");
getch();
_setvideomode(_DEFAULTMODE);
}
void Initialize()
{
_getvideoconfig(&Vc);
Ar = (float) (10 * Vc.numypixels) /
(6.5 * Vc.numxpixels);
_setlogorg(0, 0);
Xmid = Vc.numxpixels / 2;
Ymid = Vc.numypixels / 2;
Xmax = Vc.numxpixels - 1;
Ymax = Vc.numypixels - 1;
/* locate three background rectangles */
X[0] = Xmid;
Y[0] = 0;
X[1] = Xmid;
Y[1] = Ymid;
X[2] = 0;
Y[2] = Ymid;
X1 = 0.2 * Vc.numxpixels;
Yb1 = 0.2 * Vc.numypixels;
Xdelta = 0.033 * Vc.numxpixels;
Xside = 3 * Xdelta;
Yside = 3 * Ar * Xdelta;
X2 = X1 + Xside;
Y2 = Yb1 + Yside;
/* offsets for _putimage() */
Xps = .05 * Vc.numxpixels;
Xpr = .20 * Vc.numxpixels;
Xand = 0.35 * Vc.numxpixels;
Xor = .10 * Vc.numxpixels;
Xxor = .30 * Vc.numxpixels;
Ytop = .05 * Vc.numypixels;
Ybot = 2 * Ytop + Yside;
_selectpalette(0);
}
void Drawfig()
{
_setcolor(1);
_rectangle(_GFILLINTERIOR, X1, Yb1,
X1 + Xdelta, Y2);
_setcolor(2);
_rectangle(_GFILLINTERIOR, X1 + Xdelta + 1, Yb1,
X1 + 2 * Xdelta, Y2);
_setcolor(3);
_rectangle(_GFILLINTERIOR, X1 + 2 * Xdelta + 1,
Yb1, X2, Y2);
}
void Drawbackground()
{
_setfillmask(Mask);
_setcolor(1);
_rectangle(_GFILLINTERIOR, Xmid, 0, Xmax - 1, Ymid - 1);
_setcolor(2);
_rectangle(_GFILLINTERIOR, Xmid, Ymid, Xmax, Ymax);
_setcolor(3);
_rectangle(_GFILLINTERIOR, 0, Ymid, Xmid - 1, Ymax);
}
void Drawcopies()
{
int quad; /* quadrant used */
char far *storage;
storage = (char far *) malloc((unsigned)_imagesize
(X1, Yb1, X2, Y2));
_getimage(X1, Yb1, X2, Y2, storage);
for (quad = 0; quad < 3; quad++)
{
_putimage(X[quad] + Xps, Y[quad] + Ytop,
storage, _GPSET);
_putimage(X[quad] + Xpr, Y[quad] + Ytop,
storage, _GPRESET);
_putimage(X[quad] + Xand, Y[quad] + Ytop,
storage, _GAND);
_putimage(X[quad] + Xor, Y[quad] + Ybot,
storage, _GOR);
_putimage(X[quad] + Xxor, Y[quad] + Ybot,
storage, _GXOR);
}
}
──────────────────────────────────────────────────────────────────────────
Listing 15-10. The GETPUT.C program.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 15-10 can be found on p.523 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 15-10. Output of GETPUT.C.
To recognize the precise interactions, you must understand the action of
C's bitwise operators, and you must remember that the verbs operate on the
palette numbers, not the color numbers. For example, if CGA palette 0 is
in effect, and you select the color green, the action verbs use the
palette number (1), not the color number for green (2). Because of this,
the action verbs affect CGA mode colors differently than EGA or VGA
colors. For example, when CGA palette 0 is used, palette number 1 is
green. In the two-digit binary that represents CGA palette choices, the
number is actually 01. When you apply the bitwise negation operator
_GPRESET to the number, you get 10. In decimal, this is palette choice 2,
or brown. Therefore, in CGA modes, _GPRESET converts a green image to
brown. However, the EGA mode uses a 16-color palette. The default palette
represents green with palette number 2, which is 0010 in binary. _GPRESET
converts this to 1101 in binary, which is intensified magenta. Therefore,
in EGA modes, _GPRESET converts green not to brown but to intensified
magenta──quite different from the CGA operation.
Aspect Ratios
The GETPUT.C program also illustrates how to make squares. In most modes,
the pixel width is different from the pixel height, so you cannot make a
square figure by creating a rectangle with equal numbers of horizontal and
vertical pixels. To create square rectangles (or circular ellipses), you
need to calculate the proper "aspect ratio." This is the ratio of the
number of pixels in a vertical line to the number of pixels in a
horizontal line of the same physical length. The aspect ratio is the
product of two ratios:
■ (screen height in pixels) / (screen width in pixels)
■ (screen height in inches) / (screen width in inches)
The GETPUT.C program represents that ratio with the following code:
Ar = (float) (10 * Vc.numypixels) / (6.5 * Vc.numxpixels);
Then the program uses the aspect ratio to scale the number of y pixels:
Xside = 3 * Xdelta;
Yside = 3 * Ar * Xdelta;
This results in Yside and Xside representing equal lengths on the screen.
Of course, the vertical and horizontal size settings on your monitor will
affect your results.
Simple Animation
You can use the _getimage() and _putimage() functions to create animation.
The method is to erase the current image and to put a copy of the image at
a slightly displaced location. To erase, you can superimpose a copy on the
original using the _GXOR action verb. Because this operation combines like
bits to produce an off bit, all bits get turned off.
The RACE.C program (Listing 15-11) uses this animation technique to stage
a race across the screen. The contestants are three patterned circles. The
race, in this case, goes to the luckiest, for the program uses the rand()
random number function to control the motion. At each movement
opportunity, the program calls rand(). If it returns an even value, the
circle moves ahead a step; otherwise it stays put.
──────────────────────────────────────────────────────────────────────────
/* race.c -- race of the patterned circles */
/* Illustrates animation with _getimage() and */
/* _putimage(), random number use with srand() and */
/* rand(), and system clock use with time() and */
/* ftime(). */
/* Program list: race.c (for srand(), rand(), and */
/* ftime()) */
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <graph.h>
#include <time.h>
#include <sys\types.h>
#include <sys\timeb.h>
#define END 25
#define FIGNUM 3
typedef char far *PTFRCHAR;
PTFRCHAR Bufs[FIGNUM];
unsigned char Masks[FIGNUM][8] = {
{0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00},
{0xF0, 0xF0, 0xF0, 0xF0, 0x0F, 0x0F, 0x0F, 0x0F},
{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}};
short Xul[FIGNUM], Yul[FIGNUM]; /* figure locations */
short Xsize, Ysize; /* figure size */
struct videoconfig Vc;
void Initialize(void);
void Draw_n_store(void);
void Move_figs(void);
void Wait(double);
main(argc, argv)
int argc;
char *argv[];
{
int mode = _MRES4COLOR;
if (argc > 1)
mode = atoi(argv[1]);
if (_setvideomode(mode) == 0)
{
fprintf(stderr, "mode %d not supported\n", mode);
exit(1);
}
Initialize();
Draw_n_store();
_settextcolor(2);
_settextposition(1, 1);
_outtext("Place your bets and press a key");
_settextposition(25, 1);
_outtext("Press a key again when done");
getch();
Move_figs();
getch();
_setvideomode(_DEFAULTMODE);
}
void Initialize()
{
int i;
float ar; /* aspect ratio */
_getvideoconfig(&Vc);
ar = (float)(10 * Vc.numypixels) / (6.5 * Vc.numxpixels);
/* set size, initial positions */
Xsize = Vc.numxpixels / 30;
Ysize = ar * Xsize;
for(i = 0; i < FIGNUM; i++)
{
Xul[i] = 0;
Yul[i] = (i + 1) * Vc.numypixels /
(FIGNUM + 1);
}
_selectpalette(0);
_setcolor(1);
/* draw finish line */
_moveto(END * Xsize, 0);
_lineto(END * Xsize, Vc.numypixels - 1);
}
void Draw_n_store() /* draw images, save them */
{
int i;
for (i = 0; i < FIGNUM; i++)
{
_setcolor(i + 1);
_setfillmask(Masks[i]);
_ellipse(_GFILLINTERIOR, Xul[i], Yul[i],
Xul[i] + Xsize, Yul[i] + Ysize);
_ellipse(_GBORDER, Xul[i], Yul[i],
Xul[i] + Xsize, Yul[i] + Ysize);
Bufs[i] = (PTFRCHAR) malloc((unsigned int)
_imagesize(0, Yul[i], Xul[i] +
Xsize, Yul[i] + Ysize));
_getimage(Xul[i], Yul[i], Xul[i] + Xsize, Yul[i] +
Ysize, Bufs[i]);
}
}
void Move_figs()
{
int i, j;
static int dx[FIGNUM] = {0, 0, 0}; /* displacements */
time_t tval;
time(&tval); /* use the current time value */
srand(tval); /* to initialize rand() */
while (dx[0] < END && dx[1] < END && dx[2] < END)
{
for (i = 0; i < FIGNUM; i++)
{
/* Advance the figure one position if */
/* rand() returns an even number. */
if (rand() % 2 == 0)
{
/* erase old image */
_putimage(dx[i] * Xsize, Yul[i],
Bufs[i], _GXOR);
/* redraw in new position */
_putimage((1 + dx[i]) * Xsize, Yul[i],
Bufs[i], _GPSET);
dx[i]++;
}
}
Wait(0.15);
}
for (j = 0; j < 5; j++)
{
for(i = 0; i < FIGNUM; i++)
{
/* flash winning figure */
if (dx[i] >= END)
{
Wait(0.2);
_putimage(dx[i] * Xsize, Yul[i],
Bufs[i], _GPRESET);
Wait(0.2);
_putimage(dx[i] * Xsize, Yul[i],
Bufs[i], _GPSET);
}
}
}
}
void Wait(pause) /* wait for pause seconds */
double pause;
{
struct timeb start, end;
long delay;
delay = 1000 * pause; /* convert to milliseconds */
ftime(&start);
ftime(&end);
while ((1000 * (end.time - start.time) +
+ end.millitm - start.millitm) < delay)
ftime(&end);
}
──────────────────────────────────────────────────────────────────────────
Listing 15-11. The RACE.C program.
The RACE.C program uses three action verbs. The _GPSET verb generates new
("moving") images. The _GXOR verb erases the old images. Finally, the
program alternates between _GPSET and _GPRESET, creating a flashing image
for the winner. (Ties are possible, in which case the cowinners flash.)
The program also features several standard C functions. The rand()
function returns a random number in the range 0 through 32,767. Actually,
it returns a "pseudorandom" number, meaning that eventually the function
returns the same sequence of numbers. Also, rand() always starts with the
same sequence of numbers unless you first use srand() to select a
different starting point. The program uses the time() function to "seed"
srand() with a different argument each time the program is called. As a
result, the program always runs using a different sequence of random
numbers. (The time() function places the number of seconds elapsed between
00:00:00, January 1, 1970 [GMT] and the current clock time into the
location whose address is passed to it as an argument.)
Programs such as this can run into problems because different computers
and different video modes run the animation at different speeds. What
plods on one system might blur on another. To meet this problem, RACE.C
uses the system clock to run the animation at a constant rate. The Wait()
function causes the program to pause for the number of seconds indicated
by its floating-point argument. Wait(), in turn, uses the standard C
function ftime(). The argument for this function is a pointer to a type
struct timeb structure, as defined in <SYS\timeb.h>. The time field of
this structure is the time in seconds since 00:00:00 GMT, January 1, 1970
(the value provided by time()). The millitm field is a fraction of a
second in milliseconds. This added information allows Wait() to pause for
fractions of a second. (Wait() converts all times to milliseconds to
facilitate finding the difference between two times.)
Although the timeb structure tells time to the nearest millisecond, the
system clock might progress in larger jumps. The IBM PC/XT/AT clock, for
example, has 18.2 ticks per second; so each tick advances the time
approximately 50 milliseconds.
EGA Graphics
As Table 15-1 on p. 492 suggests, the EGA offers more graphics
capabilities than the CGA offers. The two adapters also map video memory
to the display differently, but the QuickC Graphics Library functions hide
those details from us. What QuickC can't hide are the different ways that
the EGA graphics modes handle color.
The Palette
For comparison, let's quickly review the CGA palette. It has four colors
numbered 0 through 3, and you can use the _setcolor() function to choose a
particular color from the palette. Color 0 is the background color, and
you set its value with the _setbkcolor() function by using one of the
manifest constants in Table 15-4 on p. 501. The other three colors are
chosen by using _selectpalette() to select one of the four preset
combinations in Table 15-3 on p. 500.
The EGA has a 16-color palette, using values in the range 0 through 15.
Palette value 0 represents the background color. By default, the EGA color
palette is set to the same colors shown in Table 15-3. As with the CGA,
you can use _setcolor() to select a particular color from this palette;
merely use the palette number as the argument. However, the EGA modes
ignore the _selectpalette() function. Instead, these modes use the
_remappalette() and _remapallpalette() functions to reassign colors to the
palette values. That is, palette value 1, by default, is blue. But the
remapping functions let you assign red, for example, to palette value 1.
The remapping functions provide you with a powerful tool. Suppose you've
drawn and colored a figure, but you want to change the colors. Rather than
redraw the figure and fill it again, you can remap the palette assignments
and change the on-screen colors almost immediately.
Specifying Palette Values and Color Values
In the EGA graphics modes, each pixel is represented by four bits in the
EGA video memory. These bits represent the palette value, which is a
number in the range 0 through 15. If a particular pixel has a palette
number of 3, for example, the EGA looks up the "color value" for that
palette number and makes the pixel that color. The EGA color value, in
turn, is a 6-bit number. Essentially, the palette number is an index to a
table of color values, and the remapping functions alter that table.
The 6-bit EGA color values can generate 64 colors, but only mode 16
(_ERESCOLOR) makes all 64 available. The other modes use only 16 colors of
the default palette; however, they let you select the palette number which
will correspond to a given color. The total range of EGA colors uses the
color values 0 through 63. However, the QuickC remapping functions use VGA
color values for compatibility reasons. This complicates accessing all 64
EGA colors because, in the VGA representation, the EGA colors are not
consecutive values. However, you can access the 16 colors of the default
palette by using the manifest constants in graph.h. (See Table 15-6 on
the following page.) These are set to the VGA values shown in the second
column. The EGA values are provided for your information. (Later we will
show you how to access all 64 colors.)
Setting the Palette
The QuickC Graphics Library contains two functions for setting the EGA
palette. One, called _remappalette(), lets you assign a color value to a
particular palette value. You can use it, for example, to change palette
value 3 from cyan to magenta.
Table 15-6 VGA Color Values for the Default Palette
#define Name VGA Color Value EGA Color Value
──────────────────────────────────────────────────────────────────────────
Hex rgb RGB Octal
_BLACK 0x000000L 000 000 000
_BLUE 0x2A0000L 000 001 001
_GREEN 0x002A00L 000 010 002
_CYAN 0x2A2A00L 000 011 003
_RED 0x00002AL 000 100 004
_MAGENTA 0x2A002AL 000 101 005
_BROWN 0x00152AL 010 100 024
_WHITE 0x2A2A2AL 000 111 007
_GRAY 0x151515L 111 000 070
_LIGHTBLUE 0x3F1515L 111 001 071
_LIGHTGREEN 0x153F15L 111 010 072
_LIGHTCYAN 0x3F3F15L 111 011 073
_LIGHTRED 0x15153FL 111 100 074
_LIGHTMAGENTA 0x3F153FL 111 101 075
_LIGHTYELLOW 0x153F3FL 111 110 076
_BRIGHTWHITE 0x3F3F3FL 111 111 077
──────────────────────────────────────────────────────────────────────────
The function uses VGA color values, for which it's convenient to use the
graph.h constants. To make palette value 3 represent magenta, use the
following call:
_remappalette(3,_MAGENTA);
The second function, called _remapallpalette(), lets you remap all the
palette values simultaneously. Its argument is an array of the desired
color values. Because the VGA color values are type long, use a type long
array initialized to the desired VGA color values.
The RINGS.C program (Listing 15-12 beginning on p. 532) presents an
interesting example of palette remapping. It initializes the newpalette[]
array to light blue for all but three palette values. Palette value 0
(which represents the background) is assigned gray, and palette values 1
and 8 are assigned red and light red. Then _remapallpalette() sets the
palette to the values provided by newpalette[]. Next, a for loop draws a
series of concentric circles:
for (index = 2; index < 16; index++)
{
xmax /= 1.4;
ymax /= 1.4;
_setcolor(index);
_ellipse(_GFILLINTERIOR, -xmax, -ymax, xmax, ymax);
}
If the palette weren't reset, this code would produce concentric rings of
different colors. However, with the new palette, all but two rings are
light blue, and all but those two rings blend into a featureless
background.
Next, the program enters a loop that shifts the color values in
newpalette[] by one array element; then it remaps the palette. The first
pass through this loop assigns red to palette value 2 and light red to
palette value 9. On the screen, this produces the illusion that each ring
has moved one position inward. (See Figure 15-12 on the following page.)
One final program feature lets you use the keys 2 through 7 to change the
ring colors──by changing the color assignments for newpalette[]. Press the
Esc key to quit.
──────────────────────────────────────────────────────────────────────────
EGA Color Values
The QuickC Graphics Library doesn't use the EGA color values explicitly,
but here's how they work. Each of the 64 EGA colors is represented by a
6-bit color value. The left three bits represent low-intensity red, green,
and blue, respectively. The right three bits represent normal-intensity
red, green, and blue, respectively. The sequence rgbRGB represents this
order symbolically. The binary value 000001, for example, has the B bit
(normal-intensity blue) set.
How then does the EGA form the intensified colors──light blue, light red,
and so on? The CGA forms them by adding low-intensity white to the
standard colors. Low-intensity white is 111000 in EGA notation, so light
blue, for example, is 111001. Table 15-6 defines brown as 010100, or red
plus low-intensity green. This produces a different tint than the
corresponding CGA color, which is yellow, or 000110.
The EGA has other combinations with no CGA equivalents. For example,
001000 would be a low-intensity blue, and 010100 would be a mixture of
low-intensity green and normal-intensity red. Altogether, the 6-bit
representation permits 64 combinations, corresponding to the integers 0
through 63. Another way of looking at the color value is that each color
is described by two bits, permitting four intensity settings for that
color: off, low, normal, and high-intensity, as shown in Figure 15-11.
The rgb bits are sometimes referred to as the "1/3" intensity bits and the
RGB bits as the "2/3" intensity bits. Setting both gives full intensity.
Note, however, that the actual ratios of intensities depend on the
display's brightness and contrast settings. (The rgbRGB system is also
reflected in the EGA hardware, which uses six wires, one for each bit, to
communicate color information to the monitor.)
┌─┐ ┌─┐
EGA color value bits r g │b│ R G │B│ Blue intensity
└─┘ └─┘
┌── 0 0 0 (off)
Bit values ──┤ 1 0 1/3 (low intensity)
│ 0 1 2/3 (normal)
└── 1 1 1 (high intensity)
Figure 15-11. The EGA supports four intensities for blue.
──────────────────────────────────────────────────────────────────────────
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 15-12 can be found on p.532 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 15-12. Output of RINGS.C.
──────────────────────────────────────────────────────────────────────────
/* rings.c -- shoots colored rings */
/* This program illustrates _remapallpalette() and */
/* how it can be used to produce the appearance of */
/* motion. The program is intended for EGA modes 13, */
/* 14, and 16. */
/* Program list: rings.c */
/* If you load graphics.qlb, no program list is needed.*/
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <graph.h>
#define ESC '\033'
long Colors[16] = {_BLACK, _BLUE, _GREEN, _CYAN,
_RED, _MAGENTA, _BROWN, _WHITE,
_GRAY, _LIGHTBLUE, _LIGHTGREEN,
_LIGHTCYAN, _LIGHTRED, _LIGHTMAGENTA,
_LIGHTYELLOW, _BRIGHTWHITE};
main(argc, argv)
int argc;
char *argv[];
{
struct videoconfig vc;
float aspect;
short xmax, ymax;
long int newpalette[16];
long int temp;
int index;
int hot1 = 1; /* first colored ring */
int hot2 = 8; /* second colored ring */
int mode = _ERESCOLOR;
int ch;
if (argc > 1)
mode = atoi(argv[1]);
if (mode < 13)
{
fprintf(stderr, "Requires EGA or VGA mode\n");
exit(1);
}
if (_setvideomode(mode) == 0)
{
fprintf(stderr, "% d mode unavailable\n", mode);
exit(2);
}
_getvideoconfig(&vc);
_setlogorg(vc.numxpixels / 2 - 1, vc.numypixels / 2 - 1);
aspect = (10.0 * vc.numypixels) / (6.5 * vc.numxpixels);
ymax = vc.numypixels / 2 - 2;
xmax = ymax / aspect;
for (index = 2; index < 16; index++)
newpalette[index] = _LIGHTBLUE;
newpalette[0] = _GRAY;
newpalette[hot1] = _RED;
newpalette[hot2] = _LIGHTRED;
_remapallpalette(newpalette); /* set initial palette */
_setcolor(1);
_ellipse(_GFILLINTERIOR, -xmax, -ymax, xmax, ymax);
/* draw concentric circles */
for (index = 2; index < 16; index++)
{
xmax /= 1.4;
ymax /= 1.4;
_setcolor(index);
_ellipse(_GFILLINTERIOR, -xmax, -ymax, xmax, ymax);
}
do
{
while (!kbhit())
{
temp = newpalette[15];
for(index = 15; index > 1; index--)
newpalette[index] = newpalette[index - 1];
newpalette[1] = temp;
_remapallpalette(newpalette);
hot1 = hot1 % 15 + 1; /* index of colored ring */
hot2 = hot2 % 15 + 1;
}
ch = getch();
if (ch > '1' && ch < '8') /* reassign colors */
{
newpalette[hot1] = Colors[ch - '0'];
newpalette[hot2] = Colors[ch - '0' + 8];
}
} while (ch != ESC);
_clearscreen(_GCLEARSCREEN);
_setvideomode(_DEFAULTMODE);
}
──────────────────────────────────────────────────────────────────────────
Listing 15-12. The RINGS.C program.
The program illustrates a useful graphics programming technique──using an
array to hold the standard color values:
long Colors[16] = {_BLACK, _BLUE, _GREEN, _CYAN,
_RED, _MAGENTA, _BROWN, _WHITE,
_GRAY, _LIGHTBLUE, _LIGHTGREEN,
_LIGHTCYAN, _LIGHTRED, _LIGHTMAGENTA,
_LIGHTYELLOW, _BRIGHTWHITE};
This use of an array lets a program access the nonsequential VGA values
with a sequential array index.
Accessing All 64 EGA Colors
The _ERESCOLOR mode lets you use all 64 EGA colors. However, to do so with
the QuickC remapping functions, you must use the VGA color code for these
colors. Although the graph.h list of manifest constants provides 16 of
these codes, 48 EGA colors are unrepresented. To access them, you need to
generate their VGA codes.
The VGA uses six bits to describe the intensity of each of the three
primary colors. Six bits produce 64 intensity levels for each primary
color, so the total number of combinations is 64 x 64 x 64, or 262,144
colors. The VGA stores this information in a 4-byte unit. The leftmost
byte is set to 0, the next byte contains the 6-bit code for blue
intensity, the next byte contains the 6-bit code for green intensity, and
the last byte contains the 6-bit code for red intensity. Each of these
color bytes has its leftmost two bits set to 0. (See Figure 15-13.)
How does this compare to the EGA system? Consider the color blue. The EGA
rgbRGB system can generate four levels of blue: 000000, 001000, 000001,
and 001001. They represent 0, 1/3, 2/3, and full intensity. The VGA has 64
levels of intensity, represented by a blue byte in the range 0 through 63.
Zero intensity is 0. The 1/3 level is 21 decimal, or 0x15 hex. The 2/3
intensity level is 42 decimal, or 0x2A hex. The full level is 63 decimal,
or 0x3F hex. Extending this analysis to red and green produces the list of
correspondences shown in Table 15-7. Hex is the natural base to use for
VGA values because two hex digits can represent one byte. Thus, the first
two hex digits represent the blue intensity, the second two hex digits
represent the green intensity, and the final two hex digits represent the
red intensity.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 15-13 can be found on p.535 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 15-13. VGA color value storage.
Table 15-7 EGA Bit to VGA Byte Conversion
EGA Color Bit VGA Equivalent Color
Setting
──────────────────────────────────────────────────────────────────────────
000 001 0x2A0000 Blue (2/3 intensity)
000 010 0x002A00 Green (2/3 intensity)
000 100 0x00002A Red (2/3 intensity)
001 000 0x150000 Blue (1/3 intensity)
010 000 0x001500 Green (1/3 intensity)
100 000 0x000015 Red (1/3 intensity)
──────────────────────────────────────────────────────────────────────────
Because the table lists all of the EGA color bits as VGA values, you can
now combine them to represent any EGA color. For example, cyan is blue
plus green, or 0x2A2A00. For another example, consider light blue, which
is 111001 in EGA notation. We can sum the VGA equivalents as follows:
EGA VGA Color
──────────────────────────────────────────────────────────────────────────
100 000 0x000015 Blue (1/3 intensity)
010 000 0x001500 Green (1/3 intensity)
001 000 0x150000 Red (1/3 intensity)
000 001 0x2A0000 Blue (2/3 intensity)
------- -------- ---------------------
111 001 0x3F1515 Light blue
──────────────────────────────────────────────────────────────────────────
The final value, 0x3F1515, matches VGA_LIGHTBLUE in Table 15-3 on p. 530.
Defining Nonpalette Colors
You can use this information to construct a header file (Listing 15-13 on
the following page) to supplement the graph.h color value constants.
──────────────────────────────────────────────────────────────────────────
/* egacolor.h -- vga equivalents for base ega colors */
#define r 0x000015L /* 1/3 intensity red */
#define g 0x001500L /* 1/3 intensity green */
#define b 0x150000L /* 1/3 intensity blue */
#define R 0x00002AL /* 2/3 intensity red */
#define G 0x002A00L /* 2/3 intensity green */
#define B 0x2A0000L /* 2/3 intensity blue */
──────────────────────────────────────────────────────────────────────────
Listing 15-13. The egacolor.h program.
The constants in egacolor.h provide the base color values. To generate any
other EGA color, use the bitwise OR operator with these values. For
example, a color whose rgbRGB representation is 101010 has the VGA value r
| b | G. What color is this? Well, 111000 is dim white (dark gray), and
101010 replaces the faint green of 111000 with a brighter green. The
result is a greenish gray.
The SCAPE.C program (Listing 15-14) demonstrates how to define other
colors in terms of the base colors. It draws a simple scene, shown in
Figure 15-14, in fresh, new colors, none of which are in the default
palette.
──────────────────────────────────────────────────────────────────────────
/* scape.c -- uses nondefault EGA colors */
/* If you load graphics.qlb, no program list is needed.*/
#include <stdio.h>
#include <graph.h>
#include "egacolor.h"
#include <stdlib.h>
#include <conio.h>
#define SKY (b | B | g)
#define OCEAN b
#define SAND (R | g | b)
#define SUN (R | G | r | g)
main(argc, argv)
int argc;
char *argv[];
{
struct videoconfig vc;
int mode = _ERESCOLOR;
short xmax, ymax, sunx, suny, sunsizex, sunsizey;
float ar;
if (argc > 1)
mode = atoi(argv[1]);
if (_setvideomode(mode) == 0)
{
fprintf(stderr, "mode %d not supported\n", mode);
exit(1);
}
_getvideoconfig(&vc);
xmax = vc.numxpixels - 1;
ymax = vc.numypixels - 1;
sunx = 0.7 * xmax;
suny = 0.2 * ymax;
ar = (float)(10 * vc.numypixels) / (6.5 * vc.numxpixels);
sunsizex = xmax / 30;
sunsizey = ar * sunsizex;
_remappalette(1, SKY);
_remappalette(2, OCEAN);
_remappalette(3, SAND);
_remappalette(4, SUN);
_setcolor(1);
_rectangle(_GFILLINTERIOR, 0, 0, xmax, 2 * ymax / 5);
_setcolor(4);
_ellipse(_GFILLINTERIOR, sunx - sunsizex, suny -
sunsizey, sunx + sunsizex, suny + sunsizey);
_setcolor(2);
_rectangle(_GFILLINTERIOR, 0, 2 * ymax / 5, xmax,
2 * ymax / 3);
_setcolor(3);
_rectangle(_GFILLINTERIOR, 0, 2 * ymax / 3, xmax, ymax);
getch();
_setvideomode(_DEFAULTMODE);
}
──────────────────────────────────────────────────────────────────────────
Listing 15-14. The SCAPE.C program.
┌────────────────────────────────────────────────────────────────────────┐
│ Figure 15-14 can be found on p.537 of the printed version of the book. │
└────────────────────────────────────────────────────────────────────────┘
Figure 15-14. Output of SCAPE.C.
Automatic Color Value Conversion
A second approach to using nonpalette colors is to write a function that
converts an EGA value to the corresponding VGA value. This lets us map the
simple EGA color values into the complicated VGA color values that the
library functions use. Listing 15-15 presents a function that takes an
EGA color value as an argument and returns the corresponding VGA color
value.
The program's egacolor >> bit operation shifts a specified bit to position
0. The &1 operation then masks all the other bits. This gives the
expression a value of 1 if the bit is 1; otherwise it is a 0. This is
multiplied by the VGA equivalent for the bit, and the loop obtains a
running total.
A Palette-Mapping Example
Now we can look at all of the hidden 48 EGA colors. The ALLCOLOR.C program
(Listing 15-16) uses Ega_to_vga() and _remappalette() to display all the
EGA colors. It divides the screen vertically into two rectangles. The left
rectangle fills with blue, and the right with red. Windows at the bottom
of each rectangle show the current color value. Pressing g advances the
color value of the left rectangle by 1 (it remaps palette value 1 to the
next color value). Similarly, h advances the color value of the right
rectangle. Pressing Shift with these keys decrements the color values.
Using this program, you can thus match any two of the 64 colors side by
side and see the sometimes subtle distinctions.
──────────────────────────────────────────────────────────────────────────
/* egatovga.c -- converts ega color values to vga */
/* color values */
long Ega_to_vga(egacolor)
int egacolor; /* ega color value */
{
static long vgavals[6] = {0x2A0000L, 0x002A00L,
0x00002AL, 0x150000L,
0x001500L, 0x000015L};
/* array holds VGA equivalents to EGA bits */
long vgacolor = 0L; /* vga color value */
int bit;
/* convert each bit to equivalent and sum */
for (bit = 0; bit < 6; bit++)
vgacolor += ((egacolor >> bit) &1) * vgavals[bit];
return (vgacolor);
}
──────────────────────────────────────────────────────────────────────────
Listing 15-15. The Ega_to_vga() function.
──────────────────────────────────────────────────────────────────────────
/* allcolor.c -- shows _ERESCOLOR 64-color palette */
/* If you load graphics.qlb, no program list is needed.*/
/* Press <g> to advance left palette, <G> to go back. */
/* Press <h> to advance right palette, <H> to go back.*/
/* Press <Esc> to quit. */
#include <stdio.h>
#include <graph.h>
#include <conio.h>
#define MAXCOLORS 64
#define ESC '\033'
long Ega_to_vga(int); /* color value conversion */
main(argc, argv)
int argc;
char *argv[];
{
struct videoconfig vc;
int mode = _ERESCOLOR;
int xmax, ymax;
int c1 = 1;
int c2 = 4;
char left[11];
char right[11];
int lpos, rpos;
char ch;
if (argc > 1)
mode = atoi(argv[1]);
if (_setvideomode(mode) == 0)
{
fprintf(stderr, "%d mode not supported\n", mode);
exit(1);
}
_getvideoconfig(&vc);
_setlogorg(vc.numxpixels / 2, vc.numypixels / 2);
xmax = vc.numxpixels / 2 - 1;
ymax = vc.numypixels / 2 - 1;
lpos = vc.numxpixels / 32 - 5;
rpos = lpos + vc.numxpixels / 16;
_setcolor(1);
_rectangle(_GFILLINTERIOR, -xmax, -ymax, 0, ymax);
_setcolor(4);
_rectangle(_GFILLINTERIOR, 1, -ymax, xmax, ymax);
sprintf(left, "<-G %2d g->", c1);
sprintf(right, "<-H %2d h->", c2);
_settextcolor(6);
_settextposition(0, 0);
_outtext("Press Esc to quit");
_settextposition(24, lpos);
_outtext(left);
_settextposition(24, rpos);
_outtext(right);
while ((ch = getch()) != ESC)
{
switch (ch)
{
case 'g': c1 = (c1 + 1) % MAXCOLORS;
_remappalette(1, Ega_to_vga(c1));
break;
case 'G': c1 = (c1 - 1) % MAXCOLORS;
_remappalette(1, Ega_to_vga(c1));
break;
case 'h': c2 = (c2 + 1) % MAXCOLORS;
_remappalette(4, Ega_to_vga(c2));
break;
case 'H': c2 = (c2 - 1) % MAXCOLORS;
_remappalette(4, Ega_to_vga(c2));
break;
}
sprintf(left, "<-G %2d ->g", c1);
sprintf(right, "<-H %2d ->h", c2);
_settextposition(0, 0);
_outtext("Press Esc to quit");
_settextposition(24, lpos);
_outtext(left);
_settextposition(24, rpos);
_outtext(right);
}
_setvideomode(_DEFAULTMODE);
}
long Ega_to_vga(egacolor)
int egacolor; /* ega color value */
{
static long vgavals[6] = {0x2A0000L, 0x002A00L, 0x00002AL,
0x150000L, 0x001500L, 0x000015L};
long vgacolor = 0L; /* vga color value */
int bit;
for (bit = 0; bit < 6; bit++)
vgacolor += ((egacolor >> bit) &1) * vgavals[bit];
return (vgacolor);
}
──────────────────────────────────────────────────────────────────────────
Listing 15-16. The ALLCOLOR.C program.
In ALLCOLOR.C, the following code does the remapping:
case 'g': c1 = (c1 + 1) % MAXCOLORS;
_remappalette(1, Ega_to_vga(c1));
break;
When g is pressed, the program increments by one the color value (c1) for
the left rectangle. The modulus operator limits the final value to the
range 0 through 63. The Ega_to_vga() function converts the EGA color value
to a VGA color value, which is then used as an argument to
_remappalette(). Notice how fast the colors change using the remapping
approach.
Remapping the Entire Palette
The RINGS.C program (Listing 15-12 beginning on p. 532) demonstrated how
to use the _remapallpalette() function to reassign the 16 default colors
to different palette values. Now let's use the function with all 64 EGA
colors.
First, we must initialize a 64-element array to the VGA color values for
the EGA colors and then initialize a 16-element array to the color values
for a particular palette. Thus, the 64-element array supplies color values
for the palette array, and we can use the palette array with
_remapallpalette() to remap the palette.
To see how this works, let's apply the approach to our MOIRE.C program;
the resulting program is REMOIRE.C (Listing 15-17). After it draws a
pattern, the program begins remapping the colors. Press any key to stop
the remapping; press any key (except Esc) again to restart the program. To
terminate the program, press the Esc key while the program is paused. The
screen shown on the back cover of this book is a sample of the moire
pattern produced by this program.
──────────────────────────────────────────────────────────────────────────
/* remoire.c -- adds palette remapping to moire.c */
#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <graph.h>
#define ESC '\033'
#define MAXCOLORS 64
#define PALCOLORS 16
long Ega_to_vga(int);
main (argc, argv)
int argc;
char *argv[];
{
struct videoconfig vc;
unsigned int col, row;
long colors[MAXCOLORS];
long palette[PALCOLORS];
int index;
int shift = 1;
int firstcol, firstrow, lastrow, lastcol;
int mode = _ERESCOLOR;
if (argc > 1)
mode = atoi(argv[1]);
if (_setvideomode(mode) == 0)
{
printf("Can't do that mode.\n");
exit(1);
}
/* Create array of all 64 color values. */
for (index = 0; index < MAXCOLORS; index++)
colors[index] = Ega_to_vga(index);
/* Create array of 16 palette choices. */
for (index = 0; index < PALCOLORS; index++)
palette[index] = colors[index];
_remapallpalette(palette);
_getvideoconfig(&vc);
firstcol = vc.numxpixels / 5;
firstrow = vc.numypixels / 5;
lastcol = 4 * vc.numxpixels / 5;
lastrow = 4 * vc.numypixels / 5;
for (col = firstcol; col <= lastcol; ++col)
{
for (row = firstrow; row <= lastrow; ++row)
{
_setcolor(((row * row + col * col) / 10)
% vc.numcolors);
_setpixel(col, row);
}
}
_settextposition(1, 1);
_outtext("Press a key to stop or start.");
_settextposition(2, 1);
_outtext("Press Esc while paused to quit.");
do
{
while (!kbhit())
{
/* Set palette array to new color values. */
for (index = 1; index < PALCOLORS; index++)
palette[index] = (colors[(index + shift)
% MAXCOLORS]);
_remapallpalette(palette);
shift++;
}
getch(); /* pause until key is pressed */
} while (getch() != ESC);
_setvideomode(_DEFAULTMODE); /* reset orig. mode */
}
long Ega_to_vga(egacolor)
int egacolor; /* ega color value */
{
static long vgavals[6] = {0x2A0000L, 0x002A00L, 0x00002AL,
0x150000L, 0x001500L, 0x000015L};
long vgacolor = 0L; /* vga color value */
int bit;
for (bit = 0; bit < 6; bit++)
vgacolor += ((egacolor >> bit) &1) * vgavals[bit];
return (vgacolor);
}
──────────────────────────────────────────────────────────────────────────
Listing 15-17. The REMOIRE.C program.
The following code initializes the arrays:
/* Create array of all 64 color values. */
for (index = 0; index < MAXCOLORS; index++)
colors[index] = Ega_to_vga(index);
/* Create array of 16 palette choices. */
for (index = 0; index < PALCOLORS; index++)
palette[index] = colors[index];
The first loop initializes colors[] to the VGA color values. The second
loop sets palette[] to the first 16 EGA colors. Note that this is not the
default EGA palette. (See Table 15-6 on p. 530.)
The following code reassigns the colors to palette[]:
do
{
while (!kbhit())
{
/* Set palette array to new color values. */
for (index = 1; index < PALCOLORS; index++)
palette[index] = (colors[(index + shift) % MAXCOLORS]);
_remapallpalette(palette);
shift++;
}
getch(); /* pause until key is pressed */
} while (getch() != ESC);
Because shift is initialized to 1, the first pass through this loop sets
the palette array to the second through seventeenth members of colors[].
When shift is incremented, the next pass moves the palette one element
further into colors[].
This example showcases the speed of remapping compared to the time the
program originally took to color the screen.
VGA Graphics
Now let's look at some programs that exclusively use VGA features. The VGA
supports all EGA modes while providing three additional modes. Modes 17
and 18 offer even a higher resolution (640 by 480 pixels) than does EGA
mode 16. Mode 17 uses a 2-color palette, and mode 18 uses a 16-color
palette. Mode 19 uses only medium resolution (320 by 200 pixels) but
offers a 256-color palette. Furthermore, you can select the colors used in
these modes from 262,144 color values. By choosing a suitable palette, you
can construct images with much more realistic shadings of color than you
can get from the CGA or EGA palettes. (The MCGA supports only mode 19 of
these three.)
To use these modes, you need the proper video display controller (VGA,
MCGA, or a clone) and the proper monitor. Unlike the CGA and EGA adapters,
which control display colors with digital signals, the VGA and MCGA use
analog signals. Thus, they cannot be used with CGA or EGA display
monitors. (The popular multisync monitors, however, can handle both
digital and analog signals and can be used with all these adapters. Some
automatically switch between digital and analog modes; others require you
to manually set a switch.)
The 256-Color Palette
The 256-color palette makes mode 19 the most interesting of the new VGA
modes, and because both the MCGA and the VGA support it, mode 19 is also
the most general. The COL256.C program (Listing 15-18) displays the
256-color default palette. It draws a rectangular border and then uses
_moveto() and _lineto() to divide the screen into a 16-by-16 array of
rectangles. Finally, the program uses _floodfill() to display all 256
colors. The expression:
_setcolor(row * ROWS + col);
in the final nested for loop sets the palette value (the argument to
_setcolor()) to 0 through 255 in turn──the full range of palette values in
this mode.
──────────────────────────────────────────────────────────────────────────
/* col256.c -- shows 256 colors in mode 19 */
/* If you load graphics.qlb, no program list is needed. */
#include <stdio.h>
#include <graph.h>
#include <conio.h>
#define ESC '\033'
#define ROWS 16
#define COLS 16
main()
{
struct videoconfig vc;
int mode = _MRES256COLOR;
short xmax, ymax; /* screen size */
short xcs[ROWS][COLS]; /* coordinates of the */
short ycs[ROWS][COLS]; /* 256 rectangles */
short row, col;
if (_setvideomode(mode) == 0)
{
fprintf(stderr, "%d mode not supported\n", mode);
exit(1);
}
_getvideoconfig(&vc);
xmax = vc.numxpixels - 1;
ymax = vc.numypixels - 1;
/* Compute an interior point for each rectangle. */
for (col = 0; col < COLS; col++)
for (row = 0; row < ROWS; row++)
{
xcs[row][col] = col * xmax / COLS + 5;
ycs[row][col] = row * ymax / ROWS + 5;
}
/* draw outside boundary */
_setcolor(1);
_rectangle(_GBORDER, 0, 0, xmax, ymax);
/* draw gridwork */
for (col = 1; col < COLS ; col++)
{
_moveto(col * (xmax + 1) / COLS, 0);
_lineto(col * (xmax + 1) / COLS, ymax);
}
for (row = 1; row < ROWS; row++)
{
_moveto(0, row * (ymax + 1) / ROWS);
_lineto(xmax, row * (ymax + 1) / ROWS);
}
/* fill in rectangles with palette colors */
for (col = 0; col < COLS; col++)
for (row = 0; row < ROWS; row++)
{
_setcolor(row * ROWS + col);
_floodfill(xcs[row][col], ycs[row][col], 1);
}
/* terminate program */
getch();
_setvideomode(_DEFAULTMODE);
}
──────────────────────────────────────────────────────────────────────────
Listing 15-18. The COL256.C program.
Changing the Palette
You can use the _remappalette() and _remapallpalette() functions to change
the palette settings. Of course, you must use the VGA color value system
to do so. Recall that the color value is represented by a 4-byte number.
The low byte represents the red intensity level and can have any value
from 0 through 63. Similarly, the next byte describes 64 levels of green
intensity, and the third byte describes 64 levels of blue intensity.
Therefore, we can represent any of the 262,144 available colors by the
following form:
colorvalue = blue << 16 | green << 8 | red;
The shift operators use type long values to place the intensity values in
the correct bytes. For example, to generate a color that is "nearly" a
blend of blue at half intensity, green at quarter intensity, and red at
three-quarters intensity, use the following color value assignment:
colorvalue = 32L << 16 | 16L << 8 | 48L
We say "nearly" because, using 63 to represent full intensity, you can't
exactly specify half, quarter, and three-quarter intensities with
integers.
The VGAMAP.C program (Listing 15-19) extends COL256.C to demonstrate the
remapping techniques for mode 19. It starts by showing the default
palette. Pressing a key initializes a 256-element array to a new palette.
(This array is declared externally so that it won't use up stack space.)
The first 64 elements are set to the 64 levels of blue; the second 64 are
set to the green levels; and the third 64 elements are set to the red
levels. The final 64 are set to some of the red-blue (magenta) blends.
(With 64 choices for each, there are 64 x 64, or 4096, red-blue blends.)
These illustrate the shadings possible by varying only one of the three
color components. The program uses _remapallpalette() to reset the palette
to the new color values in this array.
When you press another key, the program uses rand() to generate a randomly
placed rectangle. It then selects random blue, green, and red intensity
levels, constructs a color value from them, and sets the rectangle to that
color. This process continues until you press a key. Any key press (except
the Esc key) now toggles the random remapping on and off. Press the Esc
key to terminate the program. Note that _remappalette() uses a short
argument for the palette value, palval, and a long argument for the color
value, colval.
A color intensity range of 64 levels makes extremely subtle color
variations possible. The ALLVGA.C program (Listing 15-20, beginning on p.
548) modifies the ALLCOLOR.C program (Listing 15-16, beginning on p. 539)
so that you can investigate these color possibilities. Rather than using
one key to step through the 262,144 values, we use one key to control the
blue level, one to control the green level, and one to control the red
level.
──────────────────────────────────────────────────────────────────────────
/* vgamap.c -- remaps the vga mode 19 palette */
/* Program list: vgamap.c (for rand()) */
#include <stdio.h>
#include <graph.h>
#include <conio.h>
#define ESC '\033'
#define PALSIZE 256
#define ROWS 16
#define COLS 16
#define MIDBLUE 0x190000L
long newpal[PALSIZE]; /* array of color values */
main()
{
struct videoconfig vc;
int mode = _MRES256COLOR;
short xmax, ymax;
short xcs[ROWS][COLS];
short ycs[ROWS][COLS];
short row, col;
long colorval; /* VGA color value */
long index; /* looping index */
short palval; /* palette value */
int c_base; /* color base -- blue, green, or red */
int ch;
if (_setvideomode(mode) == 0)
{
fprintf(stderr, "%d mode not supported\n", mode);
exit(1);
}
_getvideoconfig(&vc);
xmax = vc.numxpixels - 1;
ymax = vc.numypixels - 1;
for (col = 0; col < COLS; col++)
for (row = 0; row < ROWS; row++)
{
xcs[row][col] = col * xmax / COLS + 5;
ycs[row][col] = row * ymax / ROWS + 5;
}
_setcolor(1);
_rectangle(_GBORDER, 0, 0, xmax, ymax);
for (col = 1; col < COLS; col++)
{
_moveto(col * (xmax + 1) / COLS, 0);
_lineto(col * (xmax + 1) / COLS, ymax);
}
for (row = 1; row < ROWS; row++)
{
_moveto(0, row * (ymax + 1) / ROWS);
_lineto(xmax, row * (ymax + 1) / ROWS);
}
for (col = 0; col < COLS; col++)
for (row = 0; row < ROWS; row++)
{
_setcolor(row * ROWS + col);
_floodfill(xcs[row][col], ycs[row][col], 1);
}
getch();
/* Initialize newpal[] to 64 shades of blue, 64
shades of green, 64 shades of red, and 64 shades
of magenta. */
for (index = 0; index < 64; index++)
{
newpal[index] = index << 16;
newpal[index + 64] = index << 8;
newpal[index + 128] = index;
newpal[index + 192] = index | MIDBLUE;
}
_remapallpalette(newpal);
getch();
/* Set squares and colors randomly -- ESC
terminates loop, and other keystrokes toggle
it on and off. */
do
{
while (!kbhit())
{
palval = rand() % PALSIZE;
colorval = 0L;
for (c_base = 0; c_base < 3; c_base++)
colorval += ((long) rand() % 64) <<
(c_base * 8);
_remappalette (palval, colorval);
}
ch = getch();
if (ch != ESC)
ch = getch();
} while (ch != ESC);
_setvideomode(_DEFAULTMODE);
}
──────────────────────────────────────────────────────────────────────────
Listing 15-19. The VGAMAP.C program.
──────────────────────────────────────────────────────────────────────────
/* allvga.c -- shows _MRES256COLOR 256K colors */
/* If you load graphics.qlb, no program list is needed.*/
#include <stdio.h>
#include <stdlib.h>
#include <graph.h>
#include <conio.h>
#define FULLBRIGHT 64
#define ESC '\033'
char label[2][7] = {"ACTIVE", " "};
main(argc, argv)
int argc;
char *argv[];
{
struct videoconfig vc;
int mode = _MRES256COLOR;
int xmax, ymax;
static long colors[2] = {_BLUE, _RED};
char left[11];
char right [11];
int lpos, rpos;
char ch;
unsigned long blue = _BLUE >> 16;
unsigned long green = 0L;
unsigned long red = 0L;
long color;
short palnum = 0;
if (argc > 1)
mode = atoi(argv[1]);
if (_setvideomode(mode) == 0)
{
fprintf(stderr, "%d mode not supported\n", mode);
exit(1);
}
_getvideoconfig(&vc);
_setlogorg(vc.numxpixels / 2, vc.numypixels / 2);
xmax = vc.numxpixels / 2 - 1;
ymax = vc.numypixels / 2 - 1;
lpos = vc.numxpixels / 32 - 5;
rpos = lpos + vc.numxpixels / 16;
_remappalette(2, _RED);
_setcolor(1);
_rectangle(_GFILLINTERIOR, -xmax, -ymax, 0, ymax);
_setcolor(2);
_rectangle(_GFILLINTERIOR, 1, -ymax, xmax, ymax);
sprintf(left, " %6lxH ", colors[0]);
sprintf(right, " %6lxH ", colors[1]);
_settextcolor(6);
_settextposition(1, 1);
_outtext("Press Tab to toggle panels, Esc to quit.");
_settextposition(2, 1);
_outtext("B increases blue level, b decreases it. ");
_settextposition(3, 1);
_outtext("G and g control green, R and r red. ");
_settextposition(24, lpos);
_outtext(left);
_settextposition(24, rpos);
_outtext(right);
_settextposition(5, 7);
_outtext(label[0]);
_settextposition(5, 27);
_outtext(label[1]);
while ((ch = getch()) != ESC)
{
switch (ch)
{
case '\t': _settextposition(5, 27);
_outtext(label[palnum]);
palnum ^= 1;
blue = (colors[palnum] << 16) & 0x3F;
green = (colors[palnum] << 8) & 0x3F;
red = colors[palnum] & 0x3F;
_settextposition(5, 7);
_outtext(label[palnum]);
break;
case 'B': blue = (blue + 1) % FULLBRIGHT;
colors[palnum] = blue << 16 |
green << 8 | red;
_remappalette(palnum + 1, colors[palnum]);
break;
case 'b': blue = (blue - 1) % FULLBRIGHT;
colors[palnum] = blue << 16 |
green << 8 | red;
_remappalette(palnum + 1, colors[palnum]);
break;
case 'G': green = (green + 1) % FULLBRIGHT;
colors[palnum] = blue << 16 |
green << 8 | red;
_remappalette(palnum + 1, colors[palnum]);
break;
case 'g': green = (green - 1) % FULLBRIGHT;
colors[palnum] = blue << 16 |
green << 8 | red;
_remappalette(palnum + 1, colors[palnum]);
break;
case 'R': red = (red + 1) % FULLBRIGHT;
colors[palnum] = blue << 16 |
green << 8 | red;
_remappalette(palnum + 1, colors[palnum]);
break;
case 'r': red = (red - 1) % FULLBRIGHT;
colors[palnum] = blue << 16 |
green << 8 | red;
_remappalette(palnum + 1, colors[palnum]);
break;
}
sprintf(left, " %6lxH ", colors[0]);
sprintf(right, " %6lxH ", colors[1]);
_settextposition(24, lpos);
_outtext(left);
_settextposition(24, rpos);
_outtext(right);
}
_setvideomode(_DEFAULTMODE);
}
──────────────────────────────────────────────────────────────────────────
Listing 15-20. The ALLVGA.C program.
────────────────────────────────────────────────────────────────────────────
Chapter 16 Debugging
Ever since that fateful day in the 1940s when a moth flew into the back of
a computer and caused a vacuum tube to short circuit, programmers have
been beset by bugs. Most errors, however, are not ex machina; they are
caused by programmers themselves.
Myriad are the ways in which a programmer can err. One common type of
program error can be classed as the misuse of symbols. For example, you
can mistype words and operators, misemploy keywords, and jumble syntax.
You detect most of these errors before a program ever runs. Logic errors,
the second major class of errors, are more insidious. These often carry
through to an operational program, producing mysterious behavior.
Fortunately, the situation is not hopeless. The compiler helps detect many
kinds of errors, and the QuickC debugger can actually help trace errors in
logic. In this chapter, we try to increase your awareness of common types
of errors so that you can develop practices that reduce the likelihood of
going astray in the first place. Many of the errors we show might seem
obvious because we present them as the central attractions in short
programs. However, in the context of a large and complex program, these
errors are much more difficult to notice.
Debugging, or the finding of errors, can be a challenging, frustrating,
rewarding, and time-consuming process. It requires a different mind-set
than programming. Programming is an inventive, creative process. Although
debugging can require creativity, it is primarily an investigative
process. You must transform yourself from a designer into a sleuth.
Keyboard-Entry Errors
With each line of code you enter from the keyboard, you run the risk of
mistyping a word and creating an error. Fortunately, the compiler detects
most of these errors.
The MISIDENT.C program (Listing 16-1) is an error-laden program. When you
compile it, QuickC returns the following message:
Fatal error C1021: (1 of 1)
bad preprocessor command 'defne'
If you use QuickC's integrated environment, the compiler places the screen
cursor on the line containing this error. If you run QuickC with QCL
instead of QC, the compiler labels the error with a line number.
What does this error message mean? When you compile a program, QuickC
first runs a preprocessor to process the # statements. The preprocessor
recognizes a limited set of directives, and #defne is not one of them.
Note that compilation stops immediately──before it checks for program
errors.
To correct the error, replace #defne with #define and compile the program
again. This time the compiler generates the error messages on the
following page.
──────────────────────────────────────────────────────────────────────────
/* misident.c -- careless typing */
#defne BIG 3
main()
{
char ltr;
integer num;
num = 2 + BIG;
lrt = 'a';
printf("%c %d\n", ltr, num);
}
──────────────────────────────────────────────────────────────────────────
Listing 16-1. The MISIDENT.C program.
error C2065: (1 of 4)
'integer' undefined
error C2146: (2 of 4)
syntax error: missing ';' before identifier 'num'
error C2065: (3 of 4)
'num' : undefined
error C2065: (4 of 4)
'lrt' : undefined
When QuickC finds several errors, it displays one message at a time, using
the cursor to mark the applicable line. Shift-F3 advances one error
message, and Shift-F4 backs up a message.
This example illustrates one of the virtues of declaring variables.
Because lrt was undeclared, the compiler quickly spots the "typo" as an
error. It also shows how a simple error can produce several different
error messages. The compiler fails to recognize integer as a type;
therefore, it fails to understand that the statement is a type
declaration. It detects a syntax error and views num as undefined.
Defensive Programming
One way to reduce errors such as typing lrt for ltr is to use recognizable
words as identifiers. If you scan through a long program, you can easily
misread lrt for the intended ltr. But if you use a name such as letter,
mistypings such as lerret or lettre are much more likely to catch your
eye.
An Anomalous Example
The compiler does not catch all typos. The BADSIGN.C program (Listing
16-2) is an interesting example of this.
──────────────────────────────────────────────────────────────────────────
/* badsign.c -- uncaught typo */
main()
{
int i;
int j = 1;
for (i = 0; i < 10; i++)
{
j =+ 10; /* transposed += */
printf("%4d ", j);
}
printf("\n");
}
──────────────────────────────────────────────────────────────────────────
Listing 16-2. The BADSIGN.C program.
This program is meant to print the numbers 1, 11, 21, 31, and so on by
incrementing j by 10 during each loop cycle. Instead, it prints the
following:
10 10 10 10 10 10 10 10 10 10
What happens is that the program uses =+ instead of +=. In the early days
of C, the addition assignment operator was written =+, so at that time,
the program would have run as intended. Later, after the switch was made
from =+ to +=, this program might have run correctly but with a compiler
warning message. If the compiler was not tolerant of anachronisms, it
might have rejected the program altogether for using an unknown operator.
QuickC runs it with no complaints about anachronisms or unknown
operators──but the program produces unwanted results. The reason the
program runs is that under the new ANSI standards for C, the + operator is
recognized as a valid unary operator, so QuickC reads the assignment
statement as follows:
j = +10;
That is, assign positive 10 to j. Under the old standard, the + could be
used only as a binary operator to indicate addition. The moral here is:
Don't be certain that the compiler will catch all mistypings.
This particular error was easy to localize because j was printed every
loop cycle. If j were not printed, finding the problem could be more
difficult.
One final point: C, unlike many popular languages, is case sensitive, so
porter, Porter, and PORTER are three distinct identifiers. If you aren't
used to this convention and QuickC rejects what you consider a valid
identifier, check your usage of case.
Syntax Errors
Syntax errors occur when you use valid symbols in an invalid manner.
Because the compiler must recognize and enforce valid syntax, it will
always catch these errors. However, it might not correctly interpret what
you were trying to do.
Misuse of Operators
The POWER.C program (Listing 16-3) might be written by a FORTRAN
programmer who is accustomed to using the exponentiation operator of that
language.
Interestingly enough, QuickC doesn't complain that the FORTRAN
exponentiation operator (**) is unknown. Instead, it returns the following
error message:
error C2100 : (1 of 1)
illegal indirection
As it happens, ** is also a valid operator in C; however, its C meaning is
quite different than the FORTRAN meaning. In C, ** means "the value at the
address pointed to by another address." Thus, it should be applied only to
a quantity that has been declared to be a pointer to a pointer, and the
number 3 fails to satisfy that requirement. The illegal indirection
message indicates that the compiler thinks you used a pointer incorrectly.
──────────────────────────────────────────────────────────────────────────
/* power.c -- attempt to raise to a power */
main()
{
int number;
number = 10**3; /* raise 10 to 3rd power? */
printf("%d\n", number);
}
──────────────────────────────────────────────────────────────────────────
Listing 16-3. The POWER.C program.
A Scrambled Operator
In the CONDITN.C program (Listing 16-4), we reverse the order of the :
and the ? in the conditional operator. The compiler finds the error, but
the error message is not very illuminating:
error C2143
syntax error : missing ';' before ':'
If you follow QuickC's analysis blindly and insert a semicolon before the
colon, you receive the same error message when you compile again:
error C2143
syntax error : missing ';' before ':'
The moral here is: The compiler is much better at detecting syntax errors
than it is at figuring out exactly what went wrong. QuickC is reliable in
locating a syntax error; however, you must take the analysis with a grain
of salt. If the problem doesn't seem to be in the indicated line,
carefully read its immediate neighbors.
──────────────────────────────────────────────────────────────────────────
/* conditn.c -- attempt to use conditional op */
main()
{
int n, m;
n = 2;
m = (n != 2) : 0 ? 1; /* almost right */
printf("%d\n", m);
}
──────────────────────────────────────────────────────────────────────────
Listing 16-4. The CONDITN.C program.
Another Anomalous Example
The human capacity to err far exceeds the capabilities of compilers to
respond helpfully. The DOWHILE.C program (Listing 16-5) provides an
example.
QuickC places the cursor on the VOOOM line and returns the following
message:
error C2061 : (1 of 1)
syntax error: identifier 'printf'
Apparently, the compiler doesn't recognize printf() as the name of a
function. Because the compiler accepted the first printf() without
complaint, this is a puzzling message. Indeed, not one of the lines near
the reported error seems wrong.
The error lies in our misuse of the do while loop. DOWHILE2.C (Listing
16-6) shows the correct version of the command.
──────────────────────────────────────────────────────────────────────────
/* dowhile.c -- misuse of do while loop */
main()
{
int i = 0;
do while (i < 10)
{
printf("Happy Fourth of July!\n");
i++;
}
printf("VOOOM\n");
}
──────────────────────────────────────────────────────────────────────────
Listing 16-5. The DOWHILE.C program.
──────────────────────────────────────────────────────────────────────────
/* dowhile2.c -- ok use of do while loop */
main()
{
int i = 0;
do
{
printf("Happy Fourth of July!\n");
i++;
} while (i < 10) ;
printf("VOOOM\n");
}
──────────────────────────────────────────────────────────────────────────
Listing 16-6. The DOWHILE2.C program.
The first version had no obvious error near the marked line. But sometimes
it is not enough to look at nearby lines. The compiler thinks in terms of
statements, and the entire do while loop counts as a single statement. In
that sense, the error was near the marked line──it was in the immediately
preceding statement, which happened to cover several lines. The moral here
is: If QuickC shows an error in a line following an extended statement and
you don't see a mistake in the marked line, check the syntax of the entire
preceding statement.
Macro Problems
When you use macros, the meaning of an error message might not be obvious.
The compiler preprocessor first replaces the macros with the corresponding
code. Next, it tries to compile the program. Thus, these compiler error
messages refer to the substituted code, not your original code. This can
be confusing, especially if you are using a system macro with which you
are not familiar. The following BADPUTC.C program (Listing 16-7) looks
innocent enough:
──────────────────────────────────────────────────────────────────────────
/* badputc.c -- misuses putc() */
#include <stdio.h>
main()
{
FILE *fp;
int ch;
if ((fp = fopen("junk", "w")) == NULL)
exit(1);
while ((ch = getchar()) != EOF)
putc(fp, ch);
fclose(fp);
}
──────────────────────────────────────────────────────────────────────────
Listing 16-7. The BADPUTC.C program.
However, the compiler places the cursor on the putc() line and delivers
the following messages:
error C2036: (1 of 9)
left of '->_cnt' must have a struct/union type
error C2105: (2 of 9)
'──' needs lvalue
error C2036: (3 of 9)
left of '->_ptr' must have a struct/union type
error C2105: (4 of 9)
'++' needs lvalue
error C2100: (5 of 9)
illegal indirection
warning C4047: (6 of 9)
'argument': different levels of indirection
warning C4024: (7 of 9)
'_flsbuf': different types: parameter 1
warning C4047: (8 of 9)
'=' : different levels of indirection
warning C4024: (9 of 9)
'_flsbuf': different types: parameter 2
These comments don't seem to relate to our code because we don't use
->_cnt and so on. However, putc() is a macro defined in stdio.h, and we
can use the /P option of the QCL compiler-linker to see what a file looks
like after the preprocessor finishes with it.
Here's the proper command line:
qcl /P badput.c
QuickC gives the processed file the same basename as the original, but it
adds a .I extension. Listing 16-8 shows part of that processed file. We
add comments to indicate the location of the getchar() and putc() macros.
──────────────────────────────────────────────────────────────────────────
/* badputc.i -- preprocessed putfile.c */
/* Not shown are all the stdio.h contents that */
/* come at the top of the file. */
main()
{
struct _iobuf *fp;
int ch;
if ((fp = fopen("junk", "w")) == 0)
exit(1);
/* original was while ((ch = getchar()) != EOF) */
while ((ch = (--((&_iob[0]))->_cnt >= 0 ? 0xff &
*((&_iob[0]))->_ptr++ : _filbuf((&_iob[0]))))
!= (-1))
/* original was putch(fp, ch); */
(--(ch)->_cnt >= 0 ? 0xff &
(*(ch)->_ptr++ = (fp)) : _flsbuf((fp),(ch)));
fclose(fp);
}
──────────────────────────────────────────────────────────────────────────
Listing 16-8. The BADPUTC.I file.
──────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────
The getc() Macro
The previous getc() and putc() macros might be a little obscure. Let's
take a closer look at one of them to see what it does. The following is
the macro definition for getc():
#define getc(f) (--(f)->_cnt >= 0 ? 0xff & *(f)->_ptr++ : _filbuf(f))
This defines getc() as a conditional expression having the form A ? B : C.
If A is nonzero, the entire expression has the value of the B expression;
if A is zero, the entire expression has the value of the C expression. The
first operand (A) for the conditional expression is as follows:
--(f)->_cnt >= 0
Loosely translated, this means, "Decrement the _cnt member of the
structure pointed to by f, and check if the result is greater than 0."
From the definition, f is the argument to getc() and is a pointer to type
file. Type file, in turn, is defined in stdio.h as a structure:
extern FILE {
char *_ptr;
int _cnt;
char *_base;
char _flag;
char _file;
} _NEAR _CDECL _iob[];
This structure describes the file and the I/O buffer in use. The _cnt
member describes the number of characters left in the buffer. If the
number is greater than zero, then a character remains to be read, and the
entire expression has the value of operand B, or:
0xff & *(f)->_ptr++
The first part (0xff ) is a mask to limit the final value to a byte. The
second part (*(f)->_ptr++) is the value pointed to by the _ptr member of
the structure pointed to by f. The _ptr member points to the current
location in the buffer. Thus, if _cnt indicates that a character remains
to be read, getc() evaluates to the buffer character currently pointed to.
The increment operator then moves the pointer to the next buffer location.
If, however, no characters remain, the entire expression has the value of
the final operand:
_filbuf(f)
This is a "hidden" C function (one that the compiler uses but that is not
part of the public library of C functions); it copies characters from the
file to the buffer, reinitializes _ptr to point to the beginning of the
buffer, and resets _cnt to the number of characters in the buffer. It also
returns the value of the first character in the buffer or of EOF if the
end of file has been reached. Therefore, if the buffer is empty, you can
call this function to refill it.
──────────────────────────────────────────────────────────────────────────
Now that we see the actual code that is passed to the compiler, the
message
error C2036: (1 of 6)
left of '->_cnt' must have a struct/union type
makes sense. The code contains the following expression:
--(ch)->_cnt
Because the macro uses the -> operator with a pointer to a structure, this
code suggests that ch should be a pointer to a structure. It isn't, so the
compiler reports an error. However, note that fp is a pointer to a
structure. If we switch fp with ch in the code, the code makes sense. And
that's our mistake──we should have used putc(ch, fp) rather than putc(fp,
ch).
This example illustrates one of the dangers of macros: They offer no
provision for function prototyping or for checking the types of arguments.
The example also illustrates the usefulness of viewing a file's
preprocessor listing.
Run-Time Errors
The most difficult errors to detect are those that the compiler misses.
These errors can be inadvertent, such as the =+ error, or they can arise
from logical errors in the design of the program.
Function Argument Problems
If you don't use function prototypes, C does not try to match the types
you pass to the expected types. Problems here can produce odd results, as
the SUMNUMS.C program (Listing 16-9) points out.
After the program initializes a to 10.0 and b to 20.0, it passes them to
sums() to be added. The final output is as follows:
sum of 10.0 and 20.0 is 0
Clearly, this is false. To see what went wrong, compile in the Debug mode
and set watch variables for a and x. Select the Trace feature from the
Debug menu and use the F8 key to step through the program. At first, the
value of a is undefined, but after the declaration is executed, variable a
contains the correct value of 10. However, when the sums() function is
entered, x is set to 0. Therefore, the problem must occur in the passing
of arguments.
──────────────────────────────────────────────────────────────────────────
/* sumnums.c -- type mismatch in function arguments */
/* No function prototyping */
int sums();
main()
{
float a = 10.0;
float b = 20.0;
int c;
c = sums(a, b);
printf("sum of %.1f and %.1f is %d\n", a, b, c); ;
}
int sums(x, y)
int x, y;
{
return (x + y);
}
──────────────────────────────────────────────────────────────────────────
Listing 16-9. The SUMNUMS.C program.
Knowing how C passes arguments can provide insight into the nature of this
problem. Consider the call sums(a, b). First, QuickC converts float types
to double when passing them as arguments. On a PC, this means that a and b
are now 8-byte quantities. Next, the arguments are put into a memory area
called the stack. The last argument, b, is pushed onto the stack first.
(See Figure 16-1 on the following page.)
When the called function executes, it reads the values off the stack and
assigns them to its formal parameters. It uses the declared types of the
formal parameters to determine how many bytes to read. And this is where
the problem arises. Because the formal parameters x and y are declared to
be type int, the sums() function reads two bytes off the stack for the
value of x and the next two bytes for the value of y. (It should read
eight bytes for x and eight bytes for y.)
The net result of this process is that the first two bytes of a are
assigned to x and the next two bytes of a are assigned to y. This leaves
four bytes of a and all eight bytes of b unread, as illustrated in Figure
16-2 on p. 565. Consequently, it is not surprising that the function gets
the wrong values from the stack.
b is 4-byte float
┌────────┬────────┬────────┬────────┐
│ │ │ │ 20 │
└────────┴────────┴────────┴────────┘
b is converted to double
┌────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┐
│ │ │ │ │ │ │ │ 20 │
└────────┴────────┴────────┴────────┴────────┴────────┴────────┴────────┘
┌─┌───────┐
│ │ 20 │
│ ├───────┤
│ │ │
│ ├───────┤
│ │ │
│ ├───────┤
│ │ │
│ ├───────┤
B is placed────────►│ │ │
in stack │ ├───────┤
│ │ │
│ ├───────┤
│ │ │
│ ├───────┤
│ │ │
│ ├───────┤
├─│ 10 │
│ ├───────┤
│ │ │
│ ├───────┤
│ │ │
│ ├───────┤
│ │ │
Then a goes────────►│ ├───────┤
through the │ │ │
same process │ ├───────┤
│ │ │
│ ├───────┤
│ │ │
│ ├───────┤
│ │ │
└─└───────┘
Figure 16-1. Passing arguments by means of the stack.
┌─┌───────┐
│ │ 20 │
│ ├───────┤
│ │ │
│ ├───────┤
│ │ │
│ ├───────┤
│ │ │
│ ├───────┤
b────►│ │ │
│ ├───────┤
│ │ │
│ ├───────┤
│ │ │
│ ├───────┤
Function call places │ │ │
double arguments on │ ├───────┤
stack ├─│ 10 │
│ ├───────┤
│ │ │
│ ├───────┤
│ │ │
│ ├───────┤
│ │ │
a────►│ ├───────┤─┐
│ │ │ │
│ ├───────┤ ├───►y
│ │ │ │
│ ├───────┤─┤ Called function removes
│ │ │ │ values from stack
│ ├───────┤ ├───►x
│ │ │ │
└─└───────┘─┘
Figure 16-2. Misaligned data.
Function Prototyping to the Rescue
The SUMNUMS2.C program (Listing 16-10) shows what happens when we add
function prototyping to the previous example.
──────────────────────────────────────────────────────────────────────────
/* sumnums2.c -- type mismatch in function arguments */
/* Function prototyping */
int sums(int, int);
main()
{
float a = 10.0;
float b = 20.0;
int c;
c = sums(a, b);
printf("sum of %.1f and %.1f is %d\n", a, b, c);
}
int sums(x, y)
int x, y;
{
return (x + y);
}
──────────────────────────────────────────────────────────────────────────
Listing 16-10. The SUMNUMS2.C program.
The program now generates the correct output:
sum of 10.0 and 20.0 is 30
With function prototyping, a and b are converted to type int before going
onto the stack, and sums() can retrieve the correct values. (See Figure
16-3.)
┌─┌───────┐─┐
│ │ 20 │ │
b──►│ ├───────┤ ├──►y
Function call │ │ │ │ Called function removes
places int argument ├─├───────┤─┤ int values from stack
on stack │ │ 10 │ │
a──►│ ├───────┤ ├──►x
│ │ │ │
└─└───────┘─┘
Figure 16-3. Passing arguments with function prototyping.
This is a convenient feature, but it can lead to another kind of error by
not alerting you to a type mismatch. After all, the fact that you are
passing the wrong type of argument can indicate a programming error. Or
you may lose part of the data when converting from float to int. To use
function prototyping without having to worry about missed errors, you
merely need to select a higher warning level.
Warning Levels
QuickC issues both error messages and warnings. Errors are mistakes that
prevent compilation. Warnings alert you to usages that might be wrong but
which don't prevent compilation. QuickC maintains four warning levels.
Level 0 informs you only of errors. Level 1, the default, displays warning
messages for those conditions most likely to cause problems, such as
mixing levels of indirection in a pointer expression. Level 2 displays
more warning messages, adding situations that are not usually serious
problems, such as failing to declare as void a function without a return
value. Level 3 warns of everything, including such "nonerrors" as non-ANSI
keywords and omitted function prototypes.
To see the effect of these warning levels, compile SUMNUMS2.C using all
four levels. Levels 0 and 1 produce no warnings. Level 2, however,
produces the following list of warnings:
warning C4051: (1 of 6)
data conversion
warning C4051: (2 of 6)
data conversion
warning C4051: (3 of 6)
data conversion
warning C4051: (4 of 6)
data conversion
warning C4016: (5 of 6)
'printf' : no function return type, using 'int' as default
warning C4035: (6 of 6)
'main' : no return value
The first two warnings refer to the declarations for a and b. The reason
for the warning is that C floating-point constants are always type double,
yet a and b are declared type float. Because float uses fewer bytes, the
potential exists for a partial loss of data.
The second two warnings refer to the two conversions that take place in
the sums() function call as a result of function prototyping. Because the
values change from float to int, there is the possibility of a partial
loss of data.
The fifth warning tells us that we haven't declared the return type for
the printf() function. Fix this by including the stdio.h file.
The final warning informs us that main() has no return value. Because we
did not explicitly declare a type for main(), the compiler assumes that
main() is type int. Therefore, it expects main() to have an integer return
value. To avoid this warning, declare main() as type void or provide a
return value at the end of the function.
Warning level 3 offers two new warnings, numbers 1 and 7:
warning C4103: (1 of 8)
'main' : function definition used as prototype
warning C4051: (2 of 8)
data conversion
warning C4051: (3 of 8)
data conversion
warning C4051: (4 of 8)
data conversion
warning C4051: (5 of 8)
data conversion
warning C4016: (6 of 8)
'printf' : no function return type, using 'int' as default
warning C4071: (7 of 8)
'printf' : no function prototype given
warning C4035: (8 of 8)
'main' : no return value
The two new warnings lament the lack of function prototypes for the main()
and printf() functions. If you want to check for the presence of function
prototypes, go to warning level 3. To see if prototype conversions risk
losing data or precision, level 2 is sufficient.
Common Run-Time Errors
Some programming errors arise from the design of the C language itself.
After we discuss how to avoid such errors in simple contexts, we will
demonstrate how to detect them in more complex contexts.
The Misleading If Statement
We begin with an error that almost every C programmer has made more than
once. The LINE_CNT.C program (Listing 16-11) counts lines of input. It
reads each character to EOF, which is signaled by Ctrl-Z with the
getchar() function. A newline character increments the line count (lines).
──────────────────────────────────────────────────────────────────────────
/* line_cnt.c -- an overly active line counter */
#include <stdio.h>
main()
{
int ch;
int lines = 0;
while ((ch = getchar()) != EOF)
if (ch = '\n')
lines++;
printf("There were %d lines\n", lines);
}
──────────────────────────────────────────────────────────────────────────
Listing 16-11. The LINE_CNT.C program.
Following is a sample run:
Cat <Enter>
Hat <Enter>
Bat <Enter>
Ctrl-Z <Enter>
There were 12 lines
The problem is that we used the assignment operator (=) instead of the
is-equal-to relational operator (==). Consider this statement:
if (ch = '\n')
lines++;
This assigns the character \n to ch, giving it the numeric value of 10
(the corresponding ASCII code). The entire expression ch = '\n' now has
the value 10, which, being nonzero, is interpreted as being true. Thus,
lines is incremented for every character, regardless of value. Because
this is a legal construction, QuickC returns no syntax error message.
Knowing about this potential error is not enough to protect you from
occasionally making it, especially if you are a Pascal programmer
accustomed to using = for comparison. It is not an eye-catching error.
How, then, can we detect it in a large program listing?
The telltale sign is an if branch or while loop that always executes, even
when you think it shouldn't. The use of = instead of == isn't the only
possible cause, but it is the first you should look for.
The Debug facility can help find this type of bug. Suppose we didn't know
what was wrong with Listing 16-11. We could select the Debug option, set
a watch on ch, and use the c modifier (separated from ch by a comma) to
display the value of ch as an ASCII character.
As we first trace through the program, ch is undefined. After it is
declared (but not initialized), it has a garbage value. Next, if we enter
the word Cat as input, we see ch take the value C as we enter the if
statement. When control goes to the lines++; statement, we see that ch now
is '\n'. Therefore, ch is being assigned a new value, and that tells us to
look for an incorrect assignment statement.
Examining Arrays, Part 1
One of the most common tasks for a for loop is to process the elements of
an array. The INDEXER.C program (Listing 16-12 on the following page) is
a simple example that is meant to initialize three arrays, calculate the
number of elements in the second array, and display its contents.
──────────────────────────────────────────────────────────────────────────
/* indexer.c -- uses indexes to display an array */
#include <stdio.h>
int code1[] = {2, 4, 6, 8};
int code2[] = {1, 3, 7, 9};
int code3[] = {5, 10, 15, 20};
main()
{
int index;
int size = (sizeof code2) / (sizeof (int));
for (index = 1; index <= size; size++)
printf("%3d ", code2[index]);
putchar('\n');
}
──────────────────────────────────────────────────────────────────────────
Listing 16-12. The INDEXER.C program.
Instead, it prints the character 3 until you press Ctrl-Break.
The reason for this failure is that the program increments size instead of
incrementing the index variable. Thus, index remains at 1, while the
comparison limit grows. (Eventually, the loop halts when size exceeds the
maximum int value and becomes negative.) This is a C error that is
inherent in the language. Most languages increment the loop variable for
you, thus preventing you from making the mistake. C, as usual, chooses
flexibility over the more restrictive but trouble-free approach.
If you don't catch this error in the code, however, how can you spot it
later? One way is to monitor the program as it runs. The QuickC Debug
option provides a quick and simple method. If you have problems in a loop,
set a watch on the loop index. Making index a watch variable and tracing
through the above program soon reveals that index does not change, and
that directs your attention to the update portion of the for control
statement.
Examining Arrays, Part 2
Fixing the above size error results in the INDEXER2.C program (Listing
16-13). Running the revised program produces the following ouput:
3 7 9 5
Because the values should be 1, 3, 7, and 9, the program still fails. The
new problem is the array index range. Array numbering starts with 0, not
1. Thus, the correct limits for the loop are as follows:
for (index = 0; index < size; index++)
The original limits print the last three members of the array and the
first element of the array that followed code2 in memory.
──────────────────────────────────────────────────────────────────────────
/* indexer2.c -- uses indexes to display an array */
#include <stdio.h>
int code1[] = {2, 4, 6, 8};
int code2[] = {1, 3, 7, 9};
int code3[] = {5, 10, 15, 20};
main()
{
int index;
int size = (sizeof code2) / (sizeof (int));
/* get number of elements in array */
for (index = 1; index <= size; index++)
printf("%3d ", code2[index]);
putchar('\n');
}
──────────────────────────────────────────────────────────────────────────
Listing 16-13. The INDEXER2.C program.
The index limit error is easy to make, especially if you are accustomed to
languages such as BASIC and FORTRAN, which start numbering from 1. This
error is difficult to detect because C does not check for bounds errors.
(Pascal gives a run-time error message when an index becomes too large or
small; C doesn't.) You can use Debug to trace the array index, but that
doesn't help if you forget the proper limits.
You can prevent this error by habitually using a standard form for the for
loop. For example, if an array has size elements, use either
for (index = 0; index < size; index++) /* OK */
or
for (index = 0; index <= size - 1; index++) /* OK */
but don't alternate between the two forms. If you do, you increase your
chances of producing an incorrect hybrid form, such as the following:
for (index = 0; index <= size; index++) /* NO */
Mirror Words
The BACKWARD.C program (Listing 16-14 on the following page) is designed
to print a word in normal order and then reverse the order of the letters.
A quick look at the program suggests that its output should be as follows:
trap backwards is part
And, indeed, the program prints that out. But it doesn't stop there. It
keeps on printing. Some of the output is garbage, some appears to be words
and phrases spelled backward. What went wrong?
──────────────────────────────────────────────────────────────────────────
/* backward.c -- the backwards word displayer */
#include <stdio.h>
#define SIZE 5
char word[SIZE] = "trap";
main()
{
unsigned int index;
printf("%s backwards is ", word);
for (index = SIZE - 2; index >= 0; index--)
putchar(word[index]);
putchar('\n');
}
──────────────────────────────────────────────────────────────────────────
Listing 16-14. The BACKWARD.C program.
The first things to check when a loop unexpectedly becomes infinite are
the loop's control statements. In this case, they look reasonable:
(index = SIZE - 2; index >= 0; index--)
SIZE is the number of elements, so SIZE - 1 is the index of the
terminating null character, and SIZE - 2 is the index of the final
character in the word. Each loop decrements the index by 1, thus moving
back a character. When index reaches 0, the loop should display the last
character and stop. But it doesn't. In this loop, the obvious variable to
examine is index. Compile the program in the Debug mode, set a watch on
index, and trace through the program step by step. The value of index
follows this sequence: 3, 2, 1, 0, 65535, 65534, and so on. The index
variable never becomes less than 0, so the loop never ends. Why not?
Because index is declared an unsigned int. Change the type to int, and the
program works properly. What at first looked like a looping error turns
out to be a type error.
The moral here is: Be careful when comparing unsigned quantities to zero.
For example, if index is unsigned, index >= 0 is always true, and index <
0 is always false. But index <= 0 can be either true or false.
Operator Priorities
Because C has so many operators, you might at first have difficulty in
remembering all the operator priorities. Certainly, multiplication has a
higher priority than addition, but how do the increment operator and the
indirect value operators compare? Does *ps++ mean (*ps)++ (use the
pointed-to value and then increment the value) or *(ps++) (use the
pointed-to value and then increment the pointer)? The second choice is the
correct one; if you think the first interpretation is correct, you won't
get the results you expect.
For another example of priorities, consider the binary shift operator used
in the SHIFTADD.C program (Listing 16-15).
──────────────────────────────────────────────────────────────────────────
/* shiftadd.c -- shifts and adds numbers */
main()
{
int x = 0x12;
int y;
y = x << 8 + 2;
printf("y is 0x%x\n", y);
}
──────────────────────────────────────────────────────────────────────────
Listing 16-15. The SHIFTADD.C program.
From appearances, this program should left-shift x 8 places and then add
2. Because each hex digit represents four binary digits, a left-shift of 8
in binary is a 2-digit shift left in hex, so x << 8 is 0x1200. Adding 2
gives 0x1202. But when you run the program, it prints a value of 0x4800.
Addition has a higher priority than shifting; therefore, QuickC interprets
the code as: Add 2 to 8 and do a 10-bit left-shift.
You can find this type of error by using the Debug mode: Trace the values
as they are calculated and compare them to calculations you perform by
hand or with the assert() macro we discuss later.
The best way to solve the problem is to avoid it in the first place. If
you're not sure of priorities, look them up. The QuickC Help menu provides
quick access to this information. Another method of avoiding the problem
is always to use parentheses to clarify your intent, as follows:
(x << 8) + 2
Scanning Problems
The IBMIQ.C program (Listing 16-16) reveals one of the most common C
errors.
──────────────────────────────────────────────────────────────────────────
/* ibmiq.c -- a short dialogue */
#include <stdio.h>
main()
{
char name[80];
int iq;
printf("Enter your first name: -> ");
scanf("%s", name);
printf("Enter your IQ: -> ");
scanf("%d", iq);
printf("Well, %s, my IQ is %d!", name, 2 * iq - 1);
}
──────────────────────────────────────────────────────────────────────────
Listing 16-16. The IBMIQ.C program.
The following is a sample dialogue produced by the program:
Enter your first name: -> Mortimer
Enter your IQ: -> 88
Well, Mortimer, my IQ is -1!
run-time error R6001
-null pointer assignment
Notice that the computer claims an IQ of -1 rather than the 175 you might
expect from glancing at the program. Also, QuickC does not issue the
run-time error message until after the program ends. These errors occur
because in the second scanf() call, the program uses iq rather than the
correct &iq.
Most programmers make this kind of error through carelessness. For
example, here we just used name as an argument. And because name didn't
need the & operator, it's easy to forget that iq does.
Let's look at the mechanics of this error and at the error message itself.
The following sequence of events occurs. First, iq starts with a value of
0. (We know this because that's the value that produces -1 for the machine
IQ. You can also use the Debug mode to verify the iq value.) The scanf()
function interprets 0 as the address 0; that is, it thinks it received a
pointer to NULL. It then places the number 88 at that address rather than
in iq. (See Figure 16-4.) Therefore, iq remains 0, and the program
calculates the machine IQ as -1. Then the program ends. However, the C
null pointer never points to valid data. To be sure of this, QuickC sets
aside a "null block" which is to be untouched. QuickC also includes
postmortem code in each program that checks the null block to see if it
has been altered. If it has, something is wrong, and QuickC displays the
null pointer error message.
Note that the postmortem code doesn't detect data written to non-null
locations. For example, if we initialize iq to 200, the program still
fails to work properly, but, because address 200 is outside the null
block, QuickC returns no error message. Thus, in general, you might not
detect this kind of error unless you notice suspect data in the output.
Unfortunately, this error can also overwrite legitimate data.
Function prototyping won't help here. If you try an error level of 2 or 3,
the compiler still doesn't complain about using a nonpointer as an
argument. Looking at the function prototype in stdio.h shows us why:
int _CDECL scanf(const char *, ...);
The scanf() function is one of those rare functions that take a variable
number of arguments. The first argument is always a string, but the others
can be different types. Therefore, the prototype doesn't limit the nature
and number of the remaining arguments: It type-checks only the first
argument.
(The _CDECL macro is set to cdecl when Microsoft enhancements are in
effect. This keyword ensures that C functions use C calling conventions
even if the /Gc compiler flag is set. That flag enables the Pascal and
FORTRAN calling sequence for programs using mixed-language modules.)
scanf ("%s", &iq) is the same as scanf
("%s", 3400), so an input of 88 is
placed in location 3400. ADDRESS
┌───────┐
scanf ("%s", &iq)────►│ 88 │ 3400
├───────┤
│ │
├───────┤
│ │
├───────┤
│ │
├───────┤
│ │
└───────┘
scanf ("%s", iq) is the same as scanf
("%s", 0), so an input of 88 is ┌───────┐
placed in location 0. │ │
├───────┤
│ │
├───────┤
scanf ("%s", iq)─────►│ 88 │ 0000
└───────┘
Figure 16-4. Entering forbidden territory.
References
The BADREF.C program (Listing 16-17) provides an example of a more subtle
pointer error.
──────────────────────────────────────────────────────────────────────────
/* badref.c -- misuses a pointer */
main()
{
char name[81];
char *pt_ch;
printf("Enter your first name: -> ");
scanf("%s", name);
*pt_ch = name[1];
printf("The second letter of your name is %c\n",
*pt_ch);
}
──────────────────────────────────────────────────────────────────────────
Listing 16-17. The BADREF.C program.
The following is a sample run of the program:
Enter your first name: -> Fillmore
The second letter of your name is i
run-time error R6001
-null pointer assignment
This program gives the correct answer, but it also produces the null
pointer error message, which means that data has been written in the null
block. The program uses two pointers to assign data. First, in the call
scanf("%s", name);
the name variable is a pointer. This variable isn't a pointer to the null
block, however, because the compiler allocates the space for name.
Now look at the following assignment:
*pt_ch = name[1];
This tells QuickC to copy the contents of name to the location pointed to
by pt_ch. The program allocates space for the pointer pt_ch, but it never
specifies where pt_ch points. Therefore, pt_ch becomes an "unreferenced"
pointer──it doesn't refer to a known location. As in the last example,
because pt_ch has an initial value of 0, the data is copied to the null
block.
The unreferenced pointer is an insidious error. If such a pointer is
initialized to a value that doesn't point to the null block, the
postmortem program will not detect it. However, there is an effective
method for preventing this kind of error. When you compile a program,
enable the "Pointer Check" feature in the Compile dialog box. This adds
code to your program that checks to see if each pointer is referenced
before it is used. Recompile BADREF.C using this option. Following is a
sample run:
Enter your first name: -> Fillmore
run-time error R6012
-Illegal Near pointer assignment
This time, instead of generating incorrect results, the program halts when
it attempts to use the unreferenced pointer. If you compiled in the Debug
mode, when you return to the editing screen, the cursor marks the guilty
line of code.
Obviously, the Pointer Check mode makes for safer programs. Unfortunately,
it slows program execution, thus sacrificing one of C's strengths.
Typically, you use this mode only when you suspect pointer problems (such
as when you receive a null pointer warning). Once you rectify the problem
by initializing the pointer to point to previously allocated memory, turn
off the pointer check and recompile.
Design Errors
So far we have focused on the misuse of the C language. However, you can
write programs that use nothing but valid C statements but that fail
because of design errors. The DIGSUM.C program (Listing 16-18), for
example, contains a design flaw.
The program examines input. Characters that are less than 0 and greater
than 9 are considered nondigits, and the program lists them as others; the
program lists values within that range as digits. Following is a sample
dialogue:
2b
4c
Ctrl-Z
digits = 6, others = 0
The program counts all input, including the newlines, as digits. The
problem lies in the following test condition:
if (ch <= '0' && ch >= '9')
The expression is always false because it asks if ch is simultaneously
less than '0' and greater than '9', which is impossible. You must use the
logical OR operator (||) to express the test condition properly.
Other than its flaw in logic, this program contains no C errors, and
QuickC compiles it with no objections. The error becomes apparent when you
analyze the output.
You can ferret out this kind of logic error (expressing a test condition
incorrectly) by using Debug to trace the program step by step. When you
see that the program takes the wrong branch each time, you know to inspect
the test condition.
──────────────────────────────────────────────────────────────────────────
/* digsum.c -- sums digits in input */
#include <stdio.h>
main()
{
int ch;
int digits = 0; /* number of digits in input */
int others = 0; /* number of nondigits in input */
while ((ch = getchar()) != EOF)
if (ch <= '0' && ch >= '9')
others++;
else
digits++;
printf("digits = %d, others = %d", digits, others);
}
──────────────────────────────────────────────────────────────────────────
Listing 16-18. The DIGSUM.C program.
Small Programs
Often, with small programs, you can locate errors merely by examining the
code, especially if the behavior of the program suggests where to look. By
becoming familiar with common errors and the effects they produce, you
will learn to detect them quickly.
If a simple inspection fails to turn up the error, don't spend a lot of
time poring over the code. Instead, gather more evidence. First, trace the
values of key variables as a program runs. This gives you an inside view
of program execution and can help uncover clues that let you deduce what
is wrong. Traditionally, C programmers do this by embedding printf()
statements at key locations. However, you do not need to use this method──
QuickC's Debug mode is a much quicker and more powerful technique.
If your problem seems array related, check your use of indexes. If it's
loop related, check your loop conditions, paying close attention to the
starting and ending values.
If pointers are involved, check to see if they are referenced properly.
You should always assign a pointer an address before using it with the
indirect value operator (*).
Test conditions that combine two or more relationships are breeding
grounds for errors, and you should put them high on your priority list of
items to be checked.
Large Programs
The difficulty of finding errors rapidly increases with program size. The
Debug mode lets you watch the progress of several variables, but a large
program can have so many things going on that Debug is of little help.
Also, large programs often use complex interdependencies that make it
difficult for you to visualize details.
The key to debugging large programs is the same as the key to writing
large programs──modularity. If you don't use a modular approach when you
begin developing a large program, you greatly decrease your chances of
debugging the result. Using the modular approach, you break a program into
smaller, more manageable pieces; as a result, you can localize most
problems with only a few debugging techniques.
Stub Functions
The "stub function" debugging technique is based on the top-down method of
programming. You begin debugging by testing the highest level of program
organization. Suppose that your main() function contains the overall
organization of the program and calls upon other functions to handle the
details. These second-level functions, in turn, might subdivide the work
into further functions.
With this method, you replace the second-level functions with simple,
error-free routines called stub functions. For example, suppose one
second-level function takes an array and an array size as arguments and
performs a complex calculation.
Replace it with the following stub function:
void complex_calc(array, size)
double array;
int size;
{
printf("Function complex_calc was called with "
"arguments %u and %d\n", array, size);
}
Do this with all second-level functions to concentrate on how main()
works. Does it perform the proper sequence of steps? Does it pass the
correct arguments? Use the Debug mode to trace the order in which the
statements execute. If main() works properly, replace the stub functions
with the originals one by one until a problem occurs. If necessary, you
can use stubs within stubs.
Drivers
Another debugging method is the bottom-up approach. Here you start with
the most basic functions. However, instead of testing them in the complex
final environment, you use small programs called "drivers" to test the
function.
For convenience, design your driver so that it can feed a variety of
values to the function to be tested. The MATHTEST.C program (Listing
16-19) offers an example of such a driver.
──────────────────────────────────────────────────────────────────────────
/* mathtest.c -- driver for do_math() */
/* Program list: mathtest.c (to link math functions) */
#include <stdio.h>
double do_math(double);
main()
{
double input, result;
printf("Enter a number: ");
while (scanf("%lf", &input) == 1)
{
result = do_math(input);
printf("input = %.2e, result = %.2e\n", input,
result);
printf("Next number (q to quit): ");
}
}
#include <math.h>
double do_math(x)
double x;
{
return (sin(x) * exp(-x));
}
──────────────────────────────────────────────────────────────────────────
Listing 16-19. The MATHTEST.C program.
The following is a sample run:
Enter a number: 0
input = 0.00e+000, result = 0.00e+000
Next number (q to quit): 1
input = 1.00e+000, result = 3.10e-001
Next number (q to quit): -2p
input = -2.00e+000, result = -6.72e+000
Next number (q to quit): q
Actually, any non-numeric input (not just q) causes scanf() to return a
value other than 1 and thus terminates the loop.
Notice that the driver function echoes the input value so that you know
the do_math() function receives the intended value. That is good
programming practice when using scanf() for input, because that function
gives you two opportunities to make mistakes: You can omit the & operator
or use the wrong format specifier. (For example, because input is a double
value, we must use %lf for input rather than %f.)
The assert() Routine
The assert() macro is another tool for locating logic errors in a large
program. Use this macro to test whether certain conditions are in fact
true. The assert() macro takes an expression as an argument. If the
expression is true, the program continues. If it is false, the program
halts and prints a message identifying the file and line number of the
incorrect assertion. The TESTER.C program (Listing 16-20) is a short
example that illustrates how assert() works.
──────────────────────────────────────────────────────────────────────────
/* tester.c -- demonstrates the assert() macro */
/* Program list: tester.c (to link math functions) */
#include <assert.h>
#include <stdio.h>
#include <math.h>
main()
{
float s1 = 3.0;
float s2 = 4.0;
float sumsq;
float hypot;
sumsq = s1*s1 - s2*s2;
assert(sumsq >= 0);
hypot = sqrt(sumsq);
printf("hypotenuse is %.2f\n", hypot);
}
──────────────────────────────────────────────────────────────────────────
Listing 16-20. The TESTER.C program.
The program calculates the hypotenuse of a right triangle by finding the
square root of the sum of the squares of the remaining sides. We
deliberately introduce an error into the calculation so that sumsq
contains a negative value. Because sumsq must always be positive or zero,
we specify that with the assert() statement. When you run the program, it
halts at the asssert() statement and displays the following message:
Assertion failed: sumsq >= 0, file c:\qc\tester.c, line 15
By placing assert() statements at strategic locations in a large program,
you can localize logic errors, which enables you either to find the error
or to use Debug more productively.
One convenient feature of assert() statements is that once you fix your
mistakes, you can recompile and eliminate the statements from the compiled
program without altering your source code. To do so, merely place the
following definition in your program:
#define DEBUG /* turns off assert() macros */
This causes the assert() macro to be defined as a blank.
A Final Word of Advice
QuickC offers you many powerful tools for debugging your programs.
However, the ultimate tool in debugging is your own mind. Become familiar
with common programming errors and study how to detect or prevent them.
Most importantly, write your programs in a structured, modular form so
that you can easily trace or localize the errors.
────────────────────────────────────────────────────────────────────────────
Appendix A Some Resources for C Programmers
Much has been published on C for every level from beginner to expert. The
following is a list of a few books and magazines that we have found to be
useful and that can supplement and extend your grasp of the topics we have
discussed in this book.
Fundamental and Comprehensive Books
Costales, Bryan. C From A To Z. Englewood Cliffs, N.J.: Prentice─Hall,
1985.
Recommended by Allen Holub as "the best introduction to the language that
[he] has seen." One of the few books used in the University of California
at Berkeley's course on C.
Kernighan, Brian W., and Dennis M. Ritchie. The C Programming Language.
Englewood Cliffs, N.J.: Prentice─Hall, 1978.
This classic book established the "K & R standard" for C, which is only
now being superseded by the ANSI draft standard. The writing is terse but
very clear, providing a synopsis of C language features and their typical
use.
LaFore, Robert / The Waite Group. Microsoft C Programming for the IBM.
Indianapolis, Ind.: Howard W. Sams, 1987.
This book is specific to the Microsoft C Compiler and the IBM PC. Its
approach is example driven, with chapters on using C for graphics,
telecommunications, and other applications.
Waite, Mitchell, Stephen Prata, and Donald Martin / The Waite Group. C
Primer Plus. Revised edition. Indianapolis, Ind.: Howard W. Sams, 1987.
This new edition of the world's best-selling introductory primer on C is
fully compatible with UNIX-based C and the Microsoft C Compiler. It covers
the new ANSI standard and C++, a new dialect of C featuring object-
oriented programming. Contains quizzes and exercises.
Books on Advanced Topics
Chesley, Harry, and Mitchell Waite / The Waite Group. Supercharging C with
Assembly Language. Reading, Mass.: Addison─Wesley, 1987.
This book shows you how to determine which parts of a program would
benefit from recoding in assembly language for the ultimate in speed and
efficiency. Includes many specific code examples.
Feuer, Alan. The C Puzzle Book. Englewood Cliffs, N.J.: Prentice─Hall,
1982.
This book poses tricky and interesting questions that challenge your
understanding of the subtle points of C.
Hansen, Augie. Proficient C. Redmond, Wash.: Microsoft Press, 1987.
Insightful advice on solving real problems with C. Written for
advanced-level and intermediate-level programmers.
Jaeschke, Rex. Solutions in C. Reading, Mass.: Addison─Wesley, 1986.
This book covers the finer points of C that programmers must master to
avoid subtle pitfalls in the handling of pointers, structures, and other
elements. For advanced programmers.
Kernighan, Brian W., and P. J. Plauger. Software Tools. Reading, Mass.:
Addison─Wesley, 1976.
A classic and a cornerstone of modern program design. This book shows how
to create versatile software tools that can be combined to solve
programming problems. The language used, RATFOR, can easily be translated
into C. If you are familiar with Pascal, you might want to try their later
book, Software Tools in Pascal. Reading, Mass.: Addison─Wesley, 1981.
Prata, Stephen / The Waite Group. Advanced C Primer ++. Indianapolis,
Ind.: Howard W. Sams, 1986.
This sequel to C Primer Plus provides a complete tutorial that covers
advanced topics, such as I/O operations, memory management, and the use of
assembly language with C. Focuses on the IBM PC family.
Periodicals
Don't neglect the many programmer-oriented magazines that feature C
projects. Among these publications are the following:
Byte. Subscription Dept., P.O. Box 6807, Piscataway, NJ 08855-9940.
This monthly magazine often has articles about the use of C for various
applications and reviews of products of interest to C programmers. Source
code available on its BIX electronic network.
Computer Language. P.O. Box 11333, Des Moines, IA 50347- 1333.
This monthly has many feature articles on C programming and reviews of
commercial C libraries and other add-on products. Source code available on
CompuServe and other bulletin boards.
Dr. Dobb's Journal. P.O. Box 3713, Escondido, CA 92025-9843.
Dr. Dobb's is the hacker's delight with the funny name. Material on C,
assembly language, unusual programming tricks, and so forth. Source code
available on CompuServe.
PC Magazine. P.O. Box 51524, Boulder, CO 80321-1524.
This IBM PC─specific magazine has a languages column that often features C
programs. This monthly magazine also maintains a bulletin board offering
many utilities and other programs.
PC Tech Journal. P.O. Box 52077, Boulder, CO 80321-2077.
This monthly is, as the name suggests, specific to the IBM PC and MS-DOS.
Besides having material on C, it also offers much news and technical
features about MS-DOS, OS/2, and other aspects of the PC programming
environment.
Programmer's Journal. P.O. Box 3000, Department EE, Denville, NJ 07834.
This IBM PC─specific magazine often features C programs. Published
bimonthly.
────────────────────────────────────────────────────────────────────────────
Appendix B Built-in QuickC Functions
If your program has only a single module, you can call any of the standard
library functions without using a program list. These functions, listed
alphabetically in the table on the next page, are part of the file QC.EXE
and are loaded into memory when you start up QuickC. They comprise the
built-in, or core, library functions for QuickC. Function names that begin
with an underline character are non-ANSI functions developed by Microsoft.
Note that this list updates the list published in the Microsoft QuickC
Programmer's Guide by including a few changes made subsequent to the
printing of that manual. See pp. 125─31 of the same manual for information
about compiling multiple-module programs and using program lists.
QuickC's Core Library Functions
──────────────────────────────────────────────────────────────────────────
abort _fmalloc ltoa segread strnset
access _fmsize malloc setbuf strpbrk
atexit fopen memavl setjmp strrchr
atof fprintf memccpy setmode strrev
atoi fputc memchr setvbuf strset
atol fputs memcmp signal strspn
bdos fread _memmax sopen strstr
brk free memmove spawnl _strtime
calloc _freect memset spawnle strtok
chdir fscanf mkdir spawnlp strupr
chmod fseek movedata spawnlpe system
clearerr fstat _msize spawnv tell
close ftell _nfree spawnvpe time
cputs fwrite _nmalloc sprintf tmpfile
creat getch _nmsize sscanf tmpnam
dosexterr getche onexit stackavail tolower
_dos_read getcwd open strcat toupper
_dos_write _getdate printf strchr tzset
eof getenv putch strcmp ultoa
_exit gets puts strcmpi ungetc
exit gettime raise strcpy unlink
_expand int86 read strcspn vfprintf
fclose int86x realloc _strdate vprintf
fflush intdosx remove strdup vsprintf
_ffree isatty rewind stricmp write
fgets itoa rmdir strlwr
filelength kbhit rmtmp strncat
flushall longjmp sbrk strncmp
lseek scanf strncpy
──────────────────────────────────────────────────────────────────────────
Mitchell Waite is President of the Waite Group, a San Francisco,
California-based developer of technical and computer books. He is also an
experienced programmer, fluent in a variety of computer languages. Waite
is a coauthor of MICROSOFT MACINATIONS, published by Microsoft Press, and
of UNIX Primer Plus and C Primer Plus, published by Howard W. Sams.
Stephen Prata, Ph.D., is Professor of Physics and Astronomy at the College
of Marin in Kentfield, California, where he teaches C and UNIX. Prata is
coauthor of several Waite Group books, including UNIX Primer Plus, C
Primer Plus, and UNIX System V Primer, all published by Howard W. Sams.
Bryan Costales is Senior Systems Programmer at EEG Systems Laboratory. He
is the author of C From A to Z, from Prentice-Hall (Simon & Schuster), and
coauthor of UNIX Communications, published by Howard W. Sams.
Harry Henderson is a freelance technical writer and editor. He has edited
and contributed to computer books for the Waite Group, Blackwell
Scientific, and Wadsworth. In addition, he is the editor for Tricks of the
UNIX Masters and The UNIX Bible, Waite Group books published by Howard W.
Sams.
────────────────────────────────────────────────────────────────────────────
Index
Symbols
! (reverse operator) 113, 138
" " (quotation marks)
preprocessor macros and 379
search current directory 101
#. See preprocessor, C/QuickC, directives
# (stringizing operator) 379
% (modulus operator) 75, 144, 168. See also format specifiers
%= (get division remainder, assign) 86 (table)
%[range] directive 272
& (address operator) 58, 82, 225─26, 233, 340, 343, 356─57, 580
& (bitwise AND operator) 218, 220─21
&& (AND operator) 91─92, 144, 373
'\0'. See string(s)
( ) (operator precedence) 77
* (multiplication) 75, 85, 238
* (pointer declaration) 226─27, 261, 578
*= (multiply, assign) 86 (table), 103
+ (addition) 75, 85, 237, 556
++ (increment operator) 87, 95, 109, 236
+= (add, assign) 86 (table), 556
- (subtraction) 75, 85, 237
-- (decrement operator) 87, 257─58
-= (subtract, assign) 86 (table)
-> (pointer to structures, unions) 341, 346, 357, 562
. (dot operator with structures, unions) 334, 353, 357
.. (return to parent directory) 38
/ (division) 75, 85, 238
/= (divide, assign) 86
/* */ (comment lines) 39, 54
: (conditional assignment statements) 129, 557
: (goto labels) 141
; (semicolon) 51, 557
avoid in #define directives 105
in for loops 97, 101, 102
in functions 150
in if statements 124
in Pascal 52
preprocessor macros and 378─79
with switch statements 132
< (relational operator) 89
<< (bitwise unary left-shift operator) 218, 222, 223─24
< > (angle bracket for include files) 99
= (assign values to variables) 59, 85, 335
vs relational == 90, 569
== (relational operator) 89
vs assignment = 90, 569
> (relational operator) 89
>> (bitwise unary right-shift operator) 218, 223─24
>> (redirection) 386
? (conditional assignment statement) 129─31, 557
@ (linking to files) 386
[] (array offsets) 193, 198, 226
^ (ASCII value char) format specifier 73 (table)
^ (bitwise exclusive OR operator) 218, 222, 460, 468
^ (first character in a range) 272─73
{} (main function statements) 40, 51
in for loops 94, 96
with functions 151, 162
with if statements 123─24
| (bitwise OR operator) 218, 221, 310, 460, 471
|| (OR operator) 91─92, 373, 577
structure members 332
~ (bitwise unary inversion operator) 218, 222, 223, 516
A
\a (alert) 67, 68, 151, 266
ACME.C program 275─77
Adapt() function 497
addition 75, 85, 237
address(es) 225─26. See also pointer(s)
assigning, to a pointer 227
functions that return 243─47
operator (&) 58, 82, 225, 233
string 266─67
type casting pointers and 241
Add Watch (Debug menu) 119, 120
ALERT.C program 163
ALLCOLOR.C program 538, 539─40, 546
ALLVGA.C program 546, 548─51
American National Standards Institute (ANSI), C standards 4─5, 14
function declarations and 151
signed/unsigned variables 60
AND operator (&&) 91─92
bitwise 218, 220─21
ANIMATE.C program 110, 111
animation
in CGA graphics mode 524─28
using while loops for character 110─11
ANSI.SYS, keyboard control with 413─20
argc argument to main() 284─86
argv argument to main() 284─86
arithmetic operators 75─82
assignment shortcuts using 85, 86 (table)
with mixed data types 78─81
on pointers 237─38
precedence of 76─78
type casting before evaluating 81─82
array(s) 189─224
advancing offsets of 205
bounds checking 195─98
debugging 569─71
declaring 191─92, 206, 213
dynamic 247─52
functions and 201─4, 209─10, 215
initializing 198─201, 207─9, 214, 264
interchangeability of * amts and amts [] 239─40
large and huge 217
lvalue/rvalue vs names of 240
memory storage of 190
negative subscripting of 216─17
passing pieces of 217
of pointers 253─55
pointers and 233─37
of pointers to structures 346, 347
referencing and using array items 193─95
strings and 280─84
of structures 343─46
three-dimensional (or more) 212─15
two-dimensional 206─12, 281
ARRAY1.C program 192
ARROW.C program 517─18
arrow keys, using with graphics in text mode 462
ASCII character(s) 64, 406─7. See also string(s)
reading scan codes and 426─29
ASCII character set 103, 461
ASCII extended character set 104, 436, 461
ASGNKEY.C program 414, 415
ASIMOV.C program 199
aspect ratios, calculating 524
assembly language, interrupts and registers 422─23
assert() macro 580─81
assert.h file 22 (table)
assignment statements 59
conditional ? 129─31, 557
vs relational operators 90 (table)
shortcut 85─86
atoi() function 500
ATTRIB.C program 457, 458─59
AUTOEXEC.BAT file 27─29, 33
automatic variables 155─57
auto storage class 157
AVGTEMP.C program 84, 85
B
\b (backspace) 266
Backspace key 403
BACKWARD.C program 571, 572
BADPUTC.C program 559, 560
BADPUTC.I file 560, 562
BADREF.C program 575─76
BADSIGN.C program 555─56
BASICA language 4, 15
BASIC language 15, 142
array declarations in 191
assignment vs relational = in 90 (table)
vs C 3─5, 7, 10 (table), 11
character and string input in, vs C 405
control variables for loops in 95
error handling in, vs C 326
file access in, vs C 294
keyboard control with, vs C 419
LEFT$, MID$ and RIGHT$ 391
string functions in, vs C 279─80
variables used in 55, 155
beep() function 163, 166
BIFFRED.C program 268
binary file routine mode 292, 296
for fopen() 297 (table)
getch() and getche() in 402
putch() in 410
BIOS (Basic Input/Output System)
background for IBM 420─21
cursor and screen control with 413 (table), 429─48
character attributes 454─59
library of C functions for 431─46
in ROAMSCRN.C text program 446─48
video input/output interrupts for 429─31
graphics mode and 492─93, 499
using QuickC to access 420─23
int86() function 423─25
interrupt 0x16 425─26
operation of interrupts 421
reading ASCII and scan codes 426─29
bios.h file 422
bit(s) 55, 56
keyboard status 426 (table)
bit fields 361─65
BITOUT.C program 371─72
BITWISE.C program 218─20
bitwise operators 218─24, 238, 361, 523
character attributes and 460
BLANK.C program 488
block input/output 300─303
BOX.C program 215
BREAK.C program 134, 135
breakpoint 120
break statement 132─38
BUBSORT.C program 202─3
BUG.C program 369, 370
BUGS.C program 116─21
Build Program command (Compile menu) 117, 383, 387
bytes 55, 56
C
C:\QC (base directory) 30
C:\TMP (current working directory) 319
calloc() function 248 (table), 250
CARD.C program 334─35
CARD2.C program 337, 338─39
CARD3.C program 341─43
case, upper/lower 58, 556
conversion of 286─89
CCOPY.C program 298─99
CCOPY2.C program 303─4
CGA. See Color Graphics Adapter (CGA)
cgets() function 409, 411─13
CH2000.C program 474
CH2001.C program 474, 475
CHANGE.C program 235─36
CHANGE2.C program 238─39
character(s). See also ASCII character set; string(s)
animating with while 110─11
BASIC vs C input of strings and 405
classification and transformation of 286─89
for loop using 103
graphics box produced by GRAPHBOX.C 107
graphics character set in text mode 461─67
input functions 403 (table), 404─5
manipulation of, in RAM 473 (table)
output functions 410─11
reading/writing 435─38
representation in text mode 451, 452, 453
character attributes 416
BIOS video input/output interrupts and 429─31, 454─59
bitwise operators and 460
manipulation of, in RAM 473 (table)
reading/writing to the screen 435─38
character boxes 452, 453
character constants (' ') 64
character escape sequences 67, 68
character generator 451
char data type 57, 64─65, 175, 205. See also unsigned char data type
strings as arrays of 263, 264
CHARS.C program 64, 65
chdir() function 318 (table), 319
Check Keyboard Buffer interrupt 425─26
CHOOSE.C program 258, 259, 260
circle() function 159
C language. See Microsoft C language; QuickC/C language
Clearscr() function 435
_clearscreen() function 500
clock, system 528
close() function 312
COBOL language, assignment vs relational = 90 (table)
CodeView debugger 116
COL256.C program 544─45
color(s)
in CGA graphics mode
background 498, 501 (table)
basics 500─501
palette 498 (table), 501 (table)
in EGA graphics mode
access to all 534─41
palette 529─34, 541─43
values, for text 456 (table)
VGA graphics mode palette 544─45
changing 546─51
video screen mixing 456
Color Graphics Adapter (CGA) 451
eliminating direct memory access snow 488─89
resolution of 452, 453 (table)
screen memory with 242
video controller for 451 (table)
video mode using 144
Color Graphics Adapter (CGA) graphics mode 497─528
animation with 524─28
changing modes using 499─500
color basics for 500─501
drawing lines using 505─6
drawing rectangles using 507─10
EGA and VGA considerations for 505
_ellipse() function with 510─12
filling figures with _setfillmask() and _floodfill() using 512─16
filling other shapes with 517─19
Graphics Library for 499
graphics palette and background with 498
logical coordinates and 506─7
physical coordinates and 501─4
replicating images using 519─24
color.h include file 481, 483, 498
command interpreter 282, 284
Command Line 299
comment lines 39, 54
compilation, C/QuickC program 25, 26
batch 21
conditional 368, 369─74
to .EXE file 40─41
#pragma instructions in 375─76
steps in 11
Compile command (Run menu) dialog box 41
compiler(s) 4─5, 20
array size supplied by 200
#pragma instructions to 375─76
QC.EXE and QCL.EXE 21
conditional assignment statement 129─31, 557
CONDITN.C program 557
CONFIG.SYS file 27─29, 32─34
expand environment space in 29
conio.h file 22 (table), 402, 409, 485
console input/output functions 409─13
character output functions 410─11
string input/output 411─13
const keyword 105, 192
Continue command (Run menu) 120
CONTINUE.C program 138, 139
continue statement 138─41
CONTROL.C program 269, 270
conventions and style 15─17
CONVERT.C program 83, 84
coordinates, graphics mode screen
logical 506─7
physical 501─4
countline() function 157
cprintf() function 409, 413
The C Programming Language 4
cputs() function 411─13
storage of an array read by 412
cscanf() 413
Ctrl-Break 97, 408
Ctrl-Z 292, 296, 402
ctype.h file 22 (table), 410
character classification in 286, 287 (table)
character transformation in 288 (table)
cube_root() function 175
Cursdn() 468
Cursdn_lim() 468
cursor
movement 438─39
setting 432─33
cursor and screen control 413 (table)
with ANSI.SYS 415─20
with BIOS calls 429─48
Cursrt() function 448
Cursrt_lim() function 438, 439, 448
D
data synchronization 309, 310
data types 55─66. See also bit fields; structure(s); variable(s)
arithmetic with mixed 78─81
char (see char data type)
enum 358─60
float 62─64
format specifiers and 73 (see also format specifiers)
help screen 67
int (see int data type)
long 61─62
type casting 81─82
union (see union data type)
unsigned char 65
using typedef 65─66, 365─66
wrong/incompatible 115
DBLBAR.C program 152, 154
flow control in 153
Debug command (Compile menu) 117, 118, 153
debugging 553─81. See also error(s), correction
controlling from the keyboard 118
with #define 369─70
design errors 577─81
keyboard entry errors 554─56
loops and 115─21
run-time errors 562─68 (see also run-time errors)
syntax errors 556─62
macro problems 559─62
operator misuse 556─59
decisions and branching 123─46
break statements 134─38
complex branching conditions 142─46
conditional assignment statement ? 129─31, 557
continue statement 138─41
goto statement 141─42
if statement 123─28
multipath branching 131─32
switch statement 132─34
#define directive 368 (table)
creation of array stack alias using 198
debugging with 369─70
macros 377
strings 265
vs typedef 365─66
use in nested loops 104─7
vs variables 105
defined keyword, and #ifdef directive 370─72
definition files. See include files
delay() function 164, 166
Delete All/Last Watch command (Debug menu) 120
device-independent programming 453─69
DIALOG.C program 274─75
DIGSUM.C 577
direct.h file 22 (table), 318─19
direct memory access (DMA) 469─80
compatibility of, increasing 474─75
example 472─74
graphics modes and 499
manipulation of characters and attributes in 473 (table)
segmented memory and 469─72
near and far pointers 471─72
segments and offsets 470, 471
storing/displaying a screen using 476─80
directories 20─26, 318─21
base, and subdirectories 20─21, 30
library functions which handle 318 (table)
return to parent (..) 38
search current 101
typical structure of 25
DIRX.C program 320
display. See also screen(s); video monitor(s)
40-by-25 435
80-by-25 424, 435─36, 451
Display() function 497
division 75, 85, 238
DMA. See direct memory access (DMA)
DO.C program 113
do loop 113─15
vs Pascal's repeat until 114
do_math() function 580
do_move() function 186
dos.h file 22 (table), 422, 423
DOS Shell (File menu) 42
DOTS.C program 502, 503─4
modify to create moire patterns 506
modify to speed up 505─6
program notes for 504
double data type 57, 63, 64, 72, 81, 174
double words 55, 56
DOWHILE.C program 558
DOWHILE2.C program 558, 559
Draw_char() function 465, 467
Draw_planes() function 215
driver programs 579─80
E
edit mode 39
Edit Program List command (File menu) 99, 100, 382
EGA. See Enhanced Graphics Adapter (EGA)
egacolor.h 536
Ega_to_vga() function 538, 541
EGGS.C program 510, 511, 512
#elif directive 368 (table), 373
#else and 372
_ellipse() function 510─12
else and if statements 126─28
matching 128
#else directive 368 (table)
#elif and 372
#endif directive 368 (table), 373
and #if 369─70
Enhanced Graphics Adapter (EGA) 505, 528─43
accessing EGA colors with VGA code 534─41
automatic color value conversion 538
defining nonpalette colors 535─37
EGA bit to VGA byte conversion 535 (table)
example 538─41
BIOS routines to control 455
palette of 529─34
color values for 531
default values for 530 (table)
four intensities for blue using 531
remapping with _remapallpalette() 541
setting the 529─34
specifying color values of 529
screen memory and 242
text modes using 489─90
Enter() function 341
Enter key 274
enum data type 358─60
envp argument to main() 285
ERR.C program 374
errno.h file 22 (table)
errno variable 324, 326
error(s)
advanced handling of, in files 325─29
correction 44, 45 (see also debugging)
detection 300
error message warning levels 566─68
philosophies of handling, BASIC vs C 326
error() function 150, 151, 152
escape sequences for printf() 67─70
event-driven programming 419
examine() function 177, 178, 179
.EXE files 24, 25
compiling to 40─41
expo() function 169─71, 175, 178
EXPO.C program 169, 170
expressions, in statements 51
EXTERNAL.C program 159─60
external variables 158─60
extern keyword 246, 390
F
\f (formfeed) 266
factorial() function 173─74, 178
factorial expression, calculation of 172, 173
far pointers 242─43
in direct memory access 471─72
fclose() function 297
fcntl.h file 22 (table), 310
values for oflag declared in 310 (table)
feof() function 300, 301
ferror() function 300, 326
fgetc() function 295─97, 310
fgetpos() function 294
fgets() function 297─99
FIELDS.C program 74, 75
field width specifiers for printf() 74─75
file(s) 20─26, 38, 291─329. See also .EXE files; header files; include
files; make files; map files; program list files
advanced error handling with 325─29
block input/output for 300─303
changing source to object 25, 26
closing 297, 312
determining position in 308─9
dividing programs into smaller 381
error detection with 300
finding current position in 318
formatted input/output of 305
line input/output of 297─99
mid-level unbuffered input/output of 309─18
moving to the beginning of 308
opening 293─94, 310─11
pointers 303─4
positioning 315─17
random access to 305─8
reading 295─97, 314─15
three levels of file input/output 291, 292
top-level buffered input/output 292─309
video input/ouput functions as 439─46
writing to 312─13
FILE.C file 382
file descriptor 310, 312
File menu 37
filenames 321─24
file pointers 303─4, 312
file system 318─25
directories 318─21
manipulating files by name 321─24
printing clear diagnostics 324─25
fill mask 512, 513
float data type 57, 62, 72, 81, 205
function argument problems using 178, 563─64
unions and 352, 356, 357
float.h file 22 (table)
floating-point arithmetic 31, 32, 204
floating-point data types 62─64
FLOATS.C program 62, 63
_floodfill() function
filling figures with 512─16
filling other shapes with 517─19
floppy-disk systems, setting up 32─34
vs hard-disk 34 (table)
FMENU.C program 322─24, 325
fonts, EGA/VGA vs MDA/CGA 489
fopen() function 293─94, 296, 318
possible modes/activities for 293 (table)
text vs binary mode for 296, 297 (table)
for loop 94─107
breaking out of, with Ctrl-Break 97
character processing with 103
combining with while loops 112─13
control variable in 95
multiple initializations/calculations in 101─3
multistatement 97─101
nesting 104─7
style 96─97
FORLOOP.C program 94, 95, 96
FORMATS.C program 73
format specifiers
%c 65, 73
%d 60, 65, 69, 70, 72, 73, 83, 177
%e 63, 72, 73
%f 63, 72, 73, 81, 84, 355, 580
%ld 72, 73
%lf 580
%s 69, 264
%x 72
^ (ASCII value char) 73
compatibility of data types and 73 (table)
specifying with printf() 71─72
FORTRAN language 95, 142
assignment vs relational = 90 (table)
fputs() function 297─99
fread() function 300─303, 309, 478
free() function 248 (table), 249, 250
fscanf() function 305
fseek() function 294, 305─8
origin positions for 305 (table)
fsetpos() function 294
ftell() function 308─9
ftime() function 528
funct() function 328
function(s) and function calls. See also library functions
addresses returned by 243─47
advantage/disadvantage of 153
arrays and 201─4, 209─10, 215
calling itself (see recursion)
calling user-defined 152─53
core library 52, 587─88
debugging argument problems in 562─65
declaring 150─51
definition of 50─53, 150─53
external variables and 158─60
information-returning 168─71
interrupt-accessing 422 (table)
large program using 180─86
local and automatic variables and 154─57
many parameters on 167─68
multiple user-written 164─66
noninteger 175─76
passing information to, with parameters 161─66
passing pointers to 230─33
passing structures to 337─39
pointers to 258─60
program design and 147─50
prototypes for 177─80, 565─66
recursion of 171─75
register variables and 160─61
static variables and 157─58
stub 578─79
unions and 355, 356
of video control registers 487 (table)
video I/O interrupt 0x10, library using 430─31 (table), 431─46
function libraries 5, 9─10
of video input/output interrupt functions 431─46
fwrite() function 300─303, 478
G
GALAX.C program 145─46
General help 42─43
getc() macro 561
getch() function 400─401, 425, 447, 448
console input/output with 409, 410
vs getchar() and getche() 403 (table)
reading extended scan code with 406, 407
typical input uses for 403─5
getchar() function 378, 400─401
buffer 402
vs getche() and getch() 403 (table)
typical input uses for 403─5
Get Character interrupt 425
GETCHAR.C program 400, 401
GETCH.C program 400, 401
getche() function 112, 113, 137, 168, 175, 400─401
console input/output with 409
vs getchar() and getch() 403 (table)
reading extended scan code 406
typical input uses for 403─5
GETCHE.C program 400, 401
GETCLOSE.C program 180─84
main() function in 186
modifying 186
overview of game created by 184─85
Getcurs() function 432─33, 438
getcwd function 318 (table), 319─20
_getimage() function 519
for animation 524
Get Keyboard Status interrupt 426
getmesg() function 420
Getpage() function 433, 434, 480
GETPUT.C program 520─23, 524
gets() function 274─75, 298, 402
_getvideoconfig() function 494─97, 500, 504
Getvmode() function 474, 475
getyn() function 175─76
GETYN.C program 176
global variables 154, 155
structures 351─52
Go_left() function 260
goto statement 141─42
GRAFCHAR.C program 462, 463
drawchar.c module 465─66
drawing with 467
initstuf.c module 463, 464─65
program details/limitations of 467─69
grafchar.h file 462, 463, 476, 477
GRAPHBOX.C program 104─5, 106, 107
graph.h file 22 (table), 142, 143, 493
mode constants from 494
_putimage() action verbs from 519, 520 (table)
video configuration information from 495
graphic(s), text mode
compatibility 460─61
programming with the graphics character set 461─67
GRAPHICS.LIB. See Graphics Library
Graphics Library 23, 31, 142
graphics modes and 493─97, 499
graphics mode and video monitors 491─551
available 491, 492 (table)
BIOS and 492─93, 499
CGA 497─528
EGA 528─43
Graphics Library and 493─97
VGA 544─51
H
hard-disk systems, setting up 30─31, 34
HARDWARE.C program 66
hardware manipulation, C/QuickC 4─5, 7, 8
hardware requirements 14─15
header files 21─23, 387─90
dependencies in 390
variables in 389─90
HELLO.C program 264, 265
help 42─44
general screens 42, 43
arithmetic operator precedence 77
data type 67
topic 42, 43
keyword 44
library function 98
HELP.C program 481─82, 484
HEXOUT.C program 204, 205
hot keys 36 (table)
huge keyword 217
HyperTalk language 11
I
IBMIQ.C program 573, 574─75
IF.C program 124, 125
#ifdef directive 368 (table)
defined keyword 370─72
#if directive 368 (table)
#endif and 369─70
logical operators and 373
nesting and indenting 373
IFELSE.C program 127, 128
#ifndef directive 368 (table)
if statement 123─28, 405
debugging misleading 568─69
else adjunct to 126─28
flowchart 125
matching else to 128
nested 126
switch vs if-else 137─38
using groups of statements with 126
vs while 125─26
_imagesize() function 519
INCDEC.C program 87
Include command (View menu) 98
#include directive 99, 368 (table)
include files 5, 9─10, 21, 22 (table)
compiling/linking with 25, 26
including in programs 99
putting user functions in 166
video input/output functions in 439, 440
increment/decrement operators 87, 95, 109
pre-increment vs post-increment 87─88
used with pointers in an array 236─37
used with pointers to pointers 257, 258
INDEXER.C program 569, 570
INDEXER2.C program 570, 571
indirection operator 228
INFLATE.C program 102, 103
inp() function, reading ports with 485─87
input/output
console 409─13
input with scanf() 82─85, 271─73
keyboard 400─405, 554─56
mid-level unbuffered 309─18
strings 271─80
text lines, with gets() and puts() 274─75
top-level buffered file 292─309
video input/output interrupts 429─46
INPUT statement (BASIC) 405
int86() function 422, 423─25, 426
int data type 57, 60─61, 175, 361─62, 375
array of 190
union and 352, 355, 356, 357
integer constant expression 191, 206, 213, 247
interpreted languages 11─12
interrupts
assembly language, registers and 422─23
functions which access 422 (table)
int86 function 423─25
interrupt 0x10 functions 430─31 (table)
interrupt 0x16 (keyboard I/O) 425─26
operation of 421
reading ASCII and scan codes with 426─29
software 422
INTVARS.C program 60, 61
INVERT.C program 288─89
io.h file 22 (table), 322
K
kbhit() function 109─10, 425
Kernighan, Brian W. 4
keyboard 399─429
accessing the BIOS to control 420─29
ANSI.SYS control of 413─20
console input/output functions 409─13
controlling debugging from 118
input errors 554─56
input functions 400─405
processor and scan codes 406─9
reading non-ASCII keys 406─9
shortcuts 36 (table)
keyboard buffer 406
keyboard macro 419, 420
keyboard status bits 426 (table)
KEYS.C file 381
modifying to use with header file 388, 389
keys.h 408, 409, 447, 462
Keyword help 44
L
L2WORDS.C program 282─83, 284
label 141, 240
large programs 379─95
design problems with 578─81
header files and 387─90
keeping track of changes in 387
libraries and 391─94
program lists and 380─83, 383─87
Quick Libraries for 394─95
leftstr() function 280
LEFTSTR.C subroutine 392, 394
libraries 23─24, 25, 26. See also function libraries; Graphics Library;
Quick Libraries
standard 31
library files (.LIB) 391─94
library functions. See also function(s) and function calls
abnormal condition handlers and diagnostic routines 325 (table)
core 52, 587─88
directory handling 318 (table)
filename manipulation 321 (table)
help window 98
memory allocation 248─52
string manipulation 279 (table)
using in for loops 97─98
for video input/output interrupt 431─46
limits.h 22 (table)
line(s), drawing in CGA graphics mode 505─6
mask 510
styles 508, 510
line() function 152─53, 154
parameters for 161, 162, 167─68
Line2words() function 282
LINE_CNT.C program 568, 569
#line directive 368 (table)
line input/output, with fgets() and fputs() 297─99
LINES.C program 167, 168
LINES43.C program 489, 490
_lineto() function 168, 505, 506, 517, 544
linked lists 346─51
LINK program 21, 385─87, 394
LOCAL.C program 155, 156
local variables 154─57
locking.h file 22 (table)
logical operators 91─92. See also AND operator (&&); OR operator (||)
LOGO language 11
assignment vs relational = 90 (table)
long data type 61─62, 375
longjmp() function 325 (table), 328, 329
loops. See repetition and looping
lseek() function 315─17
lvalue 240, 268
M
machine code optimization 7, 161
Macintosh computer and C 4
macros
debugging 559─62
preprocessor 377─79
MAGIC.C program 207─8
main() function 39─40, 567
arguments to 284─86
flow of execution begins with 53
in GETCLOSE.C 186
vs printf() 51─52
program design and 147─50
variables declared in 156─57
make files 13, 21, 24, 382
malloc() function 248─50, 252, 271, 320, 519
malloc.h file 22 (table), 247
map files 24
MASKS.C program 514─16
MATH.C program 75, 76
math.h file 22 (table), 99, 101, 169
MATHTEST.C program 579, 580
MCGA. See Multi-Color Graphics Array (MCGA)
M.C program 138, 139─40
memory 7, 55, 56
direct access to (see direct memory access)
dynamic allocation/reallocation of 247─52
stack (see stack)
storage of arrays in 190, 205, 206
video (see video memory)
memory allocation library routines 248 (table), 248─52
memory.h file 22 (table)
memory models 7, 8, 23
setting up 31, 32
menu bar 35
MENU.C program 416─18, 420
operation of 419
microprocessors
BIOS, interrupts and 421─23
data types of different 63
#pragma pack 376
registers for 8086 family 160
Microsoft C language. See also QuickC/C language
powerful capabilities of 7─10
programming fundamentals (see
programming in C and QuickC)
QuickC vs 11─14
reasons for learning 3─6
standards 4─5, 14
used with QuickC 30
Microsoft C Optimizing Compiler 14, 20. See also compilation, C/QuickC
program; compiler(s)
Microsoft QuickC Language Reference 19─20
Microsoft QuickC Programmer's Guide 32, 36, 46, 79, 121, 325
Microsoft QuickC Run-Time Library Reference 19─20, 98, 275
MIDSTR.C subroutine 392
MISIDENT.C program 554
MIXED.C program 79, 80
MIXLOOPS.C program 112, 113
MIXTYPES.C program 177, 178
mkdir() function 318 (table), 319
mktemp() function 321 (table), 322
MODEINFO.C program 495, 496─97
modules C 5, 13
MOIRE.C program 506
monitors. See screen(s); video monitor(s)
Monochrome Display Adapter (MDA) 450
attribute bits 454, 455 (table)
resolution 452, 453
video controller for 451 (table)
mouse 14
selections with 36─37
mouse driver 37
_moveto() function 168, 505, 544
MS-DOS 13
out of environment space 29
escaping to 42
interrupts and 421
signals defined for 328 (table)
variables, and C 27─29, 33
Multi-Color Graphics Array (MCGA) 450
video controller 451 (table)
multiplication 75, 85, 238
N
\n (newline) 40, 67, 68, 266, 274, 402, 410
in declarations of string constants 265
vs \r (return) 70
name :bits 362
NARROW.C program 53
near pointers 471
Newchar[] 410
NEW-CONF.SYS 27─29, 32, 33
newline. See \n (newline)
NEW-VARS.BAT 27─29, 32, 33
Next_search() function 260
noninteger functions 175─76
numbers
data types (see double data type; float data type; int data type; long
data type)
formatting with printf() 70─71
O
object (.OBJ) files 24, 384, 391, 394
combined/linked (see .EXE files)
offsets, array 193
advances in memory 205
bounds checking 195─98
consequences of referencing beyond arrays 196
ONELINE.C program 68
ON KEY (in BASIC) 419
open() function 310, 312, 318
Open command (File menu) 12, 37, 38
OPEQUAL.C program 86
operators
arithmetic (see arithmetic operators)
bitwise (see bitwise operators)
increment/decrement (see increment/decrement operators)
logical 91─92 (see also AND operator (&&); OR operator (||))
misuse of 556─59
priority errors with 572─73
relational 89─91
optimization of machine code 7, 161
OR operator (||) 91─92, 577
bitwise (|) 218, 221, 310, 460, 471
bitwise exclusive (^), 218, 222, 460, 468
outp() function, writing to a port with 487─88
outtext() function 512
P
PACK.C program 376
pages/paging 429, 480─84
getting/setting 433─34
parameters, function 161─66
actual vs formal 162
multiple 167─68
multiple user-written 164─66
operation of 163─64
passing, in Pascal and C 164
Pascal language 15
array declarations in 191
assignment and relational = 90 (table)
vs C 3─5, 7, 10 (table)
comment lines in 54
passing parameters in 164
repeat until, vs C's do 114
type checking in 178
use of the semicolon (;) in 52
variables defined in functions using 155
PASSWORD.C program 403, 404, 405
PATH command 28─29
PEEK and POKE (in BASIC) 7
PEEK.C program 241
perror() function 324─25
PHONE.C program 305, 306─8
PHWORD.C program 245─46
pixels 450
generating random 142─46
logical coordinates for 506, 507
physical coordinates for 501, 502, 503─4
PIXELS.C program 142
running 142─44
screen coordinates and output 144
variations of 145─46
pmode (permissions mode), values for 311 (table)
pointer(s) 7, 8, 164. See also address(es)
accessing structure members with 341─43
accessing variables with 228─30
arithmetic for 237─39
arrays and 233─37
arrays of 253─55
arrays of, to structures 346, 347
assigning addresses to 227
casting, to integers 242
debugging errors resulting from use of 575─76
defining/declaring 226─27
direct memory access use of 469─74
dynamic arrays and 247─52
elements not addressed by 257
far 242─43
to functions 258─60
initialized strings and 267─68
interchangeability of *amts and amts[] 239─40
lvalue vs rvalue 240
passing, to functions 230─33
passing, to structures 339─41
to pointers 255─58
type casting addresses and 241
to unions 356─58
value of 234
Pointer Check 237
POINTER.C program 228─29, 229─30
polling loop 109
port(s) 485─89
eliminating CGA snow using 488─89
reading, with inp() 485─87
writing to, with outp() 487─88
portability
of C/QuickC 4─5, 6
text modes and 451─53
PORTINFO.C program 486
postmortem code 574
pow() function 169
POWER.C program 556, 557
#pragma directive 368 (table)
#pragma pack 375, 376
PREPOST.C program 88
preprocessor, C/QuickC 368─79
conditional compilation 368, 369─74
directives 368 (table), 369─73
#pragma instructions to the compiler 375─76
preprocessor macros 377─79
Print_attr() function 457, 463
printf() function 40, 67─75, 558, 567
escape sequences for 67─70
field width specifiers for 74─75
formatting numbers with 70─71
formatting strings with 269─70
relationship to main() 51─52
specifying formats with 71─72 (see also format specifiers)
Print_row() function 217
Printval() function 356
process.h file 22 (table)
program list 13, 23, 142, 143, 380─83
creating a 99─101
program list files 383─87
dependency lines in 384─85
other arguments to LINK 386─87
production rules for 383─84
running the linker with 385
using LINK from a text file 386
programming environment, C/QuickC 12─13
programming in C and QuickC 37─41
addresses (see address(es))
arithmetic operators 75─82
basic elements of 49─53
comments in 39, 54
compilation (see compilation, C/QuickC program)
cursor (see cursor and screen control)
data types and variable declaration 55─66
debugging (see debugging; error(s), correction)
decisions and branching (see decisions and branching)
device-independent (see device-independent programming)
functions (see function(s) and function calls; library functions)
graphics (see graphics mode and video monitors)
keyboard control (see keyboard)
large programs (see large programs)
logical operators 91─92
pointers (see pointer(s))
preprocessor (see preprocessor, C/QuickC)
printf() function 67─75
punctuation and spacing in 53
relational operators in 89─91
repetition and looping in (see repetition and looping)
scanf() input 82─85
shortcut assignment statements, increments, and decrements 85─88
in text mode (see text mode and video monitors)
written resources for 583─85
PROTO.C program 179, 180
prototypes, function 177─80, 565─66
punctuation in C/QuickC programs 53
putc() function 559, 560
putch() function 104, 410─11, 436, 448
putchar() function 410─11
_putimage() function 519─24
for animation 524
puts() function 274─75, 298
action verbs for 520 (table)
Q
QC.EXE program 21
QCHELLO.C program 39
parts of 50
running 40
statements calling functions in 51, 52
as typed into edit window 39
QCL.EXE program 21
QuickBASIC 15
QuickC/C language
C language compared to Pascal and BASIC 3─10 (table), 15
directories and files used by 20─26
error correction 44─45 (see also debugging)
getting help 42─44
hardware requirements 14─15
initial screen 12
knowledge requirements 15
programming fundamentals (see
programming in C and QuickC)
reasons for using 11─14
setting up 29─34
starting/learning to use, 34─42
used with Microsoft C 30
user manual vs this book 19─20
Quick Libraries 325, 394─95
Quit() function 258
R
\r (return) 266, 274, 402, 410
vs \n (newline) 70
RACE.C program 524, 525─28
RAM, memory models of 7, 8, 23, 31, 32
rand() function 134, 168, 524, 528, 546
Range() function 244─47
read() function 309, 310, 314─15
Read Character and Attribute function 431 (table)
Read_ch_atr() function 436, 437
Read Cursor Position interrupt 430 (table)
Readkey() function 426, 427, 428
realloc() function 248 (table), 249, 250, 252
RECALL.C program 478, 479
RECEIPTS.C program 77, 78
_rectangle() function 507─10, 512
rectangles, drawing in CGA graphics mode 507─10
RECT.C program 508, 509, 510
RECURSE.C program 172, 173
recursion 171─75
stack size and 174─75
structure 346─51
register(s) 7, 8
AX 423, 424
AH 423, 425, 426, 430, 435
AL 423, 425, 426, 434, 490
BP 423
BX 423
BH 423, 434
BL 423, 424
CS 423, 470
CX 423
DI 423
DS 423, 470
DX 423
DL 435
ES 423
functions of video control 487 (table)
for Intel 8086 family 160─61
interrupts, assembly language and 422─23
ports and 485
SI 423
SP 423
SS 423
register variables 160─61
REKEY.C program 410, 411
relational operators 89─91
precedence shown by () 91
relational == vs assignment = 90, 569
RELATION.C program 89, 90
_remapallpalette() function 529, 530, 541─43, 546
_remappalette() function 529─30, 538, 546
REMOIRE.C program 541─43
remove() function 321 (table)
rename() function 321 (table)
repetition and looping 93─121
debugging and loops 115─21, 571─72
do loop 113─15
for loop 94─107
incorrectly specified loops 115
while loop 108─13
replication of images on screen 519─24
return. See \r (return)
return statement 168─71
return value 49, 171
REVERSE.C program 253, 254
REVERSE2.C program 256─57
rewind() function 308
Rewrite() function 437, 438, 448
RIGHTSTR.C subroutine 392─93
RINGS.C program 530, 532─34, 541
output 532
Ritchie, Dennis M. 4
rmdir() function 318 (table), 319
ROAMSCRN.C program 446─47
"robust" programs 325
ROLO.C program 344─46
ROLO2.C program 348, 349─51
Run menu 40
run-time errors
with arrays 569─71
function argument problems 562─66
mirror words 571─72
misleading if statements 568─69
operator priorities 572─73
references 575─76
scanning problems 573─75
warning levels 566─68
rvalue 240, 268, 359
S
SADD.C program 195, 196
SADD2.C program 197, 198
Save As command (File menu) 40
Save command (File menu) 40
SAVEGRAF.C program 476─77
Save_screen() function 477─78
sbrk() function 248 (table), 250─52
scan code 406, 407
debugging errors in 573─75
example of 407, 408
reading ASCII and 426─29
values 408─9
SCANCODE.C program 408
scanf(), getting user input with 82─85, 271─73, 336, 402, 405, 486,
574, 580
SCANLINE.C program 271, 272
SCAPE.C program 536─37
SCORE.C program 61, 62
formatting numbers in 70
SCRANGE.C program 273
screen(s). See also video monitor(s)
clearing 434─35
console input/output functions to control 409─13
control of cursor and (see cursor and screen control)
coordinates
logical 506─7
physical 501─4
initial QuickC 12
manipulating text screen with far pointers 242─43
modifying with bit fields 363─65
overview 35─36
storing and display 476─80
Screen Swapping On command (Debug menu) 118, 446, 467
SCRFUN.C program 439, 441─46
functions included 441
include file 440
SCRINV.C program 242, 243
SCRMENU.C program 363─65
scrn.h file 439, 440, 441, 447, 462, 474, 480
scroll bars 35
Scroll Up an Area of the Screen interrupt 431 (table)
SCRREST.C program 314─15
SCRSAVE.C program 312─13
search.h file 22 (table)
Select Active Display Page interrupt 430 (table)
Select Cursor Position interrupt 430 (table)
selections, making in QuickC 36─37
_selectpalette() function 505
values for 500 (table)
_setbkcolor() function 501
_setcolor() function 168, 501, 502, 508, 529, 544
SET command (MS-DOS) 286
Setcurs() function 432, 433, 438, 477, 478
SETCURS.C program 433
Set Display Mode interrupt 430 (table)
_setfillmask() function 508
filling figures with 512─16
setjmp() function 325 (table), 328, 329
setjmp.h file 22 (table)
_setlinestyle() function 508, 510
_setlogorg() function 506
setmode() function 402
Setpage() function 433, 434, 480
_setpixel() function 502─4, 505, 506
Set Program List command (File menu) 99, 382, 391
Set Runtime Options command (Run menu) 284, 295, 299
dialog box 296
_settextcolor() function 512
_settextposition() 512
SETUP program 27─29
with floppy-disk systems 32─34
with hard-disk systems 30─31
_setvideomode() function 144, 493─94
Setvmode() function 492─93
share.h file 22 (table)
SHIFTADD.C program 572, 573
SHORTIF.C program 129, 130
SHOWARGS.C program 284, 285
SHOWARGS2.C program 285, 286
Showcard() function 337─39, 341
Show_change() function 239
SHOWCODE.C program 426, 427─28
showmenu() function 420
signal(s) 326─28
analysis of 327
defined for MS-DOS 328 (table)
signal() function 325 (table)
signal.h file 22 (table), 326, 327
sizeof operator 55, 56, 200
size_t bytes 248, 301
small programs, esign problems with 578
source code files 24
to object 25, 26
spacing in C/QuickC programs 53
SPECS.C program 71, 72
sqrt() function 97─98
square() function 159, 230─32
SQUARE.C program 232─33
squares, calculating aspect ratios for 524
srand() 528
stack 153
debugging problems with arguments
passed to 563, 564, 565
how parameters work on the 163, 164
recursion and size of 174─75
standards. See American National Standards Institute (ANSI), C standards
Start command (Run menu) 40, 120
statements
assignment (see assignment statements)
break 134─38
continue 138─41
do 113─15
expressions in 51
for (see for loop)
in function definitions 40, 51─53
goto 141─42
if 123─28
multistatement for loops 97─103
switch 132─34
typedef 65─66
while (see while loop)
stat.h file 22 (table)
values for pmode in 311 (table)
STATIC.C program 157, 158
static variables 157─58
structures initialized as 351─53
status line 36
stdarg.h file 22 (table)
stdaux file pointer 303 (table)
stddef.h file 22 (table)
stderr file pointer 303 (table), 305
stdin file pointer 303 (table)
stdio.h file 22 (table), 292, 301, 315, 322, 402, 406, 409
stdlib.h file 22 (table)
stdout file pointer 303 (table)
stdprn file pointer 303 (table)
strdup() function 278
string(s) 263─89
arguments to main() 284─86
arrays and 280─84
character classification and transformation of 286─89
console input/output of 411─13
declaring and initializing 264─65
formatting with printf() 269─70
input (in BASIC) 405
input/output for 271─75
manipulation routines for 275─80
pointers and initialized 267─68
printing, with printf() 68─69
string.h file 22 (table)
string pool 266─67
STRINGS.C program 69, 295, 296
STRIO.C program 412
strlen() function 278─79
STRPOOL.C program 266, 267
struct keyword 332, 333
structure(s), 331, 332─51
accessing members 334─35
accessing members with a pointer 341─43
arrays of 343─46
arrays of pointers to 346
assignment 336─37
bit fields inside 363
initializing, with starting values 351─52
members (variables) defined/declared 332, 333
passing, to functions 337─39
pointers to 339─41
recursion and linked lists 346─51
shorthand declaration of 336
stub functions 578─79
subtraction 75, 85, 237
SUMNUMS.C program 562, 563
SUMNUMS2.C program 565, 566
sums() function 562, 567
SWITCH.C program 135, 136, 137
switch statement 132─33, 134
vs if-else 137─38
vs pointers to functions 259─60
syntax, C/QuickC 10
error in 115
system code 406
T
\t (tab) 67, 68, 69─70, 266, 410
tab. See \t (tab)
TABLE.C program 99, 100, 101
TABS.C program 69, 70
tell() function 318
TEST.C program 391, 393
TESTER.C program 580─81
TEXED.C file 380
with header file 388, 389
keeping track of changes in 387
program list files for 383─87
text color values 456 (table)
text file routine mode 292, 296
for fopen() 297 (table)
getchar() in 402
text mode and video monitors
device-independent programming 453─69
direct video memory access 469─80
EGA and VGA 489─90
graphics compatibility and 460─69
paging in 480─84
portability and 451─53
ports 485─89
then keyword 124
three-dimensional (or more) arrays 212─15
declaring 213
initializing 214
using in functions 215
time() function 114─15, 528
timeb.h file 22 (table)
time.h file 22 (table)
TIMER.C program 114
TIMER2.C program 164, 165─66
TINY.C program 49, 50
title bar 35
TODAY.C program 359, 360
tolower() macro 410
TOTAL.C program 249
TOTAL2.C program 251, 252
toupper() macro 288, 410
Trace On command (Debug menu) 117, 153
triangle() function 159
Triple() function 203
TRUTH.C program 91, 92
TTT.C program 209, 210─12
Turbo Pascal 4, 15
two-dimensional arrays 206─12
functions and 209─10
initializing 207─9
strings 281
type casting 81─82
pointers and addresses 241
type checking in Pascal and C 178
TYPE command 299
typedef statement
advanced use of 365─66
renaming data types with 65─66
types.h file 22 (table)
U
UDEMO.C program 353─55
ultime.h file 22 (table)
unary inversion (ones complement) operator (~) 218, 223, 238, 516
unary shift operators (>> <<) 218, 223─24
UNDOVER.C program 200, 201
union(s) 352─58
functions and 355
with int data type 355
pointers to 356─58
received by functions 356
REGS 423, 424, 425, 426
union data type 331, 352
UNIX System V, and C 4, 5, 14, 15, 326, 328
unlink() function 321 (table)
unsigned char data type 65, 205
unsigned int data type 60, 217
unsigned long data type 61, 301
UPPITY.C program 301─2, 303
V
values
assigning to variables 59, 85
assigning truth 129─31
lvalue vs rvalue 240
using #define to assign names to 104─7
warning 171
VARADDRS.C program 57, 58, 82─83
varargs.h file 22 (table)
variable(s) 55─66. See also data types; statements
accessing with pointers 228─30
assignment statements 59
automatic 155─57
declaring 57─58
vs #define 105
external 158─60
for loop control 95
in header files 389─90
initializing 59
local 154─57
naming rules for 58─59
register 160─61
scope of 154, 155
shortcuts with 85─88
static 157─58
structure (see structure(s), members (variables) defined/declared)
uninitialized 115
watch 119, 120
VARSIZE.C program 55, 56, 57
VGA. See Video Graphics Array (VGA)
VGAMAP.C program 546, 547─48
video, reverse 436, 448
video controller 436, 449, 450, 451 (table), 491, 492 (table)
ports to access registers on 452
similarities 451─53
summary of text mode differences 453 (table)
Video Graphics Array (VGA) 505, 544─51
color code for accessing all EGA colors 534─41
automatic color value conversion 538
color value storage 535
defining nonpalette colors 535─37
EGA bit to VGA byte conversion 535 (table)
example 538─41
color values equivalent to EGA default palette 530 (table)
palette (256-color) 544─51
screen memory, accessing 242
in text mode 489─90
video controller 451 (table)
video input/output interrupts 429─31
library of C functions that use 43─46
video memory 433, 435
BIOS routines used to place bytes into 454─59
direct access to 452, 469─80
EGA 529
video mode, set 144
video monitor(s) 449. See also console input/output functions
color and 456
controllers and 450─51
in graphics mode (see graphics mode and video monitors)
in text mode (see text mode and video monitors)
VIEW.C program 316─17
W
Wait() function 528
WHATCHAR.C program 287─88
WHILE.C program 108, 109
while loop 108─13, 116
animating characters with 110─11
combining, with for loops 112─13
comparing if statements with 125─26
flowchart 109
vs if statements 125─26
using break to exit from 134─35
words 55, 56
write() function 309, 312─13
Write Character and Attribute interrupt 431 (table)
Write_chars() function 484
Write_ch_atr() function 436, 437, 448
writechr.c module 481, 482─83
Write_str() function 484
X
XENIX system 328
XMAS.C program 194, 195