Every C++ student remembers their first segmentation fault. The program compiles clean, you hit run, and then the terminal prints “Segmentation fault (core dumped)” with zero hints about what went wrong. Welcome to C++. Unlike Java or Python, C++ trusts you to handle memory, pointers, and low-level details yourself, which means the errors hit harder and the messages are less friendly. C++ errors break down into four buckets: compile errors, linker errors, runtime errors, and memory errors. Each bucket has its own warning signs and its own fix patterns.
The trick to surviving C++ is learning which bucket your error lives in. Once you can name the bucket, the fix usually takes minutes. Without that skill, you can lose hours staring at “undefined reference to…” or watching your loop crash for no clear reason. Below you will find the 8 errors that trip up students the most, each with a real broken example and the steps to fix it.
Got a deadline closing in and a bug you cannot crack? Our team offers hands-on C++ assignment support where a real coder sits with your code until the crash is gone.
A Quick Map of Where C++ Errors Hide
Before jumping into specific errors, it helps to know the journey your code takes from text file to running program. C++ has four stages, and an error can appear at any one of them. Understanding the stages tells you where to look.
Stage 1: Compilation. The compiler (like g++) reads each .cpp file and converts it to an object file (.o). If your syntax is broken, your headers are missing, or your types do not match, compilation fails. You get a friendly error with a line number.
Stage 2: Linking. The linker stitches all object files together with the libraries you used, creating one executable. If a function was promised but never defined, or a library is missing, linking fails. The error is less friendly. No line numbers, just symbol names.
Stage 3: Loading. The OS pulls your executable into memory and starts running it. This stage rarely throws errors for students, but missing shared libraries can stop a program before it starts.
Stage 4: Execution. Your program is running. If your logic dereferences a bad pointer or runs off the end of an array, the OS or runtime kills the program. These are the trickiest errors because the compiler had no idea anything was wrong.
Here is the cheat sheet:
| Where the error happens | What it looks like | Difficulty |
|---|---|---|
| Compilation | error: expected ';' before '}' with line number | Easy |
| Linking | undefined reference to 'someFunction' | Medium |
| Loading | error while loading shared libraries: libfoo.so | Easy once spotted |
| Execution | Segmentation fault (core dumped) or silent crash | Hard |
| Anywhere | Wrong output, but program runs (logic error) | Hardest |
Keep this stage map in your head. When something breaks, the first question is always: which stage did it break at? That tells you whether to look at your syntax, your build command, your runtime environment, or your logic.
Segmentation Fault
The segmentation fault is the most famous C++ runtime crash, and the most feared by students. It happens when your program touches a piece of memory it does not own. The OS notices, says “absolutely not,” and terminates the program with a SIGSEGV signal.
Here is the simplest way to trigger one:
#include
using namespace std;
int main() {
int* ptr = nullptr;
*ptr = 42; // BOOM
return 0;
}
We told the compiler that ptr is a pointer to an integer, but we never gave it a real address. When we try to write 42 to “wherever ptr points to,” we are writing to address zero, which belongs to the OS.
Why segfaults are sneaky
There is no compiler warning. No syntax issue. The crash happens silently at runtime, and the default error message tells you almost nothing about which line broke.
Segfaults usually trace back to one of these five sources:
- Null pointer dereference. The pointer is
nullptrand you read or write through it. - Uninitialized pointer. The pointer was declared but never assigned, so it points to random memory.
- Array out of bounds. You accessed
arr[10]on an array of size 5. - Use after free. You called
deleteon a pointer, then used it again. - Stack overflow. Infinite recursion fills the stack and everything spills.
How to actually find the crash line
Add the debug flag at compile time:
g++ -g program.cpp -o program
gdb ./program
(gdb) run
(gdb) bt
The -g flag bakes debug info into the executable. When the segfault hits inside GDB, the bt (backtrace) command shows the exact line in your source code. This single trick will save you more hours than any other debugging tip in this guide.
The repair toolbox
For null and uninitialized pointers, initialize at declaration:
int* ptr = nullptr;
// later...
if (ptr != nullptr) {
*ptr = 42;
}
For arrays, never trust the index. Validate before access:
int arr[5];
int index = userInput; // who knows what this is
if (index >= 0 && index < 5) {
arr[index] = 10;
}
For dynamic memory, kill the pointer right after delete:
int* ptr = new int(10);
delete ptr;
ptr = nullptr; // now it cannot bite you
The big mindset shift is to treat every pointer as guilty until proven safe. A pointer should always be in one of three states: pointing to valid memory, set to nullptr, or being assigned in the very next line. Anything else is a future segfault waiting to happen.
Undefined Reference and Linker Errors
The linker error is the one that confuses students the most because it does not point to a line in your code. Instead, you get something like:
/usr/bin/ld: main.o: in function `main':
main.cpp:(.text+0x1a): undefined reference to `helper()'
collect2: error: ld returned 1 exit status
This translates to: “I found a place in your code that calls helper(), but I cannot find where helper() is actually written.”
What is actually happening
Your compiler does its job per file. It sees the function declaration in your header, trusts that the definition exists somewhere, and produces an object file with a placeholder. The linker then tries to fill in that placeholder by searching all the object files and libraries you provided. If it cannot find the definition, the placeholder stays empty and you get an undefined reference.
The most common causes (ranked by frequency)
Forgot to compile a file. This is the number one cause. You wrote helper.cpp but compiled with g++ main.cpp only.
Fix:
g++ main.cpp helper.cpp -o program
Function declared but never defined. You wrote the prototype in the header and forgot to write the body.
// helper.h
void helper(); // declared
// helper.cpp
// you never wrote void helper() { ... }
Fix: write the function body in the .cpp file.
Wrong namespace. Declaration and definition are in different namespaces.
// header.h
namespace utils {
void greet();
}
// source.cpp
void greet() { // ← wrong, this is in global scope
cout << "Hello";
}
Fix: wrap the definition in the matching namespace, or use utils::greet.
Capital M in Main(). Old tutorials sometimes show void Main() or Main(). C++ requires lowercase int main(). The linker looks for the entry point named main and cannot find it.
Missing library link. You used something from an external library but forgot the linker flag. Threading needs -pthread. Math functions sometimes need -lm. Graphics libraries need their own flags.
g++ program.cpp -o program -pthread
The fastest way to diagnose
Read the missing symbol name carefully. The linker tells you exactly what it cannot find. Search your codebase for that name. If you find a declaration but no definition, that is your fix. If you cannot find either, you forgot to include the file. If you find both, check namespaces and function signatures (a small typo in the parameter list creates a “different” function).
Linker errors look intimidating but they are mechanical, not mysterious. The linker is just reporting honest accounting: “you promised me this function, where is it?”
Double Free or Corruption
This crash usually shows up the first time a student tries to manage dynamic memory. The error message looks something like:
*** Error in `./program': double free or corruption (fasttop): 0x0000000000a04010 ***
Aborted (core dumped)
It happens when you tell the memory allocator to release the same block twice, or when you corrupt the allocator’s bookkeeping by writing outside an allocated block.
Three ways students trigger it
Deleting twice in a row:
int* ptr = new int(10);
delete ptr;
delete ptr; // crash
Deleting stack memory by mistake:
int x = 5;
int* ptr = &x;
delete ptr; // crash, x lives on the stack, not the heap
You can only delete memory that came from new. Stack variables clean themselves up when their scope ends.
Writing past an array boundary:
int* arr = new int[3];
arr[5] = 100; // corruption, may crash now or later
delete[] arr;
The write at arr[5] silently overwrites memory that belongs to the allocator. The crash might not happen at the bad write; it can happen later when the program tries to free unrelated memory.
The fix patterns
Pattern 1: Set to nullptr after delete. Calling delete on a nullptr is harmless. Calling delete on an already-freed pointer is a crash. So after every delete, nullify.
int* ptr = new int(10);
delete ptr;
ptr = nullptr;
delete ptr; // safe now, does nothing
Pattern 2: Match the allocation method to the deallocation method. This is the rule that catches many students off guard:
int* a = new int; // pair with delete
delete a;
int* b = new int[10]; // pair with delete[]
delete[] b;
int* c = (int*)malloc(sizeof(int)); // pair with free
free(c);
Mixing them is undefined behavior. new[] with delete is a silent corruption bomb.
Pattern 3: Use smart pointers and skip manual delete entirely.
#include
unique_ptr ptr = make_unique(10);
// no delete needed, cleanup happens when ptr goes out of scope
Smart pointers were added in C++11 specifically to kill this class of bug. In modern student code, you should reach for unique_ptr first and only use raw new and delete when an assignment explicitly requires it.
The general principle is single ownership. Every chunk of dynamic memory should have exactly one owner who is responsible for cleaning it up. If two parts of your code both think they own the same memory, double free is just a matter of time.
Memory Leaks in C++
A memory leak is the opposite of a double free. Instead of releasing memory twice, you never release it at all. The program reserves a chunk of memory with new, uses it, and then loses track of the pointer. The memory stays reserved until the program exits.
Small leaks in a homework assignment are invisible. Big leaks in a long-running program (a game, a server, a desktop app) cause the process to slowly swell in RAM usage until the system becomes unstable.
Here is a leak in three lines:
void leak() {
int* data = new int[1000];
return; // 1000 ints just got abandoned
}
Every call to leak() reserves 4000 bytes that the program will never get back.
Quick fixes you can apply today:
- Every
newneeds a matchingdeletesomewhere in the code - Wrap dynamic memory in
unique_ptrorshared_ptrso cleanup is automatic - Use STL containers like
vectorandstringthat manage their own internal memory - Compile with AddressSanitizer (
-fsanitize=address) during development to catch leaks early - Run final tests through Valgrind for a full memory audit
Because memory leaks are such a deep topic (with their own detection tools, ownership patterns, and language features), we have a dedicated guide that covers everything in detail, including step-by-step Valgrind usage and the difference between explicit and implicit leaks. You can read the full breakdown in our Memory Leaks in C++ article.
The one-line rule is: if you typed new, you owe a delete. Better still, do not type new at all when a vector or unique_ptr can do the job.
Stack Overflow in C++
The stack is a small slice of memory that holds your function calls, local variables, and return addresses. It is fast, automatic, and tiny, usually about 1 MB on most systems. A stack overflow happens when you fill it up.
The classic trigger is recursion without a base case. If recursion itself still feels like a foreign concept, take a quick detour through our beginner-friendly guide on how recursion works step by step before reading on, since the rest of this section assumes you know the basics.
void recurse() {
recurse();
}
int main() {
recurse(); // stack fills, program crashes
}
Each call to recurse() adds a new stack frame. After a few hundred thousand calls, the stack is full and the OS kills the program.
Two other common triggers
Huge local arrays:
void bad() {
int massive[500000]; // tries to use 2 MB on a 1 MB stack
}
The function never even gets to run. The frame allocation itself blows the stack.
Deep recursion with a real base case:
int sum(int n) {
if (n == 0) return 0;
return n + sum(n - 1);
}
int main() {
sum(1000000); // probably overflows even though the base case is valid
}
The recursion is logically correct but the depth (one million calls) exceeds the stack capacity.
The fixes
For runaway recursion, write the base case first. Before you write the recursive call, type the stopping condition. Make it the first line of the function body so you cannot forget it.
int factorial(int n) {
if (n <= 1) return 1; // base case first
return n * factorial(n - 1);
}
For huge data, move it to the heap. Stack is for small, short-lived data. Anything more than a few thousand elements belongs on the heap.
// instead of: int big[500000];
vector big(500000);
// or: int* big = new int[500000]; delete[] big;
For deep recursion, convert to iteration. Loops do not use the call stack the same way, so a million iterations is fine.
int sum(int n) {
int total = 0;
for (int i = 1; i <= n; i++) total += i;
return total;
}
Stack overflow is one of the few C++ errors where the fix is often a complete redesign, not a small patch. If your recursion is naturally very deep, iteration is almost always the better approach.
Compilation Errors Students Hit Most
Compile errors are the friendliest type because the compiler tells you the file, the line, and what it expected. Most beginner C++ pain comes from this group. Here are the patterns that show up in graded assignments every single semester.
The missing semicolon. C++ ends statements with ;. Forget one and the compiler points right at the next line, which can be misleading.
int x = 5
cout << x; // error reported here, but the real bug is the line above
The undeclared identifier. You used something the compiler does not know about, usually because you forgot a header or using namespace std;.
cout << "hi"; // error: 'cout' was not declared
Fix:
#include
using namespace std;
The missing header. You used a class from the STL without including its header.
vector v; // error: 'vector' was not declared
Fix: #include <vector>. Common ones to memorize:
<iostream>forcin,cout<vector>forvector<string>forstring<algorithm>forsort,find<cmath>forsqrt,pow<fstream>for file I/O
The type mismatch. You tried to assign a value of the wrong type.
int name = "Alex"; // cannot convert const char* to int
Fix: use the right type.
string name = "Alex";
The mismatched braces. Easy to do in nested blocks. Modern editors highlight matching pairs, so trust them.
if (x > 0) {
cout << "positive";
// missing }
return 0;
Assignment instead of comparison. Easy typo with serious consequences.
if (x = 5) { } // assigns 5 to x, condition always true
if (x == 5) { } // actual comparison
Modern compilers warn you about this if you enable -Wall. Always compile with warnings turned on:
g++ -Wall -Wextra program.cpp -o program
The cascade trap. When you see 20 errors, fix only the first one and recompile. A single missing semicolon at the top can create fake errors for the next 50 lines. One real fix often clears the whole list.
The general rule for compile errors is to read the message slowly, check the line above the reported line (since errors sometimes propagate forward), and trust that the compiler is genuinely trying to help.
Pointer-Related Runtime Errors
Pointers are where C++ separates itself from safer languages, and where most student bugs live. If the basics still feel shaky, our deep dive on how pointers actually work in C++ walks through every pointer type with examples. Beyond null dereferences (covered under segfault) and double frees (covered above), there are two more pointer states that cause silent havoc.”
Dangling pointers
A dangling pointer points to memory that used to be valid but is not anymore. Reading or writing through it is undefined behavior, which often means random garbage data or a delayed crash.
int* makePtr() {
int local = 42;
return &local; // returning address of a local variable
}
int main() {
int* ptr = makePtr();
cout << *ptr; // dangling, local is gone
}
When makePtr() returns, the local variable disappears. The returned address now points to stack memory that belongs to whoever runs next.
Fix: never return a pointer to a local variable. Return by value, or allocate on the heap with explicit ownership rules.
int* makePtr() {
return new int(42); // caller owns this, must delete
}
Even better, return a smart pointer:
unique_ptr makePtr() {
return make_unique(42);
}
Wild pointers
A wild pointer was declared but never initialized. It points to whatever junk was sitting in that memory location.
int* ptr;
*ptr = 10; // writing to random memory
This might crash. It might silently corrupt another variable. It might appear to work for hours and then break in production. Wild pointers are the worst kind of bug.
Fix: initialize every pointer at declaration. Either to a valid address or to nullptr.
int* ptr = nullptr;
// later...
ptr = new int(10);
Make this a rule with no exceptions. Treat an uninitialized pointer the same way you would treat an uninitialized parachute.
A pointer hygiene checklist
- Declared a pointer? Initialize it now, in the same line.
- Did
new? Plan the matchingdeletenow, before you forget. - Did
delete? Set tonullptrimmediately. - Returning from a function? Never return the address of a local variable.
- Sharing memory between objects? Use
shared_ptr, not raw pointers.
Following this checklist eliminates most pointer-related crashes that show up in homework.
Your Debugging Toolkit
When a C++ program breaks, you have four main tools to figure out why. Knowing which tool to reach for first saves enormous amounts of time.
Tool 1: Compiler warnings
This is the cheapest, fastest tool, and most students never use it. Always compile with:
g++ -Wall -Wextra -Wpedantic program.cpp -o program
These flags turn on warnings for things the compiler suspects are bugs even though they are technically legal. Things like comparing signed and unsigned numbers, using uninitialized variables, or having unused code. Treat warnings as errors and fix them before running anything.
Tool 2: GDB for runtime crashes
GDB is the standard debugger on Linux and Mac. It is essential for any segfault that does not have an obvious cause.
g++ -g program.cpp -o program
gdb ./program
(gdb) run
(gdb) bt # show where it crashed
(gdb) print var # see what a variable holds
(gdb) list # show source code at the current line
(gdb) break 42 # set a breakpoint at line 42
(gdb) step # run one line at a time
The -g flag is mandatory. Without it, GDB cannot map the crash back to your source code.
Tool 3: Valgrind for memory issues
If you suspect a memory leak, an invalid read, or a use-after-free, Valgrind is the gold standard:
g++ -g program.cpp -o program
valgrind --leak-check=full ./program
Valgrind runs your program in a virtual machine that watches every memory operation. It reports leaks, invalid accesses, and uninitialized reads with full line numbers. The downside is that programs run about 20 times slower under Valgrind, so use it for testing, not for normal development.
Tool 4: AddressSanitizer for fast memory checks
AddressSanitizer (ASan) is built into modern GCC and Clang. It catches most of what Valgrind catches but with much less slowdown:
g++ -fsanitize=address -g program.cpp -o program
./program
When the program does something wrong with memory, ASan prints a detailed report telling you the exact line and what kind of error it was. Many professional teams run ASan on all their tests by default.
When to reach for which tool
| Problem | Tool |
|---|---|
| Compile-time mystery | Compiler warnings (-Wall -Wextra) |
| Segfault with no obvious cause | GDB with -g |
| Memory leak suspected | Valgrind or ASan |
| Invalid memory access | ASan during development, Valgrind for final check |
| Wrong output, no crash | Print statements or GDB step-through |
Master these four tools and most C++ debugging becomes mechanical instead of mysterious.
How Pro C++ Developers Avoid These Errors
Catching errors is good. Not writing them in the first place is better. The habits below are what separates a student who fights C++ from a developer who works with it.
Initialize everything. Every variable, every pointer, every member. The cost is one line of code. The benefit is zero undefined behavior.
Prefer the STL over raw arrays and raw pointers. vector<int> instead of int* with new[]. string instead of char*. The STL handles memory, bounds, and resizing for you, and decades of brilliant engineers have already debugged it.
Adopt smart pointers as your default for dynamic memory. Use unique_ptr for single ownership, shared_ptr for shared, weak_ptr to break cycles. Reach for raw new and delete only when an assignment explicitly requires manual memory management.
Compile with strict warnings on day one. -Wall -Wextra -Wpedantic should be in every project from the first hello world. Fix every warning before moving on.
Use const whenever possible. Variables that should not change, function parameters that should not be modified, methods that do not modify the object. The compiler will catch accidental mutations.
Write small, focused functions. A function should do one thing and fit on one screen. Bigger functions hide more bugs and are harder to debug.
Test the edges. Empty inputs. Zero values. Negative numbers. Very large arrays. If your code only works on the example from the textbook, it does not really work.
Read the compiler output, do not skip past it. Warnings and errors contain almost all the information you need. The compiler is on your team.”
“Wrap risky code in try-catch blocks. Anything that touches files, user input, or network calls can fail at runtime. A simple try-catch keeps your program alive instead of crashing on the user. If you have not used exceptions before, our guide on exception handling in C++ covers the syntax, the standard exception types, and when to use each one.
Build these habits early and most of the errors in this guide will simply never appear in your code. The best way to lock them in is to keep building, so once you feel comfortable, pick something from our hands-on C++ project ideas and put your new debugging skills to work on a real codebase.
Frequently Asked Questions
1. Why does my C++ program compile fine but crash when I run it?
A clean compile only confirms your syntax and types are valid. It does not check your logic, your pointers, or your memory use. Runtime crashes usually come from null pointers, out-of-bounds array access, division by zero, or stack overflow. Run your program through GDB with the -g flag to find the exact line that crashes.
2. What is the difference between delete and delete[] in C++?
delete is for memory allocated with new (single objects). delete[] is for memory allocated with new[] (arrays). Mixing them is undefined behavior and often causes corruption that crashes later. Always match the form.
3. How do I know if my program has a memory leak?
You usually cannot tell from normal output. Run your program through Valgrind with valgrind --leak-check=full ./program or compile with -fsanitize=address. Both tools will list every byte of leaked memory along with the line where it was allocated.
4. What is the difference between nullptr and NULL in C++?
NULL is an old C-style macro that usually expands to 0. nullptr is a real keyword introduced in C++11 with a proper pointer type. Always use nullptr in modern code. It prevents accidental matches with integer overloads and is more readable.
5. Why does my program say “undefined reference to main”?
The linker cannot find your main function. The most common reason is that you wrote Main() or MAIN() with wrong capitalization. C++ requires lowercase int main(). Other causes include forgetting to compile the file that contains main, or putting main inside a namespace by accident.
6. Is it ever safe to use delete on the same pointer twice?
Only if you set it to nullptr between the two deletes. Calling delete on nullptr is a no-op and is completely safe. Calling delete on a previously deleted pointer is undefined behavior. The simple rule: always nullify after delete.
7. What is RAII and why does it matter?
RAII stands for Resource Acquisition Is Initialization. It is the C++ pattern where resources (memory, file handles, locks) are owned by objects, and the objects clean up automatically when they go out of scope. Smart pointers, vector, and string all use RAII. Following this pattern eliminates entire categories of bugs.
8. Why should I use vector instead of arrays?
vector manages its own memory, knows its own size, can grow and shrink, and works with all STL algorithms. Raw arrays have a fixed size, no bounds checking, and require manual cleanup if allocated dynamically. There are very few cases in modern C++ where a raw array is the better choice.
9. How do I read a long C++ compiler error message?
Start from the first error and ignore the rest until you fix it. Recompile. Many errors are caused by a single earlier mistake (especially missing semicolons or unclosed braces) that cascades into dozens of fake errors below. Template errors look terrifying but the actual problem is usually in the first or last few lines of the message.
10. Should beginners learn C++ memory management or skip straight to smart pointers?
Learn both. Understand how new and delete work and why they are dangerous. Then adopt smart pointers as your daily tool. Skipping the fundamentals leaves a gap when you read older code or debug crashes. But sticking to raw pointers in your own code is unnecessarily risky.
