Pointers, Arrays & Strings

Arrays and pointers are so tightly related in C that understanding exactly where they're the same and where they differ resolves most of the confusion beginners have with both.

Pointers Fundamentals covered a single variable and a single address. Arrays put several values of the same type side by side in memory, and it turns out one pointer can walk across the whole row by moving a fixed distance at a time. That one idea — a block of same-type values plus a pointer that can slide along it — is the key to most of what looks mysterious in C: why arr[i] and *(arr+i) are the same thing, why sizeof behaves differently inside a function than outside it, and why a string is nothing more than a char array with a marker at the end.

Array Basics

An array reserves one contiguous block of memory big enough to hold several values of the same type, and each value is reached with a numeric position called an index, counting from 0. Declaring int numbers[5] = {10, 20, 30, 40, 50}; sets aside room for five ints in a row, so numbers[0] holds 10 and numbers[4] holds 50.

Each value sits in its own reserved slot, and the number in brackets simply says how many slots past the start to look.

#include <stdio.h>

int main(void) {
    int numbers[5] = {10, 20, 30, 40, 50};
    printf("%d %d %d\n", numbers[0], numbers[2], numbers[4]);
    return 0;
}

Array Name and First Element

Use an array's bare name inside most expressions, and the compiler silently rewrites it as a pointer to the array's first element. This substitution is called array-to-pointer conversion, or decay for short, and it happens automatically with no special syntax needed to trigger it.

numbers               (an array of 5 ints)
  |
  |  decays to, in most expressions
  v
&numbers[0]           (a pointer to the first int)

An Array Is Not a Pointer

int numbers[5]; and int *p; describe two genuinely different kinds of object — one is storage for five ints, the other is a variable that only holds an address. The line int *p = numbers; compiles purely because of the decay conversion just described: it behaves exactly as if you had written int *p = &numbers[0];, so p ends up holding the address of the first element rather than becoming a copy of the array. numbers and p stay separate objects throughout the program — the array itself can never be pointed somewhere else, while p is free to be reassigned at any time.

Hold onto this distinction — later in this article it comes back as one of the most common mixups beginners make with arrays and pointers.

int numbers[5] = {10, 20, 30, 40, 50};
int *p = numbers;   /* really: int *p = &numbers[0]; */

Pointer Dereferencing an Array Element

Once p has picked up the array's starting address, *p reads the same value numbers[0] would. Shifting the offset before dereferencing reaches further elements: *(p+1) reads the second element, *(p+2) the third, and so on.

int numbers[5] = {10, 20, 30, 40, 50};
int *p = numbers;
printf("%d %d %d\n", *p, *(p + 1), *(p + 2)); /* 10 20 30 */

Array Indexing and Pointer Arithmetic

This is the single rule everything else in this article builds on: numbers[i] is defined by the language to mean exactly *(numbers + i). Square-bracket indexing isn't a separate array feature running alongside pointer arithmetic — it's the same operation with friendlier spelling.

The pointer here always moves by whole elements, never by raw bytes — the next section explains why that distinction matters.

numbers[0]  ==  *(numbers + 0)
numbers[1]  ==  *(numbers + 1)
numbers[2]  ==  *(numbers + 2)

Pointer Arithmetic

Adding a number to a pointer does not add that many bytes to its address — it advances by that many whole elements of whatever type the pointer points to. With int *p = numbers;, p+1 points at the very next int and p+2 at the int after that. C guarantees this element-based behavior without committing to any fixed byte size for int, so the exact number of bytes skipped depends on how large int happens to be on a given implementation.

int numbers[5] = {10, 20, 30, 40, 50};
int *p = numbers;
int *q = p + 2;      /* q now points at numbers[2] */

p++ with an Array

A pointer variable can also be advanced with the ++ operator, moving it forward by one element each time it's applied, rather than recomputing an offset from scratch.

p++ moves the pointer itself to the next element; the array's contents never change, only where p currently points.

int numbers[5] = {10, 20, 30, 40, 50};
int *p = numbers;
printf("%d\n", *p);  /* 10 */
p++;
printf("%d\n", *p);  /* 20 */

