The most useful way to read C declarations is from the variable name outward. This is sometimes called the "spiral rule" or "clockwise/spiral rule."
For char *, suppose you have:
char *p;
Start at p:
p* → "p is a pointer to..."char → "...a char."So you read it as: p is a pointer to a char.
Not "a pointer to a string."
Here are some increasingly complex examples.
char cchar c;
Read as: c is a char.
char *pchar *p;
Read as: p is a pointer to a char.
Memory:
p
|
v
+---+
| A |
+---+
char **ppchar **pp;
Read as: pp is a pointer to a pointer to a char.
Memory:
pp
|
v
+-----+
| p |
+--|--+
|
v
+---+
| A |
+---+
char *words[3]char *words[3];
Start at words:
words[3] → "is an array of 3..."* → "...pointers to..."char → "...chars."So: words is an array of 3 pointers to char.
This is not a pointer. It is an array.
char (*p)[3]Parentheses change everything.
char (*p)[3];
Start at p:
p* (inside the parentheses) → "is a pointer to..."[3] → "...an array of 3..."char → "...chars."So: p is a pointer to an array of 3 chars.
Without the parentheses, it would mean something completely different.
char *(*f)(void)char *(*f)(void);
Start at f:
f* → "is a pointer to..."(void) → "...a function taking no arguments..."char * → "...returning a pointer to char."So: f is a pointer to a function that returns a char *.
char * with stringsA C string is just a sequence of chars ending with '\0'.
char hello[] = "hello";
When used in an expression, hello decays to a char * pointing to the first character.
So this works:
char *p = hello;
printf("%s\n", p);
Because the pointer happens to point to the first character of a null-terminated sequence.
But a char * can also point to:
char c = 'A';
char *p = &c;
Here p is not pointing to a string—it points to a single character.
So the type char * means only: pointer to char.
Whether that pointer represents a C string depends entirely on what it points to.
The important distinction is that char ** says nothing about strings. It simply means "pointer to char *."
Let's build it up one level at a time.
char c = 'A';
c is a char.
char *p = &c;
p is a pointer to a char.
char **pp = &p;
pp is a pointer to a char *.
So the types look like this:
char
^
|
char *
^
|
char **
or as a memory diagram:
pp
|
v
+-------+
| p |--------------------+
+-------+ |
v
+-------+
| 'A' |
+-------+
A C string is simply an array of char terminated by '\0'.
For example,
char hello[] = "hello";
creates
+---+---+---+---+---+----+
| h | e | l | l | o | \0 |
+---+---+---+---+---+----+
The array hello decays to a char * when passed to a function.
Now consider
char *words[] = {
"echo",
"foxtrot"
};
The array looks something like
words
+-----+-----+
| * | * |
+--|--+--|--+
| |
| +------------> "foxtrot"
|
+------------------> "echo"
words has type char *[2] (an array of two char *).
When passed to a function,
print_pointer_array(words, 2);
the array decays to char ** because:
char *[2]
|
| array-to-pointer decay
v
char **
So inside the function,
void print_pointer_array(char **arr, size_t length)
arr points to the first element of the array, and each element is a char *.
arr
|
v
+-----+-----+
| * | * |
+--|--+--|--+
| |
| +------------> "foxtrot"
|
+------------------> "echo"
Then
arr is a char **arr[0] is a char *arr[1] is a char *arr[0][0] is a char ('e')arr[0][1] is a char ('c')A char ** is not inherently "a pointer to an array of strings." It is simply a pointer to a char *.
It can point to:
char * (as in your example),char * variable,char * values,The fact that those char * values happen to point to C strings is a property of how you're using them, not of the type itself.
The short answer is that char foo[][] is actually invalid syntax in C/C++, while char *foo[] is a valid array of pointers (commonly used to hold an array of strings).
Assuming you meant to compare a valid 2D array like char foo[][N] with char *foo[], here is the fundamental difference:
char foo[][N] is a true 2D array. It is a single, contiguous block of memory.char *foo[] is an array of pointers. It is an array where each element is a pointer that points to a completely different location in memory.char foo[][]In C and C++, you cannot declare char foo[][];. The compiler will throw an error.
When you use an array, the compiler needs to know the size of the "inner" dimensions to calculate memory offsets. If you write char foo[][10], the compiler knows that every time you move to the next "row" (foo[1], foo[2]), it needs to jump exactly 10 bytes forward in memory. If both dimensions are empty [][], the compiler has no idea how to do the math.
(Note: The only time you can omit the first dimension is in a function parameter, but you still must provide the second dimension: void myFunc(char foo[][10])).
char foo[][N] (True 2D Array)Let's look at a valid version: char foo[3][5] = {"apple", "bat", "cat"};
char (*)[5]).foo[0][0] = 'A'; to change "apple" to "Apple".char foo[][6] = {"hello", "world"}; // Creates a 2x6 grid of chars
foo[0][0] = 'H'; // PERFECTLY SAFE. Modifies the actual memory.
char *foo[] (Array of Pointers)Example: char *foo[] = {"hello", "world"};
char **).char *foo[] = {"hello", "world"}; // Creates an array of 2 pointers
foo[0][0] = 'H'; // CRASH! Segmentation fault. You are trying to modify read-only memory.
Note: If you want to modify the strings in an array of pointers, you must point them to modifiable memory, like this:
char str1[] = "hello";
char str2[] = "world";
char *foo[] = {str1, str2};
foo[0][0] = 'H'; // SAFE, because str1 is a modifiable local array.
| Feature | char foo[][N] (2D Array) |
char *foo[] (Array of Pointers) |
|---|---|---|
| Concept | A grid of characters. | A list of addresses pointing to characters. |
| Memory | One single, contiguous block. | Scattered; pointers point to different places. |
| Decays to | char (*)[N] (Pointer to an array) |
char ** (Pointer to a pointer) |
| String Literals | Copies the characters into the array. | Points directly to the read-only string literals. |
| Modifying chars | foo[0][0] = 'x'; is Safe. |
foo[0][0] = 'x'; causes a Crash (usually). |
| Memory Efficiency | Wastes space if strings are shorter than N. | Highly efficient for varying string lengths. |
char *foo[] when you have an array of strings of varying lengths (like command-line arguments argv), or when you just want to store read-only string literals efficiently.char foo[][N] when you are building a grid/matrix of characters (like a game board, or an image buffer), or when you need to heavily modify the individual characters of fixed-width strings.To understand pointer decay (officially called array-to-pointer decay), you need to understand one fundamental rule in C and C++:
The Golden Rule of Decay: Whenever you use an array's name in an expression (like passing it to a function), it automatically "decays" into a pointer to its first element.
It is called "decay" because the array loses information. Specifically, it loses its size (the number of elements it contains) and its strict array type, degrading into a simple, raw memory address.
To figure out what an array decays into, just ask: "What is the first element of this array?" The array decays into a pointer to that specific element.
A. Standard 1D Array
int arr[10];
What is the first element? An int.
What does it decay into? A pointer to an int (int *).
B. True 2D Array (char foo[][N])
char grid[3][5];
What is the first element? The first "row", which is an array of 5 chars (char[5]).
What does it decay into? A pointer to an array of 5 chars: char (*)[5].
(Notice it does NOT become char **. It becomes a pointer to a contiguous block of 5 chars).
C. Array of Pointers (char *foo[])
char *ptrs[3];
What is the first element? A pointer to a char (char *).
What does it decay into? A pointer to a pointer to a char: char **.
This is the exact reason why a 2D array and an array of pointers are not interchangeable in function arguments. They decay into completely different pointer types!
Imagine you have a physical box containing exactly 10 apples.
In C, the "GPS coordinates" is the pointer. The "number of apples" is the array size. When decay happens, the size is thrown away.
It comes down to efficiency and historical design. If you pass a 10-megabyte array to a function, copying that entire array into the function's local memory would be incredibly slow and waste memory. By automatically decaying the array into an 8-byte pointer (on a 64-bit system), C ensures that passing arrays is lightning fast. It passes the address of the original array, not a copy of the data.
Because the array loses its size information, it causes two major headaches for programmers:
Gotcha A: The sizeof Trap
void printSize(int arr[]) {
// You might expect this to be 40 (10 ints * 4 bytes)
// But because 'arr' decayed into an 'int *', it returns the size of a POINTER!
printf("%zu\n", sizeof(arr)); // Prints 8 (on a 64-bit system)
}
int main() {
int my_arr[10];
printf("%zu\n", sizeof(my_arr)); // Prints 40. Inside main, it hasn't decayed yet.
printSize(my_arr);
}
Gotcha B: You can't assign arrays to arrays
int a[5] = {1, 2, 3, 4, 5};
int b[5];
b = a; // COMPILER ERROR!
Why? Because b decays into a pointer to its first element (int *). In C, you cannot assign a new memory address to an array name. The array name is essentially a constant pointer. To copy arrays, you must use memcpy() or a for loop.
Because decay strips away the size of the array, you have to handle it manually depending on your language.
The C Way: Pass the size explicitly
Since the function doesn't know the array size, you must pass it as a separate argument.
// Good C practice
void processArray(char *data, size_t length) {
for(size_t i = 0; i < length; i++) {
// do something
}
}
(Note: For strings, C uses a null-terminator \0 instead of a size parameter, which is why strlen() has to count characters one by one until it hits the zero!)
The C++ Way: Prevent the decay
C++ allows you to pass arrays by reference, which stops the decay and preserves the size information.
// The '&' prevents decay. The compiler will enforce that exactly 10 ints are passed.
void processArray(int (&arr)[10]) {
// sizeof(arr) works perfectly here! It will be 40.
}
Modern C++ alternative: Just use std::array or std::vector. They are objects that do not decay into raw pointers and always know their own size.
char[3][5]) decays to a pointer to a row (char (*)[5]).char *[3]) decays to a pointer to a pointer (char **).char *data, size_t lengthTo call this function, you need to provide two things:
data).length).What is the length parameter?
Because of pointer decay (which we discussed earlier), when you pass an array to char *data, the function receives a raw memory address but loses all information about how big the array is.
The length parameter is you, the programmer, manually telling the function: "Here is the memory address, and you are allowed to read exactly length number of characters starting from that address."
size_t is simply a special unsigned integer type in C/C++ guaranteed to be large enough to hold the size of any object in memory. You can think of it as the standard type for "counts" and "sizes".
How you calculate and pass the length depends entirely on how you created the array. Here are the three most common ways to call it:
Scenario 1: You have a standard C-string (Null-terminated)
If your array is a standard string that ends with a hidden \0 (null terminator), you use the strlen() function to count the characters up to that null terminator.
#include <string.h>
void processArray(char *data, size_t length);
int main() {
char my_string[] = "Hello"; // Actually 6 bytes: 'H','e','l','l','o','\0'
// strlen counts characters until it hits '\0'. It returns 5.
processArray(my_string, strlen(my_string));
return 0;
}
Scenario 2: You have a raw buffer (Not a string, or contains nulls)
If your array is just a block of memory (like a file buffer, or a grid of characters) and doesn't necessarily end with a \0, strlen() will fail or crash. Instead, you use the sizeof operator to get the total allocated size.
void processArray(char *data, size_t length);
int main() {
char buffer[100]; // A raw block of 100 characters
// sizeof returns the total number of bytes allocated.
// Since it's a char array, 1 byte = 1 char, so length is 100.
processArray(buffer, sizeof(buffer));
return 0;
}
Note: sizeof only works if the array is declared in the same scope. If you pass buffer into another function first, it decays into a pointer, and sizeof will just return 8 (the size of the pointer).
Scenario 3: You allocated memory dynamically
If you created your array using malloc or new, the compiler has no idea how big it is, and there is no null terminator. You must track the size yourself.
#include <stdlib.h>
void processArray(char *data, size_t length);
int main() {
size_t my_size = 50;
// Allocate 50 bytes of memory
char *dynamic_data = (char *)malloc(my_size);
// You MUST pass the size you used to allocate it
processArray(dynamic_data, my_size);
free(dynamic_data);
return 0;
}
⚠️ The Golden Rule / Common Trap
The biggest mistake beginners make is confusingstrlen()andsizeof().
Look at this array:char text[] = "Cat";
strlen(text)returns 3 (It counts 'C', 'a', 't' and stops at\0).
sizeof(text)returns 4 (It counts 'C', 'a', 't', and the\0byte).
Which one do you pass tolength?
IfprocessArrayis going to print the text, copy the text, or do string manipulation, passstrlen(text)(3). You usually don't want to process the null terminator.
IfprocessArrayis going to write the exact raw bytes to a file, or encrypt the memory block, passsizeof(text)(4). You want to include the null terminator in the raw data.
char *users[]Here is how you dynamically allocate memory for char *users[] and how you pass both array types to functions.
mallocTo safely modify strings when using an array of pointers, you must allocate heap memory for both the array of pointers and each individual string.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
int total_users = 100;
int max_string_len = 11; // 10 chars + '\0'
// Step 1: Allocate memory for 100 pointers
char **users = malloc(total_users * sizeof(char *));
// Step 2: Allocate memory for each individual string
for (int i = 0; i < total_users; i++) {
users[i] = malloc(max_string_len * sizeof(char));
}
// Step 3: Safely modify and write data
strcpy(users[0], "Alice");
strcpy(users[1], "Bob");
printf("User 1: %s\n", users[0]);
// Step 4: Free memory in reverse order of allocation
for (int i = 0; i < total_users; i++) {
free(users[i]);
}
free(users);
return 0;
}
Because their memory layouts differ, functions must declare their parameters differently to receive them correctly.
Option A: Passing the 2D Array (char users[100][11])
When passing a 2D array, the compiler must know the size of the second dimension (the column width) so it can calculate memory offsets.
#include <stdio.h>
// You must specify the column size (11) in the parameter
void print_fixed_array(char arr[][11], int rows) {
for (int i = 0; i < rows; i++) {
if (arr[i][0] != '\0') { // Check if not empty
printf("Fixed User %d: %s\n", i, arr[i]);
}
}
}
int main() {
char fixed_users[100][11] = {"Charlie", "Delta"};
print_fixed_array(fixed_users, 2);
return 0;
}
Option B: Passing the Array of Pointers (char *users[] or char **users)
Since this is an array of pointers, the function treats it as a pointer-to-a-pointer (char **). The compiler does not need to know a fixed inner string size.
#include <stdio.h>
// char *arr[] and char **arr are identical in function parameters
void print_pointer_array(char **arr, int count) {
for (int i = 0; i < count; i++) {
printf("Pointer User %d: %s\n", i, arr[i]);
}
}
int main() {
// Array of pointers to literal strings (read-only data)
char *ptr_users[] = {"Echo", "Foxtrot"};
print_pointer_array(ptr_users, 2);
return 0;
}
char **) ExplainedThe two asterisks (**) indicate a pointer to a pointer. [1]
In C, a single asterisk creates a pointer that holds the memory address of a normal variable (like a char or an int). A double asterisk creates a pointer that holds the memory address of another pointer. [2, 3, 4]
malloc statement?1. Breaking Down the Right Side First
Look at what you are passing inside malloc:
malloc(total_users * sizeof(char *));
sizeof(char *) means "give me the size of a single memory pointer" (usually 8 bytes on modern computers). Multiplying it by total_users (100) means malloc is reserving a block of memory specifically designed to hold 100 pointers. [5]
2. Matching the Left Side Type
Because malloc is giving you the starting address of a block filled with char * variables, the variable receiving this address must match that exact type structure:
char → A letter.char * → A pointer to a letter (a string).char ** → A pointer to a pointer to a letter (an array of strings). [6]3. Visualizing the Pointer Chain
Think of it as a treasure map leading to another treasure map:
[ users ]
│
▼ (Points to the start of the pointer array)
[ Slot 0 ] ───► [ 'A' ][ 'l' ][ 'i' ][ 'c' ][ 'e' ][\0] (Heap String 1)
[ Slot 1 ] ───► [ 'B' ][ 'o' ][ 'b' ][\0] (Heap String 2)
[ Slot 2 ] ───► [ 'C' ][ 'a' ][ 't' ][\0] (Heap String 3)
users is a variable. It holds the address of Slot 0. Because it points to a pointer, its type must be char **.
users[0] (or Slot 0) holds the address of the letter 'A'. Because it points to a character, its type is char *.
*users[0] is the actual character 'A'. Its type is char. [7]
char **Pointer arithmetic with a char ** moves through memory based on the size of a pointer, not the size of a character string. [1, 2, 3]
On modern 64-bit systems, a pointer occupies 8 bytes of memory. Therefore, adding 1 to a char ** advances the memory address by exactly 8 bytes to target the next pointer slot. [4, 5, 6]
char ** ArithmeticAssume you have a dynamically allocated array of 3 string pointers, and the array starts at memory address 0x1000. [7, 8]
char **ptr = users; // Points to the first string pointer
| Expression | Memory Address | What It Points To | What it Evaluates To (*) |
|---|---|---|---|
ptr |
0x1000 | The 1st string pointer | Address of the string "Alice" |
ptr + 1 |
0x1008 (+8 bytes) | The 2nd string pointer | Address of the string "Bob" |
ptr + 2 |
0x1010 (+8 bytes) | The 3rd string pointer | Address of the string "Charlie" |
This code demonstrates how adding 1 behaves differently depending on whether you dereference the double pointer first.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
// Setup a 3-element pointer array
char **users = malloc(3 * sizeof(char *));
users[0] = strdup("Alice"); // Allocates and copies
users[1] = strdup("Bob");
users[2] = strdup("Charlie");
// Create a tracker pointer
char **ptr = users;
// 1. Moving between STRINGS (Pointer Arithmetic)
printf("Current string: %s\n", *ptr); // Output: Alice
ptr++; // Advances by 8 bytes to the next pointer slot
printf("Next string: %s\n", *ptr); // Output: Bob
// 2. Moving inside a CHARACTER string (Character Arithmetic)
// *ptr gets the 'char*' address for "Bob".
// Adding 1 to that advances it by 1 byte to the next letter.
printf("Second letter of Bob: %s\n", *ptr + 1); // Output: ob
// Clean up
for(int i = 0; i < 3; i++) free(users[i]);
free(users);
return 0;
}
Crucial Rule: The Difference in Array Types
Pointer arithmetic is the exact reason whychar **and a 2D arraychar users[100][11]are completely incompatible:
char **ptr:ptr + 1jumps 8 bytes (size of a pointer) to find the next memory address tracker.
char users[100][11]:users + 1jumps 11 bytes (size of an entire row) because the compiler knows the full grid layout.
If you try to assign a 2D grid array to achar **, the pointer math breaks completely and your program will crash.