Catto / Topic Map · Computer Science I Level 1
Computer Science I · 120 h · Topic 4 of 6

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.

C Compilation Functions Call stack Modules

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.

Why C

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
MessageStageTypical cause
fatal error: stdio.h: No such filePreprocessorHeader name misspelled
expected ';' before …CompilerA semicolon is missing on the previous line
implicit declaration of functionCompilerA function is used without a prototype or #include
undefined reference to 'sqrt'LinkerThe library is missing: add -lm
Warnings are errors that have not blown up yet

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 precedenceAssociativity
() [] -> . 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

StructurePseudocodeC
Simple or double selectionIf … then … elseif (c) { … } else { … }
Multiple selectionCaseswitch (x) { case 1: … break; default: … }
Iteration with a pre-test conditionWhilewhile (c) { … }
Iteration with a post-test conditionRepeat … untildo { … } while (c); (note: the condition is one for continuing, not for stopping)
Iteration with a counterForfor (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.

The switch falls through

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.

VariableScope (where it is visible)Lifetime (how long it lives)
LocalThe block where it is declaredWhile the block executes: it is created and destroyed on each call
Local staticThe block where it is declaredThe whole program: it keeps its value between calls
GlobalThe whole file (and others, with extern)The whole program
Global staticOnly its own fileThe 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:

Lab · the call stack, live
Stack (the top frame is the one being executed)
Screen output
Never return the address of a local

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

SpecifierTypeExample
%d / %uint / unsignedprintf("%d", -5)
%ldlongprintf("%ld", 100000L)
%f in printf, %lf in scanfdoubleprintf("%.2f", 3.14159) → 3.14
%e / %gdouble, scientific or automaticprintf("%e", 4.7e-9)
%x / %ointeger in hexadecimal / octalprintf("0x%02X", 12) → 0x0C
%c / %scharacter / stringprintf("%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

Exercise 1 · A modular resistor calculator

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.

Exercise 2 · Seeing the stack with the debugger

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.

Exercise 3 · Reading the assembly

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 an if.
  • A semicolon after the for: for (i = 0; i < n; i++); runs an empty loop.
  • Forgetting the & in scanf: the program writes to an arbitrary address.
  • Unintended integer division: 1/2 * x is always 0.
  • Uninitialized local variables: they are not zero; they hold whatever was on the stack.
  • Using %d for a double or %f for an int: 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.
Development of the topic “Structured programming language” of Computer Science I (Level 1), based on the curriculum of the UTN Electronic Engineering program, 2023 curriculum — Ordinance No. 1849 of the UTN Higher Council. Back to the Topic Map · catto.ar