Traversing an Array Using a Pointer

There are three equally valid ways to visit every element of an array in order: plain indexing, computing an offset from a base pointer each time, or repeatedly incrementing a pointer that walks forward on its own.

All three loops print the identical five numbers in the identical order — they differ only in how the current position is tracked, not in what they accomplish.

int numbers[5] = {10, 20, 30, 40, 50};

/* style 1: indexing */
for (int i = 0; i < 5; i++) {
    printf("%d \n", numbers[i]);
}

/* style 2: pointer + offset */
int *p = numbers;
for (int i = 0; i < 5; i++) {
    printf("%d \n", *(p + i));
}

/* style 3: pointer increment */
int *q = numbers;
for (int i = 0; i < 5; i++) {
    printf("%d \n", *q);
    q++;
}

Pointer Arithmetic Does Not Mean Raw Byte Arithmetic

The scaling behavior from pointer arithmetic holds for any element type, not just int. With double *p = values;, p+1 lands on the very next double — not on a location merely one byte further along — because the compiler always scales the arithmetic to the size of whatever type p points to.

double values[4] = {1.5, 2.5, 3.5, 4.5};
double *p = values;
printf("%.1f\n", *(p + 1)); /* 2.5, the next double, not one byte over */

Pointer and Array Modification

Because p and numbers[0] name the same memory, writing through the pointer changes the array itself, not some separate copy.

int numbers[5] = {10, 20, 30, 40, 50};
int *p = numbers;
*p = 100;         /* numbers[0] is now 100 */
*(p + 1) = 200;   /* numbers[1] is now 200 */

Address of a Single Element

The & operator can target one specific element directly, without going through the array's name at all, producing a pointer straight to that slot.

int numbers[5] = {10, 20, 30, 40, 50};
int *p = &numbers[2];
printf("%d\n", *p); /* 30 */

numbers and &numbers[0]

Because numbers decays to &numbers[0] in most expression contexts, int *p = numbers; and int *p = &numbers[0]; end up doing the same thing and produce the same pointer value. That equivalence is a consequence of decay happening in that particular context — it doesn't mean the array object itself has secretly turned into a pointer; the array is still the array, only its use in that expression got converted.

The sizeof Exception

Decay does not happen everywhere — sizeof applied directly to an array name is the first major exception. sizeof(numbers) reports the size of the entire array in bytes, not the size of a pointer, which is exactly why sizeof(numbers) / sizeof(numbers[0]) is the standard trick for computing how many elements an array holds.

int numbers[5] = {10, 20, 30, 40, 50};
size_t count = sizeof(numbers) / sizeof(numbers[0]); /* 5 */

The & Exception

Applying & to the array's name is the second exception: &numbers means "the address of the whole array object", with type int (*)[5] — pointer to an array of 5 ints — which is a different type from &numbers[0] (type int *, pointer to a single int). The two may report the same numeric address, but the type attached to each pointer changes how arithmetic on it behaves.

&numbers      -> type int (*)[5]   (address of the whole 5-int array)
&numbers[0]   -> type int *        (address of just the first int)

Array of Pointers vs Pointer to Array

Parentheses placement is the only thing separating two very different declarations. int (*p)[5]; declares a single pointer to an array of 5 ints. int *p[5]; declares an array of 5 separate pointers, each one capable of pointing at an int.

int (*p)[5]; /* pointer to an array of 5 ints */
int *p[5];   /* array of 5 pointers to int   */

Passing Arrays to Functions

Inside a function's parameter list, writing int arr[] is silently adjusted by the compiler to mean int *arr — the two spellings describe exactly the same parameter type. Whichever way it's written, the function receives a pointer to the array's first element, not a fresh copy of every value in the array.

void print_array(int arr[], int size) { /* ... */ }
/* the compiler treats this identically to: */
void print_array(int *arr, int size) { /* ... */ }

Why Array Size Is Usually Passed Separately

Since a function parameter written as int arr[] is really just a pointer, sizeof(arr) inside that function reports the size of a pointer, never the size of whatever array the caller passed in — the original element count has already been lost by the time the function body runs. The common fix is a clean interface that carries the count as its own argument.

