GNU C++ Compiler in Linux: Complete Guide to GCC Options and Usage

GNU C++ Compiler in Linux and explore gcc options

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.

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

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:

FlagPurpose
-o <name>Set output file name
-cCompile/assemble only, don’t link (produces .o)
-WallEnable common warnings
-WextraEnable additional warnings beyond -Wall
-WerrorTreat all warnings as errors
-gInclude debug symbols for GDB
-std=c++17 / c++20 / c++23Select 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, -OfastOptimization levels
-staticStatically link all libraries
-sharedBuild a shared library
-fPICGenerate position-independent code (needed for shared libs)
-pthreadEnable POSIX threads support
-DNAME=valueDefine a preprocessor macro from the command line
-MMDGenerate 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:

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:

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

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

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.

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.

References

Exit mobile version