C - Pointers

Introduction to Pointers in C

In C programming, a pointer is a variable that stores the memory address of another variable. Pointers enable efficient parameter passing, dynamic memory, array traversal, and systems-level programming.

Learning Objectives

  • Declare, initialize, and dereference pointers safely.
  • Explain the relationship between variables, addresses, and pointer values.
  • Use pointer arithmetic correctly with arrays.
  • Modify values via pointers in functions (pass-by-address).
  • Allocate and free dynamic memory with malloc/free without leaks.

Prerequisites

Quick Start: Compile and Run

Try this minimal example. Save as pointers_quickstart.c and compile:

// pointers_quickstart.c
#include <stdio.h>

int main(void) {
    int num = 10;
    int *ptr = # // ptr stores the address of num

    printf("num=%d, &num=%p, ptr=%p, *ptr=%d\n",
           num, (void*)&num, (void*)ptr, *ptr);
    return 0;
}
gcc -std=c11 -Wall -Wextra -O0 -g pointers_quickstart.c -o pointers_quickstart
./pointers_quickstart

Declaring and Using Pointers

Use the * operator in the declaration to make a pointer; use & to take an address, and * again to dereference (read/write the pointee):

// Declare an integer and a pointer to int
int num = 10;
int *ptr = &num; // Assign the address of num to ptr

printf("Value of num: %d\n", num);               // 10
printf("Address of num: %p\n", (void*)&num);     // address (varies)
printf("Value stored in ptr: %p\n", (void*)ptr); // same address
printf("Value pointed by ptr: %d\n", *ptr);      // 10

Visualizing Pointers

num 10 &num: 0x7ffe ptr 0x7ffe *ptr = 10
Pointer ptr stores the address of num and points to its value.

Pointer Arithmetic

Incrementing a pointer moves it by the size of its pointee type:

int arr[3] = {1, 2, 3};
int *p = arr;   // same as &arr[0]

printf("%d\n", *p); // 1
p++;                 // advance by sizeof(int)
printf("%d\n", *p); // 2

Pointers and Functions

Pass an address so the function can modify the caller's variable:

void increment(int *n) {
    (*n)++;
}

int main(void) {
    int value = 5;
    increment(&value);
    printf("%d\n", value); // 6
    return 0;
}

Real-World Examples

1) Swap Two Numbers Using Pointers

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

int main(void) {
    int x = 10, y = 20;
    swap(&x, &y);
    printf("x = %d, y = %d\n", x, y); // x = 20, y = 10
    return 0;
}

2) Dynamic Memory Allocation

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

int main(void) {
    int n = 5;
    int *arr = malloc((size_t)n * sizeof *arr);
    if (!arr) { perror("malloc"); return 1; }

    for (int i = 0; i < n; i++) {
        arr[i] = i * 2;
    }
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    free(arr);
    return 0;
}

Always check the result of malloc and free what you allocate exactly once.

Common Mistakes and Pitfalls

  • Dereferencing uninitialized or NULL pointers (undefined behavior). Initialize pointers and check for NULL when applicable.
  • Returning the address of a local variable (becomes invalid after the function returns).
  • Mismatched allocation/deallocation (alloc with malloc, free with free). No delete in C.
  • Pointer arithmetic on the wrong type or past array bounds.
  • Confusing pointer vs pointee sizes: sizeof(ptr) is the size of the pointer itself, not the array.

Checks for Understanding

  1. What does the * mean in a declaration vs. when used in an expression?
  2. Why cast to (void*) when printing addresses with %p?
  3. What does p++ do when p is an int* pointing into an array?
Show suggested answers
  1. In a declaration it makes a pointer type; in an expression it dereferences (reads/writes the pointee).
  2. %p expects a void*, so casting is portable and correct.
  3. It advances p by sizeof(int) bytes to the next array element.

Practice Exercises

  1. Write a function divmod_i(int a, int b, int *q, int *r) that writes quotient and remainder via pointers.
    Solution
    void divmod_i(int a, int b, int *q, int *r) {
        *q = a / b;
        *r = a % b;
    }
    
  2. Allocate an array of N integers on the heap, fill with squares, print them, and free the memory.
    Solution
    #include <stdio.h>
    #include <stdlib.h>
    
    void print_squares(int n) {
        int *v = malloc((size_t)n * sizeof *v);
        if (!v) { perror("malloc"); return; }
        for (int i = 0; i < n; i++) v[i] = i * i;
        for (int i = 0; i < n; i++) printf("%d ", v[i]);
        printf("\n");
        free(v);
    }
    

Capstone Challenge

Implement a minimal resizable vector of int with init, push, and free using malloc/realloc.

Show reference implementation
#include <stdlib.h>
#include <stdbool.h>
#include <stdio.h>

typedef struct {
    int *data;
    size_t len;
    size_t cap;
} ivec;

bool ivec_init(ivec *v, size_t cap) {
    v->len = 0; v->cap = cap ? cap : 1;
    v->data = malloc(v->cap * sizeof *v->data);
    return v->data != NULL;
}

bool ivec_push(ivec *v, int x) {
    if (v->len == v->cap) {
        size_t new_cap = v->cap * 2;
        int *p = realloc(v->data, new_cap * sizeof *p);
        if (!p) return false;
        v->data = p; v->cap = new_cap;
    }
    v->data[v->len++] = x;
    return true;
}

void ivec_free(ivec *v) {
    free(v->data);
    v->data = NULL; v->len = v->cap = 0;
}

int main(void) {
    ivec v; if (!ivec_init(&v, 0)) return 1;
    for (int i = 0; i < 10; i++) ivec_push(&v, i);
    for (size_t i = 0; i < v.len; i++) printf("%d ", v.data[i]);
    printf("\n");
    ivec_free(&v);
}

Key Takeaways

  • Pointers store addresses; dereferencing accesses the value at that address.
  • Pointer arithmetic moves in units of the pointee type.
  • Passing pointers enables functions to modify caller-owned data.
  • Always pair malloc with free and check for NULL.