C — Arrays
An array in C is a contiguous block of memory that holds a fixed number of elements of the same type. Arrays are one of the most fundamental data structures in C and serve as the building block for strings, matrices, buffers, and more. Unlike higher-level languages, C arrays carry no metadata — no length, no bounds checking, no type info at runtime. You get raw memory and pointer arithmetic, which is both powerful and dangerous.
Arrays are declared by specifying the element type followed by the array name and the number of elements in square brackets. You can initialize at declaration time using a brace-enclosed list of values.
| 1 | // Explicit size with initialization |
| 2 | int numbers[5] = {10, 20, 30, 40, 50}; |
| 3 | |
| 4 | // Implicit size — compiler counts the elements (size = 4) |
| 5 | int small[] = {1, 2, 3, 4}; |
| 6 | |
| 7 | // Partial initialization — remaining elements are zeroed |
| 8 | int partial[10] = {5, 10, 15}; // {5, 10, 15, 0, 0, 0, 0, 0, 0, 0} |
| 9 | |
| 10 | // All zeros |
| 11 | int zeroed[100] = {0}; |
| 12 | |
| 13 | // Designated initializers (C99) — sparse initialization |
| 14 | int sparse[10] = {[0] = 1, [5] = 42, [9] = 99}; |
| 15 | // sparse = {1, 0, 0, 0, 0, 42, 0, 0, 0, 99} |
info
Arrays in C are zero-indexed. The first element is at index 0, and the last element is at index `size - 1`. The array name itself decays to a pointer to the first element in most expressions, so `arr[0]` is equivalent to `*(arr + 0)`.
| 1 | int arr[5] = {10, 20, 30, 40, 50}; |
| 2 | |
| 3 | // Access by index |
| 4 | printf("First: %d\n", arr[0]); // 10 |
| 5 | printf("Third: %d\n", arr[2]); // 30 |
| 6 | printf("Last: %d\n", arr[4]); // 50 |
| 7 | |
| 8 | // Pointer arithmetic equivalent |
| 9 | printf("Second: %d\n", *(arr + 1)); // 20 |
| 10 | printf("Fourth: %d\n", *(arr + 3)); // 40 |
| 11 | |
| 12 | // Modify elements |
| 13 | arr[1] = 99; |
| 14 | printf("Modified: %d\n", arr[1]); // 99 |
The standard way to iterate over an array is with a for loop from index 0 to` length - 1`. Since C arrays don't know their own size, you must track the length separately.
| 1 | int arr[] = {5, 3, 8, 1, 9, 2, 7}; |
| 2 | int len = sizeof(arr) / sizeof(arr[0]); // 7 |
| 3 | |
| 4 | // Classic for loop |
| 5 | for (int i = 0; i < len; i++) { |
| 6 | printf("arr[%d] = %d\n", i, arr[i]); |
| 7 | } |
| 8 | |
| 9 | // While loop |
| 10 | int i = 0; |
| 11 | while (i < len) { |
| 12 | printf("%d ", arr[i]); |
| 13 | i++; |
| 14 | } |
| 15 | |
| 16 | // Using a pointer (more idiomatic C) |
| 17 | for (int *p = arr; p < arr + len; p++) { |
| 18 | printf("%d ", *p); |
| 19 | } |
The expression `sizeof(arr) / sizeof(arr[0])` gives you the number of elements in an array — but only when the array is declared in the same scope. When an array is passed to a function, it decays to a pointer, and `sizeof` returns the pointer size, not the array size. This is one of the most common source of bugs in C.
| 1 | void print_size(int arr[]) { |
| 2 | // DANGER: arr is now a pointer! |
| 3 | // sizeof(arr) == sizeof(int*) == 8 on 64-bit |
| 4 | printf("Inside function: %zu bytes\n", sizeof(arr)); |
| 5 | } |
| 6 | |
| 7 | int main(void) { |
| 8 | int arr[10] = {0}; |
| 9 | printf("In main: %zu bytes\n", sizeof(arr)); // 40 |
| 10 | printf("Elements: %zu\n", sizeof(arr)/sizeof(arr[0])); // 10 |
| 11 | |
| 12 | print_size(arr); // prints 8, NOT 40 |
| 13 | |
| 14 | // Always pass the length alongside the array |
| 15 | return 0; |
| 16 | } |
warning
C supports multi-dimensional arrays as arrays of arrays. A 2D array is stored in row-major order — all elements of the first row come first in memory, then the second row, and so on.
| 1 | // 2D array: 3 rows, 4 columns |
| 2 | int matrix[3][4] = { |
| 3 | {1, 2, 3, 4}, |
| 4 | {5, 6, 7, 8}, |
| 5 | {9, 10, 11, 12} |
| 6 | }; |
| 7 | |
| 8 | // Partial initialization — rest is zero |
| 9 | int sparse[3][4] = { |
| 10 | {1, 5}, |
| 11 | {0, 0, 9}, |
| 12 | {} |
| 13 | }; |
| 14 | |
| 15 | // Access elements |
| 16 | printf("%d\n", matrix[1][2]); // 7 |
| 17 | |
| 18 | // Row-major layout in memory: 1,2,3,4,5,6,7,8,9,10,11,12 |
| 19 | // Traverse 2D array |
| 20 | for (int i = 0; i < 3; i++) { |
| 21 | for (int j = 0; j < 4; j++) { |
| 22 | printf("%3d ", matrix[i][j]); |
| 23 | } |
| 24 | printf("\n"); |
| 25 | } |
3D arrays work the same way — they're arrays of 2D arrays. They're less common but useful for things like volumetric data or color images with width × height × channels.
| 1 | // 3D array: 2 layers, 3 rows, 4 columns |
| 2 | int cube[2][3][4] = { |
| 3 | { |
| 4 | {1, 2, 3, 4}, |
| 5 | {5, 6, 7, 8}, |
| 6 | {9, 10, 11, 12} |
| 7 | }, |
| 8 | { |
| 9 | {13, 14, 15, 16}, |
| 10 | {17, 18, 19, 20}, |
| 11 | {21, 22, 23, 24} |
| 12 | } |
| 13 | }; |
| 14 | |
| 15 | printf("%d\n", cube[1][2][3]); // 24 |
When you pass an array to a function, it decays to a pointer to its first element. The function receives a pointer, not a copy. This means modifications inside the function affect the original array. For 1D arrays, you must pass the size separately. For 2D arrays, you must specify all dimensions except the first.
| 1 | // 1D array parameter — size must be passed separately |
| 2 | void fill(int arr[], int size, int value) { |
| 3 | for (int i = 0; i < size; i++) { |
| 4 | arr[i] = value; |
| 5 | } |
| 6 | } |
| 7 | |
| 8 | // 2D array parameter — second dimension must be known at compile time |
| 9 | void print_matrix(int rows, int cols, int matrix[rows][cols]) { |
| 10 | for (int i = 0; i < rows; i++) { |
| 11 | for (int j = 0; j < cols; j++) { |
| 12 | printf("%3d ", matrix[i][j]); |
| 13 | } |
| 14 | printf("\n"); |
| 15 | } |
| 16 | } |
| 17 | |
| 18 | // Alternative: pass as a flat pointer with explicit indexing |
| 19 | void print_flat(int *matrix, int rows, int cols) { |
| 20 | for (int i = 0; i < rows; i++) { |
| 21 | for (int j = 0; j < cols; j++) { |
| 22 | printf("%3d ", matrix[i * cols + j]); |
| 23 | } |
| 24 | printf("\n"); |
| 25 | } |
| 26 | } |
note
C99 introduced variable-length arrays (VLAs), which allow array sizes to be determined at runtime. VLAs are allocated on the stack and their lifetime is limited to the block in which they are declared. They are optional in C11 and later.
| 1 | #include <stdio.h> |
| 2 | #include <stdlib.h> |
| 3 | |
| 4 | void dynamic_matrix(int n) { |
| 5 | // VLA — size determined at runtime |
| 6 | int matrix[n][n]; |
| 7 | |
| 8 | // Initialize as identity matrix |
| 9 | for (int i = 0; i < n; i++) { |
| 10 | for (int j = 0; j < n; j++) { |
| 11 | matrix[i][j] = (i == j) ? 1 : 0; |
| 12 | } |
| 13 | } |
| 14 | |
| 15 | |
| 16 | for (int i = 0; i < n; i++) { |
| 17 | for (int j = 0; j < n; j++) { |
| 18 | printf("%d ", matrix[i][j]); |
| 19 | } |
| 20 | printf("\n"); |
| 21 | } |
| 22 | } |
| 23 | |
| 24 | int main(void) { |
| 25 | int size; |
| 26 | printf("Enter matrix size: "); |
| 27 | scanf("%d", &size); |
| 28 | |
| 29 | if (size > 0 && size <= 1000) { |
| 30 | dynamic_matrix(size); |
| 31 | } |
| 32 | |
| 33 | return 0; |
| 34 | } |
warning
malloc() for large dynamic arrays. VLAs are also not supported in all C compilers (MSVC does not support them).C performs no bounds checking on array access. Accessing an element outside the array's allocated memory is undefined behavior — it may read garbage data, corrupt other variables, crash your program, or appear to work correctly (which is the worst outcome because the bug hides).
| 1 | int arr[5] = {10, 20, 30, 40, 50}; |
| 2 | |
| 3 | // Undefined behavior — reading past the end |
| 4 | int bad = arr[5]; // index 5 is OUT OF BOUNDS (valid: 0-4) |
| 5 | int worse = arr[-1]; // negative index is also UB |
| 6 | |
| 7 | // This might corrupt other stack variables |
| 8 | int x = 42; |
| 9 | arr[10] = 99; // writes to arbitrary memory |
| 10 | printf("%d\n", x); // x might be corrupted |
| 11 | |
| 12 | // The compiler will NOT warn you about this (usually) |
| 13 | // Always ensure your indices are in range: 0 <= i < size |
Arrays are the foundation for many classic algorithms. Here are the most essential search and sort algorithms every C programmer should know.
Linear Search — O(n)
| 1 | int linear_search(int arr[], int size, int target) { |
| 2 | for (int i = 0; i < size; i++) { |
| 3 | if (arr[i] == target) { |
| 4 | return i; // found at index i |
| 5 | } |
| 6 | } |
| 7 | return -1; // not found |
| 8 | } |
| 9 | |
| 10 | // Usage |
| 11 | int arr[] = {4, 2, 7, 1, 9, 3}; |
| 12 | int idx = linear_search(arr, 6, 7); |
| 13 | printf("Found at index: %d\n", idx); // 2 |
Binary Search — O(log n) — requires sorted array
| 1 | int binary_search(int arr[], int size, int target) { |
| 2 | int low = 0, high = size - 1; |
| 3 | |
| 4 | while (low <= high) { |
| 5 | int mid = low + (high - low) / 2; // avoid overflow |
| 6 | |
| 7 | if (arr[mid] == target) { |
| 8 | return mid; |
| 9 | } else if (arr[mid] < target) { |
| 10 | low = mid + 1; |
| 11 | } else { |
| 12 | high = mid - 1; |
| 13 | } |
| 14 | } |
| 15 | return -1; // not found |
| 16 | } |
Bubble Sort — O(n²) — simple but slow
| 1 | void bubble_sort(int arr[], int size) { |
| 2 | for (int i = 0; i < size - 1; i++) { |
| 3 | int swapped = 0; |
| 4 | for (int j = 0; j < size - 1 - i; j++) { |
| 5 | if (arr[j] > arr[j + 1]) { |
| 6 | int tmp = arr[j]; |
| 7 | arr[j] = arr[j + 1]; |
| 8 | arr[j + 1] = tmp; |
| 9 | swapped = 1; |
| 10 | } |
| 11 | } |
| 12 | if (!swapped) break; // already sorted |
| 13 | } |
| 14 | } |
Selection Sort — O(n²) — minimal swaps
| 1 | void selection_sort(int arr[], int size) { |
| 2 | for (int i = 0; i < size - 1; i++) { |
| 3 | int min_idx = i; |
| 4 | for (int j = i + 1; j < size; j++) { |
| 5 | if (arr[j] < arr[min_idx]) { |
| 6 | min_idx = j; |
| 7 | } |
| 8 | } |
| 9 | if (min_idx != i) { |
| 10 | int tmp = arr[i]; |
| 11 | arr[i] = arr[min_idx]; |
| 12 | arr[min_idx] = tmp; |
| 13 | } |
| 14 | } |
| 15 | } |
Insertion Sort — O(n²) — efficient for small or nearly sorted arrays
| 1 | void insertion_sort(int arr[], int size) { |
| 2 | for (int i = 1; i < size; i++) { |
| 3 | int key = arr[i]; |
| 4 | int j = i - 1; |
| 5 | while (j >= 0 && arr[j] > key) { |
| 6 | arr[j + 1] = arr[j]; |
| 7 | j--; |
| 8 | } |
| 9 | arr[j + 1] = key; |
| 10 | } |
| 11 | } |
Passing 2D arrays to functions in C is tricky because the compiler needs to know the number of columns to compute memory offsets. There are several approaches, each with trade-offs.
| 1 | // Method 1: Fixed column size |
| 2 | void sum_rows_fixed(int matrix[][4], int rows) { |
| 3 | for (int i = 0; i < rows; i++) { |
| 4 | int sum = 0; |
| 5 | for (int j = 0; j < 4; j++) { |
| 6 | sum += matrix[i][j]; |
| 7 | } |
| 8 | printf("Row %d sum: %d\n", i, sum); |
| 9 | } |
| 10 | } |
| 11 | |
| 12 | // Method 2: VLA parameters (C99) |
| 13 | void sum_rows_vla(int rows, int cols, int matrix[rows][cols]) { |
| 14 | for (int i = 0; i < rows; i++) { |
| 15 | int sum = 0; |
| 16 | for (int j = 0; j < cols; j++) { |
| 17 | sum += matrix[i][j]; |
| 18 | } |
| 19 | printf("Row %d sum: %d\n", i, sum); |
| 20 | } |
| 21 | } |
| 22 | |
| 23 | // Method 3: Flat pointer with manual indexing |
| 24 | void sum_rows_flat(int *matrix, int rows, int cols) { |
| 25 | for (int i = 0; i < rows; i++) { |
| 26 | int sum = 0; |
| 27 | for (int j = 0; j < cols; j++) { |
| 28 | sum += matrix[i * cols + j]; |
| 29 | } |
| 30 | printf("Row %d sum: %d\n", i, sum); |
| 31 | } |
| 32 | } |
An array of strings is a 2D char array where each row is a null-terminated string. The second dimension sets the maximum length of each string.
| 1 | // Array of strings (2D char array) |
| 2 | char fruits[][20] = { |
| 3 | "Apple", |
| 4 | "Banana", |
| 5 | "Cherry", |
| 6 | "Date" |
| 7 | }; |
| 8 | |
| 9 | int count = sizeof(fruits) / sizeof(fruits[0]); // 4 |
| 10 | |
| 11 | for (int i = 0; i < count; i++) { |
| 12 | printf("Fruit %d: %s\n", i, fruits[i]); |
| 13 | } |
| 14 | |
| 15 | // Compare strings |
| 16 | if (strcmp(fruits[0], "Apple") == 0) { |
| 17 | printf("First fruit is Apple\n"); |
| 18 | } |
| 19 | |
| 20 | // You can modify individual characters |
| 21 | fruits[0][0] = 'a'; // "apple" — valid because it's a char array |
C99 introduced composite literals, which create unnamed arrays (or structs) that exist as expressions. Unlike array literals in other languages, these are actual objects in memory. You can pass them to functions without naming them.
| 1 | #include <stdio.h> |
| 2 | |
| 3 | void print_array(int *arr, int size) { |
| 4 | for (int i = 0; i < size; i++) { |
| 5 | printf("%d ", arr[i]); |
| 6 | } |
| 7 | printf("\n"); |
| 8 | } |
| 9 | |
| 10 | int main(void) { |
| 11 | // Pass a temporary array directly to a function |
| 12 | print_array((int[]){10, 20, 30, 40, 50}, 5); |
| 13 | |
| 14 | // Assign to a pointer (lifetime = enclosing block) |
| 15 | int *dynamic = (int[]){1, 2, 3, 4, 5}; |
| 16 | printf("First: %d\n", dynamic[0]); |
| 17 | |
| 18 | // 2D composite literal |
| 19 | int (*grid)[3] = (int[][3]){ |
| 20 | {1, 2, 3}, |
| 21 | {4, 5, 6} |
| 22 | }; |
| 23 | |
| 24 | printf("%d %d %d\n", grid[0][0], grid[0][1], grid[0][2]); |
| 25 | |
| 26 | return 0; |
| 27 | } |
A flexible array member is an array of unknown size declared as the last member of a struct. It must be the only array member and must come after all fixed members. You allocate the struct with extra space using malloc.
| 1 | #include <stdio.h> |
| 2 | #include <stdlib.h> |
| 3 | #include <string.h> |
| 4 | |
| 5 | typedef struct { |
| 6 | int length; |
| 7 | char data[]; // flexible array member (no size specified) |
| 8 | } String; |
| 9 | |
| 10 | String *string_create(const char *src) { |
| 11 | int len = strlen(src); |
| 12 | String *s = malloc(sizeof(String) + len + 1); // +1 for null terminator |
| 13 | if (!s) return NULL; |
| 14 | |
| 15 | s->length = len; |
| 16 | memcpy(s->data, src, len + 1); |
| 17 | return s; |
| 18 | } |
| 19 | |
| 20 | void string_destroy(String *s) { |
| 21 | free(s); |
| 22 | } |
| 23 | |
| 24 | int main(void) { |
| 25 | String *hello = string_create("Hello, World!"); |
| 26 | printf("Length: %d\n", hello->length); // 13 |
| 27 | printf("Content: %s\n", hello->data); // Hello, World! |
| 28 | string_destroy(hello); |
| 29 | return 0; |
| 30 | } |
Arrays are a frequent source of bugs in C. Here are the most common mistakes and how to avoid them.
| Mistake | Example | Fix |
|---|---|---|
| Off-by-one error | `for (i = 0; i <= len)` | `for (i = 0; i < len)` |
| Buffer overflow | `arr[5] = 1` on size-5 array | `arr[4] = 1` (last valid index) |
| Uninitialized values | `int arr[100];` — garbage values | `int arr[100] = {0};` |
| Array assigned to pointer | `int *p = arr;` — loses size info | `int *p = arr; int n = len;` |
| sizeof on decayed array | `sizeof(arr)` in function = pointer size | `Pass size as separate parameter` |
| 1 | // Off-by-one: accessing arr[5] on a size-5 array |
| 2 | int arr[5] = {1, 2, 3, 4, 5}; |
| 3 | for (int i = 0; i <= 5; i++) { // BUG: should be i < 5 |
| 4 | printf("%d\n", arr[i]); // arr[5] is out of bounds |
| 5 | } |
| 6 | |
| 7 | // Uninitialized array |
| 8 | int values[100]; |
| 9 | // values[0] could be anything — random garbage from the stack |
| 10 | printf("%d\n", values[0]); // undefined behavior |
| 11 | |
| 12 | // Always initialize arrays you plan to read from |
| 13 | int safe[100] = {0}; // all zeros |
best practice
| Concept | Syntax |
|---|---|
| Declare + initialize | `int arr[5] = {1,2,3,4,5};` |
| Implicit size | `int arr[] = {1,2,3};` |
| Get array length | `sizeof(arr)/sizeof(arr[0])` |
| 2D array | `int m[3][4];` |
| VLA (C99) | `int arr[n];` |
| Composite literal | `(int[]){1,2,3}` |
| Flexible array member | `struct { int n; int d[]; };` |