One useful approach to use and avoidance of global variables is to consider applications and the conflict between
single or multiple documents per process.
It is useful to group the state variables in an application between "application state" (application configuration, like which windows are shown and where, what language is the user interface, and so on) and "document state" (document or current task configuration, like say page size, content language, and so on).
If you write an application that creates a new process for each document opened, then you can keep document state in global variables just like application state. However, each instance is separate, so if you e.g. change the user interface language in one, other open instances are not affected.
(Note that you do not actually waste memory doing this in current operating systems, because they use virtual memory, and directly map libraries and executables to memory. Each process has their own stack and kernel metadata, and of course any read-write data memory segments/sections/areas, but there is only one copy of the read-only code segments in memory.)
If you write an application that can handle multiple documents at once, you have "application state" (configuration) and "document state" (document settings). While "application state" is global to the entire process, the "document state" is specific to each document, and you must not keep any document state in global variables. If the user starts a new copy of the application while one is running, instead of running normally, the new instance just sends an event or file open request or new document request to the already running application, and then exits.
Now, extend that idea to a library, or even to a facility in your own program or microcontroller hardware.
If there cannot be more than one instance because of physical limitations, then global state is fine. However, often it is more useful to be able to create multiple instances of the thing/facility; and then you cannot use global state.
Here is a practical example. I love to use the Xorshift64* pseudo-random number generator, because it is extremely fast, and because if one uses only the 32 high bits of the result, it passes all randomness test in the BigCrush randomness test suite:
#include <stdint.h>
/* Xorshift64* PRNG state.
To randomize the sequence, initialize to any nonzero value.
*/
static uint64_t prng_state = 1;
static inline uint32_t prng_u32(void)
{
uint64_t x = prng_state;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
prng_state = x;
return (x * UINT64_C(2685821657736338717)) >> 32;
}
I left out the randomizing function, because I like to use the Linux-specific
getrandom() call for it, but one could also use POSIX
clock_gettime() or even the C89
time() or BSD
gettimeofday() for time-based initialization.
The above works, is very fast and very random (assuming you choose a random initial 64-bit seed,
prng_state, as otherwise it obviously produces always the same sequence).
However, you only have one generator per process.
Let's say you have a multithreaded process, but you still need repeatable results (based on seeds for each thread or task at hand), so you cannot use one global pseudorandom number generator. What to do?
Well, make it non-global, of course. The first step:
typedef struct {
uint64_t state;
} prng;
#define PRNG_INIT(seed) { .state = (seed) }
/* void prng_randomize(prng *g); -- omitted, because it tends to be OS-specific */
void prng_init(prng *g, uint64_t seed)
{
if (g) {
/* Note: zero seed is not allowed, so we replace that with 1. */
g->state = seed | !seed;
}
}
uint32_t prng_u32(prng *g)
{
if (g) {
uint64_t x = g->state;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
g->state = x;
return (x * UINT64_C(2685821657736338717)) >> 32;
} else {
return 0;
}
}
This way, in your own code, you just declare a random number generator you want to use, and initialize it to the desired seed,
prng mine = PRNG_INIT(1); /* Seeded with 1 */or equivalently
prng mine; prng_init(&mine, 1);or randomized via
prng mine; prng_randomize(&mine);and then generate the pseudorandom sequence using
prng_u32(&mine) calls.
(It is straightforward to extend such a state structure to contain both the needed state, as well as a pointer to the generator function, so that one can choose the PRNG implementation
at run time. Since the high 32 bits of Xorshift64* beats even Mersenne Twister in randomness, I don't bother anymore, though.)
See how the concept of "state" generalizes? It is quite intuitive, and functional approach to global variables.
A counterexample, and a common one seen in Arduino sketches:
int i;
void foo(void)
{
/* ... */
for (i = 0; i < 5; i++) {
/* ... */
bar();
}
}
void bar()
{
/* ... */
for (i = 0; i < 15; i++) {
baz();
}
/* ... */
}
The bug is obvious: whenever we call
bar() from within the loop in
foo(), the loop index variable gets modified, and thus the code does something completely different than what a straightforward reading of the code would indicate. In this case,
foo() will only call
bar() once.
The key is to think about
why this is a bug. We've erroneously globalized the internal state of
foo(), which then gets unexpectedly modified by a call to
foo().
Apologies for the wall of text, but hopefully it was worth reading.