C Language

Simplified Variable Declarations

An object's scope is something you can't C very clearly in source code. Or should I say you can't code scope clearly in C source? If you can't quite C where I'm headed, it's because we're just beginning a winding tour through the maze of C object visibility. Follow closely, and by the time we exit the maze, you'll have a simple map for the shortest route out.

C, like many languages, lets an identifier x refer to different objects (e.g., storage locations for variables) at different places in the source code. As a simple case, the two declarations of x below refer to different variables.

 int x;
 main( void ) {
 int x;
 ...
 }
 void func1( void ) {
 ...
 }

Let's say the first x refers to a storage location we'll call S1, and the second x refers to a storage location we'll call S2. The scope of the S1 object (storage location) is simply the region (i.e., lines) of source code where references to x are references to S1; likewise, the scope of the S2 object is the region of source code where references to x are references to S2. In this example, the scope of S1 (the first x) is everywhere outside the main function, and the scope of S2 (the second x) is only within the main function. Obviously, for the program to be clear, these two regions of the program can't overlap; each reference to x must refer to just one of the storage locations. Scope is also called "visibility" because you can "see" an object (e.g., read or change a storage location) only within its scope. In this chapter, I use "visibility" for the general concept and "scope" to refer to C's specific lexical scope attribute.

The C-nic Route

The concept of visibility is simple and useful. Among other uses, distinct regions of object visibility let you use identifiers in different parts of your code without worrying about whether the same identifier, used for different purposes, unintentionally refers to the same object. But why be merely simple and useful when you can be clever and brave, too? Take your first left turn into the C labyrinth.

C splits the single concept of visibility into scope and linkage, with scope referring to the region of program text within which an identifier's characteristics are understood. C's linkage term refers to the connection between identifiers in independently compiled translation units. Expressing visibility with two attributes instead of one may help C compiler writers, but it makes it more difficult for programmers to determine which object an identifier references. I'll try to map out C's rules while noting some language flaws that lead to C's problems. Then I'll mark an easy path to declaring variables and functions with the desired visibility.

C's rules for function visibility are simple: If you specify static storage class, a function is visible throughout the source file in which it's defined, but not in other source files. With extern or no storage class specifier, a function is visible throughout the program (i.e., across all files), and you can call it from anywhere. These rules lead to my first suggestion: Declare functions static if you intend to call them only from within the same source file.

C's rules for variable visibility are far more complex than those for function visibility. I've listed these rules in a table (Figure 5.1) that shows, for any variable declaration, where that variable is visible. I've also listed C's scope and linkage attributes and whether the declaration causes storage to be allocated (in C terminology, whether it is a definition as well as a declaration). You can use this guide to help you understand some of the mysterious changes that can occur when a C variable has unexpected visibility. You also may need it to follow my next few examples, but later I'll show you a far more useful table for C programming.

Figure 5.1
Visibility of C Variables
DECLARED OR REFERENCED INSIDE A BLOCK (function or nested block):

Where Storage Initial Storage Scope/Linkage Visibility
specified class value allocation
1. Declared (none) Yes Yes Block scope, Within same block, including all
in block auto No Yes no linkage nested blocks, except
register any nested block (and its
static nested blocks) with an identical identifier without extern
2. Declared extern No CASE A: Enclosing scope has identical, visible
in block identifier - same scope, linkage, allocation, visibility as the matching identifier
CASE B: Otherwise, same as if declared extern outside function (see 7, below)
3. Declared extern Yes ILLEGAL
in block declaration
4. Not declared but referenced in block CASE A: Enclosing scope has identical, visible identifier declared in the same source file prior to the reference - same as if declared extern in block (see 2, above)
CASE B: Otherwise, ILLEGAL declaration
EXTERNAL DECLARATIONS (declared outside any function):
Which Storage Initial Storage Scope/Linkage Visibility
declaration class value allocation
5. First (none) Yes Yes File scope Rest of file except any
declaration No external block (and its nested blocks) with an identical identifier declared without extern, and blocks it contains and Other files with an identical external linkage identifier declared in them (No other file may allocate (define) an identical external linkage identifier.)
6. First static Yes Yes File scope, Rest of file except any
declaration No internal linkage block (and its nested blocks) with an identical identifier declared without extern, and blocks it contains
7. First extern Yes Yes File scope, Rest of life except any
declaration external linkage block (and its nested blocks) with an identical identifier declared without extern, and blocks it contains and Other files with an identical external linkage identifier declared in them (One other file must have an identical external linkage identifier allocated (defined) in it.)
8. First extern Yes Yes Same as if declared outside function
declaration without extern or static (see 5, above)
9. Second or Must have same Same scope, linkage, allocation, and
later type and linkage visibility as first declaration
declaration as first declaration

Feeling a Little C-sick?

C syntax flies in the face of the "say what you mean, mean what you say" principle of programming. The keyword extern is simply an abbreviation for "external" - a word you'd expect to be related to visibility or scope and to mean something like "outside the current context." And you'd expect the other relevant keyword, static, to relate to how storage is managed. Thus, you might read the declaration

 extern int x;

to mean the variable x is visible outside the block or file in which it's declared. Likewise, for a declaration without extern (C has no intern keyword), such as

 int x;

you'd expect the variable x not to be visible outside the block or file in which it's declared. For variables declared within a function or nested block, this sensible interpretation holds. The problem arises for variables declared outside any function. (In C, such declarations are called external declarations.) In most cases, when the two previous examples appear as external declarations, both the extern and non-extern declarations mean the variable is visible outside the file in which it's declared. (If the extern declaration isn't the first declaration of x, however, and the first declaration of x isn't visible outside the file, the extern declaration also specifies that x isn't visible outside the file - a further complexity in C's approach to visibility.) Some programmers wise in the ways of C might argue, "But both declarations are external declarations, so it is consistent that they both are visible `externally' to the file." Nice try. However, when the declaration

 static int x;

is outside any function, it also is an external declaration, yet it defines a variable that is not visible outside the file.

Not only does C's syntax lack consistency, but it also confuses things by using the static storage class keyword to specify visibility. My theory is that Humpty Dumpty was on the original C design team. As he told Alice, "When I use a word, it means just what I choose it to mean - neither more nor less."

There are other confusing cases, as in the second declaration of x below,

 static int x;
 main( void ) {
 extern int x;
 ...
 }

where the use of extern results in internal linkage. I'll spare you a diversion down the cul-de-sac you enter when you have more than one external declaration for the same variable (which C allows). As we pass through the maze, however, notice that it matters whether you initialize a variable in an external declaration when you specify extern. (With an initializer, the declaration allocates storage; without one, it doesn't.) Yet when you specify static or no storage class, initialization doesn't effect storage allocation.

Although I've wandered repeatedly around the C maze using guides such as the SAA C reference manual and Kernighan and Ritchie's description of C (see the Bibliography), I still can't guarantee that Figure 5.1 is a perfect map of every C visibility rule. But there are better ways to simplify C variable declarations and make your programs more readable than building a mental map of C's visibility labyrinth.

UnC-eemly Solutions

The fastest way out of C's maze is to decide where you want to go - that is, what kinds of visibility you want to use. I've found three "classes" of visibility adequate to define most variables and functions:

  • Local: visible only within a single function or nested block.
  • Share: visible in all functions in a file, but not outside the file.
  • Export: visible in all functions in a program (all files). Export visibility implies share visibility. The file that "exports" an object allocates its storage.

You pick an appropriate one of these visibility classes when you define a variable. If a variable is local, you generally are done with your declarations for that object. In C, you define most local variables at the beginning of a function, and they are visible throughout that function (but not outside it).

If a variable has share or export visibility, however, you need one more concept - import declarations - to specify you intend to reference the variable in a specific function. One use of the "import" concept is to specify you want to use an export variable in a file other than the one containing its definition. Naturally, a file can't import a variable that isn't exported by some other file, and a file doesn't need to import variables that it exports - such variables already are available for shared use within the file. Note that in C programs, only one file can export a particular variable (this isn't true for all languages). Another use of the import concept is to specify you want to use a nonlocal variable in a function or block. After covering a couple of general rules relating to visibility, I'll explain a simple way to implement local, share, and export visibility, as well as import declarations.

The most important rule for declarations in C, or any language, is: Use the most restricted visibility possible for variables; avoid shared variables. In C, this most often means defining variables used in a function as local to that function by putting their declarations at the beginning of the function and using auto or no storage class specifier. The same rule and technique apply to nested blocks within a function.

In the special case of references to variables within a nested block that aren't local to the nested block, I usually do not declare the variables in the nested block. This results in implicit extern declarations for any variable you reference but don't declare in the nested block. When you follow this and other guidelines I present below, all references to variables not declared in a nested block resolve to a local or import declaration in an outer block. I rarely use nested blocks, except with "loop" macros, such as those I presented in Chapter 3. And in those cases, import declarations don't add any real protection or improve clarity. However, if you prefer to explicitly declare every variable in a nested block, you can either use the IMPORT macro (which I'll introduce in a moment) or define a similar macro to more specifically express your intent.

You should define share and export variables before the first function definition in a file, although C permits the confusing and dangerous practice of placing external declarations between functions, as in the following example:

 void func1( void ) {
 ...
 }
 int x;
 void func2( void ) {
 ...
 }
 void func3( void ) {
 ...
 }

This ordering makes x implicitly visible in func2 and func3, but not visible in func1, unless x is explicitly declared extern within func1. The intent of such coding usually is to declare x as a variable shared only by some - not all - of the functions in a file. If several functions are coupled by sharing data you don't want other functions to access, put the coupled functions in a file by themselves, along with the external declarations for their shared variables.

