Your First C/C++ Program: From the main Function to Compilation and Execution
The goal of writing your first program is not merely to print a sentence to the console. The true objective is to clearly observe: how source files are compiled into executables, where the entry point resides, how the return value is handed back to the operating system, and how standard library outputs actually work. Only by understanding this process thoroughly can you avoid confusing compilation errors, linker errors, and runtime errors later on.
Your First C Program
Create a file named hello.c:
#include <stdio.h>
int main(void) {
printf("Hello, C\n");
return 0;
}
Compile and run:
clang -std=c23 -Wall -Wextra -Wpedantic hello.c -o hello-c
./hello-c
Output:
Hello, C
The -o hello-c flag specifies the output filename.
If omitted, many Unix-like compilers will output an executable named a.out by default.
Your First C++ Program
Create a file named hello.cpp:
#include <iostream>
int main() {
std::cout << "Hello, C++\n";
return 0;
}
Compile and run:
clang++ -std=c++23 -Wall -Wextra -Wpedantic hello.cpp -o hello-cpp
./hello-cpp
Output:
Hello, C++
C++ programs are compiled using clang++ or g++.
The reason is that C++ programs must be linked against the C++ standard library, and standard C compiler drivers usually do not do this automatically.
main is the User-Level Entry Point
main is the standard entry point into your user code.
However, when the operating system loads your program, it actually executes runtime startup code first.
This startup code prepares arguments, initializes the runtime environment and global state, and then calls main.
OS Loads Program
-> C/C++ runtime startup
-> Initialize global state
-> Call main
-> Receive return value
-> Execute exit cleanup
Therefore, main is not the absolute first machine instruction of the process.
But it is the primary entry point you, as a programmer, need to care about.
The Significance of return 0
main returns an integer status code.
By convention, 0 indicates success, and any non-zero value indicates failure.
The shell environment can read this status code.
./hello-c
echo $?
If the output is 0, the program exited successfully as agreed.
In automated scripts and Continuous Integration (CI) pipelines, this exit code dictates whether the workflow continues or fails.
Thus, the return value is not merely decorative.
#include Does Not Import Libraries
Directives like #include <stdio.h> and #include <iostream> only import declarations.
They inform the compiler about the interface (signature) of printf or std::cout.
The actual linking of library implementations occurs in a subsequent linking phase.
Header file: Tells the compiler the names and types.
Library file: Provides the implementation for those names.
Linker: Connects the call site to the implementation.
When starting out, remember this rule of thumb: Missing a header usually results in a compilation error. Missing a library usually results in a linker error.
Compilation and Linking Can Be Separated
A single-file compilation command performs both compilation and linking in one go. However, you can decouple them to see the process.
clang -std=c23 -Wall -Wextra -c hello.c -o hello.o
clang hello.o -o hello-c
The first command generates an object file hello.o.
The second command links the object file into an executable.
hello.c -> hello.o -> hello-c
Source Object Executable
This decomposition is critical.
A multi-file engineering project is essentially a massive collection of .o files ultimately linked together.
Common Compilation Errors
Missing a semicolon:
int x = 1
The compiler might report:
error: expected ';' after expression
Using an undeclared name:
cout << "hello\n";
If you omitted std::cout or a using declaration, the compiler complains that the name does not exist:
error: use of undeclared identifier 'cout'
Always read error messages starting from the top. Subsequent errors are often a cascading reaction triggered by the first one.
Common Linker Errors
Declaring a function without providing its definition:
int add(int a, int b);
int main(void) {
return add(1, 2);
}
This will compile successfully.
However, it will fail during linking because the implementation of add cannot be found.
undefined reference to `add'
This signifies that the compiler understood the function interface, but the linker could not locate the function body.
Common Runtime Errors
Runtime errors occur after the program has successfully been built. For example, an out-of-bounds array access:
int a[3] = {1, 2, 3};
printf("%d\n", a[10]);
This might print a garbage number, it might crash, or it might silently proceed without obvious issues. C/C++ does not guarantee an immediate, loud failure upon every boundary violation. Later chapters will introduce sanitizers to expose these issues as early as possible.
How to Write Comments
C/C++ supports two types of comments:
// Single-line comment
/*
Multi-line comment
*/
Comments should not parrot the code. Good comments explain intent, constraints, and risks.
// `length` has been bounds-checked upstream; performing a hot-path copy here.
copy_bytes(dst, src, length);
This kind of comment is infinitely more valuable than "Calls the copy_bytes function."
Formatting is Not Trivial
Consistent formatting reduces review noise. As a beginner, adhere to simple rules:
- One statement per line.
- Always use full
{}braces, even for single-line blocks. - Keep indentation consistent.
- Use names that convey meaning.
- Avoid stuffing too much logic into a single line.
if (count > 0) {
average = sum / count;
}
This is much easier to audit for error paths than compressing it into a single line.
Minimal Debugging Techniques
The simplest form of debugging is printing critical state.
printf("count=%d\n", count);
In C++:
std::cout << "count=" << count << '\n';
Print debugging is suitable for beginners, but do not let production code rely solely on scattered print statements. Subsequent engineering chapters will cover logging, assertions, sanitizers, and debuggers.
Assertions for Internal Invariants
#include <assert.h>
int divide(int a, int b) {
assert(b != 0);
return a / b;
}
Assertions are used during development to catch conditions that "should never happen by design." They are not a substitute for user input validation. Release builds often disable assertions, so you must never rely on them for authorization or security checks.
Engineering Risks
Even your first program carries engineering risks:
- Using the wrong compiler driver, causing C++ standard library linking to fail.
- Ignoring warnings, thereby carrying latent bugs into subsequent chapters.
- Conflating compilation errors, linker errors, and runtime errors.
- Using platform-specific features to write non-portable code.
- Failing to document compilation commands, making issues impossible to reproduce.
Cultivating correct habits during the small-program phase ensures that large-scale engineering does not spiral out of control.
Summary
The true lesson of your first program is not how to output text, but understanding the fundamental execution pipeline of C/C++.
Source files are first compiled into object files.
Object files are then linked into executables.
The program enters your user code via main.
The return value is handed off to the operating system.
Grasping this pipeline is the gateway to mastering all syntax and underlying mechanisms.