sizeof(numbers) / sizeof(numbers[0]) is computed in main, where numbers is still a real array and the sizeof trick still works — the result is then handed in as an ordinary size_t argument.

void print_array(const int *arr, size_t size);

/* called as: */
int numbers[5] = {10, 20, 30, 40, 50};
print_array(numbers, sizeof(numbers) / sizeof(numbers[0]));

Modifying an Array in a Function

Because the function receives a pointer to the caller's actual first element rather than a copy of the whole array, changes made through that pointer are visible back in the caller once the function returns — no copying of the full array ever takes place either way.

void change_first(int *arr) {
    arr[0] = 100;
}

int numbers[5] = {10, 20, 30, 40, 50};
change_first(numbers);
printf("%d\n", numbers[0]); /* 100 */

const When Reading Arrays

Adding const to an array parameter's pointer type is a promise to the reader and the compiler that the function will not modify the elements it was handed — useful for any function whose whole job is to look at data, not change it.

void print_array(const int *arr, size_t size) {
    for (size_t i = 0; i < size; i++) {
        printf("%d \n", arr[i]);
    }
}

Strings Are Character Arrays

A C string is nothing more exotic than a character array that ends with a hidden '\0' terminator character. Writing char name[] = "Rina"; actually reserves five characters — 'R', 'i', 'n', 'a', and the terminator — even though only four letters appear in the literal.

name[0] = 'R'
name[1] = 'i'
name[2] = 'n'
name[3] = 'a'
name[4] = '\0'

String and Pointer

Since a string is just a character array, the same decay rule from earlier in this article applies to it: char name[] = "Rina"; char *p = name; makes p point at name[0], so *p reads 'R' and *(p+1) reads 'i'.

char name[] = "Rina";
char *p = name;
printf("%c %c\n", *p, *(p + 1)); /* R i */

Accessing String Characters Using a Pointer

Walking the offset forward one position at a time reaches each letter of "Rina" in turn, and even the invisible terminator is reachable the same way, at one position past the last letter.

char name[] = "Rina";
char *p = name;
printf("%c%c%c%c\n", *p, *(p + 1), *(p + 2), *(p + 3)); /* Rina */
if (*(p + 4) == '\0') {
    printf("terminator found at position 4\n");
}

Traversing a String Using a Pointer

Because a string carries no separately stored length, the standard way to visit every character is to advance a pointer one step at a time and stop the moment it hits the '\0' terminator.

The loop condition checks the character *p points to before printing it, so it stops the instant it reaches the terminator instead of printing it or reading past it.

char name[] = "Rina";
char *p = name;
while (*p != '\0') {
    printf("%c\n", *p);
    p++;
}

String Indexing vs Pointer Dereferencing

Because a string is a character array, the general index-pointer identity applies to it exactly as it does to numeric arrays: str[i] and *(str + i) always name the same character.

str[2]  ==  *(str + 2)

Pointer to a Character vs Character Array

char str[] = "Hello"; char *p = str; creates two distinct objects: str is the array that actually owns the storage, and p is a separate pointer variable that merely points into it. Writing through the pointer, as in *p = 'Y';, reaches into str's own storage and changes it (str becomes "Yello"), while moving the pointer with p++; only relocates p — the array underneath is left completely untouched.

char str[] = "Hello";
char *p = str;
*p = 'Y';   /* str is now "Yello" */
p++;        /* only p moved; str is unaffected by this line */

String Literal and Pointer

const char *p = "Hello"; points at characters belonging to a string literal, and such literals are not meant to be changed — attempting *p = 'Y'; on them is undefined behavior, and marking the pointer const makes the compiler flag any such attempt. When the text genuinely needs to be modifiable, declare it as its own array instead: char str[] = "Hello"; str[0] = 'Y'; is perfectly safe, because str owns writable storage of its own.

const char *p = "Hello";
/* *p = 'Y'; -- undefined behavior, literals are not writable storage */

