Module 5.3

Arrays and Pointers

Discover the deep connection between arrays and pointers in C. Learn how array names decay to pointers, master pointer arithmetic for efficient traversal, and understand how to create arrays of pointers and pointers to arrays for advanced data manipulation.

35 min read
Intermediate
Hands-on Examples
What You'll Learn
  • Array names as pointers (array decay)
  • Pointer arithmetic and traversal
  • Arrays of pointers
  • Pointers to arrays
  • Practical applications and patterns
Contents
01

Array Names as Pointers

In C, there is a fundamental relationship between arrays and pointers. When you use an array name in most expressions, it automatically converts (or "decays") to a pointer to its first element. Understanding this concept is essential for mastering C programming.

Concept

Array Decay

Array decay is the automatic conversion of an array name to a pointer to its first element. Think of it like this: when C sees an array name in most situations, it automatically treats it as "the address of the first box" instead of "the whole row of boxes."

Analogy: Imagine an array as a row of mailboxes numbered 0, 1, 2, 3... When you mention the array name, C gives you the address of mailbox #0. You lose the information about how many mailboxes there are - you just know where the first one is.

What happens during decay:

  • arr becomes equivalent to &arr[0] - the address of the first element
  • The type changes from "array of T" to "pointer to T" (for example, int[5] becomes int*)
  • Size information is lost - you can no longer tell how big the array was
  • This is why you must pass array size as a separate parameter to functions

When does decay happen?

  • Passing array to a function
  • Assigning array to a pointer variable
  • Using array in arithmetic expressions
  • Using array in comparisons

Exception: Array decay does NOT happen in three cases: (1) sizeof(arr) gives full array size, (2) &arr gives pointer to entire array, and (3) string literal used to initialize a char array.

Array Name Equals Pointer to First Element

#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    
    printf("arr     = %p\n", (void*)arr);
    printf("&arr[0] = %p\n", (void*)&arr[0]);
    
    // Both print the same address!
    return 0;
}

The array name arr and &arr[0] both give you the address of the first element. This is the essence of array decay.

Accessing Elements via Pointers

#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int *ptr = arr;  // ptr points to first element
    
    // These are equivalent:
    printf("arr[2] = %d\n", arr[2]);    // 30
    printf("*(arr+2) = %d\n", *(arr+2)); // 30
    printf("ptr[2] = %d\n", ptr[2]);    // 30
    printf("*(ptr+2) = %d\n", *(ptr+2)); // 30
    
    return 0;
}

Array subscript notation arr[i] is actually just syntactic sugar for *(arr + i). Both work identically whether you use an array name or a pointer.

When Decay Does NOT Happen

#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    
    // sizeof() - NO decay, gets full array size
    printf("sizeof(arr) = %zu\n", sizeof(arr));  // 20 (5 * 4 bytes)
    
    // & operator - NO decay, gets pointer to entire array
    printf("arr   = %p\n", (void*)arr);    // Address of first element
    printf("&arr  = %p\n", (void*)&arr);   // Same address, different type!
    
    // But the TYPES are different:
    // arr decays to int* (pointer to int)
    // &arr is int(*)[5] (pointer to array of 5 ints)
    
    return 0;
}

With sizeof(), you get the full array size (20 bytes for 5 ints). The &arr gives the same address as arr, but with a different type - a pointer to the entire array.

Key Insight: Although arr and &arr have the same numeric value (same memory address), they have different types. arr decays to int*, while &arr is int(*)[5].

Practice Questions

Task: Given an array int nums[4] = {5, 10, 15, 20};, write code to print all elements using pointer notation instead of array subscripts.

Show Solution
#include <stdio.h>

int main() {
    int nums[4] = {5, 10, 15, 20};
    
    for (int i = 0; i < 4; i++) {
        printf("%d ", *(nums + i));
    }
    printf("\n");  // Output: 5 10 15 20
    
    return 0;
}

Task: Write a program that demonstrates the difference between sizeof an array and sizeof a pointer to that array.

Show Solution
#include <stdio.h>

int main() {
    double arr[10];
    double *ptr = arr;
    
    printf("sizeof(arr) = %zu\n", sizeof(arr));  // 80 bytes
    printf("sizeof(ptr) = %zu\n", sizeof(ptr));  // 8 bytes (64-bit)
    
    // Calculate array length from sizeof
    size_t length = sizeof(arr) / sizeof(arr[0]);
    printf("Array length = %zu\n", length);  // 10
    
    return 0;
}

Task: Write a function that takes an array as a parameter and tries to calculate its size inside the function. Explain why it does not work.

Show Solution
#include <stdio.h>

void printSize(int arr[]) {
    // This does NOT work as expected!
    printf("Inside function: %zu\n", sizeof(arr));
    // Prints 8 (pointer size), not array size
}

int main() {
    int nums[5] = {1, 2, 3, 4, 5};
    
    printf("In main: %zu\n", sizeof(nums));  // 20 bytes
    printSize(nums);  // Prints 8 bytes!
    
    return 0;
}
// Explanation: When passed to a function, the array
// decays to a pointer, losing size information.

Task: Given int arr[5];, explain and demonstrate the difference between arr, &arr, arr+1, and &arr+1.

