In 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 that saves student names, Mathematics marks and English marks in a text file, then loads them when the program starts.
Learning objectives
By the end, you should be able to use parallel arrays, track the number of records with n, build a repeating menu, write records using fprintf(), read records using fscanf(), and explain the difference between RAM and permanent file storage.
1. Plan the student records
We need three parallel arrays: names, maths and english. The same index refers to the same student. MAX is the capacity (100); n is the number of students actually entered. Initially n = 0.
#define MAX 100
char names[MAX][50];
int maths[MAX];
int english[MAX];
int n = 0;
For example, index 0 might contain John, 75, 80; index 1 Mary, 90, 85; index 2 Peter, 65, 70. Each time a new record is added, store the values at index n, then execute n++.
2. What will the text file look like?
We will use students.txt. The first line stores the number of records. Each subsequent line stores one name and two marks:
3
John 75 80
Mary 90 85
Peter 65 70
Names are one word in this introductory example. scanf("%49s", ...) reads a single word safely into a 50-character name array.
3. Add and display records
addStudent() checks that the arrays are not full, reads one name and two marks, and increases n. displayData() loops from index 0 to n - 1, displaying only records actually entered.
4. Save the records
fopen("students.txt", "w") opens the file for writing. The "w" mode creates the file if needed but replaces any existing contents. fprintf(fp, "%d\n", n) writes the record count first; a for loop writes each record; fclose(fp) closes the file. printf() prints to the screen, while fprintf() writes to a file.
5. Load the records
fopen("students.txt", "r") opens the file for reading. If the file is missing, the program starts with no saved records. fscanf() reads the first number into n, then a for loop reads that many student records into the arrays. The program checks the record count and stops if a record is incomplete.
6. Use a menu
The main() function calls loadData() before showing the menu. A do...while loop repeats until the user chooses 4. A switch statement calls addStudent(), displayData() or saveData().
7. Complete C program
Copy the following program into Code::Blocks, compile it, and run it.
#include <stdio.h>
#define MAX 100
char names[MAX][50];
int maths[MAX], english[MAX];
int n = 0;
void loadData() {
FILE *fp = fopen("students.txt", "r");
if (fp == NULL) return;
if (fscanf(fp, "%d", &n) != 1) {
n = 0;
fclose(fp);
return;
}
if (n < 0 || n > MAX) {
n = 0;
fclose(fp);
return;
}
for (int i = 0; i < n; i++) {
if (fscanf(fp, "%49s %d %d",
names[i], &maths[i], &english[i]) != 3) {
n = i;
break;
}
}
fclose(fp);
}
void saveData() {
FILE *fp = fopen("students.txt", "w");
if (fp == NULL) {
printf("Cannot open file.\n");
return;
}
fprintf(fp, "%d\n", n);
for (int i = 0; i < n; i++)
fprintf(fp, "%s %d %d\n", names[i], maths[i], english[i]);
fclose(fp);
printf("Data saved.\n");
}
void addStudent() {
if (n >= MAX) {
printf("Arrays are full.\n");
return;
}
printf("Enter name: ");
scanf("%49s", names[n]);
printf("Enter Maths mark: ");
scanf("%d", &maths[n]);
printf("Enter English mark: ");
scanf("%d", &english[n]);
n++;
}
void displayData() {
printf("\nName\tMaths\tEnglish\n");
for (int i = 0; i < n; i++)
printf("%s\t%d\t%d\n", names[i], maths[i], english[i]);
}
int main() {
int choice;
loadData();
do {
printf("\n1. Add Student\n2. Display Students\n3. Save Data\n4. Exit\nEnter choice: ");
scanf("%d", &choice);
switch (choice) {
case 1: addStudent(); break;
case 2: displayData(); break;
case 3: saveData(); break;
case 4: printf("Goodbye!\n"); break;
default: printf("Invalid choice.\n");
}
} while (choice != 4);
return 0;
}
8. Practical activity
Activity A: Run the program. Add John (75, 80), Mary (90, 85), and Peter (65, 70). Select Display and check all three records.
Activity B: Select Save. Open students.txt in the program’s working directory. Verify that the first line is 3 and the next three lines contain the records.
Activity C: Exit and run the program again. Select Display. Explain why the records are still available.
Activity D: Add Sarah (88, 92), save, exit, restart, and verify that four records load.
9. Check your understanding
What is the difference between MAX and n?
Why must the same index be used in all three arrays?
What does n++ do?
Why is the record count stored on the first line?
What is the difference between "w" and "r"?
How do fprintf() and fscanf() differ?
Why is loadData() called before the menu?
What happens if you add a student and exit without saving?
10. Programming challenges
Try these extensions after the original program works: calculate each student’s average; find the highest Mathematics mark; save automatically when exiting; add a Computer Science mark and update both file functions; count students who scored at least 50 in Mathematics.
Summary
Arrays hold the records in memory while the program runs. A text file keeps the records for the next run. saveData() moves information from arrays to the file, and loadData() moves it from the file back into arrays.
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"
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. 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
In the class list, which three elements make up record 4?
Show answer
names[4], ages[4] and marks[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 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.
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.
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)
Every variable so far has held exactly one value. In Using Library Functions: #include, getch(), and the Input Buffer you used functions that other people wrote; in this lesson you learn to keep a whole list of values under one name. You start with five test scores in five separate variables, feel how quickly that gets painful, then store them in one array, visit every score with a short loop, work out the average, and break it on purpose by reaching past the end.
In this lesson you will
See why five separate variables (score1 to score5) do not scale
Declare an array, give it a size, and read and write its elements by index, counting from 0
Fill an array in one line with an initialiser list{ }
Meet a first for loop, just enough to visit every index, and use it to print, total and average the scores
Read values into the array with scanf
Break it on purpose: go past the end, forget the size, copy with =, and use <= in the loop
Start a new project for this lesson. Copy 003-FunctionDemoCProgram to a new folder named 004-ArraysCProgram, 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.
Try it: five scores, five variables
Suppose a teacher wants the total of five test scores. With what you know so far, you need one variable per score:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int score1 = 72;
int score2 = 85;
int score3 = 64;
int score4 = 90;
int score5 = 58;
int total = score1 + score2 + score3 + score4 + score5;
printf("Total: %d\n", total);
return 0;
}
Total: 369
It works, but imagine the class has 30 students. That is 30 declarations, and a sum line with 30 names in it. Miss one and the total is wrong with no error. Every new score means editing the program in several places. What we want is one name for the whole list of scores, and a number that says which score we mean.
First score: 72
Last score: 58
Third score, corrected: 70
What just happened
int scores[5]; declares an array of 5 int variables side by side in memory, all called scores. The number in square brackets in the declaration is the size: how many elements (boxes) the array has. After that, a number in square brackets is an index: which box you mean. scores[0] = 72; writes to a box, and scores[0] inside printf reads from it. Each element behaves exactly like an ordinary int variable.
Counting starts at 0. An array of size 5 has indexes 0, 1, 2, 3 and 4. There is noscores[5]. In memory it looks like this:
Element
scores[0]
scores[1]
scores[2]
scores[3]
scores[4]
Index
0
1
2
3
4
Contents
72
85
70
90
58
Which score
1st
2nd
3rd
4th
5th
Notice that the 1st score is at index 0 and the 5th is at index 4: the index is always one less than the position. Changing scores[2] to 70 changed one box and left the others alone.
Try it: fill it in one line
Writing each element on its own line is still long. You can fill an array as you declare it with an initialiser list: the values in curly braces, separated by commas, in index order:
The first value goes into scores[0], the next into scores[1], and so on. If you give fewer values than the size, the rest are set to 0, so int scores[5] = {0}; is a handy way to start with every box at 0, just like the defensive initialisation in the scanf lesson.
Now break the declaration:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int scores[];
printf("Second score: %d\n", scores[1]);
printf("Fourth score: %d\n", scores[3]);
return 0;
}
Break it on purpose: forget the size
Change the declaration to int scores[];, with no size and no initialiser list. Build it. The build stops with:
main.c:6:9: error: array size missing in 'scores'
C must know how many boxes to set aside before the program runs. Either put the size in the brackets, or give an initialiser list so the compiler can count the values: int scores[] = {72, 85, 64, 90, 58}; makes an array of 5. Put the 5 and the list back.
Try it: visit every index with a for loop
Printing each score with its own printf is no better than five variables. The point of an index is that it can be a variable: scores[i] means “the box whose number is in i”. A for loop lets i take the values 0, 1, 2, 3, 4 in turn. Loops get a lesson of their own later. For now, learn this one pattern:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int scores[5] = {72, 85, 64, 90, 58};
int i;
for (i = 0; i < 5; i++)
{
printf("scores[%d] = %d\n", i, scores[i]);
}
return 0;
}
Read for (i = 0; i < 5; i++) in three parts, separated by semicolons:
i = 0: start with i at 0, the first index.
i < 5: keep going while i is less than 5, the size. When i reaches 5 the loop stops.
i++: after each pass, add 1 to i. It is short for i = i + 1.
The statements in the curly braces run once for each value of i. Change the 5 to 3 and only the first three scores print. Change it back. With 30 students, you would change the array size and the 5 in the loop, and nothing else.
#include <stdio.h>
#include <stdlib.h>
int main()
{
int scores[5] = {72, 85, 64, 90, 58};
int i;
for (i = 0; i <= 5; i++)
{
printf("scores[%d] = %d\n", i, scores[i]);
}
return 0;
}
Break it on purpose: use <= in the loop
Change i < 5 to i <= 5 (“less than or equal to”). Build and run. There is no error and no warning. On our test computer it printed:
Six lines from a five-box array. With <=, the loop also runs when i is 5, and reads scores[5], a box that does not exist. The value you see is whatever happens to be in the memory just after the array, so your number may be different, and it may change from run to run. This is called an off-by-one error. For an array of size 5, the loop test is i < 5, never i <= 5. Change it back.
Try it: total and average
The loop can do more than print. Here it adds each score to a running total, then divides by 5:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int scores[5] = {72, 85, 64, 90, 58};
int i;
int total = 0;
float average = 0;
for (i = 0; i < 5; i++)
{
total = total + scores[i];
}
average = total / 5;
printf("Total: %d\n", total);
printf("Average: %.2f\n", average);
return 0;
}
Total: 369
Average: 73.00
The total is right, but 369 ÷ 5 is 73.8, not 73.00. You have seen this before in the int vs float lesson. total and 5 are both int, so C does whole-number division and truncates the answer to 73 before it is stored in the float. Storing it in a float afterwards cannot bring the .8 back. Make one side of the division a decimal number:
average = total / 5.0;
Total: 369
Average: 73.80
Trace the loop once by hand: total starts at 0, then becomes 72, 157, 221, 311 and finally 369. If you want to watch it happen, set a breakpoint on the total = total + scores[i]; line and add i and total as watches, as in the debugging lesson.
Try it: read the scores in
Now let the user type the scores. Each element is an ordinary int, so scanf needs its location, with &, just like &x in the scanf lesson:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int scores[5] = {0};
int i;
int total = 0;
for (i = 0; i < 5; i++)
{
printf("Enter score %d: ", i + 1);
scanf("%d", &scores[i]);
}
for (i = 0; i < 5; i++)
{
total = total + scores[i];
}
printf("You entered: %d %d %d %d %d\n", scores[0], scores[1], scores[2], scores[3], scores[4]);
printf("Average: %.2f\n", total / 5.0);
return 0;
}
Build and run. Type each score and press Enter:
Enter score 1: 72
Enter score 2: 85
Enter score 3: 64
Enter score 4: 90
Enter score 5: 58
You entered: 72 85 64 90 58
Average: 73.80
&scores[i] means “the location of box i”. Each element is one int box, so scanf needs the & in front of it, exactly as for a single variable. The prompt shows i + 1 so that people see 1 to 5, while the program uses indexes 0 to 4.
Try it: one past the end
Replace main with this short program, which asks for the element after the last one:
Build and run. On our test computer, GCC gave no error and no warning (even with -Wall), and printed:
scores[4] = 58
scores[5] = 0
C does not check indexes. It works out where box 5 would be and reads that memory, even though it is not part of the array. This is out of bounds access, and the C standard calls the result undefined behaviour: it may print a garbage number, it may print 0 and look fine, it may give a different answer on another computer, or the program may crash. Writing out of bounds, such as scores[5] = 99;, is worse, because it can change some other variable without warning. Keeping every index between 0 and size − 1 is your job, not the compiler’s.
Try it: copy one array into another
Suppose you want a backup of the scores before changing them:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int scores[5] = {72, 85, 64, 90, 58};
int backup[5];
backup = scores;
printf("backup[0] = %d\n", backup[0]);
return 0;
}
Break it on purpose: copy an array with =
Build it. The build stops with:
main.c:9:12: error: assignment to expression with array type
After it is declared, a whole array cannot be assigned with =. Instead, copy the elements one at a time with the loop.
Replace the backup = scores; line with a loop (and declare int i; with the other variables):
for (i = 0; i < 5; i++)
{
backup[i] = scores[i];
}
backup[0] = 72
Common mistakes
Using index 5 in an array of size 5. The last index is always size − 1.
Writing i <= 5 instead of i < 5 in the loop.
Forgetting that total / 5 with two ints truncates. Use 5.0.
Leaving out the & in scanf("%d", &scores[i]);.
Trying to copy or assign a whole array with =.
Key terms
Array: a row of variables of the same type under one name, such as int scores[5].
Element: one box in an array, such as scores[2].
Size: the number of elements, given in the declaration. It cannot change while the program runs.
Index: the position of an element, counted from 0. Valid indexes run from 0 to size − 1.
Initialiser list: values in curly braces that fill an array as it is declared, such as {72, 85, 64, 90, 58}.
for loop: repeats a block of statements; here, once for each index from 0 to size − 1.
Off-by-one error: a loop or index that goes one step too far (or stops one step short).
Out of bounds: using an index outside the array. C does not check it; the result is undefined behaviour.
Quick check
An array is declared float temps[7];. What are its first and last valid indexes?
Show answer
temps[0] and temps[6].
What is in each element after int counts[4] = {3, 9};?
Show answer
counts[0] is 3, counts[1] is 9, and counts[2] and counts[3] are 0.
A program prints scores[5] from a size-5 array and gets 0, with no error. Is the program correct?
Show answer
No. scores[5] is out of bounds. C does not check it, so the 0 is just whatever was in that memory: undefined behaviour that may print something else, or crash, elsewhere.
A total of 47 is divided by 4 with average = total / 4; and average is a float. What prints with %.2f, and how do you fix it?
Show answer
11.00, because int ÷ int truncates. Use total / 4.0 to get 11.75.
Why does scanf("%d", &scores[i]); need the &?
Show answer
Each element is an ordinary int variable, and scanf needs its location, just like &x.
Coming up: text is an array too
Every array in this lesson held numbers, but the type in each box can be anything, including char, a single character. Put a row of characters in an array and you have a word. In the next lesson you will see that a string in C is just an array of char, so everything here, from indexes starting at 0 to = refusing to copy, carries straight over.
In the last lesson, Arrays in C: Storing Many Values Under One Name, you stored many numbers under one name in an array. A string is the same idea: an array of char, one character in each box. Every variable so far has held a number, but programs work with text all the time: names, cities, answers. In this lesson you store one character, then a whole word, then read a name from the keyboard and break it on purpose. Along the way you find out what the \0 escape sequence from the printf lesson is for, and meet the input buffer from Using Library Functions: #include, getch(), and the Input Buffer again.
In this lesson you will
Store a single character in a char variable and print it with %c
Store a word as a string: a row of characters ending in '\0'
Print a string with %s and read one with scanf, and find out why it stops at a space
Read a whole line safely with fgets, and spot the Enter key it keeps
Use strlen and strcpy from string.h, and learn why = cannot copy a string
Start a new project for this lesson. Copy 004-ArraysCProgram to a new folder named 005-StringsCProgram, 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.
Try it: one character
The data-type table in the int vs float lesson listed char, a type that holds a single character, printed with %c. Try it:
#include <stdio.h>
#include <stdlib.h>
int main()
{
char initial = 'K';
printf("Your initial is %c\n", initial);
return 0;
}
Your initial is K
Notice the single quotes. 'K' is a character literal: exactly one character. Double quotes make a string literal, which can hold many. Change 'K' to "K" and the build complains. Change it back.
Try it: a whole word
A char holds one character, so a name needs a whole row of them. Replace main with this:
Hello, Ken!
Letters: K e n
After the last letter: 0
Hello, Ben!
What just happened
char name[20] declares an array, just like int scores[5] in the arrays lesson, except that each of its 20 boxes holds a char instead of an int. You reach each one by its index in square brackets, and counting still starts at 0, so name[0] is the first character and name[19] is the last.
Initialising the array with "Ken" fills it like this:
Index
0
1
2
3
4 to 19
Contents
'K'
'e'
'n'
'\0'
'\0' (unused)
Straight after the last letter sits '\0', the null character. Printed as a number with %d, it shows as 0. This is the end marker that the escape-sequence table promised to explain: in C, a string is a row of characters that ends with '\0'. %s tells printf to start at the first character and keep printing until it reaches the '\0'. Changing name[0] to 'B' changed one box, so the whole word became Ben.
Because the end marker takes up one place, an array of 20 characters can hold a string of at most 19 characters.
Try it: ask for the name
Now let the user type the name. scanf reads a string with %s:
#include <stdio.h>
#include <stdlib.h>
int main()
{
char name[20] = "";
char colour[20] = "";
printf("What is your name?\n");
scanf("%s", name);
printf("What is your favourite colour?\n");
scanf("%s", colour);
printf("Hello, %s! Your favourite colour is %s.\n", name, colour);
return 0;
}
Build and run. Type Ken and press Enter, then type blue and press Enter:
What is your name?
Ken
What is your favourite colour?
blue
Hello, Ken! Your favourite colour is blue.
Look closely: there is no& in front of name. In the scanf lesson, scanf needed &x, the location of the variable. The name of an array already stands for the location of its first character, so name on its own is enough. Add an & out of habit and GCC warns:
warning: format '%s' expects argument of type 'char *', but argument 2 has type 'char (*)[20]' [-Wformat=]
If you tried that, remove the & again and rebuild.
Break it on purpose: type a name with a space
Run the program again. At the first question type Ken Smith and press Enter. Observe: the program does not wait for the colour. It answers straight away:
What is your name?
Ken Smith
What is your favourite colour?
Hello, Ken! Your favourite colour is Smith.
Your surname has become your favourite colour.
%s in scanf reads one word. It stops at the first space, tab or Enter. So name got Ken, and the rest of the line, Smith and the Enter, stayed behind in the input buffer, the same waiting area that caught the held-down key in the previous lesson. The second scanf found Smith already waiting and took it without asking.
Try it: read the whole line with fgets
To read everything the user typed, up to and including the Enter, use fgets (“file get string”). Here the “file” is stdin, the standard input, which is normally the keyboard. The program also uses strlen (“string length”), which counts the characters before the '\0'. It is declared in string.h, so the program includes that header:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
char name[20] = "";
printf("What is your full name?\n");
fgets(name, sizeof(name), stdin);
int length = strlen(name);
printf("Hello, %s!\n", name);
printf("Your name has %d characters.\n", length);
return 0;
}
Run it and type Ken Smith. Observe two odd things:
What is your full name?
Ken Smith
Hello, Ken Smith
!
Your name has 10 characters.
The ! has dropped onto the next line, and Ken Smith has 9 characters (the space counts), not 10. Both clues point to the same thing: fgets keeps the Enter key. The string in memory is Ken Smith\n. That hidden \n is the tenth character, and printing it moves the ! to a new line.
fgets takes three things: where to store the text, the size of the array (sizeof(name), here 20), and where to read from (stdin). Because it knows the size, it stores at most 19 characters plus the '\0'.
To remove the Enter, add one line after the fgets. strcspn(name, "\n"), also from string.h, counts the characters before the first \n, which is exactly the index where the \n sits. Putting '\0' there ends the string one place earlier. Rebuild and run with Ken Smith again:
fgets(name, sizeof(name), stdin);
name[strcspn(name, "\n")] = '\0'; /* remove the Enter */
What is your full name?
Ken Smith
Hello, Ken Smith!
Your name has 9 characters.
Now try a long name, such as Alexandria Montgomery-Smith. The program prints Hello, Alexandria Montgome! (19 characters). fgets stopped when the array was full and left the rest in the input buffer. Cut off, but safe.
Why the size matters: buffer overflow.scanf("%s", name) does not know how big name is. Type more than 19 characters and it writes past the end of the array, over whatever memory comes next. This is a buffer overflow. The program may crash, quietly change other variables, or work on one computer and fail on another, and attackers use overflows to break into systems. fgets is given the size, so it cannot overflow. (Old books mention a function called gets. It has the same problem and has been removed from C. Never use it.)
Try it: give the string a new value
You can set a number with = at any time, so it seems natural to do the same with a string:
main.c:10:10: error: assignment to expression with array type
= worked in the declaration, char name[20] = "Ken";, because that is initialisation: the array is filled as it is created. Afterwards, C does not let you assign a whole array with =.
To copy text into an existing array, use strcpy (“string copy”) from string.h. It copies the characters one by one, including the '\0'. Replace the = line with:
strcpy(name, "Kenneth");
Before: Ken
After: Kenneth
Read strcpy(name, "Kenneth") as “copy "Kenneth" into name”: the destination comes first, like the left side of =. Like scanf("%s"), strcpy does not check the size, so the destination must be big enough.
Key terms
char: a data type that holds a single character, printed with %c.
Character literal: one character in single quotes, such as 'K'.
Array: a row of variables of the same type under one name, such as char name[20].
Index: the position of an item in an array, counted from 0, as in name[0].
String: in C, a row of characters in a char array, ending with '\0'.
Null character ('\0'): the character with value 0 that marks the end of a string.
%s: the format specifier for a string, in both printf and scanf.
fgets: reads a whole line of input, up to a size limit, and keeps the Enter (\n).
strlen: a string.h function that counts the characters in a string, not including the '\0'.
Buffer overflow: writing past the end of an array, into memory that belongs to something else.
Quick check
How big must a char array be to hold the string "Hello"?
Show answer
At least 6: five letters plus the '\0' at the end.
A program uses scanf("%s", city); and the user types Port of Spain. What ends up in city, and where is the rest?
Show answer
Port. %s stops at the first space, and of Spain plus the Enter wait in the input buffer for the next read.
After fgets(name, sizeof(name), stdin) reads Ana, strlen(name) says 4. Why?
Show answer
fgets keeps the Enter, so the string is Ana\n. Remove it with name[strcspn(name, "\n")] = '\0';
city = "Paris"; gives assignment to expression with array type. How do you fix it?
Show answer
Use strcpy(city, "Paris"); and add #include <string.h>. Arrays cannot be assigned with = after they are declared.
Your addition program prints its welcome message and then rushes straight on to the first question. It would be friendlier to pause with “Press any key to continue…” and wait. C has no built-in command for that, but libraries do. In this lesson you try to call a function that does not exist, find out how C locates the functions you use, and meet a sneaky problem with the keyboard.
In this lesson you will
See what happens when you call a function that is not declared anywhere
Use #include to bring in a library’s declarations
Use getch() from conio.h to wait for a key press, and learn why it only works on Windows
Compare input that echoes with input that does not
Watch a held-down key leak through the input buffer into scanf
Try it: guess a function name
Add a “Press any key” line to WelcomeMessage, followed by a call to a function that sounds right, ReadKey():
#include <stdio.h>
#include <stdlib.h>
void WelcomeMessage(void);
int main()
{
WelcomeMessage();
float x = -9999;
float y = -9999;
printf("Please enter a value for x:\n");
scanf("%f", &x);
printf("Please enter a value for y:\n");
scanf("%f", &y);
printf("The sum of %.2f and %.2f is %.2f\n", x, y, x + y);
return 0;
}
void WelcomeMessage(void)
{
printf("Welcome to the addition program\n\n");
printf("Press any key to continue...\n\n");
ReadKey();
}
Break it on purpose: call a function that does not exist
Build it. With GCC 14 the build stops with:
main.c:26:5: error: implicit declaration of function 'ReadKey' [-Wimplicit-function-declaration]
Older compilers, including the GCC bundled with many Code::Blocks installs, may only warn about the implicit declaration. The build then fails one step later, when the linker cannot find the function’s code anywhere:
undefined reference to `ReadKey'
collect2: error: ld returned 1 exit status
ReadKey exists in some other languages, but not in standard C. (In Pascal and in C#, for example, there is a function called ReadKey.) Guessing names does not work. You need to know which library a function lives in.
Including a random header does not help either. Add #include <string.h> at the top and build again, and you get the same error. string.h declares functions for working with text, and none of them is called ReadKey.
Try it: getch() from conio.h
On Windows, the function you want is getch(). It waits for a single key press and returns it. It is declared in a header file called conio.h (“console input/output”). Add the #include line and replace ReadKey():
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
void WelcomeMessage(void);
int main()
{
WelcomeMessage();
float x = -9999;
float y = -9999;
printf("Please enter a value for x:\n");
scanf("%f", &x);
printf("Please enter a value for y:\n");
scanf("%f", &y);
printf("The sum of %.2f and %.2f is %.2f\n", x, y, x + y);
return 0;
}
void WelcomeMessage(void)
{
printf("Welcome to the addition program\n\n");
printf("Press any key to continue...\n\n");
getch();
}
Build and run. The program stops after the welcome message. Press any key, for example j, and it carries on to the first question:
Welcome to the addition program
Press any key to continue...
Please enter a value for x:
Notice that the j you pressed did not appear on the screen, and you did not have to press Enter. Compare that with scanf, where everything you type is shown (echoed) and nothing happens until you press Enter. getch() reads one key straight away, without echo. That makes the pause feel smooth.
Windows only.conio.h and getch() are not part of standard C. They come with Windows compilers such as the MinGW GCC that Code::Blocks installs. On macOS or Linux the build fails with fatal error: conio.h: No such file or directory. A portable alternative is the standard function getchar() from stdio.h. It echoes, and it waits for Enter, so change the message to “Press Enter to continue…”.
What just happened
Functions such as printf, scanf and getch are library functions. Someone else wrote and compiled them, and they are supplied with your compiler. To use one, the compiler needs its declaration, its prototype, exactly as with your own functions in the earlier lessons. Libraries supply their prototypes in header files (.h files). The line #include <conio.h> tells the preprocessor to paste the contents of that header into your file before compiling, so the compiler knows what getch is. Then the linker joins your code to the library’s compiled code.
That explains both errors with ReadKey. There was no declaration, so the compiler complained. On older compilers that let it through, there was no compiled code to link to, so the linker complained instead. It also explains why stdio.h has been at the top of every program since the first lesson: it declares printf and scanf.
Try it: hold the key down
Break it on purpose: a key press that leaks
Run the getch version again. This time, at the “Press any key” pause, press the 8 key and hold it down for a moment before letting go. Observe: the first question appears with 8s already typed in, and they become the start of the value for x:
Please enter a value for x:
88815
Here the user held down 8, then typed 15 and pressed Enter, so x became 88815.
Holding a key makes it repeat. getch() takes only the first 8. The repeats are stored in the input buffer, a waiting area for keyboard input that has not been read yet. When scanf runs next, it reads from that same buffer, finds the 8s waiting, and treats them as the start of the number.
The simplest fix is to tap the key rather than hold it. On Windows you can also throw away any extra key presses straight after the getch(), using kbhit() (also from conio.h), which reports whether a key is waiting:
getch();
while (kbhit()) /* while more key presses are waiting... */
{
getch(); /* ...read them and throw them away */
}
Key terms
Library function: a ready-made function supplied with the compiler, such as printf or getch.
Header file (.h): a file of declarations (prototypes) for a library’s functions.
#include: a preprocessor instruction that inserts a header file into your code before compiling.
Preprocessor: the step before compiling that handles lines beginning with #.
Linker: the step after compiling that joins your code with library code to make the executable.
Echo: showing typed characters on the screen as they are entered.
Input buffer: where keyboard input waits until the program reads it.
Non-standard: not part of the C standard, so only available on some systems (like conio.h).
Quick check
The build reports “implicit declaration of function” for a library function. What is the usual fix?
Show answer
#include the header that declares it, for example stdio.h for printf.
Name two differences between getch() and scanf("%f", &x) from the user’s point of view.
Show answer
getch() does not echo the key, and it does not wait for Enter. scanf echoes what you type and waits for Enter.
Your friend on a Mac gets fatal error: conio.h: No such file or directory. Why?
Show answer
conio.h is non-standard and Windows-only. Use a standard function such as getchar() instead.
Why can holding down a key at a getch() pause change the value that the next scanf reads?
Show answer
The repeated key presses wait in the input buffer, and scanf reads them as the start of its input.
Your WelcomeMessage function does something, but it does not give back anything. Most useful functions take some input, process it, and hand back a result. That is the input–process–output pattern you will use in every program. In this lesson you write a function that adds two numbers and returns the answer. Then you find out what main can and cannot see inside it.
In this lesson you will
Write a function with parameters and a return value
Refactormain to call it
Discover local scope: why main cannot see a function’s variables
Use a naming convention to tell local and global names apart
Try it: a function that returns a value
Continue in your 003-FunctionDemoCProgram project. Below WelcomeMessage, add a second function:
The return type is float, not void. This function promises to hand back a float.
(float theFirstNumber, float theSecondNumber) are its parameters, the inputs it needs. Each parameter has a type and a name, separated by commas.
Inside, it creates a variable result to hold the output, works it out, and hands it back with return result;
The name is long on purpose. TheSumOfTwoNumbers describes exactly what the function gives you, so code that uses it reads like a sentence. This is called self-documenting code.
Add its prototype above main, next to the one for WelcomeMessage. Then build once, to check there are no errors before you go on.
Try it: refactor main
Now change main to use the new function instead of adding the numbers itself. Replace the float sum = x + y; line, and rename the variable to theAnswer so that it is clearly different from the function’s own result. Here is the whole program:
#include <stdio.h>
#include <stdlib.h>
void WelcomeMessage(void);
float TheSumOfTwoNumbers(float theFirstNumber, float theSecondNumber);
int main()
{
WelcomeMessage();
float x = -9999;
float y = -9999;
printf("Please enter a value for x:\n");
scanf("%f", &x);
printf("Please enter a value for y:\n");
scanf("%f", &y);
float theAnswer = TheSumOfTwoNumbers(x, y);
printf("The sum of %.2f and %.2f is %.2f\n", x, y, theAnswer);
return 0;
}
void WelcomeMessage(void)
{
printf("Welcome to the addition program\n\n");
}
float TheSumOfTwoNumbers(float theFirstNumber, float theSecondNumber)
{
float result;
result = theFirstNumber + theSecondNumber;
return result;
}
Break it on purpose: rename a variable, but not everywhere
Suppose you change float sum = … to float theAnswer = … but forget to update the printf line, which still ends x, y, sum);. Build it:
main.c:20:56: error: 'sum' undeclared (first use in this function)
sum no longer exists. Changing the structure or names in your code without changing what it does is called refactoring. When you rename something, you must change every place it is used. Code::Blocks shows a similar error in its build log. Fix the printf and build again.
Build and run, and enter 11 twice:
Welcome to the addition program
Please enter a value for x:
11
Please enter a value for y:
11
The sum of 11.00 and 11.00 is 22.00
What just happened
When main reaches TheSumOfTwoNumbers(x, y), the values of x and y are copied into the parameters: theFirstNumber receives 11, and theSecondNumber receives 11. The function adds them and stores 22 in result. return result; then sends that value back to the caller, where it takes the place of the function call. So the line in main becomes, in effect, float theAnswer = 22;
You could shorten the function to the single line return theFirstNumber + theSecondNumber;, and it would work just the same. In this series, though, you will set up a variable to hold the output, fill it, and return it. When functions grow longer and more complicated, a clearly named result variable makes the code far easier for someone else, or for you months later, to read and to check in the Watches window.
Try it: reach inside the function
Break it on purpose: use a function’s variable in main
The function worked out result, so surely main can print it? Add this line in main, just after the call: printf("Test: %f\n", result);. As you type, notice that Code::Blocks does not suggest result in its autocomplete list, which is a clue. Build:
main.c:20:26: error: 'result' undeclared (first use in this function)
From main‘s point of view, there is no such variable. Delete the line and build again.
Name it: scope
A variable declared inside a function (including its parameters) is a local variable. It exists only while that function is running, and it can only be used inside that function. This is called its scope. result, theFirstNumber and theSecondNumber are local to TheSumOfTwoNumbers. In the same way, x, y and theAnswer are local to main, and the function cannot see them either. That is why values travel in through parameters and out through return.
A variable declared outside every function, near the top of the file, is a global variable. Every function in the file can use it:
Globals are handy, but use them sparingly. Any function can change a global, so when its value is wrong, the mistake could be anywhere in the file. Parameters and return values keep each function’s inputs and outputs out in the open.
A naming convention to help you. C itself does not care how you capitalise names, but a consistent style tells the reader what kind of name they are looking at. This series uses upper camel case (PascalCase) for functions and globals, as in TheSumOfTwoNumbers and TheAnswer, and lower camel case (camelCase) for local variables and parameters, as in theAnswer and theFirstNumber. You can tell at a glance that TheAnswer is global and theAnswer is local. The convention helps you. The compiler only sees two different names.
Key terms
Parameter: a variable in a function’s brackets that receives a value when the function is called.
Argument: the actual value passed in a call, e.g. x and y in TheSumOfTwoNumbers(x, y).
Return value: the value a function sends back to its caller with return.
Caller: the code that called the function.
Refactoring: restructuring or renaming code without changing what it does.
Scope: the part of a program where a name can be used.
Local variable: declared inside a function, and only usable there.
Global variable: declared outside all functions, and usable by every function in the file.
Quick check
What does TheSumOfTwoNumbers(2.5, 4) return?
Show answer
6.5, as a float.
Why can main not print result directly?
Show answer
result is local to TheSumOfTwoNumbers. It only exists inside that function, so main must use the returned value instead.
You rename total to grandTotal and the build reports 'total' undeclared. What happened?
Show answer
At least one place still uses the old name. When refactoring, update every use of the name.
Under this series’ naming convention, which is probably global: MaxScore or maxScore?
Show answer
MaxScore (upper camel case). maxScore would be a local variable or parameter.
Your addition program works, but everything is crammed into main. Real programs are split into smaller, named pieces called functions, and you have been using them all along: printf and scanf are functions someone else wrote. In this lesson you write your own, a function that greets the user. Then you break it in two different ways to find out what the compiler needs to know about a function before you can use it.
In this lesson you will
Write and call a void function with no parameters
Follow the flow of execution into a function and back out
Tell the difference between a function declaration (prototype) and a definition
Read the “implicit declaration” and “conflicting types” errors
Start by copying your project, as you did in the scanf lesson: copy 002-UserInputCProgram to a new folder named 003-FunctionDemoCProgram, save the project under the new name, and update its title and output filename.
Try it: write a function and call it
Above main, type a new function called WelcomeMessage. Inside main, add one line, WelcomeMessage();, before everything else:
#include <stdio.h>
#include <stdlib.h>
void WelcomeMessage(void)
{
printf("Welcome to the addition program\n");
printf("I hope you enjoy this program\n\n");
}
int main()
{
WelcomeMessage();
float x = -9999;
float y = -9999;
printf("Please enter a value for x:\n");
scanf("%f", &x);
printf("Please enter a value for y:\n");
scanf("%f", &y);
float sum = x + y;
printf("The sum of %.2f and %.2f is %.2f\n", x, y, sum);
return 0;
}
Build and run, and enter 15 and 16:
Welcome to the addition program
I hope you enjoy this program
Please enter a value for x:
15
Please enter a value for y:
16
The sum of 15.00 and 16.00 is 31.00
Every C program starts running at the first line of main. When it reaches WelcomeMessage();, execution jumps to the first line inside WelcomeMessage, runs its two printf lines, and then returns to main, carrying on from the line after the call. See it for yourself with the debugger from the previous lesson. Put a breakpoint on the call, start debugging, and choose Step into: the yellow arrow moves inside your function. Choose Next line instead, and the whole function runs in one step.
What just happened
A function has a return type, a name, a list of parameters in brackets, and a body in curly braces:
void is the return type. void means “nothing”: this function does its job (printing) and hands no value back.
WelcomeMessage is the name. Choose names that say what the function does, so that main reads almost like a description of the program.
(void) in the brackets says the function takes no parameters. (You will also see empty brackets (), as in int main(). In the newest C standard they mean the same thing, but (void) is the clearest way to say “no parameters”.)
The braces hold the statements that run each time the function is called.
In this series, braces go on their own lines, and function names use upper camel case (WelcomeMessage). Many programmers put the opening brace at the end of the first line instead. Either style is fine, as long as you are consistent.
Try it: move the function below main
Many programmers like main to be the first thing in the file, so that a reader sees the big picture first. Cut the whole WelcomeMessage function and paste it belowmain, then build.
Break it on purpose: call a function the compiler has not seen yet
Observe: the build fails. With GCC 14 the first message is:
main.c:6:5: error: implicit declaration of function 'WelcomeMessage' [-Wimplicit-function-declaration]
Older compilers, such as the GCC included with many Code::Blocks installs, may report this as a warning instead, often followed by an error saying conflicting types for 'WelcomeMessage'. Either way, the program does not build cleanly.
Remember that the compiler reads from top to bottom. When it reaches the call in main, it has never heard of WelcomeMessage, so it does not know what kind of function this is.
The fix is to announce the function before main. Add this one line, ending in a semicolon, above main:
void WelcomeMessage(void);
#include <stdio.h>
#include <stdlib.h>
void WelcomeMessage(void);
int main()
{
WelcomeMessage();
float x = -9999;
float y = -9999;
printf("Please enter a value for x:\n");
scanf("%f", &x);
printf("Please enter a value for y:\n");
scanf("%f", &y);
float sum = x + y;
printf("The sum of %.2f and %.2f is %.2f\n", x, y, sum);
return 0;
}
void WelcomeMessage(void)
{
printf("Welcome to the addition program\n");
printf("I hope you enjoy this program\n\n");
}
Build and run: it works again, with the function below main.
Name it: declaration vs definition
void WelcomeMessage(void); is a function declaration, also called a prototype. It gives the function’s return type, name and parameters, and ends with a semicolon. It promises the compiler that the function exists.
The version with a body in braces is the function definition. It is the actual code that runs.
It is just like variables: float x; declares a name and a type. A function prototype does the same for a function.
Break it on purpose: break the promise
Change the definition (not the prototype) so that it takes a parameter: void WelcomeMessage(int times). Leave the prototype as void WelcomeMessage(void); and build:
main.c:23:6: error: conflicting types for 'WelcomeMessage'; have 'void(int)'
main.c:4:6: note: previous declaration of 'WelcomeMessage' with type 'void(void)'
The prototype and the definition must match exactly: same return type, same name, same parameter types. The compiler even tells you where the earlier promise was made (line 4). Undo the change and build again. Code::Blocks shows similar messages in its build log.
Key terms
Function: a named block of code that performs a task and can be called from elsewhere.
Call: using a function by writing its name followed by brackets, e.g. WelcomeMessage();
Return type: the type of value a function hands back. void means none.
Parameters: the inputs a function accepts, listed in its brackets.
Declaration (prototype): the function’s header followed by a semicolon. It tells the compiler the function exists.
Definition: the function’s header plus its body. It is the code that actually runs.
Flow of execution: the order in which statements run, including jumps into functions and back.
Quick check
Where does every C program start running?
Show answer
At the first statement inside main.
What does void mean as a return type?
Show answer
The function does not return a value. It just does its job, such as printing.
Your function is defined below main and the build reports “implicit declaration of function”. What should you add?
Show answer
A prototype above main, for example void WelcomeMessage(void);
Which line is a prototype: void Hello(void); or void Hello(void) { printf("Hi\n"); }?
Show answer
void Hello(void);. It has no body and ends with a semicolon. The other line is a definition.
In the previous lesson you found the copy-paste bug by spotting -9999 in the output. That worked because the program was tiny. In real programs you need a way to stop the program part-way through and look inside it. That tool is the debugger, and Code::Blocks has one built in. In this lesson you use it to watch the copy-paste bug happen, one line at a time.
In this lesson you will
Set a breakpoint and start a program in Debug mode
Step through code with Next line and Step into, and jump ahead with Continue
Inspect variables in the Watches window
Discover that a float cannot store 15.1 exactly
Try it: set up the buggy program
Open 002-UserInputCProgram and put the copy-paste bug back in, so that the second scanf reads into &x:
#include <stdio.h>
#include <stdlib.h>
int main()
{
float x = -9999;
float y = -9999;
printf("Please enter a value for x:\n");
scanf("%f", &x);
printf("Please enter a value for y:\n");
scanf("%f", &x);
float sum = x + y;
printf("The sum of %.2f and %.2f is %.2f\n", x, y, sum);
return 0;
}
Check that the build target in the toolbar is set to Debug (not Release). The Debug target builds your program with extra information (GCC’s -g option) that lets the debugger match the running program to your lines of code.
Try it: breakpoints
A breakpoint is a marker that says “pause here”. Click in the margin just to the right of the line number beside the first scanf (line 10). A red dot appears. (You can also put the cursor on the line and press F5 to toggle a breakpoint.) Add a second breakpoint on the line float sum = x + y;.
Break it on purpose: breakpoints that are never hit
With both breakpoints set, click the usual Build and run button. Observe: the program runs from start to finish as if the breakpoints were not there.
Breakpoints only work when the program runs under the debugger. Stop the program. Then choose Debug → Start / Continue (F8, or the red arrow on the debugger toolbar).
This time the program starts, prints the first prompt, and stops. A yellow arrow appears in the margin at your first breaThe yellow arrow marks the line that will run next. It has not run yet. That is why nothing has been typed into the console so far.
Try it: step and watch
Open Debug → Debugging windows → Watches. It lists the local variables and their current values. Right now x and y both show -9999.
Choose Debug → Next line (F7). The scanf runs and waits for you, so switch to the console, type 15.1 and press Enter. The yellow arrow moves down one line.
Look at the Watches window. x now shows something like 15.1000004. Keep that in mind for later.
Choose Debug → Start / Continue again. The program runs until it reaches the next breakpoint. On the way it asks for y: type 300.
Look at the Watches window again. x has changed to 300, but y is still -9999. You have just watched the bug happen: the value meant for y went into x.
Choose Debug → Stop debugger, fix the second scanf to use &y, and debug once more to confirm that both values arrive where they should.
What just happened
The debugger gives you three ways to move forward:
Next line (often called step over) runs the current line completely, then pauses on the next one. If the line calls a function, the whole function runs in one go.
Step into (Shift+F7) also runs one line, but if that line calls a function you wrote, it goes inside the function and pauses on its first line. This becomes useful in the next lesson, when you write your own functions. Library functions such as printf have no source code for the debugger to show, so stepping into them usually behaves like Next line.
Continue runs at full speed until the next breakpoint, or until the program ends.
One more surprise: when the program finishes under the debugger, the console window closes straight away, before you can read the output. A simple fix is to put a breakpoint on the return 0; line. The program then pauses there, with all of its output still on screen.
Name it: floats are not exact
Why did the Watches window show 15.1000004 instead of 15.1? A float stores numbers in binary, and most decimal fractions, including 0.1, cannot be written exactly in binary. In the same way, 1/3 cannot be written exactly in decimal (0.3333…). The computer stores the nearest value it can. You can see the stored value by asking printf for more decimal places than usual:
#include <stdio.h>
#include <stdlib.h>
int main()
{
float x = 15.1;
printf("%.2f\n", x);
printf("%.10f\n", x);
return 0;
}
15.10
15.1000003815
For everyday output, %.2f hides the tiny difference. Just remember never to expect a float to be exactly equal to a decimal value like 15.1. This matters later, when you start comparing values.
Key terms
Debugger: a tool that runs your program under control, so you can pause it and inspect it. Code::Blocks uses GDB.
Breakpoint: a marker on a line where the program pauses when run in Debug mode.
Next line (step over): run the current line, including any function it calls, and pause on the next line.
Step into: if the current line calls one of your functions, pause inside that function.
Continue: run until the next breakpoint.
Watches: a window showing the current values of variables.
Debug target: a build configuration that includes debugging information (-g).
Quick check
You set a breakpoint, click Build and run, and the program never pauses. Why?
Show answer
Breakpoints only work under the debugger. Start the program with Debug → Start / Continue.
The yellow arrow points at y = 6;. Has y been set to 6 yet?
Show answer
No. The arrow marks the line that will run next. Choose Next line, and then y becomes 6.
What is the difference between Next line and Step into on a line that calls one of your own functions?
Show answer
Next line runs the whole function and stops on the following line. Step into goes inside the function and stops on its first line.
Why might the Watches window show 2.0999999 for a float set to 2.1?
Show answer
Most decimal fractions cannot be stored exactly in binary, so the float holds the closest value it can.
Until now, the numbers your program adds are fixed in the code. Every time you want different numbers, you have to edit and rebuild. In this lesson the program asks the user for the numbers instead. Then you make one of the most common beginner mistakes on purpose, and use a simple trick to make it stand out.
In this lesson you will
Start a new project by copying the previous one
Read numbers from the keyboard with scanf
Meet the address-of operator, &
Use defensive initialisation to expose a logical error
Before you start: copy your project. Keep 001-FirstCProgram as it is and continue in a copy. That way you can always look back at a working version.
Close the project in Code::Blocks. In File Explorer, copy the 001-FirstCProgram folder and paste it next to the original. Rename the copy 002-UserInputCProgram.
Open the .cbp file inside the new folder. Choose File → Save project as… and save it in the new folder as 002-UserInputCProgram.cbp. Then delete the old 001-FirstCProgram.cbp from the new folder, so you cannot open it by accident.
Open Project → Properties…. On the Project settings tab, change the Title to 002-UserInputCProgram.
On the Build targets tab, change the Output filename for both Debug and Release, for example to bin\Debug\002-UserInputCProgram.exe. If you skip this, your new project still builds a program called 001-FirstCProgram.exe, which gets confusing quickly.
Try it: ask the user for the numbers
Replace main in your new project with the version below. The two scanf lines are new. Notice the & in front of each variable name, and the starting value of -9999. Both are explained shortly.
#include <stdio.h>
#include <stdlib.h>
int main()
{
float x = -9999;
float y = -9999;
printf("Please enter a value for x:\n");
scanf("%f", &x);
printf("Please enter a value for y:\n");
scanf("%f", &y);
float sum = x + y;
printf("The sum of %.2f and %.2f is %.2f\n", x, y, sum);
return 0;
}
Build and run. The program prints the first prompt and then waits. Type 15.1 and press Enter, then type 300 and press Enter:
Please enter a value for x:
15.1
Please enter a value for y:
300
The sum of 15.10 and 300.00 is 315.10
Run it again with different numbers. The program no longer needs rebuilding to add new values.
What just happened
scanf is formatted input, the mirror image of printf. Its first argument is a format string: "%f" means “read the characters the user types and convert them into a float”. Then it needs to know where to put the result.
That is the job of &, the address-of operator. x on its own means “the value inside x”. &x means “the location of x in memory”. printf only needs values to display, but scanf has to change your variable, so you must tell it where the variable lives.
Leave out the & and GCC warns you, and Code::Blocks shows a similar message in its build log:
warning: format '%f' expects argument of type 'float *', but argument 2 has type 'double' [-Wformat=]
Treat that warning as an error and fix it. If you run the program anyway, scanf tries to write to a nonsense location, and the program may crash.
Try it: the copy-paste trap
When you write the second prompt, it is tempting to copy the first two lines, paste them, and change x to y. Now forget to change one of them on purpose. Edit the second scanf so that it still reads into &x:
#include <stdio.h>
#include <stdlib.h>
int main()
{
float x = -9999;
float y = -9999;
printf("Please enter a value for x:\n");
scanf("%f", &x);
printf("Please enter a value for y:\n");
scanf("%f", &x);
float sum = x + y;
printf("The sum of %.2f and %.2f is %.2f\n", x, y, sum);
return 0;
}
Break it on purpose: read y into x
Build the program above: no errors, no warnings. Run it and enter 15.1 and 300 again. Observe:
Please enter a value for x:
15.1
Please enter a value for y:
300
The sum of 300.00 and -9999.00 is -9699.00
The program asked for y, but the 300 went into x and overwrote the 15.1. y was never touched, so it still holds its starting value.
Notice how loudly the mistake shows up. -9999 is not a number anyone would type by accident, so the moment it appears in the output you know a variable was never filled in. Choosing an obviously “impossible” starting value on purpose is called defensive initialisation.
Without it, float y; would contain whatever bits happened to be left in that memory. That value is indeterminate (unpredictable): sometimes zero, sometimes a strange number. A bug that sometimes hides is much harder to find than one that always shouts. Fix the second scanf back to &y, and test again with numbers whose sum you know.
Key terms
scanf: reads formatted input from the keyboard and stores it in variables.
Address-of operator (&): gives the memory location of a variable, e.g. &x.
Initialisation: giving a variable a starting value when you declare it.
Defensive initialisation: using a deliberately unusual starting value (such as -9999) so that a variable that is never filled in is easy to spot.
Indeterminate value: the unpredictable contents of a local variable that has not been initialised.
Quick check
Why does scanf need &x while printf only needs x?
Show answer
scanf has to store a new value in the variable, so it needs the variable’s address. printf only reads the value to display it.
A program prints The sum of 8.00 and -9999.00 is -9991.00. What has most likely gone wrong?
Show answer
The second variable was never filled in. Its scanf probably reads into the wrong variable, or is missing.
Does the copy-paste mistake cause a syntax error or a logical error?
Show answer
A logical error. The code is valid C, so it builds without warnings, but it does the wrong thing.
After copying a project folder, what two names should you change in Project → Properties?
Show answer
The project Title (Project settings tab) and the Output filename of each build target (Build targets tab).
Your addition program works perfectly for 5 and 6. But what about 5.9 and 6.9? In this lesson you feed it numbers with decimal places and watch it quietly get the answer wrong, twice, in two different ways. Each mistake teaches you something about how C stores numbers.
In this lesson you will
See what happens when a decimal number is stored in an int
Switch to the float type for numbers with a fractional part
Find out why printing a float with %d gives nonsense
Use %f and %.2f to print floats properly
Try it: decimals in an int
Here is the addition program, tidied so that each variable is initialised on one line. Change the values to 5.1 and 6.1, then build and run:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int x = 5.1;
int y = 6.1;
int sum = x + y;
printf("The sum of %d and %d is %d\n", x, y, sum);
return 0;
}
The sum of 5 and 6 is 11
No errors, and no warnings either. The program simply ignored the .1. Maybe it rounded? Test that idea: change the values to 5.9 and 6.9. If C rounded, you would expect 6 and 7.
#include <stdio.h>
#include <stdlib.h>
int main()
{
int x = 5.9;
int y = 6.9;
int sum = x + y;
printf("The sum of %d and %d is %d\n", x, y, sum);
return 0;
}
The sum of 5 and 6 is 11
Still 5 and 6. When a decimal value is stored in an int, C does not round it. It truncates it: everything after the decimal point is thrown away. An int can only ever hold whole numbers.
Try it: switch to float
To store numbers with a fractional part, use the type float (short for floating-point number). Change all three variables to float, but leave the %d specifiers as they are for now:
#include <stdio.h>
#include <stdlib.h>
int main()
{
float x = 5.9;
float y = 6.9;
float sum = x + y;
printf("The sum of %d and %d is %d\n", x, y, sum);
return 0;
}
Break it on purpose: print a float with %d
Build the program above and look at the build log first. GCC warns, once for each value:
main.c:9:25: warning: format '%d' expects argument of type 'int', but argument 2 has type 'double' [-Wformat=]
It is only a warning, so the program still runs. Observe: the output is nonsense. Your numbers will be different, and they may even change each time you run it. Here is one run:
The sum of -1238979704 and -1238979688 is -299069992
Nothing in the variables is wrong. x really does hold 5.9 now. The problem is entirely in how you asked for it to be displayed.
What just happened
Inside the computer, every variable is stored as a pattern of binary digits (0s and 1s). Even 5.9 is stored as 0s and 1s. An int and a float use completely different patterns. A float stores a sign, a set of significant digits and an exponent, a bit like scientific notation in binary.
A format specifier tells printf how to interpret the value it is given. %d says “this is an int”. Hand it a floating-point value and it reads the bits as if they were an integer. On many computers it even looks in the wrong place entirely, because floating-point values are passed differently. The computer did exactly what the specifier told it to do. That is why this is a logical error, and why the compiler can only warn you, not stop you. (The warning mentions double because printf receives a float as a double, a larger floating-point type.)
Try it: the right specifier
The specifier for floating-point numbers is %f. Change all three:
#include <stdio.h>
#include <stdlib.h>
int main()
{
float x = 5.9;
float y = 6.9;
float sum = x + y;
printf("The sum of %f and %f is %f\n", x, y, sum);
return 0;
}
The sum of 5.900000 and 6.900000 is 12.800000
Correct at last, but six decimal places is a lot. You can control this by putting a dot and a number between the % and the f. %.2f means “show 2 decimal places”:
#include <stdio.h>
#include <stdlib.h>
int main()
{
float x = 5.9;
float y = 6.9;
float sum = x + y;
printf("The sum of %.2f and %.2f is %.2f\n", x, y, sum);
return 0;
}
The sum of 5.90 and 6.90 is 12.80
%.2f only changes the display. The value stored in sum is not rounded. Printing the same variable with different precisions makes this easy to see: