Skip to content

Instantly share code, notes, and snippets.

@jakelevi1996
Last active September 24, 2023 06:39
Show Gist options
  • Save jakelevi1996/5fddfe58c25d5edb5a1da5d414824c41 to your computer and use it in GitHub Desktop.
Save jakelevi1996/5fddfe58c25d5edb5a1da5d414824c41 to your computer and use it in GitHub Desktop.
Pointers in C (including function pointers)

Pointers in C (including function pointers)

In the C programming language, pointers are used to refer to the memory locations in which variables are stored. Some code snippets are shown below which demonstrate why they are useful and how to properly use them; the full pointers.c program and its output are included at the end.

Accessing a memory location using &

In C, the & operator is used for accessing the memory address of a variable; for example:

int x = 10;
printf("The value stored in x is %i\n", x);
printf("The address in which x is stored is %p\n", &x);
// The value stored in x is 10
// The address in which x is stored is 000000000023FE44

NB The use of the %p specifier within the printf function is explained here.

Creating a pointer variable using *

The * operator is used for creating a pointer variable which can store a memory address:

int* x_address = &x; // &x is the memory address of x
printf("The value stored in x_address is %p\n", x_address);
// The value stored in x_address is 000000000023FE44

Accessing the value of a memory location using *

The * operator is also used for returning the value which is stored in a memory location given by a pointer (this is sometimes called 'dereferencing', and can be thought of as the inverse of the & operator for returning a pointer to the address of a variable):

int x_value = *x_address; // *x_address is the value stored at x_address
printf("The value stored in x_value is %i\n", x_value);
// The value stored in x_value is 10

Note that only pointer variables can be dereferenced; the statements int y = &x; int z = *y; will cause a compiler error because y is not a pointer variable, yet it is trying to be dereferenced by writing int z = *y (a compiler warning will also occur from trying to store the pointer &x in the int y). This line of code can be fixed by making sure that y is a pointer variable by writing int* y = &x; int z = *y;.

In summary:

Syntax Description
&variable_name Address of variable_name
variable_type* pointer_name Pointer called pointer_name to a variable of type variable_type
*pointer_name Value of variable stored at pointer_name (this is a dereferenced pointer)

Changing the value in a memory location

If the value at memory location x_address is changed, the change will be reflected in x and *x_address, but not in x_value. This is because the line int x_value = *x_address; above created a new variable x_value at a new location in memory, and assigned to it the value which was then stored in x_address; but changing the value stored in x_address later on does not change the value stored in &x_value.

*x_address = 11;
printf("The new values are %i %i %i\n", x, *x_address, x_value);
// The new values are 11 11 10

Changing a variable within a function

This is how not to change a variable within a function, since a copy of the variable's value is passed to the function, and changes to that copy are not reflected in the original variable:

void inc(int x) {
    x++;
}

int main() {
    int x = 10;
    printf("The value of x is %i\n", x);
    // The value of x is 10
    inc(x);
    printf("The value of x is %i\n", x);
    // The value of x is 10
}

In order to change a variable within a function, the function must accept as arguments pointers to the variable, which are then dereferenced inside the function:

void inc_pointer(int *x) { // Argument is a pointer variable
    (*x)++; // The pointer variable is dereferenced and its value is modified
}

int main() {
    int x = 10;
    printf("The value of x is %i\n", x);
    // The value of x is 10
    inc_pointer(&x); // Argument is the memory address of x
    printf("The value of x is %i\n", x);
    // The value of x is 11
}

Note that C does not support C++-style pass-by-reference.

Array variables are similar to pointers

Array variables store the memory location of the first element in the array; array variables can be dereferenced, and the value given will be the first element in the array. Integers can be added to array variables, and dereferencing those expressions will give successive elements in the array (until the end of the array is reached, at which point behaviour is undefined).

int array[] = {13, 29, 51};
printf("Array contains %i, %i, %i\n", array[0], array[1], array[2]);
printf("Array contains %i, %i, %i\n", *array, *(array + 1), *(array + 2));
// Array contains 13, 29, 51
// Array contains 13, 29, 51

Note in the snippet below, pointer arithmetic is performed automatically: array + 1 goes to the next memory location after array, but because each integer requires 4 bytes, array + 1 will skip forwards 4 bytes in memory:

printf("Memory locations %i, %i, %i\n", array, (array + 1), (array + 2));
// Memory locations 2358816, 2358820, 2358824

Pointers can be assigned to array variables, and the pointer arithmetic works the same way:

int* p = array;
printf("Array contains %i, %i, %i\n", p[0], p[1], p[2]);
printf("Array contains %i, %i, %i\n", *p, *(p + 1), *(p + 2));
// Array contains 13, 29, 51
// Array contains 13, 29, 51

There are differences between pointers and array variables, however. Using the sizeof operator will return different results for the two variables, because the array variable stores information about the full size of the array (bytes per element * number of elements = 3 * 4 = 12), whereas the pointer variable only stores the address of the array (which requires 8 bytes on a 64-bit OS), and no information about its length:

printf("Sizes are %i and %i", sizeof(array), sizeof(p));
// Sizes are 12 and 8

The loss of information about the size of the array when a pointer is assigned to that array is known as pointer decay. NB Every time an array is passed to a function, it decays to a pointer.

Another difference between pointers and array variables is that pointers can be reassigned to different locations in memory, whereas array variables cannot (although the values stored in an array can be changed): the statement int array[] = {14, 30, 52}; after array was already defined would give a compiler error.

struct pointers

Pointers can be used to point to struct variables, for example:

typedef struct {
    int waist_inches;
    int leg_inches;
    float price;
    int quantity;
    char* name;
} trousers;

int main() {
    trousers jeans = {34, 32, 15.00, 12, "Jeans"};
    trousers* pjeans = &jeans;
    return 0;
}

pjeans is now a pointer to the jeans variable. One way to access the fields of the variable that pjeans points to would be to use brackets and dereferencing, as follows:

printf("Waist size = %i inches\n", (*pjeans).waist_inches);
// Waist size = 34 inches

A shortcut notation (saving a whopping 2 characters) is to use the -> operator:

printf("Waist size = %i inches\n", pjeans->waist_inches);
// Waist size = 34 inches

This is often done when passing struct variables into functions; it is generally preferable to pass a pointer to a struct variable into a function rather than the struct variable itself, because otherwise a copy of the struct variable must be made each time the function is called, which may be inefficient. As before, the -> operator is used to access the fields of the struct which is being pointed to:

void print_total_value(trousers* pt) {
    float tvalue = pt->price * pt->quantity;
    printf("Total value of %s is $%.2f\n", pt->name, tvalue);
}

int main() {
    trousers jeans = {34, 32, 15.00, 12, "Jeans"};
    trousers* pjeans = &jeans;

    print_total_value(pjeans);
    // Total value of Jeans is $180.00
    
    return 0;
}

Function pointers

There is a special syntax for defining pointers to functions. Take, for example, the following function:

int add(int x, int y) {
    return x + y;
}

The syntax for defining a pointer variable called padd for this type of function is as follows:

int (*padd) (int, int);

This pointer can be assigned to the function and used as follows:

padd = add;
printf("Result of using function pointer is %i", padd(3, 4));
// Result of using function pointer is 7

In general, the syntax for a function pointer is:

return type (*pointer name) (list of parameter types)

One example of when function pointers are useful is for functions which take another function as a parameter, for example qsort in stdlib.h, which has the following function declaration (taken from the online reference), and takes a function pointer as its final parameter:

void qsort (void* base, size_t num, size_t size,
            int (*compar)(const void*, const void*));

The parameter int (*compar)(const void*, const void*) refers to a pointer called compar to a function which takes two parameters which are both pointers to const void variables, and returns an int. Using further information from the online reference, the following snippet demonstrates how to define a function compare which can be passed as an argument to qsort, which can then be used to sort arrays of ints (note that the pointers px and py must be cast to int pointers before they can be dereferenced, otherwise the compiler would have no way of knowing how to dereference them):

int compare(const void* px, const void* py) {
    int x = *(int*) px;
    int y = *(int*) py;
    return x - y;
}

The following snippet inserted in the main function demonstrates this function being tested:

int list[] = {5, 1, 4, 8, 2};
printf("\nThe original list is:\n");
for (int i = 0; i < 5; i++) printf("%i ", list[i]);
qsort(list, 5, sizeof(int), compare);
printf("\nThe sorted list is:\n");
for (int i = 0; i < 5; i++) printf("%i ", list[i]);
// The original list is:
// 5 1 4 8 2
// The sorted list is:
// 1 2 4 5 8

A similar notation can be used to define an array of function pointers; say for example the following functions had been defined:

int subtract(int x, int y) {return x - y;}
int multiply(int x, int y) {return x * y;}
int divide(int x, int y) {return x / y;}

An array of pointers to these functions can be defined as follows:

int (*func_list[]) (int, int) = {add, subtract, divide, multiply};

Arrays of function pointers can be especially useful when each function corresponds to a different possible state of a struct type variable, each function takes a struct type variable as an argument, and the struct contains an enum type variable which describes each possible state of the struct type variable, and in the same order as the corresponding function array. Then, when iterating through an array of the struct type variables, the appropraite function for each struct type variable can be called depending on its state in a single line as follows (without having to write switch/case logic for each possible state of the struct type variable):

for (i = 0; i < NUM_S; i++) {
    func_list[s[i].state](s[i]);
}

For a more concrete example, see Head First C; Dawn Griffiths, David Griffiths; O'Reilly Media, Inc.; April 2012, chapter 7.

pointers.c