Show Solution
#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    
    printf("arr      = %p\n", (void*)arr);
    printf("&arr     = %p\n", (void*)&arr);
    printf("arr+1    = %p\n", (void*)(arr+1));
    printf("&arr+1   = %p\n", (void*)(&arr+1));
    
    // arr and &arr have same value but different types
    // arr+1 moves by sizeof(int) = 4 bytes
    // &arr+1 moves by sizeof(arr) = 20 bytes!
    
    return 0;
}

arr+1 advances by one element (4 bytes), while &arr+1 advances by the entire array size (20 bytes) because &arr is a pointer to the whole array.

02

Pointer Arithmetic

Pointer arithmetic allows you to navigate through memory by adding or subtracting integers from pointers. Unlike regular integer arithmetic, pointer arithmetic is scaled by the size of the pointed-to type, making array traversal elegant and efficient.

Concept

Pointer Arithmetic

Pointer arithmetic is special math that works with memory addresses. Unlike regular math where 1 + 1 = 2, adding 1 to a pointer moves it to the next element, not the next byte. C automatically multiplies by the element size!

Analogy: Think of pointer arithmetic like walking through hotel rooms. If you are at room 100 and move "1 forward", you go to room 101 - not room 100.5 or 100.25. Each "step" takes you to the next complete room, regardless of room size.

How the math works:

  • For int *p where int is 4 bytes: p + 1 moves address by 4 bytes
  • For double *p where double is 8 bytes: p + 1 moves by 8 bytes
  • For char *p where char is 1 byte: p + 1 moves by 1 byte
  • Formula: new_address = old_address + (n * sizeof(type))

All pointer arithmetic operations:

  • p + n - Move forward n elements (returns new pointer)
  • p - n - Move backward n elements (returns new pointer)
  • p++ - Move to next element (modifies p)
  • p-- - Move to previous element (modifies p)
  • p2 - p1 - Count elements between two pointers (returns integer)
  • p1 < p2 - Compare positions (returns true/false)

Warning: Pointer arithmetic only works correctly within an array or allocated memory block. Moving a pointer outside its valid range causes undefined behavior - your program might crash, corrupt data, or appear to work but fail later.

Adding and Subtracting from Pointers

Operation

Adding and Subtracting from Pointers

When you add or subtract a number from a pointer, you are telling C to move forward or backward by that many elements. C automatically handles the byte-level math based on the data type - you just think in terms of "how many items to skip."

Analogy: Imagine standing in a parking lot with numbered spaces. If you are at space #5 and someone says "move 3 spaces forward," you walk to space #8. You do not care that each space is 10 feet wide - you just count spaces. Pointer arithmetic works the same way: you count elements, not bytes.

The operations:

  • ptr + 3 - Returns address 3 elements ahead (does NOT modify ptr)
  • ptr - 2 - Returns address 2 elements behind (does NOT modify ptr)
  • ptr += 3 - Moves ptr forward by 3 elements (modifies ptr)
  • ptr -= 2 - Moves ptr backward by 2 elements (modifies ptr)

Behind the scenes (for int* on typical system):

Expression What You Write What C Calculates If ptr = 1000
ptr + 1 Move 1 element ptr + (1 * 4 bytes) 1004
ptr + 3 Move 3 elements ptr + (3 * 4 bytes) 1012
ptr - 2 Back 2 elements ptr - (2 * 4 bytes) 992

Key Point: The expression ptr + n does NOT change ptr itself - it returns a new address. To actually move the pointer, use ptr = ptr + n or the shorthand ptr += n.

#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int *ptr = arr;  // Points to arr[0]
    
    printf("ptr points to: %d\n", *ptr);      // 10
    printf("ptr+1 points to: %d\n", *(ptr+1)); // 20
    printf("ptr+4 points to: %d\n", *(ptr+4)); // 50
    
    ptr = ptr + 2;  // Now points to arr[2]
    printf("After ptr+2: %d\n", *ptr);  // 30
    
    return 0;
}

Adding 1 to an int pointer advances it by sizeof(int) bytes (typically 4), not by 1 byte. This is why pointer arithmetic works naturally with arrays.

Traversing Arrays with Pointers

Technique

Traversing Arrays with Pointers

Array traversal with pointers means walking through each element of an array by moving a pointer from the beginning to the end. Instead of using an index like arr[i], you use a pointer that "steps" through memory one element at a time.

Analogy: Imagine reading a book. You can either say "read page 1, then page 2, then page 3..." (index-based), or you can put your finger on the first page and keep flipping forward until you reach the end (pointer-based). Both get you through the book, but the finger method naturally knows where you are without counting.

The basic pattern:

for (ptr = arr; ptr < arr + size; ptr++) {
    // Use *ptr to access current element
}

Breaking down the loop:

  • ptr = arr - Start at the first element (initialization)
  • ptr < arr + size - Continue while ptr is before the "end marker"
  • ptr++ - Move to the next element after each iteration
  • *ptr - Dereference to get/set the current element's value

Why use pointer traversal?

  • Efficiency: Can be faster - no repeated index calculation
  • Idiomatic: Common pattern in C libraries and professional code
  • Direct access: Works naturally with dynamically allocated memory
  • Flexible: Easy to traverse forward, backward, or skip elements