C-through Macros

The best way to declare share, export, and import variables is to define macros for the required C syntax. Figure 5.2 shows three suggested macro definitions and how to use them (along with rules for declaring local variables.) For variables you want visible throughout the file, use the SHARE macro before the file's first function definition to declare (and define) each variable. For example,

 SHARE int x;

This statement expands to

 static int x;

which gives x the desired visibility. Note that you never use SHARE inside a function because it would just define a local variable.

Figure 5.2
How to Declare C Variables and Functions
"Visibility" Macros
#define SHARE static
#define EXPORT
#define IMPORT extern

Declaring Variables

Visibility Storage Where referenced Storage/visibility specifier(s)
Local Automatic In block Define at beginning of block with no specifier
Local Static In block Define as static at beginning of block
Nonlocal - In nested block (No declaration, use implicit extern)
File Static In function Define as SHARE before first function in file
Declare as IMPORT at beginning of each function where referenced
Program Static In function Define as EXPORT before first function in file where variable is to be allocated
Declare as IMPORT before first function in file(s) where variable is used
Declare as IMPORT at beginning of each function where referenced
Declaring Functions
Callable from Visibility specifier
Same file (only) SHARE
Any file EXPORT

The SHARE macro isn't a silver bullet to slay all of C's visibility monsters, nor does it boost C's visibility features into a class with Modula 2's. But it makes a lot more sense to write SHARE rather than static when you're trying to specify a variable's visibility.

In functions or blocks where you want to use a SHARE variable, declare the variable with the IMPORT macro:

 void func1( void ) {
 IMPORT x;
 ...
 }

This declaration expands to

 extern int x;

which, although unnecessary given C's default scoping rules, makes explicit the function's intention to use x. Unfortunately, C won't protect you against unintentional references to x in functions that don't declare x. Reducing the number of nonlocal variables is the best way to reduce accidental nonlocal references.

You also should specify SHARE for functions you intend to be callable only from within the same file. Use EXPORT for functions you intend to be callable from anywhere. Functions without a visibility specifier default to EXPORT, but I find explicit visibility reminds me which functions are callable from anywhere. (I often notice EXPORT functions that don't need such broad visibility and that are better defined as SHARE.) In addition, by grouping all SHARE functions after all EXPORT functions, you can organize your functions better.

For export variables you want visible throughout all source files (i.e., the entire program), use the EXPORT macro to declare (and define) the variable before the first function definition in the file "owning" the variable. In most cases, this is the file containing the main function.

The following example shows how to define an export variable:

 EXPORT int x = 0;

This statement expands to

 int x = 0;

which gives x the desired visibility and allocates storage for x. It's good programming practice to initialize variables in their definitions, especially variables for which C's default zero initialization is not within the variable's domain (valid values). I'd also recommend you place all EXPORT variable declarations before any IMPORT declarations. Note that, as with SHARE, you never use EXPORT inside a function.

In functions where you want to use a nonlocal variable, declare the variable with IMPORT at the beginning of the function:

 void func1( void ) {
 IMPORT x;
 ...
 }

The IMPORT declaration adds the necessary extern specifier and clearly shows that the function uses a nonlocal variable. A variable declared as IMPORT in a function will resolve to one of the file's SHARE or IMPORT external declarations.

In files where you want to use a variable exported from another file, declare the variable with IMPORT once at the beginning of the file and once in each function or block where you want to use the variable. For example,

 | Before first function
 */
 IMPORT x;
 ...
 void func1( void ) {
 IMPORT x;
 ...
 }

The first IMPORT declaration adds the necessary extern specifier and clearly indicates that x is defined in another file and that x may be used in other files (as well as in the current file and in the one that defines it). Like the previous example, the second IMPORT identifies a nonlocal variable used in a function. In this case, the two IMPORT declarations are clearly warning that func1 uses a variable that functions in other files also might use.

Finding Your C Legs

SHARE, EXPORT, and IMPORT let your declarations "say what they mean and mean what they say." You may prefer different macro names; almost any descriptive names produce clearer code than C's standard syntax. I picked up the idea for the macros from Steve Schustack's suggestion in Variations in C for similar macros he calls GLOBAL, SEMIGLOBAL, and IMPORT. I think EXPORT indicates better than GLOBAL the role of a variable allocated in one file and made available to other files, and SHARE seems more informative than SEMIGLOBAL. But whatever names you choose, these simple "visibility" macros and guidelines for declarations will let you, and anyone who reads your programs, C clearly now.

C Coding Suggestions

  • Declare functions static if you intend to call them only from within the same source file.
  • Use the most restricted visibility possible for variables; avoid shared variables.
  • Put all external declarations before the first function definition in a file.
  • Put functions that must share data and external declarations for their shared variables in a file by themselves.
  • Use EXPORT, SHARE, and IMPORT macros to clarify the intended visibility of a variable.
by BrainBellupdated
Advertisement: