In this lecture, we carry on our introduction to the C language with a deep dive into C pointers.

Goals

  • to learn about C syntax for pointers, addresses, and dereferencing.

  • to recognize that all data is stored in memory, that memory is a linear sequence of bytes, and that every byte has a numeric address.

  • to understand that C stores the running program and its data in one of four areas of memory, each managed differently.

Pointers

The C programming language has a very powerful feature (and if used incorrectly a very dangerous feature), which allows a program at run-time to access its own memory. This ability is well supported in the language through the use of pointers. There is much written about the power and expressiveness of C’s pointers, and much (more recently) written about Java’s lack of pointers. More precisely, Java does have pointers, termed references, but the references to Java‘s objects are so consistently and carefully constrained at both compile and run-time, that very little can go wrong. That is not the case for C.

Pointers allow us to refer to the address of a variable rather than its value. One important use case of pointers is parameter passing to functions. Parameters to functions are typically passed by value, and so a rudimentary understanding of C’s pointers is needed to use “pass-by-reference” parameter passing in C.

Check out this animated explanation of pointers. Fun!

Pointer Definition

In essence, pointers are variables storing memory addresses. Every bit of information the computer needs must be stored somewhere in memory - whether instructions or data. The computer’s memory is a sequence of bytes, each byte with its own numeric address. If the computer has one megabyte of memory, these 2^20 bytes will be numbered from 0 to 2^20-1, that is, from 0 to 1,048,575. Or, since we’re computer scientists, we work in hexadecimal rather than decimal; the bytes are numbered from 00000 to FFFFF. In practice, we tend to write hexadecimal numbers in C notation: 0x00000 to 0xFFFFF.

Pointers are defined with asterisks (*) in front of the variable names. Here is an example defining a integer pointer xp, storing the address of an integer variable x.

int x = 42;
int* xp = &x; 
int** xxp = &xp; // a pointer to pointer

Note that & is a C operator that gets the address of a variable. You might read this operator as “address of…”.

By the way, the NULL pointer is simply address zero (0x00000). The OS arranges for that memory segment to be illegal for reading or for writing, so if you try to dereference a null pointer for reading or writing (think char *p=NULL; char c = *p;) the OS will catch it and trigger a ‘segmentation fault’.

Clearly the size of a pointer variable is the size of a memory address, which is dependent on the specific system. A 32-bit system needs 4 bytes to represent a memory address, while a 64-bit system needs 8 bytes.

Pointer Dereferencing

After the pointer definition, the asterisk placed in front of a pointer variable means dereferencing this pointer variable, i.e., getting the value/content in the memory address stored by the pointer variable. We may dereference variables on both sides of an assignment expression.

*xp = 4; 
int y = *xp; 

Printing Pointers

We can print the value of a pointer variable. Before the ANSI C standard was set, you had to use %x or %lx to format the pointer for output. But that posed the problem of the code having to “know” the size of the pointer. ANSI C solved the problem with the new %pformat specification that will automatically do the right thing. Here are a couple examples:

printf("xp stores the address %p\n", (void *)xp);
printf("variable x lives at the address %p\n", (void *)&x);

We need to cast the pointer to a generic pointer type void * so that the compiler will not complain about type mismatch. void * indicates that the pointer can point to a memory storing any data type. Thus, it can be then converted (with a cast) to any data type, e.g., int *, char *.

void *vp; 
xp = (int *)vp; 

We will come back to this in a later lecture.

Do you know that a function name stores the starting address of the memory where the function code lives? Check this example:

printf("main() function address is %p\n", (void *)main);

Read and run these programs to see more examples of printing pointer values:

Pointer Arithmetic

Pointer arithmetic allows us to advance a pointer to point to successive memory locations at run-time. It would make little sense to be able to “point anywhere” into memory, and so C automatically adjusts pointers (forwards and backwards) by values that are multiples of the size of the base types (or user-defined structures) to which the pointer points.

We specify pointer arithmetic in the same way we specify numeric arithmetic, using +, -, and pre- and post- increment and decrement operators (multiplication and division make little sense). Here is an example where we advance a character pointer to count the length of a string:

int my_strlen(char *str) {
  int len = 0;

  while(*str != '\0') {
    ++len;
    ++str;   // advance str pointer
  }

  return (len);
}

