Corrections to Budd's C++ Book

This Web page contains an extensive list of corrections to, and comments on, the book "C++ for Java Programmers," by Timothy Budd. These comments and corrections represent the opinion of the course instructor, and are in no way approved or authorized by Mr. Budd.

Index:

  • Chapter 2, "Fundamental Data Types"
  • Chapter 3, "Pointers and References"
  • Chapter 4, "Memory Management"
  • Chapter 6, "Polymorphism"
  • Chapter 7, "Operator Overloading"
  • Chapter 8, "Characters and Strings"
  • Chapter 9, "Templates and Containers"
  • Chapter 10, "Input/Output"
  • Chapter 11, "Exception Handling"
  • Chapter 12, "Features Found Only in C++"
  • Chapter 14, "Case Study - Fractions"
  • Chapter 15, "Case Study - Containers"
  • Chapter 16, "Case Study - A Card Game"
  • Chapter 2, "Fundamental Data Types"

    Page 21.
    In many places throughout the book, Budd is inconsistent about his formatting of parentheses in function declarations and invocations. For example, the declaration of max includes a blank before the left parenthesis, but the call to that function omits the blank. This is a bad habit; don't emulate it. It is your choice whether to place a blank after the function name, but you should be consistent throughout your program, in both declarations and invocations. Little stylistic details like this may seem unimportant, but they will make a big difference in the readability of your programs.

    Chapter 3, "Pointers and References"

    Page 30.
    The scanf function should not be used in C++ programs. Use cin >> *p or cin >> iinstead.
    Page 34.
    The comp routine is implemented incorrectly. It should return -1, 0, or 1, depending on whether a is respectively less than, equal to, or greater than b. A correct implementation is:
        int comp (void * a, void * b)
        {
           double * d1 = (double *) a;
           double * d2 = (double *) b;
           if (*d1 < *d2)
              return -1;
           else if (*d1 > *d2)
              return 1;
           else
              return 0;
        }
    
    Note that it is not sufficient to simply cast d1-d2 to type int and return that value; why not?

    Chapter 4, "Memory Management"

    Page 50.
    Contrary to the statement at the end of Section 4.3, it is not true that "slicing occurs only with objects that are stack-resident." Slicing can occur with any object. However, it is more difficult to cause accidental slicing with a heap-resident object.
    Page 51.
    Instead of throwing an exception or returning a NULL pointer, some C++ compilers will abort the program with an error message. The behavior of failed allocations can always be controlled by using the set_new_handler routine.
    Page 54.
    The example code on this page contains several poor stylistic choices:
    Page 60.
    Extremely short variable names (e.g., p and np) are rarely, if ever, defensible. They are a symptom of lazy typists. You will lose points on your assignments if you choose such short, non-mnemonic names. (Note also that 1 + strlen(...) should be strlen(...) + 1.)
    Page 62.
    Contrary to what is implied by the text, the global variable g will be destroyed just before the program exits. When the program terminates, the reference count for the value "abc" will reach zero, and the value will be deleted.

    In addition, you should note that global values are almost never necessary and are considered very bad style. You should avoid them like the plague, particularly in this class.

    Chapter 6, "Polymorphism"

    Page 93.
    Contrary to the comment in the example program, the size printed will not always be 8 if the virtual keyword is removed. The exact size depends on the hardware being used, and could be as small as 4 (on an Intel 286 running Windows 3.1) or as large as 16 (on a DEC Alpha). The only thing that is certain is that the size will be larger with the virtual keyword than without.

    On Turing, the size will be either 8 (without virtual) or 12 (with virtual).

    Page 96.
    The printf routine is C-style I/O, not C++-style. It should be avoided in C++ programs. Also, the variable name c is poor style, and the string arguments to printf should have embedded newlines ("\n") at the end.

    Chapter 7, "Operator Overloading"

    Page 110.
    The list of overloadable operators in Figure 7.1 is not complete. In addition to those listed, the operators ->*, new[], and delete[] may be overloaded. In addition, although they are not exactly operators, typecast coversions may be overloaded (see Section 7.15).
    Page 111.
    The example in Figure 7.2 uses poor style by making a data member (value) public. This technique violates the design principles of encapsulation and data hiding, and should be scrupulously avoided. Don't do it, even temporarily!
    Page 112.
    Note that the const keyword for value returns is necessary only for class types. If you write (a + b) = b; using integers, the compiler will reject the assignment without need for the const keyword.
    Page 115.
    Note that the const keyword is necessary only for the postfix version of the ++ operator. If the prefix version returns a reference to the object, it may legally return a non-const object. For example, if a is an integer, you can write
        ++ ++a;
    
    but not
        a++ ++;
    
    (The blanks between the plus signs are not required by the language, but may enhance readability for some people.)
    Page 118.
    The comparison in the example assignment operator is missing a required address-of operator. It should read:
        if (this == &right)
    
    The code will not compile as given.
    Page 128.
    When disabling an operator, it is better not to implement it all. As the code is given in the book, non-member functions wouldn't be allowed to do assignments, but member functions of the box class could assign freely. Since the assignment operator is implemented as a no-op (i.e., it does nothing), this would cause great confusion while debugging.

    A better approach is to write the declaration with a semicolon instead of curly braces:

        private:
           void operator = (box & right);
    
    By doing it this way, even member functions are protected against accidental use of the disabled operator (although the error will be detected later, at link time, instead of at compile time).
    Page 129.
    There is a spurious asterisk in the definition of the unary ampersand (address-of) operator. The code should read:
        emptyBox * operator & () { return this; }
    
    The code given in the book will not compile.
    Page 131.
    Returning void from an assignment operator is lazy and bad practice. It is simple and standard to return a reference to this.

    Proper const declarations are missing from the example in a number of places: the arguments to the copy constructor, assignment operator, and addition operator, as well as the int type-casting operator. This lack not only prevents these functions from being applied to const objects, but also hides an ambiguity problem involving the addition operator.

    Page 132.
    The description in the book might be confusing to some. I have written a quick test program that identifies each box as it is created and deleted, while also tracking the nesting of various functions. The output can be difficult to read, but careful study will prove to be instructive. Note that Budd's original example will not work exactly as described in the book; see the comments in my version for more details.

    Chapter 8, "Characters and Strings"

    Page 138.
    The word "hexadecimal" is misspelled at the top of the page.
    Page 138.
    In C++, numeric character constants are written in octal, not hexadecimal, by default. Furthermore, these constants are limited to 3 digits (in most implementations), and all three digits must always be given. Thus, the value '\0123' is an illegal constant. To create a character with a value of octal 123, you must write '\123'. Hexadecimal constants can be written using the letter "x": '\x53' is exactly the same as '\123'.
    Page 138.
    The book is also incorrect regarding the notation used for wide character constants (wchar_t). The notation L'a' means the letter "a" in a wide-character type; the notation L'1234' is illegal. To generate a wide constant by its octal or hexadecimal value, the backslash notation must be used: L'\123456' or L'\xA72E'. In either case, the maximum width of a wchar_t, and thus the maximum number of hex or octal digits in a character constant, is implementation-dependent.
    Page 138.
    In the discussion of string literals, Budd neglects to mention that the L modifier can be applied to strings as well as characters. For example, L"A Literal Text" is legal and generates the given text as a wchar_t.
    Page 138.
    The very last line of code on the page is an example of poor style. A case label should never be written on the same line as an executable statement, for two reasons:
    1. It is very easy to miss seeing the executable statement, and thus to misread the intent of the code,
    2. It becomes impossible to add a comment to the case label that explains what case is being handled.
    Page 139.
    The code at the top of page 139 contains another bad style example. When multiple case labels are being handled together, each should be placed on a separate line. This makes it easier to spot the relevant case (because you can simply scan down a vertical column of cases), allows each case to be commented separately, and makes it easier to keep the code neat later if you need to add or remove cases.
    Page 140.
    In the code at the bottom of the page, the second argument to strcpy and strcat should be the variable name text, not the word literal.

    Chapter 9, "Templates and Containers"

    Page 160.
    The randomInteger class suffers from two serious problems. First, there are three different library functions commonly available for generating pseudorandom numbers. Of these, rand() is by far the poorest, and it should always be avoided. Much better pseudorandom numbers are generated by the random() function, which returns a 31-bit pseudorandom integer. The best commonly available generator, however, is drand48(), which also has the advantage of returning double-precision values between zero (inclusive) and 1 (noninclusive). You should always start with drand48() as your preferred generator, and fall back to random() only if drand48() is unavailable.

    The other problem with the example code is the use of the modulo (%) operator to reduce the pseudorandom numbers to a small range. NEVER USE MODULAR ARITHMETIC WITH PSEUDORANDOM NUMBERS! Most pseudorandom numbers are less random in the least significant bits. For example, if you use a max of 16 in Budd's sample class, you may find that your "random" number repeats in a cycle of length 16. The most significant bits of the number are much more "random."

    For this reason, the output of a pseudorandom-number generator should be scaled by division. The easiest way to do it is to convert the result to a double-precision number in the range 0 to 1, and then multiply by the desired range. For example:

    #include <stdlib.h>
    ...
        int i = ((double)random() / RAND_MAX) * max;
        int j = max * drand48();
    
    Either of these sequences will generate a pseudorandom integer in the range of 0 to max - 1, inclusive.

    Finally, astute students will note that I always refer to pseudorandom, not random, numbers. The values returned by the functions under discussion are not truly random, though they can be used as such for many purposes. A complete discussion is beyond the scope of this course, however.

    Chapter 10, "Input/Output"

    Pages 165-169.
    The stdio library is not appropriate for use in C++ programs. It is a C thing, and Budd should not have given it space. On many systems, mixing C-style and C++-style I/O in the same program will cause things to break. (I have nevertheless included corrections to this section, since some people might use these pages as a reference for C programming someday.)
    Page 167.
    The example showing how to use fopen is incorrect because the message "file cannot be opened" is issued to the standard output, stdout, rather than standard error, stderr. Error messages should never be written to stdout. See the following example (after correcting it, below) for how to one method of writing to stderr.
    Page 167.
    The example using fputs is incorrect because the message (msg) does not include a newline. There is an unfortunate inconsistency between puts and fputs: puts appends a newline, whereas with fputs you must provide the newline yourself.

    In addition, there is no reason to create a variable to hold the message. It is better to simply include the string literal directly in the function call. Finally, the comments should be aligned for readability.

    Thus, the corrected code should read:

        fputc('z', fp);       // write character to fp
        int c = fgetc(fp);    // read character from fp
        fputs("unrecoverable program error\n", stderr);
                              // write message to standard error
    

    Finally, note that the sample code doesn't do anything useful, and in fact it wouldn't work with the file pointer fp from earlier on the page, because the file "mydata.dat" was opened for reading (the "r" argument to fopen), and thus fputc would be rejected.

    Page 167.
    Footnote 3 is historically misleading. Using the value zero for a null pointer came before, not after, the development of the constant NULL. Stroustrup chose to return to using zero for the null pointer for various reasons, none of which your professor finds convincing. Nevertheless, the zero is considered acceptable C++ practice -- but you will never find it in Prof. Kuenning's own code. The issue is discussed in more detail on page 21 of Kernighan and Pike's excellent book, The Practice of Programming.
    Page 170.
    As Budd mentions in footnote 4, recent versions of standard header files omit the ".h" extension, but some older compilers do not yet support the omission. However, you should also be aware that there are sometimes subtle differences between the ".h" versions and the non-".h" versions. You will not usually be significantly affected by these differences, however.

    Chapter 11, "Exception Handling"

    Pages 179-180.
    The functions fopen, fputc, and fputc are C-style I/O. Don't use them in C++ programs.
    Pages 182-184.
    The setjmp/longjmp facility was a horrible (though sometimes necessary) feature in C. There is utterly no reason to use it in C++, and there are many reasons to avoid it. Budd should never have mentioned it, and you should never use it.

    Chapter 12, "Features Found Only in C++"

    Page 200.
    The example of "casting away const" using new-style typecasts is incorrect. There is a special cast, const_cast, that is designed for this purpose. Thus, the typecast statement should read:
        char * p = const_cast<char *>(name); // cast away the const part
    
    (However, even with this change, the code will not work as given on all machines. The reason is that the memory used to store the string "Fred" is protected by the hardware. If you insert these three lines into a program and try to run it on Turing, you will get a segmentation fault.)
    Page 201.
    The example of default arguments at the bottom of the page is incorrect. C++ requires that if an argument has a default value, all following arguments must also have default specified. The example given will not compile. For the same reason, the "bbox.test(2.3)" call on page 202 will not produce the results claimed. A correct example of these two lines would be:
        void test (int v, double d = 2.3) { ... };
        ...
        bBox.test(3);
    
    Page 205.
    The word "overwritten" in the middle of the page should be "overridden". "Overwrite" implies that the function is changed, which is not the case. The derived class merely overrides draw by specifying a replacement to be used within a GraphicalDeck.

    Chapter 14, "Case Study - Fractions"

    Page 217.
    The code for the rational class can be much improved by using default arguments for the constructors. Rather than giving three different constructors with increasing numbers of arguments, only one constructor is needed, as follows:
        rational(int t = 0, int b = 1) : top(t), bottom(b) { normalize(); }
    
    Also, the formatting of these declarations is execrable. Function arguments should be placed immediately next to the function names, not separated by large amounts of whitespace. The formatting in the example makes it appear that rational is a return type, not a function name.
    Page 217.
    It is poor design and poor style to implement only a subset of the "obvious" operators in a mathematically meaningful class. If you are going to define a += operator, you should also define -=, /=, *=, etc. Similarly, if you are going to define ++, you should also define --. Users of a class should not have to experiment or examine the header file to discover which operations are supported and which are not.
    Page 217.
    The code for the prefix version of operator++ is wrong. The return statement must return *this to match the return type of the function.
    Page 231.
    There is a subtle aspect to writing an input operator that is not covered in the text, namely the question of "putting back" characters, and in particular "putting back" an end-of-file indication. The details are too complex to go into here, but you should know two things:
    1. If you need to defer processing a character until the next time input is done (i.e., you discover that the character is part of the following input item), you can put it back on the stream with the putback function:
          stream.putback(character);
      
    2. If you have collected some partial input from a stream and you then encounter an end-of-file indication, you must clear the EOF state on the stream with the following bit of magic code:
          stream.clear(stream.rdstate() & ~ios::failbit);
      
      If you forget to do this, your program will fail to see its last input item.

    Chapter 15, "Case Study - Containers"

    Page 239.
    In the middle of the page, Budd states, "There is no direct way to determine whether a map has an entry under a given key..." This is incorrect. The find operation will return this information. For example:
        cityInfo::iterator whichCity = travelCosts.find(newCity);
        if (whichCity == travelCosts.end())
            // have not seen it yet
    
    Using find is more direct than using count, and is more efficient when working with multimaps instead of maps.
    Page 240.
    The problem being solved by the main routine is known as the all-pairs shortest-path or APSP problem. The approach given here, repeatedly applying Dijkstra's single-source shortest-path algorithm, is not a particularly efficient solution to APSP. Efficient APSP algorithms are not an appropriate topic for this course, but you should be aware that this program is not a good example if you need to solve a moderatly large APSP problem.
    Page 241.
    The sample program in Figure 15.2 is very poorly commented. A good program would contain a block comment that discusses the overall algorithm, more detailed explanations in the comments given, and more comments on how the code works.

    Chapter 16, "Case Study - A Card Game"

    Page 246.
    The sample code in Figure 16.1 is a poor example of style. There should be many more comments. Variable names should be more mnemonic: single-character names like "r" and "s", and abbreviations like "fup" are meaningful only to the original author, and even then only for a few months.
    Page 257.
    Figure 16.4 is another example of very poor style. Where do the constants 0, 1, 2, and 6 come from? Why aren't the special piles given names? Why are there 13 piles? It is relatively poor design to use a single array and then have certain slots have special meanings, but if you're going to do it, at a minimum there should be symbolic names for the special piles, and there should be extensive comments at the beginning of the file that explain how the array is organized.
    Page 262.
    This page contains yet another example of poor style, especially in the use of constants. It is moderately defensible to use the constants 4, 13, and 52 in a program that plays cards, although even in that case symbols are preferable. But where do the numbers 335, 5, 268, 2, 15, 60, 55, 7, and 80 come from? If I want to change the appearance of the game by moving the tableau down and to the right on the display, which constants should I change, and by how much?

    A far better way to write this code would be to define two constants that control where the display appears, such as TABLEAU_X and TABLEAU_Y, and others such as PILE_SPACING and CARD_SPACING that control appearance within the display. Other values should then be calculated from these numbers. Changing TABLEAU_X would shift everything on the screen without a need for further changes. A major advantage of this approach is that the computer does the calculations, so there is no room for human error.

    Page 263.
    See the previous discussion on pseudorandom-number generators. At a minimum, the randomInteger class should extract the high-order bits from the return value of rand(). In addition, there is no need for the temporary variable. One line is sufficient:
        return (double)rand() * max / RAND_MAX;
    

    Furthermore, there is no need to create a special class, and use the horribly misconceived () operator. A far better (complete) implementation would be:

    unsigned int randomInteger(unsigned int max)
    {
        return (double)rand() * max / RAND_MAX;
    }
    
    This obviates the unnecessary requirement for declaring a dummy object (such as swapper on page 262) just to make the function accessible. It is also simpler and much easier to read.


    This page maintained by Geoff Kuenning.