Tutorial : C - Pointers

Pointers

A pointer is a variable that contains the address of a variable.

Pointers provide a mechanism for the direct manipulation of memory. They are arguably the most powerful, and the most dangerous, feature of the C programming language. To the novice programmer who has not before encountered address-based computation, pointers can be a difficult concept to grasp. But with a little experience, they can be used to produce concise and efficient code.

Pointers have been lumped with the goto statement as a marvelous way to create impossibleto-understand programs. This is certainly true when they are used carelessly, and it is easy to create pointers that point somewhere unexpected. With discipline, however, pointers can also be used to achieve clarity and simplicity.

 

What is a Pointer?


To explain the operation of a pointer, it is first necessary to understand, at least in a basic sense, the way in which memory is organised. A simplified picture of a layout of computer memory. A typical machine has an array of consecutively numbered memory cells. These numbers are termed addresses. Each cell consists of a set of bits, and the cell bit-pattern is the cell’s value.

When a variable is defined, it is allocated a portion of memory. Thus, the variable has a value and an address for where that value resides. A pointer is a variable whose value is the address of another variable. Consider an example. (We will ignore, for the moment, various details such as the different byte-sizes for different types.) Let x be defined and initialised to the value 3.

char x = 3;

Assume this variable is stored at address 62. A pointer px is subsequently defined, assume it is stored at address 25, and initialised with the address of x as follows.

char *px = &x;

The value of px, therefore, is 62. Notice that a pointer is just another type of variable; it, also, has an address and may in turn be pointed-to by a pointer-to-a-pointer variable.

Memory cells may be grouped together to represent different variable types. On most machines, a cell is 8-bits long (i.e., one-byte). A char is usually one cell, a short int two cells, and a long int four cells. Each type (e.g., a double) has an associated pointer type (e.g., a double *), which is aware of the number of cells that the type occupies, and enables the compiler to behave appropriately with sequences of a particular type (e.g., an array of doubles).

On most machines, pointer variables also require multi-cell storage; typically for a 16-bit machine the pointer type is two-bytes, and for a 32-bit machine the pointer type is four-bytes. However, it is unwise to make assumptions about the size of a pointer type as such code is likely to be non-portable.

The size of a pointer type determines the maximum value it can represent, and hence the maximum range of addresses it can deal with. For example, a 16-bit pointer can only handle addresses between 0 and 216 − 1 (i.e., 65535). The main reason for the development of 32-bit machines was to enable greater memory addressing; a 32-bit pointer can address 0 to 232 − 1 (i.e., 4294967295) bytes of memory.

 

Pointer Syntax

A pointer of a particular type is declared using the * symbol, and the address of a variable is obtained using the “address-of” operator &. For example,

int i;
int *j = &i;

defines a pointer-to-int variable j and initialises it with the address of i. This operation might equivalently have been written,

int *j, i;
j = &i;

It is worth noting that the * in a list of definitions refers only to the adjacent variable, and the spacing is irrelevant. For example, in the following,

int* i, j, * k;

i and k are pointers-to-int, while j is a plain int. The best style for such definitions is usually to place the * against the variable to which it refers.

int *i, *k, j;

The value of the variable to which a pointer points can be obtained using the indirection or dereferencing operator *.

int i = 2;
int *j = &i; /* Define a pointer-to-int j, and initialise with address of i. */
int x = *j; /* x is assigned the value of i (that is, 2). */

The dereferencing use of * should not be confused with its use in pointer-declaration syntax. The declaration *, meaning “is a pointer-type variable” occurs only in variable or function declarations, and in all other circumstances the * means dereference or “access the pointed-to object”. Some examples of simple pointer operations are shown below.

char c = ’A’;
char *pc = &c; /* pc points to c */
double d = 5.34;
double *pd1, *pd2;
*pc = ’B’; /* Dereferenced pointer: c is now equal to ’B’. */
pd1 = &d; /* pd1 points to d */
pd2 = pd1; /* pd2 and pd1 now both point to d. */
*pd1 = *pd2 * 2.0; /* Equivalent to d=d* 2.0; */

