Storing Records with Parallel Arrays: Names, Ages, and Marks

In Strings in C: char Arrays, %s, fgets, and strlen you stored a name in a char array, and in the arrays lesson before it you stored five scores in an int array. A real class list needs both, and more: for every student, a name, an age and an average mark. In this lesson you keep a small class list in three arrays that work together, print it as a neat table, type the records in, look one up, find the top mark, and then scramble the records on purpose.

In this lesson you will

  • Describe a student as a record made of fields: name, age and average mark
  • Store the records in parallel arrays, where the same index in every array is one record
  • Print a lined-up table with printf widths such as %-10s, %3d and %6.1f
  • Enter records with fgets and scanf, and deal with the leftover Enter
  • Look up one record by its number, and find the record with the highest mark
  • Break it on purpose: mismatch an index, and reorder one array but not the others

Start a new project for this lesson. Copy 005-StringsCProgram to a new folder named 006-RecordsCProgram, save the project under the new name, and update its title and output filename, as before. Then replace main.c with each program below in turn.

Records and fields

The thing we keep data about, here a student, is called an entity. All the data about one student is a record, and each item in the record is a field. Our records have three fields: a name (text), an age (a whole number) and an average mark (a decimal number). Because the fields have different types, they cannot share one array. Instead we use one array per field:

    char names[5][20];   /* 5 names, each up to 19 characters */
    int ages[5];
    float marks[5];

char names[5][20] is an array of 5 strings, and each string is itself a char array of 20. Read it as “5 rows of 20 characters”. names[0] is the first name, and you use it anywhere you used name in the strings lesson. The key idea is that the three arrays are parallel: the same index in each array belongs to the same student. Record 2 is names[2], ages[2] and marks[2], together:

Record (index i)Name: names[i]Age: ages[i]Average mark: marks[i]
0"Ken"1672.5
1"Aaliyah"1788.0
2"Ravi"1664.5
3"Shania"1891.5
4"Marcus"1779.0

Each row is one record. Each column is one field, stored in its own array. Nothing in C ties the three arrays together: the only link between "Ravi", 16 and 64.5 is that they all sit at index 2. Keep that in mind for the break-it sections.

Try it: print the class list

Start with the data typed into the program, using initialiser lists. An array of strings takes a list of string literals:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    char names[5][20] = {"Ken", "Aaliyah", "Ravi", "Shania", "Marcus"};
    int ages[5] = {16, 17, 16, 18, 17};
    float marks[5] = {72.5, 88.0, 64.5, 91.5, 79.0};
    int i;

    printf("%-3s %-10s %3s %6s\n", "No", "Name", "Age", "Mark");
    for (i = 0; i < 5; i++)
    {
        printf("%-3d %-10s %3d %6.1f\n", i, names[i], ages[i], marks[i]);
    }
    return 0;
}
No  Name       Age   Mark
0   Ken         16   72.5
1   Aaliyah     17   88.0
2   Ravi        16   64.5
3   Shania      18   91.5
4   Marcus      17   79.0

The loop is the same for (i = 0; i < 5; i++) pattern from the arrays lesson. On each pass, one printf prints field after field of record i, so one pass is one row of the table.

What just happened: printf widths

A number between the % and the letter is a width: the least number of characters the value takes up. Shorter values are padded with spaces, which is what lines the columns up:

  • %-10s: a string in 10 characters. The - means left-aligned, padded on the right. Ken becomes Ken plus 7 spaces.
  • %3d: an int in 3 characters, right-aligned (no -), so 16 becomes a space and 16.
  • %6.1f: a float in 6 characters with 1 decimal place. You already know .1 from %.2f; the 6 is the width.
  • %-3d: the record number, left-aligned in 3 characters.

The heading line uses the same widths with %s for every column, so the titles sit above their columns. Try changing %-10s to %10s in both lines and run again: the names line up on the right instead. Change it back. A value longer than its width is never cut off; it just pushes the rest of the line along.

#include <stdio.h>
#include <stdlib.h>

int main()
{
    char names[5][20] = {"Ken", "Aaliyah", "Ravi", "Shania", "Marcus"};
    int ages[5] = {16, 17, 16, 18, 17};
    float marks[5] = {72.5, 88.0, 64.5, 91.5, 79.0};
    int i;

    printf("%-3s %-10s %3s %6s\n", "No", "Name", "Age", "Mark");
    for (i = 0; i < 5; i++)
    {
        printf("%-3d %-10s %3d %6.1f\n", i, names[i], ages[i + 1], marks[i]);
    }
    return 0;
}

Break it on purpose: mix up the index

In the loop, change ages[i] to ages[i + 1] and leave the other two as they are. The program is shown above. Build and run. There is no error. On our test computer it printed:

No  Name       Age   Mark
0   Ken         17   72.5
1   Aaliyah     16   88.0
2   Ravi        18   64.5
3   Shania      17   91.5
4   Marcus       0   79.0

