meta_title: Array of Pointers in C Practical Memory Guide Now meta_description: Learn how an array of pointers in C works, why it saves memory, and where it fits in real system software with examples, comparisons, and debugging tips reading_time: 7 min read
You're probably dealing with lists that don't fit neatly into a fixed box. Command-line arguments, config directives, log labels, handler tables, and variable-length strings all show up in systems work. A plain array can hold data, but it can't always hold it efficiently. That's where an array of pointers in C becomes a practical memory management tool instead of just another syntax trick.
Stop paying for idle resources. Server Scheduler automatically turns off your non-production servers when you're not using them.
An array of pointers in C is an array where each element stores an address instead of the final value. If you have several strings, for example, each array slot can point to a different string in memory. That's why this pattern shows up so often in command-line tools, parsers, and other low-level software.
The model is old and proven. Foundational C material traces this style of string handling back to the original 1978 C implementation by Dennis Ritchie at Bell Labs, where arrays of pointers were used to store multiple strings efficiently through separate references rather than one rigid block of character storage (reference video).
A lot of confusion starts with declarations. In practice, you read the variable name first, then the operators around it.
Practical rule: if each slot needs to point somewhere else, you probably want an array of pointers, not a flat array.
For DevOps engineers who touch C in agents, extensions, or performance-sensitive tooling, this matters for the same reason memory layouts matter in infrastructure design. You separate a compact directory from the objects it references. That same idea shows up in service registries, index tables, and queue metadata. If you want a broader systems perspective, this pairs nicely with enterprise application development patterns.
| Declaration | Meaning |
|---|---|
char *names[4]; |
Array of four pointers to char |
int *p[4]; |
Array of four pointers to int |
char names[4][20]; |
Four fixed-size character arrays |
Memory layout is the whole point. The pointer array itself is contiguous, but the data each pointer references can live in different places. This is similar to a book's table of contents. The contents page is one ordered block, but each entry leads you to a chapter somewhere else.

That's why this structure works so well for variable-length data. One string might be five characters long, another fifty. You don't have to reserve the same width for every row. Verified material notes that storing 100 strings with an average length of 20 characters in an array of pointers uses only 400 bytes for the pointer array on a system with 4-byte pointers, plus the actual string storage, instead of forcing all strings into a fixed rectangular layout (reference video).
In C, array indexing is pointer arithmetic. The C standard explicitly defines a[i] as equivalent to *(a + i), so indexing walks to the correct slot and dereferences it (C indexing reference). For arrays of pointers, that means the compiler first finds the i-th pointer, then follows that address to the target data.
The practical outcome is simple. The pointer list stays compact and sequential, while the underlying strings or structs can be allocated independently. If you work with shell-style argument processing, bash command patterns make more sense once you visualize memory this way.
A short visual helps:
The array is contiguous. The data it points to doesn't have to be.
The most familiar use case is an array of strings. Each element points to the first character of a different string, so you can manage labels, error messages, commands, or arguments without forcing them into equal-length rows.
#include <stdio.h>
int main(void) {
char *errors[] = {
"File not found",
"Access denied",
"Disk full"
};
printf("%s\n", errors[1]);
return 0;
}

That code is small, but the design choice is big. You're moving pointers around, not the underlying text. Verified material also notes that when sorting strings, swapping pointers instead of string contents can produce a 4x speedup because the program changes pointer addresses rather than copying much larger character data (discussion reference).
The more systems-oriented pattern is a function pointer table. Instead of a long switch, you index directly into an array of handlers.
#include <stdio.h>
void handle_start(void) { puts("start"); }
void handle_stop(void) { puts("stop"); }
int main(void) {
void (*handlers[])(void) = { handle_start, handle_stop };
int msg_type = 1;
handlers[msg_type]();
return 0;
}
This style matters in message loops, schedulers, and protocol handling. Arrays of function pointers can reduce CPU cycles per message by 30-40% compared to switch-case logic in embedded systems (Stack Overflow discussion). That's why dispatch tables show up in code that cares about predictable response times.
Why engineers like it: direct indexed lookup removes branch-heavy control flow.
If you work in repos that mix infrastructure logic with native components, it's worth treating handler tables as first-class design tools, just like you'd treat common Git commands as part of your operational workflow.
Most bugs here come from thinking these forms are interchangeable. They aren't. They can look similar, but they describe different memory shapes.
An array of pointers such as int *p[10]; is ten pointer variables stored next to each other. A 2D array such as int p[10][5]; is one contiguous block containing fifty integers. A pointer to an array such as int (*p)[5]; is one pointer that refers to an entire row-shaped block.

The syntax trap is operator precedence. Parentheses change the meaning. Enclosing the array name in (*array_name) makes it a pointer to an array, not an array of pointers, and that can break program logic if you misread the declaration (video explanation).
| Form | What it stores | Best fit |
|---|---|---|
char *rows[10] |
10 pointers | Variable-length strings, jagged data |
char rows[10][20] |
Fixed character matrix | Uniform-width rows |
char (*rows)[20] |
One pointer to an array row | Passing row-based blocks |
A useful rule is this: choose an array of pointers when each row may differ in size. Choose a 2D array when you need strict contiguity and fixed dimensions. That distinction also becomes clearer when reviewing pointer-heavy diffs in C code, especially with Git branch comparisons.
Pointers don't forgive sloppy habits. If an array slot hasn't been initialized, dereferencing it is undefined behavior. If a slot may be empty, set it to NULL and check before use.
For arrays of pointers to structures, allocate each element before writing through it. Verified guidance shows the standard pattern: for (int i = 0; i < 20; i++) { array2[i] = malloc(sizeof(TestType)); }, which allocates 20 structure instances for the array before access (structure allocation example). Free each allocated element later, or leaks will pile up in long-running processes.
Check every pointer before dereferencing it, and match every allocation path with a cleanup path.
That discipline matters most when you're debugging process crashes, memory growth, or stale state in daemon-style programs. A lot of the same operational caution applies when you're cleaning up terminal sessions and processes in Linux, which is why exiting screen sessions safely is the same kind of habit-building exercise.
Server Scheduler helps teams automate cloud operations like start, stop, resize, and reboot scheduling without scripts or cron sprawl. If you want a simpler way to control non-production uptime and reduce infrastructure waste, take a look at Server Scheduler.