Working with Global Variables
Declare variables accessible from any function and understand their initialization order.
What Is a Global Variable?
A global variable is a variable defined outside of every function. Because it sits at namespace scope rather than inside a block, the compiler gives it two properties at once: its name can be used by every function that appears after it in the file, and the object itself exists for the entire run of the program.
Those two properties are independent, and keeping them separate is the whole trick to reading global variables correctly. One is about the name and is decided at compile time. The other is about the object and is a fact about the running process. A local variable happens to bundle its name and its object together, so beginners often assume globals do too. They do not.
By convention a global variable is defined at the top of the file, just below the #include directives, before any function that uses it. The rest of this lesson explains why that convention exists rather than treating it as arbitrary.
A Controller That Needs Process-Wide State
Globals earn their place when a value genuinely belongs to the program rather than to any one function. A greenhouse controller is a fair example: however many times a sensor is read, there is exactly one running tally of samples and exactly one count of dry alerts.
#include <iostream>
// Defined outside every function, so every function below can reach them
namespace Greenhouse
{
int samplesLogged{}; // one object for the whole run
int dryAlerts{};
}
constexpr int dryThreshold{18}; // percent
void logSample(int moisturePercent)
{
++Greenhouse::samplesLogged;
if (moisturePercent < dryThreshold)
{
++Greenhouse::dryAlerts;
std::cout << "dry alert at " << moisturePercent << "%\n";
}
}
void printDailyTotals()
{
std::cout << "samples logged: " << Greenhouse::samplesLogged << '\n';
std::cout << "dry alerts: " << Greenhouse::dryAlerts << '\n';
}
int main()
{
logSample(31);
logSample(12);
logSample(27);
logSample(9);
printDailyTotals();
return 0;
}
dry alert at 12%
dry alert at 9%
samples logged: 4
dry alerts: 2
Notice what logSample() never had to do: it did not receive the tallies as parameters, and it did not return them. Neither did printDailyTotals(). The two functions communicate through the objects themselves, because both objects outlive every call.
Two Properties, Not One
Here is the same distinction laid out against the local variables you already know.
| Local variable | Global variable | |
|---|---|---|
| Where it is defined | Inside a function or block | Outside every function, at namespace scope |
| Scope, a compile-time property | From its definition to the closing brace of its block | From its declaration to the end of the translation unit |
| Duration, a run-time property | Automatic: born on entry, destroyed at the closing brace | Static: born before main() starts, destroyed after main() returns |
| How many objects exist | A fresh one for every call or block entry | Exactly one, for the whole run |
| Value when no initializer is given | Indeterminate, and reading it is undefined behavior | Zero |
| What can modify it | Only code inside that block | Any function that can see the name |
The "how many objects exist" row is the one that changes how programs behave. This function keeps one of each kind side by side:
#include <iostream>
int g_totalLiters{}; // one object, alive from before main() until after it returns
void openValve(int liters)
{
int sessionLiters{}; // a fresh object on every call, gone at the closing brace
sessionLiters += liters;
g_totalLiters += liters;
std::cout << "this session: " << sessionLiters
<< ", running total: " << g_totalLiters << '\n';
}
int main()
{
openValve(4);
openValve(7);
openValve(2);
return 0;
}
this session: 4, running total: 4
this session: 7, running total: 11
this session: 2, running total: 13
sessionLiters restarts at zero on every call because a new object is created each time. g_totalLiters keeps climbing because there has only ever been one of it.
Visibility Starts at the Declaration
A global's name is not magically available everywhere in the file. It is available from its declaration onwards, exactly like a function name. Move the definition below a function that uses it and the program stops compiling.
The following does not compile:
#include <iostream>
void reportZone()
{
std::cout << "zone " << g_activeZone << '\n'; // the name is not in scope yet
}
int g_activeZone{4};
int main()
{
reportZone();
return 0;
}
GCC rejects it with error: 'g_activeZone' was not declared in this scope, and it points at the use inside reportZone(), not at the definition. Lifting the definition above the function is all it takes:
#include <iostream>
int g_activeZone{4}; // at the top of the file, below the includes
void reportZone()
{
std::cout << "zone " << g_activeZone << '\n';
}
int main()
{
reportZone();
return 0;
}
zone 4
This is the entire reason for the "top of the file, below the includes" convention. Put your globals there and no function in the file can be written too early to see them.
Define global variables at the top of the file, immediately below the
#include directives and above every function. Anything lower down is a visibility bug waiting for the next function you add.
Note that the reach of the name is the translation unit, not literally the source file you are looking at. A header included after the definition is part of the same translation unit, so code in that header sees the global too.
Globals in a Namespace Are Still Globals
Putting a global in the global namespace makes its name compete with every other name in the program. Two subsystems that both want to call something level cannot both have it, and you end up inventing prefixes to keep them apart.
A user-defined namespace solves that properly, and the variables inside it are still global variables in every way that matters.
#include <iostream>
namespace Soil
{
int level{31}; // moisture, percent
}
namespace Tank
{
int level{86}; // water remaining, percent
}
int main()
{
std::cout << "soil " << Soil::level << "%, tank " << Tank::level << "%\n";
return 0;
}
soil 31%, tank 86%
Moving a variable into a namespace changes how you spell its name, not what it is.
Soil::level is defined outside every function, has static duration, and lives for the whole program run, so it is a global variable.
The qualified name carries a second benefit at the point of use. Soil::level announces itself as non-local, which a bare level never would.
Put your globals in a user-defined namespace. Leaving them bare in the global namespace buys nothing and costs you a name that the whole program has to work around.
Naming a Global So It Cannot Be Mistaken for a Local
A reader scanning the middle of a function needs to know whether an assignment they are looking at will still be visible after the function returns. Two conventions answer that question, and which one you need depends on where the variable lives.
- In the global namespace, prefix the name with
g_, as ing_totalLiters. The prefix keeps the name out of the way of unprefixed names, and it warns the reader that the assignment they are reading has effects beyond this function. - Inside a user-defined namespace, the qualifier already does that job.
Greenhouse::samplesLoggedcannot be mistaken for a local, so the prefix is optional. Some codebases keep it anyway as an extra reminder that the write persists.
Use a
g_ prefix for globals defined in the global namespace. For globals inside a namespace, let the qualified name do the work and keep the prefix only if your codebase already uses it consistently.
Globals Are Zero-Initialized, Locals Are Not
Local variables with no initializer hold indeterminate values, and reading one is undefined behavior. Variables with static duration are different: before any of your code runs, the compiler and runtime have already zero-initialized every one of them.
#include <iostream>
int g_zoneCount; // no initializer, but still zero-initialized
int g_faultCount{}; // value-initialized, which for an int also means zero
int main()
{
std::cout << "zones: " << g_zoneCount << '\n';
std::cout << "faults: " << g_faultCount << '\n';
return 0;
}
zones: 0
faults: 0
That guarantee is real, so int g_zoneCount; is not a bug. It is still worse code than int g_zoneCount{};, because a reader cannot tell whether you meant zero or forgot to type a value. Here is the full set of forms:
| Declaration | What you get |
|---|---|
int g_zoneCount; |
Zero-initialized, but silent about whether that was the intent |
int g_faultCount{}; |
Value-initialized, which for an int also means zero, and says so |
int g_activePump{2}; |
Initialized with a chosen value |
const int maxZones; |
Does not compile |
const int maxZones{6}; |
A constant every function in the file can read |
constexpr int pumpSeconds{45}; |
A constant usable where a constant expression is required |
Give every global an explicit initializer, even when the zero you would get by default is the value you want.
The two rows flagged in that table are worth seeing for real. Zero-initialization does not rescue a constant, because a constant gets exactly one chance to receive a value and leaving the initializer off wastes it. This does not compile:
#include <iostream>
const int maxZones; // no initializer
constexpr int pumpSeconds; // no initializer
int main()
{
std::cout << maxZones << ' ' << pumpSeconds << '\n';
return 0;
}
GCC reports error: uninitialized 'const maxZones' [-fpermissive] for the first and error: uninitialized 'const pumpSeconds' [-fpermissive] for the second. The wording is worth noticing: the compiler describes the constexpr variable as const too, because constexpr implies const on a variable.
When a Global Comes to Life, and When It Dies
Static duration is easy to state and easy to get slightly wrong, so it is worth being precise about the two things that happen before main() runs.
Static initialization comes first. Storage for every global is set aside and zero-filled, and any global whose initializer is a constant expression gets its real value at this point. No code of yours has run yet.
Dynamic initialization follows. Globals whose initializers require actually executing something, such as a function call, are initialized now. In a single-file program this finishes before the first statement of main(), which you can watch happen:
#include <iostream>
int readCalibrationOffset()
{
std::cout << "reading calibration offset\n";
return 3;
}
int g_calibrationOffset{readCalibrationOffset()};
int main()
{
std::cout << "controller starting\n";
std::cout << "offset in use: " << g_calibrationOffset << '\n';
return 0;
}
reading calibration offset
controller starting
offset in use: 3
At the other end, globals are destroyed after main() returns, in the reverse of the order they were initialized. For an int there is nothing to observe, but the ordering matters as soon as a global owns something that has to be cleaned up. The practical consequence is that a global is still alive during program shutdown, after the last line of main() has executed.
This is where a familiar comment style misleads people. With a local, writing } // goes out of scope here on the closing brace marks both the end of the name and the end of the object, because for locals those coincide. Written at the bottom of a file for a global, the same comment only marks the end of the name. The object is untouched by it and outlives main() regardless.
Dynamic initialization is only ordered within a single translation unit. Across two
.cpp files, nothing says which file's globals are initialized first, so a global initialized from a global in another file may read a zero that has not been overwritten yet. Never write that dependency.
When Not to Reach for a Global
The convenience on display in the first example is exactly what makes non-const globals dangerous. Any function in the file can write to Greenhouse::dryAlerts, which means that when the number comes out wrong, every function is a suspect. A parameter narrows the search to one call site; a global does not narrow it at all.
The rule of thumb for now:
- A constant global, especially a
constexprone in a namespace, is safe and useful. Nothing can change it, so it cannot surprise you. - A non-constant global is justified only when there is genuinely one of the thing in the entire program and passing it everywhere would add noise rather than clarity.
- Everything else should be a parameter, a return value, or a local.
New programmers reach for non-const globals because they remove the chore of passing values between functions. That chore is what keeps a program's data flow readable. Reach for a parameter first and a global only when you can say out loud why one object is the right number.
Looking Forward
Three follow-ups build directly on this lesson. Variable shadowing covers what happens when a local variable reuses a global's name, and how the compiler decides which one you meant. Linkage covers whether a global's name is reachable from other translation units, which is what makes const and non-const globals behave so differently across files. And the lesson on why non-const globals are evil takes the initialization hazard mentioned above and shows it biting.
Key Terminology
- Global variable: a variable defined outside of every function, at namespace scope
- Global namespace scope: the scope of a name declared outside every function and every namespace, running from the declaration to the end of the translation unit
- Static duration: a lifetime that begins before
main()starts and ends aftermain()returns - Static variable: any variable with static duration
- Zero-initialization: the first phase of setting up static-duration objects, which fills their storage with zero before any code runs
- Dynamic initialization: the later phase that runs initializers requiring actual execution, such as a function call
- Translation unit: one
.cppfile after the preprocessor has pasted in everything it includes
Summary
- A global variable is defined outside every function, conventionally at the top of the file just below the includes
- Its name is usable from the declaration to the end of the translation unit, so a function written above the definition cannot see it
- Its object has static duration: created before
main()begins and destroyed aftermain()returns, giving exactly one object for the whole program run - Scope and duration are separate properties. The end of the file ends the name, not the object
- Globals with no initializer are zero-initialized, unlike locals, whose values are indeterminate. Write the initializer anyway
- Constant globals must be initialized, and
constexpris the right choice when the value is known at compile time - A variable sitting in a user-defined namespace is still a global, and its qualified name is worth having at every use
- Prefix globals in the global namespace with
g_; inside a namespace the qualifier already signals non-local - Non-const globals let any function change the value, which is why they are the exception rather than the tool of first resort
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Working with Global Variables - Quiz
Test your understanding of the lesson.
Practice Exercises
Global Variables Basics
Understand global variable scope and lifetime, and see how they differ from local variables.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!