As with every modern programming language, C uses functions to create modularity as a step toward robust software. Encapsulating portions of a program’s code this way allows the programmer to isolate the functions for the purposes of testing and debugging. One key aspect of writing functions is to get the scope of variables correct. Code Listing A.22 illustrates the three main scopes for variables in C programs: global, local, and static. The split of global and local is fairly straightforward: if the variable declaration occurs inside the body of a function (see line 13), it is local to that function and unavailable to others; if the declaration is outside any function definition (see line 7), the variable is global and accessible by all functions for reading or modification.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | /* Code Listing A.22:
Declaring local and global functions in a helper function
*/
#include <stdio.h>
int global = 1;
static int global_static = 2;
void
helper (void)
{
int local = 5;
static int local_static = 10;
printf ("global = %d\n", global++);
printf ("global_static = %d\n", global_static++);
printf ("local = %d\n", local++);
printf ("local again = %d\n", local++);
printf ("local_static = %d\n\n", local_static++);
{
static int hidden = 20;
printf ("hidden = %d\n\n", hidden++);
}
/* hidden cannot be accessed here */
}
|
In addition to their differences in visibility, local and global variables differ in another key way: initialization. Global variables are always initialized, whether the code does so explicitly or not. Consider changing line 7 from Code Listing A.22 as shown here:
int global;
The variable global
is still declared as an int
variable. It is also initialized to the
value 0. One subtle difference is that the variable now exists in a different memory section than
before. Recall that we can collectively refer to the data segment as the portion of memory
storing global variables. This segment is subdivided into smaller sections, including .data
(initialized global variables), .bss
(block started by symbol for uninitialized global
variables), and .rodata
(initialized read-only data, such as string constants). In the original
form of Code Listing A.22, global
was allocated space in .data
, and the
executable file would contain its initial value (1). In this modified version, global
would be
allocated to .bss
. In the executable file, the .bss
stores nothing, because it doesn’t need
to; everything allocated to .bss
has the same initial value of 0. As such, the executable only
needs to contain information about what variables map to .bss
to know the total size that needs
to be allocated in memory when the process starts. All global variables are mapped into one of these
three sections, ensuring that they all start with some initial value.
Local variables are different. Local variables are never initialized unless done so explicitly.
Unlike global variables, in which there is a single instance that can be stored persistently in the
executable file, local variables are only created at run-time when a function is called. This
allocation happens in a single step: adjusting the stack pointer register (%rsp
in x86).
Figure 10.5.1 illustrates the portion of the stack before main()
calls
helper()
and after doing so.
All global variables have their initial values loaded automatically when the program is first loaded
into memory. Local variables are automatically allocated space in the stack frame when the function
defining their scope is called. However, initializing the variable requires extra instructions, and
these are only performed if the program specifically requests it. That is, if you do not explicitly
initialize your local variables (as line 13 of Code Listing A.22 does), then the
initial value of the local variable happens to be whatever was already in that memory location. Note
that, in general, other functions are likely to have been called before the function you are
currently writing; i.e., main()
(or related start-up functions) likely called other functions
before helper()
gets called the first time. Consequently, the initial value of local
would
be whatever data was left over from those previous function calls.
Bug Warning
At the risk of overkill on this particular point, it is critical to develop the habit of always initializing local variables. When this is not done, the program can behave truly randomly. It may work fine nine times in a row before having a segmentation fault on the tenth run; when the program is then re-run to debug the segmentation fault, it goes back to working perfectly. It is incredibly common that such random behavior can be traced back to an uninitialized local variable.
This failure to initialize local variables often arises when working with non-primitive data types,
such as arrays or structs. In this case, the simplest approach is to use memset()
. The first
parameter is a pointer to a buffer to initialize, the second parameter is what value to write into
each byte and the third parameter is the length of the buffer.
int data[100];
memset (data, 0, sizeof (data)); // initialize all elements to 0
struct stat info;
memset (&info, 0, sizeof (info)); // initialize all fields to 0
C library functions – <ctype.h>
void * memset(void *b, int c, size_t len);
To return to the general discussion of scope, the third type of variable scope centers around the
static
keyword. Variables that are declared as static
occupy a sort of middle ground between
global and local. Static scope indicates that the variable is lexically bound but persistent.
The phrase lexically bound means that the variable name can only be referenced by the code block
that contains the declaration; code blocks in C are defined by files, loop constructs (e.g., for
or while
loops) curly braces ({ ... }
). In Code Listing A.22, line 14 declares
local_static
so that it can only be accessed from within the helper() function; no other
function can have direct access to this variable. Line 23 declares hidden so that it is only
accessible within the block of code in lines 22 – 25; once that block is finished, as the comment on
line 26 indicates, the hidden variable can no longer be accessed, even within the helper()
function. The global_static
variable, declared on line 8, is visible throughout all of the
functions in this particular file. However, if this file is compiled and linked along with a
different piece of C code, that other code would not be able to access global_static
.
In addition to being lexically bound, static variables are persistent. Unlike normal local variables
that are created and destroyed with every call of a function, there is only one copy of each static
variable, and that copy persists for the duration of the process execution. In that way, static
variables are akin to global variables. Code Listing A.23 demonstrates this fact by
calling helper()
twice. Each time that helper()
is called, the local
variable is
initialized to 5 and incremented twice. In contrast, local_static
is initialized once to the
value 10. With the first call of helper()
, line 20 of Code Listing A.22 prints this
initial value and increments it to 11. This value is then used when helper()
is called again. In
other words, the initialization on line 14 of Code Listing A.22 is very deceiving; it
only executes once, rather than every time the function is called. Similarly, the static variable
hidden is also initialized once to 20 and incremented during the first call to helper()
; the
second call to the function begins with the modified value 21.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | /* Code Listing A.23:
Using a separate file to call the function in Code Listing A.22
*/
#include <stdio.h>
/* Function prototypes and extern variable declarations
are normally declared in a separate .h header file */
void helper (void);
extern int global;
int
main (void)
{
printf ("global is originally %d\n\n", global);
printf ("First call to helper:\n");
helper();
printf ("Second call to helper:\n");
helper();
printf ("global ends up as %d\n", global);
return 0;
}
|
Lines 9 and 10 of Code Listing A.23 illustrate behavior that is normally specified in a
header file. For instance, if Code Listing A.22 was stored in a file called
"scope.c"
, these two lines would likely be in a file called "scope.h"
that would look like
Code Listing A.24. The first line is a function prototype for helper(), which
serves the purpose of declaring the parameter types and return type for the function. The compiler
uses this information to know that any calls to the function are correctly formatted. The extern
keyword is used for a global variable to indicate that this variable is defined somewhere. When
Code Listing A.23 is compiled (before it is linked), the compiler only needs to know
that global
is an int
variable to know that lines 15 and 22 are valid. The linking process
will later tie these lines of code to the correct variable.
1 2 3 4 5 6 7 8 9 10 11 | /* Code Listing A.24:
A header file for the public interface of Code Listing A.22
*/
#ifdef __csf_appendix_scope_h__
#define __csf_appendix_scope_h__
void helper (void);
extern int global;
#endif
|
Bug Warning
Global variables should never appear in a header file without the extern
keyword. Without this
keyword, the C compiler will think that the programmer’s intention is to create an instance of this
global variable in the compiled object. As such, if the header file is included in multiple C
source code files, the compiler will create such a global variable inside each of them. During the
compilation stage, this is not a problem; the problem arises later during the linking stage.
Specifically, C strictly indicates that there can be only one instance of a variable name in a
given scope. (Note that the type doesn’t matter; int x
and char x
would not be allowed in
the same scope.) When the linker would try to combine the compiled objects into a single
executable, it would report an error due to multiple global variables with the same name.
C functions can be defined to take any number of parameters and return a single value. This definition follows from the mathematical definition of a function. For example, consider the following mathematical function definition:
The function f(x) takes one input parameter (x) and maps it to a single value by squaring it. C
functions operate on the same principle, as shown in Code Listing A.25. In this
example, the add()
function takes two input parameters, adds them together, and returns the
result; that is, add()
would map two input values to a particular output in the traditional
mathematical sense. Observe that function parameters operate like local variables; the variables
x
and y
can be accessed from within the function body and their values are not persistent
from one call to the next.
1 2 3 4 5 6 7 8 9 | /* Code Listing A.25:
A trivial C function to add two numbers
*/
int
add (int x, int y)
{
return x + y;
}
|
This traditional notion of a function does not completely match with the common use of C functions
as subroutines. The semantic difference is that a subroutine is simply a
modular piece of code to encapsulate some behavior; subroutines are not bound by the idea of mapping
inputs to a single return value. One common example of this is using a void
return type, which
indicates that there is no mapped result. In mathematical terms, it would be nonsensical to talk
about a function f(x) that does not map any input value x to a specific output; such an f(x) would
not be a function [1] in the conventional sense. In C subroutines, it happens all the time.
Consider a function that takes two inputs and passes them to printf()
along with a format
string; this function would have no need for a return type.
When working with subroutines rather than mathematical functions, there are several types of
behavior that C supports to create flexible programming styles. One such behavior is the ability to
specify a variable number of parameters. Perhaps the most common example of this behavior is the
printf()
function. The first parameter is a string constant to represent how the output is to be
formatted; the number of additional parameters depends on the number of format specifiers (such as
%d
or %s
). The function prototype for printf()
is written as follows:
int printf (const char *, ...);
The ellipsis (...
) here is not this book’s notation to indicate “more stuff here.” Rather, the ellipsis is part of the C syntax to indicate that there are additional variables of unknown types. In the case of printf()
, only the first argument’s type (const char *
) is explicitly declared. Code Listing A.26 demonstrates how to define a function with a variable-length parameter list. The first key feature is that the stdarg.h
header file must be included (line 6). This header file defines a number of preprocessor macros—which look like functions—to process the arguments.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | /* Code Listing A.26:
Defining a function with a variable-length parameter list
*/
#include <stdio.h>
#include <stdarg.h>
int
sum (size_t length, ...)
{
/* Declare and initialize the variable argument list
with the specified length */
va_list args;
va_start (args, length);
/* Loop through each argument, adding it to the total */
int total = 0;
for (size_t i = 0; i < length; i++)
{
/* Get the next argument as an int */
int arg = va_arg (args, int);
total += arg;
}
va_end (args);
return total;
}
|
In Code Listing A.26, the sum() function takes a size_t
variable to indicate the number of integer values to add together. To begin processing these inputs, lines 13 and 14 declare and instantiate a variable-length argument list (type va_list
) of the specified length. From there, each argument can be accessed from the list exactly once, using the va_arg()
macro (line 21). The second argument to this function indicates the type of the variable; in this case, each argument in the list will be converted to an int
. Code Listing A.27 demonstrates how to use the sum()
function. In both calls, the first argument is required to indicate the number of arguments that should be added together; line 5 only passes the value 42, whereas line 7 will calculate the sum 2 + 4 + 6 + 8.
1 2 3 4 5 6 7 8 | /* Code Listing A.27:
Calling a function with a variable-length parameter list
*/
int total = sum (1, 42);
printf ("Total is %d\n\n", total); // prints 42
total = sum (4, 2, 4, 6, 8);
printf ("Total is %d\n\n", total); // prints 20
|
Bug Warning
It is common to return pointers for a variety of reasons. For instance, a function might dynamically allocate some space (see the section on Pointers and Dynamic Allocation) to use as a buffer or make a copy of a string. However, it is critical that a function should never return a pointer to a local variable. This rule includes returning local copies of strings, as shown in the following example:
1 2 3 4 5 6 | char *
broken_function (void)
{
char string[] = "Hello!"; // string exists in stack frame
return string; // stack frame becomes invalid
}
|
The problem with returning pointers to local variables is that the data associated with the variable exists in a stack frame that is linked to the function call; once the function returns, the stack frame is de-allocated, making any future access to this part of memory invalid. Unfortunately, such code occasionally works without crashing because the stack frame has not been overwritten yet; this makes debugging the code very difficult, because the crashes seem random.
The limitation of returning only a single value can be frustrating, as a complex subroutine may need
to provide the caller with multiple pieces of data. One example of this need is a function that is
supposed to return a pointer. A common convention for such functions is to return the NULL
pointer if an error occurs in the function. However, this approach does not explain what the error
was or how the caller should react; if the function took a file descriptor in as input, was the
problem that the file was closed, the user running the program did not have access to the file, or
the file had no contents? Clearly, it would be desirable to provide such information.
One approach to providing multiple returns is simply to cheat: use global variables. This is the
approach that is commonly used for error indications as in the previous example. If the function
returns NULL
because of an error, it can also set the errno
global variable to indicate the
reason why the failure occurred. With few clearly defined exceptions (such as errno
), this
approach is undesirable and generally unsafe. Global variables require the programmer to be
careful to avoid name collisions. Furthermore, the very nature of global variables allows any
function to change them at any point (assuming the variable name is made visible with extern
).
This global accessibility is particularly fraught in concurrent systems, as multiple threads might
simultaneously need to call the same function, potentially creating a race condition with
interleaved modifications of the variable.
A better approach is to use call-by-reference parameters. Consider performing traditional integer division as taught in the early grades of school. Calculating 17 ÷ 5 has two parts to the answer: a quotient of 3 and a remainder of 2 (since 5 * 3 + 2 = 17). Code Listing A.28 demonstrates how to define a division function using a call-by-reference parameter.
1 2 3 4 5 6 7 8 9 10 11 12 13 | /* Code Listing A.28:
Returning multiple parameters with call-by-reference
*/
int
divide (int dividend, int divisor, int *remainder)
{
/* Set the remainder (dividend % divisor), then
return the quotient (dividend / divisor) */
assert (remainder != NULL);
*remainder = dividend % divisor;
return dividend / divisor;
}
|
In this function, the remainder
parameter is a pointer to an int
. The assert()
call on
line 10 is a safety check that will prevent anyone from passing a NULL
pointer to this function.
Within the divide()
function, we will use this pointer to change the original int
’s value.
Line 11 performs this by dereferencing the pointer and storing the result of the modulus operation
dividend % divisor
. Once we have done this, line 12 returns the normal C integer division
quotient (dividend / divisor
). Code Listing A.29 demonstrates how to call this
function by passing the address of remainder
. (Recall that pointers store addresses, so the
address of a variable becomes a pointer.) Figure 10.5.6 illustrates the
relationship between the stack frames for main()
(Code Listing A.29) and the call to
divide()
. The remainder parameter for divide()
contains a pointer back to the rem
variable
in main()
’s stack frame.
1 2 3 4 5 6 7 8 9 10 | /* Code Listing A.29:
Calling a function with a variable-length parameter list
*/
int rem = 0;
/* pass the address of rem for divide to set its value */
int quot = divide (17, 5, &rem);
printf ("17 ÷ 5 is %d R %d\n", quot, rem);
|
Bug Warning
A very common mistake among programmers who are new to C’s call-by-reference parameters is to try to
match the variable declaration instead of using the address-of operator (&
) as illustrated here:
int *remainder; // Don't EVER leave a pointer uninitialized!
int quotient = divide (17, 5 remainder);
This code would probably work successfully, but it is wrong and very dangerous. Yes, the types
are correct. The remainder
variable is an int*
, which matches the type that the
divide()
function expects. The problem lies in the answer to this question: What is the initial
value of remainder
? In other words, what portion of memory is remainder
pointing to?
In C, local variables are never initialized unless the programmer explicitly does so. The example
code above declares a pointer, but does not initialize it. When this happens, the value of the
variable (remainder
in this case) is whatever random values happen to already be there on the
stack. In other words, this code tells the machine to initialize remainder
so that it
points to a random location. In the context of a large, complex program, this code will produce a
segmentation fault if we are lucky! The segmentation fault would be an indication that there is a
problem. If we are unlucky, remainder
(by random chance) points to a valid location; the
problem is that the divide()
function will overwrite the contents of that random location.
Depending on that that memory location is supposed to be storing, this bug could cause a completely
unrelated part of the program to crash seconds, minutes, or hours later, with no indication that
this call to divide()
is the cause.
The difference between this code and Code Listing A.29 is that the address-of operator
requires us to know what we are initializing the pointer to. That is, it requires us to answer
“address of what?” By passing &remainder
for an int
variable, rather than declaring and
passing an int*
, we are providing the answer: the address of the local variable remainder
that was just declared.
In the earlier discussion on arrays, we made the observation that array lengths must be passed as
explicit parameters when an array is passed to another function. The reason for this is that arrays
are always passed as call-by-reference parameters. Code Listing A.30 provides an
example of a function that takes an array as a parameter and modifies the values stored in the
array. On line 6, the parameter list indicates that values is an array of int values. This
declaration is not entirely true; values is, in fact, just a pointer to an int
. Using the
declaration int values[]
instead of int *values
serves the purpose of indicating how values
will be used, but both declarations are acceptable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | /* Code Listing A.30:
All arrays are passed as call-by-reference pointers
*/
void
dub_all (size_t length, int values[])
{
for (size_t i = 0; i < length; i++)
values[i] *= 2;
}
int
main (void)
{
int data[] = { 1, 2, 3 };
dub_all (3, data);
for (size_t i = 0; i < 3; i++)
printf ("data[%zd] = %d\n", i, data[i]);
int faker = 5;
dub_all (1, &faker);
printf ("faker is now %d\n", faker);
return 0;
}
|
Figure 10.5.8 illustrates the relationship between main()
and the first
call to dub_all()
(for simplicity, the variable faker
is not shown). Although data
is
ostensibly passed to the dub_all()
function, it is actually just the address of the first
element that is passed. This structure is consistent with an observation that we made earlier: array
names are simply aliases for the address of their first element.
To emphasize the point even further, observe that line 21 makes another call to dub_all()
. In
this call, we are passing the address of the local variable faker
as an array of size 1. Since
array names and pointers can both be indexed using the array bracket notation, dub_all()
can
still successfully access the faker variable and change its value. From the perspective of
dub_all()
, there is no way to tell if the values
parameter is pointing to a single int
or something that was actually declared as an array. For this reason, the length of the array must
also be passed as a parameter to dub_all()
to control the number of iterations in that
function’s for
-loop on line 8.
[1] | Pedantically, such an f(x) would fit the definition of a mathematical function with an empty codomain. Such a function would not be conventional, however. |