4.1 Overview
In C, a storage class determines where functions and variables are stored, how long variables persist, and where functions and variables can be made visible. For functions, the key issue is visibility (scope) because the lifetime of a function is the lifetime of the program that contains the function. For variables, both lifetime and scope are of interest to the programmer.
Storage classes also shed further light on the distinction between declarations and definitions in C. In large programs, with the constituent functions typically residing in different files, the distinction is especially important. Once again, code examples illustrate the basics and advanced features. The chapter ends with a discussion of type qualifier volatile, yet another aspect of C’s close-to-the-metal personality.
4.2 Storage Class Basics
The scope or visibility of the storage. For example, C functions are extern by default, which means that they can be made visible to any other function in the same program. To be extern in C is to be potentially global in scope.
The lifetime of the storage, which depends directly on where the storage is provided. The name storage class derives from the fact that different parts of the memory hierarchy are in play. For example, a local variable—that is, a variable defined inside a block—is auto by default. Storage for such a variable comes from the stack or a CPU register, and the variable’s lifetime is the time span during which the containing block is active because some instruction within the block is still executing.
A definition implements, whereas a declaration describes. A declared function describes how the function is called and excludes the function’s body; a defined function includes the body as well. For variables, the distinction matters only in the case of extern variables, where there is one definition but there can be more than one declaration. For variables of every other storage class, the definition and the declaration are effectively the same.
Functions are either extern or static, with extern as the default.
Variables defined outside of all blocks are either extern or static, with extern as the default.
Variables defined inside a block are either auto, or register, or static, with auto as the default.
In summary, neither static (functions or variables) nor register (variables only) is a default storage class. For a function or variable defined outside all blocks, extern is the default; for a variable defined inside a block, auto is the default.
On modern computers, C functions are stored in the text area of memory, and a function’s lifetime is accordingly the lifetime of the program to which the function belongs. However, a static function or variable is not visible outside of its containing source file, whereas an extern function or variable can be made visible throughout a program—no matter the file that contains its definition.
In the case of variables, in particular large arrays, the storage classes extern and static raise issues of efficiency. If an array is extern or static, then the array’s lifetime is the program’s lifetime. In effect, the size of the array becomes part of the program’s runtime memory footprint. It is best to keep arrays on the stack or the heap so that storage for the arrays persists only as needed.
4.3 The auto and register Storage Classes
The auto and register specifiers
The body of function main is the outer block, and int variables i and n are declared in this block. Each is visible from the point of its declaration until the end of the block, in this case the end of function main. In particular, local variables i and n are visible inside the for loop, a nested code block.
The for loop’s body is another block. Declared therein is the register variable r, which is visible only within the body of the for loop.
The declarations for variables i and n are equivalent, although only the one for variable i explicitly uses the auto specifier. Because auto is the default specifier for a variable declared inside a block, this specifier is almost never used—except for demonstration purposes, as in the autoreg program.
The register specifier , shown here in the declaration for variable r, also is rarely used in modern C, as clarified shortly. If the compiler cannot implement variable r with a CPU register, then the storage class reverts to the default, auto. The scope for auto and register variables is the same in any case: the containing block.
The register specifier has become outdated because an optimizing compiler tries to use a CPU register to store scalar values such as the ones stored in r during the for loop. It is more productive to flag the compiler for optimization (e.g., gcc -O1...) than to use the register specifier. The auto specifier also has become outdated because an optimizing compiler opts for CPU registers whenever possible and uses the stack as the fallback for scratchpad. From now on, the code examples dispense with explicit uses of auto and register.
4.4 The static Storage Class
The static specifier applies to both functions and variables. A variable can be declared as static either inside a block (with resulting block scope) or outside all blocks (with a scope from that point until the end of the file). The first code example deals with static variables.
A detailed profiling analysis is printed to the screen.
Using static variables to profile function calls
Because each variable is declared inside a function, each variable has function scope only. Accordingly, the two distinct variables can have the same name, in this case n.
Unlike an auto variable (stack based), a static variable (not stack based) maintains its state across function calls. For example, each time that the foo function is called, its variable n is incremented and retains this new value even when foo exits. An initialized auto variable would be reinitialized on every call to the function that encapsulates the variable.
A static variable has the lifetime of the program regardless of where the variable is declared, but its scope does differ depending on where the variable is declared. If declared inside a block, a static variable has block scope. If declared outside all blocks, a static variable has file scope: it is visible from the point of declaration until the end of the containing file.
To define a function as static is to restrict the function’s scope to the file in which it resides. Functions are extern by default, which means that they are potentially visible throughout the compiled program, regardless of the source file that happens to contain them. Making a function static is as close as it comes to private in C: static functions might be described as private to the file. Scope is the only difference that matters between extern and static functions: the former can have program scope, whereas the latter can have file scope only.
4.5 The extern Storage Class
The source code for a large program is likely distributed among many files. A function housed in one file may need to call a function housed in another file. For example, a program that invokes a library function such as printf is thereby calling a function housed in another file—the library’s delivery file. Furthermore, a program may require that the same variable—not just different variables with the same name—be accessible across files. But neither a static function nor a static variable can be made visible outside of its containing file. Such functions and variables have program lifetime due to their static character, but they have only file scope at most.
A variable or function is defined, implicitly or explicitly, as extern in one file. (A variable defined outside all blocks defaults to extern, and functions in general default to extern.) The term extern can but need not be used in the definition.
This variable or function is then declared as extern in any other file that requires access.
The rule of thumb for making life easy on the programmer is to avoid the explicit extern in a definition (in particular for variables) and to use the explicit extern only in a declaration.
One source file in the prog2files program
Defines the int variable global_num outside all blocks. This makes the variable extern, although the specifier extern does not occur in the definition. The variable also is initialized to -999. Were the variable not initialized explicitly, the compiler would set its value to 0. There is subtle syntax at play here. If the specifier extern were used, then the variable would have to be initialized explicitly in order to distinguish its single definition from one of its many possible declarations. The safe approach is to omit the specifier extern from the definition and to use this specifier only in declarations. The second file in the prog2files program shows a declaration for global_num with the specifier extern.
Declares the function doubleup as extern , thereby signaling that this function is defined elsewhere—in this case, in the other source file, prog2files2.c.
Defines the function print using the specifier extern. The extern is not necessary because any defined function is extern by default unless explicitly specified to be static.
Defines the function main as extern, but without using the specifier.
The other source file in the prog2files program
The variable global_num is declared with the specifier extern and not initialized. If the variable global_nums were initialized here, this would count as a definition, thereby breaking the rule that an extern variable (or function) must be defined exactly once in a program. The declaration for global_num occurs outside all blocks but could occur within the function doubleup. In any case, the declaration of global_num with the required specifier extern signals that this variable is defined elsewhere, which happens to be the other source file prog2files1.c.
The function doubleup is defined here and is extern by default. This function is declared in the other source file with the specifier extern.
Never use the specifier extern in function or variable definitions, which must occur outside all blocks. The variables then can be initialized or not according to need.
Use the specifier extern only in declarations of functions and variables. A variable cannot be initialized in a declaration, as this would transform the declaration into a definition.
Recall that the parameters to the qsort comparison function are const void*, in effect a promise that such pointers will not be used to modify the values pointed to.
4.6 The volatile Type Qualifier
The volatile qualifier cautions the compiler against doing any optimization on a variable so qualified, in this case n. For example, there are situations in which an optimizing compiler should not implement a variable as a CPU register. Two sample situations are introduced in the following.
The first example deals with an interrupt service routine (ISR) . As the name indicates, an ISR handles interrupts, which originate from outside the executing program. For example, imagine an ISR written in C to handle input from one of the machine’s data ports, for example, the port for the keyboard. The programmer might define and initialize a variable nextc to store the next character read from the keyboard. An optimizing compiler, unaware that the data source for the variable is outside the executing program, may reason that nextc acts within the program like a constant best implemented in read-only storage; in other words, the compiler sees the initialization but does not see any updates to nextc. As a result, the compiler might deliver only this initial value to functions that read nextc. This optimization would undermine the ISR ’s task of reading arbitrary characters from the keyboard.
A core is a fabrication component that contains a processing unit: one or more CPUs (processors), registers, cache memory, and other architectural components. A multicore machine is therefore a multiprocessor machine, with one or more CPUs per core; hence, a multicore machine can support true parallelism.
The second example concerns multithreading, which Chapter 7 covers in detail. In a multithreaded program , multiple threads of execution (sequences of instructions) can communicate with one another through shared memory, for example, through a global variable N that is visible across the threads because N is implemented as storage in main memory. On a multicore machine, however, the registers on a particular core would be visible only to a thread executing on the core’s processor(s). The point deserves emphasis: if thread T1 executes on core C1, then T1 sees only the registers on C1. If the compiler were to implement global variable N as a register on core C1, then threads executing on some other core would not see N. In short, it is important that N be implemented in main memory if N is to be visible across the multiple threads in the process. The programmer could make this point to the compiler by qualifying global variable N as volatile, thereby recommending that the compiler not optimize by implementing N as a CPU register.
A program with no volatile qualifications may compile to the same executable as a version of the same program with many such qualifications. The volatile qualifier does not guarantee anything; instead, the qualifier is only a cautionary note that the programmer sends to an optimizing compiler.
Although the syntax for volatile is close to that for storage classes, volatile is not a storage class. The volatile qualifier has no connection whatsoever with how a variable, thus qualified, is stored.
- 1.
Compile with the -g flag:
% gcc -g -o fpoint point.c - 2.
Invoke the debugger on the compiled file:
% gdb fpoint
Inside the debugger, there is a help menu.
4.7 What’s Next?
Every program in execution requires at least one processor (CPU) to execute its instructions and memory to store these instructions and the data that together make up the program. Except for special cases, a program uses I/O devices as well, which are accessible to a program as files of one sort or another. A file in this generic, abstract sense is just a collection of words, and a word is just a formatted collection of bits. For example, a camera in a smartphone and the lowly keyboard on a desktop machine are both files in this sense. The role of input and output operations is, of course, to allow a program to interact with the outside world. The next chapter gets into the details by highlighting C’s flexible approach to input/output operations.