char str[] = "Hello";
str[0] = 'Y'; /* fine -- str owns its own modifiable copy */

Pointer to String vs Array of Strings

There is more than one way to represent several names in C. A single char *p; can point at any one string's first character, but only one at a time. A 2D array like char names[3][20]; reserves fixed 20-character storage for each of three names directly inline. An array of pointers, const char *names[] = {"Rina", "Amit", "Rahul"};, instead stores three separate pointers, each one aimed at its own string kept elsewhere — no fixed per-name storage limit and no wasted space for short names.

char *p;                                              /* one pointer, one string at a time */
char names[3][20];                                    /* three fixed 20-char slots, inline */
const char *names2[] = {"Rina", "Amit", "Rahul"};     /* three pointers, each to its own string */

Two-Dimensional Character Arrays

char names[3][20] = {"Rina", "Amit", "Rahul"}; lays out three rows back to back in memory, each row being its own 20-character array holding one name plus its terminator.

names[0] -> 'R''i''n''a''\0' . . . (20 bytes total)
names[1] -> 'A''m''i''t''\0' . . . (20 bytes total)
names[2] -> 'R''a''h''u''l''\0'. . . (20 bytes total)

Pointer to a Row of a 2D Array

For a numeric grid like int matrix[3][4];, a plain int * cannot correctly represent one row — the pointer needed is int (*p)[4], read as "pointer to an array of 4 ints". Once such a pointer is set to point at the matrix, ordinary double-bracket syntax like p[0][0], p[1][2], and p[2][3] still reaches individual elements as expected. This row-pointer type is exactly what matters when a multi-dimensional array gets passed into a function.

int matrix[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};
int (*p)[4] = matrix;
printf("%d %d %d\n", p[0][0], p[1][2], p[2][3]); /* 1 7 12 */

Pointer Arithmetic and 2D Arrays

Because p above is typed as "pointer to an array of 4 ints", advancing it with p+1 does not step to the next int within a row — it jumps over an entire row of 4 ints and lands at the start of the next one, since the pointed-to type is int[4], not int.

int matrix[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};
int (*p)[4] = matrix;
int (*next_row)[4] = p + 1; /* points at matrix[1], not at matrix[0][1] */

Pointer Arithmetic Boundaries

For an array like int arr[5];, a pointer is allowed to range across every element and is also permitted to hold the address exactly one position past the last element — commonly called the one-past-the-end pointer. That special position is valid to compute and compare against, but it must never be dereferenced, since it doesn't correspond to a real element.

int arr[5] = {1, 2, 3, 4, 5};
for (int *p = arr; p != arr + 5; p++) {
    printf("%d \n", *p);
}

Pointer Comparison Within an Array

Two pointers that both point into the same array (including the one-past-the-end position) can be meaningfully compared with == and !=. A common idiom keeps a start pointer and an end pointer and loops until the two meet.

int arr[5] = {1, 2, 3, 4, 5};
int *start = arr;
int *end = arr + 5;
while (start != end) {
    printf("%d \n", *start);
    start++;
}

Pointer Difference

Two pointers into the same array can also be subtracted from each other, producing how many elements separate them. The result's type is ptrdiff_t, declared in <stddef.h>, and the subtraction is only meaningful when both pointers point into the same array (or one position past it) — subtracting pointers from unrelated arrays is undefined behavior.

#include <stddef.h>

int arr[10];
int *p = &arr[2];
int *q = &arr[7];
ptrdiff_t difference = q - p; /* 5 */

Important: Don't Confuse p + 1 with &p + 1

p + 1 and &p + 1 look similar but operate on completely different types, so they compute completely different addresses. If p is an int *, then p + 1 moves forward by one int, to the next element of whatever array p points into. But &p is the address of the pointer variable p itself, with type int **, so &p + 1 moves forward by the size of one whole pointer object — well past p's own storage, not anywhere near the array p points into.

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
p + 1;    /* next int in the array p points into      */
&p + 1;   /* one pointer-object's-width past p itself, unrelated to arr */

Arrays and sizeof

