Functions and Program Structure II

CHAPTER 7

Functions and Program Structure II

Storage classes

Reference: Kelley & Pohl, Chapter 5 (5.11 - 5.12)

·  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;
...
}
}
}


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 <stdio.h>
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 <stdio.h>
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 <stdio.h>
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 <stdio.h>
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 <stdio.h>
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