MFC Programmer's SourceBook : Thinking in C++
Bruce Eckel's Thinking in C++, 2nd Ed Contents | Prev | Next

Static elements from C

In both C and C++ the keyword static has two basic meanings, which unfortunately often step on each other’s toes:

  1. Allocated once at a fixed address; that is, the object is created in a special static data area rather than on the stack each time a function is called. This is the concept of static storage.
  2. Local to a particular translation unit (and class scope in C++, as you will see later). Here, static controls the visibility of a name, so that name cannot be seen outside the translation unit or class. This also describes the concept of linkage, which determines what names the linker will see.
This section will look at the above meanings of static as they were inherited from C.

static variables inside functions

Normally, when you create a variable inside a function, the compiler allocates storage for that variable each time the function is called by moving the stack pointer down an appropriate amount. If there is an initializer for the variable, the initialization is performed each time that sequence point is passed.

Sometimes, however, you want to retain a value between function calls. You could accomplish this by making a global variable, but that variable would not be under the sole control of the function. C and C++ allow you to create a static object inside a function; the storage for this object is not on the stack but instead in the program’s static storage area. This object is initialized once the first time the function is called and then retains its value between function invocations. For example, the following function returns the next character in the string each time the function is called:

//: C10:Statfun.cpp
// Static vars inside functions
#include <iostream>
#include "../require.h"
using namespace std;

char onechar(const char* string = 0) {
  static const char* s;
  if(string) {
    s = string;
    return *s;
  }
  else
    require(s, "un-initialized s");
  if(*s == '\0')
    return 0;
  return *s++;
}

char* a = "abcdefghijklmnopqrstuvwxyz";

int main() {
  // Onechar(); // require() fails
  onechar(a); // Initializes s to a
  char c;
  while((c = onechar()) != 0)
    cout << c << endl;
} ///:~ 

The static char* s holds its value between calls of onechar( ) because its storage is not part of the stack frame of the function, but is in the static storage area of the program. When you call onechar( ) with a char* argument, s is assigned to that argument, and the first character of the string is returned. Each subsequent call to onechar( ) without an argument produces the default value of zero for string, which indicates to the function that you are still extracting characters from the previously initialized value of s. The function will continue to produce characters until it reaches the null terminator of the string, at which point it stops incrementing the pointer so it doesn’t overrun the end of the string.

But what happens if you call onechar( ) with no arguments and without previously initializing the value of s? In the definition for s, you could have provided an initializer,

static char* s = 0;

but if you do not provide an initializer for a static variable of a built-in type, the compiler guarantees that variable will be initialized to zero (converted to the proper type) at program start-up. So in onechar( ), the first time the function is called, s is zero. In this case, the if(!s) conditional will catch it.

The above initialization for s is very simple, but initialization for static objects (like all other objects) can be arbitrary expressions involving constants and previously declared variables and functions.

static class objects inside functions

The rules are the same for static objects of user-defined types, including the fact that some initialization is required for the object. However, assignment to zero has meaning only for built-in types; user-defined types must be initialized with constructor calls. Thus, if you don’t specify constructor arguments when you define the static object, the class must have a default constructor. For example,

//: C10:Funobj.cpp
// Static objects in functions
#include <iostream>
using namespace std;

class X {
  int i;
public:
  X(int ii = 0) : i(ii) {} // Default
  ~X() { cout << "X::~X()" << endl; }
};

void f() {
  static X x1(47);
  static X x2; // Default constructor required
}

int main() {
  f();
} ///:~ 

The static objects of type X inside f( ) can be initialized either with the constructor argument list or with the default constructor. This construction occurs the first time control passes through the definition, and only the first time.

Static object destructors

Destructors for static objects (all objects with static storage, not just local static objects as in the above example) are called when main( ) exits or when the Standard C library function exit( ) is explicitly called, main( ) in most implementations calls exit( ) when it terminates. This means that it can be dangerous to call exit( ) inside a destructor because you can end up with infinite recursion. Static object destructors are not called if you exit the program using the Standard C library function abort( ).

You can specify actions to take place when leaving main( ) (or calling exit( )) by using the Standard C library function atexit( ). In this case, the functions registered by atexit( ) may be called before the destructors for any objects constructed before leaving main( ) (or calling exit( )).

Destruction of static objects occurs in the reverse order of initialization. However, only objects that have been constructed are destroyed. Fortunately, the programming system keeps track of initialization order and the objects that have been constructed. Global objects are always constructed before main( ) is entered, so this last statement applies only to static objects that are local to functions. If a function containing a local static object is never called, the constructor for that object is never executed, so the destructor is also not executed. For example,

//: C10:StaticDestructors.cpp
// Static object destructors
#include <fstream>
using namespace std;
ofstream out("statdest.out"); // Trace file

class Obj {
  char c; // Identifier
public:
  Obj(char cc) : c(cc) {
    out << "Obj::Obj() for " << c << endl;
  }
  ~Obj() {
    out << "Obj::~Obj() for " << c << endl;
  }
};

Obj a('a'); // Global (static storage)
// Constructor & destructor always called

void f() {
  static Obj b('b');
}

void g() {
  static Obj c('c');
}

int main() {
  out << "inside main()" << endl;
  f(); // Calls static constructor for b
  // g() not called
  out << "leaving main()" << endl;
} ///:~ 

In Obj, the char c acts as an identifier so the constructor and destructor can print out information about the object they’re working on. The Obj a is a global object, so the constructor is always called for it before main( ) is entered, but the constructors for the static Obj b inside f( ) and the static Obj C inside g( ) are called only if those functions are called.

To demonstrate which constructors and destructors are called, inside main( ) only f( ) is called. The output of the program is

Obj::Obj() for a
inside main()
Obj::Obj() for b
leaving main()
Obj::~Obj() for b
Obj::~Obj() for a 

The constructor for a is called before main( ) is entered, and the constructor for b is called only because f( ) is called. When main( ) exits, the destructors for the objects that have been constructed are called in reverse order of their construction. This means that if g( ) is called, the order in which the destructors for b and c are called depends on whether f( ) or g( ) is called first.

Notice that the trace file ofstream object out is also a static object. It is important that its definition (as opposed to an extern declaration) appear at the beginning of the file, before there is any possible use of out. Otherwise you’ll be using an object before it is properly initialized.

In C++ the constructor for a global static object is called before main( ) is entered, so you now have a simple and portable way to execute code before entering main( ) and to execute code with the destructor after exiting main( ). In C this was always a trial that required you to root around in the compiler vendor’s assembly-language startup code.

Controlling linkage

Ordinarily, any name at file scope (that is, not nested inside a class or function) is visible throughout all translation units in a program. This is often called external linkage because at link time the name is visible to the linker everywhere, external to that translation unit. Global variables and ordinary functions have external linkage.

There are times when you’d like to limit the visibility of a name. You might like to have a variable at file scope so all the functions in that file can use it, but you don’t want functions outside that file to see or access that variable, or to inadvertently cause name clashes with identifiers outside the file.

An object or function name at file scope that is explicitly declared static is local to its translation unit (in the terms of this book, the .cpp file where the declaration occurs); that name has internal linkage. This means you can use the same name in other translation units without a name clash.

One advantage to internal linkage is that the name can be placed in a header file without worrying that there will be a clash at link time. Names that are commonly placed in header files, such as const definitions and inline functions, default to internal linkage. (However, const defaults to internal linkage only in C++; in C it defaults to external linkage.) Note that linkage refers only to elements that have addresses at link/load time; thus, class declarations and local variables have no linkage.

Confusion

Here’s an example of how the two meanings of static can cross over each other. All global objects implicitly have static storage class, so if you say (at file scope),

int a = 0;

then storage for a will be in the program’s static data area, and the initialization for a will occur once, before main( ) is entered. In addition, the visibility of a is global, across all translation units. In terms of visibility, the opposite of static (visible only in this translation unit) is extern, which explicitly states that the visibility of the name is across all translation units. So the above definition is equivalent to saying

extern int a = 0;

But if you say instead,

static int a = 0;

all you’ve done is change the visibility, so a has internal linkage. The storage class is unchanged – the object resides in the static data area whether the visibility is static or extern.

Once you get into local variables, static stops altering the visibility (and extern has no meaning) and instead alters the storage class.

With function names, static and extern can only alter visibility, so if you say,

extern void f();

it’s the same as the unadorned declaration

void f();

and if you say,

static void f();

it means f( ) is visible only within this translation unit; this is sometimes called file static.

Other storage class specifiers

You will see static and extern used commonly. There are two other storage class specifiers that occur less often. The auto specifier is almost never used because it tells the compiler that this is a local variable. The compiler can always determine this fact from the context in which the variable is defined, so auto is redundant.

A register variable is a local ( auto) variable, along with a hint to the compiler that this particular variable will be heavily used, so the compiler ought to keep it in a register if it can. Thus, it is an optimization aid. Various compilers respond differently to this hint; they have the option to ignore it. If you take the address of the variable, the register specifier will almost certainly be ignored. You should avoid using register because the compiler can usually do a better job at of optimization than you.

Contents | Prev | Next


Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru