Every programming journey must begin somewhere, and tradition demands that we begin by making the computer greet the world.1
This page has a deliberately small goal: establish a working C development environment, create a simple program, compile it, run it, and make a few small changes.
We will explain what the compiler is actually doing in the next section.
What Is C?
C is a general-purpose programming language that provides relatively direct access to memory and machine-level operations while remaining portable across many different computer architectures and operating systems. C is a compiled programming language: source code is translated into machine code before execution2.
Compared with many modern languages, C provides relatively few built-in abstractions. Features such as dynamic arrays, hash tables, garbage collection, and object systems must either be provided by libraries or constructed from more primitive language facilities.
C’s syntax is surprisingly sparse compared with many modern programming languages.3 The basic language can be learned relatively quickly, but its small set of constructs provides enormous expressive power. Learning what the syntax does is much easier than learning to reason well about the programs it can produce. C is a language that can be learned quickly and studied for a lifetime.
That characteristic is one of the reasons C is useful in this source. It allows us to expose many of the representations and relationships that higher-level languages ordinarily hide.
For now, however, we need only enough C to make the computer do something.
A Brief History of C
C was developed by Dennis Ritchie at Bell Labs in the early 1970s while he and others were developing the Unix operating system. It evolved most directly from the B programming language, which was itself influenced by BCPL.
Early operating systems were commonly written largely in assembly language, tying substantial portions of their implementation to a particular processor. Rewriting much of Unix in C demonstrated that a systems language could provide low-level control while still allowing significant portions of a system to be moved between different machines.
C subsequently became one of the most influential programming languages ever created. Unix and its descendants helped spread it widely, and its syntax and programming model influenced numerous later languages.
C was standardized by ANSI in 1989 and later by ISO. The language has continued to evolve, but its relatively small core and long history mean that much of the C encountered in older software remains recognizable to modern C programmers.
Prerequisites
The examples in this primer assume a Debian-based Linux development environment and were tested using Ubuntu. Other distributions may behave differently, but the examples should generally work as long as the required tools are available.
Install the tools required for the first few sections with your system’s package manager. On Ubuntu and other Debian-based distributions, run:
sudo apt update
sudo apt install clang lldb -y
For now, the important tools are4:
- Clang — the compiler used to build the C programs in this primer
- LLDB — the debugger used to inspect and troubleshoot those programs at runtime
These descriptions are intentionally brief. Clang and LLDB will be introduced in more detail when we begin using them in later sections. Additional tools will be introduced only when they become useful.
Verify that Clang is available:
clang --version
Verify that LLDB is available:
lldb --version
The exact version numbers are not important for this introductory exercise.5
Environment Note: The practical examples in this primer use Linux, Clang, LLDB, and other Unix development tools so that the behavior we discuss can be directly observed. C itself is not specific to Linux, Clang, or LLDB.
A Brief History of Clang and LLDB
Clang and LLDB are both part of the broader LLVM project, a collection of modular compiler and toolchain technologies. LLVM began in 2000 as a research project led by Chris Lattner at the University of Illinois and was first publicly released in 2003. Apple began contributing to LLVM in 2005, helping accelerate its development into a production-quality compiler infrastructure.
Clang emerged during the mid-2000s as a new front end for C, C++, and Objective-C built on top of LLVM. Rather than treating the compiler as one monolithic program, Clang was designed around reusable libraries, fast compilation, and high-quality diagnostics. It eventually became a major part of the LLVM toolchain and replaced GCC as the default compiler in several Apple development environments.
LLDB followed around 2010 as a debugger built around the same modular philosophy. It was designed to reuse LLVM and Clang infrastructure for tasks such as disassembly and expression parsing rather than duplicating that functionality in a completely separate toolchain.
LLVM has since grown far beyond its original role in the C-family ecosystem. It is also used as the primary code-generation backend for Rust and has provided compiler infrastructure for numerous other languages.
This shared foundation is one reason we use Clang and LLDB together in this primer: the compiler and debugger are separate tools, but they belong to the same ecosystem and are built around many of the same underlying components.
Hello, World
Follow these steps to create, compile, and run your first C program.
1. Create the Source File
Open the text editor or IDE of your choice and create a new file named:
hello.c
Add the following code:
#include <stdio.h>
int main(void)
{
printf("Hello, World!\n");
return 0;
}
Save the file.
There are already several unfamiliar pieces of syntax here. We will explain them properly as we learn C. For now, a minimal interpretation is sufficient.
#include <stdio.h>makes declarations for standard input and output facilities available to the program. One of those facilities isprintf.int main(void)defines a function namedmain. In an ordinary hosted C program,mainis the function through which the C implementation transfers control to the code you write.- The statements belonging to the function are enclosed in braces:
{ ... } printf("Hello, World!\n");writes the textHello, World!to standard output.6- The
\nrepresents a newline. return 0;ends themainfunction and indicates successful completion.7
That is enough syntax for now.
2. Compile the Program
Open a terminal in the directory containing hello.c and run:
clang hello.c -o hello
This command contains three important pieces:
clang hello.c -o hello
│ │ │
│ │ └── name the resulting executable "hello"
│ └─────────────── source file to compile
└────────────────────────── run the Clang compiler
Unlike many modern language toolchains that automatically discover the source files belonging to a project, Clang operates on the files explicitly provided to it. In this case, we have only one:
hello.c
Later, when a program contains multiple C source files, those files must all become part of the build.8 For example:
clang main.c list.c -o program
The -o option specifies the name of the output file. Without it, Clang will
use a default executable name on many Unix-like systems.
If the command succeeds, it will normally produce no output.
You should now have two files:
hello.c
hello
hello.c contains the C source code that you wrote.
hello is an executable program created from that source.
We will examine exactly how one became the other in the next section.
3. Run the Program
Execute the program:
./hello
You should see:
Hello, World!
Congratulations! You have now written, compiled, and executed a C program.
We now have something concrete to investigate—source code, an executable, and a running program whose behavior we can observe.
4. Change the Program
Modify the message:
printf("What lies beneath the abstraction?\n");
Save the file, compile it again:
clang hello.c -o hello
and run it:
./hello
The important workflow is:
flowchart LR
A[Edit Source] --> B[Compile]
B --> C[Run]
C --> A
You will repeat this cycle constantly while learning C.
5. Break the Program
Before moving on, intentionally introduce an error.
Remove the semicolon from:
return 0;
so that it becomes:
return 0
Compile the program again:
clang hello.c -o hello
This time Clang should reject the program and display a diagnostic. Read the diagnostic before fixing the error. Compiler messages are an important source of information. Do not develop the habit of treating them as noise to be dismissed as quickly as possible.
Restore the semicolon and verify that the program compiles and runs again.
Pro Tip: A program that compiles is not necessarily correct. A syntax error violates the grammatical rules of the language and will generally prevent the compiler from translating the program, while a logic error can produce a perfectly valid program that simply does the wrong thing. Compilation tells us that the compiler was able to translate the source; it does not prove that the program behaves as intended.
Where Do We Go From Here?
You should now be comfortable with the basic edit, compile, and run cycle. At
this point, however, the command that turns hello.c into hello is still
largely a black box. We know that Clang accepts C source code and produces
something the operating system can execute, but we have not yet examined what
happens in between.
That gives us the next question:
How does C source code become an executable program?
The next section will begin to elucidate that mechanism.
Next: The C Compiler
Exercises
-
Change the program so that it prints your name.
-
Change the program so that it prints at least three separate lines.
-
Change the name of the executable, compile and run it.
-
Introduce at least two different syntax errors. Compile the program after each change and read the diagnostic before fixing it.
-
Create a second source file named
goodbye.cthat prints a different message. Compile it into an executable namedgoodbyeand run it.
-
The use of “Hello, World!” as an introductory programming example is generally traced to Brian Kernighan’s work at Bell Labs. An early version appeared in his tutorial for the B programming language and was later used in his 1974 Programming in C: A Tutorial. The example became especially well known after appearing in Kernighan and Ritchie’s 1978 book The C Programming Language, helping establish “Hello, World!” as a programming tradition. ↩
-
Strictly speaking, the C standard specifies program behavior through an abstract machine rather than requiring a particular implementation strategy. This primer uses the native-compilation model provided by Clang on Linux, which is overwhelmingly how C is implemented and used in practice. ↩
-
The original ANSI C standard defined only 32 keywords. Modern C has grown, but the language remains comparatively small: C17 defines 44 keywords and C23 defines 54. For comparison, C++23 defines 92 keywords. Keyword count is an imperfect measure of language complexity, but it helps illustrate the relatively small syntactic core of C. ↩
-
Other C compilers and debuggers are available. Clang and LLDB are used in this primer primarily because they are the author’s preferred tools and because they integrate cleanly within the LLVM ecosystem. Clang also provides strong diagnostics and broad target support, while LLDB reuses LLVM and Clang infrastructure for tasks such as expression parsing and disassembly. ↩
-
Exact compiler and tool versions become important when building and running the algorithms examples. The expected development environment and version requirements are documented in the Source Code section. ↩
-
Standard output, commonly abbreviated
stdout, is the default output stream provided to a process by its execution environment. When a program is run from a terminal,stdoutis usually connected to that terminal, but it can be redirected to a file, pipe, or another destination without changing the program itself. ↩ -
Returning
0frommainreports successful program termination to the execution environment. On Unix-like systems, this becomes the process’s exit status. Nonzero values are commonly used to indicate failure or some other exceptional result, but the specific meaning of a given nonzero value is defined by the program or surrounding convention rather than by C itself. ↩ -
Build tools such as Make are commonly used to keep track of compilation units, dependencies, compiler options, and the commands required to build a larger program. We will introduce build tooling later, after the underlying compilation process is clear. ↩