The "one past the end" concept:

The expression arr + size points to the position just AFTER the last element. This is a valid address to compare against, but you must NEVER dereference it. It serves as a "stop sign" telling you when you have gone too far.

Common Mistake: Using ptr <= arr + size instead of ptr < arr + size will read one element past the array - this is undefined behavior and can crash your program or produce garbage values.

#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int *ptr;
    
    // Method 1: Using pointer increment
    printf("Forward: ");
    for (ptr = arr; ptr < arr + 5; ptr++) {
        printf("%d ", *ptr);
    }
    printf("\n");  // 10 20 30 40 50
    
    // Method 2: Using pointer with index
    printf("Backward: ");
    for (int i = 4; i >= 0; i--) {
        printf("%d ", *(arr + i));
    }
    printf("\n");  // 50 40 30 20 10
    
    return 0;
}

The condition ptr < arr + 5 checks if the pointer is still within the array bounds. Using ptr++ is a common idiom for traversing arrays efficiently.

Pointer Subtraction

#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int *start = arr;
    int *end = &arr[4];
    
    // Subtracting pointers gives element count
    printf("Elements between: %ld\n", end - start);  // 4
    
    // Find position of a value
    int *found = &arr[2];  // Points to 30
    printf("Position: %ld\n", found - arr);  // 2
    
    return 0;
}

Subtracting two pointers gives you the number of elements between them (not bytes). Both pointers must point to elements of the same array.

Pointer Comparison

Operation

Pointer Comparison

Pointer comparison lets you check the relative positions of two pointers in memory. You can ask questions like "Does this pointer come before that one?" or "Are these two pointers pointing to the same location?"

Analogy: Think of house numbers on a street. You can compare them: "Is house 105 before house 120?" (Yes, 105 < 120). Similarly, if two pointers point into the same array, you can compare which element comes first.

Comparison operators for pointers:

  • p1 < p2 - True if p1 points to an earlier element
  • p1 > p2 - True if p1 points to a later element
  • p1 <= p2 - True if p1 is at or before p2
  • p1 >= p2 - True if p1 is at or after p2
  • p1 == p2 - True if both point to the exact same location
  • p1 != p2 - True if they point to different locations

Common use cases:

  • Loop termination: while (ptr < end)
  • Finding elements: comparing a found pointer with array bounds
  • Two-pointer algorithms: checking if start and end have crossed
  • Null checks: if (ptr != NULL)

Important Rule: You can only meaningfully compare pointers that point to elements of the same array (or one past the end). Comparing pointers to unrelated memory locations is undefined behavior.

int arr[5] = {10, 20, 30, 40, 50};
int *p1 = &arr[1];  // Points to 20 (index 1)
int *p2 = &arr[3];  // Points to 40 (index 3)

First, we set up two pointers pointing to different positions in the same array. The pointer p1 is assigned the address of arr[1], which holds the value 20. The pointer p2 gets the address of arr[3], which holds 40. Since both pointers reference the same array, we can now compare them to determine their relative positions in memory. Remember that &arr[1] is equivalent to arr + 1 - both give us the address of the second element.

if (p1 < p2) {
    printf("p1 comes before p2\n");
}

Here we use the less-than operator (<) to compare the two pointers. This does not compare the values they point to (20 vs 40) - it compares their memory addresses. Since p1 points to index 1 and p2 points to index 3, p1 has a lower memory address, so p1 < p2 evaluates to true. This is useful in loops where you need to check if a pointer has reached or passed another pointer, such as in two-pointer algorithms for reversing arrays or finding pairs.

if (p2 - p1 == 2) {
    printf("There are 2 elements between them\n");
}

Pointer subtraction calculates how many elements exist between two pointers. When we compute p2 - p1, C automatically divides the byte difference by sizeof(int), giving us the element count. Since p2 points to index 3 and p1 points to index 1, the difference is 2 elements (index 3 minus index 1 = 2). This is incredibly useful for finding an element's position - if you have a pointer to a found element, subtracting the array's base address gives you its index: found - arr.

int *max = arr;  // Start assuming first element is max
for (int *p = arr + 1; p < arr + 5; p++) {
    if (*p > *max) {
        max = p;  // Update max pointer if current is larger
    }
}
printf("Max value: %d at index %ld\n", *max, max - arr);

This practical example combines multiple pointer comparison concepts to find the maximum value in an array. We initialize max to point to the first element, assuming it is the largest. The loop starts a pointer p at the second element (arr + 1) and uses p < arr + 5 for bounds checking - this compares p against "one past the last element" to know when to stop. Inside the loop, *p > *max compares the actual values (not addresses) to find larger elements. When found, we update max to point to that element. Finally, max - arr uses pointer subtraction to convert the pointer back to an index. This pattern is common in searching and sorting algorithms.

Practice Questions

Task: Write a function that calculates the sum of an array using pointer arithmetic instead of array indexing.

Show Solution
#include <stdio.h>

int sumArray(int *arr, int size) {
    int sum = 0;
    int *end = arr + size;
    
    while (arr < end) {
        sum += *arr;
        arr++;
    }
    return sum;
}

int main() {
    int nums[] = {1, 2, 3, 4, 5};
    printf("Sum: %d\n", sumArray(nums, 5));  // 15
    return 0;
}