This example is a little simple, because the character pointer will advance only one byte at a time, as a character is one-byte long. Here is an example of pointer arithmetic to sum up an integer array:

int sum_array(int *values, int n) {
  int sum = 0;

  for (int i = 0; i<n; i++) {
    sum += *(values+i);
  }

  return (sum);
}

Memory

To truly understand C and the use of pointers, you need to understand how C programs use memory. There are two types of memory used by C programs: code memory and data memory.

When you run your program, it becomes a process within Unix. (To see a list of all your processes, type ps at the bash command line. To see all the processes running on the system, try ps ax.) Your program’s executable code is copied into memory so the processor can read the instructions and execute them. This region is often called the code segment or text segment).

Data memory is where all the data your program manipulates live. There are three distinct regions of data memory (aka data segments). C manages each differently. Let’s look at each in turn.

Global memory (aka static)

The simplest region is called ‘global’ (or ‘static’) storage, and it’s where global variables live. If you define a constant or variable outside of any function, it has

  • global scope - meaning that it is visible to all code in any function within the current file, and
  • static storage - meaning that C allocates space for it (and initializes the contents) before the program begins executing; its address will never change throughout the execution of the program (hence the name ‘static’).

For example,

const float pi=3.1415926535;
const char usage[] = "Usage: dog [-n] [-m mapfile] [-] [filename]...\n";
int error_count = 0;

int main()
{
...
}

The above declares three global variables, two of which are constants and one of which is variable. All are visible to functions defined below their declaration. Global variables are always declared at the top of a C file, after #include directives and before any function definitions.

Global variables can be handy, and sometimes necessary, but it is good style to avoid use of global variables. (Global constants are generally ok.) Well-modularized programs keep their data close - passing the data (or pointers to data) among functions.

There are two kinds of ‘global’ in C: global within a C file, and global across all C files in the program. The former are reasonable, if used carefully, but the latter are more dangerous.

The Stack (for local variables)

All of the example code we’ve seen so far makes extensive use of local variables; these variables are defined within a function and have

  • local scope - meaning that the variable is visible within the function only, and
  • stack storage - meaning that C allocates space for the variable within the stack frame for this particular instance of this function call.

Note: Local variables include the function’s parameters.

Think about how a stack works. When the program starts, C allocates a chunk of bytes on the stack to hold all the variables defined in main. This chunk is called a ‘stack frame’. It does not initialize these bytes - the program must initialize variables, either in the variable definition or in the code to follow. Later, when main calls another function, say, readline, it allocates another chunk of bytes on the stack (you could say, it pushes another frame on the stack) to hold the variables defined by readline . When readline calls fgetc, it pushes another stack frame on the stack, a chunk of bytes to hold the variables defined within fgetc. When fgetc returns, it pops the frame off the stack. When readline returns, it pops that frame off the stack. The local variables defined in readline are not just syntactically inaccessible to main (out of scope), their memory is no longer allocated. Indeed, when main calls another function, say, printf, a stack frame is pushed on to the stack for printf, re-using the same memory that had been allocated to readline and fgetc).

The Heap (dynamically allocated memory)

The third kind of data memory is called the “heap”. It is a large region of memory managed by malloc(). Each call to malloc() selects a chunk of bytes from the heap region, and returns a pointer to that chunk. It keeps careful records of which chunks have been allocated, and which are free. It is the programmer’s responsibility to, eventually, return unused chunks by calling free(p) where p is a pointer earlier returned by malloc. If the programmer forgets to call free, that chunk can never be reused, and eventually malloc will run out of free chunks to be allocated. (This situation is called a ‘memory leak.’) It is also the programmer’s responsibility not to call free multiple times for the same pointer; doing so may corrupt the records kept by the memory-allocation library, and will likely lead to program crashes.

There are four related functions you should understand:

  • p = malloc(n) - allocates n bytes of heap memory
  • p = calloc(count, size) allocates count*size bytes of heap memory
  • p = realloc(p, n) - where p is a pointer to heap memory - expands (or shrinks) its allocation to n bytes.
  • free(p) - where p is a pointer to heap memory - releases that portion of heap memory for future use.

On exit

When the process exits, all its memory is released - the four segments (code, global, stack, and heap) disappear.

Activity

Today’s activity looks at a program that copies a string - and tries to find a bug.