Road to C Programmer #7 - Pointers Cont'd

Last Edited: 8/5/2024

The blog post introduces more concepts related to pointers.

C Pointers

NULL Pointers

When declaring a pointer without initializing it, we might think it points to nothing. However, this is not the case.

int main () {
    int *p;
    printf("%p\n", p); // => 0x16 (or some other garbage address)
    return 0;
}

To ensure that a pointer is not pointing to any address yet, we can use NULL pointers.

int main () {
    int *p = NULL;
    printf("%p\n", p); // => 0x0
    return 0;
}

When is it useful? When we request too much memory with malloc, calloc, or realloc, they return NULL and we can handle that by checking if the returned pointer is NULL. We can also use NULL pointers to prevent dangling pointers, where pointers still point to the same address after freeing up the memory.

int main () {
    int *p = malloc(sizeof(int) * 10);
 
    if (p == NULL) {
        printf("Pointer is NULL");
    } // Check if p is NULL after malloc
 
    free(p);
    p = NULL; // Prevent dangling pointers
 
    return 0;
}
 

The dangling pointers should be avoided because accessing them after the memory has been freed up (which happens in a large codebase) can cause unpredictable behaviors and segmentation faults.

Void Pointers

We have always been setting types for pointers to avoid mistakes. This is generally the best practice, but there are cases where you want to be more ambiguous about the data types of the variables the pointers are pointing to. In such cases, void pointers come into play.

int main () {
    int a = 5;
    float b = 3;
 
    void *p; // Void pointer
 
    p = &a;
    p = &b; // No warning
 
    return 0;
}

One way void pointers are helpful is when we want to create a function that takes a pointer of any type and/or returns a pointer of any type. malloc, calloc, and realloc are examples of such functions. Remember that you cannot dereference a void pointer directly, and you cannot perform pointer arithmetic with a void pointer without casting it to another type.

Function Pointers

Just as we can set up a pointer to a variable, we can set up a pointer to a function, as shown below.

void function (int x) {
    printf("%d\n", x);
}
 
int main () {
    // Function pointer
    void (*pFunction) (int);
 
    pFunction = function;
 
    int a = 5;
    
    pFunction(a); // "5"
 
    return 0;
}

Why is this useful? Function pointers are useful when you want to pass a function as an argument to another function, which we refer to as callback functions.

// Callback Function
// Celcius
int freezing_C (float a) {
    if (a <= 0) {
        return 1;
    } else {
        return 0;
    }
}
 
 
// Fahrenheit
int freezing_F (float a) {
    if (a <= 32) {
        return 1;
    } else {
        return 0;
    }
}
 
void is_freezing ( int (*freezing)(float), float a ) {
    if (freezing(a) == 1) {
        printf("Freezing!");
    } else {
        printf("Not freezing.");
    }
}
 
int main () {
    char scale;
    float temperature;
    printf("Select temperature scale with F or C: ");
    scanf("%c", &scale);
    printf("Temperature: ");
    scanf("%f", &temperature);
 
    // Passing function as an argument
    if (scale == 'F') {
        is_freezing(freezing_F, temperature);
    } else {
        is_freezing(freezing_C, temperature);
    }
 
    return 0;
}

Similar to void pointers, we cannot perform pointer arithmetic with function pointers, as it does not make sense in this context.

Exercises

From this article, there will be an exercise section where you can test your understanding of the material introduced in the article. I highly recommend solving these questions by yourself after reading the main part of the article. You can click on each question to see its answer.

Resources