Task: Write a function that reverses an array in place using two pointers (start and end).

Show Solution
#include <stdio.h>

void reverse(int *arr, int size) {
    int *start = arr;
    int *end = arr + size - 1;
    
    while (start < end) {
        int temp = *start;
        *start = *end;
        *end = temp;
        start++;
        end--;
    }
}

int main() {
    int nums[] = {1, 2, 3, 4, 5};
    reverse(nums, 5);
    
    for (int i = 0; i < 5; i++) {
        printf("%d ", nums[i]);
    }
    printf("\n");  // 5 4 3 2 1
    return 0;
}

Task: Write a function that searches for a value in an array and returns its index using pointer subtraction. Return -1 if not found.

Show Solution
#include <stdio.h>

int findIndex(int *arr, int size, int target) {
    int *end = arr + size;
    int *base = arr;
    
    while (arr < end) {
        if (*arr == target) {
            return arr - base;  // Pointer subtraction
        }
        arr++;
    }
    return -1;
}

int main() {
    int nums[] = {10, 20, 30, 40, 50};
    printf("Index of 30: %d\n", findIndex(nums, 5, 30));  // 2
    printf("Index of 99: %d\n", findIndex(nums, 5, 99));  // -1
    return 0;
}

Task: Write a function that copies elements from one array to another using only pointer arithmetic (no array indexing).

Show Solution
#include <stdio.h>

void copyArray(int *dest, int *src, int size) {
    int *end = src + size;
    
    while (src < end) {
        *dest = *src;
        dest++;
        src++;
    }
}

int main() {
    int source[] = {1, 2, 3, 4, 5};
    int destination[5];
    
    copyArray(destination, source, 5);
    
    printf("Copied: ");
    for (int *p = destination; p < destination + 5; p++) {
        printf("%d ", *p);
    }
    printf("\n");  // 1 2 3 4 5
    return 0;
}
03

Arrays of Pointers

An array of pointers is an array where each element is a pointer. This is incredibly useful for storing strings, creating ragged arrays, or managing collections of dynamically allocated objects. It is one of the most powerful patterns in C programming.

Concept

Array of Pointers

An array of pointers is an array where each element stores an address (pointer) instead of actual data. Think of it as a collection of arrows, where each arrow points to data stored somewhere else in memory.

Analogy: Imagine a bulletin board with sticky notes. Each sticky note does not contain the full document - instead, it says "Document is in Room 101", "Document is in Room 205", etc. The sticky notes (pointers) tell you where to find the actual content. The documents can be different sizes since they are stored separately.

Declaration syntax:

  • int *arr[5] - Creates 5 boxes, each holding an address of an int
  • char *names[10] - Creates 10 boxes, each holding an address of a string
  • double *data[100] - Creates 100 boxes, each holding an address of a double

Why use arrays of pointers?

  • Strings: Perfect for storing words/sentences of different lengths
  • Efficiency: Swapping pointers is faster than moving large data
  • Flexibility: Each element can point to different-sized data (jagged arrays)
  • Sharing: Multiple pointers can point to the same data

Common example - storing names:

char *fruits[] = {"Apple", "Banana", "Cherry"};
// fruits[0] points to "Apple" (6 bytes)
// fruits[1] points to "Banana" (7 bytes)
// fruits[2] points to "Cherry" (7 bytes)

Memory Layout: The array itself only stores addresses (typically 8 bytes each on 64-bit systems). The actual strings/data live elsewhere in memory. This is much more efficient than a 2D char array where every row must be the same size.

Array of Strings

Pattern

Array of Strings

An array of strings in C is really an array of pointers to characters. Each element is a char* that points to the first character of a string. This is one of the most common uses of pointer arrays.

Analogy: Imagine a table of contents in a book. Each entry does not contain the full chapter - it just tells you "Chapter 1 starts on page 5", "Chapter 2 starts on page 23". Similarly, each pointer in the array tells you where each string starts.

Declaration syntax:

char *words[] = {"Hello", "World", "C"};

How it works in memory:

  • words[0] is a pointer to 'H' (beginning of "Hello")
  • words[1] is a pointer to 'W' (beginning of "World")
  • words[2] is a pointer to 'C' (beginning of "C")
  • Each string can be a different length - no wasted space!

Accessing characters:

  • words[0] - The entire first string ("Hello")
  • words[0][0] - First character of first string ('H')
  • words[1][2] - Third character of second string ('r')

Note: String literals like "Hello" are stored in read-only memory. If you need to modify strings, you must allocate writable memory using malloc() or use a 2D char array instead.

#include <stdio.h>

int main() {
    // Array of pointers to strings
    char *colors[] = {"Red", "Green", "Blue", "Yellow"};
    int count = sizeof(colors) / sizeof(colors[0]);
    
    printf("Colors:\n");
    for (int i = 0; i < count; i++) {
        printf("  %d: %s\n", i, colors[i]);
    }
    
    // Each pointer points to a string literal
    printf("\nFirst char of Blue: %c\n", colors[2][0]);  // B
    
    return 0;
}

Each element in colors is a pointer to a string literal. The strings can have different lengths - this is much more memory-efficient than a 2D char array.

Array of Pointers to Integers

Pattern

