15 Common C++ Beginner Mistakes: From Compile Errors to Runtime Bugs
Key takeaways
Fifteen common C++ beginner mistakes with compiler output, root cause, and fix for each: missing semicolons, forgotten headers, void main, uninitialized pointers, off-by-one loops, = vs ==, and more.
Introduction
Learning C++ means encountering a set of errors that every beginner hits in roughly the same order. The compiler’s error messages can look overwhelming — a single misplaced character can trigger five follow-on errors. But each mistake has a predictable pattern, and once you recognize the pattern, the fix takes ten seconds.
This guide is organized by mistake: fifteen habits that produce most beginner errors, including the runtime and logic bugs the compiler does not catch, with the actual output, the root cause, and the fix. If you already have a specific compiler or linker message in front of you, 10 C++ Compile and Link Errors Beginners Hit First is organized by message and explains how to tell compile, link and runtime errors apart.
Key rule: always fix the first error in the compiler output. Later errors are often caused by the first one, so fixing them one by one is slower than addressing the root.
Part 1: Compile-Time Mistakes
Missing Semicolon After Class/Struct Definition
// WRONG
class Player {
int health;
std::string name;
} // ← missing semicolon
int main() {
Player p; // error cascades here
}
Compiler output (GCC):
error: expected ';' after class definition
Current GCC and Clang recognize this exact case and even print a fix-it hint pointing at the brace. The confusing version happens when the class lives in a header: the error is then reported in the .cpp file that includes it, on the first line after the #include, often with messages like multiple types in one declaration or expected unqualified-id. When an error points at a line that looks perfectly fine, check the end of the last header included above it.
// CORRECT
class Player {
int health;
std::string name;
}; // ← semicolon required after class/struct/union
Why: The C++ grammar requires a semicolon after the closing brace of a class definition because you can declare variables after the brace: class Player { ... } p1, p2;. The semicolon ends the declaration statement.
Missing #include
// WRONG
int main() {
std::cout << "Hello\n"; // cout not declared
std::vector<int> v; // vector not declared
std::sort(v.begin(), v.end()); // sort not declared
}
Compiler output:
error: 'cout' is not a member of 'std'
error: 'vector' is not a member of 'std'
GCC often adds a note such as 'std::cout' is defined in header '<iostream>'; did you forget to '#include <iostream>'?, which answers the question directly. A trap in the other direction: code sometimes compiles without an include because another standard header happened to pull it in (<iostream> drags in parts of <string> on some implementations). That is not guaranteed, and the same file can fail on a different compiler or library version. Include what you use, even when it seems to work without.
// CORRECT
#include <iostream> // std::cout, std::cin, std::cerr
#include <string> // std::string
#include <vector> // std::vector
#include <algorithm> // std::sort, std::find, std::max
#include <memory> // std::unique_ptr, std::shared_ptr
#include <cmath> // std::sqrt, std::pow, std::abs
int main() {
std::cout << "Hello\n";
std::vector<int> v = {3, 1, 4, 1, 5};
std::sort(v.begin(), v.end());
}
Rule: include the header for every standard name you use. If the compiler says something is “not declared in this scope,” the first question is “do I have the right include?”
Missing std:: (Namespace Prefix)
// WRONG
#include <iostream>
int main() {
cout << "Hello\n"; // error: 'cout' was not declared
string name = "Alice"; // error: 'string' was not declared
}
// CORRECT — explicit prefix (recommended)
#include <iostream>
#include <string>
int main() {
std::cout << "Hello\n";
std::string name = "Alice";
}
// ALSO CORRECT — using declaration in .cpp files (not headers)
#include <iostream>
using std::cout;
using std::string;
int main() {
cout << "Hello\n";
string name = "Alice";
}
Avoid: using namespace std; in header files. It pollutes every file that includes yours. In .cpp files it’s acceptable but verbose using declarations for specific names are safer.
void main Instead of int main
// WRONG — not standard C++
void main() {
std::cout << "Hello\n";
}
Compiler output (Clang):
error: 'main' must return 'int'
// CORRECT
int main() {
std::cout << "Hello\n";
return 0; // 0 = success; can be omitted in main (implicit in C++11+)
}
// With command-line arguments:
int main(int argc, char* argv[]) {
// argc = argument count, argv = argument strings
return 0;
}
Using Variables Before Declaration
// WRONG
int main() {
total = 0; // error: 'total' was not declared
int total;
total = 100;
}
// CORRECT: declare before use, initialize at declaration
int main() {
int total = 0;
total = 100;
}
In C++, variables must be declared before they are used. Declare them as close to their first use as possible — this makes code easier to read and reduces the scope of each variable.
Const Reference for Read-Only Parameters
// WRONG: takes string by value — copies every time
void printName(std::string name) {
std::cout << name << '\n';
}
printName("Alice"); // copies "Alice" into a new std::string
printName(user.displayName); // copies the entire string
// CORRECT: const reference — no copy
void printName(const std::string& name) {
std::cout << name << '\n';
}
printName("Alice"); // still builds a temporary std::string from the literal
printName(user.displayName); // binds to the existing string, no copy
String-by-value vs string-by-const-reference is not just style: a by-value parameter copies the characters (and may allocate) on every call. The rule: if a function only reads a string (doesn’t modify or store it), use const std::string&, or std::string_view in C++17, which also avoids the temporary when the caller passes a literal. The exception is a function that stores the string, such as a constructor setting a member; there, taking by value and std::move-ing into the member is the usual idiom, because the caller can move in and avoid the copy altogether.
This mistake doesn’t produce an error, which is why it’s on the list: the program is correct, just slower, and nothing tells you. Things like const correctness and parameter passing are best learned as habits early rather than hunted down in a profiler later.
Forgetting to Initialize Variables
// WRONG: uninitialized variable — undefined behavior
int main() {
int count;
if (count > 0) // count has garbage value — behavior undefined
std::cout << "positive\n";
}
// CORRECT: always initialize
int count = 0;
// or
int count{}; // value-initializes to 0 for fundamental types
Uninitialized variables have indeterminate values. The program may print different results, crash, or appear to work on debug builds but fail on release builds. Always initialize.
Declaration/Definition Mismatch
// header.h
int computeSum(int a, int b); // declared as returning int
// implementation.cpp
double computeSum(int a, int b) { // WRONG: different return type
return a + b;
}
Compiler output (GCC, when the .cpp includes the header):
error: ambiguating new declaration of 'double computeSum(int, int)'
note: old declaration 'int computeSum(int, int)'
The header and implementation must match exactly: return type, parameter types, const qualifiers, and namespace.
A return-type mismatch is the easy case, because the compiler catches it. The harder one is a parameter mismatch, such as computeSum(int, int) in the header and computeSum(long, int) in the .cpp. That is not an error at compile time: the .cpp simply defines a second, overloaded function. The failure arrives at link time, as undefined reference to 'computeSum(int, int)' from GCC’s linker or LNK2019: unresolved external symbol from MSVC, and the message names the function you declared, not the one you wrote. When I see an undefined reference to a function I am sure I implemented, the first thing I do is put the header declaration and the definition side by side and compare them character by character, including const on member functions and the enclosing namespace. The LNK2019 post covers the other causes.
Part 2: Runtime Mistakes
Uninitialized Pointers
// WRONG: pointer is garbage — crash on dereference
int* ptr;
*ptr = 42; // crash: writing to random memory address
std::cout << *ptr; // crash: reading garbage address
// CORRECT: always initialize pointers
int* ptr = nullptr; // explicitly null — safe to check
// Only dereference after confirming non-null
if (ptr != nullptr) {
*ptr = 42;
}
// Better: use smart pointers
auto smartPtr = std::make_unique<int>(42);
std::cout << *smartPtr << '\n'; // no manual null check needed
// Automatically freed when smartPtr goes out of scope
Golden rules for raw pointers:
- Initialize to
nullptrif not yet pointing to valid memory - Always check
if (ptr != nullptr)before dereferencing - Prefer
std::unique_ptrorstd::shared_ptrfor ownership
Off-by-One Loop Errors
// WRONG: goes one past the end — undefined behavior
int arr[] = {1, 2, 3, 4, 5};
for (int i = 0; i <= 5; ++i) // ← should be < 5
std::cout << arr[i]; // arr[5] is out of bounds!
// CORRECT: index 0 to size-1
int arr[] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; ++i)
std::cout << arr[i];
// Even better: range-for (no index, no off-by-one possible)
for (int x : arr)
std::cout << x;
// For std::vector: use .size() and < (not <=)
std::vector<int> v = {1, 2, 3, 4, 5};
for (size_t i = 0; i < v.size(); ++i)
std::cout << v[i];
// Or safe access with bounds check:
v.at(4); // throws std::out_of_range if index invalid
Array indices in C++ run from 0 to size-1. Accessing index size is undefined behavior — the program may crash, produce wrong results, or appear to work and crash later.
Comparing C-Style Strings with ==
// WRONG: compares pointer addresses, not contents
char s1[] = "hello";
char s2[] = "hello";
if (s1 == s2) { // always false! comparing addresses
std::cout << "equal\n";
}
// CORRECT option 1: use strcmp
#include <cstring>
if (strcmp(s1, s2) == 0) {
std::cout << "equal\n";
}
// CORRECT option 2: use std::string (recommended)
std::string str1 = "hello";
std::string str2 = "hello";
if (str1 == str2) { // compares contents — correct
std::cout << "equal\n";
}
C-style character arrays decay to pointers to their first element in most expressions, including ==, so the comparison checks whether the two arrays live at the same address. They never do. Comparing two arrays this way is deprecated in C++20, and recent GCC and Clang warn about it. Use std::string (or std::string_view) to avoid this class of errors entirely.
A variant that fools people: const char* a = "hello"; const char* b = "hello"; a == b may print equal, because the compiler is allowed to merge identical string literals into one copy. The code seems to work, then stops working when one string comes from user input. That inconsistency is the telltale sign of an address comparison.
Returning Address of a Local Variable
// WRONG: local variable destroyed when function returns — dangling pointer
int* getNumber() {
int x = 42;
return &x; // x is destroyed at function end!
}
int* ptr = getNumber();
std::cout << *ptr; // undefined behavior — reads destroyed memory
// CORRECT option 1: return by value (most common)
int getNumber() {
return 42; // copy of value returned safely
}
// CORRECT option 2: return heap-allocated via smart pointer
std::unique_ptr<int> getNumber() {
return std::make_unique<int>(42);
}
// CORRECT option 3: output parameter (use when avoiding allocation)
void getNumber(int& result) {
result = 42;
}
Part 3: Logic Mistakes
Assignment = Instead of Comparison ==
// WRONG: assigns 0 to x (condition is always false)
int x = 5;
if (x = 0) { // assigns 0 to x, then tests if 0 is truthy
std::cout << "zero\n"; // never reached
}
std::cout << x; // prints 0, not 5!
// CORRECT: == for comparison
if (x == 0) {
std::cout << "zero\n";
}
Trick: “Yoda conditions” — put the constant on the left. If you accidentally use =, it’s a compile error (can’t assign to a constant):
if (0 == x) { ... } // Yoda: "if zero equals x"
if (nullptr == ptr) { ... }
Many linters and compilers warn about assignment in conditions. Enable warnings with -Wall -Wextra.
Integer Division Truncation
// WRONG: integer division — the fractional part is discarded
int total = 7;
int count = 2;
double average = total / count; // 7/2 = 3, not 3.5
std::cout << average; // prints 3, not 3.5
// CORRECT: cast to double before dividing
double average = static_cast<double>(total) / count;
// or
double average = (double)total / count; // C-style cast, same effect
// or
double average = 1.0 * total / count; // multiply by 1.0 to promote
std::cout << average; // 3.5
When both operands of / are integers, C++ performs integer division (truncates toward zero). To get floating-point division, at least one operand must be a floating-point type. The key point is that the type of the variable on the left (double average) plays no role: the division total / count is evaluated first, as int, and only the result 3 is converted to 3.0. Casting the result, static_cast<double>(total / count), is therefore the same mistake with more typing. The same rule gives -7 / 2 == -3 and -7 % 2 == -1, which surprises people coming from Python, where // rounds toward negative infinity.
Unsigned Integer Underflow
// WRONG: unsigned can't be negative — wraps to huge number
size_t size = 0; // size_t is unsigned
size_t result = size - 1; // wraps to 18446744073709551615 on 64-bit!
// Common hidden version with .size():
std::vector<int> v = {1, 2, 3};
for (size_t i = 0; i < v.size() - 1; ++i) // WRONG if v is empty: 0-1 wraps
process(v[i], v[i + 1]);
// CORRECT: check before subtracting from unsigned
if (!v.empty()) {
for (size_t i = 0; i < v.size() - 1; ++i)
process(v[i], v[i + 1]);
}
// Or use signed integer for the loop
for (int i = 0; i < static_cast<int>(v.size()) - 1; ++i)
process(v[i], v[i + 1]);
// Or use iterators (no index arithmetic)
for (auto it = v.begin(); it + 1 != v.end(); ++it) // still needs !v.empty(): begin()+1 is past end()
process(*it, *(it + 1));
// Or rewrite the condition so nothing is subtracted
for (size_t i = 0; i + 1 < v.size(); ++i)
process(v[i], v[i + 1]);
The last form is the one I reach for: moving the - 1 to the other side as i + 1 < v.size() makes the empty case fall out naturally, with no special check. The underflow version is particularly nasty because it passes every test with non-empty input, then reads far out of bounds (usually a segfault) the first time an empty vector shows up. -Wsign-compare (part of -Wall for C++) flags the related mistake of comparing a signed int against v.size(), which is worth fixing rather than silencing with a cast.
Reading Compiler Errors
Fix the first error only, then compile again; later errors are often consequences of the first. Read the file and line (main.cpp:15:3:), then the kind (error:, warning:, note: for context), and compile with warnings on (g++ -Wall -Wextra -std=c++17) so many of the mistakes above are reported before they become runtime bugs. How to tell compiler, linker and runtime errors apart, and what the ten most common messages mean, is covered in 10 C++ Compile and Link Errors Beginners Hit First.
Debugging Habits
# Use a small reproducer — comment out code until the error disappears
# Then you know what caused it
# For runtime crashes: use AddressSanitizer
g++ -fsanitize=address -g main.cpp -o main && ./main
# Reports buffer overflows, use-after-free, and more with exact line numbers
# Online compilers for quick tests
# godbolt.org — multiple compilers, instant feedback
# wandbox.org — run code and see output
Top Five to Memorize
};— class/struct definitions need a semicolon after the closing brace#include— each standard name needs its header; error “not declared” means a missing includestd::— prefix standard names, or useusing std::cout;declarations in.cppfiles- Initialize pointers — always
int* ptr = nullptr;before use; check before dereferencing < size, not<= size— array/vector indices are 0 to size-1; use range-for to avoid indexing entirely
Frequently Asked Questions (FAQ)
Q. Why does if (x = 5) compile at all?
A. Assignment is an expression whose value is the assigned value, so x = 5 yields 5, which converts to true: the branch always runs and x is overwritten. The compiler can warn about it: GCC and Clang with -Wall suggest parentheses around an assignment used as a truth value, and MSVC reports C4706 at /W4. Turning on warnings, and ideally treating them as errors, catches this and several other mistakes on this list.
Related Articles
- 10 C++ Compile and Link Errors Beginners Hit First
- C++ Segmentation Fault: Five Causes and Debugging with GDB
- What Is C++? History, Standards and Use Cases
- C++ Pointers Explained
- C++ Development Environment Setup