CS 111 Assignment 7
Tutorial on records, arrays of records, and program development


  1. The biggest program example you've seen so far.   Copy your Assignment 6 files TextUtility2.cpp and TextUtility2.h into your hw07 directory, for Assignment 8. Then compile gradeLookUp.cpp by typing:

       g++ gradeLookUp.cpp textUtility2.cpp
    

    (The program gradeLookUp.cpp uses only those functions that were already in textUtility2.cpp before you began modifying it. So, you may use either the original versions of textUtility2.cpp and textUtility2.h or your modified versions, if you've completed the Assignment 6 practice problems.)

    Before you run the program, look first at text data file quizRoster.txt. It contains a listing of students' ID numbers, names, and 5 quiz scores, with the names furthest to the right, as follows:

    012345678     10    9    8    7    6     ANWAR ABDUL
    987654321     10    9    9   10    9     CARDUCCI GIOVANNI
    789012345      9    8    7    7    9     MARTINEZ MARIA
    654321987      9    9    9   10   10     SMITH JANE LEE
    123456789     10    8    7    5    8     WARREN JERRY
    135792468      9    9    9    9    9     ZHU WENG HENG
    

    Then run the program. It displays the following non-graphical text menu:

    Welcome to Quiz Grade Look-Up.
    You may do any of the following:
    
         L) Look up a student's quiz grades.
         C) Change a student's record.
         A) Add a new student.
         D) Drop (delete) a student.
         Q) Quit.
    
    What do you want to do? (L/C/A/D/Q):>
    

    Try entering an inappropriate letter (not one of 'L', 'C', 'A', 'D', and 'Q'). You will be prompted repeatedly until you enter one of the appropriate choices. Both uppercase and lowercase 'L', 'C', 'A', 'D', and 'Q' are accepted as valid choices.

    Now try entering 'L' or 'l' (lowercase 'L'), to look up a student's quiz grades. You will then be prompted to enter an ID number. Enter the ID number of one of the students in quizRoster.txt. The student's information will be displayed.

    You will then be asked if you want to see the menu again. Type 'Y' or 'y' to answer yes. (If you were to enter 'N' or 'n' to answer no, the program would quit at that point.) If you answer yes, the menu will then be displayed again. Type 'L' or 'l' again. This time, enter an ID number that is NOT in quizRoster.txt. You will be told, "Not found," and then asked whether you want to see the menu again.

    Experiment similarly with all the other menu options.

    If you opt to "Change a student's record" ('C' or 'c'), you will be prompted to enter an ID number, and the contents of the student's record will be displayed, if there is a student with that ID in the roster. You will then be prompted to enter data for the name and quiz scores. If a student with the requested ID was not found, you will be told, "Not found."

    If you select "Add a new student" ('A' or 'a'), you will be promted to enter an ID number, and you will then be prompted to enter the other data for the student only if there does NOT already exist, in the roster, a student record with the ID number you entered. If a student with the entered ID number does already exist in the roster, you will be told this, and the data for that student will be displayed.

    If you select "Drop (delete) a student" ('D' or 'd'), you will, as usual, be prompted to enter an ID number. If a student record with that ID number exists in the roster, the data for that student will be displayed, and you will be asked whether you really want to delete this student's record. It will be deleted if you answer yes ('Y' or 'y'). If there is no student record with the requested ID in the roster, you will be told, as usual, "Not found."

    Experimentally verify all the above. It will be easier for you to understand how this program works if you first are thoroughly familiar with what it does.

    The program will finish running when you either select "Quit" ('Q' or 'q') from the menu or answer no ('N' or 'n') when asked whether you want the menu displayed again.

    When the program is finished running, look again at the data file quizRoster.txt. Its contents will have been modified in accordance with any changes you made to the roster, using the program.


  2. Records (structs).   As we have seen, there are many, many data types in C++. Data types may be categorized as simple data types vs. structured data types, where a variable of a simple data type (e.g. int, float, or char) contains only a single piece of information, e.g. a single number or character, whereas structured data types contain multiple pieces of information. The structured data types we've used so far have been arrays and a few classes, e.g. string, ifstream, and ofstream.

    We will now look at another kind of structured data type, records, also known as structs. (The word "struct" is a term specific to C and C++, whereas "record" is the more general, non-language-specific term for this category of data type.)

    Like an array, a record does nothing but store data. It does not have functions or operators defined for it, as classes do. Recall that an array stored multiple data items of the same smaller data type. On the other hand, a record stores multiple data items of possibly different data types.

    In an array, the smaller data items are known as elements, and they are stored in numbered (indexed) locations. In a record, the smaller data items are known as fields or members, and they have names, not numbers. In both records and arrays, the members/fields/elements may be of any data type, including other records and arrays.

    Below is an example of a record type declaration which defines a new data type, Student:

    struct Student  {
       int iD;
       char name[41];
       float quizzes[5];
    };
    

    A record type declaration begins with the keyword struct, followed by the name of the record data type which the declaration defines, in this case type Student. After the name of the record data type is a pair of curly braces which enclose a series of declarations of the fields of the record. The above record type declaration defines a Student record as containing three fields: iD, of type int, name, whose type will be an array of characters, and quizzes, of whose type will be an array of float.After the closing curly brace is a semicolon.

    Note that a record type declaration is NOT a VARIABLE declaration. Thus, it does not actually reserve any memory to store values of the defined data type. An actual region in memory, structured as defined in the Student record type declaration, will be created only when we declare a variable of type Student.

    When a variable of a record type is declared, it creates an instance of the record type (in this case, type Student) in memory. By an instance of the record type, we mean a single region in memory structured as defined in the record declaration.

    Following are sample declarations of some variables of type Student. Each declaration creates a separate instance of type Student in memory:

       Student student;
       Student anotherStudent;
       Student yetAnotherStudent;
    

    Recall that C++ is case-sensitive. In the first declaration, note that student (beginning with a lowercase 's') is not the same identifier as Student (beginning with an uppercase 'S'.). Here, Student is a data type, whereas student is a variable.

    Inside a record type declaration, the field declarations look like variable declarations. And, in some senses, they can be thought of as variable declarations. But they are NOT variable declarations in the sense of actually reserving memory locations. Memory is reserved for the fields only when memory is reserved for an entire region of memory to be structured as defined in the record type declaration, i.e., when a variable of the record type is declared.

    Once an instance of a record type is created, we can access is fields. For example, to assign a value to the iD field of anotherStudent:

       anotherStudent.iD = 123456789;
    

    To access a field of a record, we need an expression consisting of the name of the record VARIABLE (not the record type) followed by a dot, followed by the name of the field.

    If a field happens to be an array, we can access the array's elements the same way we would access the elements of any other array, combining record notation with array notation as follows:

       for ( int i = 0; i < 5; i++ )
          cout << " " << anotherStudent.quizzes[i];
    

    where anotherStudent is a record variable (instance) of type Student, and quizzes, one of the fields of record type Student, has been defined as a float array of length 5.

    When a record is first declared, its fields contain garbage. So, the fields must be given values before they are accessed in any way which assumes they already have meaningful values.


  3. The need for constants and typedef.   The record type declaration above uses some bad programming practices. For example, using numeric literals for the lengths of array fields is a bad idea. For example, suppose this record type declaration is being used in a big long program, and suppose we want to modify the program so that there are 6 quizzes instead of 5. We would have to find every instance of the number 5 in the program, determine whether it is being used as the length of an array of quizzes, and change all such 5's to 6's (without changing any other 5's to 6's. This would be quite tedious and error-prone. So, as a very minimum, we should declare the array lengths as global constants, as follows:

    const int NUMBER_OF_QUIZZES = 5;
    const int MAX_NAME_LENGTH = 40;  // Length of C-string,
                                     // not including null character
    
    struct Student  {
       int iD;
       char name[MAX_NAME_LENGTH + 1];
       float quizzes[NUMBER_OF_QUIZZES];
    };
    

    Suppose now that we have a very complicated program which uses lots of C-strings and lots of float arrays of varying lengths. Suppose also that there are lots of functions that take these arrays as parameters. We want to be able to recognize, easily, a function which takes the quiz array as a parameter, as distinct from a function which takes some other float array, of a different length, as a parameter.

    To that end, instead of declaring the array of quizzes as a generic array of float, we can define a special data type for it using a typedef declaration. Likewise for the name array.

    The program gradeLookUp.cpp contains the following global declarations at the top:

    const int NUMBER_OF_QUIZZES = 5;
    typedef float QuizArray[NUMBER_OF_QUIZZES];
    const int MAX_NAME_LENGTH = 40;  // last name first;
                                     // includes spaces between
                                     // parts of name.
    typedef char NameText[MAX_NAME_LENGTH + 1];
    
    struct Student  {
       int iD;
       NameText name;
       QuizArray quizzes;
    };
    

    Observe that NameText and QuizArray are array types defined in typedef declarations and then used as data types for the fields of the record type Student.

    A typedef declaration doesn't really define a totally new data type; it simply gives a new name, for convenience, to a data type that already exists and could have been declared directly instead, e.g. an array with a particular length, On the other hand, a record type declaration really does define a new data type. It defines a new way of structuring a region in memory. For example, the record declaration for type Student defines a region in memory consisting first of an int location, then a sequence of 5 float locations (the quizArray), and then a sequence of 41 char locations (the NameText array).

    In gradeLookUp.cpp, the global declarations also include the following:

    const int MAX_NUMBER_OF_STUDENTS = 40;
    typedef Student Roster[MAX_NUMBER_OF_STUDENTS];
    

    Roster is declared as a typedef name for an array of Student structs. However, no memory is reserved for such an array until a variable of type Roster is declared in the main function:

       Roster students;
    


  4. Records as parameters to functions.   For some examples of records as parameters to functions, and for some additional simple examples of how the fields of a record are accessed, see the functions printStudent (under "Output functions") and askForStudentData (under "Input functions") in the program gradeLookUp.cpp. Both these functions take a Student record as a parameter and access its fields within the body of the function.

    Recall that parameters of simple data types can be passed either by value or by reference. When a parameter is passed by value, the parameter is a distinct variable containing a copy of the argument value, so that any changes to the value of the parameter do not affect the value of the argument, On the other hand, when a parameter is passed by value, the parameter is NOT a distinct variable, but rather another name for the same memory location also named by the argument, which must be a variable.

    On the other hand, arrays as parameters are passed by pointer value, which, as far as the elements in the array are concerned, is like passing by reference. When an array is passed as a parameter, a copy of the array is never made. Instead,only a pointer to the array is copied.

    Although records, like arrays, are structured data types, records as parameters can be passed either by value or by reference -- just like simple data types, not like arrays. When a record is passed by value, a copy is made of the entire record -- an entire, possibly very big region in memory, structured as defined by the record type. And, if one of the fields of the record happens to be an array, the entire array is copied -- not just a pointer to the array.

    So, to save memory space, it is often a good idea to pass records as reference parameters only, not value parameters, even when a parameter is being used only as an in-parameter, not as an out-parameter, i.e. when it is being used to bring information into the function without affecting the values of any variables outside the function, When a reference parameter is being used only as an in-parameter, it is a good idea to declare it const, to prevent you from changing its value inadvertantly within the function.

    In gradeLookUp.cpp, the printStudent function (under "Output functions") has the following heading:

    void printStudent(const Student& student, ostream& out)
    

    Here, a record student, of type Student, is declared as a reference parameter. Because this function's sole purpose is to output the contents of the record, it does not need to modify the contents of the record in any way. Thus, student is being used as an in-parameter only. To prevent us from inadvertantly changing it within the function, it is declared const.

    When a parameter or other variable is declared const, any attempt to modify it is a syntax error, producing compiler error messages. Although syntax error messages may be annoying, they are less annoying than the kinds of hard-to-debug runtime errors that are prevented by syntactic restrictions. For example, if a parameter is declared const, you don't have to worry about runtime errors caused by inadvertantly changing that parameter's value.

    Of course, sometimes we do need to modify one or more of the fields of a record parameter, in which case we don't declare it const. A parameter needs to be modified when it is being used either as an out-parameter only or as both an in-parameter and an out-parameter.

    In gradeLookUp.cpp, the askForStudentData function (under "Input functions") has the following heading:

    bool askForStudentData(Student& student)
    

    This function prompts the user for a student's name and quiz scores, which are then put into the name and quizzes fields of record parameter student. The intent is that this information then be communicated to the part of the program which called the askForStudentData function. In other words, the parameter must serve as an out-parameter. So, we have declared student a reference parameter and have NOT declared it const.


  5. An array of records.   Look now at the global constant declarations and type declarations at the top of gradeLookUp.cpp:

    const int NUMBER_OF_QUIZZES = 5;
    typedef float QuizArray[NUMBER_OF_QUIZZES];
    const int MAX_NAME_LENGTH = 40;  // last name first;
                                     // includes spaces between
                                     // parts of name.
    typedef char NameText[MAX_NAME_LENGTH + 1];
    
    struct Student  {
       int iD;
       NameText name;
       QuizArray quizzes;
    };
    
    const int MAX_NUMBER_OF_STUDENTS = 40;
    typedef Student Roster[MAX_NUMBER_OF_STUDENTS];
    

    Observe the typedef declarations for three array types: NameText, QuizArray, and Roster. Array types NameText and QuizArray are used as types of two of the fields of record type Student. Then, array type Roster is declared as an array containing elements of type Student.

    Function readStudents (under "Input functions") has the following heading:

    bool readStudents(const char inputFilename[],
                      Roster roster,
                      int& numberOfStudents)
    

    The purpose of function readStudents is to load the contents of the specified file into an array of records.

    One of this function's parameters is roster, of type Roster, an array of Student records. Inside the function, the other parameter numberOfStudents is set initially to 0 and incremented each time data for one student is loaded into the array. Input from the file is done within a while loop, where each iteration of the loop inputs one student's data.

    Inside the while loop, the ID field of one of the records in array roster is accessed as follows, where numberOfStudents is the index of that particular element in the array:

          inFile >> roster[numberOfStudents].iD;
    

    And the elements of the quizzes field of that same record are accessed as follows:

          for ( int i = 0; i < NUMBER_OF_QUIZZES; i++ )
             inFile >> roster[numberOfStudents].quizzes[i];
    

    In the above for loop, we are accessing elements of an array which is a field of a record in an array of records. Stare at the notation for a little while, until it fully makes sense to you.

    Like all array parameters of functions, parameter roster has been passed by pointer value. As far as the elements are concerned, it is like passing by reference. Thus, any changes to the records in roster will be remembered by the caller's argument variable when the function is finished executing.


  6. Classification of the functions in gradeLookUp.cpp.   In gradeLookUp.cpp, there are a lot of functions. To help us keep track of them all, they are grouped into categories, as follows:

    // ----------------------------------------
    // ***** High-level tasks of program: *****
    // ----------------------------------------
    int lookUpStudent(const Roster roster,
                      int numberOfStudents);
    void changeStudent(Roster roster,
                       int numberOfStudents);
    void addNewStudent(Roster roster,
                       int& numberOfStudents);
    void deleteStudent(Roster roster,
                       int& numberOfStudents);
    
    // ------------------------------------------
    // ***** Auxiliary to high-level tasks: *****
    // ------------------------------------------
    int askIDAndSearch(const Roster roster,
                       int numberOfStudents,
                       int& index);
    
    // ----------------------------
    // ***** Input functions: *****
    // ----------------------------
    bool readStudents(const char inputFilename[],
                      Roster roster,
                      int& numberOfStudents);
    bool askForID(int& iD);
    bool askForStudentData(Student& student);
    bool userSaysYes(const char question[]);
    char askSelection(const char question[],
                      const char choices[]);
    void skipRestOfLine(istream& in);
    void skipRestOfLine(istream& in, char& x);
    
    // ------------------------------------
    // ***** Computational functions: *****
    // ------------------------------------
    int searchForID(int iD,
                    const Roster roster,
                    int numberOfStudents);
    char toUpperCase(char x);
    bool equalsIgnoreCase(char x, char y);
    
    // -----------------------------
    // ***** Output functions: *****
    // -----------------------------
    void displayChoices();
    void printStudent(const Student& student, ostream& out);
    bool writeStudents(const char outputFilename[],
                       const Roster roster,
                       int numberOfStudents);
    

    The function prototypes not only serve the needs of the compiler, but also serve the human reader as a table of contents for the file, Here, the prototypes, together with comments, are also used to show a categorization of the functions.

    From bottom to top, the bottom three categoriea are:

    1. Output functions. Functions which do nothing but output, e.g. to the terminal window or to a file. In a program of any significant size, all output, except for one-line errpr messages and prompts, should be done by output functions.

    2. Computational functions. Functions which do neither input nor output, except possibly to output brief error messages when given inappropriate parameter values.

    3. Input functions. Functions whose primary task is input, and which do no output except for error messages and prompts. In a program of any significant size, all input should be done by input functions. An input function should do as little else as possible besides input and ensuring the validity of input, e.g. by error-checking. ALL of your program's input should be done by input functions (rather than by direct use of an input stream in a function which is not specifically an input function), to ensure adequate error checking of all input.

    Do not confuse input and output with a function's in-parameters, out-parameters, and return values. The terms input and output refer to flow of information between the program and the rest of the world, via peripheral devices such as the keyboard and monitor, or via secondary storage (files on a disk). On the other hand, a function's parameters and return values pertain to information flow NOT between the program and the rest of the world, but between the function and the rest of the program. Thus, an output function typically has mostly in parameters, because its purpose is to take in information from elsewhere in the program and then send it out to the rest of the world. On the other hand, an input function typically has mostly out parameters and/or a return value, because its purpose is to take in information from the rest of the world and then send it out to the part of the program which called the function.

    In gradeLookUp.cpp, at the top of our list of function prototypes is the category "High-level tasks of program." These are major tasks that one might think of in the first stages of functional decomposition, and which are not easily categorized as input functions, output functions, or computational functions. In our present program, there are four high-level tasks, corresponding to the four menu options other than "Quit." All these functions involve a combination of input, output, and computational tasks, calling lower-level functions to perform these more specialized aspects of their job.

    The remaining category, "Auxiliary to high-level tasks," refers to functions which make the high-level task functions easier to write, but which, likewise, are not specialized enough to be categorized as input functions, output functions, or computational functions.

    What does it mean to describe a function as "high-level" or "low-level"? The lowest-level functions are those which are most specialized and which do not need to call any other functions. The next lowest-level functions are those which call no functions except the lowest-level functions. And so on. At the other extreme, the highest-level function is the main function, which is not called by any other function. The highest-level functions below the main function are those which are called directly by the main function, and which call other, lower-level functions.


  7. Top-down design vs. bottom-up design.   A long and complicated program should never be written all at once. You should write your programs in phases, stopping to compile, run, and debug at each phase. This is much easier than trying to debug an entire large program all at once. There are two opposite approaches to planning out the phases of a programming project:

    1. Bottom-up design. Start by writing low-level functions which perform simple tasks that can be easily isolated. Test each function using simple test programs, also known as driver programs. Write the lowest level functions first, then the next higher level, and so on, never writing a function without first writing all the functions that it needs to call. After each function has been written, test it by writing a simple main function which acts solely as a test program (driver program) for the most recently written function. Write the main function of your final program last.

    2. Top-down design. Define the overall structure of the program first. Before writing any of the individual pieces of the program, define how the pieces will interact with each other. Then, if top-down design is being applied not only to the preliminary design phase, but also to the actual writing of a program, this would mean writing the main function first, including calls to functions you haven't written yet. So that the main function can compile, you would also need to write trivial, simplified versions of these functions, known as stubs, which will be expanded into the actual functions later. (For example, in Assignments 6 and 7, textUtility.cpp and textUtility2.cpp contained stubs for the isNaturalNumber function.) A program with stubs can be tested to see how the various parts of the program interact with each other. Then, one by one, you would replace the stubs with functions that implement the actual desired behavior.

    Often, a combination of top-down and bottom-up design is most useful. Fpr a beginning programmer, it is probably easiest to use top-down design for the preliminary informal design phase, before you actually write the program, but then to use bottom-up design to write the program itself.

    What Dale calls "functional decomposition" is top-down design applied to the preliminary informal design phase. First, you outline the major tasks of the program. Then, subdivide those tasks into smaller tasks, and so on.


  8. A data-first approach to designing and writing programs.   Before you design or write a program in terms of the functions it will contain, regardless of whether you use top-down design, bottom-up design, or some combination of the two, it is a good idea to think first in terms of the data structures that a program will use. For example, the program gradeLookUp.cpp revolves around an array of records, the contents of which are read from a data file at the beginning, then accessed and modified by the user in various ways, then written back to the data file.

    For your purposes at the prsent time, the term "data structures" should be taken as meaning arrays and records. In future courses, you'll learn about more complicated data structures.

    Many of a program's functions, and most if not all of its major tasks, will involve the program's data structures. Thus, thinking first about the program's data structures will help you organize your thoughts about what the functions should do.

    Later, when you actually write your program, it is a good idea to write your global constant declarations, array typedef declarations, and record type declarations first, before you write anything else, since so many of your functions will need them in order to compile.


  9. An example of data-first, top-down design thinking.   The program gradeLookUp.cpp revolves around an instance of structured data type Roster, an array of records of type Student. Student records are read into the array from a text data file. Then, the user is repeatedly offered several options for accessing and modifying the contents of the array: (1) simply looking up a student's record, (2) changing a student's record, (3) adding a new student's record, and (4) deleting a student's record. Finally, when the user opts to quit, or when input fails, the contents of the array are saved to the data file before the program terminates.

    With this data structure in mind, below is a functional decomposition for gradeLookUp.cpp. Where appropriate, tasks will be classified as "input," "output," or "computational."

    The main function will need to call functions which do the following tasks:

    1. Read the data file into a Roster array. (Input.)

    2. Repeatedly ask the user to select one of the following tasks, until the user selects "Quit," or until the user asks not to repeat the menu, or until input fails:

      • Look up a student's grades.
      • Change a student's record.
      • Add a new student.
      • Drop (delete) a student.
      • Quit.

    3. Write the (possibly modified) contents of the Roster array to the original data file. (Output.)

    Task 1 can be accomplished by an input function, readStudents. Task 3 can be accomplished by an output function, writeStudents. Task 2 needs some further elaboration, below.

    The loop for task 2, above, must do the following, in each iteration:

    1. Display the menu of choices. (Output.)

    2. Obtain a valid selection from the user. (Input.)

    3. Execute the user's selection, one of the following, all of which will involve the Roster:

      • Look up a student's grades.
      • Change a student's record.
      • Add a new student.
      • Drop (delete) a student.
      • Quit.

    4. Obtain a valid indication as to whether the user wants to see the menu again. (Input.)

    The loop will repeat only if the user answers yes to the question about whether the menu should be displayed again. Thus, the loop condition should be a boolean indication of the user's answer (true if yes, false if no). But we want to execute all preceding steps at least once BEFORE asking the user whether to display the menu again. The simplest way to accomplish this is via a do/while loop.

    The tasks listed above will be accomplished by the following functions:

    1. displayChoices (output).

    2. askSelection (input).

    3. Execute the user's selection, one of the following, all of which will involve the Roster:

      • Look up a student's grades: lookUpStudent.
      • Change a student's record: changeStudent.
      • Add a new student: addNewStudent.
      • Drop (delete) a student: deleteStudent.
      • Quit: writeStudents (output), then quit program.

    4. userSaysYes (input).

    Look now at the main function of gradeLookUp.cpp, and observe how it calls the above functions within a do/while loop.

    The askSelection and userSaysYes functions will be written to do somewhat more general tasks than are required here, so that we can re-use them in other programs. userSaysYes will be written to obtain an answer to ANY yes/no question, not just a question about whether the user wants to display the menu again. So, it will take the question as a parameter. Likewise, askSelection will be written to obtain a user's selections from ANY text menu. It will take, as a parameters, (1) the question, and (2) another C-string containing the characters that the user may validly type in response.

    It should be noted that what the userSaysYes function does is actually a special case of what the askSelection function does. Obtaining an answer to a yes/no question is equivalent to obtaining a selection from a menu for which the valid user responses are 'Y' and 'N'. So, the userSaysYes function is very short, with most of its work delegated to a call to askSelection.

    The askSelection function is supposed to guarantee that the response it obtains is a valid selection, i.e. that it is one of the letters which represent choices in the menu. So, if the user enters an invalid response, the askSelection function will need to prompt the user repeatedly until a valid response is obtained.

    In the main function's do/while loop, once a task is selected by the user, one of the following functions is called to perform the task:

    • Look up a student's grades: lookUpStudent.
    • Change a student's record: changeStudent.
    • Add a new student: addNewStudent.
    • Drop (delete) a student: deleteStudent.
    • Quit: writeStudents (output), then quit program.

    The appropriate function is selected via a switch statement which tests the user's response, i.e. it tests the value returned by askSelection. Because the askSelection function guarantees a valid response (eventually), the switch statement does not need to have a default case (at least not in the final version of the program, though a default case could be very helpful in debugging).

    For menu selections, it is preferable for the program not to be fussy about whether the user enters a lowercase letter or an uppercase letter. To eliminate sensitivity to case, the askSelection and userSaysYes functions call functions toUpperCase and equalsIgnoreCase, which are classified as computational functions because they do neither input nor output.

    Let us now consider the functions lookUpStudent, changeStudent, addNewStudent, and deleteStudennt. All these functions will take, as parameters, the Roster array and an indication of the number of students for whom there are meaningful (non-garbage) Student records in the Roster.

    The lookUpStudent function will (1) prompt the user to enter an ID number, and read the entered ID number; (2) find a non-garbage Student record with that ID number in the Roster array, if it exists; and (3) display all the student's data to the terminal window, if a record with that ID number was found, or display "Not found" otherwise.

    The changeStudent and deleteStudent functions both do everything that the lookUpStudent function does, plus some more. So, the bodies of the changeStudent and deleteStudent functions both begin with calls to lookUpStudent.

    The addNewStudent function begins by doing almost, but not quite, everything that the lookUpStudent function does. Both these functions prompt the user for an ID number and search for that ID number in the Roster. So, these common tasks are delegated to a separate auxiliary function, askIDAndSearch, which is called by both lookUpStudent and addNewStudent.

    The auxiliary function askIDAndSearch performs two distinct tasks:

    1. Asking the user for an ID number, and reading that ID number. This is clearly an input task, involving nothing but input plus the output of a one-line prompt and a one-line error message if input fails. It is performed by input function askForID, which is called by askIDAndSearch.

    2. Searching the Roster array (or at least a subarray containing only the meaningfully-defined Student records) for a record with a given ID number. This task does not involve input or output, and is thus classified as a computational task. It is performed by computational function searchForID, called by askIDAndSearch.

    The functions changeStudent and addNewStudent both need to obtain, from the user, the student's name and quiz scores, not just the ID. To that end, they both call input function askForStudentData.

    All of the functions lookUpStudent, changeStudent, addNewStudent, and deleteStudennt need to display data about a student, if a Student with the entered ID number has been found in the Roster. The changeStudent and deleteStudent functions do this as part of the sequence of tasks that have been delegated to a call to lookUpStudent. So, we will now consider how the lookUpStudent and addNewStudent functions display the data for one student. They both accomplish this via a call to the printStudent function.

    When you run the program, observe that the student data is displayed in the form of a line of text identical to the corresponding line in the text data file quizRoster.txt. Thus, we can write one function which can either (1) print the contents of a Student record to the terminal window, as a single line of text, or (2) print the same line of text to a data file. In the source code of gradeLookUp.cpp, the printStudent function is called by writeStudents, where it writes a line of text to a file, as well as by changeStudent and deleteStudent, where it writes a line of text to the terminal window.

    As we have seen in earlier example programs, the printStudent function accomplishes such versatility by taking advantage of polymorphism. The output goes to a parameter object of type ostream. Remember that cout is an object of class ostream, and that class ostream is a superclass of class ofstream, the class of objects which do text file output. So, the printStudent function can be used to write to either cout or an ofstream object, depending on which of these is passed to the function as an argument.


  10. Maintainability vs. efficiency.   Besides a data-first, top-down approach to design, another principle we've been following here is known as the "Write Everything Once" pattern. If, in our functional decomposition, we discover that we have designated two different functions to perform some of the same tasks, then, according to the "Write Everything Once" pattern, the two functions' bodies should NOT contain two or more identical lines of code to perform the tasks common to the two functions. Instead, either one of the two functions should call the other function, or they both should call a third function which performs the common tasks.

    The "Write Everything Once" pattern makes a program easier to modify, when you are either debugging it or upgrading it. If the same lines of code appear multiple places in your program, then, when you change one of them, you need to remember to change them all -- which may be hard to remember, especially if you don't remember WHERE all the identical lines of code are. This difficulty can be avoided by simply avoiding, as much as possible, multiple copies of the same (or very similar) lines of code in one program, by delegating common tasks to functions.

    Unfortunately, the "Write Everything Once" pattern may cause the program to be less efficient than it would otherwise be. The "Write Everything Once" pattern tends to result in a program containing a lot more short functions and a lot more calls to functions than would be needed otherwise. And the extra function calls take up extra computer time.

    Function calls take up quite a bit of overhead in terms of computer time. First, the processor must set memory for the following purposes: (1) the parameters and local variables of the called function; (2) a location with a pointer to the instruction it was executing in the part of the program that called the function, so that, when the function is finished executing, the caller will then be able to pick up where it left off; and (3) various other purposes you'll learn about when you study assembly language. Then, when the function is finished executing, the memory that was set aside for all these various purposes must be reclaimed for future use, and the processor must go back to what it was doing before the function call. Thus, too many function calls can be quite time-consuming.

    So, there is a trade-off between program maintainability (ease of debugging and upgrading) and performance (efficiency). Which of these two is more important? That will depend on the kind of program you are writing, and on the policies of your employer.

    These days, most software companies value maintainability over efficiency. Computers have become so fast that efficiency simply doesn't matter very much anymore, except for the slowest tasks. On the other hand, a programmer's time is expensive. So, it is imperative that programmers not need to spend an excessive amount of time on debugging or on figuring out what another programmer was trying to do.

    But don't imagine that efficiency has become totally irrelevant. There are still plenty of tasks that still tend to be slow, despite enormous gains in computer hardware speed. For these tasks, efficiency is still crucial. But, for most other tasks, maintainability is more important. Thus, it is good to learn programming practices that facilitate maintainability, such as "Write Only Once" and polymorphism.


  11. Ending a line of input.   When you run gradeLookUp.cpp, a typical output to the terminal window might look like the following, where the echoed user input is indicated by boldface type:

    Welcome to Quiz Grade Look-Up.
    You may do any of the following:
    
         L) Look up a student's quiz grades.
         C) Change a student's record.
         A) Add a new student.
         D) Delete a student's record.
         Q) Quit.
    
    What do you want to do? (L/C/A/D/Q):>k
    Please enter L, C, A, D,  or Q:>l
    Enter student ID number:>123456789
    123456789    10.0  8.0  7.0  5.0  8.0     WARREN JERRY
    Display menu again? (Y/N):>y
    Welcome to Quiz Grade Look-Up.
    You may do any of the following:
    
         L) Look up a student's quiz grades.
         C) Change a student's record.
         A) Add a new student.
         D) Delete a student's record.
         Q) Quit.
    
    What do you want to do? (L/C/A/D/Q):>q
    

    The input function askSelection reads characters typed by the user, both to select a menu item and (within the body of the userSaysYes function) to select Y or N. The askSelection function must be able to read a letter when it expects a letter, rather than -- at that time -- reading the end-of-line marker immediately before or after the letter.

    In keyboard input, end-of-line markers are generated when the user presses [Enter]. (On a Unix system, the end-of-line marker consists of a newline ('\n') character. On a DOS/Windows system, the end-of-line marker consists of a carriage return ('\r') character followed by a newline ('\n') character.)

    To ensure that askSelection will read a letter when it expects to read a letter (provided the user has indeed typed a letter, of course), we need to ensure that cin has already consumed the end-of-line marker immediately preceding the letter, so that it is then ready to read the letter. In other words, must to ensure that cin is ready to begin reading a line of input, which is equivalent to saying that it has ended any previous line of input, i.e. that it has consumed the ENTIRE previous line, including end-of-line marker.

    In this program, cin is also used, in the input functions askForID and askForStudentData, to read numbers. When cin reads a number, it does NOT consume anything after the last digit of the number. It consumes any white space (spaces, tabs, and end-of-line markers) BEFORE the number, but not afteward. So, when cin reads a number that is on a line by itself, or at the end of a line of input, there is still an unconsumed end-of-line marker that needs to be dealt with somehow before askSelection is called.

    Also, recall that askSelection prompts the user repeatedly for one of the appropriate letters, if the user responds inappropriately. So, the askSelection function itself will need to consume end-of-line markers in between the multiple letters that it may need to read.

    End-of-line markers are also an issue within the function askForStudentData. The first data item it prompts for is the student's name, which will be read character by character, up to end of line. For this to work correctly, we will need any preceding end-of-line marker to have been consumed, so that the function will read up to the end-of-line marker at the end of the name, rather than stopping at the end-of-line marker BEFORE the name.

    Look now at the comments above the functions askSelection, userSaysYes (which calls askSelection), and askForStudentData. Regarding the global variable cin, the comments above all three of these functions specify, as a precondition, that cin must be ready to begin a line.

    Failure to consume end-of-line markers before calling these functions may result in output like the following (where I've indicated the echoed input in boldface type):

    Welcome to Quiz Grade Look-Up.
    You may do any of the following:
    
         L) Look up a student's quiz grades.
         C) Change a student's record.
         A) Add a new student.
         D) Delete a student's record.
         Q) Quit.
    
    What do you want to do? (L/C/A/D/Q):>c
    Enter student ID number:>123456789
    123456789    10.0  8.0  7.0  5.0  8.0     WARREN JERRY
    Enter student name (last name first): 
    Enter 5 quiz scores:
    

    Here, the askForStudentData function did not wait for the user to enter the name, but instead jumped to the next prompt.

    Another possible end-of-line-related bug might be something like the following:

    Welcome to Quiz Grade Look-Up.
    You may do any of the following:
    
         L) Look up a student's quiz grades.
         C) Change a student's record.
         A) Add a new student.
         D) Delete a student's record.
         Q) Quit.
    
    What do you want to do? (L/C/A/D/Q):>l
    Enter student ID number:>123456789
    123456789    10.0  8.0  7.0  5.0  8.0     WARREN JERRY
    Display menu again? (Y/N):>Please enter Y or N:>
    

    Here, the program did not wait for the user to enter Y or N after displaying the prompt "Display menu again? (Y/N):>". Instead, the program immediately displayed the nag prompt on the same line.

    Another possible bug of this kind is the following:

    Welcome to Quiz Grade Look-Up.
    You may do any of the following:
    
         L) Look up a student's quiz grades.
         C) Change a student's record.
         A) Add a new student.
         D) Delete a student's record.
         Q) Quit.
    
    What do you want to do? (L/C/A/D/Q):>l
    Enter student ID number:>123456789
    123456789    10.0  8.0  7.0  5.0  8.0     WARREN JERRY
    Display menu again? (Y/N):>y
    
    Welcome to Quiz Grade Look-Up.
    You may do any of the following:
    
         L) Look up a student's quiz grades.
         C) Change a student's record.
         A) Add a new student.
         D) Delete a student's record.
         Q) Quit.
    
    What do you want to do? (L/C/A/D/Q):>
    

    Here, nothing happens when the user first presses [Enter] after pressing 'y' in response to the "Display menu again? (Y/N):>" prompt. Only after the user presses [Enter] a second time is the menu displayed again.

    If you encounter similar bugs in any programs you write, they are probably caused by improper processing of end-of-line markers in either the input or the output.

    To facilitate consumption of end-of-line markers, I've defined two very short input functions named skipRestOfLine. (These two functions have the same name but different parameter lists, and are thus an example of function overloading.) These functions are called, directly or indirectly, in all input functions that use cin, including askSelection, askForStudentData, userSaysYes (via the call to askSelection), and askForID.

    Look now at the comments above all four functions askForID, askForStudentData, userSaysYes, and askSelection. Regarding the global variable cin, they all specify, as a postcondition, that cin has ended a line.

    By having all our input functions which use cin do cleanup of any leftover end-of-line markers on the line they read, we have ensured that there will be no trouble caused by end-of-line markers the next time we call askSelection, userSaysYes, or askForStudentData.

    In your programming project for Assignment 8, it is strongly recommended that you too have all your interactive input functions clean up leftover end-of-line markers.

    Details of this sort are another reason why ALL your input should be done by input functions. In a large program, you should not do ANY ad hoc input in functions which are not primarily input functions. If ALL input is confined to input functions, this makes input-related problems much easier to debug than they would be otherwise, and it also makes input-related problems AVOID in the first place, after you've finished writing and thoroughly testing your input functions.


  12. A guide to data-first, bottom-up program writing.   As mentioned earlier, when writing a long progroam, it is recommended that you use top-down design for the preliminary informal design phase, before you actually write the program. But then, when you do write the program, it is recommended that you use a bottom-up approach.

    Write your program one small step at a time, compiling and if possible running the program at each step. Each time you have something that works correctly, however incompletely, save it as a separate file, so that, in your next step, if you encounter a problem you can't debug no matter how hard you try, you can go back to something that works and start your latest step over again, more carefully this time, without having to write the whole program all over again.

    As your very first step in writing the program, it is recommended that you first write a program consisting of nothing but (1) the global constant declarations and type declarations (including record type declarations and array typedef declarations) needed for the data structures that your program will use and (2) a trivial main function, i.e. a main function whose body contains nothing but a return 0 statement. Make sure this compiles before you do anything else.

    As an example of such a first step, see gradeLookUpPrelim.cpp, a first step in writing gradeLookUp.cpp.

    Then add functions to your program one at a time. After writing each function, put a prototype for that function at the top of the file and write a simple main function which does as little as possible except to test the function you wrote most recently. Thus, at each step, you should write (1) a function, (2) its prototype, and (3) a main function which tests it.

    Make sure each such program compiles and runs, making any changes necessary so that what you've written so far works correctly. Once you've gotten it to work correctly, save it as a duplicate file with a different filename, allowing one copy to remain intact while you revise the other copy.

    At each step, you'll need to delete or modify the body of your main function so that it will test your latest function instead of the previous one. Your main function may need to call one or more previously-written functions in order to help you test your most recent function.

    In accordance with bottom-up design, you should write, at each step, a function which does not need to call any functions tha haven't bee written yet. Only as your very last step will you write the final version of your main function.

    It is also recommended that you write your functions in the following order, insofar as you can do this consistently with bottom-up design:

    1. Output functions.  Insofar as you can conveniently do so, output functions should be written first, for two reasons:

      • They are easy to test, without having written other functions first. In your main function, just declare one or more variables of the type(s) that are output by your output function, then assign values to these variables, then call your output function, and see whether it outputs the values correctly.

      • Your output functions will make it easier for you to test other functions you write later. They will enable you to view the results of what your other functions are doing. And at least some of your output functions should allow you to view the contents of the data structures that are manipulated by other functions.

      Your output functions should, of course, be written in bottom-up order. For example, in gradeLookUp.cpp, it would make sense to write and test the printStudent function before writing the writeStudents function, which calls printStudent.

    2. Input functions.  These should be written next, if possible, because they too will make the testing of other functions easier. Input functions will enable testing of other functions for a variety of parameter values, by allowing you to input those values.

      When testing input functions, call your output functions (in the main function of your test program) to make sure that your input functions are, in fact, inputting the values you think they are inputting.

    3. Computational functions.  Because these functions don't do any input or output, their behavior cannot be observed directly. Hence both your output functions and your input functions may be very helpful in testing them. For example, to test the searchForID function in gradeLookUp.cpp, you need a Roster array, whose contents can be generated most easily via a call to readStudents (an input function) in the main function of your test program.

      Therefore, as much as possible, it is preferable to write input functions before computational functions. But this isn't always possible. For example, in gradeLookUp.cpp, the functions askSelection and userSaysYes, which are input functions, call the functions toUpperCase and equalsIgnoreCase, which are computational functions. Hence, to avoid writing functions which call other functions you haven't written yet, toUpperCase and equalsIgnoreCase should be written before askSelection and userSaysYes.

      You won't necessarily need to call your output functions and input functions in order to test ALL your computational functions. For the simplest computational functions, testing can be done by just using cin and cout in the main function of your test program.

    4. Auxiliary to high-level tasks.  These functions are likely to call functions in all three of the above categories, and are not likely to be called by any functions other than the functions which perform high-level program tasks.

    5. High-level program tasks.  These functions are not called by any function except the main function, and so can be written as your very last steps before writing the final version of the main function.

    Sometimes, even writing functions one at a time may be too big a step. If you have difficulty debugging one of your functions, it may be simplest to go back to your previous step, then start writing the current function again, this time only a few lines at a time, compiling and testing at each step.


  13. Some tips on debugging.   Although you should test each function throroughly before writing the next function, sometimes, alas, a function may still have a subtle bug that doesn't make itself noticed until later.

    To debug runtime errors in a program with lots of functions, one technique is to use cout statements to output the values of variables at various points. These cout statements should output not only the names and values of the variables, but also the name of the function and, possibly, other identifying information about the location of the cout statements.

    When debugging a program, all bets are off. Do not assume that you know, for sure, that ANY part of your program is working correctly. Verify absolutely everything that is relevant to your error by using cout statements to output the values of relevant variables at points in your program relevant to the error.

    For an example of a program containing cout statements for debugging, compile and run gradeLookUpDebug.cpp, which I used to debug some problems with the askSelection and userSaysYes functions which I didn't notice until I wrote the main function of the final program. (These functions weren't reading the characters I was typing. My problem turned out to be a failure to keep track of when cin had reached the end of a line of input. The present version of gradeLookUpDebug.cpp works fine; I've just kept it as an example of how to use cout statements for debugging.)

    I'll now discuss some of the more cryptic compiler error messages I ran into when writing this program, so you'll be able to make sense of similar syntax error messages if you encounter them:

    In method `istream::istream(const istream &)':
    /usr/local/lib/gcc-lib/alphaev56-dec-osf4.0e/2.95.3/include/g++/streambuf.h:128:
    `ios::ios(const ios &)' is private
    

    The above message turned out to have been caused by using an istream object as a value parameter in my skipRestOfLine functions. Objects of class istream, ostream, ifstream, and ofstream must be used as reference parameters only, never as value parameters.

    You'll understand more precisely what the above error message means after you've taken Computer Science 211, next semester.

    ANSI C++ forbids comparison between pointer and integer
    

    This one was caused by using a string literal instead of a character literal. Remember that 'X' is a value of type char, whereas "X" is a C-string containing the character 'X' followed by the null character ('\0'). In the above error message, the compiler complained that I was comparing a char value (char is one of the integer types) with a pointer to my C-string. (In the expression where I used it, my C-string literal had been converted automatically to a pointer expression, as arrays often are.)

    gradeLookUp7.cpp:117: passing `const Student' as `this' argument of 
    `struct Student & Student::operator =(const Student &)'
    discards qualifier
    

    This was caused by declaring a Roster array as a const parameter to a function in which an element of that array appeared on the left-hand side of an assignment statement -- thereby potentially modifying it.

    Typically, syntax error messages that contain the words const and "discards qualifier" mean that you've done something you're not supposed to do with a const parameter or with some other named constant.

    gradeLookUpE.cpp: In function `void deleteStudent(Student *, int &)':
    gradeLookUpE.cpp:587: parse error before `delete'
    

    Usually, "parse error before" simply means there is some syntax error (often a missing semicolon, parenthesis, curly brace, or comma) before the named identifier. In my deleteStudent function, I had a variable named delete. After searching in vain for a syntax error between the beginning of the function and the declaration of my variable, I finally remembered that delete cannot be used as a variable name, because it is a C++ keyword. So I fixed the problem by simply changing the name of the variable. In general, you cannot use C++ keywords (e.g. if, while, return, int) as names of variables or functions).

    You'll learn next semester what the keyword delete means. For a complete list of C++ keywords, which you cannot use as identifiers for other purposes, see Dale, Appendix A: Reserved Words.


  14. Re-using code.   In gradeLookUp.cpp, there are some functions that can easily be re-used in other programs, including your Assignment 8 programming project. These functions are:

    • userSaysYes
    • askSelection
    • The two skipRestOfLine functions
    • toUpperCase
    • equalsIgnoreCase

    To make these functions easier to re-use, we've put them in a separate file, inputUtility.cpp. To use them, you will need to #include header file inputUtility.h, as is done in gradeLookUp2.cpp, a version of gradeLookup.cpp which no longer contains definitions of the above-listed functions itself.

    Because gradeLookUp2.cpp includes the header file textUtility2.h as well as inputUtility.h, it must be compiled by typing:

       g++ gradeLookUp2.cpp textUtility2.cpp inputUtility.cpp
    

    because it uses functions defined in all three of these source code files.


Back to: