Pointers are one of the most important and challenging concepts in the C programming language.

Overview

What is a pointer? In essence, it’s a value representing a memory address, acting as a signpost to a specific location in memory.

The asterisk * represents a pointer and is usually placed after the type keyword to indicate what type of value the pointer is referencing. For example, char* is a pointer to a character, and float* is a pointer to a float.

1
int* intPtr;

The above example declares a variable intPtr, which is a pointer that points to a memory address storing an integer.

The asterisk * can be placed in various positions between the variable name and the type, and all are valid, like:

1
2
3
int   *intPtr;
int * intPtr;
int* intPtr;

This book follows the convention of placing the asterisk immediately after the type keyword (i.e., int* intPtr;) to emphasize that the pointer variable is just a regular variable, except its value is a memory address.

However, if you declare two pointer variables on the same line, be mindful of this:

1
2
3
4
5
// Correct
int *foo, *bar;

// Incorrect
int* foo, bar;

In the incorrect example, foo is a pointer to an integer, but bar is just an integer. The * only applies to the first variable.

You can also have pointers to pointers, indicated by two asterisks **.

1
int** foo;

This example declares foo as a pointer to another pointer, where the second pointer points to an integer.

Dereferencing Operator *

The * symbol, besides indicating a pointer, also acts as an operator to retrieve the value at the memory address the pointer references.

1
2
3
void increment(int* p) {
*p = *p + 1;
}

In this example, the function increment() takes a pointer p as its parameter. Inside the function, *p refers to the value that p points to. By assigning a new value to *p, you’re modifying the value at the memory address p is pointing to.

The function adds 1 to the input value, and there’s no need for a return statement. This is because the function operates directly on the memory address passed in, so any changes affect the original value outside the function. This is a common practice in C for passing values through pointers.

Passing the address of a variable instead of the variable itself also has the advantage of saving memory and time, especially when dealing with large variables. Copying large variables would be inefficient, whereas passing a pointer is more effective.

Address-of Operator &

The & operator retrieves the memory address of a variable.

1
2
int x = 1;
printf("x's address is %p\n", &x);

In this example, x is an integer variable, and &x is the memory address of x. The %p format specifier in printf() outputs the memory address.

The function to increment a value could be used like this:

1
2
3
4
5
6
7
void increment(int* p) {
*p = *p + 1;
}

int x = 1;
increment(&x);
printf("%d\n", x); // Output: 2

Here, after calling increment(), the value of x increases by 1 because the function received the memory address of x (&x).

The & and * operators are inverses of each other, so the following expression is always valid:

1
2
3
int i = 5;

if (i == *(&i)) // True

Pointer Initialization

When you declare a pointer variable, the compiler allocates memory for the pointer itself, but its value is initially random, meaning the pointer points to a random memory address. You should never read or write to this random address, as it could lead to severe consequences.

1
2
int* p;
*p = 1; // Error

The above code is incorrect because p points to a random address. Writing 1 to that address can cause unpredictable results.

The correct approach is to assign a valid memory address to the pointer before reading or writing to it. This process is known as pointer initialization.

1
2
3
4
5
int* p;
int i;

p = &i;
*p = 13;

Here, p is a pointer variable. After declaration, p points to a random memory address. To fix this, we declare an integer i and assign p to point to i‘s memory address (p = &i;). After initialization, we can safely assign a value to the memory address p points to (*p = 13;).

To prevent the use of uninitialized pointers, it’s a good practice to set them to NULL:

1
int* p = NULL;

NULL is a constant in C that represents the memory address 0, which is inaccessible. Attempting to read or write to this address will raise an error.

Pointer Arithmetic

Pointers are essentially unsigned integers representing memory addresses, but pointer arithmetic follows its own set of rules.

  1. Addition/Subtraction with Integers

When you add or subtract an integer to a pointer, it moves the pointer by a certain number of memory units.

1
2
3
short* j;
j = (short*)0x1234;
j = j + 1; // Now points to 0x1236

Here, j is a pointer to a memory address 0x1234. Since j is of type short*, adding 1 moves the pointer by two bytes (the size of a short), resulting in 0x1236. Similarly, subtracting 1 would give 0x1232.

The amount by which the pointer moves depends on the size of the data type it points to.

  1. Pointer Addition

Pointers cannot be added together. This is illegal in C.

1
2
3
unsigned short* j;
unsigned short* k;
x = j + k; // Illegal
  1. Pointer Subtraction

You can subtract pointers of the same type. This operation returns the number of data units between them, not the difference in memory addresses.

1
2
3
4
5
6
7
8
short* j1;
short* j2;

j1 = (short*)0x1234;
j2 = (short*)0x1236;

ptrdiff_t dist = j2 - j1;
printf("%td\n", dist); // Output: 1

In this example, j1 and j2 are short pointers, and the difference between their memory addresses is 1, because the two pointers are one short value apart (2 bytes).

  1. Pointer Comparison

You can compare pointers to see which memory address is greater. This comparison returns 1 (true) or 0 (false).