Notice that pointers have different types specifying the type of object to which they can point. It is an error to assign a pointer to an object of a different type without an explicit cast.

float i = 2.f;
unsigned long *p1 = &i; /* Error: type mismatch, won’t compile. */
unsigned long *p2 = (unsigned long *)&i; /* OK, but strange. */

The exception to this rule is the void* pointer which may be assigned to a pointer of any type without a cast.

It is dangerous practice to leave a pointer uninitialised, pointing to an arbitrary address. If a pointer is supposed to point nowhere, it should do so explicitly via the NULL pointer. NULL is a symbolic constant defined in the standard headers stdio.h and stddef.h. It is usually defined as

#define NULL ((void*) 0)

The constant values 0 or 0L may be used in place of NULL to specify a null-pointer value, but the symbolic constant is usually the more readable option. Pointers may be declared const; and this may be done in one of two ways. The first, and most common, is to declare the pointer const so that the object to which it points cannot be changed.

int i = 5, j = 6;
const int *p = &i;
*p = j; /* Invalid. Cannot change i via p. */

However, the pointer itself may be changed to point to another object.

int i = 5, j = 6;
const int *p = &i;
p = &j; /* Valid. p now points to j. */
*p = i; /* Invalid. Cannot change j via p. */

The second form of const declaration specifies a pointer that may only refer to one fixed address. That is, the pointer value may not change, but the value of the object to which it points may change.

int i = 5, j = 6;
int * const p = &i;
*p = j; /* Valid. i is now 6 */
p = &j; /* Invalid. p must always point to i. */

It is possible to combine these two forms to define a non-changing pointer to a non-changeable object.

int i = 5, j = 6;
const int * const p = &i;
*p = j; /* Invalid. i cannot be changed via p. */
p = &j; /* Invalid. p must always point to i. */

 

 

Pass By Reference

When a variable is passed to a function, it is always passed by value. That is, the variable is copied to the formal parameter of the function argument list. As a result, any changes made to the local variables within the function will not affect the variables of the calling function. For example, the following code to swap two variables will not work as intended.

swap(a, b); /* Pass values of a and b, respectively. */
void swap(int x, int y)
/* x and y are copies of the passed arguments. */
{
	int tmp = x; /* The variable x is unrelated to the variable a */
	x = y; /* so this operation does not affect a. */
	y = tmp;
}

The variables x and y are different to a and b; they are stored at different addresses, and are simply initialised with the values of a and b.

The desired effect of this function can be achieved by using pointers. Pointers, as with any other variable, are passed by value, but their values are addresses which, when copied, still point to the original variables.

swap(&a, &b); /* Pass pointers to a and b, respectively. */
void swap(int* px, int* py)
/* px and py are copies of the passed pointer arguments. */
{
	int tmp = *px; /* The value of px is still the address of a */
	*px = *py; /* so this dereferencing operation is equivalent to a = b. */
	*py = tmp;
}

Pointers provide indirect access to variables. This is why the * operator is called the indirection operator. Passing pointers as function arguments, therefore, is known as “pass-by-reference”.

Pass by reference semantics is useful for implementing functions, such as swap() above, that require multiple return values. It is also useful as a mechanism to avoid copying large objects between functions; rather than make a copy of a large object, it is sufficient to pass a pointer to the object. (Arrays are a good example2 of this and, in C, arrays are passed by reference by default. When passed as a function argument, an array name is automatically converted to a pointer to its first element.) It is possible to prevent unwanted change to a pass-by-reference argument by declaring the parameter const. For example

void cannot_change(const double *array, int len)
/* May perform read-only operations on array */
{
	int i;
	for (i = 0; i<len; ++i) {
		*(array + i) = 3.2; /* Invalid. Pointed-to objects cannot be changed. */
		array[i] = 5.4; /* Invalid. */
		printf("%f ", array[i]); /* Valid. */
	}
}

A const-pointer declaration has two purposes. It enables the compiler to enforce compile-time checks that the passed object is not changed within a function (i.e., it assists in ensuring the function is correct), and it informs the users of a function that the function will not modify the object they pass to it (i.e., it specifies a “non-modifying” guarantee).

 

