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
printfwidths such as%-10s,%3dand%6.1f - Enter records with
fgetsandscanf, 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" | 16 | 72.5 |
| 1 | "Aaliyah" | 17 | 88.0 |
| 2 | "Ravi" | 16 | 64.5 |
| 3 | "Shania" | 18 | 91.5 |
| 4 | "Marcus" | 17 | 79.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.KenbecomesKenplus 7 spaces.%3d: anintin 3 characters, right-aligned (no-), so16becomes a space and16.%6.1f: afloatin 6 characters with 1 decimal place. You already know.1from%.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]withages[i + 1]. - Reordering, adding or deleting in one array but not the others.
- Forgetting
getchar()afterscanf, so the nextfgetsreads an empty line. - Putting
&in front ofnames[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
chararray such aschar 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
In the class list, which three elements make up record 4?
Show answer
names[4],ages[4]andmarks[4]: Marcus, 17, 79.0.What does
printf("[%-6s][%4d]", "Ana", 15);print?Show answer
[Ana ][ 15]: Ana left-aligned in 6 characters, 15 right-aligned in 4.A program reads a mark with
scanf("%f", &marks[i]);and then the next name withfgets, but every name after the first is empty. Why, and what is the fix?Show answer
scanfleaves the Enter in the input buffer, andfgetsreads it as an empty line. Addgetchar();after thescanf.Why does the top-mark program remember
bestas an index rather than the mark itself?Show answer
An index identifies the whole record, so
names[best]andages[best]give the name and age that go with the mark.You sort
namesinto alphabetical order but leaveagesandmarksalone. 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)
[…] Part of the Introduction to C series. Next: Storing Records with Parallel Arrays […]
[…] the previous lesson, Storing Records with Parallel Arrays, you learned to keep related information at the same index. Now you will build a small C program […]