Given int arr[10]; int *p = arr;, sizeof(arr) reports the size of all ten ints combined, while sizeof(p) reports only the size of the pointer variable itself — typically 4 or 8 bytes regardless of how large the array it points into happens to be. This gap between the two is a favorite interview gotcha precisely because arr and p can look interchangeable in ordinary code.

int arr[10];
int *p = arr;
printf("%zu\n", sizeof(arr)); /* size of all 10 ints, e.g. 40 */
printf("%zu\n", sizeof(p));   /* size of one pointer, e.g. 8  */

Common Mistake: Thinking Arrays Are Pointers

The correct mental model to keep throughout this article is that an array is an array object in its own right, one that frequently converts to a pointer to its first element inside a particular expression — it never actually becomes a pointer variable. That distinction is exactly why int *p = arr; compiles (decay supplies the address), while arr = p; does not (arrays are not assignable objects, no matter what's on the right-hand side).

A quick way to keep this straight: a pointer is a variable you can reassign at will; an array's name is a fixed label for its own block of storage and can never be pointed somewhere else.

You Cannot Assign to an Array

Given int a[5], b[5];, the statement a = b; simply does not compile — arrays are not assignable as whole objects in C. To copy one array's contents into another, the elements have to be copied one at a time, whether by a hand-written loop or by a library routine such as memcpy when it fits what's actually being copied.

int a[5], b[5] = {1, 2, 3, 4, 5};
/* a = b; -- does not compile */
for (int i = 0; i < 5; i++) {
    a[i] = b[i]; /* copy element by element instead */
}

Arrays and Strings: Important Relationship

A string like char str[] = "Hello"; is, underneath, just an array — so str[0] and *(str + 0) reach the same 'H' by two different routes. The full chain runs: a string is a character array, which decays to a pointer to its first character in most expressions, and pointer arithmetic on that pointer reaches every individual character in turn.

Example: Reverse a String Using Pointers

A classic use of two pointers walking toward each other from opposite ends: one starts at the string's first character, the other at its last (found by skipping to the '\0' and stepping back one), and the two swap and step inward until they meet or cross.

left and right start at opposite ends of the same array and step toward each other on every pass, swapping the pair of characters they currently point to until they meet in the middle.

left begins at text[0] ('H'), right begins at text[4] ('o'), the last real character before the terminator.

Each loop iteration swaps *left and *right, then moves left one step right and right one step left.

The loop stops as soon as left is no longer strictly less than right, meaning the two pointers have met or swapped past each other.

By the end, every character has been moved to its mirrored position, turning "Hello" into "olleH".

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

void reverse_string(char *str) {
    char *left = str;
    char *right = str + strlen(str) - 1;

    while (left < right) {
        char temp = *left;
        *left = *right;
        *right = temp;
        left++;
        right--;
    }
}

int main(void) {
    char text[] = "Hello";
    reverse_string(text);
    printf("%s\n", text); /* olleH */
    return 0;
}

Example: Find String Length Using a Pointer

The idea behind the standard library's strlen function is simple enough to write by hand: walk a pointer forward from the string's start until it lands on the '\0' terminator, then the distance traveled — the pointer difference between where it stopped and where it started — is the string's length.

p walks forward one character at a time until it hits the terminator; p - str then measures exactly how many characters were passed over on the way, which is the string's length.

#include <stdio.h>

int string_length(const char *str) {
    const char *p = str;
    while (*p != '\0') {
        p++;
    }
    return (int)(p - str);
}

int main(void) {
    printf("%d\n", string_length("Turing")); /* 6 */
    return 0;
}

Example: Print an Array Using Pointers

Putting several ideas from this article together at once: a function receives a pointer to the array's first element, walks forward using pointer arithmetic instead of indexing, and stops the moment it reaches the one-past-the-end position.

numbers decays to a pointer when passed in, print_array treats it purely as a pointer plus a count, and the loop walks that pointer forward until it reaches the position exactly size elements past the start.

#include <stdio.h>
#include <stddef.h>

void print_array(const int *arr, size_t size) {
    for (const int *p = arr; p != arr + size; p++) {
        printf("%d \n", *p);
    }
}

int main(void) {
    int numbers[5] = {10, 20, 30, 40, 50};
    print_array(numbers, 5);
    return 0;
}

Pointer-Based Mental Model

For int arr[5] = {10, 20, 30, 40, 50}; int *p = arr;, it helps to picture p as a movable marker sliding across the same five boxes the array already occupies: p+0 sits over the first box, p+1 over the second, and so on, with arr[i] and p[i] both always landing on the exact same box for a given i.

p was built straight from arr — no & written anywhere — because arr had already decayed into &arr[0] by the time the assignment ran.

p[i] Works Too

It often surprises beginners the first time they see it, but square brackets are not exclusive to arrays: for a plain pointer, p[i] is defined as *(p + i), the exact same rule that governs array indexing, so it produces the exact same result as ordinary array indexing would.

int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
printf("%d\n", p[1]); /* 20 */

A Curious Property of C

Since a[b] always expands to *(a + b), and addition doesn't care which side an operand appears on, *(a + b) and *(b + a) are the same expression — which means arr[2] and 2[arr] genuinely compile to identical code. It's a fun illustration of how literally the language takes its own indexing rule, but it's a curiosity to understand, never something to actually write in real code.

arr[2]   ==   *(arr + 2)   ==   *(2 + arr)   ==   2[arr]

Key Differences

Arrays and pointers overlap in enough places to blur together, but they differ in every property that actually matters once you look closely.

Property                  Array                          Pointer
------------------------  -----------------------------  -----------------------------
What it is                A block holding N elements     A variable holding one address
Stores multiple values    Yes, directly, inline          No, only one address at a time
Can be reassigned         No                             Yes
sizeof gives               Total size of all elements     Size of the pointer itself
Decays to a pointer       Yes, in most expressions       Already is one -- nothing to decay
[] indexing                Works (via decay)              Works directly
Pointer arithmetic        Works only after decaying      Works directly on the pointer

Arrays, Pointers and Strings Together

The whole article boils down to one short chain, repeated in two forms. For a plain array: the array converts to a pointer at its first element, and dereferencing that pointer (at whatever offset) reaches the current element. For a string specifically: a character array holding letters plus a trailing '\0' converts the same way to a pointer at its first character, and the same pointer, walked forward, keeps reaching characters until the '\0' finally stops it.

An array's name, when used in most expressions, 'decays' into a pointer to its first element. arr[i] is defined to mean *(arr + i) — array indexing is literally pointer arithmetic with friendlier syntax.

Understanding this equivalence explains why arrays are passed to functions 'by reference' in effect (only a pointer is copied, not the whole array), why sizeof behaves surprisingly on array parameters, and how C strings (arrays of char) really work under the hood.

Every function that takes an array parameter, every string function in string.h, and every data structure that walks through contiguous memory relies on this array/pointer relationship.

An array is a street of numbered houses built one after another. The array's name is really just 'the address of house #0' — knowing that address and the fact that houses are evenly spaced lets you compute the address of house #5 without ever needing a separate label for it.

arr[i] and *(arr + i) side by side Prints the same elements two different ways to show arr[i] and *(arr + i) really are the exact same operation.

int nums[4] = {10, 20, 30, 40}; allocates 4 contiguous ints; nums itself decays to the address of nums[0] almost everywhere it's used in an expression.

nums + i computes 'i ints past nums[0]' — pointer arithmetic automatically scales by sizeof(int), so nums + 2 means +8 bytes, not +2 bytes.

*(nums + i) dereferences that computed address; nums[i] is defined by the C standard to mean exactly this — the two expressions compile to identical code.

nums + 2 computes 0x7ffee208 directly from nums[0]'s address (0x7ffee200) plus 2 × sizeof(int) — no searching required.

Passing the decayed array to a function Only the array's decayed pointer (its starting address) is actually copied into the function — not the whole array's contents.

printArray(nums, 4) passes nums, which decays to a pointer to nums[0], plus the size 4 as a separate argument — a pointer alone carries no length information.

Inside printArray, int *arr receives that address; arr[i] works exactly like it would in main, because arr and nums point at the very same memory.

Because only an address (not a full copy of the 4 ints) crosses the function boundary, this is cheap regardless of how large the original array actually is.

void printArray(int arr[]) { int len = sizeof(arr) / sizeof(arr[0]); // always wrong here } void printArray(int arr[], int len) { // len is passed in explicitly by the caller }

Once an array decays into a function parameter, it's just a pointer — sizeof(arr) reports the pointer's size (4 or 8), not the original array's byte size. The length must always be passed alongside it.

Array decay happens in most, but not all, contexts: sizeof(arr) inside the same scope the array was declared in still correctly reports the full array size, but once an array is passed to a function, the parameter is really just a pointer and sizeof on it reports the pointer's size instead.

A C string is simply a char array terminated by a '\0' (null) byte — string functions like strlen scan forward until they hit that terminator, which is why an unterminated char buffer causes strlen and friends to read past the intended data.

Array parameters in a function signature (void f(int arr[]) vs void f(int *arr)) are exactly equivalent to the compiler — the [] syntax is purely a readability convention for 'this is conceptually an array', not a different mechanism.

A 2D array in C is stored as one contiguous block in row-major order, so arr[i][j] is really computed as *(*(arr + i) + j) once decay and pointer arithmetic are unwound.

Using sizeof on a pointer parameter expecting it to report the original array's length — it reports the pointer's own size (typically 4 or 8 bytes) instead, a classic source of buffer-size bugs.

Forgetting to leave room for, or to actually write, the '\0' terminator when building a string manually — every string function assumes it's there and will read/write out of bounds if it isn't.

Passing the wrong size when a function needs an array's length, since a decayed array pointer alone carries no length information — the size must always travel alongside it as a separate parameter or a sentinel value.

Always pairing an array parameter with an explicit size (or count) parameter in any function signature, rather than assuming the callee can somehow know how big the array is.

Reserving one extra byte for '\0' whenever sizing a buffer meant to hold a string of n visible characters — a buffer of exactly n bytes is one byte too small.

Using strncpy/snprintf-style bounded functions over their unbounded counterparts (strcpy, sprintf, gets) specifically because a decayed pointer gives no built-in bounds checking.

Decay is a conversion rule, not a storage change The array itself is still stored as a genuine contiguous block; decay is only what happens to the *expression* referring to it in most contexts. &arr (address of the whole array) has a different type (pointer-to-array) than arr decayed (pointer-to-first-element), even though both hold the same numeric address — a subtlety that trips up even experienced C programmers reading pointer arithmetic on multi-dimensional arrays.

Why this design exists C deliberately keeps arrays 'thin' (no built-in length, no bounds checking) to guarantee that array access compiles to nothing more than a raw memory address computation and a load/store — the same design philosophy that makes C fast and close to the hardware also makes bounds-safety entirely the programmer's responsibility.

Is an array the same thing as a pointer in C?

No — an array is a block of contiguous storage with a fixed size known at compile time; a pointer is a variable holding an address. They're related because an array's name decays into a pointer to its first element in most expressions, but sizeof and the & operator can still tell them apart.

Why do you need to pass an array's size separately to a function?

Because once an array decays to a pointer at the function boundary, only the starting address is transferred — the length information is lost, so it must be passed alongside as its own parameter.

What actually terminates a C string?

A single null byte, '\0', appended after the last visible character — string functions rely entirely on finding this byte to know where the string ends, independent of how much memory was actually allocated for the buffer.

In your own words, what does it mean for an array to "decay" into a pointer, and where does that conversion NOT happen?

Decay is the automatic conversion of an array's name into a pointer to its first element, which happens in almost every expression it appears in. The two well-known exceptions are sizeof applied directly to the array (which reports the whole array's size) and & applied directly to the array (which produces a pointer to the whole array, not just its first element).

Why doesn't sizeof(arr) give you the original array's size when arr is a function parameter declared as int arr[]?

In a parameter list, int arr[] is treated by the compiler as int *arr — a plain pointer parameter, not an array. By the time the function body runs, the original element count is gone, so sizeof(arr) reports the size of a pointer, not the size of whatever array the caller passed in.

Why does int a[5], b[5]; a = b; fail to compile, while int *p = a; works fine?

Arrays are not assignable objects in C — there is no operation that copies one array's contents into another via =. int *p = a; compiles for a different reason entirely: a decays to a pointer to its first element in that expression, so p is really being assigned an address, not the array itself.

What is the practical difference between numbers and &numbers, given that they can print the same numeric address?

numbers, when it decays, has type int * (pointer to a single int, the first element). &numbers has type int (*)[N] (pointer to the whole N-element array). The numeric address may match, but arithmetic behaves very differently on the two: adding 1 to the first moves by one int, while adding 1 to the second jumps past the entire array.

Why is *p = 'Y'; undefined behavior when p is declared as const char *p = "Hello";, but perfectly safe when str is declared as char str[] = "Hello"; and you write str[0] = 'Y';?

p points at a string literal, which typically lives in read-only memory that the program was never meant to modify — so writing through it is undefined behavior even though the type system may not always stop you. str, by contrast, is its own array with genuinely writable storage that was copied from the literal at initialization, so changing its elements is completely safe.

Write a C function that sums the elements of an int array using pointer arithmetic (not array indexing), and a main function that calls it.

The function takes a pointer and a count, and instead of writing arr[i] it computes *(arr + i) at each step, which is defined to mean exactly the same thing but makes the pointer arithmetic explicit.

Write a C program that reverses a string in place using two char pointers, one starting at the first character and one at the last, swapping toward the middle.

left advances from the front and right retreats from the back, swapping the pair of characters they point to on every pass, until they meet or cross in the middle — at which point the whole string has been mirrored in place.

Write your own version of strlen() using pointer traversal — walk a pointer forward until it reaches the '\0' terminator and return how far it traveled.

p starts at the string's first character and moves forward one position at a time until it lands on the terminator; the pointer difference p - str then equals the number of characters that were passed over, which is the string's length.

Write a C function that finds the maximum element of an int array using a pointer parameter and pointer-based traversal, rather than indexing.

The function starts max at the first element and then walks a pointer p across the remaining elements, comparing *p to the current max and updating it whenever a larger value turns up, stopping when p reaches the one-past-the-end position.

Write a C function that copies one int array into another using pointer arithmetic instead of indexing.

At each step the function reads *(source + i) and writes that value to *(dest + i), which is exactly what indexed assignment would do, just written with explicit pointer arithmetic on both sides.

Write a C function print_array(const int *arr, size_t size) that prints every element of an array read-only, using pointer traversal and the one-past-the-end idiom.

The const on the pointer parameter promises the caller that print_array will only read through arr, never write to it, and the loop walks p forward until it reaches arr + size, the valid one-past-the-end stopping point.

Write a C program that demonstrates pointer difference: take two pointers into the same int array and compute how many elements separate them using ptrdiff_t.

Subtracting two pointers that point into the same array gives the number of elements between them as a ptrdiff_t value, here 5, since q points 5 positions further into arr than p does.

Write a C function that counts the vowels in a string by walking a char pointer forward until it reaches the '\0' terminator.

The function walks p one character at a time, checking each one against the set of vowels before advancing, and stops the moment *p equals the null terminator — the same traversal pattern used to implement strlen.

Write a C program that prints a 3x4 int matrix using a pointer to a row (int (*)[4]) instead of plain double indexing on the array name.

mat is declared as a pointer to an array of 4 ints, which is exactly the type a 2D array with 4 columns decays to when passed to a function, so mat[r][c] correctly reaches each element row by row.

#include <stdio.h>

int main() {
    int nums[4] = {10, 20, 30, 40};

    for (int i = 0; i < 4; i++) {
        printf("nums[%d] = %d, *(nums+%d) = %d\n", i, nums[i], i, *(nums + i));
    }
    return 0;
}
#include <stdio.h>

void printArray(int *arr, int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    int nums[4] = {10, 20, 30, 40};
    printArray(nums, 4);
    return 0;
}