/* Compile and run by entering in terminal:
gcc pointers.c -o pointers && pointers
*/
#include <stdio.h>
#include <stdlib.h>

void inc(int x) {
    x++;
}

void inc_pointer(int *x) { // Argument is a pointer variable
    (*x)++; // The pointer variable is dereferenced and its value is modified
}

typedef struct {
    int waist_inches;
    int leg_inches;
    float price;
    int quantity;
    char* name;
} trousers;

void print_total_value(trousers* pt) {
    float tvalue = pt->price * pt->quantity;
    printf("Total value of %s is $%.2f\n", pt->name, tvalue);
}

int add(int x, int y) {
    return x + y;
}

int compare(const void* px, const void* py) {
    int x = *(int*) px;
    int y = *(int*) py;
    return x - y;
}

int subtract(int x, int y) {return x - y;}
int multiply(int x, int y) {return x * y;}
int divide(int x, int y) {return x / y;}

int main()
{
    // Create a variable, print its address
    int x = 10;
    printf("The value stored in x is %i\n", x);
    printf("The address in which x is stored is %p\n", &x);
    // The value stored in x is 10
    // The address in which x is stored is 000000000023FE44
    
    // Store the address in a pointer variable
    int *x_address = &x; // &x is the memory address of x
    printf("The value stored in x_address is %p\n", x_address);
    // The value stored in x_address is 000000000023FE44
    
    // Store the value in a new variable
    int x_value = *x_address; // *x_address is the value stored at x_address
    printf("The value stored in x_value is %i\n", x_value);
    // The value stored in x_value is 10
    int *y = &x; int z = *y; printf("z = %i\n", z);

    // Change the value stored in x_address
    *x_address = 11;
    printf("The new values are %i %i %i\n", x, *x_address, x_value);
    // The new values are 11 11 10

    // How not to change x within a function
    x = 10;
    printf("The value of x is %i\n", x);
    // The value of x is 10
    inc(x);
    printf("The value of x is %i\n", x);
    // The value of x is 10

    // How to do it properly
    inc_pointer(&x); // Argument is the memory address of x
    printf("The value of x is %i\n", x);
    // The value of x is 11

    // Arrays act similar to pointers
    int array[] = {13, 29, 51};
    printf("Array contains %i, %i, %i\n", array[0], array[1], array[2]);
    printf("Array contains %i, %i, %i\n", *array, *(array + 1), *(array + 2));
    // Array contains 13, 29, 51
    // Array contains 13, 29, 51

    printf("Memory locations %i, %i, %i\n", array, (array + 1), (array + 2));
    // Memory locations 2358816, 2358820, 2358824

    int* p = array;
    printf("Array contains %i, %i, %i\n", p[0], p[1], p[2]);
    printf("Array contains %i, %i, %i\n", *p, *(p + 1), *(p + 2));
    // Array contains 13, 29, 51
    // Array contains 13, 29, 51

    printf("Sizes are %i and %i\n", sizeof(array), sizeof(p));
    // Sizes are 12 and 8


    trousers jeans = {34, 32, 15.00, 12, "Jeans"};
    trousers* pjeans = &jeans;

    printf("Waist size = %i inches\n", (*pjeans).waist_inches);
    // Waist size = 34 inches
    printf("Waist size = %i inches\n", pjeans->waist_inches);
    // Waist size = 34 inches

    print_total_value(pjeans);
    // Total value of Jeans is $180.00

    // Function pointers
    int (*padd) (int, int);
    padd = add;
    printf("Result of using function pointer is %i", padd(3, 4));
    // Result of using function pointer is 7

    int list[] = {5, 1, 4, 8, 2};
    printf("\nThe original list is:\n");
    for (int i = 0; i < 5; i++) printf("%i ", list[i]);
    qsort(list, 5, sizeof(int), compare);
    printf("\nThe sorted list is:\n");
    for (int i = 0; i < 5; i++) printf("%i ", list[i]);
    // The original list is:
    // 5 1 4 8 2
    // The sorted list is:
    // 1 2 4 5 8

    // Array of function pointers
    int (*func_list[]) (int, int) = {add, subtract, divide, multiply};

    return 0;
}

Full output

>gcc pointers.c -o pointers && pointers
The value stored in x is 10
The address in which x is stored is 000000000023FE0C
The value stored in x_address is 000000000023FE0C
The value stored in x_value is 10
z = 10
The new values are 11 11 10
The value of x is 10
The value of x is 10
The value of x is 11
Array contains 13, 29, 51
Array contains 13, 29, 51
Memory locations 2358784, 2358788, 2358792
Array contains 13, 29, 51
Array contains 13, 29, 51
Sizes are 12 and 8
Waist size = 34 inches
Waist size = 34 inches
Total value of Jeans is $180.00
Result of using function pointer is 7
The original list is:
5 1 4 8 2
The sorted list is:
1 2 4 5 8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment