Common Beginner Mistakes
Every C++ beginner trips over the same set of pitfalls. Most of them look harmless — a misplaced semicolon, a = where == was needed, a loop counter that goes one too far. The compiler catches some of these; others compile silently and produce wrong output for hours before you notice. This page catalogs the most common mistakes, explains exactly why each one goes wrong, and shows the correct pattern to replace it.
Uninitialized variables
Variables, Types & Operators (Module 2)
In C++, declaring a variable does not set it to zero, empty, or any other safe default. A local variable that has been declared but not initialized holds whatever bytes happened to be at that memory address from a previous use — effectively random garbage. Reading that garbage value is undefined behavior: the program may print a nonsensical number, it may crash, or it may appear to work correctly and then fail mysteriously on a different run or a different machine.
// WRONG — total holds garbage, result is meaningless: int total; total = total + 5; // adds 5 to some unknown value std::cout << total; // could print anything // CORRECT — always initialize: int total = 0; total = total + 5; std::cout << total; // prints 5
Enable compiler warnings (-Wall -Wextra with GCC/Clang) and they will flag most uninitialized-variable reads. Make it a habit to initialize every variable at the point where you declare it.
Unintended integer division
Variables, Types & Operators (Module 2)
When both operands of / are integers, C++ performs integer division: the result is truncated toward zero, discarding any fractional part. This surprises beginners who expect a decimal result. The mistake is especially insidious because the code compiles without error and often produces a plausible-looking (but wrong) answer.
// WRONG — integer division discards the remainder: int a = 7, b = 2; double result = a / b; // 7/2 = 3 (integer), then stored as 3.0 std::cout << result; // prints 3, not 3.5 // CORRECT — cast to double before dividing: double result = static_cast<double>(a) / b; // 3.5 // Or: use double literals double result2 = 7.0 / 2; // 3.5 double result3 = 7 / 2.0; // 3.5 // Classic trap in averages: int sum = 17, count = 5; double avg = sum / count; // WRONG: 3.0 double avg2 = static_cast<double>(sum) / count; // CORRECT: 3.4
Confusing = (assignment) with == (equality)
Control Flow (Module 3)
The single equals sign = is assignment — it changes the value of a variable. The double equals sign == is comparison — it tests whether two values are equal. Using = inside an if condition compiles without error (it assigns the value and then evaluates the assigned value as a Boolean), but the branch runs based on the new value rather than the original one — almost never what you intended.
// WRONG — assigns 0 to flag, then tests whether flag is non-zero (it's 0 = false):
int flag = 1;
if (flag = 0) // assigns 0, condition is false
std::cout << "zero"; // never printed
std::cout << flag; // prints 0 — flag was silently overwritten!
// CORRECT:
if (flag == 0)
std::cout << "zero";
// Compile-time trick: put the literal on the left ("Yoda condition"):
if (0 == flag) // if you accidentally write = instead of ==, it won't compile
std::cout << "zero";GCC and Clang warn about assignments inside conditions with -Wparentheses (included in -Wall). The fix is one character: change = to ==.
Semicolon after the if condition
Control Flow (Module 3)
A semicolon immediately after the if (condition) terminates the if statement with an empty body — the do-nothing statement ;. The block of code in the following braces is then unconditional: it runs whether the condition was true or false. The book Big C++ Late Objects (Horstmann) calls this one of the most common errors beginners make.
// WRONG — semicolon makes the if body empty:
if (floor > 13) ; // semicolon ends the if here
{
floor--; // this always runs, regardless of the condition!
}
// CORRECT:
if (floor > 13)
{
floor--; // only runs when floor > 13
}
// Same problem with else:
if (x > 0) ;
else ; // both branches do nothing
{ std::cout << x; } // always runsComparing floating-point numbers with ==
Control Flow (Module 3)
Floating-point arithmetic is not exact. The value 0.1 + 0.2 is not stored as exactly 0.3 in binary floating point — it is something like 0.30000000000000004. Testing two floating-point numbers for exact equality with == therefore fails in cases where a human mathematician would consider them equal. Instead, test whether their absolute difference is smaller than a small tolerance value.
#include <cmath> // for std::abs
// WRONG — may never be true due to floating-point rounding:
double x = 0.1 + 0.2;
if (x == 0.3) std::cout << "equal"; // often fails
// CORRECT — compare within a tolerance (epsilon):
const double EPSILON = 1e-9;
if (std::abs(x - 0.3) < EPSILON)
std::cout << "equal";
// Loop that tries to reach exactly 1.0 may loop forever:
double val = 0.0;
while (val != 1.0) // WRONG: may overshoot 1.0 by a tiny amount
val += 0.1;
while (val < 1.0 - EPSILON) // CORRECT: stop when close enough
val += 0.1;Off-by-one errors in loops
Control Flow (Module 3) / Loops (Module 3)
An off-by-one error is a loop that runs one iteration too many or too few. It is one of the most persistent bugs in programming. The book Big C++ Late Objects (Horstmann) describes the fix: rather than randomly adding or subtracting 1 until the program seems to work, trace through the loop by hand with simple values and build a rationale for each boundary decision. The pattern i = 0; i < n visits exactly n elements; the pattern i = 1; i <= n also visits nelements but with 1-based indexing.
// Print 1..10 — which bound is correct?
for (int i = 0; i < 10; i++) // prints 0..9 — WRONG if 1-based is intended
std::cout << i;
for (int i = 1; i <= 10; i++) // prints 1..10 — CORRECT
std::cout << i;
// Access all elements of a vector — use size(), not size()-1 or size()+1:
std::vector<int> v = {10, 20, 30};
for (std::size_t i = 0; i < v.size(); i++) // 0, 1, 2 — all valid indices
std::cout << v[i];
for (std::size_t i = 0; i <= v.size(); i++) // WRONG: i==3 is out of bounds
std::cout << v[i]; // undefined behavior on last iteration
// Range-for eliminates the boundary question entirely:
for (int x : v)
std::cout << x;Infinite loops — forgetting to update the control variable
Loops (Module 3)
A while loop runs as long as its condition is true. If the body never changes the variables that the condition tests, the condition stays true forever and the program hangs. The Big C++ Late Objects textbook notes a second common cause: accidentally incrementing a variable that should be decremented (or vice versa), so the loop moves away from the termination condition rather than toward it.
// WRONG — forgot to increment year; loop never ends:
int year = 1;
while (year <= 20)
{
balance = balance * 1.05;
// year++ was forgotten here!
}
// WRONG — increment instead of decrement; overshoots the condition:
int year = 20;
while (year > 0)
{
std::cout << year << ' ';
year++; // should be year-- ! year grows, never reaches <= 0
}
// CORRECT:
while (year > 0)
{
std::cout << year << ' ';
year--; // moves toward the exit conditionIf a loop runs longer than expected, reach for Ctrl+C to terminate the program and then trace through the first two iterations on paper to find why the condition never becomes false.
Forgetting braces — the dangling-else trap
Control Flow (Module 3)
When an if or else branch controls a single statement, braces are optional. But omitting them invites a subtle bug: an else always pairs with the nearest preceding unmatched if, regardless of how the code is indented. This is called the dangling else problem.
// Indentation implies outer if owns the else — but the compiler disagrees:
if (x > 0)
if (x < 100)
std::cout << "in range";
else // paired with inner if (x < 100), NOT outer if!
std::cout << "negative"; // runs when x >= 100, not when x <= 0
// CORRECT — braces make intent unambiguous:
if (x > 0)
{
if (x < 100)
std::cout << "in range";
}
else
{
std::cout << "negative"; // now correctly paired with outer if
}The Big C++ Late Objects textbook (Programming Tip 3.2) recommends always using braces around every branch body, even single-statement ones. The braces cost nothing and eliminate an entire class of ambiguity.
Missing return value — falling off the end of a function
Functions (Module 4)
Every path through a non-void function must reach a return statement. If any path exits without returning, the behavior is undefined — the function may return garbage, crash, or produce inconsistent results. The compiler often warns about this with -Wall, but it does not always catch every case.
// WRONG — no return in the else branch when x < 0:
int absolute_value(int x)
{
if (x >= 0) return x;
// path falls off here if x < 0 — undefined behavior!
}
// CORRECT — every path returns:
int absolute_value(int x)
{
if (x >= 0) return x;
return -x;
}
// WRONG — loop may never execute, leaving no return:
int find_first_even(const std::vector<int>& v)
{
for (int x : v)
if (x % 2 == 0) return x;
// no return if v is empty or has no even number — undefined behavior
}
// CORRECT — return a sentinel or use std::optional:
int find_first_even(const std::vector<int>& v)
{
for (int x : v)
if (x % 2 == 0) return x;
return -1; // caller checks for -1 to detect "not found"
}Mixing >> and getline — the leftover newline
Standard Library (Module 6) / File I/O (Module 6)
std::cin >> x reads a value and stops — it does not consume the newline character the user pressed after typing. std::getline, on the other hand, reads until it finds a newline and then discards it. If you call getline immediately after >>, the leftover newline is consumed instantly and the user never gets a chance to type anything — the string comes back empty.
// WRONG — getline immediately sees the leftover '\n' and returns empty: int age; std::cin >> age; // reads "25", leaves '\n' in buffer std::string name; std::getline(std::cin, name); // reads "\n" — name is ""! // CORRECT — discard the leftover newline before calling getline: int age; std::cin >> age; std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); std::string name; std::getline(std::cin, name); // now reads a full line correctly
The Big C++ Late Objects textbook (Common Error 8.1) identifies this mixing of >> and getline as one of the most common stream errors beginners encounter. The fix is a single cin.ignore(…) call after every >> that precedes a getline.
Out-of-bounds vector / array access
Standard Library (Module 6)
Indexing a vector or array with an index that is negative or >= size() is undefined behavior. The operator [] does not check bounds in release builds; it simply computes the address and reads or writes whatever is there. The result is often a crash, but it can also silently corrupt other variables and produce mysterious wrong results much later in the program.
std::vector<int> v = {10, 20, 30}; // valid indices: 0, 1, 2
v[3] = 99; // WRONG: index 3 is out of bounds — undefined behavior
// Use .at() during development — it throws std::out_of_range on bad index:
v.at(3) = 99; // throws, giving you a clear error instead of silent corruption
// Loop bound error:
for (int i = 0; i <= v.size(); i++) // WRONG: i reaches 3 on last iteration
std::cout << v[i]; // v[3] is out of bounds
for (int i = 0; i < v.size(); i++) // CORRECT: 0, 1, 2 only
std::cout << v[i];Using raw pointers when smart pointers fit
Memory & Pointers (Module 7)
Raw new and delete require you to manually match every allocation with exactly one deallocation. Forget to call delete and you have a memory leak. Call it twice and you have undefined behavior. Call it on a pointer that was never returned by new and you have a crash. These errors do not always manifest immediately, which makes them hard to diagnose.
// WRONG — raw new/delete requires perfect pairing and error-free paths:
int* p = new int{42};
// ... if an exception is thrown here, delete never runs → memory leak
delete p;
// CORRECT — std::unique_ptr deletes automatically when it goes out of scope:
#include <memory>
auto p = std::make_unique<int>(42);
// p is freed automatically — no delete needed, no leak, no double-free
// WRONG — dangling pointer: using pointer after delete:
int* q = new int{10};
delete q;
std::cout << *q; // undefined behavior: memory was freed
// CORRECT — let the smart pointer manage lifetime:
{
auto q = std::make_unique<int>(10);
std::cout << *q; // safe
} // q destroyed here, memory freed automaticallyIn modern C++ (C++11 and later) you should rarely write new or delete directly. Prefer std::vector for arrays, std::unique_ptr for exclusive ownership, and std::shared_ptr for shared ownership.
Quick-reference summary
| Mistake | Wrong | Correct |
|---|---|---|
| Uninitialized variable | int x; cout << x; | int x = 0; cout << x; |
| Integer division | double avg = sum / count; | double avg = (double)sum / count; |
| Assignment in condition | if (x = 0) | if (x == 0) |
| Semicolon after if | if (x > 0) ; { x--; } | if (x > 0) { x--; } |
| Float equality | if (x == 0.3) | if (std::abs(x - 0.3) < 1e-9) |
| Off-by-one (vector) | i <= v.size() | i < v.size() |
| Infinite loop | while (n > 0) { /* no n-- */ } | while (n > 0) { n--; } |
| getline after >> | cin >> n; getline(cin, s); | cin >> n; cin.ignore(…); getline(cin, s); |
| Raw new/delete | int* p = new int(5); delete p; | auto p = make_unique<int>(5); |