C Programming: Low-Level Mastery
HomeInsightsCoursesC ProgrammingIntroduction to Pointers
Pointers

Pointers Basics

Master C's most powerful and feared feature - pointers. Learn what pointers are, how they work, declaration, dereferencing, NULL pointers, and the fundamentals that unlock C's true potential.

What Are Pointers?

A pointer is a variable that stores a memory address. Instead of holding a value directly, it holds the location where a value is stored. Pointers enable dynamic memory allocation, efficient array manipulation, data structure implementation, and direct hardware access. They're C's superpower - powerful but requiring careful handling.

Every variable resides at a specific memory address. Pointers let you work with these addresses directly. This indirection enables passing large structures efficiently, modifying function arguments, creating dynamic data structures, and implementing callbacks. Understanding pointers transforms you from a C beginner to a proficient systems programmer.

C
#include <stdio.h>

int main(void) {
    int x = 42;        // Regular variable
    int *ptr;          // Pointer to int
    
    ptr = &x;          // ptr now holds address of x
    
    printf("Value of x: %d\n", x);           // 42
    printf("Address of x: %p\n", (void*)&x); // e.g., 0x7ffd5c3e2a4c
    printf("Value of ptr: %p\n", (void*)ptr);// Same address
    printf("Value pointed to: %d\n", *ptr);  // 42 (dereference)
    
    *ptr = 100;        // Modify x through pointer
    printf("New value of x: %d\n", x);       // 100
    
    return 0;
}

/* Key concepts:
   - & (address-of): Gets address of variable
   - * (dereference): Gets value at address
   - Pointer stores address, not value
*/

Pointer Declaration and Initialization

Pointer syntax can be confusing at first. The asterisk (*) in declarations means "pointer to," but it's also used for dereferencing. Understanding the syntax and proper initialization prevents many common errors. Always initialize pointers before use - uninitialized pointers are dangerous.

C
/* Pointer declaration syntax */
int *ptr1;        // Pointer to int
double *ptr2;     // Pointer to double
char *ptr3;       // Pointer to char

/* Multiple pointer declarations */
int *p1, *p2, *p3;  // Three pointers to int
int *p4, x, *p5;    // p4 and p5 are pointers, x is int

/* Common mistake */
int* ptr6, ptr7;    // Only ptr6 is pointer! ptr7 is int
// Better style:
int *ptr8, *ptr9;   // Clear that both are pointers

/* Initialization */
int x = 10;
int *ptr = &x;      // Initialize with address of x

/* Initialize to NULL */
int *ptr_null = NULL;  // Safe: points to nothing

/* Dangerous: Uninitialized pointer */
int *bad_ptr;       // Contains garbage address!
// *bad_ptr = 5;    // CRASH: Writing to random memory

/* Pointer types must match */
int x = 10;
int *ptr_int = &x;   // OK: int pointer to int variable

double y = 3.14;
// int *ptr_bad = &y;  // WARNING: Type mismatch!

/* Void pointer (generic pointer) */
void *generic_ptr;     // Can point to any type
generic_ptr = &x;      // OK
generic_ptr = &y;      // OK
// printf("%d\n", *generic_ptr);  // ERROR: Can't dereference void*

/* Const pointers */
const int *ptr_to_const;    // Pointer to const int
int *const const_ptr = &x;  // Const pointer to int
const int *const both = &x; // Const pointer to const int

Dereferencing Pointers

Dereferencing a pointer means accessing the value at the address it contains. The asterisk (*) operator dereferences pointers. You can read from or write to the dereferenced location. Dereferencing NULL or invalid pointers causes crashes - always verify pointers before dereferencing.

C
/* Dereferencing to read */
int x = 42;
int *ptr = &x;

printf("%d\n", *ptr);  // 42 (read value at address)

/* Dereferencing to write */
*ptr = 100;             // Modify x through pointer
printf("%d\n", x);     // 100 (x was changed)

/* Pointer to pointer value relationship */
int a = 10;
int *p = &a;

printf("a = %d\n", a);      // 10
printf("*p = %d\n", *p);    // 10 (same value)

a = 20;                      // Change a directly
printf("*p = %d\n", *p);    // 20 (p still points to a)

*p = 30;                     // Change a through pointer
printf("a = %d\n", a);      // 30

/* Multiple pointers to same variable */
int value = 5;
int *ptr1 = &value;
int *ptr2 = &value;

*ptr1 = 10;                  // Modify through ptr1
printf("*ptr2 = %d\n", *ptr2);  // 10 (ptr2 sees the change)

/* Pointer arithmetic and dereferencing */
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;

printf("%d\n", *p);        // 10 (first element)
printf("%d\n", *(p + 1));  // 20 (second element)
printf("%d\n", *(p + 4));  // 50 (last element)

/* Increment and dereference */
p = arr;
printf("%d\n", *p++);      // 10, then increment p
printf("%d\n", *p);        // 20
printf("%d\n", *++p);      // 30, increment first
printf("%d\n", (*p)++);    // 30, then increment value
printf("%d\n", *p);        // 31

/* Dangerous: Dereferencing invalid pointer */
int *bad = NULL;
// printf("%d\n", *bad);   // CRASH: Dereferencing NULL

int *uninitialized;
// printf("%d\n", *uninitialized);  // CRASH: Random address

NULL Pointers

NULL is a special pointer value representing "points to nothing." It's defined as 0 or ((void*)0). Always initialize pointers to NULL if you don't have a valid address yet. Check for NULL before dereferencing to prevent crashes. NULL is your safety net in pointer programming.

C
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    /* NULL pointer initialization */
    int *ptr = NULL;  // Safe initialization
    
    /* Check before dereferencing */
    if (ptr != NULL) {
        printf("%d\n", *ptr);
    } else {
        printf("Pointer is NULL\n");  // This prints
    }
    
    /* Common pattern: Check allocation success */
    int *array = malloc(100 * sizeof(int));
    if (array == NULL) {
        printf("Allocation failed\n");
        return 1;
    }
    /* Use array... */
    free(array);
    
    /* Reset to NULL after free */
    array = NULL;  // Prevents dangling pointer
    
    /* NULL in conditionals */
    int *p1 = NULL;
    int x = 10;
    int *p2 = &x;
    
    if (!p1) {  // Same as: if (p1 == NULL)
        printf("p1 is NULL\n");
    }
    
    if (p2) {  // Same as: if (p2 != NULL)
        printf("p2 is not NULL\n");
    }
    
    /* Defensive programming */
    void safe_print(int *ptr) {
        if (ptr == NULL) {
            printf("NULL pointer\n");
            return;
        }
        printf("%d\n", *ptr);
    }
    
    safe_print(NULL);  // Safe
    safe_print(&x);    // Safe
    
    return 0;
}

/* NULL vs 0 */
// NULL and 0 are interchangeable for pointers
int *p1 = NULL;  // Standard
int *p2 = 0;     // Also works (less clear)

/* NULL is not the same as uninitialized */
int *null_ptr = NULL;      // Safe: known to point nowhere
int *uninit_ptr;           // Dangerous: random address

/* Common NULL check idioms */
if (ptr) { /* non-NULL */ }
if (!ptr) { /* NULL */ }
if (ptr == NULL) { /* NULL */ }
if (ptr != NULL) { /* non-NULL */ }

Pointers and Functions

Pointers enable functions to modify caller's variables (pass by reference simulation), return multiple values, and handle large data efficiently. Understanding pointer parameters is crucial for C programming. Remember: C is always pass-by-value, but passing pointers gives you pass-by-reference-like behavior.

C
/* Modifying variables through pointers */
void increment(int *ptr) {
    (*ptr)++;  // Increment value at address
}

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main(void) {
    int x = 10;
    increment(&x);  // Pass address of x
    printf("x = %d\n", x);  // 11 (modified!)
    
    int a = 5, b = 10;
    printf("Before: a=%d, b=%d\n", a, b);
    swap(&a, &b);
    printf("After: a=%d, b=%d\n", a, b);  // Swapped!
    
    return 0;
}

/* Returning multiple values via pointers */
void divide_and_remainder(int dividend, int divisor, 
                         int *quotient, int *remainder) {
    *quotient = dividend / divisor;
    *remainder = dividend % divisor;
}

void usage(void) {
    int q, r;
    divide_and_remainder(17, 5, &q, &r);
    printf("17 / 5 = %d remainder %d\n", q, r);  // 3 remainder 2
}

/* Returning pointer from function */
int* get_larger(int *a, int *b) {
    return (*a &gt; *b) ? a : b;
}

void pointer_return(void) {
    int x = 10, y = 20;
    int *larger = get_larger(&x, &y);
    printf("Larger: %d\n", *larger);  // 20
    *larger = 100;  // Modifies y
    printf("y = %d\n", y);  // 100
}

/* Array parameters are pointers */
void modify_array(int *arr, int size) {
    for (int i = 0; i < size; i++) {
        arr[i] *= 2;
    }
}

/* Const pointer parameters */
void print_value(const int *ptr) {
    printf("%d\n", *ptr);
    // *ptr = 10;  // ERROR: Can't modify through const pointer
}

