Code Capsules

Pointers, Part 1: The Basics

Chuck Allison


Chuck Allison is a regular columnist with CUJ and a software architect for the Family History Department of the Church of Jesus Christ of Latter Day Saints Church Headquarters in Salt Lake City. He has a B.S. and M.S. in mathematics, has been programming since 1975, and has been teaching and developing in C since 1984. His current interest is object-oriented technology and education. He is a member of X3J16, the ANSI C++ Standards Committee. Chuck can be reached on the Internet at allison@decus.org, or at (801)240-4510.

Programming on the Edge

Segmentation Violation
Access Violation
Suspicious Pointer Conversion
Non-portable Pointer Conversion
Null Pointer Assignment
Do any of these messages sound familiar? Your compiler issues these messages when you use pointers improperly. Pointers gone awry are a C programmer's worst nightmare. Indeed, pointers and the raw power they give the developer are a popular criticism of C. "It's just too dangerous," people say. The philosophy of C, however, is "Trust the Programmer." The truth is not that C is dangerous, but simply that some programmers aren't ready to be trusted. You must master pointers to use C productively (and safely). Fortunately, that mastery follows naturally from a few basic principles and techniques.

The Basics

All data objects other than register variables reside somewhere in memory. That "somewhere" has an address. On platforms that number each byte of memory in sequence starting from zero, an address is simply the sequence number of a byte. (Some systems have more sophisticated addressing schemes). The following program shows how to find the address of program variables:

/* address.c */
#include <stdio.h>

main()
{
  int i = 7, j = 8;

  printf("i == %d, &i == %p\n",i,&i);
  printf("j == %d, &j == %p\n",j,&j);
  return 0;
}

/* Output:
i == 7, &i == FFF4
j == 8, &j == FFF2
*/
The & unary operator returns the address of a data object. The %p edit descriptor displays an address in a compiler-dependent format (usually hexadecimal). For all the examples in this article, both integers and addresses are 16-bit quantities (your output may vary).

The memory layout for i and j above looks like this:

Where your compiler allocates memory is not critical. For instance, i and j happen to be adjacent in memory, but some architectures have gaps between objects due to alignment requirements. Notice that my compiler allocated i after j in memory ("after" meaning at a higher memory address). How the computer actually stores the bits of a number also depends on your system. In fact, on the PC, the 7 in i is not really stored in the rightmost portion of i's memory. You can think of it that way most of the time, though, because it is logically 0x0007, no matter how the bits are physically laid out in memory.

A pointer is nothing more than a variable that holds the address of another object. Usually you just want to use it to refer to an object in memory so don't concern yourself with the actual numerical value of an address. The program in Listing 1 illustrates the use of pointers. A pointer always points to an object of some type, so the referenced type must always appear in the declaration (we speak of pointer to int or pointer to char). When an asterisk precedes a pointer, the resulting expression refers to the value pointed at. The declaration

int *ip;
indicates that *ip is an int, therefore ip is a pointer to an int. The process of referring to memory indirectly through a pointer is called indirection or dereferencing (take your pick). Indirection can occur on either side of an assignment statement. With the declarations in Listing 1, the statement

*ip = 9;
would have the same effect as

i = 9;
If you could define a pointer without mentioning the referenced type, the expression *ip would be meaningless and indirection would be impossible. Never forget that a pointer doesn't just point somewhere in memory; it points to an object of some type. The only exception to this rule, a pointer that points nowhere, happens when you assign a pointer the special value NULL (defined in stdio.h, stdlib.h, stddef.h, locale.h, string.h, and time.h). You cannot dereference a NULL pointer; you can only compare it to other pointers.

The memory layout for the program pointer.c is:

Although addresses usually appear as numbers, you should not assume any relationship exists between pointers and integral data types. Pointers are a unique data type and should be treated as such. The only things you can do with a pointer are:

Since the relative memory position of objects is not important (except for array elements, of course), it is usually better to depict the logical layout of memory, like this:

One usually says ip points to i or jp points at j.

The notion of a pointer is so simple that novices often frustrate themselves by looking beyond the mark. If you want to avoid needless hours of confusion, just remember this:

(Of course, the learning process will create some hours of struggle, but remembering Grand Pointer Principle #1 will keep them to a minimum). Notice that Grand Pointer Principle #1 says that a pointer is an address instead of holds an address. Both are true. A pointer is an address like an int is an integer, yet one usually doesn't say that an int i, for instance, "holds" an integer. I just want to emphasize that when you use a pointer, think address. What could be simpler?

Since all objects have an address, you can have pointers to any kind of object, including another pointer. Program ptr2ptr.c (Listing 2) shows how to define a pointer to a pointer to an integer. The memory layout for this program is

If ipp is a pointer to a pointer to an integer, then *ipp is the pointer it points at. To finally arrive at the integer 7 in i requires another level of indirection, hence the expression **ipp. In other words,

**ipp == *ip == i

Pointer Arithmetic

When a pointer references an array element, you can add (or subtract) an integer to it to point to other array elements. Adding 1 to such a pointer increases its value by the number of bytes in the referenced type, so that it points to the next array element. The program in Listing 3 performs integer arithmetic within an array of floats. The array looks like this:

On my system a float occupies four bytes. Incrementing p by 1 actually increases its value by 4 (from FFEA to FFEE). Subtracting two pointers to array elements performs the expected complementary operation: it yields the number of array elements between the two addresses. In other words, if p and q are pointers to the same referenced type, then the statement q = p + n implies q - p == n. The portable way to store the difference between two pointers is in a ptrdiff_t, defined in stddef. h. The portable way to print a ptrdiff_t is by casting it to a long.

The rules of pointer arithmetic can be summarized by the following formulas:

p  n == (char *) p  n * sizeof(*p)
which says "adding (or subtracting) an integer n to (or from) a pointer moves the pointer up (or down) in memory n elements of the referenced type," and

p - q ==  n
where n is the number of elements between p and q.

Of course, pointer arithmetic can only be meaningful within arrays since the formulas assume a sequence of equally-sized objects. However, you can interpret any single object as an array of bytes. The program in Listing 4 dissects an integer by storing its address in a pointer to char and visiting each byte by pointer arithmetic. Note the cast in the initialization of cp. Assigning pointers of different types requires a cast to convince the compiler that you know what you're doing (otherwise it will tell you that it suspects that you don't — hence the warning, Suspicious pointer conversion). You don't need casts, however, when converting to and from void pointers. (More on this in the section "Generic Pointers.")

Take a closer look at the output in Listing 4:

This reveals an interesting fact: the Intel processor in my PC stores things "backwards," in that the least significant values of an object are stored at the lower memory addresses. This storage scheme is called little-endian, because as you move up through memory, you encounter the "little end" first. VAX machines are also little-endian, but IBM mainframes are big-endian. This is usually not a concern in common data processing applications, but sometimes it is important.

Suppose, for example, that you want to efficiently store a date from this century. You need storage as follows:

Fortunately, this combines to 16 bits, the size of an integer on a PC. One obvious way, then, to store a date in an int is to use bitwise operations (see Listing 5) . The logical layout of the bits for the date August 2, 1992 (B902) is as expected:

But a little-endian machine stores it physically like this:

Using the following bit-field structure makes for a more readable program (see Listing 6) :

struct date
{
  unsigned day: 5; 
  unsigned mon: 4; 
  unsigned year: 7; 
};
To interpret an integer as a bit-field structure, simply cast a pointer to it into a struct date pointer. Now you can access the date components by name without the shifting and masking that bitwise operations require. To access a structure member via a pointer you need to first dereference the pointer and then name the member:

(*dp).mon
You can replace this unwieldy syntax with the shorthand:

dp->mon
that I've heard pronounced dp arrow mon, dp pointing to mon, and few other ways not worth mentioning.

Pass-By-Reference Semantics

C always passes arguments to functions by value, meaning functions use a copy of each argument locally. As a result, a function can't change the corresponding value of a parameter in the calling program. For example, the naive approach to swapping two integers in Listing 7 has no effect on i and j in main.

A scheme called pass-by-reference allows changes to function arguments to persist after leaving a function. You can simulate pass-by-reference semantics in C by passing pointers to the arguments that you want to change. You alter the values in the calling program indirectly through those pointers (see Listing 8) .

Generic Pointers

Certain operations apply equally well to objects of different types. It is often convenient, therefore, to write functions that can accept parameters that point to any type. For example, the standard library function memcpy copies a block of memory from one address to another. You might want to call memcpy to copy structures of your own making:

struct mystruct a, b;
/* . . . */
memcpy(&a,&b,sizeof (struct mystruct));
To handle a pointer of any type, memcpy declares its first two arguments as pointers to void. You can assign a pointer to any type to and from a void * without a cast. Here is a portable implementation of memcpy that illustrates void pointers:

void *memcpy(void *target,const void *source,size_t n)
{
  char *targetp = target;
  const char *sourcep = source;
  while (n-)
    *targetp++ = *sourcep++;
  return target;
}
This version of memcpy must assign the pointers to void to pointers to char so it can traverse the memory blocks a byte at a time, copying as it goes. It makes no sense to try to dereference a void *, since its size is unknown.

const Pointers

Note the const keyword in the second parameter of the previous code fragment. This tells the compiler that memcpy will not be changing any of the values that source points to. When passing pointers as parameters, always use the const qualifier when it applies. It protects you not only from inadvertently making an incorrect assignment, but even from passing const objects to functions that modify their pointer parameters. For example, if the declaration in Listing 8 had been

const int i = 7, j = 8;
you would get a warning for the statement

swap(&i,&j);
because swap actually does change the values its arguments point to.

Look through the include files stdio.h, string.h, and stdlib.h provided with your compiler and you will see generous use of const. When const appears anywhere before the asterisk in a declaration, it indicates that the referenced contents will not change:

const char *p;  // pointer to const char
char const *q;  // likewise, pointer to const char
char c;
c = *p;         // OK (assuming p an q are initialized:)
*q = c;         // Error
You can also declare that the pointer itself cannot change by putting the const after the asterisk:

char * const p;
*p = 'a';       // OK, only the pointer is const
++p;            // Error, can't modify pointer
To disallow modification of both the pointer and the contents referenced, use const in both places:

const char * const p;
char c;
c = *p;      // OK - can read contents
*p 'a';      // Error
++p;         // Error
The function inspect in Listing 5 shows how to print the individual bytes of any object. (The output from inspect.c is in Figure 1) . Since I don't alter the contents of the object, the first parameter is a pointer to const, and I am careful to convert it to a pointer to const char before using it. You will notice that I pass the array s without taking its address. This is because C passes arrays by reference. Next month's capsule will explore the intricate relationship between arrays and pointers.

Summary

Proper use of pointers is unquestionably the sticky wicket of C. In this capsule I have only covered the basics:

Listing 9