Every age is now from the next student: Ken has Aaliyah’s age, Aaliyah has Ravi’s, and so on. Each row looks perfectly sensible, which is what makes this mistake dangerous: the compiler cannot know which age belongs to whom. The last row is worse. For i = 4 it reads ages[5], which is out of bounds, so the 0 is just whatever was in memory there (undefined behaviour, as in the arrays lesson); yours may differ. The rule for parallel arrays: one record, one index, the same in every array. Change it back to ages[i].

Try it: type the records in

Now let the user enter each student. Names can contain a space (Ken Ali), so read them with fgets and remove the Enter, as in the strings lesson. names[i] is a char array, so it needs no &; ages[i] and marks[i] are single numbers, so they do. Here is a first try:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    char names[5][20] = {""};
    int ages[5] = {0};
    float marks[5] = {0};
    int i;

    for (i = 0; i < 5; i++)
    {
        printf("Record %d name: ", i);
        fgets(names[i], sizeof(names[i]), stdin);
        names[i][strcspn(names[i], "\n")] = '\0';   /* remove the Enter */
        printf("Age: ");
        scanf("%d", &ages[i]);
        printf("Average mark: ");
        scanf("%f", &marks[i]);
    }

    printf("%-3s %-10s %3s %6s\n", "No", "Name", "Age", "Mark");
    for (i = 0; i < 5; i++)
    {
        printf("%-3d %-10s %3d %6.1f\n", i, names[i], ages[i], marks[i]);
    }
    return 0;
}

Break it on purpose: forget the leftover Enter

Build and run. Type Ken Ali, 16 and 72.5, pressing Enter after each. Observe: the program never waits for the second name. It jumps straight to Age::

Record 0 name: Ken Ali
Age: 16
Average mark: 72.5
Record 1 name: Age: 

Keep typing the same answers in the same order (Aaliyah Mohammed, 17, 88, and so on) wherever the program stops. On our test computer, the table at the end came out like this:

No  Name       Age   Mark
0   Ken Ali     16   72.5
1                0    0.0
2   Aaliyah Mohammed  17   88.0
3                0    0.0
4   Ravi Singh  16   64.5

Records 1 and 3 are empty and everyone else has shifted down. This is the input buffer again. scanf("%f") read 72.5 and stopped, leaving your Enter waiting. The next fgets read that Enter as a complete, empty line, so record 1 got an empty name. Then scanf("%d") met the letters of Aaliyah, could not read them as a number, and left them (and ages[1], still 0) alone, so the next fgets picked up the name one record late. One leftover Enter threw every record after it out of line.

The fix is to take that one leftover Enter out of the buffer before the next fgets. getchar(), from stdio.h, reads exactly one character. Add it straight after the scanf for the mark:

        scanf("%f", &marks[i]);
        getchar();   /* eat the Enter left after the mark */

Build and run again, and enter all five students:

Record 0 name: Ken Ali
Age: 16
Average mark: 72.5
Record 1 name: Aaliyah Mohammed
Age: 17
Average mark: 88
Record 2 name: Ravi Singh
Age: 16
Average mark: 64.5
Record 3 name: Shania Joseph
Age: 18
Average mark: 91.5
Record 4 name: Marcus Lee
Age: 17
Average mark: 79
No  Name       Age   Mark
0   Ken Ali     16   72.5
1   Aaliyah Mohammed  17   88.0
2   Ravi Singh  16   64.5
3   Shania Joseph  18   91.5
4   Marcus Lee  17   79.0

Every record is right. Notice rows 1 and 3: Aaliyah Mohammed and Shania Joseph are longer than 10 characters, so, as promised, %-10s does not cut them off and the rest of the row is pushed along. Widen the name column to %-18s in both printf lines and the table lines up again. Remember too that each name holds at most 19 characters; fgets stops there, safely.

Try it: look up one record

Because a record is just an index, finding a student by number is one scanf. Go back to the typed-in data and replace main with this:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    char names[5][20] = {"Ken", "Aaliyah", "Ravi", "Shania", "Marcus"};
    int ages[5] = {16, 17, 16, 18, 17};
    float marks[5] = {72.5, 88.0, 64.5, 91.5, 79.0};

    int n = 0;

    printf("Which record (0 to 4)? ");
    scanf("%d", &n);

    printf("Name: %s\n", names[n]);
    printf("Age: %d\n", ages[n]);
    printf("Average mark: %.1f\n", marks[n]);
    return 0;
}

Run it and type 3:

Which record (0 to 4)? 3
Name: Shania
Age: 18
Average mark: 91.5

The single number n picks out all three fields of record 3. Now run it again and type 7. There are only 5 records, so names[7], ages[7] and marks[7] are all out of bounds: you may get garbage, an empty name, or a crash. The program cannot refuse a bad number yet, because that needs an if, which you meet properly in a later lesson.

Try it: who has the top mark?

To find the best student, look at every mark and remember the index of the highest so far. This needs one new idea, the if statement, shown here only lightly: if (condition) { ... } runs the statements in the braces only when the condition is true. You will learn it properly later.

#include <stdio.h>
#include <stdlib.h>

int main()
{
    char names[5][20] = {"Ken", "Aaliyah", "Ravi", "Shania", "Marcus"};
    int ages[5] = {16, 17, 16, 18, 17};
    float marks[5] = {72.5, 88.0, 64.5, 91.5, 79.0};
    int i;

    int best = 0;   /* index of the best record so far */

    for (i = 1; i < 5; i++)
    {
        if (marks[i] > marks[best])
        {
            best = i;
        }
    }

    printf("Top mark: %s, age %d, with %.1f\n", names[best], ages[best], marks[best]);
    return 0;
}
Top mark: Shania, age 18, with 91.5

best starts at 0, assuming the first student is the best so far, so the loop starts from index 1. On each pass, marks[i] > marks[best] asks “is this mark higher than the best so far?” If it is, best becomes i. Trace it: 88.0 beats 72.5 (best = 1), 64.5 does not, 91.5 beats 88.0 (best = 3), 79.0 does not. Because best is an index, not a mark, it finds the whole record: the name and age come along for free. Try it: change > to < and the program finds the lowest mark instead. What does it print?

#include <stdio.h>
#include <stdlib.h>

int main()
{
    char names[5][20] = {"Ken", "Aaliyah", "Ravi", "Shania", "Marcus"};
    int ages[5] = {16, 17, 16, 18, 17};
    float marks[5] = {91.5, 88.0, 79.0, 72.5, 64.5};
    int i;

    printf("%-3s %-10s %3s %6s\n", "No", "Name", "Age", "Mark");
    for (i = 0; i < 5; i++)
    {
        printf("%-3d %-10s %3d %6.1f\n", i, names[i], ages[i], marks[i]);
    }
    return 0;
}

Break it on purpose: sort one array and not the others

A teacher wants the list from highest mark to lowest. A thought experiment: what if you sort only marks? Go back to the table program and change just the marks list to {91.5, 88.0, 79.0, 72.5, 64.5}, the same five marks in order, leaving names and ages alone. The program is shown above. Build and run:

No  Name       Age   Mark
0   Ken         16   91.5
1   Aaliyah     17   88.0
2   Ravi        16   79.0
3   Shania      18   72.5
4   Marcus      17   64.5

The marks are sorted, but now Ken has Shania’s 91.5 and Shania has Ken’s 72.5. Every mark still exists, but most are attached to the wrong student. With parallel arrays, any reordering must move all the fields of a record together: whenever you move marks[i], you must move names[i] and ages[i] in exactly the same way. Forget one array once, and the records are scrambled for good.

That is the weakness of parallel arrays: the fields of one record are spread across separate arrays, held together only by your care with the index. C has a better tool, the struct, which groups a record’s fields into one variable, so a student’s name, age and mark travel together. Structs are coming in a later lesson.

Common mistakes

  • Using different indexes for the same record, such as names[i] with ages[i + 1].
  • Reordering, adding or deleting in one array but not the others.
  • Forgetting getchar() after scanf, so the next fgets reads an empty line.
  • Putting & in front of names[i] (a string), or leaving it off &ages[i] (a number).
  • Looking up a record number outside 0 to 4.

Key terms

  • Entity: the thing you store data about, here a student.
  • Record: all the data about one entity, such as one student’s name, age and mark.
  • Field: one item in a record, such as the age.
  • Parallel arrays: separate arrays, one per field, where the same index in each array is one record.
  • Array of strings: a two-dimensional char array such as char names[5][20]: 5 strings of up to 19 characters.
  • Field width: the number in a format specifier, such as the 10 in %-10s, giving the least number of characters to print. - means left-aligned.
  • getchar(): reads one character from the input; used here to remove a leftover Enter.
  • if: runs a block of statements only when a condition is true (a full lesson comes later).
  • struct: a C type that groups the fields of a record together (coming in a later lesson).

Quick check

  1. In the class list, which three elements make up record 4?

    Show answer

    names[4], ages[4] and marks[4]: Marcus, 17, 79.0.

  2. What does printf("[%-6s][%4d]", "Ana", 15); print?

    Show answer

    [Ana ][ 15]: Ana left-aligned in 6 characters, 15 right-aligned in 4.

  3. A program reads a mark with scanf("%f", &marks[i]); and then the next name with fgets, but every name after the first is empty. Why, and what is the fix?

    Show answer

    scanf leaves the Enter in the input buffer, and fgets reads it as an empty line. Add getchar(); after the scanf.

  4. Why does the top-mark program remember best as an index rather than the mark itself?

    Show answer

    An index identifies the whole record, so names[best] and ages[best] give the name and age that go with the mark.

  5. You sort names into alphabetical order but leave ages and marks alone. What goes wrong?

    Show answer

    The records are scrambled: names no longer line up with their ages and marks, because each record is held together only by its index.

Part of the Introduction to C series. Next: Storing Data in Files (Plain Text and Binary)

2 thoughts on “Storing Records with Parallel Arrays: Names, Ages, and Marks”

Leave a Reply