void modify_pointer(int **ptr_to_ptr) {
    // Can modify the pointer itself
    *ptr_to_ptr = NULL;
}

Common Pointer Errors

Pointer errors are among the most common and dangerous bugs in C. They cause crashes, security vulnerabilities, and mysterious behavior. Understanding these pitfalls helps you write safer code.

C
/* Error 1: Dereferencing NULL */
int *ptr = NULL;
// *ptr = 5;  // CRASH: Segmentation fault

/* Error 2: Uninitialized pointer */
int *ptr;  // Contains garbage address
// *ptr = 5;  // CRASH: Writing to random memory

/* Error 3: Dangling pointer (pointing to freed memory) */
int *ptr = malloc(sizeof(int));
free(ptr);
// *ptr = 5;  // UNDEFINED BEHAVIOR: Use after free
ptr = NULL;  // Fix: Set to NULL after free

/* Error 4: Dangling pointer (pointing to local variable) */
int* bad_function(void) {
    int local = 42;
    return &local;  // WRONG: local destroyed after return
}

/* Error 5: Lost pointer (memory leak) */
int *ptr = malloc(100 * sizeof(int));
ptr = malloc(200 * sizeof(int));  // Lost original allocation!

/* Fix: Free before reassigning */
int *ptr2 = malloc(100 * sizeof(int));
free(ptr2);
ptr2 = malloc(200 * sizeof(int));

/* Error 6: Buffer overflow through pointer */
int arr[5];
int *ptr = arr;
ptr[10] = 42;  // WRONG: Out of bounds

/* Error 7: Type mismatch */
int x = 10;
double *ptr = (double*)&x;  // Dangerous cast
// *ptr = 3.14;  // WRONG: Writing double to int location

/* Error 8: Modifying string literal */
char *str = "Hello";
// str[0] = 'h';  // CRASH: String literals are read-only

/* Fix: Use array */
char str2[] = "Hello";
str2[0] = 'h';  // OK

/* Error 9: Pointer arithmetic overflow */
int arr[5];
int *ptr = arr + 10;  // Points outside array
// *ptr = 42;  // UNDEFINED BEHAVIOR

/* Error 10: Forgetting & when passing address */
void modify(int *ptr) {
    *ptr = 100;
}

int x = 10;
// modify(x);  // WRONG: Passing value, not address
modify(&x);   // CORRECT

Best Practices

Following best practices for pointers makes your code safer, more maintainable, and less prone to bugs. These guidelines come from decades of C programming experience.

C
/* Practice 1: Always initialize pointers */
int *ptr = NULL;  // Good
// int *ptr;  // Bad: uninitialized

/* Practice 2: Check for NULL before dereferencing */
if (ptr != NULL) {
    *ptr = 10;
}

/* Practice 3: Set to NULL after free */
free(ptr);
ptr = NULL;

/* Practice 4: Use const for read-only pointers */
void print(const int *ptr) {
    printf("%d\n", *ptr);
}

/* Practice 5: Check allocation success */
int *arr = malloc(100 * sizeof(int));
if (arr == NULL) {
    // Handle error
    return -1;
}

/* Practice 6: Avoid returning pointers to local variables */
// Bad:
int* bad(void) {
    int x = 10;
    return &x;
}

// Good: Return pointer to static or allocated memory
int* good(void) {
    static int x = 10;
    return &x;
}

/* Practice 7: Document pointer ownership */
/**
 * Returns allocated memory. Caller must free.
 */
int* create_array(int size) {
    return malloc(size * sizeof(int));
}

/* Practice 8: Use size_t for sizes and pointer arithmetic */
size_t size = 100;
int *arr = malloc(size * sizeof(int));

/* Practice 9: Avoid excessive pointer indirection */
// Bad: Hard to read
int ***ptr;

// Better: Use structs or different design

/* Practice 10: Be consistent with pointer style */
// Choose one:
int* ptr;   // Emphasizes type
int *ptr;   // Emphasizes variable (more common)

Summary & What's Next

Key Takeaways:

  • ✅ Pointers store memory addresses
  • ✅ & (address-of) gets variable's address
  • ✅ * (dereference) accesses value at address
  • ✅ Always initialize pointers (use NULL if no address yet)
  • ✅ Check for NULL before dereferencing
  • ✅ Pointers enable pass-by-reference-like behavior
  • ✅ Set pointers to NULL after freeing memory
  • ✅ Uninitialized and dangling pointers cause crashes

What's Next?

Let's learn about pointer arithmetic and advanced pointer operations!