C / GETTING STARTED
What C is and where it still runs
Explain what kind of language C is, read the machine-level meaning of a small C program, and name the places C is still the default choice today.
What you will learn
- Describe C as a compiled language with no garbage collector and almost no runtime
- Work out how many bytes a small array of int or char occupies, and why
- Name real systems written in C: kernels, firmware, SQLite, CPython, FFmpeg
- Explain why unsigned wraparound is defined but a bad array index is not caught
Understanding What C is and where it still runs
C is a small compiled language: roughly three dozen keywords, a handful of built-in types that correspond to the units of storage a processor actually works with, and a standard library you can skim in an afternoon. There is no interpreter, no bytecode, no garbage collector and no exception machinery; a compiled C program is machine code plus a short startup stub, and the only thing running is the code you wrote. The mental model to carry through the rest of this track is a machine made of bytes and addresses: an int is a fixed number of bytes somewhere, an array is those bytes laid end to end, and a pointer is the address of the first one. The compiler's job is to make real hardware behave like that model, which is why you can usually predict what a line of C costs.
Demanding so little is exactly why C is still under the software you use every day. Anything that runs before an operating system exists, or without one at all, has nowhere to put a runtime: the kernels of Linux, Windows and macOS are largely C, as are device drivers, bootloaders, router firmware, engine controllers and infusion pumps. The second reason is interoperability, because the C calling convention is the neutral meeting point between languages: SQLite, zlib, OpenSSL, FFmpeg, curl and the CPython, Ruby and Lua interpreters are C code, and Python, Rust, Go, Java and JavaScript all reach outward through C-shaped interfaces. Learning C means learning the layer those languages are written in and defined against.
The price of the bargain is that nothing checks you while the program runs. Reading past the end of an array, using memory after freeing it, or overflowing a signed integer are not reported errors but undefined behaviour, which means the compiler may assume you never do them and optimise on that assumption, so the symptom usually appears far from the cause. Portable also means less than beginners expect: the source is portable, but the size of int, whether char is signed, and where padding sits inside a struct are left to the implementation, precisely so that one language can compile efficiently for an 8-bit microcontroller and for a 64-bit server. That is why careful C asks the target through limits.h and stdint.h instead of assuming numbers.
Getting comfortable with that trade is the real work of learning C, and it starts by reading small programs as descriptions of memory rather than as instructions to a helpful runtime.
<stdio.h>
<limits.h>
int main(void)
{
unsigned char counter = 255;
counter = counter + 1; /* 256 does not fit in one byte */
printf("bits in a char: %d\n", CHAR_BIT);
printf("bytes in an int: %zu\n", sizeof(int));
printf("255 + 1 in an unsigned char: %d\n", counter);
printf("the character 'A' is the number %d\n", 'A');
return 0;
}
C is a thin, portable notation for the machine's own operations, bytes and addresses and arithmetic with almost no runtime beneath it, which is why it still sits underneath operating systems, firmware and other languages.
Worked examples
An array is just bytes in a row
Shows that C arrays carry no length or type information, and that pointer arithmetic counts elements rather than bytes.
<stdio.h>
int main(void)
{
int numbers[3] = { 10, 20, 30 };
char letters[3] = { 'a', 'b', 'c' };
printf("3 ints fill %zu bytes\n", sizeof numbers);
printf("3 chars fill %zu bytes\n", sizeof letters);
printf("numbers + 1 reads %d\n", *(numbers + 1));
printf("letters + 1 reads %c\n", *(letters + 1));
return 0;
}
Example explained
Line 1sizeof numbers is 12 because the array is exactly three 4-byte ints end to end, with no length field, type tag or object header stored alongside them.
Line 2*(numbers + 1) moves 4 bytes forward, since pointer arithmetic is measured in elements of the pointed-to type.
Line 3*(letters + 1) moves 1 byte with the identical + 1, so the type of the pointer, not the expression, sets the stride.
Line 4Because the length is nowhere in memory, writing numbers[7] would compile and touch whatever bytes follow; there is no runtime that could object.
Fixed-width types, the way firmware writes them
Demonstrates the stdint.h types and bit masking that make the same C source work from an 8-bit register to a 32-bit word.
<stdio.h>
<stdint.h>
int main(void)
{
uint8_t status = 0x0F;
uint32_t word = 0xDEADBEEF;
printf("uint8_t holds %zu byte, value 0x%02X\n", sizeof status, (unsigned)status);
printf("uint32_t holds %zu bytes, value 0x%08X\n", sizeof word, (unsigned)word);
status |= 1u << 7;
printf("after setting bit 7: 0x%02X\n", (unsigned)status);
return 0;
}
Example explained
Line 1uint8_t and uint32_t are defined to be exactly 8 and 32 bits wide, and a target that cannot provide such a type simply does not define it, which is why protocol and driver code prefers them over int.
Line 21u << 7 builds the mask 0x80 by hand and |= writes the whole byte back, because C offers no abstraction that sets a named bit for you.
Line 3The cast to unsigned is there because printf cannot inspect what it was handed; the conversion specifier is the only description of the value it receives.
Line 4This is literally the shape of code that flips a bit in a hardware control register on a microcontroller.
Important notes
The exact numbers here come from a mainstream desktop compiler; the standard only guarantees minimums, so the same source on a 16-bit microcontroller can correctly print bytes in an int: 2.
C is a series of standards, C89 through C23, plus compiler extensions rather than one fixed language, so code accepted in a compiler's default mode can be rejected in a stricter standard mode.
Common mistakes
Treating C and C++ as one language and pasting class, new, std::string or cout into a .c file; the C compiler rejects all of them because C has no classes, templates or iostream, and you lose time debugging a language mismatch rather than a bug.
Believing sizeof(int) is 4 by definition and hard-coding sizes, as in malloc(n * 4); on a target where int is 2 bytes it wastes memory, and if the element type later becomes double the allocation is silently too small and the program corrupts memory.
Expecting an error when reading numbers[7] from a three-element array, the way Python raises IndexError; C stores no length, so the read just happens on neighbouring bytes and the program may print plausible garbage now and crash somewhere unrelated later.
Try it yourself
Change, predict, then run
In a browser C editor, print CHAR_BIT and the sizes of char, short, int and a 10-element int array. Next to each number write whether the C standard fixes it or your compiler chose it.
Open the C workspaceCheck your understanding
Why is C still the usual choice for a microcontroller's firmware and for an operating system kernel, rather than a language like Python or Java?
- Because a C program needs no interpreter, virtual machine or garbage collector under it, so the compiled code can be the bottom layer on the machine
- Because C's arithmetic is faster than any other language's, since each operator becomes exactly one instruction
- Because C checks every memory access at runtime, which is what safety-critical systems require
- Because C's standard library is far larger and already contains hardware access
Show answer
Kernels and firmware run before, or entirely without, the machinery other languages assume: no process, no managed heap, nothing to host an interpreter or a collector, and C is usable in exactly that situation. Speed is the tempting answer, but Rust, C++ and Fortran compile to comparably fast code, so what decides it is how little C requires underneath it. C does no runtime memory checking at all, and its standard library is deliberately small and says nothing about hardware.