Docs

README

String Arrays and Pointers in C

Table of Contents

  1. Introduction
  2. Arrays of Strings
  3. Pointer Arrays vs 2D Character Arrays
  4. Allocating String Arrays
  5. Accessing String Array Elements
  6. Sorting String Arrays
  7. Searching String Arrays
  8. Command Line Arguments
  9. Common Patterns
  10. Memory Management
  11. Best Practices
  12. Summary

Introduction

Working with multiple strings requires understanding how to organize them in arrays. C provides two main approaches:

  1. 2D Character Arrays: Fixed-size rows and columns
  2. Array of Pointers: Flexible, dynamic string storage

Understanding these concepts is essential for:

  • Processing word lists
  • Handling command line arguments
  • Building dictionaries
  • Text processing applications

Arrays of Strings

2D Character Array Approach

A 2D character array stores strings in fixed-size rows.

// Declare array of 5 strings, each up to 19 characters + null
char names[5][20] = {
    "Alice",
    "Bob",
    "Charlie",
    "David",
    "Eve"
};

Memory Layout

Memory (2D Array - Fixed Size):

        [0]    [1]    [2]    ...   [19]
       ┌──────┬──────┬──────┬─────┬──────┐
Row 0  │  A   │  l   │  i   │     │  \0  │  "Alice"
       ├──────┼──────┼──────┼─────┼──────┤
Row 1  │  B   │  o   │  b   │ \0  │      │  "Bob"
       ├──────┼──────┼──────┼─────┼──────┤
Row 2  │  C   │  h   │  a   │     │  \0  │  "Charlie"
       ├──────┼──────┼──────┼─────┼──────┤
Row 3  │  D   │  a   │  v   │     │  \0  │  "David"
       ├──────┼──────┼──────┼─────┼──────┤
Row 4  │  E   │  v   │  e   │ \0  │      │  "Eve"
       └──────┴──────┴──────┴─────┴──────┘

Total: 5 rows × 20 columns = 100 bytes (contiguous)

Array of Pointers Approach

An array of pointers stores addresses of strings.

// Array of 5 string pointers
char *names[] = {
    "Alice",
    "Bob",
    "Charlie",
    "David",
    "Eve"
};

Memory Layout (Pointer Array)

Pointer Array:              String Literals (in read-only memory):

names[0] ──────────────────► "Alice\0"
names[1] ──────────────────► "Bob\0"
names[2] ──────────────────► "Charlie\0"
names[3] ──────────────────► "David\0"
names[4] ──────────────────► "Eve\0"

Pointer array: 5 × sizeof(char*) = 40 bytes (on 64-bit system)
Strings: Variable sizes in separate memory locations

Pointer Arrays vs 2D Character Arrays

Comparison Table

Feature2D Array char arr[N][M]Pointer Array char *arr[N]
MemoryContiguous blockScattered (pointers + strings)
String SizeFixed (M-1 max)Variable
ModificationStrings modifiableLiterals are read-only
Memory UsageMay waste spaceEfficient
ReorderingExpensive (copy strings)Cheap (swap pointers)
DeclarationSize neededCan omit size

When to Use 2D Arrays

// Good for: Modifiable strings of similar length
char menu[4][20] = {
    "New Game",
    "Load Game",
    "Settings",
    "Exit"
};

// Can modify:
strcpy(menu[0], "Start New");  // OK

When to Use Pointer Arrays

// Good for: Read-only strings, varying lengths, sorting
const char *months[] = {
    "January", "February", "March", "April",
    "May", "June", "July", "August",
    "September", "October", "November", "December"
};

// For sorting: Just swap pointers (fast)
const char *temp = months[0];
months[0] = months[1];
months[1] = temp;

Modifiability Issue

// Pointer to string literal - READ ONLY
char *str = "Hello";
// str[0] = 'J';  // UNDEFINED BEHAVIOR! (may crash)

// Character array - MODIFIABLE
char str2[] = "Hello";
str2[0] = 'J';  // OK: str2 is now "Jello"

Allocating String Arrays

Static Allocation (2D Array)

// Fixed at compile time
#define MAX_STRINGS 10
#define MAX_LENGTH 50

char strings[MAX_STRINGS][MAX_LENGTH];

// Initialize
strcpy(strings[0], "First");
strcpy(strings[1], "Second");

Dynamic Allocation (Pointer Array)

// Allocate array of pointers
int n = 5;
char **strings = malloc(n * sizeof(char *));

if (strings == NULL) {
    // Handle error
}

// Allocate each string
for (int i = 0; i < n; i++) {
    strings[i] = malloc(50 * sizeof(char));
    if (strings[i] == NULL) {
        // Handle error, free previous allocations
    }
}

// Use strings
strcpy(strings[0], "Hello");
strcpy(strings[1], "World");

// Free in reverse order
for (int i = 0; i < n; i++) {
    free(strings[i]);
}
free(strings);

Dynamic Allocation with Variable Lengths

char **create_string_array(const char *data[], int count) {
    char **arr = malloc(count * sizeof(char *));
    if (arr == NULL) return NULL;

    for (int i = 0; i < count; i++) {
        // Allocate exact size needed
        arr[i] = malloc(strlen(data[i]) + 1);
        if (arr[i] == NULL) {
            // Cleanup on failure
            for (int j = 0; j < i; j++) {
                free(arr[j]);
            }
            free(arr);
            return NULL;
        }
        strcpy(arr[i], data[i]);
    }

    return arr;
}

Using strdup() for Convenience

#define _POSIX_C_SOURCE 200809L  // For strdup
#include <string.h>

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

// strdup allocates and copies
names[0] = strdup("Alice");    // Allocates strlen("Alice")+1
names[1] = strdup("Bob");
names[2] = strdup("Charlie");

// Don't forget to free!
for (int i = 0; i < 3; i++) {
    free(names[i]);
}
free(names);

Accessing String Array Elements

Indexing Syntax

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

// Access string
printf("%s\n", words[0]);      // "Hello"

// Access character
printf("%c\n", words[0][1]);   // 'e'

// Using pointers
printf("%c\n", *(words[0]+1)); // 'e' (same as above)
printf("%c\n", *words[0]);     // 'H' (first char)

Iterating Through String Arrays

// Method 1: Using counter
int count = 3;
char *words[] = {"one", "two", "three"};

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

// Method 2: NULL-terminated array
char *fruits[] = {"apple", "banana", "cherry", NULL};

for (int i = 0; fruits[i] != NULL; i++) {
    printf("%s\n", fruits[i]);
}

// Method 3: Using pointer to pointer
char **ptr = fruits;
while (*ptr != NULL) {
    printf("%s\n", *ptr);
    ptr++;
}

Calculating Array Size

// For static arrays
char *words[] = {"Hello", "World", "C"};
int count = sizeof(words) / sizeof(words[0]);  // 3

// For 2D arrays
char names[5][20] = {"Alice", "Bob", "Charlie"};
int rows = sizeof(names) / sizeof(names[0]);   // 5
int cols = sizeof(names[0]);                    // 20

Sorting String Arrays

Bubble Sort for Strings

void bubble_sort_strings(char **arr, int n) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (strcmp(arr[j], arr[j + 1]) > 0) {
                // Swap pointers (efficient!)
                char *temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

// Usage
char *names[] = {"Charlie", "Alice", "Bob"};
bubble_sort_strings(names, 3);
// Now: {"Alice", "Bob", "Charlie"}

Using qsort()

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

// Comparison function for qsort
int compare_strings(const void *a, const void *b) {
    // a and b are pointers to char*
    const char *str_a = *(const char **)a;
    const char *str_b = *(const char **)b;
    return strcmp(str_a, str_b);
}

// Case-insensitive comparison
int compare_strings_nocase(const void *a, const void *b) {
    const char *str_a = *(const char **)a;
    const char *str_b = *(const char **)b;
    return strcasecmp(str_a, str_b);  // POSIX
}

// Usage
char *words[] = {"banana", "Apple", "cherry"};
int count = sizeof(words) / sizeof(words[0]);

qsort(words, count, sizeof(char *), compare_strings);

Sorting 2D Character Arrays

// Sorting 2D arrays requires copying strings
void bubble_sort_2d(char arr[][20], int n) {
    char temp[20];

    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (strcmp(arr[j], arr[j + 1]) > 0) {
                // Must copy entire strings
                strcpy(temp, arr[j]);
                strcpy(arr[j], arr[j + 1]);
                strcpy(arr[j + 1], temp);
            }
        }
    }
}

Searching String Arrays

Linear Search

