Chapter 8: Handling Arrays and Strings
Arrays group values of the same type under one name; strings are byte arrays that end with a null character. This chapter shows how to declare and initialize arrays, how to work with multidimensional layouts, how C represents strings, and how to avoid the most frequent mistakes that cause crashes or silent data corruption.
Declaring and initializing arrays
Arrays are one of the cornerstones of C programming. They let you store groups of related values of the same type under a single name, with each element accessible by its index. Arrays make it possible to work efficiently with large datasets, sequences of numbers, and fixed collections of items that would otherwise require separate variables. Once defined, an array’s size remains fixed for its lifetime, and understanding how initialization and indexing work is essential for safe and efficient use.
This section explores how to declare arrays of various kinds, how initialization rules differ depending on scope and storage class, and how the compiler treats arrays as pointers in most expressions. Mastery of these concepts is critical before moving on to pointers, strings, or dynamic memory later in the language.
Fixed-size arrays and default initialization
You declare a fixed-size array by placing the length in brackets after the element type. Local (automatic) arrays hold indeterminate values until you assign them; file-scope or static arrays are zero-initialized.
int scores[5]; // automatic; contains indeterminate values
static double temps[3]; // static storage; initialized to 0.0
char flags[8]; // bytes; often used for small sets of markers
Initializing with brace lists
Use a brace list to provide initial values. If you give fewer elements than the length, the remainder becomes zero. If you omit the length, the compiler counts elements for you.
int primes[6] = {2, 3, 5, 7, 11, 13};
int odds[] = {1, 3, 5, 7}; // length is 4
double zeros[4] = {0}; // all four become 0.0
char letters[] = {'A', 'B', 'C', '\n'}; // character constants
Designated initializers (C99+)
Designators set specific indices and leave the rest as zero. This is clear and maintainable when only a few positions matter.
int table[10] = {[0] = 42, [5] = 99}; // others become 0
sizeof to compute element counts at compile time: size_t n = sizeof array / sizeof array[0]; This avoids hard-coded magic numbers.
Indexing, iteration, and bounds
Array indices start at zero and end at length minus one. Loop counters should use an unsigned index type that matches the standard library, usually size_t.
#include <stdio.h>
int main(void) {
int a[] = {10, 20, 30, 40};
size_t n = sizeof a / sizeof a[0];
for (size_t i = 0; i < n; i++) {
printf("%zu: %d\n", i, a[i]);
}
return 0;
}
Arrays decay to pointers
In most expressions an array converts to a pointer to its first element; the main exceptions are when used with sizeof, with unary &, or as a string literal initializing a char array.
int b[3] = {1,2,3};
int *p = b; // decay to &b[0]
size_t sz_array = sizeof b; // size of all elements
size_t sz_ptr = sizeof p; // size of the pointer
Multidimensional arrays
When data naturally forms a grid, table, or higher-dimensional structure, C provides multidimensional arrays. They can represent anything from a 2D image to a 3D matrix of measurements, all stored in a contiguous block of memory. Each dimension adds a layer of indexing that helps you organize complex data logically while maintaining predictable layout and performance.
Although conceptually simple, multidimensional arrays in C require care with declarations and function parameters. Because C stores data in row-major order, the rightmost index changes fastest in memory. Understanding this detail helps you reason about performance and compatibility with other languages and libraries.
Declaring 2D and 3D arrays
C stores multidimensional arrays in row-major order (the rightmost index varies fastest). You specify each dimension in brackets.
int grid[3][4]; // 3 rows, 4 columns
double cube[2][3][4]; // 2 layers, 3 rows, 4 columns
Initializing and accessing
Use nested braces for clarity. Access with a pair (or more) of indices.
int grid[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
int x = grid[1][2]; // 6
Passing multidimensional arrays to functions
For true multidimensional arrays, all sizes except the first must be known to the callee so the compiler can compute element addresses. From C99 onward you can use variable length arrays for this.
#include <stdio.h>
void print_mat(size_t rows, size_t cols, int m[rows][cols]) {
for (size_t r = 0; r < rows; r++) {
for (size_t c = 0; c < cols; c++) {
printf("%d ", m[r][c]);
}
printf("\n");
}
}
index = r * cols + c. This works well with dynamic allocation and libraries.
Pointer-to-array types
A pointer to the first row has type int (*)[COLS]. This differs from int **. Keep the parentheses to bind correctly.
void fill(size_t rows, size_t cols, int (*m)[cols]) {
for (size_t r = 0; r < rows; r++)
for (size_t c = 0; c < cols; c++)
m[r][c] = (int)(r + c);
}
String basics and the standard library
In C, strings are not first-class objects but rather arrays of characters terminated by the special null character '\0'. This convention, inherited from the earliest days of the language, keeps string handling lightweight and compatible with low-level memory operations. However, it also places the burden of safety and correctness on the programmer: every operation must respect the boundaries of the allocated buffer and the presence of the terminator.
The C standard library provides a powerful yet risky set of functions for copying, concatenating, searching, and formatting strings. Used properly, they make text handling straightforward. Misused, they are a leading source of program errors. This section covers how strings are represented, how to use the most important library functions, and how to apply defensive habits to prevent data corruption.
What a string is in C
A string is a sequence of bytes ending with the null character '\0'. The length that strlen reports counts characters before the terminator; the capacity of a buffer must include space for the terminator.
char hello[] = "Hello"; // 6 bytes: 'H' 'e' 'l' 'l' 'o' '\0'
size_t len = strlen(hello); // 5
Array vs pointer string declarations
char s[] = "Hi"; creates a writable array initialized from the literal. char *p = "Hi"; points at a string literal in read-only storage; writing through p is undefined behavior.
char s[] = "Hi";
char *p = "Hi";
// s[0] = 'h'; // OK
// p[0] = 'h'; // undefined behavior
Safe input and output of strings
Use fgets for input (it limits by buffer size) and printf with %s for output. Avoid gets (it was removed) and avoid unbounded scanf("%s", ...).
#include <stdio.h>
int main(void) {
char buf[32];
if (fgets(buf, sizeof buf, stdin)) {
printf("You typed: %s", buf); // may include a newline
}
return 0;
}
Essential string and memory functions
These functions live in <string.h>. Prefer the size-aware variants and always keep track of buffer capacities.
| Function | Purpose |
strlen(s) | Count characters before '\0'. |
strcpy(d, s) | Copy until '\0' (requires enough space in d). |
strncpy(d, s, n) | Copy at most n bytes (may not append '\0'). |
strcat(d, s) | Append s to end of d (requires spare capacity). |
strncmp(a, b, n) | Compare at most n bytes. |
strchr(s, c) | Find first occurrence of character. |
strstr(h, n) | Find substring n in h. |
memcpy(d, s, n) | Copy n bytes (non-overlapping regions). |
memmove(d, s, n) | Copy n bytes (handles overlap). |
memcmp(a, b, n) | Compare n bytes. |
snprintf(d, n, "...") | Format into d with a byte limit. |
snprintf returns the number of bytes it wanted to write (not counting the terminator). If the return value is greater than or equal to the buffer size then truncation occurred.
Building strings correctly
Accumulate text with size checks. Keep one variable for capacity and one for the current length. Reserve one byte for '\0'.
#include <stdio.h>
#include <string.h>
void join_three(const char *a, const char *b, const char *c,
char *out, size_t cap) {
size_t len = 0;
int w = snprintf(out + len, cap - len, "%s", a);
if (w < 0) return;
if ((size_t)w >= cap - len) { out[cap - 1] = '\0'; return; }
len += (size_t)w;
w = snprintf(out + len, cap - len, "%s", b);
if (w < 0) return;
if ((size_t)w >= cap - len) { out[cap - 1] = '\0'; return; }
len += (size_t)w;
(void)snprintf(out + len, cap - len, "%s", c);
}
Common pitfalls with '\0' and buffer overflow
The simplicity of C’s memory model is both its strength and its danger. Since strings and arrays have no built-in bounds checking, forgetting to reserve space for the null terminator or writing past the end of a buffer can lead to undefined behavior, security vulnerabilities, or silent data loss. These mistakes are notoriously easy to make and difficult to detect after the fact.
This section explains the classic errors that occur when handling arrays and strings, why they happen, and how to avoid them through disciplined coding practices. Understanding the difference between array size, string length, and buffer capacity is essential for writing robust C programs that behave predictably in all environments.
Forgetting space for the terminator
The capacity must be at least length plus one. If you allocate exactly the visible characters you will write past the end when you add '\0'.
size_t len = 5;
char *bad = malloc(len); // too small for "Hello"
char *good = malloc(len + 1); // space for '\0'
sizeof vs strlen
sizeof array reports the total storage in bytes for arrays with known size at compile time; strlen walks memory until it finds '\0'. After decay to a pointer, sizeof gives the pointer size; not the array size.
char s[] = "Hi";
size_t a = sizeof s; // 3
size_t b = strlen(s); // 2
char *p = s;
size_t c = sizeof p; // pointer size, often 8
Using strncpy without checking for termination
strncpy does not guarantee a terminator when it truncates. Append one yourself if you rely on C strings.
char dest[8];
strncpy(dest, "Longish", sizeof dest);
dest[sizeof dest - 1] = '\0'; // ensure termination
Unsafe input functions
gets was removed because it cannot limit input. Plain scanf("%s", buf) is unsafe. Use a width with scanf or prefer fgets.
char name[16];
// safer scanf usage with width: leaves one for '\0'
scanf("%15s", name);
Off-by-one in loops and concatenation
When appending, the usable space is capacity minus current length minus one. strncat expects a count of available space minus one because it also adds '\0'.
char buf[10] = "Hi";
size_t cap = sizeof buf;
size_t len = strlen(buf);
size_t avail = cap - len - 1;
strncat(buf, " there", avail); // safe append
Passing arrays to functions without sizes
Because arrays decay to pointers, the callee cannot know how many elements exist. Provide a length parameter for every array parameter.
int sum(const int *a, size_t n) {
int total = 0;
for (size_t i = 0; i < n; i++) total += a[i];
return total;
}
Mixing text and binary operations
String routines stop at '\0'. For arbitrary bytes (including zero) use memcpy, memmove, and memcmp, and track explicit lengths.
unsigned char data[4] = {1, 0, 2, 3};
/* strlen((char*)data) is meaningless here; use sizeof or tracked length */
struct slice { char *ptr; size_t len; }; This pattern reduces many mistakes.