In lesson 6.3 -- Local variables and shadowing, we covered that local variables are variables defined inside a function (or function parameters). Local variables have block scope (are only visible within the block they are declared), and have automatic duration (they are created at the point of definition and destroyed when the block is exited).
In C++, variables can also be declared outside of a function. Such variables are called global variables.
Declaring and naming global variables
By convention, global variables are declared at the top of a file, below the includes, but above any code. Here’s an example of a global variable being defined:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <iostream> // Variables declared outside of a function are global variables int g_x; // global variable g_x void doSomething() { // global variables can be seen and used everywhere in the file g_x = 3; std::cout << g_x << "\n"; } int main() { doSomething(); // global variables can be seen and used everywhere in the file g_x = 5; std::cout << g_x << "\n"; return 0; } // g_x goes out of scope here |
The above example prints:
3 5
By convention, many developers prefix global variable identifiers with “g” or “g_” to indicate that they are global.
Best practice
Consider using a g” or “g_” prefix for global variables to help differentiate them from local variables.
Global variables have file scope and static duration
Global variables have file scope (also informally called global scope or global namespace scope), which means they are visible from the point of declaration until the end of the file in which they are declared. Once declared, a global variable can be used anywhere in the file from that point onward! In the above example, global variable g_x
is used in both functions doSomething()
and main()
.
Because they are defined outside of a function, global variables are considered to be part of the global namespace (hence the term “global namespace scope”).
Global variables are created when the program starts, and destroyed when it ends. This is called static duration. Variables with static duration are sometimes called static variables.
Unlike local variables, which are uninitialized by default, static variables are zero-initialized by default.
Global variable initialziation
Non-constant global variables can be optionally initialized:
1 2 3 |
int g_x; // no explicit initializer (zero-initialized by default) int g_y {}; // zero initialized int g_z { 1 }; // initialized with value |
Constant global variables
Just like local variables, global variables can be be constant. As with all constants, constant global variables must be initialized.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <iostream> const int g_x; // error: const variables must be initialized const int g_y { 1 }; // const global variable g_y, initialized with a value constexpr int g_z { 2 }; // constexpr global variable g_z, initialized with a value void doSomething() { // global variables can be seen and used everywhere in the file std::cout << g_y << "\n"; std::cout << g_z << "\n"; } int main() { doSomething(); // global variables can be seen and used everywhere in the file std::cout << g_y << "\n"; std::cout << g_z << "\n"; return 0; } // g_y and g_z goes out of scope here |
Local variables can shadow global ones
Similar to how variables in a nested block can shadow variables in an outer block, local variables with the same name as a global variable will shadow the global variable wherever the local variable is in scope.
Because global variables are part of the global namespace, we can use the scope operator (::) with no prefix to tell the compiler we mean the global variable instead of the local variable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <iostream> int value { 5 }; // global variable int main() { int value = 7; // hides the global variable value ++value; // increments local value, not global value --(::value); // decrements global value, not local value (parenthesis added for readability) std::cout << "local variable value: " << value << "\n"; std::cout << "global variable value: " << ::value << "\n"; return 0; } // local value is destroyed |
This code prints:
local variable value: 8 global variable value: 4
For the same reason that we recommend avoiding shadowing local variables, we recommend avoiding shadowing global variables as well. This is trivially avoidable if all of your global names use a “g_” prefix.
A word of caution about (non-constant) global variables
New programmers are often tempted to use lots of global variables, because they can be used without having to explicitly pass them to every function that needs them. However, use of non-constant global variables should generally be avoided altogether! We’ll discuss why in a couple of lessons.
Quick Summary
1 2 3 4 5 6 7 8 9 10 11 12 |
// Non-constant global variables int g_x; // defines non-initialized global variable (zero initialized by default) int g_x {}; // defines explicitly zero-initialized global variable int g_x { 1 }; // defines explicitly initialized global variable // Const global variables const int g_y; // error: const variables must be initialized const int g_y { 2 }; // defines initialized global constant // Constexpr global variables constexpr int g_y; // error: constexpr variables must be initialized constexpr int g_y { 3 }; // defines initialized global const |
![]() |
![]() |
![]() |
I will give a concrete example of when global variables are necessary. This is not to imply that they should not be kept to a minimum --- that is certainly true.
Suppose you are a lattice gauge theorist writing a library intended to solve for quark propagators on GPUs (example, QUDA). It is going to be used by hundreds of people who are integrating it with other codes that they are already using. You want to present users of your library with as simple an interface as possible. That would just be a small set of functions to call, with no fancy abstract types, and perhaps a structure or two to contain parameters. The basic user will be solving a linear algebra problem, MX=B, where M is a large sparse matrix, B is a given "source" vector, and X will be the solution vector. For practical reasons this is done in two steps:
void load_gauge(void *gauge_array, Params *par);
void solve(void *solution, void *source);
load_gauge is done once, but solve is typically done many times with different sources. void pointers are used because different precisions of floating point number are in use, depending on a setting in par. That is for performance reasons. The array gauge_array is loaded to the GPU and a pointer (not gauge_array) to that memory on the GPU must be passed somehow to solve() so that its own functions that it calls to carry out the operation on the GPU can use this array for defining the matrix M. To keep the interface simple, and to hide all of these implementation details from users, the pointer to the GPU memory is passed between the functions using a global variable, say gpu_gauge_array. Note that it is not even in the same memory space as gauge_array.
The alternative is to force users of the library to adapt to a much more involved interface where they would call functions that are members of classes so that the pointer could be a member variable. This is far from optimal since half the community is actually using C code, not C++ code. By using the simple function interface given above, and a struct for the Params type, it can interface with either type of user. Of course everything in the interface code has to be declared with extern "C", but that is not a big deal.
The point of this example is that in the professional setting you may not be writing code just for yourself, or for one company, but you may be writing for a world-wide community that needs simple, flexible code to link to and use. Having a simple interface will often force some data into global scope.