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.

Practical Programming Techniques Using C++ · 2014. 4. 16. · Practical Programming Techniques Using C++ June 2004 Edition Part 1. C In More Detail We will begin by examining some

  • Upload
    others

  • View
    5

  • Download
    0

Embed Size (px)

Citation preview

  • 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.

  • 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

  • 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

  • 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

  • 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

  • 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 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, 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 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

  • 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, 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 .

    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

  • 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

  • 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

  • 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

  • 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

  • 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

  • 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

  • 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 and C++'s ) 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

  • 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 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 . 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

  • 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 . Before any direct terminal I/O is performed, the program must call

    initscr();

    Part 1 (More C) Page 14

  • 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 '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

  • 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 ), 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

  • 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 and 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

  • 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 #else #include #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

  • 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

  • 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 or . 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 and .

    Part 1 (More C) Page 20

  • 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

  • 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

  • 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

  • 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

  • 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

  • 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

  • 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

  • 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

  • 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

  • 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

  • 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

  • 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

  • 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