Array of Pointers to Integers

An array of pointers to integers stores multiple int* values. Each pointer can point to a different integer variable or to an element in another array. This lets you create indirect references to scattered data.

Analogy: Think of a remote control with buttons. Each button does not contain the TV, lamp, or fan - it just has a link to control that device. Similarly, each pointer in the array links to an integer stored somewhere else.

Declaration and usage:

int x = 10, y = 20, z = 30;
int *ptrs[3] = {&x, &y, &z};  // Array of 3 pointers

Key operations:

  • ptrs[0] - The pointer itself (address of x)
  • *ptrs[0] - The value at that address (10)
  • *ptrs[1] = 100 - Changes y to 100 (modifies original!)

Why use this pattern?

  • Group related variables for batch processing
  • Create indirect access to existing data
  • Sort pointers instead of moving large data
  • Implement data structures like graphs

Remember: Modifying *ptrs[i] changes the original variable that the pointer points to. This is powerful but requires careful tracking of what each pointer references.

#include <stdio.h>

int main() {
    int a = 10, b = 20, c = 30;
    
    // Array of pointers to int
    int *ptrs[3] = {&a, &b, &c};
    
    // Access values through array of pointers
    for (int i = 0; i < 3; i++) {
        printf("ptrs[%d] = %p, *ptrs[%d] = %d\n", 
               i, (void*)ptrs[i], i, *ptrs[i]);
    }
    
    // Modify through pointer
    *ptrs[1] = 200;
    printf("b is now: %d\n", b);  // 200
    
    return 0;
}

Each element of ptrs stores the address of a different integer variable. Changing *ptrs[1] modifies the original variable b.

Command Line Arguments

Concept

Command Line Arguments (argc and argv)

Command line arguments let users pass information to your program when they run it. C provides two special parameters to main(): argc (argument count) and argv (argument vector - an array of string pointers).

Analogy: When you call a friend, you might say "Hey, bring pizza and soda." Command line arguments are like those extra instructions. Running ./program pizza soda passes "pizza" and "soda" as arguments to your program.

The two parameters:

  • int argc - Count of arguments (always at least 1)
  • char *argv[] - Array of pointers to argument strings

What is in argv?

  • argv[0] - Always the program name ("./program")
  • argv[1] - First actual argument ("pizza")
  • argv[2] - Second argument ("soda")
  • argv[argc] - Always NULL (marks the end)

Common patterns:

// Check if enough arguments provided
if (argc < 2) {
    printf("Usage: %s <filename>\n", argv[0]);
    return 1;
}

Important: All arguments in argv are strings (char*). If you need numbers, you must convert them using atoi(), atof(), or sscanf().

#include <stdio.h>

// argc = argument count
// argv = array of pointers to strings (arguments)
int main(int argc, char *argv[]) {
    printf("Program: %s\n", argv[0]);
    printf("Arguments: %d\n", argc - 1);
    
    for (int i = 1; i < argc; i++) {
        printf("  arg[%d]: %s\n", i, argv[i]);
    }
    
    return 0;
}
// Run: ./program hello world
// Output:
// Program: ./program
// Arguments: 2
//   arg[1]: hello
//   arg[2]: world

The argv parameter is the most common example of an array of pointers. It holds pointers to each command-line argument string.

Dynamic Array of Pointers

Pattern

Dynamic Array of Pointers

A dynamic array of pointers is created at runtime using malloc(). This is useful when you do not know how many items you need until the program runs. Each pointer in the array can then point to its own dynamically allocated data.

Analogy: Imagine building a parking garage. First, you build the structure with empty parking spaces (allocate the pointer array). Then, as cars arrive, each space gets assigned a specific car (allocate data for each pointer). When cars leave, you free up spaces (free each allocation, then the structure).

The two-step allocation:

  1. Allocate the array of pointers: char **arr = malloc(n * sizeof(char*))
  2. Allocate data for each pointer: arr[i] = malloc(size)

The two-step deallocation (reverse order!):

  1. Free each individual data block: free(arr[i])
  2. Free the pointer array itself: free(arr)

Why is order important?

If you free the pointer array first, you lose access to the addresses stored in it. Those data blocks become "orphaned" - still allocated but unreachable. This is a memory leak.

Best Practice: Always check if malloc() returns NULL (allocation failed). In production code, handle this error gracefully rather than crashing.

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

We include three essential headers: <stdio.h> for input/output functions like printf(), <stdlib.h> for memory allocation functions malloc() and free(), and <string.h> for string manipulation functions like strcpy(). These headers are required whenever you work with dynamic memory and strings in C.

int n = 3;
char **names = (char**)malloc(n * sizeof(char*));

This is the first step of the two-step allocation process. We declare names as a pointer to a pointer (char**) - this will hold the address of our dynamically created array. The malloc() call allocates memory for 3 pointers (not 3 strings!). On a 64-bit system, each pointer is 8 bytes, so we allocate 24 bytes total. Think of this as creating an empty array with 3 slots, where each slot will eventually hold the address of a string. The cast (char**) converts the generic void* returned by malloc to our specific pointer type.

names[0] = (char*)malloc(10);
strcpy(names[0], "Alice");

names[1] = (char*)malloc(10);
strcpy(names[1], "Bob");

names[2] = (char*)malloc(10);
strcpy(names[2], "Charlie");

This is the second step - allocating memory for each individual string. For each element in our pointer array, we call malloc(10) to allocate 10 bytes (enough for short names plus the null terminator). Each malloc() returns an address that we store in names[0], names[1], and names[2]. Then strcpy() copies the string content into that allocated memory. Note that each string can be a different size - "Alice" needs 6 bytes, "Bob" needs 4, and "Charlie" needs 8. We allocated 10 bytes each for simplicity, but in real code you might use strlen() + 1 to allocate exactly what is needed.

for (int i = 0; i < n; i++) {
    printf("%s\n", names[i]);
    free(names[i]);
}
free(names);

When freeing dynamically allocated memory, you must free in the reverse order of allocation. First, we loop through and free each individual string (free(names[i])). This releases the memory holding "Alice", "Bob", and "Charlie". Only after all the strings are freed do we free the array of pointers itself (free(names)). If you free names first, you lose access to the individual string addresses and create memory leaks - that memory becomes permanently unavailable until your program ends. Think of it like emptying boxes before throwing away the shelf that holds them.

Practice Questions

Task: Create an array of pointers containing 5 weekday names, then print them with their indices.

Show Solution
#include <stdio.h>

int main() {
    char *weekdays[] = {
        "Monday", "Tuesday", "Wednesday", 
        "Thursday", "Friday"
    };
    
    int count = sizeof(weekdays) / sizeof(weekdays[0]);
    
    for (int i = 0; i < count; i++) {
        printf("Day %d: %s\n", i + 1, weekdays[i]);
    }
    
    return 0;
}

Task: Write a function that takes an array of strings and returns a pointer to the longest string.

Show Solution
#include <stdio.h>
#include <string.h>

char* findLongest(char *strings[], int count) {
    char *longest = strings[0];
    
    for (int i = 1; i < count; i++) {
        if (strlen(strings[i]) > strlen(longest)) {
            longest = strings[i];
        }
    }
    return longest;
}

int main() {
    char *words[] = {"cat", "elephant", "dog", "mouse"};
    char *longest = findLongest(words, 4);
    printf("Longest: %s\n", longest);  // elephant
    return 0;
}

Task: Sort an array of strings alphabetically by swapping the pointers (not the strings themselves).

Show Solution
#include <stdio.h>
#include <string.h>

void sortStrings(char *arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = i + 1; j < n; j++) {
            if (strcmp(arr[i], arr[j]) > 0) {
                char *temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
        }
    }
}

int main() {
    char *fruits[] = {"Banana", "Apple", "Cherry", "Date"};
    sortStrings(fruits, 4);
    
    for (int i = 0; i < 4; i++) {
        printf("%s\n", fruits[i]);
    }
    // Apple, Banana, Cherry, Date
    return 0;
}

Task: Write a program that reads 3 names from the user, stores them in a dynamically allocated array of pointers, prints them, and properly frees all memory.

Show Solution
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    int n = 3;
    char **names = (char**)malloc(n * sizeof(char*));
    char buffer[100];
    
    for (int i = 0; i < n; i++) {
        printf("Enter name %d: ", i + 1);
        scanf("%99s", buffer);
        
        names[i] = (char*)malloc(strlen(buffer) + 1);
        strcpy(names[i], buffer);
    }
    
    printf("\nYou entered:\n");
    for (int i = 0; i < n; i++) {
        printf("  %s\n", names[i]);
    }
    
    // Free each string, then the array
    for (int i = 0; i < n; i++) {
        free(names[i]);
    }
    free(names);
    
    return 0;
}
04

Pointers to Arrays

A pointer to an array is different from an array of pointers. It is a single pointer that points to an entire array as a unit. This concept is crucial for working with multidimensional arrays and understanding how C handles array parameters.

Concept

Pointer to Array

A pointer to an array is a single pointer that points to an entire array as one unit. Instead of pointing to just the first element, it points to the whole "package" of elements together.

Analogy: Think of the difference between pointing at "the first egg in a carton" vs pointing at "the whole egg carton." A regular pointer (int*) points to one egg. A pointer to array (int(*)[12]) points to the entire carton of 12 eggs. Moving this pointer forward skips the entire carton to the next one!

The tricky syntax explained:

  • int (*ptr)[5] - Pointer to array of 5 ints (one pointer)
  • int *ptr[5] - Array of 5 pointers to int (five pointers)
  • The parentheses (*ptr) make ALL the difference!
  • Without parentheses, [] binds first, making it an array

How to read the declaration:

  1. Start with the identifier: ptr
  2. (*ptr) - "ptr is a pointer..."
  3. [5] - "...to an array of 5..."
  4. int - "...integers"
  5. Final: "ptr is a pointer to an array of 5 integers"

When to use pointer to array:

  • Working with 2D arrays (rows are arrays)
  • Function parameters that need to know column size
  • Iterating through rows of a matrix
  • When ptr++ should skip an entire row

Key difference: If int (*p)[5] and int is 4 bytes, then p + 1 moves forward by 5 * 4 = 20 bytes (one whole row). But for int *p, p + 1 only moves by 4 bytes (one element).

Declaring and Using Pointer to Array

