Skip to content
C++

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 runs

Comparing 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 condition

If 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 automatically

In 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

MistakeWrongCorrect
Uninitialized variableint x; cout << x;int x = 0; cout << x;
Integer divisiondouble avg = sum / count;double avg = (double)sum / count;
Assignment in conditionif (x = 0)if (x == 0)
Semicolon after ifif (x > 0) ; { x--; }if (x > 0) { x--; }
Float equalityif (x == 0.3)if (std::abs(x - 0.3) < 1e-9)
Off-by-one (vector)i <= v.size()i < v.size()
Infinite loopwhile (n > 0) { /* no n-- */ }while (n > 0) { n--; }
getline after >>cin >> n; getline(cin, s);cin >> n; cin.ignore(…); getline(cin, s);
Raw new/deleteint* p = new int(5); delete p;auto p = make_unique<int>(5);
Sign in to track progress