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 one comes with a real broken example, the exact message you will see, and the steps to fix it. After that, you get a simple routine for debugging any C++ error and the tools that make it faster.
We compiled and ran the examples in this guide with g++ 13 on Linux, so the error messages you see here are real output, not made-up samples. Clang on a Mac prints similar compile errors, though its linker and crash messages look a little different. Visual Studio words things differently too, but the causes and fixes are the same.
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.
In this guide:
- Where C++ errors happen: the 4 stages
- Segmentation fault
- Undefined reference and linker errors
- Undefined reference to main
- Double free or corruption
- Memory leaks
- Stack overflow
- Compilation errors students hit most
- Dangling and wild pointers
- Uncaught exceptions and exception handling
- How to debug any C++ error step by step
- Your debugging toolkit: GDB, Valgrind, and ASan
- How to avoid these errors
- Frequently asked questions
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. You often get 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 |
| Execution | terminate called after throwing an instance of ... | Medium |
| 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.
Writing plain C instead of C++? Many of these errors look the same, but C has a few of its own. Our guide to errors in C programming covers the C side.
8 Common C++ Errors and How to Fix Each One
Each section below shows the error in real code, the message you will see, what causes it, and the fixes you can use in your assignment right away.
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 operating system catches the bad access and stops the program with a SIGSEGV signal.
Here is the simplest way to trigger one:
#include <iostream>
using namespace std;
int main() {
int* ptr = nullptr;
*ptr = 42; // crashes here
return 0;
}
We told the compiler that ptr is a pointer to an integer, but we never gave it a real address. A null pointer points to nothing your program owns, so writing 42 through it is illegal. On Linux you see Segmentation fault (core dumped), and a Mac terminal shows zsh: segmentation fault. On Windows the program just closes, or Visual Studio reports an "access violation."
Why segfaults are sneaky
There is no compiler error. As far as the compiler can tell, the code is legal C++. The crash happens at runtime, and the default message tells you 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 or very deep recursion uses up the stack. More on this below.
How to actually find the crash line
Add the debug flag at compile time, then run the program inside GDB:
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, plus the chain of function calls that led there. Here is a small program where main() passes a null pointer to another function:
#include <iostream>
using namespace std;
void setScore(int* score) {
*score = 95;
}
int main() {
int* score = nullptr;
setScore(score);
cout << *score << endl;
return 0;
}
And here is what GDB printed when we ran it:
Program received signal SIGSEGV, Segmentation fault.
0x0000555555555179 in setScore (score=0x0) at program.cpp:5
5 *score = 95;
(gdb) bt
#0 0x0000555555555179 in setScore (score=0x0) at program.cpp:5
#1 0x00005555555551a2 in main () at program.cpp:10
Read it from the top. Frame #0 is where the crash happened: line 5, inside setScore(). The score=0x0 part tells you the pointer was null. Frame #1 shows that main() called it from line 10. Now you know exactly where to look. It is the fastest way to find the crash line.
The repair toolbox
For null and uninitialized pointers, initialize at declaration and check before you use it:
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, reset 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 usually does not point to a clear line in your code. Instead, you get something like this:
/usr/bin/ld: /tmp/ccw27vv7.o: in function `main':
main.cpp:(.text+0x9): 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." If you compile with -g, the linker can also name the file and line of the call. On a Mac, the same problem reads Undefined symbols for architecture arm64 (or x86_64).
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
Forgot to compile a file. This one catches students most often. You wrote helper.cpp but compiled with g++ main.cpp only. Fix it by listing every .cpp file in the command:
g++ main.cpp helper.cpp -o program
IDEs have the same problem in a different form. In Code::Blocks, CLion, or Visual Studio, check that every .cpp file is actually part of the project. In VS Code, the default "Run C/C++ File" button compiles only the file you have open, so a multi-file project needs a build command that lists all your files.
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. The declaration and the definition are in different namespaces, so the linker sees two different functions.
// header.h
namespace utils {
void greet();
}
// source.cpp
void greet() { // wrong: this is a different, global greet()
cout << "Hello";
}
The linker reports undefined reference to `utils::greet()'. Fix it by defining the function inside the matching namespace:
// source.cpp
void utils::greet() { // matches the declaration in header.h
cout << "Hello";
}
Missing library link. You used code from a library but did not tell the linker to include it. Outside libraries each need their own flag, like -lsqlite3 for SQLite or -lsfml-graphics for SFML. Two classic cases: std::thread on older Linux systems needs -pthread, and in plain C, math functions like sqrt() need -lm (g++ adds the math library for you in C++).
g++ program.cpp -o program -pthread
Undefined reference to main
This is the linker error most beginners see first. With g++, it looks like this:
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o: in function `_start':
(.text+0x1b): undefined reference to `main'
collect2: error: ld returned 1 exit status
On Windows with MinGW (the compiler behind Code::Blocks, Dev-C++, and many VS Code setups), the same problem usually shows up as undefined reference to `WinMain' or WinMain@16. Same meaning, same fixes.
Every C++ program needs exactly one function named main. It is where your program starts. When the linker builds your executable, it looks for that function. If it cannot find a global function spelled exactly main, you get this error. Here are the causes, in the order we suggest checking them:
- The file is not saved. Editors like VS Code do not always save before you build, so the compiler reads an empty or old file with no
main. We tested it: compiling an empty.cppfile gives this exact error. Save (Ctrl+S or Cmd+S) and build again. - There is no
main()at all. You wrote the functions but forgot the function that calls them. - A typo or a capital letter.
Main(),MAIN(), andmian()are all different names to C++. It must be lowercasemain. - You compiled a file that has no
mainby itself. Runningg++ helper.cpp -o programtries to build a whole program fromhelper.cppalone. mainis inside a namespace or class. The linker only looks for a globalmain. If you wrapped it innamespace app { ... }, move it out.
Here is the "forgot main" version, which shows up all the time in first-week assignments. The function is fine, but nothing ever calls it:
#include <iostream>
using namespace std;
void divide() {
int a = 20;
int b = 4;
cout << "The result is: " << a / b << endl;
}
// There is no main(), so nothing ever calls divide()
The fix is to add a main() that calls your function:
#include <iostream>
using namespace std;
void divide() {
int a = 20;
int b = 4;
cout << "The result is: " << a / b << endl;
}
int main() {
divide();
return 0;
}
Output:
The result is: 5
For the "compiled the wrong file" case, the fix is in your build command:
# wrong: helper.cpp has no main
g++ helper.cpp -o program
# right: build all the files together
g++ main.cpp helper.cpp -o program
# or compile one file without linking (this makes helper.o)
g++ -c helper.cpp
What about void main()? A lot of students expect this one to cause the same error, but it gives a different one. g++ stops at compile time with this message:
voidmain.cpp:4:1: error: '::main' must return 'int'
4 | void main() {
| ^~~~
You still see void main() in old notes because Turbo C++ and Microsoft's compiler let it slide. Standard C++ does not. Use one of these two forms:
// Pick ONE of these. A program can only have one main().
int main() {
// your code
return 0;
}
int main(int argc, char* argv[]) {
// argc = how many command-line arguments, argv = the arguments
return 0;
}
One small thing: if you leave out return 0; at the end of main(), C++ adds it for you. Writing it anyway makes your intent clear.
The fastest way to diagnose any linker error
Read the missing symbol name carefully. The linker tells you exactly what it cannot find. Search your code for that name. If you find a declaration but no definition, write the definition. If you find both, check that the .cpp file holding the definition is in your build command, then check namespaces and function signatures (a small typo in the parameter list creates a "different" function). If the name is not in your code at all, it comes from a library you forgot to link.
Linker errors look intimidating, but they are mechanical, not mysterious. The linker is just keeping honest records: "you promised me this function, where is it?"
Double Free or Corruption
This crash usually shows up the first time a student manages dynamic memory with new and delete. On a current Linux system, the message looks like this:
free(): double free detected in tcache 2
Aborted (core dumped)
Older systems, and a lot of tutorials online, show the older wording:
*** Error in `./program': double free or corruption (fasttop): 0x0000000000a04010 ***
You may also see cousins of this message, like free(): invalid pointer, munmap_chunk(): invalid pointer, or malloc(): corrupted top size. They all come from the same place. The memory allocator noticed that its records no longer make sense. That happens when you release the same block twice, release memory you never allocated, or write outside a block and damage the allocator's bookkeeping.
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. When we ran this, it crashed with munmap_chunk(): invalid pointer. g++ also warns about it if you compile with -Wall: 'void operator delete(void*, std::size_t)' called on unallocated object 'x'. If the difference between stack and heap memory is still fuzzy, our guide to dynamic memory allocation in C explains how heap memory works with malloc() and free(). The same ideas apply to new and delete.
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. Here is the scary part: when we ran this exact snippet, it did not crash at all. It exited normally. In a slightly bigger test, where a loop wrote past the end of the array and the program then called new again, the crash came from that later new with malloc(): corrupted top size. The bug and the crash were on different lines. AddressSanitizer, covered in the toolkit below, catches this bug on the exact line of the bad write.
A real student example
Here is a pattern that shows up in homework a lot. The task was to add two numbers using pointers. The code compiles, and it even prints the right answer before it crashes:
#include <iostream>
#include <cstdlib>
using namespace std;
int main() {
int* first = new int(10);
int* second = new int(20);
int* sum; // never initialized
*sum = *first + *second; // writes to a random address
cout << "Result: " << *sum << endl;
free(first); // wrong: memory came from new
free(second);
free(sum); // sum never came from new at all
return 0;
}
Output:
Result: 30
free(): invalid pointer
Aborted (core dumped)
Two bugs are hiding here. First, sum was never given any memory, so *sum = ... writes to a random address. Second, memory from new must be released with delete, not free(). Compiling with -Wall -Wextra flags both problems before you ever run the program:
student_wrong.cpp:10:10: warning: 'sum' is used uninitialized [-Wuninitialized]
student_wrong.cpp:13:9: warning: 'void free(void*)' called on pointer returned from a mismatched allocation function [-Wmismatched-new-delete]
Here is the fixed version. Every pointer gets real memory, every new gets a matching delete, and each pointer is reset afterward:
#include <iostream>
using namespace std;
int main() {
int* first = new int(10);
int* second = new int(20);
int* sum = new int; // now it points to real memory
*sum = *first + *second;
cout << "Result: " << *sum << endl;
delete first; // new pairs with delete
first = nullptr;
delete second;
second = nullptr;
delete sum;
sum = nullptr;
return 0;
}
To be honest, for two numbers you do not need new at all. Plain int variables do the same job with zero risk. Use pointers when the assignment asks for them, and keep them simple.
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, reset the pointer.
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. The program might crash right away, crash much later, or seem fine on your computer and then fail on your instructor's.
Pattern 3: Use smart pointers and skip manual delete entirely.
#include <memory>
unique_ptr<int> ptr = make_unique<int>(10);
// no delete needed, cleanup happens when ptr goes out of scope
Smart pointers like unique_ptr came with C++11 to stop exactly this kind of bug. (make_unique arrived in C++14, so compile with -std=c++14 or newer if your setup is old. Current g++ uses C++17 by default.) In modern student code, you should reach for unique_ptr first and only use raw new and delete when an assignment 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 usually invisible when you run it. 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 slows down or the program crashes. A leak never causes a compile error, and usually not even a warning. The program compiles, runs, and prints the right output. You only find leaks by looking for them with a tool.
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 about 4,000 bytes (1,000 ints at 4 bytes each) that the program will never get back.
Three common ways students leak memory
1. Leaving a function before the delete. An early return (or an exception) skips the cleanup line.
#include <iostream>
using namespace std;
bool loadScores(int count) {
int* scores = new int[count];
if (count > 100) {
cout << "Too many scores" << endl;
return false; // leak: we leave before delete[]
}
for (int i = 0; i < count; i++) {
scores[i] = 0;
}
delete[] scores;
return true;
}
int main() {
loadScores(500);
return 0;
}
Valgrind reported 2,000 bytes in 1 blocks are definitely lost for this one. That is the 500 ints that never came back.
2. Overwriting the only pointer to a block. Once you lose the address, you cannot free that memory, even if you want to.
int* ptr = new int(5); // first block
ptr = new int(10); // the address of the first block is now lost
delete ptr; // frees only the second block
3. shared_ptr objects that point at each other. Smart pointers prevent most leaks, but not this one. If object A holds a shared_ptr to B and B holds one back to A, each keeps the other alive forever. This one is harder to spot because you never wrote new by hand.
#include <iostream>
#include <memory>
using namespace std;
class B;
class A {
public:
shared_ptr<B> bPtr;
~A() { cout << "A destroyed" << endl; }
};
class B {
public:
shared_ptr<A> aPtr;
~B() { cout << "B destroyed" << endl; }
};
int main() {
shared_ptr<A> a = make_shared<A>();
shared_ptr<B> b = make_shared<B>();
a->bPtr = b;
b->aPtr = a; // A and B now keep each other alive
return 0; // neither destructor runs
}
Nothing prints, because neither object is ever destroyed. The fix is to make one side a weak_ptr, which can see the other object without keeping it alive:
class B {
public:
weak_ptr<A> aPtr; // weak_ptr does not keep A alive
~B() { cout << "B destroyed" << endl; }
};
With that one change, the program prints A destroyed and B destroyed when main() ends.
How to find a leak with Valgrind
Compile with -g so Valgrind can show line numbers, then run your program through it:
g++ -g leak.cpp -o leak
valgrind --leak-check=full ./leak
Here is the program we tested:
#include <iostream>
using namespace std;
void printPrice() {
double* price = new double(19.99);
cout << "Price: " << *price << endl;
// no delete, so this memory is never given back
}
int main() {
printPrice();
return 0;
}
And here is the part of Valgrind's report that matters:
==1154== HEAP SUMMARY:
==1154== in use at exit: 8 bytes in 1 blocks
==1154== total heap usage: 3 allocs, 2 frees, 77,832 bytes allocated
==1154==
==1154== 8 bytes in 1 blocks are definitely lost in loss record 1 of 1
==1154== at 0x4846FA3: operator new(unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==1154== by 0x1091BE: printPrice() (leak.cpp:5)
==1154== by 0x109220: main (leak.cpp:11)
==1154==
==1154== LEAK SUMMARY:
==1154== definitely lost: 8 bytes in 1 blocks
==1154== indirectly lost: 0 bytes in 0 blocks
==1154== possibly lost: 0 bytes in 0 blocks
==1154== still reachable: 0 bytes in 0 blocks
Read the "definitely lost" block from the bottom up: main at line 11 called printPrice(), which called new at line 5, and that memory was never freed. That is your leak. The 8 bytes is one double.
Here is what the leak summary categories mean:
- definitely lost: a real leak. No pointer to this memory exists anymore. Fix these first.
- indirectly lost: memory you could only reach through a block that is definitely lost, like the nodes of a linked list whose head pointer leaked. Fix the definite leak and these usually go away.
- possibly lost: Valgrind found a pointer into the middle of the block, not its start. Worth a look.
- still reachable: a pointer to the memory still existed when the program ended. Lower priority, though a clean report is still the goal.
Valgrind runs on Linux, and on Windows through WSL. It does not support recent versions of macOS. On a Mac, the built-in leaks --atExit -- ./program command does a similar job.
Finding leaks with AddressSanitizer
If you compile with AddressSanitizer, you get leak checking for free on Linux. When the program exits, its leak checker prints a report like this:
g++ -fsanitize=address -g leak.cpp -o leak
./leak
==1146==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 8 byte(s) in 1 object(s) allocated from:
#0 0x7f098d8fe548 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:95
#1 0x55b0f864b27e in printPrice() /home/student/leak.cpp:5
#2 0x55b0f864b321 in main /home/student/leak.cpp:11
Same answer, different format: line 5 inside printPrice(), called from line 11 in main().
How to fix and prevent leaks
Here is the leak above, fixed three ways. Each version passed Valgrind with zero leaks:
// Fix 1: delete what you new
void printPrice() {
double* price = new double(19.99);
cout << "Price: " << *price << endl;
delete price;
}
// Fix 2 (better): let a smart pointer do it
void printPrice() {
auto price = make_unique<double>(19.99);
cout << "Price: " << *price << endl;
} // memory is freed here automatically
// Fix 3 (best here): you never needed the heap
void printPrice() {
double price = 19.99;
cout << "Price: " << price << endl;
}
And the habits that keep leaks out of your code in the first place:
- Every
newneeds a matchingdeletesomewhere in the code, on every path out of the function. - Wrap dynamic memory in
unique_ptrorshared_ptrso cleanup is automatic. - Use STL containers like
vectorandstringthat manage their own internal memory. If vectors are new to you, here are six ways to initialize a vector in C++. - Compile with AddressSanitizer (
-fsanitize=address) during development to catch leaks early. - Run your final tests through Valgrind for a full memory check.
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 limited. The default size is usually 8 MB on Linux and Mac and only 1 MB on Windows. A stack overflow happens when you fill it up.
The classic trigger is recursion without a base case. If recursion itself still feels new, our guide to recursive sorting algorithms shows how a function calls itself and when it stops, using merge sort and quick sort as examples.
void recurse() {
recurse();
}
int main() {
recurse(); // stack fills, program crashes
}
Each call to recurse() adds a new stack frame. After enough calls (anywhere from thousands to hundreds of thousands, depending on your system and how much each call stores), the stack is full and the program crashes. On Linux, all you see is Segmentation fault (core dumped), the same message as a bad pointer, and a Mac shows the same kind of segmentation fault line. That is why stack overflows are so easy to misread. g++ 12 and newer warn you about this exact code if you compile with -Wall: warning: infinite recursion detected [-Winfinite-recursion].
One odd thing we noticed in testing: with optimization turned on (-O2), g++ turned this function into an endless loop. The program did not crash. It just hung. So if a recursive program freezes instead of crashing, check your base case too.
Two other common triggers
Huge local arrays:
void bad() {
int massive[5000000]; // about 20 MB, far more than the stack holds
massive[0] = 1;
}
The program crashes as soon as the function starts using the array. Watch out for the in-between sizes, too. In our tests, a 2 MB array (500,000 ints) ran fine on Linux, where the stack is 8 MB. The same code would overflow the 1 MB stack on Windows. That is how "it works on my machine" happens.
Deep recursion with a real base case:
#include <iostream>
using namespace std;
long long sum(int n) {
if (n == 0) return 0;
return n + sum(n - 1);
}
int main() {
cout << sum(1000000) << endl; // crashes even though the base case is fine
return 0;
}
The recursion is logically correct, but one million nested calls need more stack than you have. With default settings, it crashed with a segmentation fault. With -O2, g++ managed to turn it into a loop and it ran fine, but you should not count on the optimizer to save you.
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. The stack is for small, short-lived data. Anything more than a few thousand elements belongs on the heap.
// instead of: int big[5000000];
vector<int> big(5000000);
// or: int* big = new int[5000000]; ... delete[] big;
For deep recursion, convert to iteration. A loop does not add a stack frame for each step, so a million iterations is fine.
long long sum(int n) {
long long total = 0;
for (int i = 1; i <= n; i++) total += i;
return total;
}
Notice we use long long here. The sum of 1 to 1,000,000 is 500,000,500,000, which is too big for an int. With int, you would trade a crash for a silently wrong answer.
Stack overflow is one of the few C++ errors where the fix is often a 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 semester, with the real messages g++ prints for each one.
The missing semicolon. C++ ends statements with ;. Forget one and the compiler often points at the next line, which can be misleading.
#include <iostream>
using namespace std;
int main() {
int x = 5
cout << x; // error reported here, but the real bug is the line above
return 0;
}
semi.cpp:6:5: error: expected ',' or ';' before 'cout'
6 | cout << x; // error reported here, but the real bug is the line above
| ^~~~
g++ blames line 6, but the missing semicolon is at the end of line 5. When an error makes no sense on the line it names, look at 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 in this scope
Fix:
#include <iostream>
using namespace std;
You can also skip using namespace std; and write std::cout instead. Many instructors prefer that style.
The missing header. You used a class from the STL without including its header.
vector<int> v; // error: 'vector' was not declared in this scope
Newer versions of g++ even tell you the fix:
vec.cpp:2:1: note: 'std::vector' is defined in header '<vector>'; did you forget to '#include <vector>'?
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<memory>forunique_ptr,shared_ptr<stdexcept>forruntime_error,invalid_argument
The type mismatch. You tried to assign a value of the wrong type.
int name = "Alex"; // error: invalid conversion from 'const char*' to 'int'
Fix: use the right type (and include <string>).
string name = "Alex";
The mismatched braces. Easy to do in nested blocks. Modern editors highlight matching pairs, so trust them.
#include <iostream>
using namespace std;
int main() {
int x = 3;
if (x > 0) {
cout << "positive";
// missing }
return 0;
}
brace.cpp:10:2: error: expected '}' at end of input
10 | }
| ^
brace.cpp:4:12: note: to match this '{'
4 | int main() {
| ^
Notice the note points at the opening brace of main(), not at the if that is actually missing its partner. So when you see "expected '}' at end of input," check every block inside that function.
Assignment instead of comparison. An 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: warning: suggest parentheses around assignment used as truth value. 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. Even our one missing #include <vector> produced two separate errors. 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 show up one line late), 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 trouble, often without crashing right away.
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.
#include <iostream>
using namespace std;
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, so the pointer is left pointing at memory that is no longer yours. In our test with g++ 13, the program crashed right away, because g++ quietly returns a null pointer for this kind of undefined behavior. Other compilers may print a garbage number instead, which is even harder to spot. g++ warns you about it, even without -Wall: warning: address of local variable 'local' returned.
Fix: never return a pointer to a local variable. Return by value, or allocate on the heap with clear ownership rules.
int* makePtr() {
return new int(42); // caller owns this, must delete
}
Even better, return a smart pointer:
unique_ptr<int> makePtr() {
return make_unique<int>(42);
}
Wild pointers
A wild pointer was declared but never initialized. It points to whatever junk was sitting in that memory location.
#include <iostream>
using namespace std;
int main() {
int* ptr;
*ptr = 10; // writing to random memory
cout << *ptr << endl;
}
This might crash. It might silently corrupt another variable. When we ran it, it printed 10 and exited normally, and that is exactly the problem. Code like this can pass your own tests and then fail when your instructor's autograder runs it. -Wall catches this one too: warning: 'ptr' is used uninitialized.
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.
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 prevents most of the pointer crashes that show up in homework.
Uncaught Exceptions and Exception Handling in C++
Some C++ errors do not crash the program directly. Instead, the code throws an exception. It stops what it is doing and reports a problem, hoping another part of your program will deal with it. If nothing does, the program ends with a message like this:
terminate called after throwing an instance of 'std::out_of_range'
what(): vector::_M_range_check: __n (which is 10) >= this->size() (which is 3)
Aborted (core dumped)
That came from this code, which asks a 3-item vector for item number 10:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> scores = {90, 85, 77};
cout << scores.at(10) << endl; // there is no index 10
return 0;
}
Read the message in two parts. The type in quotes (std::out_of_range) tells you what kind of problem it was. The what() line gives the details: index 10 was requested, but the size is only 3. Here are the most common ways students end up with an uncaught exception:
vec.at(i)orstr.at(i)with a bad index throwsstd::out_of_range.stoi("abc")throwsstd::invalid_argument. The message is justwhat(): stoi, which is not much help, so it is worth knowing.stoi("99999999999")throwsstd::out_of_rangebecause the number does not fit in anint.newthrowsstd::bad_allocwhen it cannot get enough memory.- Your own
throwstatement, with no matchingcatchanywhere.
The fix is to catch the exception or to avoid it in the first place. That is what exception handling is for.
What try and catch cannot catch
This trips up a lot of students, especially if you are coming from Java or Python. In C++, many runtime errors are not exceptions, so try and catch will not stop them:
- Dividing an integer by zero
- Dereferencing a null or dangling pointer
- Reading past the end of an array with
[] - Stack overflow
These are undefined behavior, and the program usually just crashes. Here is proof:
#include <iostream>
using namespace std;
int main() {
int a = 10;
int b = 0;
try {
cout << a / b << endl; // this does NOT throw an exception
}
catch (...) {
cout << "Caught it" << endl; // never runs
}
return 0;
}
Floating point exception (core dumped)
The catch block never runs. Despite its name, "Floating point exception" is a signal from the operating system, not a C++ exception, and it shows up for integer division by zero too. The fix is to check the value yourself before the risky operation, and throw your own exception if you want one. That is exactly what the next example does.
How try, throw, and catch work
C++ handles exceptions with three keywords that work together:
trywraps the code that might fail.throwsignals that something went wrong. Control jumps out of thetryblock right away.catchreceives what was thrown and handles it.
When something is thrown, C++ skips the rest of the try block and looks for a catch whose type matches. If it finds one, that block runs and the program carries on after it. If it finds none, even in the functions that called this one, the program ends with the "terminate called" message you saw above.
Here is the classic division example, done the right way. We check for zero ourselves and throw a standard exception with a clear message:
#include <iostream>
#include <stdexcept>
using namespace std;
double divide(int a, int b) {
if (b == 0) {
throw runtime_error("Division by zero is not allowed");
}
return static_cast<double>(a) / b;
}
int main() {
int a, b;
cout << "Enter first number: ";
cin >> a;
cout << "Enter second number: ";
cin >> b;
try {
double result = divide(a, b);
cout << "Result: " << result << endl;
}
catch (const runtime_error& e) {
cout << "Error: " << e.what() << endl;
}
cout << "Program keeps running" << endl;
return 0;
}
Output when the second number is 4:
Enter first number: 10
Enter second number: 4
Result: 2.5
Program keeps running
Output when the second number is 0:
Enter first number: 10
Enter second number: 0
Error: Division by zero is not allowed
Program keeps running
The last line is the whole point. Without try and catch, the program would have ended at the error. With them, it reports the problem and keeps going.
Catch exceptions by reference
Always write your catch blocks as catch (const SomeType& e). The & means you get the original exception object, not a copy. Here is what goes wrong when you catch by value:
#include <iostream>
#include <stdexcept>
using namespace std;
int main() {
try {
throw invalid_argument("Age cannot be negative");
}
catch (exception e) { // by value: the object gets sliced
cout << e.what() << endl;
}
return 0;
}
std::exception
Our message, "Age cannot be negative," is gone. Because we caught a plain exception by value, C++ copied only the base part of the object and threw the rest away. This is called slicing. g++ even warns about it with -Wall: catching polymorphic type 'class std::exception' by value. Add const and &, and the real message comes back:
catch (const exception& e) { // by const reference: nothing is lost
cout << e.what() << endl; // prints: Age cannot be negative
}
Handling more than one exception type
You can stack several catch blocks after one try. C++ checks them from top to bottom and runs the first one that matches, so put the most specific types first and the general ones last:
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
int main() {
string input = "abc";
try {
int age = stoi(input);
cout << "Age: " << age << endl;
}
catch (const invalid_argument& e) {
cout << "That is not a number." << endl;
}
catch (const out_of_range& e) {
cout << "That number is too big." << endl;
}
catch (const exception& e) {
cout << "Something else went wrong: " << e.what() << endl;
}
catch (...) {
cout << "Unknown error." << endl;
}
return 0;
}
That is not a number.
catch (...), with three dots, catches anything at all, even values that are not exception classes. It makes a useful last line of defense, but it tells you nothing about what went wrong, so do not use it as your only handler. In this example, the thrown int skips the char handler and lands in the catch-all:
#include <iostream>
using namespace std;
int main() {
int x = 10;
try {
throw x; // throwing an int
}
catch (char) {
cout << "Caught a char"; // skipped, the type does not match
}
catch (...) {
cout << "Caught in catch-all"; // catches anything
}
return 0;
}
Caught in catch-all
C++ standard exception classes
The standard library comes with ready-made exception classes. Most live in <stdexcept>, with std::exception in <exception>, bad_alloc in <new>, and bad_cast and bad_typeid in <typeinfo>. Use these before you write your own:
| Exception | When you see it |
|---|---|
std::exception | The parent of all standard exceptions. Catching const std::exception& catches every one of them. |
std::logic_error | Parent class for mistakes in program logic, the kind you could in theory catch before running. |
std::invalid_argument | A function got a value it cannot use, like stoi("abc"). |
std::out_of_range | An index or value is outside the allowed range, like at(10) on a 3-item vector. |
std::length_error | You tried to make a string or vector bigger than its maximum size. |
std::runtime_error | Parent class for problems you can only detect while the program runs. A good default when you throw your own errors. |
std::overflow_error, std::underflow_error | An arithmetic result is too big or too small. Only thrown by some library functions and your own code. Normal int math that overflows does not throw. |
std::range_error | A result cannot be represented. Rare in student code. |
std::bad_alloc | new could not get the memory you asked for. |
std::bad_cast | A dynamic_cast to a reference type failed. |
std::bad_typeid | typeid was used on a null pointer to a polymorphic object. |
Creating your own exception class
When none of the standard classes describe your error well, make your own. The easiest way is to inherit from std::runtime_error, so you get what() for free:
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
class InvalidGrade : public runtime_error {
public:
explicit InvalidGrade(const string& message) : runtime_error(message) {}
};
char letterGrade(int score) {
if (score < 0 || score > 100) {
throw InvalidGrade("Score must be between 0 and 100, got " + to_string(score));
}
if (score >= 90) return 'A';
if (score >= 80) return 'B';
if (score >= 70) return 'C';
if (score >= 60) return 'D';
return 'F';
}
int main() {
try {
cout << letterGrade(85) << endl;
cout << letterGrade(105) << endl;
}
catch (const InvalidGrade& e) {
cout << "Grade error: " << e.what() << endl;
}
return 0;
}
B
Grade error: Score must be between 0 and 100, got 105
letterGrade(85) printed B. Then letterGrade(105) threw, and the catch block reported the exact bad value. A clear message like that saves you, and whoever grades your code, a lot of guessing.
So which should you throw? Here is a quick guide:
| What you throw | Example | Use it when |
|---|---|---|
| A built-in type | throw 42; | Almost never. Whoever catches it has no idea what 42 means. |
| A standard exception | throw invalid_argument("Age cannot be negative"); | Most of the time. |
| Your own class | throw InvalidGrade("..."); | The caller needs to handle this error differently from other errors. |
Common exception handling mistakes
- Catching by value. You lose the real message, as the slicing example showed. Catch by
constreference. - An empty catch block.
catch (...) {}hides the error completely. At the very least, printe.what(). - Using exceptions for normal flow. Do not throw just to leave a loop or return a value. Exceptions are for real problems, and throwing one is much slower than a normal
iforreturn. - Throwing ints or strings.
throw -1;compiles, but the catcher has no idea what -1 means. Throw an exception class with a clear message. - Forgetting that a throw skips your
delete. If an exception fires betweennewanddelete, that memory leaks. This is one more reason to usevectorand smart pointers. They clean up even when an exception is thrown.
void process() {
int* data = new int[100];
riskyStep(); // if this throws...
delete[] data; // ...this line never runs (Valgrind: 400 bytes lost)
}
void processFixed() {
vector<int> data(100);
riskyStep(); // vector frees its memory even when this throws
}
If you have written Java before, C++ exceptions will look familiar, with two big differences. C++ has no finally block (destructors and smart pointers do that job), and the compiler never forces you to catch anything. Our guide to common Java errors shows how Java handles the same ideas.
How to Debug Any C++ Error Step by Step
Knowing the 8 errors above helps, but sooner or later you will hit a bug that does not match any of them. Here is a step-by-step routine that works for any error, whether it shows up while compiling, linking, or running.
Step 1: Read the first error message, all of it
Scroll up to the first error, not the last one. Find the file name, line number, and column (like semi.cpp:6:5). Then read the actual words. g++ messages often include the fix, like the "did you forget to #include <vector>" note you saw earlier. If you can name the type of error and the line, half the work is done.
Step 2: Name the stage
Use the map from the top of this guide. Did it fail while compiling (a line number and error:), while linking (undefined reference and ld returned 1 exit status), or while running (a crash, terminate called, or wrong output)? Each stage has a different list of suspects.
Step 3: Turn on warnings and rebuild
g++ -Wall -Wextra -g main.cpp -o main
In this guide alone, warnings caught an uninitialized pointer, a free() on memory from new, a delete on a stack variable, a returned address of a local variable, infinite recursion, and an exception caught by value. That is six runtime bugs found before the program ever ran. If you use an IDE, look for the compiler flags or "additional options" setting in your project and add the flags there.
Step 4: Make the bug happen every time
Find the smallest input that causes the problem. If your program crashes on a 500-line input file, try 10 lines, then 3. A bug you can trigger on purpose is a bug you can fix. A bug that shows up only "sometimes" usually means uninitialized memory or reading past an array, so jump ahead to Step 7.
Step 5: Print what is happening with cerr
When you are not sure where things go wrong, print the values as the program runs. Use std::cerr instead of cout for this. cerr is not buffered, so your message shows up right away, even if the program crashes on the very next line. With cout, the last few messages can get lost in the crash.
Here is a function that should return the biggest number in an array, but gives a wrong answer:
#include <iostream>
using namespace std;
int findMax(int arr[], int size) {
int best = arr[0];
for (int i = 1; i <= size; i++) { // bug: should be i < size
cerr << "i = " << i << ", arr[i] = " << arr[i] << endl;
if (arr[i] > best) best = arr[i];
}
return best;
}
int main() {
int nums[] = {4, 9, 2};
int result = findMax(nums, 3);
cout << "Max: " << result << endl;
return 0;
}
Output:
i = 1, arr[i] = 9
i = 2, arr[i] = 2
i = 3, arr[i] = 2105332224
Max: 2105332224
The debug lines make the bug obvious. The array has 3 items (index 0 to 2), but the loop reached i = 3 and read a garbage value. The fix is i < size instead of i <= size. The garbage number will be different every time you run it. Delete the cerr lines once the bug is fixed.
For checks you want to keep while you work, use assert. It stops the program with a clear message the moment something you assumed turns out to be false:
#include <iostream>
#include <cassert>
using namespace std;
double average(int total, int count) {
assert(count > 0 && "count must be positive");
return static_cast<double>(total) / count;
}
int main() {
cout << average(270, 3) << endl;
cout << average(0, 0) << endl;
return 0;
}
90
assert: assert.cpp:6: double average(int, int): Assertion `count > 0 && "count must be positive"' failed.
Aborted (core dumped)
Asserts are removed when you compile with -DNDEBUG, so they cost nothing in a final build. Do not delete them before the code is fully tested. They are cheap insurance.
Step 6: Step through the code with a debugger
A debugger lets you pause your program on any line and look at every variable. On the command line, that is GDB (see the toolkit below). Most IDEs have one built in: VS Code, CLion, Visual Studio, and Code::Blocks all let you click next to a line number to set a breakpoint, then step through one line at a time. Compile with -g first, or the debugger cannot match the program to your source code.
If you would rather have someone walk through the debugger with you on your own assignment, our live programming tutoring sessions do exactly that.
Step 7: Run a memory checker, even if the output looks right
g++ -fsanitize=address -g main.cpp -o main
./main
Memory bugs often give correct output on your computer and wrong output on someone else's. AddressSanitizer or Valgrind will point to the exact line. On the findMax() example from Step 5, ASan stopped the program at the bad read and named the spot: stack-buffer-overflow ... in findMax(int*, int) cerr.cpp:7.
Step 8: Recompile and retest everything
After every fix, rebuild and run again. It sounds obvious, but running an old executable after changing the code is one of the most common reasons students think a fix "did not work." Then rerun your earlier test inputs too, to make sure the fix did not break something else.
Debugging mistakes students make
- Skimming the compiler message, or reading the last error instead of the first.
- Changing three things at once, so you cannot tell which change fixed (or broke) the code.
- Forgetting to recompile after a change.
- Using only print statements when a debugger would find the problem in two minutes. Prints are fine for quick checks, but learn your debugger.
- Removing asserts and checks before the code is fully tested.
- Stopping once the output looks right, without checking for memory leaks and invalid reads.
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 a lot 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. If you want g++ to enforce that for you, add -Werror.
Tool 2: GDB for runtime crashes
GDB is the standard command-line debugger on Linux, and the go-to tool for any segfault that does not have an obvious cause. On a Mac, the built-in debugger is LLDB, which uses almost the same basic commands.
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
(gdb) catch throw # stop the moment an exception is thrown
The -g flag is required. Without it, GDB cannot map the crash back to your source code.
That last command is handy for the "terminate called after throwing" error. Type catch throw before run, and GDB pauses right where the exception is thrown. Then bt shows the call chain. The top few frames are inside the standard library, so scan down to the first line that names your own file. In our vector.at(10) example, that was main () at uncaught.cpp:7.
Tool 3: Valgrind for memory issues
If you suspect a memory leak, an invalid read, or a use-after-free, Valgrind is the classic tool:
g++ -g program.cpp -o program
valgrind --leak-check=full ./program
Valgrind runs your program on a simulated CPU that watches every memory operation. It reports leaks, invalid accesses, and reads of uninitialized memory, with line numbers when you compile with -g. The downside is speed. The Valgrind quick start guide says programs run about 20 to 30 times slower, so use it for testing, not every run.
Tool 4: AddressSanitizer for fast memory checks
AddressSanitizer (ASan) is built into modern GCC and Clang on Linux and Mac, and Visual Studio supports it too with the /fsanitize=address option. (MinGW's g++ on Windows does not support it, so Windows users should use Visual Studio or WSL.) Its leak checker only works on Linux. It catches most of the same bugs as Valgrind, but a program typically runs only about 2 times slower, according to the AddressSanitizer documentation.
g++ -fsanitize=address -g program.cpp -o program
./program
When the program does something wrong with memory, ASan stops it and prints a detailed report with the exact line and the kind of error. One thing ASan does not catch is reading an uninitialized variable. Valgrind does. Many teams run their automated tests with ASan turned on, and it is a good habit for your own projects too.
When to reach for which tool
| Problem | Tool |
|---|---|
| Compile-time mystery | Compiler warnings (-Wall -Wextra) |
| Segfault with no obvious cause | GDB with -g |
terminate called after throwing... | GDB with catch throw, then bt |
| Memory leak suspected | Valgrind or ASan |
| Invalid memory access | ASan during development, Valgrind for a final check |
| Wrong output, no crash | Print statements with cerr, 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 a whole class of undefined behavior that simply never happens.
Prefer the STL over raw arrays and raw pointers. vector<int> instead of int* with new[]. string instead of char*. The STL handles memory and resizing for you, .at() checks bounds (plain [] does not), and it has been tested far more than any code you write from scratch.
Adopt smart pointers as your default for dynamic memory. Use unique_ptr for single ownership, shared_ptr for shared, and weak_ptr to break cycles. Reach for raw new and delete only when an assignment 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 changes.
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. Add short comments where the logic is not obvious.
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.
Plan for things that fail at runtime. Files that do not exist, bad user input, and network calls fail all the time. File streams and cin do not throw by default, so check them yourself (if (!file), if (!(cin >> x))) and throw your own exception when you want the caller to deal with it. Then wrap the risky call in a try-catch so your program reports the problem instead of crashing. The exception handling section above shows how to catch by reference and which standard exceptions to use.
Build these habits early and you will hit the errors in this guide far less often. 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. On Linux, both tools list each leaked block and the line where it was allocated, as long as you compiled with -g. On a Mac, use the built-in leaks --atExit -- ./program command.
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 a function named exactly main. Check these in order: the file is saved, main() exists, it is spelled in lowercase, you are compiling the file that contains it, and it is not inside a namespace or class. On Windows with MinGW, the same problem shows up as "undefined reference to WinMain."
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 does nothing and is completely safe. Calling delete on a pointer that was already deleted is undefined behavior. The simple rule: always reset to nullptr 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 prevents whole categories of bugs, like leaks and double frees.
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 use 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 unnecessary risk.
11. Can I use void main() in C++?
No. Standard C++ requires main to return int. g++ and Clang stop with an error like '::main' must return 'int'. Some older compilers, like Turbo C++, accepted void main(), which is why you still see it in old notes. Write int main() instead.
12. Why do I get "undefined reference to main" when I compile my class files?
You are probably compiling one .cpp file on its own, and that file has no main(). A command like g++ Student.cpp tries to build a complete program from that one file. Compile all the files together (g++ main.cpp Student.cpp -o program), or add -c to compile just that file into an object file.
13. Does try-catch catch segmentation faults or division by zero in C++?
No. In C++, a null pointer dereference, integer division by zero, and reading past an array with [] are undefined behavior, not exceptions. The program usually crashes and your catch block never runs. Check the values yourself before the risky operation, and throw your own exception if you need one.
14. What does "terminate called after throwing an instance of" mean?
Your program threw an exception and nothing caught it, so C++ ended the program. The type in quotes, such as std::out_of_range, tells you what went wrong, and the what() line gives the details. Wrap the risky call in a try block with a matching catch, or fix the bad index or input that caused it.
15. Why should I catch exceptions by reference in C++?
Catching by const reference gives you the original exception object. Catching by value makes a copy, and if you catch a base class like std::exception by value, the copy loses the details. Your message gets replaced by something generic like "std::exception." Always write catch (const std::exception& e).
16. Will the compiler warn me about a memory leak?
No. A memory leak does not cause a compile error or a warning. The program compiles and usually runs correctly. You find leaks by running tools like Valgrind or AddressSanitizer.
17. Is a small memory leak in a homework program a big deal?
In a short program, the operating system takes all the memory back when the program ends, so you will not notice anything. But some courses check submissions with Valgrind, and a leak inside a loop or a long-running program keeps growing until it causes real trouble. Fix leaks as a habit, even small ones.
