© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
M. KalinModern C Up and Runninghttps://doi.org/10.1007/978-1-4842-8676-0_4

4. Storage Classes

Martin Kalin1  
(1)
Chicago, IL, USA
 

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

Here are two examples of where a storage class shows up in C code :
static int counter;                /* static is a storage-class specifier */
extern void main() { /* body */ }  /* extern is a storage-class specifier */
C has four storage class specifiers: extern, static, auto, and register. It is rare for the last two to be used explicitly in modern C because the compiler, on its own, does what the specifiers call for. The first two specifiers, extern and static, remain relevant. A function can be either extern or static only; a variable can be any one of the four. A storage class also impacts the following:
  • 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.

How Does a Variable Definition Differ From a Declaration?

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.

Here is a summary of the default storage classes for functions and variables :
  • 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 details of the auto and register specifiers can be clarified through a code example.
#include <stdio.h>
#include <stdlib.h> /* rand() */
int main() {
  /* i and n are visible from their declaration to the end of main */
  auto int i; /* auto is the default in any case */
  int n = 10; /* auto as well */
  for (i = 0; i < n; i++) {
    register int r = rand() % 100; /* if no register available, auto */
    printf("%i ", r);
  } /* r goes out of scope here */
  putchar(' '); /* instead of the usual printf(" ") */
  return 0;
}
Listing 4-1

The auto and register specifiers

The autoreg program (see Listing 4-1) shows how the auto and register specifiers could be used. Recall that these specifiers are used for variables only, and only for variables declared inside a block. In this example, there are two blocks:
  • 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.

Does the C Compiler Support Profiling?
Yes. The flag -pg enables profiling:
% gcc -pg profile.c   ## produces executable a.out (on Windows: A.exe)
Running the program produces the file gmon.out, and the utility gprof then can be executed from the command line:
% gprof

A detailed profiling analysis is printed to the screen.

Although the C compiler includes support for profiling (see the sidebar), this code example shows how the static specifier can be used to keep track of how many times a particular function is invoked.
#include <stdio.h>
#define SizeF 109
#define SizeB 87
void foo() {
  static unsigned n = 0; /* initialized only once */
  if (SizeF == ++n) printf("foo: %i ", n);
}
void bar() {
  static unsigned n; /* initialized automatically to zero */
  if (SizeB == ++n) printf("bar: %i ", n);
}
void main() {
  unsigned i = 0, limit_foo = SizeF, limit_bar = SizeB;
  while (i++ < limit_foo) foo(); /* call foo() a bunch of times */
  i = 0;
  while (i++ < limit_bar) bar(); /* call bar() a bunch of times */
}
Listing 4-2

Using static variables to profile function calls

The profile program (see Listing 4-2) tracks the number of times that main calls two other functions, foo and bar. Each of the called functions has a local static variable named n. The compiler initializes a static variable to zero unless the program provides an initial value. Two points about these static variables are important in this example:
  • 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.

The extern storage class supports truly global scope, although the programmer needs to do some work to make this happen. The basic two steps for global scope go as follows:
  • 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.

A code example should help to clarify the details. The example consists of two files, prog2files1.c and prog2files2.c. These will be considered in order.
#include <stdio.h>
/* definition of the extern variable: keyword extern is absent, but could be present if the variable were initialized in its definition. */
int global_num = -999; /* would be initialized to 0 otherwise */
extern void doubleup(); /* declaration of a function defined in another file */
extern void print() { /* extern could be dropped from this definition */
  printf("global_num: %i ", global_num);
}
/* set2zero can be invoked only by functions within this file */
static void set2zero() {
  global_num = 0;
}
void main() { /* extern could be added, but not necessary */
  doubleup(); /* function in another file */
  doubleup(); /* call doubleup() again */
  print(); /* -3996 */
  set2zero(); /* function in this file */
  print(); /* 0 */
}
Listing 4-3

One source file in the prog2files program

The prog2files1.c file (see Listing 4-3) does the following:
  • 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 main function , housed in the source file prog2files1.c, invokes the doubleup function twice—a function housed in the program’s other source file, prog2files2.c. If the doubleup function were not declared in prog2files1.c, the compiler would complain. The main function also invokes the static function set2zero. Because set2zero is static, it must be invoked by a function such as main in the same source file, prog2files1.c.
/* declaration: keyword extern is required, and the variable must not
 be initialized here because it then would be a definition. */
extern int global_num;
void doubleup() {   /* definition: doubleup is declared elsewhere, defined here */
  global_num *= 2;  /* the global_num defined elsewhere, but accessed here */
}
Listing 4-4

The other source file in the prog2files program

The second source file prog2files2.c (see Listing 4-4) is deliberately simple. There are two points of interest:
  • 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.

The source files in the prog2files program are compiled in the usual way:
% gcc -o prog2files prog2files1.c prog2files2.c   ## file names could be in any order
For review, here again is the rule of thumb that sidesteps the legalese surrounding the specifier extern. This rule can be spelled out as two related recommendations:
  • 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.

What Does const Mean in C?
The qualifier const for constant originated in C++ and was brought into C. A few code segments clarify.
const int n = 17; /** n is constant or read-only **/
n = -999;         /** ERROR: won't compile -- n is read-only **/
There are workarounds through pointers, however.
int* ptr = &n; /* n is const */
*ptr = -999;   /** WARNING: bad idea, but works **/
The const-ness can be cast away from the pointer:
int* ptr = (int*) &n; /* (int*) cast is critical here, as &n is (const int*) */
*ptr = -999;          /* no error, no warning */

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

A variable of any type, including pointers and struct types, can be qualified as volatile:
volatile int n; /* int could be left of volatile */

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.

What’s a Multicore Machine?

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.

Does C Come With a Debugger?
The standard compilers have a debugger with the usual support: breakpoints, stepping, viewing and resetting variables, and so on. Here is an example with the fpoint.c as the source file:
  1. 1.

    Compile with the -g flag:

    % gcc -g -o fpoint point.c
     
  2. 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.218.184