practical programming techniques using c++ programming techniques using c++ june 2004 edition...

110
Practical Programming Techniques Using C++ by Evan Weaver School of Computer Studies Seneca College of Applied Arts and T echnology June 2004 ©1996-2004 by Evan Weaver and Seneca College. Effective 2014, this work is made available under the Creative Commons Attribution 4.0 International License. Visit http://creativecommons.org/licenses/by/4.0/ for details on how you may use it.

Upload: vannhi

Post on 29-Mar-2018

238 views

Category:

Documents


15 download

TRANSCRIPT

Page 1: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++

by Evan Weaver

School of Computer Studies Seneca College of Applied Arts and Technology

June 2004

©1996-2004 by Evan Weaver and Seneca College. Effective 2014, this work is made available under the Creative Commons Attribution 4.0 International License. Visit http://creativecommons.org/licenses/by/4.0/ for details on how you may use it.

Page 2: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++

by Evan Weaver

School of Computer Studies Seneca College of Applied Arts and Technology June 2004

Table of Contents PREFACE.................................................1

PART 1. C IN MORE DETAIL................................3 Preprocessor Directives.........................3 Case Study: Direct Terminal I/O................12 Isolating Platform Dependence..................17 Basic Data Types...............................21 Casting and Passing............................25 Multi-Dimensional Arrays.......................26 Addresses and Pointers.........................28 Pointer Arithmetic.............................29 Pointers and Arrays............................30 Generic Pointers...............................34 Pointers to Functions..........................35 More on Structures.............................37 Unions.........................................38 Enumerated Data................................40 Creating Custom Data Type Names................41 More C Operators...............................42 Control Flow...................................49 Declaration Modifiers..........................53 The Real Syntax of Main........................58 Variable Parameter Lists.......................59

PART 2. C++ IN MORE DETAIL.............................63 Default Parameters.............................63 Initializing Members Directly..................64 The bool Data Type.............................64 Namespaces.....................................64 Forward Declarations...........................67 Linked Lists...................................67 File Streams...................................76 Binary File Access.............................77 Inline Functions...............................84 Function Templates.............................85 Class Templates................................86 Declaration Modifiers Revisited................89 Reference Return Values........................92 Multiple Inheritance...........................94 Virtual Base Classes...........................96 Abstract Base Classes..........................98 Exception Handling.............................99 Casting and Run-Time Type Information.........103 Introduction to the Standard Template Library.105

EPILOGUE..............................................107

Page 3: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Preface These notes constitute the third (and final) part of a series on introductory programming using the C and C++ languages. In the earlier works, we introduced the reader to the fundamentals of structured programming using C (Foundations of Programming Using C) and the basic concepts of object oriented programming using C++ (Foundations of Object Oriented Programming Using C++). While the earlier notes provide a broad introduction to all facets of programming using modern languages, they lack the depth that the reader must experience in order to become a truly proficient programmer. The purpose of these notes is to follow through on the topics begun in those earlier works, by studying more syntax of the C and C++ languages and by working at a higher level of complexity and abstraction.

After learning what is in these notes, the reader will know most of what appears in the typical C++ textbook. The approach we use as Seneca is a bit different from the approach used by most books, however, which is why we needed to develop these notes in the first place.

The typical C++ (or C, for that matter) book takes the major elements of the language and covers them quite thoroughly, one at a time, showing all the possible variations on each theme. If you attempt to skip around such a book, you tend to find that later examples use all the syntax shown in earlier sections, and if you haven't covered all the preceding material, problems ensue. We have never been able to teach all of an introductory language in one or even two semesters, and yet we want students to experience the whole program development process as many times as possible, particularly throughout the early semesters.

Our approach has been to introduce the student to a subset of the C and C++ languages in the early semesters. Useful programs of all sorts can be written after Foundations of Programming Using C. All kinds of organizational improvements, due to the benefits of object oriented technology, can be implemented after Foundations of Object Oriented Programming Using C++. The sacrifice has been one of completeness - after working through the first two sets of notes, students do not fully understand such things as the power of pointers, the proper use of multidimensional arrays or the implications of multiple inheritance after working through the first two sets of notes, nor are they ready to read example programs from trade journals. There is a great deal of language syntax (often somewhat redundant) which the first two works omit for the sake of clarity and brevity. These notes tie up such loose ends.

In the earlier works, needless detail was intentionally omitted to enable the reader to focus on learning the issues underlying the programming process rather than getting too caught up in the

Preface Page 1

Page 4: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

syntactical issues of a particular programming language. But a professional programmer needs to understand the subtleties of the language in order to do an effective job, and so a large part of these notes is simply the presentation of additional syntax. But these notes contain much more than new syntax. Because the reader has previous programming experience, we spend a lot of time one various non-trivial programming techniques (for example, linked lists), techniques which are more complicated than what we feel first year students are ready to understand.

As with the earlier notes, these notes are designed to be used in conjunction with other C and C++ books. At the very least, the reader should have a good C book and a good C++ book near at hand. Most people would probably prefer a few of each. Since we have never found a book that everyone thinks is good, yet everyone has a particular book that they think is the best, we have not geared these notes to work with a specific book.

In case you haven't worked through the earlier sets of notes, then before reading these notes you should already know the following C topics:

- basic data types (char, int, long, float, double), variables and constants - logic control structures (if, if/else, switch, while, do/while, for) - basic operators (assignment, arithmetic, relational, logical) - functions, parameter passing (including passing the address of a variable) and return values - one-dimensional arrays - character strings (char arrays with a terminating null byte) and arrays of strings (a special case of two-dimensional arrays) - structures - C library functions to: do basic terminal input/output (printf, scanf, etc.) access sequential files (fopen, fprintf, etc.) manipulate character strings (strcpy, strcmp, etc.)

as well as the following C++ enhancements:

- classes (including member functions and public, private and protected access) - constructors (including copy constructors) and destructors - overloading functions and operators - dynamic memory allocation using new and delete - single inheritance - virtual functions - Input using cin and output using cout

Preface Page 2

Page 5: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Part 1. C In More Detail

We will begin by examining some familiar aspects of C in more detail. Unless otherwise noted, the topics presented in this section apply equally well to C++. But since programmers occasionally still have to work strictly in C, it makes sense to be aware of what elements of the language are available if you are just using C and not C++.

Preprocessor Directives C (and C++) compilers typically make at least three passes through the source programs that you write. The first pass, called the preprocessor, takes the original source code and produces a temporary set of source files that contain the C code after applying the preprocessor directives, which, you should recall, are the C commands that begin with a #.