#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    
    int (*ptr)[5] = &arr;  // Pointer to array of 5 ints
    
    // Access elements (need to dereference first)
    printf("(*ptr)[0] = %d\n", (*ptr)[0]);  // 10
    printf("(*ptr)[2] = %d\n", (*ptr)[2]);  // 30
    
    // Alternative syntax
    printf("ptr[0][3] = %d\n", ptr[0][3]);  // 40
    
    return 0;
}

Note that &arr gives us a pointer to the entire array. To access elements, we first dereference with (*ptr) then use the index.

Array of Pointers vs Pointer to Array

Aspect Array of Pointers Pointer to Array
Declaration int *arr[5] int (*ptr)[5]
Meaning 5 pointers to int 1 pointer to array of 5 ints
Size 5 * sizeof(int*) = 40 bytes sizeof(int*) = 8 bytes
Increment Moves by sizeof(int*) Moves by 5 * sizeof(int)
Use Case Array of strings, ragged arrays 2D arrays, row pointers

Pointer to Array with 2D Arrays

Application

Pointer to Array with 2D Arrays

When working with 2D arrays, a pointer to an array becomes incredibly useful. A 2D array is really an "array of arrays" - each row is itself an array. A pointer to an array can point to one row at a time, and incrementing it moves to the next row.

Analogy: Think of a 2D array as a bookshelf with multiple shelves. Each shelf (row) holds several books (elements). A pointer to an array is like pointing at "the entire second shelf" rather than "the first book on the second shelf." When you move this pointer forward, you jump to the next complete shelf, not the next book.

The key insight:

  • A 2D array int matrix[3][4] has 3 rows, each containing 4 ints
  • The name matrix decays to int (*)[4] - pointer to first row
  • matrix + 1 points to the second row (skips 4 ints = 16 bytes)
  • Each row is an array of 4 ints, so the pointer type is int (*)[4]

Accessing elements:

  • rowPtr[i][j] - Row i, column j (most readable)
  • (*(rowPtr + i))[j] - Move to row i, then access column j
  • *(*(rowPtr + i) + j) - Fully dereferenced form

Why this matters: When you pass a 2D array to a function, the compiler needs to know how many columns each row has to calculate memory offsets. Using int (*matrix)[4] tells the compiler "each row is 4 ints wide," enabling proper 2D indexing inside the function.

int matrix[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};

We declare a 2D array with 3 rows and 4 columns. In memory, this is stored as 12 consecutive integers: row 0 (1,2,3,4), then row 1 (5,6,7,8), then row 2 (9,10,11,12). Each row occupies 16 bytes (4 ints × 4 bytes each). Understanding this layout is crucial for pointer arithmetic with 2D arrays.

int (*rowPtr)[4] = matrix;

Here we declare rowPtr as a pointer to an array of 4 ints. We assign matrix to it - when a 2D array name is used in an expression, it decays to a pointer to its first row. Notice we do NOT use &matrix here because matrix already decays to the correct type (int (*)[4]). The rowPtr now points to the first row of the matrix.

printf("Row 0: ");
for (int i = 0; i < 4; i++) {
    printf("%d ", rowPtr[0][i]);
}
printf("\n");  // Output: 1 2 3 4

We access elements using 2D indexing: rowPtr[0][i] means "row 0, column i". This works because rowPtr[0] gives us the first row (an array of 4 ints), and [i] then accesses the i-th element of that row. The syntax looks just like regular 2D array access, which is the beauty of using pointer to array.

rowPtr++;
printf("Row 1: ");
for (int i = 0; i < 4; i++) {
    printf("%d ", (*rowPtr)[i]);
}
printf("\n");  // Output: 5 6 7 8

This is where the magic of pointer to array shines. When we do rowPtr++, the pointer moves forward by the size of one complete row (4 ints = 16 bytes), NOT by the size of a single int. Now rowPtr points to the second row. We access elements with (*rowPtr)[i] - first dereference to get the array (the row), then index into it. This form is equivalent to rowPtr[0][i] after the increment. The ability to "jump" entire rows with a single ++ makes row-by-row processing elegant and efficient.

Function Parameters with Array Size

Technique

Function Parameters with Array Size

When passing a 2D array to a function, the compiler needs to know the column size to calculate memory offsets correctly. Using a pointer to an array as the parameter (int (*arr)[COLS]) preserves this column information, enabling natural 2D indexing inside the function.

Analogy: Imagine giving someone directions to a seat in a theater. You could say "row 3, seat 7" - but this only works if they know how many seats are in each row. If each row has 10 seats, seat 7 in row 3 is the 37th seat overall (3×10 + 7). The column size is like knowing "seats per row" - essential for calculating the actual position.

Why the column size is required:

  • A 2D array is stored as a flat block of memory (row-major order)
  • To find arr[i][j], C calculates: base + (i * columns + j) * sizeof(element)
  • Without knowing columns, the compiler cannot compute the correct offset
  • The row count can be passed separately, but column size must be in the type

Three ways to pass 2D arrays:

  • void func(int arr[][4], int rows) - Array notation (most readable)
  • void func(int (*arr)[4], int rows) - Pointer notation (equivalent)
  • void func(int *arr, int rows, int cols) - Flat pointer (flexible but manual indexing)

