C++ / FUNDAMENTAL TYPES AND VARIABLES
Variables, declarations, and initialisation styles
Declare variables in C++ with copy, direct, brace, and value initialisation, and know exactly when a variable holds zero, a value you chose, or garbage.
What you will learn
- Write int x{}; when you need a guaranteed zero instead of an indeterminate local
- Separate initialisation, which happens once at creation, from later assignment
- Read int* p, q; correctly: the * binds to p, so q is a plain int
- Predict whether a variable without an initialiser is zeroed from its storage duration
Understanding Variables, declarations, and initialisation styles
A C++ declaration is built from a type specifier followed by one or more declarators: in int x, *p, a[4]; the specifier is int, and x, *p and a[4] are declarators that each derive a different type from it. The declarator, not the specifier, decides whether a name ends up being an object, a pointer, an array, or a function, which is why the * in int* p, q; applies to p alone. A declaration that also brings an object into existence is a definition, and only a definition can carry an initialiser.
Initialisation is the one-time act of giving an object a value as it comes into existence, and it has four spellings: copy initialisation int a = 7;, direct initialisation int b(7);, list initialisation int c{7};, and value initialisation int d{};. Anything that happens afterwards, including d = 7;, is assignment, which overwrites an object that already exists. That distinction is not pedantry: const objects and references have no assignment step available at all, so the initialiser is their only chance to get a value.
If you omit the initialiser you get default initialisation, and for built-in types the result depends on storage duration rather than on syntax. A local int n; has automatic storage duration, so its bytes are whatever was already sitting in that stack slot; reading it is undefined behaviour, not "a random number", and the optimiser may assume such a read never happens. The same int n; at file scope, or with static, has static storage duration and is zero-initialised before main runs, which is why testing the rule with a global leads people to conclude wrongly that C++ zeroes everything. Writing int n{}; asks for value initialisation and gives a defined zero wherever the variable lives.
<iostream>
int main() {
int a = 7; // copy initialisation
int b(7); // direct initialisation
int c{7}; // list initialisation
int d{}; // value initialisation: guaranteed 0
int e; // default initialisation: indeterminate, unsafe to read
e = 7; // assignment, not initialisation
const double half = 0.5; // const gets no second chance, so an initialiser is required
std::cout << a << ' ' << b << ' ' << c << ' ' << d << ' ' << e << '\n';
std::cout << half << '\n';
}
Naming a variable and giving it a value are separate acts: with no initialiser a local built-in variable holds an indeterminate value, while {} guarantees a defined zero.
Worked examples
Storage duration decides who gets zeroed
The same missing initialiser is harmless at file scope and dangerous inside a function.
<iostream>
int g; // static storage duration: zero-initialised before main
int nextCall() {
static int calls; // static local: zero-initialised once, keeps its value
++calls;
return calls;
}
int main() {
std::cout << g << '\n';
std::cout << nextCall() << '\n';
std::cout << nextCall() << '\n';
std::cout << nextCall() << '\n';
}
Example explained
Line 1int g; has no initialiser, but static storage duration means its bytes are zeroed before any code runs, so printing it is well defined.
Line 2static int calls; is initialised to 0 exactly once, not on every call, which is why the counter keeps climbing.
Line 3Remove the static and calls becomes an ordinary local with an indeterminate value: ++calls would then read garbage and the program would have undefined behaviour.
Line 4Nothing in the syntax distinguishes these cases, so you have to look at where the variable lives.
One specifier, several declarators
How the * in a declaration binds to a single declarator instead of the whole line.
<iostream>
int main() {
int i = 1, j = 2;
int* p = &i, q = 5; // p is int*, q is int
std::cout << i << ' ' << j << ' ' << *p << ' ' << q << '\n';
p = &j;
*p = 20;
std::cout << j << ' ' << q << '\n';
}
Example explained
Line 1int i = 1, j = 2; is one declaration carrying two declarators, and each declarator needs its own initialiser.
Line 2In int* p = &i, q = 5; the * is part of the declarator *p, so only p is a pointer while q is a plain int holding 5.
Line 3Putting the * next to the type is purely a formatting choice; it does not change how the comma-separated list is parsed.
Line 4*p = 20; writes through the pointer into j, leaving q untouched, which shows the two names really do have different types.
Empty parentheses declare a function
Why int x(); is not a zero-initialised variable, and what {} does instead.
<iostream>
int seven() { return 7; }
int main() {
int seven(); // parsed as a function declaration, not a variable
int n{}; // really a variable, value-initialised
std::cout << n << '\n';
std::cout << seven() << '\n';
}
Example explained
Line 1Parentheses in a declarator mean "function taking these parameters", so int seven(); declares a function with no parameters returning int.
Line 2That block-scope declaration has external linkage, so seven() calls the function defined above and prints 7.
Line 3int n{}; cannot be read as a function declaration, so braces give unambiguous value initialisation and n is 0.
Line 4If you write int n(); expecting a zero, the error surfaces later as a link error or as "n does not refer to a value" when you try to use it.
Important notes
{} means value-initialise, which is zero for built-in types but a default-constructor call for class types, so std::string s; and std::string s{}; are both the empty string; the difference only shows for plain structs of built-ins.
Compile with -Wall -Wextra (or /W4), but remember -Wmaybe-uninitialized is a heuristic that usually needs optimisation enabled and can miss a read that is still undefined behaviour.
Common mistakes
Writing int sum; and then sum += x; in a loop: the first read is undefined behaviour, and it often looks like 0 in a debug build and a huge number once optimisations are enabled, which gets misdiagnosed as a compiler bug.
Testing the rule with a global int n;, seeing 0, and assuming locals behave the same way: storage duration is what differs, so the identical line inside a function yields an indeterminate value.
Writing const int limit; and planning to assign the value later: it fails to compile because a const object's initialiser is its only opportunity to get a value.
Try it yourself
Change, predict, then run
In one main, declare five ints using = 5, (5), {5}, {}, and no initialiser at all, assign 5 to the last one before printing, and print all five on a single line. Then add const int k; on its own line, record the exact compiler error, and delete it.
Open the C++ workspaceCheck your understanding
Both int a; at file scope and int b; inside a function compile. Why is printing a well defined while printing b is not?
- a has static storage duration and is zero-initialised before the program starts, while b is default-initialised in automatic storage and its value is indeterminate
- a is implicitly const at file scope, and const objects are always zeroed
- The compiler zeroes both, but printing b is undefined because it has not been assigned since the zeroing
- b is indeterminate only in debug builds; with optimisations enabled the compiler zeroes automatic variables
Show answer
The two declarations are spelled identically, so the syntax cannot be what differs: it is storage duration. Static-duration objects are zero-initialised before main runs, while an automatic variable simply names a piece of stack that nothing has written to. Option 3 is tempting because uninitialised locals so often print 0, but that is leftover data, not a guaranteed zero, and reading it is undefined behaviour rather than reading a stale value.