About Pointers in C

The C programming language (and similar languages) use a coding concept called “pointers”. Pointers point to a memory address. The pointer itself does not contain the data. Rather, the pointer stores the memory address (like the index of a book). Many people may have problems understanding pointers and addressing, so I hope this helps.

A pointer is a programming object that references a memory location. A pointer contains the memory address of a particular part of memory. A pointer is like a page number in the index of a book. The page number is the data stored by pointer. The words on the actual page is like the data in memory.

NOTE: “Dereferencing” is the act of obtaining the data on memory pointed to by the pointer.

A pointer must be of the same datatype as the data to which it points. Also, a pointer must be initialized before it can be used.

To make a pointer that points to an integer in memory, use the below code.

int int_in_mem = 32; // Initialize and declare integer
int* int_ptr; // Declare
int_ptr = &int_in_mem; // Initialize
int* ptr2 = ptr; // Create a second pointer that points to the same data

In the example, “int_in_mem” is an integer in memory. “int_ptr” only stores the memory address of the location of “int_in_mem“. “int_in_mem” has the value “32” while “int_ptr” contains the value the indicates where in memory “int_in_mem” resides. “ptr2” points at the same memory location as “ptr“. This is helpful when a particular memory location must be remembered when the other pointer will be changed to point to another location.

The ampersand (&) means memory location. Therefore, “&int_in_mem” gives the memory address of the data “int_in_mem“. The code &int_ptr would give the address in memory where “int_ptr” is stored. Thus, it is possible to have a pointer that points to a pointer. Also, the ampersand is helpful if code needs to know the literal memory address of a particular variable.

In the example, “int_ptr” is the plain pointer. *int_ptr is the data at that memory location (in this case, “32”). *int_ptr++ reads the value of the memory location and then increments the pointer. This means that the pointer will point to the memory location that comes after the location that is storing the “32”. (*int_ptr)++ will increment the data stored at that location. Thus, “32” will become “33”, but the pointer itself remains unchanged. Below are various pointer notations and their meaning and effects.

PointerMemory AddressData
ptrAccess memory addressUnchanged
*ptrUnchangedAccess data on memory
*ptr++Increment address after readingUnchanged
*(ptr++)Increment address after readingUnchanged
(*ptr)++UnchangedIncrement data after reading
*++ptrIncrement address before readingUnchanged
*(++ptr)Increment address before readingUnchanged
++*ptrUnchangedIncrement data before reading
++(*ptr)UnchangedIncrement data before reading
--*ptrUnchangedDecrement data before reading
ptr*++InvalidInvalid
ptr++*InvalidInvalid

Be careful when declaring multiple pointers. For instance, int* ptr_a, ptr_b; is equivalent to int* ptr_a; int ptr_b;.

Arrays notation is a special form of a pointer as seen in the table below.

ArrayPointer Equivalent
arrayptr*
array[1]*(ptr + 1)
array[2]*(ptr + 2)
array[1]*(array + 1)

An array is a collection of elements of data. Arrays are stored on memory and pointers are used to point and retrieve members/elements from the array. For instance, array[0] is a pointer to the first element on the array (computers start counting at zero). In many languages (such as C), strings are arrays of characters.

If using two pointers, the length of an array can be measured. For instance, the below code creates a string (character array) and measures the length of the string.

char ALPHABET[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ\0";
char* first_ptr = &ALPHABET[0];
char* second_ptr = &ALPHABET[5];
int distance = second_ptr - first_ptr;

distance will contain the value “5” since the memory address pointed to by “second_ptr” minus the address “first_ptr” is five. Thus, there are five elements from the first element of the array/string ([0]) to the sixth element ([5]). Remember, computers start counting at zero.

Now for another example.

int int_in_mem = 32; // Initialize and declare integer
int* int_ptr; // Declare
int_ptr = &int_in_mem; // Initialize
int* ptr2 = ptr; // Create a second pointer that points to the same data
int our_num = *ptr2; // Access data at the memory location indicated by ptr2
int num2 = (*int_ptr)++; // Access data at the memory location indicated by ptr2

In the above example, int_in_mem stores “32” while both int_ptr and ptr2 store the location of int_in_memour_num contains “32” because the code retrieves the data stored at the pointed memory location. num2 will contain “33” because the code gets the data on memory and then increments the value before storing it in num2.

To change the value of data in memory use the below code which changes “32” to “7”. If the code were int_ptr = 7; instead of *int_ptr = 7;, then int_ptr would point to memory address 7.

int int_in_mem = 32; // Initialize and declare integer
int* int_ptr; // Declare
int_ptr = &int_in_mem; // Initialize
*int_ptr = 7; // Place "7" at the pointed memory address

As for pointers to pointers, the below code, d points to the memory address that stores the “3” placed in a.

int a = 3;
int *b = &a;
int **c = &b;
int ***d = &c;

A NULL pointer (such as int* ptr = 0;) is a pointer that pointers to zero. This means that it does not point to any data.

A function pointer is a pointer to a function. Function pointers allow code to take functions as arguments which may be used to tell a function to use a particular function.

void test_func(int x) { printf("%d\n", x); } // Function
void (*func_ptr)(int); // Declare function pointer
func_ptr = &test_func; // Initialize function pointer
func_ptr(2); // Same as test_func(2)

Type Qualifiers in C

There are many different type-qualifiers in the C programming language. Here is a brief description of some type qualifiers in C (and similar languages).

Do remember the “Clockwise/Spiral Rule” – Read the type qualifiers backwards

  • int* – pointer to int
  • int const* – pointer to const int (const int* == int const*)
  • int *const – const pointer to int
  • int const *const – const pointer to const int (const int* const == int const *const)
  • int ** – pointer to pointer to int
  • int **const – const pointer to pointer to int
  • int *const * – pointer to const pointer to int
  • int const ** – pointer to pointer to const int
  • int *const *const – const pointer to const pointer to int
  • volatile int *const – constant pointer to volatile int
  • void (*signal(int, void (*fp)(int)))(int) – signal is a function passing an int and a pointer to a function passing an int returning nothing (void) returning a pointer to a function passing an int returning nothing (void)

Storage classes (listed below) come before type-qualifiers (also listed below). Only one storage class can be used when declaring a variable. Both storage and type qualifiers come before the datatype.

Storage Classes

  • auto – Stored in stack during the code-block
  • extern – Lasts the whole program, block, or compilation unit; globally visible
  • register – Stored in stack or CPU-register during the code block
  • static – Lasts the whole program, block, or compilation unit; private in program
  • typedef – The data specifies a new datatype
  • __thread – Thread-local-storage; one instance per thread
  • _Thread_local – Thread-local data

Type-Qualifiers

  • const – Value does not change; read-only
  • restrict – For the lifetime of the pointer, the object can only be accessed via the pointer
  • volatile – Optimizing-compilers must not change
  • _Atomic – Map a variable to a basic built-in type (depending on the processor) so that reading and writing are guaranteed to happen in a single instruction