«  10.4. Arrays, Structs, Enums, and Type Definitions   ::   Contents   ::   10.6. Pointers and Dynamic Allocation  »

10.5. Functions and Scope

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.

Location of %rsp before and after allocating the stack frame for helper()

Figure 10.5.1: Location of %rsp before and after allocating the stack frame for helper()

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.

Decorative bug warning

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
Decorative C library image

C library functions – <ctype.h>


void * memset(void *b, int c, size_t len);
Sets len consecutive bytes to the value c, starting at location b.

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
Decorative bug warning

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.

10.5.1. Function Parameters and Return Values

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:

$f(x) = x^2$

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
Decorative bug warning

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.

10.5.2. Call-by-Reference Parameters

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;
}
Stack frames for main() and divide()

Figure 10.5.6: Stack frames for main() and divide()

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);
Decorative bug warning

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.

10.5.3. Arrays as Parameters

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;
}
Stack frames for main() and dub_all()

Figure 10.5.8: Stack frames for main() and dub_all()

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.
«  10.4. Arrays, Structs, Enums, and Type Definitions   ::   Contents   ::   10.6. Pointers and Dynamic Allocation  »

Contact Us License