The second pass, called the compiler, actually translates this source code (which has no # directives left in it) into machine language, but leaves out the addresses of external data and functions (such as variables and functions from the standard libraries). These missing addresses are usually called "unresolved external references", because they can't be resolved (or filled in) until the external items are placed in the program along with the code that is calling them. On most systems, each C source file results in such a machine language file, called an object file. (Use of the word "object" here has nothing to do with objects in Object Oriented Programming, but is rather used for historical reasons).

The third pass, called the linker, combines the object files in a program together, and also extracts the necessary machine language code from the standard libraries, to make an executable program. (As it combines the required machine language files, the linker is able to resolve all the external references).

There are two important things to realize about this process. First, anything involving preprocessor directives must be available and completely known at compile time, not run time. Second, when a program is divided over several source files (a common practice which allows the reuse of common code), the individual source files are translated to machine language independently from each other, and are only joined together in the final linking step.

#include Directive

The syntax for the #include directive is

#include FILENAME

where FILENAME is the name of a source file enclosed either in

Part 1 (More C) Page 3

Page 6: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

double quotes ("") or angle brackets (<>). Remember that the distinction between the two is that double quotes imply that the file will be found in the current location, while the angle brackets imply that the file will be found in the compiler's own special location. Generally, double quotes are used for #including your own files, while angle brackets are used for #including standard files that are considered part of the compiler system.

Keep in mind that some compilers will eventually look in both the current location and the system location(s), so that the kind of bracketing only affects the order in which the file search is performed, while other compilers will only search the current location for double quoted files, and only the system location(s) for angle bracketed files. So even though you may find that, for example, you can #include standard files using double quotes, your code will not necessarily be portable to another compiler.

All the #include directive does is copy the named file into the current source file. After copying in the file, the newly imported code is then passed through the preprocessor (so that any directives in the imported code are also dealt with).

Any compilable code MAY appear in an #included file, and the #included file MAY have any name, which leads to a lot of confusion over exactly what SHOULD be in an #included file. But there is a very clear intention for the #include directive, which is to allow source files, which call code located in other source files, to be compiled separately.

For example, the file <stdio.h> is designed to be #included whenever you want to call functions in the standard input/output library (which is already compiled and was delivered that way with the compiler). Thus, <stdio.h> contains just enough C code to enable the code that follows the #include to correctly use all parts of the standard I/O library. Note that <stdio.h> does not contain any code for any of these standard functions, but simply has the declarations (i.e. prototypes) for those functions.

Remember that C programmers make a distinction between a declaration, which is code that describes the nature of something, and a definition, which is code that causes something to be created (and thus occupy memory when the program is run). A definition is also a declaration, since in creating something you must also describe it, but a declaration might or might not be a definition, since it might describe something that has been created elsewhere.

As an example, the following is a definition (and thus also a declaration) of a function that triples a number:

Part 1 (More C) Page 4

Page 7: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

double triple(double x) { return 3 * x; }

whereas

double triple(double);

is a declaration, but not a definition, of the same function.

If you define the same thing more than once in a program, the linker will complain about "multiple definitions" when it attempts to link all the pieces together. If, for example, <stdio.h> contained any definitions whatsoever, then you would get a linker error if you tried to compile and link together two source files which both happened to #include <stdio.h>.

The guiding principle behind proper use of #include then becomes: definitions should never appear in a file that will be #included.

To support this, programmers use the term header file to describe a source file which is designed to be #included rather than compiled directly. Such files are given names that end with ".h" so that it is clear they should not be compiled directly. Furthermore, most programmers also abide by the convention that any time you put some code into a separate source file (so that you can re-use it in many different situations), you also create a header file that declares all the parts of that source file which are allowed to be used by other functions. Typically, this header file will have the same name as the source file, except that the file name will end with ".h" instead of ".c".

If you stick with these conventions, you will never be in any doubt as to what you do or do not need to #include. The rule becomes: if a source file does not directly use something declared in a header file, then there is no need to #include the header file. Conversely, if you use anything not defined in the current source file, you should #include the appropriate header file.

Typical elements, with which you are already familiar, found in header files are:

- other #include directives - #define directives - struct (and, in C++, class) declarations - function prototypes

Part 1 (More C) Page 5

Page 8: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Other typical elements, which we will see later, are:

- union and enum declarations - typedef declarations - declarations of global variables defined elsewhere (extern) - templates (C++ only) - inline function definitions (C++ only)

On the other hand, a header file should never contain the definition of a variable or a non-inline function.

#define Directive

You already know how #define can be used to give a symbolic name to a constant value, as in:

#define PI 3.141592654

What really occurs is that the preprocessor is told to look for the name, PI, in subsequent code, and when it finds it, to remove it and replace it with the value 3.141592654. The second pass of the compiler never sees the name PI at all.

What you may not be aware of is that you can place any code you like after the name, and that code will be substituted for the name. For example, you could (but shouldn't!) do something like:

#define set_x_to x =

and then later, in a section of code where there happens to be a variable named x already defined:

set_x_to 6;

which would cause the compiler to "see" the statement:

x = 6;

Hopefully, it is obvious that if you start to write code like this, soon you will end up with code that no one, not even you, can read. (However, this can be a useful technique to use if you are trying to quickly port code written in a similar language, such as Pascal, to C).

The code that will be substituted for a #define name ends with the end of the line. If you really want to, you can force the #define value to span several lines by preceding the newline character with a backwards slash (\) on all except the last line. This "escapes" the special meaning of the newline, which is to terminate the #define.

Part 1 (More C) Page 6

Page 9: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Macros

Another syntax for #define is shown in this example:

#define MAX(n1, n2) (((n1) > (n2)) ? (n1) : (n2))

Later code would then use MAX like a function, as in the statement:

z = MAX(x + 4, (y + 1) / 2);

Just as with the plain #define, the preprocessor will replace the name MAX with the rest of the #define line, although the substitution is a little more complicated in this case. The names in parentheses after the #define name (n1 and n2, in this example) are called place-holders, and they appear in the rest of the #define line. Wherever those names appear later, they will be filled in with the actual code supplied when the #define name is used. In this example, the preprocessor will change the statement into:

z = (((x + 4) > ((y + 1) / 2)) ? (x + 4) : ((y + 1) / 2));

since everywhere the name n1 appears, it will be replaced with "x + 4", and everywhere n2 appears, it will be replaced with "(y + 1) / 2". Note how the arguments supplied to the macro call might be expressions which are used "as is" in the macro's expansion. Because such expressions just might contain very low precedence operators, it is common practice to use all sorts of extra parentheses in the code for the macro. This is an attempt to ensure that the order of operations in the final line matches that of the original macro regardless of the arguments supplied.

The rules for the names of the place-holders are the same as the rules for other C names.

Although a macro works much like a function, there are significant differences. For comparison, consider the following function:

int max(int n1, int n2) { return n1 > n2 ? n1: n2; }

The function, max, would perform the same calculation as the macro, MAX, if both arguments were simple int values. But because the code for the macro is inserted where is it called, before compilation, the final executable program will not have the overhead (of calling a function) which would exist in the case of using the function. (When a function is called, the arguments are first copied into the parameters, the processor

Part 1 (More C) Page 7

Page 10: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

jumps down to the code for the function, its logic is executed, the return value is copied back, the local variables are disposed of, and the processor jumps back to where the function was called). For short macros, the actual execution time of the entire macro can be less than the overhead of a simple function call (not counting the actual computation, which will be the same for the macro and the function). This can be a significant saving in very time-critical parts of a program.

Another useful aspect of a macro, compared to an equivalent function, is the fact that the macro does not impose any predetermined data types upon the arguments. At compile time, if the arguments make sense to the compiler, the code will be compiled according to the data types actually used. For instance, suppose that x, y and z are all double variables. Then

z = MAX(x, y);

will become

z = (((x) > (y)) ? (x) : (y));

Note that since x and y are doubles, so is the value that is assigned to z, and z will correctly become the larger of the two arguments. But in the case of

z = max(x, y);

the double value x will be cast into the int parameter n1 (truncating it), and the same thing will happen with y. The int value that is returned from the function will then get cast back to a double in the process of assigning it to z. The net effect will be that z is assigned the truncation of the largest argument, which is not necessarily the desired result!

The solution to this problem with the function is that, in this case, int was probably not the best type to choose for the parameters or return value. The best approach would most likely be to have separate functions for "int" maximum and "double" maximum, and to use the appropriate function in different situations. But the flexibility of the macro, which compiles the correct code in both cases, is undeniably an advantage.

So, macros have efficiency and flexibility going for them. But there are drawbacks as well. If a macro is large and is called several times, the same logic will be duplicated in several parts of the executable, whereas a function would appear once in the executable, and only the overhead of calling it is duplicated. Heavy use of macros can result in a considerably larger executable program, which will then take longer to load into memory, and will consume more memory, both of which may affect the load on the system, effectively countering the

Part 1 (More C) Page 8

Page 11: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

efficiency benefits.

More seriously, however, is the potential for "side effects" of a macro. Consider the case of three int variables, i, j and k. In the function call

k = max(++i, ++j);

the variables i and j are both incremented (by 1) before being passed to max. In the case of the macro

k = MAX(++i, ++j);

we get

k = (((++i) > (++j)) ? (++i) : (++j));

which will cause the larger variable to be incremented twice. Here it is the macro that produces the unexpected result. Such side effects are actually the cost of the flexibility benefit of a macro, and the only way to guard against them is to consider what the macro will expand to every time you call it.

Most of the time, the benefits of a macro (efficiency and flexibility) are not worth the potential cost (larger code size and unpredictability). But when you have a small snippet of code that you are prepared to use carefully, a macro can contribute to speeding up the execution time (and coding time) of a program.

Still, because of the care you must take when you call a macro, most programmers use a naming convention (typically, using all capital letters, as we have done above) when naming macros, in order to make macro calls stand out in the code. This helps to ensure that you remember to look for potential side effects when you call a macro.

#undef Directive

The syntax

#undef NAME

"undefines" a name, i.e. it reverses, from this point forward, the effect of a previous #define (where NAME stands for a name that was previously #defined). This can be useful if you want to use a variable or function name that is the same as a macro (which you don't plan to use) that appears in a header file that you have included.

Part 1 (More C) Page 9

Page 12: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Conditional Compilation

There is a collection of preprocessor directives which allow the source file to contain several different version of code, and gives the programmer control over which of those versions actually gets compiled. This facility is often used to put debugging code into a program which can be excluded from the final (production) version without having to be physically removed from the code. It is also used to help support multi-platform development, where some parts of the code might need to be different in different operating environments.

The directives are: #if #ifdef #ifndef #else #elif #endif and work in a similar fashion to the C if/else statement.

The basic syntax is

#if SOME-CONDITION ...code to compile if CONDITION is true #elif ANOTHER-CONDITION ...code to compile if SOME-CONDITION is false ...but ANOTHER-CONDITION is true #else ...code to compile if SOME-CONDITION and ...ANOTHER-CONDITION are both false #endif

although there may be as many #elif sections as you like. Here the names SOME-CONDITION and ANOTHER-CONDITION represent some C condition (i.e. a true/false value). Remember that in C, the numeric value 0 is treated as "false", and any other value is treated as "true". In this case the conditions may be C expressions, as long as the expressions only involve constant values, since the expressions are evaluated by the compiler at compile time.

Note that the #elif and #else sections are optional (just as the else is optional in the C if statement).

#ifdef is a possible replacement for #if. Its syntax is

#ifdef SOME-NAME

and the code which follows it will be compiled if SOME-NAME (which represents some name) has been defined with a #define statement, and will not be compiled if that name hasn't been

Part 1 (More C) Page 10

Page 13: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

"#define"d (or has been "#undef"ed). If you use #ifdef instead of #if, the mere presence or absence of a name determines whether code gets compiled or skipped - the value which the name is defined to stand for is irrelevant. For this reason, you may see code like

#define DEBUG

which defines DEBUG, even though it "stands for" nothing. In a case like this, you can be sure that later, there will be something like

#ifdef DEBUG ...some code #endif

#ifndef is another possible replacement for #if, and is like #ifdef, except that it will compile the code which follows it if the specified name is NOT defined.

Recent compilers also allow a special operator syntax that can be used in the condition for #if and #elif:

defined(SOME-NAME)

evaluates to a true value if SOME-NAME (which represents some name) has been "#define"d, and a false value if it hasn't. Thus, with these newer compilers,

#ifdef ABC

is the same as

#if defined(ABC)

but is more flexible, since you can also do such things as

#if defined(ABC) && !defined(DEF)

Condition compilation is often used in header files to guard against the possibility of including the file more than once in a single compilation. (For example, one header file might include another, and a program could include both header files, causing the second header file to be processed twice, potentially causing errors or warning messages). The trick used is to pick an unlikely name to "#define" the first time that header file is included. For example, in a header file named "abc.h", you could begin the header file with

#ifndef _abc_h_ #define _abc_h_

Part 1 (More C) Page 11

Page 14: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

and end it with

#endif

so that the rest of the code will only be processed if the name _abc_h_ hasn't been defined, which will only be possible if this file has not yet been processed. Here the underscore character has been liberally used just to make the name (_abc_h_) less likely to have been defined by some other, completely unrelated, part of the program.

Case Study: Direct Terminal I/O

As an example of a typical situation in which conditional compilation is useful, we will examine a commonly available terminal input/output mechanism which is not part of the standard I/O library. Many different vendor-specific solutions exist. Any program which wants to use these features must then necessarily have different code for different systems.

The standard I/O libraries (both C's <stdio.h> and C++'s <iostream.h>) provide a basic input/output facility which works the same regardless of the input and output devices involved. For example, printf output can go to a video monitor, a printer or a file, depending not on how the program is written, but on how it is executed. (A user can use the command line redirection capabilities of the operating system to "send" the standard output of the program to a file or a printer). This is a very useful feature, since it encourages programmers to write programs that, although they may be interactive, can be run non-interactively from operating system scripts.

But most users prefer a more sophisticated interface than what the standard library can provide. Although in the 1970's (when the concepts behind the standard I/O libraries were developed) CRT terminals with cursor control capabilities were rare and expensive, today virtually every connection to a computer is through a terminal which is capable of allowing the computer to move the cursor around the screen. Unfortunately, this sort of facility has become ubiquitous fairly late in the history of computing, too late for the various manufacturers to agree on a standard way in which these devices should operate.

Consequently, no standard libraries exist which allow the cursor to be moved around the screen, or which allow keystrokes to be captured as keys are pressed. Rather, each compiler has some proprietary library routines to access terminal I/O at a lower level than that provided by the standard I/O libraries. Any program that wants to use these facilities can, but the code will not be portable to other environments.

We will look specifically at two environments: Borland C/C++ for

Part 1 (More C) Page 12

Page 15: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

PCs (running MS-DOS or MS-Windows), and AIX (IBM's version of the UNIX operating system) for RS/6000 computers.

Console I/O for Borland Compilers

Using the Borland C/C++ compiler for the PC, the header file <conio.h> declares the functions necessary to communicate directly with the "console" (the built-in monitor and keyboard) of a PC using the Borland C compiler. We will look at a useful subset of these routines. The functions we will look at can be used to build MS-DOS applications or, except as noted, "EasyWin" MS Windows programs. (EasyWin programs are simple text-based programs that run in a fixed size window, rather than requiring an MS-DOS session).

void clrscr(void) - clears the screen, leaving the cursor in the upper left-hand corner.

void gotoxy(int x, int y) - moves the cursor to column number x, where 1 is the leftmost columns, and row number y, where 1 is the top row.

int putch(int c) - displays the character c at the current cursor position and advances the cursor one position. (The return value is the character that was displayed).

int cputs(char *s) - [not for EasyWin programs] displays the string s, starting at the current cursor position. The cursor is advanced as each character is displayed. (The return value is the last character that was displayed).

int getch(void) - returns a key code of the next key pressed. If the key pressed is a standard ASCII character, then the key code is the ASCII code.

When a non-ASCII key, such as the Cursor Up key, is pressed, this function returns 0. A second call to getch() must then be made, and the number returned can be used to identify the key that was pressed. (Note that this second code number will be the same as some ASCII key, and the only reason that you know it isn't an ASCII key is because the first call to getch() returned 0). These non-ASCII keys are called extended keys, and this two-code mechanism for identifying extended keys is unique to the MS-DOS platform.

void gettextinfo(struct text_info *r) - fills the struct, pointed to by r, with information about the screen. The data type "struct text_info" is also declared in <conio.h>. While there are many different fields, we will concern ourselves only with the two fields r->screenheight and r->screenwidth, which contain the height of the screen (in rows) and the width of the screen (in columns), respectively.

Part 1 (More C) Page 13

Page 16: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Console I/O for AIX Compilers

Most UNIX-like systems use a set of library functions called Curses to provide direct terminal access in a terminal independent manner. (Presumably, the name is related to "cursor control", and not to what the programmer does when using the library). The situation in UNIX is a bit more complicated than on the PC, for two reasons.

First, UNIX tries to support all brands of terminal, even though different brands of terminal use different codes to do such things as move the cursor. UNIX allows this by giving the system administrator the ability to identify what codes are used to perform the different standard cursor-control actions. (The most common mechanism is called "terminfo", and involves creating a text file which describes the particular terminal, and then passing that file through the "tic" - terminal info compiler - command. The UNIX environment variable TERM is then used to identify to the system which model of terminal you are using). The curses routines end up translating the program's requests to terminal-specific codes by using this information.

Second, UNIX terminals may be connected to the computer in a variety of ways (e.g. Ethernet, serial connection), some of which may be relatively slow from the computer's point of view. In contrast, the PC's direct connection enables screen updates at the speed of the system bus. The curses routines make an attempt to optimize the communication line by comparing what is currently on the screen with what the program is requesting the screen to look like. If the curses routines are able to determine that they can make the screen look correct by sending fewer (and probably different) characters than what the program is asking to send, then that is what they will send. (For example, if the program wants to put the letter "x" into the fifth spot on the third row, but there already is an "x" there, then the curses functions will probably not send anything at all to the terminal).

Unfortunately, these extra bits of complexity cause the curses library to be a little bit more complicated to use than Borland's conio library. Also unfortunately, while most UNIX systems offer curses, there are slight differences between different versions of UNIX, and some "porting" usually needs to be done when moving code from one UNIX to another. We will look at curses as offered by IBM's version of UNIX, AIX.

To use the curses library, the program should include the header file <curses.h>. Before any direct terminal I/O is performed, the program must call

initscr();

Part 1 (More C) Page 14

Page 17: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

which will set things up so the other curses routines will expect the model of terminal identified by the UNIX environment variable TERM. If that terminal does not support direct terminal I/O (for example, it might be a printing terminal where output appears on paper rather than on a monitor), then the program will be terminated by initscr().

There are a few other setup steps which, while not absolutely required, are usually desired. The first is to call

noecho();

which will turn off the operating system's echoing of characters as they are typed. Normally, if you are letting your program take control of the terminal, you want your program, rather than the operating system, to totally control what characters are displayed.

Next, you'll probably want to call

cbreak();

which makes control revert to your program on each keystroke, rather than the operating system's normal tendency to use the Enter key as a "turnaround" character. (Recall, for example, how <stdio.h>'s getchar() causes the program to wait until an entire line, ending with the Enter key, is entered before returning). Finally, you should also call

keypad(stdscr, 1);

which tells curses that you want the cursor keypad (and other non-ASCII) keys to be treated as keys the user is allowed to press. (Otherwise, you'll probably only be able to get plain ASCII keys). The parameters for keypad allow different parts of the screen to handle the keyboard in different ways. The arguments shown (stdscr is a global variable set by initscr()) work properly for the subset of curses functions we will present here.

Because this setup is required, it is also necessary to call the function

endwin();

when the program is finished using curses. This puts the terminal back in the mode it was in before initscr() was called. Failure to call endwin() can result in the terminal being left in an unusable mode (e.g. characters may not be echoed to the screen as you type).

Part 1 (More C) Page 15

Page 18: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Once curses has been successfully initialized, you may use the following functions (among the many functions that are available):

int getch(void) - returns the next key pressed. Note that each key has a unique code, including non-ASCII keys. Typically, the non-ASCII key codes are numbers above 256. Also note that, for example, an "up arrow" key will return the same number through getch() regardless of the brand of terminal. For example, on a DEC VT100, the up arrow key actually sends three bytes - Escape, [ and A - while an IBM 3151 sends two bytes - Escape and A. In both cases, getch() returns ____ (do an experiment to fill in the blank).

int move(int row, int col) - moves the cursor to the row and column specified, where row 0 is the top row, and column 0 is the leftmost column. Note that this, unlike Borland's gotoxy(), treats the screen like a 2-dimensional array of characters.

int erase(void) - clears the screen.

int addch(int c) - displays the character, c, at the current cursor position and advances the cursor one position.

int addstr(char *s) - displays the null-terminated string, s, advancing the cursor after each character displayed.

int refresh(void) - tells curses that the physical screen must be brought up to date. Normally, as you call move(), addch(), and the other functions which cause output, nothing is actually sent to the terminal. Rather, curses builds an image, in memory, of what the screen should look like. When refresh() is finally called, curses then compares this new version of the screen with what is currently shown on the screen, and sends the smallest number of characters possible to make the physical screen show the desired appearance.

In all of the functions above except getch(), the return value indicates whether an error occurred or not. If the return value is ERR (a name defined in <curses.h>), then something went wrong, and any other return value indicates success. In most programming, however, these return values are ignored.

As well, initscr() sets two global int variables, LINES and COLS. LINES stores the number of lines (rows) on the screen, and COLS store the number of columns.

Part 1 (More C) Page 16

Page 19: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Isolating Platform Dependence

We now know enough to write a collection of simple functions that we could use to write programs which need to perform direct terminal I/O. The plan is to write one set of functions, containing both AIX and Borland versions of the code. These functions will then be used by programs, rather than requiring the programs themselves to contain both <conio.h> and <curses.h> elements. In this way, we will not only simplify the process of writing direct terminal I/O programs on these two platforms, but we will also make it easier to port those programs to other platforms. All we would need to do to support another platform would be to add another conditionally-compiled set of code to each of the functions in our collection.

Keep in mind that any program wishing to be portable across platforms must address issues that any of the platforms require (even if the other platforms do not). For example, since the curses routines need to be initialized and de-initialized, then our set of routines has to allow for this, which we will do by providing a single setup function and a single shut-down function. Programs using our routines would need to call these functions, even though they wouldn't really need to be called in a DOS environment, in order to be portable.

Here, then, is the header file, named dtio.h, for our set of "portable" direct-terminal functions:

/********************************************************** * dtio.h - header file for direct-terminal I/O functions * * supporting both Borland C and AIX cc platforms * **********************************************************/

#ifndef _dtio_h_ #define _dtio_h_

#define BORLANDC 1 #define AIXC 2 /* change the following line to support one of the above */ #define PLATFORM BORLANDC

/* some platform-dependent keys (obtained by experimentation) */ #if PLATFORM == AIXC #define ENTER_KEY 10 #define UP_KEY 1859 #define DOWN_KEY 1858 #define LEFT_KEY 1860 #define RIGHT_KEY 1861 #else #define ENTER_KEY 13 #define UP_KEY 1072 #define DOWN_KEY 1080

Part 1 (More C) Page 17

Page 20: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

#define LEFT_KEY 1075 #define RIGHT_KEY 1077 #endif

void dt_start(void); /* initializations for dt routines */ void dt_stop(void); /* shutdown of dt routines */ int dt_rows(void); /* find # of rows of screen */ int dt_columns(void); /* find # of columns of screen */ void dt_clear(void); /* clear screen */ void dt_flush(void); /* flush any un-written output */ int dt_getchar(void); /* get one key press */ void dt_cursor(int row, int column); /* move cursor */ void dt_putchar(int c);/* output one character */ void dt_puts(char *s); /* output a string */

#endif /* end _dtio_h_ */

and here is the code, which we might put in a file named dtio.c:

/********************************************************** * dtio.c - direct-terminal I/O functions supporting both * * Borland C and AIX cc platforms. A program that * * wants to use these should: #include "dtio.h" * **********************************************************/

#include "dtio.h" #if PLATFORM == AIXC #include <curses.h> #else #include <conio.h> #endif

/* Initialize. Note that Borland version does nothing. */ void dt_start(void) { #if PLATFORM == AIXC initscr(); noecho(); cbreak(); keypad(stdscr, 1); #endif }

/* Shutdown */ void dt_stop(void) { #if PLATFORM == AIXC refresh(); endwin(); #else /* we don't want the cursor left in the middle of a screen. AIX's endwin() handles

Part 1 (More C) Page 18

Page 21: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

this for us, but on the PC we must take care of this. Clearing the screen is an easy way. */ clrscr(); #endif }

/* Return number of screen rows */ int dt_rows(void) { #if PLATFORM == AIXC return LINES; #else struct text_info x; gettextinfo(&x); return x.screenheight; #endif }

/* Return number of screen columns */ int dt_columns(void) { #if PLATFORM == AIXC return COLS; #else struct text_info x; gettextinfo(&x); return x.screenwidth; #endif }

/* Clear screen */ void dt_clear(void) { #if PLATFORM == AIXC erase(); #else clrscr(); #endif }

/* Bring screen up-to-date. Note that since dt_stop() and * dt_getchar() both bring the screen up-to-date, programs * will only have to call this if the screen must be brought * up-to-date when a long pause (other than waiting for * input) is expected. */ void dt_flush(void) { #if PLATFORM == AIXC refresh(); #endif }

Part 1 (More C) Page 19

Page 22: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

/* Return one keystroke, bringing screen up-to-date first */ int dt_getchar(void) { #if PLATFORM == AIXC refresh(); return getch(); #else /* For extended keys in DOS, return 1000 + second key code */ int key; key = getch(); return key == 0 ? 1000 + getch() : key; #endif }

/* Move cursor. (0, 0) is the upper-left corner */ void dt_cursor(int row, int column) { #if PLATFORM == AIXC move(row, column); #else gotoxy(column + 1, row + 1); #endif }

/* Output one character at cursor location */ void dt_putchar(int c) { #if PLATFORM == AIXC addch(c); #else putch(c); #endif }

/* Output character string at cursor location */ void dt_puts(char *s) { #if PLATFORM == AIXC addstr(s); #else cputs(s); #endif }

One important aspect of the function set shown above is that any source file which uses these routines does not itself include <conio.h> or <curses.h>. This helps to ensure that the source file doesn't accidentally use platform-specific functions, and also prevents potential naming conflicts between the source file and platform specific names declared in <conio.h> and <curses.h>.

Part 1 (More C) Page 20

Page 23: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Basic Data Types

By now you should be intimately familiar with the data types char, int and double. The entire collection of basic data types is actually only slightly larger. There are two distinct "families" of basic data types: integer and floating point.

Integer Types

The integer types all store non-negative numbers in binary, and negative numbers in "two's complement" form. (Recall that to obtain the two's complement of a number, you take the binary representation of the number, then switch all 0s to 1s and all 1s to 0s, and then finally add one). The difference between the integer types is simply the number of bytes used to store the data, which, in turn, affects the range of numbers that can be stored by that type. The complete list of integer types, in order from smallest to largest, is:

char short (also known as: short int) int long (also known as: long int)

Each char occupies one byte, while the size of int is system-dependent. The actual size of an int is related to the natural word size of the CPU. Since int computations are by far the most frequently performed operations, it is important that they be as fast as possible. On a computer with a 16-bit CPU, an int will usually be 16 bits (2 bytes) big, because using 32 bits for int would impose tremendous unnecessary overhead in most situations. On 32-bit CPUs, however, 32-bit computations and 16-bit computations take the same amount of time, so ints are usually 32 bits (4 bytes) big to take advantage of the larger potential range.

On a platform like a Pentium-based PC, which has a CPU that can operate in either a 16-bit mode (for compatibility with older programs) or a 32-bit mode, the size of int is determined by what kind of application you are compiling. If you are compiling a DOS or Windows 3.1 application, which will run in the 16-bit mode, then ints will be 16 bits big, and if you are compiling a Windows 95 (or later) application, then ints will be 32 bits big.

The data type, short, is somewhere between char and int (possibly the same as one of them) while the data type, long, is as big or bigger than int. It is at the discretion of the compiler what the actual sizes of these two will be.

Part 1 (More C) Page 21

Page 24: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Typical sizes and ranges for a 16-bit CPU are:

char - 1 byte (-128 to +127) short - 2 bytes (-32768 to +32767) int - 2 bytes (-32768 to +32767) long - 4 bytes (-2147483648 to +2147483647)

while typical values for a 32-bit CPU are:

char - 1 byte (-128 to +127) short - 2 bytes (-32768 to +32767) int - 4 bytes (-2147483648 to +2147483647) long - 4 bytes (-2147483648 to +2147483647)

Be aware that a given CPU may have different settings than either of these. In the near future, 64-bit CPUs will be confusing things further, although it is likely that only long will change (to 8 bytes) from the 32-bit situation.

There is enough 16-bit development still going on that you should pay attention to situations where you use int. If you expect a value to fall outside the range for a 16-bit int (roughly + or - 32 thousand), then it is better to use long, rather than int, even if you are working on a 32-bit platform. On your platform, you will incur no penalty for using long (since long is the same as int), and should you ever need to port your program to a 16-bit machine, you will save a potential major headache. Similarly, if it is important that a value only occupies 2 bytes (for space reasons), then you should probably use short, rather than int.

Besides capacity and space, the various integer types differ in how you represent constant values. A "normal" numeric constant, such as 56, -203 or +33, is an int, while a char constant is usually a single character (or backslash sequence) contained in single quotes, such as 'A' or '\n'.

Other forms of int constants are to specify the constant in octal, rather than decimal, by preceding the number with a leading zero (which is something COBOL programmers, who are used to being forced to include leading zeros, should take particular note of). Hexadecimal int constants can be specified by beginning the number with a leading 0x (or 0X). The "alphabetic" (A-F) digits of a hexadecimal value can be typed in either upper or lower case.

The following four int constants are all the same:

27 033 (octal) 0x1b (hexadecimal) 0X1B (also hexadecimal)

Part 1 (More C) Page 22

Page 25: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Similarly, character constants can be specified in octal by using a backslash sequence containing an octal number, or hexadecimal, by using a backslash sequence beginning with \x (or \X). For some reason, there is no facility for a character constant to be expressed directly in decimal. For example, the following char constants are the same:

'A' '\101' (octal) '\x41' (hexadecimal)

Appending the character l or L (L is preferred, since it doesn't look like a one) to the end of an integer is how you specify a long constant, such as:

875462L 0156732L (octal) 0xA005FL (hexadecimal)

Bear in mind that some compilers will act as if you had put an L when you use an int constant that doesn't fit in an int (such as 876462 on a 16-bit machine), while others will truncate the value so that it does fit in an int. This inconsistency makes it important to use long constants for any values that don't fit in an int on the smallest target platform, regardless of how your current compiler operates.

For some reason, there is no facility for making a short constant. If it is important to have a short constant (it rarely is), you would have to cast a char or int constant to short.

Unsigned Integers

Each of the integer types may be preceded with the keyword unsigned. Such values do not use the leading bit of the number to identify whether the number is positive or negative, but rather treat all the bits of the number as part of the number. Unsigned values may never be less than zero, but the range is the same as for signed values. For example, while signed char values can range from -128 to +127, unsigned char values can range from 0 to 255.

Unsigned integer types are useful when you know a value will never be negative. For example, on a 16-bit machine, if you know a value may range between 1 and 50,000, you can use unsigned int for that value even though you can't use int (because ints only go up to +32,767). This will result in much faster computations than using long.

Unsigned int constants look like regular int constants, except that they may not have a leading + or -, and they end with u or

Part 1 (More C) Page 23

Page 26: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

U, such as 34987U. Unsigned long constants end with ul or UL. Char constants, such as '\213' (i.e. larger than 128), will be treated as signed, and you'd need to cast them to unsigned char if you want to treat them as unsigned.

A related keyword is signed, which can be combined with an integer data type to indicate that it is signed. Since almost all the integer data types are signed unless unsigned is specified, the only practical use for signed is in combination with char. Unfortunately, whether char is a signed or an unsigned data type is not specified in the language standard, and therefore varies from compiler to compiler. If it is important for you to have a character data type that stores numbers between -128 and +127, then it is best to use signed char, rather than just plain char, in case you compile the code on a machine where the C compiler makes char an unsigned type. Conversely, if you need a char type which stores numbers between 0 and 255, then it is best to use unsigned char. Fortunately, most of the time when you are using character data, it doesn't matter whether numerically it is considered signed or unsigned, and so usually just plain char is sufficient.

Floating Point Types

Floating point types store numeric information in a format similar to scientific notation. Recall that in scientific notation, 123.5 is written

2 +1.235 x 10

where 1.235 is called the mantissa, + is the sign, 10 is the base and 2 is the exponent. While the actual details of floating point formats vary from processor to processor, they always store the sign, mantissa and exponent, relying on the "floating point unit" (which may be hardware or a software module) to fill in the base and put things together. The base is always fixed, and is typically either 2 or 16 (but rarely is 10). The sign occupies one bit of the value, the exponent typically occupies another seven or eight bits, and the remainder is used for the mantissa.

There are three floating point data types:

float double long double

and, usually, the only difference between them is the number of bits used to store the mantissa. As with the integer types, float is the smallest, long double is the largest and double falls somewhere in between. On many machines, floats occupy 4

Part 1 (More C) Page 24

Page 27: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

bytes, while doubles and long doubles are the same at 8 bytes each. (Some newer floating point units allow for 10-byte long doubles).

In a situation where the exponent occupies 8 bits, a 4-byte float would then allow 23 bits for the mantissa, while an 8-byte double would allow 55 bits. Since the exponent is the same size, the range of values stored by the different floating point types is the same. Where they differ is in terms of accuracy. The typical float type is accurate to only 5 or 6 significant decimal digits, while the typical double is accurate to 12 or 13 significant decimal digits. This means that while double is useful for many currency applications, float usually isn't. However, for scientific or graphics use, such accuracy may not be important, because the raw data itself is accurate only to 4 or 5 significant digits, and the space savings (and sometimes speed) of float may be valuable.

Floating point constants are always specified in decimal, and may either use a decimal point or the letter e (or E), or both. The e, if present, stands for "times ten to the power" and is followed by an integer specifying an exponent for 10. (Note that the internal floating point format will probably use a different base). For example, the following double constants are all the same:

123.5 1235e-1 1.235e2

the default for these constants is double. A trailing f (or F) indicates a float constant (e.g. 1.56f), while a trailing L (or l) indicates a long double constant (e.g. 2.5234123e45L).

Casting and Passing

Values of one of the basic types may freely be assigned and compared to values of the other basic types, with the compiler automatically performing the necessary casts. (Of course, you are encouraged to put your own explicit casts in the code, when you are performing a questionable cast, or when you are not happy with the way the compiler would have automatically casted).

Generally, the compiler will cast "up", from a smaller type to a larger type, rather than "down", which may result in loss of information, whenever possible. For example, in the expression

(x < 5.6)

where x is an int variable, x will be cast to a double, which will then be compared to 5.6, rather than truncating the 5.6 to

Part 1 (More C) Page 25

Page 28: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

an integer. However, in some situations, such as

x = 5.6;

casting the smaller item (x in this case) is not an option, and the larger value will be cast, possibly resulting in loss of information (in this case x will become 5, not 5.6). Some compilers will generate a non-fatal warning message in these situations, and putting in your own cast, such as

x = (int)5.6;

will make the warning message go away.

There is a curious historical-based oddity about passing the basic data types between functions in C. While C++ compilers allow you to pass all the basic data types to and from functions, C compilers only allow passing of int, long, double and long double. Values of types char and short are always promoted to int when you pass them. If the receiving parameter is char or short, then the int that was passed will be "demoted" once it is received by the function. Similarly, float values are promoted to double when they are passed.

When using parameter passing in C, then, you gain nothing by making a parameter or return value char or short or float. This is why you'll see that many of the standard library routines are passed an int in situations where you might think they should have been passed a char, and also explains why, in printf, you can use either %c or %d to display a char, and why %f and %lf work equally well with double values. Note that this process of promotion does NOT apply to pointers, which is why, in scanf, you cannot use %d for a char or %f for a double.

Multi-Dimensional Arrays

Recall that, to define an array of, say, 20 ints, you would:

int data[20];

where "data" is the name for the array, and the 20 ints are data[0], data[1] and so on, up to data[19]. Each of these array elements can be treated as any other int variable would be treated. To pass the entire array to a function, say foo(), you pass the name of the array, as in:

foo(data)

where the header line for foo() would be something like:

void foo(int x[])

Part 1 (More C) Page 26

Page 29: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Within foo(), the variable name x then refers to the same array that data refers to in the calling program, and any changes that foo() makes to the elements of x will therefore be seen in the elements of data once foo() is finished. Note that the number of elements of x is not specified (and would be ignored by the compiler if you did specify it). This allows the function to be passed different size arrays at different times.

You can also create and pass two-dimensional arrays, which we usually think of as a table, with rows and columns. For example,

int table[3][5];

defines an array, named table, with three rows and five columns. This definition looks like an array of three arrays of five elements each, and indeed, it is. To access a particular int element of the array, you use the expression

table[r][c]

where r is between 0 and 2 and c is between 0 and 4, just as you would any other int variable. You may also use

table[r]

where r is between 0 and 2, just as you would use any other 1-dimensional array. Here, table[0] is an array of 5 ints (the first row of table), as is table[1] (the second row of table) and so on. You may pass the entire array to a function, say bar(), by passing its name:

bar(table);

The header line for bar() would be something like:

void bar(int y[][5])

where y, within bar(), will refer to the same array as table does in the calling program. Note how we specified the number of columns for y. We have omitted the number of rows, for the same reason we omit the number of elements when receiving a 1-dimensional array, but we must specify the number of columns, so that the compiler will be able to compile an expression like

y[2][3]

2-dimensional arrays are stored in memory (which is a 1-dimensional device) one row after another. To get to column 3 of row 2, it must go past rows 0 and 1 to get to the start of row 2, before it can find the 4th element of this row. In order to skip the first two rows, the compiler must know, then, how big each row is. Since C does not store the size of the array as

Part 1 (More C) Page 27

Page 30: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

part of the array, it is up to the program to specify the size somewhere. In the case of a 2-dimensional array received from another function, the mechanism used is to require that the number of columns be specified in the declaration of a 2-dimensional array parameter.

In fact, an array in C may have as many dimensions as you like, where you use a new set of square brackets for each new dimension. When receiving such an array as a parameter, you must hard-code all sizes except the very first dimension. For example,

int a[3][4][2][5];

declares an array, a, to be an array of 3 arrays of 4 arrays of 2 arrays of 5 ints each, a 4-dimensional array. To pass a to a function, say, foobar(), we would:

foobar(a);

where foobar()'s header line would be something like:

void foobar(int z[][4][2][5])

Some people like to think of a 3-dimensional array as a book, with pages containing rows containing columns, and a 4-dimensional array as a library, with books containing pages containing rows containing columns, but this sort of analogy starts to fall apart as you get to higher dimensions. No matter how many dimensions you have, however, the last dimension is always columns, the next-to-last is always rows, and the sizes for all dimensions except the first must be specified in a parameter declaration.

Addresses and Pointers

Recall that the operator & takes a variable as an operand and returns the memory location or address of that variable. The & operator is typically read "address of". Recall also that if you have an address, you can access the contents of memory at that address by using the unary operator *, with the address as the operand. The * operator is said to "resolve" the address, or to "de-reference" the address, and is typically read "data at".

A pointer is a variable which stores an address. Pointers are declared, in what some people think is a backwards fashion, using *, as in

int *ptr;

which defines a variable, named ptr, that will "point to" an int. Literally, this declaration states that "int is the data

Part 1 (More C) Page 28

Page 31: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

type of *ptr", and since *ptr is what ptr points to, it follows that ptr must be a pointer to an int.

The most common use of addresses is to pass variables in C in such as way that the calling function can change them. The only parameter passing mechanism in C is "pass by value", where the argument (the actual value specified when the function is called) is copied into the parameter (the corresponding variable defined in the header line of the function). A program can allow a function to change a variable simply by passing the variable's address to the function, which will resolve the corresponding pointer in order to access the original variable. The passing mechanism here is still "pass by value", since the value being passed is a address which is copied into the pointer parameter. It is the program code itself that must compute the address in the first place (by using &, for example), and resolve it (using, say, *).

In C++, of course, there is a "pass by reference" mechanism, whereby a variable can be passed in such a way that the calling program can change it. (Remember that this is done simply by preceding the parameter name by an & in the header line of the function - the parameter then becomes a reference to the original argument, rather than a copy of it). Pointers are not as necessary in C++ as they are in C because of this, although there are still places where pointers can be useful, in situations that are more complicated than simply "passing a variable so that it can be changed".

Pointer Arithmetic

If you have an address, you can access other, nearby memory locations by using what is commonly called pointer arithmetic. Integer values can be added to or subtracted from addresses, resulting in other addresses. For example, if ptr is a pointer to an int variable, then

ptr + 5

is the address of an int variable five locations "past" where ptr points. Similarly

ptr - 2

is the address of an int variable 2 locations "before" where ptr points. Note that what "past" and "before" mean is system dependent, although on most systems, if a global variable, a, is defined before another global variable, b, then a will be located "before" b in memory.

Note also that the size of one "location" is dependent on the size of the kind of data that is being pointed to. If we are

Part 1 (More C) Page 29

Page 32: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

working on a 16-bit CPU, then

ptr + 5

will point 10 bytes past where ptr points, since each of the 5 locations will occupy 2 bytes. On a 32-bit CPU, the same expression will point 20 bytes past where ptr points. In a similar vein, if we had a pointer which pointed to a structure, then the size of each location would be the size of one instance of the entire structure.

Pointers and pointer arithmetic are very powerful tools. They can be used to do things, such as access hardware that is physically installed to look like memory (this is called "memory-mapped hardware"), which are impossible in most high level languages that do not support pointers. But they can also be the cause of some of the most hard-to-find bugs in a program. Since it is very simple, using pointer arithmetic, to move around in memory, it is very easy to damage variables (or perhaps program code itself) that is completely unrelated to the variable whose address you had originally passed. Extreme caution is always necessary when using pointers.

Pointers and Arrays

No matter what compiler you use, arrays in C (and C++) are always laid out in memory the same way. As an example, consider the 3-D array

int x[3][4][2];

which has 3 "pages" or "planes", of 4 rows and 2 columns each. The entire first page (x[0]) appears first in memory, before the second page, which in turn is before the third page. Within each page, the entire first row (e.g. x[0][0]) appears before the second row, and so on. Within each row of each page, the first column (e.g. x[0][0][0]) appears before the second, and so on.

Thus, the order of the 24 elements of x in memory would be:

x[0][0][0], x[0][0][1], x[0][1][0], x[0][1][1], x[0][2][0], x[0][2][1], x[0][3][0], x[0][3][1], x[1][0][0], x[1][0][1], x[1][1][0], x[1][1][1], x[1][2][0], x[1][2][1], x[1][3][0], x[1][3][1], x[2][0][0], x[2][0][1], x[2][1][0], x[2][1][1], x[2][2][0], x[2][2][1], x[2][3][0], x[2][3][1]

If we had a variable

int *ptr = &x[0][0][0]

which points to the first element of the array x, then

Part 1 (More C) Page 30

Page 33: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

ptr + i * 4 * 2 + j * 2 + k

works out to be the same as

&x[i][j][k]

(For example, consider x[2][2][1]. Counting in the list above, it is 21 locations past x[0][0][0], and 2 * 4 * 2 + 2 * 2 + 1 is 21).

The same sort of thing applies no matter how many dimensions an array has, and it turns out that pointer arithmetic can be used, instead of array indexing, to access the elements of an array.

Let us go back to a single dimensional array, such as

int y[10];

and suppose we also have

int *py = &y[0];

Since

py + i

is the same as

&y[i]

it follows that

*(py + i)

is the same as

y[i]

and so you can always use pointer notation as an alternative to array indexing notation. In fact, the name of an array without any indexing brackets (y, in this case) is actually defined to be a pointer to the beginning of the array, so that

y[i]

and

*(y + i)

are equivalent. Also, if y were passed to a function, the receiving parameter could be declared either as

Part 1 (More C) Page 31

Page 34: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

int a[]

or

int *a

One consequence of all of this is that any time you have a pointer, it might point to a single variable, or it might point to the first element of an array. It is the logic of your program that will determine which of these two cases it is - C cannot tell the difference.

While pointer arithmetic can have serious negative effects on the readability of your code, it can result in faster execution. For example, compare these two versions of the library routine, strcpy:

char *strcpy(char to[], char from[]) { int i = 0; do { to[i] = from[i]; } while (to[i++] != '\0'); return to; }

and

char *strcpy(char *to, char *from) { char *temp = to; while ((*to++ = *from++) != '\0') ; return temp; }

The first version is probably much easier to read and understand, but the second version has some important efficiency benefits. Each time through the first version's loop, there are three array indexings, one variable increment, one assignment and one comparison. Each time through the second version's loop, there are two increments and two pointer resolutions, one assignment and one comparison. We have just seen that each array indexing is the equivalent to a multiplication, an addition and a pointer resolution. On most CPUs, an increment is faster than a generic addition, and multiplication is one of the slowest executing instructions. This makes the second version much faster (easily twice as fast, if not more) than the first.

(Note that many programmers will omit the "!= '\0'" in the condition for the loop in both versions. Remember that it is unnecessary to ask if a value is non-zero in C - non-zero values are automatically true by themselves. Putting a redundant test

Part 1 (More C) Page 32

Page 35: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

to see if a char is not the null byte may help readability, however, and will prevent a warning message from overzealous compilers that see a loop

while (*to++ = *from++) ;

and report that you have a "possible unintended assignment", when what we really have is a very intentional assignment.)

That being said, do not become obsessed with these kind of efficiencies. In general, even though such routines may be able to be doubled or tripled in speed, the overall effect on the program's speed is usually negligible, since these sort of routines are often just a small part of what is going on. The usual trade-off these days is to prefer readability over efficiency, since programmer time is much more costly than CPU time. However, in situations where you have identified certain areas of the program as being performance bottlenecks, this sort of technique can help to avoid having to resort to assembly language, which is even less readable and definitely less portable.

Moving back to multi-dimensional arrays, if we have an array, say,

int x[3][4][2];

then x (without any brackets) is a pointer to the start of the array, but it is NOT a pointer to the first int value in the array. Rather, it is a pointer to the first 2-D array in the array. (After all, x is an array of arrays of arrays of int). The size of each of these 2-D arrays is the size of 8 ints, and so

x + 1

is a pointer to the second 2-D array, 8 ints worth of data past x. To express

x[i][j][k]

in pointer notation, starting with the pointer, x, we get:

*(*(*(x + i) + j) + k)

Since x + i is a pointer to page i, *(x + i) is page i, which is a 2-D array, hence a pointer to the first of many 1-D arrays. Adding j to this moves forward by j of these 1-D arrays, and resolving that new pointer results in a pointer to the int at the start of row j on page i. Adding k to this last pointer moves forward by k ints, and by resolving that we obtain the int

Part 1 (More C) Page 33

Page 36: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

element we wanted.

Compare this to the earlier calculation we did with a pointer to an int, where we had to include the number of rows and columns in our calculations. Now, because we are working with pointers to arrays, the compiler, knowing how many rows and columns there are, will put those values in by itself.

Each time you use array indexing (i.e. with one set of square brackets), then, you are performing one pointer resolution, equivalent to one use of *. Conversely, you can view this same situation from the opposite point of view: every time you leave off one set of square brackets when using an array, you are computing a pointer to the start of the array, equivalent to one use of &.

Generic Pointers

The special data type

void *

is used to store an address, when we don't care what kind of data is being pointed to. Pointers declared this way are called generic pointers. A generic pointer cannot be directly resolved; you must cast it to a specific type of address before resolving it. In C++, where you may not mix types of addresses when assigning pointers, any kind of address can be assigned to a generic pointer without requiring a cast.

Generally, generic pointers are used in situations where you need a pointer which, under different circumstances, will store different kinds of addresses. Such situations must have some sort of logical context that will allow the pointer to be cast back to the correct type when it needs to be resolved.

Consider the following function, which is passed the address of some variable and the number of bytes occupied by that variable, and then "dumps" the variable by displaying its bytes in hexadecimal:

void dump(void *p, int size) { int i; for (i = 0; i < size; i++) printf("%02x ", ((char *)p)[i]); }

You can call this function, passing it any sort of pointer as long as the number of bytes (which you pass as the second parameter) is appropriate for that data type. For example, if x is a double variable and doubles are 8 bytes big, we could call

Part 1 (More C) Page 34

Page 37: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

dump(&x, 8);

to see the hexadecimal representation of the contents of x.

Pointers to Functions

You can have a pointer to anything that is in memory. Since the program code itself is in memory, you can have and use pointers to functions.

Remember that the name of an array, without the square brackets, is the address of the start of the array. In a similar vein, the name of a function, when you leave off the parentheses, is the address of the start of that function. If you have wondered why the compiler accepts a line like:

printf;

yet doesn't perform any output (think back to when you first started programming in C: this was probably something that you tried), it is because what that line is doing is computing the address of the function printf, and then not using that value for any purpose. It is not a syntax error, but it doesn't do anything useful, either.

To define a variable which will store the address of a function, you write what looks like a function prototype, except that instead of a function name, you put parentheses enclosing an asterisk followed by a variable name. For example,

int (*p)(int, char *, double);

declares the variable, p, to be a pointer to a function which is passed an int, a char pointer and a double, and which returns an int. As with a function prototype, you may include or exclude the local names for the parameters. When you first start making function pointers, you may wish to exclude them, as we have done, to avoid confusing yourself by having too many names in the same declaration.

On the other hand, if you have a pointer to a function, such as the variable p declared above, you can call the function by explicitly resolving the pointer, as in:

n = (*p)(5, "Test", 3.14);

or by taking advantage of the fact that a function's name is just the address of the function, so that a function pointer can therefore also be used as a function name (since it stores the address of a function):

Part 1 (More C) Page 35

Page 38: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

n = p(5, "Test", 3.14);

Function pointers can be useful in developing situations where you want to be able to perform the same sort of operation on different kinds of data (or different operations on the same sort of data) at different times, without having to code switch (or nested if) statements. This can lead to logic that can grow to include more situations without any modification, other than writing new versions of functions that can fit into the preexisting framework.

For example, the following program has a main loop that displays different pieces of data. Each piece of data has a display function associated with it (the program has an array of pointers to the data, and an array of pointers to the respective functions), and the main loop simply goes through these parallel arrays, passing each piece of data to its function. We could add new functions, and new types of data, to this program without changing the main loop's logic at all.

#include <stdio.h>

/* Various functions to display data */ void showint(void *p) { printf("Integer: %d\n", *(int *)p); } void showstr(void *p) { printf("String: %s\n", (char *)p); } void showdbl(void *p) { printf("Double: %.2lf\n", *(double *)p); }

main() { int n = 5, i; double d = 4.5; char s[] = "Test"; void *pdata[4]; /* array of pointers to some data */ void (*show[4])(void *); /* matching display functions */

pdata[0] = &n; show[0] = showint; pdata[1] = &d; show[1] = showdbl; pdata[2] = s; show[2] = showstr; pdata[3] = &i; show[3] = showint;

for (i = 0; i < 4; i++) show[i](pdata[i]); }

This program outputs:

Part 1 (More C) Page 36

Page 39: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Integer: 5 Double: 4.50 String: Test Integer: 3

This example is obviously over-simplified in order to show the use of the syntax in a reasonable amount of space with little else cluttering it up. More realistic situations, where function pointers are truly useful, are very complicated. For example, for doing Windows programming, Microsoft has a library of routines which do most of the "grunt" work of handling the details of the graphical user interface. You need to pass the addresses of your own functions (that perform your own detailed calculations) when calling many of the library routines, which in turn will call your functions when they decide it is appropriate to do so.

More on Structures

Recall that, in C, structures may contain only data members (often called fields), and that all these data members are publicly accessible. Also, remember that the keyword, struct, is required every time you refer to a struct type. (In C++, struct or class is only required the first time you declare the structure or class tag).

One feature of struct which you may not know is that the fields of a struct may be smaller than a byte, where the number of bits used for a field is specified in the struct declaration. Such fields, call bit fields, are usually of type unsigned, although most modern compilers also accept int, so that the field can be signed. The number of bits for the field is given following a colon after the field's variable name. For example,

struct status { unsigned leading:1; unsigned flag1:3; unsigned flag2:2; unsigned flag3:2; };

declares a structure type, struct status, which is one byte big and has four fields. The first field, leading, is one bit long, while the second field, flag1, is three bits long (and hence can hold a number between 0 and 7), and so on.

While bit fields can be used to maximize storage capacity of a struct (by using only as many bits as you actually need), they are more often used in system dependent code, particularly when communicating with hardware devices, which typically use individual bits of a word for different purposes.

Part 1 (More C) Page 37

Page 40: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Another feature of struct which may be new to you is the fact that you do not need to supply a structure tag when declaring the struct, if you are never going to need to use it. Such structures are called anonymous. For example, consider the structure

struct point { struct { int x; int y; } coords; int colour; };

which declares a structure, struct point, that stores the x and y coordinates, as well as the colour, for a point. The first field, coords, is itself a struct containing the x and y coordinates. As long as we are always going to pass entire struct point items, there is no need to give a name to this inner structure. For example, a program could declare two points, say

struct point a, b;

and could set the coordinates individually, such as

a.coords.x = 5; a.coords.y = 300;

or, if we have another point variable, both coordinates could be set at the same time, such as

b.coords = a.coords;

Unions

The keyword, union, works the same as a C struct, except that the fields of a union share the same position in memory, instead of appearing one after the other. The size of a union is the size of its largest field.

A union provides multiple ways of viewing the same memory location, and thus provides a safe alternative to taking the address of a variable and casting it to an address of a different type, in order to view the memory location differently. For example, the following function displays a double variable as a series of 1-byte hexadecimal numbers. While this is similar to something we had done earlier with pointers, this function does not directly "mess around" with memory locations, and therefore has less potential for being the source of a hard-to-find, but serious, bug:

Part 1 (More C) Page 38

Page 41: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

union dbl_as_char { double d; char c[8]; /* this assumes doubles are 8 bytes big */ };

void dumpdouble(double n) { union dbl_as_char x; int i; x.d = n; for (i = 0; i < 8; i++) printf("%02x ", x.c[i]); }

Like structs, unions may be anonymous. Since this union is only used once, we could localize its declaration and make it anonymous:

void dumpdouble(double n) { union { double d; char c[8]; } x; int i; x.d = n; for (i = 0; i < 8; i++) printf("%02x ", x.c[i]); }

Another typical application of a union is shown by the following structure:

struct transaction { int type; union { char name[31]; double amount; int code; } data; };

Here, we might use the type field to identify whether a particular transaction will use data.name, data.amount or data.code. This would allow us, for example, to have a file containing fixed length records (a technique we will be examining later), even though individual records might have different sizes. Of course, using this technique, each record will have to be the size of the largest possible record, but sometimes the advantages of fixed length records outweigh the disadvantages of some wasted space.

Part 1 (More C) Page 39

Page 42: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Enumerated Data

The keyword, enum, is used to create an enumerated data type, i.e. a data type that is an ordered list of named items, rather than having specific values. The declaration for enums is similar to the declaration for structs and unions, except that, instead of a field list, a series of comma separated names is supplied. These names then become constant values which can be assigned and compared to variables of this enum type.

For example, we could create an enum type:

enum colours { red, green, yellow, blue };

Then a function could declare variables of this type, such as

enum colours x;

to which we can do things like

x = blue;

or

if (x == red) ...

or even

for (x = red; x <= blue; x++) ...

The possible values of an enum type represent an ordered list, in that each value is considered to be one more than the previous value, which is what enables the "for" loop shown above. What enum really does is create a data type that is essential the same (except in name) to int, and the various values are automatically assigned increasing values, starting at 0. In this example, much the same thing could be achieved by using the data type int, instead of enum colours, and issuing the following:

#define red 0 #define green 1 #define yellow 2 #define blue 3

The only real advantages of enums, over int, are (1) because an enum type has a name, this provides some sort of "documentation" as to the purpose of the data (e.g. "enum colours" is more descriptive than "int"), and (2) the compiler automatically assigns the actual numeric values for the named values, so that for example, we could insert a new colour between red and green without having to change the numeric values of green, yellow and

Part 1 (More C) Page 40

Page 43: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

blue.

Enumerated types do not provide any checking that illegal values are assigned (for example, after the for loop shown above, x is one more than blue, which is not a valid colour).

Normally, the first value is assigned numeric value 0, and each subsequent value is one more than the value which precedes it. But it is possible to override the default numeric values, by specifying a number when the enum is declared, as in

enum colours { red, green, yellow = 10, blue };

In this case, red will be 0, green 1, yellow 10 and blue 11. If you do this, then x + 1 will not make sense when x == green. This facility is sometimes used as an alternative to issuing a series of #defines, rather than to create a true enumerated (hence ordered) list.

Creating Custom Data Type Names

The keyword typedef can be used to give a name to a data type, in case the data type is so awkward it causes readability problems. The syntax for using typedef is the same as the syntax for declaring a variable, except that the declaration begins with the word, typedef, and the name, which otherwise would have been a variable, becomes the new name for that type of data. For example,

typedef int *intptr;

makes "intptr" a new data type name, standing for "pointer to an int". We can then define variables such as

intptr x, y, z;

all of which will be pointers to ints. (Contrast this with what would have happened if we had done "#define intptr int *" instead of typedef - then only x would be an int pointer; y and z would be both be ints).

As another example,

typedef int (*funcptr)(int [], int);

makes "funcptr" the data type for a pointer to a function that is passed an int array and an int, and which returns an int. Also, C programmers typically use typedef when declaring structs, unions and enums, in order to avoid having to repeat the struct (or union or enum) keyword in later uses of the same data type. For example

Part 1 (More C) Page 41

Page 44: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

typedef struct abc_struct { int a; int b; int c; } abc;

makes "abc" a name which represents "struct abc_struct". In fact, this struct may be made anonymous; since we can refer to this struct type as "abc", we'll never need the tag "abc_struct" and can omit it from the declaration.

More C Operators

While you have seen the fundamental C operators for arithmetic (+, -, *, / and %), relational (<, <=, ==, >=, >, !=) and logical (&&, || !) purposes, as well as the assignment operators (=, ++, --, +=, *=, etc.) and a few other miscellaneous operators (&, *, [], ., ->, ?:, grouping using (), as well as casting using a data type enclosed in ()) there are many other useful operators available.

Before we look at new operators, however, there are some facts, concerning the operators you already know, with which you may not be familiar.

Logical Operators

Recall that the logical operators work with true and false values, and that in C, such values are simply integers (normally int) where 0 is used to represent false, and all other values represent true. Even though the logical operators, as well as the relational operators, return the value 1 when the result is true, you should avoid comparing logical expressions to 1 in order to see if they are "true", since they may have some other non-zero value. In fact, comparisons to "true" are totally unnecessary - if an expression has a non-zero value, it is already "true" as far as C is concerned, without having to further compare that value to anything. Similarly, comparing a value to see that it is "!= 0" is also redundant; the "!= 0" can always be safely removed.

Conversely, if you test something to see if it is "== 0", you can instead take the expression and apply the "not" operator (!) to it, since testing for equality to zero is the same as testing to see if something is false. In this case, you should probably let readability be your guide, since there is no guarantee which method will be more efficient (although on many machines, "!" is slightly faster than "== 0").

The || and && operators are designed to minimize the number of expressions that are evaluated, and consequently will not evaluate the right argument, if the left argument determines the

Part 1 (More C) Page 42

Page 45: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

result of the operation. For example, int the expression:

i < 40 && x[i] != y

if i is 40 or more, then the array index and comparison on the right size will not be evaluated, since the result of the && will be false regardless of whether x[i] is equal to y or not. Contrast this to the very similar, but potentially incorrect, expression

x[i] != y && i < 40

which might cause a memory fault if the array x has only 40 elements, and i happens to be 40 or more. (Note that if this second example doesn't cause the program to crash, then it will have the same value as the first example.)

Similarly, if you have an expression such as

foo(x, y) || bar(&y)

(presumably, foo() and bar() return true/false values), then bar() will not be called if foo() returns a non-zero value. The fact that bar() will not be called is significant if bar() changes the variable y, because it means that in this expression, y may or may not be changed.

Assignment Operators

The assignment operators (=, ++, --, +=, etc.) all return values - a copy of the value that was assigned. This can be used to perform an assignment as part of a larger expression. For example, if z is 6, then

x = (2 * (1 + y = z));

sets y to be 6 and x to be 14. Keep in mind that such compact code can be hard to read, hence hard to debug, will little or no gain in efficiency. On the other hand, something like

x = y = z = 0;

is a perfectly clear way to set x, y, and z to zero, and can save trees by reducing the number of lines in your source code (which may get printed out on paper at some point).

You should be very careful about using a variable elsewhere in a statement in which an assignment operator is applied to that variable. About the only safe situation is when the final operation is to assign a computation involving a variable back to the same variable, such as

Part 1 (More C) Page 43

Page 46: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

x = 3 * x + 1; /* perfectly safe */

Anything more complicated can lead to trouble. Consider:

x = (x = 3) + ++x; /* trouble! */

where, depending on whether the compiler evaluates the left side or the right side of the + operator first (an allowable variation between compilers) you may get a very different result in x.

The ++ and -- operators themselves are somewhat unique - they are the only unary operators in C which have two syntaxes, pre-fix and post-fix. Recall that the pre-fix use (the operator appears before the argument) will change the argument and return the new value, while the post-fix use returns the starting value of the argument and also changes the argument. For example, if y is 12 and z is 6, then after

x = --y + z++;

x will be 17 (11 + 6), y will be 11 and z will be 7. Since the ++ and -- operators don't look like assignments, it can be easy to write lines like

z = x++ + x; /* trouble! */

without realizing that you are tripping over the same sort of compiler-dependency discussed above.

Conditional Expression

The conditional expression operator (?:) is a unique operator in that it has three arguments. The first argument is a conditional expression, the second is an expression to be evaluated if the condition is true, and the third is an expression to be evaluated if the condition is false. Keep in mind that only one of the last two arguments is evaluated. The conditional expression can be very handy, as in

printf("%d item%s", n, n == 1 ? "" : "s");

(which would print out "0 items" or "1 item" or "2 items", etc.). But it detracts from readability, with no gain in reduced redundancy, if you use it in place of an if statement, as in:

n==1 ? printf("one item") : printf("%d items", n); /*yuck!*/

This would be better written, trees notwithstanding, as

Part 1 (More C) Page 44

Page 47: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

if (n == 1) printf("one item"); else printf("%d items", n);

In fact, the only reason why the compiler would accept the conditional expression version if this statement is that printf actually does have a return value (the number of characters that were output) which ends up simply being ignored in this case.

Casting

Recall that in C, an explicit cast is performed by placing the desired data type in parentheses just before the expression you wish to be converted to that type. (In C++, remember, you may also use the desired data type like a function name to perform a cast). This normally results in the compiler creating a temporary object of the desired data type. The item being cast is not itself changed.

Keep in mind that the casting mechanism is fairly simplistic, and in particular, knows nothing about strings. For example, if s is a char array containing the string "234", then

n = (int)s;

will not set n to be 234, but rather will be the result of casting the memory location of the array to an int. (Remember that an array name, without the square brackets, is an address).

Sizeof Operator

The keyword, sizeof, is actually a unary operator that returns the size of its operand in bytes. The operand may be an expression, or it may be a data type enclosed in parentheses. This can be used to find out how big objects are using a given compiler, as in:

printf("An int is %d bytes big\n", sizeof(int));

or can be used to remove compiler dependent sizes that you may have to place in your code, as in:

dump(&x, sizeof x);

to call a function we wrote earlier. The result of sizeof is actually a constant determined at compile time. If you ask for the sizeof an array, and the compiler knows the size of the array, then sizeof will return that size, rather than simply reporting the size of a pointer (which, strictly speaking, the name of an array is). For example, suppose you have an array

Part 1 (More C) Page 45

Page 48: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

char s[40];

then sizeof s will be 40. On the other hand, if you are inside a function declared as

void foo(char str[])

then sizeof str will be the size of a pointer, NOT the size of the array whose address was passed, since at compile time, the size of the array that might be passed into str is not known.

Another thing to note about sizeof is that sizeof a struct might be larger than simply the sum of the sizes of the individual fields. On some platforms it is important that certain data types begin at certain kinds of memory locations (for example, on some CPUs each int must start on a memory location that is a multiple of the word size for that CPU), and the compiler may need to insert some unused "filler" in between the fields of the struct, in order to arrange this.

Joining Two Expressions

The comma symbol (,) is used in C as an operator to join two expressions, in addition to its use in function parameter lists. This "join" operator evaluates both the expression on its left side and the expression on its right side, and then returns the value of the expression on the right side, discarding the result of the left side.

There is really only one place where this operator can be used in a way that doesn't negatively affect the readability of a program, and that is to enable a for loop to evaluate several initialization expressions or several "end-of-loop" expressions. For example, the following function reverses a string:

void reverse(char s[]) { int i, j; char c; for (i = 0, j = strlen(s) - 1; i < j; i++, j--) { c = s[i]; s[i] = s[j]; s[j] = c; } }

Bitwise Operators

There are a collection of operators for manipulating values at the individual bit (binary digit) level. These are collectively known in C as the bitwise operators. (Unfortunately, the more desirable term "binary operator" was already used to refer to an

Part 1 (More C) Page 46

Page 49: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

operator with two arguments, as compared to a "unary" operator). The bitwise operators are

& - bitwise and | - bitwise or ^ - bitwise XOR ~ - bitwise not << - shift left >> - shift right

The first three, &, |, and ^, take two arguments of the same type and return a result of that type. Each bit of the result is obtained by performing the specified operation on the corresponding bit from each of the operands. Details for each operation are:

& (and) - 1 & 1 is 1, anything else is 0 | (or) - 0 | 0 is 0, anything else is 1 ^ (XOR) - 1 ^ 0 and 0 ^ 1 are 1, 0 ^ 0 and 1 ^ 1 are 0

XOR, also known as "exclusive or", is 1 if the two bits are different, 0 if they are the same.

For example, 27 & 14 is 10. Looking at the binary values (where the 0... indicates a series of leading zeros)

27 is 0...00011011 14 is 0...00001110 ------------ 0...00001010 which is 10

Similarly, 27 | 14 is 31 (0...00011111), and 27 ^ 14 is 21 (0...00010101).

An interesting fact about XOR is that is is a self-inverting process, in the sense that if you do

z = x ^ y;

then z ^ x will be y (and also z ^ y will be x).

Bitwise not (~) simply returns a value that results from exchanging 1 bits for 0 bits and vice versa. For example, -1 is stored in 2's complement form as all 1s. ~(-1) is then 0. Similarly ~27 is -28.

The shift operators shift the pattern of bits in a value to the left or to the right. The left argument is the value to be shifted, and must be an integer type. The right argument is the number of bits to shift. Shifts to the left cause the left-most bits to disappear, and the resulting number is padded with trailing zeros. Shifts to the right cause the right-most bits to

Part 1 (More C) Page 47

Page 50: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

disappear, and the resulting number is padded with the sign of the number (0 if positive or unsigned, 1 if negative).

For example, 6 << 3 is 48. Looking at the binary values, 6 is 0...0110. Shifting 3 bits to the left, the three leading 0s are removed, and instead three trailing 0s are added, giving 0...0110000 or 48.

Similarly 27 >> 2 is 6 (0...011011 becomes 0...0110), and -33 >> 1 is -17 (1...1011111 becomes 1...101111).

Note that each bit a number is shifted to the left effectively multiplies by 2, and that each bit a number is shifted to the right effectively divides by 2. In time critical sections of code, where you need to do integer multiplications or divisions by a power of two, then bit shifting is vastly faster than multiplication or division, which are two of the slowest operations in most CPUs.

The most frequent use of the bitwise operators is to access the bits in a variable directly. For example, to set the last bit in the variable x to one, you can

x = x | 1;

or even

x |= 1;

Similarly, To set the next to last bit to 0, you can

x &= ~2;

Most programmers like to number the bits from 0, starting at the rightmost bit (i.e. bit 0 is the last bit, bit 1 is the next to last, and so on). Using this scheme, a general formula for setting bit n of x to 1 is:

x |= (1 << n);

and the formula for setting bit n of x to 0 is:

x &= ~(1 << n);

Similarly, an expression which tests to see if bit n of x is 1 is:

x & (1 << n)

Most programmers seldomly use the bitwise operators. They are the sort of operators that you don't usually need, but when you do need them, they are invaluable. Like the bit fields in a

Part 1 (More C) Page 48

Page 51: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

struct, they are used extensively in code that has to communicate directly with the computer's hardware, where typically every bit sent to the hardware matters. Other situations where fiddling with bits comes into play is in managing large volumes of data, where working with just as many bits as you need, and no more, can save tremendous amounts of space. As an example, consider images stored in a computer, where you usually store a certain number of bits - corresponding to a colour - for each "pixel" of the image. Every extra bit stored for each pixel allows the image to contain a larger variety of colours at the expense of a considerable amount of space. For a relatively small 100 pixel wide by 100 pixel high image, each extra bit per pixel works out to 10,000 bits, which is well over a kilobyte.

By the way, you should have noticed that the bit-shifting operators have absolutely nothing to do with input and output. When he developed the C++ language, Bjarne Stroustrup used these rarely-used operators to demonstrate the power of operator overloading, by making them perform input and output (when working with istreams and ostreams, not integers) rather than bit-shifting. Even though he warned against taking an operator and overloading it in a way that is not consistent with its intended purpose, he obviously felt that the bit-shifting operators were obscure enough that making an exception in this case would not seriously affect the readability of the resulting programs. Just keep in mind that even in C++, << and >> are bit-shifting operators, and their I/O uses with streams are implemented by the stream library routines, not as part of the language itself.

Control Flow

Moving away from operators on to other elements of the C language, we will now discuss control flow options. The main language elements that control logic flow in a program are, of course, the if, if/else, while, do/while and switch statements, along with the function-call/return mechanism.

Some other control flow keywords are break, continue and goto. The break statement, which you have seen to end a "section" in a switch statement, can also be used to immediately terminate the nearest loop (which may be a for, a while or a do/while loop). For example, the following function converts the first 30 characters of a string to upper case:

Part 1 (More C) Page 49

Page 52: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

void upper30(char s[]) { int i; for (i = 0; i < 30; i++) { if (s[i] == '\0') break; if ('a' <= s[i] && s[i] <= 'z') s[i] -= 'a' - 'A'; } }

The function's main loop should repeat 30 times. However, if a null byte is encountered in the string before then, the loop is terminated by the break statement instead of its controlling condition.

A word of warning goes with this use of break: good programming style dictates that nicely presented logic has a single entry point at the top and a single exit point at the bottom. This way, the normal indentation technique (where the body of a loop or selection statement is indented) clearly shows all logical dependencies that affect the flow of the program. When you use break to prematurely terminate a loop, you are making more than one exit point from the loop, and are hiding the fact that the following, unindented code is actually skipped. Most programmers would prefer to restructure the for loop in upper30() to be:

for (i = 0; i < 30 && s[i]; i++) if ('a' <= s[i] && s[i] <= 'z') s[i] -= 'a' - 'A';

which is both shorter and clearer than the version with break. It is always possible to structure a loop without having to resort to break, and generally, only lack of time or energy should prevent you from doing so.

Keep in mind that the use of break, within a switch statement, to end a "case" is necessary, and hence is not to be avoided.

The continue statement affects loops in a way similar to break. Rather than leaving the loop entirely, however, it jumps immediately to the bottom of a loop, ready to start the next iteration. For example, the loop above could be written:

for (i = 0; i < 30 && s[i]; i++) { if (s[i] < 'a' || 'z' < s[i]) continue; s[i] -= 'a' - 'A'; }

As with break, it is always possible to structure a loop without using continue, and avoiding continue is probably a good design goal.

Part 1 (More C) Page 50

Page 53: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

A third control flow statement is the most flexible of all, and is also considered one of the most "dangerous": goto. The goto statement allows you to jump immediately to some other statement of the function. To use goto, you must first put a label into the code somewhere. You do this by picking a unique name (i.e. a name you aren't using for a variable or function), and putting in that name followed by a colon (:). Then, anywhere else in the function you may issue the statement

goto LABEL;

(where LABEL stands for the name you'd used as a label). Control will immediately go to the statement immediately following the specified label. For example, we could rewrite upper30 as:

void upper30(char s[]) { int i = 0; loop: if (i == 30 || s[i] == '\0') goto done; if ('a' <= s[i] && s[i] <= 'z') s[i] -= 'a' - 'A'; i++; goto loop; done: }

While the if and goto combination is inherently powerful and simple (in fact, most machine languages use it as the primary flow control mechanism), history has shown that the price of this flexibility is the readability and maintainability of the resulting code.

You may have heard the term "spaghetti code". This refers to logic that jumps here, there and everywhere, and would look like a plate of spaghetti if you drew lines tracing the logic flow. Most programmers who extensively use goto invariably write spaghetti code. It is always possible to structure logic using if, if/else, while, do/while and switch without resorting to goto, and you should always do so. While goto avoidance doesn't guarantee that your code will not be a mess, habitual goto use almost guarantees that it will be.

The only benefit to goto is that in certain situations it may allow you to avoid one or two needless comparisons that a "better" logic structure might force the computer to do. (A comparison is, in most machine languages, one of the fastest executing instructions). Most programmers feel that this benefit is not worth the mess you get when you stop structuring your logic carefully, and gladly sacrifice the occasional machine cycle by avoiding goto entirely. The general feeling is that if

Part 1 (More C) Page 51

Page 54: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

the code needs to be so efficient that you feel you must use goto, then you should be working in assembly language for that piece of code, not C!

While we are discussing programming style, the return statement, used improperly, can lead to the same sort of readability and maintainability problems as break, continue and goto. It is considered good programming style for a function to have a single exit point, which will then obviously be at the end of the function. Functions which return a value should, following this guideline, have a single return statement at the very end, while functions with no return value should not have a return statement at all.

In many cases, programmers put multiple return statements in a function in order to avoid making a variable to hold the return value. This may be an example of false economy. Consider, for example, a typical debugging situation where you think the problem may be in a particular function. A common debugging technique would be to temporarily display some variables right at the beginning of the function, and some more right at the end of the function, so see how things progressed during the function's execution. If you have multiple return statements, you'll want to display things before each one (so that, among other things, you can determine which exit path is taken), and you'll start to wish you'd structured things so that there was a single return.

A somewhat different situation occurs if you have certain places in your logic where you can determine that something has happened which makes it impossible for your program to proceed. There is a library function, exit(), which is declared in <stdlib.h> and which terminates the entire program after closing all opened files. The exit() function takes an int value as an argument, and returns this value to the operating system. (Operating systems like MS-DOS and UNIX consider a return value of zero from a program as "normal completion", so an abnormal completion should probably return something other than zero). For example, you might code something like:

if (fp == NULL) { puts("Fatal error - cannot access file"); exit(1); }

You should only use exit() under very specific and catastrophic conditions. Casual use of exit() in general purpose functions almost always leads to programs that keep terminating when the user tries to run them - a very frustrating experience for a user. But when there is something that really does make it impossible to proceed, using exit() can help you avoid adding a lot of logic to terminate the program in a more structured way.

Part 1 (More C) Page 52

Page 55: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

If this is truly an exceptional circumstance, then, rather than losing readability by avoiding proper logic structure, you gain readability by avoiding a complicated logic structure which only supports a rarely encountered fatal set of events.

Declaration Modifiers

The keywords const, extern, static, auto, register and volatile may precede a declaration, and affect the nature of the declaration.

The const Modifier

A simple variable definition may be preceded with the word const to indicate that the variable's contents are constant and may not be changed. Such a "variable" must then be initialized (i.e. set when it is defined) since it cannot be changed later. For example,

const double pi=3.141592654;

makes a constant named "pi". This provides an alternative to using #define to do the same sort of thing. Most C programmers prefer to use #define because, with #define, the actual constant value is substituted into the code before it gets compiled, and the compiler is therefore able to avoid making a variable (along with the tiny overhead involved in accessing a variable from memory).

In C++, the rules change a bit, and const variables' values are substituted in-line whenever possible, giving the same efficiency as a #define, with the additional type-safety of a declared variable. So, in C++, const is preferred over #define for this purpose.

When declaring a pointer, the const keyword can be placed in different spots to specify whether the pointer is constant, or the location it points to is constant. For example, if a function parameter is declared as:

const char *str

then the data to which str points is constant and may not be modified. This is a very common technique used to protect the contents of an array when it is passed. On the other hand declaring

int * const p = &n;

indicates that p points to the (presumably int) variable n, and may not be made to point anywhere else. Of course, you can use const twice, once before the data type and again before the

Part 1 (More C) Page 53

Page 56: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

variable name, to make a constant pointer to constant data.

The extern Modifier

Preceding a declaration with word extern indicates that you are not defining anything, but rather are declaring a name that is defined elsewhere. Function prototypes are, by default, extern declarations, so the extern is not required before the prototype of a function which is defined in another source file. Variables are another story, however.

Recall that if you define a variable outside of a function, then that variable is called a global variable, and all subsequently coded functions may access that variable. If you declare a global variable in one source file, and want to access that global variable from another source file, then you use extern in that second source file. For example, suppose that source file "abc.c" contains the global variable definition:

int Counter = 0;

and suppose also that within another source file, "def.c", which will be compiled together with "abc.c", you wish to access this variable. In "def.c" you would have to declare Counter as

extern int Counter;

If you didn't put the word extern here, then "def.c" would actually create a second variable named Counter, and when the two object files are linked, the linker would complain about a duplicate name.

Keep in mind that many people consider global variables in general, and global variables that span several source files in particular, to be evidence of poorly structured logic, and should be avoided if possible.

The static Modifier

The keyword static may precede a variable definition, and gives that variable a "local" scope but a "global" existence. To be more specific, there are two distinct situations.

First, a global variable may be declared as static, which indicates that it should be considered local to the source file in which it is defined. In other words, if you precede a global variable definition with static, then no other source file will be able to use extern to access that variable, and no other source file will have a naming conflict if it tries to make its own global variable having the same name.

Second, a local variable may be declared as static. This

Part 1 (More C) Page 54

Page 57: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

indicates that the variable will exist globally, even though it can only be accessed within this function. In addition, if you initialize a static local variable, then that initialization will be done only once, when the program starts, rather than each time the function is called. If the function is called more than once, then the static variable retains its value from the last time it was called.

Static local variables can be very useful as an acceptable way to avoid a global variable in many situations. Consider, for example, the following function, which displays how many times it has been called:

void foo(void) { static int count = 0;

printf("This function has been called "); if (++count == 1) printf("once\n"); else printf("%d times\n", count); }

Static local variables can also be used when writing recursive functions (functions which call themselves either directly or indirectly), as a means for the different instances of the function currently in memory to communicate with the others. For example, suppose that the function foobar() calls itself somewhere in its logic, and that we know that the computer will crash if it calls itself more than, say, 5000 times, in one invocation. We could do something like

void foobar(void) { static int count = 0; ..the other variables, which may or may not be static..

count++; /* count this invocation */ if (count > 5000) { printf("Function nested too deep. Aborting\n"); exit(1); } ..the rest of the code, which sometimes calls foobar().. count--; /* done with this invocation, so un-count it */ }

to stop the program before it crashes the computer. In C, recursive programming is a potentially dangerous thing, and should be avoided if possible. The reason recursive programming is a problem is that the stack, the part of memory used for local variables, is of limited size and is normally not checked

Part 1 (More C) Page 55

Page 58: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

for room when a function is called. If a recursive function calls itself over and over again, more and more of the stack gets used for the non-static local variables with each call. If this happens more than a handful of times, it is likely that the stack will become exhausted, and whatever is in memory next to the stack will be overwritten. Since any recursive logic can be written non-recursively, you should probably do so unless you are certain the nesting will never become too deep.

Actually, this stack issue presents another use for static local variables. If you want a large local array, there is a good chance with many compilers that a large array could exceed the size of the stack. Some systems use a default stack as small as 8 KB. By making a local array variable static, however, the space for it comes out of another area of memory, usually called the heap, used for global variables, which is set up by the compiler to be as big as necessary (since the compiler knows at compile time how many global and static variables there will be). In general, it is a good idea to make any local variables more than a few hundred bytes big into static local variables, as a precautionary measure even if there is no evidence of trouble when you test the program.

Yet another use of static local variables is shown by the following function, which "returns" the name of the month, given the month's number (where 1 is January, etc.)

const char *month(int n) { static char months[][10] = { "bad month", "January", "February", "March","April", "May", "June", "July", "August", "September", "October", "November", "December"};

return months[1 <= n && n <= 12 ? n : 0]; }

Here, we are returning the address of a local variable. Normally, this would be a problem, since the variable would be gone by the time we returned. Because the variable is static, however, it will be in memory until the program finishes. Notice how we made the return type a "pointer to a constant character", so that the calling program will be prevented from accidentally changing month()'s local array.

The auto Modifier

The auto keyword is the opposite of static, and means that the variable being defined is to be automatically created and destroyed. It may only be applied to a local variable declaration, and is the default value for local variables anyway. Consequently, it is never used, and is an unfortunate

Part 1 (More C) Page 56

Page 59: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

waste of a keyword that would otherwise appeal to automobile enthusiasts. (When something is a keyword, you may not have functions or variables by that name).

The register Modifier

The keyword register may precede the declaration of a local variable, and tells the compiler that it would be nice if that variable could be kept in a register (one of the memory locations of the CPU itself) rather than in memory (which is much slower than a register). If possible, the compiler will then use a register for the variable. But be aware that the number of registers is very limited, and with most CPUs almost all the registers are used simply to keep the program running, so it is quite likely that a register request will simply be ignored by the compiler.

Still, in some time-critical operations on a given platform, making one or two variables into register variables may give enough of a speed boost to avoid having to rewrite some code in assembly language.

The volatile Modifier

Variable declarations may also be preceded by the word volatile. This tells the compiler that, regardless of any compiler optimizations that are performed, every time this variable is accessed by the source code, then its memory location should be accessed in the executable program. While this may sound like what should happen anyway, most compilers today actually look at the flow of operations, and will try to keep values in registers, avoiding memory accesses, as much as possible.

For example, in the following lines:

x = y * 12; y += 5; x += y; x++; y++;

the compiler, if it is smart enough, would have x and y loaded only once from memory into the CPU, and would write the new values back into x and y only once at the end when all the computations are finished. Normally, this sort of optimization should present no problems, and yet can give a tremendous performance boost, compared to a less clever compiler that would continually go back and forth to memory, doing the computations one step at a time. It is the level of sophistication of these sorts of optimizations, in fact, that makes one brand of compiler produce programs that are faster or slower than other brands on a given platform.

Part 1 (More C) Page 57

Page 60: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

If one of the variables is somehow tied to a piece of hardware (memory mapped I/O devices usually "look" like variables to a program), or the program somehow shares the variable with other running processes, then it may be important that every access of the variable causes a memory access. This is because the memory location used by the variable may be changing, at the same time that the program is running, due to these external influences. In this case the particular variable should be declared as volatile.

Generally speaking, if you ever need to use volatile, you'll know it, because the documentation for the functions you'd be calling to set things up would probably tell you exactly what you'd need to declare as volatile. If you don't think something needs to be volatile, it probably doesn't. Since making a variable volatile would most likely slow things down considerably, it is best not to do so unless you really need to.

The Real Syntax of Main

Although C programmers commonly pretend that main() is passed nothing and returns nothing, the "official" syntax of main() is:

int main(int argc, char *argv[])

The main() that you write is always passed an int, containing the number of items (or arguments) on the command line that were used to run the program, and an array of pointers to character strings - each string stores one of the items on the command line. The array, argv, will have argc + 1 elements, where argv[0] is the name of the program, argv[1] is the first command line parameter, argv[argc - 1] is the last command line parameter, and argv[argc] is always a null pointer (the pointer value 0, named NULL in many of the standard header files).

The value returned by main() is returned to the operating system which may use it. For example, a UNIX shell script may examine the environment variable $? immediately after running a program to get the number returned. Similarly Microsoft DOS/Windows batch files can use the IF ERRORLEVEL statement to test the value returned by a program, or the %ERRORLEVEL% environment variable can be examined. Other operating systems have similar capabilities.

Generally, a return value of 0 is used to indicate successful completion, while a non-zero value is used to indicate some sort of abnormal termination. However unless you actually write some operating scripts to use the return value, it usually doesn't matter what the return value is, which is why many programmers just ignore the issue and pretend that main doesn't return anything (which effectively returns garbage).

Part 1 (More C) Page 58

Page 61: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

The following program takes numbers as command line parameters and displays the total. If one of the parameters is invalid, however, an error message is displayed and a non-zero value is returned.

int main(int argc, char *argv[]) { double tot = 0, n; int i, ok, rc = 0;

for (i = 1; rc == 0 && i < argc; i++) { if (1 == sscanf(argv[i], "%lf", &n)) tot += n; else { printf("Invalid number %s\n", argv[i]); rc = 1; } } if (rc == 0) printf("%.2lf\n", tot); return rc; }

Variable Parameter Lists

If you've ever wondered why the printf() and scanf() functions have a variable parameter list, but none of your functions do, it is probably because you haven't investigated the <stdarg.h> header file, which contains some macros that can be used to write functions with parameter lists that can be decided at run-time.

In the header line of a function, you may end the parameter list with three periods (...), which tells the compiler that there is a variable list of parameters from this point on. There must be at least one fixed parameter. For example, in printf() and scanf(), the first parameter is a character string which then dictates what the remaining parameters will be. When writing the function, you may then access the parameter list by including <stdarg.h> and using the following names:

va_list - this is a data type for a variable you'll need to create which will be used to store the current position in the argument list.

va_start(a, b) - this is a macro, whose first parameter is the va_list variable you've created, and the second parameter is the last fixed parameter in the function header's list. You must call this to initialize things before trying to access the variable parameters.

Part 1 (More C) Page 59

Page 62: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

va_arg(a, b) - this is another macro. The first parameter is, again, the va_list variable you've created and initialized with va_start(). The second parameter is a data type - the desired data type for the next available parameter. This macro returns the next parameter. Note that your code must know the data type of the next parameter - there is no way to find out what was passed unless your code's logic make that possible.

va_end(a) - this macro takes the va_list variable you've been using and terminates the use of the macros. You should call this to clean up the parameter passing stack after you've accessed all the parameters.

For example, the following function, show(), has a variable parameter list, and allows the user to show a series of values of differing types on the screen. Each value must be preceded with an "enum type", which is:

enum type { NONE, INT, DOUBLE, STRING };

where INT indicates an int, DOUBLE a double, and STRING a character string. NONE is used to indicate that no parameter follows and we have therefore reached the end of the list to display. In the case of a double value, the desired number of decimal places must also be specified, after the "enum type" but before the value to be displayed. For example

show(INT,5,DOUBLE,3,3.1415926,INT,-67,STRING,"Hi",NONE); show(DOUBLE,1,4.5678,DOUBLE,4,5.0,NONE);

would display the lines

Integer:5,Double:3.142,Integer:-67,String:Hi Double:4.6,Double:5.0000

Note that the function will work incorrectly if we do not ensure that the requested data types match the given data, or if NONE is omitted from the end of the list. Here is the code for show():

Part 1 (More C) Page 60

Page 63: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

#include <stdio.h> #include <stdarg.h>

void show(enum type what, ...) { va_list argptr; /* needed for variable parameter list */

int firsttime = 1; /* is this the first item? */ int n; /* used for INT parameter */ char *s; /* used for STRING parameter */ double d; /* used for DOUBLE parameter */ int decimals; /* # of decimal places for DOUBLE */

va_start(argptr, what); /* set things up */ while (what != NONE) { if (firsttime) firsttime = 0; else putchar(','); switch (what) { case INT: /* integer value follows */ n = va_arg(argptr, int); /* get int */ printf("Integer:%d", n); break; case DOUBLE: /* # of dec places, then a double */ decimals = va_arg(argptr, int); /* get int */ d = va_arg(argptr, double); /* get double */ printf("Double:%.*lf", decimals, d); break; case STRING: /* pointer to string follows */ s = va_arg(argptr, char *); /* get string */ printf("String:%s", s); break; } what = va_arg(argptr, enum type); /* get next type */ } putchar('\n'); va_end(argptr); /* done with variable parameter list */ }

Part 1 (More C) Page 61

Page 64: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Part 1 (More C) Page 62

Page 65: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Part 2. C++ In More Detail

From now on, we will switch to C++. As programs get more complicated, the benefits of using an object-oriented approach increase. Although some of the topics we will be examining, particularly linked lists and binary file access, do lend themselves to implementation in C, C++ implementations will be shown in order to increase your exposure to object-oriented code.

Default Parameters

Because C++ allows a function to be overloaded as much as you like, it is possible to define various versions of a function so that it looks like there are "optional" parameters. But this requires making a new version of the function for each different way you could call the function.

There is a simpler facility for making parameters optional in a function's parameter list: when the function is first declared, put an equal sign, followed by the desired default value, after each parameter declaration you wish to make optional. Then, when calling the function, the calling program may supply fewer arguments than required, and the compiler will insert the default values you previously specified in the missing spots. For example, a function might be prototyped as:

int foo(int, int=5, int=0, int=12);

and later coded as

int foo(int w, int x, int y, int z) { return w + x + y + z; }

Then we could call foo, supplying one, two, three or four parameters. Some samples are:

foo(3) returns 20 foo(3, 1) returns 16 foo(3, 1, 10) returns 26 foo(3, 1, 10, 4) returns 18

Note that you may only omit arguments from the end of the list and not from the middle or the beginning. Thus, you should put the parameters more likely to be omitted after those less likely to be omitted. Also note that you can choose to make all parameters optional.

Part 2 (More C++) Page 63

Page 66: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Initializing Members Directly

Data members of a class may be initialized directly, in a manner similar to the way the base class can be constructed explicitly in a derived class, rather than just having default members created which are then reset in the constructor. For example, consider the following fragment of a class:

class item { int qty; double cost; public: item(int q, double c):qty(q), cost(c) {} ... };

When an item is constructed, an int and a double must be supplied. The int is used to construct the qty member, and the double is used to construct the cost member. Note how, in this case, the constructor body is empty, since all we needed to do was initialize the data members. (The alternative, of course, would be to have the constructor itself set the data members: {qty = q; cost = c;}). Note that you can initialize some of the data members explicitly, while setting the rest in the body of the constructor.

When data members are instances of a class, it is more efficient to initialize them directly than to just let them be created using their default constructors and resetting them in the containing class' constructor. Direct member initialization also allows you to have data members which are instances of classes that have constructors but no default constructor.

The bool Data Type

While any integer data type can be used as a condition in C++, relatively recent additions to the language have included a new data type, bool, which is intended to store a condition. A bool variable stores only one of two values, true or false, both of which are keywords in C++.

While the size of a bool is compiler dependent, typically a compiler will use one byte to store a bool value, even though only one bit of that byte is actually needed. The major benefit of using bool is readability - when a variable is declared as bool, you know it will store a condition, and setting a variable to true or false is much clearer than using 0 and 1.

Namespaces

Another relatively recent addition to the C++ language is the concept of a namespace. One potential problem with writing large

Part 2 (More C++) Page 64

Page 67: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

programs that make use of a lot of prewritten code is that there could be naming conflicts between global names in different parts.

For example, one library you want to use might have a class named foobar, and another library you want to use in the same application might have a very different class also named foobar, simply because when each library was written, the programmer involved had no knowledge of the other library.

A namespace is a way to group a collection of global names, such as function names, class names and global variable names, together under one name, so that they won't conflict with other global names, provided that the other global names are in a different namespace.

The idea is that if you write a collection of code that you intend to reuse in a number of different applications, then you should put all that code (classes, global variables and functions) into a namespace, so that the names you've chosen won't conflict with code written by someone else. Of course, if two namespaces have the same name, then the same old conflict potential arises for the names in those two namespaces, but at least the chances of this are drastically reduced.

Syntactically, a namespace looks much like a class. For example, you could make a namespace that has some handy utility functions like this:

namespace myutils { void uppercase(char *); void lowercase(char *); int getint(); double getdouble(); void getstring(char *, int); }

One difference between a namespace and a class is that the namespace doesn't have a semicolon (;) after the closing bracket. Since a namespace groups global things, there won't be instances of the namespace, and the trailing semicolon for a class is there to let you create instances of the class when you declare it.

Similarly, a namespace doesn't have constructors or destructors, and the concept of privacy doesn't apply to a namespace - everything is accessible.

If you later wanted to code, say, the uppercase() function, you would write:

Part 2 (More C++) Page 65

Page 68: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

void myutils::uppercase(char *s) { while (*s) { if ('a' <= *s && *s <= 'z') *s -= 'a' - 'A'; s++; } }

and whenever you wanted to call the uppercase() function, you would have to use the scope resolution operator (::) to specify the namespace as well as the function name, such as

myutils::uppercase(str);

which converts the string str to uppercase.

If later in the same source code you create another namespace with the same name, the effect is to add the new names to the original namespace. This is how the C++ standard libraries (not the C libraries), which are spread over many, many header files, manage to have almost everything in one namespace called std - each header does its own "namespace std" declaration.

Although it is nice to have a scheme to avoid global name conflicts, it is a pain to have to always use scope resolution, when actual name conflicts are a relatively rare thing. For this reason, there is a syntax to tell subsequent code to automatically use names from a namespace without requiring explicit scope resolution: the "using" statement.

For example, the using declarations

using myutils::uppercase; using myutils::lowercase;

tell the compiler that whenever the global names uppercase and lowercase are encountered, it should assume that they are in the namespace myutils. Alternatively, you can issue the using directive

using namespace myutils;

telling the compiler to assume that whenever it encounters a global name which is the same as one of the names in myutils, it should use the item from myutils. If you issue multiple using statements that cause the same name from different namespaces to collide, then you will need to use scope resolution when using that name.

Part 2 (More C++) Page 66

Page 69: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Forward Declarations

As the complexity of an object oriented program grows, it often becomes impossible to avoid the situation where the declaration for one class mentions another class, and vice versa. For example, if there are two classes,

class a { .... public: void foo(b); ... };

and

class b { .... public: void bar(a); ... };

there will be a compilation problem related to the fact that the data type b must be known before a's declaration can be compiled, and the data type a must be known before b's declaration can be compiled. Putting either class first will trigger the same sort of error.

This can be fixed by picking one of the classes to be fully declared first, and then making a forward declaration of the other one just before it. A forward declaration is telling the compiler that a particular name refers to a class, without giving the full details of that class, as follows:

class b;

Putting this before the declaration of class a above will allow the compilation of both classes. Note that this is possible because the declaration of class a doesn't need to know anything more about b than the fact that it is indeed a class. The full declaration of the class b will need to be done before we make any more detailed use of b, such as trying to use one of its members.

Linked Lists

You already know that dynamic memory allocation is a useful technique for allowing a program to use only as much memory as it needs, rather than grabbing, in advance, a large enough fixed-size block that can satisfy all reasonable situations.

Part 2 (More C++) Page 67

Page 70: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Typically, a potentially large array is allocated dynamically (using either the C++ new operator, or the C malloc() function), rather than having the array declared at compile time. Remember that this necessitates that your program remembers to deallocate (using delete in C++, or free() in C) such memory at a later point. Failure to do so could result in your program causing some memory to become unavailable for future use, a situation sometimes called "leaking" memory.

The problem with a simple scheme like this is that if, while the program is running, a dynamically allocated array needs to be made larger, the only way to accomplish this is to allocate an array of the larger size, copy over all the data from the original array, and then de-allocate the original. Since an array must be contiguous (i.e. all in one piece in memory), it is not possible simply to "make the existing array a little bigger", because the memory right after the original array is most likely already used for something else by the time you want to change the array's size.

But making an array larger by copying it into a fresh, larger array is very expensive, in terms of memory (it momentarily requires over twice the memory of the original array), and CPU time (copying a large array takes time). Various techniques have been developed to allocate memory in a slightly different fashion, to alleviate the overhead associated with growing (and shrinking) a dynamically allocated array while maintaining most of the benefits of array access (notably, the ability to write loops to process all the elements).

Most of these techniques differ in the kind of access they provide to the data being stored. None of them gives quite the same immediate direct access that an array provides (recall that an array index operation is just a little bit of pointer arithmetic and a pointer resolution). But the different techniques do provide reasonably fast access to the data in certain other ways, which might be good enough for a given application. For example, a stack lets you store a number of items, and then retrieve those items quickly in the reverse order that they were stored (like a stack of papers). A queue stores a number of items, and then lets you retrieve them in the same order that they were stored (like a line-up, or queue, to buy movie tickets). A linked list stores a list of items in order, allowing you to access them sequentially in that order, and to add or remove items at any point within the list. A binary tree (and the related b-tree and n-tree) lets you store data in a sorted fashion, providing reasonably quick access to an item by the key field.

All these techniques use the same fundamental principle: allocating memory one item at a time, and using pointers within each item to locate other items in the list. We will examine the

Part 2 (More C++) Page 68

Page 71: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

most commonly encountered of these data structures: the linked list.

The term node is used to refer to one item in a linked list. Each node contains a pointer to the next node in the list. This is how the list is stored in order. The list itself is typically comprised of just two pointers: one pointer to the start of the list (i.e. to the first node), and another pointer to the current node in the list. The "current" pointer is allowed to be changed, and is used to move around within the list. The list is traversed (i.e. you move through all the items in the list) by setting the current pointer to be the same as the start pointer, and then moving to each subsequent item by resetting the current pointer to be the pointer to the next node stored within the current node itself.

For example, the following picture shows how a linked list of two items, called "a" and "b", might look:

start data for a next node after a | +--------------+--+ \---------> | | \| +--------------+--\ | data for b next | +--------------+--+ \---------> | |XX| +--------------+--+

Here, the XX in b's next pointer indicates that there is no next node - b is the end of the list. Typically the NULL address is used for this purpose. Also, we haven't shown the "current" pointer, since it could point either to a or to b (or perhaps nowhere at all, if it were NULL).

Inserting a third item, named "c", in between a and b, would cause this picture to change into something like this:

start data for a next node after a | +--------------+--+ \---------> | | || +--------------+-|+ / data for b next /---------------------/ +--------------+--+ | /------> | |XX| | data for c next | +--------------+--+ | +--------------+--+ | \-> | | ------/ +--------------+--+

In this picture, notice how a and b did not have to be moved in order to allow c to be placed in between them in the list - the pointers themselves determine the order, not the location of the

Part 2 (More C++) Page 69

Page 72: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

nodes. This characteristic is precisely what allows the nodes to be allocated independently from each other.

We will create a linked list of items, where each item is comprised of a character string and a double, although any other class could be used:

class item { char nm[40]; double amt; public: item() { nm[0] = '\0'; amt = 0; } item(char s[], double x) { strncpy(nm, s, sizeof nm); nm[(sizeof nm) - 1] = '\0'; amt = x; } const char *name() const { return nm; } double amount() const { return amt; } };

[Note how we have made the return value from the string() function return a pointer to a constant array, so that the "protection" afforded by keeping the nm array private is not violated by returning a pointer to it: the calling program will be able to copy or display the string, but not set it.]

We could then build a node as follows:

struct node { item data; node *next; };

We have used struct (which in C++ is exactly like a class except that the default access is public: rather than private:), instead of class, to make it clear to the reader that this is a "data-only", C-style struct, and not a full-fledged object. The reason for making such a simple object for a node is that we are going to use a node only within the linked list code. All the "data protection" we'd normally put in an object won't be worth the effort, since we are going to use in in a relatively small, highly localized, amount of code.

Note how the "next" member is a pointer to a the same type of structure. There is no problem with this, although there would be a problem if the member were a node, and not a pointer to one (because then one node would be infinitely large, by containing a node which contains a node which contains a node, and so on).

We now have the building blocks to make a linked list. Our list will have the following member functions:

Part 2 (More C++) Page 70

Page 73: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

void gostart() - go to the start of the list. void gonext() - go to the next item in the list. bool end() - are we past the end of the list? item get() - returns a copy of the current item. bool append(item) - append the specified item after the current item. The new item becomes the current item. Returns success. bool insert(item) - inserts the specified item just before the current item. The new item becomes the current item. Returns success. void remove() - removes the current item from the list. The next item becomes the current item.

To facilitate moving backwards in the list (which we will need to do in order to insert an item before the current one), we will also write a member function

node *previous()

which returns a pointer to the previous node (i.e. the one just before the current node). Since this returns a pointer to some internal data, and is only intended for use by some of the other member functions, we will make this function private.

Since this object uses dynamic memory allocation to tie extra memory to itself while in existence, it is a good example of a situation where you should code a destructor (to ensure that all allocated memory is deallocated), a copy constructor (to ensure the object is properly copied - including the allocation of new memory for the copy) and an = operator (to ensure the existing data is deallocated before allocating new memory for the copy).

Here is the declaration for the linked list class:

class linkedlist { node *start; node *current; node *previous(); public: linkedlist() { start = current = NULL; } // initially empty linkedlist(const linkedlist &); linkedlist &operator=(const linkedlist &); ~linkedlist(); void gostart() { current = start; } void gonext() { if (current) current = current->next; } bool end() const { return current == NULL; } item get() const { return current ? current->data : item("not available", 0); } void remove(); bool append(item); bool insert(item); };

Part 2 (More C++) Page 71

Page 74: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Note, in this declaration, how all the very short functions are coded right in the declaration. This asks the compiler to substitute the necessary code in-line whenever the function is called, rather than compiling the function once and calling that code. In short, these functions will have the efficiency of a macro, although without the side-effects of a macro. The compiler is free to ignore such an in-line request, and most compilers will ignore the request if the code for the function contains a loop or is the least bit complex. It makes sense, then, only to code the very shortest of functions within the actual class declaration.

Here is the code for the other functions:

// A macro to allocate memory, setting the pointer // ptr to NULL if insufficient memory is available. ///////////// #include <new> #define NEW(ptr, type) try { ptr=new type; } \ catch(std::bad_alloc x) { ptr = NULL; }

// Returns a pointer to the node before the current node, // or NULL if there is no node before the current one. ///////////// node *linkedlist::previous() { node *prev = NULL; if (current != start) { prev = start; while (prev->next != current) prev = prev->next; } return prev; }

// Copy constructor duplicates the object being copied, and // makes the last node be the current node of the new list. ///////////// linkedlist::linkedlist(const linkedlist &l) { start = current = NULL; *this = l; // uses = operator below }

Part 2 (More C++) Page 72

Page 75: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

// Destructor removes all nodes that remain ///////////// linkedlist::~linkedlist() { gostart(); while (!end()) remove(); }

// removes the current node from the list, if there is // a current node, making the next one current. Does // nothing if there is no current node. ///////////// void linkedlist::remove() { if (current) { // anything to delete? node *temp = current; current = (current == start) ? // start of list? start = start->next : // advance start previous()->next = current->next; // jump over current delete temp; } }

// = operator empties the list before copying over all the // nodes from the source. Makes the last node current and // returns a reference to the current list when done. ///////////// linkedlist &linkedlist::operator=(const linkedlist &l) { // empty out the current list gostart(); while (!end()) remove(); // copy everything from l node *temp = l.start; while (temp) { append(temp->data); temp = temp->next; } return *this; }

// Appends x immediately after the current node, or at the // end of the list if current is past the end. Returns true // if successful, false if there is no more memory. The new // node becomes the current node. ///////////// bool linkedlist::append(item x) { bool success = false; node *temp;

Part 2 (More C++) Page 73

Page 76: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

NEW(temp, node) if (temp) { // allocation worked success = true; temp->data = x; if (current) { // current is a node in the list temp->next = current->next; current->next = temp; } else { // current is past the end of the... temp->next = NULL; // list, so just append to end if (start) // list is not empty previous()->next = temp; else // list is empty start = temp; } current = temp; } return success; }

// Inserts x immediately before the current node, or at the // end of the list if current is past the end. Returns true // if successful, false if there is no more memory. The new // node becomes the current node. ///////////// bool linkedlist::insert(item x) { bool success = false;

if (current == start) { // insert at start of list node *temp; NEW(temp, node) if (temp) { // allocation worked success = true; temp->data = x; temp->next = start; start = current = temp; } } else { // not at start of list current = previous(); success = append(x); } return success; }

Trying to follow this sort of code can be very confusing at first. Part of the problem is that there are all sorts of special cases, such as the list being empty, or the current node pointer being past the end of the list, which need special treatment, compared to the "normal" situation in the middle of the list somewhere. It is suggested that you try to draw

Part 2 (More C++) Page 74

Page 77: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

pictures, similar to the ones shown earlier, and observe what pointers you need to redraw in order to accomplish the operation being performed. Then relate your actions to the code shown above.

As an example of using this code, the following:

linkedlist l;

l.append(item("hammer", 7.95)); l.append(item("screwdriver", 5.00)); l.insert(item("nail", .10)); l.gostart(); l.insert(item("wrench", 4.50)); l.gonext(); l.append(item("hose", 9.99));

would put the items into the list in the following order: wrench, hammer, hose, nail, screwdriver, with hose being the current item. Similarly,

item i; l.gostart(); while (!l.end()) { i = l.get(); cout << i.name() << ": " << i.amount() << '\n'; l.gonext(); }

would display each item in the list, from beginning to end.

There are a number of possible variations on this theme. For example, you could write an insert function that would insert the node into the correct point in the list, so that the list is always kept in, say, alphabetical order by name, rather than simply inserting nodes near the current node. In this way the list can always be quickly traversed in alphabetical order.

Another possible variation is a doubly-linked list, which has two pointers in each node, one pointing to the next node, and the other pointing to the previous node. In such a list, inserting and removing nodes is a little bit more complicated, but moving backwards in the list is much faster than with the singly-linked list shown (where moving backwards one node requires traversing the list from the beginning).

In a similar vein, you can have a multiply-linked list, where each node has several different "next node" pointers. Each one could point to the "next" node in a different order. For example, one could point to the next node in alphabetical order by name, while another could point to the next node in numeric order by amount. This would allow fast sequential access by any

Part 2 (More C++) Page 75

Page 78: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

one of a number of key fields, although insertions into such a list would be somewhat intense.

File Streams

Recall that the C header file, <stdio.h>, declares terminal I/O (input/output) functions (such as printf() and scanf()) as well as almost identical functions (fprintf() and fscanf()) for performing file I/O.

Similarly, the C++ library has file I/O capabilities that are almost identical to istream and ostream (which are declared in <iostream>). The header file <fstream> declares a class called ifstream, which is derived from istream and allows "cin-style" input from a file. Similarly, the same header file also declared a class, ofstream, which is derived from ostream and provides "cout-style" output to a file. Both of these classes may be passed a file name (as a character string) when they are instantiated. The variables thus created will then access the file in the same way that cin and cout access the terminal.

For example, the following function creates a file, whose name is given by the first parameter, containing lines which contain a name from the names array, a semi-colon, and a double value from the amounts array. The last parameter indicates how many elements are in the arrays. The function returns a false value if it is unable to make the file:

#include <fstream> using namespace std;

bool makefile(const char file[], const char names[][41], const double amounts[], int size) { ofstream fout(file); bool rc = false;

if (!fout.fail()) { for (int i = 0; i < size; i++) fout << names[i] << "; " << amounts[i] << '\n'; rc = true; } return rc; }

It is not necessary to close a file stream - there is a destructor in the class that takes care of closing the file for you.

As an example of using an input file, the following function displays the contents of a named file, reading, and then echoing to the screen, one character at a time.

Part 2 (More C++) Page 76

Page 79: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

#include <iostream> #include <fstream> using namespace std;

void showfile(const char file[]) { ifstream fin(file); char c; if (!fin.fail()) while (!fin.get(c).fail()) { cout << c; } }

Binary File Access

Text file access is as simple as standard terminal access, but, like terminal access, has its share of limitations. For example, changing a line in the middle of a text file is problematical, because there is no requirement that lines of a text file has the same length. If you want to replace a line by something else, the replacement will have to occupy exactly the same number of bytes, or else the rest of the data in the entire file will have to be moved to accommodate the change. Because of this, it is usually easiest to change a text file by making a new file incorporating the change and then deleting the original file.

It is, however, possible to store data in a file in exactly the same way it is stored in memory when the program is running. This is called binary file access, and allows, for example, a double field in a record of a file to always occupy 8 bytes (or however large a double is on the computer in question), and to store the number to the full accuracy that the computer can handle. Binary access also makes it easy to enforce fixed record lengths, which allows the possibility of replacing records in the middle of a file without having to rearrange any of the rest of the file.

Although binary file access can be performed on input-only or output-only files, it is more commonly performed on files that are opened for both input and output. There is a third file stream class, called simply fstream, which allows both input and output to a file. The most common syntax for constructing an fstream is to pass two arguments: a string containing the file name, followed by an access control setting. The access control setting is a "bitwise or" of some of the following values:

Part 2 (More C++) Page 77

Page 80: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

ios::in - allow input from the file. ios::out - allow output to the file. ios::app - only write data to the end of the file (append), rather than anywhere. ios::ate - initial position should be at the end of the file, rather than the beginning. ios::trunc - if the file already exists, replace it with a new empty file. ios::binary - use binary access, not text access.

The ios::binary setting, if present, prevents the operating system-level conversions that would normally be applied when accessing text files. For example, in MS-DOS and MS-Windows, each line of a text file must end with a carriage return/line feed combination, because Microsoft interprets "carriage return" as meaning "go to the beginning of the line you are on", and "line feed" as meaning "go down to the next line, staying on the same column". Whenever a newline character is written to a text file, the newline character (which uses the ASCII code associated with the line feed character, by the way) is written as a carriage return, followed by a line feed. Similarly, a carriage return/line feed pair is read in from a text file as a single newline character.

In UNIX systems, such a translation is not required, since UNIX interprets "line feed" to mean "go to the start of a new line". But even in a UNIX system, there may be differences between text and binary access. For example, if a Control/D is encountered in a text file, it marks the end of the file (even if there is more data following), and causes an end-of-file condition rather than being read into the program as a character. (In MS-DOS, Control/Z is similar). Using binary access, however, no character value is special - each byte from the file is read into the program as one byte (and vice versa) with no translation whatsoever.

You have already seen the notation ios:: used before some constant values (such as ios::left, used with cout.setf()). The ::, as you recall, is used to explicitly state which class something is a member of. Normally, which class something is a member of is clear from the context, because the instance is mentioned along with the member. But when the instance is not supplied (such as when defining a member function outside of the class declaration), this scope resolution operator (::) is used to clarify things.

Well, it turns out that there is a class, named ios, which is part of the whole package of which istream, ostream, ifstream, etc. are a part. In fact, the ios class is the "common core" of all the input and output streams: they are all derived from it. There are many constant members of ios. Since constants never change, they are not actually stored with each ios instance

Part 2 (More C++) Page 78

Page 81: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

(only the variables are stored with each instance).

Unlike member functions, whose use requires an instance in order to know which set of member variables needs to be accessed, constant members do not need an instance of the class in order to be used. But if you wish to access a constant member of a class without specifying an instance of the class, then you must use the scope resolution operator to identify the class in question. Since an fstream is an instance of ios, if we had a variable f of type fstream, we could use either

f.in

or

ios::in

to refer to the constant member, in. The latter is preferred, since it can be consistently used in all situations.

Getting back to fstream construction, a couple of typical file access settings are:

ios::in | ios::binary

to read an existing binary file, and

ios::in | ios::out | ios::binary

to open a binary file for reading and writing. Unfortunately, [at the time this was written] with different compilers a new file may or may not be created if the named file does not already exist. A reliable way to open a binary file for read/write access, creating a new file if it doesn't exist, is to try to create the fstream using ios::in|ios::out|ios::binary. If this fails, first clear() the fstream and then try to open it again (using the open() member function, which takes the same arguments as the constructor) using the access setting

ios::in | ios::out | ios::trunc | ios::binary

Although you can use text-based I/O operations on a file opened for binary access, you typically want a more direct connection between the file and the program's variables than they provide. Instead, there are two simple member functions read() and write(), which are passed a pointer to some variable, and the size of that variable. They transfer the specified number of bytes directly between the file and the variable. The pointer is declared in the functions' headers as a character pointer, so a pointer cast is usually required. For example, if f is an fstream, then

Part 2 (More C++) Page 79

Page 82: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

char name[30]; f.read(name, sizeof name);

would read thirty bytes from the file directly into the variable, name. Similarly, if there were a double variable named x, say, then

f.write((char *)&x, sizeof(double));

writes the contents of x directly to the file. Note that, just as with the text access tools, the current position in the file moves as you read and write data.

Two more member functions that are useful for binary file access are seekg() and seekp(). (Generally, all functions involving reading are members of ifstream and all functions involving writing are members of ofstream, by the way. They are all members of fstream). These allow the program to move the position from where the next read (seekg()) or write (seekp()) will be done. The "g" in seekg refers to the position from where we will "get" data, and the "p" in seekp refers to where we will "put" data.

These "seeking" functions are usually called one of two ways: you can specify the exact byte position in the file to which you want to move (where byte 0 is the first byte in the file) just by passing a non-negative number. For example, suppose you are reading a file containing records that are 102 bytes each, and you want to go the start of the third record. You could:

f.seekg(204);

to get there directly (effectively skipping over the first two records).

Alternatively, you can specify a number bytes and a "position", which is one of:

ios::beg - the beginning of the file ios::cur - the current position in the file ios::end - the end of the file

to move the specified number of bytes (which can be negative, to "move backwards") from the specified position. For example, to move to the end of the file in order to append a new record:

f.seekp(0, ios::end);

and to move backwards 50 bytes from the current position:

f.seekp(-50, ios::cur);

Part 2 (More C++) Page 80

Page 83: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Note that if n is non-negative, then f.seekp(n) is the same as f.seekp(n, ios::cur).

Two more useful functions for binary access are tellg() and tellp(), which simply return the byte number of the current reading (tellg()) or writing (tellp()) position in the file. For example, if a file has 102 bytes per record, then

f.seekg(0, ios::end); long n = f.tellg()/102;

sets n to be the number of records currently in the file.

With the exception of the "tell" functions, these functions all return (a reference to) the stream variable that was used, so that you can check the state of the stream in the same statement as performing an operation, such as

if (f.read(name, 20).fail()) { cout << "Error reading file\n"; f.clear(); }

Remember that when a stream goes "bad" (fail() returns "true) then you must call the clear() member to clear the error state before attempting any more operations on the stream. You encountered this when you first learned to validate input using cin, but it applies to all streams.

We are now ready for a complete little application that allows the user to add, change and display records from a file. Each record contains a string (30 bytes) and a double. Note that for simplicity this program doesn't bother to validate user input, so be careful when you are running it!

#include <iostream> #include <fstream> using namespace std;

struct record { char str[30]; double num; };

void menu(fstream &); void enter(fstream &, long &, long); void change(fstream &, long); long display(fstream &, long);

Part 2 (More C++) Page 81

Page 84: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

int main() { fstream f("test.dat", ios::in | ios::out | ios::binary); if (f.fail()) { // try to create file if it wasn't there f.clear(); f.open("test.dat", ios::in | ios::out | ios::trunc | ios::binary); }

if (!f.fail()) menu(f); else cout << "Cannot open file test.dat\n"; return 0; }

// Prompts the user to add, change and display // records of the file "f" /////////////// void menu(fstream &f) { f.seekg(0, ios::end); long filesize = f.tellg()/sizeof(record);

cout << "There are " << filesize << " records on file\n"; char choice; do { cout << "Add, Change, Display or Quit? (a/c/d/q) "; cin >> choice; cin.ignore(2000, '\n'); switch (choice) { case 'a': case 'A': enter(f, filesize, filesize + 1); break; case 'c': case 'C': change(f, filesize); break; case 'd': case 'D': display(f, filesize); break; case 'q': case 'Q': choice = 'q'; // make sure it is lower case break; default: cout << "Unknown option: " << choice << '\n'; } } while (choice != 'q'); }

Part 2 (More C++) Page 82

Page 85: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

// Lets the user enter a record, and then places it // into record "recnum", of the file "f" which has // "fsize" records. If the record number is not in the // file, then a new record is added, increasing "fsize" /////////////// void enter(fstream &f, long &fsize, long recnum) { record r; if (recnum < 1 || recnum > fsize) recnum = ++fsize; cout << "Enter something: "; cin.get(r.str, sizeof r.str); cin.ignore(2000, '\n'); cout << "Enter a number: "; cin >> r.num; cin.ignore(2000, '\n'); f.seekp((recnum - 1) * sizeof r); f.write((char *)&r, sizeof r); }

// Lets the user display and re-enter a record // from the file "f" containing "fsize" records. /////////////// void change(fstream &f, long fsize) { long rn = display(f, fsize); if (rn) { cout << "Enter new values for this record:\n"; enter(f, fsize, rn); } }

// Displays a record from the file "f" containing // "fsize" records, after asking the user for the // desired record number. Returns the record number, // or, if an invalid record number is entered, 0. /////////////// long display(fstream &f, long fsize) { long recnum = 0; if (fsize) { cout << "Enter the desired record number: (1-" << fsize << ") "; cin >> recnum; cin.ignore(2000, '\n'); if (recnum < 1 || recnum > fsize) { cout << "Invalid record number " << recnum << '\n'; recnum = 0; } else { record r;

Part 2 (More C++) Page 83

Page 86: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

f.seekg((recnum - 1) * sizeof r); f.read((char *)&r, sizeof r); cout << "Record " << recnum << '\n'; cout << "String: " << r.str << ", Number: " << r.num << '\n'; } } else cout << "No records on file\n"; return recnum; }

As a final note about binary files, be aware that you should not attempt to view or print out a binary file as you would a text file. Programs and devices that are designed to display text are designed to show printable characters, and not all the byte values that might be in a binary file are printable. Rather, you should use a "dump" utility (or write a program) to view binary files.

Inline Functions

We have already mentioned that member functions defined within a class declaration will be substituted in-line when called (like a #define macro), rather than using the normal function calling mechanism, if the compiler deems it practical to do so. Actually, any function can be requested to be in-line, by preceding the function header line with the keyword inline. For example,

inline int max(int x, int y) { return x > y ? x : y; }

would ask the compiler to compile the logic for the max function in-line whenever it is called. The compiler is free to ignore an inline request if it decides that that the function is too complex to code in-line, so it is best to keep inline functions short and simple.

Note that the fact that a function is in-line must be declared before the function is first used, and the code for the function must appear in the same source file (or an #included file) as its use. Typically, inline functions are defined in header files for this reason, just like macros.

Inline functions have the efficiency benefits of macros, with the lack of side effects that macros may have. However, they also lack the flexibility of macros, since they have declared data types for the parameters and the return value, just like a regular function.

Part 2 (More C++) Page 84

Page 87: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Function Templates

Macros have two benefits over regular functions: efficiency due to the lack of function calling overhead, and flexibility due to lack of data type commitment until the macro is actually used. The primary drawback of macros is the possibility of side effects, which is a direct result of its combination of benefits! Inline functions in C++ sacrifice the flexibility to gain the efficiency without the side-effect risk. Templates, on the other hand, are a technique to gain the flexibility of macros without the side-effect risk.

A template is the layout for a function definition or class declaration, where everything is specified except for one or more data types. When you use the template, you must specify the missing data type(s), and the compiler will then use that information to build an actual function definition or class declaration. [You can think of a template as a big, multi-line macro which only allows the "parameters" to be data types.]

An example of a function template is

template <class t> t max(t x, t y) { return x > y ? x : y; }

What will happen after this template is coded is that if we use the function name, max, with two parameters of the same type (which we have called t in the template, but could be any data type), then a function max will be automatically defined with that particular data type substituted for the name t in the macro above. For example,

cout << max(5, 8) << ", " << max(3.4, 2.1) << '\n';

would make one version of max with "int" substituted for t, and another version with double substituted for t. (The data type involved could be a built-in type or a class). The output would be:

8, 3.4

Note that the data type MUST be used in the signature of the function, in such a way that the compiler can unequivocally determine the data type when the template is invoked.

More than one class may be involved. For example,

template <class type1, class type2>

Part 2 (More C++) Page 85

Page 88: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

type1 &operator+=(type1 &x, const type2 &y) { return x = x + y; }

will cause the compiler to make a += operator when += is used in a situation such as

x += y

wherever + is already defined between the type of x and the type of y, the = operator works correctly for the type of x, and yet the += operator has not been defined to handle this specific situation.

Note that if you have a template which would match a particular situation, but that situation has already been handled, then the existing code will be used and the template will not be applied. Templates are only applied if the compiler encounters a situation for which there is no existing exact match. For this reason, we do not have to worry about any conflict between our += operator template and any existing += (such as the built-in += between the built-in arithmetic data types): if += already works, our template will not be used.

Class Templates

You can also have the template for a class, where some of the data types involved in the class declaration are decided at compile time. For example,

template <class what> class accum { what data; public: accum(what x) { data = x; } accum<what> add(what x); what get() { return data; } };

is a template for a class which will store a piece of data (named "data") of some type (temporarily called "what"), which allows us to "add" other values of that same type to the stored value using the add() function, and to get the value currently stored using the get() function.

With a class template, you need to explicitly state the missing data type(s) when you use the class name in any situation other than the following three cases: - the name of the class immediately after the class keyword. - the name of the constructor(s). - the name of the destructor.

Part 2 (More C++) Page 86

Page 89: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

For example, the add function returns a copy of some accum object (presumably a copy of the current object) and we need to clarify what kind of accum object, which we do by using "accum<what>", i.e. an accum object storing the same kind of data as the current object.

We could instantiate different sorts of accum objects, such as

accum<double> x(3.14159);

where x stores a double value (initially 3.14159), and

accum<int> y(0);

where y stores an int (initially 0).

In this example, we have decided to code the add function outside the class declaration itself. The use of the template keyword only lasts for one function definition or one class declaration, so when we code the add function we will have to make a function template for it:

template <class type> accum<type> accum<type>::add(type x) { data = data + x; return *this; }

Here we have used a different name ("type" instead of "what") for the data type to emphasize the fact that these two templates are completely separate, even though they are related and both must be present. In practice, you would probably just re-use the same place-holder names for the data types in each of the function templates for a given class template.

Note that the code for the accum class we have shown assumes that the following are valid for the data type we use when constructing an accum object:

- the = operator works - the copy constructor works - the + operator works

If this is not the case, the template won't work correctly.

As a more complex example, consider the linked list class we developed earlier, which made a linked list of a specific kind of data that we called an item at the time. We could easily generalize this code to be a template for a linked list of any sort of object. In this case we will still use the name "item"

Part 2 (More C++) Page 87

Page 90: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

to be the type of data stored by the list, because it is a convenient and descriptive name, even though it will not necessarily be the simple item class previously shown.

We would make templates for both the node structure and the linked list class. The node requires little change:

template <class item> struct node { item data; node<item> *next; };

The linkedlist itself also requires little change. But because we had used a "data-type" specific constructor for the item class in the get() function (to return a "no-good" item), we will simplify that situation by using a default item (i.e. one made with the default constructor).

template <class item> class linkedlist { node<item> *start; node<item> *current; node<item> *previous(); public: linkedlist() { start = current = NULL; } linkedlist(const linkedlist<item> &); linkedlist<item> &operator=(const linkedlist<item> &); ~linkedlist(); void gostart() { current = start; } void gonext() { if (current) current = current->next; } bool end() const { return current == NULL; } item get() const { return current ? current->data : item(); } void remove(); bool append(item); bool insert(item); };

The templates for the functions would be simple modifications for what we originally had, basically only changing "node" to a specific kind of node, and "linkedlist" to a specific kind of linkedlist. For example, the destructor becomes:

template <class item> linkedlist<item>::~linkedlist() { gostart(); while (!end()) remove(); }

Part 2 (More C++) Page 88

Page 91: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

and the remove() member function becomes:

template <class item> void linkedlist<item>::remove() { if (current) { node<item> *temp = current; current = (current == start) ? start = start->next : previous()->next = current->next; delete temp; } }

The other functions would be similar. Note that the code we have written here assumes that whatever data type is used for item has a default constructor, and assumes that the copy constructor and = operator (and, implicitly, the destructor) work properly.

One important characteristic of templates is that each time you use a template in a new situation, the compiler effectively generates a whole new version of the function or class. (If a particular data type has been used earlier, the compiler will re-use the code previously generated). So keep in mind that doing

linkedlist<int> list_of_ints, x, another; linkedlist<double> list_of_doubles;

will cause two sets of the linked list logic (one for int, another for double) to appear in the executable program, even though the source code logic was written once.

Another important characteristic is that, like macros, templates are applied at compile time, hence the code for the template must have been encountered by the compiler before the template is actually used. For this reason, templates are commonly placed entirely in header files. This does not conflict with the philosophy that header files should only be declarations which are not definitions. A function template may look like a function definition, but no function is actually defined unless and until a data type is supplied, which won't occur until the template is used.

Declaration Modifiers Revisited

You already know how some of the declaration modifiers from C have additional uses in C++. For example, a member function in a class can be declared as "const" (by placing the keyword const at the end of the header line), to state that the function does not change any of the data members of the object. Such const functions may then be called for const instances of the class,

Part 2 (More C++) Page 89

Page 92: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

and so it is always a good idea to declare a member function as const if it can be.

There is also a slight change in how const "variables" are used in C++. If possible, the compiler will insert the const value in-line when compiling the code, so that a const variable access in most cases does not cause a memory access. This is done to encourage the use of const over #define. (Most C programmers prefer #define to const, because it can be used to avoid a memory access. But #define, being as flexible as it is, poses unnecessary risks for the simple purpose of making a symbolic name for a constant.)

Also, we have recently discussed how a constant data member of a class can be accessed using scope resolution (::), rather than as a member of a particular instance, since the constant will be the same for all instances.

Another declaration modifier that has extended uses in C++ is static. A static member variable will exist globally, even though it can only be accessed as a member of the class. What this means in practice is that one copy of the variable will be shared among the various instances of the class. As with constants which are members of a class, you may access a static variable using scope resolution, rather than as a member of a particular instance. And, as with constant members, you may access a static member variable even when you have no instances of the class (using scope resolution).

One small technical detail about static member variables is that when you declare one in a class, this is only a declaration and not a definition of the variable, and so you must externally (i.e. outside the class declaration) define the static variable, much as you define member functions outside the class. For example, if a class, named foo, say, has a member declared as

static int n;

then outside the class declaration you need to define (and, most probably, initialize) this variable:

int foo::n = 0;

Static member functions are similar: you may call them as members of an instance or by using scope resolution, and you don't need an instance of the class in order to call a static member function as long as you use the scope resolution syntax.

As an example, consider the following class, which simply keeps track of how many instances of this class are currently in existence. Even though this particular class does nothing else, this functionality could be added to any class. The class

Part 2 (More C++) Page 90

Page 93: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

declaration, which could appear in a header file, is:

class counter { static int cnt; public: counter() { cnt++; } ~counter() { cnt--; } static void show(); };

while the following definitions would appear in a source file:

int counter::cnt = 0; // this occurs only once! void counter::show() { cout << "There " << (cnt == 1 ? "is": "are") << " currently " << cnt << " counter instance" << (cnt == 1 ? "":"s") << "\n"; }

A program could then call

counter::show()

at any time in order to see how many instances of the class counter are currently in existence.

Note that a static member function may not access any other members that are not themselves static (or constant), since the non-static members require a specific instance of the class, and no such instance may be present when a static member function is called.

Sometimes, static member variables and functions are called class variables and class functions, because their usage depends only on the class being declared and not on individual instances being defined.

Yet another special use of one of the declaration modifiers is for the keyword extern. Because of the function overloading capability of C++, most C++ compilers actually add some extra characters to the end of each function name at compile time. (For example, if there are three versions of a function named foo, a compiler might call the first version it sees foo__Axe1, the second version foo__Axe2, and the third one foo__Axe3. The linker then doesn't have to understand the concept of function overloading, because they look like three different functions to the linker. The actual names a compiler might pick are entirely compiler dependent, by the way.) This can cause a problem when the compiler tries to link C++ code with code compiled by, say, a C compiler, where each function's name in the C source code usually is an exact match for its name in the unlinked-but-compiled object code.

Part 2 (More C++) Page 91

Page 94: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Similarly, C compilers automatically promote char parameters to int, and float to double, and we have previously discussed. C++ compilers do no such automatic promotion - a char is passed as a char, and a float is passed as a float. So any function compiled with a C compiler which passes a char will not link up properly with C++ code which calls it, unless the function is declared to pass an int in the C++ code.

To solve both these problems, it is possible to declare names in C++ source code that are being separately compiled by a pure C compiler, simply by putting those declarations inside a set of braces ({}) after the words, extern "C". For example,

extern "C" { int foo(char, float); #include "c_utils.h" }

declares a C-compiled function named foo, as well as #includes a set of pure C declarations from the file "c_utils.h". On many platforms, cross compilation between C++ and other languages besides C may be permitted, in which case some string other than "C" (such as, say, "Pascal") may be used to declare names compiled by a different language compiler.

Reference Return Values

Recall that C++ adds a new style of parameter passing to the language. In C, all parameter passing is "by value" (i.e. the argument values are copied into the parameters). This necessitates the use of addresses (which are passed by value) in order to have a function change a variable.

In C++, an instance may also be passed "by reference" (i.e. the parameter is just another name to refer to the argument), simply by placing an ampersand (&) after the parameter's data type in the function header. This facility makes most passing of pointers unnecessary.

Return values follow the same rules as parameters, and it is possible to make a function return a reference to some other variable. Care should be taken, however, to ensure that if you do return a reference to a variable, then that variable should still be in existence when the function returns.

The following template uses reference return values to implement a general array type, where invalid array indexing will not cause problems. This class dynamically allocates an array of the size requested at the time of construction, but also has one extra element for use when an invalid array index is used. The array indexing operator is overloaded to enable checking of the

Part 2 (More C++) Page 92

Page 95: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

index, so that these arrays will "look" just like regular arrays when you use them in a function. Note that since the class dynamically allocates memory, a copy constructor, a destructor and an = operator are necessary to ensure correct operation.

template <class type> class array { type *p; // the actual array unsigned int size; // how many elements type junk; // an extra one public: array(unsigned int sz = 0) { // basic constructor p = (type *)0; if (sz) try { p = new type [sz]; } catch(bad_alloc x) { } size = p ? sz : 0; } array(array<type> &x) { // copy constructor p = (type *)0; if (x.size) try { p = new type [sz]; } catch(bad_alloc x) { } size = p ? x.size : 0; *this = x; // uses operator= below } ~array() { if (p) delete [] p; }

array<type> &operator=(array<type> &);

type &operator[](unsigned int i) { return i < size ? p[i] : junk; } unsigned int length() const { return size; } };

template <class type> array<type> &array<type>::operator=(array<type> &x) { for (unsigned int i = 0; i < size; i++) p[i] = x[i]; return *this; }

A program, for example, could make an array of 20 doubles with:

Part 2 (More C++) Page 93

Page 96: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

array<double> x(20);

and from this point on, x can be indexed just like an array, except that the program will not mysteriously crash if an index outside the range of 0 to 19 is used. (Of course, this kind of array access is a bit slower than a real array access, because of the extra checking going on, which is why plain arrays don't do such checking).

The reason you can do something like

x[5] = 25.2;

is because the operator[] function (which is being used here) returns a reference to a single position in the dynamically allocated array. The use of the operator[] is therefore just another name for that position, and anything we do to it will be done to that position.

While we were at it, we included a function, length(), which returns the size of the array, so that you could write functions like

void show(array<double> a) { for (int i = 0; i < a.length(); i++) cout << i << ": " << a[i] << '\n'; }

without having to pass the length of the array separately.

Note that while such an "array" looks like an array, it is in fact an object. In particular, passing one of these arrays makes a copy of the entire array (through our copy constructor, in fact), which is quite different from passing a real array, where only a pointer to the start of the array is copied.

Multiple Inheritance

It is possible for a class to be derived from two or more classes at once. For example, suppose we have the classes:

class a { int n; public: a() { n = 0; } a(int num) { n = num; } void display() { cout << n; } };

Part 2 (More C++) Page 94

Page 97: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

class b { int m; public: b() { m = 0; } b(int num) { m = num; } void display() { cout << m; } };

then we could declare:

class c: public a, public b { .... };

in which case c will contain all the members of both a and b, and everything which is protected in a and b will be protected in c, and everything which is public in a and b will be public in c. (You can control the access to a members and b members separately, by using private or protected rather than public when deriving c).

If there is a name conflict between members of a and b, then scope resolution can be used to clarify the ambiguity. For example, if both a and b had a function foo(), then if x were an instance of class c,

x.foo();

would be ambiguous, and we would have to specify either x.a::foo() or x.b::foo().

To further flesh out the example started above, consider the following full declaration of c:

class c: public a, public b { int k; public: c() { k = 0; } c(int num): a(num), b(num) { k = num; } c(int num1, int num2, int num3): a(num1), b(num2) { k = num3; } void display() { a::display(); cout << ','; b::display(); cout << ',' << k; } };

Notice how we are able to trigger specific constructors of the two base classes if the default constructor is not the desired

Part 2 (More C++) Page 95

Page 98: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

one. Also note how this particular example has dealt with the ambiguity issue by supplying its own version of the function display(), which appears in both base classes.

Virtual Base Classes

While you are prohibited from deriving a class with a base class appearing twice in the base class list, it is possible for a base class to appear twice in the inheritance hierarchy of a derived class. For example, if b is derived from a and c is also derived from a, we would be permitted to derive class d from b and c. This means that there will be two instances of a inside one instance of d (one from the b side and the other from the c side), and scope resolution must be used to clarify any ambiguities. Unfortunately, this is seldom what is desired in this situation; what you probably would want is to have one instance of the class a, with all the extra features of both b and c added to it.

As a more concrete example, consider a base class, employee, which stores the basic information for an employee of a company (name, address, etc.). From this you might derive a class, hourly, which adds pay rate information based on an hourly pay rate. From employee, you might also derive a class, fringe, which contains fringe benefit information. It would be desirable to then be able to derive a class, fulltime, from both hourly and fringe, while only maintaining one copy of the basic employee information.

This can be achieved by declaring the base class as virtual in the first level of derivation. To see how this is done, consider the following base class:

class a { int n; public: a(int num = 0) { n = num; } void show() { cout << n; } };

and the two derived classes:

class b: virtual public a { char c; public: b(char ch = ' ', int num = 1): a(num) { c = ch; } void show() { cout << '('; a::show(); cout << ", " << c << ')'; } };

Part 2 (More C++) Page 96

Page 99: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

class c: virtual public a { double db; public: c(double x = 0.0, int num = 2): a(num) { db = x; } void show() { cout << '('; a::show(); cout << ", " << db << ')'; } };

Then we could derive:

class d: public b, public c { public: d(double x = 1.5, char ch = '*', int num = 3) : a(num), b(ch), c(x) {} void show() { b::show(); cout << ';'; c::show(); } };

Because we declared a as a virtual base class for both b and c (by putting the word virtual before the class name in the derivation list), when we derive d from both of these, there will be only one copy of a which will be used by both the b and the c code. Notice also how in the constructor for d, we have triggered a specific constructor for this virtual base class explicitly. The a portion, because there is only one, will be constructed only once, and if we omit specifying how the a portion will be constructed in making a d object, then it will be constructed using the default constructor, rather than trying to decide if b should be allowed to construct it or if c should. In this example, if we did

d x; x.show();

then the ou tput would be: (3, *);(3, 1.5)

Some people find it helpful to draw a diagram showing the inheritance hierarchy. In this case we would get something like:

a / \ b c \ / d

If you draw such a picture, and it forms a closed shape (a diamond in this case), then the class at the top of the shape should be used as a virtual base class, rather than just a plain base class.

Part 2 (More C++) Page 97

Page 100: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Abstract Base Classes

Sometimes you want a series of classes to share some common elements, but cannot think of an implementable base class. You can declare a base class where one or more of the functions are declared but not coded. This is accomplished by (1) declaring the function as virtual and (2) "setting" the function to 0 (using =), instead of coding it. Such a function is called a pure virtual function, and it must be overloaded in derived classes since no definition exists in the base class.

A class which contains one or more pure virtual functions is called an abstract base class, because you cannot instantiate it directly (you can only instantiate classes derived from it).

As an example, we will make an abstract base class called showable, which will mean that we can display it to an ostream using <<. Normally, we'd have to code a separate << operator for each class which we want to work this way, but instead, we will code the operator once for all objects that are showable. Each showable class will, however, have to have a member function that will display the object on an ostream. We will call this function show, and it will be a pure virtual function in this class.

The abstract base class, showable, would be:

class showable { public: virtual ostream &show(ostream &) const = 0; };

and the << operator that goes with this class would be:

ostream &operator<<(ostream &os, const showable &x) { return x.show(os); }

From now on, if we want to be able to display a class using <<, we could (1) derive the class from showable, and (2) code the show function whose prototype appears in the showable class. For example,

class foo: public showable { int n; double d; public: foo(int num = 0, double dbl = 0.0) { n = num; d = dbl; } ostream &show(ostream &os) const { return os << n << ", " << d; } };

Part 2 (More C++) Page 98

Page 101: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

(Incidentally, in this example, the only reason the show() function is const is because we have decided that displaying an object should not change the object in any way, and has nothing to do with abstract base classes).

Similarly, if we had a class that isn't set up to work with ostreams, we could derive a new class from both it and showable, adding only the requisite show function, which will work with ostreams.

Abstract base classes are a useful way of organizing capabilities which you'd like a variety of different classes to share. By including an abstract base class in the list of classes from which a new class is derived, you are essentially guaranteeing that certain functions with a particular syntax will be present in the class, and other code can then be written expecting that those functions will be present.

Exception Handling

A relatively new addition to the C++ language is an alternative form of logic control called exception handling. In many ways, this is like an enhanced version of the exit() function from <stdlib.h> in the standard C library. Exception handling gives the programmer the capability of signalling an error condition which can immediately stop the program. But unlike exit(), this signal can be caught by another part of the program, and then handled. Thus, it can be used as a way to transfer control directly several functions back, rather than the return statement, which goes one function back, or the exit() function, which goes all the way back, out of main().

Three language keywords are associated with exception handling: throw, try and catch. The most basic of these is throw. The syntax for throw is:

throw SOME-EXPRESSION;

where SOME-EXPRESSION stands for some expression of any type you like. This statement immediately causes control to leave the current function. If nothing is done elsewhere to handle this, it will cause the program to terminate. The act of throwing some value is usually called an exception.

The other "end" of exception handling is to try to catch these exceptions. The two keywords try and catch are used for this purpose. The syntax for this is

Part 2 (More C++) Page 99

Page 102: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

try { ...some code that might generate an exception... } catch (DATA-TYPE LOCAL-NAME) { ...code that might use LOCAL-NAME... } catch (ANOTHER-DATA-TYPE ANOTHER-LOCAL-NAME) { ...code that might use ANOTHER-LOCAL-NAME... }

with as many of these "catch" sections as you like. Inside the block of code after try, you put some code which may or may not throw an exception. If no exception is thrown, then the entire block will be executed and execution of the program will continue after all the catch sections. If an exception is thrown, however, the data type of the value thrown will be compared to the data types specified in each of the catch sections, in turn, until a match is found. When a match is found, then the value thrown is copied into the local name specified, and the code block following the catch() is executed. If no match is found, then the program will terminate because of the exception.

[As the very last catch, you may use ... inside the parentheses, instead of a data type and variable name, to catch any exception that hasn't already been caught. Be careful using this, however, since you won't know what caused the exception, and so it may be hard to make a good decision about what to do.]

As an illustration of this technique, let use create a class that will read a file full of int values. Instead of using traditional error handling, we will simply throw an exception if a bad situation arises. To help distinguish between the severity of things that could go wrong, we will create two types of values that can be thrown. The first we will call an "error", indicating a fatal problem, and the second we will call a "warning", indicating a problem that may not be completely fatal. In each of these values we will store a description of the bad thing that occurred. First, the error class:

class error { char mesg[41]; public: error(char *s) {strcpy(mesg, s);} ostream & show(ostream &os) const { return os << mesg; } };

ostream &operator<<(ostream &os, const error &e) { return e.show(os); }

Note that all we can do with an error is create it and display it. Next, the warning class:

Part 2 (More C++) Page 100

Page 103: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

class warning: public error { public: warning(char *s):error(s) {}; };

Note that this is identical to the error class (using inheritance), except that it is a different data type! Now, we will extend the ifstream data type (from <fstream.h>), into a file that we can simply read integers from:

class rfile: public ifstream { public: rfile(char *name); int read(); };

rfile::rfile(char *name):ifstream(name) { if (fail()) throw error("Cannot open file"); }

int rfile::read() { if (fail()) throw error("Unexplained error reading file"); int n; *this >> n; if (eof()) throw warning("end of file was reached"); else if (fail()) throw warning("invalid data was encountered"); return n; }

Here, we have considered end-of-file and invalid data as "warnings", since we can probably try to recover from them and continue to process the file, but other situations, such as being unable to open the file, we have considered to be fatal errors. A main() which might use this object to display the contents of a file full of integers could be:

int main() { try { rfile f("trycatch.dat"); // The next loop is intentionally infinite. // Eventually we'll get "thrown" out. while (1) cout << f.read() << '\n'; } catch (warning w) {

Part 2 (More C++) Page 101

Page 104: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

cout << "Program ended because " << w << endl; } catch (error e) { cout << "Program aborted: " << e << endl; } return 0; }

In this program, we try to open the file, and then read integers from it. If anything bad happens (and it will in this case, since the file we are reading cannot be infinitely big), then an exception is thrown and we will catch it. Until something bad happens, however, this program just keeps displaying the integers read from the file, one per line.

Even though both kinds of exceptions we have decided to throw will terminate this particular program, we have made different messages when they occur for illustrative purposes. If it is a warning, we simply tell why the program ended, but we give a more severe message with an error.

Note that warning appears before error in the catch blocks. This is important since we had derived warning from error. If we tested for error first, all our exceptions would go through the error section, since, after all, a warning is an error. Because an error is not a warning, we test for warning first, if it is not a warning, then we test for error. If any other type of thing had been thrown (although nothing in our code as it is shown does such a thing), then the program would simply stop with some sort of error message from the operating system.

Is exception handling a good thing? If used carefully and sparingly, it can greatly simplify a program by allowing the detailed logic to focus on the situation where things are working fine, rather than having excessive emphasis on suitable handling situations where something has gone wrong. But using exception handling carelessly, to avoid proper logic design, you can end up with code that is about as maintainable as code containing goto statements, which is to say not very easily maintainable at all! As a case in point, it could be argued that the intentional infinite loop coded in the example main() is a symbol of what is wrong with exception handling: the program says one thing (loop forever), when it will do something else entirely (loop until something bad happens).

We can probably expect that the relatively new concept of logic control will contribute to the development of some new techniques to use it effectively, as programmers become more familiar with the pitfalls and benefits of its use.

Part 2 (More C++) Page 102

Page 105: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Casting and Run Time Type Information

Up until now, we have been using C-style casts, (TYPE)data, or the C++ variation on this, the function-style cast, TYPE(data), to convert data, of one type, to another data type, TYPE. Such casts are potentially quite dangerous, and there are a few (relatively new) C++ casting operators designed to make explicit data conversions safer and easier to debug. These casting operators all have a template-like syntax, where the destination data type is specified in angle brackets after the cast keyword. They are:

static_cast - designed for situations where you want to convert between related types, such as between different kinds of related pointers, or from one numeric type to another. A common use of static cast is to take a block of data, pointed to by a generic pointer, say, and convert it to a pointer of a type you know the data to be. Some examples are:

// truncate a double, x, to int int n = static_cast<int>(x);

// treat a block of memory, pointed to by the generic // pointer p (declared: void *p) as an array of doubles double *values = static_cast<double *>(p);

reinterpret_cast - designed for situations where you know the bit pattern of the underlying data to match the destination data type, this cast simply treats the data as if it had been declared of the destination type. Probably the most dangerous of the casts, this allows you to convert between pointer types that are unrelated enough for static_cast not to work.

const_cast - casts away the const characteristic of data. A possibly legitimate use of this is within a const member function (one which is not supposed to change the member data), where you really do want to change some of the data, but in such a way that it doesn't matter to any of the calling code. You could use const_cast to cast the "this" pointer (which is const, since you are in a const member function) to a non-const pointer in order to manipulate its members. But situations where this is not a bad idea are very few and far between indeed.

dynamic_cast - intended for situations where you are converting between pointers or references in the same polymorphic inheritance hierarchy. The operand must be a pointer or reference to an instance of a class with at least one virtual function. If the conversion is not possible, dynamic_cast returns the value 0 in the case of a pointer cast, or throws a bad_cast exception in the case of a reference cast. Note that static_cast can do most of these same conversions, but without a runtime check to see if the instance can safely be treated as

Part 2 (More C++) Page 103

Page 106: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

the new data type. Also note that if the operand is a pointer or reference of a virtual base class type, only dynamic_cast, and not static_cast, will be able to do the conversion.

The dynamic_cast capability is part of the recent run-time type information (RTTI) addition to C++, which includes another operator, typeid, that can be used to test if an instance is of a specific type. For example, if "a" is a class, and "b" is another class derived from a, then

a *p[2]; p[0] = new a; p[1] = new b; for (i = 0; i < 2; i++) { if (typeid(*p[i]) == typeid(b)) cout << "p[" << i << "] is an instance of b\n"; else cout << "p[" << i << "] is of type" << typeid(*p).name() << '\n'; }

outputs something like p[0] is of type a p[1] is an instance of type b although the exact way that the typeid(*p).name() function (which returns a character string) formats the class name varies from compiler to compiler.

Although RTTI seems like a powerful tool, before using it you should try to think of ways to achieve the same goal using well designed virtual functions instead - you will likely end up with a more robust solution. However, when using class hierarchies of which you do not control the source code, you may have no alternative to using RTTI.

Part 2 (More C++) Page 104

Page 107: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Introduction to the Standard Template Library

While our emphasis in studying C/C++ has been on how to write code, and not on what code has already been written for you, at some point you will have to spend some time learning more about the available standard libraries functions, classes and templates. Perhaps the most daunting of these is the Standard Template Library (STL), a library of templates for standard data structures and algorithms.

With our examination of a linked list, we have seen how dynamic memory allocation can be harnessed to make objects that can grow and shrink as needed. The STL has a thorough, reliable implementations of a linked list, and many other similar, yet different, data structures, and many algorithms, such as searching and sorting, that help to manipulate the data structures. The STL is vast enough that you could easily spend months learning to effectively use it.

We will introduce you to the STL with a simple example that will, hopefully, help you get over the initial barrier to learning the STL: figuring out what is going on!

Three of the things you will find in the STL are containers, which store data, iterators, which let you move around containers similar to the way pointers let you move around arrays, and algorithms, which manipulate the data in a container.

The container we will look at is called a vector, and is very much like an array that can grow (or shrink) as needed. Actually, vector is a template for a class, and it must be supplied the type of data that is going to be stored in the vector. Each container has a header file; for vector, it is <vector>.

The member function push_back() adds a new element to the end (the back) of the vector. The member size() returns the number of elements in the vector. Two other members, begin() and end(), return the iterator of the first element and one after the last element, respectively. (Remember, an iterator is to a container as a pointer is to an array).

The algorithm we will demonstrate is a basic sort algorithm, where you pass it two iterators: the first element you want to sort, and the one just past the last element you want to sort, and it sorts everything in between. The thing to note about sort is that it is a function template that works with most of the containers, not just a vector.

We will show two ways to move around a vector, the first using array-style notation, and the second using an iterator, which

Part 2 (More C++) Page 105

Page 108: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

uses pointer-style notation. Note that the data type for the vector's iterator is actually only known inside the vector class (it is a typedef done inside the class declaration), which is why scope resolution is used when specifying the iterator data type.

This sample program lets the user enter any number of numbers, and then displays them in the order they were entered and then displays them again in sorted order.

#include <iostream> // to do user I/O #include <vector> // for the vector template #include <algorithm> // for the sort algorithm template using namespace std; // so we don't have to always std:: int main() { vector<int> v; // make a vector storing int values

// let the user enter a bunch of non-zero numbers into // the vector, using 0 as the way to terminate input. int n; do { cout << "Enter a number (0 to quit): "; cin >> n; if (n) v.push_back(n); // add n to the vector's end } while (n);

// display the vector, using array notation for (int i = 0; i < v.size(); i++) cout << v[i] << ' '; cout << '\n';

// sort the entire vector sort(v.begin(), v.end());

// display the vector again, but using an iterator // this time, just to demonstrate how iterators // work. The name p is used, because it "feels" so // much like a pointer. vector<int>::iterator p; for (p = v.begin(); p != v.end(); p++) cout << *p << ' '; cout << '\n'; return 0; }

Part 2 (More C++) Page 106

Page 109: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

Epilogue

This concludes our study of the C and C++ languages. While we have not covered every single element of these languages, we have covered most of the important points. If you have gotten this far, you should now be able to read books, such as Kernighan and Ritchie's The C Programming Language, and Stroustrup's The C++ Programming Language, which have complete treatments but are definitely not for beginners. And we have only touched the surface of the available subroutine libraries, whether they are from the standard C library or the standard C++ library, or from non-standard libraries.

Part of the reason behind our emphasis on the language content rather than the libraries, is that libraries are more transitory in nature (new ones being developed all the time, often in platform specific ways), and can easily be learned by someone already familiar with the rest of the language syntax. In fact, learning too much about available library routines early in your development can actual slow your progress as an emerging programmer, since you can often find something very close to what you need in a library somewhere, and can therefore avoid having to develop small bits of logic yourself. Even though such avoidance isn't good for the person first learning how to program, it is certainly useful to a trained professional, who already knows how to develop logic, but is short on time. For this reason you are probably now ready to start looking at the libraries available on your systems, to see the variety of pre-written code that is there for you to use if you so desire.

If you wish to start teaching yourself about available library routines, here are a couple of suggested starting exercises:

1. Just as printf, fprintf and sprintf are related, so are ostream, ofstream and ostrstream. An ostrstream allow you to output to a string instead of to the standard output device or to a file. Similarly, istrstream allows text data in a string to be converted into other types of data using the standard stream input mechanisms. You can learn more about "string" streams by investigating the <strstream> header file.

2. A neat STL template is a map, which lets you store values by a key field. You can then access the data in the map by the key field (rather than by the position as you do with a vector). See if you can learn how to use a map. You will have to look in a book or two for this - you will get lost pretty quickly if you try to figure it out just by looking at the system header files.

Keep in mind that we have seen a lot of syntax, with, generally, very short examples of its use. Quite frankly, for small programs, you don't need all these fancy "variations on a theme", and a more basic, simple language would do just as well.

Epilogue Page 107

Page 110: Practical Programming Techniques Using C++ Programming Techniques Using C++ June 2004 Edition syntactical issues of a particular programming language. But a professional programmer

Practical Programming Techniques Using C++ June 2004 Edition

But experience has shown that as programs grow more complex, the benefits of all this extra syntax start to become self evident. Until now, you haven't really had the opportunity to appreciate the syntax for what it can do for you - you've simply been trying to figure out how it all works. But as you program more and more, you will find yourself using these fancier techniques more and more.

It takes years to become a good practitioner of the programming trade, and you should bear this in mind during your first few of those years. Try to find applications for the techniques we have been studying, even if it will take you slightly longer to get things working. Gradually, as your facility with the techniques improves, you will find yourself saving far more time than you "wasted" when you first tried to apply them.

Also, keep in mind that while it often looks like programming is mostly "logic" and very little "memorization", in fact a very large part of programming success is due to memorization of the basic syntax and techniques of the trade. Sure, an experienced programmer is not afraid to look up a small detail (or even a major one) when the right syntax doesn't come readily to mind, but in order to truly be productive, most of the code a programmer writes must come out fairly quickly, and memorization, conscious or not, is a large part of developing that ability.

Always remember that the reason that you can (hopefully) quickly add 54 and 28 is that you spent a lot of time a very long time ago memorizing the base 10 addition tables, and memorizing the technique of carrying digits from one column to the next. While programming is a more sophisticated skill than basic arithmetic, it is not really so very different in its fundamental nature.

One difference between programming and arithmetic, however, is the dynamic quality of programming, as compared to the static quality of arithmetic. Programming is one of the most frequently changing professions. Adaptability must be one of the hallmarks of the good programmer. You can never expect to know everything about a particular kind of computer or computer language, because they change so rapidly. Also, you cannot expect your current level of knowledge to last you any length of time: one simple technological advance could potentially out-date most of what you know today. But a programmer is almost by definition a problem solver and a quick learner. The more problems you solve, the better you'll be able to solve new problems, and the more you learn, the better you'll be able to learn more. These are the real reasons that good programmers seldom have difficulty finding fulfilling work.

Hopefully, this is one trend that will continue into the future.

Epilogue Page 108