Arrays of Pointers in C: Syntax, Examples, and Embedded Systems Use Cases

Arrays of Pointers in C: Syntax, Examples, and Embedded Systems Use Cases
An array of pointers in C is an array where every element stores a memory address instead of a value. For example, int *ptr[5]; creates five pointers to int. Each element can point to a different variable, string, array, or function, which makes it ideal for string tables, jagged arrays, menus, and command dispatch tables in embedded firmware.

Picture a small device with a 16×2 LCD and a menu: Start, Settings, Calibrate sensor, About. Store those labels in a 2D char array and every row gets padded to the length of the longest label. Store them in an array of pointers and each label uses only the bytes it needs.

That’s just one reason arrays of pointers show up everywhere in real firmware: error message tables, UART command parsers, state machines, even the interrupt vector table at the start of your flash.

In this guide, you’ll learn:

  • What an array of pointers is, and how to read its declaration without guessing
  • How it differs from a pointer to an array (the mix-up that catches almost everyone)
  • How to build string tables, jagged arrays, and dynamic tables
  • How arrays of function pointers power dispatch tables and state machines
  • Which mistakes cause hard faults, and how to avoid them

What Is an Array of Pointers in C?

An array of pointers is a normal array whose elements are all pointers of the same type. Think of the contacts list on your phone: each entry doesn’t contain the person, only the number that reaches them. Each slot in an array of pointers works the same way. It holds an address, and the real data lives somewhere else.

Key Points

  • Every element has the same pointer type (int *, char *, struct sensor *, or a function pointer type)
  • The pointer slots sit side by side in memory, but the things they point to can be anywhere
  • Any slot can be re-pointed at runtime
  • A slot that points nowhere should hold NULL
  • Total size = number of elements × pointer size, and pointer size depends on your target:
PlatformTypical Pointer Size
8051 (Keil C51, generic pointer)3 bytes
8-bit AVR (Arduino Uno)2 bytes
ARM Cortex-M4 bytes
64-bit PC8 bytes

Always confirm with sizeof(int *) on your own toolchain.

Here’s the picture to keep in your head:

ptr (array of pointers)        Targets in memory

+-----------+                  +------+
| ptr[0]    | ---------------> |  10  |  a
+-----------+                  +------+
| ptr[1]    | ---------------> |  20  |  b
+-----------+                  +------+
| ptr[2]    | ---------------> |  30  |  c
+-----------+                  +------+

How to Declare an Array of Pointers in C

Syntax

data_type *array_name[size];

Example

int *ptr[5];

How to Read It, Step by Step

  1. Start at the name: ptr
  2. Look right: [5] means “array of 5”
  3. Look left: * means “pointers to”
  4. Finish with the type: int

Result: ptr is an array of 5 pointers to int. This works because [] binds tighter than * in a declaration. Hold onto that rule, because it matters again in a moment.

registor_now_P

Initializing and Accessing Elements

#include 

int main(void)
{
    int a = 10, b = 20, c = 30;
    int *ptr[3] = { &a, &b, &c };

    for (int i = 0; i < 3; i++) {
        printf("ptr[%d] holds %p and points to %d\n", i, (void *)ptr[i], *ptr[i]);
    }

    *ptr[1] = 99;                  /* changes b through the array */
    printf("b is now %d\n", b);    /* prints 99 */

    return 0;
}

Output:

ptr[0] holds 0x7ffd5c3a1a24 and points to 10
ptr[1] holds 0x7ffd5c3a1a28 and points to 20
ptr[2] holds 0x7ffd5c3a1a2c and points to 30
b is now 99

(Your addresses will differ.)

Key Points

  • ptr[i] is the address stored in slot i
  • *ptr[i] is the value at that address ([] is evaluated first, then *)
  • Writing through *ptr[i] changes the original variable
  • %p expects a void *, so cast the pointer when printing

Array of Pointers to Strings (The Most Popular Use)

A string literal like "Timeout" sits in memory as a read-only block of characters, and a const char * can point to it. Put several of those pointers in an array and you get a string table.

#include 

int main(void)
{
    const char *errors[] = {
        "OK",
        "Timeout",
        "Checksum mismatch",
        "Buffer overflow",
        "Sensor not found"
    };

    size_t count = sizeof errors / sizeof errors[0];

    for (size_t i = 0; i < count; i++) {
        printf("[%u] %s\n", (unsigned)i, errors[i]);
    }

    return 0;
}

Output:

[0] OK
[1] Timeout
[2] Checksum mismatch
[3] Buffer overflow
[4] Sensor not found

Why Engineers Love This Pattern

  • Each string takes only the space it needs, with no padding
  • Error codes map straight to messages: errors[status]
  • Adding a message is a one-line change
  • Sorting is cheap, because you swap pointers instead of copying characters
  • You’ve already used one: char *argv[] in main is an array of pointers to strings, ending with a NULL pointer

Flash vs RAM: The Detail Most Guides Skip

On a microcontroller, where the table lives matters as much as how it looks. This is what typically happens on a Cortex-M build with file-scope or static declarations:

DeclarationPlacement
const char *msg[] = {...};Text in flash, pointer table in RAM (the pointers are writable)
const char *const msg[] = {...};Text and pointer table both in flash, zero RAM
char msg[5][18] = {...};One 90-byte block in RAM, plus a flash copy used to initialize it

Exact section names depend on your linker script, so check your .map file to confirm.

Heads-up for Arduino and AVR users: on 8-bit AVR chips, string literals are copied into SRAM at startup by default. To keep text in flash, use PROGMEM (and the F() macro for Serial.print), then read it back with the pgm_read_* helpers instead of normal dereferencing.

In short: write lookup tables as static const and use const char *const for text, so the compiler can keep them in flash.

Array of Pointers vs Pointer to an Array

These two declarations look almost identical, yet they mean very different things:

int *p[5];     /* array of 5 pointers to int */

int (*q)[5];   /* ONE pointer to an array of 5 ints */
 Array of pointersPointer to an array
Declarationint *p[5];int (*p)[5];
Meaning5 separate pointers to int1 pointer to a block of 5 ints
sizeof(p)5 × pointer size1 × pointer size
Element access*p[i](*p)[i]
Typical useString tables, jagged arrays, dispatch tablesPassing rows of a 2D array to functions

A Pointer to an Array in Action

int matrix[2][3] = { {1, 2, 3}, {4, 5, 6} };

int (*row_ptr)[3] = matrix;        /* points to the first row (3 ints) */

printf("%d\n", row_ptr[1][2]);     /* prints 6 */

How to Remember It

  • Parentheses around *name mean the name is a pointer first
  • No parentheses means the name is an array first

Array of Pointers vs 2D Array: Which Should You Use?

Feature2D array: char s[5][18]Array of pointers: const char *s[5]
Row lengthFixed, padded to the longestEach string can have its own length
Memory layoutOne contiguous blockPointer table plus separate strings
Extra costWasted padding bytesOne pointer per element
Reorder or sortCopy whole rowsSwap pointers only
Edit the textAllowed (if not const)Not allowed for string literals
Access costDirectOne extra memory read

Real numbers for the five error messages above (longest string is 17 characters plus the terminator):

  • 2D array: 5 × 18 = 90 bytes ("OK" wastes 15 of its 18 bytes)
  • Array of pointers: 62 bytes of text + 5 pointers = 82 bytes on a 32-bit MCU, or 72 bytes on 8-bit AVR with 2-byte pointers
  • The savings grow as your string lengths become more uneven, and with const placement the whole thing can cost zero RAM

Choose an Array of Pointers When

  • The text is read-only (menus, messages, labels)
  • Lengths vary a lot
  • You need cheap sorting or reordering

Choose a 2D Char Array When

  • You need editable buffers with a known maximum length
  • You need contiguous memory (for DMA or memcpy, for example)

Array of Pointers to Arrays (Jagged Arrays in C)

A jagged array is a table where each row has a different length, with no wasted space:

#include 

int main(void)
{
    int row0[] = { 1, 2, 3 };
    int row1[] = { 4, 5 };
    int row2[] = { 6, 7, 8, 9 };

    int *jagged[]          = { row0, row1, row2 };
    const size_t lengths[] = { 3, 2, 4 };

    for (size_t i = 0; i < 3; i++) {
        for (size_t j = 0; j < lengths[i]; j++) {
            printf("%d ", jagged[i][j]);
        }
        printf("\n");
    }

    return 0;
}

Output:

1 2 3
4 5
6 7 8 9

Key Points

  • jagged[i][j] uses the same syntax as a 2D array
  • A pointer carries no length, so track it yourself with a lengths[] array or a sentinel value
  • sizeof jagged[i] returns the size of a pointer, not the row, which is a very common bug

Explore Courses - Learn More

Dynamic Array of Pointers with malloc()

When row sizes are only known at runtime, each slot can own a heap block:

#include 
#include 

#define ROWS 3
#define COLS 4

int main(void)
{
    int *table[ROWS];                          /* the array of pointers */

    for (int i = 0; i < ROWS; i++) {
        table[i] = malloc(COLS * sizeof *table[i]);

        if (table[i] == NULL) {
            for (int k = 0; k < i; k++) {      /* roll back earlier rows */
                free(table[k]);
            }
            return 1;
        }

        for (int j = 0; j < COLS; j++) {
            table[i][j] = i * 10 + j;
        }
    }

    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j < COLS; j++) {
            printf("%2d ", table[i][j]);
        }
        printf("\n");
    }

    for (int i = 0; i < ROWS; i++) {
        free(table[i]);
    }

    return 0;
}

Key Points

  • Always check malloc() for NULL, and free what you already allocated if a later call fails
  • Free every row, then the table itself if it was allocated too
  • If even the row count is a runtime value: int **table = malloc(rows * sizeof *table);
  • An int ** is not the same as int arr[2][3]. The memory layouts differ, so a true 2D array can’t be passed where int ** is expected

Embedded Reality Check

  • Heap fragmentation can cause allocation failures weeks into deployment
  • MISRA C:2012 (Rule 21.3) prohibits malloc() and free(), which is why safety-critical projects such as automotive firmware avoid them
  • Safer options: static arrays or fixed-size memory pools sized at compile time

Array of Function Pointers (Dispatch Tables)

Each element holds the address of a function, so you can pick an action by index instead of writing a long if-else chain.

void (*handlers[3])(void);

Read it the same way: handlers is an array of 3 pointers to functions that take no arguments and return nothing. A typedef makes it far easier on the eyes:

typedef void (*handler_t)(void);

handler_t handlers[3];

A Complete, Runnable Example

#include 
#include 

typedef void (*handler_t)(void);

static void led_on(void)      { printf("LED ON\n");      }
static void led_off(void)     { printf("LED OFF\n");     }
static void led_toggle(void)  { printf("LED TOGGLE\n"); }

static const handler_t handlers[] = { led_on, led_off, led_toggle };

#define HANDLER_COUNT (sizeof handlers / sizeof handlers[0])

static void dispatch(uint8_t cmd)
{
    if (cmd < HANDLER_COUNT) {
        handlers[cmd]();
    } else {
        printf("Invalid command: %u\n", (unsigned)cmd);
    }
}

int main(void)
{
    dispatch(0);   /* LED ON */
    dispatch(2);   /* LED TOGGLE */
    dispatch(9);   /* Invalid command: 9 */
    return 0;
}

On real hardware, replace printf with GPIO writes.

Why This Pattern Is So Powerful

  • Replaces long switch chains with a one-line lookup
  • Adding a command means adding one table entry
  • The table can be const and live in flash
  • It’s the backbone of state machines: state_table[current_state]();
  • The Cortex-M vector table is itself a table of addresses at the start of flash (entry 0 is the initial stack pointer, the rest are handler addresses), and many C startup files declare it as an array of function pointers in a dedicated section

Safety Rules

  • Always bounds-check the index. A bad index jumps to a garbage address and crashes the MCU
  • Use designated initializers with an enum so the table can’t silently drift out of order: [STATE_IDLE] = state_idle (a C99 feature, so older compilers such as Keil C51 may not support it)
  • Fill unused slots with a safe default handler

Real-World Embedded Use Cases

1. UART Command Parser

#include 

typedef struct {
    const char *name;    /* text typed by the user */
    handler_t   run;     /* function to call */
} command_t;

static const command_t commands[] = {
    { "on",     led_on     },
    { "off",    led_off    },
    { "toggle", led_toggle }
};

void run_command(const char *input)
{
    for (size_t i = 0; i < sizeof commands / sizeof commands[0]; i++) {
        if (strcmp(input, commands[i].name) == 0) {
            commands[i].run();
            return;
        }
    }

    /* unknown command: report an error over UART */
}

This is technically an array of structs that contain pointers, but it’s the natural next step once arrays of pointers feel comfortable.

2. Driving Several GPIO Ports from One Loop (STM32 HAL)

GPIO_TypeDef *const led_ports[] = { GPIOA, GPIOB, GPIOC };
const uint16_t      led_pins[]  = { GPIO_PIN_5, GPIO_PIN_0, GPIO_PIN_13 };

for (size_t i = 0; i < sizeof led_ports / sizeof led_ports[0]; i++) {
    HAL_GPIO_TogglePin(led_ports[i], led_pins[i]);
}

Adjust the pins for your board, and keep the two arrays the same length.

More Places You’ll See Them

  • LCD/OLED menus: one array of labels, a parallel array of handlers
  • Error and status tables: errors[status] instead of a switch
  • Sensor driver tables: an array of pointers to structs, each holding init() and read() function pointers (C-style polymorphism)
  • Callback registries: store ISR or event callbacks and call them in a loop
  • Sorting names: swap two pointers instead of copying whole strings

Common Mistakes and How to Avoid Them

MistakeWhat Goes WrongFix
Dereferencing an uninitialized elementWrites to a random address: corruption or HardFaultInitialize every slot to a valid address or NULL, and check before use
Storing addresses of local variables beyond their scopeDangling pointers and garbage valuesUse static or global storage, or let the caller own the buffer
Editing a string literal through const char *Undefined behavior: may fault, or silently do nothingCopy into a char buf[N] if you need to change text
Using sizeof on a row accessed through a pointerReturns pointer size, not row lengthKeep a lengths array or pass the length explicitly
Using sizeof arr inside a function that received the arrayThe array decayed to a pointer, so the result is wrongPass the element count as a parameter
Calling table[i]() without a bounds checkJumps to a garbage addressValidate i before the call
Not freeing every malloc’d rowMemory leaks and fragmentationFree each row, then the table, or avoid the heap entirely
Mixing up int *p[3] and int (*p)[3]Wrong type, warnings, or silent bugsRead declarations right to left and use typedefs

Best Practices for Arrays of Pointers in Embedded C

  • Declare lookup tables static const so they can live in flash
  • Use const char *const for text tables
  • typedef your function pointer types
  • Compute counts with sizeof arr / sizeof arr[0], and never hard-code them
  • Use a NULL terminator as a sentinel when you don’t want a length array: { "a", "b", NULL }
  • Mark pointers to memory-mapped registers volatile
  • Document who owns and frees each allocation
  • Compile with -Wall -Wextra and run static analysis to catch pointer problems early

Quick Reference Cheat Sheet

GoalDeclaration
Array of 5 int pointersint *p[5];
Array of 5 string pointersconst char *s[5];
Read-only string table (flash)static const char *const s[] = {...};
Pointer to an array of 5 intsint (*p)[5];
Array of 4 function pointersvoid (*f[4])(void);
Array of pointers to structsstruct sensor *list[8];
Pointer to pointer (dynamic table)int **table;

Try It Yourself

  1. Build a menu table of 6 labels, print them with their index, then sort them alphabetically by swapping pointers
  2. Create a jagged array of sensor readings with row lengths 3, 5, and 2, and print each row’s average
  3. Write a command table with 4 UART-style commands and a safe fallback for unknown input

Talk to Academic Advisor

Conclusion: Key Takeaways

  • An array of pointers stores addresses, not data: int *p[5] is five pointers to int
  • [] binds before *, and parentheses change the meaning completely
  • const char *const string tables keep both text and pointers in flash
  • Jagged arrays need their own length tracking
  • Arrays of function pointers give you clean dispatch tables and state machines
  • In firmware: initialize every slot, bounds-check every index, and prefer static allocation

FAQs

 int *p[5] is five pointers to int. int (*p)[5] is a single pointer to a block of five ints. The parentheses decide which one is the main thing.

Each string uses only the space it needs, while a 2D array pads every row to the longest string. With const, the table and text can also stay in flash and save RAM. A 2D array is better only when you need editable, fixed-size buffers.

Use sizeof arr / sizeof arr[0]. This works only in the scope where the array is declared. Once the array is passed to a function it decays to a pointer, so pass the count separately.

Not directly, because all elements share one type. You can use void * elements or a struct with a type tag, but you must cast correctly and track the type yourself (see our guide to void pointers in C).

It builds dispatch tables: command handlers, state machines, menu actions, and callback registries. You pick the action by index instead of a long switch.

Author

Embedded Systems trainer – IIES

Updated On: 07-10-26


10+ years of hands-on experience delivering practical training in Embedded Systems and it's design