1 The Problem
We want a small program that converts a temperature from Celsius to Fahrenheit, or the other way around. The user picks which direction, types a number, and the program does the maths and shows the answer. Simple — but it teaches the core shape of every calculator-style tool: ask, calculate, show.
2 How to Think About It
Six conversions, one function. The interesting design question isn’t the math — it’s how a C function reports failure when it has no exceptions to throw.
CtoF) and a number. → 2. Look up which formula that code means. → 3. If it’s a real code, compute the result and report success. → 4. If it isn’t, report failure without touching the output. That’s the whole program.
3 The Build — explained part by part
The conversion logic lives in convert, which returns a bool and writes its real answer through an output parameter — C’s standard way to report both a result and whether that result is even valid.
#ifndef TEMPERATURE_CONVERTER_H
#define TEMPERATURE_CONVERTER_H
#include <stdbool.h>
/* Converts `value` using `choice` (one of "CtoF","FtoC","CtoK","KtoC","FtoK","KtoF").
* On success returns true and writes the result to *out. On an unknown choice
* returns false and leaves *out untouched. */
bool convert(const char *choice, double value, double *out);
#endif
#include "TemperatureConverter.h"
#include <string.h>
bool convert(const char *choice, double value, double *out) {
if (strcmp(choice, "CtoF") == 0) { *out = value * 9.0 / 5.0 + 32.0; return true; }
if (strcmp(choice, "FtoC") == 0) { *out = (value - 32.0) * 5.0 / 9.0; return true; }
if (strcmp(choice, "CtoK") == 0) { *out = value + 273.15; return true; }
if (strcmp(choice, "KtoC") == 0) { *out = value - 273.15; return true; }
if (strcmp(choice, "FtoK") == 0) { *out = (value - 32.0) * 5.0 / 9.0 + 273.15; return true; }
if (strcmp(choice, "KtoF") == 0) { *out = (value - 273.15) * 9.0 / 5.0 + 32.0; return true; }
return false;
}
#include "TemperatureConverter.h"
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
if (argc != 3) {
fprintf(stderr, "Usage: %s <CtoF|FtoC|CtoK|KtoC|FtoK|KtoF> <value>\n", argv[0]);
return 1;
}
double value = atof(argv[2]);
double result;
if (!convert(argv[1], value, &result)) {
fprintf(stderr, "Unknown conversion: %s\n", argv[1]);
return 1;
}
printf("%.2f\n", result);
return 0;
}
Optional or Rust’s Option can. The idiomatic C pattern instead is an out-parameter: the return value (bool) says whether it worked, and the real answer is written through a pointer only when it did. Callers who ignore the bool and read *out anyway get whatever garbage was already sitting in that memory — which is exactly why the test below checks that a failed call leaves *out completely untouched.strcmp(choice, "CtoF") == 0 — C has no string
switch the way Java or JavaScript does (a switch in C only works on integers), so a chain of strcmp-and-compare calls is the standard, if verbose, C idiom for branching on string content.const char *choice — marking the parameter
const is a promise, checked by the compiler, that convert will never modify the string it was handed.
double and using some sentinel value like -999.0 to mean “invalid” — a legitimate conversion could genuinely produce that number.bool return value and an out-parameter, as shown, so success/failure can never be confused with a real answer.*out without first checking the bool that convert returned.main.c does before printing.== in tests — rounding error means two mathematically equal values can differ in their last bit.CLOSE macro in the tests below does.4 Test & Prove Each Part
C has no built-in test framework, and this sandbox cannot reach a package registry for one, so these tests use plain assert() calls — a small, genuinely real practice for a project this size, covered in the course’s Testing lesson.
#include "TemperatureConverter.h"
#include <assert.h>
#include <stdio.h>
#include <math.h>
#define RUN(name) do { name(); printf("PASS: %s\n", #name); } while (0)
#define CLOSE(a, b) (fabs((a) - (b)) < 0.001)
static void freezing_point_c_to_f(void) {
double out;
assert(convert("CtoF", 0.0, &out));
assert(CLOSE(out, 32.0));
}
static void boiling_point_f_to_c(void) {
double out;
assert(convert("FtoC", 212.0, &out));
assert(CLOSE(out, 100.0));
}
static void absolute_zero_c_to_k(void) {
double out;
assert(convert("CtoK", -273.15, &out));
assert(CLOSE(out, 0.0));
}
static void round_trip_c_to_f_to_c_is_identity(void) {
double f, c;
assert(convert("CtoF", 37.0, &f));
assert(convert("FtoC", f, &c));
assert(CLOSE(c, 37.0));
}
static void unknown_choice_returns_false_and_leaves_out_untouched(void) {
double out = 999.0;
assert(!convert("CtoMoon", 10.0, &out));
assert(CLOSE(out, 999.0));
}
int main(void) {
RUN(freezing_point_c_to_f);
RUN(boiling_point_f_to_c);
RUN(absolute_zero_c_to_k);
RUN(round_trip_c_to_f_to_c_is_identity);
RUN(unknown_choice_returns_false_and_leaves_out_untouched);
printf("All tests passed.\n");
return 0;
}
Compile and run with gcc -o test_run TemperatureConverter.c test_TemperatureConverter.c -lm && ./test_run. The -lm links the math library for fabs(), used by the tolerance-based CLOSE comparison macro.
5 The Interface
Even a tiny program has an interface. Here is its contract, documented plainly.
CtoF 100What it expects
./convert CtoF 100What it returns
212.006 Run It & Automate It
Save the code as TemperatureConverter.h / TemperatureConverter.c / main.c and compile it with gcc — that turns your source directly into a native executable for your machine. No separate runtime needed: the compiled binary runs on its own.
gcc -o convert main.c TemperatureConverter.c -lm && ./convert CtoF 100Swap
CtoF for FtoC, CtoK, KtoC, FtoK, or KtoF, and the number for anything you like.A CI tool like Jenkins runs the same compile-then-test-then-check-for-leaks steps automatically whenever the code changes — every line below has a plain explanation.
$ ./convert CtoF 100
212.00
$ ./convert FtoC 32
0.00ctof or a stray space.sqrt, but if you extend it to use one, remember C's math functions require linking with -lm — it's not automatic.choice then value, e.g. CtoF 100, not 100 CtoF.// Jenkinsfile — compiles, tests, and checks for leaks on every change.
pipeline {
agent any
stages {
stage('Get the code') {
// download the latest code
steps { checkout scm }
}
stage('Compile') {
steps {
// confirm a compiler is installed
sh 'gcc --version'
// compile with strict warnings on
sh 'gcc -std=c17 -Wall -Wextra -o app *.c -lm'
}
}
stage('Run the tests') {
steps {
// prints PASS/FAIL, exits non-zero on failure
sh './app'
}
}
stage('Check for memory leaks') {
steps {
// fails the build on any leak or invalid access
sh 'valgrind --error-exitcode=1 --leak-check=full ./app'
}
}
}
post {
success { echo 'All tests passed, no leaks found.' }
failure { echo 'A test or Valgrind check failed — see above.' }
}
}
strcmp chains replace a string switch; marking parameters const; and comparing floating-point results with a tolerance instead of ==.