int linear_search(char **arr, int n, const char *target) {
    for (int i = 0; i < n; i++) {
        if (strcmp(arr[i], target) == 0) {
            return i;  // Found at index i
        }
    }
    return -1;  // Not found
}

// Case-insensitive version
int linear_search_nocase(char **arr, int n, const char *target) {
    for (int i = 0; i < n; i++) {
        if (strcasecmp(arr[i], target) == 0) {
            return i;
        }
    }
    return -1;
}

Binary Search (Sorted Array)

int binary_search_string(char **arr, int n, const char *target) {
    int left = 0, right = n - 1;

    while (left <= right) {
        int mid = left + (right - left) / 2;
        int cmp = strcmp(arr[mid], target);

        if (cmp == 0) {
            return mid;  // Found
        } else if (cmp < 0) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }

    return -1;  // Not found
}

Using bsearch()

#include <stdlib.h>

int compare_for_bsearch(const void *key, const void *element) {
    const char *search_key = *(const char **)key;
    const char *arr_element = *(const char **)element;
    return strcmp(search_key, arr_element);
}

// Usage (array must be sorted!)
char *words[] = {"apple", "banana", "cherry", "date"};
int count = 4;

char *target = "cherry";
char **result = bsearch(&target, words, count, sizeof(char *),
                        compare_for_bsearch);

if (result != NULL) {
    printf("Found: %s\n", *result);
}

Command Line Arguments

Understanding argc and argv

int main(int argc, char *argv[]) {
    // argc: Argument count (including program name)
    // argv: Array of argument strings

    printf("Number of arguments: %d\n", argc);

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

    return 0;
}

Example Execution

$ ./program hello world 123

Output:
Number of arguments: 4
argv[0] = "./program"
argv[1] = "hello"
argv[2] = "world"
argv[3] = "123"

Memory Layout of argv

argv (array of pointers):

argv[0] ──► "./program\0"
argv[1] ──► "hello\0"
argv[2] ──► "world\0"
argv[3] ──► "123\0"
argv[4] ──► NULL  (always null-terminated)

Processing Command Line Options

int main(int argc, char *argv[]) {
    int verbose = 0;
    const char *filename = NULL;

    for (int i = 1; i < argc; i++) {
        if (strcmp(argv[i], "-v") == 0 ||
            strcmp(argv[i], "--verbose") == 0) {
            verbose = 1;
        } else if (strcmp(argv[i], "-f") == 0 && i + 1 < argc) {
            filename = argv[++i];
        } else {
            printf("Unknown option: %s\n", argv[i]);
        }
    }

    if (verbose) printf("Verbose mode enabled\n");
    if (filename) printf("File: %s\n", filename);

    return 0;
}

Alternative Main Signature

// With environment variables
int main(int argc, char *argv[], char *envp[]) {
    // envp contains environment variables
    for (int i = 0; envp[i] != NULL; i++) {
        printf("%s\n", envp[i]);
    }
    return 0;
}

Common Patterns

Tokenizing Input into String Array

int tokenize(char *str, char *tokens[], int max, const char *delim) {
    int count = 0;
    char *token = strtok(str, delim);

    while (token != NULL && count < max) {
        tokens[count++] = token;
        token = strtok(NULL, delim);
    }

    return count;
}

// Usage
char input[] = "one,two,three,four";
char *tokens[10];
int count = tokenize(input, tokens, 10, ",");

Building String Array from Input

char **read_lines(FILE *file, int *count) {
    char **lines = NULL;
    char buffer[256];
    int capacity = 0;
    *count = 0;

    while (fgets(buffer, sizeof(buffer), file)) {
        // Remove newline
        buffer[strcspn(buffer, "\n")] = '\0';

        // Grow array if needed
        if (*count >= capacity) {
            capacity = capacity == 0 ? 10 : capacity * 2;
            char **new_lines = realloc(lines, capacity * sizeof(char *));
            if (new_lines == NULL) {
                // Handle error
                break;
            }
            lines = new_lines;
        }

        // Allocate and copy line
        lines[*count] = strdup(buffer);
        if (lines[*count] == NULL) break;
        (*count)++;
    }

    return lines;
}

Finding Unique Strings

int find_unique(char **arr, int n, char **unique) {
    int unique_count = 0;

    for (int i = 0; i < n; i++) {
        int is_duplicate = 0;

        for (int j = 0; j < unique_count; j++) {
            if (strcmp(arr[i], unique[j]) == 0) {
                is_duplicate = 1;
                break;
            }
        }

        if (!is_duplicate) {
            unique[unique_count++] = arr[i];
        }
    }

    return unique_count;
}

Filtering Strings

int filter_by_prefix(char **arr, int n, char **result,
                     const char *prefix) {
    int count = 0;
    size_t prefix_len = strlen(prefix);

    for (int i = 0; i < n; i++) {
        if (strncmp(arr[i], prefix, prefix_len) == 0) {
            result[count++] = arr[i];
        }
    }

    return count;
}

Memory Management

Proper Cleanup Pattern

void free_string_array(char **arr, int count) {
    if (arr == NULL) return;

    for (int i = 0; i < count; i++) {
        free(arr[i]);  // Free each string
    }
    free(arr);         // Free the array of pointers
}

// Usage
char **words = create_string_array(...);
// ... use words ...
free_string_array(words, count);
words = NULL;  // Prevent dangling pointer

Resizing String Arrays

char **resize_string_array(char **arr, int old_count, int new_count) {
    char **new_arr = realloc(arr, new_count * sizeof(char *));

    if (new_arr == NULL) {
        return NULL;  // Original array still valid
    }

    // Initialize new slots to NULL
    for (int i = old_count; i < new_count; i++) {
        new_arr[i] = NULL;
    }

    return new_arr;
}

Copying String Arrays

char **copy_string_array(char **src, int count) {
    char **dest = malloc(count * sizeof(char *));
    if (dest == NULL) return NULL;

    for (int i = 0; i < count; i++) {
        dest[i] = strdup(src[i]);
        if (dest[i] == NULL) {
            // Cleanup on failure
            for (int j = 0; j < i; j++) {
                free(dest[j]);
            }
            free(dest);
            return NULL;
        }
    }

    return dest;
}

Best Practices

1. Use const for Read-Only Arrays

// Good: Clear intent
void print_words(const char * const words[], int count) {
    for (int i = 0; i < count; i++) {
        printf("%s\n", words[i]);
    }
}

2. Always Track Array Size

typedef struct {
    char **data;
    int count;
    int capacity;
} StringArray;

void init_string_array(StringArray *arr) {
    arr->data = NULL;
    arr->count = 0;
    arr->capacity = 0;
}

3. Validate Before Access

const char* get_string(char **arr, int count, int index) {
    if (arr == NULL || index < 0 || index >= count) {
        return NULL;
    }
    return arr[index];
}

4. Use NULL Termination for Flexibility

// Self-describing array (no need to pass count)
char *colors[] = {"red", "green", "blue", NULL};

// Can iterate without knowing size
for (char **p = colors; *p != NULL; p++) {
    printf("%s\n", *p);
}

5. Choose the Right Approach

// Use 2D array when:
// - Strings have similar lengths
// - Need to modify strings
// - Know maximum count and length at compile time
char names[MAX_NAMES][MAX_NAME_LEN];

// Use pointer array when:
// - String lengths vary greatly
// - Need efficient sorting/reordering
// - Dynamic number of strings
char **names;

Summary

Key Concepts

ConceptDescription
char arr[N][M]2D array, fixed size, contiguous memory
char *arr[N]Array of pointers, flexible lengths
char **arrPointer to pointer, fully dynamic
argvCommand line arguments array

Memory Allocation Patterns

// Static 2D array
char strings[10][50];

// Static pointer array
char *strings[10];

// Dynamic pointer array
char **strings = malloc(n * sizeof(char *));
for (int i = 0; i < n; i++) {
    strings[i] = malloc(len);
}

Access Patterns

// Access string
arr[i]

// Access character
arr[i][j]

// Using pointers
*(arr + i)      // Same as arr[i]
*(*(arr + i) + j)  // Same as arr[i][j]

Common Operations

OperationPointer Array2D Array
SortSwap pointersCopy strings
Comparestrcmp(arr[i], arr[j])Same
Copyarr[i] = strdup(src)strcpy(arr[i], src)
SearchLinear/BinarySame
Resizerealloc + allocateLimited

Cleanup Pattern

// Always free in reverse order
for (int i = 0; i < count; i++) {
    free(arr[i]);  // Free strings first
}
free(arr);         // Then free array
arr = NULL;
README - C Programming Tutorial | DeepML