Limitation: The column size must be known at compile time when using the first two methods. For truly dynamic 2D arrays where both dimensions are unknown until runtime, you need to use a flat pointer or array of pointers approach.

void printMatrix(int (*matrix)[4], int rows) {

This function signature declares matrix as a pointer to an array of 4 integers. The [4] tells the compiler that each row contains exactly 4 integers, which is essential for calculating memory offsets when you use 2D indexing like matrix[i][j]. The rows parameter tells the function how many rows to process - this can vary, but the column count is baked into the type. Note that int (*matrix)[4] and int matrix[][4] are equivalent in function parameters - both decay to a pointer to the first row.

    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%3d ", matrix[i][j]);
        }
        printf("\n");
    }
}

Inside the function, we can use familiar 2D array syntax matrix[i][j] to access elements. The outer loop iterates through rows (using the rows parameter), while the inner loop iterates through columns (we know there are 4 because it's in the type). The expression matrix[i][j] is computed as: start at matrix, skip i complete rows of 4 ints each, then access the j-th element. The %3d format specifier prints each integer with a width of 3 characters for neat alignment.

int main() {
    int data[2][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8}
    };
    
    printMatrix(data, 2);
    return 0;
}

In main(), we declare a 2×4 matrix and pass it to our function. When we write printMatrix(data, 2), the array name data decays to a pointer to its first row - which is exactly what int (*matrix)[4] expects. We pass 2 as the row count because the function cannot deduce this from the pointer. The key insight is that we could pass any 2D array with 4 columns (like int arr[10][4] or int arr[100][4]) to this same function - the column size (4) is fixed, but the row count is flexible.

Practice Questions

Task: Declare a pointer to an array of 3 doubles, point it to an array, and print all elements using the pointer.

Show Solution
#include <stdio.h>

int main() {
    double values[3] = {1.1, 2.2, 3.3};
    
    double (*ptr)[3] = &values;
    
    for (int i = 0; i < 3; i++) {
        printf("(*ptr)[%d] = %.1f\n", i, (*ptr)[i]);
    }
    
    return 0;
}

Task: Write a function that takes a pointer to a 2D array with 3 columns and calculates the sum of each row.

Show Solution
#include <stdio.h>

void rowSums(int (*matrix)[3], int rows) {
    for (int i = 0; i < rows; i++) {
        int sum = 0;
        for (int j = 0; j < 3; j++) {
            sum += matrix[i][j];
        }
        printf("Row %d sum: %d\n", i, sum);
    }
}

int main() {
    int data[2][3] = {
        {1, 2, 3},
        {4, 5, 6}
    };
    
    rowSums(data, 2);
    // Row 0 sum: 6
    // Row 1 sum: 15
    
    return 0;
}

Task: Given a 3x3 matrix, use a pointer to array to print only the diagonal elements (0,0), (1,1), (2,2).

Show Solution
#include <stdio.h>

int main() {
    int matrix[3][3] = {
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9}
    };
    
    int (*ptr)[3] = matrix;
    
    printf("Diagonal elements: ");
    for (int i = 0; i < 3; i++) {
        printf("%d ", ptr[i][i]);
    }
    printf("\n");  // 1 5 9
    
    return 0;
}

Task: Write a function that transposes a 3x3 matrix in place using pointer to array notation.

Show Solution
#include <stdio.h>

void transpose(int (*m)[3]) {
    for (int i = 0; i < 3; i++) {
        for (int j = i + 1; j < 3; j++) {
            int temp = m[i][j];
            m[i][j] = m[j][i];
            m[j][i] = temp;
        }
    }
}

void printMatrix(int (*m)[3]) {
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%d ", m[i][j]);
        }
        printf("\n");
    }
}

int main() {
    int matrix[3][3] = {
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9}
    };
    
    printf("Original:\n");
    printMatrix(matrix);
    
    transpose(matrix);
    
    printf("\nTransposed:\n");
    printMatrix(matrix);
    
    return 0;
}

Key Takeaways

Array Decay

Array names automatically convert to pointers to their first element in most contexts. Size information is lost.

arr[i] Equals *(arr+i)

Array subscript notation is syntactic sugar for pointer arithmetic. Both work with arrays and pointers.

Scaled Arithmetic

Pointer arithmetic is automatically scaled by the size of the pointed-to type. p+1 moves by sizeof(*p) bytes.

Arrays of Pointers

int *arr[5] creates 5 pointers. Perfect for strings and ragged arrays where each element can be different size.

Pointers to Arrays

int (*ptr)[5] is a pointer to an array of 5 ints. Parentheses matter - essential for 2D array parameters.

2D Array Handling

A 2D array name decays to a pointer to its first row. Use int (*p)[cols] for function parameters.

Knowledge Check

Quick Quiz

Test your understanding of arrays and pointers in C

1 What does "array decay" mean in C?
2 If int arr[5]; and int *ptr = arr;, what is *(ptr + 3) equivalent to?
3 What is the difference between int *arr[5] and int (*arr)[5]?
4 When adding 1 to an int* pointer, how many bytes does the address increase by (assuming 4-byte int)?
5 Which scenario does NOT cause array decay?
6 What is char *argv[] in main(int argc, char *argv[])?
Answer all questions to check your score