An Introduction to Pointers in C

CS35: Programming and Problem Solving
Ray Ontko
Department of Computer Science
Earlham College

Why Pointers?

In our calls to scanf(), we have used the magical & as a prefix to the variables we wish to read. You are now about to be admitted to the exclusive, inner circle of people who know what & means.

When we use a variable name as an argument to a typical function, we are passing the value stored in that variable. In other words, the name of the variable evaluates to the value stored in the variable. Similarly, when we pass a literal number, like 12.3, we're passing the value represented by 12.3.

The C programming language provides a special mechanism for passing variables to functions that is a little unintuitive, but extremely powerful. C provides two operators, & and *, which allow you to pass a variable instead of its value (by using & in the call to the function), and to indicate that a parameter is a variable instead of a value (by using * in the declaration and definition of the function). By careful placement of & in the arguments of a call, and of * in the definition and declaration of the function, it is possible for a function to modify variables in the calling function.

How Does it Work?

The & operator applies to the thing which immediately follows it, usually a variable. What it does is return the address of the variable, instead of the value of the variable. It returns a pointer to the variable. For example:
main()
{
int x = 7 ;

printf( "%d\n" , x ) ;  /* show the value of x */
printf( "%d\n" , &x ) ; /* show the address of x */
}

When run, the above program might produce the following output:

7
-1073742924
where "7" is (obviously) the value of x, and "-1073742924" is the address of x (expressed as a signed decimal number).

The * operator also applies to the thing which immediately follows it, usually the address of a variable. What it does is return the variable that the address points to, instead of the numerical value of the address. It dereferences a pointer to a variable. For example:


main()
{
int x = 7 ; /* x is an integer with the value 7 */
int *px ;   /* px is a pointer to an integer */

px = &x ;   /* px gets the address of x */
*px = 8 ;   /* the thing pointed to by px (x) gets the value 8 */

printf( "%d\n" , x ) ;  /* show the value of x */
printf( "%d\n" , &x ) ;  /* show the address of x */
printf( "%d\n" , px ) ;  /* show the value of px */
printf( "%d\n" , *px ) ; /* show the value of what px points to */
}
When run, the above program might produce the following output:
8
-1073742924
-1073742924
8
where the first "8" is (obviously) the value of x, the first "-1073742924" is the address of x (expressed as a signed decimal number), the second "-1073742924" is the value of px (expressed as a signed decimal number), and the second "8" is the dereferenced value stored at the address pointed to by px.

Note that we can not only read a variable if we know its address (by using the * operator in an expression), but we can also modify a variable if we know its address (by using the * operator in front of a variable on the left of the equals sign).

Further note that we define px as pointer to an int by including it in an "int" declaration and prefixing the name of the variable (px) with "*". As you will see below, we can delcare pointer types for the parameters and return-values of functions.

It is important to note that & and * are unary operators and apply to the "thing" (expression) that follows them. Unlike the usual meanings of the arithmetic operators, +, -, *, and /, each of which has two operands, & and * (in this context) only have one operand.

How Does This Relate to Functions?

What this means is that if we want to allow a function to modify a variable, we can pass to the function the address of the variable using the & operator. The function, knowing that the parameter is an address of a variable, can dereference the address to access the variable itself using the * operator.

We do this by declaring a parameter to be of a pointer type, instead of a base type. In the function itself we use the * operator as a prefix to the name of the parameter when we with to read or modify the value at the address pointed to by the parameter.

For example, suppose you want to perform a number of calculations involving the time of day. For your purposes, it is sufficient to think of times in terms of hours, minutes, and seconds (you don't care about smaller intervals of time). You decide that your programming would be much easier if you had routines that allowed you to operate on times as if they were seconds past midnight, or as hours, minutes, and seconds. You then implement the following routines and test program.

void hms_to_spm( int hours , int minutes , int seconds , 
                 int *seconds_past_midnight ) ;

void spm_to_hms( int seconds_past_midnight , 
                 int *hours , int *minutes , int *seconds ) ;
main()
{
  int h, m, s, spm ;

  printf("Enter a time (hh:mm:ss)\n" ) ;
  scanf( "%d:%d:%d" , &h, &m, &s) ;

  hms_to_spm( h, m, s, &spm ) ;
  spm += 15 ;
  spm_to_hms( spm , &h, &m, &s ) ;

  printf( "The time, fifteen seconds later, is %02d:%02d:%02d\n" , h , m , s ) ;
}

void hms_to_spm( int hours , int minutes , int seconds , 
                 int *seconds_past_midnight )
{
  *seconds_past_midnight = hours * 3600 + minutes * 60 + seconds ;
}

void spm_to_hms( int seconds_past_midnight , 
                 int *hours , int *minutes , int *seconds )
{
  *hours = ( seconds_past_midnight / 3600 ) % 24 ;
  *minutes = ( seconds_past_midnight / 60 ) % 60 ;
  *seconds = seconds_past_midnight % 60 ;
}

When run, the program might produce the following dialogue:

Enter a time (hh:mm:ss)
18:23:48
The time, fifteen seconds later, is 18:24:03 

In our main(), note that we pass the address of spm to hms_to_spm(), and then the addresses of h, m, and s to spm_to_hms().

In hms_to_spm(), note that seconds_past_midnight is declared as "int *" (int pointer) and that all references to it are dereferrenced using the * operator. Similarly, in spm_to_hms(), note that hours, minutes, and seconds are declared as "int *" (int pointers) and that all references to them are dereferrenced using the * operator.

One final thought. Since hms_to_spm() only "returns" one value, we might have implemented it using the declaration

int hms_to_spm( int hours , int minutes , int seconds ) ;

In our example we chose not to do this for symetry with spm_to_hms(), but the consequence of our choice is that we can't use on the right hand side of an assignment; we must always have a variable in which to put the result. For this reason, most functions which return a single value do so using the return-value mechanism of functions.

Copyright © 1999, Ray Ontko (rayo@ontko.com).