Structured programming language
Three control structures, functions that can be understood separately and a compiler that translates all of it into machine instructions. C is the language where you can see both at once.
01From jumps to blocks
The first programs were written with jumps: “if such-and-such happens, go to line 340.” They worked, but beyond a certain size nobody could follow the thread, because any line could be reached from any other. In 1968 Edsger Dijkstra published a letter with a title that became famous, Go To Statement Considered Harmful: the unrestricted jump makes it impossible to reason about a program.
Structured programming is the answer: write everything with the three structures of Böhm and Jacopini, each with a single entry and a single exit, and split the problem into functions that can be understood, tested and reused separately. The program reads from top to bottom and each block can be replaced by a black box.
C, created by Dennis Ritchie at Bell Labs between 1969 and 1973 to write Unix, is a structured language that also lets you see the machine: addresses, bits and sizes. That is why it is the language of operating systems and microcontrollers, and why it is taught in electronic engineering. The basic elements of the language are covered in basic elements of the C language; here the focus is on how to think about a structured program and what the machine does with it.
02From source to executable, hands-on
The gcc compiler lets you stop at each stage. It is worth doing once to lose the fear of
error messages, which tell you at which stage they failed:
gcc -E hello.c -o hello.i # preprocess only: see what is left of the #include lines
gcc -S hello.c -o hello.s # compile to assembly
gcc -c hello.c -o hello.o # assemble: object code
gcc hello.o -o hello # link with the standard library
gcc -Wall -Wextra -std=c11 hello.c -o hello # all in one, with warnings
| Message | Stage | Typical cause |
|---|---|---|
fatal error: stdio.h: No such file | Preprocessor | Header name misspelled |
expected ';' before … | Compiler | A semicolon is missing on the previous line |
implicit declaration of function | Compiler | A function is used without a prototype or #include |
undefined reference to 'sqrt' | Linker | The library is missing: add -lm |
Always compile with -Wall -Wextra and do not leave any warning without understanding it. An
if (x = 5) compiles, but the compiler warns: it is an assignment, not a comparison.
03Types and expressions: what C does without warning
In C every expression has a type, and the type decides the operation. 7 / 2 is an
integer division and gives 3, because both operands are integers; 7.0 / 2 gives 3.5. The result does not
depend on where it is stored: double x = 7 / 2; stores 3.0, because the division was done first.
When types are mixed, C applies the usual arithmetic conversions: types smaller than
int are promoted to int, and in an operation between two different types the “smaller” one is
converted to the “larger” one. The best-known trap:
int a = -1;
unsigned int b = 1;
if (a < b) // false!
printf("logical");
When an int is compared with an unsigned int, −1 is converted to unsigned and becomes
4,294,967,295 (with 32 bits), which is greater than 1. The bits did not change: the reading changed, exactly
as in number systems.
| Operators, from highest to lowest precedence | Associativity |
|---|---|
() [] -> . and the postfix ++ -- | left to right |
! ~ - * & sizeof, prefix operators and casts (type) | right to left |
* / % | left to right |
+ - | left to right |
<< >> | left to right |
< <= > >=, then == != | left to right |
&, then ^, then | | left to right |
&&, then || | left to right |
?: and the assignments = += -= … | right to left |
There is a surprise in the table: & has lower precedence than ==. That is why
if (x & 0x08 == 0) does not do what it seems to; write if ((x & 0x08) == 0).
When in doubt, use parentheses.
04Control structures in C
| Structure | Pseudocode | C |
|---|---|---|
| Simple or double selection | If … then … else | if (c) { … } else { … } |
| Multiple selection | Case | switch (x) { case 1: … break; default: … } |
| Iteration with a pre-test condition | While | while (c) { … } |
| Iteration with a post-test condition | Repeat … until | do { … } while (c); (note: the condition is one for continuing, not for stopping) |
| Iteration with a counter | For | for (i = 0; i < n; i++) { … } |
In C any nonzero value is true. break exits the loop or the
switch and continue jumps to the next iteration: they break the single-exit rule,
but in a controlled way, and they are accepted when they simplify things. goto exists; in structured
code it is only tolerated for leaving several nested levels when an error occurs.
If a case does not end with break, execution continues into the next one. Sometimes this is
done on purpose to group cases; almost always it is an oversight.
05Functions and parameter passing
A function is an algorithm with a name, inputs (parameters) and an output (return value). In C all parameters are passed by value: the function receives a copy and cannot modify the original variable. To let it do so, you pass the variable’s address, a pointer:
void swap(int *p, int *q)
{
int aux = *p; // what is at address p
*p = *q;
*q = aux;
}
...
swap(&x, &y); // the addresses of x and y are passed
For the same reason scanf("%d", &n) takes &: it needs to know
where to write. Pointers are covered in
complex data containers.
| Variable | Scope (where it is visible) | Lifetime (how long it lives) |
|---|---|---|
| Local | The block where it is declared | While the block executes: it is created and destroyed on each call |
Local static | The block where it is declared | The whole program: it keeps its value between calls |
| Global | The whole file (and others, with extern) | The whole program |
Global static | Only its own file | The whole program |
06The call stack: where local variables live
How can a function have “its own” n if it calls itself? Each call creates in memory a
frame (also called a stack frame) with its parameters, its local variables and the address to which it must
return. Frames are stacked: the last one in is the first one out. On return, the frame is discarded and
its variables cease to exist. Follow the execution of a recursive factorial:
Stack (the top frame is the one being executed)
Screen output
Since the frame is discarded on return, a function that returns &local hands over the address of
something that no longer exists. The program may work by chance and fail later, far from the error. And a
recursion without a base case stacks frames until the stack is exhausted: this is the stack overflow. On a
microcontroller, with 2 KiB of RAM, the stack runs out much sooner.
07Programs in several files
A structured program grows split into modules: a .c file with the code and a
.h file with what the rest of the program needs to know about it, namely the prototypes, types and
constants.
/* resistors.h — the interface: what the module offers */
#ifndef RESISTORS_H // guard against double inclusion
#define RESISTORS_H
double parallel(const double r[], int n);
double series(const double r[], int n);
#endif
/* resistors.c — the implementation: how it does it */
#include "resistors.h"
static int has_short(const double r[], int n); // private to the module
...
It is compiled with gcc main.c resistors.c -o calc. Separating interface from implementation allows
changing the how without touching whoever uses the module: it is the same idea as layered abstraction, at
the scale of a program.
08Standard input and output
| Specifier | Type | Example |
|---|---|---|
%d / %u | int / unsigned | printf("%d", -5) |
%ld | long | printf("%ld", 100000L) |
%f in printf, %lf in scanf | double | printf("%.2f", 3.14159) → 3.14 |
%e / %g | double, scientific or automatic | printf("%e", 4.7e-9) |
%x / %o | integer in hexadecimal / octal | printf("0x%02X", 12) → 0x0C |
%c / %s | character / string | printf("%s", "hello") |
scanf returns how many items it managed to read. A robust program checks it:
if (scanf("%lf", &r) != 1) means that the user typed something that is not a number.
09In the lab
Program the equivalent resistance example from the topic on
algorithms in three files
(main.c, resistors.c, resistors.h), with input validation
and the unusual cases. Check the results with the site’s
resistor calculator to
read off the values of real components.
Compile the factorial with gcc -g and run it in gdb (or in the VS Code
debugger). Set a breakpoint at the base case and use bt (backtrace) to see all
the stacked frames, with the value of n in each one.
Compile a function that adds two integers with gcc -S -O1 and locate in the .s file the
add instruction and the return. Repeat with -O0 and compare the length. Development
environments are covered in C and C++
development environments.
10Common mistakes
=instead of==inside anif.- A semicolon after the
for:for (i = 0; i < n; i++);runs an empty loop. - Forgetting the
&inscanf: the program writes to an arbitrary address. - Unintended integer division:
1/2 * xis always 0. - Uninitialized local variables: they are not zero; they hold whatever was on the stack.
- Using
%dfor adoubleor%ffor anint: it prints garbage without warning.
11Self-assessment
What is the value of double x = 5 / 2;?
2.0. The integer division is done first and gives 2; only afterward is it converted to double.
Why does scanf need &n while printf needs only n?
Because C passes everything by value. printf only needs the value; scanf has to modify the variable and for that it needs its address.
How many frames are on the stack at most when computing fact(5) called from main?
Six: main’s and five of fact’s, with n = 5, 4, 3, 2 and 1.
What is the difference between an ordinary local variable and a static one?
Both are visible only in their block, but the static one lives for the whole program and keeps its value between calls; the ordinary one is created and destroyed on each call.
Which stage gives the error undefined reference and what does it mean?
The linker: the function was declared, but it cannot find its code in any object file or library.
What does if (-1 < 1u) printf("a"); else printf("b"); print?
b. The −1 is converted to unsigned and becomes the maximum value.
12Further reading
- Brian W. Kernighan and Dennis M. Ritchie. The C Programming Language. 2nd ed. (Spanish edition, “El lenguaje de programación C”), Prentice Hall Hispanoamericana, 1991. By the creators of the language. Brief, dense and with exercises that teach you to think in C.
- Harvey M. Deitel and Paul J. Deitel. Cómo programar en C/C++ y Java. 4th ed., Pearson Educación, 2004 (in Spanish). Step by step, with a great many complete examples and good practices.
- Luis Joyanes Aguilar and Ignacio Zahonero Martínez. Programación en C. Metodología, algoritmos y estructura de datos. 2nd ed., McGraw-Hill, 2005 (in Spanish). Connects the pseudocode of the previous topic with the language, in Spanish.
- K. N. King. C Programming: A Modern Approach. 2nd ed., W. W. Norton, 2008. The best modern C textbook: rigorous about the standard and very clear.
- Edsger W. Dijkstra. Go To Statement Considered Harmful. Communications of the ACM, 11(3), 1968. A page and a half that changed the way people program.