Grouping Data with struct

Example 1: Test Scores and Averages

For a course that involves three tests and a final exam, the record for a single student might have the form:

struct student 
{
   char name[20];
   double test1;
   double test2;
   double test3;
   double final;
};

Consider the program test-scores-1.c which declares four student variables, initializes the variables, and prints their records.

  1. Copy test-scores-1.c to your account, compile and run it, and review how it works.
    • Where is struct student declared? Why do you think the declaration comes before procedure printStudent and before main?
    • How are the fields of variable stu1 initialized?
    • Why do you think strcpy is used to initialize the name field for stu4?
    • In printing the output, how are the titles and printf statements coordinated, so that the test scores appear in aligned columns?
    • The format %-20s is used to print the name of a student. What does the minus sign accomplish? (Hint: What happens if the minus sign is removed?)
  2. Add function computeSemesterAverage to test-scores-1.c. This function should take a struct student as a parameter and return (not print) the weighted average that counts each test with a weight of 1 and the final exam with a weight of 2. That is, the semester average should be computed as:

    (test1 + test2 + test3 + 2 * final) / 5.0;

    Use computeSemesterAverage to add a column to the output of the program. This should include two parts (plus the definition of the function):
    • expand the printf statement in printStudent to include another value—the average for the student, and
    • expand the printing of the title in main to label the new column.
  3. As an experiment, change the value of the test1 field within computeSemesterAverage to 120.0. Is this new value printed in printStudent? Do you think the parameter struct student stu references the original data or makes a copy? Explain.
  4. To change a value in main, step 3 illustrates that one must pass the address of struct, so the parameter will refer back to the original value. Write a procedure addTenPercent that adds 10% to test2 of a student. The relevant procedure signature is:
    void
    addTenPercent (struct student * stu)
    This function would be called within main with the address operator (&), with a call such as
    addTenPercent (&stu1);
    Within addTenPercent, remember to use the asterisk * to refer back to the original struct in main:
    (*stu).test2 = ...
    After printing the original records, call addTenPercent for each of the four students, and then print their records again to check that the test2 scores have been changed.

An Array of Student Scores

Although test-scores-1.c was satisfactory for four students, the declaration of a different variable (e.g., stu1, stu2, stu3, stu4) for each student is tedious. As an alternative, program test-scores-2.c defines an array of struct students.

In this program:

  1. Working with test-scores-2.c, copy functions computeSemesterAverage and addTenPercent from test-scores-1.c. Also, add the revised printStudent procedure and the revised printf for the title.
    1. Compile and run the updated test-scores-2.c, and check that the output is the same as you obtained from test-scores-1.c.
    2. Add at least six more student records, so that the students array contains information for at least ten students.
    3. Write a procedure printMinMax that computes and prints the maximum and minimum semester averages for the entire class. Do NOT assume that all averages will be between 0.0 and 100.0, but rather initialize your search for a maximum and minimum with the averages of the first student. The signature of this procedure should be
      void
      printMinMax (struct student students[], int numStudents)
      where numStudents indicates the number of students in the students array.
    4. Modify printMinMax so that it prints the maximum and minimum semester averages, but also the names of the students with those averages.
    5. Modify test-scores-2.c so that 10% is added to each student's score for test 2, using the add10Percent procedure. This adjustment of student scores should occur after initialization, but before scores are printed or averages computed.

typedef statements

When working with test-scores-1.c and test-scores-2.c, you may have found it somewhat tedious to write struct student in the declaration of every variable and parameter. To simplify this syntax, C allows programmers to define new types. In this case, we might write

typedef struct 
{
   char name[20];
   double test1;
   double test2;
   double test3;
   double exam;
} student_t;

This defines a new data type student_t that you can use freely within your program with no further explicit mention of the keyword struct.

  1. Copy test-scores-3.c to your account, compile and run it, and review how the typedef statement works.
    1. What happens if you move the typedef declaration after the definition of printStudent?
    2. Append an additional field, semesterAvg, to the student_t definition, but leave the initialization as it is. Does the program compile and run?
    3. Add printing of the average member to printStudent, and observe what value is printed for stu.semesterAvg. How is a field initialized, if other fields of a struct are initialized, but all fields are initialized?
  2. After initializing the student_t array, use a loop to compute and store each student's semester average:
    
    for (int i = 0; i < 4; i++)
    {
      students[i].semesterAvg = computeSemesterAverage(students[i]);
    }
    Check that these computed averages are now printed by the program.

Musical Notes

Consider a song as a sequence of notes, each of which has a pitch and a duration. Playing a song might including announcing the song's title and then playing the notes. In this context, a note might have the following specification:

typedef struct
{
  int pitch;
  double duration;
} note_t;
  1. Write a program to announce two songs and play them, organizing your program in two parts:
    • Write a procedure to play a song, using the following signature:
      void
      playSong (char * title, note_t song[], int numNotes)

      In this signature, the title is a string giving the title of the song, song is the sequence of notes as an array, and numNotes specifies how many notes are in the song array.

      Procedure playSong should use printf to announce a song, using the given title. Then, playSong should go through the note_t sequence, telling the Scribbler 2 robot to play each note with the designated pitch and frequency.

    • Write a main program containing at least two songs, defined as note_t arrays of at least 10 notes each. After declaring and initializing the song arrays, use playSong to announce and play each song.
  2. Write a procedure to adjust the length of a note, using the following signature
    void
    scaleNoteLength (note_t * note, double factor)
    note refers back to the variable in main, so that a call to scaleNoteLength will change the original stored note. In processing, factor indicates the multiplier for a note's duration. For example:
    • When factor is 0.5, the duration of the note will be changed to half as long as the original duration.
    • When factor is 1.0, the duration of the note will be unchanged.
    • When factor is 2.0, the duration of the original note will be doubled.
    Within the program,
    • play each song once, as initialized.
    • call scaleNoteLength for the first song with an adjustment of 0.5, so each note will be shorter. Then play the song using playSong, so the entire song will be completed in half the time of the original.
    • call scaleNoteLength for the second song with an adjustment of 2.0, so each note will be longer. Then play the song using playSong, so the entire song will require twice the time of the original.