Continuing from Pointers — Part I
int a[3][4]; // 3 rows, 4 columns
Think of it as a table:
int a[3][4] is stored as 12 consecutive integers:
To find a[i][j], the compiler computes:
Peel apart a[1][2] using what you already know about pointer arithmetic.
int a[3][4];
a // pointer to row 0 type: int (*)[4]
a + 1 // pointer to row 1 (moves 16 bytes — skips 4 ints)
*(a + 1) // dereference → row 1 type: int * (points to a[1][0])
*(a+1) + 2 // move 2 ints forward points to a[1][2]
*(*(a+1)+2)// dereference → the value same as a[1][2]
a[1][2] ≡ *(a[1] + 2) ≡ *(*(a + 1) + 2)
a[i] means "dereference the i-th row pointer" — it gives you an int * pointing to the first element of that row. From there, adding j and dereferencing gives the element.
a and *a (i.e. a[0]) hold the same numerical address — both point to a[0][0]. But their types differ: a is int (*)[4], so a+1 skips 16 bytes; *a is int *, so (*a)+1 skips 4 bytes. Same value, completely different arithmetic.
a is NOT int **This is the most common confusion with 2D arrays.
int a[3][4];
int **p = a; // ✗ TYPE ERROR — won't work correctly
int (*p)[4] = a; // ✓ CORRECT type for a row pointer
int **pExpects p to point to an int *.
Dereferencing once gives another pointer.
Not what a 2D array has.
int (*p)[4]Points to a block of 4 ints.
Dereferencing once gives an int *.
Matches how a 2D array is laid out.
int ** IS correct for a dynamic 2D array (where each row is a separately malloc'd int *). But for a stack 2D array like int a[3][4], the type is int (*)[4].
The column size is mandatory. The compiler needs it to compute row offsets.
// ✗ WRONG — won't compile
void print(int a[][], int rows, int cols) { ... }
// ✓ CORRECT — column size must be specified
void print(int a[][4], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 4; j++) {
printf("%d ", a[i][j]);
}
printf("\n");
}
}
int main() {
int a[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
print(a, 3);
}
a[i][j] — it does not know how many elements to skip to get to row i.
int (*p)[4]A pointer can point to an entire row, not just a single element.
int a[3][4];
int (*p)[4] = a; // p points to row 0 (an array of 4 ints)
p++; // now p points to row 1 (skips 4 ints = 16 bytes)
(*p)[2]; // element at row 1, column 2 ← same as a[1][2]
int (*p)[4]; // pointer to array of 4 ints
int *p[4]; // array of 4 int pointers
// (totally different!)
int *q = a[0]; // q++ moves 4 bytes
int (*p)[4]=a; // p++ moves 16 bytes
int n;
scanf("%d", &n);
int a[n]; // risky — VLA
// may fail for large n
// gone when function returns
int n;
scanf("%d", &n);
int *a = malloc(n * sizeof(int));
// works for any n (within RAM)
// lives until you free it
malloc — Allocate Memory#include <stdlib.h>
int n = 5;
int *arr = malloc(n * sizeof(int));
if (arr == NULL) {
// malloc failed (out of memory)
return 1;
}
arr[0] = 10; // use it just like a normal array
arr[1] = 20;
// ...
free(arr); // always free when done
malloc(bytes) — asks the OS for a block of bytesfreecalloc — Allocate and Zero// malloc — uninitialized (garbage)
int *a = malloc(5 * sizeof(int));
// calloc — zero-initialized
int *b = calloc(5, sizeof(int));
// b[0] == 0, b[1] == 0, ... guaranteed
Fast. No zeroing. Use when you'll overwrite every element anyway.
Slightly slower. All bytes are zero. Safer default when you need a clean slate.
calloc(count, size) is exactly malloc(count * size) + zeroing all bytes.
realloc — Resize an AllocationYou started with space for 5 ints, but now need 10. Use realloc.
int *arr = malloc(5 * sizeof(int));
// ... fill arr with data ...
// Need more space — resize to 10
int *tmp = realloc(arr, 10 * sizeof(int));
if (tmp == NULL) {
// realloc failed — arr is still valid
free(arr);
return 1;
}
arr = tmp; // now arr points to the bigger block
arr = realloc(arr, ...) directly. If realloc fails and returns NULL, you lose your only pointer to the original memory — a memory leak.
free — Two Rulesint *p = malloc(sizeof(int));
free(p);
free(p); // ✗ double-free
// undefined behavior
int *p = malloc(sizeof(int));
*p = 42;
free(p);
printf("%d", *p); // ✗ use-after-free
// undefined behavior
free(p);
p = NULL;
#include <stdio.h>
#include <stdlib.h>
int main() {
int n;
printf("How many numbers? ");
scanf("%d", &n);
int *arr = malloc(n * sizeof(int));
if (!arr) { return 1; }
for (int i = 0; i < n; i++) {
printf("Enter number %d: ", i + 1);
scanf("%d", &arr[i]);
}
int sum = 0;
for (int i = 0; i < n; i++) sum += arr[i];
printf("Sum = %d\n", sum);
free(arr);
return 0;
}
Allocate an array of pointers, then allocate each row separately.
int rows = 3, cols = 4;
// Step 1: array of row pointers
int **a = malloc(rows * sizeof(int *));
// Step 2: allocate each row
for (int i = 0; i < rows; i++) {
a[i] = malloc(cols * sizeof(int));
}
// Use exactly like a static 2D array
a[1][2] = 42;
// Free in reverse order
for (int i = 0; i < rows; i++) {
free(a[i]); // free each row first
}
free(a); // then free the pointer array
int a[3][4]12 ints — one contiguous block
int **aRows may be scattered in heap
int **, each row is a separate malloc — rows are not contiguous. a[i][j] syntax works, but the memory is scattered. There is a third approach that gives you dynamic sizing AND contiguous memory.
One single malloc for all elements — row-major, just like a stack 2D array.
int rows = 3, cols = 4;
// ONE allocation for everything
int *flat = malloc(rows * cols * sizeof(int));
// Access: compute the offset manually
flat[1 * cols + 2] = 42; // same as a[1][2]
flat[i * cols + j] = value; // general form
// Free: just one call
free(flat);
int a[3][4], but the size is decided at runtime and you manage the lifetime manually.
int a[3][4] |
int **a |
int *flat |
|
|---|---|---|---|
| Memory layout | One block, row-major | Scattered rows | One block, row-major |
| Size at runtime? | No | Yes | Yes |
| Access syntax | a[i][j] |
a[i][j] |
flat[i*cols+j] |
| Rows same length? | Yes (fixed) | No — can vary | Yes (fixed) |
| Free | Automatic | Loop + free(a) |
One free |
int ** when rows can have different lengths (jagged arrays). Use int *flat when you need dynamic sizing with contiguous memory. Use int a[N][M] when the size is fixed at compile time.
int* make_array() {
int arr[5] = {1,2,3,4,5};
return arr; // DANGER!
// arr lives on the stack
// destroyed when function returns
}
The caller gets a dangling pointer.
int* make_array(int n) {
int *arr = malloc(n * sizeof(int));
for (int i = 0; i < n; i++)
arr[i] = i + 1;
return arr; // heap — persists!
}
int *p = make_array(5);
// use p...
free(p); // caller must free
malloc. The caller is then responsible for calling free.
sizeof Proves They Are DifferentThe clearest way to see the difference between a 2D array and int **:
int a[3][4]; // stack 2D array
int **p; // pointer to pointer
sizeof(a) // 48 ← 3 × 4 × sizeof(int) = full array
sizeof(p) // 8 ← just one pointer (on 64-bit system)
sizeof(a[0]) // 16 ← one row = 4 ints
sizeof(p[0]) // 8 ← one int* pointer
sizeof on a true 2D array gives you the full size of all the data. sizeof on a pointer gives you the size of the pointer — not what it points to. This is why functions receiving arrays cannot use sizeof to find the length; the array decays to a pointer when passed.
int a[3][4] and int **p both support a[i][j] syntax, but they are fundamentally different in memory layout, type, and size.
| Syntax / Concept | What it means |
|---|---|
int a[3][4] |
12 contiguous ints, addressed as 3 rows × 4 cols |
a[i][j] |
Element at row i, col j — offset = i×cols + j |
int a[][4] in a function |
Column size required so compiler can compute row offsets |
int (*p)[4] |
Pointer to a row of 4 ints — p++ skips 16 bytes |
malloc(n * sizeof(T)) |
Allocate n items on the heap, uninitialized |
calloc(n, sizeof(T)) |
Same as malloc but zero-initialized |
realloc(p, new_size) |
Resize a heap allocation — always use a temp pointer |
free(p); p = NULL; |
Return heap memory; nullify to prevent accidental reuse |
return malloc(...) |
Safe — heap outlives the function; caller must free |