I spent way too long as a beginner treating g++ myfile.cpp -o myfile as some kind of magic incantation. It worked, so I never bothered learning what else GCC could actually do. It wasn’t until I started debugging a nasty segmentation fault and someone told me to “just compile with -g and run it through gdb” that I realized how much power was sitting in flags I’d never touched. This guide is the one I wish I’d had — a genuinely complete walkthrough of GCC/G++ on Linux, from installation to the compiler internals to the flags that actually matter in real projects.
What Is GCC, and What’s the Difference Between gcc and g++?
GCC stands for the GNU Compiler Collection — it’s not just one compiler, but a suite of compilers for C, C++, Objective-C, Fortran, Ada, Go, and more, all sharing a common backend.
gcc– historically the C compiler front-end. It can compile.cppfiles, but it won’t automatically link the C++ standard library.g++– the C++ front-end. It treats input files as C++ by default and automatically links againstlibstdc++.
Here’s the practical difference:
gcc hello.cpp -o hello # Compiles fine, but linker error: undefined reference to std::cout
g++ hello.cpp -o hello # Compiles and links correctly
If you ever see “undefined reference to std::__cxx11::basic_string...” or similar errors, it’s almost always because you compiled C++ code with gcc instead of g++.
Installing GCC on Different Linux Distributions
Debian/Ubuntu:
sudo apt update
sudo apt install build-essential
Fedora/RHEL/CentOS:
sudo dnf groupinstall "Development Tools"
Arch Linux:
sudo pacman -S base-devel
Confirm your installation and check the version:
g++ --version
Example output:
g++ (Ubuntu 13.2.0-4ubuntu3) 13.2.0
Copyright (C) 2023 Free Software Foundation, Inc.
Knowing your GCC version matters because C++ standard support (especially C++20/23 features) depends heavily on it. GCC 11+ has strong C++20 support; GCC 13+ adds more C++23 features.
The Anatomy of a Compilation Command
Let’s break down a typical command piece by piece:
g++ -std=c++17 -Wall -Wextra -g -O2 main.cpp utils.cpp -o app
-std=c++17— sets the language standard-Wall -Wextra— enables extra compiler warnings-g— includes debug symbols-O2— enables optimization level 2main.cpp utils.cpp— source files-o app— names the output binaryapp
The Four Stages of Compilation (With Real Commands)
GCC doesn’t just magically turn source into an executable — it moves through four distinct stages, and you can stop it at each one to inspect what’s happening.
Consider this simple program:
// calc.cpp
#include <iostream>
int square(int x) {
return x * x;
}
int main() {
int n = 5;
std::cout << "Square of " << n << " is " << square(n) << std::endl;
return 0;
}
Stage 1: Preprocessing (-E)
g++ -E calc.cpp -o calc.i
This expands all #include and #define directives. Open calc.i and you’ll find thousands of lines — the entire contents of <iostream> and its dependencies dumped inline, followed by your actual code at the bottom.
Stage 2: Compilation to Assembly (-S)
g++ -S calc.i -o calc.s
This produces human-readable (if you know assembly) machine instructions. You’ll see something like:
square(int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov eax, DWORD PTR [rbp-4]
imul eax, eax
pop rbp
ret
Stage 3: Assembling to Object Code (-c)
g++ -c calc.s -o calc.o
This turns assembly into binary machine code — but it’s not yet a runnable program, since it doesn’t have startup code or resolved library references.
Stage 4: Linking
g++ calc.o -o calc
./calc
Output:
Square of 5 is 25
The linker’s job here is to resolve std::cout‘s implementation from libstdc++, add C runtime startup code, and produce a final executable. Understanding this stage explains why “undefined reference” errors are linker errors, not compiler errors — the compiler already succeeded; it’s the final assembly step that’s missing a piece.
Essential GCC/G++ Compiler Options
Here’s a practical reference table of flags I use constantly:
| Flag | Purpose |
|---|---|
-o <name> | Set output file name |
-c | Compile/assemble only, don’t link (produces .o) |
-Wall | Enable common warnings |
-Wextra | Enable additional warnings beyond -Wall |
-Werror | Treat all warnings as errors |
-g | Include debug symbols for GDB |
-std=c++17 / c++20 / c++23 | Select the C++ standard |
-I<path> | Add a directory to search for header files |
-L<path> | Add a directory to search for libraries |
-l<name> | Link against a library (e.g., -lpthread) |
-O0 to -O3, -Ofast | Optimization levels |
-static | Statically link all libraries |
-shared | Build a shared library |
-fPIC | Generate position-independent code (needed for shared libs) |
-pthread | Enable POSIX threads support |
-DNAME=value | Define a preprocessor macro from the command line |
-MMD | Generate dependency files for use in Makefiles |
Example: Compiling With Multiple Source and Header Directories
g++ -std=c++17 -Wall -Iinclude -Llib src/main.cpp src/utils.cpp -lmylib -o app
This tells G++ to look for headers in include/, look for libraries in lib/, and link against libmylib.so or libmylib.a.
Debugging With -g and GDB
Compile with debug symbols enabled:
g++ -g -std=c++17 -O0 calc.cpp -o calc
Note the -O0 here — debugging optimized code is painful because variables can get reordered or eliminated. Always debug with optimizations off.
Then step through it with GDB:
gdb ./calc
Inside the GDB prompt:
(gdb) break square
(gdb) run
(gdb) print x
(gdb) next
(gdb) continue
(gdb) backtrace
backtrace is especially useful after a crash — it shows the exact call chain that led to the segfault, which is often the fastest way to find a null pointer dereference or stack overflow.
Optimization Flags: What They Actually Do
Optimization levels control tradeoffs between compile time, binary size, and runtime speed:
-O0— no optimization (default). Fastest to compile, best for debugging.-O1— basic optimizations with minimal compile-time cost.-O2— the standard choice for production builds; enables most safe optimizations (inlining, loop unrolling, instruction scheduling) without sacrificing precision.-O3— aggressive optimizations, including vectorization; can occasionally increase binary size without meaningful speed gains.-Ofast—-O3plus relaxed adherence to strict IEEE floating-point standards; faster but can introduce subtle numerical differences.
Here’s a simple demonstration of the performance difference:
// sum.cpp
#include <iostream>
#include <chrono>
int main() {
long long sum = 0;
for (long long i = 0; i < 1000000000; i++) {
sum += i;
}
std::cout << sum << std::endl;
return 0;
}
g++ -O0 sum.cpp -o sum_debug
g++ -O2 sum.cpp -o sum_fast
time ./sum_debug
time ./sum_fast
On most machines, the -O2 build runs noticeably faster — often several times quicker — because the compiler can unroll the loop and keep the accumulator in a register instead of repeatedly touching memory.
Static vs. Dynamic Linking: What Happens Internally
When you link a program, GCC resolves every external symbol (like std::cout) against either:
- Static libraries (
.a) — code gets copied directly into your executable at link time. Larger binary, but no runtime dependency on that library being installed. - Shared libraries (
.so) — the executable only stores a reference; the actual code loads at runtime from a shared.sofile, saving disk space and memory across multiple programs using the same library.
g++ main.cpp -static -o app_static # fully static binary
g++ main.cpp -o app_dynamic # dynamically linked (default)
ldd app_dynamic # lists shared library dependencies
If you ever deploy to a machine that lacks the same library versions, -static avoids “library not found” errors entirely, at the cost of a bigger executable.
Using GCC With Makefiles and CMake
A typical Makefile using GCC-specific flags:
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra -O2
LDFLAGS = -lpthread
app: main.o utils.o
$(CXX) $(CXXFLAGS) main.o utils.o -o app $(LDFLAGS)
main.o: main.cpp
$(CXX) $(CXXFLAGS) -c main.cpp
utils.o: utils.cpp
$(CXX) $(CXXFLAGS) -c utils.cpp
clean:
rm -f *.o app
And the equivalent in CMake, which under the hood still invokes G++ with similar flags:
cmake_minimum_required(VERSION 3.15)
project(App)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "-Wall -Wextra -O2")
add_executable(app main.cpp utils.cpp)
target_link_libraries(app pthread)
Common GCC Errors and How to Fix Them
undefined reference to 'someFunction()' The function is declared but never defined, or you forgot to compile/link the source file containing its definition.
fatal error: somefile.h: No such file or directory The compiler can’t find your header. Add its directory with -I<path>.
error: 'std::cout' was not declared in this scope You likely forgot #include <iostream>, or you’re missing using namespace std; / the std:: prefix.
Segmentation fault (core dumped) This is a runtime error, not a compile error — usually a null or dangling pointer dereference, or an out-of-bounds array access. Recompile with -g and run through GDB with backtrace to locate it.
multiple definition of 'someVariable' Usually caused by defining a variable in a header file without inline or static, so it gets duplicated across every translation unit that includes it.
Best Practices When Using GCC
- Always enable
-Wall -Wextraduring development — treat warnings as bugs waiting to happen. - Use
-Werrorin CI pipelines to prevent warnings from silently accumulating. - Keep debug and release build flags separate (
-g -O0vs-O2, no debug symbols). - Pin your
-std=flag explicitly rather than relying on GCC’s default, which changes between versions. - Use
-fsanitize=addressand-fsanitize=undefinedduring testing to catch memory bugs GDB alone might miss:
g++ -fsanitize=address -g main.cpp -o app_asan
./app_asan
This instruments your binary to catch buffer overflows, use-after-free, and similar memory errors immediately, with a readable stack trace pointing to the exact line.
Real-World Usage of GCC
GCC isn’t just a teaching tool — it’s the compiler behind an enormous amount of production software. The Linux kernel itself is built with GCC (with growing Clang support). Countless embedded toolchains (arm-none-eabi-gcc, avr-gcc) are GCC cross-compilers targeting microcontrollers. Distribution package managers like apt and dnf build nearly their entire ecosystem of C/C++ packages using GCC by default, which is part of why it remains one of the most battle-tested compilers in existence.
Interview Questions About GCC
- What’s the difference between
gccandg++? - What are the four stages of compilation in GCC, and which flags isolate each one?
- What’s the difference between
-O2and-O3, and when might-O3actually hurt? - Explain the difference between static and dynamic linking, including their respective file extensions.
- What does
-fPICdo, and why is it required for shared libraries? - How would you use GDB to find the source of a segmentation fault?
Frequently Asked Questions
Do I need to specify -std=c++17 every time? Yes, unless you want to rely on whatever default your GCC version ships with — which changes across versions and can silently break newer syntax or leave you on an older standard than you expect.
Why does my program compile but crash immediately? Compilation errors and runtime errors are different categories entirely. A segfault or crash means the compiler succeeded, but something is wrong at runtime — recompile with -g and inspect it in GDB.
Is -O3 always faster than -O2? Not necessarily. -O3 enables more aggressive optimizations like vectorization, which can help in numeric-heavy code but occasionally bloats binary size or has negligible impact elsewhere. Benchmark both before deciding.
What’s the safest optimization level for production code? -O2 is the industry-standard default — a strong balance of performance and reliability without the edge cases sometimes introduced by -O3 or -Ofast.
Summary and Key Takeaways
GCC is far more than the single command most beginners memorize — it’s a complete, configurable toolchain with real control over every stage of turning source code into a running binary.
g++is the correct front-end for C++ code;gccwill compile but won’t link the C++ standard library automatically.- Compilation happens in four stages: preprocessing, compilation, assembly, and linking — each inspectable with
-E,-S,-c, and no flag respectively. - Warning flags (
-Wall -Wextra) and sanitizers (-fsanitize=address) catch bugs long before they become production incidents. - Optimization flags (
-O0through-O3,-Ofast) trade compile time and debuggability for runtime speed — use-O0 -gfor development and-O2for release builds. - Static and dynamic linking are different tradeoffs between binary portability and size, controlled with
-staticand.solibraries respectively.
Once you’re comfortable with these options, GCC stops being a black box and becomes one of the most transparent, controllable compilers you’ll ever work with.
