Why this matters
A variable is a named place in memory that holds a value. When you write int total; you ask for a place that can hold a whole number, and you give it a name. You do not say what the value is. In C++, a local int that you do not set holds whatever bits were in that memory before. This is an uninitialised variable. It is a common bug in C++ code, and it is hard to spot because the program often prints a number that looks normal.
This lesson shows which variables start with a value and which do not, how the bug hides, and how to write code so that every variable has a known value from the start. The lesson on booleans and the = trap covers another assignment mistake. The lesson on naming and reading declarations covers how to read the type and the name in a line like int total;.
What an uninitialised variable is
Start with a small program. It declares a variable, does not give it a value, and prints it.
The compiler reads the program and prints a warning before it runs. The exact text depends on the compiler. With g++ 15.2 and the flag -Wall, the warning is:
u1.cpp: In function ‘int main()’:
u1.cpp:6:28: warning: ‘total’ is used uninitialized [-Wuninitialized]
6 | cout << "total is " << total << endl;
| ^~~~~
u1.cpp:5:9: note: ‘total’ was declared here
5 | int total;
| ^~~~~The warning points at the line where total is read, and at the line where it was declared. The line numbers match the file you wrote.
When this program ran, it printed:
total is 0The value is 0 by chance in this run. The bits in that memory happened to be zero when the program ran. Another run, another compiler, or another change in the code can leave a different number there. The program has no way to know what total should be, so it prints whatever is in the memory.
A variable with no value is called uninitialised. The word "initialise" means to give a variable its first value. The C++ standard says that reading an uninitialised variable of type int is undefined behaviour. In plain words, the standard does not say what happens, so the program is not guaranteed to do anything sensible. The compiler is allowed to assume the read never happens, and it can produce output that does not match the code you wrote.
Where the leftover bits come from
A local variable is a variable declared inside a function, such as total above. The program stores local variables in a region of memory called the stack. When a function returns, its local variables stop being in use, but their bits are not erased. The next function that uses the same part of the stack gets the old bits. So an uninitialised variable often holds the value from an earlier, unrelated part of the program.
That is why the value can look reasonable. It may be a number from a previous calculation, a pointer, or a part of a different variable. Nothing in the program tells you which one it is.
Which variables start with a value
Not every variable is uninitialised. The rules depend on where the variable is declared and what type it has. The next program declares four kinds of variable and prints them.
The compiler prints a warning for local_count only:
u2.cpp: In function ‘int main()’:
u2.cpp:11:34: warning: ‘local_count’ is used uninitialized [-Wuninitialized]
11 | cout << "local_count = " << local_count << endl;
| ^~~~~~~~~~~
u2.cpp:8:9: note: ‘local_count’ was declared here
8 | int local_count;
| ^~~~~~~~~~The output was:
global_count = 0
saved = 0
local_count = 0Here are the rules this program shows.
A global variable is one declared outside every function, such as global_count. The compiler sets it to zero before the program starts. Reading it is safe and gives 0.
A static variable is declared with the word static inside a function. It keeps its value between calls to the function, and it is also set to zero before the program starts. saved prints 0.
A local variable of a built-in type, such as int, double or bool, is not set. local_count is one of these. This is the case that causes trouble. The output shows 0 here, but that is luck, and the warning tells you so.
A local variable of a class type, such as std::string or std::vector, is set by the class itself. The class has a constructor, which is a piece of code that runs when an object is made and sets it to a valid starting state. An empty std::string or an empty std::vector is always a valid object, even when you write nothing after the name. The problem is only with the plain built-in types.
Structs and arrays
A struct groups several values into one type. A plain struct with int members has the same problem as a local int. The next program declares a struct with two int members and an array of four int values, and prints them without setting any of them.
The compiler warns about all three reads. The program printed different numbers on two runs. The first run printed:
p.x = -815055425, p.y = 32192
values[0] = 1The second run printed:
p.x = -1580515905, p.y = 29082
values[0] = 1The numbers change between runs because the bits in memory depend on what the program did before. Your numbers will differ too. A program that prints garbage like this is clearly wrong. A program that uses the garbage as input to a calculation can be wrong in ways that look fine.
The bug can stay hidden
The most dangerous case is a program that uses an uninitialised variable in a calculation and prints a plausible result. The next example computes the average of three scores. The sum starts without a value. The static_cast<int> converts the list size to an int, so the division is a whole-number division.
The correct average of 70, 85 and 90 is 81, because the total is 245 and 245 divided by 3 is 81 with the decimal part dropped. Without optimisation, which is the default for g++ when you pass no -O flag, the program printed garbage. Two runs printed (your numbers will differ):
average = -369073174
average = 574645225The compiler did not warn about this program when it was built without optimisation. With the flag -O2, which asks g++ to optimise the code, the compiler did warn:
r1.cpp: In function ‘int main()’:
r1.cpp:9:13: warning: ‘sum’ is used uninitialized [-Wuninitialized]
9 | sum += s;
| ~~~~^~~~
r1.cpp:7:9: note: ‘sum’ was declared here
7 | int sum;
| ^~~The optimised build printed average = 81. That is the right answer, but only because the memory happened to hold 0 when the program started. This is the trap. The same source code gives a wrong result in one build and the right result in another, and the right result does not mean the code is correct.
Same binary, different results
The compiler setting changes what the program does with the leftover bits. The next program has an if statement that sets grade only when the score is 50 or more. The score here is 42, so the if body never runs and grade is never set.
#include <iostream>
using namespace std;
int main() {
int score = 42;
int grade;
if (score >= 50) {
grade = 1;
}
cout << "grade = " << grade << endl;
return 0;
}Built without optimisation, the program printed grade = 1, with no warning. Built with -O2, the compiler printed this warning and the program printed grade = 0:
u3.cpp: In function ‘int main()’:
u3.cpp:10:27: warning: ‘grade’ is used uninitialized [-Wuninitialized]
10 | cout << "grade = " << grade << endl;
| ^~~~~
u3.cpp:6:9: note: ‘grade’ was declared here
6 | int grade;
| ^~~~~The score is 42, so the correct grade is not 1. The program printed 1 in one build and 0 in another, and neither value is reliable. A test that checks only one build can pass, while the same program fails in a release build.
A good habit is to build with warnings on, both with and without -O2. Warnings about uninitialised variables depend on the optimisation level, so a program can be clean in one build and warned about in another.
How to fix it
The fix is to give every variable a value when you declare it. C++ has two main ways to write this.
The first is the assignment form, which uses =:
int grade = 0;The second is the brace form, which uses {}:
int grade{0};The brace form is a little stricter. It refuses to convert a value that would lose information, such as putting 2.5 into an int. The empty brace form int sum{}; sets the variable to zero for built-in types. Use the one your team uses, and be consistent.
The next program shows the forms that set a value. Every line prints the value you would expect.
The output is:
0 0 0 [] 0
0 0 0 0The program compiled without any warnings. Each variable has a known starting value:
total{}is zero.averageandfoundare set by their assignments.nameis an emptystd::stringbecausestd::stringsets itself.itemsis an emptystd::vector.pis a struct whose members have default values of zero, because each member has{}after its name. Without{}, the members would be uninitialised again. Adding{}to each member is the fix for a struct.arris an array with all three values set to zero.
Two more forms are useful in the same way. Point p{}; sets every member to zero without changing the struct, and Point q{1}; sets x to 1 and y to zero. Any member you leave out in the braces is set to zero.
The auto keyword needs a value to work out the type of a variable. It does not compile without one, as the next section shows.
Common mistakes
Mistake 1: a counter or sum that never gets a starting value
This is the most common version. The programmer writes int count; and then adds to it inside a loop. The first addition starts from a garbage value.
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> values = {3, 8, 12, 5};
int big;
for (int v : values) {
if (v > 4) {
++big;
}
}
cout << "values above 4: " << big << endl;
return 0;
}The fix is int big{};. With that change the program prints values above 4: 3, because 8, 12 and 5 are above 4.
Mistake 2: writing auto without a value
The keyword auto asks the compiler to work out the type of a variable from its value. Without a value there is nothing to work from, so the compiler stops with an error:
int main() {
auto x;
return x;
}The error is:
r3.cpp: In function ‘int main()’:
r3.cpp:2:5: error: declaration of ‘auto x’ has no initializer
2 | auto x;
| ^~~~The fix is to give auto a value, such as auto x = 0;. The compiler then knows that x is an int.
Mistake 3: trusting a build with no warnings
A program with no warnings is not proven correct. The uninitialised-variable warning is produced only in some cases. The warning in the grade example appeared only with -O2, not in the normal build. The fix is to give every variable a value, so that no build can read garbage. Do not rely on the compiler to find the bug for you.
Mistake 4: thinking every type starts at zero
Only globals and statics start at zero automatically. A local int does not. Some programmers learn this from a global example and then write a local with no value. The safe rule is simple: if the variable is local and has a built-in type, write a value at the point where you declare it.
Practice
Exercise 1. The program below should print the number of values above 4. It prints the wrong number, or a different number on each run. Find the bug and fix it.
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> values = {3, 8, 12, 5};
int big;
for (int v : values) {
if (v > 4) {
++big;
}
}
cout << "values above 4: " << big << endl;
return 0;
}Show the answer
The variable big is never given a value, so ++big adds one to a leftover number. The fix is to give big a value of zero when it is declared:
int big{};After the change, the program prints values above 4: 3.
Exercise 2. The code below sets grade only in one branch. Change it so that grade always has a defined value, and say what it prints for a score of 42.
int score = 42;
int grade;
if (score >= 50) {
grade = 1;
}
cout << "grade = " << grade << endl;Show the answer
Give grade a value when it is declared, for example int grade{0};. The if still runs only when the score is 50 or more. For a score of 42, the program prints grade = 0 in every build.
Exercise 3. A struct is declared as shown below. What do c.retries and c.verbose contain when the program starts, and how do you make them zero?
struct Config {
int retries;
bool verbose;
};
int main() {
Config c;
return 0;
}Show the answer
Both members hold leftover bits, so their values are not known. Write Config c{}; in place of Config c; to set both to zero. c.retries is then 0, and c.verbose is false, which prints as 0 when boolalpha is not set.
Exercise 4. Explain why the following line does not compile, and fix it so that x has the type int with the value 0.
auto x;Show the answer
The auto keyword needs a value to work out the type, and this line gives none. The compiler reports declaration of ‘auto x’ has no initializer. Write auto x = 0; instead. The type of x is int, because 0 is an int literal.
Exercise 5. A variable declared outside every function is written as int hits;. Is it safe to print before anything sets it? What does it print?
Show the answer
Yes. A global variable is set to zero before the program starts. The program below prints hits starts as 0:
#include <iostream>
int hits;
int main() {
std::cout << "hits starts as " << hits << "\n";
return 0;
}Global variables are safe in this respect, but a global that is changed by many parts of a program is still hard to follow. Prefer local variables with a value, passed to functions where they are needed.
Summary
- A local variable of a built-in type, such as
int, has no value until you give it one. It holds leftover bits from memory. - Reading an uninitialised variable is undefined behaviour, so the program can print any value and the compiler can change its meaning.
- Globals and statics start at zero. Local built-in types do not, and struct members and arrays do not either unless you add
{}. - Give every variable a value when you declare it, with
= 0or{0}, or use{}to set a value of zero. - Build with
-Wallat both the normal level and with-O2. Some uninitialised-variable warnings appear only with optimisation.