Pointers and Arrays

Pointers and arrays are strongly related; so much so that C programmers often assume they are the same thing. This is frequently the case, but not always. Whenever an array name appears in an expression, it is automatically converted to a pointer to its first element. For example

unsigned buffer[256];
unsigned *pbuff1 = buffer; /* Buffer converted to pointer, & not required. */
unsigned *pbuff2 = buffer + 5; /* A "pointer-plus-offset" expression. */

Here pbuff1 points to element 0 of the array, and pbuff2 points to element 5. Similarly, when an array name is passed to a function, it is converted to a pointer. Thus, in the following example, pdouble and darray are equivalent; they are both pointers.

void func(double *pdouble, int len);
void func(double darray[], int len);

An array name and a pointer to an array may be used interchangeably in many circumstances, such as array indexing. Consider the following example, where, within each commented group, the statements perform exactly the same operation.

char letters[26];
char *pc1 = letters; /* Equivalent pointer values. */
char *pc2 = &letters;
char *pc3 = &letters[0];
letters[4] = ’e’; /* Equivalent indexes. */
pc1[4] = ’e’;
*(letters + 4) = ’e’;
*(pc2 + 4) = ’e’;
pc3 = &letters[10]; /* Equivalent addresses. */
pc3 = &pc1[10];
pc3 = letters + 10;
pc3 = pc2 + 10;

Notice that, for an array, its name (e.g., letters) when used in an expression is equivalent to its address (e.g., &letters), which is equal to the address of its first element (e.g., &letters[0]). The elements of an array can be accessed via the index operator (e.g., pc1[4]) or by a dereferenced pointer offset (e.g., *(pc2 + 4)). And the address of an array element can be obtained using the address-of operator (e.g., &letters[10]) or directly from the pointer offset (e.g., letters + 10).

However, an array is not equivalent to a pointer, and there are several important differences. First, an array is not a variable; its value cannot be changed.

int a1[10], a2[10];
int *pa = a1;
a1 = a2; /* Error: won’t compile. */
a1++; /* Error: won’t compile. */
pa++; /* Fine, a pointer is a variable. */

Second, an array name always refers to the beginning of a section of allocated memory, while a pointer may point anywhere at all (e.g., allocated memory, free memory, NULL). And third, the size of an array is the number of characters of memory allocated, while the size of a pointer is just the size of the pointer variable.

double a1[10];
double *pa = a1;
size_t s1 = sizeof(a1); /* s1 equals 10 * sizeof(double) */
size_t s2 = sizeof(pa); /* s2 equals sizeof(double *) */

 

 

Pointer Arithmetic

Each variable type has a corresponding pointer type. This allows the compiler to automatically calculate the byte-offset required for indexing an array of that type. For example, on many machines a double is 8-bytes long; suppose a double-pointer pd has some initial value and is then incremented by one pd++. The address now held by pd is 8-bytes beyond the original address, so that it points to the next double. This mechanism makes pointer arithmetic simple and uniform for all types, and hides the details of type-sizes from the programmer.

So, in general, if p is a pointer to some element of an array, then p++ increments p to point to the next element, and p += n increments it point n elements beyond where it did originally. C permits a variety of arithmetic operations to be performed on pointer types as illustrated in the following example.

float fval, array[10];
float *p1, *p2, *p3 = &array[5];
int i=2, j;
p1 = NULL; /* Assignment to NULL (or to 0 or 0L). */
p2 = &fval; /* Assignment to an address. */
p1 = p2; /* Assignment to another pointer (of same type). */
p2 = p3 - 4; /* Addition or subtraction by an integer: a pointer-offset expression. */
p2 += i; /* Another pointer-offset expression. */
j = p3 - p2; /* Pointer subtraction: gives the number of elements between p2 and p3. */
i = p2 < p3; /* Relational operations <, >, ==, !=, <=, >= */

At the end of the sequence of operations above, p1 points to the address of fval, p2 points to the fourth element of array, p3 points to the sixth element of array, i equals one, and j equals 2.

