Your First C Programs
Now you write C. This volume takes the five pieces every C program is made of - printing, variables, decisions, loops and functions - and shows each one in a program short enough to type in a minute. Every program here was compiled and run, so the output on the page is the output you will get.
- What every line of a hello world program is for
- How to declare variables, and how to print them correctly
- How to make decisions with if, else if and switch
- How for, while and do-while differ, and when to use each
- How to write your own functions, and why a function cannot change its caller’s variables
- Volume 00, or any way to compile and run a C program
1.1 Hello, world - every line explained
Every C program is made of functions, and one of them is special: main. That is where the program starts.
Here is the whole program. Type it, run it, then read the table under it.
#include <stdio.h>
int main(void)
{
printf("Hello, embedded world!\n");
return 0;
}
Hello, embedded world!
Every line, one at a time
| Line | What it is for |
|---|---|
#include <stdio.h> |
Brings in a header file that describes printf. Without it, the compiler has never heard of printf. |
int main(void) |
Declares the function where the program starts. int is the kind of value it hands back; void says it takes nothing. |
{ and } |
Mark the start and end of the function's body. Everything between them is main's work. |
printf("..."); |
Calls a ready-made function from the standard library to print text. |
\n |
Inside the text, this means "new line". Without it, the next print would carry on the same line. |
return 0; |
Ends main and hands back 0, which by tradition means "all went well". |
; |
Ends a statement. C needs one after every instruction. |
A program is a list of instructions. Functions are named groups of instructions. When the program starts, the computer looks for main and runs the instructions inside it, in order, from top to bottom.
Three rules that catch everyone
- C is case sensitive.
mainis notMain, andprintfis notPrintf. - Every statement ends with a semicolon. The compiler will not guess where you meant to stop.
- Text goes in double quotes.
'A'is one character;"A"is text.
Forgetting the semicolon. Leave out the one after printf and gcc says:
hello.c: In function 'main':
hello.c:5:39: error: expected ';' before 'return'
5 | printf("Hello, embedded world!\n")
| ^
| ;
6 | return 0;
| ~~~~~~
Read it like an address: the file, then the line, then the column, then what it wanted. The message points at line 5 - one line above where it noticed - because that is where the semicolon should have been. That is normal for a missing semicolon, and worth remembering.
What does return 0 at the end of main mean?
Show the answer
Answer: B. main hands a number back to whatever started it. By tradition 0 means success and anything else means a problem. On a bare-metal chip nothing is waiting for that number, but the shape of main stays the same.
1.2 Variables and types
A variable is a named box in memory. Its type says how big the box is and how to read the bits inside.
Declaring and using them
#include <stdio.h>
#include <stdint.h>
int main(void)
{
int count = 12; /* a whole number, positive or negative */
unsigned tries = 3u; /* a whole number, never negative */
char grade = 'B'; /* one character */
float volts = 3.3f; /* a number with a fractional part */
uint8_t brightness = 255; /* exactly 8 bits, 0 to 255 */
printf("count = %d\n", count);
printf("tries = %u\n", tries);
printf("grade = %c\n", grade);
printf("volts = %.2f\n", (double)volts);
printf("brightness = %u\n", brightness);
count = count + 1; /* variables can change */
brightness = 0; /* the LED goes dark */
printf("after: count = %d, brightness = %u\n", count, brightness);
printf("an int takes %zu bytes here\n", sizeof(int));
return 0;
}
count = 12
tries = 3
grade = B
volts = 3.30
brightness = 255
after: count = 13, brightness = 0
an int takes 4 bytes here
Each line declares a variable: first the type, then the name, then an initial value. The text
between /* and */ is a comment, written for people and ignored by the
compiler.
The types you will use
| Type | Holds | Typical size |
|---|---|---|
char |
one character, or a small number | 1 byte |
int |
a whole number, positive or negative | 4 bytes on a computer, sometimes 2 on a small chip |
unsigned |
a whole number that is never negative | same size as int |
float |
a number with a fractional part | 4 bytes |
double |
the same, with more digits | 8 bytes |
uint8_t, uint16_t, uint32_t |
exactly that many bits, never negative | 1, 2 and 4 bytes, on every machine |
In embedded C, prefer the fixed-width types: uint8_t, int16_t, uint32_t. They say exactly
what you mean, on every chip. Plain int changes size between machines, and that difference has
broken a lot of firmware. Volume 02 takes this apart properly.
Printing them
printf needs a format specifier for each value: a small code saying how to read it.
| Specifier | Prints |
|---|---|
%d |
a signed whole number |
%u |
an unsigned whole number |
%c |
one character |
%f |
a number with a fractional part |
%s |
text |
%x |
a whole number in hexadecimal |
%zu |
a size, as sizeof gives |
Using the wrong specifier, such as %d for a float. The compiler may not stop you, and the output
is nonsense, because printf reads the bytes the way the specifier told it to. Turn warnings on
(-Wall) and the compiler will point at it.
Which type should you choose for a value that is always between 0 and 255, on any chip?
Show the answer
Answer: C. uint8_t is exactly 8 bits and never negative, on every machine. Plain char may be signed or unsigned depending on the compiler, and int is bigger than you need.
1.3 Making decisions: if and switch
if runs a piece of code only when something is true. switch picks one path out of many, when you are choosing on a single value.
int temperature = 42;
if (temperature > 60) {
printf("too hot: turn the heater off\n");
} else if (temperature < 20) {
printf("too cold: turn the heater on\n");
} else {
printf("just right: do nothing\n");
}
just right: do nothing
The tests run in order, and the first true one wins. else catches everything left over.
The comparisons
| You write | It asks |
|---|---|
a == b |
are they equal? |
a != b |
are they different? |
a > b, a >= b |
is a bigger (or equal)? |
a < b, a <= b |
is a smaller (or equal)? |
a && b |
are both true? |
a \|\| b |
is either true? |
!a |
is a false? |
In C there is no separate true or false type to learn first: 0 is false, and anything else is
true. That is why if (count) means "if count is not zero".
Writing = where you meant ==:
if (mode = 3) { /* assigns 3 to mode, and is always true */
= puts a value in; == compares. The buggy line sets mode to 3, then tests 3, which is not zero,
so the branch always runs. Compilers warn about this, and the extra brackets in
if ((mode = 3)) are how you tell the compiler you meant it.
switch: many fixed choices
switch (button) {
case 1:
printf("button 1: start\n");
break;
case 2:
printf("button 2: stop\n");
break;
case 3:
printf("button 3: reset\n");
break;
default:
printf("no such button\n");
break;
}
With button set to 2, that prints button 2: stop.
break leaves the switch. Without it, C carries straight on into the next case - which is
sometimes useful and usually a bug:
fell into case 4
...and kept going into case 5
Give every switch a default, even when you think the other cases cover everything. In firmware,
a value you did not expect is exactly the case you want to catch.
What does a missing break at the end of a case do?
Show the answer
Answer: A. Cases fall through by design: without break, control continues into the next case's code. It is occasionally what you want, and far more often a bug.
1.4 Repeating: for, while and do-while
All three loops do the same job - repeat while something stays true. They differ only in where the check happens, and how much bookkeeping they do for you.
for: when you know how many times
for (int i = 1; i <= 5; i++) {
printf("blink %d\n", i);
}
blink 1
blink 2
blink 3
blink 4
blink 5
The brackets hold three parts, separated by semicolons.
while: when you do not know how many times
int level = 40;
while (level > 0) {
level = level - 15;
}
printf("level ended at %d\n", level); /* level ended at -5 */
int tries = 0;
do {
tries++;
} while (tries < 3);
printf("tried %d times\n", tries); /* tried 3 times */
A while loop checks first, so it can run zero times. A do-while runs the body first and checks
afterwards, so it always runs at least once - even when the condition was false from the start:
this line runs once, although 99 > 100 is false
Leaving early: break and continue
break leaves the loop completely. continue skips the rest of this turn and goes to the next
one. Adding up 1 to 10, skipping 4 and stopping at 7, gives:
sum of 1,2,3,5,6 = 17
Firmware ends with a loop that never finishes:
for (;;) { /* or: while (1) */
do_the_work();
}
An empty for (;;) means "no set-up, no check, no step" - so it repeats for ever. On a chip, this
infinite loop is not a bug. It is the design.
The off-by-one. for (int i = 0; i <= 5; i++) runs six times, not five: 0, 1, 2, 3, 4, 5. For
an array of five items, that last turn reads past the end. The usual shape is i < count, starting
at 0, which runs exactly count times.
How many times does the body of a do-while loop run if its condition is false from the very start?
Show the answer
Answer: B. A do-while checks after the body, so the body always runs at least once. That is the one thing it does differently from while.
1.5 Functions
A function is a named piece of work. You hand it values, it does its job, and it can hand one value back. Firmware is mostly a set of small functions called from one loop.
#include <stdio.h>
/* declarations first, so main can call them */
int add(int a, int b);
int larger(int a, int b);
void print_banner(void);
void try_to_change(int value);
int main(void)
{
print_banner();
printf("add(2, 3) = %d\n", add(2, 3));
printf("larger(9, 4) = %d\n", larger(9, 4));
int reading = 100;
try_to_change(reading);
printf("reading is still %d\n", reading);
return 0;
}
int add(int a, int b)
{
return a + b;
}
int larger(int a, int b)
{
if (a > b) {
return a;
}
return b;
}
void print_banner(void)
{
printf("--- sensor board ---\n");
}
/* value is a copy, so changing it changes nothing in main */
void try_to_change(int value)
{
value = 0;
printf("inside the function, value = %d\n", value);
}
--- sensor board ---
add(2, 3) = 5
larger(9, 4) = 9
inside the function, value = 0
reading is still 100
The parts of a function
| Part | In int add(int a, int b) |
|---|---|
| Return type | int - the kind of value it hands back. void means it hands nothing back |
| Name | add - how you call it |
| Parameters | int a, int b - the values it works on. (void) means none |
| Body | the code between { and } |
| Return | return a + b; ends the function and hands the value back |
The lines at the top of the file, ending in a semicolon, are declarations. They tell the compiler what exists before it reaches the full definitions. Volume 15 shows how headers do this across several files.
The one surprise: C copies your arguments
Look at the last two lines of the output. The function set value to 0, yet reading in main is
still 100. That is pass by value: the function received a *copy*,
and changed the copy.
You photocopy a page and hand over the copy. Whatever they scribble on it, your original is untouched.
Expecting a function to change the variable you passed. It cannot, not like this. To let a function change something of yours, you hand over its address instead - and that is exactly what pointers are for, in Volume 07.
After calling try_to_change(reading), why is reading still 100?
Show the answer
Answer: C. C passes arguments by value. The parameter is a separate variable holding a copy, so assigning to it inside the function leaves the caller's variable exactly as it was.
What you learned
- Every C program starts at main, and every statement ends with a semicolon.
- #include brings in a header so the compiler knows what printf and friends are.
- A variable has a type, which fixes its size and how its bits are read.
- printf needs the right format specifier for each value: %d, %u, %c, %f, %x.
- if and else if choose in order; switch picks one case, and needs break.
- for, while and do-while all repeat; only do-while always runs once.
- for (;;) is the endless loop every bare-metal program ends with.
- A function takes copies of its arguments, so it cannot change your variables.
Key words from this volume
Every word below has a plain-English entry in the glossary.
- Function
- main
- Header file
- Standard library
- Statement
- Variable
- Data type
- Comment
- Format specifier
- Infinite loop
- Parameter
- Return value
- Pass by value
Practice
Count the turns
How many times does each loop run its body?
for (int i = 0; i < 5; i++) { /* A */ }
for (int i = 1; i <= 5; i++) { /* B */ }
for (int i = 0; i <= 5; i++) { /* C */ }
Show the solution
A: five times (i is 0, 1, 2, 3, 4). B: five times (1 to 5). C: six times (0 to 5) - the off-by-one.
For an array of five items, only A is safe: array places are numbered from 0 to 4, so i < 5 is
the shape to learn.
Predict the output
What does this print?
int x = 0;
do {
printf("%d ", x);
x++;
} while (x < 3);
printf("| x = %d\n", x);
Show the solution
0 1 2 | x = 3
The body runs with x at 0, 1 and 2. After the turn that printed 2, x becomes 3, the condition
3 < 3 is false, and the loop ends. x keeps its final value, 3.
Write a function
Write a function called clamp that limits a reading to the range 0 to 100. Below 0 becomes 0,
above 100 becomes 100, and anything else is handed back unchanged.
Show the solution
int clamp(int value)
{
if (value < 0) {
return 0;
}
if (value > 100) {
return 100;
}
return value;
}
Returning early, as here, is usually clearer than building one long chain of if and else. This little function appears in real firmware constantly: sensor readings arrive noisy, and a driver should never be handed a value outside the range it expects.
Find the bug
This is meant to print "unlocked" only when code is 42. It prints it every time. Why?
if (code = 42) {
printf("unlocked\n");
}
Show the solution
= assigns; == compares. The line stores 42 in code, and the value of that assignment is 42.
In C anything that is not 0 counts as true, so the branch always runs - and the real code is
thrown away as a bonus bug.
The fix is if (code == 42). Compilers warn about this when you turn warnings on, which is one
reason to always build with -Wall.
Interview corner
while or do-while?
"When would you choose a do-while loop over a while loop?"
Show the solution
"When the body has to run at least once before you can tell whether to repeat it. A good example is reading a status register: you have to read it once before you can test the busy bit. A while loop checks first, so it can run zero times; a do-while checks afterwards, so it always runs at least once."
Pass by value
"I call swap(a, b) and my variables are unchanged. What is going on?"
Show the solution
"C passes arguments by value, so the function is swapping its own copies. The caller's variables
never move. To swap them for real, the function has to take their addresses - void swap(int *a, int *b) - and work through the pointers. This is the classic first pointer exercise, and it is why
scanf needs an ampersand in front of its argument."
Next, Volume 02 goes under the surface: bits, bytes and addresses, hexadecimal, the fixed-width types, two's complement, overflow, and why endianness bites when bytes travel between chips.