Simple Calculator
Lesson 1 of 3
1🖩 Simple Calculator (C - Simple Calculator)
Your first "real" C program that reads input, processes it with multi-branch logic, and produces a result, a perfect exercise tying together all of the first stage's fundamentals in one small program.
What the picture shows: The program reads "3", "+" and "4" from argv, converts the numbers, picks the operation with switch, and prints 7.
Two ways into the same idea
A calculator is the first C program that brings together everything you learned: it takes two numbers and an operation, decides what to do based on the operation, then gives you the answer. It must also behave politely if you type a letter instead of a number or divide by zero, instead of crashing.
You read input with scanf or fgets then strtod, check the return value, pick the operation with switch, and handle division by zero and invalid input. You split the program into small functions and compile with -Wall -Wextra to see warnings.
Before You Start
⚙️C fundamentals
Conditionals, argc/argv, and converting strings to numbers (atoi) are the core tools here.
The Core Idea: Decomposing "Compute a operator b"
A simple calculator receives three separate parts from argv, a number, then an operator symbol, then another number, and all three arrive as raw strings with no arithmetic meaning yet. The real task here isn't "add two numbers"; it's three independent, sequential decisions: how those strings get read and converted into actual values, how one of four possible operations gets selected, and how that operation gets executed without crashing in the one case where it legitimately can (division by zero). Each of these three decisions fails in a completely different way from the other two, which is exactly why they're handled with three different mechanisms instead of one catch-all solution.
Start with the dispatch itself: switch versus an if/else if chain. Purely in terms of meaning, there's little difference here, both run exactly one branch based on op's value. The real difference is in how the compiler is allowed to actually implement that decision. An if/else if chain forces the compiler to run each comparison in source order: to reach the fourth case for /, it must first test +, then -, then *, and fail each one, a cost that scales linearly with however many cases sit ahead of it in the code. A switch, by contrast, hands the compiler every possible value at once inside a single construct, which frees it to pick whichever dispatch strategy actually fits that specific set of values best: a short sequential comparison chain when the values are few and scattered (exactly the situation with four scattered operator characters like + - * /), a binary-search tree of comparisons when there are many values, or, in the ideal case where the case values are packed into a tight, consecutive numeric range, a jump table, where the value itself is used directly as an index into an array of code addresses, with no comparison at all. Which strategy actually gets used is an internal compiler decision that never shows up in the source code; all the C standard guarantees is the observable behavior: execution begins at the first case label that matches op's value.
From that point on, execution is explicitly sequential, not implicit: the code after the matching case label simply keeps running line by line, there's no automatic "jump back to the top of the switch", until it hits a break (which jumps immediately past the entire switch) or the end of the switch block itself. That means accidentally omitting break doesn't repeat the current case; it "falls through" into the code of the next case in source order, regardless of what op's actual value was, a completely silent bug, because the compiler treats this as intentional, legal language behavior, not a defect. And default isn't "the last case checked in sequence", it's a path that only runs once every listed case has failed to match, exactly the way an if/else if chain would only reach its final else after every one of its conditions failed.
Once the operator is resolved, the other two strings still need converting into real numbers via atoi. What matters isn't just that it "turns a string into a number", it's how it behaves on failure: atoi raises no error and gives you no signal at all that conversion failed. Feed it a string that doesn't start with a valid number (like "abc") and it silently returns 0, the exact same value you'd get if the user had actually typed "0". After that call, your program has no way left to distinguish "the user genuinely typed zero" from "the user typed something non-numeric that silently became zero", which is exactly why the standard C library also offers functions like strtol, which hand you back an explicit end pointer you can check, though that's outside this small project's scope.
Finally, division by zero specifically isn't just "a logic error" like any other, it is formally undefined behavior in the C language standard itself (section 6.5.5 states this explicitly). That classification isn't a theoretical footnote: on the processors that actually compile and run your code (the x86/x86-64 processors a typical Linux environment runs on), the processor's own integer division instruction (idiv) is what detects the zero divisor, as a hardware fault, your program's logic doesn't "notice" the error; the processor itself refuses to execute the instruction and raises a hardware exception, which the kernel immediately translates into a SIGFPE signal delivered to your process. Since your program never installed a handler for that signal, its default action terminates the process immediately, often printed by the shell as something like "Floating point exception," even though the operation is integer, not floating-point, arithmetic. Crucially, this exact behavior can vary across compilers, processors, and even optimization levels, so there is no single "expected outcome" you can safely reason about after the fact. That's exactly what makes checking beforehand the only reasonable strategy: there is no meaningful "afterward" in which to handle the error, since the process can be terminated immediately with no ordinary, easily interceptable notification. This is the real reason the order of checks in the example below matters: each guard rules out a completely different failure category, at the one specific moment that failure actually becomes possible, no earlier, no later.
Figure 1, switch compares op's value against each case in order until it finds a match, then jumps straight into executing only that branch; failing to match any listed case automatically falls through to default.
Worked Example: A Complete Calculator Structure
int main(int argc, char *argv[])
{
int a, b, result;
char op;
if (argc != 4)
{
printf("Usage: %s number operator number\n", argv[0]);
return (1);
}
a = atoi(argv[1]);
op = argv[2][0]; /* the first character of the string, e.g. argv[2]="+" */
b = atoi(argv[3]);
switch (op)
{
case '+': result = a + b; break;
case '-': result = a - b; break;
case '*': result = a * b; break;
case '/':
if (b == 0)
{
printf("Error: division by zero\n");
return (1);
}
result = a / b;
break;
default:
printf("Error: unknown operator '%c'\n", op);
return (1);
}
printf("%d\n", result);
return (0);
}
Figure 2, Notice the check ordering: argument count first, then operator validity, then division-by-zero specifically right before executing it, each guard prevents a possible failure exactly at its own spot.
argc before accessing argv[1] or argv[3]. Accessing an argv element that doesn't actually exist (because the user passed fewer arguments than expected) is dangerous undefined behavior.
Deep Dive: Project Method, scanf Mechanics and Edge Cases
The calculator project tests your programming maturity more than your arithmetic. Here we detail three things: a systematic way of approaching any project, how scanf really handles input, and what the strict compiler settings mean.
1. A framework for approaching any project
The biggest early mistake is opening the editor before you know exactly what is required. The school’s framework delays typing on the computer until you are sure on paper: read the task, restate it in your own words, check expected behavior with example inputs and outputs, then design the solution in words, then pseudocode, then code, and at each stage "run it like a machine": do only what is written. Only then do you write, test and debug. Break the project into small pieces and repeat the cycle on each, building and testing one piece at a time.
Figure 3, The project framework: paper first, then computer, one small piece per cycle.
2. How scanf really handles input
What you type in the terminal does not go straight into your variables; it is first stored in an input buffer (stdin) and scanf reads from it. scanf follows the conversion format (such as %d for an integer), skips white space, consumes characters that suit that type, and stops at the first character that does not. It returns the number of items successfully read. That return value is the key to every check: ask for one item and get 1, you succeeded; get 0, matching failed; get EOF, the input ended.
Figure 4, The cause of the famous endless loop: scanf does not consume what failed to match, so it stays in the buffer.
This is exactly what the optional challenge asks: find out what remains in stdin after a failure, and devise a strategy to discard the leftovers up to the end of the line before asking again. The general idea (without a ready solution): after any failure, consume characters up to the end of the line before prompting the user again.
3. Edge cases in arithmetic
| Case | What happens | How to think about it |
|---|---|---|
| Division by zero | Undefined behavior for integers and the program may crash | Check the divisor before dividing |
| Integer division | 7 ÷ 2 gives 3 because the fraction is truncated | Decide and document: integers or decimals? |
| Floating point | 0.1 + 0.2 may not equal 0.3 exactly | Floating-point numbers are approximate in memory |
| Overflow | Exceeding the largest integer yields a wrong result | Know the type’s limits and state them in the README |
4. What the compiler flags say
| Flag | The idea |
|---|---|
-Wall / -Wextra | Enable most useful warnings |
-Werror | Turn every warning into an error that blocks compilation |
-pedantic | Follow the standard strictly |
-std=gnu89 | An older language version: declare variables at the start of a block |
A warning is not a nuisance but a free review: a strict compiler reveals mistakes that would otherwise appear later as mysterious crashes.
Try it yourself
What does scanf return when you ask for one integer and the user types letters?
scanf returns the number of items it read successfully. 0 means the match failed, and what did not match is not consumed, which causes the famous endless loop.
What does the C standard guarantee about switch?
How it branches (comparisons, a tree, a jump table) is the compiler's own decision. The guarantee is the behaviour: start at the matching case and run on until break.
Why is division by zero handled as a special case?
Integer division by zero is undefined behaviour, so we check the divisor before running the operation.
Quick Review Flashcards
Check Your Understanding
If the user passed an unknown operator like "%", what happens in the code above?
It falls into the default case in the switch, so a clear error message is printed and a failure code (1) is returned instead of executing an undefined operation or crashing silently.
Why does the framework delay typing on the computer?
Because finding a mistake on paper is far cheaper than finding it inside a lot of code.
Resources for this part
Sorted by kind: start with the official docs when you want the precise reference, or with an article or video when you want the explanation first.
Resources for this lesson
Grouped by type. Start with the official docs for precision, or an article or video for the explanation.
Official docs2
Done with this lesson?
Sign in to save your progress.