Integer arithmetic operations (i.e., pointer-offset expressions) permit access to array elements at some location relative to the current element. Relational comparisons (i.e., ==, <, >=, etc) are for determining whether the position of an array element is before or after another. And a numerical value for this relative position can be found by pointer subtraction.

Note. The arithmetic and relational operations are only valid for pointers to elements of the same array. Their behaviour is undefined if applied to pointers from different arrays.

The following arithmetic operations may not be applied to pointers. Addition or subtraction by a floating-point value, and multiplication or division by a value of any type. Assignment to any non-pointer type is not permitted (although this can be forced using a cast). Also, while subtraction of two pointers is valid, addition of two pointers is not.

As an example of using pointer arithmetic in place of array indexing, consider the following implementations of strcpy(), a variant of the standard library function for copying a string t to a character array s. The first implementation uses array indexing

void strcpy (char *s, char *t)
{
	int i=0;
	while ((s[i] = t[i]) != ’\0’)
	++i;
}

Recall that an array index operation is equivalent to a dereferenced pointer offset (e.g., s[i] is equivalent to *(s+i)), so each iteration of the above loop involves three additions. The next implementation increments the pointers directly

void strcpy (char *s, char *t)
{
	while ((*s = *t) != ’\0’) {
	++s;
	++t;
	}
}

This results in just two additions per loop. The final implementation below is a common variant of the second one. It neglects the explicit comparison with \0 (as the \0 character has value zero), and moves the increment operations inside the conditional expression. The loop will now terminate when the value of *s is 0.

void strcpy (char *s, char *t)
{
	while (*s++ = *t++);
}

This code is perhaps more cryptic to the novice than the previous implementations, but occurs often in C programs. Learning the idioms of experienced C programmers is part of the process of becoming a proficient C programmer.

Style note. The efficiency gains of pointers over array indexes is largely irrelevant with modern optimising compilers. For the above implementations of strcpy(), a good compiler would produce exactly the same executable code. It is usually bad practice to write obscure pointer-based code solely for the sake of efficiency; pointer arithmetic is best used when it makes code simpler and more readable.

A couple of lesser-known facts about pointers and arrays. First, an index can be negative so long as it still refers to an element within the array. For example,

int array[10];
int *pa = array + 5;
int i = pa[-3]; /* i equals the value of array[2] */

Second, the pointer one-past-the-end of an array is valid for pointer arithmetic.

int array[10];
int *pa, *end = &array[10];
for (pa = array; pa != end; ++pa)
	*pa = end - pa; /* Values of array elements will be: 10,9,8,7,6,...,1 */

However, this pointer may not be dereferenced. A pointer one-before-the-beginning of an array may not, in general, be used for pointer arithmetic; it is non-standard but works on many (perhaps most) machines.

int array[N];
int *one_past1 = array + N; /* OK */
int *one_past2 = &array[N]; /* OK */
int val1 = *(array + N); /* Undefined */
int val2 = array[N]; /* Undefined */
int *one_before = array - 1; /* Non-standard, but works on most platforms. */

Important. Pointer accesses outside of array bounds are not checked by the compiler. For example,

int array[10];
int *pa = array + 10;
int val = *++pa; /* Invalid, but will compile OK. */

Out-of-bounds accesses are arguably the greatest source of subtle runtime bugs, rivaled only by dynamic memory issues (such as memory-leaks or accessing deallocated memory). The problem with faulty pointer arithmetic is that the error might not have an obvious effect. In the best case, it might cause the program to crash immediately. More likely, the program will continue to run, may produce occasional weird results, and will crash unexpectedly at some critical moment (such as while demonstrating the program to a customer). It is imperative that pointers only access allocated memory regions, and that appropriate bounds checking is used to prevent access to invalid memory locations.

 

Return Values and Pointers : 

A function may return a pointer. For example,

int* func_returns_pointer(void);

However, in many circumstances returning a pointer is a mistake and will not do what is expected. The problem arises if the object to which the pointer refers is local to the function. The local object will be destroyed when the function finishes, and the returned address will point to an invalid location. This error also applies to arrays defined within functions. In the following example,

int* misguided(void)
{
	int array[10], i; /* array has local extent: destroyed at end-of-block. */
	for (i = 0; i < 10; ++i)
	array[i] = i;
	return array;
}

the pointer to array ceases to refer to valid memory once array has been destroyed. Returning pointers from functions is fine provided the object pointed-to remains in existence. Thus, it is valid to return a pointer to a static variable (which has static extent).

double* geometric_growth(void)
{
	static double grows = 0.1; /* grows exists for lifetime of program. */
	grows *= 1.1;
	return &grows;
}

Similarly, variables with dynamic extent (i.e., variables allocated using malloc()) can be safely returned by a pointer. Pointers may also be returned if they refer to a function input argument which is itself a pointer. For example, a pointer might refer to an element within a passed array.

char* find_first(char* str, char c)
/* Return pointer to first occurrence of c in str. Return NULL if not found. */
{
	while(*str++ != ’\0’)
	if (*str == c) return str;
	return NULL;
}

 

Pointers to Pointers

Sometimes it is desirable to pass a pointer to a function and to change the value of the pointer itself; that is, to change what it points to. This can be done using a pointer-to-a-pointer as in the following (rather contrived) example.

void func(int **pptr, int i, int j)
{
	if (i<j) *pptr = &i;
	else *pptr = &j;
}

This concept may be applied further, so that we can have a pointer-to-a-pointer-to-a-pointer, and so on. But such constructions are rarely necessary.

Changing the value of a pointer inside a function is just one application of pointers-to-pointers. More frequently they appear in relation to arrays of pointers.

 

Function Pointers : 

Function pointers are a very useful mechanism for selecting, substituting or grouping together functions of a particular form. For example, they may be used to pass functions as arguments to other functions. Or, they may be collected into an array of function pointers as a “dispatch table”, where a certain function is invoked based on an array index.

The declaration of a function pointer must specify the number and type of the function arguments and the function return type. For example, the following declaration is a pointer to a function that has a double and an int argument and returns a double.

double (*pf)(double, int);

Thus, it is not possible to define a completely generic function pointer, only one that points to a certain category of function that fits the declaration.

Notice the use of parentheses around the identifier (*pf). This is necessary to distinguish a pointer-to-a-function from a function-that-returns-a-pointer.

int (*pf)(char); /* pointer-to-function: input char, return int */
int *f(char); /* function: input char, return pointer-to-int */

In a function declaration, it is possible to declare a function pointer without using an identifier; (this is the case also with any other function argument).

void myfunc(char *, int, unsigned (*)(int));

However, it is usually better practice to include identifiers in function prototypes, as it improves readability.

void myfunc(char *message, int nloops, unsigned (*convert)(int));

The following example program uses function pointers to pass functions as arguments to another function. This allows the the latter function to perform a variety of operations without any change to its own algorithm.

1 #include <stdio.h>
2 #include <assert.h>
3
4 double add(double a, double b) { return a + b; }
5 double sub(double a, double b) { return a − b; }
6 double mult(double a, double b) { return a * b; }
7 double div(double a, double b) { assert(b != 0.0); return a / b; }
8
9 void execute operation(double (*f)(double,double), double x, double y)
10 {
11 		double result = f(x,y);
12 		printf("Result of operation on %3.2f and %3.2f is %7.4f\n", x, y, result);
13 }
14
15 int main(void)
16 {
17 		double val1=4.3, val2=5.7;
18 		execute operation(add, val1, val2);
19 		execute operation(sub, val1, val2);
20 		execute operation(mult, val1, val2);
21 		execute operation(div, val1, val2);
22 }

Function pointers may be used to separate out certain sub-algorithm operations from within a general purpose main algorithm. This allows different sub-algorithms to be swapped in or out at run time. A common example is a generic sorting algorithm that uses function pointers to implement its sorting criterion.

void generic_sort(int *array, int len, int (*compare)(int, int));
int greater_than(int a, int b) { return a > b; } /* Possible sorting criterion. */
int less_than(int a, int b) { return a < b; } /* Possible sorting criterion. */
generic_sort(values, 20, less_than); /* Using the sort algorithm. */