Transcript
CHAPTER 7 Functions and Program Structure II
Storage classes
There are two attributes associated with every C variable
data type (e.g. int, double, etc.)
storage class
The storage class of a variable tells the compiler how the variable is to be stored
It also specifies how long the variable remains in existence, i.e. its lifetime
Thirdly, it specifies the variable's visibility, i.e. its scope
The scope of a variable determines where, in the program, the variable can be accessed
There are four storage classes in C
automatic, external, static, and register
The corresponding keywords in C to specify such are
auto
extern
static
register
We shall see in subsequent pages where it is valid to use these storage classes and what they mean
Also, variables have a default storage class if we do not specify otherwise
Scope Rules
The basic rule of scoping:
Identifiers are accessible only with the block in which they are declared
They are unknown outside the boundaries of that block
Blocks may be defined inside of other blocks
A variable may be redefined inside an inner block
The innermost block's variable declarations take precedence over the outer block
Consider the following code fragment:
{
int a = 2; /*outer block*/
printf ("a=%d", a); /*2 is printed*/
{
int a = 5; /*inner block*/
printf ("a=%d, a); /*5 is printed*/
}
printf ("a=%d", a); /*2 is printed*/
}
An equivalent code fragment is:
{
int a_outer = 2;
printf ("a=%d", a_outer); /*2 is printed*/
{
int a_inner = 5;
printf ("a=%d, a_inner); /*5 is printed*/
}
printf ("a=%d", a_outer); /*2 is printed*/
}
Conceptually, a new a is born in the innermost block
It lives throughout that inner block, hiding any external a
It dies when we leave the block and the outer a then takes effect
Parallel and nested blocks (1)
Two blocks can come one after another
In this case, the second block has no knowledge of the variables declared within the first block
Such blocks which reside at the same level are called parallel blocks
Functions are declared in parallel at the outermost level
One block may exist within another block as well
Such a block is called a nested block
{
int a, b;
...
{ /*inner block 1*/
float b;
... /*int a is known, but not b*/
}
...
{ /*inner block 2*/
float a;
... /*int b is known, but not int a*/
/*nothing in inner block 1 is known*/
}
...
}
Parallel and nested blocks (2)
The formal syntax for a statement is as follows
statement
::=
labeled-statement
|
expression ;
|
;
|
compound-statement
|
selection-statement
|
iteration-statement
|
jump-statement
compound-statement
::=
{ {declarator}* {statement}* }
Notice that any statement can be a compound statement
A compound statement may include declarations
A compound statement with declarations is a block
Variables declared within a block are only visible from the point of declaration to the end of the block
The scope of a variable is the section of the program in which it is visible and active
Variable names within a block supersede other variables of the same name outside the block (this is called “hiding”)
void foo (int i)
{
if (i > 0) {
double i; /* declare a new i */
...
}
}
The storage class: auto
Variables declared within function bodies are by default automatic
If a compound statement starts with variable declarations, then these variables can be acted on within the scope of the compound statement
A compound statement with declarations will be called a block to distinguish it from those which do not begin with declarations
Declarations of variables within a block are implicitly of storage class automatic
One may use the keyword auto to explicitly specify this storage class
Usage of this keyword is very rare as the default, of any block, is auto
typical code fragment
Equivalent code fragment
{
int x, y;
double z;
...
}
{
auto int x, y;
auto double z;
...
}
When the block is entered, the system allocates memory for the automatic variables
This is usually performed by incrementing the stack frame pointer
This is very efficient
Automatic variables defined within such a block are considered local to that block
When the block is exited, the system deallocates the memory it had allocated for the automatic variables
This is usually performed by decrementing the stack frame pointer
The initial values of automatic variables, if not specified, is undefined
One should assume that its contents are random bits
More scope (1)
The scope of an object determines who can see what
The lifetime of an object determines when it is activated and when it is deactivated
The chief reason for blocks is to allow memory for variables to be created where needed
Each block is given its own activation frame
Conceptually, when a block is entered, space is set aside on the system stack to hold all of its variables
If the block is that of a function, space for the function's parameters is also set aside
Notice that inner blocks can see the outer block's environment
However, the outer block cannot see inside the inner block
In the following example, identify which variables are active and visible within each block
Also draw the activation frame for each block
void foo (void)
{
int a, b, c, d;
...
{ /* block-1 */
int c, d, e, f;
...
}
{ /* block-2 */
double c, d, e;
...
{ /* block-2.1 */
int b, c;
...
}
}
}
More scope (2)
void foo(void)
{
int a, b, c, d;
...
{ /* block-1 */
int c, d, e, f;
...
}
{ /* block-2 */
double c, d, e;
...
{ /* block-2.1 */
int b, c;
...
}
}
}
block
variables visible to block
foo
int a, int b, int c, int d
block-1
foo::a, foo::b
int c, int d, int e, int f
block-2
foo::a, foo::b
double c, double d, double e
block-2.1
foo::a
block-2::d, block-2::e
int b, int c
More scope (3)
void foo(void)
{
int a, b, c, d;
...
{ /* block-1 */
int c, d, e, f;
...
}
{ /* block-2 */
double c, d, e;
...
{ /* block-2.1 */
int b, c;
...
}
}
}
\* mergeformat
More scope (4)
Activation frame upon entering foo()
var
declaration
stack
block-name
d
int d
(int) d
foo
c
int c
(int) c
b
int b
(int) b
a
int a
(int) a
Activation frame upon entering block-1
var
declaration
stack
block-name
f
int f
(int) f
block-1
e
int e
(int) e
d
int d
(int) d
c
int c
(int) c
int d
(int) d
foo
int c
(int) c
b
int b
(int) b
a
int a
(int) a
More scope (5)
Activation frame upon entering block-2
var
declaration
stack
block-name
e
double e
(double) e
block-2
d
double d
(double) d
c
double c
(double) c
int d
(int) d
foo
int c
(int) c
b
int b
(int) b
a
int a
(int) a
Activation frame upon entering block-2.1
var
declaration
stack
block-name
c
int c
(int) c
block-2.1
b
int b
(int) b
e
double e
(double) e
block-2
d
double d
(double) d
double c
(double) c
int d
(int) d
foo
int c
(int) c
int b
(int) b
a
int a
(int) a
The storage class: register
The register specifier may be used to declare heavily used variables
Hint to compiler to try to optimize use of variable
This does not guarantee that a register will actually be used
example
register int x;
register char c;
Can only be applied to automatic variables, i.e. function or function arguments:
void f (register unsigned m, register long n)
{
register int i;
...
}
The register declaration is taken as advice to the compiler
If a register variable cannot be stored in a register, it defaults to automatic
Cannot take the address of a register variable (more on this later)
register float radius;
printf ("Enter radius: ");
scanf ("%f", &radius); /*illegal*/
The compiler forbids us from taking the address of radius because it may be stored in a hardware register, not memory
Compilers with good optimizers can do the job without register allocations. However, it may be useful in “guaranteeing” optimal register allocation in critical code.
The storage class: extern
One method of transmitting information across blocks and functions is to use external variables
When a variable is declared outside a function,
storage is permanently assigned to it
its storage class is extern
The declaration of a variable outside of a function looks just like those inside a function
One does not need to explicitly specify that they use external linkage
Such a variable is considered to be global to all functions declared after it
The variable exists when the program begins execution and is deallocated when the program terminates
#include
int a = 1, b = 2, c = 3;
int f (void); /*forward declaration of f()*/
int main (void)
{
printf ("%3d\n", f ()); /* 12 is printed */
printf ("%3d %3d %3d\n", a, b, c); /* 4 2 3 is printed*/
}
int f (void)
{
int b, c; /* masks off global b and c */
a = b = c = 4;
return a + b + c; /* return 12 */
}
External variables and functions
#include
int a = 1, b = 2, c = 3;
int f (void); /*forward declaration of f()*/
int main (void)
{
printf ("%3d\n", f ()); /* 12 is printed */
printf ("%3d %3d %3d\n", a, b, c); /* 4 2 3 is printed*/
}
int f (void)
{
int b, c; /*masks off global b and c*/
a = b = c = 4;
return a + b + c; /*return 12*/
}
Technically speaking, this program file exports has five external symbols:
data variables: a, b, c
functions: main, f
Because we said nothing to the contrary (and soon we will see just how to be contrary), each of the objects defaulted to external linkage
Another way we could have written this, though some C compilers might complain when they see it, is...
#include
extern int a = 1, b = 2, c = 3;
extern int f (void); /*forward declaration of f()*/
extern int main (void) /*be explicit about it*/
{
printf ("%3d\n", f ()); /* 12 is printed */
printf ("%3d %3d %3d\n", a, b, c); /* 4 2 3 is printed*/
}
extern int f (void)
{
int b, c; /*masks off global b and c*/
a = b = c = 4;
return a + b + c; /*return 12*/
}
External variables and functions (2)
Now, consider splitting this into multiple files
In one file we shall have main() and the global variables
In another file we shall implement f()
/*file1.c*/
#include
int a = 1, b = 2, c = 3;
extern int f (void); /*look elsewhere for f()*/
int main (void)
{
printf ("%3d\n", f ()); /* 12 is printed */
printf ("%3d %3d %3d\n", a, b, c); /* 4 2 3 is printed*/
}
/*file2.c*/
int f (void)
{
extern int a; /*look elsewhere for a*/
int b, c;
a = b = c = 4;
return a + b + c; /*return 12*/
}
int g (void)
{
return a * a; /*declaration of a out of scope*/
}
The declaration of a inside of f() using the extern keyword tells the compiler that a is not to be found inside of f() but elsewhere
However, a is visible in file2.c only inside of f() because the declaration appears inside of f()
External variables and functions (3)
The declaration
extern int a = 1, b = 2;
is a variable definition
It forces space to be allocated for a and b
The presence of an initializer make this a definition rather than just a declaration
In contrast, the declaration
extern int a, b;
is a variable declaration
It tells the compiler about the existence of a and b and that it should look elsewhere for its definition
It tells the compiler that the variables are of type int
It also tells the compiler that the variables are of storage class external and may be found in another file (compilation unit)
No space is set aside for these variables
External variables and functions (4)
/*file1.c*/
int a = 1, b = 2;
int f (void)
{
a = a * b;
return a;
}
/*file2.c*/
#include
int a = 1, b = 3;
int f (void)
int main (void)
{
printf ("%3d\n", f ());
printf ("%3d %3d\n",
a, b);
}
In this code fragment, file1.c defines two variables using storage class extern
It also defines a function f() using storage class extern
The symbol identifier table for file1.c contains three entries: variables a and b, and function f
Also, file2.c defines two variables using storage class extern
It declares the existence of function f()
It defines a function main() using storage class extern
Function main() calls a function f() that is not defined in file2.c
As a result, the symbol identifier table for file2.c contains three entries: variables a and b, and function main
It also contains an undefined reference to a function called f()
The linker will examine the tables of both files and determine the following
the unresolved reference of f() in file2.c can be resolved by binding it to the function symbol f() in file1.c
However, the linker sees two definitions of a and b and cannot determine which ones it should really use
As a result, we get a link error: multiple definitions of variables a and b
The storage class: static
The keyword static alters either the scope or lifetime of the object (or both)
The lifetime is extended to be from the beginning to the end of the program
the lifetime of a static variable is no longer limited to the block
like own variables in Algol
when applied to local variables, lifetime is made global (program)
The scope is affected only in declarations outside of functions
in this case, the visibility of the object is limited to the file (source file)
example
static char buf[BUFSIZ]; /*visible within file*/
static int bufp = 0;
/**
** error function for this file
**/
static void error (const char *s)
{
...
}
int getch (void)
{
static int counter = 0;
counter++; /*#-times getch called*/
...
}
void ungetch (int c)
{
static int counter = 0;
counter++; /*#-times ungetch called*/
...
}
Static variables and functions
void foo(void)
{
int a, b;
static double c = 10;
{ /*block-1*/
static int a;
char b;
...
{ /*block-1.1*/
int d;
static double b;
...
}
}
}
What does the activation stack look like when block-1.1 is activated?
Automatic variables, i.e. those declared within functions and not static, are allocated on the stack
Global variables and static variables are usually allocated elsewhere
they are usually allocated in some global location
they are initialized at the beginning of the program
Static variables and functions (2)
Before entering foo(), all of the global variables and static variables are allocated
For the sake of this discussion, assume that all variables are allocated on the stack
Global variables and static variables will be put at the bottom of the stack
\* mergeformat
Linkage
file1.c
file2.c
extern int sp;
extern double val[];
void push (double f){...}
double pop (void) {...}
int sp = 0;
double val[MAXVAL];
void push (double f);
extern double pop (void);
file1.c contains external declarations for variables sp and val
file1.c contains the definitions for functions push and pop
file2.c contains definitions for variables sp and val
file2.c contains the external declarations for functions push and pop
It is important to distinguish between a declaration of an object and its definition
A declaration announces the properties of an object (for variables this is primarily its type, for functions it includes the return value and the parameters)
A definition also causes storage to be set aside (in the case of a variable) or includes the code making up the function
In file2.c, because the lines
int sp = 0;
double val[MAXVAL];
appear outside of a function, they define the variables sp and val to be external and cause storage to be set aside
On the other hand, the lines in file1
extern int sp;
extern double val[];
declare for the rest of the source file that sp is an int and that val is a double array (whose size is determined elsewhere)
They do not reserve any storage for them
The storage specifier extern simply asserts that this function can be used externally; in this case, no change in meaning
Linkage (2)
file1.c
file2.c
int a = 1;
int b = 1;
extern int c;
int a;
extern double b;
extern int c;
If these two files are compiled and linked together as a program, there are three problems with it
1. Variable a is defined twice
in file1.c, it is defined "int a=1;"
in file2.c, it is defined "int a=0;"
2. Variable b is declared twice with different types
in file1.c, b is defined to be an integer
in file2.c, b is declared to be a double
3. Variable c is declared extern twice but never defined
None of these problems will be caught by the compiler
The linker will probably catch the first problem and report a "multiple definition of a"
The linker will also catch the third problem and report that "variable c is undefined"
However, the linker will probably fail to catch the second problem and the result will be strange results at runtime
Linkage (3)
file1.c
file2.c
int a;
int f (void)
{
return a;
}
int a;
int g (void)
{
return f ();
}
The first problem with this is that variable a is multiply defined
This is the case even though it has the same initial value
Many C systems would handle this fine because the linkers would not complain as long as the initial values of the variables matched
For portability, one should define a variable in one place, and declare it extern within a common header file
Another problem is that in file2.c, function f() is never declared
In C this is not a problem because f() defaults to int
In C++, this is a problem because functions must be declared before being used
file1.c
file2.c
static int a = 6;
static int f (void) { ... }
static int a = 7;
static int f (void) { ... }
A name may be made local to a file by declaring it static
In this example, because f() and a are declared static in file1.c and file2.c, the resulting program is correct
In this example, file1.c and file2.c each have their own respective variable a and function f()
An object or a function with a name that is local to a file is said to have internal linkage
Conversely, an object or a function with a name that is not local to a file is said to have external linkage
Initialization
If no initialization has been specified...
external (global) and static variables are initialized to zero
automatic and register variables are undefined (garbage)
Scalar and floating point variables can be initialized when defined, i.e.
int x = 1;
double d = 4.1;
char squote = '\'';
long day = 1000 * 60 * 60 * 24; /*microsec/day*/
For external and static variables, the initializer must be a constant expression
the initialization is performed once
conceptually done before the program begins execution
For automatic and register variables, initialization is done each time a function or block is entered
initializer is not restricted to being a constant
it may be any expression involving previously defined values
it may even be function calls
Initialization (2)
example
FILE *openfile (const char *filename)
{
FILE *fp = fopen (filename, "r");
if (fp == 0)
perror (filename);
return fp;
}
example
int binsearch (int x, int v[], int n)
{
int low = 0;
int high = n - 1;
int mid = (low + high) / 2;
...
}
instead of...
int binsearch (int x, int v[], int n)
{
int low, high, mid;
/* … gap … */
low = 0;
high = n - 1;
mid = (low + high) / 2;
...
}