What is C, and How Does Source Code Become a Running Program?

Explain what C is and why it matters as the foundation of Objective-C, describe the full pipeline that turns a text file of source code into a running program (preprocessor, compiler, object file, linker, executable), and write, compile, and run a first C program from the command line.

Objective-C is often described as 'C with objects' — and that description is almost literally true: Objective-C is a strict superset of C, meaning every valid C program is also a valid Objective-C program, and Objective-C simply layers an object-messaging system on top. That means before a single line of Objective-C-specific syntax makes sense, you need to actually understand C: what it is, how a C file gets turned into something a computer can run, and how to read the small, dense pieces of syntax that make up a C program. This first lesson does not assume you know any C, or any programming language at all beyond basic familiarity with a computer. We start from the very beginning: what a compiled language is, what a compiler actually does to your text, and how to write and run the smallest possible working program.

C is a Compiled Language, Not an Interpreted One

When you write code in a language like C (or Objective-C), you are writing plain text in a file — nothing about that text does anything by itself. A file full of C source code is inert until a separate program called a compiler reads it and translates it into a form the computer's processor can actually execute: machine code, a sequence of raw binary instructions specific to your CPU's architecture (x86-64, ARM64, etc.). This is fundamentally different from an interpreted language like Python or JavaScript running in a browser, where another program (the interpreter) reads your source code and carries out its instructions line by line, every single time the program runs, translating on the fly.

C, by contrast, is compiled ahead of time: you run the compiler once, it produces a standalone executable file, and from then on you run that executable directly — the compiler is not involved at all when the program actually runs. This has real consequences. Compiled programs tend to start up faster and run faster, because all the translation work happened once, in advance, rather than being repeated on every execution. It also means compiled programs can fail differently: an interpreter might run your program right up until it hits a broken line, while a compiler typically refuses to produce an executable at all if anything in your source code is invalid — you find out about many mistakes before the program ever runs, not while it's running.

C was created by Dennis Ritchie at Bell Labs in the early 1970s, originally to help build the Unix operating system. It became enormously influential because it strikes an unusual balance: it lets you write code that is reasonably readable by humans, while still giving you very direct, low-level control over memory and hardware — closer to what assembly language offers, but with far more structure. That combination is exactly why Objective-C, when Brad Cox and Tom Love designed it in the early 1980s, chose to build an object system as a thin layer on top of C rather than inventing an entirely new language from scratch: C was already fast, already close to the machine, and already had a mature compiler ecosystem. Objective-C inherited all of that, and simply added the message-passing object model on top.

• C is compiled: source code is translated once into machine code, then the resulting executable is run directly. • An interpreted language (Python, JavaScript in a browser) re-translates and executes source code line by line, every run. • Compiled programs generally start and run faster, and many mistakes are caught before the program ever runs. • C was created in the early 1970s by Dennis Ritchie at Bell Labs, originally to build Unix. • Objective-C is C plus an object-messaging system layered on top — every valid C program is valid Objective-C.

The Full Pipeline: From Text File to Running Program

Turning a .c source file into a program you can actually run is not one single step — it's a pipeline with several distinct stages, each performed by a different tool (even though, in practice, one command usually runs all of them for you). Understanding these stages individually matters because the errors you'll encounter as a beginner map directly onto them: a 'preprocessor error', a 'compiler error' (a syntax mistake), and a 'linker error' (a missing piece the compiler couldn't find) are three very different problems, and knowing which stage failed tells you where to look.

The first stage is the preprocessor. Before your actual C code is even looked at grammatically, the preprocessor scans your source file for lines starting with # — these are preprocessor directives, not C statements, and they don't end in a semicolon. The most common one, #include, literally copies the entire contents of another file into your source file at that exact point, textually, before compilation begins. #include <stdio.h> pulls in declarations for standard input/output functions like printf. The preprocessor also handles #define (textual substitution, covered in the constants lesson) and conditional compilation directives like #ifdef. The output of this stage is a single, larger block of pure C code with no # directives left in it — this intermediate form is called 'translated' or 'preprocessed' source.

The second stage is the compiler proper, which takes that preprocessed C code and translates it into an object file — a file of machine code and metadata, but not yet a runnable program. Compilation itself has sub-phases (lexing the text into tokens, parsing those tokens into a structured representation of the program, generating machine instructions from that structure, and optimizing those instructions), but from a beginner's perspective the important fact is simpler: the compiler checks that your code is grammatically valid C and, if so, produces machine code for it. An object file (typically ending in .o on Unix-like systems, .obj on Windows) contains machine instructions for everything defined in that one source file, but it usually is not runnable on its own — especially if your code calls functions (like printf) that are defined somewhere else.

The third stage is the linker. Real programs are rarely built from a single source file, and even a program that is one file still typically calls functions from libraries it didn't write itself (like the C standard library, which provides printf). The linker's job is to take one or more object files, plus any libraries they depend on, and stitch them all together into a single, final, runnable executable — resolving every function call to its actual machine-code address in the process. If the linker cannot find the machine code for something your object file references (a typo in a function name, a library you forgot to link against), you get a 'linker error' or 'undefined symbol' error, which is a different category of mistake from a compiler error: your code was grammatically fine, but something it depends on couldn't be located.

• Preprocessor: handles lines starting with # (like #include, #define) by textual substitution, before real compilation begins. • Compiler: translates preprocessed C source into an object file (.o) of machine code, checking grammar/syntax along the way. • Linker: combines one or more object files plus needed libraries into a single final executable, resolving function calls to real addresses. • Three different stages produce three different kinds of errors: preprocessor errors, compiler (syntax) errors, and linker (undefined symbol) errors. • An object file alone usually isn't runnable; only after linking do you get a runnable executable.

From Executable to a Running Process

Once the linker produces an executable file, you still haven't 'run the program' yet — the executable is just a file sitting on disk, like any other. Running it means asking the operating system to load that file into memory and create a new process from it: the OS reads the executable's machine code and data into RAM, sets up a stack for local variables and function calls, sets up a heap for dynamically allocated memory, and then transfers control to the program's designated entry point, which in C is the special function named main. From that moment, the CPU is executing your program's own machine instructions directly, one at a time, until main returns (or the program otherwise exits), at which point the operating system reclaims the process's memory and resources.

The word 'runtime' refers to this period while the program is actually executing as a process — as distinct from 'compile time', when the compiler was translating your source code, before the program ever ran. Some kinds of errors are only detectable at compile time (like a syntax mistake — the compiler simply refuses to produce an executable), while other kinds only show up at runtime (like trying to divide by zero, or accessing invalid memory) because they depend on the actual data and conditions present while the program is executing, which the compiler cannot fully predict in advance. This compile-time versus runtime distinction is one of the most important habits of mind in C and Objective-C programming, and it will come up constantly: a problem the compiler catches is usually cheap and quick to fix, while a problem that only appears at runtime can be much harder to track down.

This whole chain — preprocessor, compiler, linker, then OS-driven execution — is exactly what happens, essentially unchanged in shape, when you build an Objective-C program. Objective-C source files use the .m extension (.mm for Objective-C++), and they are compiled by clang, the same modern C-family compiler front end used for C and C++, extended to also understand Objective-C's object-messaging syntax. When you build an iOS or macOS app in Xcode, clang runs the exact same preprocess → compile → link pipeline described above on every .m file in the project, and the linker stitches the results together with Apple's Objective-C runtime library and frameworks like Foundation and UIKit. Understanding this pipeline now, using plain C, means that when you see Xcode's build log later and it says 'Compiling MyClass.m' followed by a separate 'Linking' step, none of it will feel like magic — it's the exact same pipeline you're about to run by hand from the command line.

• Running a program: the OS loads the executable into memory as a new process and jumps to the entry point, main. • 'Runtime' = while the process is executing; 'compile time' = while the compiler was translating your source, before any of it ran. • Compile-time errors (syntax) are caught before the program runs; runtime errors (like invalid memory access) only appear during execution. • Objective-C .m files go through the identical preprocess → compile → link pipeline, run by clang. • An Xcode build for an app runs this same pipeline automatically across every source file, then links in Apple's runtime and frameworks.

Writing and Compiling Your First C Program

It's time to actually do this. A C program's text lives in a file with a .c extension. The very first line of almost every C program that produces output is #include <stdio.h> — a preprocessor directive that pulls in the declarations for the 'standard input/output' library, which is where the printf function (used to print text) comes from. Without this line, the compiler would have no idea what printf even is, and would reject the file with an error.

Every C program must have exactly one function named main — this is the entry point the operating system jumps to when it runs your executable, as described above. int main(void) declares a function called main that takes no parameters (void here means 'no parameters') and returns an int (a whole number) to the operating system when it finishes — by convention, returning 0 signals 'the program succeeded', and any nonzero value signals some kind of failure or error code, a convention every shell and script that runs your program can check.

Inside main, the line printf("Hello, Objective-C world!\n"); calls the printf function, passing it a string literal (text in double quotes) to print. The \n inside the string is an escape sequence representing a newline character — it is not the two characters backslash-and-n, but a single newline, telling the terminal to move to the next line after printing. The line ends in a semicolon, ;, because in C (and later, Objective-C), every statement must end with a semicolon — it's how the compiler knows where one instruction ends and the next begins; forgetting it is one of the single most common beginner mistakes and produces a compiler error. Finally, return 0; ends the main function, handing the value 0 back to the operating system as the program's exit status.

• #include <stdio.h> — pulls in declarations needed to use printf. • int main(void) { ... } — the required entry-point function; returns an int exit status to the OS. • printf("..."); — prints text; \n inside the string is a newline escape sequence, not two literal characters. • Every C statement ends with a semicolon ;. • return 0; signals success to the operating system; nonzero return values conventionally signal an error.

Code Examples

Line by line: #include <stdio.h> tells the preprocessor to paste in printf's declarations. int main(void) declares the required entry-point function, taking no arguments and returning an int. The printf call prints the text followed by a newline (\n). return 0; ends main and reports success to the operating system. Expected result: Hello, Objective-C world!

clang hello.c -o hello runs the whole preprocess-compile-link pipeline in one command and writes the final executable to a file named hello (the -o flag names the output). ./hello asks the operating system to load and run that executable as a new process. echo $? prints the exit status of the last command — 0, matching the return 0; in main, confirming the program reported success. Expected result: Hello, Objective-C world! 0 clang hello.c -o hello

Passing different flags to clang stops the pipeline at different stages: -E stops after preprocessing (you'd see stdio.h's contents pasted into the file), -S produces human-readable assembly instead of final linking, -c stops after compiling to an object file (no linking yet, so hello.o alone usually cannot run), and the last command performs just the linking step on an already-compiled object file. You don't need to run these separately in normal development — a plain clang hello.c -o hello does all four stages — but seeing them broken apart makes the earlier diagram concrete.

#include <stdio.h>

int main(void) {
    printf("Hello, Objective-C world!\n");
    return 0;
}
$ clang hello.c -o hello
$ ./hello
Hello, Objective-C world!
$ echo $?
0
$ clang -E hello.c -o hello.i     # preprocess only
$ clang -S hello.i -o hello.s      # compile to assembly (human-readable-ish)
$ clang -c hello.c -o hello.o      # compile to an object file (machine code)
$ clang hello.o -o hello           # link the object file into an executable

From Source Code to a Running Program

Source Code → Preprocessor → Compiler → Object File → Linker → Executable → Operating System. Objective-C's .m files travel this exact same pipeline through clang.

int main(void) { printf("Hello\n") return 0; } int main(void) { printf("Hello\n"); return 0; }

The missing semicolon after the printf call is a compiler (syntax) error — C requires every statement to end in ;, and the compiler will refuse to produce an executable until it's fixed. This is a compile-time error, not a runtime one: the program never even gets built.

int main(void) { printf("Hello, Objective-C world!\n"); } int main(void) { printf("Hello, Objective-C world!\n"); return 0; }

Omitting return 0; from a function declared to return int is undefined/implementation-dependent behavior in C (some compilers warn, some don't, and the actual exit code becomes unpredictable) — always explicitly return a value that matches the function's declared return type.

A missing semicolon after return 0 is the same class of mistake as after printf — every C statement needs one, including return statements, and the compiler will report a syntax error near the closing brace.

What is the key difference between a compiled language like C and an interpreted language? A. Compiled languages cannot use functions; interpreted languages can B. A compiled language is translated to machine code once in advance and then run directly; an interpreted language is translated and executed line by line on every run C. Interpreted languages always run faster than compiled languages D. There is no real difference, just different tool names

B. A compiled language is translated to machine code once in advance and then run directly; an interpreted language is translated and executed line by line on every run — Compilation happens once ahead of time, producing a standalone executable; interpretation re-reads and re-executes the source code every time the program runs.

What does the linker do that the compiler does not? A. It checks your code for syntax errors B. It converts your source file into an object file of machine code C. It combines one or more object files and needed libraries into a single final, runnable executable, resolving references between them D. It translates C into Objective-C automatically

C. It combines one or more object files and needed libraries into a single final, runnable executable, resolving references between them — The compiler produces per-file object files containing machine code; the linker is the separate stage that stitches those object files together with any libraries they depend on into one runnable executable.

Which line in a C file is a preprocessor directive rather than a C statement? A. return 0; B. printf("Hello\n"); C. #include <stdio.h> D. int main(void) {

C. #include <stdio.h> — Lines beginning with # (like #include) are handled by the preprocessor via textual substitution before real compilation, and unlike C statements, they do not end with a semicolon.

Why does understanding this C compilation pipeline matter for learning Objective-C? A. It doesn't — Objective-C uses a completely different build process B. Objective-C .m files are compiled by clang through the exact same preprocess → compile → link pipeline C. Objective-C code is only ever interpreted, never compiled D. Because Objective-C replaces the linker with a different tool entirely

B. Objective-C .m files are compiled by clang through the exact same preprocess → compile → link pipeline — Objective-C is C plus an object system; its .m source files travel through the same clang-driven preprocess/compile/link pipeline described in this lesson, which is why understanding it now pays off directly later.

hello.c              hello.i                hello.o            hello (executable)
 ┌──────────┐  strip   ┌──────────┐  compile  ┌──────────┐  link  ┌────────────────┐
 │ source    │ #incl.  │ pure C    │ ────────▶│ machine   │──────▶│ runnable        │
 │ code text │────────▶│ (expanded)│  (clang   │ code +    │(clang │ program on disk │
 └──────────┘ preproc. └──────────┘  -c step) │ metadata  │ link) └────────────────┘
                                                └──────────┘             │
                                                                          ▼
                                                                  Operating System
                                                              loads it, creates a
                                                              process, jumps to main()